Amateur radio / DSP
TrailLink: An amateur-radio data link
A Blackbird hackathon build log for a custom 4-FSK packet link: phone, Raspberry Pi field node, amateur FM handheld, HackRF base station, and short Claude replies over RF.
TrailLink was my Blackbird hackathon project: an amateur-radio data link for off-grid comms. A phone with no internet sends a short message to a base station, gets a reply, and receives that reply back over RF.
The phone joins a Raspberry Pi hotspot and uses a small web UI. The Pi turns the message into audio modem tones, keys an amateur FM handheld, and sends a packet. At the other end, a HackRF and laptop demodulate the packet, optionally ask Claude for a short reply, and transmit the response back through the same link.
The link is built for short plain-text messages, predictable framing, and diagnostics that say where it failed: sync, CRC, FEC, audio level, or the radio path. The source is on GitHub at emm312/traillink.

The modem
The modem crate is pure Rust with no hardware or async dependencies. It takes frames to samples and samples back to frames. That split let me test the protocol in software before blaming a sound card, squelch, handheld audio path, or RF noise.
Each symbol chooses one of four audio tones: 600, 1200, 1800, or 2400 Hz. At 48 kHz and 600 symbols per second, every symbol is exactly 80 samples. The tones are spaced at the symbol rate, which makes them orthogonal over one symbol window and keeps the demodulator simple.
The modulator uses a continuous phase accumulator instead of restarting the sine wave at every symbol boundary. That keeps the transmitted audio cleaner and avoids extra energy when the symbol changes.
What the radio words mean
4-FSK means four-frequency shift keying. Each symbol is one of four tones, so each tone carries two bits. At 600 symbols per second, that gives 1200 raw bits per second before headers, CRC bytes, and FEC overhead.
CRC is a checksum. The receiver runs the same CRC-16 calculation over the received bytes and compares the result with the CRC that came in the packet. If they differ, the frame got corrupted and should be dropped.
FEC is forward error correction. I used Hamming(7,4): split a byte into two nibbles, add parity bits to each nibble, and the receiver can correct a single bad bit in each 7-bit codeword. It burns airtime, but it can turn a marginal packet back into a valid one.
A Goertzel filter is a cheap way to measure one specific frequency. Instead of running a full FFT for every symbol, the receiver checks the four expected tones and picks the one with the most energy.
What a packet looks like
A packet starts with an alternating-symbol preamble, then a 32-bit sync word. After sync, the receiver expects a compact frame: version and message type, a 16-bit payload length, payload bytes, and interleaved CRC-16 blocks every 255 bytes.
Optional Hamming(7,4) FEC is applied after CRC insertion. Each byte becomes two protected nibbles. It costs airtime, but it helps short field messages survive a rough channel.

Receiving without fancy DSP
The demodulator is noncoherent. For each symbol window it checks tone energy, chooses the strongest bin, and rebuilds bytes from two-bit symbols. Sync search is correlation-based: scan for the 32-bit sync pattern, refine around the best candidate, then decode the header and payload from there.
The receiver reports sync score, SNR estimate, CRC pass/fail, FEC corrections, and the last decode error. Those numbers matter because a radio packet link fails in parts. Did it hear energy but miss sync? Did the header decode but CRC fail? Did the frame pass with a bunch of corrections?
The field node
The field side is a Raspberry Pi target with an axum HTTPS server and a compact phone UI. The browser can send text, attach a manual or browser-provided location, start repeated SOS transmissions, and upload small JPEGs that get split into 250-byte chunks.
Audio I/O can use command-line audio paths to feed the handheld through a USB sound card. Software VOX/gating decides when incoming audio is worth trying to decode.

The base station
The base program is a ratatui dashboard around the receiver, transmitter, link stats, image handling, and reply mode. In manual mode, I type the reply. In Claude mode, the base station asks for a short answer and trims it to fit the radio frame budget before transmitting it back with the callsign prefix.
The prompt is part of the radio design. It asks for terse plain text, avoids tables and long lists, and includes a field location hint when the incoming packet provides one. Model output gets handled as a low-bandwidth radio payload.

Next work
Next is range testing with better antennas and a cleaner audio path, then better retransmission. Image chunks already have the shape of selective retransmit, but the main win is still text: a short-message link with explicit diagnostics and no dependency on mobile data at the field end.
I also want a cleaner enclosure and battery story for the field node. A hackathon prototype can be cables and tape. Field gear has to keep working when the demo-room assumptions disappear.