diff --git a/app_k3.go b/app_k3.go
index c75378a..796e569 100644
--- a/app_k3.go
+++ b/app_k3.go
@@ -80,6 +80,11 @@ func (a *App) SetKenwoodXIT(on bool) error {
return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodXIT(on) })
}
+// NudgeKenwoodRIT moves the RIT/XIT offset by delta Hz.
+func (a *App) NudgeKenwoodRIT(delta int) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.NudgeKenwoodRIT(delta) })
+}
+
// ClearKenwoodRIT zeroes the RIT and XIT offsets together, which is what the
// radio's own RC does and what an operator means by "clear it".
func (a *App) ClearKenwoodRIT() error {
diff --git a/changelog.json b/changelog.json
index bf36fdb..5cff80d 100644
--- a/changelog.json
+++ b/changelog.json
@@ -2,8 +2,16 @@
{
"version": "0.26.13",
"date": "",
- "en": [],
- "fr": []
+ "en": [
+ "Elecraft console: MOX unkeys again, and the power and SWR bars move while transmitting. The poll stops while the carrier is up — the rig refuses most questions then — and the panel was left believing the radio was still receiving, so pressing MOX a second time sent another transmit command instead of stopping.",
+ "Elecraft console: RIT and XIT can be moved — ±10 and ±100 Hz buttons with the offset shown beside them. A lit RIT button says the feature is on and nothing about where it has put the receiver.",
+ "Elecraft console: a 4.0 kHz filter button, the K3 maximum and the one FT8 wants."
+ ],
+ "fr": [
+ "Console Elecraft : MOX repasse bien en réception, et les barres de puissance et de ROS bougent pendant l'émission. L'interrogation s'arrête quand la porteuse est levée — la radio refuse alors la plupart des questions — et le panneau croyait donc la radio toujours en réception : un second appui sur MOX envoyait une nouvelle commande d'émission au lieu d'arrêter.",
+ "Console Elecraft : le RIT et le XIT se règlent — boutons ±10 et ±100 Hz avec le décalage affiché à côté. Un bouton RIT allumé dit que la fonction est active et rien sur l'endroit où elle a mis le récepteur.",
+ "Console Elecraft : un bouton de filtre à 4,0 kHz, le maximum du K3 et celui qu'il faut pour le FT8."
+ ]
},
{
"version": "0.26.12",
diff --git a/frontend/src/components/ElecraftPanel.tsx b/frontend/src/components/ElecraftPanel.tsx
index 3b4f0b5..9d83b79 100644
--- a/frontend/src/components/ElecraftPanel.tsx
+++ b/frontend/src/components/ElecraftPanel.tsx
@@ -4,7 +4,7 @@ import {
GetKenwoodState, RefreshKenwood, SetKenwoodPower, SetKenwoodAFGain, SetKenwoodTX, TuneKenwoodATU,
SetKenwoodRFGain, SetKenwoodMicGain, SetKenwoodSquelch, SetKenwoodPreamp, SetKenwoodAtt,
SetKenwoodNB, SetKenwoodNR, SetKenwoodAGC, SetKenwoodFilter, SetKenwoodAntenna,
- SetKenwoodRIT, SetKenwoodXIT, ClearKenwoodRIT, SetKenwoodKeySpeed, ToggleKenwoodATU,
+ SetKenwoodRIT, SetKenwoodXIT, ClearKenwoodRIT, SetKenwoodKeySpeed, ToggleKenwoodATU, NudgeKenwoodRIT,
GetCATState,
} from '../../wailsjs/go/main/App';
import { cn } from '@/lib/utils';
@@ -20,7 +20,7 @@ type KenwoodState = {
power_meter: number; swr: number; swr_raw: number;
rf_power: number; af_gain: number; rf_gain: number; mic_gain: number; squelch: number;
preamp: boolean; att: boolean; nb: boolean; nr: boolean; agc?: string;
- filter_hz: number; antenna: number; rit: boolean; xit: boolean; key_speed: number;
+ filter_hz: number; antenna: number; rit: boolean; xit: boolean; rit_offset: number; key_speed: number;
meters_provisional: boolean;
};
@@ -29,14 +29,17 @@ const ZERO: KenwoodState = {
s_meter: 0, s_meter_raw: 0, power_meter: 0, swr: 0, swr_raw: 0,
rf_power: 0, af_gain: 0, rf_gain: 0, mic_gain: 0, squelch: 0,
preamp: false, att: false, nb: false, nr: false,
- filter_hz: 0, antenna: 0, rit: false, xit: false, key_speed: 0,
+ filter_hz: 0, antenna: 0, rit: false, xit: false, rit_offset: 0, key_speed: 0,
meters_provisional: true,
};
// Filter widths worth a button. CW work happens at 200-500 Hz, SSB at 2.4-2.8
// kHz; the rest of the range is reachable from the radio's own knob, and a
// panel offering thirty widths is slower to use than the knob it replaces.
-const FILTERS = [200, 400, 700, 1000, 1800, 2400, 2800];
+// 4.0k is the K3 maximum and the one FT8 wants: a 2.8 kHz filter clips the
+// top of the FT8 sub-band, and the decodes that go missing are the ones
+// nobody notices are missing.
+const FILTERS = [200, 400, 700, 1000, 1800, 2400, 2800, 4000];
// Raw S-meter → S units. The K3 answers 0-21 across S0…S9+60; S9 is taken at
// raw 9 and each step above it as 6 dB. PROVISIONAL, like the rest of the
@@ -281,6 +284,15 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
SetKenwoodRIT(!view.rit).catch(setErrMsg)} />
SetKenwoodXIT(!view.xit).catch(setErrMsg)} />
+ {/* The offset itself. A lit RIT button says the feature is on and
+ nothing about where it has put the receiver. */}
+ {[-100, -10, 10, 100].map((d) => (
+ 0 ? '+' : '') + d} on={false} off={off}
+ onClick={() => NudgeKenwoodRIT(d).catch(setErrMsg)} />
+ ))}
+
+ {view.rit_offset > 0 ? '+' : ''}{view.rit_offset || 0} Hz
+
ClearKenwoodRIT().catch(setErrMsg)} />
{/* Antenna only when the radio answered AN — a K3 without the internal
ATU has one socket and no switch to offer. */}
diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts
index 72eaefe..4591844 100644
--- a/frontend/wailsjs/go/main/App.d.ts
+++ b/frontend/wailsjs/go/main/App.d.ts
@@ -803,6 +803,8 @@ export function NetSetDefaults(arg1:string,arg2:string,arg3:string,arg4:string):
export function NetUpdateActive(arg1:qso.QSO):Promise;
+export function NudgeKenwoodRIT(arg1:number):Promise;
+
export function OpenADIFFile():Promise;
export function OpenAwardsFolder():Promise;
diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js
index ecb3558..c2e0c66 100644
--- a/frontend/wailsjs/go/main/App.js
+++ b/frontend/wailsjs/go/main/App.js
@@ -1546,6 +1546,10 @@ export function NetUpdateActive(arg1) {
return window['go']['main']['App']['NetUpdateActive'](arg1);
}
+export function NudgeKenwoodRIT(arg1) {
+ return window['go']['main']['App']['NudgeKenwoodRIT'](arg1);
+}
+
export function OpenADIFFile() {
return window['go']['main']['App']['OpenADIFFile']();
}
diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts
index 1b3db30..21ec84e 100644
--- a/frontend/wailsjs/go/models.ts
+++ b/frontend/wailsjs/go/models.ts
@@ -1094,6 +1094,7 @@ export namespace cat {
antenna: number;
rit: boolean;
xit: boolean;
+ rit_offset: number;
key_speed: number;
meters_provisional: boolean;
@@ -1129,6 +1130,7 @@ export namespace cat {
this.antenna = source["antenna"];
this.rit = source["rit"];
this.xit = source["xit"];
+ this.rit_offset = source["rit_offset"];
this.key_speed = source["key_speed"];
this.meters_provisional = source["meters_provisional"];
}
diff --git a/internal/cat/kenwood.go b/internal/cat/kenwood.go
index 5af8b84..138dab1 100644
--- a/internal/cat/kenwood.go
+++ b/internal/cat/kenwood.go
@@ -91,11 +91,18 @@ type Kenwood struct {
keyWPM int // CW keyer speed, for pacing the KY buffer (see kenwood_cw.go)
// Commands this rig answered "?;" to — asked once, then never again.
unsupported map[string]bool
+ // noLatch suspends that memory while a refusal is expected to be about
+ // timing rather than capability (see ask).
+ noLatch bool
// 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
+ // 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
panelCycle int
panelLoaded bool
metersLogged int
@@ -309,6 +316,15 @@ func (k *Kenwood) ReadState() (RigState, error) {
if k.tx && !k.txAt.IsZero() && time.Since(k.txAt) < 30*time.Second {
s := k.lastState
s.Connected = true
+ // The panel still has to be told the radio is transmitting, and the
+ // transmit meters still have to be read — this early return used to skip
+ // both. The panel therefore showed MOX unlit while the rig was keyed, so
+ // pressing it again sent ANOTHER transmit command instead of unkeying,
+ // and the K3 stayed in TX. The power and SWR bars never moved either,
+ // for the same reason: the only moment they mean anything is the one
+ // moment they were not being read.
+ k.panel.Transmitting = true
+ k.readTXMeters()
return s, nil
}
raw, err := k.ask("IF;")
@@ -663,7 +679,12 @@ func (k *Kenwood) ask(cmd string) (string, error) {
// NOT "unsupported". Latching them off would blind the poll loop for
// good and read as "lost the rig". Only remember the OPTIONAL commands
// (FR/FT/…) so the poll loop stops paying a 600 ms timeout for those.
- if want != "IF" && want != "ID" {
+ // While TRANSMITTING the rig refuses a great deal that it answers
+ // perfectly well on receive, so a "?;" then says nothing about what
+ // the radio supports. Latching it would silence a meter for the rest
+ // of the session on the strength of one badly timed question — and
+ // the meters are read precisely while transmitting.
+ if want != "IF" && want != "ID" && !k.noLatch {
k.unsupported[want] = true
debugLog.Printf("kenwood: this rig does not support %q — not asking again", cmd)
}
diff --git a/internal/cat/kenwood_panel.go b/internal/cat/kenwood_panel.go
index fa1a6f0..1278de6 100644
--- a/internal/cat/kenwood_panel.go
+++ b/internal/cat/kenwood_panel.go
@@ -68,9 +68,14 @@ type KenwoodTXState struct {
// offer a switch that goes nowhere.
Antenna int `json:"antenna"`
- RIT bool `json:"rit"`
- XIT bool `json:"xit"`
- KeySpeed int `json:"key_speed"` // WPM
+ RIT bool `json:"rit"`
+ XIT bool `json:"xit"`
+ // RITOffset is the RIT/XIT offset in Hz — the number the buttons move and
+ // the one an operator is actually reading when they look at RIT at all. A
+ // lit RIT button with no offset beside it says the feature is on and
+ // nothing about where it has put the receiver.
+ RITOffset int `json:"rit_offset"`
+ 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
@@ -97,6 +102,7 @@ type KenwoodPanelController interface {
SetKenwoodAntenna(int) error
SetKenwoodRIT(bool) error
SetKenwoodXIT(bool) error
+ NudgeKenwoodRIT(int) error
ClearKenwoodRIT() error
SetKenwoodTX(bool) error
TuneKenwoodATU() error
@@ -222,9 +228,16 @@ func (k *Kenwood) readPanelSettings() {
if v, ok := k.askNum("XT;", "XT", 1); ok {
k.panel.XIT = v != 0
}
+ // RO carries a sign, so it is read as a whole rather than through askNum.
+ if r, err := k.ask("RO;"); err == nil {
+ if hz, ok := parseKenwoodOffset(r); ok {
+ k.panel.RITOffset = hz
+ }
+ }
if v, ok := k.askNum("KS;", "KS", 3); ok {
k.panel.KeySpeed = v
}
+ k.probeIcons()
}
// The full-scale value of the analogue controls differs between an Elecraft and
@@ -310,6 +323,10 @@ var kenwoodMeterProbes = []struct {
// readTXMeters reads the transmit meters.
func (k *Kenwood) readTXMeters() {
now := time.Now()
+ // Refusals here are about WHEN the question was asked, not about what the
+ // radio can do: a K3 says "?;" to plenty while the carrier is up.
+ k.noLatch = true
+ defer func() { k.noLatch = false }()
if v, ok := k.askNum("BG;", "BG", 2); ok {
k.panel.PowerMeter = k.powerPeak.update(kenwoodBargraphPercent(v), now)
}
@@ -396,6 +413,31 @@ func (k *Kenwood) SetKenwoodAFGain(p int) error {
return k.setPanel(fmt.Sprintf("AG%03d;", p*255/100))
}
+// probeIcons logs the K3's icon/status word whenever it changes.
+//
+// There is no command that asks "is the ATU in line": the reference says the
+// front-panel switch functions show up as icon changes readable through IC (or
+// DS), which means the answer is a bit in a word nobody here can name yet. So
+// the word is logged when it changes, and an operator toggling the ATU while
+// watching the log hands us the bit — after which the ATU button can light up
+// honestly instead of guessing from what it last sent.
+func (k *Kenwood) probeIcons() {
+ if k.iconProbes >= 40 {
+ return
+ }
+ r, err := k.ask("IC;")
+ if err != nil {
+ k.iconProbes = 40 // this rig has no IC; stop asking
+ return
+ }
+ if r == k.lastIcons {
+ return
+ }
+ k.lastIcons = r
+ k.iconProbes++
+ debugLog.Printf("kenwood: icon status changed → %s (toggle ATU/PRE/ATT while watching this line to name the bits)", r)
+}
+
// 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())))
@@ -456,6 +498,60 @@ func (k *Kenwood) SetKenwoodAntenna(n int) error {
return k.setPanel(fmt.Sprintf("AN%d;", n))
}
+// parseKenwoodOffset reads "RO+0100;" / "RO-0250;" into Hz.
+func parseKenwoodOffset(frame string) (int, bool) {
+ body := strings.TrimSuffix(strings.TrimPrefix(frame, "RO"), ";")
+ if len(body) < 2 {
+ return 0, false
+ }
+ sign := 1
+ switch body[0] {
+ case '-':
+ sign = -1
+ body = body[1:]
+ case '+':
+ body = body[1:]
+ }
+ n, err := strconv.Atoi(strings.TrimSpace(body))
+ if err != nil {
+ return 0, false
+ }
+ return sign * n, true
+}
+
+// NudgeKenwoodRIT moves the RIT/XIT offset by delta Hz.
+//
+// The offset is SET rather than stepped, because RO is where the radio keeps
+// it and stepping commands differ across the family. The panel's own reading is
+// the starting point, so two quick presses do not both start from the same
+// stale value.
+func (k *Kenwood) NudgeKenwoodRIT(delta int) error {
+ k.mu.Lock()
+ cur := k.panel.RITOffset
+ k.mu.Unlock()
+ next := cur + delta
+ // A pile-up is chased with a few hundred hertz of RIT; ±5 kHz is already
+ // past anything an offset is for, and past what the rig accepts.
+ if next > 5000 {
+ next = 5000
+ }
+ if next < -5000 {
+ next = -5000
+ }
+ sign := "+"
+ if next < 0 {
+ sign = "-"
+ }
+ mag := next
+ if mag < 0 {
+ mag = -mag
+ }
+ k.mu.Lock()
+ k.panel.RITOffset = next // optimistic, like the sliders
+ k.mu.Unlock()
+ return k.setPanel(fmt.Sprintf("RO%s%04d;", sign, mag))
+}
+
func (k *Kenwood) SetKenwoodRIT(on bool) error {
return k.setPanel(fmt.Sprintf("RT%d;", boolDigit(on)))
}