feat(k3): a mode row on the Elecraft console — CW/USB/LSB/DATA/DATA-RTTY

The report: switching the K3 to a data mode 'does not work for FT8 — the K3
does not go into DATA'. The mode digit is only half the answer on that radio:
MD6 keeps whatever DT submode the previous session left, and with FSK D still
armed the rig keys FT8 with no rear-audio modulation.

The panel buttons say the whole thing: DATA is MD6+DT0 (DATA A, the soundcard
path), DATA RTTY is MD6+DT2 (FSK D). The DT submode is also read on the
settings beat — Elecraft only, a plain Kenwood would '?;' it — so the active
DATA button follows what the rig is actually in, front-panel changes included.
A dedicated SetKenwoodPanelMode rather than the logger's SetMode: a panel
button is the operator saying exactly what the rig should do, not an ADIF mode
to be mapped through preferences.
This commit is contained in:
2026-08-28 23:35:25 +02:00
parent 0f4e31853b
commit 7e7ad50f60
8 changed files with 127 additions and 4 deletions
+69
View File
@@ -35,6 +35,11 @@ type KenwoodTXState struct {
Model string `json:"model,omitempty"`
Elecraft bool `json:"elecraft"` // a K3/K4 rather than a Kenwood
Mode string `json:"mode,omitempty"`
// DataSub is the K3/K4 DATA submode while in DATA mode (DT): "DATA A",
// "AFSK A", "FSK D" or "PSK D". Empty on a Kenwood, and outside DATA. The
// panel's two DATA buttons need it to show WHICH data mode the rig is in —
// FT8 wants DATA A, RTTY wants FSK D, and MD6 alone cannot say which.
DataSub string `json:"data_sub,omitempty"`
Transmitting bool `json:"transmitting"`
Split bool `json:"split"`
@@ -111,6 +116,7 @@ type KenwoodPanelController interface {
SetKenwoodTX(bool) error
TuneKenwoodATU() error
ToggleKenwoodATU() error
SetKenwoodPanelMode(string) error
}
// kenwoodPanelSlowBeat is how many polls pass between full re-reads of the
@@ -212,6 +218,14 @@ func (k *Kenwood) readPanelSettings() {
if v, ok := k.askNum("NR;", "NR", 1); ok {
k.panel.NR = v != 0
}
// The DATA submode, Elecraft only (a plain Kenwood has no DT and would "?;"
// it). Read every settings beat: the operator changes it from the rig's own
// front panel mid-session, and the two DATA buttons must follow.
if k.elecraft {
if v, ok := k.askNum("DT;", "DT", 1); ok {
k.panel.DataSub = kenwoodDataSubName(v)
}
}
if v, ok := k.askNum("GT;", "GT", 3); ok {
k.panel.AGC = kenwoodAGCName(v)
}
@@ -650,3 +664,58 @@ func (k *Kenwood) askNum(cmd, prefix string, digits int) (int, bool) {
}
return n, true
}
// kenwoodDataSubName decodes DT (K3/K4 programmer's reference).
func kenwoodDataSubName(v int) string {
switch v {
case 0:
return "DATA A"
case 1:
return "AFSK A"
case 2:
return "FSK D"
case 3:
return "PSK D"
}
return ""
}
// SetKenwoodPanelMode is the panel's mode row: CW / USB / LSB / DATA / RTTY.
//
// It exists because SetMode is the LOGGER's path — it maps an ADIF mode and
// honours the data-mode preference — while a panel button is the operator
// saying exactly what the rig should do. The two DATA cases are the point:
// "DATA" is MD6 + DT0 (DATA A, the soundcard submode FT8/FT4 modulate through)
// and "RTTY" is MD6 + DT2 (FSK D, the K3's direct-keyed RTTY). MD6 alone keeps
// whatever submode a prior session left, which is how a K3 "in DATA" transmits
// FT8 with no audio — the report behind this row.
func (k *Kenwood) SetKenwoodPanelMode(mode string) error {
k.mu.Lock()
defer k.mu.Unlock()
if k.port == nil {
return fmt.Errorf("kenwood: not connected")
}
var cmds []string
switch strings.ToUpper(strings.TrimSpace(mode)) {
case "CW":
cmds = []string{"MD3;"}
case "USB":
cmds = []string{"MD2;"}
case "LSB":
cmds = []string{"MD1;"}
case "DATA":
cmds = []string{"MD6;", "DT0;"}
case "RTTY":
cmds = []string{"MD6;", "DT2;"}
default:
return fmt.Errorf("kenwood panel: unknown mode %q", mode)
}
for _, c := range cmds {
if err := k.write(c); err != nil {
return err
}
}
// Re-read the settings on the next poll so Mode and DataSub confirm at once.
k.panelCycle = kenwoodPanelSlowBeat
return nil
}