Multi-protocol SDR signal decoder with real-time streaming
WaveKit connects to Software Defined Radio sources and decodes multiple signal types simultaneously. Aircraft tracking, ship positions, pager messages, digital voice, weather sensors—all decoded in parallel and streamed via WebSocket. It can also expose the internal IQ stream as an RTL-TCP endpoint so SDR++ can tune locally without opening a second upstream connection.
┌─────────────────┐ ┌──────────────────────────────────────┐ ┌─────────────────┐
│ RTL-SDR │ │ WaveKit Container │ │ Your Apps │
│ on Raspberry Pi│────▶│ │────▶│ │
│ rtl_tcp :1234 │ IQ │ 8 Decoders running in parallel: │ WS │ CLI Dashboard │
│ │ │ ✈️ ADS-B 🚢 AIS 📟 Pagers 📻 DMR │ │ Web UI │
└─────────────────┘ └──────────────────────────────────────┘ └─────────────────┘
# Clone and build
git clone https://github.com/coriou/wavekit.git && cd wavekit
make dev-up
# Open the interactive dashboard
make dev-dashboardThe dashboard shows decoder health, live decoded messages, backpressure status, and source connections—all in your terminal.
WaveKit's primary interface is an interactive terminal dashboard built with Ink/React:
┌─ WaveKit Dashboard ─────────────────────────────────────────────────────────┐
│ [1] Dashboard [2] Decoders [3] Output [4] Backpressure [5] Sources [6] Audio [7] Resources [8] Tuner │
├─────────────────────────────────────────────────────────────────────────────┤
│ DECODERS │
│ Running: 5/8 Healthy: 5/5 Total Events: 12,847 │
│ ● dsd-fme ● multimon-ng ● readsb ● ais-catcher │
│ │
│ BACKPRESSURE │
│ Status: All flowing Drop Rate: 0 B/s Flowed: 847 MB Dropped: 0 B │
│ │
│ SOURCES │
│ Connected: 1/1 Fanout Consumers: 5 │
│ ● sdrpp-main @ tcp://192.168.1.69:5555 │
│ │
│ RECENT MESSAGES │
│ 14:23:45 [readsb] aircraft ICAO:A4B2C1 ALT:35000 SPD:450 │
│ 14:23:44 [ais] ship MMSI:123456789 LAT:37.77 LON:-122.41 │
│ 14:23:43 [multimon] message POCSAG1200 ADDR:1234567 "Test message" │
├─────────────────────────────────────────────────────────────────────────────┤
│ [q] Quit [r] Reconnect [1-7] Switch tabs │
└─────────────────────────────────────────────────────────────────────────────┘
Keyboard shortcuts:
1-8— Switch between tabs (Dashboard, Decoders, Output, Backpressure, Sources, Audio, Resources, Tuner)r— Reconnect WebSocketq— Quit
Environment variables:
WAVEKIT_WS_URLS=ws://localhost:9000/ws # WebSocket endpoints (comma-separated)
WAVEKIT_API_URL=http://localhost:9000 # REST API endpoint| Decoder | Signals | Use Case |
|---|---|---|
| readsb | ADS-B 1090 MHz | Aircraft tracking |
| AIS-catcher | AIS 162 MHz | Ship tracking |
| acarsdec | ACARS VHF | Aircraft data link |
| dumpvdl2 | VDL2 136 MHz | Aviation data link |
| dsd-fme | DMR, P25, YSF, D-Star | Digital voice |
| multimon-ng | POCSAG, FLEX, DTMF | Pagers, tones |
| direwolf | APRS 144 MHz | Amateur radio |
| rtl_433 | ISM 433/915 MHz | Weather sensors, IoT |
All decoders are pre-built in the Docker image. Enable/disable via configuration.
SDR Source (rtl_tcp/SDR++)
│
▼
SourceManager ──────────────────────────────────────────┐
│ │
▼ │
FanoutManager ─────┬─────┬─────┬─────┬─────┐ │
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ │
dsd-fme multimon readsb ais acars vdl2 │
│ │ │ │ │ │ │
└────────────┴─────┴─────┴─────┴─────┘ │
│ │
▼ │
DecoderManager ◀──────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
REST API WebSocket Audio TCP Tuner Relay
:9000/api :9000/ws :8080 :1234
Key components:
- SourceManager — TCP connections to SDR sources with auto-reconnect
- FanoutManager — Multiplexes audio to all decoders with backpressure handling
- DecoderManager — Spawns/monitors decoder processes, handles restarts
- API Server — Fastify REST + WebSocket for control and real-time events
- Tuner Controller — Primary RTL-TCP tuner control with internal/external handoff
- Tuner Relay — Optional RTL-TCP server for local tuner clients (SDR++)
# Health check
curl http://localhost:9000/health
# Full system status
curl http://localhost:9000/api/status
# List decoders
curl http://localhost:9000/api/decoders
# Start/stop a decoder
curl -X POST http://localhost:9000/api/decoders/readsb/start
curl -X POST http://localhost:9000/api/decoders/readsb/stop
# List sources
curl http://localhost:9000/api/sources
# List tuner states
curl http://localhost:9000/api/tuner
# Set tuner frequency
curl -X POST http://localhost:9000/api/tuner/rtl-pi/frequency \\
-H "Content-Type: application/json" -d '{"hz":144800000}'Connect to ws://localhost:9000/ws and subscribe to channels:
const ws = new WebSocket("ws://localhost:9000/ws")
ws.send(
JSON.stringify({
type: "subscribe",
channels: [
"decoders",
"sources",
"metrics",
"health",
"fanout",
"live-audio",
],
}),
)
ws.onmessage = event => {
const msg = JSON.parse(event.data)
// msg.type: 'decoder:output', 'decoder:health', 'source:connected', etc.
// msg.data: event payload
}Channels:
decoders— Decoder output, start/stop eventssources— Source connection eventsmetrics— Data rate metrics (~5s intervals)health— Decoder health state changesfanout— Backpressure snapshotslive-audio— Live demod status/config eventsresources— Container, SDR host, and backpressure metricstuner— RTL-TCP tuner state/command events
Decoded audio streams over TCP port 8080:
# Play with sox
nc localhost 8080 | play -t raw -r 48000 -e signed -b 16 -c 1 -
# Play with ffplay
nc localhost 8080 | ffplay -f s16le -ar 48000 -ac 1 -nodisp -WaveKit can directly control RTL-TCP sources without SDR++. Use the CLI Tuner tab or the REST API to tune frequency, gain, and other settings.
CLI keys (Tuner tab):
up/down— Tune frequencyleft/right— Change tuning step (also supports[ ] , . /)g— Toggle gain mode (AGC/manual)+/-— Adjust gain (manual only)s— Edit sample ratep— Edit PPM correctiona— Toggle RTL AGCb— Toggle bias-teed— Cycle direct sampling (off/I/Q)o— Toggle offset tuningc— Release/reclaim control (internal/external)
# List tuner states
curl http://localhost:9000/api/tuner
# Set frequency
curl -X POST http://localhost:9000/api/tuner/rtl-pi/frequency \\
-H "Content-Type: application/json" -d '{\"hz\":144800000}'
# Release control to SDR++
curl -X POST http://localhost:9000/api/tuner/rtl-pi/control-mode \\
-H "Content-Type: application/json" -d '{\"mode\":\"external\"}'
# Reclaim control
curl -X POST http://localhost:9000/api/tuner/rtl-pi/control-mode \\
-H "Content-Type: application/json" -d '{\"mode\":\"internal\"}'Expose the internal IQ stream as an RTL-TCP compatible endpoint for SDR++ (or any RTL-TCP client). Control commands are forwarded upstream so you only keep a single connection to the remote RTL-SDR.
tunerRelay:
enabled: true
host: "0.0.0.0"
port: 1234
sourceId: "rtl-pi"
controlPolicy: "exclusive" # or "shared"
commandHistoryLimit: 200 # 0 disables command historyUsage:
- Start WaveKit with the relay enabled.
- In SDR++, select RTL-TCP and connect to
tcp://<wavekit-host>:1234. - Tune as usual — SDR++ commands are forwarded to the upstream
rtl_tcp/rtlmux.
Notes:
- The relay expects an IQ source in
U8_IQformat (standard RTL-TCP/rtlmux output). - In
exclusivemode, the first client gets control and others are read-only. - The relay streams the primary source (first in
sources), so setsourceIdto match it. - RTL-TCP commands are tracked and available via
GET /api/tuner-relay. - Relay commands update the internal tuner state and will switch control mode to
externalwhile a relay control client is active. When the relay becomes idle, control returns tointernalunless you explicitly released control.
Dynamic Sample Rate:
When SDR++ changes the sample rate, WaveKit automatically:
- Updates source capabilities
- Restarts the LiveDemodulator with new decimation rates
- Restarts affected decoders to maintain optimal decoding
- Broadcasts
source:caps-changedto WebSocket clients
This enables seamless tuning without manual reconfiguration.
Live demodulates IQ in real time and serves mono audio over HTTP. Ideal for quick monitoring with ffplay/VLC without touching decoder configs.
Quick start:
liveDemod:
enabled: true
sourceId: "rtl-pi"
httpPort: 8081
modulation: "nfm"
bandwidth: 12500
squelch: 0
noiseReduction: "off"
lowPass: 0
highPass: 0
gain: 10.0
deEmphasis: false
deEmphasisTau: 50
audioFormat: "s16le"
iqDcBlock: true# Start demodulation (if not auto-started)
curl -X POST http://localhost:9000/api/live-audio/start
# Play the stream (use the effectiveSampleRate from /status)
ffplay -nodisp -autoexit -f s16le -ar 24976 -ch_layout mono http://localhost:8081/streamConfiguration reference:
sourceId— IQ source to demodulate (defaults to first source)modulation—nfm|wfm|am|usb|lsb|dsb|cw|rawbandwidth— Target audio bandwidth in Hz (0 allowed only forraw)squelch— dBFS threshold (-160 to 0).0keeps squelch opennoiseReduction—off|voice|noaa-apt|narrow-bandlowPass/highPass— Optional audio filters in Hzgain— Audio gain multiplier (float)deEmphasis/deEmphasisTau— FM de-emphasis (50 or 75 microseconds)audioFormat—s16leorf32leiqDcBlock— Apply IQ DC blocking before decimation
API examples:
# Status
curl http://localhost:9000/api/live-audio/status
# Update modulation on the fly
curl -X PATCH http://localhost:9000/api/live-audio/config \
-H "Content-Type: application/json" \
-d '{"modulation":"am","bandwidth":10000}'
# Presets
curl http://localhost:9000/api/live-audio/presetsConfiguration via YAML (config/default.yaml) or environment variables:
# Sources
sources:
- id: "sdrpp-main"
type: "sdrpp-network"
host: "192.168.1.69"
port: 5555
caps:
kind: "audio_pcm"
sampleRate: 48000
format: "FLOAT32LE"
# Decoders
decoders:
- id: "dsd"
type: "dsd-fme"
enabled: true
sourceId: "sdrpp-main"
options:
mode: "auto"
# API
api:
host: "0.0.0.0"
port: 3000
# Audio output
audio:
tcpPort: 8080
format: "S16LE"
sampleRate: 48000
# Tuner relay (RTL-TCP)
tunerRelay:
enabled: true
host: "0.0.0.0"
port: 1234
sourceId: "sdrpp-main"
controlPolicy: "exclusive"Environment overrides:
WAVEKIT_API_PORT=9000
WAVEKIT_LOG_LEVEL=debug
WAVEKIT_SOURCES_0_HOST=192.168.1.100
WAVEKIT_TUNER_RELAY__ENABLED=true
WAVEKIT_TUNER_RELAY__PORT=1234- Node.js 20+ (see
.nvmrc) - pnpm 10+ (
npm install -g pnpm@10) - Docker with BuildKit
- RTL-SDR dongle (or rtl_tcp server)
# Development (Docker)
make dev-up # Build + start container (uses config/dev_test.yaml)
make dev-up CONFIG=dev_acars # Start with specific config (config/dev_acars.yaml)
make dev-configs # List all available configs
make dev-dashboard # Interactive CLI dashboard
make dev-logs # Tail logs (pretty JSON)
make dev-stop # Stop container
# Monorepo Tasks (pnpm + Turborepo)
pnpm ws:build # Build all packages
pnpm ws:typecheck # Type check all packages
pnpm ws:lint # Lint all packages
pnpm ws:test # Test all packages
# Single Package Commands
pnpm test # Run root tests
pnpm run test:coverage # With coverage
# Docker Images
make docker-build-core # Build core image (external SDR++)
make docker-build-full # Build full image (SDR++ included)WaveKit uses a pnpm monorepo with Turborepo for task orchestration:
wavekit/
├── packages/ # Internal packages
│ ├── shared/ # @wavekit/shared — Logger, errors
│ ├── api-types/ # @wavekit/api-types — Shared API types
│ └── sdr-host/ # @wavekit/sdr-host — Remote dongle host
│
├── src/ # Core WaveKit
│ ├── index.ts # Entry point
│ ├── config.ts # Zod schemas + config loading
│ ├── core/ # Stream infrastructure
│ │ ├── source-manager.ts
│ │ ├── fanout-manager.ts
│ │ └── audio-output.ts
│ ├── decoders/ # Decoder plugin system
│ │ ├── base-decoder.ts
│ │ ├── manager.ts
│ │ └── builtin/ # 8 decoder adapters
│ ├── api/ # Fastify REST/WebSocket
│ └── utils/
│
└── cli/ # @wavekit/cli — Terminal dashboard (Ink/React)
└── source/
├── app.tsx
├── components/
└── hooks/
- Create
src/decoders/builtin/my-decoder.tsextendingBaseDecoder - Implement
getCommand(),getArgs(),parseOutput() - Register in
src/decoders/registry.ts - Add config schema to
src/config.ts
See docs/DECODER-GUIDE.md for detailed instructions.
Three build targets:
| Target | Contents | Use Case |
|---|---|---|
wavekit:latest |
SDR++ + API + Decoders | All-in-one |
wavekit:latest-core |
API + Decoders | External SDR++ |
wavekit:latest-sdrpp |
SDR++ only | Dedicated SDR host |
# Build core (recommended for development)
make docker-build-core
# Run with external SDR++
docker run -p 9000:3000 -p 8080:8080 \
-p 1234:1234 \
-e WAVEKIT_SOURCES_0_HOST=192.168.1.69 \
-e WAVEKIT_SOURCES_0_PORT=5555 \
-e WAVEKIT_TUNER_RELAY__ENABLED=true \
wavekit:latest-coreFor optimal reception:
# On Raspberry Pi - start rtl_tcp with AGC
rtl_tcp -a 0.0.0.0 -p 1234 -f 446524920 -s 2048000 -g 0
# Key settings:
# -g 0 AGC mode (critical for weak signals)
# -s 2048000 Sample rate 2.048 Msps (not 2.4!)For multiple clients, use rtlmux:
rtlmux -a 0.0.0.0 -p 5555 -s 5556 127.0.0.1 1234- API Reference — Full REST/WebSocket documentation
- Docker Setup — Container deployment guide
- SDR Host Setup — Remote dongle hosting with rtlmux
- Decoder Guide — Adding new decoders
- Architecture — System design deep dive
- Security — Version pinning, CVE tracking
ISC