Architecture
One program
Section titled “One program”waved is one Rust program, /data/waved/waved, built from the workspace in
waved/. It runs its services on a tokio runtime, plus a few threads of its
own where hardware needs blocking reads and writes: the Bluetooth and MCU
serial links, audio capture, the USB audio bridges and the screen.
Two start-up scripts come with it, both from waved/deploy/. S60waved runs
it; S03splash draws the splash:
/etc/init.d/S60wavedstarts waved at boot and restarts it about 3 seconds after any exit. Before each start it runs the update fall-back check (see Updates below). It also loads the firewall rules. The first waved after a boot is stopped after 5 seconds on purpose: its display output doesn’t work, and the restarted one’s does./etc/init.d/S03splashwrites the splash image to the screen about 2 seconds after power-on, long before waved draws its own screens.
The Wave’s read-only system partition is never written. waved replaces the
stock app through the overlay on /data: a stub S60pspapp_daemon keeps the
stock app from starting, and an empty libapplogrs.so stands in for the
stock telemetry library. The Overview
lists every file.
The state bus
Section titled “The state bus”waved-state defines the one StateBus that every service shares. It has
three kinds of channel:
- State, one watch channel per area, such as audio, battery, Bluetooth, Wi-Fi, Wi-Fi setup, recording, the MCU, settings, files, playback and updates. Each holds the latest value, so a reader always sees the current state.
- Commands, one broadcast channel: what the touch screen, the setup page, the computer and the Wave Controller app ask for, such as mute, a volume, an audio mode or “start recording”. Each service subscribes and acts on the commands it owns.
- Events, one broadcast channel: things that happened, such as a power key press, low battery, a phone connecting or an upload that succeeded.
A service publishes its state and takes commands from the bus. The screen never calls a service directly: it reads a snapshot of the state and sends commands.
Services
Section titled “Services”| Crate | Job |
|---|---|
waved |
The program: loads config.toml, builds the bus, starts every service, handles shutdown and power off. |
waved-state |
The bus, and the state, command and event types every other crate uses. |
waved-config |
config.toml: its format, loading with a fall-back to config.toml.bak and then defaults, migration from older versions, validation, the setup page’s form fields, and the PIN hash. |
waved-settings |
Seeds the live settings from config.toml at boot and writes changes made on the Wave back to it. |
waved-audio |
Capture from the eight microphones, direction of arrival, beamforming, echo cancellation, gain, the speaker’s processing, USB audio (UAC2) and Bluetooth audio. Writes /tmp/audio_stats.json every 500 ms. |
waved-meeting |
Recording: the Opus and FLAC writers (from waved-codec) and the pre-recording buffer, the recordings list in SQLite, numbered speakers, webhook uploads with retries, pruning, and playback for Files. |
waved-ui |
The touch screen: LVGL, the screens, navigation, gestures and the PIN gate. |
waved-bt |
The Bluetooth module on /dev/ttyS1: pairing, disconnecting, calls and the connected phone. |
waved-uart |
The /dev/ttyS1 serial port and its frame parser, which waved-bt runs on. |
waved-mcu |
The microcontroller on /dev/ttyS4: battery, the power key, the mic lift and power off. |
waved-mavlink |
MAVLink v2 framing, shared by the MCU link and the Wave Controller link. |
waved-hid |
The USB HID interface: volume and mute keys to the computer, call state from it, and the Wave Controller (“PC Link”) protocol. |
waved-wifi |
Wi-Fi as a client through wpa_supplicant, saved networks, and setting the clock from time servers. |
waved-provision |
Wi-Fi setup: the temporary setup network, its DHCP and DNS, and the setup page. |
waved-system |
Screen dimming and backlight, time zone and real-time clock, About, recording tones, factory reset, and whether a computer is connected over USB. |
waved-update |
Signed updates from your own server, the trial boots, and waved-pack, the tool that packs and signs a release. |
waved-audio also links waved-npu, which holds the DeepFilterNet noise
reduction. Despite its name, that runs on the CPU: nothing in waved uses the
NPU today. The NPU work happens next to waved, in waved/scripts/npu and
waved/tools: it already runs NVIDIA’s MarbleNet voice-activity model on the
NPU in full, on fixed four-second windows
(MarbleNet on the NPU).
The other crates are tools and probes, not part of the installed program:
waved-aec-probe, waved-latency-probe and waved-wakeup-probe measure
echo cancellation and latency on a Wave; waved-audio-bench replays a
recorded capture through the audio service; waved-capture-record records
raw microphone audio; waved-doa-tap prints the direction-of-arrival stream;
waved-mic-map maps ALSA channels to physical microphones;
waved-npu-probe tests whether a process of ours can reach the NPU; and
waved-device-test is an older on-device check (see
Testing).
flowchart LR
subgraph Hardware
TTYS1["/dev/ttyS1<br/>Bluetooth module"]
TTYS4["/dev/ttyS4<br/>MCU"]
GADGET["USB gadget<br/>UAC2 audio, HID"]
ALSA["ALSA card 0<br/>mics, speaker, Bluetooth audio"]
WLAN["wlan0<br/>Wi-Fi"]
FB["/dev/fb0 and touch"]
end
subgraph Services
BT["waved-bt"]
MCU["waved-mcu"]
HID["waved-hid"]
AUDIO["waved-audio"]
WIFI["waved-wifi<br/>waved-provision"]
MEETING["waved-meeting"]
OTHER["waved-settings<br/>waved-system<br/>waved-update"]
end
BUS["waved-state bus"]
UI["waved-ui"]
TTYS1 <--> BT
TTYS4 <--> MCU
GADGET <--> HID
GADGET <--> AUDIO
ALSA <--> AUDIO
WLAN <--> WIFI
AUDIO --> MEETING
BT <--> BUS
MCU <--> BUS
HID <--> BUS
AUDIO <--> BUS
WIFI <--> BUS
MEETING <--> BUS
OTHER <--> BUS
BUS <--> UI
UI <--> FB
The audio path
Section titled “The audio path”The microphones are eight MEMS mics read as one 8-channel ALSA device
(hw:0,2, 48 kHz). A capture thread hands each block of samples to the audio
service, which works in this order:
- Direction of arrival, on the raw microphone signal. The time differences between mic pairs give the direction a voice comes from. It drives the talker arc on the screen, the Follow talker mode and the speaker numbers in recordings. When most of what the mics hear is the Wave’s own speaker, the estimate skips that moment.
- Beamforming combines the eight channels into the pickup pattern of the current audio mode.
- Echo cancellation removes what the Wave’s speaker played from the microphone signal: Speex’s adaptive filter, then its linked residual-echo suppression.
- Voice clean-up: late-reverb suppression and a voice equaliser, then a fixed make-up gain.
- Noise reduction (DeepFilterNet, on the CPU) is off for now: it is too slow.
- Microphone gain (the Mic level setting) and a limiter.
The result goes three ways: to the computer over USB (UAC2), to the recording, and to the phone during a Bluetooth call (in mono).
The other direction: audio from the computer (USB), from a phone (Bluetooth)
and from the Wave itself (Files playback, tones) is mixed, goes through a
high-pass filter, an equaliser, a multiband level control and a limiter, then
the volume, and plays on the speaker (hw:0,1). That same signal, after the
volume, is the echo canceller’s reference.
Details: Audio hardware.
The screen
Section titled “The screen”waved-ui runs LVGL 9.2 on its own thread. The panel is a round LCD driven by
fb_ili79400: a 400×304 framebuffer (/dev/fb0, 16-bit colour), of which a
circle of radius about 198 pixels shows. The panel is mounted upside down, so
waved rotates and byte-swaps every frame as it writes it. Touch comes from
/dev/input/event1 and is flipped the same way.
Display has the
measurements.
Every screen has a snapshot test: it renders the screen on a computer, checks
that every button lies inside the visible circle, and compares the frame with
a PNG in waved/crates/waved-ui/golden/. The screenshots on this site are
those PNGs. Testing
shows how to run them.
Recordings
Section titled “Recordings”The recording service gets the processed microphone signal, plus the far end
of a call, from the audio service. Each recording is a folder in
/mnt/udisk/recordings with audio.opus (or audio.flac, or audio.wav
for recordings made before 2026-10-14) and metadata.json. The list of
recordings and each one’s upload state live in a SQLite database,
/data/waved/meetings.db.
A recording is saved on the Wave before it is queued for upload. The queue is
in the database, so it survives a restart. Each upload is one HTTP POST to
your server, and retries reuse the recording’s ID as the idempotency key.
Recording webhooks has the
format.
Updates
Section titled “Updates”waved-update fetches latest.json from a server you run and downloads the
.wup package it names. The package is signed with ed25519; the Wave checks
the signature against the public key compiled into waved, then installs. A
package can only replace waved and its own files (the start-up scripts, the
splash, the firewall and logging configuration), never the system partition,
the kernel or the bootloader.
After an install the new version is on trial. S60waved counts boots: if the
new waved hasn’t run for 60 seconds with working microphones by the third
boot, or restarts more than 10 times in one boot, it puts back the previous
files and config.toml. All of this is host-tested only.
Updates
has the details.