Skip to content

Guru-RF/PILoRa433APRSiGate

Repository files navigation

PiLoRa 433 APRS iGate

A lightweight APRS iGate written in native C for the Raspberry Pi Zero 2 W with the 64-bit Raspberry Pi OS. It receives LoRa (433 MHz) packets from a Guru-RF RX-only LoRa HAT and forwards them to APRS-IS. No runtime dependencies beyond libc — no Python, no Blinka, no pip packages.

What it does:

  • Receives LoRa packets at 433.775 MHz (SF12 / BW125 / CR4:5, RadioHead-style)
  • Forwards packets carrying the 0x3C 0xFF 0x01 prefix to APRS-IS over TCP
  • Beacons the iGate position every 15 minutes
  • Blinks the activity LED on each received packet
  • Logs to a remote RFC5424 syslog (and to stdout / journald)

Files

File Purpose
igate.conf Station configuration (callsign, location, hosts, pins) — read at runtime
config.c/.h Config file parser + defaults
igate.c Main loop: APRS-IS connection, RX polling, beacon, LED
rfm9x.c/.h SX127x LoRa driver over Linux spidev
aprs.c/.h Base-91 APRS position + timestamp encoding
gpio.c/.h GPIO output via the kernel character device (uAPI v2)
log5424.c/.h RFC5424 syslog over UDP
Makefile Build
install.sh Build + install as a systemd service
piaprsigate.service systemd unit

How the hardware is accessed

  • SPI uses /dev/spidev0.1. On the Pi, spidev0.1 is chip-select CE1 = BCM GPIO7, which is the pin the original Python used as the radio CS (board.D7). Each register transaction is a single SPI message so CS stays asserted across the address + data bytes, matching the CircuitPython driver. If your HAT wires CS to CE0 (BCM GPIO8) instead, set spi_device = /dev/spidev0.0 in igate.conf.
  • GPIO (reset BCM25, activity LED BCM19, power LED BCM13) uses the Linux GPIO character device (uAPI v2). This works on Bullseye, Bookworm and Trixie without depending on a specific libgpiod version, and the chip that owns the 40-pin header is auto-detected.

Build

On the Pi:

sudo apt update
sudo apt install -y build-essential git
git clone https://github.com/Guru-RF/PILoRa433APRSiGate.git
cd PILoRa433APRSiGate
make

This produces the piaprsigate binary.

Configure

All configuration lives in igate.conf, a plain key = value text file read at startup (callsign, passcode, lat/lon, altitude, symbol, APRS-IS host/port, syslog host/port, GPIO pins, frequency, TX power). Editing it does not require a recompile — just restart the service:

sudo vi /opt/PiAPRSiGate/igate.conf
sudo systemctl restart PiAPRSiGate

The config path is resolved in this order: the command-line argument (piaprsigate /path/to/igate.conf), then /opt/PiAPRSiGate/igate.conf, then ./igate.conf in the current directory. Any key you omit keeps its built-in default; unknown keys and malformed lines are warned about and skipped. # and ; start comments; # is also allowed inline (preceded by a space).

Install as a service

sudo ./install.sh

This installs build dependencies, ensures SPI is enabled in /boot/firmware/config.txt, compiles, installs the binary and a default igate.conf (an existing one is preserved) to /opt/PiAPRSiGate/, and enables the PiAPRSiGate.service systemd unit.

If you just enabled SPI for the first time, reboot before starting so /dev/spidev0.x appears.

sudo systemctl start PiAPRSiGate
sudo systemctl status PiAPRSiGate

Logs

The service logs to stdout, which systemd captures in the journal (it also sends RFC5424 syslog to syslog_host if configured). View it with journalctl:

# Follow live (most common — like tail -f)
journalctl -u PiAPRSiGate -f

# Last 100 lines
journalctl -u PiAPRSiGate -n 100

# Everything since the last boot
journalctl -u PiAPRSiGate -b

# Only today / a time window
journalctl -u PiAPRSiGate --since today
journalctl -u PiAPRSiGate --since "2026-06-08 09:00" --until "2026-06-08 17:00"

# Errors only (the app logs RX/TX at info, failures at err)
journalctl -u PiAPRSiGate -p err

# Newest first, no pager (dump to terminal)
journalctl -u PiAPRSiGate -r --no-pager

# Grep received/sent packets
journalctl -u PiAPRSiGate -f | grep -E "Received|Sent packet"

Useful flags: -u selects the unit, -f follows, -n N limits lines, -b is this boot, -p err filters by priority, -o short-iso shows ISO timestamps, and --no-pager prints straight to the terminal. If the journal isn't persistent across reboots, enable it once with sudo mkdir -p /var/log/journal && sudo systemctl restart systemd-journald.

Not running under systemd? Start it in a terminal — sudo /opt/PiAPRSiGate/piaprsigate /opt/PiAPRSiGate/igate.conf — and the same lines print straight to stdout.

Design notes

  • The Python asyncio design (three concurrent tasks + a queue) is replaced by one cooperative loop that polls the radio every 10 ms, fires the 15-minute beacon, drains the APRS-IS socket to detect disconnects, and advances a non-blocking LED blink state machine so the blink never stalls reception.
  • TCP keepalive options (SO_KEEPALIVE, TCP_KEEPIDLE=300, TCP_KEEPINTVL=30, TCP_KEEPCNT=5) match the original.
  • SIGTERM/SIGINT shut the radio down and turn the LEDs off cleanly.
  • The base-91 position encoder was verified to produce byte-identical output to the original APRS.py.
  • lora_timeout is cosmetic (logged once at startup). Because reception is a non-blocking poll rather than a blocking timed receive, it does not bound a receive window. The default (900) is kept for parity with earlier versions.

Credits

Built by RF.Guru for experimental APRS use with LoRa on Raspberry Pi. The rfm9x driver is a C port of Adafruit's MIT-licensed CircuitPython RFM9x library (© 2017 Tony DiCola / Jerry Needell, Adafruit Industries).

License

MIT License — see LICENSE. Use it, fork it, improve it!

About

The code for running an LoRa APRS iGate on a raspberry pi with the RF.Guru LoRa hat!

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors