Three field reports. Icom CI-V address 00 could not be kept: zero was read as "not configured" in all three places that validate it, so every save put the rig back to the IC-7610's 0x98 — and the model dropdown followed, since it is derived from the address rather than stored. Picking "Other (custom address)" also had no effect of its own: the list re-derived itself and snapped back to whatever rig matched. It now stays chosen. Cloudlog/Wavelog showed "17m/17m" on ordinary FT8 contacts (OE6CLD). Every QSO is stamped with a receive side equal to the transmit side, and the uploaded record carried it; Wavelog draws band/band_rx whenever both are there. In ADIF an absent BAND_RX means "same as transmit", so the uploaded record now writes the receive side only when it differs. The copy forwarded to another logger over UDP keeps writing it in full — that is why it was stamped in the first place (Log4OM reads BAND_RX) — through its own ForwardRecordADIF. "S/F" in a spot comment joins superfox / sfox / F-H as FT8.
92 lines
4.6 KiB
TypeScript
92 lines
4.6 KiB
TypeScript
// Shared helpers used by the cluster table and the band map — keeps the
|
|
// mode-inference logic and the status-cache key in one place so both
|
|
// surfaces always read the same data.
|
|
|
|
export function cleanSpotter(s: string): string {
|
|
if (!s) return '';
|
|
const i = s.indexOf('-');
|
|
return i > 0 ? s.slice(0, i) : s;
|
|
}
|
|
|
|
// inferSpotMode picks an ADIF mode for a cluster spot. Comment text is
|
|
// the strongest hint (skimmers say "FT8", "CW 24 WPM", "RTTY"); when it
|
|
// fails we fall back to the IARU R1 band-plan segment for the frequency.
|
|
// Returns '' when nothing matches — caller should leave the rig mode
|
|
// alone instead of guessing wrong.
|
|
export function inferSpotMode(comment: string, freqHz: number): string {
|
|
const c = (comment || '').toUpperCase();
|
|
// SuperFox and Fox/Hound are FT8 — they are WSJT-X's DXpedition transmit modes,
|
|
// not modes of their own, and they turn up in a comment written every way an
|
|
// operator can shorten them: "super fox", SFOX, S/F, F/H.
|
|
//
|
|
// A spot commented "super fox" fell through to the band plan and came out
|
|
// DATA, and that verdict is not cosmetic: the
|
|
// band+mode status is computed from this answer, so a ZD8 on 21.071 read as a
|
|
// new DATA slot rather than the new FT8 one it is.
|
|
if (/\bSUPER\s*FOX\b|\bSFOX\b|\bS\/F\b|\bFOX\s*\/?\s*HOUND\b|\bF\/H\b/.test(c)) return 'FT8';
|
|
if (/\bFT8\b/.test(c)) return 'FT8';
|
|
if (/\bFT4\b/.test(c)) return 'FT4';
|
|
if (/\bJS8\b/.test(c)) return 'JS8';
|
|
if (/\bQ65\b/.test(c)) return 'Q65';
|
|
if (/\bMSK144\b/.test(c)) return 'MSK144';
|
|
if (/\bJT65\b/.test(c)) return 'JT65';
|
|
if (/\bJT9\b/.test(c)) return 'JT9';
|
|
if (/\bRTTY\b/.test(c)) return 'RTTY';
|
|
if (/\bPSK(63|125|250|500)\b/.test(c)) return RegExp.$1 ? `PSK${RegExp.$1}` : 'PSK31';
|
|
if (/\bPSK31?\b/.test(c)) return 'PSK31';
|
|
if (/\bOLIVIA\b/.test(c)) return 'OLIVIA';
|
|
if (/\bMFSK\b/.test(c)) return 'MFSK16';
|
|
if (/\bCW\b/.test(c) || /\bWPM\b/.test(c)) return 'CW';
|
|
if (/\bFM\b/.test(c)) return 'FM';
|
|
if (/\bAM\b/.test(c)) return 'AM';
|
|
if (/\b(SSB|USB|LSB)\b/.test(c)) return 'SSB';
|
|
|
|
const mhz = freqHz / 1_000_000;
|
|
type Seg = [number, number, string];
|
|
const segs: Seg[] = [
|
|
[1.8, 1.838, 'CW'], [1.838, 1.84, 'FT8'], [1.84, 2.0, 'SSB'],
|
|
[3.573, 3.575, 'FT8'], [3.575, 3.578, 'FT4'], [3.5, 3.58, 'CW'], [3.58, 3.6, 'DATA'], [3.6, 4.0, 'SSB'],
|
|
[5.357, 5.36, 'FT8'], [5.3, 5.5, 'SSB'],
|
|
[7.0, 7.04, 'CW'], [7.074, 7.077, 'FT8'], [7.0475, 7.0485, 'FT4'],
|
|
[7.04, 7.1, 'DATA'], [7.1, 7.3, 'SSB'],
|
|
[10.136, 10.139, 'FT8'], [10.14, 10.143, 'FT4'], [10.1, 10.13, 'CW'], [10.13, 10.15, 'DATA'],
|
|
[14.0, 14.07, 'CW'], [14.074, 14.077, 'FT8'], [14.08, 14.0815, 'FT4'],
|
|
[14.07, 14.1, 'DATA'], [14.1, 14.35, 'SSB'],
|
|
[18.1, 18.103, 'FT8'], [18.104, 18.107, 'FT4'], [18.068, 18.095, 'CW'], [18.095, 18.11, 'DATA'], [18.11, 18.168, 'SSB'],
|
|
[21.0, 21.07, 'CW'], [21.074, 21.077, 'FT8'], [21.14, 21.143, 'FT4'],
|
|
[21.07, 21.15, 'DATA'], [21.15, 21.45, 'SSB'],
|
|
[24.915, 24.918, 'FT8'], [24.919, 24.922, 'FT4'], [24.89, 24.915, 'CW'], [24.915, 24.94, 'DATA'], [24.94, 24.99, 'SSB'],
|
|
[28.0, 28.07, 'CW'], [28.074, 28.077, 'FT8'], [28.18, 28.183, 'FT4'],
|
|
[28.07, 28.3, 'DATA'], [28.3, 29.7, 'SSB'],
|
|
[50.0, 50.1, 'CW'], [50.313, 50.316, 'FT8'], [50.318, 50.321, 'FT4'],
|
|
[50.1, 50.5, 'SSB'],
|
|
[144.0, 144.15, 'CW'], [144.174, 144.177, 'FT8'], [144.15, 144.5, 'SSB'],
|
|
];
|
|
for (const [lo, hi, m] of segs) {
|
|
if (mhz >= lo && mhz < hi) return m;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
// spotModeCategory buckets the fine-grained mode from inferSpotMode into
|
|
// the three families the cluster filter exposes: 'SSB' (phone: SSB/FM/AM),
|
|
// 'CW', and 'DATA' (every digital mode). Returns '' when the mode is
|
|
// unknown so callers can decide how to treat un-categorisable spots.
|
|
export function spotModeCategory(mode: string): 'SSB' | 'CW' | 'DATA' | '' {
|
|
const m = (mode || '').toUpperCase();
|
|
if (m === '') return '';
|
|
if (m === 'CW') return 'CW';
|
|
if (m === 'SSB' || m === 'USB' || m === 'LSB' || m === 'FM' || m === 'AM') return 'SSB';
|
|
// Everything else inferSpotMode can return (FT8/FT4/JS8/RTTY/PSK*/…/DATA)
|
|
// is a digital mode.
|
|
return 'DATA';
|
|
}
|
|
|
|
// spotStatusKey is the cache key for ClusterSpotStatuses results. Must be
|
|
// computed identically in the fetcher and every reader — including the
|
|
// band map and the spot table — so a CW spot's status doesn't get looked
|
|
// up under an empty-mode key (which always misses → false "new-slot").
|
|
export function spotStatusKey(call: string, band: string, comment: string, freqHz: number): string {
|
|
return `${call}|${band}|${inferSpotMode(comment, freqHz)}`;
|
|
}
|