fix(tci): send the spot colour as an unsigned ARGB integer

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.
This commit is contained in:
2026-08-28 16:23:32 +02:00
parent fd7ae77a61
commit 47ddbed665
2 changed files with 31 additions and 8 deletions
+4 -2
View File
@@ -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."
]
},
{
+27 -6
View File
@@ -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.