Server daemon for XDR-F1HD / XDR-GTK FM tuners. Exposes the tuner over the network so multiple clients can connect simultaneously.
Supports two independent transports that can run concurrently:
| Transport | Protocol | Default port |
|---|---|---|
| TCP raw | Newline-delimited text | 7373 |
| WebSocket | RFC 6455, same text messages as WS frames | 7374 (optional) |
| Dependency | Linux | macOS |
|---|---|---|
| gcc | apt install gcc |
Xcode CLT |
| OpenSSL | apt install libssl-dev |
brew install openssl |
| pthreads | included in glibc | included in SDK |
# Linux
make
# macOS (OpenSSL path detected automatically via brew)
make
# Run the synthetic WebSocket test suite
make testmake test runs the synthetic test suite in test_ws.c — handshake, framing
(including fragmentation, control frames and masking) and the auth flow — using
socketpair(), so no hardware or network is needed. See the test binary's
output for the current list.
xdrd [ -s serial ] [ -t port ] [ -w wsport ] [ -u users ]
[ -p password ] [ -f command ] [ -l command ]
[ -hgxb ]
| Flag | Description | Default |
|---|---|---|
-s |
Serial port of the tuner | /dev/ttyUSB0 |
-t |
TCP raw port | 7373 |
-w |
WebSocket port (disabled if omitted) | — |
-p |
Password (required) | — |
-u |
Maximum simultaneous users | 10 |
-g |
Allow guest login (read-only, no password) | off |
-x |
Power tuner off when last user disconnects (currently not implemented — flag is accepted but has no effect) | off |
-f |
Command to run when first user connects | — |
-l |
Command to run when last user disconnects | — |
-b |
Run in background (daemon mode) | off |
TCP only (original behaviour):
./xdrd -s /dev/ttyUSB0 -p mypasswordTCP + WebSocket on separate ports:
./xdrd -s /dev/ttyUSB0 -t 7373 -w 7374 -p mypasswordmacOS with USB modem:
./xdrd -s /dev/tty.usbmodem21103 -t 7373 -w 7374 -p mypasswordBackground daemon:
./xdrd -s /dev/ttyUSB0 -t 7373 -w 7374 -p mypassword -bBoth transports share the same challenge–response mechanism:
- Server sends a 16-character random salt as plain text (or WS frame).
- Client responds with
SHA1(salt + password)as a lowercase hex string (40 chars). - Server replies with
a0(rejected) ora1(accepted as guest). Authenticated users receive no reply — the tuner data stream starts immediately.
With the -g flag guests are allowed without a password and receive read-only access (commands they send are ignored).
Client ──── TCP ──── xdrd ──── serial ──── tuner
Messages are newline-terminated UTF-8 strings sent directly over a TCP stream. No framing overhead.
Pros
- Minimal overhead — zero protocol bytes beyond the
\nterminator. - Works with netcat, telnet, and any raw TCP socket library.
- Compatible with the original xdr-gtk client and all legacy tools.
- Slightly lower latency at very high message rates.
Cons
- Not accessible from a browser without a proxy.
- Requires implementing the custom stream framing on the client side.
- Blocked by most corporate firewalls and reverse proxies (non-HTTP traffic).
- No standard tooling for debugging (no browser DevTools, no standard WS clients).
Connect:
nc localhost 7373
# Server sends: AbCdEfGh01234567
# You send: <sha1(salt+password)>Browser / JS / Python ──── HTTP Upgrade ──── WS frames ──── xdrd ──── serial ──── tuner
Messages are identical to TCP raw but wrapped in RFC 6455 WebSocket frames. The same tuner data is broadcast to both TCP and WS clients simultaneously.
Pros
- Browser native — connect directly from any web page with
new WebSocket(...). - Standard protocol — client libraries exist for every language (JS, Python, Go, Rust, etc.).
- Travels over HTTP port 80/443 through nginx/Apache reverse proxy with
proxy_pass. - Firewall and CDN friendly (looks like regular HTTP traffic).
- Built-in ping/pong keep-alive handled transparently by the server.
- Debuggable with browser DevTools Network tab,
wscat,websocat.
Cons
- ~2–6 bytes of frame overhead per message (2 bytes header server→client; 6 bytes header + XOR mask client→server).
- Two
send()syscalls per broadcast message instead of one (header + payload). - HTTP upgrade handshake adds ~1 round-trip on connection setup.
- Incompatible with legacy xdr-gtk clients (they expect raw TCP).
In practice the overhead is negligible. The serial port runs at 115 200 baud (~11.5 KB/s), which is the real throughput ceiling. WebSocket framing costs are completely absorbed by that limit.
A Python test client is included:
pip3 install websockets
python3 ws_client_test.py <password> [host] [port]Example session:
Connecting to ws://localhost:7374 ...
[auth] salt received: XI0DO7AebkU3iDkR
[auth] sending hash: 8f14dd1d84ba5bb989edecdf618cf39db91e7101
[auth] authenticated OK
[tuner] Ss49.1,0,0,-1
Listening for tuner data (Ctrl+C to quit).
Type a command and press Enter to send (e.g. T87500):
T98700
[tuner] Ss62.3,1,1,-1
[tuner] P1234
[tuner] Rsome station name
Messages are newline-terminated ASCII strings. All commands and responses use the same format on both transports.
| Command | Example | Description |
|---|---|---|
T<freq> |
T87500 |
Tune to frequency in kHz |
M<mode> |
M0 |
Set mode (0=FM, 1=AM) |
Y<vol> |
Y100 |
Set volume (0–100) |
D<de> |
D0 |
De-emphasis (0=75µs, 1=50µs) |
A<agc> |
A2 |
AGC setting |
F<filter> |
F3 |
Filter bandwidth |
W<bw> |
W180 |
Bandwidth in kHz |
Z<ant> |
Z0 |
Antenna input |
G<gain> |
G00 |
RF gain |
V<daa> |
V0 |
DAA setting |
Q<sq> |
Q0 |
Squelch level |
C<rot> |
C180 |
Rotator position |
I<s>,<d> |
I100,0 |
Sampling interval and detector |
X |
X |
Shutdown / disconnect |
| Message | Example | Description |
|---|---|---|
Ss<level>,<st>,<rds>,<mp> |
Ss62.3,1,1,-1 |
Signal level (dBf), stereo, RDS present, multipath |
T<freq> |
T87500 |
Frequency confirmed |
M<mode> |
M0 |
Mode confirmed |
P<pi> |
P1234 |
RDS Programme Identification code |
R<text> |
Rsome station |
RDS RadioText or PS name |
a0 |
Auth failed | |
a1 |
Accepted as guest | |
X |
Tuner powered off |
Use socat to create a virtual serial port pair:
# Terminal 1 — virtual serial pair
brew install socat # or apt install socat
socat -d -d pty,raw,echo=0 pty,raw,echo=0
# prints: PTY is /dev/ttys004 and PTY is /dev/ttys005
# Terminal 2 — run xdrd on one end
./xdrd -s /dev/ttys004 -t 7373 -w 7374 -p mypassword
# Terminal 3 — inject fake tuner data from the other end
echo -e "T87500\nSs62.3,1,1,-1\n" > /dev/ttys005GPL-2.0-or-later — see LICENSE.
Original code © 2013–2023 Konrad Kosmatka — http://fmdx.pl/