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.