feat(antgenius): the selected antenna can fill MY_ANTENNA

A station with a switch knows something the log does not: which antenna
is actually connected. The working conditions hold what was PLANNED for
the band, and stay right until the operator throws the switch — after
which every QSO keeps claiming the other antenna.

Optional, and off by default: a station that names its antennas
differently in the two places would otherwise find its log quietly
rewritten. The name written is the one configured on the device, since
that is the name the operator gave it and the one they will look for.

WHICH PORT is the real problem, and the reason this is not a one-liner.
The switch has two, the radio has two jacks, and the QSO went out through
one of them; naming the wrong port's antenna is worse than naming the
band default, because it looks authoritative. The radio's own TX antenna
selection decides, and the jack-to-port wiring is a setting — it is the
station's cabling and neither device can report it.

When the port cannot be told — a transverter jack, a rig that reports
nothing, both switch ports live — nothing is written and the band default
stands. Silence beats a confident guess in a field nobody re-checks.
This commit is contained in:
2026-08-25 15:08:48 +02:00
parent ed099a660e
commit df1d2767cd
7 changed files with 275 additions and 38 deletions
+48 -33
View File
@@ -781,8 +781,8 @@ type App struct {
// so closing the QSL Manager — or starting another download — stops the previous
// one instead of leaving it running against the app-lifetime context (which made
// a still-running QRZ sync bleed its log into a freshly started LoTW download).
confDLMu sync.Mutex
confDLCancel context.CancelFunc
confDLMu sync.Mutex
confDLCancel context.CancelFunc
// hamlogUnmatched holds the confirmations the last HAMLOG.online import
// could not place onto a QSO, kept so they can be exported and worked
@@ -790,31 +790,31 @@ type App struct {
// accumulated: it describes one run, not a history.
hamlogUnmatchedMu sync.Mutex
hamlogUnmatched []qso.QSO
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
adifMonMu sync.Mutex // guards the ADIF-monitor config (file list + per-file read offsets)
syncMu sync.Mutex // serialises folder synchronisation: config, the seq counter, and the append to our own file
syncSent int64 // changes written to the folder this session
syncReceived int64 // changes taken from the other machines this session
syncLast time.Time // last completed pass, for the status panel
syncErr string // last folder error, shown in settings — a share that dropped is otherwise invisible
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
relayAutoLast map[string]bool // deviceID|relay → last applied on/off, so we only switch on a real change
relayAutoOn atomic.Bool // cached "auto-control enabled" so the CAT hot path skips work when off
relayDrvMu sync.Mutex // guards the cached relay drivers below
relayDrv map[string]cachedRelay // deviceID → live driver (reused across polls; stateful boards can't be reopened per call)
pttPort serial.Port // open serial port while PTT (RTS/DTR) is asserted
pttKeyedMethod string // "cat" | "rts" | "dtr" while keyed; "" when idle
pttGen int64 // bumped on every key; a delayed unkey only fires if unchanged (guards against a stale release cutting a new transmission)
startupErr string // captured for surfacing to the frontend
settingsScoped atomic.Bool // true once a.settings is scoped to the active profile — GetUIPref/SetUIPref (per-profile) must wait for it, else an early call reads the wrong scope and e.g. resets the theme
dbPath string // settings/config database file (settings + profiles); may be a user-chosen location
logbookPath string // default SQLite logbook file (QSOs), next to the settings db — used when a profile doesn't point elsewhere
logDbPath string // resolved SQLite file actually backing logDb ("" on MySQL, or when the settings db serves as the logbook) — the file the backup snapshots
logDb *sql.DB // QSO logbook connection — MySQL, a per-profile SQLite file, or the default logbook.db (never the settings db, except on fallback)
dbBackend string // "sqlite" | "mysql" — the logbook backend actually opened at startup
dbBackendErr string // non-empty when a configured MySQL backend failed and we fell back to SQLite
offlineQ *offlineq.Queue // ADIF outbox: QSOs logged while the DB was unreachable
offlineMode bool // last write failed because the DB was unreachable
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
adifMonMu sync.Mutex // guards the ADIF-monitor config (file list + per-file read offsets)
syncMu sync.Mutex // serialises folder synchronisation: config, the seq counter, and the append to our own file
syncSent int64 // changes written to the folder this session
syncReceived int64 // changes taken from the other machines this session
syncLast time.Time // last completed pass, for the status panel
syncErr string // last folder error, shown in settings — a share that dropped is otherwise invisible
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
relayAutoLast map[string]bool // deviceID|relay → last applied on/off, so we only switch on a real change
relayAutoOn atomic.Bool // cached "auto-control enabled" so the CAT hot path skips work when off
relayDrvMu sync.Mutex // guards the cached relay drivers below
relayDrv map[string]cachedRelay // deviceID → live driver (reused across polls; stateful boards can't be reopened per call)
pttPort serial.Port // open serial port while PTT (RTS/DTR) is asserted
pttKeyedMethod string // "cat" | "rts" | "dtr" while keyed; "" when idle
pttGen int64 // bumped on every key; a delayed unkey only fires if unchanged (guards against a stale release cutting a new transmission)
startupErr string // captured for surfacing to the frontend
settingsScoped atomic.Bool // true once a.settings is scoped to the active profile — GetUIPref/SetUIPref (per-profile) must wait for it, else an early call reads the wrong scope and e.g. resets the theme
dbPath string // settings/config database file (settings + profiles); may be a user-chosen location
logbookPath string // default SQLite logbook file (QSOs), next to the settings db — used when a profile doesn't point elsewhere
logDbPath string // resolved SQLite file actually backing logDb ("" on MySQL, or when the settings db serves as the logbook) — the file the backup snapshots
logDb *sql.DB // QSO logbook connection — MySQL, a per-profile SQLite file, or the default logbook.db (never the settings db, except on fallback)
dbBackend string // "sqlite" | "mysql" — the logbook backend actually opened at startup
dbBackendErr string // non-empty when a configured MySQL backend failed and we fell back to SQLite
offlineQ *offlineq.Queue // ADIF outbox: QSOs logged while the DB was unreachable
offlineMode bool // last write failed because the DB was unreachable
catFlexSpots bool // push cluster spots to the FlexRadio panadapter
catFlexDVKDax bool // raise the Flex transmit DAX button around a voice message
@@ -3330,6 +3330,9 @@ func (a *App) applyStationDefaults(q *qso.QSO, includeIdentity bool) {
if q.MyAntenna == "" {
q.MyAntenna = p.MyAntenna
}
// The switch has the last word, when asked to: everything above is what was
// PLANNED for this band, and the Antenna Genius knows what is connected.
a.applyAntGeniusAntenna(&q.MyAntenna)
if q.TXPower == nil && p.TxPower != nil {
v := *p.TxPower
q.TXPower = &v
@@ -17653,6 +17656,12 @@ type AntGeniusSettings struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Password string `json:"password"` // remote-access password; leave blank on LAN (no AUTH)
// UseForMyAntenna stamps the SELECTED antenna's name into MY_ANTENNA on
// every QSO logged, ahead of the band default from Operating conditions.
UseForMyAntenna bool `json:"use_for_my_antenna"`
// Ant1Port says which switch port the radio's ANT1 jack is wired to
// (1 = A, 2 = B). Station wiring; neither device can report it.
Ant1Port int `json:"ant1_port"`
}
// GetAntGeniusSettings returns the persisted Antenna Genius config.
@@ -17661,13 +17670,19 @@ func (a *App) GetAntGeniusSettings() (AntGeniusSettings, error) {
if a.settings == nil {
return out, fmt.Errorf("db not initialized")
}
m, err := a.settings.GetMany(a.ctx, keyAntGeniusEnabled, keyAntGeniusHost, keyAntGeniusPassword)
m, err := a.settings.GetMany(a.ctx, keyAntGeniusEnabled, keyAntGeniusHost, keyAntGeniusPassword,
keyAntGeniusMyAnt, keyAntGeniusPortForA1)
if err != nil {
return out, err
}
out.Enabled = m[keyAntGeniusEnabled] == "1"
out.Host = m[keyAntGeniusHost]
out.Password = m[keyAntGeniusPassword]
out.UseForMyAntenna = m[keyAntGeniusMyAnt] == "1"
out.Ant1Port = 1
if m[keyAntGeniusPortForA1] == "2" {
out.Ant1Port = 2
}
return out, nil
}
@@ -17677,9 +17692,11 @@ func (a *App) SaveAntGeniusSettings(s AntGeniusSettings) error {
return fmt.Errorf("db not initialized")
}
for k, v := range map[string]string{
keyAntGeniusEnabled: boolStr(s.Enabled),
keyAntGeniusHost: strings.TrimSpace(s.Host),
keyAntGeniusPassword: s.Password,
keyAntGeniusEnabled: boolStr(s.Enabled),
keyAntGeniusHost: strings.TrimSpace(s.Host),
keyAntGeniusPassword: s.Password,
keyAntGeniusMyAnt: boolStr(s.UseForMyAntenna),
keyAntGeniusPortForA1: map[bool]string{true: "2", false: "1"}[s.Ant1Port == 2],
} {
if err := a.settings.Set(a.ctx, k, v); err != nil {
return err
@@ -20343,8 +20360,6 @@ func clampSpotMax(n int) int {
return n
}
// GetSpotTTLMinutes returns how long a spot stays in the list, in minutes.
// 0 means spots are kept until the count cap pushes them out, which is what
// OpsLog always did.