Files
gods-eye-view/src/ui/localSdrControls.test.mjs
T
1fc795577a 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]>
2026-09-23 16:00:53 -05:00

208 lines
5.6 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { LocalSdrControls } from './localSdrControls.js';
function stubDocument() {
const elements = new Map();
const doc = {
activeElement: null,
getElementById(id) {
if (!elements.has(id)) {
const listeners = new Map();
elements.set(id, {
id,
value: '',
hidden: false,
disabled: false,
textContent: '',
ownerDocument: doc,
classList: { toggle() {} },
setAttribute() {},
closest: () => null,
addEventListener(type, handler) {
listeners.set(type, handler);
},
fire: (type, event = {}) => listeners.get(type)?.(event),
});
}
return elements.get(id);
},
};
return doc;
}
function stubReceiver() {
const listeners = new Set();
const calls = [];
let state = {
webUsbSupported: true,
connected: false,
status: 'idle',
message: 'Connect an RTL-SDR to begin.',
mode: 'fm',
frequencyHz: 98_500_000,
volume: 0.8,
gain: 'auto',
locationStatus: 'unknown',
};
return {
calls,
getState: () => ({ ...state }),
subscribe(listener) {
listeners.add(listener);
listener({ ...state });
return () => listeners.delete(listener);
},
emit(patch) {
state = { ...state, ...patch };
for (const listener of listeners) listener({ ...state });
},
async stop(options) {
calls.push(['stop', options]);
this.emit({ connected: false, status: 'idle' });
return true;
},
async setMode(mode) {
calls.push(['setMode', mode]);
this.emit({ mode });
return true;
},
async setGain(value) {
calls.push(['setGain', value]);
return value;
},
async tuneFm(hz) {
calls.push(['tuneFm', hz]);
return true;
},
};
}
function stubRadio() {
const listeners = new Set();
const calls = [];
return {
calls,
subscribe(listener) {
listeners.add(listener);
listener({ audioState: 'idle' });
return () => listeners.delete(listener);
},
emit(state) {
for (const listener of listeners) listener(state);
},
stopPlayback(options) {
calls.push(options);
this.emit({ audioState: 'idle' });
},
};
}
test('starting internet radio stops local FM, and local FM stops internet radio', async (t) => {
const receiver = stubReceiver();
const radio = stubRadio();
const controls = new LocalSdrControls({
document: stubDocument(),
receiver,
radio,
});
t.after(() => controls.destroy());
receiver.emit({ connected: true, status: 'streaming', mode: 'fm' });
assert.equal(radio.calls.length, 0, 'nothing to stop while radio is silent');
radio.emit({ audioState: 'loading' });
assert.deepEqual(receiver.calls.at(-1), [
'stop',
{ message: 'Local FM stopped: internet radio started' },
]);
radio.emit({ audioState: 'playing' });
receiver.emit({ connected: true, status: 'streaming', mode: 'fm' });
assert.deepEqual(radio.calls, [{ origin: 'user' }]);
});
test('ADS-B reception and internet radio play together', async (t) => {
const receiver = stubReceiver();
const radio = stubRadio();
const controls = new LocalSdrControls({
document: stubDocument(),
receiver,
radio,
});
t.after(() => controls.destroy());
receiver.emit({ connected: true, status: 'streaming', mode: 'adsb' });
radio.emit({ audioState: 'playing' });
assert.equal(receiver.calls.length, 0);
assert.equal(radio.calls.length, 0);
});
test('card controls drive gain, FM mode and the Local ADS-B layer', async (t) => {
const doc = stubDocument();
const receiver = stubReceiver();
const layerCalls = [];
const controls = new LocalSdrControls({
document: doc,
receiver,
actions: {
isLocalAdsbEnabled: () => true,
setLocalAdsbEnabled: async (enabled) => layerCalls.push(enabled),
},
});
t.after(() => controls.destroy());
const gain = doc.getElementById('sdr-gain-select');
gain.value = '20.7';
gain.fire('change');
assert.deepEqual(receiver.calls.at(-1), ['setGain', '20.7']);
receiver.emit({ connected: true, status: 'streaming', mode: 'adsb' });
await doc.getElementById('sdr-mode-fm-btn').fire('click');
assert.deepEqual(layerCalls, [false]);
assert.deepEqual(receiver.calls.at(-1), ['setMode', 'fm']);
await doc.getElementById('sdr-mode-adsb-btn').fire('click');
assert.deepEqual(layerCalls, [false, true]);
assert.equal(controls.isActive(), true);
});
test('the card shows one read-only decoder-feed line after a single probe', async (t) => {
const doc = stubDocument();
const listeners = new Set();
let probes = 0;
let state = { configured: null, polling: false, feeds: [] };
const feeds = {
getState: () => state,
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
async probe() {
probes += 1;
},
set(patch) {
state = { ...state, ...patch };
for (const listener of listeners) listener(state);
},
};
const controls = new LocalSdrControls({
document: doc,
receiver: stubReceiver(),
feeds,
});
t.after(() => controls.destroy());
const line = doc.getElementById('sdr-feed-status');
assert.equal(probes, 1);
assert.equal(line.hidden, true);
feeds.set({
configured: true,
polling: true,
feeds: [
{ band: '1090', status: 'live' },
{ band: '978', status: 'live' },
],
});
await new Promise((resolve) => setTimeout(resolve, 30));
assert.equal(line.hidden, false);
assert.equal(line.textContent, 'Decoder feeds: 1090 live · 978 live');
});