Setting up your radio to receive radiosondes
Before any decoding runs, HTCommander needs to hear a sonde on a channel it recognizes. The rule is the same one used for SARSAT beacons, and it's simple:
Name the channel
Radiosonde. Case doesn't matter —Radiosonde,radiosonde, andRADIOSONDEall work. HTCommander watches for audio arriving on a channel whose name is "radiosonde" and feeds only that channel into the sonde decoders.
The channel needs a receive-oriented configuration:
- Receive frequency — wherever your local sonde is transmitting, somewhere in the 400–406 MHz meteorological band (403 MHz is a common launch frequency). Many Benshi-class radios have wideband receive that covers this band.
- Wide FM — radiosondes use a wider deviation than 12.5 kHz voice; wideband demodulation generally recovers the signal best.
- Flat / no de-emphasis — the decoders want the raw demodulated waveform, not audio shaped for a human ear.
- Receive-only — this is a licensed meteorological band; you listen, you don't transmit.
- Mute — so the data bursts don't buzz out of the speaker while you work.
Finding a sonde to hear
Radiosondes launch on a schedule — most stations release one around 00:00 and
12:00 UTC, and the balloon is in the air (and in radio range) for a couple of
hours as it climbs and then drifts back down under a parachute. To find one near
you and its exact frequency, the community trackers
SondeHub and radiosondy.info
show live launches, frequencies, and predicted landing sites. Tune your
Radiosonde channel to that frequency during a launch window and the decoding
below runs automatically.
What is a radiosonde?
A radiosonde is the disposable instrument package on a weather balloon. It's the single most important source of upper-air data feeding the world's weather models. The routine goes like this:
- A meteorological station fills a latex balloon with hydrogen or helium and ties on a sonde — a foam box the size of a paperback with a GPS receiver, temperature and humidity sensors, a battery, and a small UHF transmitter.
- The balloon rises at ~5 m/s to around 35 km, sending a telemetry frame every second or two: GPS position and altitude, plus PTU (Pressure, Temperature, Humidity).
- The balloon expands until it bursts. The sonde parachutes back down, transmitting the whole way.
- On the ground, a hobby of sonde chasing has grown up around recovering the landed sondes — which is a big part of why decoding them on a handheld is fun.
Everything HTCommander does here is receive-only. We're eavesdropping on a public transmission, turning a telemetry burst into a position on the map.
What HTCommander supports
The catch with radiosondes is that there is no single standard. Different
manufacturers — Vaisala, Graw, Meteomodem, InterMet, Lockheed Martin — each use
their own modulation, framing, and error-correction. HTCommander decodes six
families, ported from the reference C decoders in the
radiosonde_auto_rx /
rs1729 RS projects (author zilog80) into pure
Dart:
| Family | Maker | Modem | Error correction |
|---|---|---|---|
| RS41 | Vaisala | GFSK 4800 baud, NRZ | Reed–Solomon(255,231) ×2 |
| DFM (06/09/17) | Graw | 2-FSK, Manchester, 2500 baud | Hamming(8,4) |
| M10 | Meteomodem | Manchester + differential, 9615 baud | linear checksum |
| M20 | Meteomodem | Manchester + differential, 9600 baud | linear checksum |
| iMet-1-RS / iMet-4 | InterMet | Bell-202 AFSK 1200 baud, 8N1 | CRC-16-CCITT |
| LMS6 | Lockheed Martin | NRZ 4800 baud | convolutional + Reed–Solomon(255,223) |
The one common thread: they all reduce, after FM demodulation, to a stream of telemetry frames carrying a GPS position. That's what we extract — latitude, longitude, altitude, velocity, satellites, time, and (where available) the sonde serial number.
Not supported: RS92. Vaisala's older RS92 doesn't transmit a computed position — it sends raw GPS pseudoranges, and you need live broadcast ephemeris (the satellites' orbital parameters, fetched from the internet) to solve them into a lat/lon. Offline, from audio alone, there's simply nothing to turn into a coordinate, so RS92 is out of scope.
One channel, six modems
You don't pick a sonde type. The monitor
(radiosonde_monitor.dart)
buffers received audio and, on each flush, runs the buffer through every
decoder in turn. Each family has its own demodulator + frame parser; whichever
one produces a frame that passes its own error-correction wins. A DFM sonde and
an RS41 sonde look nothing alike on the air, but from your side it's the same
channel — you tune to the frequency, and the right decoder lights up.
Most of the demodulators share the same skeleton, honed first on the DFM decoder and reused across the FM-baseband families:
- DC-block and normalize the incoming PCM, and build a prefix-sum array so any window's mean is an O(1) lookup.
- Find the frame with a matched filter: correlate the sonde's fixed sync pattern against the signal to locate frame starts and recover polarity (whether the FM demod came out inverted).
- Refine the symbol rate from the measured spacing between successive sync hits — real transmissions drift, and a symbol clock that's off by 0.1% will slip a bit halfway through a long frame.
- Integrate-and-dump each symbol and slice it to a bit.
What differs — and what made each family its own project — is everything after the bits.
The families, and what makes each interesting
RS41 — Reed–Solomon and a whitening mask
Vaisala's RS41 is the most common sonde in the world, and the most robust. It's
plain 4800-baud NRZ GFSK, but the payload is scrambled with a fixed 64-byte
XOR "whitening" mask (so the transmitted signal has plenty of transitions to keep
a receiver's clock locked), and it's protected by two interleaved
Reed–Solomon(255,231) codewords — one over the even bytes, one over the odd —
that between them can fix a dozen byte errors per frame. The position is a full
ECEF vector (Earth-Centered, Earth-Fixed X/Y/Z in centimetres), which we run
through the WGS-84 ellipsoid math to get lat/lon/alt, plus an ECEF velocity we
rotate into ground speed, heading, and climb rate. Decoder:
rs41_decoder.dart.
DFM — Manchester and Hamming
Graw's DFM sondes (06/09/17) are 2-FSK, Manchester-coded at 2500 baud. Each frame
is bit-interleaved and protected by a Hamming(8,4) code, and the interesting
wrinkle is that the sonde's serial number and calibration are spread across many
frames — so the decoder
(dfm_decoder.dart) keeps state and
assembles a complete fix once the GPS sub-packets have all arrived. This was the
first family we built, and its demodulator became the template for the rest.
M10 & M20 — differential Manchester, and a checksum you have to reverse-engineer
Meteomodem's M10 runs at 9615 baud with Manchester coding plus a differential
(NRZI-style) layer on top — which has the neat side effect of making the
recovered bitstream immune to signal inversion. M20 is the same modem at
9600 baud with a more compact, differently-laid-out frame. Both are guarded not
by a standard CRC but by a custom bit-twiddling linear checksum, ported
verbatim. Because the two share a modem, our M10 demodulator is parameterized
(sync bytes, frame length, baud) and the M20 support is the same code with
different constants. Decoders:
m10_decoder.dart,
m20_decoder.dart.
iMet — a different beast entirely: AFSK
InterMet's iMet-1-RS is the odd one out. It doesn't send an FM-baseband bit
stream at all — it's Bell-202 AFSK, the same 1200-baud tones as APRS
(1200 Hz for a 1, 2200 Hz for a 0), framed as ordinary 8N1 async serial
bytes. So its demodulator
(imet_demodulator.dart) is a
tone detector: a recursive sliding-DFT compares the energy at the mark and space
frequencies per sample, then the bit stream is rebuilt from tone transitions and
de-framed byte by byte. The bytes carry SOH-delimited packets — a GPS packet
with IEEE-754 float latitude/longitude and time-of-day, plus PTU and optional
ozone-sensor packets — each checked with a CRC-16-CCITT. (A subtle bug we hit
and fixed: the last byte of a frame has no trailing transition, so the bit
reconstruction has to flush the final run or the closing CRC byte goes missing.)
LMS6 — the deep end: convolutional coding plus Reed–Solomon
Lockheed Martin's LMS6 stacks two layers of error-correction. The 4800-baud NRZ
bits are a rate-1/2, constraint-length-7 convolutional code (transmitted with
every other bit inverted — the (c0, inv(c1)) trick), which wraps a
Reed–Solomon(255,223) CCSDS block. Decoding is the whole pipeline in reverse:
undo the odd-bit inversion, deconvolve the convolutional code, pack bytes,
verify a 5-byte block sync, run the RS correction (over the byte-reversed
codeword the format uses), check a CRC-16, and finally read a big-endian GPS
frame. Making this work meant extending our Reed–Solomon core to the CCSDS
variant (a different field polynomial and a non-trivial primitive-root offset)
and writing a convolutional codec from scratch. Decoder:
lms6_decoder.dart, codec:
convolutional.dart.
The shared toolbox: error correction
Three families lean on the same Reed–Solomon engine
(reed_solomon.dart) — a GF(2⁸)
decoder (syndromes → Berlekamp–Massey → Chien search → Forney) that RS41 uses in
its standard form and LMS6 uses in the CCSDS configuration. The others each bring
their own guard: Hamming(8,4) for DFM, CRC-16-CCITT for iMet, the custom linear
checksum for M10/M20. In every case the check does double duty: it corrects (or
at least detects) bit errors, and — just as important for a weak, one-shot
signal — it tells the decoder for certain when it's holding a real frame
versus noise that happened to look frame-shaped. Nothing reaches the map without
passing its family's check.
How we proved it works — and an honest caveat
Verification here is synthetic round-trip. For each family we wrote the
inverse of the decoder — a small modulator that renders a known frame (with
valid error-correction and CRC) as ideal audio — and fed it straight back through
the production demodulator, asserting it recovers the exact position, serial, and
time we put in. Those tests also inject byte errors to confirm the Reed–Solomon
and convolutional layers actually correct them, and exercise inversion-immunity
(M10) and the AFSK tone path (iMet). It's all in the test suite —
rs41_test.dart,
dfm_test.dart,
m10_test.dart,
m20_test.dart,
imet_test.dart,
lms6_test.dart, and
reed_solomon_test.dart — 25 tests in
all, and the whole thing analyzes clean.
The honest part: a round-trip test proves the DSP, framing, error-correction, and field-parsing are all internally consistent and ported faithfully from the reference decoders — but it does not by itself prove bit-for-bit compatibility with a real over-the-air sonde. The constants (sync words, masks, field offsets, polynomials) were taken straight from the reference C, so the odds are good; but the strongest possible test — dropping a real recording through the production path — is the one piece still outstanding. If you can capture a 32 kHz mono WAV of a live sonde, it becomes a permanent regression fixture, exactly as the SARSAT self-test recording did.
How it shows up in HTCommander
When a sonde decodes, it lands in the Comms tab as its own message type — a
light-blue icon and a Radiosonde label — carrying the sonde family
(RS41, DFM09, M20, iMet, LMS6, …), the serial, position, altitude, and,
when present, temperature. Opening the message details breaks the frame down
field by field. Repeated fixes from the same sonde coalesce into a single,
updating bubble rather than spamming the history.
On the Map tab, each sonde with a position gets a marker that tracks its latest fix, so you can watch a balloon climb, drift, and descend across the map in real time — and, if you're the chasing type, see where it's headed.
It all self-activates the moment audio arrives on your Radiosonde channel and
goes quiet when you switch away — no checkbox, no per-type setting, no assumption
about frequency. Name the channel, tune to a launch, and watch the stratosphere
report in.