An operator with a callsign in the field, switching FT8 to SSB, kept "+00" as the report. The edited flag protects a report the operator chose — 57 rather than 59 — and it was holding across a change of mode as well, where it protects nothing: a decibel figure is not a weak SSB report, it is not a report at all. Worse, anything that fills the field from the rig sets that flag too (the S-meter readouts in the rig consoles), so the field could be stuck in the wrong notation for the rest of the QSO. The flag now holds only while the report still belongs to the mode's family. When it does not, the operator's own judgement is carried across where it can be — 57 becomes 579, 599 becomes 59 — and the mode's preset answers where it cannot.
121 lines
5.5 KiB
TypeScript
121 lines
5.5 KiB
TypeScript
// sMeterRST turns an S-meter reading into the RST report you SEND to the other
|
|
// station (RST tx / rst_sent) — because your S-meter is how strong THEY are.
|
|
//
|
|
// s : S-unit 0-9 (used when below S9)
|
|
// overDb : dB over S9 (0 when at/below S9)
|
|
// mode : operating mode — CW/digital get a 3-digit RST, phone a 2-digit RS
|
|
//
|
|
// For S9+ the dB is rounded UP to the nearest 5 (a 9+3 reading reports 59+5,
|
|
// 9+12 reports 59+15, …), matching how operators give strong-signal reports.
|
|
export function sMeterRST(s: number, overDb: number, mode?: string): string {
|
|
const cw = /CW|RTTY|PSK|FT[0-9]|JT|JS8|MFSK|OLIVIA|DATA|DIG/i.test(mode || '');
|
|
const strength = Math.max(1, Math.min(9, Math.round(s)));
|
|
if (cw) return overDb > 0 ? '599' : `5${strength}9`;
|
|
if (overDb > 0) return `59+${Math.ceil(overDb / 5) * 5}`;
|
|
return `5${strength}`;
|
|
}
|
|
|
|
// RST dropdown lists, shared by the entry form and the QSO editor so both offer
|
|
// the same per-mode choices (Settings → Modes → RST report lists).
|
|
export type RSTLists = { phone: string[]; cw: string[]; digital: string[] };
|
|
|
|
// rstCategory maps an ADIF mode to its RST family (phone / cw / digital).
|
|
export function rstCategory(mode: string): keyof RSTLists {
|
|
const m = (mode || '').toUpperCase();
|
|
const digital = ['FT8', 'FT4', 'JT65', 'JT9', 'JS8', 'Q65', 'MSK144', 'FST4', 'FST4W', 'MFSK', 'OLIVIA', 'JT4', 'WSPR'];
|
|
if (digital.includes(m)) return 'digital';
|
|
if (['CW', 'RTTY', 'PSK31', 'PSK63', 'PSK', 'PSK125'].includes(m)) return 'cw';
|
|
return 'phone';
|
|
}
|
|
|
|
// rstOptions returns the valid report choices for a mode from the user's
|
|
// editable lists, with a tiny fallback before they load.
|
|
export function rstOptions(mode: string, lists: RSTLists): string[] {
|
|
const cat = rstCategory(mode);
|
|
const l = lists[cat];
|
|
if (l && l.length) return l;
|
|
return cat === 'phone' ? ['59', '58', '57'] : cat === 'cw' ? ['599', '589', '579'] : ['+00', '-10', '-20'];
|
|
}
|
|
|
|
// stepRST moves a report one step up or down, the way an operator would say it.
|
|
//
|
|
// The dropdown beside these fields is a list of the values worth having to hand,
|
|
// not of every legal report: nobody keeps 41 dB figures in it. So the wheel
|
|
// works on the VALUE, not on the list — a digital report moves by a decibel and
|
|
// an RST by one S-unit, which is what the hand on the wheel is asking for.
|
|
//
|
|
// dir is +1 for wheel up (a better report) and -1 for down.
|
|
export function stepRST(value: string, dir: number, mode: string): string {
|
|
const v = (value || '').trim();
|
|
if (v === '') return v;
|
|
if (rstCategory(mode) === 'digital') {
|
|
// A dB report: "-12", "+05", "0". Kept in its own shape — signed and two
|
|
// digits — because that is how every decoder writes it and how the operator
|
|
// reads it back off the screen.
|
|
const n = parseInt(v, 10);
|
|
if (!Number.isFinite(n)) return v;
|
|
// The range WSJT-X itself reports in, with room either side. Beyond it the
|
|
// number stops meaning anything.
|
|
const next = Math.max(-30, Math.min(35, n + dir));
|
|
return (next < 0 ? '-' : '+') + String(Math.abs(next)).padStart(2, '0');
|
|
}
|
|
// RST/RS, and the S9+ ladder above it. What an operator counts through is
|
|
//
|
|
// … 57 58 59 59+5 59+10 59+15 59+20 …
|
|
//
|
|
// — one S-unit up to nine, then five decibels at a time, because that is how
|
|
// a strong signal is reported and how the S-meter is read (see sMeterRST).
|
|
// Stepping the S digit and leaving the "+20" where it was would have gone
|
|
// from 59+20 to 58+20, which nobody has ever said on the air.
|
|
//
|
|
// R and T do not move: they are judgements about readability and tone, and a
|
|
// wheel has no business changing them.
|
|
const m = /^(\d)(\d)(\d?)(?:\+(\d+))?$/.exec(v);
|
|
if (!m) return v;
|
|
const head = m[1];
|
|
const tone = m[3];
|
|
let sUnit = parseInt(m[2], 10);
|
|
let over = m[4] === undefined ? 0 : parseInt(m[4], 10);
|
|
if (over > 0) {
|
|
// Above S9: five at a time, and back down through +5 to a plain 59.
|
|
over = Math.max(0, Math.min(60, over + dir * 5));
|
|
} else if (sUnit >= 9 && dir > 0) {
|
|
over = 5; // 59 → 59+5
|
|
} else {
|
|
sUnit = Math.max(1, Math.min(9, sUnit + dir));
|
|
}
|
|
return head + String(sUnit) + tone + (over > 0 ? '+' + over : '');
|
|
}
|
|
|
|
// rstFitsMode says whether a report belongs to the family a mode reports in.
|
|
//
|
|
// The three families are written differently and are not interchangeable: a
|
|
// signed decibel figure ("+00"), a three-figure RST ("599"), a two-figure RS
|
|
// ("59", with an optional "+20" above S9). "+00" on SSB is not a weak report,
|
|
// it is not a report at all.
|
|
export function rstFitsMode(value: string, mode: string): boolean {
|
|
const v = (value || '').trim();
|
|
if (v === '') return false;
|
|
switch (rstCategory(mode)) {
|
|
case 'digital': return /^[+-]\d{1,2}$/.test(v);
|
|
case 'cw': return /^\d{3}(\+\d+)?$/.test(v);
|
|
default: return /^\d{2}(\+\d+)?$/.test(v);
|
|
}
|
|
}
|
|
|
|
// convertRST carries a report across the CW/phone divide, keeping the operator's
|
|
// own judgement of the signal: 57 becomes 579, 599 becomes 59.
|
|
//
|
|
// Only between those two — a decibel figure says nothing about readability and
|
|
// an RST says nothing in decibels, so there the preset is the honest answer.
|
|
// Returns '' when it cannot be done.
|
|
export function convertRST(value: string, toMode: string): string {
|
|
const v = (value || '').trim();
|
|
const to = rstCategory(toMode);
|
|
if (to === 'digital') return '';
|
|
const m = /^(\d)(\d)(\d?)((?:\+\d+)?)$/.exec(v);
|
|
if (!m) return '';
|
|
if (to === 'cw') return m[1] + m[2] + '9' + m[4]; // RS → RST, tone 9
|
|
return m[1] + m[2] + m[4]; // RST → RS, tone dropped
|
|
}
|