feat(steppir): a Calibrate button, and Retract explains itself

Calibrate was in the protocol notes at the top of the package ('V') and nowhere
else. It drives every element to its end stop so the controller re-learns where
zero is, and it is the cure for the one SteppIR fault an operator cannot reason
about: an antenna that tunes to the wrong length. The controller counts steps
from a remembered position, so once that memory is wrong — a power cut in
mid-move, elements pushed by hand, a motor that slipped — every frequency after
it is wrong by the same amount, silently.

It asks before it runs. Minutes, every element travelling its full length, and
the antenna unusable until it finishes: right answer to a mistuned beam, wrong
answer to a stray click during a contest.

It sits beside Retract as a two-column row rather than a second full-width
button. The docked widget is narrow and already tall, and the pair reads as what
it is — the two commands that move every element at once.

Retract (Home) was already there and worked on a SteppIR; it now carries a
tooltip saying what it is for, because "retract elements" does not tell an
operator that this is the storage position and that the next tune brings them
back out unaided.

An Ultrabeam controller has no calibration command, and the adapter says so
instead of pretending — the antenna interface is shared, and a button that
silently does nothing is worse than one that explains itself.

Untested on hardware: the byte is from SteppIR's own Transceiver Interface note,
which is the same source the rest of this package was cross-checked against.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-09-10 08:00:22 +02:00
co-authored by Claude Opus 5
parent ce7b3686f5
commit a88e871640
7 changed files with 99 additions and 14 deletions
+26
View File
@@ -18212,6 +18212,10 @@ type motorAntenna interface {
SetFrequency(khz, dir int) error SetFrequency(khz, dir int) error
SetDirection(dir int) error SetDirection(dir int) error
Retract() error Retract() error
// Calibrate re-learns where the elements are by driving them to their end
// stops. SteppIR only: an Ultrabeam has no such command, and says so rather
// than pretending.
Calibrate() error
LastSetKHz() int LastSetKHz() int
Status() motorStatus Status() motorStatus
// Elements returns the per-element lengths (mm) when the antenna exposes them // Elements returns the per-element lengths (mm) when the antenna exposes them
@@ -18244,6 +18248,9 @@ func (a ubAdapter) Stop() { a.c.Stop() }
func (a ubAdapter) SetFrequency(k, d int) error { return a.c.SetFrequency(k, d) } func (a ubAdapter) SetFrequency(k, d int) error { return a.c.SetFrequency(k, d) }
func (a ubAdapter) SetDirection(d int) error { return a.c.SetDirection(d) } func (a ubAdapter) SetDirection(d int) error { return a.c.SetDirection(d) }
func (a ubAdapter) Retract() error { return a.c.Retract() } func (a ubAdapter) Retract() error { return a.c.Retract() }
func (a ubAdapter) Calibrate() error {
return fmt.Errorf("calibration is a SteppIR command — an Ultrabeam controller has none")
}
func (a ubAdapter) LastSetKHz() int { return a.c.LastSetKHz() } func (a ubAdapter) LastSetKHz() int { return a.c.LastSetKHz() }
func (a ubAdapter) SetElement(n, mm int) error { return a.c.ModifyElement(n, mm) } func (a ubAdapter) SetElement(n, mm int) error { return a.c.ModifyElement(n, mm) }
func (a ubAdapter) ReadElements() ([]int, error) { return a.c.ReadElements() } func (a ubAdapter) ReadElements() ([]int, error) { return a.c.ReadElements() }
@@ -18282,6 +18289,7 @@ func (a steppirAdapter) Stop() { a.c.Stop() }
func (a steppirAdapter) SetFrequency(k, d int) error { return a.c.SetFrequency(k, d) } func (a steppirAdapter) SetFrequency(k, d int) error { return a.c.SetFrequency(k, d) }
func (a steppirAdapter) SetDirection(d int) error { return a.c.SetDirection(d) } func (a steppirAdapter) SetDirection(d int) error { return a.c.SetDirection(d) }
func (a steppirAdapter) Retract() error { return a.c.Retract() } func (a steppirAdapter) Retract() error { return a.c.Retract() }
func (a steppirAdapter) Calibrate() error { return a.c.Calibrate() }
func (a steppirAdapter) LastSetKHz() int { return a.c.LastSetKHz() } func (a steppirAdapter) LastSetKHz() int { return a.c.LastSetKHz() }
func (a steppirAdapter) Elements() []int { return nil } // SteppIR: no per-element control func (a steppirAdapter) Elements() []int { return nil } // SteppIR: no per-element control
func (a steppirAdapter) SetElement(_, _ int) error { func (a steppirAdapter) SetElement(_, _ int) error {
@@ -19198,6 +19206,24 @@ func (a *App) restartMotorFollow(s UltrabeamSettings) {
go a.ultrabeamFollowLoop(a.motorAnt, s.TrackMode, s.StepKHz, s.Bands, stop) go a.ultrabeamFollowLoop(a.motorAnt, s.TrackMode, s.StepKHz, s.Bands, stop)
} }
// MotorCalibrate runs a SteppIR calibration: the elements go to their end stops
// so the controller re-learns where zero is.
//
// The cure for an antenna that tunes to the wrong length — after a power cut
// mid-move, after the elements were pushed by hand, after a motor slipped. The
// controller counts steps from a remembered position, and once that memory is
// wrong, every frequency after it is wrong by the same amount.
//
// Minutes, and every element travels its full length. Not a contest-time button,
// which is why the interface asks first.
func (a *App) MotorCalibrate() error {
if a.motorAnt == nil {
return fmt.Errorf("antenna not connected")
}
applog.Printf("antenna: calibration started — the elements go to their end stops, this takes minutes")
return a.motorAnt.Calibrate()
}
// UltrabeamRetract retracts all elements (storage / safe position). // UltrabeamRetract retracts all elements (storage / safe position).
func (a *App) UltrabeamRetract() error { func (a *App) UltrabeamRetract() error {
if a.motorAnt == nil { if a.motorAnt == nil {
+4 -2
View File
@@ -9,7 +9,8 @@
"While tracking, the two frequencies and the antenna bearing sit beside the Tracking button. During a pass an operator watches the radio and the antenna, not a column on the far side of the window — and that column is the first thing hidden to get the map full width. The compass spins while the antenna is still slewing: a mast takes tens of seconds to cross a pass, and the difference between \"on its way\" and \"stuck\" is the whole reason to look at it.", "While tracking, the two frequencies and the antenna bearing sit beside the Tracking button. During a pass an operator watches the radio and the antenna, not a column on the far side of the window — and that column is the first thing hidden to get the map full width. The compass spins while the antenna is still slewing: a mast takes tens of seconds to cross a pass, and the difference between \"on its way\" and \"stuck\" is the whole reason to look at it.",
"Satellite frequencies are shown to a hundred hertz instead of one. The Doppler moves about sixty hertz a second on 70 cm, so the last two digits changed every tick and the display was a blur of numbers nobody could read and nobody needed. The radio still gets the whole figure — this is only how much of it is worth putting in front of you. The shift beside it now reads \"+9.7 kHz\" rather than \"+9741 Hz\".", "Satellite frequencies are shown to a hundred hertz instead of one. The Doppler moves about sixty hertz a second on 70 cm, so the last two digits changed every tick and the display was a blur of numbers nobody could read and nobody needed. The radio still gets the whole figure — this is only how much of it is worth putting in front of you. The shift beside it now reads \"+9.7 kHz\" rather than \"+9741 Hz\".",
"Rotator Genius: OpsLog no longer clamps a target to 360°, and reads the limits the Genius reports so it can drive an overlap when the controller offers one. In practice a Rotator Genius is a 360° controller — its Limits fields say where the mechanical stop sits within one turn, not how far the mast travels — so an operator with a 450° rotator still gets 360° of it, and that limit is the controllers, not OpsLogs. The rotator range is therefore not offered for it: a setting that can only ever be refused by the box is worse than none.", "Rotator Genius: OpsLog no longer clamps a target to 360°, and reads the limits the Genius reports so it can drive an overlap when the controller offers one. In practice a Rotator Genius is a 360° controller — its Limits fields say where the mechanical stop sits within one turn, not how far the mast travels — so an operator with a 450° rotator still gets 360° of it, and that limit is the controllers, not OpsLogs. The rotator range is therefore not offered for it: a setting that can only ever be refused by the box is worse than none.",
"FlexRadio, satellite: the uplink slice is properly armed. Creating a slice is asynchronous — the radio reports its number afterwards — and OpsLog carried on without waiting, so everything meant for the uplink went nowhere: it was never tuned (it sat at the 435.100 it was created with), never got its sideband, its antenna or its CTCSS tone, and never became the transmitter, leaving the radio transmitting on the DOWNLINK slice. Arming now waits for both slices, adopts one that the radio announces without a reply of its own, and gives a late-arriving uplink everything it was owed." "FlexRadio, satellite: the uplink slice is properly armed. Creating a slice is asynchronous — the radio reports its number afterwards — and OpsLog carried on without waiting, so everything meant for the uplink went nowhere: it was never tuned (it sat at the 435.100 it was created with), never got its sideband, its antenna or its CTCSS tone, and never became the transmitter, leaving the radio transmitting on the DOWNLINK slice. Arming now waits for both slices, adopts one that the radio announces without a reply of its own, and gives a late-arriving uplink everything it was owed.",
"SteppIR: a Calibrate button, beside Retract. It drives every element to its end stop so the controller re-learns where zero is — the cure for an antenna that tunes to the wrong length after a power cut mid-move, after the elements were pushed by hand, or after a motor slipped. It takes minutes and the antenna is unusable until it finishes, so it asks first. Ultrabeam controllers have no such command and say so rather than pretending. Retracting the elements was already there and now explains itself: it is the storage position, and the next tune brings them back out on its own."
], ],
"fr": [ "fr": [
"Si OpsLog sapprête à créer une base de réglages neuve et vide dans un dossier qui en contient déjà une pleine, il le dit — dans le journal de démarrage et à l’écran — au lieu de souvrir sans bruit comme si rien n’était configuré. Rien nest supprimé et rien nest deviné : le message nomme lautre fichier, là où vos réglages sont toujours.", "Si OpsLog sapprête à créer une base de réglages neuve et vide dans un dossier qui en contient déjà une pleine, il le dit — dans le journal de démarrage et à l’écran — au lieu de souvrir sans bruit comme si rien n’était configuré. Rien nest supprimé et rien nest deviné : le message nomme lautre fichier, là où vos réglages sont toujours.",
@@ -18,7 +19,8 @@
"Pendant le suivi, les deux fréquences et le cap de lantenne sont affichés à côté du bouton Tracking. Pendant un passage, on regarde la radio et lantenne, pas une colonne à lautre bout de la fenêtre — et cest la première chose quon masque pour avoir la carte en pleine largeur. La boussole tourne tant que lantenne est en mouvement : un pylône met des dizaines de secondes à traverser un passage, et distinguer « en route » de « bloqué » est toute la raison de la regarder.", "Pendant le suivi, les deux fréquences et le cap de lantenne sont affichés à côté du bouton Tracking. Pendant un passage, on regarde la radio et lantenne, pas une colonne à lautre bout de la fenêtre — et cest la première chose quon masque pour avoir la carte en pleine largeur. La boussole tourne tant que lantenne est en mouvement : un pylône met des dizaines de secondes à traverser un passage, et distinguer « en route » de « bloqué » est toute la raison de la regarder.",
"Les fréquences satellite sont affichées à la centaine de hertz au lieu du hertz. Le Doppler se déplace denviron soixante hertz par seconde en 70 cm : les deux derniers chiffres changeaient à chaque tick et laffichage était une bouillie de chiffres illisible et inutile. La radio reçoit toujours la valeur complète — il ne sagit que de ce qui vaut la peine d’être mis sous vos yeux. Le décalage à côté indique désormais « +9,7 kHz » plutôt que « +9741 Hz ».", "Les fréquences satellite sont affichées à la centaine de hertz au lieu du hertz. Le Doppler se déplace denviron soixante hertz par seconde en 70 cm : les deux derniers chiffres changeaient à chaque tick et laffichage était une bouillie de chiffres illisible et inutile. La radio reçoit toujours la valeur complète — il ne sagit que de ce qui vaut la peine d’être mis sous vos yeux. Le décalage à côté indique désormais « +9,7 kHz » plutôt que « +9741 Hz ».",
"Rotator Genius : OpsLog n’écrête plus une consigne à 360° et lit les limites que le Genius rapporte, de façon à exploiter un recouvrement quand le contrôleur en offre un. Dans les faits, le Rotator Genius est un contrôleur 360° — ses champs Limits indiquent où se trouve la butée mécanique dans un tour, pas la course du pylône — donc un rotor 450° nen donne que 360, et cette limite est celle du contrôleur, pas dOpsLog. Lamplitude du rotor nest donc pas proposée pour lui : un réglage que le boîtier ne pourra que refuser est pire que pas de réglage du tout.", "Rotator Genius : OpsLog n’écrête plus une consigne à 360° et lit les limites que le Genius rapporte, de façon à exploiter un recouvrement quand le contrôleur en offre un. Dans les faits, le Rotator Genius est un contrôleur 360° — ses champs Limits indiquent où se trouve la butée mécanique dans un tour, pas la course du pylône — donc un rotor 450° nen donne que 360, et cette limite est celle du contrôleur, pas dOpsLog. Lamplitude du rotor nest donc pas proposée pour lui : un réglage que le boîtier ne pourra que refuser est pire que pas de réglage du tout.",
"FlexRadio, satellite : la tranche de montée est correctement armée. Créer une tranche est asynchrone — la radio annonce son numéro ensuite — et OpsLog continuait sans attendre : tout ce qui était destiné à la montée partait dans le vide. Elle n’était jamais accordée (elle restait sur le 435,100 de sa création), ne recevait ni sa bande latérale, ni son antenne, ni sa tonalité CTCSS, et ne devenait jamais l’émettrice — la radio émettait donc sur la tranche de DESCENTE. Larmement attend maintenant les deux tranches, adopte celle que la radio annonce sans réponse propre, et donne à une montée arrivée en retard tout ce qui lui était dû." "FlexRadio, satellite : la tranche de montée est correctement armée. Créer une tranche est asynchrone — la radio annonce son numéro ensuite — et OpsLog continuait sans attendre : tout ce qui était destiné à la montée partait dans le vide. Elle n’était jamais accordée (elle restait sur le 435,100 de sa création), ne recevait ni sa bande latérale, ni son antenne, ni sa tonalité CTCSS, et ne devenait jamais l’émettrice — la radio émettait donc sur la tranche de DESCENTE. Larmement attend maintenant les deux tranches, adopte celle que la radio annonce sans réponse propre, et donne à une montée arrivée en retard tout ce qui lui était dû.",
"SteppIR : un bouton Calibrer, à côté de Rétracter. Il amène chaque élément en butée pour que le contrôleur retrouve son zéro — le remède à une antenne qui saccorde à la mauvaise longueur après une coupure en pleine course, après avoir poussé les éléments à la main, ou après un moteur qui a glissé. Cela prend plusieurs minutes et lantenne est inutilisable jusqu’à la fin : une confirmation est donc demandée. Les contrôleurs Ultrabeam nont pas cette commande et le disent, au lieu de faire semblant. Rétracter les éléments existait déjà et sexplique maintenant : cest la position de rangement, et le prochain accord les fait ressortir tout seuls."
] ]
}, },
{ {
+20 -2
View File
@@ -5,10 +5,10 @@
// how one of them quietly stops matching the other — a pattern button that sets // how one of them quietly stops matching the other — a pattern button that sets
// a different direction, a band list that tunes somewhere else. // a different direction, a band list that tunes somewhere else.
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { ArrowDownToLine, ChevronDown, ChevronUp, Loader2, Minus, Plus, RefreshCw, Antenna as AntennaIcon, X } from 'lucide-react'; import { ArrowDownToLine, ChevronDown, ChevronUp, Loader2, Minus, Plus, RefreshCw, Ruler, Antenna as AntennaIcon, X } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
SetUltrabeamDirection, UltrabeamRetract, MotorSetElement, MotorReadElements, SetUltrabeamDirection, UltrabeamRetract, MotorCalibrate, MotorSetElement, MotorReadElements,
MotorTuneKHz, MotorNudgeKHz, SetMotorFollow, MotorTuneKHz, MotorNudgeKHz, SetMotorFollow,
} from '../../wailsjs/go/main/App'; } from '../../wailsjs/go/main/App';
@@ -235,11 +235,29 @@ export function MotorAntennaWidget({ ant, refetch, t, onClose, essentialsOnly }:
</div> </div>
)} )}
{/* Home, and — on a SteppIR — calibrate beside it. Two buttons on one
row rather than a second full-width one: the widget is narrow and
already tall, and the pair reads as "the two things that move every
element at once", which is what they are. */}
<div className={cn('grid gap-1.5', isUB ? 'grid-cols-1' : 'grid-cols-2')}>
<button type="button" disabled={!ant.connected} <button type="button" disabled={!ant.connected}
onClick={() => run(UltrabeamRetract())} onClick={() => run(UltrabeamRetract())}
title={t('station.retractTip')}
className="w-full flex items-center justify-center gap-1.5 rounded-md border border-warning-border bg-warning-muted text-warning-muted-foreground py-1 text-xs font-semibold hover:brightness-95 disabled:opacity-40"> className="w-full flex items-center justify-center gap-1.5 rounded-md border border-warning-border bg-warning-muted text-warning-muted-foreground py-1 text-xs font-semibold hover:brightness-95 disabled:opacity-40">
<ArrowDownToLine className="size-3.5" /> {t('station.retract')} <ArrowDownToLine className="size-3.5" /> {t('station.retract')}
</button> </button>
{/* Asked first, always. Calibration runs every element to its end
stop and takes minutes — it is the right answer to an antenna that
tunes wrong, and the wrong answer to a stray click mid-contest. */}
{!isUB && (
<button type="button" disabled={!ant.connected}
onClick={() => { if (window.confirm(t('station.calibrateConfirm'))) run(MotorCalibrate()); }}
title={t('station.calibrateTip')}
className="w-full flex items-center justify-center gap-1.5 rounded-md border border-border bg-muted/40 py-1 text-xs font-semibold hover:brightness-95 disabled:opacity-40">
<Ruler className="size-3.5" /> {t('station.calibrate')}
</button>
)}
</div>
{isUB && !essentialsOnly && ( {isUB && !essentialsOnly && (
<div> <div>
+8
View File
@@ -271,6 +271,10 @@ const en: Dict = {
'station.rigOff': 'CAT is switched off', 'station.noPort': 'no port configured', 'station.rigOff': 'CAT is switched off', 'station.noPort': 'no port configured',
'station.connect': 'Connect', 'station.noVoiceMsg': 'No message recorded — Settings ▸ Audio.', 'station.connect': 'Connect', 'station.noVoiceMsg': 'No message recorded — Settings ▸ Audio.',
'station.title': 'Station Control', 'station.rotator': 'Rotator', 'station.rotateTo': 'Rotate to {az}°', 'station.rotatorNoRead': 'No heading read', 'station.bands': 'Bands', 'station.nudgeUp': 'Up {n} kHz', 'station.nudgeDown': 'Down {n} kHz', 'station.trackOn': 'Tracking on', 'station.trackOff': 'Tracking off', 'station.trackStepTip': 'Re-tune only when the rig has moved this far', 'station.trackModeTip': 'When the antenna is allowed to re-tune', 'station.trackAlways': 'Every frequency change', 'station.trackStep': 'Past a step', 'station.trackBand': 'Band change only', 'station.trackAlwaysTip': 'Follow every frequency change. Best resonance, but the motors run constantly — and on a SteppIR every move blocks transmit while the elements travel.', 'station.trackStepTipMode': 'Re-tune only once the rig has moved further than the step. Follows a QSY, ignores tuning around.', 'station.trackBandTip': 'Re-tune only when the band changes. The motors move a few times a day and are left alone within a band.', 'station.pattern': 'Pattern', 'station.bi': 'Bi', 'station.motorWidgetShow': 'Motorised antenna · click to show', 'station.motorWidgetHide': 'Motorised antenna — shown · click to hide', 'station.hideWidget': 'Hide this widget', 'station.retract': 'Retract elements', 'station.moving': 'MOVING', 'station.elements': 'Elements (mm)', 'station.read': 'Read', 'station.readLengths': 'Read current element lengths from the controller', 'station.noLengths': 'Lengths unknown — click Read to fetch them from the controller.', 'station.element': 'Element', 'station.reflector': 'Reflector', 'station.driven': 'Driven', 'station.director': 'Dir', 'station.set': 'Set', 'station.elementsHint': 'Each press lengthens/shortens the element by 2 mm (like the physical console). Verify which element responds on your antenna.', 'station.setExactLen': 'Click to type the exact current length (fixes the baseline if the auto-read is off).', 'station.atMax': 'Controller refused — the element is likely at its maximum length for this band, so it can\'t extend further.', 'station.go': 'Go', 'station.stop': 'Stop', 'station.dragHint': 'Drag the grip handle on the left of a panel to move it. Pick a column count to lay them out in a grid.', 'station.dragMove': 'Drag to move this panel', 'station.colsAuto': 'Auto', 'station.addDevice': 'Add device', 'station.editDevice': 'Edit device', 'station.empty': 'No relay boards yet. Add a WebSwitch 1216H or a KMTronic 8-relay board to control your station power and accessories.', 'station.online': 'Online', 'station.offline': 'Offline', 'station.edit': 'Edit', 'station.delete': 'Delete', 'station.relay': 'Relay', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': 'Device type', 'station.name': 'Name', 'station.host': 'Host / IP', 'station.hostHint': 'LAN IP for local use. To reach the board from OUTSIDE, put a full URL here — e.g. https://relay.yourdomain.com — pointing at a reverse proxy (Nginx Proxy Manager…) that fronts the boards HTTP port.', 'station.user': 'Username', 'station.pass': 'Password', 'station.optional': 'optional', 'station.ftdiSerial': 'FTDI serial number', 'station.detect': 'Detect', 'station.ftdiHint': 'The Denkovi board is driven via FTDI bit-bang (not the COM port). Pick its serial (e.g. DAE0006K). Needs the FTDI D2XX driver installed.', 'station.channels': 'Relays', 'station.comPort': 'COM port', 'station.noPorts': 'No ports found', 'station.usbRelayHint': 'Cheap USB-serial relay boards (CH340/LCUS) using the A0 command protocol. If yours does not switch, tell me its model / command set.', 'station.labels': 'Relay labels', 'station.cancel': 'Cancel', 'station.save': 'Save', 'station.test': 'Test connection', 'station.testOk': 'Connected — {n} relays', 'station.testFail': 'Not connected', 'station.detectNone': 'No FTDI board found — check the cable and that the D2XX driver is installed.', 'station.detectFound': '{n} board(s) detected.', 'station.title': 'Station Control', 'station.rotator': 'Rotator', 'station.rotateTo': 'Rotate to {az}°', 'station.rotatorNoRead': 'No heading read', 'station.bands': 'Bands', 'station.nudgeUp': 'Up {n} kHz', 'station.nudgeDown': 'Down {n} kHz', 'station.trackOn': 'Tracking on', 'station.trackOff': 'Tracking off', 'station.trackStepTip': 'Re-tune only when the rig has moved this far', 'station.trackModeTip': 'When the antenna is allowed to re-tune', 'station.trackAlways': 'Every frequency change', 'station.trackStep': 'Past a step', 'station.trackBand': 'Band change only', 'station.trackAlwaysTip': 'Follow every frequency change. Best resonance, but the motors run constantly — and on a SteppIR every move blocks transmit while the elements travel.', 'station.trackStepTipMode': 'Re-tune only once the rig has moved further than the step. Follows a QSY, ignores tuning around.', 'station.trackBandTip': 'Re-tune only when the band changes. The motors move a few times a day and are left alone within a band.', 'station.pattern': 'Pattern', 'station.bi': 'Bi', 'station.motorWidgetShow': 'Motorised antenna · click to show', 'station.motorWidgetHide': 'Motorised antenna — shown · click to hide', 'station.hideWidget': 'Hide this widget', 'station.retract': 'Retract elements', 'station.moving': 'MOVING', 'station.elements': 'Elements (mm)', 'station.read': 'Read', 'station.readLengths': 'Read current element lengths from the controller', 'station.noLengths': 'Lengths unknown — click Read to fetch them from the controller.', 'station.element': 'Element', 'station.reflector': 'Reflector', 'station.driven': 'Driven', 'station.director': 'Dir', 'station.set': 'Set', 'station.elementsHint': 'Each press lengthens/shortens the element by 2 mm (like the physical console). Verify which element responds on your antenna.', 'station.setExactLen': 'Click to type the exact current length (fixes the baseline if the auto-read is off).', 'station.atMax': 'Controller refused — the element is likely at its maximum length for this band, so it can\'t extend further.', 'station.go': 'Go', 'station.stop': 'Stop', 'station.dragHint': 'Drag the grip handle on the left of a panel to move it. Pick a column count to lay them out in a grid.', 'station.dragMove': 'Drag to move this panel', 'station.colsAuto': 'Auto', 'station.addDevice': 'Add device', 'station.editDevice': 'Edit device', 'station.empty': 'No relay boards yet. Add a WebSwitch 1216H or a KMTronic 8-relay board to control your station power and accessories.', 'station.online': 'Online', 'station.offline': 'Offline', 'station.edit': 'Edit', 'station.delete': 'Delete', 'station.relay': 'Relay', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': 'Device type', 'station.name': 'Name', 'station.host': 'Host / IP', 'station.hostHint': 'LAN IP for local use. To reach the board from OUTSIDE, put a full URL here — e.g. https://relay.yourdomain.com — pointing at a reverse proxy (Nginx Proxy Manager…) that fronts the boards HTTP port.', 'station.user': 'Username', 'station.pass': 'Password', 'station.optional': 'optional', 'station.ftdiSerial': 'FTDI serial number', 'station.detect': 'Detect', 'station.ftdiHint': 'The Denkovi board is driven via FTDI bit-bang (not the COM port). Pick its serial (e.g. DAE0006K). Needs the FTDI D2XX driver installed.', 'station.channels': 'Relays', 'station.comPort': 'COM port', 'station.noPorts': 'No ports found', 'station.usbRelayHint': 'Cheap USB-serial relay boards (CH340/LCUS) using the A0 command protocol. If yours does not switch, tell me its model / command set.', 'station.labels': 'Relay labels', 'station.cancel': 'Cancel', 'station.save': 'Save', 'station.test': 'Test connection', 'station.testOk': 'Connected — {n} relays', 'station.testFail': 'Not connected', 'station.detectNone': 'No FTDI board found — check the cable and that the D2XX driver is installed.', 'station.detectFound': '{n} board(s) detected.',
'station.retractTip': 'Home the elements into the hubs — the storage position, for a gale or a winter. The next time the antenna is tuned it comes back out on its own.',
'station.calibrate': 'Calibrate',
'station.calibrateTip': 'SteppIR: drive every element to its end stop so the controller re-learns where zero is. The cure for an antenna that tunes to the wrong length — after a power cut mid-move, after the elements were pushed by hand, after a motor slipped.',
'station.calibrateConfirm': 'Calibrate the antenna?\n\nEvery element travels its full length and this takes several minutes. Do not start it during a contest — the antenna is unusable until it finishes.',
'awards.followHint': 'Awards shown in the Awards tab. Leave the right side empty to show them all.', 'awards.available': 'All awards', 'awards.followed': 'Followed', 'awards.search': 'Filter…', 'awards.addAll': 'Add all', 'awards.clear': 'Clear', 'awards.allTracked': 'All awards are followed.', 'awards.noneFollowed': 'Nothing followed yet — the Awards tab shows all.', 'awards.followHint': 'Awards shown in the Awards tab. Leave the right side empty to show them all.', 'awards.available': 'All awards', 'awards.followed': 'Followed', 'awards.search': 'Filter…', 'awards.addAll': 'Add all', 'awards.clear': 'Clear', 'awards.allTracked': 'All awards are followed.', 'awards.noneFollowed': 'Nothing followed yet — the Awards tab shows all.',
'sec.awards': 'Awards', 'sec.cat': 'CAT interface', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'CW Keyer', 'sec.awards': 'Awards', 'sec.cat': 'CAT interface', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'CW Keyer',
'sec.antenna': 'Ultrabeam / Steppir', 'sec.antgenius': 'Antenna Genius', 'sec.tunergenius': 'Tuner Genius', 'sec.pgxl': 'Amplifier', 'sec.psu': 'Power supply', 'sec.antenna': 'Ultrabeam / Steppir', 'sec.antgenius': 'Antenna Genius', 'sec.tunergenius': 'Tuner Genius', 'sec.pgxl': 'Amplifier', 'sec.psu': 'Power supply',
@@ -908,6 +912,10 @@ const fr: Dict = {
'station.rigOff': 'le CAT est désactivé', 'station.noPort': 'aucun port configuré', 'station.rigOff': 'le CAT est désactivé', 'station.noPort': 'aucun port configuré',
'station.connect': 'Connecter', 'station.noVoiceMsg': 'Aucun message enregistré — Réglages ▸ Audio.', 'station.connect': 'Connecter', 'station.noVoiceMsg': 'Aucun message enregistré — Réglages ▸ Audio.',
'station.title': 'Contrôle station', 'station.rotator': 'Rotator', 'station.rotateTo': 'Tourner vers {az}°', 'station.rotatorNoRead': 'Azimut non lu', 'station.bands': 'Bandes', 'station.nudgeUp': 'Monter de {n} kHz', 'station.nudgeDown': 'Descendre de {n} kHz', 'station.trackOn': 'Suivi actif', 'station.trackOff': 'Suivi inactif', 'station.trackStepTip': 'Ne réaccorder que si le rig a bougé d au moins ça', 'station.trackModeTip': "Quand l'antenne a le droit de se réaccorder", 'station.trackAlways': 'À chaque changement', 'station.trackStep': 'Au-delà d un pas', 'station.trackBand': 'Changement de bande', 'station.trackAlwaysTip': "Suivre chaque changement de fréquence. Résonance idéale, mais les moteurs tournent en permanence — et sur une SteppIR chaque déplacement bloque l'émission le temps du mouvement.", 'station.trackStepTipMode': "Ne réaccorder qu'une fois le rig sorti du pas. Suit un QSY, ignore la recherche autour.", 'station.trackBandTip': "Ne réaccorder qu'au changement de bande. Les moteurs bougent quelques fois par jour et restent tranquilles dans une bande.", 'station.pattern': 'Diagramme', 'station.bi': 'Bi', 'station.motorWidgetShow': 'Antenne motorisée · cliquer pour afficher', 'station.motorWidgetHide': 'Antenne motorisée — affichée · cliquer pour masquer', 'station.hideWidget': 'Masquer ce widget', 'station.retract': 'Rétracter les éléments', 'station.moving': 'EN MOUVEMENT', 'station.elements': 'Éléments (mm)', 'station.read': 'Lire', 'station.readLengths': 'Lire les longueurs actuelles depuis le contrôleur', 'station.noLengths': 'Longueurs inconnues — clique sur Lire pour les récupérer depuis le contrôleur.', 'station.element': 'Élément', 'station.reflector': 'Réflecteur', 'station.driven': 'Radiateur', 'station.director': 'Dir', 'station.set': 'Régler', 'station.elementsHint': "Chaque appui allonge/raccourcit l'élément de 2 mm (comme le pupitre). Vérifie quel élément répond sur ton antenne.", 'station.setExactLen': "Clique pour taper la longueur actuelle exacte (recale la base si la lecture auto est fausse).", 'station.atMax': "Refusé par le contrôleur — l'élément est probablement en butée (longueur max pour cette bande), il ne peut plus s'allonger.", 'station.go': 'Aller', 'station.stop': 'Stop', 'station.dragHint': 'Glisse la poignée à gauche d un panneau pour le déplacer. Choisis un nombre de colonnes pour la disposition.', 'station.dragMove': 'Glisser pour déplacer ce panneau', 'station.colsAuto': 'Auto', 'station.addDevice': 'Ajouter un appareil', 'station.editDevice': "Modifier l'appareil", 'station.empty': "Aucune carte relais. Ajoute un WebSwitch 1216H ou une carte KMTronic 8 relais pour piloter l'alimentation et les accessoires de ta station.", 'station.online': 'En ligne', 'station.offline': 'Hors ligne', 'station.edit': 'Modifier', 'station.delete': 'Supprimer', 'station.relay': 'Relais', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': "Type d'appareil", 'station.name': 'Nom', 'station.host': 'Hôte / IP', 'station.hostHint': "IP du LAN en local. Pour joindre la carte depuis L'EXTÉRIEUR, saisis une URL complète ici — ex. https://relais.tondomaine.com — pointant vers un reverse proxy (Nginx Proxy Manager…) qui expose le port HTTP de la carte.", 'station.user': "Nom d'utilisateur", 'station.pass': 'Mot de passe', 'station.optional': 'optionnel', 'station.ftdiSerial': 'Numéro de série FTDI', 'station.detect': 'Détecter', 'station.ftdiHint': "La carte Denkovi se pilote en FTDI bit-bang (pas via le port COM). Choisis son numéro de série (ex. DAE0006K). Nécessite le driver FTDI D2XX installé.", 'station.channels': 'Relais', 'station.comPort': 'Port COM', 'station.noPorts': 'Aucun port', 'station.usbRelayHint': "Cartes USB-série bon marché (CH340/LCUS) protocole A0. Si la tienne ne commute pas, donne-moi le modèle / jeu de commandes.", 'station.labels': 'Libellés des relais', 'station.cancel': 'Annuler', 'station.save': 'Enregistrer', 'station.test': 'Tester la connexion', 'station.testOk': 'Connecté — {n} relais', 'station.testFail': 'Non connecté', 'station.detectNone': 'Aucune carte FTDI trouvée — vérifie le câble et que le driver D2XX est installé.', 'station.detectFound': '{n} carte(s) détectée(s).', 'station.title': 'Contrôle station', 'station.rotator': 'Rotator', 'station.rotateTo': 'Tourner vers {az}°', 'station.rotatorNoRead': 'Azimut non lu', 'station.bands': 'Bandes', 'station.nudgeUp': 'Monter de {n} kHz', 'station.nudgeDown': 'Descendre de {n} kHz', 'station.trackOn': 'Suivi actif', 'station.trackOff': 'Suivi inactif', 'station.trackStepTip': 'Ne réaccorder que si le rig a bougé d au moins ça', 'station.trackModeTip': "Quand l'antenne a le droit de se réaccorder", 'station.trackAlways': 'À chaque changement', 'station.trackStep': 'Au-delà d un pas', 'station.trackBand': 'Changement de bande', 'station.trackAlwaysTip': "Suivre chaque changement de fréquence. Résonance idéale, mais les moteurs tournent en permanence — et sur une SteppIR chaque déplacement bloque l'émission le temps du mouvement.", 'station.trackStepTipMode': "Ne réaccorder qu'une fois le rig sorti du pas. Suit un QSY, ignore la recherche autour.", 'station.trackBandTip': "Ne réaccorder qu'au changement de bande. Les moteurs bougent quelques fois par jour et restent tranquilles dans une bande.", 'station.pattern': 'Diagramme', 'station.bi': 'Bi', 'station.motorWidgetShow': 'Antenne motorisée · cliquer pour afficher', 'station.motorWidgetHide': 'Antenne motorisée — affichée · cliquer pour masquer', 'station.hideWidget': 'Masquer ce widget', 'station.retract': 'Rétracter les éléments', 'station.moving': 'EN MOUVEMENT', 'station.elements': 'Éléments (mm)', 'station.read': 'Lire', 'station.readLengths': 'Lire les longueurs actuelles depuis le contrôleur', 'station.noLengths': 'Longueurs inconnues — clique sur Lire pour les récupérer depuis le contrôleur.', 'station.element': 'Élément', 'station.reflector': 'Réflecteur', 'station.driven': 'Radiateur', 'station.director': 'Dir', 'station.set': 'Régler', 'station.elementsHint': "Chaque appui allonge/raccourcit l'élément de 2 mm (comme le pupitre). Vérifie quel élément répond sur ton antenne.", 'station.setExactLen': "Clique pour taper la longueur actuelle exacte (recale la base si la lecture auto est fausse).", 'station.atMax': "Refusé par le contrôleur — l'élément est probablement en butée (longueur max pour cette bande), il ne peut plus s'allonger.", 'station.go': 'Aller', 'station.stop': 'Stop', 'station.dragHint': 'Glisse la poignée à gauche d un panneau pour le déplacer. Choisis un nombre de colonnes pour la disposition.', 'station.dragMove': 'Glisser pour déplacer ce panneau', 'station.colsAuto': 'Auto', 'station.addDevice': 'Ajouter un appareil', 'station.editDevice': "Modifier l'appareil", 'station.empty': "Aucune carte relais. Ajoute un WebSwitch 1216H ou une carte KMTronic 8 relais pour piloter l'alimentation et les accessoires de ta station.", 'station.online': 'En ligne', 'station.offline': 'Hors ligne', 'station.edit': 'Modifier', 'station.delete': 'Supprimer', 'station.relay': 'Relais', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': "Type d'appareil", 'station.name': 'Nom', 'station.host': 'Hôte / IP', 'station.hostHint': "IP du LAN en local. Pour joindre la carte depuis L'EXTÉRIEUR, saisis une URL complète ici — ex. https://relais.tondomaine.com — pointant vers un reverse proxy (Nginx Proxy Manager…) qui expose le port HTTP de la carte.", 'station.user': "Nom d'utilisateur", 'station.pass': 'Mot de passe', 'station.optional': 'optionnel', 'station.ftdiSerial': 'Numéro de série FTDI', 'station.detect': 'Détecter', 'station.ftdiHint': "La carte Denkovi se pilote en FTDI bit-bang (pas via le port COM). Choisis son numéro de série (ex. DAE0006K). Nécessite le driver FTDI D2XX installé.", 'station.channels': 'Relais', 'station.comPort': 'Port COM', 'station.noPorts': 'Aucun port', 'station.usbRelayHint': "Cartes USB-série bon marché (CH340/LCUS) protocole A0. Si la tienne ne commute pas, donne-moi le modèle / jeu de commandes.", 'station.labels': 'Libellés des relais', 'station.cancel': 'Annuler', 'station.save': 'Enregistrer', 'station.test': 'Tester la connexion', 'station.testOk': 'Connecté — {n} relais', 'station.testFail': 'Non connecté', 'station.detectNone': 'Aucune carte FTDI trouvée — vérifie le câble et que le driver D2XX est installé.', 'station.detectFound': '{n} carte(s) détectée(s).',
'station.retractTip': 'Rentrer les éléments dans les moyeux — la position de rangement, pour un coup de vent ou pour lhiver. Au prochain accord, lantenne ressort delle-même.',
'station.calibrate': 'Calibrer',
'station.calibrateTip': 'SteppIR : amener chaque élément en butée pour que le contrôleur retrouve son zéro. Le remède à une antenne qui saccorde à la mauvaise longueur — après une coupure en pleine course, après avoir poussé les éléments à la main, après un moteur qui a glissé.',
'station.calibrateConfirm': 'Calibrer lantenne ?\n\nChaque élément parcourt toute sa course et cela prend plusieurs minutes. À ne pas lancer pendant un concours — lantenne est inutilisable jusqu’à la fin.',
'awards.followHint': 'Diplômes affichés dans longlet Awards. Laisse la colonne de droite vide pour tous les afficher.', 'awards.available': 'Tous les diplômes', 'awards.followed': 'Suivis', 'awards.search': 'Filtrer…', 'awards.addAll': 'Tout ajouter', 'awards.clear': 'Vider', 'awards.allTracked': 'Tous les diplômes sont suivis.', 'awards.noneFollowed': 'Aucun pour linstant — longlet Awards les montre tous.', 'awards.followHint': 'Diplômes affichés dans longlet Awards. Laisse la colonne de droite vide pour tous les afficher.', 'awards.available': 'Tous les diplômes', 'awards.followed': 'Suivis', 'awards.search': 'Filtrer…', 'awards.addAll': 'Tout ajouter', 'awards.clear': 'Vider', 'awards.allTracked': 'Tous les diplômes sont suivis.', 'awards.noneFollowed': 'Aucun pour linstant — longlet Awards les montre tous.',
'sec.awards': 'Diplômes', 'sec.cat': 'Interface CAT', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'Manipulateur CW', 'sec.awards': 'Diplômes', 'sec.cat': 'Interface CAT', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'Manipulateur CW',
'sec.antenna': 'Antenne motorisée', 'sec.antgenius': 'Antenna Genius', 'sec.tunergenius': 'Tuner Genius', 'sec.pgxl': 'Amplificateur', 'sec.psu': 'Alimentation', 'sec.antenna': 'Antenne motorisée', 'sec.antgenius': 'Antenna Genius', 'sec.tunergenius': 'Tuner Genius', 'sec.pgxl': 'Amplificateur', 'sec.psu': 'Alimentation',
+2
View File
@@ -853,6 +853,8 @@ export function LookupCallsign(arg1:string,arg2:string):Promise<lookup.Result>;
export function LookupCallsignFresh(arg1:string,arg2:string):Promise<lookup.Result>; export function LookupCallsignFresh(arg1:string,arg2:string):Promise<lookup.Result>;
export function MotorCalibrate():Promise<void>;
export function MotorNudgeKHz(arg1:number):Promise<void>; export function MotorNudgeKHz(arg1:number):Promise<void>;
export function MotorReadElements():Promise<Array<number>>; export function MotorReadElements():Promise<Array<number>>;
+4
View File
@@ -1638,6 +1638,10 @@ export function LookupCallsignFresh(arg1, arg2) {
return window['go']['main']['App']['LookupCallsignFresh'](arg1, arg2); return window['go']['main']['App']['LookupCallsignFresh'](arg1, arg2);
} }
export function MotorCalibrate() {
return window['go']['main']['App']['MotorCalibrate']();
}
export function MotorNudgeKHz(arg1) { export function MotorNudgeKHz(arg1) {
return window['go']['main']['App']['MotorNudgeKHz'](arg1); return window['go']['main']['App']['MotorNudgeKHz'](arg1);
} }
+25
View File
@@ -741,3 +741,28 @@ func portBusyHint(mode, com string, err error) string {
} }
return " — another program already has " + com + " open (the SteppIR control window, PstRotator, a terminal). A COM port has one owner: close the other program, then OpsLog can connect." return " — another program already has " + com + " open (the SteppIR control window, PstRotator, a terminal). A COM port has one owner: close the other program, then OpsLog can connect."
} }
// Calibrate runs the controller's calibration: the elements are driven to their
// end stops so the controller re-learns where zero is.
//
// It is the cure for an antenna that tunes to the wrong length after a power cut
// mid-move, after the elements have been retracted by hand, or after a motor has
// slipped — the controller counts steps from a remembered position, and once
// that memory is wrong every frequency after it is wrong by the same amount.
//
// It takes MINUTES and moves every element the whole way, so it is not something
// to do during a contest. Like Retract it drops the controller out of AUTOTRACK,
// which is handled transparently: the next SetFrequency re-issues AUTOTRACK ON.
func (c *Client) Calibrate() error {
// A valid frequency accompanies every SET frame; the controller ignores it
// for this command, but a malformed frame is refused outright.
khz := c.LastSetKHz()
if khz <= 0 {
if st, _ := c.GetStatus(); st != nil && st.Frequency > 0 {
khz = st.Frequency
} else {
khz = 14000
}
}
return c.writeCmd(buildSet(khz*1000, DirNormal, 'V'))
}