@websdr/frontend-core
v0.5.3
Published
This is the core frontend package for WebSDR
Downloads
172
Maintainers
Readme
@websdr/frontend-core
Frontend-focused TypeScript core for the WebSDR ecosystem.
This package provides:
- Small frontend common helpers (debug flag, NNG-over-WebSocket client, WASM errno enum).
- Minimal HTTP API helpers for browser apps.
- A WebUSB control + streaming layer used by WebSDR-compatible devices.
Install
npm install @websdr/frontend-corepnpm add @websdr/frontend-coreyarn add @websdr/frontend-coreImporting
This package is published as ESM (see type: module).
Import from the root:
import { apiFetch, setApiBase, ensureWebUsb } from '@websdr/frontend-core';Or use subpath exports:
import { debug_mode, NngWebSocket, Protocol } from '@websdr/frontend-core/common';
import { apiFetch, setApiBase } from '@websdr/frontend-core/services';
import { ensureWebUsb, WebUsbManagerMode, getWebUsbManagerInstance } from '@websdr/frontend-core/webusb';Examples
API helpers
apiFetch() builds a URL from the configured base, includes cookies (credentials: 'include'), and throws on non-OK responses.
import { setApiBase, apiFetch } from '@websdr/frontend-core/services';
setApiBase('http://localhost:3000');
type Profile = { id: string; username: string };
const profile = await apiFetch<Profile>('/api/auth/profile');You can also set the API base via a global variable (useful for index.html deployments):
import { apiFetch } from '@websdr/frontend-core/services';
(globalThis as any).__API_BASE__ = 'http://localhost:3000';
const profile = await apiFetch('/api/auth/profile');Error handling (JSON errors are provided via Error.cause):
import { apiFetch } from '@websdr/frontend-core/services';
try {
await apiFetch('/api/auth/profile');
} catch (e) {
const err = e as any;
console.error(err.message);
if (err.cause) console.error('cause:', err.cause);
}NNG-over-WebSocket (REQ/SUB)
import { NngWebSocket, Protocol } from '@websdr/frontend-core/common';
const ws = new NngWebSocket({
url: 'ws://localhost:8000/ws',
protocol: Protocol.SUB,
});
await ws.open();
ws.addEventListener('message', (ev) => {
// handle events emitted by the class
});REQ example (request/response). The send() promise resolves when a reply with the same request id arrives:
import { NngWebSocket, Protocol } from '@websdr/frontend-core/common';
const ws = new NngWebSocket({
url: 'ws://localhost:8000/rpc',
protocol: Protocol.REQ,
binaryType: NngWebSocket.TEXT,
});
await ws.open();
const reply = await ws.send('ping', 1000);
console.log('reply:', reply);WebUSB: ensure implementation + request a device
In browsers, navigator.usb exists when WebUSB is supported. In Node.js, you can use a polyfill implementation.
ensureWebUsb() attempts to provide navigator.usb (prefers the usb package, falls back to webusb).
import {
ensureWebUsb,
WebUsbManagerMode,
getWebUsbManagerInstance,
} from '@websdr/frontend-core/webusb';
await ensureWebUsb();
const mgr = getWebUsbManagerInstance(WebUsbManagerMode.SINGLE);
const picked = await mgr.requestDevice(); // requires user gesture in browsers
if (!picked) throw new Error('No device selected');
const fd = await mgr.open(picked.vendorId, picked.productId, picked.device);
const name = await mgr.getName(fd);
const serial = await mgr.getSerialNumber(fd);
console.log({ name, serial });
await mgr.close(fd);WebUSB: control + streaming via ControlWebUsb
ControlWebUsb is a high-level helper built on top of WebUsbManager. It prepares structured control commands (connect/discover/params/stream control).
import { CHUNK_SIZE, DataType } from '@websdr/core/common';
import {
ensureWebUsb,
WebUsbManagerMode,
getWebUsbManagerInstance,
ControlWebUsb,
WebUsbChannels,
WebUsbDirection,
} from '@websdr/frontend-core/webusb';
const mode = WebUsbManagerMode.SINGLE;
await ensureWebUsb();
const mgr = getWebUsbManagerInstance(mode);
const picked = await mgr.requestDevice(); // requires user gesture
if (!picked) throw new Error('No device selected');
const fd = await mgr.open(picked.vendorId, picked.productId, picked.device);
if (fd < 0) throw new Error('Failed to open device');
const control = new ControlWebUsb({ mode });
await control.open(fd);
await control.sendCommand('CONNECT');
const discovered = await control.sendCommand('DISCOVER');
console.log('discover:', discovered);
const info = await control.getDeviceInfo(false);
console.log('device:', info);
await control.sendCommand('SET_RX_FREQUENCY', { chans: WebUsbChannels.CHAN1, frequency: 100e6 });
await control.sendCommand('SET_RX_GAIN', { chans: WebUsbChannels.CHAN1, gain: 15 });
// Prepare streaming (device-specific firmware decides how these map to actual stream state)
await control.sendCommand('START_STREAMING', {
chans: WebUsbChannels.CHAN1,
samplerate: 1e6,
packetsize: CHUNK_SIZE,
mode: WebUsbDirection.RX_TX,
dataformat: DataType.ci16,
});
console.log('stream status:', await control.getStreamStatus());
await control.sendCommand('STOP_STREAMING');
await control.sendCommand('DISCONNECT');
await control.close();
await mgr.close(fd);WebUSB: receive RX packets via WebUsbManager
submitRxPacket() requests one RX packet worth of IQ samples and returns a decoded RXBuffer.
import { DataType } from '@websdr/core/common';
import {
ensureWebUsb,
WebUsbManagerMode,
getWebUsbManagerInstance,
} from '@websdr/frontend-core/webusb';
await ensureWebUsb();
const mgr = getWebUsbManagerInstance(WebUsbManagerMode.SINGLE);
const picked = await mgr.requestDevice();
if (!picked) throw new Error('No device selected');
const fd = await mgr.open(picked.vendorId, picked.productId, picked.device);
if (fd < 0) throw new Error('Failed to open device');
// Drivers may adjust your requested sample count (alignment, framing, etc.)
const cfg = await mgr.getConfiguration(fd);
const samples = await mgr.getRXSamplesCount(fd, cfg.defaultSamplesCount);
const rx = await mgr.submitRxPacket(fd, samples, {
datatype: DataType.ci16,
extra_meta: true,
id: 1,
});
if (rx.datatype === DataType.ci16) {
// Complex int16 IQ is typically interleaved: I0,Q0,I1,Q1,...
const iq = new Int16Array(rx.data);
const i0 = iq[0];
const q0 = iq[1];
console.log({ i0, q0, samples: rx.samples, ts: rx.timestamp });
} else if (rx.datatype === DataType.cf32) {
const iq = new Float32Array(rx.data);
const i0 = iq[0];
const q0 = iq[1];
console.log({ i0, q0, samples: rx.samples, ts: rx.timestamp });
}
await mgr.close(fd);WebUSB: transmit TX packets via WebUsbManager
sendTxPacket() encodes and sends an IQ buffer to the device. In many device firmwares TX requires the stream to be started first (for example via ControlWebUsb commands).
import { DataType } from '@websdr/core/common';
import {
ensureWebUsb,
WebUsbManagerMode,
getWebUsbManagerInstance,
} from '@websdr/frontend-core/webusb';
await ensureWebUsb();
const mgr = getWebUsbManagerInstance(WebUsbManagerMode.SINGLE);
const picked = await mgr.requestDevice();
if (!picked) throw new Error('No device selected');
const fd = await mgr.open(picked.vendorId, picked.productId, picked.device);
if (fd < 0) throw new Error('Failed to open device');
// A tiny dummy complex waveform (I/Q int16). Fill with real signal in your app.
const iq = new Int16Array(2 * 1024);
iq[0] = 0x1000;
iq[1] = 0;
const tx = await mgr.sendTxPacket(
fd,
{
data: iq.buffer,
byteOffset: iq.byteOffset,
byteLength: iq.byteLength,
datatype: DataType.ci16,
discard_timestamp: true,
timestamp: 0n,
},
{ allowDrop: false }
);
console.log('tx status:', tx.usbOutTransferResult?.status);
await mgr.close(fd);Using WebSDR with Vite (WASM Configuration)
@websdr/frontend-core includes an Emscripten-based WebAssembly module (control.wasm) that is loaded at runtime next to its corresponding JavaScript file.
When using Vite, additional configuration is required in development mode.
Why this is necessary
In dev mode, Vite performs dependency pre-bundling (optimizeDeps) and moves JavaScript dependencies into:
However, the WebAssembly file (control.wasm) is loaded dynamically by the Emscripten runtime and is not automatically copied into .vite/deps/.
As a result, the browser may attempt to load:
This file does not exist, leading to: Error requesting USB device: RuntimeError: Aborted(CompileError: WebAssembly.instantiate(): expected magic word 00 61 73 6d, found 3c 21 44 4f @+0)
Recommended Vite configuration
To prevent Vite from pre-bundling @websdr/frontend-core, add the following to your vite.config.ts:
import { defineConfig } from 'vite'
export default defineConfig({
...
optimizeDeps: {
exclude: [
'@websdr/frontend-core',
],
},
...
})Public API (summary)
@websdr/frontend-core/common:debug_modeProtocol,NngWebSocketWASMErrno
@websdr/frontend-core/services:setApiBase,getApiBase,apiUrl,apiFetchlogin,logout,getProfile
@websdr/frontend-core/webusb(high level):ensureWebUsbControlWebUsb,WebUsbChannels,ControlWebUsbInitialParamsWebUsbManager,WebUsbManagerMode,getWebUsbManagerInstanceregisterWebUsbInstance,getWebUsbInstance,SDRDevicesIds- WebUSB primitives and types:
WebUsb,WebUsbEndpoints,DeviceStreamType,DeviceDataType, etc.
Notes / caveats
- WebUSB device registrations:
@websdr/frontend-core/webusbauto-importswebUsbDevices.autogen. When building from source inside the monorepo, this is generated byscripts/prebuild.js. - Node vs Browser: WebUSB is browser-native on supported platforms. For Node usage,
ensureWebUsb()tries to loadusb(preferred) orwebusbdynamically. - User gesture requirement:
navigator.usb.requestDevice()must be called in response to a user interaction (browser security requirement).
Development
From the repository root:
npm installFrom this package folder:
Build
npm run prebuild
npm run buildTest
npm testSource links
This package publishes dist/ to npm. Source is available in the GitHub repository:
- Entry point: https://github.com/wavelet-lab/websdr/blob/main/packages/frontend-core/src/index.ts
- Common exports: https://github.com/wavelet-lab/websdr/blob/main/packages/frontend-core/src/common/index.ts
- Services exports: https://github.com/wavelet-lab/websdr/blob/main/packages/frontend-core/src/services/index.ts
- WebUSB exports: https://github.com/wavelet-lab/websdr/blob/main/packages/frontend-core/src/webusb/index.ts
Package folder (GitHub): https://github.com/wavelet-lab/websdr/tree/main/packages/frontend-core
License
WebSDR is MIT licensed
