High-performance HF modem for amateur radio
Last updated: 2026-05-02
EXPERIMENTAL SOFTWARE — WORK IN PROGRESS
Active development. Not production-ready. APIs and protocols may change. Use at your own risk for experimentation and amateur-radio research.
ProjectUltra is a software modem for reliable HF data transfer. It ships three things in one repo:
- Modem core — adaptive OFDM + MC-DPSK waveforms with LDPC FEC and Selective-Repeat ARQ.
- GUI application — real-time waterfall, constellation, and message log (ImGui + SDL2).
- VARA-compatible TCP TNC —
ultra_tncexposes the modem via the same TCP command/data API used by Pat, Winlink Express, BPQ32, and other clients. Drop-in alternative on Linux, macOS, and Windows.
ProjectUltra exposes throughput at two layers. Both matter; they answer different questions.
Useful payload bits / second over the air, ignoring ARQ overhead and auto-rate adaptation. This is what one bulk frame delivers in steady state on a stable channel.
OFDM 1024-FFT, 59 carriers, CP=96, ~42.9 sym/s:
| SNR | Mode | Throughput | Notes |
|---|---|---|---|
| 5+ | MC-DPSK (8 car) | 938 bps | ±50 Hz CFO, robust sync |
| 8+ | OFDM-NARROW R1/4 | 103 bps | 500 Hz BW for crowded bands |
| 8+ | OFDM-NARROW R1/2 | 230 bps | 500 Hz BW |
| 10+ | OFDM DQPSK R1/4 | 1264 bps | Fading-tolerant baseline |
| 15+ | OFDM DQPSK R1/2 | 2271 bps | Good + moderate fading |
| 20+ | OFDM DQPSK R2/3 | 3028 bps | Good fading only |
| 20+ | OFDM DQPSK R3/4 | 3536 bps | AWGN only |
| 25+ | OFDM 16QAM R3/4 | 5657 bps | Stable paths (NVIS, ground wave) |
| 30+ | OFDM 32QAM R3/4 | 7071 bps | Stable paths only |
What a user actually sees over the cable / air, including handshake, ACK roundtrips, retransmissions, and auto-rate downgrades. Auto-rate ladder is on; Connection picks among R1/4 / R1/2 / R2/3 / R3/4 based on measured SNR and fading.
| Test | Channel | Wall | Throughput | Notes |
|---|---|---|---|---|
| 50 KB Mac↔Pi5 cable | Clean USB cable | 174 s | 2354 bps | Auto DQPSK R3/4 @ SNR=28 |
| 20 KB Mac↔Pi5 injected | AWGN, SNR=15 | 72 s | 2266 bps | Auto DQPSK R2/3, 0 retx, byte-exact |
| 20 KB Mac↔Pi5 injected | Watterson Good, SNR=15 | 100 s | 1631 bps | Auto DQPSK R1/2, 0 retx, byte-exact |
| 5 KB Mac↔Pi5 injected ×5 | Watterson Good, SNR=15 | 28 s | 1440 bps | Median of 5 seeds, R1/2; 5/5 PASS post-BUG-RATE-001 fix (worst-case 684 bps; was 444 bps pre-fix with R1/2→R1/4 panic) |
| 500 KB Mac↔Pi5 injected | Watterson Good, SNR=15 | 3742 s | 1094 bps | Long-haul, 1346 retx, 0 failed, byte-exact |
End-to-end results match or exceed real-world numbers reported for existing commercial HF data modems in equivalent conditions. The 500 KB Good15 result is the realistic-HF baseline; 50 KB cable is the upper bound given a clean channel.
Waveforms. MC-DPSK (chirp sync, low-SNR robust), OFDM-CHIRP (wideband 2.8 kHz, 59 carriers), OFDM-NARROW (500 Hz crowded-band mode), OFDM-COX (Schmidl-Cox sync), SC-DPSK (very low SNR).
Modulation + FEC. DBPSK / DQPSK / D8PSK / BPSK / QPSK with 802.11n LDPC at four code rates (R1/4, R1/2, R2/3, R3/4). Min-sum belief-propagation decoder.
Synchronization. Dual-chirp detection with FFTW-accelerated correlation, Schmidl-Cox training, light-preamble (LTS-only) for in-session frames, LTS-residual CFO refinement, per-symbol pilot tracking with common-phase-error correction.
ARQ. Selective-repeat with cumulative + selective ACKs, window 1 (DPSK / narrowband) or 8–16 (wideband OFDM), variable 1–8 codeword frames, wire-negotiated CW count, frame + optional channel + burst interleavers.
Adaptive rate. SNR + fading-index ladder (R3/4 / R2/3 / R1/2 / R1/4) with bootstrap cap, per-burst clean-window upgrade, two-window hysteresis on downshift to prevent panic-downshift on short fading transfers.
Per-carrier RX erasure. Each OFDM-CHIRP frame computes
γ_k = |H_k|² / σ²_k from its own LTS + pilots; carriers below
-6 dB emit LLR = 0 to LDPC after a persistence gate (3
consecutive symbols or 2 consecutive multi-CW frames). Bits are
spread across LDPC base columns by the CarrierLDPC v1
interleaver a = (307·i) mod (648·Ncw). Silent on AWGN /
light fading; converts deep stationary notches and in-band QRM
from a TEST FAILED into a clean decode (validated A/B on a
fixed -25 dB notched carrier: 2,271 bps vs 15-frame loss
baseline).
HARQ Chase soft-combining. LLR-accumulating buffer keyed by full PHY digest (rate, modulation, interleaver, carrier mask, erasure-policy epoch). Currently active only when the frame's header CW decodes; broader CW0-fail integration is in experimental branches awaiting further hardware validation.
Channel testing. Built-in Watterson HF channel injector (ITU-R F.1487) with AWGN, Good (0.1 Hz / 0.5 ms), Moderate (0.5 Hz / 1 ms), Poor (1 Hz / 2 ms) presets. Two-machine hardware harness (Mac ↔ Pi5 over USB sound cards) with byte-exact end-to-end validation.
Protocol v2. PING / PONG, CONNECT / CONNECT_ACK, MODE_CHANGE, DATA, ACK / SACK, DISCONNECT. Wire-level CRC-16 on every frame, capability flags, measured-SNR + fading-index exchange.
TNC integration. ultra_tnc daemon exposes the modem over
the same TCP command/data API used by Pat, Winlink Express,
BPQ32, and similar clients (cmd port 8300 / data port 8301);
verified end-to-end with real Pat sessions across all major
B2F message types.
GUI application. ultra_gui with real-time waterfall,
constellation, message log, and ARQ health view (ImGui +
SDL2). Virtual-station / simulator mode for development.
SNR Waveform Reason
─────────────────────────────────────────────────────────────────────
5–10 dB MC-DPSK (8 carriers) Differential encoding, dual-chirp sync
5–10 dB OFDM-NARROW (500 Hz) Crowded bands, low SNR
10–17 dB OFDM-CHIRP (1024) Dual-chirp sync, 59 carriers
17+ dB OFDM-COX (1024) Schmidl-Cox sync, faster acquisition
25+ dB NVIS OFDM 16QAM Coherent + pilot tracking
Selection happens during CONNECT (peer-advertised SNR + fading index)
and continues adapting during the session. See docs/PROJECT_GOALS.md
for the throughput/reliability targets driving this work.
ultra_tnc is a daemon that exposes ProjectUltra's modem through the
VARA HF TCP TNC protocol. Existing clients can connect to it the same
way they connect to a commercial HF data modem — no protocol changes
on the client side.
┌──────────────┐ TCP 8300 (cmd) ┌──────────────┐
│ Pat / │ TCP 8301 (data) │ ultra_tnc │ Audio ┌─────────┐
│ Winlink / │ ◄──────────────► │ (modem + │ ◄──────► │ HF │
│ BPQ32 / ... │ │ TCP shell) │ │ Radio │
└──────────────┘ └──────────────┘ └─────────┘
# Build (see Getting Started below)
cmake -S . -B build && cmake --build build -j 4
# Listen on default ports 8300/8301
./build/ultra_tnc --audio-output "USB Audio Device" \
--audio-input "USB Audio Device" \
--callsign N0CALL
# Smoke test from another terminal
printf 'VERSION\r' | nc 127.0.0.1 8300
# → VERSION 4.9.0Full command reference: docs/TNC_INTERFACE.md.
Standard VARA: VERSION, MYCALL, LISTEN, CONNECT, DISCONNECT,
ABORT, BW500 / BW2300 / BW2750, BUFFER, SN, BITRATE,
COMPRESSION, CHAT, CWID, plus Mercury / Pat-Vara compatibility
no-ops (PUBLIC, P2P, WINLINK, IGNOREKISSDCD, RETRIES,
CALLINT).
ProjectUltra extension:
STATS— single-line ARQ + PHY snapshot for debugging stalled sessions:frames_sent,frames_recv,retx,timeouts,failed,out_of_order, currentrate/mod/mode,snr,bps,backlog. Pat ignores unknown commands, so this is safe to leave on.
- Cross-platform: Linux + macOS + Windows. CI matrix all green.
- ctest: 37/37 (TNC parser, TCP integration, bridge tests, plus
modem regressions including the new
CarrierLDPC v1math gate and per-carrier mask plumbing). - End-to-end byte-exact transfers (hardware harness, Mac ↔ Pi5 over USB sound cards): see throughput table at the top of this README. 50 KB cable run hits 2,354 bps; injected Watterson Good at SNR=15 holds 1,631 bps clean; forced R3/4 at SNR=15 AWGN delivers 2,676 bps clean (2026-05-07 calibration).
- Real Pat client validated end-to-end with Pat 1.0.0 (Mac)
↔ Pat 0.15.1 (Pi5) over real audio cable: full B2F session
matrix passes byte-exact (empty connect/disconnect, text up
to 12.5 KB, binary attachments, bidirectional, both
directions). Five real bugs found + fixed during integration;
full audit at
docs/PAT_VARA_AUDIT.md. - Known TNC limitation: back-to-back sessions within ~30 s of
teardown don't always recover cleanly (~1/3 retry success).
Root cause traced upstream to pat-vara
(
vara.go:344-349drops inbound connection ifAccept()hasn't re-armed). Single-session flows are reliable. - Winlink Express on Windows: spec-compatible, not yet manually tested.
- Linux, macOS, or Windows
- CMake 3.16+
- C++20 compiler (GCC 10+, Clang 12+, MSVC 2019+)
- SDL2 (GUI + audio I/O for
cli_simulator/ultra_tnc) - FFTW3 (required for fast chirp detection — Cooley-Tukey fallback is unusable for real-time)
# Ubuntu/Debian
sudo apt install libsdl2-dev libfftw3-dev cmake build-essential pkg-config
# macOS
brew install sdl2 fftw cmake pkg-config
# Windows (vcpkg)
vcpkg install sdl2 fftw3
git clone https://github.com/secup/ProjectUltra.git
cd ProjectUltra
cmake -S . -B build
cmake --build build -j 4GUI (operator UI with waterfall and constellation):
./build/ultra_gui # Normal mode
./build/ultra_gui -sim # Developer / simulator mode (no radio needed)macOS Gatekeeper note (downloaded prebuilt binaries only). macOS quarantines anything downloaded from the internet. If you grabbed a release bundle and macOS shows "cannot be verified" when you try to launch it, run this once per binary to remove the quarantine flag:
xattr -d com.apple.quarantine /path/to/ultra_guiOr right-click the binary in Finder → Open → confirm Open in the warning dialog (one-time exception). Binaries built locally from source are not affected. Proper code-sign
- notarization is on the post-alpha release roadmap.
TNC (VARA-compatible TCP shell, see TNC section above):
./build/ultra_tnc --audio-output "USB Audio" --audio-input "USB Audio"CLI simulator (full protocol, two-station, channel injection):
./build/cli_simulator --snr 15 --channel good --rate auto --testCLI tools (single-frame transmit/decode, for offline analysis):
./build/ultra ptx "Hello" -s MYCALL -d THEIRCALL | aplay -f FLOAT_LE -r 48000
arecord -f FLOAT_LE -r 48000 | ./build/ultra prx┌────────────────────────────────────────────────────────┐
│ Pat / Winlink Express / BPQ32 / your own client │
├────────────────────────────────────────────────────────┤
│ ultra_tnc (TCP cmd 8300, data 8301) │
│ TNCSession (VARA command parser + state machine) │
│ TNCBridge (ModemAdapter ↔ ProtocolEngine) │
├────────────────────────────────────────────────────────┤
│ Connection (PING/CONNECT/MODE_CHANGE/DATA/DISC) │
│ ARQ (Selective-Repeat, window=16, SACKs) │
│ Frame v2 (4-CW fixed frames + 1-CW control) │
├────────────────────────────────────────────────────────┤
│ Waveforms (OFDM-CHIRP, OFDM-NARROW, OFDM-COX, │
│ MC-DPSK + adaptive selection) │
│ LDPC (IEEE 802.11n, R1/4 to R5/6) │
│ Sync / CFO (dual chirp + Schmidl-Cox + LTS) │
├────────────────────────────────────────────────────────┤
│ Audio I/O (SDL2 — Linux ALSA, macOS CoreAudio, │
│ Windows WASAPI) │
└────────────────────────────────────────────────────────┘
- PING/PONG — fast presence probe (~1 s each) before committing to a full CONNECT.
- CONNECT — callsign exchange (FCC Part 97.119 compliant) over MC-DPSK.
- MODE_CHANGE — picks data waveform/rate from peer-advertised SNR and fading.
- DATA — Selective-Repeat ARQ with cumulative SACKs.
- DISCONNECT — graceful with callsign ID.
If no PONG after 5 PINGs (~15 s), connection fails fast.
| Parameter | MC-DPSK | OFDM |
|---|---|---|
| Sample rate | 48 kHz | 48 kHz |
| Bandwidth | ~2.4 kHz | ~2.8 kHz |
| Center freq. | 1500 Hz | 1500 Hz |
| Carriers | 8 | 59 |
| FFT size | n/a | 1024 |
| Symbol rate | ~94 baud | ~42.9 baud |
| Cyclic prefix | n/a | 96 (~2 ms) |
| Sync | Dual chirp | Dual chirp / Schmidl-Cox |
| LDPC codeword | 648 bits | 648 bits |
cmake --build build -j 4
ctest --test-dir build --output-on-failure -j 434 tests covering modem primitives, protocol/ARQ, TNC parser, TNC TCP reactor, and TNC bridge. CI runs the full matrix on Linux + macOS + Windows with ASAN/UBSAN and coverage gates.
# Default regression (full handshake + ARQ data + disconnect)
./build/cli_simulator --snr 15 --channel good --rate auto --test
# Force specific PHY config
./build/cli_simulator --snr 20 --channel awgn --mod dqpsk --rate r2_3 --test
# CFO chain verification
./tests/verify_cfo_chain.sh --cfo 50 --channel awgn --snr 20 --seed 42# 50 KB Mac↔Pi over real audio cable
SSH_KEY=$HOME/.ssh/id_pi5 PAYLOAD_SIZE=51200 \
./tools/tnc_loopback_test.shtnc_loopback_test.sh orchestrates two ultra_tnc instances (one
local, one over SSH), pushes a binary payload through the TCP data
port, and CRC-checks delivery.
--mod: dqpsk (default, 2 bits/sym), d8psk (3 bits/sym), dbpsk
(1 bit/sym, most robust), qam16/qam32/qam64 (coherent, stable
paths only).
--rate: r1_4, r1_2, r2_3, r3_4, r5_6, auto (default;
adaptive ladder).
- SSB transceiver with 2.8+ kHz filter bandwidth (or 500 Hz for OFDM-NARROW)
- Audio interface (SignaLink, RigBlaster, USB soundcard, or direct cable)
- PTT control (VOX, CAT, or hardware)
- TX: clean signal without ALC compression
- RX: comfortable listening level, avoid clipping (peak < 0.9)
| Band | Frequency | Notes |
|---|---|---|
| 80m | 3.590 MHz | Above narrow digital, below voice |
| 40m | 7.102 MHz | Common for wideband digital |
| 30m | 10.145 MHz | Check for WSPR at 10.140 |
| 20m | 14.108 MHz | Above FT8 crowd |
| 15m | 21.110 MHz | Above narrow digital segment |
| 10m | 28.120 MHz | Plenty of room |
Avoid: 14.070–14.095 MHz (FT8/PSK31), any .074 MHz (FT8), 14.100 MHz (NCDXF beacons). Listen 10–15 s before TX. Use minimum power necessary. Be ready to QSY.
- MC-DPSK baseline (5+ dB SNR, ±50 Hz CFO tolerance).
- OFDM-CHIRP DQPSK R1/4 → R3/4 with adaptive ladder (R3/4 hardware-calibrated at SNR ≥ 15 + fading < 0.10).
- OFDM-NARROW (500 Hz) for crowded bands or low-SNR conditions.
- Per-carrier RX erasure with CarrierLDPC v1 interleaver (deep notch / QRM survival on OFDM-CHIRP).
- Adaptive MODE_CHANGE with hysteresis (BUG-RATE-001 fixed: no panic-downshift on short fading transfers).
- Selective-repeat ARQ with cumulative + selective ACKs, hardened DISCONNECT, no timeout storms.
- TNC subsystem: cross-platform Linux / macOS / Windows, byte-exact end-to-end, validated with real Pat sessions.
- Hardware-in-the-loop test rig (Mac ↔ Pi5 with Watterson injection) with byte-exact file-transfer validation.
- First real OTA validation (2026-05-03): ProjectUltra
audio played over the air by KC3VPB (US, 40m band,
7.102 / 7.113 MHz USB), captured via the NA5B KiwiSDR in
Maryland, decoded byte-exact offline. Recordings in
recordings/.
- HARQ Chase soft-combining: math validated, infrastructure
in place; broader CW0-fail integration is on
experimental/harq-audit-2026-05-06pending further hardware validation. - D8PSK on fading channels: variable run-to-run, gated to high SNR + AWGN-class fading only.
- Coherent QPSK / 16QAM / 32QAM: stable-path only (NVIS, ground wave, clean cable).
- Long-running stability soak (multi-hour
ultra_tncuptime). - Bootstrap-rate cap relaxation: short transfers can't yet reach R3/4 even on clean AWGN because the initial-rate cap requires SNR ≥ 24; hardware validation needed before lowering.
- Real over-the-air validation infrastructure: currently limited to the synthetic Watterson hardware harness; KiwiSDR-based remote-receiver TX validation is the near-term plan for solo operators.
- Higher-order constellations (16/32/64-QAM) as production ladder rungs: enum reserved, no production code. Real capacity headroom (~2× peak throughput) but needs proper EVM gating, coherent phase tracking, IQ-imbalance handling, and a new auto-rate policy layer — multi-week scope, not autonomous-friendly.
- Iterative LDPC ↔ equalizer (turbo) loops.
- OTFS / MFSK: enum reserved for wire compatibility, no production implementation.
- Code-sign + notarization for prebuilt macOS binaries (Apple Developer ID needed; on the post-alpha roadmap).
Active engineering goal lives in docs/PROJECT_GOALS.md.
Recent design + audit notes are in docs/CHANGELOG.md,
docs/PHASE2_CARRIER_MASK_DESIGN.md, and the
docs/SESSION_*.md series. Speculative / archived research
lives under docs/archive/ — historical only, not part of
the production build.
Contributions welcome. Easiest entry points:
- On-air testing reports (especially with
STATSoutput included). - Pat / Winlink Express interop reports — what worked, what didn't.
- Bug fixes and DSP optimizations (profile first; see
docs/QUALITY_STRATEGY.md). - Documentation.
Please open an issue before submitting large PRs.
Real ionospheric propagation has characteristics simulation can't capture. Even "failed" recordings are valuable.
To record your own signal: tune a WebSDR (websdr.org or kiwisdr.com) to your TX frequency, start recording, transmit using ProjectUltra, stop and save the file. Submit via GitHub issue with the "Recording" label, including: callsign, location, band/freq/UTC, WebSDR used, path distance, and S-meter readings if known.
MIT License. See LICENSE.
- Community OTA testers, especially KC3VPB, for sharing real-station logs that helped diagnose post-handshake sync rejects and buffer- overflow edge cases.
- Dear ImGui — GUI framework.
- SDL2 — audio and windowing.
- FFTW3 — Fast Fourier Transform.
- miniz — compression.