fix(elecraft): calibrate the S-meter, and stop the SWR spike lingering

The S-unit mapping was provisional, waiting for a real radio. One arrived:
side by side with a K3's own display, raw 5 reads S7 and raw 9 reads S9+20,
so S9 sits near raw 6.5 — not 9 — and each raw step above it is worth ~8 dB.
The old scale showed everything two S-units low.

The SWR meter borrowed the power meter's needle inertia — hold the peak, then
close a quarter of the gap per poll. The K3 throws a brief SWR spike as an FT8
frame ends, and that decay turned one bad sample into twelve seconds of red on
the next transmission. SWR now holds the peak for the same 1.5 s and then
snaps back to the live reading: it is a warning light, not a needle.
This commit is contained in:
2026-08-29 13:52:40 +02:00
parent 6eb6f1b339
commit 704b614c38
4 changed files with 48 additions and 13 deletions
+16 -12
View File
@@ -42,18 +42,22 @@ const ZERO: KenwoodState = {
// 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
// scaling: the raw value is on screen and in the log, so a real radio settles
// it rather than this comment.
const S9_RAW = 9;
const DB_PER_RAW = 6;
function sParts(rawV: number): { s: number; over: number; label: string } {
if (rawV >= S9_RAW) {
const over = Math.max(0, Math.round((rawV - S9_RAW) * DB_PER_RAW));
// Raw S-meter → S units, CALIBRATED against a real K3 beside its own display
// (2026-08): raw 5 reads S7 on the radio, raw 9 reads S9+20. So S9 sits near
// raw 6.5 — not 9, which showed everything two S-units low — and each raw step
// above it is worth ~8 dB, shown in the 10 dB steps the K3's own meter uses.
// A Kenwood answers 0-30 on the same command and keeps the simple 1-per-raw
// scale until someone calibrates one against a real radio too.
const K3_S9_RAW = 6.5;
const K3_DB_PER_RAW = 8;
function sParts(rawV: number, elecraft: boolean): { s: number; over: number; label: string } {
const s9raw = elecraft ? K3_S9_RAW : 9;
if (rawV >= s9raw) {
let over = Math.max(0, Math.round((rawV - s9raw) * (elecraft ? K3_DB_PER_RAW : 6)));
if (elecraft) over = Math.round(over / 10) * 10;
return { s: 9, over, label: over > 0 ? `S9+${over}` : 'S9' };
}
const s = Math.max(0, Math.min(9, rawV));
const s = Math.max(0, Math.min(9, Math.round(rawV * (elecraft ? 9 / K3_S9_RAW : 1))));
return { s, over: 0, label: `S${s}` };
}
@@ -211,10 +215,10 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<MeterBar label="S-METER" value={view.transmitting ? 0 : view.s_meter} lo={0} hi={100}
accent="#16a34a" segColor={sSegColor}
display={view.transmitting ? '—' : sParts(view.s_meter_raw).label}
display={view.transmitting ? '—' : sParts(view.s_meter_raw, view.elecraft).label}
onClick={() => {
if (view.transmitting || !onReportRST) return;
const sp = sParts(view.s_meter_raw);
const sp = sParts(view.s_meter_raw, view.elecraft);
onReportRST(sMeterRST(sp.s, sp.over, view.mode));
}}
title={t('k3.sMeterHint', { raw: String(view.s_meter_raw) })} />