fix(rotator): the Rotator Genius is a 360° controller, and OpsLog says so

Correcting the previous commit, which offered a 450° rotator range for the
Rotator Genius. It cannot do it.

The evidence is the operator's own box and 4O3A's manual together. His Rotator
Configuration reads "Limits: 5 to 4" — that is where the mechanical stop sits
within ONE turn, a dead zone at four and a half degrees, not a range of travel.
The manual is unambiguous about what happens past it: "you will not be able to
give it a target beyond the limits". A 450° mast on a Rotator Genius is a 450°
mast used as a 360° one, and that limit belongs to the controller.

So the setting goes. Offering an operator a 450° option that the box can only
ever refuse is worse than not offering one — it spends their evening proving
that the software was wrong about their station.

What stays is the half that was genuinely ours: GoTo no longer clamps to 360
before sending, and the limits the Genius reports on every heading query are now
read instead of skipped over. The overlap branch is driven entirely by what the
device answers — no setting, no assumption — so a controller that one day
reports a range past 360 is driven through it without a line changing here.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-09-09 23:58:49 +02:00
co-authored by Claude Opus 5
parent 3dad00f8ad
commit 3c93684b2b
3 changed files with 32 additions and 59 deletions
+21 -52
View File
@@ -22518,71 +22518,40 @@ func (a *App) IcomConsolePTT(on bool) error {
return a.cat.SetPTT(on) return a.cat.SetPTT(on)
} }
// rotgeniusGoTo picks which way round to reach a bearing on a mast with an // rotgeniusGoTo picks which way round to reach a bearing when the controller
// overlap. // has an overlap to offer.
// //
// A 450° rotator can be at 010° twice over: once at 10 and once at 370, and // THE GENIUS DECIDES, and it needs no setting from us. It reports the limits it
// only the second reaches it without unwinding the cable back through north. // is configured with on every heading query, and those are the truth about what
// OpsLog used to clamp every target to 360, so an operator with such a mast // is bolted to the tower: if the far side of an overlap is reachable it says so,
// watched the Rotator Genius stop dead at 359 and had to press "clockwise" by // and if it is not, asking anyway turns a working command into a rejected one.
// hand to get through north. // Its manual is unambiguous — "you will not be able to give it a target beyond
// the limits".
// //
// THE GENIUS DECIDES WHAT IS REACHABLE, not us and not a setting. It reports the // In practice today that means the plain bearing, every time. A Rotator Genius
// limits it is configured with, and they are the truth about what is bolted to // is a 360° controller: its Limits fields say where the mechanical stop sits
// the tower: a box configured "5 to 4" — the factory 360° range — refuses a // within one turn ("5 to 4" is a dead zone at four and a half degrees), not how
// target of 370, and asking anyway turns a working command into a rejected one. // far the mast can travel, and an operator with a 450° rotator gets 360° of it.
// So the overlap is used only when the Genius itself says it has one, and when // The overlap branch stays because the decision is made from what the device
// it does not, the operator is told, because the setting to change is in the // reports rather than from an assumption about it — a controller that one day
// Genius and not here. // answers 450 will be driven through the overlap without a line changing here.
// //
// Which of the two forms is right then depends on where the antenna IS, so the // Which of the two forms is right depends on where the antenna IS, so the
// heading is read first and the nearer one wins. That is the point of an // heading is read first and the nearer one wins: a beam at 350° heading for 010°
// overlap: a beam at 350° heading for 010° should cross north, not travel the // should cross north, not travel the other 340 degrees.
// other 340 degrees.
func rotgeniusGoTo(l rotorLink, az int) error { func rotgeniusGoTo(l rotorLink, az int) error {
c := rotgenius.New(l.Host, l.Port) c := rotgenius.New(l.Host, l.Port)
a := ((az % 360) + 360) % 360 a := ((az % 360) + 360) % 360
if l.MaxAz <= 360 {
return c.GoTo(l.Num, a)
}
st, _, err := c.Heading(l.Num) st, _, err := c.Heading(l.Num)
if err != nil || !st.Connected { if err != nil || !st.Connected || st.LimitCW <= 360 {
// No reading to compare against. The plain bearing is always reachable;
// the overlap is an optimisation, not a requirement.
return c.GoTo(l.Num, a) return c.GoTo(l.Num, a)
} }
if st.LimitCW <= 360 {
// OpsLog is set to 450 and the Genius is not. Said once per move rather
// than silently doing the wrong thing — the fix is in the Genius's own
// rotator configuration, and nothing here can reach past its limits.
rotgeniusRangeWarn(l, st)
return c.GoTo(l.Num, a)
}
target := a
if alt := a + 360; alt <= st.LimitCW && absInt(alt-st.Azimuth) < absInt(a-st.Azimuth) { if alt := a + 360; alt <= st.LimitCW && absInt(alt-st.Azimuth) < absInt(a-st.Azimuth) {
target = alt
applog.Printf("rotator: %d° is nearer as %d° from the antenna's %d° (Genius limit %d)", applog.Printf("rotator: %d° is nearer as %d° from the antenna's %d° (Genius limit %d)",
a, alt, st.Azimuth, st.LimitCW) a, alt, st.Azimuth, st.LimitCW)
return c.GoTo(l.Num, alt)
} }
return c.GoTo(l.Num, target) return c.GoTo(l.Num, a)
}
// rotgeniusRangeWarn says, at most once a minute, that the two ends disagree
// about the mast.
var rotgeniusWarnedAt sync.Map // host:port → time.Time
func rotgeniusRangeWarn(l rotorLink, st rotgenius.Status) {
key := fmt.Sprintf("%s:%d/%d", l.Host, l.Port, l.Num)
if v, ok := rotgeniusWarnedAt.Load(key); ok {
if t, _ := v.(time.Time); time.Since(t) < time.Minute {
return
}
}
rotgeniusWarnedAt.Store(key, time.Now())
applog.Printf("rotator: OpsLog is set to a 450° mast but the Rotator Genius is configured %d° to %d° — "+
"a range it will not go past, whatever is asked. Change the limits in the Genius's own Rotator "+
"Configuration; until then the antenna takes the long way round through north.",
st.LimitCCW, st.LimitCW)
} }
func absInt(v int) int { func absInt(v int) int {
+2 -2
View File
@@ -6,13 +6,13 @@
"The antenna readout no longer flickers in and out during a pass. The rotator is asked where it is every three seconds, but the tracking status was rebuilt from scratch every second and dropped the answer in between — so the antenna appeared for one second in three, which reads as a rotator that keeps disconnecting.", "The antenna readout no longer flickers in and out during a pass. The rotator is asked where it is every three seconds, but the tracking status was rebuilt from scratch every second and dropped the answer in between — so the antenna appeared for one second in three, which reads as a rotator that keeps disconnecting.",
"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: a 450° mast can now be pointed through the overlap instead of the long way round. OpsLog clamped every target to 360°, so a bearing just past north meant the rotator stopped at 359 and the operator pressed \"clockwise\" by hand to get through it. A rotator range now appears for the Rotator Genius like the other backends it applies to, and the nearer of the two ways to a bearing is taken — 010° reached as 370° when the antenna is already at 35. The Genius's own limits are read and they win: a box configured for 360° refuses a target beyond it, so OpsLog does not ask, and says in the log that the range has to be changed in the Genius's own Rotator Configuration." "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 36 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."
], ],
"fr": [ "fr": [
"Laffichage de lantenne ne clignote plus pendant un passage. Le rotor est interrogé toutes les trois secondes, mais l’état du suivi était reconstruit de zéro chaque seconde et perdait la réponse entre-temps — lantenne apparaissait donc une seconde sur trois, ce qui se lit comme un rotor qui se déconnecte sans arrêt.", "Laffichage de lantenne ne clignote plus pendant un passage. Le rotor est interrogé toutes les trois secondes, mais l’état du suivi était reconstruit de zéro chaque seconde et perdait la réponse entre-temps — lantenne apparaissait donc une seconde sur trois, ce qui se lit comme un rotor qui se déconnecte sans arrêt.",
"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 : un pylône 450° peut désormais être pointé à travers le recouvrement au lieu de faire le tour. OpsLog écrêtait toute consigne à 360°, donc un cap juste après le nord arrêtait le rotor à 359 et il fallait cliquer « clockwise » à la main pour le franchir. Lamplitude du rotor apparaît maintenant pour le Rotator Genius comme pour les autres pilotes concernés, et le plus court des deux chemins est pris — 010° atteint comme 370° quand lantenne est déjà à 350°. Les limites propres au Genius sont lues et priment : un boîtier configuré en 360° refuse une consigne au-delà, donc OpsLog ne la lui envoie pas et écrit dans le journal que lamplitude est à changer dans la configuration du Genius lui-même." "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."
] ]
}, },
{ {
+9 -5
View File
@@ -5136,11 +5136,15 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
// does its own overlap; two programs each deciding to go the long // does its own overlap; two programs each deciding to go the long
// way round is how an antenna unwinds mid-pass. // way round is how an antenna unwinds mid-pass.
// //
// A Rotator Genius is in between: it has its OWN limits, and they // NOT offered for a Rotator Genius. Its manual is plain — "you will
// win — this setting only tells OpsLog it may ask for the far side // not be able to give it a target beyond the limits" — and its
// of the overlap at all. If the Genius is configured 360°, it says // Limits fields say where the mechanical stop sits within ONE turn
// so in the log rather than sending commands the box refuses. // ("5 to 4" is a dead zone at four and a half degrees), not how far
const ownsOverlap = isERC || isEasycomm || isRG; // the mast travels. Offering 450° there would be offering a setting
// that can only ever be refused by the box. Whether the overlap is
// used is decided from what the Genius itself reports, in
// rotgeniusGoTo, and needs no setting at all.
const ownsOverlap = isERC || isEasycomm;
return ( return (
<div key={dev.id || i} className="rounded-xl border border-border bg-card/40 p-3 space-y-3"> <div key={dev.id || i} className="rounded-xl border border-border bg-card/40 p-3 space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">