From 8acfda4e42b7bf8182dbbceadd227e2b522f2efc Mon Sep 17 00:00:00 2001 From: rouggy Date: Fri, 28 Aug 2026 22:51:25 +0200 Subject: [PATCH] fix(tci): resubscribe to the meters at 'ready', and log what comes back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The report from a real SunSDR: transmit power and SWR still empty after the sensors fix. The subscription went out once, at connect — while ExpertSDR3 is still streaming its initial state dump, exactly where a unidirectional control command can be ignored. It is now renewed every time the server says 'ready'. The sensor messages also join the logged-on-arrival set, capped like the rest: the next log will say whether TX_SENSORS ever arrives, which is the question — a radio that never sends it (AetherSDR may not) and a frame that arrived and was dropped leave the same blank meters and need opposite fixes. --- changelog.json | 10 ++++++++++ internal/cat/tci.go | 21 +++++++++++++++++++-- internal/cat/tci_panel.go | 3 ++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/changelog.json b/changelog.json index 5e9a062..f32cdf2 100644 --- a/changelog.json +++ b/changelog.json @@ -1,4 +1,14 @@ [ + { + "version": "0.26.24", + "date": "", + "en": [ + "TCI (SunSDR): the meter subscription is renewed when the radio announces it is ready — sent only at connect, it could fall inside the initial state dump and be ignored, which left the transmit power and SWR empty. The log now also records the subscription and the first sensor frames, so a report can tell “the radio never sends them” from “they arrived and were dropped”." + ], + "fr": [ + "TCI (SunSDR) : l'abonnement aux mesures est renouvelé quand la radio annonce qu'elle est prête — envoyé seulement à la connexion, il pouvait tomber pendant l'envoi initial de l'état et être ignoré, laissant la puissance et le ROS vides en émission. Le journal enregistre aussi l'abonnement et les premières trames de mesure, pour distinguer « la radio ne les envoie jamais » de « elles arrivaient et étaient perdues »." + ] + }, { "version": "0.26.23", "date": "", diff --git a/internal/cat/tci.go b/internal/cat/tci.go index d344268..53454cb 100644 --- a/internal/cat/tci.go +++ b/internal/cat/tci.go @@ -165,8 +165,7 @@ func (t *TCI) Connect() error { 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;") + t.subscribeSensors("connect") if t.spotsEnabled { // Forget what we thought was on the panorama at the same moment the radio // is told to drop it. Kept, the memory would suppress the next spot for @@ -461,6 +460,17 @@ func (t *TCI) SetTXAudioSource(src string) { } // send writes a command to the WebSocket (one writer at a time). +// subscribeSensors asks the radio to push its meters. Nothing measures anything +// until this goes out — the S-meter, the transmit power and the SWR are all +// subscription-only (TCI §4.4) — and it is sent at connect AND again at every +// "ready", because a subscription sent during the server's initial dump can be +// dropped. 200 ms is the rate the protocol's own examples use. +func (t *TCI) subscribeSensors(when string) { + e1 := t.send("rx_sensors_enable:true,200;") + e2 := t.send("tx_sensors_enable:true,200;") + debugLog.Printf("TCI: sensor subscription sent (%s): rx=%v tx=%v", when, e1, e2) +} + func (t *TCI) send(cmd string) error { t.mu.Lock() c := t.conn @@ -567,6 +577,13 @@ func (t *TCI) handle(msg string) { } case "ready", "start": t.ready = true + // (Re)subscribe to the meters HERE, not only at connect. ExpertSDR3 + // dumps its whole state and then says "ready"; a unidirectional control + // command sent while that dump is still in flight can be ignored, and + // the report from a real SunSDR — transmit meters still empty after the + // connect-time subscription — has exactly that shape. From a goroutine: + // send takes t.mu, which this handler holds. + go t.subscribeSensors("ready") case "stop": t.ready = false case "vfo": diff --git a/internal/cat/tci_panel.go b/internal/cat/tci_panel.go index 0204c73..06e2160 100644 --- a/internal/cat/tci_panel.go +++ b/internal/cat/tci_panel.go @@ -113,7 +113,8 @@ func (t *TCI) handlePanel(name string, get func(int) string, args string) bool { // it. What the radio actually announces after the command settles whether // this is our reading or its doing, and no amount of reasoning will. switch name { - case "mute", "sql_enable", "sql_level", "tx_power", "tx_swr", "tune": + case "mute", "sql_enable", "sql_level", "tx_power", "tx_swr", "tune", + "tx_sensors", "rx_sensors", "rx_channel_sensors": // Logged on arrival so an ANSWER can be told from a SILENCE: the log // showed the transmit meters being asked for and nothing coming back, // which on its own proves nothing — a reply that arrived and failed to