feat: HAMLOG.online upload and confirmations, Yaesu antenna, a named port holder

HAMLOG.online is a seventh external service: one API key, one ADIF record per
QSO, immediate / delayed / on-close like the rest. They publish no API
documentation, so the protocol is read from THEIR OWN client — the HAMLOG Agent
(github.com/hamlogonline/Agent), which is the authoritative source short of
asking them:

	POST https://hamlog.online/api/agent/
	{"ADIFADD": {"APIKEY": k, "ADIFDATA": record}}   → {"STATUS":"OK"}
	{"KEYSTATUS": {"APIKEY": k}}                     → {"STATUS":"OK","CALLSIGN":…}

Success is STATUS == OK, not "no ERROR field": their failure carries ERROR and
no STATUS, and reading an unknown reply — a proxy page, a maintenance notice —
as an acceptance is how a contact goes missing without anyone noticing.

KEYSTATUS buys something no other service here offers: the key can be checked
BEFORE the first QSO, and the answer names the account. A key pasted from
another callsign is caught in the settings panel rather than through a week of
silent refusals.

Their confirmations are also an award source now, ticked like LoTW rather than
expressed through "custom". It reads the ADIF extras, not a column: the standard
names a field for hamlog.EU and none for hamlog.ONLINE, and borrowing the other
site's field would write a falsehood into every exported log. Three plausible
key names from their own export are accepted too, so nobody has to rename a
column by hand after an export.

Yaesu gains antenna selection (AN), remembered per band — the antenna picked on
a band comes back with it, with no table to fill in anywhere. Rigs with one
socket never answer AN and never show the row; the log says which case it is.

And a serial port that is refused now names its likely holder. OmniRig stays
resident once activated and keeps the port of the rig configured in it, so a
native backend never gets it — "Serial port busy" alone accused nobody, and an
FTDX10 spent a morning being blamed for it.
This commit is contained in:
2026-08-23 12:18:27 +02:00
parent 3014796c1d
commit ee148732dd
21 changed files with 603 additions and 18 deletions
+80
View File
@@ -19882,6 +19882,86 @@ func (a *App) SetYaesuPreamp(n int) error {
return a.yaesuDo(func(y cat.YaesuController) error { return y.SetYaesuPreamp(n) })
}
// SetYaesuAntenna selects an antenna jack on a Yaesu, and REMEMBERS it for the
// band the rig is on.
//
// No settings page for this, deliberately. A per-band antenna table is one more
// form to fill in and one more menu to find, for a fact the operator states
// perfectly well by choosing the antenna once on that band. So the choice is
// learnt where it is made, and re-applied on the next visit — see
// applyYaesuBandAntenna.
func (a *App) SetYaesuAntenna(n int) error {
if err := a.yaesuDo(func(y cat.YaesuController) error { return y.SetYaesuAntenna(n) }); err != nil {
return err
}
if band := strings.ToLower(strings.TrimSpace(a.cat.State().Band)); band != "" && n > 0 {
m := a.yaesuBandAntennas()
if m[band] != n {
m[band] = n
a.saveYaesuBandAntennas(m)
applog.Printf("yaesu: antenna %d remembered for %s", n, band)
}
}
return nil
}
// keyYaesuBandAnt holds the learnt band → antenna map, as JSON.
const keyYaesuBandAnt = "yaesu.band_antennas"
func (a *App) yaesuBandAntennas() map[string]int {
out := map[string]int{}
if raw := a.settingOr(keyYaesuBandAnt, ""); raw != "" {
_ = json.Unmarshal([]byte(raw), &out)
}
return out
}
func (a *App) saveYaesuBandAntennas(m map[string]int) {
if b, err := json.Marshal(m); err == nil {
a.setSetting(keyYaesuBandAnt, string(b))
}
}
// GetYaesuBandAntennas exposes the learnt map, so the panel can show which band
// carries which antenna without the operator having to change band to find out.
func (a *App) GetYaesuBandAntennas() map[string]int { return a.yaesuBandAntennas() }
// ForgetYaesuBandAntenna drops the memory for one band — the way to undo a
// choice made by mistake, without a table to edit.
func (a *App) ForgetYaesuBandAntenna(band string) {
m := a.yaesuBandAntennas()
delete(m, strings.ToLower(strings.TrimSpace(band)))
a.saveYaesuBandAntennas(m)
}
// YaesuApplyBandAntenna is applyYaesuBandAntenna as a binding, called from the
// same place the Flex one is — a band change or a spot click.
func (a *App) YaesuApplyBandAntenna(band string) error {
a.applyYaesuBandAntenna(band)
return nil
}
// applyYaesuBandAntenna puts the remembered antenna back when the rig changes
// band. Silent when nothing was ever learnt for it: an operator who has not
// expressed a preference for 17 m must not have their antenna moved.
func (a *App) applyYaesuBandAntenna(band string) {
b := strings.ToLower(strings.TrimSpace(band))
if b == "" || a.cat == nil {
return
}
want := a.yaesuBandAntennas()[b]
if want <= 0 {
return
}
_ = a.cat.YaesuDo(func(y cat.YaesuController) error {
if st := y.YaesuState(); st.Antenna == want || st.Antenna == 0 {
return nil // already there, or a rig with a single socket
}
applog.Printf("yaesu: band %s → antenna %d (remembered)", b, want)
return y.SetYaesuAntenna(want)
})
}
func (a *App) SetYaesuAtt(db int) error {
return a.yaesuDo(func(y cat.YaesuController) error { return y.SetYaesuAtt(db) })
}