SWT20 was written from memory of the reference rather than from Table 7, and 20 is not an ATU switch at all — it would have pressed something else on a real K3. An operator read the table out for us: switch 19 is the ATU row, TAP for a tuning cycle and HOLD for tuner in line or bypassed. Adds the HOLD as its own ATU button, because on the radio they are two different things: tuning is a cycle you start, bypassing is a state you leave it in. Power takes 0-110 W on an Elecraft, per the reference — a K3 makes a little over its rated output — while a Kenwood keeps 200.
555 lines
17 KiB
Go
555 lines
17 KiB
Go
package cat
|
|
|
|
// Elecraft K3/K4 control panel — power, volume, S-meter, SWR, MOX and ATU tune.
|
|
//
|
|
// Built on the Kenwood-dialect backend, because a K3 speaks it: plain ASCII,
|
|
// every command terminated by ';'. What is Elecraft-specific is the vocabulary
|
|
// below, taken from the K3 Programmer's Reference; the K4 accepts the same set.
|
|
//
|
|
// NO K3 WAS AVAILABLE WHILE WRITING THIS, and that shapes it:
|
|
//
|
|
// - The commands that SET something are the ones the reference documents
|
|
// unambiguously and whose effect is visible and reversible (PC, AG, TX/RX).
|
|
// - The meters are the opposite: their scaling differs between models and
|
|
// firmware, and a wrongly scaled SWR bar is worse than none — it reports a
|
|
// good match on a bad antenna. So the raw answers are LOGGED, for a real
|
|
// radio to settle, and until then the panel says the scaling is provisional.
|
|
//
|
|
// The same discipline as the Yaesu meters, which were guessed wrong twice and
|
|
// only settled when an FTDX10 keyed a carrier at two known power levels.
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// KenwoodTXState is what the panel shows.
|
|
type KenwoodTXState struct {
|
|
Available bool `json:"available"`
|
|
Model string `json:"model,omitempty"`
|
|
Elecraft bool `json:"elecraft"` // a K3/K4 rather than a Kenwood
|
|
Mode string `json:"mode,omitempty"`
|
|
|
|
Transmitting bool `json:"transmitting"`
|
|
Split bool `json:"split"`
|
|
SplitTXHz int64 `json:"split_tx_hz"`
|
|
|
|
// SMeter is 0-100 for the bar. SMeterRaw is what the rig actually answered,
|
|
// kept because the scaling is not yet confirmed on a real K3 and a number
|
|
// nobody can check is worth less than the reading it came from.
|
|
SMeter int `json:"s_meter"`
|
|
SMeterRaw int `json:"s_meter_raw"`
|
|
// PowerMeter is 0-100 while transmitting. SWR is the ratio; 0 means "not
|
|
// measured", NOT a perfect match.
|
|
PowerMeter int `json:"power_meter"`
|
|
SWR float64 `json:"swr"`
|
|
SWRRaw int `json:"swr_raw"`
|
|
|
|
RFPower int `json:"rf_power"` // watts, the PC setting
|
|
AFGain int `json:"af_gain"` // 0-100
|
|
RFGain int `json:"rf_gain"` // 0-100
|
|
MicGain int `json:"mic_gain"` // 0-100
|
|
Squelch int `json:"squelch"` // 0-100
|
|
|
|
// Receive controls. Preamp and attenuator are the K3's single-step ones
|
|
// (PA/RA); a rig that answers neither leaves them false and the row shows
|
|
// the state it read, not a state it assumed.
|
|
Preamp bool `json:"preamp"`
|
|
Att bool `json:"att"`
|
|
NB bool `json:"nb"`
|
|
NR bool `json:"nr"`
|
|
AGC string `json:"agc,omitempty"` // "OFF", "SLOW", "FAST"
|
|
// FilterHz is the DSP bandwidth in Hz (the K3 reports it in 10 Hz units).
|
|
FilterHz int `json:"filter_hz"`
|
|
// Antenna is 1 or 2 on a K3 with the internal ATU, 0 when the rig does not
|
|
// answer AN — which is also how the panel knows to hide the row rather than
|
|
// offer a switch that goes nowhere.
|
|
Antenna int `json:"antenna"`
|
|
|
|
RIT bool `json:"rit"`
|
|
XIT bool `json:"xit"`
|
|
KeySpeed int `json:"key_speed"` // WPM
|
|
|
|
// MetersProvisional says the meter scaling has not been confirmed against a
|
|
// real radio. The panel says so rather than presenting a guess as a
|
|
// measurement.
|
|
MetersProvisional bool `json:"meters_provisional"`
|
|
}
|
|
|
|
// KenwoodPanelController is the K3/K4 panel capability. Separate from
|
|
// KenwoodController (the CW keyer) so a backend can offer one without the other.
|
|
type KenwoodPanelController interface {
|
|
KenwoodState() KenwoodTXState
|
|
RefreshKenwood() error
|
|
SetKenwoodPower(int) error
|
|
SetKenwoodAFGain(int) error
|
|
SetKenwoodRFGain(int) error
|
|
SetKenwoodMicGain(int) error
|
|
SetKenwoodSquelch(int) error
|
|
SetKenwoodPreamp(bool) error
|
|
SetKenwoodAtt(bool) error
|
|
SetKenwoodNB(bool) error
|
|
SetKenwoodNR(bool) error
|
|
SetKenwoodAGC(string) error
|
|
SetKenwoodFilter(int) error
|
|
SetKenwoodAntenna(int) error
|
|
SetKenwoodRIT(bool) error
|
|
SetKenwoodXIT(bool) error
|
|
ClearKenwoodRIT() error
|
|
SetKenwoodTX(bool) error
|
|
TuneKenwoodATU() error
|
|
ToggleKenwoodATU() error
|
|
}
|
|
|
|
// kenwoodPanelSlowBeat is how many polls pass between full re-reads of the
|
|
// settings. The meters are read every poll; a power setting is not.
|
|
const kenwoodPanelSlowBeat = 8
|
|
|
|
// KenwoodState returns the panel snapshot.
|
|
func (k *Kenwood) KenwoodState() KenwoodTXState {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
st := k.panel
|
|
st.Available = k.port != nil
|
|
st.Model = k.model
|
|
st.Elecraft = k.elecraft
|
|
return st
|
|
}
|
|
|
|
// RefreshKenwood forces the settings to be re-read on the next poll, so a value
|
|
// changed on the radio's own front panel shows up at once.
|
|
func (k *Kenwood) RefreshKenwood() error {
|
|
k.mu.Lock()
|
|
k.panelCycle = kenwoodPanelSlowBeat
|
|
k.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// readPanel refreshes the panel. Called from ReadState with the mutex HELD, so
|
|
// it shares the same serialised link as everything else.
|
|
func (k *Kenwood) readPanel(mode string, split bool, txHz int64) {
|
|
k.panel.Mode = mode
|
|
k.panel.Split = split
|
|
k.panel.SplitTXHz = 0
|
|
if split {
|
|
k.panel.SplitTXHz = txHz
|
|
}
|
|
k.panel.Transmitting = k.tx
|
|
k.panel.MetersProvisional = true
|
|
|
|
if k.panel.Transmitting {
|
|
k.readTXMeters()
|
|
} else {
|
|
// Cleared, not frozen: a power bar left standing after the carrier drops
|
|
// reads as a live transmission.
|
|
k.panel.PowerMeter = 0
|
|
k.panel.SWR, k.panel.SWRRaw = 0, 0
|
|
k.powerPeak, k.swrPeak = meterPeak{}, meterPeak{}
|
|
// The S-meter only means anything while receiving.
|
|
if v, ok := k.askNum("SM;", "SM", 4); ok {
|
|
k.panel.SMeterRaw = v
|
|
k.panel.SMeter = kenwoodSMeterPercent(v)
|
|
}
|
|
}
|
|
|
|
k.panelCycle++
|
|
if k.panelLoaded && k.panelCycle < kenwoodPanelSlowBeat {
|
|
return
|
|
}
|
|
k.panelCycle = 0
|
|
k.panelLoaded = true
|
|
k.readPanelSettings()
|
|
}
|
|
|
|
// readPanelSettings re-reads what a knob can change.
|
|
func (k *Kenwood) readPanelSettings() {
|
|
// PC is watts on both the K3 and the Kenwoods — a setting, not a scale.
|
|
if v, ok := k.askNum("PC;", "PC", 3); ok {
|
|
k.panel.RFPower = v
|
|
}
|
|
// AF gain. The K3 answers three digits 000-255; a Kenwood answers AG0nnn,
|
|
// which is why the plain form is tried first and the addressed one after.
|
|
if v, ok := k.askNum("AG;", "AG", 3); ok {
|
|
k.panel.AFGain = scale255(v)
|
|
} else if v, ok := k.askNum("AG0;", "AG0", 3); ok {
|
|
k.panel.AFGain = scale255(v)
|
|
}
|
|
// RF gain. The K3 counts 000-250 in dB of reduction; a Kenwood uses the
|
|
// familiar 0-255. Both are shown as a percentage, so the slider means the
|
|
// same thing on either radio.
|
|
if v, ok := k.askNum("RG;", "RG", 3); ok {
|
|
k.panel.RFGain = scalePercent(v, k.rfGainFull())
|
|
}
|
|
if v, ok := k.askNum("MG;", "MG", 3); ok {
|
|
k.panel.MicGain = scalePercent(v, k.micGainFull())
|
|
}
|
|
if v, ok := k.askNum("SQ;", "SQ", 3); ok {
|
|
k.panel.Squelch = scalePercent(v, k.squelchFull())
|
|
} else if v, ok := k.askNum("SQ0;", "SQ0", 3); ok {
|
|
k.panel.Squelch = scale255(v)
|
|
}
|
|
if v, ok := k.askNum("PA;", "PA", 1); ok {
|
|
k.panel.Preamp = v != 0
|
|
}
|
|
if v, ok := k.askNum("RA;", "RA", 2); ok {
|
|
k.panel.Att = v != 0
|
|
}
|
|
if v, ok := k.askNum("NB;", "NB", 1); ok {
|
|
k.panel.NB = v != 0
|
|
}
|
|
if v, ok := k.askNum("NR;", "NR", 1); ok {
|
|
k.panel.NR = v != 0
|
|
}
|
|
if v, ok := k.askNum("GT;", "GT", 3); ok {
|
|
k.panel.AGC = kenwoodAGCName(v)
|
|
}
|
|
// Filter width. The K3 answers BW in 10 Hz units (BW0270 = 2.7 kHz); a
|
|
// Kenwood that does not implement it says nothing and the row stays as it
|
|
// was rather than showing a zero-width filter.
|
|
if v, ok := k.askNum("BW;", "BW", 4); ok && v > 0 {
|
|
k.panel.FilterHz = v * 10
|
|
}
|
|
// Antenna. Only a K3 with the internal ATU answers; 0 means "this radio has
|
|
// no antenna switching", which is what hides the row.
|
|
if v, ok := k.askNum("AN;", "AN", 1); ok {
|
|
k.panel.Antenna = v
|
|
}
|
|
if v, ok := k.askNum("RT;", "RT", 1); ok {
|
|
k.panel.RIT = v != 0
|
|
}
|
|
if v, ok := k.askNum("XT;", "XT", 1); ok {
|
|
k.panel.XIT = v != 0
|
|
}
|
|
if v, ok := k.askNum("KS;", "KS", 3); ok {
|
|
k.panel.KeySpeed = v
|
|
}
|
|
}
|
|
|
|
// The full-scale value of the analogue controls differs between an Elecraft and
|
|
// a Kenwood, and using one rig's scale on the other silently halves or doubles
|
|
// every setting. Kept as three small functions rather than a table so each one
|
|
// carries the range it comes from.
|
|
func (k *Kenwood) rfGainFull() int {
|
|
if k.elecraft {
|
|
return 250 // K3: 000-250, dB of reduction
|
|
}
|
|
return 255
|
|
}
|
|
|
|
func (k *Kenwood) micGainFull() int {
|
|
if k.elecraft {
|
|
return 60 // K3: 000-060
|
|
}
|
|
return 255
|
|
}
|
|
|
|
func (k *Kenwood) squelchFull() int {
|
|
if k.elecraft {
|
|
return 29 // K3: 000-029
|
|
}
|
|
return 255
|
|
}
|
|
|
|
// scalePercent turns a raw value into 0-100 against its own full scale.
|
|
func scalePercent(v, full int) int {
|
|
if full <= 0 || v <= 0 {
|
|
return 0
|
|
}
|
|
if v >= full {
|
|
return 100
|
|
}
|
|
return v * 100 / full
|
|
}
|
|
|
|
// kenwoodAGCName decodes GT. The K3 answers 000 (off), 002 (slow) or 004
|
|
// (fast); a Kenwood uses the same three values for the same three states.
|
|
func kenwoodAGCName(v int) string {
|
|
switch {
|
|
case v == 0:
|
|
return "OFF"
|
|
case v <= 2:
|
|
return "SLOW"
|
|
default:
|
|
return "FAST"
|
|
}
|
|
}
|
|
|
|
// kenwoodAGCValue is the inverse, for the buttons.
|
|
func kenwoodAGCValue(name string) int {
|
|
switch strings.ToUpper(strings.TrimSpace(name)) {
|
|
case "OFF":
|
|
return 0
|
|
case "SLOW":
|
|
return 2
|
|
default:
|
|
return 4
|
|
}
|
|
}
|
|
|
|
// kenwoodMeterProbes are the candidate meter commands, asked once per
|
|
// transmission burst so a real radio can settle what they mean.
|
|
//
|
|
// They are NOT interchangeable — BG is a bargraph position, SM during transmit
|
|
// is something else again — which is exactly why the log records which one
|
|
// answered and what it said, next to the power SETTING: the meter that tracks a
|
|
// known carrier at two different power levels is the power meter, and no amount
|
|
// of reading the reference settles that as well as one transmission does.
|
|
var kenwoodMeterProbes = []struct {
|
|
cmd string
|
|
prefix string
|
|
digits int
|
|
}{
|
|
{"SM;", "SM", 4},
|
|
{"BG;", "BG", 2},
|
|
{"SW;", "SW", 4},
|
|
{"PO;", "PO", 3},
|
|
}
|
|
|
|
// readTXMeters reads the transmit meters.
|
|
func (k *Kenwood) readTXMeters() {
|
|
now := time.Now()
|
|
if v, ok := k.askNum("BG;", "BG", 2); ok {
|
|
k.panel.PowerMeter = k.powerPeak.update(kenwoodBargraphPercent(v), now)
|
|
}
|
|
if v, ok := k.askNum("SW;", "SW", 4); ok {
|
|
k.panel.SWRRaw = v
|
|
// Tenths of a ratio, provisionally: 15 → 1.5. Reported as raw as well,
|
|
// so the log can correct this without anyone having to trust the bar.
|
|
if v > 0 {
|
|
k.panel.SWR = float64(k.swrPeak.update(v, now)) / 10
|
|
}
|
|
}
|
|
if k.metersLogged >= 20 {
|
|
return
|
|
}
|
|
k.metersLogged++
|
|
raw := make([]string, 0, len(kenwoodMeterProbes))
|
|
for _, p := range kenwoodMeterProbes {
|
|
if v, ok := k.askNum(p.cmd, p.prefix, p.digits); ok {
|
|
raw = append(raw, fmt.Sprintf("%s=%d", strings.TrimSuffix(p.cmd, ";"), v))
|
|
} else {
|
|
raw = append(raw, strings.TrimSuffix(p.cmd, ";")+"=-")
|
|
}
|
|
}
|
|
debugLog.Printf("kenwood: TX meters at PC=%dW: %s (compare two power settings, and a known SWR)",
|
|
k.panel.RFPower, strings.Join(raw, " "))
|
|
}
|
|
|
|
// kenwoodSMeterPercent turns the S-meter answer into a bar percentage.
|
|
//
|
|
// The K3 reports 0-21 across S0…S9+60, which is not a linear dB scale but is
|
|
// what its own display shows — and matching the radio's own bar is something an
|
|
// operator can check at a glance. A Kenwood answers 0-30 on the same command;
|
|
// both are covered by clamping rather than by guessing which rig is on the
|
|
// other end.
|
|
func kenwoodSMeterPercent(v int) int {
|
|
switch {
|
|
case v <= 0:
|
|
return 0
|
|
case v >= 30:
|
|
return 100
|
|
}
|
|
return v * 100 / 21
|
|
}
|
|
|
|
// kenwoodBargraphPercent scales the K3 bargraph (0-12 segments) to the bar.
|
|
func kenwoodBargraphPercent(v int) int {
|
|
switch {
|
|
case v <= 0:
|
|
return 0
|
|
case v >= 12:
|
|
return 100
|
|
}
|
|
return v * 100 / 12
|
|
}
|
|
|
|
// SetKenwoodPower sets the transmit power, in watts.
|
|
//
|
|
// A K3 takes PC000-110: 110 rather than 100 because the radio will make a
|
|
// little over its rated output and the reference says so. A Kenwood of the
|
|
// TS-890 sort goes to 200, so the ceiling follows the radio rather than being
|
|
// one number that is wrong for one of them.
|
|
func (k *Kenwood) SetKenwoodPower(w int) error {
|
|
max := 200
|
|
if k.elecraft {
|
|
max = 110
|
|
}
|
|
if w < 0 {
|
|
w = 0
|
|
}
|
|
if w > max {
|
|
w = max
|
|
}
|
|
return k.setPanel(fmt.Sprintf("PC%03d;", w))
|
|
}
|
|
|
|
// SetKenwoodAFGain sets the volume, 0-100, scaled to the rig's 0-255.
|
|
func (k *Kenwood) SetKenwoodAFGain(p int) error {
|
|
if p < 0 {
|
|
p = 0
|
|
}
|
|
if p > 100 {
|
|
p = 100
|
|
}
|
|
return k.setPanel(fmt.Sprintf("AG%03d;", p*255/100))
|
|
}
|
|
|
|
// SetKenwoodRFGain sets the RF gain, 0-100 of the rig's own scale.
|
|
func (k *Kenwood) SetKenwoodRFGain(p int) error {
|
|
return k.setPanel(fmt.Sprintf("RG%03d;", clampPercentTo(p, k.rfGainFull())))
|
|
}
|
|
|
|
func (k *Kenwood) SetKenwoodMicGain(p int) error {
|
|
return k.setPanel(fmt.Sprintf("MG%03d;", clampPercentTo(p, k.micGainFull())))
|
|
}
|
|
|
|
func (k *Kenwood) SetKenwoodSquelch(p int) error {
|
|
return k.setPanel(fmt.Sprintf("SQ%03d;", clampPercentTo(p, k.squelchFull())))
|
|
}
|
|
|
|
func (k *Kenwood) SetKenwoodPreamp(on bool) error {
|
|
return k.setPanel(fmt.Sprintf("PA%d;", boolDigit(on)))
|
|
}
|
|
|
|
// SetKenwoodAtt switches the attenuator. Two digits: the K3 reads RA01 as its
|
|
// single 10 dB pad, and a Kenwood with several steps takes the first one.
|
|
func (k *Kenwood) SetKenwoodAtt(on bool) error {
|
|
if on {
|
|
return k.setPanel("RA01;")
|
|
}
|
|
return k.setPanel("RA00;")
|
|
}
|
|
|
|
func (k *Kenwood) SetKenwoodNB(on bool) error {
|
|
return k.setPanel(fmt.Sprintf("NB%d;", boolDigit(on)))
|
|
}
|
|
|
|
func (k *Kenwood) SetKenwoodNR(on bool) error {
|
|
return k.setPanel(fmt.Sprintf("NR%d;", boolDigit(on)))
|
|
}
|
|
|
|
func (k *Kenwood) SetKenwoodAGC(name string) error {
|
|
return k.setPanel(fmt.Sprintf("GT%03d;", kenwoodAGCValue(name)))
|
|
}
|
|
|
|
// SetKenwoodFilter sets the DSP bandwidth in Hz — sent in the K3's 10 Hz units.
|
|
func (k *Kenwood) SetKenwoodFilter(hz int) error {
|
|
if hz < 50 {
|
|
hz = 50
|
|
}
|
|
if hz > 4000 {
|
|
hz = 4000
|
|
}
|
|
return k.setPanel(fmt.Sprintf("BW%04d;", hz/10))
|
|
}
|
|
|
|
// SetKenwoodAntenna selects antenna 1 or 2 (a K3 with the internal ATU).
|
|
func (k *Kenwood) SetKenwoodAntenna(n int) error {
|
|
if n < 1 {
|
|
n = 1
|
|
}
|
|
if n > 2 {
|
|
n = 2
|
|
}
|
|
return k.setPanel(fmt.Sprintf("AN%d;", n))
|
|
}
|
|
|
|
func (k *Kenwood) SetKenwoodRIT(on bool) error {
|
|
return k.setPanel(fmt.Sprintf("RT%d;", boolDigit(on)))
|
|
}
|
|
|
|
func (k *Kenwood) SetKenwoodXIT(on bool) error {
|
|
return k.setPanel(fmt.Sprintf("XT%d;", boolDigit(on)))
|
|
}
|
|
|
|
// ClearKenwoodRIT zeroes the RIT/XIT offset, both at once — which is what RC
|
|
// does and what the operator means by "clear it".
|
|
func (k *Kenwood) ClearKenwoodRIT() error {
|
|
return k.setPanel("RC;")
|
|
}
|
|
|
|
// clampPercentTo maps 0-100 onto a rig's own full scale.
|
|
func clampPercentTo(p, full int) int {
|
|
if p < 0 {
|
|
p = 0
|
|
}
|
|
if p > 100 {
|
|
p = 100
|
|
}
|
|
return p * full / 100
|
|
}
|
|
|
|
// SetKenwoodTX keys or unkeys the transmitter — the panel's MOX.
|
|
//
|
|
// Goes through SetPTT rather than writing TX;/RX; here, so the backend's own
|
|
// idea of transmitting stays true: it suppresses polling while the carrier is
|
|
// up, and a panel that keyed behind its back would leave it polling a rig that
|
|
// answers "?;" to everything.
|
|
func (k *Kenwood) SetKenwoodTX(on bool) error {
|
|
return k.SetPTT(on)
|
|
}
|
|
|
|
// The K3 has no dedicated "tune" command: the reference exposes the front panel
|
|
// instead, through SWT (tap) and SWH (hold) with a switch number from Table 7.
|
|
// Switch 19 is the ATU row — TAP starts a tuning cycle, HOLD puts the tuner in
|
|
// or out of line.
|
|
//
|
|
// It was written as SWT20 first, from memory of the reference rather than from
|
|
// the table, and 20 is not an ATU switch at all. Corrected against Table 7 of
|
|
// the K3 Programmer's Reference, which an operator read out for us. Every send
|
|
// is still logged: a switch-emulation command presses a real button on a real
|
|
// front panel, so what OpsLog sent must be recoverable afterwards.
|
|
const (
|
|
kenwoodATUTune = "SWT19;" // tap ATU TUNE — start a tuning cycle
|
|
kenwoodATUToggle = "SWH19;" // hold ATU — tuner in line / bypassed
|
|
)
|
|
|
|
// TuneKenwoodATU starts an ATU tuning cycle.
|
|
func (k *Kenwood) TuneKenwoodATU() error {
|
|
debugLog.Printf("kenwood: ATU tune — sending %q (K3 Table 7: tap of the ATU TUNE switch)", kenwoodATUTune)
|
|
return k.setPanel(kenwoodATUTune)
|
|
}
|
|
|
|
// ToggleKenwoodATU puts the tuner in line or bypasses it — the HOLD of the same
|
|
// switch, which is how it is done on the radio itself.
|
|
func (k *Kenwood) ToggleKenwoodATU() error {
|
|
debugLog.Printf("kenwood: ATU in/out — sending %q (K3 Table 7: hold of the ATU switch)", kenwoodATUToggle)
|
|
return k.setPanel(kenwoodATUToggle)
|
|
}
|
|
|
|
// setPanel writes one command and schedules a settings re-read, so the panel
|
|
// shows what the radio did rather than what it was asked to do.
|
|
func (k *Kenwood) setPanel(cmd string) error {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
if k.port == nil {
|
|
return fmt.Errorf("kenwood: not connected")
|
|
}
|
|
if err := k.write(cmd); err != nil {
|
|
return err
|
|
}
|
|
k.panelCycle = kenwoodPanelSlowBeat // re-read on the next poll
|
|
return nil
|
|
}
|
|
|
|
// askNum asks a command and parses its numeric body. ask() already remembers
|
|
// what this rig answered "?;" to and refuses to ask it again, so an unsupported
|
|
// meter costs one timeout for the life of the session, not one per poll.
|
|
func (k *Kenwood) askNum(cmd, prefix string, digits int) (int, bool) {
|
|
r, err := k.ask(cmd)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
body := strings.TrimSuffix(strings.TrimPrefix(r, prefix), ";")
|
|
if len(body) < digits {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.Atoi(strings.TrimSpace(body[:digits]))
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return n, true
|
|
}
|