Browsers can now talk to serial ports (Web Serial) and process audio in real time (Web Audio and WebAssembly), which is everything a packet station needs. So I’ve built two packet radio pages that run entirely in the browser, with nothing to install. Both are in packet-net/pdn-web and need Chrome or Edge, since those have Web Serial.

A TNC2 terminal: packet-term-web

packet-term.m0lte.uk is a terminal for a KISS modem on a serial port, such as a NinoTNC on USB. It looks and behaves like a TNC2 in terminal mode, on a phosphor green screen: connect to a station, type, read the replies. The AX.25 connected-mode work is done by @packet-net/ax25, the TypeScript library built on the same generated AX.25 state tables as packet.net (see Generating AX.25 code from the specification), and the terminal is xterm.js.

A sound card station: soundmodem-web

The soundmodem page needs no TNC at all. It uses a plain USB audio interface on the radio, and a serial line or a CM108 GPIO pin for PTT. It runs every NinoTNC mode, with connected-mode sessions from the keyboard, a level meter, a TXDELAY slider and a transmitter test.

Underneath it is the modem from pdn-soundmodem, compiled from C# to WebAssembly and published on npm as @packet-net/soundmodem. It is the same code, from the same release, as the Linux daemon: every pdn-soundmodem release publishes the .deb packages, the NuGet package and the npm package at the same version.

The package sits where a KISS TNC on a serial port would: raw AX.25 frames in, raw AX.25 frames out, and a carrier-sense reading. The boundary between C# and JavaScript is small on purpose: open a mode, push received audio, pull decoded frames, read carrier sense, modulate a frame to audio, and render a test burst. The receive and transmit level controls never cross into the WebAssembly at all; they are gain nodes in the Web Audio graph on either side of it.

Is it the same modem?

That was the first thing to check, because the demodulators carry a lot of measured tuning and I didn’t want to measure it all again.

  • Decoding is identical. Every NinoTNC mode, and Dire Wolf’s test recordings, decode byte for byte the same in WebAssembly as natively: the same frames, the same count, the same bytes.
  • Transmit round trips. Ten of eleven modes modulate a frame and demodulate it back identically. The eleventh, C4FSK at 9600 (and 19200 with it), fails the same way natively in that noiseless loopback, so it’s a property of the test, not of WebAssembly.
  • It is fast enough. On one core of a Ryzen 7 8745H, the ahead-of-time compiled WebAssembly runs at about the same speed as native. Even the slower interpreted build leaves three quarters of a core spare on the heaviest mode, BPSK300, which runs nine demodulators at once.
  • The download is small: 1.37 MB gzipped for the interpreted build and 2.75 MB for the compiled one, and less again with Brotli.

A test harness runs two complete stations in Node, with real DSP and real AX.25 over a simulated wire, and takes a connected-mode session all the way through: connect, data, reply, disconnect.

What the simulated wire taught me

Two early failures were the simulation being unrealistic, and they’re worth knowing before wiring anything like this to a radio:

  • A receiver must never be fed nothing. The first harness only delivered audio during transmissions. The busy detector decays over audio, not over clock time, so with nothing arriving between bursts every station stayed busy for ever. A sound card always delivers audio, so the harness has to as well.
  • Digital silence is not a noise floor. Exact zeros left the detectors nothing to measure a signal against. Adding band noise at -54 dBFS fixed it, which is also why the real thing runs with the squelch open.

And one is a real property of the slow modes: AX.25’s usual 6 second T1 is a VHF number. At 300 baud, a frame plus TXDELAY plus the reply doesn’t fit inside it, and the link retries frames that were never lost. A station needs to pick T1 from the mode’s speed.

What’s still open

Browser audio capture is the one real unknown. The page asks the browser to turn off echo cancellation, noise suppression and automatic gain control, but the operating system’s own mixer can still apply gain the browser can’t see. The page shows the input level the demodulator actually receives, and has a receive gain trim, which is what a bench session with a real radio needs. The timing between PTT and audio, which the page pads by the latency the browser reports, also wants checking on a scope.

Licence

Both pages are AGPL-3.0-or-later. The modem core inside @packet-net/soundmodem is GPL-3.0-or-later, combined under the AGPL as GPLv3 allows, so if you host a page that uses it you need to offer its source.