From da8f60a7b391e5c2379ea08183a5261ca5ca1cef Mon Sep 17 00:00:00 2001 From: rouggy Date: Sat, 29 Aug 2026 21:53:16 +0200 Subject: [PATCH] fix(icom): the SUB dial is always shown, and split aligns the TX mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unselected VFO was read only when split was on, so a dual-receiver rig's second dial sat blank on the console. It is now read on the same slow beat regardless. And engaging split copies the main's mode (and data flag) onto the TX VFO with 0x26 01 — a split whose TX VFO still speaks yesterday's mode transmits FM into a CW pileup; rigs that predate 0x26 NAK it harmlessly. --- changelog.json | 6 ++++-- frontend/src/components/IcomPanel.tsx | 7 +++++-- frontend/wailsjs/go/models.ts | 2 ++ internal/cat/cat.go | 10 +++++++--- internal/cat/icomserial.go | 20 ++++++++++++++++++-- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/changelog.json b/changelog.json index 4b985ca..3b24c29 100644 --- a/changelog.json +++ b/changelog.json @@ -25,7 +25,8 @@ "Icom network audio: listening is now a remembered choice — Stop listening keeps the speakers off across reconnects and restarts, while the stream stays open for the QSO recorder and the voice keyer.", "Icom console: a speaker button beside ON/OFF toggles listening to the network RX audio right from the console — no more trip through Settings → Audio; recordings and the voice keyer keep working either way, and the choice is remembered.", "CAT settings: a “Play it through the speakers” checkbox sits under the Icom RX-audio option — the same remembered switch as the console’s speaker button, applied immediately.", - "Icom network audio: “Talk to radio” now works over the LAN too — live microphone straight to the rig, PTT included. With RX audio and a headset, OpsLog is a complete remote station: hear, talk, key CW and log over one network link." + "Icom network audio: “Talk to radio” now works over the LAN too — live microphone straight to the rig, PTT included. With RX audio and a headset, OpsLog is a complete remote station: hear, talk, key CW and log over one network link.", + "Icom console: the SUB display shows the sub receiver’s frequency at all times (it was blank until split), and engaging split copies the mode to the TX VFO so it matches the main." ], "fr": [ "Console Elecraft : le S-mètre est calibré sur un vrai K3 — S9 et les +dB correspondent désormais à l’affichage de la radio (il lisait environ deux points S trop bas).", @@ -50,7 +51,8 @@ "Audio réseau Icom : l’écoute est désormais un choix mémorisé — Stop listening garde les enceintes coupées à travers reconnexions et redémarrages, tandis que le flux reste ouvert pour l’enregistreur de QSO et le voice keyer.", "Console Icom : un bouton haut-parleur à côté de ON/OFF bascule l’écoute du RX audio réseau depuis la console — fini l’aller-retour dans Réglages → Audio ; enregistrements et voice keyer continuent de fonctionner, et le choix est mémorisé.", "Réglages CAT : une case « Écouter dans les enceintes » sous l’option RX audio Icom — le même interrupteur mémorisé que le bouton haut-parleur de la console, appliqué immédiatement.", - "Audio réseau Icom : « Talk to radio » fonctionne aussi par le LAN — micro en direct vers la radio, PTT compris. Avec le RX audio et un casque, OpsLog devient une station remote complète : écouter, parler, manipuler la CW et loguer sur un seul lien réseau." + "Audio réseau Icom : « Talk to radio » fonctionne aussi par le LAN — micro en direct vers la radio, PTT compris. Avec le RX audio et un casque, OpsLog devient une station remote complète : écouter, parler, manipuler la CW et loguer sur un seul lien réseau.", + "Console Icom : l’affichage SUB montre la fréquence du sub receiver en permanence (il restait vide hors split), et activer le split copie le mode sur le VFO TX pour qu’il suive le main." ] }, { diff --git a/frontend/src/components/IcomPanel.tsx b/frontend/src/components/IcomPanel.tsx index 459bc84..6784bb8 100644 --- a/frontend/src/components/IcomPanel.tsx +++ b/frontend/src/components/IcomPanel.tsx @@ -20,7 +20,7 @@ import { ShiftRow } from '@/components/ShiftRow'; type IcomState = { available: boolean; model?: string; mode?: string; - transmitting: boolean; split: boolean; + transmitting: boolean; split: boolean; sub_hz?: number; s_meter: number; power_meter: number; swr_meter: number; rf_power: number; mic_gain: number; af_gain: number; rf_gain: number; @@ -692,7 +692,10 @@ export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (r // other is TX (freq_hz); otherwise there's a single VFO (freq_hz). const split = !!cat?.split; const mainHz: number = split ? (cat?.freq_rx_hz || 0) : (cat?.freq_hz || 0); - const subHz: number = split ? (cat?.freq_hz || 0) : 0; + // The sub receiver's dial is worth seeing whether or not split is on — + // in split the CAT state's TX freq is the authority, otherwise the panel's + // own sub_hz read. + const subHz: number = split ? (cat?.freq_hz || 0) : (st.sub_hz || 0); const curMode: string = cat?.mode || st.mode || ''; // Mode-dependent controls: VOX / speech-comp / mic are voice-only (hidden on // CW and data); APF (audio peak filter) is CW-only. Fold USB/LSB into phone. diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 1937b4a..e8ec81b 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -983,6 +983,7 @@ export namespace cat { mode?: string; transmitting: boolean; split: boolean; + sub_hz: number; s_meter: number; power_meter: number; swr_meter: number; @@ -1030,6 +1031,7 @@ export namespace cat { this.mode = source["mode"]; this.transmitting = source["transmitting"]; this.split = source["split"]; + this.sub_hz = source["sub_hz"]; this.s_meter = source["s_meter"]; this.power_meter = source["power_meter"]; this.swr_meter = source["swr_meter"]; diff --git a/internal/cat/cat.go b/internal/cat/cat.go index 1dbb693..703dd8a 100644 --- a/internal/cat/cat.go +++ b/internal/cat/cat.go @@ -572,9 +572,13 @@ type IcomTXState struct { // Transmit + live status (polled). Transmitting bool `json:"transmitting"` Split bool `json:"split"` - SMeter int `json:"s_meter"` // 0-100 (raw 0-255; S9≈120) - PowerMeter int `json:"power_meter"` // 0-100 (TX Po) - SWRMeter int `json:"swr_meter"` // 0-100 (TX SWR) + // SubHz is the unselected VFO / sub receiver's frequency — shown on the + // console's SUB display whether or not split is on: a dual-receiver rig + // (IC-7610/7760) has a second dial worth seeing at all times. + SubHz int64 `json:"sub_hz"` + SMeter int `json:"s_meter"` // 0-100 (raw 0-255; S9≈120) + PowerMeter int `json:"power_meter"` // 0-100 (TX Po) + SWRMeter int `json:"swr_meter"` // 0-100 (TX SWR) // RIT / ΔTX (XIT). RITHz int `json:"rit_hz"` // RIT/XIT offset, signed Hz RITOn bool `json:"rit_on"` diff --git a/internal/cat/icomserial.go b/internal/cat/icomserial.go index 232689c..379a3ee 100644 --- a/internal/cat/icomserial.go +++ b/internal/cat/icomserial.go @@ -414,8 +414,13 @@ func (b *IcomSerial) ReadState() (RigState, error) { // all, for the user's Set* commands. if b.pollN%4 == 1 { b.splitOn, b.splitTXFreq = false, 0 - if on, ok := b.readSplit(); ok && on { - if txHz, ok2 := b.readTXFreq(); ok2 && txHz > 0 { + on, okSplit := b.readSplit() + // The unselected VFO is read split or NOT: on a dual-receiver rig it is + // the sub receiver's dial, and the console showed it blank until split + // was engaged. + if txHz, ok2 := b.readTXFreq(); ok2 && txHz > 0 { + b.setCache(func(st *IcomTXState) { st.SubHz = txHz }) + if okSplit && on { b.splitOn, b.splitTXFreq = true, txHz } } @@ -1872,6 +1877,17 @@ func (b *IcomSerial) SetIcomSplit(on bool) error { } _ = b.exec(append([]byte{civ.CmdVfoFreq, civ.SubVfoUnselected}, civ.FreqToBCD(rx+offset)...)...) } + // And the MODE crosses with it: a split where the TX VFO is still on + // yesterday's mode transmits FM into a CW pileup. 0x26 0x01 sets the + // unselected VFO's mode + data flag in one frame; rigs that predate + // 0x26 NAK it harmlessly and behave as they always did. + if b.curModeByte != 0 { + dataByte := byte(0) + if got := b.readDataMode(); got { + dataByte = 1 + } + _ = b.exec(civ.CmdModeDataFil, 0x01, b.curModeByte, dataByte, 0x01) + } } if err := b.exec(civ.CmdSplit, boolByte(on)); err != nil { return err