merge: the SunSDR console, working this time
Everything here came from one operator with the radio in front of him, and
most of it was mine to fix. The drive commands need the transceiver index
('drive:0,15;', not 'drive:15;') or the radio ignores them without a word.
The console now holds a click the radio does not echo, so a working button
stops looking dead — and lets go the moment the radio contradicts it, so a
REFUSED setting still tells the truth. Filters per mode, APF only in CW,
levels on wide rows with typed values, the RIT control shared with the
Icom console, the transmit meters, and a TUNE that can be switched off
again.
Two findings worth keeping: this radio re-asserts sql_enable eighty
milliseconds after being told to turn the squelch off, and it never
answers TX_POWER or TX_SWR — both are its own doing, and both are visible
in the log rather than argued about.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -304,6 +304,22 @@ 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.
|
||||
// Keyed by PTT **or** by TUNE. A tune carrier is exactly when the meters
|
||||
// matter most — it is the carrier an operator is watching an SWR on — and
|
||||
// asking only on t.tx left them at zero for the whole tune, because the
|
||||
// radio reports tuning as its own state and not as a transmission.
|
||||
if t.tx || t.panel.st.Tuning {
|
||||
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)
|
||||
|
||||
@@ -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"`
|
||||
@@ -77,6 +88,8 @@ type TCIPanelState struct {
|
||||
// arrives alongside.
|
||||
type tciPanel struct {
|
||||
st TCIPanelState
|
||||
// logged counts what has been written per message type — see handlePanel.
|
||||
logged map[string]int
|
||||
}
|
||||
|
||||
// handlePanel takes the messages the console cares about.
|
||||
@@ -95,6 +108,28 @@ func (t *TCI) handlePanel(name string, get func(int) string, args string) bool {
|
||||
yes := func(s string) bool { return strings.EqualFold(strings.TrimSpace(s), "true") }
|
||||
|
||||
p := &t.panel.st
|
||||
// Mute and squelch are LOGGED as they change, because a report from a real
|
||||
// radio says pressing MUTE lights the squelch and nothing here can explain
|
||||
// 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":
|
||||
// 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
|
||||
// parse leaves exactly the same trace as one that never came.
|
||||
//
|
||||
// Capped per message type. The meters are asked for four times a second
|
||||
// while transmitting, and a diagnostic that fills an evening's log is
|
||||
// one that gets switched off instead of read.
|
||||
if t.panel.logged == nil {
|
||||
t.panel.logged = map[string]int{}
|
||||
}
|
||||
if n := t.panel.logged[name]; n < 20 {
|
||||
t.panel.logged[name] = n + 1
|
||||
debugLog.Printf("TCI: %s:%s", name, args)
|
||||
}
|
||||
}
|
||||
switch name {
|
||||
case "protocol":
|
||||
p.Protocol = strings.TrimSpace(args)
|
||||
@@ -120,7 +155,15 @@ func (t *TCI) handlePanel(name string, get func(int) string, args string) bool {
|
||||
p.Volume = n
|
||||
}
|
||||
case "mute":
|
||||
p.Mute = yes(get(1))
|
||||
// Both shapes. This radio reports "mute:0,false" and the reference shows
|
||||
// "mute:true" elsewhere — reading only one of them left the button
|
||||
// showing the opposite of the truth, which is worse than showing
|
||||
// nothing.
|
||||
if get(1) != "" {
|
||||
p.Mute = yes(get(1))
|
||||
} else {
|
||||
p.Mute = yes(get(0))
|
||||
}
|
||||
case "agc_mode":
|
||||
if forRX0() {
|
||||
p.AGC = strings.ToLower(strings.TrimSpace(get(1)))
|
||||
@@ -178,6 +221,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
|
||||
@@ -228,12 +279,22 @@ func (t *TCI) TCIPanel() TCIPanelState {
|
||||
// later.
|
||||
|
||||
// SetDrive sets the transmit drive, 0-100.
|
||||
func (t *TCI) SetDrive(v int) error { return t.send(fmt.Sprintf("drive:%d;", clampTCIPct(v))) }
|
||||
//
|
||||
// THE TRX INDEX IS PART OF THE COMMAND — "drive:0,15;", not "drive:15;". Sent
|
||||
// without it the radio simply ignores it: no error, no answer, the power
|
||||
// unchanged. The rule is the one the radio's own reports follow, and it was
|
||||
// there to read all along: this radio announces "drive:0,85" at connect.
|
||||
func (t *TCI) SetDrive(v int) error { return t.send(fmt.Sprintf("drive:0,%d;", clampTCIPct(v))) }
|
||||
|
||||
// SetTuneDrive sets the drive used by TUNE, 0-100.
|
||||
func (t *TCI) SetTuneDrive(v int) error { return t.send(fmt.Sprintf("tune_drive:%d;", clampTCIPct(v))) }
|
||||
// SetTuneDrive sets the drive used by TUNE, 0-100. Indexed, like drive.
|
||||
func (t *TCI) SetTuneDrive(v int) error {
|
||||
return t.send(fmt.Sprintf("tune_drive:0,%d;", clampTCIPct(v)))
|
||||
}
|
||||
|
||||
// SetMicLevel sets the microphone gain, 0-100.
|
||||
// Mic gain and volume are the two that are NOT indexed — the radio reports
|
||||
// them as "mic_level:100" and "volume:-12", with no receiver in front. Sending
|
||||
// the shape the radio speaks in is the whole rule here.
|
||||
func (t *TCI) SetMicLevel(v int) error { return t.send(fmt.Sprintf("mic_level:%d;", clampTCIPct(v))) }
|
||||
|
||||
// SetVolume sets the receive volume in dB. TCI's scale is negative — 0 is full
|
||||
@@ -248,8 +309,9 @@ func (t *TCI) SetVolume(db int) error {
|
||||
return t.send(fmt.Sprintf("volume:%d;", db))
|
||||
}
|
||||
|
||||
// SetMute mutes or unmutes the receiver.
|
||||
func (t *TCI) SetMute(on bool) error { return t.send(fmt.Sprintf("mute:%t;", on)) }
|
||||
// SetMute mutes or unmutes the receiver. Indexed — the radio reports
|
||||
// "mute:0,false", and a mute sent without the index goes nowhere.
|
||||
func (t *TCI) SetMute(on bool) error { return t.send(fmt.Sprintf("mute:0,%t;", on)) }
|
||||
|
||||
// SetAGC picks the AGC speed: off, long, slow, med, fast.
|
||||
func (t *TCI) SetAGC(mode string) error {
|
||||
@@ -295,7 +357,18 @@ func (t *TCI) SetLock(on bool) error { return t.send(fmt.Sprintf("lock:0,%t;", o
|
||||
//
|
||||
// It TRANSMITS, at tune_drive rather than at drive — which is the setting to
|
||||
// check before pressing it, and why the panel shows the two side by side.
|
||||
func (t *TCI) SetTune(on bool) error { return t.send(fmt.Sprintf("tune:0,%t;", on)) }
|
||||
//
|
||||
// The state is recorded HERE rather than waited for. This radio does not echo
|
||||
// "tune:0,true", so the panel had no way of knowing a tune was running: the
|
||||
// button stayed on TUNE and every further press sent another START, which is
|
||||
// why it could not be switched off again. Whatever the radio says afterwards
|
||||
// still wins — it simply never says anything.
|
||||
func (t *TCI) SetTune(on bool) error {
|
||||
t.mu.Lock()
|
||||
t.panel.st.Tuning = on
|
||||
t.mu.Unlock()
|
||||
return t.send(fmt.Sprintf("tune:0,%t;", on))
|
||||
}
|
||||
|
||||
func clampTCIPct(v int) int {
|
||||
if v < 0 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user