Opulent Voice Protocol - A digital voice protocol for amateur radio.
Self-contained C++ implementations of the OPV modulator and demodulator, designed for use with PlutoSDR/LibreSDR hardware and Interlocutor.
# Build
make
# Loopback test
make test
# Full transceiver with Interlocutor
./opv-pluto.sh -f 435000000 -v| Parameter | Value |
|---|---|
| Modulation | MSK (Minimum Shift Keying) |
| Symbol Rate | 54,200 baud |
| Sample Rate | 2,168,000 SPS |
| Frequency Deviation | ±13,550 Hz |
| Samples/Symbol | 40 |
| Field | Size | Description |
|---|---|---|
| Sync Word | 24 bits | 0x02B8DB |
| Encoded Payload | 2144 bits | Rate 1/2 convolutionally coded |
| Total | 2168 symbols | ~40 ms per frame |
| Offset | Size | Field |
|---|---|---|
| 0-5 | 6 bytes | Station ID (Base-40 encoded) |
| 6-8 | 3 bytes | Token |
| 9-11 | 3 bytes | Reserved |
| 12-133 | 122 bytes | Voice/Data payload |
- Convolutional Code: Rate 1/2, K=7
- G1 = 0x4F (171 octal)
- G2 = 0x6D (133 octal)
- Interleaver: 67×32 block with bit reversal
- Randomizer: CCSDS 8-bit LFSR (polynomial x⁸+x⁷+x⁵+x³+1)
Full-duplex transceiver for use with Interlocutor. One script, just like Dialogus.
./opv-pluto.sh # 435 MHz simplex (default)
./opv-pluto.sh -f 905050000 # 905.05 MHz
./opv-pluto.sh -f 144390000 -v # 2m band, verbose
./opv-pluto.sh --tx-freq 435000000 --rx-freq 440000000 # Split operation
./opv-pluto.sh -u ip:192.168.3.1 # Custom Pluto IPWorkflow:
- Start
./opv-pluto.sh - Start Interlocutor (TX to UDP 57372, listen on UDP 57373)
- Use Interlocutor to send messages and make calls
Port Configuration Note:
Dialogus (running on the Pluto itself) uses port 57372 for both directions because Interlocutor and Dialogus are on different IP addresses. When running opv-modem on the same computer as Interlocutor, we need separate ports to avoid conflicts:
| Direction | Port | Description |
|---|---|---|
| Interlocutor → opv-modem | 57372 | Frames to transmit |
| opv-modem → Interlocutor | 57373 | Received frames |
Configure Interlocutor with: TX port = 57372, RX port = 57373
Options:
| Option | Description |
|---|---|
-f, --frequency |
Simplex frequency in Hz |
--tx-freq |
TX frequency (split operation) |
--rx-freq |
RX frequency (split operation) |
--tx-gain |
TX gain in dB (default: -20) |
--rx-gain |
RX gain in dB (default: 40) |
--tx-port |
UDP port from Interlocutor (default: 57372) |
--rx-port |
UDP port to Interlocutor (default: 57373) |
-u, --uri |
PlutoSDR URI (default: ip:192.168.2.1) |
-v |
Verbose output |
UDP server for Interlocutor integration. Used internally by opv-pluto.sh.
Usage: bin/opv-modem [OPTIONS]
Modes:
-l Loopback: UDP → mod → demod → UDP (testing)
-t TX mode: UDP → mod → stdout (to PlutoSDR)
-R RX mode: stdin → demod → UDP (from PlutoSDR)
Options:
-p PORT UDP port to listen on (default: 57372)
-r PORT UDP port to send to (default: 57373)
-c CALL Rewrite callsign (loopback repeater mode)
-d PATH Path to opv-demod (default: ./bin/opv-demod)
-v Verbose output
Usage: bin/opv-mod -S CALLSIGN -B FRAMES [-t TOKEN] [-c] [-v]
Options:
-S CALLSIGN Station callsign (e.g., W5NYV, KB5MU)
-B FRAMES Number of frames to transmit
-R Raw mode (read 134-byte frames from stdin)
-t TOKEN 24-bit token (default: 0xBBAADD)
-c Continuous mode (loop forever)
-v Verbose output
Output: 16-bit I/Q samples (little-endian) to stdout
Usage: bin/opv-demod [options] < input.iq
Options:
-s Streaming mode (real-time from radio)
-r Raw output (134-byte frames to stdout)
-c Coherent mode (Costas loop, ~3dB SNR improvement)
-p <hz> PLL bandwidth in Hz (default: 50, coherent mode only)
-a <bw> AFC bandwidth alpha (default: 0.001)
-o <hz> Initial frequency offset in Hz (streaming mode)
-q Quiet mode (suppress all stderr output)
Input: 16-bit I/Q samples (little-endian) from stdin
Features:
- Automatic Frequency Control (AFC)
- Symbol Timing Recovery (early-late gate timing error detector)
- Soft-decision Viterbi decoding
- Sync tracking with flywheel
- Optional coherent demodulation via 2nd-order Costas loop
Minimum burst length: The sync state machine requires two frames to acquire
lock (HUNTING → VERIFYING → LOCKED), so the first frame of any transmission is
always consumed by acquisition. A burst of N frames will produce N-1 decoded
frames at the receiver. For PTT-style operation, transmit at least 3 frames to
guarantee one decoded frame at the far end. The preamble frames sent by
opv-modem and opv-pluto.sh exist partly to allow the demodulator to acquire
symbol timing before the first data frame arrives, but frame sync still requires
two data frames to confirm.
Coherent mode note: The -c flag enables a 2nd-order Costas loop for
continuous carrier phase tracking, yielding a theoretical 3 dB SNR improvement
over non-coherent detection. In synthetic loopback tests both modes perform
identically due to the steep FEC waterfall masking the coherent gain. The
advantage becomes meaningful under real hardware conditions — oscillator drift,
phase noise, and dynamic Doppler on a live satellite pass. Use
make test-doppler FREQ_MHZ=<band> to characterize Doppler performance for
your operating frequency.
For use without Interlocutor (BERT testing, debugging).
scripts/opv-pluto-rx.sh # Receive until Ctrl+C
scripts/opv-pluto-rx.sh -t 10 # Receive for 10 seconds
scripts/opv-pluto-rx.sh -f 905036750 -g 50 # Custom frequency and gain
scripts/opv-pluto-rx.sh -o capture.iq # Save raw IQ for debuggingscripts/opv-pluto-tx.sh -S W5NYV -B 10 # Send 10 BERT frames
scripts/opv-pluto-tx.sh -S W5NYV -B 10 -c # Continuous BERT (Ctrl+C to stop)
scripts/opv-pluto-tx.sh -S W5NYV -g -10 # Adjust TX gainRequires iio_attr and iio_rwdev (libiio-utils).
opv-cxx-demod/
├── Makefile # Build system
├── README.md # This file
├── LICENSE # CERN-OHL-S-2.0
├── opv-pluto.sh # Full transceiver script
├── bin/ # Built binaries (created by make)
│ ├── opv-mod
│ ├── opv-demod
│ └── opv-modem
├── src/
│ ├── opv-mod.cpp # Modulator (self-contained)
│ ├── opv-demod.cpp # Demodulator (self-contained)
│ └── opv-modem.cpp # Modem server (self-contained)
├── scripts/
│ ├── opv-pluto-rx.sh # Standalone RX script
│ └── opv-pluto-tx.sh # Standalone TX script
└── docs/
├── numerology.ipynb # Design calculations
└── filter-taps.ipynb # Filter design
- Interlocutor: Full integration via UDP (text messages, voice calls)
- Loopback: Successfully modulates and demodulates to itself
- Demodulates: LibreSDR HDL modem Locutus transmissions
- Modulation: To Be Tested with LibreSDR HDL modem Locutus receiving
- Sample Format: 16-bit signed I/Q, little-endian, interleaved
Requirements:
- C++17 compiler (g++ or clang++)
- No external dependencies (self-contained)
- libiio-utils for PlutoSDR scripts
make # Build all programs
make test # Verify loopback works
make test-raw # Test raw frame mode
make test-rx # Test RX mode UDP output
make test-coherent # Verify coherent mode decodes as many frames as non-coherent
make test-coherent-compare # Compare coherent vs non-coherent, static offset + noise
make test-coherent-compare SNR=-5 OFFSET=300 # Custom conditions
make test-doppler # LEO Doppler stress test at 905 MHz (default)
make test-doppler FREQ_MHZ=433 # 70cm band (±11.3 kHz swing, 220 Hz/sec)
make test-doppler FREQ_MHZ=2400 # 2.4 GHz (±62.4 kHz swing, 1200 Hz/sec)
make test-doppler FREQ_MHZ=5000 # 5 GHz uplink (±130 kHz swing, 2535 Hz/sec)
make clean # Remove binariesDoppler rate scales with carrier frequency (f · v²/c·h at zenith for 400 km LEO).
LEO is the worst case: HEO only reaches LEO rates briefly at perigee, GEO drifts
a few Hz/sec. Note that synthetic Doppler tests may show equal coherent/non-coherent
performance due to the FEC waterfall masking the 3 dB coherent gain — true
validation of the coherent advantage requires over-the-air testing on a live
satellite pass with real oscillator drift.
CERN Open Hardware License - Strongly Reciprocal (CERN-OHL-S-2.0)
Open Research Institute, Inc.
https://openresearch.institute
Developed as part of the Phase 4 Ground project for amateur radio digital communications.
Thanks to Rob Riggs of Mobilinkd LLC for the M17 implementation that originally inspired this codebase.