I’m a sailor and I built a Navtex receiver using a Raspberry Pi, USB receiver dongle, and antenna. The Pi receives radio broadcasts, saves them as audio files and decodes using multi-level error correction. The text messages are saved in a database and accessible via web interface. Open Source code

Why using a seemingly dated technology like Navtex when we can have broadband internet via satellite even in the middle of an ocean? Because Starlink, a private company, does not seem that reliable to me and needs quite a lot of power (Mini: 20-40 Watts during active use1 vs. 2 Watts for the Pi2) – a precious commodity on a sailboat. Navtex is a free international service operated by governments.
Existing decoders mostly assume a sailor at a laptop. What I wanted instead was a headless service: RTL-SDR in, Pi running unattended on boat battery power, messages in a database, web UI and push warnings out. Standalone commercial Navtex receivers run reliably, but they’re closed appliances and do not allow to extend the receiver, e.g. to RTTY or weatherfax. Last but not least, it’s fun to build something.
From the sinking of the Titanic to Navtex
Navtex is a maritime safety information system for broadcasting navigational warnings and weather forecasts. On the 518 kHz medium frequency,3 messages are transmitted by stations with a range of a few hundred nautical miles. Each station has a 10-minute time slot every four hours.4 Navtex is part of the Global Maritime Distress and Safety System (GMDSS).
Navtex uses frequency modulation to encode text.5 Specifically, the frequency of a carrier wave is rapidly shifted by 170 Hz, where the high frequency is a mark (1), and the low frequency is a space (0). Using this frequency-shift keying (FSK), 7-bit characters are encoded using the CCIR 476 set and transmitted with a speed of 100 baud (about 14 characters per second). This kind of text transmission, called SITOR-B (for Simplex Teleprinter Over Radio), was developed by the Dutch PTT in 1970. It uses forward error correction (FEC) of characters to compensate for disturbances in the radio transmission.
The technique is an improvement over radioteletype (RTTY) that uses the ITA2 character code. Its predecessor was the Baudot code invented for telegraphy by Emile Baudot in the 1870s. These are codes for telegraphic transmission, of which Morse was the first. The first transatlantic telegraph cable in 1858 was a glorious achievement, but of no value for ships.
The use of radio waves was a milestone for the safety of ships. Electromagnetic waves, self-sustaining oscillations of electric and magnetic fields, need no medium to travel – no aether – and propagate through vacuum at the speed of light. James Clerk Maxwell predicted them on paper in the 1860s, Heinrich Hertz demonstrated them in the laboratory in 1887 and Guglielmo Marconi sent the first wireless telegraph signals across open water in 1897, then across the Atlantic in 1901. The Marconi company was responsible for radiotelegraphy on the RMS Titanic. The two radio operators of the RMS Titanic were busy sending telegrams instead of watching ice reports. A significant chunk of their pay came as commission on passenger telegrams. In consequence, the ice warnings of two nearby ships, the SS Mesaba and the SS California, never made it to RMS Titanic’s bridge.6
The catastrophic sinking in 1912 was the catalyst for the International Convention for the Safety of Life at Sea (SOLAS) that spurred the requirement for 24-hour radio monitoring on ships. Modern safety systems like Navtex are direct evolutions of these regulations.
The hardware of the Navtex receiver: a Pi
A Raspberry Pi Zero 2 W (512 MB RAM) is used with the Pi OS 64-bit Lite (Debian 13 ‘Trixie’). A UPS-HAT-(C) makes sure that the Pi survives power cuts on the sailboat.
The Pi lives in a small project box (hint: to cut an opening into hard plastic, it helps to use heat…). A rotary knob (KY-040 encoder) allows to choose options and to safely power down the Pi. A small 1.54” OLED display shows a status message – it’s much too small (128x64px) to display an entire Navtex message. A future possibility is to use a separate e-ink display to show the latest Navtex message.
A RTL-SDR V4 dongle receiver is connected via USB. It gets enough power from the Pi even without a powered USB-hub. A BiasT sends power to a Nasa Marine H Vector Navtex antenna that was bought for its weather sealing rather than rolling a DIY ferrite-rod antenna with hundreds of turns of enamelled copper wire. For receiving RTTY broadcasts, a simple wire antenna with DIY 1:9 balun is rigged.7
The software on the Pi
What sounds straightforward and looks like a modest Python codebase was actually quite a journey. The first time the code actually accessed the signal was a thrill!
The decoding chain starts with audio: after USB-sideband demodulation, the two FSK tones land in the audible band. An FFT over a few hundred milliseconds locates them as the two dominant peaks below roughly 2 kHz; for Navtex these are near 915 and 1085 Hz, but the exact pair depends on dial offset and is estimated from each recording rather than hard-coded. Mark and space amplitudes are then tracked by narrow filters at each tone frequency.8 At every bit period (10 ms at 100 baud), the synchronous slicer compares the two amplitudes and emits 1 for mark, 0 for space. SITOR-B’s bit clock runs continuously across a message, so one symbol phase is locked at the start and reused throughout. The resulting bit stream feeds the CCIR 476 framer.9

2. Power spectrum (Welch, 30 s averaged). Two peaks at 915 Hz (mark) and 1085 Hz (space), 170 Hz apart, with the rest of the band three orders of magnitude below. This is what the FFT-based tone-detection step sees.
3. Mark and space amplitudes over 200 ms, with decoded bits. Blue is the mark-tone amplitude, red the space-tone amplitude, both from a sliding complex matched filter. They swap dominance every 10 ms (one bit period). The black step function is the synchronous slicer's output: 1 when mark wins at the bit centre, 0 when space wins. Reads end-to-end as the bit stream that feeds the CCIR 476 framer.
The Pi Zero 2 W’s modest capacity requires lean signal processing. The readout of the RTL-SDR at the default 2.048 MS/s dropped parts of the signal, so the sample rate was reduced to 1.024 MS/s.10 Reading the dongle in a continuous stream and decimating on the fly didn’t work either: even on the development Mac the streaming decimator sustained only 86% of realtime, dropping samples in bursts. The fix was to read 30 seconds of samples in a single batch and decimate offline. The decimation itself was a second cost. Going from 1.024 MS/s down to the 48 kS/s audio rate in a single step would have demanded a long Finite Impulse Response (FIR)11 filter running at the full input rate, around 150 million multiply-accumulates per second. The Pi can’t sustain that. A two-stage cascade does it instead: a short 81-tap filter decimates first to 256 kS/s, then a polyphase resample drops to 48 kS/s. Each stage stays under 25 million operations per second, an order of magnitude cheaper than the one-shot filter, with the same audio output.
During development, the incoming radio signal ran through SDR++ and BlackHole audio on a Mac. The next step was a sdr_source.py module wrapping pyrtlsdr and reading the signal directly inside Python. Worth noting: Pyrtlsdr’s 0.4 release changed the buffer API in a way that breaks the read pattern the decoder uses. It had been unit tested against a synthetic signal, but no actual dongle had ever opened it.
A codebreaking-style debugging of a three-layered bug
For testing, a synthetic encoder fed the demodulator and round-tripped cleanly. The next step was using a WebSDR12 in an area with Navtex signal access. One of the early trials fed a WebSDR-recorded signal from Heraklion. 757 seconds of strong signal produced this output: KQARK GBKGWGRH BSGKR JCQ JCQ .... No ‘ZCZC’ frame anywhere (every Navtex message is framed by ZCZC … NNNN), although the waveform clearly showed the start of the transmission.
The first bug was the alphabet table – I had somehow got it wrong. SITOR-B uses a 35-symbol code over 7-bit constant-weight codewords (four mark bits, three space bits). The synthetic round-trip had not caught the bug because encoder and decoder shared the same dictionary. Correcting the table produced output that was more English-shaped, but still no ‘ZCZC’.
The second bug lay in FEC: CCIR 476 mode B sends every character twice, with the second copy delayed by five codeword positions (350 ms at 100 baud);13 the receiver uses the first copy if it satisfies the 4-of-7 constant-weight check, otherwise falls back to the delayed one.14 Every codeword has exactly four marks and three spaces, so any single-bit error breaks the weight invariant and is detected without parity bits.15
The third bug was the bit order on the wire. With alphabet and FEC fixed, the only remaining unknowns were the alignment phase and the bit transform. The transmitter sends each codeword’s bit B1 first; the decoder had been packing it into the most significant position instead of the least. The pattern is cryptanalytic: when the alphabet is right but the output is not English, enumerate the small space of remaining structural transforms, score each candidate by English-shaped statistics, take the winner.
After correcting these three bugs, the Heraklion message dropped out immediately. ZCZC HE23 032000 UTC MAY 26 IRAKLEIO RADIO/WEATHER FORECAST.
Restoring corrupted text without LLM in 2026?
One of the most interesting aspects of developing a Navtex receiver is to apply different strategies for error correction of the transmitted characters. The default reflex in 2026 is to reach for a language model. A recent paper by Cho et al16 benchmarks dictionary lookup,17 character n-grams and transformer-encoder Masked Language Modeling (MLM) on this exact restoration task. The MLM wins overall: 85% restoration against 64% for five-grams and 59% for dictionary alone.
The MLM in the paper’s 50-million-parameter model was trained on an A100 GPU with 80 GB memory. A Pi Zero 2 W with 512 MB RAM cannot host it, and a model small enough to fit collapses to n-gram quality. The MLM also masks errors uniformly at random (MCAR, in the authors’ terminology), while real Navtex corruption is burst-shaped from fading and would follow MAR or MNAR; the authors flag this as a limitation, having no channel statistics in their dataset.18 Finally, the model restores digits with only 45% accuracy. Navtex digits are coordinates, frequencies and timestamps, where a confidently wrong character is dangerous. An asterisk (signifying a dropped character or digit) is more honest than a guess.
The decoder therefore plans a five-gram character model, with a dictionary lookup against the public Navtex vocabulary as a fallback.
The lightbuoy off Juist is unlit
When I was in Switzerland and not on my boat in the Netherlands, I had no access to Navtex signals – they just don’t make it that far. Therefore, I considered validating the signal chain on DCF77, the German time signal at 77.5 kHz, approximately 300 km away in Frankfurt. I plugged the wire in, fired up SDR++, tuned 77.5 kHz, and saw nothing. The reason is mundane. The RTL-SDR V4’s internal upconverter cuts off around 500 kHz; 77.5 kHz is below that. The dongle simply can’t tune low enough.
So, no testing possible in Switzerland? Actually, the short-wave broadcasts of Pinneberg do make it here! In Pinneberg near Hamburg is the transmitter station of Deutscher Wetterdienst (DWD), and they transmit RTTY on short-wave frequencies at 50-baud with 450 Hz shift.
RTTY is not Navtex, but parts of the decoder are general. The FSK demodulator and tone estimator are parameterised by mark, space and baud rate. A small Baudot/ITA-2 framer of around 80 lines reads the bit stream. The synchronous slicer used for SITOR-B produced regular bit slips on RTTY, because RTTY is asynchronous and resyncs on each character’s start bit. A per-character edge-aligned sampler fixed it.
The first clean message received on the Mac was a bulletin from the Marine Weather Service Hamburg of 4 May 2026, 17:00 UTC, containing navigational warning No. 183: a lightbuoy off Juist had gone dark overnight.
Later, and only after sundown, end-to-end RTTY decode on the Pi was successful. What a difference a day makes: lower-frequency radio stations can travel much further at night.19
The Pi’s interface
Accessing the stored Navtex messages is via web interface from a laptop, tablet or smartphone. Urgent warnings are pushed to the smartphone of the sailor via ntfy. The SQLite-database allows for filtering by Navtex message area (e.g. Navarea 1 – North Atlantic, North Sea, Baltic Sea) and type (e.g. navigational or meteorological warnings, forecasts).
As an addition to the Navtex functionality, a BME280 sensor reads barometric pressure. The 3-hour pressure tendency is one of the most important weather indicators a sailor has. It’s displayed on the OLED. The full barometric pressure curve is provided via web UI.
Potentially, Navtex messages could be republished on a Signal K bus for consumption by OpenPlotter or OpenCPN.

I first wrote about the idea of a headless Pi receiver in 2023, during a stormy christmas week in the Dutch Wadden Islands playing with an RTL-SDR, a wire antenna, and a Python RTTY decoder. Then the project slept. The arrival of LLM-based programming with Claude Code allowed me to develop the current Navtex implementation at a pace that kept my motivation high. Of course, vibe coding has led to an inflation of code, much as smartphone snapshots devalued photography over the last two decades. But for a complex solo niche project, that kind of help can be the difference between something that ships and something that stays an idea.
Footnotes
-
Starlink Mini numbers are from SpaceX’s datasheet ↩
-
Pi Zero 2 W idle is ~0.4 W at the 5 V rail; under continuous USB read from an RTL-SDR V4 (~0.3 A on its own) and Wi-Fi active, total bus draw is roughly 1–2 W according to Pi Foundation power docs. ↩
-
518 kHz medium-frequency ground-wave propagates well over saltwater with a 300-400 nautical miles coverage. ITU-R M.540 explains the allocation. ↩
-
A list of all Navarea coordinators. ↩
-
Operational and technical characteristics for the Navtex system ITU-R M.540. ↩
-
British Wreck Commissioner’s Inquiry, 1912, evidence of Cyril Evans. Also, there is an interview with Harold Bride, one of the two radio operators of the Titanic, in The New York Times, 28 April 1912 (Jack Phillips, the other operator, died in the sinking). See the Titanic Inquiry Project. ↩
-
A 1:9 balun on a wire (antenna) lowers impedance ~450 Ω against ground at HF to ~50 Ω for the SDR. ↩
-
The classical tool for detecting a single tone is the Goertzel algorithm. It would be slow in Python because each step depends on the previous one and cannot be parallelised across samples. The decoder instead uses matched filters built on NumPy primitives that run as compiled C under the hood, which is fast enough on the Pi. ↩
-
Signal-processing chain: 1.024 MS/s signal from the RTL-SDR – tune to 518 kHz baseband – decimate to ~24 kS/s – USB demodulation produces a real audio stream where the two FSK tones land near 915/1085 Hz – 30-second WAV files written to disk – the FFT/matched-filter/slicer/framer chain runs on the stored audio rather than on a live stream. ↩
-
The math allows much lower rates, but the hardware doesn’t reliably deliver them, and the engineering payoff after the cascade fix is small. ↩
-
An IIR filter would use fewer computational cost, but linear phase matters for the synchronous FSK slicer (mark and space tones must keep the same group delay), and IIR can’t use the polyphase decimation trick that lets the FIR skip computing samples it would discard anyway. The compute advantage shrinks in a decimation context. ↩
-
A useful list of WebSDR. ↩
-
ITU-R M.625 and various secondary references describe the same interleaving in slightly different counting conventions (some count the gap inclusively, some exclusively, some count character intervals rather than codeword positions). Empirically lag-5 codeword autocorrelation dominates real signals (verified against a strong Heraklion WebSDR recording: 2815 vs ~500 matches at lag 5 vs lags 3/4/6). This corresponds to DX(N) at even position 2N and RX(N) at odd position 2N+5, i.e. a 5-codeword (350 ms) gap. ↩
-
As per technical standard ITU-R M.476-5. ↩
-
Any single-bit flip changes the weight by 1, so 4 becomes 3 or 5 and the check fires. Even-numbered errors that swap one mark and one space preserve the weight and slip through. ↩
-
Cho et al. JMSE 2025;13:1657. ↩
-
The corpus source is not named; the only large public Navtex archive I’m aware of is The Navtex Archive. ↩
-
MCAR / MAR / MNAR is a taxonomy from missing-data theory, formalized by Donald Rubin in 1976. It classifies why data is missing. Applied to Navtex: when ionospheric fading drops the signal-to-noise ratio, you lose a burst of consecutive characters, not isolated ones. The missingness pattern is structured by an underlying observable: the channel state, i.e. the properties of the propagation path between transmitter and receiver. ↩
-
Daytime attenuation of lower-frequency signals is caused by D-layer absorption. The D-layer, roughly 60–90 km high, absorbs frequencies below the 20-meter band. After dark, these lower frequencies are no longer absorbed. ↩