From 1201b44908d2f93c11508082607b149349873ab3 Mon Sep 17 00:00:00 2001 From: rouggy Date: Wed, 26 Aug 2026 21:14:58 +0200 Subject: [PATCH] feat(tci): the transmit meters, and no temperature invented MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TX_POWER and TX_SWR are read-only commands the radio answers when asked, so they are asked for on each poll of a KEYED radio and not at all otherwise: a receiving station pays nothing for meters nobody is watching. Both appear next to the S-meter only while transmitting, because showing them the rest of the time would show the last thing that happened as if it were now. An SWR of 0 draws as '—' rather than as 1.0. A perfect match on an antenna nobody has measured is the one reading an operator should never be handed. There is no temperature. The protocol's command list has TX_POWER and TX_SWR and nothing thermal at all — so rather than leave the question hanging, it is written down where the next person will look for it. A temperature invented from something else, on a transmitter, is exactly the kind of number somebody would trust. --- frontend/src/components/TCIPanel.tsx | 23 ++++++++++++++++++++--- frontend/wailsjs/go/models.ts | 4 ++++ internal/cat/flex.go | 2 +- internal/cat/kenwood.go | 6 +++--- internal/cat/tci.go | 12 ++++++++++++ internal/cat/tci_panel.go | 19 +++++++++++++++++++ internal/cat/yaesu_panel.go | 14 +++++++------- 7 files changed, 66 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/TCIPanel.tsx b/frontend/src/components/TCIPanel.tsx index 0425751..c93f5b7 100644 --- a/frontend/src/components/TCIPanel.tsx +++ b/frontend/src/components/TCIPanel.tsx @@ -23,6 +23,7 @@ type TCIState = { filter_lo: number; filter_hi: number; rit: boolean; rit_offset: number; xit: boolean; xit_offset: number; lock: boolean; split: boolean; smeter: number; modulations?: string[]; + tx_power_w: number; tx_swr: number; }; const ZERO: TCIState = { @@ -30,6 +31,7 @@ const ZERO: TCIState = { volume: 0, mute: false, squelch_on: false, squelch: 0, nb: false, nr: false, anf: false, apf: false, filter_lo: 0, filter_hi: 0, rit: false, rit_offset: 0, xit: false, xit_offset: 0, lock: false, split: false, smeter: 0, + tx_power_w: 0, tx_swr: 0, }; // The widths worth a button, PER MODE — because 250 Hz is useless in SSB and @@ -265,9 +267,12 @@ export function TCIPanel({ onReportRST }: { onReportRST?: (rst: string) => void {off &&
{t('tcip.waiting')}
} {!!err &&
{err}
} - {/* Meters — one for now: TCI reports the receive level and does not - publish a transmit power reading, so a PWR bar here would be an - empty promise. */} + {/* Meters. The S-meter while receiving, power and SWR while + transmitting — the radio answers TX_POWER and TX_SWR only when it is + keyed, so showing them the rest of the time would be showing the + last thing that happened as if it were now. + There is no temperature: the protocol has no such command, and a + made-up figure on a transmitter is the kind somebody trusts. */} void onReportRST(sMeterRST(s.s, s.over, mode)); }} title={t('tcip.sMeterHint')} /> + {st.tx && ( +
+ + {/* 0 is "not measured yet", and it must not draw as a perfect + match: an SWR of 1.0 on an antenna nobody has measured is the + one reading an operator should not be handed. */} + 0 ? Math.min(100, (st.tx_swr - 1) * 50) : 0} lo={0} hi={100} + accent="#f59e0b" display={st.tx_swr > 0 ? st.tx_swr.toFixed(1) : '—'} + segColor={(f) => (f > 0.5 ? '#dc2626' : f > 0.25 ? '#f59e0b' : '#16a34a')} /> +
+ )}
{/* Transmit */} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 1a113e8..c310dd1 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1238,6 +1238,8 @@ export namespace cat { lock: boolean; split: boolean; smeter: number; + tx_power_w: number; + tx_swr: number; modulations?: string[]; static createFrom(source: any = {}) { @@ -1273,6 +1275,8 @@ export namespace cat { this.lock = source["lock"]; this.split = source["split"]; this.smeter = source["smeter"]; + this.tx_power_w = source["tx_power_w"]; + this.tx_swr = source["tx_swr"]; this.modulations = source["modulations"]; } } diff --git a/internal/cat/flex.go b/internal/cat/flex.go index e4a76ac..6576a51 100644 --- a/internal/cat/flex.go +++ b/internal/cat/flex.go @@ -59,7 +59,7 @@ type Flex struct { meterRawLogged bool // log the first raw meter-definition status once txRawLogged bool // log the first raw transmit status once (field-name audit) - spotsEnabled bool // push cluster spots + manage the panadapter overlay + spotsEnabled bool // push cluster spots + manage the panadapter overlay // foreignSpotSeen counts what probeForeignSpot has already reported, so a // skimmer posting all evening cannot turn the log into its own transcript. foreignSpotSeen int diff --git a/internal/cat/kenwood.go b/internal/cat/kenwood.go index ea01a5d..d6f7d1b 100644 --- a/internal/cat/kenwood.go +++ b/internal/cat/kenwood.go @@ -98,11 +98,11 @@ type Kenwood struct { // Panel state — the K3/K4 control panel, see kenwood_panel.go. Read on the // same serialised link as everything else, on a slow beat for the settings // and every poll for the meters. - panel KenwoodTXState + panel KenwoodTXState // The icon/status word, for working out which bit says "ATU in line" — see // probeIcons. Kept so only CHANGES are logged. - lastIcons string - iconProbes int + lastIcons string + iconProbes int panelCycle int panelLoaded bool metersLogged int diff --git a/internal/cat/tci.go b/internal/cat/tci.go index 24968e4..1d99fef 100644 --- a/internal/cat/tci.go +++ b/internal/cat/tci.go @@ -304,6 +304,18 @@ func (t *TCI) ReadState() (RigState, error) { } else { st.FreqHz = t.freqA } + // The transmit meters are asked for, not pushed: TX_POWER and TX_SWR are + // read-only commands the radio answers when asked, and asking is only worth + // anything while it is keyed. Fired and forgotten from here — the answers + // arrive on the reader like everything else — and only while transmitting, + // so a receiving station pays nothing for a meter nobody is watching. + if t.tx { + tx := t + go func() { + _ = tx.send("tx_power;") + _ = tx.send("tx_swr;") + }() + } st.Mode = tciModeToADIF(t.mode, t.digitalDefault) if st.FreqHz > 0 { st.Band = BandFromHz(st.FreqHz) diff --git a/internal/cat/tci_panel.go b/internal/cat/tci_panel.go index e8fd39d..ce89db7 100644 --- a/internal/cat/tci_panel.go +++ b/internal/cat/tci_panel.go @@ -68,6 +68,17 @@ type TCIPanelState struct { // several times a second while receiving. SMeter int `json:"smeter"` + // TXPowerW and TXSWR are the transmit meters. READ-ONLY in TCI, and only + // answered while transmitting — asked for on every poll of a keyed radio, + // see ReadState. + // + // There is no temperature in this protocol. The command list has TX_POWER + // and TX_SWR and nothing thermal at all, so a temperature reading here would + // have to be invented, and an invented temperature on a transmitter is the + // kind of number somebody trusts. + TXPowerW float64 `json:"tx_power_w"` + TXSWR float64 `json:"tx_swr"` + // Modulations is what this radio will accept, straight from its own // announcement, so the mode buttons are the radio's and not a guess. Modulations []string `json:"modulations,omitempty"` @@ -194,6 +205,14 @@ func (t *TCI) handlePanel(name string, get func(int) string, args string) bool { if forRX0() { p.Lock = yes(get(1)) } + case "tx_power": + if v, err := strconv.ParseFloat(strings.TrimSpace(get(0)), 64); err == nil { + p.TXPowerW = v + } + case "tx_swr": + if v, err := strconv.ParseFloat(strings.TrimSpace(get(0)), 64); err == nil { + p.TXSWR = v + } case "rx_smeter": if n, ok := num(get(1)); ok && forRX0() { p.SMeter = n diff --git a/internal/cat/yaesu_panel.go b/internal/cat/yaesu_panel.go index 348821e..82db697 100644 --- a/internal/cat/yaesu_panel.go +++ b/internal/cat/yaesu_panel.go @@ -51,13 +51,13 @@ type YaesuTXState struct { // NarrowSupported says the rig answered NA at all. A button that reports a // state the radio never gave, and does nothing when pressed, is worse than // an absent one: it looks like a fault in the radio. - NarrowSupported bool `json:"narrow_supported"` - MicGain int `json:"mic_gain"` // 0-100 - AFGain int `json:"af_gain"` // 0-100 - RFGain int `json:"rf_gain"` // 0-100 - Squelch int `json:"squelch"` // 0-100 - AGC string `json:"agc,omitempty"` - Preamp int `json:"preamp"` // 0=IPO, 1=AMP1, 2=AMP2 + NarrowSupported bool `json:"narrow_supported"` + MicGain int `json:"mic_gain"` // 0-100 + AFGain int `json:"af_gain"` // 0-100 + RFGain int `json:"rf_gain"` // 0-100 + Squelch int `json:"squelch"` // 0-100 + AGC string `json:"agc,omitempty"` + Preamp int `json:"preamp"` // 0=IPO, 1=AMP1, 2=AMP2 // Antenna is the selected jack, 1-3, or 0 when the rig has no AN command — // an FT-891 or FT-991A has a single socket and answers nothing. 0 is what // tells the panel to draw no selector at all rather than a dead one.