fix(tci): resubscribe to the meters at 'ready', and log what comes back

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.
This commit is contained in:
2026-08-28 22:51:25 +02:00
parent db24c38e63
commit 8acfda4e42
3 changed files with 31 additions and 3 deletions
+10
View File
@@ -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", "version": "0.26.23",
"date": "", "date": "",
+19 -2
View File
@@ -165,8 +165,7 @@ func (t *TCI) Connect() error {
if t.spotsEnabled { if t.spotsEnabled {
debugLog.Printf("TCI: panorama spots are ON — spots will be sent to the radio") debugLog.Printf("TCI: panorama spots are ON — spots will be sent to the radio")
} }
_ = t.send("rx_sensors_enable:true,200;") t.subscribeSensors("connect")
_ = t.send("tx_sensors_enable:true,200;")
if t.spotsEnabled { if t.spotsEnabled {
// Forget what we thought was on the panorama at the same moment the radio // 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 // 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). // 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 { func (t *TCI) send(cmd string) error {
t.mu.Lock() t.mu.Lock()
c := t.conn c := t.conn
@@ -567,6 +577,13 @@ func (t *TCI) handle(msg string) {
} }
case "ready", "start": case "ready", "start":
t.ready = true 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": case "stop":
t.ready = false t.ready = false
case "vfo": case "vfo":
+2 -1
View File
@@ -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 // it. What the radio actually announces after the command settles whether
// this is our reading or its doing, and no amount of reasoning will. // this is our reading or its doing, and no amount of reasoning will.
switch name { 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 // 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, // showed the transmit meters being asked for and nothing coming back,
// which on its own proves nothing — a reply that arrived and failed to // which on its own proves nothing — a reply that arrived and failed to