Introduction
PodSDR is a complete software-defined-radio suite for OpenHPSDR-compatible hardware — Hermes, Hermes Lite 2, Angelia, Orion — written in Rust. It also drives conventional CAT-controlled transceivers (Xiegu X6100 and G90, QRP Labs QMX, Icom IC-7300/IC-705 and Icom network radios, Yaesu FTDX10/FTDX3000/FT-991A, BG2FX FX-4CR), and it ships its own radio emulator so you can try everything with no hardware at all.
This guide covers the parts you install and use as an operator:
| Component | What it is | Where it runs |
|---|---|---|
| podsdr-proxy (server bundle) | Serves the browser client, relays radio traffic over WebRTC, and optionally uplinks to the cloud broker for access from anywhere. Also hosts CAT radios and composite radios. | Raspberry Pi, Linux, macOS, Windows |
| podsdr-emu (in the server bundle) | A Protocol-1 radio emulator that appears on your LAN like real hardware. Try the whole stack without a radio. | Same platforms as the proxy |
| Desktop GUI | Native desktop client with spectrum/waterfall, full RX/TX control, CW keyer, digimodes. | Linux, macOS, Windows |
| Web client | The same radio experience in a browser, served by the proxy. Nothing to install on the operating position. | Any modern browser |
| Android app | Mobile client with touch spectrum, TX, CW keyer and background operation. | Android (arm64) |
Which setup do I want?
- Radio and operator at the same desk → install the Desktop GUI. It auto-discovers OpenHPSDR radios on the LAN; no server needed.
- Radio in the shack, operate from anywhere in the house → run the server bundle on a Raspberry Pi (or any always-on machine) next to the radio, then open the web client from any browser on the LAN.
- Operate from outside your network — no port forwarding → run the server bundle with the cloud broker uplink and pair it to your account; then connect from the web, desktop or Android client anywhere.
- No radio yet → start the bundled emulator and point any client at it.
How the pieces connect
LAN (UDP, OpenHPSDR Protocol 1/2)
Radio hardware ◄──────────────────────────────► podsdr-proxy ──┐
(or podsdr-emu) │ │ wss uplink
│HTTPS + │ (optional)
Desktop GUI ◄── direct UDP to the radio │WebRTC ▼
│ cloud broker
Browser / Android ◄────────────────────────────────────┘◄─── (podsdr-broker)
The desktop GUI talks to radios directly over UDP. Browsers can’t do raw UDP, so the proxy bridges radio traffic into a WebRTC data channel and serves the web app itself. The Android app connects the same way the browser does — via a proxy on the LAN or through the cloud broker.
Downloads & release bundles
PodSDR is distributed as a set of self-contained bundles, one per platform and
role. A full release (produced by ./build-all.sh, see
Building the bundles yourself) contains:
| File | Contents | For |
|---|---|---|
podsdr-proxy-pi-bundle.tar.gz | Server bundle, aarch64 Linux | Raspberry Pi 3/4/5 running a 64-bit OS |
podsdr-proxy-x86_64-bundle.tar.gz | Server bundle, x86_64 Linux | Any 64-bit Linux box (glibc) |
podsdr-proxy-macos-bundle.tar.gz | Server bundle, universal macOS binary | macOS 11+ (Intel & Apple Silicon) |
podsdr-proxy-windows-bundle.zip | Server bundle, x86_64 Windows | Windows 10/11 (64-bit) |
podsdr-gui-linux-x86_64.tar.gz | Desktop GUI | 64-bit Linux (Ubuntu 20.04 / Debian 11 or newer) |
podsdr-gui-macos-universal.tar.gz | Desktop GUI as PodSDR.app | macOS 11+ (Intel & Apple Silicon) |
podsdr-gui-windows-x86_64.zip | Desktop GUI (podsdr-gui.exe) | Windows 10/11 (64-bit) |
podsdr-android-arm64.apk | Android app | arm64 Android phones/tablets |
SHA256SUMS.txt | Checksums for all of the above | Verifying downloads |
Verify a download before installing:
shasum -a 256 -c SHA256SUMS.txt # macOS
sha256sum -c SHA256SUMS.txt # Linux
What’s inside a server bundle
Every server bundle unpacks to a single directory containing:
podsdr-proxy[.exe] the proxy server binary
podsdr-emu[.exe] the Protocol-1 radio emulator
web/ the browser client the proxy serves
index.html
podsdr_web.js
podsdr_web_bg.wasm
run.sh / run.bat start the proxy as a LAN web server (HTTPS on :8080)
run-cloud.sh / run-cloud.bat
start the proxy with a cloud-broker uplink
(plus the LAN web server, unless disabled)
README.txt a condensed version of this guide
Platform extras:
- Linux / Raspberry Pi:
podsdr-proxy.service— a systemd unit for running at boot (see Running as a system service). - macOS:
org.podsdr.proxy.plist— the equivalent launchd job. - Windows: instructions in
README.txtfor wrapping the proxy with NSSM as a Windows service.
The emulator is shipped alongside the proxy so you can spin up a virtual radio
on the LAN without any extra tooling. It is not started by run.sh or the
service unit — you launch it yourself when you want it:
./podsdr-emu --radio hermeslite
What’s inside a desktop GUI bundle
- Linux: the
podsdr-guibinary plus arun.shlauncher. - macOS: a signed (ad-hoc)
PodSDR.appyou can double-click in Finder, plus arun.shthat launches it with the terminal attached so you can see logs. - Windows:
podsdr-gui.exeplus arun.batlauncher.
Nothing is installed system-wide — each bundle runs from wherever you extract it, and removing the directory removes the program.
Quick start
Three common paths, fastest first. All of them work with no radio hardware if you substitute the bundled emulator for the radio.
1. Desktop GUI on the same LAN as the radio
- Extract the GUI bundle for your platform (downloads).
- Launch it:
- Linux:
./run.sh - macOS: double-click
PodSDR.app(first launch: right-click → Open, orxattr -dr com.apple.quarantine PodSDR.app— the app is ad-hoc signed, not notarized). - Windows: double-click
podsdr-gui.exe.
- Linux:
- The GUI auto-discovers OpenHPSDR radios on the LAN. Pick your radio and connect.
No hardware? Start the emulator from a server bundle first — on the same machine or anywhere on the LAN:
./podsdr-emu --radio hermeslite
It shows up in the radio picker like a real Hermes Lite.
2. Web client served from a Raspberry Pi in the shack
- Copy the Pi server bundle over and unpack it:
scp podsdr-proxy-pi-bundle.tar.gz pi@pi.local:~/ ssh pi@pi.local 'tar xzf podsdr-proxy-pi-bundle.tar.gz && ./podsdr-proxy-pi-bundle/run.sh' - On any computer/tablet on the LAN, open
https://pi.local:8080. - Accept the self-signed-certificate warning (once per browser profile — see why HTTPS).
- Pick a radio from the device list and connect.
To survive reboots, install the bundled systemd service — Running as a system service.
3. Operate from anywhere via the cloud broker
- On the machine next to the radio, start the cloud uplink instead of (or in
addition to) the LAN server:
./run-cloud.sh home # "home" is the name this proxy will show as - Watch the console for the highlighted line:
>>> PAIRING CODE for "home": XXXX -- enter it in a client Cloud hub panel <<< - In any client (web, desktop GUI, Android), open the Cloud hub panel and enter the pairing code. That claims the proxy under your account — from then on your authorized clients can connect to it from any network, with no port forwarding.
Details: Remote access through the cloud broker.
Building the bundles yourself
All release artifacts are produced by shell scripts in the repository root. You can build any single bundle, or everything at once.
Build everything: build-all.sh
./build-all.sh [OUTPUT_DIR] # default output: ./artifacts
Runs all seven bundle scripts plus the Android release APK, then collects the
resulting .tar.gz / .zip / .apk files into one directory together with a
SHA256SUMS.txt manifest. Each step uses a different toolchain, so a missing
toolchain fails only that step — the script always finishes and prints a
PASS/FAIL summary (its exit code is the number of failed steps).
Prerequisites by target
| Target | Toolchain |
|---|---|
| Linux & Windows bundles (proxy and GUI) | cross (cargo install cross) and a running Docker daemon. cross builds inside Docker images that carry the ALSA/X11/Wayland (Linux) or mingw-w64 (Windows) toolchains — a bare cargo build --target … from macOS fails on the C dependencies. |
| macOS bundles | Native cargo on a Mac (CoreAudio frameworks only exist there), plus Xcode command-line tools (lipo, codesign). Both x86_64-apple-darwin and aarch64-apple-darwin rust targets must be installed; the scripts pin MACOSX_DEPLOYMENT_TARGET=11.0 so binaries run on Big Sur and later. |
| Web client (inside every server bundle) | rustup target add wasm32-unknown-unknown and cargo install wasm-bindgen-cli. The pipeline is cargo build --target wasm32-unknown-unknown --release + wasm-bindgen --target web — not wasm-pack. |
| Android APK | Android SDK + NDK 27 (sdkmanager 'ndk;27.2.12479018'), rustup target add aarch64-linux-android. Built with Gradle (./gradlew :app:assembleRelease in crates/podsdr-android/android), which drives cargo itself. |
Rust targets for the cross-compiled bundles must also be installed first, e.g.:
rustup target add aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu x86_64-pc-windows-gnu
Each script checks its own prerequisites up front and exits with an installation hint if something is missing.
Individual bundle scripts
| Script | Produces |
|---|---|
./bundle-proxy-pi.sh | podsdr-proxy-pi-bundle.tar.gz (aarch64 Linux) |
./bundle-proxy-x86_64.sh | podsdr-proxy-x86_64-bundle.tar.gz |
./bundle-proxy-macos.sh | podsdr-proxy-macos-bundle.tar.gz (universal) |
./bundle-proxy-windows.sh | podsdr-proxy-windows-bundle.zip¹ |
./bundle-gui-linux.sh | podsdr-gui-linux-x86_64.tar.gz |
./bundle-gui-macos.sh | podsdr-gui-macos-universal.tar.gz |
./bundle-gui-windows.sh | podsdr-gui-windows-x86_64.zip¹ |
¹ Falls back to .tar.gz when zip isn’t available on the build host.
Every server bundle script builds the WASM web client, cross-compiles
podsdr-proxy and podsdr-emu in one invocation, and writes the
launchers, service unit and README.txt described in
Downloads.
Development runs (no bundle)
For working directly from a source checkout:
./run-web.sh [port] # build WASM + serve it via a locally built proxy (plain HTTP, default port 8080)
./run-proxy-cloud.sh [name] # build + run the proxy against the public cloud broker
./run-digimodes-web.sh [port] # standalone FT8/FT4 web decoder
cargo run --release -p podsdr-gui # desktop GUI
cargo run --release -p podsdr-emu -- --radio hermeslite # emulator
The server bundle — overview
The server bundle is what you install on the machine that sits near your radio (a Raspberry Pi, a shack PC, a Mac — anything that’s on the same LAN as the radio). It contains two programs:
-
podsdr-proxy— the server. It does up to three jobs at once, all in one process:- Serves the web client over HTTP(S) from the bundled
web/directory. - Bridges radio traffic between browsers/Android (WebRTC data channels) and the radio (UDP on the LAN). It discovers OpenHPSDR radios itself and offers them to clients in a device list.
- Optionally uplinks to the cloud broker, so clients outside your network can reach the radio with no port forwarding.
Beyond OpenHPSDR hardware, the proxy can also host traditional CAT transceivers (Xiegu, QRP Labs QMX, Icom — including network-connected Icoms — Yaesu, BG2FX; see the supported list) and composite radios that combine two devices into one (e.g. RX on one radio, TX on another). Those appear in the same device list as the SDRs.
- Serves the web client over HTTP(S) from the bundled
-
podsdr-emu— a Protocol-1 radio emulator. Started manually when you want a virtual radio on the LAN; see Using the emulator.
The bundle also carries the web/ directory with the browser client the
proxy serves — see
what’s inside a server bundle.
The two launchers
| Launcher | What it starts |
|---|---|
run.sh / run.bat | LAN web server only: HTTPS on port 8080 (pass a port number to change it, or --no-tls as the first argument for plain HTTP). |
run-cloud.sh / run-cloud.bat | Cloud-broker uplink plus the same LAN web server. Set NO_LOCAL_WEB=1 to run a pure uplink with no local web server. |
Both are thin wrappers over podsdr-proxy command-line flags — see the
command-line reference if you want to compose your own invocation.
Where to go next
- Serving the web client on your LAN
- Remote access through the cloud broker
- Running as a system service
- Configuration file — CAT radios, RTL-SDR, composite radios, receive-only bands, audio gain
Serving the web client on your LAN
Extract the server bundle and run:
./run.sh # HTTPS on port 8080
./run.sh 8443 # HTTPS on port 8443
./run.sh --no-tls # plain HTTP on port 8080
./run.sh --no-tls 9000
(Windows: run.bat with the same arguments.)
Then open https://<host>:8080 from any browser on the LAN — for a Raspberry
Pi that’s typically https://pi.local:8080 or https://<pi-ip>:8080.
Why HTTPS by default
Browsers only expose AudioWorklet (low-latency audio), WebGPU (spectrum rendering), WebMIDI (CW paddles) and getUserMedia (your microphone for TX) to pages on a secure origin. A Pi on your LAN served over plain HTTP is not one, and the web client would lose those capabilities — so the launcher enables TLS by default.
On first start the proxy generates a self-signed certificate (cert.pem /
key.pem) next to the binary and reuses it afterwards. Because it’s
self-signed, each browser profile shows a “connection is not private” warning
once — accept it and it won’t ask again. If the certificate warning
reappears on every visit, or you connect by IP address rather than hostname,
see --tls-san in the command-line reference for adding the IP to
the certificate.
Use --no-tls only when something else already terminates TLS in front of
the proxy (a reverse proxy such as nginx or Caddy) — the secure-context
requirement is then satisfied by that frontend.
Gotcha: if the proxy runs without
--tlsand you openhttps://<host>:8080anyway, the browser reportsERR_SSL_PROTOCOL_ERROR. That’s not a certificate problem — it’s a TLS handshake hitting a plaintext socket. Either usehttp://or start the proxy with TLS.
What you’ll see
The served page is the full PodSDR web client. It asks
the proxy for the device list, which contains every radio the proxy can see:
OpenHPSDR radios discovered on the LAN, a running podsdr-emu, plus any
configured CAT radios, RTL-SDR dongles and composite radios.
Checking it works
RUST_LOG=info ./run.sh
prints discovery and connection activity to the console. From another machine,
curl -k https://<host>:8080/api/devices should return the device list as
JSON.
Remote access through the cloud broker
The cloud broker lets you operate your radio from outside your network with
no port forwarding, no VPN and no dynamic-DNS setup. The proxy next to your
radio opens a persistent outbound WebSocket (wss) uplink to the broker;
clients anywhere connect to the broker, prove they’re yours, and the broker
brokers a WebRTC session between them and your proxy.
The public broker runs at podsdr-broker.fly.dev. You can also
self-host one.
Step 1 — start the uplink
From the server bundle directory, on the machine next to the radio:
./run-cloud.sh home # "home" = the name this proxy shows in clients
PROXY_NAME=shack ./run-cloud.sh # same, via environment variable
(Windows: run-cloud.bat home.)
By default this also keeps serving the web client on the LAN over HTTPS on
port 8080, exactly like run.sh — one process does both. Control that with:
PORT=8443 ./run-cloud.sh— change the local HTTPS port.NO_LOCAL_WEB=1 ./run-cloud.sh— pure cloud uplink, no LAN web server.
Step 2 — pair the proxy to your account
While the proxy is unclaimed (not yet attached to any account), the broker issues it a short pairing code, which the launcher highlights in the console:
>>> PAIRING CODE for "home": XXXX -- enter it in a client Cloud hub panel to claim this proxy <<<
Open the Cloud hub panel in any client — web, desktop GUI or Android — and enter the code. That claims the proxy under your account:
- The broker stops issuing pairing codes for it.
- On later starts the launcher prints
>>> proxy "home" is already claimed -- authorized clients can connect now <<<. - Only clients authorized under your account can reach it. The broker signs session tokens with its key, and your proxy verifies them against the hub public key it was started with — the broker can’t be silently swapped out from under you.
If the code scrolled away, it’s in the proxy log; when running as a systemd service:
journalctl -u podsdr-proxy | grep "with code:"
Step 3 — connect from anywhere
In a client’s Cloud hub panel, your claimed proxies (and the radios each one offers) are listed once you’re signed in to the same account. Pick one and connect — the audio/IQ/control traffic flows over a WebRTC connection negotiated through the broker, traversing NAT on both ends.
Self-hosting the broker
run-cloud.sh targets the public broker via two environment variables you can
override:
BROKER_URL=wss://my-broker.example HUB_PUBLIC_KEY=<hex key> ./run-cloud.sh shack
The broker (podsdr-broker) is designed to deploy on fly.io;
see docs/deploy-fly.md in the repository for the deployment walkthrough. The
HUB_PUBLIC_KEY must be the public half of your broker’s signing key —
proxies refuse session tokens signed by any other broker.
Running as a system service
For an always-on operating position (a Pi in the shack, a shack PC), install the proxy as a service so it starts at boot and restarts if it crashes. Every server bundle ships the pieces for its platform.
All service setups below serve HTTPS on port 8080 with the auto-generated
self-signed certificate. The working directory matters: the certificate
(cert.pem / key.pem) is created in the working directory on first run,
so keeping it stable means browsers only see the certificate warning once.
Linux / Raspberry Pi (systemd)
The bundle contains podsdr-proxy.service. Install the bundle to
/opt/podsdr and enable the unit:
sudo cp -r podsdr-proxy-pi-bundle /opt/podsdr
sudo cp /opt/podsdr/podsdr-proxy.service /etc/systemd/system/
sudo systemctl enable --now podsdr-proxy
The unit runs:
/opt/podsdr/podsdr-proxy --static-dir /opt/podsdr/web --port 8080 --bind 0.0.0.0 --tls
with WorkingDirectory=/opt/podsdr and Restart=always. Check the User=
line matches an account that exists on your system (the Pi bundle uses pi,
the x86_64 bundle uses podsdr) and edit it if needed, then:
sudo systemctl status podsdr-proxy # is it running?
journalctl -u podsdr-proxy -f # live logs
Cloud uplink as a service: the shipped unit serves the LAN only. To run the broker uplink at boot, add the
--broker-url/--hub-public-key/--proxy-nameflags from the cloud chapter to theExecStartline. The pairing code (needed only once, while unclaimed) appears injournalctl -u podsdr-proxy | grep "with code:".
macOS (launchd)
The macOS bundle contains org.podsdr.proxy.plist, a LaunchDaemon that runs
the same command from /opt/podsdr:
sudo mkdir -p /opt/podsdr && sudo cp -r podsdr-proxy-macos-bundle/* /opt/podsdr/
sudo cp /opt/podsdr/org.podsdr.proxy.plist /Library/LaunchDaemons/
sudo launchctl load -w /Library/LaunchDaemons/org.podsdr.proxy.plist
Logs go to /opt/podsdr/proxy.log. Stop with
sudo launchctl unload -w /Library/LaunchDaemons/org.podsdr.proxy.plist.
If Gatekeeper blocks the binaries (they are ad-hoc signed, not notarized), clear the quarantine flag once after extracting:
xattr -dr com.apple.quarantine .
Windows (NSSM)
The proxy is a plain console program, so wrap it with a service manager such as NSSM:
nssm install PodSDRProxy "C:\podsdr\podsdr-proxy.exe" ^
--static-dir "C:\podsdr\web" --bind 0.0.0.0 --port 8080 --tls
nssm set PodSDRProxy AppDirectory "C:\podsdr"
nssm start PodSDRProxy
AppDirectory plays the role of the working directory — set it so the TLS
certificate persists across restarts.
podsdr-proxy command-line reference
podsdr-proxy [OPTIONS]
podsdr-proxy configure [--config <PATH>]
The run.sh / run-cloud.sh launchers are thin wrappers over these flags —
use them directly when you need a non-default setup.
Precedence: the configuration file is the base; any flag you pass explicitly on the command line overrides the matching config value for that run.
Subcommand: configure
podsdr-proxy configure
An interactive terminal wizard that edits the config file: add/remove CAT
radios (with serial-port autodetection), hide discovered OpenHPSDR radios,
and so on. It writes the same proxy.toml described in
Configuration file and exits. The same settings can also be
edited from the web client’s config panel when browsing the proxy on the LAN.
Server options
| Flag | Default | Meaning |
|---|---|---|
--config <path> | <config-dir>/podsdr/proxy.toml | Config file to load (~/.config/podsdr/proxy.toml on Linux, ~/Library/Application Support/podsdr/proxy.toml on macOS). |
--port <n> | 8080 | HTTP(S) listen port. |
--bind <addr> | 0.0.0.0 | Listen address. |
--static-dir <path> | (none) | Directory with the web client to serve (the bundle’s web/). Without it the proxy only offers the API/WebRTC endpoints. |
--origin <url> | (all allowed) | Allowed CORS origin; repeatable. |
--max-sessions <n> | 32 | Maximum concurrent client (WebRTC) sessions. |
TLS options
| Flag | Default | Meaning |
|---|---|---|
--tls | off | Serve HTTPS. With no cert flags, a self-signed cert.pem/key.pem is generated next to the binary (or in ~/.podsdr if that isn’t writable) on first run and reused for 10 years, so the browser warning appears once per profile. |
--tls-cert <path> / --tls-key <path> | (auto-generate) | Use your own certificate/key PEM pair (must be given together). |
--tls-san <name-or-ip> | (none) | Extra SubjectAltName baked into the auto-generated certificate; repeatable. The certificate always includes localhost, pi.local, 127.0.0.1 and ::1 — add your host’s LAN IP or hostname here so browsers don’t report a name mismatch. |
Changing SANs after the certificate exists has no effect — delete
cert.pem/key.pemto force regeneration.
Cloud uplink options
| Flag | Default | Meaning |
|---|---|---|
--broker-url <url> | (none — uplink off) | Broker to uplink to, e.g. wss://podsdr-broker.fly.dev. |
--proxy-name <name> | (none) | Human-friendly name shown to your clients (e.g. home, shack). |
--hub-public-key <hex> | (none) | The broker’s Ed25519 public key. Required for any non-localhost broker — the proxy uses it to verify the broker’s signed session tokens. |
--identity-file <path> | <config-dir>/podsdr/proxy_ed25519.key | This proxy’s identity keypair; generated on first use. The public key is the proxy’s permanent ID — keep the file to keep the identity (and the claim on your account). |
--insecure-open-mode | off | Allow a non-localhost broker without a hub key. Every relayed client then gets full TX rights. Development only. |
Traditional-radio quick flags
These exist for the simple one-radio case; for anything more (several radios,
network Icoms, per-radio options) use the configuration file.
--cat-port replaces the config file’s [[traditional]] list for that
run.
| Flag | Default | Meaning |
|---|---|---|
--cat-port <path> | (none) | Serial device of the CAT radio, e.g. /dev/ttyUSB0, /dev/cu.usbmodem1101, COM5. |
--radio <model> | x6100 | Which radio is on that port: x6100, qmx, ic7300, ic705, ftdx10, ftdx3000, ft991a, g90, fx4cr-v1, fx4cr-v2. |
--audio-in <name> | (system default) | Audio capture device = the radio’s USB-audio output (RX audio). |
--audio-out <name> | (system default) | Audio playback device = the radio’s USB-audio input (TX audio). |
--force-data-mode <bool> | true | Switch the radio to its DATA mode on TX so its mic processing stays out of the audio path. |
--baud <rate> | (model default) | CAT serial baud override. |
--tx-audio-gain-db <dB> | 0 | TX audio makeup gain (−40…+40 dB) into the rig; also adjustable live from clients. |
Logging
RUST_LOG=info ./podsdr-proxy ...
Without RUST_LOG the proxy logs warnings globally and info-level messages
from its own components (discovery, sessions, audio, CAT). Set
RUST_LOG=debug for protocol-level detail.
Configuration file
Everything the proxy can do is configurable in one TOML file:
- Linux:
~/.config/podsdr/proxy.toml - macOS:
~/Library/Application Support/podsdr/proxy.toml - Anywhere: pass
--config <path>
Three ways to edit it:
- The web client’s config panel — browse to the proxy on the LAN and open the configuration UI. It edits CAT radios (with dropdowns of detected serial ports and audio devices), composite radios, discovery options and cloud settings, and applies changes live — connected sessions on an edited radio are reconnected automatically. For safety this panel only works from the proxy’s own LAN origin, never through the cloud broker.
- The terminal wizard —
podsdr-proxy configure(works over SSH). - A text editor — the schema below.
Every field is optional. Command-line flags override their config counterparts for that run. If the file has a syntax error the proxy still starts, with defaults, and prints a prominent warning.
Full example
[server]
port = 8080
bind = "0.0.0.0"
static_dir = "/opt/podsdr/web"
max_sessions = 32
tls = true
tls_sans = ["pi.local", "192.168.1.10"]
# origins = ["https://pi.local:8080"] # restrict CORS; default: allow all
# tls_cert = "/etc/ssl/my-cert.pem" # bring your own cert (with tls_key)
# tls_key = "/etc/ssl/my-key.pem"
[cloud]
broker_url = "wss://podsdr-broker.fly.dev"
proxy_name = "My Shack"
hub_public_key = "<64 hex chars>"
# identity_file = "proxy_ed25519.key" # this proxy's identity keypair
# Station-wide receive-only ranges (Hz) — TX is blocked on EVERY radio here.
rx_only_bands = [[144000000, 148000000]]
# ---- Traditional (CAT) radios — one [[traditional]] block per radio ----
[[traditional]]
id = "shack-qmx" # stable id (used by composites); optional
name = "Shack QMX" # label shown in clients
port = "/dev/cu.usbmodem1101" # CAT serial port
radio = "qmx" # model, see the supported list
audio_in = "QMX" # capture device = the rig's USB-audio OUT
audio_out = "QMX" # playback device = the rig's USB-audio IN
force_data_mode = true # default true
baud = 9600 # override the model default
tx_audio_gain_db = 12.0 # −40…+40 dB makeup gain into the rig
rx_audio_gain_db = -6.0 # −40…+40 dB makeup gain from the rig
cw_key_line = "dtr" # "dtr" or "rts"; CW keying line
# cw_key_port = "/dev/cu.usbserial-KEY" # separate keying port, if any
# rx_only_bands = [[7000000, 7200000]] # per-radio receive-only ranges
[[traditional]]
name = "Shack IC-7300"
port = "/dev/cu.usbserial-IC7300"
radio = "ic7300"
baud = 115200
cat_spectrum = true # use the rig's own CI-V scope as panadapter
[[traditional]]
name = "IC-705 (WLAN)" # a NETWORK Icom: set host, no serial port
host = "192.168.1.50"
# control_port = 50001 # default
username = "operator"
password = "secret"
cat_spectrum = true
# ---- Composite radios — combine two radios into one ----
[[composite]]
name = "FTDX + Hermes panadapter"
[composite.roles]
tx = "shack-ic-7300" # who transmits
rx_audio = "shack-ic-7300" # who you listen to
spectrum = "remote-hermes" # who draws the panadapter
# control = ... # defaults to the tx radio
[composite.tracking]
spectrum_follows = "rx" # "rx", "tx" or "fixed"
# rx_offset_hz / rx_cal_ppm / spectrum_offset_hz / spectrum_cal_ppm
[composite.tx_sequencing]
# pre_tx_delay_ms / hang_ms
# rx_mute_on_tx = true
# spectrum_freeze_on_tx = true
# ---- Discovery: control what shows in the device list ----
[discovery]
disabled = ["AA:BB:CC:DD:EE:FF"] # hide these Hermes radios (by MAC)
[discovery.names] # rename radios in the picker
"11:22:33:44:55:66" = "Shack Hermes"
[discovery.rx_only] # receive-only ranges per Hermes MAC
"11:22:33:44:55:66" = [[144000000, 148000000]]
[[discovery.hosts]] # manual host: reach a Hermes that
id = "remote-hermes" # broadcast discovery can't see
addr = "10.8.0.5" # (VPN / routed subnet)
name = "Remote Hermes"
Section notes
[server] mirrors the CLI flags of the same names — see the
command-line reference.
[cloud] — see Remote access through the cloud broker. The
proxy’s identity keypair normally lives in proxy_ed25519.key next to the
config; keep it — its public key is the proxy’s permanent ID and what your
account’s claim is attached to.
[[traditional]] — see Traditional (CAT) radios
for models, audio wiring and feature details.
[[composite]] — see Composite radios.
Receive-only bands exist at three levels: station-wide
(rx_only_bands at the top of the file), per CAT radio
(rx_only_bands inside a [[traditional]] block), and per Hermes radio
([discovery.rx_only]). They merge. While tuned inside a receive-only range,
the proxy refuses PTT, TUNE, CW and keyer macros server-side, and clients grey
out their PTT button (it reads “RX ONLY”).
Manual hosts ([[discovery.hosts]]) are also an allow-list: the proxy
only relays to radios it discovered on the LAN or that you listed here, so
this is how you attach a Hermes reachable over a VPN or another subnet.
Traditional (CAT) radios
The proxy can host conventional transceivers alongside SDRs: it controls the radio over CAT (serial or network), moves RX/TX audio over the radio’s USB audio interface, and presents the result to every PodSDR client like any other radio — same spectrum display, same controls, same web/cloud access.
Supported models
| Radio | radio = | Notes |
|---|---|---|
| Xiegu X6100 | x6100 | CI-V (R1CBU/Aether firmware supported) |
| Xiegu G90 | g90 | CI-V subset |
| QRP Labs QMX / QMX+ | qmx | Kenwood TS-480-style ASCII CAT |
| Icom IC-7300 | ic7300 | CI-V; supports cat_spectrum |
| Icom IC-705 | ic705 | CI-V over USB; supports cat_spectrum |
| Icom network radios | (set host instead of port) | IC-705 over WLAN, IC-9700, IC-7610, IC-905, IC-R8600, or any rig behind RS-BA1 — control and audio over the network, no cables |
| Yaesu FTDX10 | ftdx10 | |
| Yaesu FTDX3000 | ftdx3000 | |
| Yaesu FT-991A | ft991a | |
| BG2FX FX-4CR | fx4cr-v1 | stock firmware (TS-590S protocol) |
| BG2FX FX-4CR (f5bud firmware) | fx4cr-v2 |
Quick single-radio setup (CLI)
./podsdr-proxy --static-dir ./web --tls \
--cat-port /dev/ttyUSB0 --radio ic7300 \
--audio-in "USB Audio CODEC" --audio-out "USB Audio CODEC"
For more than one radio, or a network Icom, use [[traditional]] blocks in
the configuration file (or the web config panel / the
podsdr-proxy configure wizard, both of which can autodetect the serial
port).
Audio wiring
Traditional radios deliver audio, not IQ, so get the direction right:
audio_in— a capture device on the proxy host: the radio’s USB-audio output (what you hear).audio_out— a playback device: the radio’s USB-audio input (what gets transmitted).
Level trims: rx_audio_gain_db (config/web UI) balances quiet rigs against
your SDRs; tx_audio_gain_db (config, CLI, or live from the client’s audio
panel — the “TXa” control) sets drive into the rig. Both are −40…+40 dB,
0 = unity.
force_data_mode = true (the default) flips the radio into its DATA mode when
transmitting, so the rig’s own mic compressor/EQ stays out of a path that
PodSDR is already processing.
Panadapter
By default the spectrum display for a CAT radio is computed from its RX audio,
so it’s only as wide as the receiver’s filter. On radios with a CI-V scope
(IC-7300, IC-705 — USB or network), set cat_spectrum = true to stream the
radio’s own wideband panadapter instead. A high CI-V baud rate (e.g. 115200)
makes the scope noticeably smoother.
CW keying
CW is keyed over a serial control line: cw_key_line = "dtr" or "rts",
optionally on a dedicated cw_key_port separate from the CAT port. The proxy
buffers keying ~150 ms to absorb network jitter, so remote CW stays clean.
Network Icom radios
Set host (plus username/password, and control_port if not 50001) in a
[[traditional]] block and leave port unset. Control, RX audio and TX audio
all travel over the Icom network protocol — no USB or audio cables. The CI-V
scope (cat_spectrum) works over the network too.
Receive-only bands
Per-radio rx_only_bands = [[low_hz, high_hz], ...] blocks all transmission
(PTT, TUNE, CW, macros) while tuned inside a range — enforced by the proxy,
not just the client UI. Useful for rigs whose PA must not be keyed on some
bands, or transverter setups. Station-wide ranges can be set at the top level
of the config; see Configuration file.
Sharing caveat
A serial port is exclusive: one client session drives a given CAT radio at a time. A second client trying to connect to the same CAT radio will fail to open the port. (Cloud listener roles are the exception — they receive audio without contending for control.)
Composite radios
A composite radio combines two radios the proxy already drives into a single virtual device, assigning each role to whichever radio is best at it. Clients see one ordinary radio — one spectrum, one set of controls, one PTT.
Classic use cases:
- Great transceiver, no panadapter — e.g. FTDX3000 for TX and RX audio, a Hermes for the wideband waterfall.
- RX-only SDR + any transmitter — listen on the SDR, key the rig.
You do the antenna wiring (separate antennas, a T/R relay off the rig’s RX-OUT jack, or an IF tap). The proxy handles everything else: command routing, frequency tracking between the two radios, TX/RX muting, and merging the two radios’ capabilities.
Defining one
In the configuration file (or the web config panel, which validates role references for you):
[[composite]]
name = "FTDX + Hermes panadapter"
[composite.roles]
tx = "shack-ftdx" # who transmits (PTT, CW, drive)
rx_audio = "shack-ftdx" # whose audio you hear
spectrum = "shack-hermes" # who draws the panadapter
# control = "shack-ftdx" # frequency/mode master; defaults to the tx radio
[composite.tracking]
spectrum_follows = "rx" # "rx" (default follow), "tx", or "fixed"
# rx_offset_hz = 0 # constant offset between the two radios
# rx_cal_ppm = 0.0 # frequency-calibration trim
# spectrum_offset_hz = 0
# spectrum_cal_ppm = 0.0
[composite.tx_sequencing]
# pre_tx_delay_ms = 0 # relay settling time before RF
# hang_ms = 0 # delay before returning to RX
# rx_mute_on_tx = true # mute the RX radio while transmitting
# spectrum_freeze_on_tx = true # freeze the waterfall while transmitting
Each role names a radio by its stable id: the id you gave a
[[traditional]] radio, a manual host’s id, a Hermes MAC address, or a
literal IP. The proxy refuses to save a composite whose references are unknown
or ambiguous.
Composites appear in every client’s device list as their own entry (protocol
COMPOSITE) next to the radios they’re built from. Connecting to one starts
both child radios; the usual sharing rules still apply (a CAT radio’s serial
port is exclusive, so don’t connect to the composite and its child at once).
Tracking and TX behavior
spectrum_follows = "rx"keeps the panadapter centered on where you’re listening;"tx"follows the transmit frequency (useful with split);"fixed"leaves the spectrum radio where you park it.- The offset/ppm fields absorb a constant frequency difference between the two radios (transverters, IF taps, calibration error).
- On PTT the proxy sequences the changeover: optional pre-TX delay for your relay, RX muting and waterfall freeze during TX (both on by default), and an optional hang time before RX resumes.
Using the emulator (podsdr-emu)
podsdr-emu emulates an OpenHPSDR Protocol 1 radio on your LAN. Any
PodSDR client (and other HPSDR software) discovers and connects to it exactly
as it would to real hardware. It ships inside every
server bundle
next to podsdr-proxy.
Typical uses:
- Try the PodSDR clients before you own a radio.
- Test a proxy/web/cloud deployment end-to-end without keying real RF.
- Host a shared virtual band where several operators hear each other
(
--echo-live).
Starting it
./podsdr-emu --radio hermeslite
The emulator binds UDP port 1024 on all interfaces and answers discovery broadcasts. Clients on the same LAN (or the same machine) will list it like a real radio.
Linux note: port 1024 is the last privileged port, so on some Linux systems the emulator needs either root or the bind capability:
sudo setcap 'cap_net_bind_service=+ep' ./podsdr-emu
What you will see and hear
Out of the box the emulator produces a flat noise floor — no test tones or recorded signals. RX meters read the noise floor; when you transmit, the emulator synthesizes plausible power / SWR / supply-voltage meter readings (no actual RF anywhere).
To hear something, use an echo mode: the emulator records your own TX IQ and plays it back on RX, shifted to whatever frequency your receiver is tuned to (attenuated 30 dB, so it sounds like a signal, not a blast):
--echo— loop mode. Records while PTT is down, then loops the recording forever (leading/trailing silence trimmed, a half-second gap between repeats). Successive PTT cycles within 30 seconds append to the same recording, so slow CW sent letter-by-letter accumulates into one message. Great for testing your own TX audio or CW by yourself.--echo-live— live mode. Your TX appears on RX in near real-time (~21 ms delay) and plays once. Multiple simultaneous transmitters on the same frequency are mixed together like real co-channel traffic — this is the mode for hosted multi-operator “virtual band” servers.
Echo recordings are capped at 10 seconds. In loop mode, only one operator per
frequency is mixed correctly; use --echo-live for concurrent operators.
Command-line reference
podsdr-emu --radio <RADIO> [options]
| Flag | Default | Meaning |
|---|---|---|
--radio <name> | (required) | Hardware to emulate: atlas, hermes, hermesii, angelia, orion, orionmkii, hermeslite, saturn, saturnmkii. (hermeslite presents as a Hermes Lite.) |
--mac <hex> | 02:AA:BB:CC:DD:EE | MAC address reported in discovery. Give each emulator its own if you run several. |
--noise <f> | 1.26e-5 | Noise-floor level as a fraction of full scale. |
--echo | off | Loop-back echo mode (see above). |
--echo-live | off | Live echo mode (see above). |
--bind <ip> | 0.0.0.0 | Bind a specific interface. Use when a client discovers the emulator via the wrong interface (e.g. a VPN). |
--max-clients <n> | 32 | Maximum concurrent client sessions. Raise for hosted multi-user deployments. |
-v, --verbose | off | Debug logging. |
There is no port option — the Protocol-1 port is fixed at UDP 1024, so you
can run at most one emulator per IP address. To run two on one machine, bind
them to different addresses with --bind.
Recipes
Emulator + desktop GUI on one machine — start the emulator, then the GUI; it appears in the radio picker:
./podsdr-emu --radio hermeslite
Emulator + proxy + web client (the emulator and proxy can share a host):
./podsdr-emu --radio hermeslite --echo-live &
./run.sh # proxy; open https://<host>:8080, the emulator is in the device list
Shared virtual band on a public server — run podsdr-emu --echo-live and
podsdr-proxy (with a cloud uplink if you like) on one server; every
connected operator hears everyone transmitting on the same frequency.
Limitations
- Protocol 1 only — no Protocol 2 emulation.
- Everything is synthetic: no propagation, fading, AGC-capture or band effects, and TX power/SWR readings are cosmetic.
- Sample rate follows the client (48/96/192/384 kHz; starts at 48 kHz).
- One session per client address; additional connect attempts from the same address are ignored while a session is active.
Desktop GUI
The desktop client (podsdr-gui, window title PodSDR SDR Radio) is the
full-featured native operating position for Linux, macOS and Windows.
Installation is covered in Downloads and
Quick start.
Connecting
The PodSDR Discovery panel at the top offers three tabs:
- Direct — find radios on the local network (direct UDP, no proxy).
Discover broadcasts on the LAN; Discover at IP targets an address
you type. This finds OpenHPSDR radios and a running
podsdr-emu. - Local proxy — enter a proxy URL (e.g.
https://pi.local:8080) to list and connect to that proxy’s radios over WebRTC — including CAT radios and composites the proxy hosts. - ☁ Cloud — sign in to the broker (callsign/email + password; hub URL
and token under Advanced, default
wss://podsdr-broker.fly.dev), claim a proxy with its pairing code, then connect to any of its radios from anywhere. See the cloud chapter.
Two extra buttons live here: Emulator (Off/On) spins up a built-in Hermes Lite emulator in-process and connects to it (with an Emu Settings noise slider), and Wipe all settings resets the app (with confirmation).
Each radio keeps its own profile — connecting to a different radio restores that radio’s frequencies, mode, DSP and calibration.
The main window
The whole window is spectrum + waterfall, with controls overlaid:
- Click to tune (snaps to the tune step); drag the band to slew. A toggle switches drag precision between Fast · step and Slow · 1 Hz.
- Drag the filter edges in the shaded passband to set custom low/high cuts.
- Scroll the mouse wheel over the frequency display to tune by the current step; click the digits to type a frequency.
- S-meter top right (IARU scale with peak hold,
S9+20 (-53 dBm)-style readout). During TX it’s replaced by FWD/REV power, SWR (color-coded), MIC and ALC bars, and a mic-clip counter. - Status pills along the top are live and clickable: NB, NR, ANF, AGC open mini editors; Split/RIT/XIT badges, CW pitch/WPM, codec, CPU%, RTT and a connection-health dot round it out.
Frequency & bands. Large VFO readout (active VFO), dimmed second VFO, a row of band buttons (edit the set via 📻 Bands…), and per-band memories — each band remembers its VFO A/B, split and mode.
Modes: LSB, USB, AM, FM, CWU, CWL, DSB, SAM, DIGL, DIGU, with mode-aware filter preset chips F1–F6 next to the picker.
RX row: Volume, Drive, Attenuator (0–31 dB), tune Step (1 Hz…1 MHz).
VFO extras (capability-gated — hidden if the radio doesn’t support them): VFO A/B, A=B, A↔B, SPLIT with quick +1/+2/+5 kHz DX offsets, RIT/XIT with drag offsets, momentary REV, and DW — dual watch, VFO A in the left ear and VFO B in the right.
The control tabs
A tab strip along the bottom:
- DSP-RX — noise blanker (+threshold), noise reduction (NR1 spectral subtraction, RNN Orig stock RNNoise, RNN Ham and SSB RNN ham-trained neural models, Multiband expander), auto-notch (ANF), out-of-phase headphone mode, superhet emulation, and AGC (Fixed/Long/Slow/Medium/Fast with target, decay, and manual gain when Fixed).
- Filter — all presets plus custom low/high; in CW also pitch, rise time and click-tune offset.
- TX-Audio — mic gain (+AUTO), Tune mic (records 5 s and proposes settings), CESSB, ALC, compressor (Legacy or Spectral with profiles), 3-band EQ, leveler, VOX (threshold + hang), and TX makeup gain for CAT radios.
- Monitor/Rec — TX monitor with its own audio-spectrum window, RX
recording to WAV (48 kHz, saved under the app’s
recordings/folder), a 10-second TX-loop record/preview buffer (never transmitted), and the voice keyer: four message slots with Record / Play (preview) / Send (on-air) / Delete, a Repeat loop for contest CQs, and Stop. - Display — waterfall palette (Classic SDR, Sunrise, Cool Blue, Grayscale, Viridis, Volcano, Blue-Orange), smoothing, gamma, swipe sensitivity.
- Keyer and CW Macros — appear in CW modes; see below.
CW
The keyer runs client-side even on proxy connections, so paddle-to-sidetone latency stays sub-millisecond.
- Modes: Iambic A / Iambic B / Straight / Bug / Ultimatic / Single paddle; 5–60 WPM, weight, Iambic-B timing, auto letter spacing, reversed paddles, QSK hang, sidetone volume/delay. Settings adds pitch, rise time, Farnsworth, key compensation, dynamic dit/dah ratio and paddle-memory style. Practice keys sidetone without transmitting; a CW reader decodes received CW on screen.
- Paddles: keyboard (default Z=dit, X=dah), serial dongles (pick port + CTS/DSR/DCD/RI pins), or MIDI keyers (pick device, then MIDI-learn dit/dah).
- Macros: M1–M8 (default hotkeys F1–F8) with
<MYCALL>/<CALL>variables, plus a free-text Send: field — type and press Enter to key a one-off message.
Working with other software
Settings → CAT Control (rigctld) starts a Hamlib NET rigctl server (default port 4532). WSJT-X, loggers and anything Hamlib-compatible can read/set frequency, mode and PTT. This is the FT8 path: the desktop GUI has no built-in FT8/FT4 decoder — run WSJT-X against rigctld and the GUI’s audio devices, or use the standalone web decoder. In DIGU/ DIGL the GUI automatically bypasses CESSB/compressor/leveler during TX so data signals stay clean.
Keybindings
⌨ Keybindings… (in Settings) maps keyboard and MIDI to nearly every action — tuning, band/mode/filter changes, AGC/NB/NR/ANF, volume, PTT (toggle and hold-to-talk), TUNE, CW paddles and macros, WPM, and more. Each action has a primary and an alternate slot; click a slot and press the key or MIDI control to capture it. Continuous actions (volume, drive, WPM…) bind to MIDI faders (absolute) or encoders (relative). A dedicated MIDI device can be selected for bindings, or share the keyer’s. Defaults include ←/→ tune (Shift = coarse), ↑/↓ step, +/− volume, M mode, Space PTT.
Settings that matter
- Audio Devices — input (mic) and output pickers.
- DSP Pipeline — Full (f64, best quality, default), Lite F32, Lite Q15, Lite uSDX.
- Remote proxy — audio codec (PCM ~770 kbps, µ-law, Opus 32/64 kbps, Adaptive PCM default — filter-sized, lossless on CW/SSB), waterfall stream on/off, bins (512–4096) and rate (3/6/12 Hz) — the knobs for slow links.
- TX Options — tune power, SWR protection (auto TX cutoff with threshold and dwell; a red alarm banner with Reset appears when it trips).
- Calibration — QRG offset, ADC gain (set a known −73 dBm signal to read S9), TX audio gain for CAT rigs.
- Appearance — ☀ Light / 🌙 Dark / 🖥 System (quick-toggle in the HUD too).
- Profiles — named snapshots of everything; per-radio state is kept automatically regardless.
Settings live in podsdr-gui/settings.json under your OS config directory
(~/.config/podsdr-gui/ on Linux, ~/Library/Application Support/podsdr-gui/
on macOS), auto-saved about once a second. A corrupted file is moved aside and
the app starts with defaults rather than crashing. Reset All Settings is
under Settings → System.
Current limitations
- Spectrum zoom/pan isn’t wired yet — the display spans the receiver’s sample-rate bandwidth.
- No squelch control in this UI.
- No TCI server (use rigctld); no built-in FT8 decoding (see above).
Web client
The web client is the same radio experience in a browser — spectrum,
waterfall, RX/TX, CW keyer — with nothing to install at the operating
position. It is served by podsdr-proxy (see
Serving the web client on your LAN) or directly by the
cloud broker, and talks to radios over WebRTC.
Opening it
Browse to the proxy, e.g. https://pi.local:8080. On first visit per browser
profile you’ll accept the self-signed-certificate warning (see
why HTTPS).
The landing page (“PodSDR Discovery”) shows:
- Connection tabs: Local proxy (radios offered by the proxy that served the page, or any proxy URL you enter) and Cloud (the broker). There is no “Direct” tab — browsers can’t do LAN UDP, so the proxy always brokers.
- An Emulator (Off/On) toggle and a Hermes (Embedded) entry that is always in the device list — a complete virtual radio running inside the browser tab (browser-side DSP, no proxy or hardware involved). Ideal for a first look; once connected, an Emu Settings window sets its noise level.
- ⚙ Proxy config (only when browsing the proxy on the LAN) — the web editor for the proxy’s configuration: CAT radios, composites, discovery, cloud settings.
- Wipe all settings (with confirmation).
Each real radio row offers two connect buttons: Server DSP (demodulation runs on the proxy — lowest browser load, required for CAT and composite radios) and Client DSP (raw IQ streams to the browser and DSP runs locally). If the connection drops, the client shows Connection lost and reconnects automatically with backoff; Disconnect stops that.
Cloud tab
Sign in with callsign/email + password (hub URL and manual token under Advanced; the session token — never your password — is stored in the browser). Enter a proxy’s pairing code and Claim proxy to attach it to your account, then Refresh proxies and Connect to any online radio. When the page is served by the broker itself, links to the Account and Proxies pages appear here. Cloud connections always use server DSP and include TURN relaying (even over 443) for restrictive networks.
Browser requirements
Any modern browser works. The page must be a secure context (HTTPS, or
http://localhost) — otherwise the browser withholds low-latency audio
(AudioWorklet), the GPU spectrum, mic capture and MIDI. Spectrum rendering
uses WebGPU when available and falls back to WebGL2. MIDI paddles/keybindings
need a browser with WebMIDI (Chrome and derivatives; Firefox behind a flag).
On phones/tablets the layout adapts below ~700 px width.
Permission prompts you’ll see: microphone the first time you transmit, and MIDI on startup if the browser supports it.
Feature set
Operating the web client matches the desktop GUI almost one-for-one — the two share their control panels:
- Spectrum + waterfall with click-to-tune, drag tuning (Fast/Slow toggle) and filter-edge dragging; palettes/smoothing/gamma in Display.
- Full mode list, band buttons with per-band memories, VFO A/B, split, RIT/XIT, dual watch (stereo, capability-gated).
- The complete DSP-RX set: NB, NR (including the RNNoise neural models), ANF, AGC, superhet emulation — real DSP in the browser in client mode, mirrored to the proxy in server mode.
- TX with mic capture (echo cancellation and browser AGC deliberately off), CESSB/compressor/EQ/leveler/VOX, TUNE, TX meters.
- Voice keyer (4 slots + Repeat) and RX recording — recordings
download as
rx-<n>.wav(60 s max per file); voice messages persist in the browser’s local storage. - Keybindings editor (keyboard + MIDI) and the band editor.
- Theming: ☀ Light / 🌙 Dark / 🖥 System.
- A CW reader decodes received CW in every connection mode.
CW keying in the browser deserves a note: the keyer engine runs inside
the audio thread (an AudioWorklet), so sidetone and element timing are
sample-accurate even when the browser throttles the page. Paddle sources:
keyboard, WebMIDI, WebSerial. Macros M1–M8, <MYCALL>/<CALL> variables and
the free-text send field all match the desktop.
Backgrounded tabs keep working. Audio, keyer, TX and the connection keepalive are driven from the audio thread’s heartbeat, which browsers don’t throttle — you can switch tabs mid-QSO. Returning to the tab resumes instantly (mobile browsers suspend audio more aggressively; the client re-arms itself when you come back).
Bandwidth on slow links
Settings → Web Settings (server-DSP connections):
- Audio codec: PCM (~770 kbps), µ-law (~385 kbps), Opus 32 kbps, Opus 64 kbps, or the default Adaptive PCM — sized to the current filter, so CW/SSB stay lossless at a fraction of the rate.
- Waterfall stream off saves ~400 kbps; otherwise choose bins (512–4096) and rate (3/6/12 Hz).
- A 24-bit IQ option exists for high-fidelity client-DSP use.
Status pills show the active codec, round-trip time, and DSP load.
Quirks worth knowing
- Audio starts on your Connect click (browsers require a user gesture to start sound) — there is no separate “enable audio” step.
- If browser storage is corrupted, settings are moved aside and the client starts fresh with a notice.
Android app
The Android client (podsdr-android-arm64.apk) is a full operating position
on a phone or tablet: touch spectrum, RX/TX, CW keyer with real paddles over
USB, and background operation with lock-screen controls.
Installation
- Requirements: a 64-bit ARM device (arm64-v8a) running Android 10 or newer.
- The app is side-loaded (not on a store). Install over USB with
adb install -r podsdr-android-arm64.apk, or copy the APK to the phone and open it (you’ll need to allow installs from your file manager). - Upgrades install over the top and keep your settings.
On first use the app asks for the permissions it needs as you touch each feature: microphone (TX audio), notifications (the background session notification), and per-device USB access when you plug in a paddle or MIDI dongle.
Connecting
The start screen (“PodSDR Discovery”) has three tabs — the phone can use all three paths:
- Direct — talk straight to an OpenHPSDR radio (or
podsdr-emu) over LAN UDP. Tap Discover for a broadcast search, or enter an IP and tap At IP. An Embedded Emulator tile is always available here — a built-in virtual radio requiring no network at all. - Local proxy — enter a proxy URL (e.g.
https://pi.local:8080) to list that proxy’s radios and connect over WebRTC with server-side DSP. This is how you reach CAT radios and composites from the phone. - Cloud — sign in to the broker (callsign/email + password; hub URL and API token under Advanced), claim a proxy with its pairing code (Claim), then Refresh proxies and tap Connect on any online radio. Works from anywhere — cellular included.
Operating
Spectrum & tuning. Tap the spectrum to tune (snaps to the tune step); drag to slew with momentum. The pill at the top right toggles tuning precision: Fast · step / Slow · 1 Hz. Tap the frequency digits to type an exact frequency; [-] / [+] step by the tune step. Band buttons sit under the control row. Waterfall palette, smoothing and gamma are in Settings.
Bottom controls. Mode (LSB, USB, AM, FM, CWU, CWL, DSB, SAM, DIGL, DIGU — plus filter presets and tune step), Flt (filter presets 5.0k…1.0k and two custom slots), DSP (NB, NR — including RNNoise-based “RNN”, “Ham” and “SSB” models — ANF, AGC modes Fix/Lng/Slw/Med/Fst with target and decay, superhet emulation on Full DSP), Mic (volume, TX drive, attenuator, mic gain with AUTO, CESSB/EQ/leveler/compressor, VOX), Mon (monitor, recording, voice keyer), Spl (split, VFO A/B, swap/equalize, RIT/XIT), and in CW modes Key and Mcr.
Status chips across the top mirror the active DSP settings (NB, NR, AGC, Split, codec, CW pitch/WPM, CPU%) — tap a chip to jump to its panel. A CW reader overlays decoded CW copy beneath them.
PTT. The floating PTT button is tap to key, tap again to unkey — a long press picks it up so you can drag it anywhere (the position is remembered). During TX it shows forward/reverse power, SWR (color-coded), a mic-peak bar with clip counter, and an ALC bar. On a receive-only band it reads RX ONLY and won’t key. Hold-to-talk is available by binding a key of an external keyboard in Keybindings.
Dual watch (VFO A in the left ear, VFO B in the right) appears in the split panel when the connected radio supports it (Full-DSP connections; not CAT radios).
CW on Android
- Keyer modes: Iambic A/B, Straight, Bug, Ultimatic, Single paddle; WPM 5–60, weighting, Farnsworth, hang time, sidetone volume/pitch. A Practice switch keys the sidetone without transmitting.
- Paddles: plug in a USB dongle via OTG —
- MIDI keyers (USB MIDI) — pick the device in Settings and assign dit/dah with MIDI-learn.
- Serial dongles — FTDI, CH340, CP210x and generic USB-CDC (Arduino, RP2040…); the paddle contacts are read from the CTS/DSR/DCD/RI pins (CDC-ACM devices don’t expose CTS — wire to DSR/DCD/RI).
- Macros (Mcr): eight editable slots M1–M8 with
<MYCALL>/<CALL>variables, plus a free-text field — type a message and Enter/Send keys it once.
Voice keyer & recording (Mon panel)
- Record RX captures receive audio to a 48 kHz WAV in the app’s
recordings/folder. - Voice messages: four slots, each with Record / Play (local preview) / Send (on-air) / Clear, plus a Repeat toggle for contest CQ loops and a global Stop.
- TX monitor with its own volume, a short record/replay buffer for checking your own signal, and a draggable floating mini spectrum overlay.
Running in the background
While connected, a persistent notification (media-style, visible on the lock screen) shows the frequency, mode and live status — signal level in dBm on RX, power and SWR during TX — with a Disconnect action. Audio keeps flowing with the screen off; the app holds a wake lock for the session. Tap the notification to jump back into the app.
Working with other software
Settings → Rig Control (rigctld) exposes a Hamlib NET rigctl TCP server (default port 4532, off by default). A logger or WSJT-X on the LAN can read/set frequency and mode and key PTT — that is also the intended path for FT8: there is no built-in FT8 decoder on Android; use WSJT-X pointed at the phone’s rigctld, or the web decoder.
Settings highlights
- Profiles — save/restore named snapshots of your whole setup.
- Keybindings — full keyboard + MIDI binding editor (external keyboards work, including momentary PTT and paddle keys).
- Appearance — Dark / Light / System (also a quick 🌙/☀/🖥 toggle on the main screen).
- DSP pipeline — Full (f64) or Lite (f32 / Q15) backends.
- Remote proxy — audio codec (PCM, µ-law, Opus 32/64k, Adaptive PCM), waterfall on/off, bins and rate — the bandwidth savers for cellular use.
- Bands — edit band edges, names, and which bands show as buttons.
- If the settings file is ever corrupted, the app moves it aside on start and offers a clean Wipe all settings.
Limitations vs desktop
- No built-in FT8/FT4 decoding (use rigctld + WSJT-X) and no TCI server.
- No pinch-zoom on the spectrum (tap-to-tune + pan/fling + Fast/Slow toggle).
- USB serial is for paddle input only — CAT-over-USB radios are hosted by a proxy, not by the phone.
- arm64 only; Android 10+.
Standalone FT8/FT4 web decoder
podsdr-digimodes-web is a separate single-page app that decodes FT8, FT4
and FT2 in the browser using PodSDR’s parallel decoder — no radio, proxy
session or WSJT-X required. It is the digimode companion to the main clients
(which have no built-in FT8 decoding).
Running it
From a source checkout:
./run-digimodes-web.sh [port] # builds the WASM app and serves it on :8080
Then open http://localhost:8080.
Using it
The Controls panel on the left:
- Source — where the audio comes from:
- KiwiSDR — connect to a public KiwiSDR over WebSocket. Pick one of the presets (SK3W Sweden, Hack Green UK, NA5B Washington DC, Hungary, Various) or enter any host/port; a status line and Reconnect button track the link.
- Microphone — decode whatever your sound card hears (e.g. a receiver’s speaker output or an audio cable from your rig).
- Mode — FT8 (15 s cycles), FT4 (7.5 s) or FT2 (30 s). Decoding is aligned to real UTC cycle boundaries, marked as white lines on the waterfall.
- Waterfall — Min/Max dB sliders for display contrast.
The central panel is a GPU waterfall of the audio passband. Click or drag on the spectrum to select a frequency band (highlighted yellow): decodes are then split into All Decodes and Filtered Decodes columns. Each decode line shows time, SNR, time offset and audio frequency alongside the message text.
Ports & firewall
Everything at a glance
| Port | Protocol | Used by | Direction |
|---|---|---|---|
8080 (default, --port) | TCP — HTTP(S) | proxy web server + WebRTC signaling | inbound to the proxy |
| 1024 | UDP — OpenHPSDR Protocol 1 | radio ↔ proxy / desktop GUI / emulator (discovery + data) | LAN |
| 1024–1029 | UDP — OpenHPSDR Protocol 2 | radio ↔ proxy / desktop GUI | LAN |
| ephemeral high UDP | WebRTC media | proxy ↔ browsers/Android | LAN (or via TURN) |
| 443 | TCP — wss | proxy → cloud broker uplink; clients → broker | outbound only |
| 4532 (default) | TCP — Hamlib NET rigctl | desktop GUI / Android → WSJT-X, loggers | inbound to the client, off by default |
| 50001 (default) | UDP — Icom network control | proxy → network Icom radios | LAN |
Practical rules
LAN web operation (proxy on a Pi, browsers in the house): allow inbound TCP to the proxy’s port (8080) and UDP on high ports to the proxy host. WebRTC on the LAN path uses no STUN server at all, so no internet access is needed.
Cloud operation: the proxy makes only outbound connections — wss to
the broker (443) and UDP to the broker’s STUN/TURN servers. No inbound rules,
no port forwarding. That’s the point of the design.
Radio side: the proxy (or desktop GUI) must be able to exchange UDP with the radio on port 1024 (Protocol 1) / 1024–1029 (Protocol 2), including the initial broadcast discovery. Radios on other subnets won’t answer broadcasts — add them as manual hosts and make sure unicast UDP routes both ways.
Emulator: binds UDP 1024, which is a privileged port on some Linux
systems — grant it once with
sudo setcap 'cap_net_bind_service=+ep' ./podsdr-emu.
Troubleshooting
The universal first step for the server and desktop apps: run from a terminal with logging on.
RUST_LOG=info ./run.sh # both the server and GUI bundles ship run.sh
RUST_LOG=debug ./run.sh # protocol-level detail
(Windows: set RUST_LOG=info then run the .bat / .exe from a terminal.)
Installing & launching
macOS: “PodSDR can’t be opened” / app is killed immediately.
The bundles are ad-hoc signed, not notarized. Right-click → Open once, or
clear quarantine: xattr -dr com.apple.quarantine PodSDR.app (for the server
bundle: xattr -dr com.apple.quarantine . inside the bundle directory).
Linux GUI won’t start. Needs glibc 2.31+ (Ubuntu 20.04 / Debian 11+), an
X11 or Wayland session, ALSA/PulseAudio, and ideally a Vulkan-capable GPU
(wgpu falls back to GL). RUST_LOG=info ./run.sh shows which piece is
missing.
Windows: window never appears. Run podsdr-gui.exe from a terminal with
RUST_LOG=info; the GUI wants a DirectX 12-capable GPU/driver.
Browser & web client
Certificate warning on every visit / “connection not private”.
Expected once per browser profile with the self-signed certificate. If it
never sticks, or you connect by an address that isn’t in the certificate,
restart the proxy with --tls-san <your-ip-or-hostname> after deleting the
old cert.pem/key.pem (they’re next to the proxy binary).
ERR_SSL_PROTOCOL_ERROR. You opened https:// but the proxy is running
without --tls (plain HTTP). Use http://, or start with TLS (the default
run.sh does).
No microphone / MIDI / choppy audio over plain HTTP. Browsers withhold
mic capture, WebMIDI and low-latency audio outside a secure context. Serve
over HTTPS (the default) or via http://localhost only.
Radio list is empty in the browser. Check /api/devices on the proxy
(e.g. curl -k https://pi.local:8080/api/devices). If it’s empty, the proxy
can’t see the radio: same subnet? UDP 1024 open? For radios on other subnets
add a manual host.
Radio & connection
Desktop GUI doesn’t find the radio / emulator. Both ends must be on the
same broadcast domain. If discovery lands on the wrong interface (VPNs),
use Discover at IP, or start the emulator with --bind <lan-ip>.
Emulator fails to bind port 1024 on Linux.
sudo setcap 'cap_net_bind_service=+ep' ./podsdr-emu (or run it once with
sudo).
Emulator connects but I hear only noise. That’s what it produces by
default. Start it with --echo or --echo-live to hear your own TX back —
see Using the emulator.
“session limit reached” from the proxy. More concurrent clients than
--max-sessions (default 32). Raise it, or wait — dead sessions are reaped
within ~30 s.
Second client can’t connect to a CAT radio. Serial ports are exclusive; one control session per CAT radio at a time. Cloud listener roles can still monitor.
PTT greyed out / button reads “RX ONLY”. You’re tuned inside a configured receive-only range (or connected with a listener role). See receive-only bands.
Cloud
No pairing code in the console. Codes are only issued while the proxy is
unclaimed. If it was claimed before (message: already claimed), connect
with the owning account, or look in the logs:
journalctl -u podsdr-proxy | grep "with code:".
Proxy refuses to start the uplink (“hub public key”). A non-localhost
broker requires --hub-public-key. Get it from your broker deployment (the
public bundles bake in the public broker’s key); --insecure-open-mode
bypasses the check for development only — it gives every client TX rights.
Proxy shows online but connect fails from far away. Give it a moment — TURN relaying (including over 443) engages automatically on restrictive networks. Check the client-side stage label for where negotiation stops.
CAT radios
Serial port won’t open. Is the radio powered on and the cable in? Is
another program (logger, WSJT-X) holding the port? The proxy retries the
open briefly, then logs an actionable error. The web config panel and
podsdr-proxy configure can autodetect the right port.
TX audio too quiet/loud on a CAT rig. Adjust tx_audio_gain_db (also
live from the client’s TX-Audio panel); RX level via rx_audio_gain_db.
Icom scope panadapter is jerky. Raise the CI-V baud rate (e.g. 115200) on both the radio and the config.
Settings
All clients move a corrupted settings file aside (*.corrupt.<timestamp>)
and start with defaults, telling you about it — nothing is silently lost.
“Wipe all settings” (discovery screen) and “Reset All Settings” (desktop
Settings → System) are the explicit resets.