From 47ddbed665ea9a089a31de12c156ea9732b3a4c4 Mon Sep 17 00:00:00 2001 From: rouggy Date: Fri, 28 Aug 2026 16:23:32 +0200 Subject: [PATCH] fix(tci): send the spot colour as an unsigned ARGB integer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expert Electronics' own document gives the command whole: SPOT:RN6LHF,CW,7100000,16711680,ANY_TEXT; 16711680 is 0x00FF0000 — positive. This backend sent the same field as a SIGNED 32-bit value, from a third-party example: with the alpha byte set to FF for opacity, 0xFFFFA500 becomes -22336, and a spot whose colour ExpertSDR cannot read is dropped without a reply or an error. Reported on ExpertSDR3 1.3, which speaks TCI 2.x — the protocol version was never the problem, and my first reading of that report was wrong. The first three spots are now logged verbatim: an ignored spot leaves no trace whatsoever, so that line is the only evidence of what went out. --- changelog.json | 6 ++++-- internal/cat/tci.go | 33 +++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/changelog.json b/changelog.json index 8d58ae5..c65d902 100644 --- a/changelog.json +++ b/changelog.json @@ -13,7 +13,8 @@ "A busy cluster no longer makes the rest of the interface sluggish: incoming spots are grouped into fewer, larger updates as the feed gets faster (up to half a second), instead of redrawing the window twenty times a second. A quiet cluster still shows each spot as it lands.", "SunSDR console: the meters work. The S-meter, transmit power and SWR are pushed by the radio only to a client that subscribes, and OpsLog never did — it was reading commands ExpertSDR3 does not send.", "Cluster: the “N new spots” counter no longer jumps to the whole buffer. It was looking for the row it had frozen on, and a station spotted again replaces its row — so the count fell through to “everything is new”.", - "E-mail: a refused SMTP login now says what to do about it — Microsoft 365 and outlook.com have switched off password-based SMTP, and an app password does not bring it back." + "E-mail: a refused SMTP login now says what to do about it — Microsoft 365 and outlook.com have switched off password-based SMTP, and an app password does not bring it back.", + "TCI panorama spots: the colour was sent as a negative number and ExpertSDR dropped every spot in silence. It now goes out as the unsigned ARGB integer the protocol document uses, and the first few spots are written to the log verbatim." ], "fr": [ "Téléchargement LoTW : les détails QSL (date du QSL, locator, état, comté) deviennent optionnels et désactivés par défaut — LoTW met environ dix fois plus longtemps à construire ce rapport, vingt minutes contre deux sur le même compte, et marquer une confirmation n'en a pas besoin. Toujours demandés automatiquement quand on ajoute les QSO absents du log.", @@ -26,7 +27,8 @@ "Un cluster chargé ne ralentit plus le reste de l'interface : les spots entrants sont regroupés en mises à jour moins nombreuses à mesure que le flux s'accélère (jusqu'à une demi-seconde), au lieu de redessiner la fenêtre vingt fois par seconde. Sur un cluster calme, chaque spot s'affiche toujours dès son arrivée.", "Console SunSDR : les mesures fonctionnent. Le S-mètre, la puissance et le ROS ne sont envoyés qu'à un client qui s'abonne, ce qu'OpsLog ne faisait pas — il lisait des commandes qu'ExpertSDR3 n'envoie pas.", "Cluster : le compteur « N nouveaux spots » ne saute plus à la taille du tampon. Il cherchait la ligne sur laquelle il s'était figé, or une station re-spottée remplace sa ligne — le compte basculait donc sur « tout est nouveau ».", - "E-mail : un refus d'authentification SMTP explique désormais quoi faire — Microsoft 365 et outlook.com ont désactivé le SMTP par mot de passe, et un mot de passe d'application ne le rétablit pas." + "E-mail : un refus d'authentification SMTP explique désormais quoi faire — Microsoft 365 et outlook.com ont désactivé le SMTP par mot de passe, et un mot de passe d'application ne le rétablit pas.", + "Spots sur le panorama TCI : la couleur partait en nombre négatif et ExpertSDR écartait chaque spot en silence. Elle est désormais envoyée en entier ARGB non signé, comme dans la documentation du protocole, et les premiers spots sont écrits tels quels dans le journal." ] }, { diff --git a/internal/cat/tci.go b/internal/cat/tci.go index 4b82786..d344268 100644 --- a/internal/cat/tci.go +++ b/internal/cat/tci.go @@ -33,6 +33,8 @@ type TCI struct { // What the server said it is, from its "protocol:" announcement. serverName string serverVersion string + // How many spots have been logged verbatim (the first few only). + spotsSent int // OnSpotClick is called when the user clicks one of our spots on the TCI // panorama (callsign + freq), so the host can fill the entry form. Set before @@ -160,6 +162,9 @@ func (t *TCI) Connect() error { // only to a client that has subscribed. 200 ms is the rate the protocol's own // examples use — fast enough for a needle, slow enough not to flood a socket // that also carries audio. + if t.spotsEnabled { + debugLog.Printf("TCI: panorama spots are ON — spots will be sent to the radio") + } _ = t.send("rx_sensors_enable:true,200;") _ = t.send("tx_sensors_enable:true,200;") if t.spotsEnabled { @@ -241,11 +246,19 @@ func (t *TCI) SendSpot(s SpotInfo) error { // other two matching what already works here. _ = t.send(fmt.Sprintf("spot_delete:%s;", call)) } - // TCI's SPOT command wants the colour as a signed 32-bit DECIMAL integer in - // 0xAARRGGBB order — NOT a "0x…" hex string (e.g. "spot:UN7GK,cw,14025000, - // -16776961,test;"). ExpertSDR silently drops a spot whose colour field it - // can't parse as a number, which is why spots never showed on the panorama - // while tuning (a separate command) still worked. + // The colour is a DECIMAL ARGB integer, and an UNSIGNED one. + // + // Expert Electronics' own protocol document gives the whole command: + // + // SPOT:RN6LHF,CW,7100000,16711680,ANY_TEXT; + // + // 16711680 is 0x00FF0000 — positive, alpha zero. This backend was sending + // the same number as a SIGNED 32-bit value, taken from a third-party + // example: with the alpha byte set to FF for opacity, 0xFFFFA500 becomes + // -22336, and a spot whose colour field ExpertSDR cannot read is dropped in + // silence. Reported on ExpertSDR3 1.3 (which speaks TCI 2.x, so the version + // was never the problem): everything else worked and the panorama stayed + // empty. hex := strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(s.Color), "#"), "0x") if hex == "" { hex = "FFFFA500" // opaque orange default @@ -266,7 +279,15 @@ func (t *TCI) SendSpot(s SpotInfo) error { } // Commas/semicolons would break TCI's comma-separated argument parsing. text := strings.NewReplacer(",", " ", ";", " ").Replace(s.Comment) - return t.send(fmt.Sprintf("spot:%s,%s,%d,%d,%s;", call, mode, s.FreqHz, int32(argb), text)) + cmd := fmt.Sprintf("spot:%s,%s,%d,%d,%s;", call, mode, s.FreqHz, argb, text) + // The first few, verbatim. A spot that the radio ignores leaves no trace at + // all — no reply, no error — so the only evidence that OpsLog sent one, and + // in what shape, is this line. + if n := t.spotsSent; n < 3 { + t.spotsSent = n + 1 + debugLog.Printf("TCI: sending spot #%d: %s", n+1, strings.TrimSuffix(cmd, ";")) + } + return t.send(cmd) } // Disconnect closes the WebSocket; the reader goroutine then exits.