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:
@@ -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>
|
||||
|
||||
@@ -116,7 +116,7 @@ function RotatorWidget({ hd, refetch, centerLat, centerLon, bearing, t }: Rotato
|
||||
);
|
||||
}
|
||||
|
||||
type AntStatus = { enabled: boolean; type: string; connected: boolean; direction: number; frequency: number; moving: boolean; elements: number[]; follow?: boolean; step_khz?: number; bands?: string[] };
|
||||
type AntStatus = { enabled: boolean; type: string; connected: boolean; direction: number; frequency: number; moving: boolean; elements: number[]; follow?: boolean; step_khz?: number; track_mode?: string; bands?: string[] };
|
||||
|
||||
// Where each band button points the antenna.
|
||||
//
|
||||
@@ -277,23 +277,38 @@ function MotorAntennaWidget({ ant, refetch, t }: { ant: AntStatus; refetch: () =
|
||||
|
||||
{/* Tracking. Here rather than only in Settings because it is an operating
|
||||
decision — off to park the antenna, on to resume — not something set
|
||||
up once. The step only shows when tracking is on: a threshold for
|
||||
something switched off is a question the operator cannot act on. */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button"
|
||||
onClick={() => run(SetMotorFollow(!ant.follow, 0))}
|
||||
className={cn('flex-1 rounded-md border py-1.5 text-xs font-semibold transition-colors',
|
||||
ant.follow ? 'bg-success-muted text-success-muted-foreground border-success-border' : 'border-border hover:bg-muted')}>
|
||||
{ant.follow ? t('station.trackOn') : t('station.trackOff')}
|
||||
</button>
|
||||
up once. Mode and step only show when tracking is on: settings for
|
||||
something switched off are questions the operator cannot act on. And
|
||||
the step only shows in step mode, where it is the one thing it means. */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button"
|
||||
onClick={() => run(SetMotorFollow(!ant.follow, 0, ''))}
|
||||
className={cn('flex-1 rounded-md border py-1.5 text-xs font-semibold transition-colors',
|
||||
ant.follow ? 'bg-success-muted text-success-muted-foreground border-success-border' : 'border-border hover:bg-muted')}>
|
||||
{ant.follow ? t('station.trackOn') : t('station.trackOff')}
|
||||
</button>
|
||||
{ant.follow && (ant.track_mode || 'step') === 'step' && (
|
||||
<select
|
||||
value={String(ant.step_khz || 50)}
|
||||
onChange={(e) => run(SetMotorFollow(true, parseInt(e.target.value, 10), ''))}
|
||||
className="h-[30px] rounded-md border border-border bg-background px-1 text-xs font-mono"
|
||||
title={t('station.trackStepTip')}
|
||||
>
|
||||
{[25, 50, 100].map((s) => <option key={s} value={s}>{s} kHz</option>)}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
{ant.follow && (
|
||||
<select
|
||||
value={String(ant.step_khz || 50)}
|
||||
onChange={(e) => run(SetMotorFollow(true, parseInt(e.target.value, 10)))}
|
||||
className="h-[30px] rounded-md border border-border bg-background px-1 text-xs font-mono"
|
||||
title={t('station.trackStepTip')}
|
||||
value={ant.track_mode || 'step'}
|
||||
onChange={(e) => run(SetMotorFollow(true, 0, e.target.value))}
|
||||
className="w-full h-[30px] rounded-md border border-border bg-background px-1.5 text-xs"
|
||||
title={t('station.trackModeTip')}
|
||||
>
|
||||
{[25, 50, 100].map((s) => <option key={s} value={s}>{s} kHz</option>)}
|
||||
<option value="always" title={t('station.trackAlwaysTip')}>{t('station.trackAlways')}</option>
|
||||
<option value="step" title={t('station.trackStepTipMode')}>{t('station.trackStep')}</option>
|
||||
<option value="band" title={t('station.trackBandTip')}>{t('station.trackBand')}</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -982,7 +982,7 @@ export function SetDVKLabel(arg1:number,arg2:string):Promise<void>;
|
||||
|
||||
export function SetKenwoodKeySpeed(arg1:number):Promise<void>;
|
||||
|
||||
export function SetMotorFollow(arg1:boolean,arg2:number):Promise<void>;
|
||||
export function SetMotorFollow(arg1:boolean,arg2:number,arg3:string):Promise<void>;
|
||||
|
||||
export function SetOpsLogQSLReceived(arg1:number,arg2:boolean):Promise<void>;
|
||||
|
||||
|
||||
@@ -1906,8 +1906,8 @@ export function SetKenwoodKeySpeed(arg1) {
|
||||
return window['go']['main']['App']['SetKenwoodKeySpeed'](arg1);
|
||||
}
|
||||
|
||||
export function SetMotorFollow(arg1, arg2) {
|
||||
return window['go']['main']['App']['SetMotorFollow'](arg1, arg2);
|
||||
export function SetMotorFollow(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['SetMotorFollow'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function SetOpsLogQSLReceived(arg1, arg2) {
|
||||
|
||||
@@ -3283,6 +3283,7 @@ export namespace main {
|
||||
baud: number;
|
||||
follow: boolean;
|
||||
step_khz: number;
|
||||
track_mode: string;
|
||||
tx_inhibit: boolean;
|
||||
bands: string[];
|
||||
freq_min_mhz: number;
|
||||
@@ -3303,6 +3304,7 @@ export namespace main {
|
||||
this.baud = source["baud"];
|
||||
this.follow = source["follow"];
|
||||
this.step_khz = source["step_khz"];
|
||||
this.track_mode = source["track_mode"];
|
||||
this.tx_inhibit = source["tx_inhibit"];
|
||||
this.bands = source["bands"];
|
||||
this.freq_min_mhz = source["freq_min_mhz"];
|
||||
@@ -3320,6 +3322,7 @@ export namespace main {
|
||||
elements: number[];
|
||||
follow: boolean;
|
||||
step_khz: number;
|
||||
track_mode: string;
|
||||
bands: string[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
@@ -3338,6 +3341,7 @@ export namespace main {
|
||||
this.elements = source["elements"];
|
||||
this.follow = source["follow"];
|
||||
this.step_khz = source["step_khz"];
|
||||
this.track_mode = source["track_mode"];
|
||||
this.bands = source["bands"];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user