Let the config choose protocol 2 or 3, and fall back for one session only.

The default stays v2. A reflector that refuses that version and names the other gets one session of it, then the station announces the configured protocol again.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-22 16:28:42 +02:00
co-authored by Cursor
parent 3533b97d00
commit 2c621d4b7f
21 changed files with 361 additions and 82 deletions
+72 -6
View File
@@ -20,6 +20,7 @@ type loaded struct {
callsign string
authKey string
talkGroup int
protocolMajor uint16
certificateDir string
certFatal string
enableLogging bool
@@ -33,6 +34,7 @@ type fileShape struct {
Callsign jsontext.Value `json:"callsign"`
AuthKey jsontext.Value `json:"authKey"`
TalkGroup jsontext.Value `json:"talkGroup"`
Protocol jsontext.Value `json:"protocol"`
CertificateDir jsontext.Value `json:"certificateDir"`
EnableLogging jsontext.Value `json:"enableLogging"`
LogFile jsontext.Value `json:"logFile"`
@@ -44,6 +46,7 @@ type templateFile struct {
Callsign string `json:"callsign"`
AuthKey string `json:"authKey"`
TalkGroup int `json:"talkGroup"`
Protocol string `json:"protocol"`
CertificateDir string `json:"certificateDir"`
EnableLogging bool `json:"enableLogging"`
LogFile string `json:"logFile"`
@@ -76,13 +79,14 @@ func loadConfig(path string) (configLoad, error) {
return parseConfig(path, body, true)
}
// ConfigForm is the eight fields the Config window edits.
// ConfigForm is the fields the Config window edits.
type ConfigForm struct {
Host string `json:"host"`
Port int `json:"port"`
Callsign string `json:"callsign"`
AuthKey string `json:"authKey"`
TalkGroup int `json:"talkGroup"`
Protocol string `json:"protocol"`
CertificateDir string `json:"certificateDir"`
EnableLogging bool `json:"enableLogging"`
LogFile string `json:"logFile"`
@@ -101,7 +105,7 @@ func templateForm() (ConfigForm, error) {
if err != nil {
return ConfigForm{}, err
}
return ConfigForm{CertificateDir: "./funknetz", LogFile: logFile}, nil
return ConfigForm{Protocol: "v2", CertificateDir: "./funknetz", LogFile: logFile}, nil
}
func parseConfig(path string, body []byte, rewrite bool) (configLoad, error) {
@@ -114,7 +118,8 @@ func parseConfig(path string, body []byte, rewrite bool) (configLoad, error) {
return configLoad{form: form, detail: fmt.Errorf("the file could not be read: %w", err).Error()}, nil
}
certBad := present(shape.CertificateDir) && shape.CertificateDir.Kind() != jsontext.KindString
if block, notice := blockedLog(shape); block || certBad {
protoBad := present(shape.Protocol) && shape.Protocol.Kind() != jsontext.KindString
if block, notice := blockedLog(shape); block || certBad || protoBad {
cfg, err := parseOperational(shape)
if err != nil {
return refused(shape, err)
@@ -125,7 +130,7 @@ func parseConfig(path string, body []byte, rewrite bool) (configLoad, error) {
}
return finish(shape, applyCertificate(cfg, shape, path))
}
if missingCertificate(shape) || missingLog(shape) {
if missingCertificate(shape) || missingLog(shape) || missingProtocol(shape) {
if !operationalKindsOK(shape) {
_, err := parseOperational(shape)
if err != nil {
@@ -165,7 +170,7 @@ func refused(shape fileShape, cause error) (configLoad, error) {
func formFromShape(shape fileShape) ConfigForm {
fallback, err := templateForm()
if err != nil {
fallback = ConfigForm{CertificateDir: "./funknetz"}
fallback = ConfigForm{Protocol: "v2", CertificateDir: "./funknetz"}
}
return mergeSalvage(shape, fallback)
}
@@ -192,6 +197,11 @@ func mergeSalvage(shape fileShape, fallback ConfigForm) ConfigForm {
fallback.TalkGroup = n
}
}
if s, err := stringField("protocol", shape.Protocol); err == nil && present(shape.Protocol) {
if label, ok := protocolLabel(s); ok {
fallback.Protocol = label
}
}
if s, err := stringField("certificateDir", shape.CertificateDir); err == nil && present(shape.CertificateDir) {
fallback.CertificateDir = s
}
@@ -225,6 +235,10 @@ func parseOperational(shape fileShape) (loaded, error) {
if err != nil {
return loaded{}, err
}
protocolMajor, err := protocolField(shape.Protocol)
if err != nil {
return loaded{}, err
}
host = strings.TrimSpace(host)
if host == "" {
@@ -237,7 +251,45 @@ func parseOperational(shape fileShape) (loaded, error) {
if !callsignPattern.MatchString(call) {
return loaded{}, fmt.Errorf("callsign is invalid")
}
return loaded{host: host, port: port, callsign: call, authKey: key, talkGroup: tg}, nil
return loaded{host: host, port: port, callsign: call, authKey: key, talkGroup: tg, protocolMajor: protocolMajor}, nil
}
func protocolField(v jsontext.Value) (uint16, error) {
if !present(v) {
return 2, nil
}
s, err := stringField("protocol", v)
if err != nil {
return 0, err
}
if strings.TrimSpace(s) == "" {
return 2, nil
}
if _, ok := protocolLabel(s); !ok {
return 0, fmt.Errorf("protocol is invalid")
}
if strings.TrimSpace(s) == "v3" {
return 3, nil
}
return 2, nil
}
func protocolLabel(s string) (string, bool) {
switch strings.TrimSpace(s) {
case "v2":
return "v2", true
case "v3":
return "v3", true
default:
return "", false
}
}
func protocolName(major uint16) string {
if major == 3 {
return "v3"
}
return "v2"
}
func stringField(name string, v jsontext.Value) (string, error) {
@@ -306,6 +358,14 @@ func missingCertificate(shape fileShape) bool {
return !present(shape.CertificateDir)
}
func missingProtocol(shape fileShape) bool {
if !present(shape.Protocol) {
return true
}
s, err := stringField("protocol", shape.Protocol)
return err == nil && strings.TrimSpace(s) == ""
}
func operationalKindsOK(shape fileShape) bool {
if present(shape.Host) && shape.Host.Kind() != jsontext.KindString {
return false
@@ -322,6 +382,9 @@ func operationalKindsOK(shape fileShape) bool {
if present(shape.TalkGroup) && shape.TalkGroup.Kind() != jsontext.KindNumber {
return false
}
if present(shape.Protocol) && shape.Protocol.Kind() != jsontext.KindString {
return false
}
return true
}
@@ -348,6 +411,9 @@ func withLogDefaults(shape fileShape) (fileShape, error) {
shape.Callsign = rawOr(shape.Callsign, `""`)
shape.AuthKey = rawOr(shape.AuthKey, `""`)
shape.TalkGroup = rawOr(shape.TalkGroup, "0")
if missingProtocol(shape) {
shape.Protocol = jsontext.Value(`"v2"`)
}
shape.CertificateDir = rawOr(shape.CertificateDir, `"./funknetz"`)
shape.EnableLogging = rawOr(shape.EnableLogging, "false")
if !present(shape.LogFile) {