feat(antenna): three tracking modes for Ultrabeam and SteppIR

The follow loop only ever had one behaviour — re-tune once the rig moved
further than a fixed step. The SteppIR's own controller software offers three,
and operators arrive with that mental model: every frequency change, past a
threshold, or only on a band change. They are real operating trade-offs, not
preferences: "always" keeps resonance perfect at the cost of motors running
constantly (and on a SteppIR every move inhibits transmit while the elements
travel), "band" moves them a handful of times a day.

"Always" is implemented as the threshold mode with a 1 kHz threshold rather
than as a separate branch, so all modes keep the one deadband reference that
matters: the rig frequency last commanded for, NOT the antenna's own reported
frequency — a SteppIR flips its reported frequency between the commanded value
and its home value, which would re-issue a SET on nearly every poll and leave
the operator permanently unable to transmit.

Band mode compares against the band last COMMANDED for, not the rig's previous
band, so the first move after startup and any move made by hand still get
reconciled. It applies to the immediate spot-click path too: a click inside the
band the antenna is already resonant in moves nothing.

An unset or unrecognised mode resolves to the step mode, so every config
written before this option behaves exactly as it did.

Also routes the antenna settings block through t() — it was hardcoded English.
This commit is contained in:
2026-08-12 08:31:44 +02:00
parent 99d903eb44
commit 65bbaa85f3
9 changed files with 236 additions and 60 deletions
+35 -14
View File
@@ -1250,8 +1250,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
const [rotatorTest, setRotatorTest] = useState<{ ok: boolean; msg: string } | null>(null);
// Motorized antenna (Ultrabeam TCP or SteppIR TCP/serial) settings.
const [ultrabeam, setUltrabeam] = useState<{ enabled: boolean; type: string; transport: string; host: string; port: number; com: string; baud: number; follow: boolean; step_khz: number; tx_inhibit: boolean; bands: string[]; freq_min_mhz: number; freq_max_mhz: number }>({
enabled: false, type: 'ultrabeam', transport: 'tcp', host: '', port: 23, com: '', baud: 9600, follow: false, step_khz: 50, tx_inhibit: false, bands: [], freq_min_mhz: 13, freq_max_mhz: 54,
const [ultrabeam, setUltrabeam] = useState<{ enabled: boolean; type: string; transport: string; host: string; port: number; com: string; baud: number; follow: boolean; step_khz: number; track_mode: string; tx_inhibit: boolean; bands: string[]; freq_min_mhz: number; freq_max_mhz: number }>({
enabled: false, type: 'ultrabeam', transport: 'tcp', host: '', port: 23, com: '', baud: 9600, follow: false, step_khz: 50, track_mode: 'step', tx_inhibit: false, bands: [], freq_min_mhz: 13, freq_max_mhz: 54,
});
const [ubTesting, setUbTesting] = useState(false);
const [ubTest, setUbTest] = useState<{ ok: boolean; msg: string } | null>(null);
@@ -3140,20 +3140,41 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
<div className="border-t border-border/60 pt-3 space-y-2">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={ultrabeam.follow} onCheckedChange={(c) => setUltrabeam((s) => ({ ...s, follow: !!c }))} />
Follow rig frequency (auto-tune the antenna)
{t('hw.motorFollow')}
</label>
{ultrabeam.follow && (
<div className="flex items-center gap-3 pl-6">
<Label className="text-sm">Re-tune step</Label>
<Select value={String(ultrabeam.step_khz)} onValueChange={(v) => setUltrabeam((s) => ({ ...s, step_khz: parseInt(v, 10) || 50 }))}>
<SelectTrigger className="h-8 w-32"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="25">25 kHz</SelectItem>
<SelectItem value="50">50 kHz</SelectItem>
<SelectItem value="100">100 kHz</SelectItem>
</SelectContent>
</Select>
<span className="text-xs text-muted-foreground">re-tune only when the frequency moves this far</span>
<div className="space-y-2 pl-6">
<div className="flex items-center gap-3">
<Label className="text-sm">{t('station.trackModeTip')}</Label>
<Select value={ultrabeam.track_mode || 'step'} onValueChange={(v) => setUltrabeam((s) => ({ ...s, track_mode: v }))}>
<SelectTrigger className="h-8 w-52"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="always">{t('station.trackAlways')}</SelectItem>
<SelectItem value="step">{t('station.trackStep')}</SelectItem>
<SelectItem value="band">{t('station.trackBand')}</SelectItem>
</SelectContent>
</Select>
</div>
{/* The step is only a question in step mode the other two modes
have nothing to threshold. */}
{(ultrabeam.track_mode || 'step') === 'step' && (
<div className="flex items-center gap-3">
<Label className="text-sm">{t('hw.motorStep')}</Label>
<Select value={String(ultrabeam.step_khz)} onValueChange={(v) => setUltrabeam((s) => ({ ...s, step_khz: parseInt(v, 10) || 50 }))}>
<SelectTrigger className="h-8 w-32"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="25">25 kHz</SelectItem>
<SelectItem value="50">50 kHz</SelectItem>
<SelectItem value="100">100 kHz</SelectItem>
</SelectContent>
</Select>
</div>
)}
<p className="text-xs text-muted-foreground">
{(ultrabeam.track_mode || 'step') === 'always' ? t('station.trackAlwaysTip')
: (ultrabeam.track_mode || 'step') === 'band' ? t('station.trackBandTip')
: t('station.trackStepTipMode')}
</p>
</div>
)}
</div>