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
+17 -3
View File
@@ -3,7 +3,7 @@ import { Radio, AudioLines, Mic, Activity, SlidersHorizontal, Antenna } from 'lu
import {
GetYaesuState, RefreshYaesuPanel,
SetYaesuPower, SetYaesuMicGain, SetYaesuAFGain, SetYaesuRFGain, SetYaesuSquelch,
SetYaesuAGC, SetYaesuPreamp, SetYaesuAtt, SetYaesuNB, SetYaesuNR, SetYaesuNRLevel,
SetYaesuAGC, SetYaesuPreamp, SetYaesuAtt, SetYaesuAntenna, SetYaesuNB, SetYaesuNR, SetYaesuNRLevel,
SetYaesuNarrow, SetYaesuVOX, SetYaesuSplit, SetYaesuBand, TuneYaesuATU,
SetYaesuModeRaw, SetYaesuSplitOffset, SetYaesuKeySpeed, SetYaesuBreakIn, YaesuZeroIn, GetCATState,
} from '../../wailsjs/go/main/App';
@@ -17,7 +17,7 @@ type YaesuState = {
transmitting: boolean; split: boolean;
s_meter: number; power_meter: number; swr_meter: number;
rf_power: number; mic_gain: number; af_gain: number; rf_gain: number; squelch: number;
agc?: string; preamp: number; att: number;
agc?: string; preamp: number; att: number; antenna: number;
nb: boolean; nr: boolean; nr_level: number; narrow: boolean; vox: boolean;
split_tx_hz?: number; key_speed?: number; break_in?: boolean; swr?: number; power_w?: number;
};
@@ -26,7 +26,7 @@ const ZERO: YaesuState = {
available: false, transmitting: false, split: false,
s_meter: 0, power_meter: 0, swr_meter: 0,
rf_power: 0, mic_gain: 0, af_gain: 0, rf_gain: 0, squelch: 0,
preamp: 0, att: 0, nb: false, nr: false, nr_level: 0, narrow: false, vox: false,
preamp: 0, att: 0, antenna: 0, nb: false, nr: false, nr_level: 0, narrow: false, vox: false,
};
// Band buttons use the rig's OWN band memory (CAT "BS"), not a frequency we
@@ -74,6 +74,10 @@ function activeMode(raw?: string): { id: string; side: 'U' | 'L' } | null {
// IPO bypasses the preamp entirely (best on a quiet, high-signal band), AMP1 and
// AMP2 add gain. Presenting it as a toggle would hide the middle position.
const PREAMPS = [{ v: '0', l: 'IPO' }, { v: '1', l: 'AMP1' }, { v: '2', l: 'AMP2' }];
// Three jacks is the most any of these rigs has (FTDX101); an FTDX10 answers
// with 1 or 2 and the third button simply never gets used. A rig with a single
// socket answers nothing at all and reports 0, and the row is not drawn.
const ANTENNAS = [{ v: '1', l: 'ANT1' }, { v: '2', l: 'ANT2' }, { v: '3', l: 'ANT3' }];
const AGCS = [{ v: 'FAST', l: 'FAST' }, { v: 'MID', l: 'MID' }, { v: 'SLOW', l: 'SLOW' }, { v: 'AUTO', l: 'AUTO' }];
// The attenuator is a three-step pad on these rigs (6/12/18 dB), not a toggle.
const ATTS = [{ v: '0', l: 'OFF' }, { v: '6', l: '6dB' }, { v: '12', l: '12dB' }, { v: '18', l: '18dB' }];
@@ -424,6 +428,16 @@ export function YaesuPanel({ onReportRST, onKeySpeed }: {
<Row label="ATT">
<Segmented value={String(view.att)} options={ATTS} onChange={(v) => push('att', parseInt(v, 10), () => SetYaesuAtt(parseInt(v, 10)))} />
</Row>
{/* Only on a rig that HAS a choice: 0 means the AN command went
unanswered, which is what a single-socket FT-891 does. And the
choice is remembered for the band it was made on — no table to
fill in anywhere, the backend learns it here. */}
{view.antenna > 0 && (
<Row label="ANT">
<Segmented value={String(view.antenna)} options={ANTENNAS}
onChange={(v) => push('antenna', parseInt(v, 10), () => SetYaesuAntenna(parseInt(v, 10)))} />
</Row>
)}
</Card>
{/* Noise + filter */}