fix(antenna,udp): name the reason tracking is off; infer the remote-tune unit
The SteppIR report was not a SteppIR fault. In the log the antenna starts, answers every poll, and sits at 21050 kHz while the rig works 21074 — and the status line only prints when the frame CHANGES, so a link that is alive and parked looks identical to one that died. The one line that explained it said "ultrabeam: follow loop stopped", which covers two quite different situations and, at startup where nothing was running, reads as a fault. Tracking was simply switched off. It now says which of the two it is, names the antenna type, and states the consequence — that the antenna will not follow the rig. Same log, a second and unrelated fault: every launch failed a tune request from DXHunter with "frequency 2107400000000 out of the 11-digit CAT range". <FREQ> was read as MHz, but DXHunter had echoed back the value OpsLog itself published in the N1MM RadioInfo broadcast, whose <Freq> is in tens of Hz. Both units come from the same peer, so the unit is now inferred: try MHz, kHz, Hz, tens of Hz, and keep the first that lands on an amateur band — anything a station is asked to tune to is in one. Hz before tens of Hz because their only overlap, 40 m against 4 m, is far more likely to be 40 m. Nothing plausible tunes nothing and says so, rather than sending the rig to a wrong band.
This commit is contained in:
@@ -15900,13 +15900,24 @@ func (a *App) restartMotorFollow(s UltrabeamSettings) {
|
|||||||
close(a.ubFollowStop)
|
close(a.ubFollowStop)
|
||||||
a.ubFollowStop = nil
|
a.ubFollowStop = nil
|
||||||
}
|
}
|
||||||
if !s.Follow || a.motorAnt == nil {
|
// Say WHICH of the two reasons it is. "follow loop stopped" covered both, and
|
||||||
applog.Printf("ultrabeam: follow loop stopped")
|
// at startup — where nothing was running to stop — it read as a fault. An
|
||||||
|
// operator whose antenna sat at 21050 while the rig worked 21074 sent a log
|
||||||
|
// that said the antenna had started, answered every poll, and had its follow
|
||||||
|
// loop "stopped": three lines that together looked like a link going dead,
|
||||||
|
// when tracking was simply switched off and the antenna was waiting to be
|
||||||
|
// tuned by hand.
|
||||||
|
if a.motorAnt == nil {
|
||||||
|
applog.Printf("antenna: tracking not started — no antenna connected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !s.Follow {
|
||||||
|
applog.Printf("antenna: %s connected, but TRACKING IS OFF in Settings — it will not follow the rig, and only moves when you tune it by hand", s.Type)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stop := make(chan struct{})
|
stop := make(chan struct{})
|
||||||
a.ubFollowStop = stop
|
a.ubFollowStop = stop
|
||||||
applog.Printf("ultrabeam: follow loop restarting — covered bands %v, mode %s, step %d kHz", s.Bands, normMotorTrackMode(s.TrackMode), s.StepKHz)
|
applog.Printf("antenna: %s tracking the rig — covered bands %v, mode %s, step %d kHz", s.Type, s.Bands, normMotorTrackMode(s.TrackMode), s.StepKHz)
|
||||||
go a.ultrabeamFollowLoop(a.motorAnt, s.TrackMode, s.StepKHz, s.Bands, stop)
|
go a.ultrabeamFollowLoop(a.motorAnt, s.TrackMode, s.StepKHz, s.Bands, stop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -7,14 +7,18 @@
|
|||||||
"DX cluster: when spots arrive and every one is filtered out, the panel says so, names the filters doing it and offers to clear them — it used to say “waiting for spots” beside a counter reading 76 live.",
|
"DX cluster: when spots arrive and every one is filtered out, the panel says so, names the filters doing it and offers to clear them — it used to say “waiting for spots” beside a counter reading 76 live.",
|
||||||
"DX cluster: the log times the connection and the first spot, so a slow first launch can be told apart from a quiet node.",
|
"DX cluster: the log times the connection and the first spot, so a slow first launch can be told apart from a quiet node.",
|
||||||
"Generic HTTP relay: its URLs were never actually sent — fixed. {value} sends the relay's label, {relay-1} counts from zero, no host needed.",
|
"Generic HTTP relay: its URLs were never actually sent — fixed. {value} sends the relay's label, {relay-1} counts from zero, no host needed.",
|
||||||
"Switching profile now reconnects the amplifier, antenna, Antenna Genius and tuner — they stayed on the previous profile's ports until you saved Settings."
|
"Switching profile now reconnects the amplifier, antenna, Antenna Genius and tuner — they stayed on the previous profile's ports until you saved Settings.",
|
||||||
|
"The log now says when a motorized antenna is connected but tracking is off, instead of a line that read like a fault.",
|
||||||
|
"DXHunter spot clicks tune the rig again: the frequency it sends is read in whatever unit it uses, not always as MHz."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"WinKeyer : la sonde d’ouverture part en un seul envoi, calquée sur la capture d’un client qui dialogue avec le même manipulateur K3NG, et les octets de la poignée de main sont toujours journalisés pour diagnostiquer un manipulateur muet.",
|
"WinKeyer : la sonde d’ouverture part en un seul envoi, calquée sur la capture d’un client qui dialogue avec le même manipulateur K3NG, et les octets de la poignée de main sont toujours journalisés pour diagnostiquer un manipulateur muet.",
|
||||||
"Cluster DX : quand des spots arrivent et que tout est filtré, le panneau le dit, nomme les filtres responsables et propose de les effacer — il affichait « en attente de spots » à côté d’un compteur à 76 en direct.",
|
"Cluster DX : quand des spots arrivent et que tout est filtré, le panneau le dit, nomme les filtres responsables et propose de les effacer — il affichait « en attente de spots » à côté d’un compteur à 76 en direct.",
|
||||||
"Cluster DX : le journal chronomètre la connexion et le premier spot, pour distinguer un premier lancement lent d’un nœud silencieux.",
|
"Cluster DX : le journal chronomètre la connexion et le premier spot, pour distinguer un premier lancement lent d’un nœud silencieux.",
|
||||||
"Relais HTTP générique : ses URL n’étaient jamais envoyées — corrigé. {value} envoie le libellé du relais, {relay-1} compte de zéro, hôte inutile.",
|
"Relais HTTP générique : ses URL n’étaient jamais envoyées — corrigé. {value} envoie le libellé du relais, {relay-1} compte de zéro, hôte inutile.",
|
||||||
"Changer de profil reconnecte l’ampli, l’antenne, l’Antenna Genius et le tuner — ils restaient sur les ports du profil précédent jusqu’à un Enregistrer."
|
"Changer de profil reconnecte l’ampli, l’antenne, l’Antenna Genius et le tuner — ils restaient sur les ports du profil précédent jusqu’à un Enregistrer.",
|
||||||
|
"Le journal indique désormais qu’une antenne motorisée est connectée mais que le suivi est désactivé, au lieu d’une ligne qui ressemblait à une panne.",
|
||||||
|
"Les clics de spot DXHunter accordent à nouveau le rig : la fréquence envoyée est lue dans son unité réelle, plus toujours en MHz."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package udp
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// A remote-call <FREQ> arrives in whatever unit the sender happens to use, and
|
||||||
|
// the same sender uses more than one. Every form has to land on the same dial
|
||||||
|
// frequency, because the alternative is a rig sent to the wrong band.
|
||||||
|
func TestRemoteTuneUnits(t *testing.T) {
|
||||||
|
const m20 = 14_074_000
|
||||||
|
for _, c := range []struct {
|
||||||
|
in string
|
||||||
|
want int64
|
||||||
|
why string
|
||||||
|
}{
|
||||||
|
{"14.074", m20, "MHz, the documented DXHunter form"},
|
||||||
|
{"10.136", 10_136_000, "MHz, 30 m"},
|
||||||
|
{"14074", m20, "kHz"},
|
||||||
|
{"14074.0", m20, "kHz with a decimal point"},
|
||||||
|
{"1407400", m20, "tens of Hz — what N1MM RadioInfo publishes, echoed back"},
|
||||||
|
{"2107400", 21_074_000, "the value from the field log that failed every time"},
|
||||||
|
{"14074000", m20, "Hz"},
|
||||||
|
{"7000000", 7_000_000, "Hz on 40 m, not tens of Hz on 4 m"},
|
||||||
|
{"0", 0, "no frequency"},
|
||||||
|
{"999999999999", 0, "nothing plausible — tune nothing rather than guess"},
|
||||||
|
{"abc", 0, "not a number"},
|
||||||
|
} {
|
||||||
|
if got := remoteTuneHz(c.in); got != c.want {
|
||||||
|
t.Errorf("remoteTuneHz(%q) = %d, want %d (%s)", c.in, got, c.want, c.why)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,38 @@ import (
|
|||||||
"hamlog/internal/applog"
|
"hamlog/internal/applog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// remoteTuneHz turns a <FREQ> value from a remote-call packet into Hz.
|
||||||
|
//
|
||||||
|
// The field has NO agreed unit, and the same sender uses two of them. DXHunter
|
||||||
|
// documents "<FREQ>10.136" — MHz — but a log from a working station showed it
|
||||||
|
// echoing "<FREQ>2107400" straight back: the frequency OpsLog had just
|
||||||
|
// published to it in the N1MM RadioInfo broadcast, whose <Freq> is in units of
|
||||||
|
// 10 Hz. Read as MHz, that asked the rig for 2 107 400 MHz, and every tune
|
||||||
|
// request failed with "out of the 11-digit CAT range" from the first second
|
||||||
|
// after launch.
|
||||||
|
//
|
||||||
|
// So the unit is inferred: try each one and keep the first that lands on an
|
||||||
|
// amateur band. Anything a station is asked to tune to is, by definition, in
|
||||||
|
// one. Hz is tried before 10 Hz because the one overlap between them — a 40 m
|
||||||
|
// frequency in Hz reads as a 4 m one in tens of Hz — is far more likely to be
|
||||||
|
// 40 m. Nothing plausible means nothing is tuned: a wrong band is worse than a
|
||||||
|
// request that visibly did nothing.
|
||||||
|
func remoteTuneHz(s string) int64 {
|
||||||
|
v, err := strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil || v <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, hz := range []int64{int64(v * 1e6), int64(v * 1e3), int64(v), int64(v * 10)} {
|
||||||
|
if bandFromHz(hz) != "" {
|
||||||
|
return hz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Refusing in silence is how the previous version's failure looked from the
|
||||||
|
// outside: a spot clicked in another program, and nothing happening here.
|
||||||
|
applog.Printf("udp: remote_call <FREQ>%s is not a frequency in any amateur band in MHz, kHz or Hz — not tuning\n", s)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// remoteFreqRe / remoteModeRe pull the optional tune request out of a
|
// remoteFreqRe / remoteModeRe pull the optional tune request out of a
|
||||||
// ServiceRemoteCall packet: "<FREQ>10.136" (MHz) and "<MODE>FT8". Both accept
|
// ServiceRemoteCall packet: "<FREQ>10.136" (MHz) and "<MODE>FT8". Both accept
|
||||||
// an optional closing tag for proper-XML senders.
|
// an optional closing tag for proper-XML senders.
|
||||||
@@ -397,9 +429,7 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
|||||||
// otherwise leave their values as stray tokens and corrupt the
|
// otherwise leave their values as stray tokens and corrupt the
|
||||||
// "last token = callsign" heuristic.
|
// "last token = callsign" heuristic.
|
||||||
if m := remoteFreqRe.FindStringSubmatch(text); m != nil {
|
if m := remoteFreqRe.FindStringSubmatch(text); m != nil {
|
||||||
if mhz, err := strconv.ParseFloat(m[1], 64); err == nil && mhz > 0 {
|
ev.TuneFreqHz = remoteTuneHz(m[1])
|
||||||
ev.TuneFreqHz = int64(mhz * 1e6)
|
|
||||||
}
|
|
||||||
text = strings.Replace(text, m[0], " ", 1)
|
text = strings.Replace(text, m[0], " ", 1)
|
||||||
}
|
}
|
||||||
if m := remoteModeRe.FindStringSubmatch(text); m != nil {
|
if m := remoteModeRe.FindStringSubmatch(text); m != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user