Pi-StAPrS

Pi-StAPrS is actually a ham radio programming project I hacked together in 2019. On one hand, it belongs on the ham blog I’ve done nothing with since getting involved with this one. On the other, it’s a coding project I’ve done; and I feel that it should be with all my other coding projects as a portfolio. The name is a mash-up of Pi-Star and APRS.

A Brief History of Why

In ham radio we have a couple of digital voice modes that are in active use on the VHF bands and up. While all three standards are incompatible; the provide the same main function that people really want, internet linking. These modes are able to digitally link repeaters over the internet; and the most common usage of these modes is to just talk to other people on a radio that normally just covers local areas.

However, there are a number of issues with this. The primary one is “people tying up the repeater”; one guy hogging it to talk to some people online while two guys want to talk locally. The reality is most repeaters on VHF and UHF aren’t used all that much. But of course; you actually have to be able to get a signal to them. With many HOA’s restricting outdoor antennas; many aren’t able to get in to the repeater from home.

“Digital Voice” hotspots for ham radio solve this problem. These devices basically contain a special transceiver that is able to modulate and demodulate the digital modes; and through the magic of an STM32 microcontroller and a bunch of other software, they can provide that last link between the internet-connected networks of these digital modes, and your radio. The configuration of hardware I was using was an MMDVM (multi mode digital voice modem) RF board (STM32 and 2/3/4PSK radio) connected to a RaspberryPi; which provides the internet connectivity, user interface, as well as handling configuration and communication of the radio board. The main software for this is called Pi-Star.

APRS is also an amateur-radio only thing. The origins are vast and the system has been retooled and even “abused” by most everyone. It’s based on the AX.25 packet format and is the primary use of packet radio in ham these days. The basic idea (rather than the inteded use or actual use), is that you have a small radio transmit a packet of data that contains GPS information. This signal is then picked up and transmitted to a wider area. Since these packets can now make it to the internet where you can largely see all the APRS activity; most people (myself), largely just “play” with it. I don’t do much except turn it on when I’m driving around.

“I Want People To Know Where I’m At”

I was planning a big road trip; and in addition to my normal ham radio in the car I had both of my hotspots. I knew I’d be driving in places where there wasn’t any repeater coverage; so I would at least have the internet option. But I also wanted some of my other ham-friends who had a digital setup to talk to me, if they wanted, while I rolling down the highway. I didn’t want to stick myself only on certain talkgroups/reflectors; I wanted the ability to move around my usual spots and those that wanted to find me, would know where I was at.

It started to occur to me that the internet function of APRS might work well. It’s possible to push packets directly to the network if you’re a licensed ham and have the authorization code; which I do. So I found myself in a position I’d found myself in before; I had an idea for something I wanted to do, but no clue how to accomplish it. I was just coming off a RPi/Python project that started out the same way…and is in fact what pushed me to pick up more coding and such along the way. (Sometimes confidence is the best thing to have when learning.) Assuming I would just be plugging programs together, rather than try this in Python (which I could have done); I went with a bash script. It would be my first.

pistaprs.sh

#!/bin/bash
# script activation/webui control
[ ! -s "/tmp/aprs" ] &&  exit

#       Script Configuration

# Make sure you're really tested the script before you activate!!!!!
activate=0
#GPS server IP/Port
ip=hostnameorip
port=port
# Use GPSD? (0 = No, 1 = Yes)
gpsd=1
# Android "gps tethering" app (or NMEA over IP)?
gpstether=0
# Is 'GPS Tether' UDP?
gpsudp=0
# APRS-IS user id
user=urcall
# APRS-IS passcode
password=urpasscode
# APRS Object ID/SSID
senduser=urcall-ssid
# APRS Icon Selection (refer to pdf)
# Standard table
table="/"
# Alternate Table - this MUST be double backslash or it won't work
#table="\\"
# Symbol - If your symbol is a backslash, you must double backslash.
symbol=">"
# What system are you running? (And if you say YSF then good luck pal.)
# YSF is completely untested.
mode=dstar
#mode=dmr
#mode=ysf


# Why the hell did I have to add more GPS support?
[ "$gpsd" = "$gpstether" ] && echo "Invalid GPS Configuration" && exit
[ "$gpsd" -eq 1 ] && nema=$(gpspipe -n 9 -r "$ip:$port")
[ "$gpstether" -eq 1 ] && [ "$gpsudp" -eq 1 ] && nema=$(timeout 3 ncat -u $ip $port)
[ "$gpstether" -eq 1 ] && [ "$gpsudp" -eq 0 ] && nema=$(timeout 3 ncat $ip $port)
# New filtering method for altitude and non-standard sentence names.
rmc=$(printf "$nema" | sed -n '/$G.RMC/{p;q}')
[ -z "$rmc" ] | echo "Better luck next time. (Or modify script for more gpspipe data.)" && exit
gga=$(printf "$nema" | sed -n '/$G.GGA/{p;q}')
# GPS Lock Check
gpss=$(printf "$rmc" | cut -d ',' -f3)
[ "$gpss" = "V" ] && exit
# GPS Coordinate Set
lat=$(printf "$rmc" | awk -F, '{printf "%07.2f%c", $4, $5;}')
[ -z "$lat" ] && echo "Latitude error?" && exit
lon=$(printf "$rmc" | awk -F, '{printf "%08.2f%c", $6, $7;}')
[ -z "$long" ] && echo "Longitude error?" && exit
# Set heading & speed
hsp=$(printf "$rmc" | awk -F, '{printf "%03.0f%c%03.0f", $9, "/", $8}')
# Altitude (aka literally the only reason we pull a $gpgga)
altm=$(echo "$gga" | cut -d ',' -f10)
ft=$(echo "/A=$(echo "$altm*3.280839895" | bc | xargs printf "%06.0f")")
# DMR - Scrape for TGs and set comment.
[ "$mode" = "dmr" ] && tg=$(curl -s http://127.0.0.1/pistaprs/bmscrape.php| sed 's/<[^>]\+>//g' | sed 's/None//' | sed ':a;N;$!ba;s/\n/ /g' | sed 's/TG/#/g')
comment="Brandmeister TGs: $tg"
# DSTAR - Scrape for reflector and set comment.
[ "$mode" = "dstar" ] &&  comment="DStar "$(curl -s http://127.0.0.1/mmdvmhost/repeaterinfo.php | egrep "Linked|linked" | sed 's/<[^>]\+>//g' | sed 's/L/l/')
# Dear god why the hell are you using an entirely untested mode?
[ "$mode" = "ysf" ] &&  comment="YSF "$(curl -s http://127.0.0.1/mmdvmhost/repeaterinfo.php | egrep "Linked|linked" | sed 's/<[^>]\+>//g' | sed 's/L/l/')
# Here's how we hand-craft an APRS packet. (Who needs a client anyway?)
data="$senduser>APRS,TCPIP*:!$lat$table$lon$symbol$hsp $comment $ft"
# Send data to the terminal for testing
[ "$activate" -eq 0 ] && printf "%s\n" "user $user pass $password" "$data"
# Make sure output is sane before actually commiting to server.
[ "$activate" -eq 1 ] && printf "%s\n" "user $user pass $password" "$data" | ncat rotate.aprs2.net 14580

In order for this to work, it requires a source of GPS information. At the time I’d hacked together a stratum-1 NTP server using a RPi and Adafruit GPS hat, so I had this nice installation of gpsd already at my disposal. There are also some smartphone apps that do GPS sharing over wifi and/or bluetooth. The one I tried was just literally pushing the data on a port; so it wasn’t too critical to add support for these.

# script activation/webui control
[ ! -s "/tmp/aprs" ] &&  exit

The first thing the script does is check /tmp/aprs to see if it’s blank. This script was designed to be run as a cronjob every 5 minutes; but I don’t want or need the thing sending a beacon all the time. Since the main interface to Pi-Star is through a web-browser; I hacked together some PHP that would write a character to /tmp/aprs to activate, or null the file to deactivate. The use of /tmp means the file disappears after reboot, making the behavior off by default; but Pi-Star is also a read-only filesystem and /tmp is one of the few places you can always write to.

The chunk of code after that (which I’m not duplicating because it’s too much) is all script configuration; which gps method are you using, what’s your aprs-ssid, what’s your password, which digital mode are you using, which symbol do you want for your APRS icon, and variable that you have to activate to actually send data. This was done because I did not want people accidentally sending garbage to the APRS-IS and my script being to blame. So it ensures that you’ve configured everything.

[ "$gpsd" = "$gpstether" ] && echo "Invalid GPS Configuration" && exit

This accomplishes the same thing…it’s the “you didn’t configure this” idiot check I’ve seen in a lot of scripts. Except where those scripts had a whole variable just for this purpose; I look to see if the GPS options match, which would be invalid. The script “ships” with both turned off…triggering the same error.

[ "$gpsd" -eq 1 ] && nema=$(gpspipe -n 9 -r "$ip:$port")
[ "$gpstether" -eq 1 ] && [ "$gpsudp" -eq 1 ] && nema=$(timeout 3 ncat -u $ip $port)
[ "$gpstether" -eq 1 ] && [ "$gpsudp" -eq 0 ] && nema=$(timeout 3 ncat $ip $port)

So what we’re actually going to do here, is depending on the GPS configuration; just pull a number of NEMA sentences and drop them in to the nema variable. gpsd support was easy using gpspipe; but the generic tethering options required options for UDP.

# New filtering method for altitude and non-standard sentence names.
rmc=$(printf "$nema" | sed -n '/$G.RMC/{p;q}')
[ -z "$rmc" ] | echo "Better luck next time. (Or modify script for more gpspipe data.)" && exit
gga=$(printf "$nema" | sed -n '/$G.GGA/{p;q}')
# GPS Lock Check
gpss=$(printf "$rmc" | cut -d ',' -f3)
[ "$gpss" = "V" ] && exit

When I added support for GPS tethering, I had to modify the sentence names to basically wildcard a character; this was due to how the tethering apps sent their data. Thankfully, it was just one character I could wildcard. There are also two sanity checks; the first looks to see if we grabbed a RMC sentence, the other looks to see if the GPS lock is valid. If either one fail, we aren’t running the script. That echo is actually a debugging throwback as I was trying to figure out how much data I needed to pull.

# GPS Coordinate Set
lat=$(printf "$rmc" | awk -F, '{printf "%07.2f%c", $4, $5;}')
[ -z "$lat" ] && echo "Latitude error?" && exit
lon=$(printf "$rmc" | awk -F, '{printf "%08.2f%c", $6, $7;}')
[ -z "$long" ] && echo "Longitude error?" && exit
# Set heading & speed
hsp=$(printf "$rmc" | awk -F, '{printf "%03.0f%c%03.0f", $9, "/", $8}')
# Altitude (aka literally the only reason we pull a $gpgga)
altm=$(echo "$gga" | cut -d ',' -f10)
ft=$(echo "/A=$(echo "$altm*3.280839895" | bc | xargs printf "%06.0f")")

I have to really thank Google and all the random forum users out there. At this point I knew how I needed the data formatted..I just did not know how. It was through massive searching that I borrowed (and sometimes stole) awk or sed commands; usually trying to have some kind of idea about how it worked. But a lot of this is the result of just googling how to format or convert data. I do recall the conversion from meters to ft was largely just copy/pasted from a forum posting…in fact that was a common conversion requirement.

# DMR - Scrape for TGs and set comment.
[ "$mode" = "dmr" ] && tg=$(curl -s http://127.0.0.1/pistaprs/bmscrape.php| sed 's/<[^>]\+>//g' | sed 's/None//' | sed ':a;N;$!ba;s/\n/ /g' | sed 's/TG/#/g')
comment="Brandmeister TGs: $tg"
# DSTAR - Scrape for reflector and set comment.
[ "$mode" = "dstar" ] &&  comment="DStar "$(curl -s http://127.0.0.1/mmdvmhost/repeaterinfo.php | egrep "Linked|linked" | sed 's/<[^>]\+>//g' | sed 's/L/l/')
# Dear god why the hell are you using an entirely untested mode?
[ "$mode" = "ysf" ] &&  comment="YSF "$(curl -s http://127.0.0.1/mmdvmhost/repeaterinfo.php | egrep "Linked|linked" | sed 's/<[^>]\+>//g' | sed 's/L/l/')

So here was the main goal I wanted; automatically updating reflector/talkgroup information. APRS has support for comments, so it seemed natural to slip my comments in to the APRS beacon for each hotspot; I just had to figure out how to actually retrieve that information.

Thankfully, since Pi-Star is all web-based; the information I want is generated as HTML pages and I’m willing to scrape them. In the case of DMR information; this is actually retrieved using another file that interfaces with the network’s back-end. I use a modified copy that returns just the group numbers. The D-Star and Yaesu modes are scraped from a pi-star page directly. The sed commands here….more googling and trial and error. I still credit forum users for most of this; I just figured out how to plug it all together.

# Here's how we hand-craft an APRS packet. (Who needs a client anyway?)
data="$senduser>APRS,TCPIP*:!$lat$table$lon$symbol$hsp $comment $ft"
# Send data to the terminal for testing
[ "$activate" -eq 0 ] && printf "%s\n" "user $user pass $password" "$data"
# Make sure output is sane before actually commiting to server.
[ "$activate" -eq 1 ] && printf "%s\n" "user $user pass $password" "$data" | ncat rotate.aprs2.net 14580

Now we’re at the end of the script. We hand-craft the APRS packet to a variable, and then either echo it to the terminal for sanity checking; or if we know the script is functioning, ncat it directly to the APRS server

I sadly don’t have any screenshots from when I tested this; and I never actually had a chance to use it as intended. I attempted; but something broke along the way. Due to how that trip turned out, I never looked in to it. Oh well; it’s here if I ever want to give it a try again.

Site comments disabled. Please use Twitter.