The radio has a keyer and a command to feed it (KY), so an FTDX10 needs no WinKeyer and no second cable, exactly as the Icom CI-V and Flex CWX engines already do. The rig keys with its own timing, which is why the spacing is right where a PC keying a line through USB latency drifts. Text is filtered to what the keyer can actually send: an unsupported byte does not produce an error on a Yaesu, it can abort the whole buffer, so the rest of a macro would vanish silently. It is then fed in 24-character pieces, waiting for room between them — the rig DROPS what does not fit, again with no error, so a contest CQ would lose its tail. STOP is the honest gap. Yaesu documents no buffer-clear, so it drops the transmitter (TX0) instead: nothing queued reaches the air, which is what Escape means to an operator. It deliberately does NOT send "KY0;" — a plausible-looking clear that the rig would read as the CHARACTER zero and transmit. A test caught a real one on the way: tabs and newlines were dropped as "unsupported", gluing the words either side together, so a macro written on two lines went out as CQCQ. Whitespace now becomes a word gap before filtering. Settings warn when the Yaesu keyer is selected without the Yaesu CAT backend — otherwise it simply never keys, with nothing on screen saying why. Send path follows the CAT reference and how Hamlib drives these rigs; NOT yet verified on the air.
610 lines
17 KiB
Go
610 lines
17 KiB
Go
package cat
|
|
|
|
// The Yaesu control panel: meters and the settings an operator reaches for
|
|
// mid-QSO. Split out from yaesu.go, which stays the small fast path the whole
|
|
// application depends on — a panel read that hangs must never delay the
|
|
// frequency display.
|
|
//
|
|
// Reads are STAGGERED. Meters change constantly and are polled every cycle;
|
|
// settings (power, gains, AGC, filters) only change when someone turns a knob,
|
|
// so they are refreshed every 8th cycle and immediately after any set. At the
|
|
// default 250 ms cycle that is a two-second worst case for a knob turned on the
|
|
// radio, against a serial link that would otherwise carry twenty queries a
|
|
// second and starve the frequency poll it shares.
|
|
//
|
|
// Verified on: FTDX10. Commands come from its CAT reference; a control a model
|
|
// does not implement simply never answers, and askNum keeps the previous value
|
|
// rather than showing a zero that reads as "it reset itself".
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// YaesuTXState is the panel snapshot handed to the frontend.
|
|
type YaesuTXState struct {
|
|
Available bool `json:"available"`
|
|
Model string `json:"model,omitempty"`
|
|
Mode string `json:"mode,omitempty"`
|
|
// RawMode is the rig mode with its sideband (CW-U, DATA-L…), which ADIF
|
|
// deliberately does not record but the panel has to show.
|
|
RawMode string `json:"raw_mode,omitempty"`
|
|
|
|
Transmitting bool `json:"transmitting"`
|
|
Split bool `json:"split"`
|
|
// SplitTXHz is where the rig will TRANSMIT while split — the panel showed a
|
|
// lit SPLIT button and nothing else, which does not tell an operator whether
|
|
// they are up 1 or up 5.
|
|
SplitTXHz int64 `json:"split_tx_hz"`
|
|
SMeter int `json:"s_meter"` // 0-100 (raw 0-255)
|
|
PowerMeter int `json:"power_meter"` // 0-100, TX only
|
|
SWRMeter int `json:"swr_meter"` // 0-100, TX only
|
|
|
|
RFPower int `json:"rf_power"` // watts
|
|
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
|
|
Att int `json:"att"` // 0=off, else dB
|
|
NB bool `json:"nb"`
|
|
NR bool `json:"nr"`
|
|
NRLevel int `json:"nr_level"` // 1-15
|
|
Narrow bool `json:"narrow"` // NAR filter
|
|
VOX bool `json:"vox"`
|
|
// CW-only controls. Read (and shown) only in CW, where MIC and VOX mean
|
|
// nothing and these are what an operator reaches for.
|
|
KeySpeed int `json:"key_speed"` // WPM
|
|
BreakIn bool `json:"break_in"`
|
|
}
|
|
|
|
// YaesuController is the typed escape the Manager exposes for the panel, in the
|
|
// same shape as FlexController and IcomController.
|
|
type YaesuController interface {
|
|
YaesuState() YaesuTXState
|
|
RefreshYaesu() error
|
|
SetYaesuPower(int) error
|
|
SetYaesuMicGain(int) error
|
|
SetYaesuAFGain(int) error
|
|
SetYaesuRFGain(int) error
|
|
SetYaesuSquelch(int) error
|
|
SetYaesuAGC(string) error
|
|
SetYaesuPreamp(int) error
|
|
SetYaesuAtt(int) error
|
|
SetYaesuNB(bool) error
|
|
SetYaesuNR(bool) error
|
|
SetYaesuNRLevel(int) error
|
|
SetYaesuNarrow(bool) error
|
|
SetYaesuVOX(bool) error
|
|
SetYaesuKeySpeed(int) error
|
|
SetYaesuBreakIn(bool) error
|
|
YaesuZeroIn() error
|
|
// CW keying through the rig's own keyer (KY) — the fifth CW engine.
|
|
SendCW(string) error
|
|
StopCW() error
|
|
SetYaesuSplit(bool) error
|
|
SetYaesuSplitOffset(int64) error
|
|
SetYaesuBand(string) error
|
|
SetYaesuModeRaw(string) error
|
|
TuneYaesuATU() error
|
|
}
|
|
|
|
func (y *Yaesu) YaesuState() YaesuTXState {
|
|
y.mu.Lock()
|
|
defer y.mu.Unlock()
|
|
st := y.panel
|
|
st.Available = y.port != nil
|
|
st.Model = y.model
|
|
return st
|
|
}
|
|
|
|
// readPanel refreshes the meters, and the settings on the slower beat. Called
|
|
// from ReadState with the mutex HELD, so it shares the same serialised link.
|
|
func (y *Yaesu) readPanel(mode string, split bool, txHz int64) {
|
|
y.panel.Mode = mode
|
|
y.panel.Split = split
|
|
y.panel.SplitTXHz = 0
|
|
if split {
|
|
y.panel.SplitTXHz = txHz
|
|
}
|
|
|
|
// TX state first: which meters mean anything depends on it, and a power
|
|
// reading shown while receiving is how a panel lies.
|
|
if r, err := y.ask("TX;"); err == nil && len(r) >= 3 {
|
|
y.panel.Transmitting = r[2] != '0'
|
|
}
|
|
if v, ok := y.askNum("SM0;", "SM0", 3); ok {
|
|
y.panel.SMeter = scale255(v)
|
|
}
|
|
if y.panel.Transmitting {
|
|
if v, ok := y.askNum("RM4;", "RM4", 3); ok {
|
|
y.panel.PowerMeter = scale255(v)
|
|
}
|
|
if v, ok := y.askNum("RM5;", "RM5", 3); ok {
|
|
y.panel.SWRMeter = scale255(v)
|
|
}
|
|
} else {
|
|
// Zeroed rather than frozen: a stale SWR bar from the last transmission
|
|
// reads as a live measurement.
|
|
y.panel.PowerMeter, y.panel.SWRMeter = 0, 0
|
|
}
|
|
|
|
y.panelCycle++
|
|
if y.panelLoaded && y.panelCycle < 8 {
|
|
return
|
|
}
|
|
y.panelCycle = 0
|
|
y.panelLoaded = true
|
|
y.readPanelSettings()
|
|
}
|
|
|
|
// readPanelSettings re-reads everything a knob can change. Separate so a set can
|
|
// force it without waiting for the slow beat.
|
|
func (y *Yaesu) readPanelSettings() {
|
|
if v, ok := y.askNum("PC;", "PC", 3); ok {
|
|
y.panel.RFPower = v // watts, not a 0-255 scale
|
|
}
|
|
if v, ok := y.askNum("MG;", "MG", 3); ok {
|
|
y.panel.MicGain = scale255(v)
|
|
}
|
|
if v, ok := y.askNum("AG0;", "AG0", 3); ok {
|
|
y.panel.AFGain = scale255(v)
|
|
}
|
|
if v, ok := y.askNum("RG0;", "RG0", 3); ok {
|
|
y.panel.RFGain = scale255(v)
|
|
}
|
|
if v, ok := y.askNum("SQ0;", "SQ0", 3); ok {
|
|
y.panel.Squelch = scale255(v)
|
|
}
|
|
if v, ok := y.askNum("GT0;", "GT0", 1); ok {
|
|
y.panel.AGC = yaesuAGCName(v)
|
|
}
|
|
if v, ok := y.askNum("PA0;", "PA0", 1); ok {
|
|
y.panel.Preamp = v
|
|
}
|
|
if v, ok := y.askNum("RA0;", "RA0", 1); ok {
|
|
y.panel.Att = yaesuAttDB(v)
|
|
}
|
|
if v, ok := y.askNum("NB0;", "NB0", 1); ok {
|
|
y.panel.NB = v != 0
|
|
}
|
|
if v, ok := y.askNum("NR0;", "NR0", 1); ok {
|
|
y.panel.NR = v != 0
|
|
}
|
|
if v, ok := y.askNum("RL0;", "RL0", 2); ok {
|
|
y.panel.NRLevel = v
|
|
}
|
|
if v, ok := y.askNum("NA0;", "NA0", 1); ok {
|
|
y.panel.Narrow = v != 0
|
|
}
|
|
if v, ok := y.askNum("VX;", "VX", 1); ok {
|
|
y.panel.VOX = v != 0
|
|
}
|
|
// CW keyer. Read unconditionally — it is two more queries on the SLOW beat,
|
|
// and having the value ready means the CW card is populated the instant the
|
|
// operator switches mode rather than a poll cycle later.
|
|
if v, ok := y.askNum("KS;", "KS", 3); ok {
|
|
y.panel.KeySpeed = v
|
|
}
|
|
if v, ok := y.askNum("BI;", "BI", 1); ok {
|
|
y.panel.BreakIn = v != 0
|
|
}
|
|
}
|
|
|
|
// askNum sends a query and reads a fixed-width decimal field out of the reply.
|
|
// ok=false when the rig does not answer, or answers something else — a control
|
|
// this model lacks then keeps its previous value instead of dropping to zero,
|
|
// which would look like a setting that reset itself.
|
|
func (y *Yaesu) askNum(cmd, prefix string, digits int) (int, bool) {
|
|
r, err := y.ask(cmd)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
if !strings.HasPrefix(r, prefix) || len(r) < len(prefix)+digits {
|
|
debugLog.Printf("yaesu: unexpected reply %q to %q", r, cmd)
|
|
return 0, false
|
|
}
|
|
n, err := strconv.Atoi(r[len(prefix) : len(prefix)+digits])
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return n, true
|
|
}
|
|
|
|
// setAndRefresh writes a command then re-reads the settings, so the panel shows
|
|
// what the RIG ended up with rather than what we asked for — the two differ
|
|
// whenever a value is out of range or the current mode forbids the control.
|
|
func (y *Yaesu) setAndRefresh(cmd string) error {
|
|
y.mu.Lock()
|
|
defer y.mu.Unlock()
|
|
if y.port == nil {
|
|
return fmt.Errorf("yaesu: not connected")
|
|
}
|
|
if err := y.write(cmd); err != nil {
|
|
return err
|
|
}
|
|
time.Sleep(30 * time.Millisecond) // let the rig apply it before reading back
|
|
y.readPanelSettings()
|
|
return nil
|
|
}
|
|
|
|
func (y *Yaesu) RefreshYaesu() error {
|
|
y.mu.Lock()
|
|
defer y.mu.Unlock()
|
|
if y.port == nil {
|
|
return fmt.Errorf("yaesu: not connected")
|
|
}
|
|
y.readPanelSettings()
|
|
return nil
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuPower(w int) error {
|
|
return y.setAndRefresh(fmt.Sprintf("PC%03d;", clampInt(w, 5, 100)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuMicGain(p int) error {
|
|
return y.setAndRefresh(fmt.Sprintf("MG%03d;", from100(p)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuAFGain(p int) error {
|
|
return y.setAndRefresh(fmt.Sprintf("AG0%03d;", from100(p)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuRFGain(p int) error {
|
|
return y.setAndRefresh(fmt.Sprintf("RG0%03d;", from100(p)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuSquelch(p int) error {
|
|
return y.setAndRefresh(fmt.Sprintf("SQ0%03d;", from100(p)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuAGC(name string) error {
|
|
return y.setAndRefresh(fmt.Sprintf("GT0%d;", yaesuAGCCode(name)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuPreamp(n int) error {
|
|
return y.setAndRefresh(fmt.Sprintf("PA0%d;", clampInt(n, 0, 2)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuAtt(db int) error {
|
|
return y.setAndRefresh(fmt.Sprintf("RA0%d;", yaesuAttCode(db)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuNB(on bool) error {
|
|
return y.setAndRefresh(fmt.Sprintf("NB0%d;", boolDigit(on)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuNR(on bool) error {
|
|
return y.setAndRefresh(fmt.Sprintf("NR0%d;", boolDigit(on)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuNRLevel(n int) error {
|
|
return y.setAndRefresh(fmt.Sprintf("RL0%02d;", clampInt(n, 1, 15)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuNarrow(on bool) error {
|
|
return y.setAndRefresh(fmt.Sprintf("NA0%d;", boolDigit(on)))
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuVOX(on bool) error {
|
|
return y.setAndRefresh(fmt.Sprintf("VX%d;", boolDigit(on)))
|
|
}
|
|
|
|
// SetYaesuSplit uses whichever command this rig answered at connect. Sending the
|
|
// other one would be silently ignored, and the operator would get a split button
|
|
// that does nothing.
|
|
func (y *Yaesu) SetYaesuSplit(on bool) error {
|
|
// Turning split ON also PLACES the transmit VFO. Flipping the flag alone
|
|
// transmits wherever the other VFO happens to sit — it held 18.115 from an
|
|
// earlier session while the operator was listening on 14.244, so pressing
|
|
// SPLIT threw them onto another band entirely. The other VFO is stale by
|
|
// nature; the only frequency that makes sense is one derived from where the
|
|
// operator is listening NOW.
|
|
//
|
|
// The distance is the mode's usual one: 1 kHz on CW and the data modes,
|
|
// 5 kHz on phone. The +1k / +5k buttons remain for anything else.
|
|
if on {
|
|
return y.SetYaesuSplitOffset(y.defaultSplitOffset())
|
|
}
|
|
y.mu.Lock()
|
|
cmd := y.splitCmd
|
|
y.mu.Unlock()
|
|
if cmd == "" {
|
|
return fmt.Errorf("yaesu: this rig answered neither ST; nor FT; — split cannot be set")
|
|
}
|
|
return y.setAndRefresh(fmt.Sprintf("%s0;", cmd))
|
|
}
|
|
|
|
// defaultSplitOffset is what SPLIT means on this mode: up 1 kHz on CW and the
|
|
// data modes, up 5 kHz on phone — the offsets operators actually call.
|
|
func (y *Yaesu) defaultSplitOffset() int64 {
|
|
y.mu.Lock()
|
|
raw := strings.ToUpper(y.panel.RawMode)
|
|
y.mu.Unlock()
|
|
switch {
|
|
case strings.HasPrefix(raw, "CW"), strings.HasPrefix(raw, "RTTY"), strings.HasPrefix(raw, "DATA"):
|
|
return 1000
|
|
}
|
|
return 5000
|
|
}
|
|
|
|
// SetYaesuBand switches band with BS, which lands the rig on ITS OWN last-used
|
|
// frequency for that band — the radio's band memory, not a frequency we choose.
|
|
// SetYaesuModeRaw selects an exact rig mode, sideband included — "CW-U",
|
|
// "DATA-L", "RTTY-U"… The plain SetMode path takes an ADIF mode and can only
|
|
// choose a sideband by convention, but CW, RTTY and the data modes are routinely
|
|
// run on EITHER sideband and the operator is the one who knows which. This is
|
|
// what the panel's mode buttons use.
|
|
func (y *Yaesu) SetYaesuModeRaw(name string) error {
|
|
d, ok := yaesuRawModeDigit(name)
|
|
if !ok {
|
|
return fmt.Errorf("yaesu: unknown rig mode %q", name)
|
|
}
|
|
return y.setAndRefresh(fmt.Sprintf("MD0%c;", d))
|
|
}
|
|
|
|
// yaesuRawModeDigit maps a rig-mode name to its MD digit.
|
|
func yaesuRawModeDigit(name string) (byte, bool) {
|
|
switch strings.ToUpper(strings.TrimSpace(name)) {
|
|
case "LSB":
|
|
return '1', true
|
|
case "USB":
|
|
return '2', true
|
|
case "CW-U", "CWU":
|
|
return '3', true
|
|
case "CW-L", "CWL":
|
|
return '7', true
|
|
case "FM":
|
|
return '4', true
|
|
case "AM":
|
|
return '5', true
|
|
case "RTTY-L", "RTTYL":
|
|
return '6', true
|
|
case "RTTY-U", "RTTYU":
|
|
return '9', true
|
|
case "DATA-L", "DATAL", "DIGI-L":
|
|
return '8', true
|
|
case "DATA-U", "DATAU", "DIGI-U":
|
|
return 'C', true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func (y *Yaesu) SetYaesuBand(band string) error {
|
|
code, ok := yaesuBandCode(band)
|
|
if !ok {
|
|
return fmt.Errorf("yaesu: no band code for %q", band)
|
|
}
|
|
return y.setAndRefresh(fmt.Sprintf("BS%02d;", code))
|
|
}
|
|
|
|
// TuneYaesuATU starts a tuning cycle. Deliberately not followed by a settings
|
|
// read: the rig is transmitting into the tuner for several seconds and answers
|
|
// little, so the reads would just time out one after another.
|
|
func (y *Yaesu) TuneYaesuATU() error {
|
|
y.mu.Lock()
|
|
defer y.mu.Unlock()
|
|
if y.port == nil {
|
|
return fmt.Errorf("yaesu: not connected")
|
|
}
|
|
return y.write("AC002;")
|
|
}
|
|
|
|
// ── small mappings ────────────────────────────────────────────────────────
|
|
|
|
func scale255(v int) int {
|
|
if v <= 0 {
|
|
return 0
|
|
}
|
|
if v >= 255 {
|
|
return 100
|
|
}
|
|
return v * 100 / 255
|
|
}
|
|
|
|
func from100(p int) int { return clampInt(p, 0, 100) * 255 / 100 }
|
|
|
|
func clampInt(v, lo, hi int) int {
|
|
if v < lo {
|
|
return lo
|
|
}
|
|
if v > hi {
|
|
return hi
|
|
}
|
|
return v
|
|
}
|
|
|
|
func boolDigit(b bool) int {
|
|
if b {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func yaesuAGCName(code int) string {
|
|
switch code {
|
|
case 0:
|
|
return "OFF"
|
|
case 1:
|
|
return "FAST"
|
|
case 2:
|
|
return "MID"
|
|
case 3:
|
|
return "SLOW"
|
|
case 4:
|
|
return "AUTO"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func yaesuAGCCode(name string) int {
|
|
switch strings.ToUpper(strings.TrimSpace(name)) {
|
|
case "OFF":
|
|
return 0
|
|
case "FAST":
|
|
return 1
|
|
case "MID", "MEDIUM":
|
|
return 2
|
|
case "SLOW":
|
|
return 3
|
|
}
|
|
return 4 // AUTO — the safe default for anything unrecognised
|
|
}
|
|
|
|
// The FTDX10/FTDX101 attenuator is a THREE-step pad (RA00..RA03 = off, 6, 12,
|
|
// 18 dB), not the single step this first assumed — a panel offering only one
|
|
// step hides two thirds of the control. Reporting the dB rather than the raw
|
|
// code lets the buttons label themselves honestly.
|
|
func yaesuAttDB(code int) int {
|
|
switch code {
|
|
case 1:
|
|
return 6
|
|
case 2:
|
|
return 12
|
|
case 3:
|
|
return 18
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func yaesuAttCode(db int) int {
|
|
switch {
|
|
case db >= 18:
|
|
return 3
|
|
case db >= 12:
|
|
return 2
|
|
case db >= 6:
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// yaesuBandCode maps an ADIF band to the BS command's band number.
|
|
func yaesuBandCode(band string) (int, bool) {
|
|
switch strings.ToLower(strings.TrimSpace(band)) {
|
|
case "160m":
|
|
return 0, true
|
|
case "80m":
|
|
return 1, true
|
|
case "60m":
|
|
return 2, true
|
|
case "40m":
|
|
return 3, true
|
|
case "30m":
|
|
return 4, true
|
|
case "20m":
|
|
return 5, true
|
|
case "17m":
|
|
return 6, true
|
|
case "15m":
|
|
return 7, true
|
|
case "12m":
|
|
return 8, true
|
|
case "10m":
|
|
return 9, true
|
|
case "6m":
|
|
return 10, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// yaesuRawModeName is the inverse of yaesuRawModeDigit: what the rig is on,
|
|
// sideband included, for the panel's mode buttons to highlight.
|
|
func yaesuRawModeName(d byte) string {
|
|
switch d {
|
|
case '1':
|
|
return "LSB"
|
|
case '2':
|
|
return "USB"
|
|
case '3':
|
|
return "CW-U"
|
|
case '4':
|
|
return "FM"
|
|
case '5':
|
|
return "AM"
|
|
case '6':
|
|
return "RTTY-L"
|
|
case '7':
|
|
return "CW-L"
|
|
case '8':
|
|
return "DATA-L"
|
|
case '9':
|
|
return "RTTY-U"
|
|
case 'C':
|
|
return "DATA-U"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// SetYaesuSplitOffset puts the transmit VFO a fixed distance above the receive
|
|
// one and turns split on, in a single action.
|
|
//
|
|
// This is the split an operator actually uses when working a pile-up: listen on
|
|
// the DX, transmit up 5 kHz on phone or up 1 kHz on CW. Doing it by hand means
|
|
// swapping VFOs, retuning and swapping back, which is exactly the fumbling a
|
|
// panel should remove.
|
|
//
|
|
// The offset is applied to the RECEIVE frequency and written to the OTHER VFO —
|
|
// whichever that is. On VFO B the roles are mirrored, so listening on B writes A.
|
|
func (y *Yaesu) SetYaesuSplitOffset(offsetHz int64) error {
|
|
y.mu.Lock()
|
|
defer y.mu.Unlock()
|
|
if y.port == nil {
|
|
return fmt.Errorf("yaesu: not connected")
|
|
}
|
|
rx := y.curRXFreq
|
|
if rx <= 0 {
|
|
return fmt.Errorf("yaesu: no receive frequency read yet")
|
|
}
|
|
tx := rx + offsetHz
|
|
if tx <= 0 || tx > 999_999_999 {
|
|
return fmt.Errorf("yaesu: split frequency %d out of the CAT range", tx)
|
|
}
|
|
// Write the VFO we are NOT listening on.
|
|
cmd := "FB"
|
|
if y.curVFO == "B" {
|
|
cmd = "FA"
|
|
}
|
|
if err := y.write(fmt.Sprintf("%s%09d;", cmd, tx)); err != nil {
|
|
return err
|
|
}
|
|
if y.splitCmd == "" {
|
|
return fmt.Errorf("yaesu: this rig answered neither ST; nor FT; — split cannot be set")
|
|
}
|
|
time.Sleep(30 * time.Millisecond)
|
|
if err := y.write(fmt.Sprintf("%s1;", y.splitCmd)); err != nil {
|
|
return err
|
|
}
|
|
y.panel.Split = true
|
|
y.panel.SplitTXHz = tx
|
|
return nil
|
|
}
|
|
|
|
// SetYaesuKeySpeed sets the internal keyer speed in words per minute. The rig
|
|
// clamps to its own 4-60 range; clamping here too keeps a slider from sending a
|
|
// value that would simply be ignored, which reads as a dead control.
|
|
func (y *Yaesu) SetYaesuKeySpeed(wpm int) error {
|
|
return y.setAndRefresh(fmt.Sprintf("KS%03d;", clampInt(wpm, 4, 60)))
|
|
}
|
|
|
|
// SetYaesuBreakIn toggles CW break-in (BK).
|
|
func (y *Yaesu) SetYaesuBreakIn(on bool) error {
|
|
return y.setAndRefresh(fmt.Sprintf("BI%d;", boolDigit(on)))
|
|
}
|
|
|
|
// YaesuZeroIn is the CW ZIN function: the rig retunes itself so the station
|
|
// being received lands exactly on the operator's CW pitch. It is a one-shot
|
|
// action with no state to read back, so no settings refresh follows — and the
|
|
// frequency change arrives through the normal poll like any other.
|
|
func (y *Yaesu) YaesuZeroIn() error {
|
|
y.mu.Lock()
|
|
defer y.mu.Unlock()
|
|
if y.port == nil {
|
|
return fmt.Errorf("yaesu: not connected")
|
|
}
|
|
return y.write("ZI;")
|
|
}
|