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
+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.