radio: quiet hours default 00:00-08:00; banks 1-3 fill script (ISS, calling, Gouda-area repeaters); docs
All checks were successful
Deploy production / deploy (push) Successful in 1m20s

This commit is contained in:
2026-10-26 17:24:03 +00:00
parent f954881bcc
commit 7ba26f132d
5 changed files with 198 additions and 10 deletions

View File

@@ -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.

178
scripts/radio-banks-1-3.ts Normal file
View File

@@ -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();

View File

@@ -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';

View File

@@ -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);
});
});

View File

@@ -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;