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
All checks were successful
Deploy production / deploy (push) Successful in 1m20s
This commit is contained in:
@@ -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
178
scripts/radio-banks-1-3.ts
Normal 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();
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user