feat(sdr): local receivers — browser RTL-SDR, Local ADS-B layer and decoder feeds (#732)

Adds a Local ADS-B layer fed by your own receivers: a browser WebUSB
RTL-SDR (with FM listening) and local 1090/978 MHz decoder feeds
(LOCAL_RECEIVER_FEEDS, local and private addresses only). Local aircraft
draw in magenta with class, silhouettes, 3D models, trails and a position
sanity filter; the click card names the band and input that heard them.

Builds on Sameh Khamis's SDR prototype and the initial GEV port by
Mazeyar Moeini Feizabadi (#134); the decoder-feed design follows the
notes on #57. Closes #57.

Co-authored-by: Sameh Khamis <[email protected]>
Co-authored-by: Mazeyar Moeini Feizabadi <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-09-23 16:00:53 -05:00
1 parent 143cffd7ca
commit 1fc795577a
78 files changed
+12267 -108

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { expandApplicationHtml } from '../../build/application-html.js';
import { createBrowserViteConfig } from '../../build/vite.js';
import { R820T_GAIN_STEPS_DB } from '../sdr/gain.js';
const html = expandApplicationHtml(
readFileSync(new URL('../../index.html', import.meta.url), 'utf8'),
);
const css = readFileSync(
new URL('./styles/radio.css', import.meta.url),
'utf8',
);
test('the Local RTL-SDR card lives inside the Radio panel with every control', () => {
const radioStart = html.indexOf('id="radio-panel"');
const cardStart = html.indexOf('id="sdr-radio-card"');
const radioEnd = html.indexOf('</section>', radioStart);
assert.ok(radioStart >= 0 && cardStart > radioStart && cardStart < radioEnd);
for (const id of [
'sdr-connection-state',
'sdr-connect-btn',
'sdr-locate-btn',
'sdr-change-device-btn',
'sdr-mode-fm-btn',
'sdr-mode-adsb-btn',
'sdr-gain-select',
'sdr-adsb-stats',
'sdr-stat-rate',
'sdr-stat-heard',
'sdr-stat-positioned',
'sdr-stat-iq',
'sdr-frequency-input',
'sdr-tune-btn',
'sdr-seek-back-btn',
'sdr-seek-forward-btn',
'sdr-volume',
'sdr-status',
'sdr-feed-status',
]) {
assert.match(html, new RegExp(`id="${id}"`), `${id} is missing`);
}
assert.match(html, /id="sdr-status"[^>]*>Connect an RTL-SDR to begin\.</);
assert.match(css, /\.sdr-radio-card\s*\{/);
});
test('the gain control offers AUTO plus every R820T step', () => {
const start = html.indexOf('id="sdr-gain-select"');
const select = html.slice(start, html.indexOf('</select>', start));
const values = [...select.matchAll(/<option value="([^"]+)"/g)].map(
(match) => match[1],
);
assert.deepEqual(values, [
'auto',
...R820T_GAIN_STEPS_DB.map((step) => step.toFixed(1)),
]);
});
test('worker-reached SDR dependencies are pre-bundled and builds keep a separate cache', () => {
const serve = createBrowserViteConfig();
for (const dependency of [
'@jtarrio/signals/demod/demodulator.js',
'@jtarrio/signals/demod/modes.js',
'@jtarrio/webrtlsdr/rtlsdr.js',
]) {
assert.ok(serve.optimizeDeps.include.includes(dependency), dependency);
}
assert.equal(serve.cacheDir, undefined);
assert.equal(
createBrowserViteConfig({ command: 'build' }).cacheDir,
'node_modules/.vite-build',
);
});