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