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:
@@ -233,6 +233,7 @@ const (
|
||||
keyUltrabeamPort = "ultrabeam.port"
|
||||
keyUltrabeamFollow = "ultrabeam.follow" // "1" → re-tune to the rig frequency
|
||||
keyUltrabeamStep = "ultrabeam.step_khz" // re-tune hysteresis: 25 | 50 | 100 kHz
|
||||
keyMotorTrackMode = "motor.track_mode" // "always" | "step" | "band"
|
||||
keyMotorType = "ultrabeam.type" // "ultrabeam" | "steppir" (default ultrabeam)
|
||||
keyMotorTransport = "ultrabeam.transport" // "tcp" | "serial" (default tcp)
|
||||
keyMotorCOM = "ultrabeam.com" // serial device name (COM3, /dev/ttyUSB0)
|
||||
@@ -12365,6 +12366,17 @@ func (a *App) ultrabeamFollowNow(freqHz int64) {
|
||||
if ref <= 0 {
|
||||
ref = c.LastSetKHz()
|
||||
}
|
||||
switch normMotorTrackMode(s.TrackMode) {
|
||||
case motorTrackAlways:
|
||||
// Every frequency change means every frequency change, including this one.
|
||||
case motorTrackBand:
|
||||
// The antenna is already resonant somewhere in this band — that is all the
|
||||
// operator asked for in band mode, so a spot click inside it moves nothing.
|
||||
if ref > 0 && bandForHz(int64(ref)*1000) == bandForHz(freqHz) {
|
||||
applog.Printf("ultrabeam: followNow stays in band %q (antenna at %d kHz) — no move", bandForHz(freqHz), ref)
|
||||
return
|
||||
}
|
||||
default:
|
||||
diff := khz - ref
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
@@ -12373,6 +12385,7 @@ func (a *App) ultrabeamFollowNow(freqHz int64) {
|
||||
applog.Printf("ultrabeam: followNow within deadband (%d kHz vs ref %d, step %d) — no move", khz, ref, step)
|
||||
return // within the deadband — don't chase a tiny QSY
|
||||
}
|
||||
}
|
||||
a.noteMotorMoveCommanded()
|
||||
if err := c.SetFrequency(khz, st.Direction); err != nil {
|
||||
applog.Printf("ultrabeam: immediate re-tune to %d kHz failed: %v", khz, err)
|
||||
@@ -14653,6 +14666,17 @@ type UltrabeamSettings struct {
|
||||
Baud int `json:"baud"` // serial baud
|
||||
Follow bool `json:"follow"` // re-tune the antenna to the rig's frequency
|
||||
StepKHz int `json:"step_khz"` // re-tune only when the freq moved this far (25/50/100)
|
||||
// When the follow loop is allowed to move the motors. The three choices the
|
||||
// SteppIR's own controller software offers, because operators arrive with
|
||||
// that mental model:
|
||||
// "always" — every frequency change. Resonance is always right, at the cost
|
||||
// of motors running constantly; on a SteppIR every move also
|
||||
// inhibits transmit while the elements travel.
|
||||
// "step" — only past a threshold (StepKHz). The default, and the sane
|
||||
// middle: the antenna follows a QSY but ignores tuning around.
|
||||
// "band" — only when the band changes. Motors move a handful of times a
|
||||
// day; resonance is whatever the band-entry frequency gave.
|
||||
TrackMode string `json:"track_mode"`
|
||||
TXInhibit bool `json:"tx_inhibit"` // block Flex transmission while the elements are moving
|
||||
// Bands the antenna covers — the follow filter. The follow loop only re-tunes
|
||||
// (and only lets TX-inhibit trigger) on a band in this set; on any other band
|
||||
@@ -14667,16 +14691,39 @@ type UltrabeamSettings struct {
|
||||
FreqMaxMHz int `json:"freq_max_mhz"`
|
||||
}
|
||||
|
||||
// Tracking modes. Stored as strings rather than an int so a settings row stays
|
||||
// readable when diagnosing an antenna that moves too much or not at all.
|
||||
const (
|
||||
motorTrackAlways = "always"
|
||||
motorTrackStep = "step"
|
||||
motorTrackBand = "band"
|
||||
)
|
||||
|
||||
// normMotorTrackMode keeps an unknown or empty value on the threshold mode
|
||||
// instead of guessing — a config written before this option existed then
|
||||
// behaves exactly as it did.
|
||||
func normMotorTrackMode(m string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(m)) {
|
||||
case motorTrackAlways:
|
||||
return motorTrackAlways
|
||||
case motorTrackBand:
|
||||
return motorTrackBand
|
||||
default:
|
||||
return motorTrackStep
|
||||
}
|
||||
}
|
||||
|
||||
// GetUltrabeamSettings returns the persisted motorized-antenna config, defaulting
|
||||
// to the pre-SteppIR behaviour (Ultrabeam over TCP) so an existing install is
|
||||
// unchanged.
|
||||
func (a *App) GetUltrabeamSettings() (UltrabeamSettings, error) {
|
||||
out := UltrabeamSettings{Type: "ultrabeam", Transport: "tcp", Port: 23, Baud: 9600, StepKHz: 50}
|
||||
out := UltrabeamSettings{Type: "ultrabeam", Transport: "tcp", Port: 23, Baud: 9600, StepKHz: 50, TrackMode: motorTrackStep}
|
||||
if a.settings == nil {
|
||||
return out, fmt.Errorf("db not initialized")
|
||||
}
|
||||
m, err := a.settings.GetMany(a.ctx, keyUltrabeamEnabled, keyUltrabeamHost, keyUltrabeamPort, keyUltrabeamFollow, keyUltrabeamStep,
|
||||
keyMotorType, keyMotorTransport, keyMotorCOM, keyMotorBaud, keyMotorTXInhibit, keyMotorFreqMin, keyMotorFreqMax, keyMotorBands)
|
||||
keyMotorType, keyMotorTransport, keyMotorCOM, keyMotorBaud, keyMotorTXInhibit, keyMotorFreqMin, keyMotorFreqMax, keyMotorBands,
|
||||
keyMotorTrackMode)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
@@ -14700,6 +14747,7 @@ func (a *App) GetUltrabeamSettings() (UltrabeamSettings, error) {
|
||||
if st, _ := strconv.Atoi(m[keyUltrabeamStep]); st == 25 || st == 50 || st == 100 {
|
||||
out.StepKHz = st
|
||||
}
|
||||
out.TrackMode = normMotorTrackMode(m[keyMotorTrackMode])
|
||||
out.FreqMinMHz, _ = strconv.Atoi(m[keyMotorFreqMin])
|
||||
out.FreqMaxMHz, _ = strconv.Atoi(m[keyMotorFreqMax])
|
||||
// Bands is the follow filter. If it was saved, use it verbatim. Otherwise this
|
||||
@@ -14765,6 +14813,7 @@ func (a *App) SaveUltrabeamSettings(s UltrabeamSettings) error {
|
||||
keyUltrabeamPort: strconv.Itoa(s.Port),
|
||||
keyUltrabeamFollow: boolStr(s.Follow),
|
||||
keyUltrabeamStep: strconv.Itoa(s.StepKHz),
|
||||
keyMotorTrackMode: normMotorTrackMode(s.TrackMode),
|
||||
keyMotorType: s.Type,
|
||||
keyMotorTransport: s.Transport,
|
||||
keyMotorCOM: strings.TrimSpace(s.COM),
|
||||
@@ -15004,10 +15053,21 @@ func (a *App) motorTXInhibitLoop(c motorAntenna, bands []string, stop <-chan str
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) ultrabeamFollowLoop(c motorAntenna, stepKHz int, bands []string, stop <-chan struct{}) {
|
||||
func (a *App) ultrabeamFollowLoop(c motorAntenna, mode string, stepKHz int, bands []string, stop <-chan struct{}) {
|
||||
if stepKHz <= 0 {
|
||||
stepKHz = 50
|
||||
}
|
||||
mode = normMotorTrackMode(mode)
|
||||
// "Every time the frequency changes" is the threshold mode with the smallest
|
||||
// threshold there is: the loop already re-tunes when the rig has moved at
|
||||
// least stepKHz from the last commanded frequency, and 1 kHz makes that
|
||||
// "moved at all". Expressing it this way keeps ONE decision path, so the
|
||||
// deadband reference — which is the rig, not the antenna's own flaky reported
|
||||
// frequency — cannot drift out of step between modes.
|
||||
if mode == motorTrackAlways {
|
||||
stepKHz = 1
|
||||
}
|
||||
lastCmdBand := "" // band of the last commanded move — the reference in band mode
|
||||
ticker := time.NewTicker(1500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
lastRigKHz := 0 // only log when the followed rig frequency actually changes
|
||||
@@ -15065,6 +15125,20 @@ func (a *App) ultrabeamFollowLoop(c motorAntenna, stepKHz int, bands []string, s
|
||||
ref = c.LastSetKHz()
|
||||
}
|
||||
}
|
||||
// Band mode ignores the threshold entirely: the antenna is re-tuned once
|
||||
// on entering a band and then left alone however far the rig roams
|
||||
// inside it. The reference is the band we last COMMANDED for, not the
|
||||
// rig's previous band — otherwise a first move after startup, or any
|
||||
// move the operator made by hand, would never be reconciled.
|
||||
if mode == motorTrackBand {
|
||||
b := bandForHz(rs.FreqHz)
|
||||
if b == lastCmdBand {
|
||||
continue
|
||||
}
|
||||
if newFreq {
|
||||
applog.Printf("ultrabeam: band changed %q → %q — re-tuning to %d kHz", lastCmdBand, b, rigKHz)
|
||||
}
|
||||
} else {
|
||||
diff := rigKHz - ref
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
@@ -15072,12 +15146,14 @@ func (a *App) ultrabeamFollowLoop(c motorAntenna, stepKHz int, bands []string, s
|
||||
if ref > 0 && diff < stepKHz {
|
||||
continue // within the deadband — leave the motors alone
|
||||
}
|
||||
}
|
||||
a.noteMotorMoveCommanded()
|
||||
if err := c.SetFrequency(rigKHz, st.Direction); err != nil {
|
||||
applog.Printf("ultrabeam: follow re-tune to %d kHz failed: %v", rigKHz, err)
|
||||
} else {
|
||||
lastCmdKHz = rigKHz
|
||||
applog.Printf("ultrabeam: followed rig → %d kHz (dir %d, was ref %d kHz, step %d)", rigKHz, st.Direction, ref, stepKHz)
|
||||
lastCmdBand = bandForHz(rs.FreqHz)
|
||||
applog.Printf("ultrabeam: followed rig → %d kHz (dir %d, was ref %d kHz, mode %s, step %d)", rigKHz, st.Direction, ref, mode, stepKHz)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15099,6 +15175,7 @@ type UltrabeamStatusInfo struct {
|
||||
// and change tracking without loading the whole settings block for a poll.
|
||||
Follow bool `json:"follow"`
|
||||
StepKHz int `json:"step_khz"`
|
||||
TrackMode string `json:"track_mode"`
|
||||
// Bands the antenna is configured to cover — the widget offers exactly these
|
||||
// as buttons rather than inventing its own list, so a band dropped in Settings
|
||||
// cannot be clicked here.
|
||||
@@ -15113,6 +15190,7 @@ func (a *App) GetUltrabeamStatus() UltrabeamStatusInfo {
|
||||
out.Type = s.Type
|
||||
out.Follow = s.Follow
|
||||
out.StepKHz = s.StepKHz
|
||||
out.TrackMode = normMotorTrackMode(s.TrackMode)
|
||||
out.Bands = append(out.Bands, s.Bands...)
|
||||
if a.motorAnt == nil {
|
||||
return out
|
||||
@@ -15218,7 +15296,7 @@ func (a *App) MotorNudgeKHz(deltaKHz int) error {
|
||||
// opening Settings. Both are ordinary operating decisions — an operator turns
|
||||
// tracking off to park the antenna and back on to resume — and a preferences
|
||||
// dialog is the wrong place for something changed that often.
|
||||
func (a *App) SetMotorFollow(on bool, stepKHz int) error {
|
||||
func (a *App) SetMotorFollow(on bool, stepKHz int, mode string) error {
|
||||
s, err := a.GetUltrabeamSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -15230,6 +15308,11 @@ func (a *App) SetMotorFollow(on bool, stepKHz int) error {
|
||||
default:
|
||||
return fmt.Errorf("step must be 25, 50 or 100 kHz")
|
||||
}
|
||||
// An empty mode leaves it alone, so the caller toggling tracking on and off
|
||||
// does not have to know or resend it.
|
||||
if strings.TrimSpace(mode) != "" {
|
||||
s.TrackMode = normMotorTrackMode(mode)
|
||||
}
|
||||
s.Follow = on
|
||||
|
||||
// Persist WITHOUT the restart. SaveUltrabeamSettings tears the client down and
|
||||
@@ -15251,6 +15334,9 @@ func (a *App) SetMotorFollow(on bool, stepKHz int) error {
|
||||
if err := a.settings.Set(a.ctx, keyUltrabeamStep, strconv.Itoa(s.StepKHz)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.settings.Set(a.ctx, keyMotorTrackMode, normMotorTrackMode(s.TrackMode)); err != nil {
|
||||
return err
|
||||
}
|
||||
a.restartMotorFollow(s)
|
||||
return nil
|
||||
}
|
||||
@@ -15268,8 +15354,8 @@ func (a *App) restartMotorFollow(s UltrabeamSettings) {
|
||||
}
|
||||
stop := make(chan struct{})
|
||||
a.ubFollowStop = stop
|
||||
applog.Printf("ultrabeam: follow loop restarting — covered bands %v, step %d kHz", s.Bands, s.StepKHz)
|
||||
go a.ultrabeamFollowLoop(a.motorAnt, s.StepKHz, s.Bands, stop)
|
||||
applog.Printf("ultrabeam: follow loop restarting — covered bands %v, mode %s, step %d kHz", s.Bands, normMotorTrackMode(s.TrackMode), s.StepKHz)
|
||||
go a.ultrabeamFollowLoop(a.motorAnt, s.TrackMode, s.StepKHz, s.Bands, stop)
|
||||
}
|
||||
|
||||
// UltrabeamRetract retracts all elements (storage / safe position).
|
||||
|
||||
+4
-2
@@ -6,13 +6,15 @@
|
||||
"QSOs logged from WSJT-X now carry the space weather and the distance, like hand-logged ones. The UDP path stamped the station profile, the DXCC and the QSL defaults but not SFI, A, K or distance — so an operator running digital had those fields empty across the whole log. Space weather is only stamped on a contact less than a day old: a logger re-broadcasting its backlog would otherwise be handed this morning readings for last month contacts.",
|
||||
"Band openings: EME contacts are no longer mistaken for an opening. A 2 m opening was announced at 9650 km towards Japan on stations working moonbounce — real contacts, but the moon says nothing about the band, and an antenna pointed that way finds nothing. Each band now has the longest path the atmosphere can actually carry: 3500 km on 2 m, 4000 on 4 m, and no limit at all on 6 and 10 m where multi-hop really does go round the world.",
|
||||
"Kenwood: WSJT-X \"Fake It\" no longer leaves the dial on the transmit frequency. A frequency set while transmitting was not recorded, so WSJT-X was told the radio was already back on the receive frequency and never restored it.",
|
||||
"PowerGenius XL: the Station Control card now shows power, current, SWR and temperature without a FlexRadio. The meters were only ever drawn from the radio's stream, so a station on any other rig got an empty card while the amplifier was reporting all four over its own link."
|
||||
"PowerGenius XL: the Station Control card now shows power, current, SWR and temperature without a FlexRadio. The meters were only ever drawn from the radio's stream, so a station on any other rig got an empty card while the amplifier was reporting all four over its own link.",
|
||||
"Motorized antennas (Ultrabeam and SteppIR): tracking now offers the three modes the SteppIR controller software has — every frequency change, past a step of 25/50/100 kHz, or only when the band changes. Existing setups keep the step mode they already had."
|
||||
],
|
||||
"fr": [
|
||||
"Les QSO enregistrés depuis WSJT-X portent désormais la météo spatiale et la distance, comme ceux saisis à la main. Le chemin UDP posait le profil station, le DXCC et les défauts QSL mais ni SFI, ni A, ni K, ni distance — un opérateur en numérique avait donc ces champs vides sur tout son log. La météo spatiale n est posée que sur un contact de moins d un jour : sinon un logiciel qui rediffuse son historique se verrait attribuer les relevés de ce matin sur des contacts du mois dernier.",
|
||||
"Ouvertures de bande : les contacts EME ne sont plus pris pour une ouverture. Une ouverture 2 m était annoncée à 9650 km vers le Japon sur des stations en rebond lunaire — de vrais contacts, mais la Lune ne dit rien de la bande, et une antenne pointée par là ne trouve rien. Chaque bande a désormais la distance maximale que l atmosphère peut réellement porter : 3500 km en 2 m, 4000 en 4 m, et aucune limite en 6 et 10 m où les sauts multiples font vraiment le tour du monde.",
|
||||
"Kenwood : le « Fake It » de WSJT-X ne laisse plus le VFO sur la fréquence d émission. Un changement de fréquence pendant l émission n était pas enregistré, WSJT-X croyait donc la radio déjà revenue sur la fréquence de réception et ne la remettait jamais en place.",
|
||||
"PowerGenius XL : la carte du Contrôle station affiche désormais puissance, courant, ROS et température sans FlexRadio. Les mesures n étaient tirées que du flux de la radio, si bien qu une station sur une autre radio n avait qu une carte vide alors que l amplificateur remontait les quatre sur sa propre liaison."
|
||||
"PowerGenius XL : la carte du Contrôle station affiche désormais puissance, courant, ROS et température sans FlexRadio. Les mesures n étaient tirées que du flux de la radio, si bien qu une station sur une autre radio n avait qu une carte vide alors que l amplificateur remontait les quatre sur sa propre liaison.",
|
||||
"Antennes motorisées (Ultrabeam et SteppIR) : le suivi propose désormais les trois modes du logiciel du contrôleur SteppIR — à chaque changement de fréquence, au-delà d un pas de 25/50/100 kHz, ou seulement au changement de bande. Les installations existantes conservent le mode par pas qu elles avaient déjà."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,11 +3140,26 @@ 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>
|
||||
<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>
|
||||
@@ -3153,7 +3168,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<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>
|
||||
)}
|
||||
<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,19 +277,21 @@ 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. */}
|
||||
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))}
|
||||
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.follow && (ant.track_mode || 'step') === 'step' && (
|
||||
<select
|
||||
value={String(ant.step_khz || 50)}
|
||||
onChange={(e) => run(SetMotorFollow(true, parseInt(e.target.value, 10)))}
|
||||
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')}
|
||||
>
|
||||
@@ -297,6 +299,19 @@ function MotorAntennaWidget({ ant, refetch, t }: { ant: AntStatus; refetch: () =
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
{ant.follow && (
|
||||
<select
|
||||
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')}
|
||||
>
|
||||
<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>
|
||||
|
||||
<button type="button" disabled={!ant.connected}
|
||||
onClick={() => run(UltrabeamRetract())}
|
||||
|
||||
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"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// The tracking mode decides how often a motorized antenna's elements run, so a
|
||||
// value that fails to parse must not silently become the most aggressive
|
||||
// setting. Anything unrecognised — including the empty string every config
|
||||
// written before this option existed contains — has to land on the threshold
|
||||
// mode, which is exactly what those configs already did.
|
||||
func TestNormMotorTrackMode(t *testing.T) {
|
||||
for _, tc := range []struct{ in, want string }{
|
||||
{"always", motorTrackAlways},
|
||||
{"ALWAYS", motorTrackAlways},
|
||||
{" band ", motorTrackBand},
|
||||
{"step", motorTrackStep},
|
||||
{"", motorTrackStep}, // never configured — behaves as before
|
||||
{"everytime", motorTrackStep}, // near-miss, not "always"
|
||||
{"per-band", motorTrackStep}, // near-miss, not "band"
|
||||
{"25", motorTrackStep}, // a step value fed in by mistake
|
||||
} {
|
||||
if got := normMotorTrackMode(tc.in); got != tc.want {
|
||||
t.Errorf("normMotorTrackMode(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Band mode compares the band the rig is on against the band the antenna was
|
||||
// last commanded for. That comparison is bandForHz, and the property it has to
|
||||
// have is that two frequencies far apart within one band agree while two
|
||||
// frequencies close together across a band edge do not — otherwise the antenna
|
||||
// either never moves or moves on every QSY.
|
||||
func TestBandForHzDrivesBandTracking(t *testing.T) {
|
||||
same := [][2]int64{
|
||||
{14000000, 14350000}, // both ends of 20 m — one band, no move
|
||||
{7000000, 7200000}, // 40 m
|
||||
{50000000, 52000000}, // 6 m, a wide one
|
||||
}
|
||||
for _, p := range same {
|
||||
if a, b := bandForHz(p[0]), bandForHz(p[1]); a != b || a == "" {
|
||||
t.Errorf("%.3f MHz is %q but %.3f MHz is %q — band mode would re-tune inside one band",
|
||||
float64(p[0])/1e6, a, float64(p[1])/1e6, b)
|
||||
}
|
||||
}
|
||||
// A small QSY that crosses from 30 m into 20 m has to read as a band change.
|
||||
if a, b := bandForHz(10150000), bandForHz(14000000); a == b {
|
||||
t.Errorf("30 m and 20 m both read %q — band mode would never re-tune between them", a)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user