From 2d67e3d57ffd5e291e5858d82372c20d27391ba2 Mon Sep 17 00:00:00 2001 From: rouggy Date: Fri, 4 Sep 2026 18:48:25 +0200 Subject: [PATCH] fix(udp): ':' is Q65 to WSJT-X and FT4 to JTDX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported from a real shack: JTDX decoding FT4 came through as Q65. A Decode carries a one-character marker rather than a mode name, and the forks do not agree on that one — so the character alone cannot answer, and its wrong answer reached everything behind it: new mode, new slot, the mode filter, all computed against a mode nobody was using. The sending program's own Status settles it. It comes from that program, names the mode in full, and is re-sent whenever it changes, so it knows what is being decoded in a way one character never can. Only the ambiguous marker consults it: the ones both forks agree on keep answering from the table, Status or no Status, and with no Status at all ':' still reads as Q65 — WSJT-X's meaning, the older and commoner. --- changelog.json | 6 +++-- internal/integrations/udp/decodemode_test.go | 24 ++++++++++++++++++++ internal/integrations/udp/wsjt.go | 23 ++++++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/changelog.json b/changelog.json index 95ac887..0fc3f64 100644 --- a/changelog.json +++ b/changelog.json @@ -9,7 +9,8 @@ "“Send to Cloudlog / Wavelog” joins the right-click upload menu. It was the one configured service missing from it: Cloudlog keeps no per-QSO sent status — deliberately, since it dedupes server-side — and the menu had been built around that status. An explicit selection needs no status to be safe, which is exactly why the absence stops mattering here.", "HAMLOG.online uploads are closed. The site no longer issues API keys and its upload API takes nothing else, so the auto-upload switch and the “Send to” entry armed something that could only fail. Everything else stays: their confirmations still import from a file (which never needed a key), and the sent/received state already in your log remains readable, filterable and bulk-editable. The upload code is kept whole against the day keys come back.", "Yaesu console: the power slider no longer springs back to 100 W on a 200 W radio. The console already knew an FTDX101MP could do 200 — that is what it drew the slider to — but the command that sets it was clamped to 100, so the rig was politely given half of what was asked for and the next poll showed it. FTDX101MP, FT-DX5000 and FTDX9000 reach 200 W now, and a rig reporting more than expected is still believed.", - "Chase new: clicking a row now does what clicking a cluster spot does — including telling WSJT-X, JTDX or MSHV to change mode, so picking an FT4 station while the decoder sits in FT8 actually moves it. It also brings across the mode and its RST preset, and any park or summit reference. It used to do a hand-picked half of that, which left the one thing the window exists for — jumping onto a station — decoding the wrong mode." + "Chase new: clicking a row now does what clicking a cluster spot does — including telling WSJT-X, JTDX or MSHV to change mode, so picking an FT4 station while the decoder sits in FT8 actually moves it. It also brings across the mode and its RST preset, and any park or summit reference. It used to do a hand-picked half of that, which left the one thing the window exists for — jumping onto a station — decoding the wrong mode.", + "FT decodes: JTDX on FT4 no longer reads as Q65. A decode carries a one-character mode marker, and the two programs disagree about “:” — Q65 in WSJT-X, FT4 in JTDX — so the character alone cannot answer, and the wrong answer poisoned every verdict behind it: new mode, new slot, the mode filter. The sending program’s own status names the mode in full and now settles it; the markers both forks agree on are untouched." ], "fr": [ "Résolution des entités avec le fichier ClubLog activé : les préfixes retirés sont de nouveau reconnus. cty.dat décrit le monde tel qu’il est AUJOURD’HUI — Niue est passée en E6, donc ZK2 y est revenu à la Nouvelle-Zélande, et tous les contacts ZK2 jamais faits étaient silencieusement réétiquetés Nouvelle-Zélande, emportant une entité confirmée hors du DXCC de l’opérateur. ZK1 perdait les Cook du Sud de la même façon. La table de préfixes ClubLog connaît toujours les deux : elle est désormais consultée dès qu’aucune exception par indicatif ne s’applique, et non plus seulement pour les indicatifs qui en avaient une. Elle ne prime jamais sur une entrée exacte « =INDICATIF » de cty.dat, et là où elle n’a pas de réponse c’est cty.dat qui tranche. Repassez un import concerné par « Mettre à jour depuis ClubLog ».", @@ -18,7 +19,8 @@ "« Envoyer vers Cloudlog / Wavelog » rejoint le menu d’envoi du clic droit. C’était le seul service configuré qui y manquait : Cloudlog ne conserve aucun statut d’envoi par QSO — volontairement, puisqu’il dédoublonne côté serveur — et le menu était construit autour de ce statut. Une sélection explicite n’a besoin d’aucun statut pour être sûre : c’est précisément pourquoi cette absence cesse de compter ici.", "Les envois vers HAMLOG.online sont fermés. Le site ne délivre plus de clé API et son interface d’envoi n’accepte rien d’autre : la case d’envoi automatique et l’entrée « Envoyer vers » armaient donc quelque chose qui ne pouvait qu’échouer. Tout le reste demeure : leurs confirmations s’importent toujours depuis un fichier (ce qui n’a jamais demandé de clé), et l’état envoyé/reçu déjà présent dans votre log reste lisible, filtrable et modifiable en masse. Le code d’envoi est conservé intact pour le jour où les clés reviendraient.", "Console Yaesu : le curseur de puissance ne revient plus à 100 W sur une radio de 200 W. La console savait déjà qu’un FTDX101MP peut sortir 200 — c’est à cela qu’elle dimensionnait son curseur — mais la commande d’envoi était bornée à 100 : le poste recevait donc poliment la moitié de ce qu’on lui demandait, et le sondage suivant l’affichait. FTDX101MP, FT-DX5000 et FTDX9000 atteignent désormais 200 W, et une radio qui annonce plus que prévu reste crue sur parole.", - "Chase new : cliquer une ligne fait désormais ce que fait un clic sur un spot du cluster — y compris demander à WSJT-X, JTDX ou MSHV de changer de mode, si bien que choisir une station FT4 alors que le décodeur est en FT8 l’y amène vraiment. Le mode et son RST par défaut suivent aussi, de même que toute référence de parc ou de sommet. La fenêtre n’en faisait qu’une moitié choisie à la main, ce qui laissait la seule chose pour laquelle elle existe — sauter sur une station — décoder dans le mauvais mode." + "Chase new : cliquer une ligne fait désormais ce que fait un clic sur un spot du cluster — y compris demander à WSJT-X, JTDX ou MSHV de changer de mode, si bien que choisir une station FT4 alors que le décodeur est en FT8 l’y amène vraiment. Le mode et son RST par défaut suivent aussi, de même que toute référence de parc ou de sommet. La fenêtre n’en faisait qu’une moitié choisie à la main, ce qui laissait la seule chose pour laquelle elle existe — sauter sur une station — décoder dans le mauvais mode.", + "FT decodes : JTDX en FT4 ne s’affiche plus en Q65. Un décodage porte un marqueur de mode d’un seul caractère, et les deux logiciels ne s’accordent pas sur « : » — Q65 pour WSJT-X, FT4 pour JTDX : le caractère seul ne peut donc pas trancher, et sa mauvaise réponse contaminait tout ce qui en découle — nouveau mode, nouveau slot, filtre de mode. Le statut envoyé par le logiciel lui-même nomme le mode en entier et tranche désormais ; les marqueurs sur lesquels les deux s’accordent ne changent pas." ] }, { diff --git a/internal/integrations/udp/decodemode_test.go b/internal/integrations/udp/decodemode_test.go index 29f4678..a2559ba 100644 --- a/internal/integrations/udp/decodemode_test.go +++ b/internal/integrations/udp/decodemode_test.go @@ -113,3 +113,27 @@ func TestDecodeKeepsTheRawModeMarkerForReplies(t *testing.T) { ev.DecodeModeRaw, "~") } } + +// Reported from a real shack: JTDX decoding FT4 showed up as Q65. +// +// The two forks disagree about ":" — Q65 in WSJT-X, FT4 in JTDX — so the +// character alone cannot answer, and the wrong answer poisons every verdict +// that follows: new mode, new slot, the mode filter. The program's own Status +// names the mode in full and settles it. +func TestAmbiguousModeCharDefersToTheProgram(t *testing.T) { + cases := []struct { + raw, status, want, why string + }{ + {":", "FT4", "FT4", "JTDX decoding FT4"}, + {":", "Q65", "Q65", "WSJT-X decoding Q65"}, + {":", "", "Q65", "no Status yet — WSJT-X's reading, the older and commoner"}, + // The unambiguous markers are unaffected, Status or no Status. + {"~", "FT4", "FT8", "a tilde is FT8 in both"}, + {"+", "", "FT4", "a plus is FT4 in both"}, + } + for _, c := range cases { + if got := DecodeModeName(c.raw, c.status); got != c.want { + t.Errorf("DecodeModeName(%q, %q) = %q, want %q — %s", c.raw, c.status, got, c.want, c.why) + } + } +} diff --git a/internal/integrations/udp/wsjt.go b/internal/integrations/udp/wsjt.go index 88c382e..9b8c85c 100644 --- a/internal/integrations/udp/wsjt.go +++ b/internal/integrations/udp/wsjt.go @@ -523,10 +523,24 @@ var decodeModeChar = map[string]string{ "#": "JT65", "@": "JT9", "&": "MSK144", - ":": "Q65", "`": "FST4", } +// ambiguousModeChar is a marker the forks do not agree on. +// +// ":" is Q65 in WSJT-X and FT4 in JTDX, so the character alone cannot answer: +// a JTDX operator decoding FT4 was told they were on Q65, and every verdict +// downstream — new mode, new slot, the mode filter — was computed against a +// mode nobody was using. +// +// The sender's own Status settles it. It comes from the same program, names the +// mode in full, and is re-sent whenever it changes, so it knows what that +// program is decoding in a way one character never can. The fallback is +// WSJT-X's reading, which is the older and commoner one. +var ambiguousModeChar = map[string]string{ + ":": "Q65", +} + // DecodeModeName resolves a Decode's mode field to a real mode name. statusMode // is the mode from the same program's last Status, used when the field is a // marker we do not know, or empty. @@ -535,6 +549,13 @@ func DecodeModeName(raw, statusMode string) string { if m, ok := decodeModeChar[raw]; ok { return m } + if fallback, ok := ambiguousModeChar[raw]; ok { + // Believe the program over the character it happened to print. + if st := strings.ToUpper(strings.TrimSpace(statusMode)); st != "" { + return st + } + return fallback + } // A mode name is at least two alphanumeric characters ("FT8", "JS8", "Q65"). // Anything shorter, or carrying punctuation, is a marker rather than a name. if len(raw) >= 2 {