fix(layers): keep the Local ADS-B receiver error in English

The Local ADS-B layer copied the SDR controller's `message`, which is
Radio card text in the UI language, into its `error` stat. The controller
now publishes an English `errorText` next to the translated `message` for
every receiver error, and the layer reports that. A receiver is any
object with getState(); one that does not publish errorText still shows
its error from `message`, as before. The Radio card text is unchanged,
and the Layers panel still shows the error translated.
This commit is contained in:
daikaginza committed 2026-10-18 13:57:48 +07:00
1 parent 4d2a71c1b2
commit 3d64a54a21
4 files changed
+75 -38

No files matched your search

+5 -3
View File
@@ -66,10 +66,12 @@ const DEFAULT_DISPLAY = Object.freeze({
* Local ADS-B layer: draws aircraft heard by the user's own receivers. * Local ADS-B layer: draws aircraft heard by the user's own receivers.
* *
* Two inputs feed it. The receiver is any object with `getState()` returning * Two inputs feed it. The receiver is any object with `getState()` returning
* `{ connected, status, message, mode, webUsbSupported, aircraft, messagesPerSecond }`, * `{ connected, status, message, errorText, mode, webUsbSupported, aircraft, messagesPerSecond }`,
* `subscribe(listener)` and `setMode(mode)`; `aircraft` holds records from * `subscribe(listener)` and `setMode(mode)`; `aircraft` holds records from
* `src/sources/adsbRecords.js`. The browser WebUSB session is one such * `src/sources/adsbRecords.js`, and the optional `errorText` is the English
* receiver. The optional `feeds` session (`./feeds.js`) supplies records read * text of an `error` status (`message`, the UI-language one, stands in for a
* receiver without it). The browser WebUSB session is one such receiver.
* The optional `feeds` session (`./feeds.js`) supplies records read
* by the server from local 1090/978 MHz decoders; it polls only while this * by the server from local 1090/978 MHz decoders; it polls only while this
* layer is enabled. Records are merged by ICAO (`mergeLocalAdsbRecords`). * layer is enabled. Records are merged by ICAO (`mergeLocalAdsbRecords`).
* Aircraft heard only on 978 MHz UAT carry a thin ring around the marker. * Aircraft heard only on 978 MHz UAT carry a thin ring around the marker.
+1 -1
View File
@@ -51,7 +51,7 @@ export function feedsConfigured(feedState) {
function webUsbStatus(receiver, heard, feedState) { function webUsbStatus(receiver, heard, feedState) {
if (receiver.status === 'error') if (receiver.status === 'error')
return { status: 'error', error: receiver.message }; return { status: 'error', error: receiver.errorText ?? receiver.message };
if (receiver.status === 'connecting' || receiver.status === 'tuning') if (receiver.status === 'connecting' || receiver.status === 'tuning')
return { loading: true, loadingLabel: msg('opening receiver') }; return { loading: true, loadingLabel: msg('opening receiver') };
if ( if (
+40 -1
View File
@@ -2,7 +2,8 @@
// "USB error", feed problems, even as the whole error) read in the UI language. // "USB error", feed problems, even as the whole error) read in the UI language.
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { setLocale } from '../../i18n/index.js'; import { setLocale, t } from '../../i18n/index.js';
import { SdrController } from '../../sdr/controller.js';
import { LayerPanel } from '../../ui/layerPanel.js'; import { LayerPanel } from '../../ui/layerPanel.js';
import { localAdsbStatus } from './status.js'; import { localAdsbStatus } from './status.js';
@@ -66,3 +67,41 @@ for (const locale of ['es', 'ru']) {
}); });
} }
} }
// A receiver error is English data for the layer (stats, and what the voice
// tools and the analyst read); the Radio card and the row show it translated.
for (const locale of ['es', 'ru']) {
test(`${locale}: a WebUSB receiver error stays English in stats and reads translated`, async () => {
const receiver = new SdrController({
storage: null,
webUsb: {},
providerFactory: () => ({
get: async () => {
throw new Error('No device was selected.');
},
}),
});
setLocale(locale);
try {
assert.equal(await receiver.connect('adsb'), false);
const state = receiver.getState();
assert.equal(state.status, 'error');
assert.equal(state.errorText, 'No RTL-SDR device selected');
assert.equal(state.message, t('No RTL-SDR device selected'));
assert.notEqual(state.message, state.errorText);
const status = localAdsbStatus({
receiver: state,
feedState: null,
heard: 0,
});
assert.equal(status.error, 'No RTL-SDR device selected');
const row = rowFor(status, 0);
assert.ok(row.includes(state.message), row);
assert.doesNotMatch(row, /No RTL-SDR device selected/, row);
} finally {
setLocale('en');
await receiver.destroy();
}
});
}
+29 -33
View File
@@ -283,22 +283,32 @@ function clampFmFrequency(value) {
); );
} }
/** A failed connection in English; errorState() shows it translated. */
function describeConnectionError(error) { function describeConnectionError(error) {
const message = String(error?.message || error || '').trim(); const message = String(error?.message || error || '').trim();
if (/No device was selected|NotFoundError/i.test(message)) if (/No device was selected|NotFoundError/i.test(message))
return t('No RTL-SDR device selected'); return msg('No RTL-SDR device selected');
if ( if (
/does not support the WebUSB|usb/i.test(message) && /does not support the WebUSB|usb/i.test(message) &&
/support/i.test(message) /support/i.test(message)
) { ) {
return t('WebUSB requires desktop Chrome or Edge on localhost/HTTPS'); return msg('WebUSB requires desktop Chrome or Edge on localhost/HTTPS');
} }
if (/access|permission|claim|busy/i.test(message)) { if (/access|permission|claim|busy/i.test(message)) {
return t( return msg(
'RTL-SDR access failed; close other SDR apps and check USB permissions', 'RTL-SDR access failed; close other SDR apps and check USB permissions',
); );
} }
return message ? translateKnown(message) : t('RTL-SDR connection failed'); return message || msg('RTL-SDR connection failed');
}
/**
* State fields for a receiver error: `message` in the UI language for the
* Radio card, `errorText` as English data for the Local ADS-B layer's stats.
* @param {string} text English error text (ours or the browser's).
*/
function errorState(text) {
return { status: 'error', message: translateKnown(text), errorText: text };
} }
function streamingMessage(mode) { function streamingMessage(mode) {
@@ -555,10 +565,7 @@ export class SdrController {
// A failed worker (e.g. its module did not load) never recovers: drop // A failed worker (e.g. its module did not load) never recovers: drop
// it so the next session builds and configures a fresh one. // it so the next session builds and configures a fresh one.
this._discardWorker(); this._discardWorker();
this._setState({ this._setState(errorState(msg('SDR decoder worker failed')));
status: 'error',
message: t('SDR decoder worker failed'),
});
if (this._device) void this.stop({ preserveMessage: true }); if (this._device) void this.stop({ preserveMessage: true });
}; };
return worker; return worker;
@@ -779,16 +786,16 @@ export class SdrController {
// Published before stopping; stop() preserves it, and nothing is set // Published before stopping; stop() preserves it, and nothing is set
// afterwards, so a reconnect started during teardown keeps its own state. // afterwards, so a reconnect started during teardown keeps its own state.
this._setState({ this._setState({
status: 'error', ...errorState(
reason === 'startup'
? msg('SDR decoder did not start; receiver stopped')
: reason === 'stalled'
? msg('SDR decoder stopped responding; receiver stopped')
: msg('SDR decoder fell behind; receiver stopped'),
),
connected: false, connected: false,
decoderOverload: reason, decoderOverload: reason,
decoderOverloadSnapshot: snapshot, decoderOverloadSnapshot: snapshot,
message:
reason === 'startup'
? t('SDR decoder did not start; receiver stopped')
: reason === 'stalled'
? t('SDR decoder stopped responding; receiver stopped')
: t('SDR decoder fell behind; receiver stopped'),
}); });
void this.stop({ preserveMessage: true }); void this.stop({ preserveMessage: true });
} }
@@ -1085,7 +1092,7 @@ export class SdrController {
// consumes the click activation. // consumes the click activation.
if (nextMode === 'fm' && !(await this._ensureAudio())) { if (nextMode === 'fm' && !(await this._ensureAudio())) {
throw new Error( throw new Error(
t('Browser audio is suspended; press Connect again to resume it'), msg('Browser audio is suspended; press Connect again to resume it'),
); );
} }
if (superseded()) return false; if (superseded()) return false;
@@ -1153,8 +1160,7 @@ export class SdrController {
if (superseded()) return false; if (superseded()) return false;
this._setState({ this._setState({
connected: false, connected: false,
status: 'error', ...errorState(describeConnectionError(error)),
message: describeConnectionError(error),
}); });
return false; return false;
} }
@@ -1507,8 +1513,7 @@ export class SdrController {
console.warn('[SDR] Sample read failed:', error); console.warn('[SDR] Sample read failed:', error);
this._setState({ this._setState({
connected: false, connected: false,
status: 'error', ...errorState(msg('RTL-SDR sample stream stopped')),
message: t('RTL-SDR sample stream stopped'),
}); });
await this.stop({ preserveMessage: true }); await this.stop({ preserveMessage: true });
break; break;
@@ -1553,7 +1558,7 @@ export class SdrController {
try { try {
if (nextMode === 'fm' && !(await this._ensureAudio())) { if (nextMode === 'fm' && !(await this._ensureAudio())) {
throw new Error( throw new Error(
t('Browser audio is suspended; select FM again to resume it'), msg('Browser audio is suspended; select FM again to resume it'),
); );
} }
// A stall teardown, stop() or reconnect during the switch supersedes // A stall teardown, stop() or reconnect during the switch supersedes
@@ -1578,10 +1583,7 @@ export class SdrController {
} catch (error) { } catch (error) {
if (!this._owns(token, device)) return false; if (!this._owns(token, device)) return false;
console.warn('[SDR] Mode switch failed:', error); console.warn('[SDR] Mode switch failed:', error);
this._setState({ this._setState(errorState(describeConnectionError(error)));
status: 'error',
message: describeConnectionError(error),
});
return false; return false;
} }
} }
@@ -1622,10 +1624,7 @@ export class SdrController {
if (targetMode === 'adsb') this._setState(this._resetAdsbCounters()); if (targetMode === 'adsb') this._setState(this._resetAdsbCounters());
} catch (error) { } catch (error) {
console.warn('[SDR] Gain change failed:', error); console.warn('[SDR] Gain change failed:', error);
this._setState({ this._setState(errorState(msg('RTL-SDR gain change failed')));
status: 'error',
message: t('RTL-SDR gain change failed'),
});
return null; return null;
} }
return setting; return setting;
@@ -1679,10 +1678,7 @@ export class SdrController {
}); });
return true; return true;
} catch (error) { } catch (error) {
this._setState({ this._setState(errorState(describeConnectionError(error)));
status: 'error',
message: describeConnectionError(error),
});
return false; return false;
} }
} }