A minimal-parts AX.25 / APRS TNC that can pair with a phone.

Public Chat
Similar projects worth following
A cheap (<$20 in parts) TNC for amateur packet radio, designed to attach cheap handheld radios.

Some people my city's CERT team have been experimenting with AX.25 for radio message passing. Not counting the radio, a standard setup is $200 plus $20+ cable. I think can do better, using essentially just an ESP32.

The ESP32 will pair with a phone over bluetooth or wifi, and will send/receive audio signals from the handheld radio. The board will use the radio's battery for power.


  • require as few components possible, both number and price.
  • prefer off-the-shelf components.
  • simple enough to hand-build.
  • Be as useful as possible without requiring any software installed in advance. The whole point of this is to use when there's no internet. I can't convince every ham in my neighborhood to install an app.
  • Support both APRS and connected-mode AX.25, for connecting to PBBS

  • PCB prototype testing

    Evan10/20/2020 at 03:45 0 comments

    My PCB prototypes arrived today, and I went straight to test them. They generally seem to work!

    A day or two after I ordered the PCBs, I noticed that I had used a 6.3V-tolerant capacitor across Vbat and ground. Since the Baofeng uses a 2-cell lithium ion battery, max battery voltage is around 8.4V. Whoops. So, before testing, I desoldered this capacitor.

    I did some basic continuity tests, checked the polarity of the diodes, and then applied 5V across the 5V/GND pins on the programming header and checked Vcc. This read 3.57V -- higher than the 3.3V spec, but within the absolute maximum rating of the ESP32. (Once I soldered the ESP32 on, which provided a load for the power supply, this dropped to 3.31V -- much better.)

    I did some voltage checks on the programming header to make sure I wasn't going to blow up my wESP32-Prog, then plugged in the programmer and flashed the ESP-IDF Hello World app. This worked, though I had to keep constant torque on the pins to get good contact. The wESP32 has staggered pins for its programming header, which gives a friction fit for the wESP32-Prog. I had meant to do this, but didn't get around to it before ordering.

    Things to change for revision 2:

    • The slot where the clip is supposed to nestle needs to be a tiny bit wider -- I'm not getting much engagement on it.
    • The slot for the clip also needs to be moved -- it's too far from the top of the board, so once the ESP32 is soldered on, the clip doesn't reach it. This will probably mean moving the programming header to the other side of the slot. This ought to be more convenient anyway, as it would allow the board to be programmed while attached to the back of the radio.
    • Remove or change the part number for the Vbat bypass capacitor to something that can tolerate 8.4V
    • Possibly replace the entire power supply with a circuit that has lower quiescent current.
    • Stagger the programming header pins, to allow wESP32-prog to friction fit.
    • Labels on the PCB for the different headers. At the moment I need to reference the schematic and this handy page to figure out which headset wire needs to go to which pin.
    • Some sort of strain relief for the headset cable -- possibly just a pair of ~2mm holes to weave it through.

  • PCB prototypes ordered

    Evan10/10/2020 at 06:33 0 comments

    While procrastinating on solving the software issues around the ADC, I've been working on a PCB layout. I finally finished the layout tonight, and ordered some PCBs from JLCPCB.

    Here's hoping it all works!

    The board has:

    • Breakout of most of the ESP32-WROVER module's pins
    • A cutout where the Baofeng's belt clip can sit and keep the board in place.
    • A header meant to interface with the wESP32-Prog module (to help minimize the number of components)
    • A DC-DC power supply based around the XL1509 (the cheapest non-inverting DC-DC converter IC available as a "basic part" from JLCPCB).
    • The audio components
      • a clamp circuit that lets us get an interrupt when the audio input goes high without blowing out the GPIO pin.
      • a 4:1 voltage divider to drop the Baofeng's 0-8.4V audio output down to a range tolerable by the ESP32's ADC
      • A voltage divider / RC filter / DC blocking capacitor for smoothing the DAC output and dropping it to a level that the Baofeng's microphone input can handle.

    The clamp circuit on the PCB is different than what I've prototyped (it uses a 3-terminal voltage reference instead of 2 red LEDs), and my prototypes have been using DC-DC modules from aliexpress, so if I made a mistake it's likely in one of those two places.

    For the DC-DC converter, I was going to use the same IC as these aliexpress modules, the MP2315, but that's an "extended component", and I balked at paying an extra $3. That may have been a mistake in hindsight -- the XL1509 has higher quiescent current and lower efficiency than the MP2315, which may end up negating the benefits of using a DC-DC converter instead of a simpler linear voltage regulator. I only noticed this after placing my order.

    Assuming this works & doesn't require too many more components, I'm pretty happy with the result. In small quantities (5pcs), these boards cost $8 each for manufacture/assembly/shipping. JLCPCB doesn't stock any ESP32 modules or do through-hole assembly, so 0.1" header pins + the ESP32 module + the pogo pins I'm using as battery contacts still need to be added, which should be less than $5 per board.

    This is my first PCB layout, so I'd love to hear any feedback you have on the design.

  • PTT working, final prototype assembly, and some bug fixes

    Evan09/30/2019 at 05:30 0 comments

    Today I finally permanently soldered my headset connector into my board, which means I can actually pick up the project and carry it in one piece.

    Since I’m cannibalizing an off the shelf headset for the Baofeng, the wires inside my cable were super tiny and had enameled insulation. For now I’m using the fire method for stripping these but it’s not ideal: the insulation lights on fire and flames travel along the wire. Apparently fine grit sandpaper or some scary chemicals work pretty well. I think I’ll try sandpaper next time.

    Barring any issues that come up, I think this means I won’t have to make hardware changes for a while. I’d like to make a PCB for this instead of perfboard, and probably use an ESP32 castellated module instead of a breakout board, but in the meantime I think I’ll work mostly on software.

    On the software side, I coded up a PTT pin (up until now I had just been holding the PTT button when I wanted to transmit 😅), and fixed a bug with the formatting of my APRS locations, so now they are actually being parsed correctly by

  • A simple webapp that uses phone's GPS and sends location via APRS

    Evan09/26/2019 at 17:20 1 comment

    In my most recent commit, I added a simple webserver that has two endpoints:

    • Some static HTML+javascript served from /
    • A POST target that takes latitude+longitude and sends them out via an APRS packet

    The code ended up being fairly straightforward. It's certainly not amazing UX right now -- I really should add a button or something on the HTML page to trigger geolocation sending, as right now it just sends location when you load the page.

    Ultimately I think it would be nice make an APRS web interface that would allow you to both send your current location and see the received location of others. (I would have the esp32 store these locations whenever it hears an APRS packet).

    In iOS13, it looks like webapps saved to the home screen have the ability to execute code in the background, which is exciting: if we can access geolocation and XHR apis while backgrounded, we could periodically broadcast our location via APRS.

    Unfortunately, there's still no way for a webapp (even one installed to the homes screen) to create push notifications on iOS. This means I still can't use a webapp for the PBBS functionality I want to add, where users would receive some sort of push notification when we detect a message for them, at least on iOS. I think the Android PWA ecosystem is much more full-featured.

  • AX25 transmission works

    Evan09/18/2019 at 06:54 0 comments

    I finally managed to get my hacked up version of LibAPRS for ESP32 transmitting properly. I've had it almost working for several weeks, but couldn't get the audio output to be smooth -- it would occasionally jump suddenly between values. (The AFSK modulation used in AX.25 is supposed to be continuous-phase, and LibAPRS produces correct values, so this was a problem in my code.) I think I was underflowing my I2S buffers or something. By tweaking buffer sizes (both audio and AFSK bytes), it finally seems to be somewhat reliable. Good thing the ESP32 has a lot more memory than an ATMEGA328P.

    It's still not 100% reliable, even when going straight from audio output to my KPC-3+. Seems to decode properly about 50% of the time. But I was able to transmit at least one packet to an APRS IGate!

  • Successfully decoding packets

    Evan07/26/2019 at 21:58 0 comments

    Via a dirty patch to LibAPRS that removes or emulates all Arduino-specific APIs, I was able to get packets decoding correctly.

    The adc1_get_raw is the most straightforward way to sample from the ADC, but is not ideal:

    • A task that repeatedly spins on the CPU until another 1/9600th of a second has passed (the way many Arduino sketches work) would monopolize the CPU too much to work well in the concurrent FreeRTOS environment on the ESP32.
    • 9600 timer interrupts per second would be a lot of interrupts, and unless we dedicate one of the ESP32's two CPUs to the sampling task  we would not be guaranteed access to the CPU in time
    • Reading from the ADC this way takes ~1/6000th of a second.

    The datasheet mentions that the ADC can run at 200ksps when controlled by the RTC controller (I'm not sure what the DIG controller is). What gives?

    Well, it turns out you can instruct the i2s peripheral to sample from the internal ADC, and write samples into a buffer using DMA.

    In my patched LibAPRS, I disable the interrupt code that executes every 9600th of a second, and then in my application I call AFSK_adc_isr repeatedly, once we have a buffer full of audio.

    This works well, though it does mean I don't process packets until my radio's squelch closes. I think I may be able to refactor this to process smaller buffers as they come in.

  • Powering the ESP32 from the UV-5R’s battery

    Evan06/29/2019 at 23:32 0 comments

    My original plan was to power the ESP32 from the +V pin on the headset connector. However, I quickly discovered that the +V pin has 10kOhm resistance— certainly not capable of providing the 250mA+ needed by the ESP32 while using WiFi.

    Plan B is to use the UV-5R’s battery’s charging contacts on the back:

    This little backpack board is using two spring-loaded test pins, plus a 7805. (From what I’ve heard, the onboard regulator may be able to handle more than 5V input, but I’d rather play it safe. Going from 5V to the 8.4V of a freshly charged lithium ion battery would triple the voltage drop and power dissipation requirements of the regulator.)

    It’s held in place with the spring-loaded clip on the back of the Baofeng, which happens to be just the right width to fit between the two rows of headers that mate with my ESP32 dev board.

  • Playing with Bluetooth PAN

    Evan06/13/2019 at 17:55 0 comments

    I learned that the ESP32 requires something like 90mA to maintain a wifi connection. This would limit the battery life -- the UV-5R's stock battery is 1800mAH. If using a linear regulator, the ESP32 in this mode would drain the battery in 20 hours on its own. With the HT's load, we probably couldn't expect more than about 12 hours of battery -- not super great in an emergency! (DC-DC converters could improve this, but would add to the BOM cost.)

    However, I think we can do better. We only really need a network connection to the phone when there's data to send either direction. Can we shut down the connection until we receive audio from the HT (for receiving), or the user presses a button or something (for sending)? I'm not sure this is really possible with wifi -- if a phone isn't awake, I don't know if it bothers to look for wifi networks. (I still need to test this.) However, I think a bluetooth device can initiate a connection to a phone with which it has already paired.

    Since I'm trying to avoid the need for any custom apps, I'm pretty much limited to TCP/IP. For Bluetooth, this means the PAN profile.

    It doesn't seem like ESP-IDF supports PAN, but this github issue pointed me to BTStack, an alternate bluetooth stack that can run on the esp32, and has a PAN demo in its examples folder. Unfortunately, this demo is not supported on the esp32 -- it wants a POSIX system so it can dump packets into a TAP interface. However, reading the code a bit, I realized that if I implemented the functions exported in btstack_network.h, I could maybe get this to work. That was my goal with this repository, which tries to bridge btstack and lwip (the TCP/IP stack on esp32), and runs a DHCP server and a demo HTTP server.

    After running into a few crashes and learning how to use core dumps and the gdb stub, I was able to get it to run without crashing when someone tries to pair with it, but it does not function properly yet. My laptop doesn't get an IP address assigned by the dhcp server, and cannot talk to the HTTP server. I suspect I'm doing something silly - maybe I need to unwrap the received packets a bit before forwarding them to LWIP.

    After sharing my progress on the github issue, the maintainer of BTStack showed up with a new example that should do more or less the same thing, but hopefully work better than mine.

View all 8 project logs

Enjoy this project?



jphillips7291 wrote 04/27/2020 at 02:59 point

Hi Evan,

Did this ever get finished? i have an ESP32 just waiting for this project.

  Are you sure? yes | no

Evan wrote 04/27/2020 at 04:50 point

Unfortunately no, I haven't spent much time on this recently. Current status is pretty much the same as when I replied to Ryan Kinnett on 2020-03-31.

  Are you sure? yes | no

Ryan Kinnett wrote 03/31/2020 at 00:26 point

Hi Evan, thanks for sharing this project.

I’m looking at building a similar TNC as a modem for Winlink, to support ARES in my area.  

1) Do you think you might finish implementing the kiss protocol?  If not soon, I might take a crack at merging with kiss libraries others have built. I think Mark Qvist has one.

2) Have you had any success getting 9600 bd working reliably?  I can try to merge the apll bug fix but it will take some time for me to understand it.

3) Do you expect any issues sending and receiving  emails, rather than just position data, particularly over a Bluetooth link to/from the TNC?

I was originally planning to just make my Esp32 an audio I/O relay and do the modem work in software on the PC, but I needed a way to remotely control PTT anyway, and  there are many advantages to encapsulating the audio demodulation on hardware.  The audio relay approach is still interesting though.  If someone were to write a software modem app for iOS, for example, then an iphone could be used as an endpoint despite lack of BLE serial support.  I might still develop this approach for the Windows to Esp32 link as a fallback option if I can’t get demodulation working reliably on the Esp32.

Anyway, thanks again, and let me know if I can help. I don’t have an off-the-shelf TNC to test with but I might be able to borrow one from a local club.

Come to think of it, you could also test your mod/demod code by connecting to the mic and aux-out ports on a PC and using a software modem.

  Are you sure? yes | no

Evan wrote 03/31/2020 at 02:50 point

Yeah, I also thought about trying to basically use the esp32 as a bluetooth audio adapter. I worry about audio compression making (de)modulation impossible.

I'm pretty sure the modulation and demodulation code is correct, since I took that from LibAPRS, but I think I'm not producing/consuming samples quickly enough or something - there's jitter in the audio output / input, so what LibAPRS produces isn't what comes out of the ADC.

Yeah, I have found the best way to test is to wire up the ADC/DAC to a PC and record or play audio, though I have a partially built test rig, with two ESP32s wired together.

To answer your specific questions:

1) Feel free to take a crack at the KISS protocol. I think it's fairly simple, but my first priority is getting ADC/DAC working properly without hiccups.

2) I haven't tried 9600 baud AX.25 if that's what you're asking. I think I successfully worked around the i2s sample rate bug, though. I should check whether they've fixed the bug upstream in the mean time.

3) To get emails working on-device (I plan to have an SMTP/POP<->PBBS bridge), I need to get the first few layers of the stack working. As I mentioned, the physical layer (audio rx/tx) is not quite working. Layer 2 (packets) more or less work for APRS at least, and I don't think raw AX.25 packets are going to be too much work. Layer 3 (connected mode) will be a bit more work. LibAPRS doesn't implement this, since APRS is datagram mode only. I'll have to find+port someone's connection state machine, or implement my own.

Of course if KISS worked, you could configure winlink to use that.

I'll play around with audio rx/tx tonight, see if I can get it working reliably.

  Are you sure? yes | no

Evan wrote 03/31/2020 at 05:58 point

(I ended up fighting fighting my development environment all night. Will try again tomorrow.)

  Are you sure? yes | no

Ryan Kinnett wrote 04/01/2020 at 01:39 point

Good call.  Have to nail ADC/DAC first.  

I'll try and setup IDF tonight and see if I can compile and flash your code to my NodeMCU Esp32.  I have an oscilloscope to test with.  Also, I'll record some winlink traffic to help with testing.

EDIT:  I misread your comment about Layer 3.  As you noted, if we get KISS working with 2-way AX.25, then terminal apps i.g. winlink handle RF link handshaking.  Are there use cases that require the connection layer to be handled by the modem?  Is that to support all operating modes other than KISS?  Are you interested in porting other protocols, i.g. winmor to support HF?

Thanks for spinning this back up :)

  Are you sure? yes | no

W5DMH wrote 01/21/2020 at 13:36 point

So your last post on this project was September 2019, have you made any further progress with this? 

I have been working on a PiZero webserver attached to a Kenwood TH-D72a (HT with a built in KISS TNC)  this is working but a bit cumbersome and battery  draining . I am really interested in a PBBS capable packet solution using an ESP32.

  Are you sure? yes | no

Evan wrote 01/21/2020 at 18:07 point

Yeah, I've been distracted away from this project for a while, with holidays and other hobbies.

The current status is that somehow, receive broke (my guess is a timing issue... Getting a steady stream of audio without hiccups is surprisingly easy to mess up). The next thing I'd like to do is put together a test board, with two esp32s wired together so that audio out on one goes to audio in on the other. This would let me set up automated tests.

If you've already got a KISS TNC, it would be interesting to wire it up to the esp32 and then do network applications from there. I'd love to share effort on ESP32/AX25 applications somehow -- I currently just have a webserver[1] that serves a javascript app that requests the phone's location and then transmits that over APRS, and a simple TCP server[2] that doesn't yet implement the KISS protocol, despite the name.



  Are you sure? yes | no

Tait Leaney wrote 11/14/2019 at 10:38 point

Hi Evan, I'm struggling to get the tnc to work using Arduino IDE. Could you share which dev environment you used?

  Are you sure? yes | no

Evan wrote 11/14/2019 at 18:30 point

It's based around ESP-IDF, so you should just need to `make flash`. You may need to `make menuconfig` and play around with baud rate settings, clock speed, etc. to work with your specific esp32 board.

  Are you sure? yes | no

dfmcwhir wrote 11/17/2019 at 00:50 point

I'm also having problems getting the code to compile in the Arduino IDE. I'm not sure if it is a configuration issue on my end or if it is a problem in the code. It the code here: supposed to be working? I was getting errors about uint8_t type not being defined which I fixed but now I'm having problems with multiple definitions of 'Serial'. I assume this related to the FakeArduino.h files, but I'm not sure how to fix it. Any suggestions would be appreciated because I really want to make this work.

  Are you sure? yes | no

Evan wrote 01/21/2020 at 18:13 point

It might be possible to make LibAPRS-esp32-i2s compile under Arduino by deleting FakeArduino.h/.cpp and changing all the #include "FakeArduino.h" to #include <Arduino>

But as is, this project is meant to build under ESP-IDF:

  Are you sure? yes | no

jphillips7291 wrote 10/22/2019 at 22:04 point

This is such a great project! Looking forward to it's completion. I would love to add a DHT22 sensor into this for temp and humidity reporting like a weather station. that would be epic, and cheap.

  Are you sure? yes | no

Evan wrote 10/22/2019 at 23:52 point

Yeah, for a remote APRS node that would be awesome. I think for my use case (several field teams in a relatively small area), weather won't matter much - everybody will be experiencing more or less the same weather.

Since I'm intending for my project to be used in a disaster scenario (major earthquake), maybe a natural gas detector or smoke detector would be useful. But then again, since each of these will be paired with a human, I can also just rely on the humans reporting unusual smells.

When I get around to laying out a board, I'll make sure to leave some pins exposed for i2c comms and/or ADC measurements.

  Are you sure? yes | no

Tait Leaney wrote 09/30/2019 at 10:12 point

This is a great project. I'm new to C programming but thought I would try my hand at using an ESP32 + an SA828 to construct an APRS device. Your work here seems pretty handy!

  Are you sure? yes | no

Evan wrote 09/30/2019 at 19:31 point

Thanks! I hadn’t heard of the SA828 before, it looks interesting. Maybe I’ll make a version of this that uses that board.

I think my software should be useful to you, especially the  LibAPRS fork. Probably you just need to add code to control the frequency and maybe adjust input and output audio levels.

If you make a Hackaday project page for your project, make sure to share it here!

  Are you sure? yes | no

Ido wrote 09/04/2019 at 18:31 point

This is great project!
Do you need some kind of modem?
How the connection to the radio is done?

  Are you sure? yes | no

Evan wrote 09/30/2019 at 16:42 point

Thanks! The ESP32 acts as the modem, using my patched version of LibAPRS, plus a few analog components: resistors, capacitors, and diodes.

  Are you sure? yes | no

Evan wrote 06/13/2019 at 17:23 point

Great find! I'd definitely like to reuse the mechanical parts of that.

I haven't played with 9600 baud yet, but that would definitely be useful -- might make it feasible to send more than just data. I wonder if the Baofeng has good enough audio to support it.

  Are you sure? yes | no

Arnaldo Pirrone wrote 06/06/2019 at 18:15 point

Hi, you can get ideas from this project:

It would be great if you can also do 9600+ baud packet radio.

  Are you sure? yes | no

James Hall wrote 06/23/2019 at 18:55 point

I really wish the Baofeng's would breakout a motomod style of connector for this kind of experimentation.

  Are you sure? yes | no

Evan wrote 09/30/2019 at 16:57 point

9600+ baud would be awesome. My understanding is that 9600 baud is much trickier, since it starts to approach the bandwidth limit. I've also heard that it may require ports that most radios don't have ("discriminator output"), but maybe that's FUD. I honestly haven't looked into it much, since my first goal is to interoperate with existing packet nodes around me, and I have only heard of people using 1200 baud near me.

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates