diff --git a/docs/radio-tracker.md b/docs/radio-tracker.md index 9ef1865..4bf8a2e 100644 --- a/docs/radio-tracker.md +++ b/docs/radio-tracker.md @@ -24,4 +24,10 @@ Private (owner-only, 404 for everyone else) tracker for Opa Chiel's scanner hobb ## Quiet hours -Reminders whose send time (event minus 10 min) falls in 23:00-07:30 Europe/Amsterdam are skipped. Override with `RADIO_QUIET_HOURS=HH:MM-HH:MM` (or `off`) in `~/.config/radio.env`; the page shows the code default (`QUIET_HOURS` in `src/lib/radio/index.ts`). `bun scripts/radio.ts upcoming` marks skipped reminders as QUIET-SKIP. +Reminders whose send time (event minus 10 min) falls in 00:00-08:00 Europe/Amsterdam are skipped. Override with `RADIO_QUIET_HOURS=HH:MM-HH:MM` (or `off`) in `~/.config/radio.env`; the page shows the code default (`QUIET_HOURS` in `src/lib/radio/index.ts`). `bun scripts/radio.ts upcoming` marks skipped reminders as QUIET-SKIP. + +## Reminders and banks (26 Oct 2026) + +Reminders are on only for live events worth tuning into: ISS passes >= 10 deg and the Haagse ronde. The routine coastguard broadcasts (weather 13:05/19:05, nav warnings 17:30) and the tentative KLM ronde (Sun 12:30, 3785 kHz LSB, from one observation) are off. + +Banks 1-3 were filled by `scripts/radio-banks-1-3.ts` (idempotent; run once in the app container): bank 1 ISS + APRS, bank 2 calling channels + Haagse ronde, bank 3 FM repeaters around Gouda from the RDI list, cross-checked with Wikipedia. diff --git a/scripts/radio-banks-1-3.ts b/scripts/radio-banks-1-3.ts new file mode 100644 index 0000000..cf57764 --- /dev/null +++ b/scripts/radio-banks-1-3.ts @@ -0,0 +1,178 @@ +// One-off data fill for scanner banks 1-3 (ham + ISS). Idempotent: reuses a frequency with the +// same freq_hz + name, and only fills empty channels. +import postgres from 'postgres'; +const c = postgres(process.env.DB_URL!, { prepare: false, onnotice: () => {} }); +const ARISS = 'ARISS https://www.ariss.org/contact-the-iss.html + status page (7 Oct 2026)'; +const RDI = + 'RDI repeater list via elinek.nl, cross-checked with nl.wikipedia Lijst van amateurrelaiszenders'; +type F = { + ch: number; + hz: number; + mode: string; + name: string; + tag: string; + loc?: string; + notes: string; + tags: string[]; + src: string; + existing?: number; +}; +const rows: F[] = [ + { + ch: 1, + hz: 145_800_000, + mode: 'NFM', + name: 'ISS voice downlink (ARISS)', + tag: 'ISS VOICE/SSTV', + notes: '', + tags: [], + src: '', + existing: 330 + }, + { + ch: 2, + hz: 437_800_000, + mode: 'FM', + name: 'ISS FM repeater downlink', + tag: 'ISS RPT DN', + notes: + 'Cross-band voice repeater (NA1SS): uplink 145.990 MHz CTCSS 67 Hz, downlink 437.800. Allow ±10 kHz Doppler.', + tags: ['iss', 'satellite', '70cm'], + src: ARISS + }, + { + ch: 3, + hz: 437_825_000, + mode: 'FM', + name: 'ISS APRS digipeater (UHF, active)', + tag: 'ISS APRS UHF', + notes: + 'APRS 1200 bd packet; ARISS status says APRS is currently active here (RS0ISS). Decode with software; on the scanner you hear bursts.', + tags: ['iss', 'satellite', 'aprs', '70cm'], + src: ARISS + }, + { + ch: 4, + hz: 145_825_000, + mode: 'FM', + name: 'ISS APRS digipeater (VHF)', + tag: 'ISS APRS VHF', + notes: 'Usual worldwide ISS packet frequency (1200 bd APRS); currently moved to 437.825.', + tags: ['iss', 'satellite', 'aprs', '2m'], + src: ARISS + }, + { + ch: 5, + hz: 437_550_000, + mode: 'FM', + name: 'ISS SSTV downlink (events)', + tag: 'ISS SSTV UHF', + notes: 'SSTV events (Robot36), announced by ARISS. Decode with an SSTV app.', + tags: ['iss', 'satellite', 'sstv', '70cm'], + src: ARISS + }, + { + ch: 6, + hz: 144_800_000, + mode: 'FM', + name: 'APRS (Europe, terrestrial)', + tag: 'APRS EU', + notes: 'IARU Region 1 APRS frequency, 1200 bd packet.', + tags: ['aprs', '2m'], + src: 'IARU Region 1 VHF band plan' + }, + { + ch: 51, + hz: 145_500_000, + mode: 'FM', + name: '2 m FM calling', + tag: '2M CALL', + notes: 'FM simplex calling channel (V40).', + tags: ['calling', '2m'], + src: 'IARU Region 1 VHF band plan' + }, + { + ch: 52, + hz: 433_500_000, + mode: 'FM', + name: '70 cm FM calling', + tag: '70CM CALL', + notes: 'FM simplex calling channel (U280).', + tags: ['calling', '70cm'], + src: 'IARU Region 1 UHF band plan' + }, + { + ch: 53, + hz: 145_450_000, + mode: 'NFM', + name: 'Haagse ronde', + tag: 'HAAGSE RONDE', + notes: '', + tags: [], + src: '', + existing: 328 + }, + ...( + [ + [ + 101, + 430_225_000, + 'PI2SWK', + 'Gouda', + '431.825', + '88.5', + 'Also pi2swk.nl (Goudse ronde, EchoLink 669316).' + ], + [102, 145_750_000, 'PI3RAZ', 'Zoetermeer', '145.150', '88.5', ''], + [103, 145_675_000, 'PI3RTD', 'Rotterdam', '145.075', '88.5', ''], + [104, 430_300_000, 'PI2RTD', 'Rotterdam', '431.900', '88.5', ''], + [105, 430_087_500, 'PI1DFT', 'Delft', '431.6875', '88.5', 'Same club as WebSDR PI1DFT.'], + [106, 430_350_000, 'PI2HGL', 'Den Haag', '431.950', '88.5', ''], + [107, 430_050_000, 'PI2LDN', 'Leiden', '431.650', '88.5', ''], + [ + 108, + 145_575_000, + 'PI3UTR', + 'IJsselstein', + '144.975', + '77', + 'Regional, wide coverage (Utrecht/Hilversum TX).' + ] + ] as const + ).map(([ch, hz, call, loc, inp, tone, extra]) => ({ + ch, + hz, + mode: 'FM', + name: `${call} repeater ${loc}`, + tag: `${call} ${loc}`.slice(0, 16), + loc, + notes: `FM repeater output; input ${inp} MHz, CTCSS ${tone} Hz. ${extra}`.trim(), + tags: ['repeater', hz < 200_000_000 ? '2m' : '70cm'], + src: RDI + })) +]; +let added = 0, + set = 0; +for (const r of rows) { + let id = r.existing; + if (!id) { + const [f] = + await c`SELECT id FROM radio.frequencies WHERE freq_hz = ${r.hz} AND name = ${r.name}`; + id = f?.id; + if (!id) { + [{ id }] = + await c`INSERT INTO radio.frequencies ${c({ freq_hz: r.hz, mode: r.mode, band: 'ham', name: r.name, location: r.loc ?? null, notes: r.notes, tags: r.tags, source: r.src })} RETURNING id`; + added++; + } + } + const ins = + await c`INSERT INTO radio.channels ${c({ channel: r.ch, frequency_id: id, alpha_tag: r.tag.slice(0, 16) })} ON CONFLICT (channel) DO NOTHING`; + set += ins.count; +} +console.log(JSON.stringify({ frequenciesAdded: added, channelsSet: set })); +console.log( + JSON.stringify( + await c`SELECT (SELECT count(*)::int FROM radio.frequencies) freqs, (SELECT count(*)::int FROM radio.channels) chans, (SELECT count(*)::int FROM radio.channels WHERE channel <= 150) b123` + ) +); +await c.end(); diff --git a/scripts/radio.ts b/scripts/radio.ts index 44aecc2..d3898be 100644 --- a/scripts/radio.ts +++ b/scripts/radio.ts @@ -9,7 +9,7 @@ * * Reminders go to RADIO_NTFY_URL (an ntfy topic URL, optional RADIO_NTFY_TOKEN), passed in with * `docker exec --env-file ~/.config/radio.env`. radio.reminders makes every reminder fire once. - * RADIO_QUIET_HOURS (default 23:00-07:30, "off" to disable) skips reminders sent in that window. + * RADIO_QUIET_HOURS (default 00:00-08:00, "off" to disable) skips reminders sent in that window. */ import postgres from 'postgres'; import * as sat from 'satellite.js'; diff --git a/src/lib/radio/index.test.ts b/src/lib/radio/index.test.ts index 269cda2..9021de5 100644 --- a/src/lib/radio/index.test.ts +++ b/src/lib/radio/index.test.ts @@ -89,15 +89,19 @@ describe('scanner', () => { }); describe('quiet hours', () => { - const q = parseQuiet('23:00-07:30'); - it('parses and wraps midnight (Amsterdam time)', () => { - expect(q?.label).toBe('23:00–07:30'); + const q = parseQuiet('00:00-08:00'); + it('parses and checks Amsterdam wall time', () => { + expect(q?.label).toBe('00:00–08:00'); expect(parseQuiet('off')).toBeNull(); expect(inQuiet(fromZoned(2026, 10, 27, 2, 11), q)).toBe(true); expect(inQuiet(fromZoned(2026, 10, 27, 7, 1), q)).toBe(true); - expect(inQuiet(fromZoned(2026, 10, 27, 7, 30), q)).toBe(false); - expect(inQuiet(fromZoned(2026, 10, 26, 18, 55), q)).toBe(false); - expect(inQuiet(fromZoned(2026, 10, 26, 23, 0), q)).toBe(true); + expect(inQuiet(fromZoned(2026, 10, 27, 8, 0), q)).toBe(false); + expect(inQuiet(fromZoned(2026, 10, 26, 23, 59), q)).toBe(false); expect(inQuiet(fromZoned(2026, 10, 26, 18, 55), null)).toBe(false); }); + it('wraps midnight', () => { + const w = parseQuiet('23:00-07:30'); + expect(inQuiet(fromZoned(2026, 10, 26, 23, 0), w)).toBe(true); + expect(inQuiet(fromZoned(2026, 10, 27, 7, 30), w)).toBe(false); + }); }); diff --git a/src/lib/radio/index.ts b/src/lib/radio/index.ts index 8f9e5e5..cfe61c3 100644 --- a/src/lib/radio/index.ts +++ b/src/lib/radio/index.ts @@ -41,9 +41,9 @@ export const ISS_MIN_EL = 10; export const REMIND_MINUTES = 10; export const ISS_DOWNLINK_HZ = 145_800_000; /** No reminders are sent when the send time falls in this Amsterdam window (RADIO_QUIET_HOURS overrides). */ -export const QUIET_HOURS = '23:00-07:30'; +export const QUIET_HOURS = '00:00-08:00'; export type Quiet = { from: number; to: number; label: string } | null; -/** "23:00-07:30" -> minutes of day; "off" / "" -> null (no quiet hours). */ +/** "00:00-08:00" -> minutes of day; "off" / "" -> null (no quiet hours). */ export function parseQuiet(spec: string | null | undefined): Quiet { const m = /^\s*(\d{1,2}):(\d{2})\s*[-–]\s*(\d{1,2}):(\d{2})\s*$/.exec(spec ?? ''); if (!m) return null;