2026-02-15
TL;DR: I got thoroughly distracted and started building a terminal node controller out of an Arduino Nano in Rust. The firmware exposes a KISS interface over its USB serial connection and only supports transmission so far.
In my second blog post, I explained that I used an external microphone connected to my laptop to decode APRS messages from my Baofeng HT. I also mentioned that I might set up a proper electrical connection between the two devices to improve the quality of the received signal and maybe be able to transmit. I wasn’t intending to work on that last week, but I started wandering around in my brain and realized I could probably do it somewhat easily with three more years of tools and experience.
It turns out that I was mostly right about that. My Baofeng has since
stopped working, but I bought a TIDRADIO TD H3 that has the same
two-plug connector as basically every other HT out there. It came with
one of those mostly useless microphone/earbud combos, but it has the
special connector on one end, so I cut it off and soldered the tiny
wires to a TRRS breakout board. Then I plugged that into a
USB-C audio adapter, which I could connect to my phone or laptop for use
with APRSdroid or direwolf,
respectively.
After some research into audio connector standards and experimentation with volume levels, I managed to decode my local APRS traffic and transmit a message to the network. This was mildly interesting and could have been a reasonable end for the project.
One tiny annoyance remained. To send an APRS message, something has to tell the radio to key up (i.e., momentarily switch to transmit mode) for the duration of the transmission. The period during which the radio is keyed up must not be substantially longer than the transmission itself because it’s impolite to make other stations wait too long to use the channel. With the setup I’d built so far, there wasn’t any way to tell the radio to key up, so I basically had two options: I could use the radio’s built-in VOX feature, which keys up when it sees audio on the microphone input, or I could key the radio manually by pushing the PTT button myself. Using VOX on APRS is considered bad etiquette because most VOX circuits hold the radio in transmit for at least a few hundred milliseconds after the transmission ends. Manual PTT is the only realistic solution without more hardware1, so that’s what I’d been doing for my tests.
Then I had the idea that I could move the waveform generation logic into a microcontroller and expose a serial interface to the USB host. The microcontroller would be able to pull the PTT line low whenever it needed to, but the microcontroller would also have a lot more work to do than the simple passive connector hack I’d already put together. It turns out that this is already a common idea that’s about as old as packet radio itself. The device I’m describing is called a terminal node controller (TNC), and the serial interface almost always follows the Keep It Simple, Stupid (KISS) protocol.
TNCs are available as commercial products, and some even have Bluetooth. But one thing they all seem to have in common is high cost (~$100). I can guess why the costs are high—a combination of relatively rich buyers and low sale volume—but the actual material cost for a minimal homebrew TNC is much lower. I decided to try to prove this to myself by implementing a KISS TNC for an Arduino Nano, which has become my go-to platform for inexpensive microcontroller hacking.2
N1ABC>K9XYZ:This is a test message..
This led me through a bramble of old specifications, best practices,
hearsay, conventions, source code, and a multi-decade history of
patches, updates, and hacks. I definitely don’t understand even most of
what I’ve read, but I seem to have built something that
direwolf can decode, and that’s good enough for now. My
rough understanding of the situation follows, but I’ve listed several
resources that I found useful at the end of this post.
APRS is a system for sending short messages to other stations. These messages may be directed to particular stations and optionally acknowledged, but most often the messages are sent with the intention of being broadcast to all stations. Stations that receive these broadcast messages may then choose to rebroadcast them immediately afterward according to certain rules, which allows messages to propagate through the network beyond the capabilities on any single station. Operators typically use APRS for any information that is both timely and location-specific, such as weather reports and vehicle positions. The APRS specification defines several standard message formats for use cases where that makes sense.
That’s the top layer of the de facto standard APRS stack. The next layer down is AX.25, which is mostly a framing protocol. I didn’t have to do much with AX.25 with this project because the host-side KISS client handles it internally. AX.25 frames have a handful of fields with special meanings depending on the context which have grown and changed over time. Much of packet radio predates the internet, so there are also plenty of leaky abstractions between what we now consider to be different layers in the OSI model. As far as I know, anything that goes into or comes out of a TNC is AX.25, but the TNC itself can mostly treat it as opaque data. That’s good enough for me.
The TNC has to care much more about the next layer down. AX.25 packets are transmitted as High-level Data Link Control (HDLC) frames. During transmission, the TNC must convert the KISS message into an HDLC frame, which is a matter of extracting the AX.25 payload, wrapping it in frame delimiters, computing a CRC, and sending the bits in the right order. I’m eliding a lot of detail because it isn’t that interesting, but getting this part right took me a long time.
The final task is to actually send the bits. Amateur VHF packet radio
is based on Bell 202
AFSK, and HDLC frames are transmitted with NRZS
encoding. This takes a little extra thought to build because bits
are encoded by either changing the tone (a 0 bit in NRZS)
or keeping it the same (a 1 bit) across symbol periods. I
implemented this with a pair of hardware timers that drive a state
machine and a five-bit R-2R DAC. At 16 MHz, the Nano can comfortably
generate the required tones in software.
I know that was a lot of jargon that probably didn’t make much sense
unless you already understand how the system works. This is mainly a
summary for my own use and amusement. For details, I suggest looking at
the code (either mine or
direwolf’s) or perusing the resources I’ve linked at the
bottom of this page.
Yesn’t. I haven’t tested it on the air, but I can send KISS data
commands from my desktop to the Arduino, the TNC converts them into
sound, and then direwolf can decode the sound back into the
original packet. I’ve also compared the waveforms that both systems
produce when given the same input, and they appear practically identical
at the sample level. I haven’t built any kind of receiver/decoder yet,
but I expect to be able to sample an analog pin, do some kind of clock
recovery, and identify symbols using zero crossings or similar. Then I
just have to push the decoded frames back through the KISS link. I don’t
expect the receiver to be any easier to build than the transmitter, but
I at least won’t have to re-learn the APRS stack from zero.
My plan for next time is to finalize the circuit (which I didn’t really discuss here) and make a PCB for it. Then I’ll probably test transmitting for real and begin the receiver/decoder.