feat: colour the Yaesu S-meter like a real one — green, amber past S9, red at +20

The bar was one colour for its whole travel, so nothing distinguished a signal
that is merely readable from one that belongs in the log as 59+20.

Green to S9, amber through the S9+ range, red from +20 dB. The thresholds are
computed from the SAME S9 point the label uses rather than hard-coded
percentages: the S9 position is still a hypothesis on this rig, and when it is
corrected the colours have to move with it or the meter would say 59+20 in
amber.
This commit is contained in:
2026-07-29 11:59:58 +02:00
parent 6d309cada1
commit 8cc6997eec
+20 -4
View File
@@ -105,15 +105,31 @@ function bandOfHz(hz?: number): string {
// falls; the front panel puts it at roughly half travel, which is the 50 used // falls; the front panel puts it at roughly half travel, which is the 50 used
// here. That figure is a HYPOTHESIS — if reports come out consistently one S // here. That figure is a HYPOTHESIS — if reports come out consistently one S
// unit off on a radio, this is the number to correct, not the RST helper. // unit off on a radio, this is the number to correct, not the RST helper.
const S9_PCT = 50; // where S9 falls on the 0-100 reading (see above)
const DB_PER_PCT = 60 / 50; // above S9 the scale runs to roughly +60 dB
function sParts(v: number): { s: number; over: number; label: string } { function sParts(v: number): { s: number; over: number; label: string } {
if (v >= 50) { if (v >= S9_PCT) {
const over = Math.max(0, Math.round((v - 50) * 60 / 50)); const over = Math.max(0, Math.round((v - S9_PCT) * DB_PER_PCT));
return { s: 9, over, label: over > 0 ? `S9+${over}` : 'S9' }; return { s: 9, over, label: over > 0 ? `S9+${over}` : 'S9' };
} }
const s = Math.max(0, Math.min(9, Math.round(v / 5.5))); const s = Math.max(0, Math.min(9, Math.round(v / (S9_PCT / 9))));
return { s, over: 0, label: `S${s}` }; return { s, over: 0, label: `S${s}` };
} }
// Segment colour, the way a radio's own meter is printed: green up to S9, amber
// through the S9+ range, red once the signal is strong enough to be reported as
// 59+20 or more. Derived from the SAME S9 point as the label, so the colour
// change always lands exactly where the numbers say it should — if the S9 point
// is ever corrected, the colours follow on their own.
const RED_OVER_DB = 20;
function sSegColor(frac: number): string {
const pct = frac * 100;
if (pct < S9_PCT) return '#16a34a';
if ((pct - S9_PCT) * DB_PER_PCT < RED_OVER_DB) return '#f59e0b';
return '#dc2626';
}
function Slider({ value, onChange, disabled, accent = 'var(--primary)', min = 0, max = 100 }: { function Slider({ value, onChange, disabled, accent = 'var(--primary)', min = 0, max = 100 }: {
value: number; onChange: (v: number) => void; disabled?: boolean; accent?: string; min?: number; max?: number; value: number; onChange: (v: number) => void; disabled?: boolean; accent?: string; min?: number; max?: number;
}) { }) {
@@ -311,7 +327,7 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi
consoles read alike instead of each having its own instrument style. */} consoles read alike instead of each having its own instrument style. */}
<Card icon={Activity} title={t('yaesu.meters')}> <Card icon={Activity} title={t('yaesu.meters')}>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<MeterBar label="S-METER" value={view.s_meter} lo={0} hi={100} accent="#16a34a" <MeterBar label="S-METER" value={view.s_meter} lo={0} hi={100} accent="#16a34a" segColor={sSegColor}
display={sParts(view.s_meter).label} display={sParts(view.s_meter).label}
onClick={onReportRST ? () => { const sp = sParts(view.s_meter); onReportRST(sMeterRST(sp.s, sp.over, view.mode)); } : undefined} onClick={onReportRST ? () => { const sp = sParts(view.s_meter); onReportRST(sMeterRST(sp.s, sp.over, view.mode)); } : undefined}
title={onReportRST ? t('yaesu.sToRst') : undefined} /> title={onReportRST ? t('yaesu.sToRst') : undefined} />