A working recipe for a headless, bidirectional APRS fill-in iGate using:
- A modern systemd-based Linux distribution (Debian/Ubuntu, Fedora, Arch, etc.)
- A Bluetooth KISS TNC as the hardware modem (this guide is written against the Mobilinkd TNC3 but applies to any BT-SPP KISS TNC — see Device variations below)
aprxas the iGate daemonsystemdto manage the RFCOMM link and aprx itself
The configuration is bidirectional: RF→APRS-IS for every received packet, APRS-IS→RF for messages addressed to stations heard on RF within ~30 minutes. No RF→RF digipeating.
What you need before starting
| Example | Notes | |
|---|---|---|
| Callsign + SSID | N0CALL-10 | -10 is conventional for iGates; -15 for fixed/internet stations also seen |
| APRS-IS passcode | 12345 | Generated from your callsign via the standard algorithm (search “APRS-IS passcode generator”) |
| Position | 40.000000, -100.000000 | Decimal degrees; will be converted to APRS DMM format below |
| Beacon comment | Linux aprx Fill-in iGate | Any string, ~36 chars recommended |
| BT MAC of your TNC | AA:BB:CC:11:22:33 | Get via bluetoothctl scan on |
| TNC RFCOMM channel | 6 | Device-specific. See Device variations below |
Throughout this guide, substitute your own values wherever the examples appear.
1. Install packages
Debian / Ubuntu
sudo apt-get install -y bluez bluez-tools aprx expect
Fedora / RHEL
sudo dnf install -y bluez bluez-tools aprx expect
Arch
sudo pacman -S --needed bluez bluez-utils aprx expect
Then in all cases:
sudo systemctl enable --now bluetooth
hciconfig -a hci0 # confirm: UP RUNNING
If your machine has no built-in Bluetooth, plug in a USB BT 4.0+ dongle. Most Realtek/CSR/Intel/Broadcom adapters work out of the box on Linux. Confirm with dmesg | grep -i bluetooth after plugging in.
2. Pair the TNC over Bluetooth
Power the TNC on. Find its Bluetooth MAC by scanning:
sudo bluetoothctl
[bluetooth]# scan on
# wait until your TNC's name appears, note its MAC
[bluetooth]# scan off
[bluetooth]# quit
Then pair it. The trick is that BlueZ drops devices from its discovery cache shortly after scan off, so a separate pair command often gets “Device not available”. This expect script keeps scan active until the device is in cache, then pairs in the same session.
Save as /tmp/pair.exp (substitute <TNC_MAC> and your TNC’s advertised name like Mobilinkd, TNC4, etc., in the expect "..." line):
#!/usr/bin/expect -f
log_user 1
set timeout 60
spawn bluetoothctl
expect "#"
send "power on\r"; expect "#"
send "agent NoInputNoOutput\r"; expect "#"
send "default-agent\r"; expect "#"
send "scan on\r"
expect "Mobilinkd"
sleep 2
send "scan off\r"
sleep 1
send "pair <TNC_MAC>\r"
expect {
-re "Pairing successful|already.*paired|AlreadyExists" {
send "trust <TNC_MAC>\r"; expect "#"
}
"Failed" { }
timeout { }
}
send "info <TNC_MAC>\r"; expect "#"
send "quit\r"
expect eof
Run it:
sudo chmod +x /tmp/pair.exp
sudo /tmp/pair.exp
Verify success — bluetoothctl info <TNC_MAC> should show:
Paired: yes
Bonded: yes
Trusted: yes
The bond is persistent (stored under /var/lib/bluetooth/) and survives reboots, so this is a one-time step per TNC.
TNCs that require a PIN
Some older BT TNCs prompt for a numeric PIN (often 0000 or 1234). Change the agent from NoInputNoOutput to KeyboardOnly in the expect script and add a clause:
expect -re "PIN code|Passkey"
send "0000\r"
3. Find the TNC’s RFCOMM channel
Standard SPP (Serial Port Profile) is on RFCOMM channel 1 by default, but many TNCs use a non-default channel. Confirm with:
sudo sdptool browse <TNC_MAC> | grep -A2 -B1 "Serial Port"
Look for the Channel: line under the "Serial Port" (0x1101) entry. Use this number in the rfcomm unit below.
Device variations
| Device | RFCOMM channel | Notes |
|---|---|---|
| Mobilinkd TNC3 / TNC4 | 6 | Confirmed via SDP |
| Generic SPP BT modules (HC-05, BBT-1) | 1 | Default SPP |
| NinoTNC + BT bridge | depends on bridge config | Browse with sdptool |
| TinyTrak4 BT | varies by firmware | Browse with sdptool |
4. systemd unit to hold the RFCOMM link
Substitute <TNC_MAC> and the channel number (6 here is the Mobilinkd value — use what SDP showed you).
Create /etc/systemd/system/rfcomm-tnc.service:
[Unit]
Description=Hold Bluetooth RFCOMM link to KISS TNC
After=bluetooth.service
Requires=bluetooth.service
[Service]
Type=simple
ExecStartPre=-/usr/bin/rfcomm release 0
ExecStartPre=/bin/sleep 5
ExecStart=/usr/bin/rfcomm connect 0 <TNC_MAC> 6
Restart=always
RestartSec=5s
StartLimitIntervalSec=300
StartLimitBurst=30
KillMode=process
[Install]
WantedBy=multi-user.target
Key points:
- The
sleep 5ExecStartPre absorbs the boot-time race wherebluetooth.servicereports active beforehci0is actually ready to make outbound connections Restart=always(noton-failure) —rfcomm connectexits 0 on clean disconnect, and we want it to come right backrfcomm release 0as a no-fail pre-step ensures a stale binding doesn’t block a fresh open
USB-serial TNCs instead
If your TNC is wired USB or RS-232 (not Bluetooth), skip this entire section and skip the BT pairing too. In aprx.conf you’ll point serial-device directly at /dev/ttyUSB0 (or whatever your device enumerates as). Consider adding a udev rule so the same physical TNC always gets the same /dev/serial/by-id/... name — it makes the config stable across reboots and re-plugs.
KISS-over-TCP instead
If your “TNC” is actually another machine running Direwolf or similar exposing KISS over TCP, skip the BT pairing AND the systemd rfcomm unit. In aprx.conf use:
<interface>
tcp-device 192.0.2.10 8001 KISS
callsign $mycall
tx-ok true
</interface>
5. systemd override for aprx
Make aprx depend on the RFCOMM link and wait for /dev/rfcomm0 to appear before starting:
sudo mkdir -p /etc/systemd/system/aprx.service.d
Create /etc/systemd/system/aprx.service.d/override.conf:
[Unit]
After=rfcomm-tnc.service network-online.target
Requires=rfcomm-tnc.service
ExecStartPre=/bin/sh -c 'for i in $(seq 1 20); do [ -e /dev/rfcomm0 ] && exit 0; sleep 1; done; exit 1'
The pre-start loop waits up to 20 seconds for /dev/rfcomm0 before starting aprx, avoiding errors if the BT link is still establishing.
Non-Bluetooth setups
If you’re using a wired serial TNC, the device file already exists at boot — drop the Requires=/After=rfcomm-tnc.service lines and the ExecStartPre loop. The standard aprx.service shipped with the package is enough.
6. aprx configuration
The lat/lon must be in APRS DMM format (DDMM.mmN / DDDMM.mmW). To convert from decimal degrees:
40.500000 → 40° + 0.500000 × 60 = 4030.00 N
100.250000 → 100° + 0.250000 × 60 = 10015.00 W
Latitude positive = North, negative = South. Longitude positive = East, negative = West.
Replace /etc/aprx.conf with the following — substitute your values:
# /etc/aprx.conf -- APRX Fill-in iGate
mycall N0CALL-10
myloc lat 4030.00N lon 10015.00W
<aprsis>
passcode 12345
server noam.aprs2.net # see "Regional servers" below
</aprsis>
<logging>
pidfile /var/run/aprx.pid
rflog /var/log/aprx/aprx-rf.log
aprxlog /var/log/aprx/aprx.log
</logging>
# Bluetooth KISS TNC on /dev/rfcomm0
# For a wired serial TNC, replace with e.g.
# serial-device /dev/ttyUSB0 9600 8n1 KISS
# For a network KISS TNC, replace with e.g.
# tcp-device 192.0.2.10 8001 KISS
<interface>
serial-device /dev/rfcomm0 9600 8n1 KISS
callsign $mycall
tx-ok true
telem-to-is false
</interface>
# Position beacon: "I&" = Tx-iGate symbol on the primary table.
# Use "R&" for an RX-only iGate, "I#" for Tx-iGate + Digipeater.
# 10-minute cycle satisfies FCC §97.119 (US) station-ID-every-10-minutes.
<beacon>
beaconmode both
cycle-size 10m
beacon symbol "I&" $myloc \
comment "Linux aprx Fill-in iGate"
</beacon>
# Tx-iGate only -- no RF->RF digipeating.
# Heard-list is fed automatically by interface RX; no local-RF <source>
# is needed (and adding one would enable digipeating by default).
<digipeater>
transmitter $mycall
ratelimit 60 120
<source>
source APRSIS
relay-type third-party
viscous-delay 5
ratelimit 60 120
</source>
</digipeater>
Regional APRS-IS servers
Pick the closest to you:
| Server | Region |
|---|---|
noam.aprs2.net | North America |
soam.aprs2.net | South America |
euro.aprs2.net | Europe / Africa |
asia.aprs2.net | Asia |
aunz.aprs2.net | Oceania |
rotate.aprs2.net | DNS round-robin global default |
Why no local-RF <source> block
This is the easiest thing to get wrong. aprx’s <digipeater> block contains <source> subblocks that default to relay-type digipeated. If you add a <source> source $mycall </source> block (as the shipped sample config shows), you are configuring aprx as an active RF→RF digipeater — it will retransmit every packet it hears with your callsign inserted in the path.
For a pure iGate you do NOT need a local-RF source. The Tx-iGate heard-list is populated automatically by <interface> RX, independent of the digipeater block. The only <source> you need is APRSIS for IS→RF gating.
If you DO want to also be a digipeater (most fill-in iGates do not), see the aprx sample config under /etc/aprx.conf.dist (or wherever your distribution put the example) for the correct settings — typically you want viscous-delay set, careful filters, and a clear understanding of what your area needs.
Why telem-to-is false
aprx by default publishes per-interface utilization stats (channel busy %, RX/TX rates, dropped frames) to APRS-IS as APRS telemetry packets. For a personal iGate this clutters your aprs.fi station page with channel labels and graphs that mostly aren’t useful. Set telem-to-is false on the <interface> to disable.
If you leave this enabled and later want it off, note that aprs.fi caches the telemetry channel definitions — the cached labels in the map-popup may stick around for days/weeks after you stop publishing. To clear faster, either email the aprs.fi operator (contact on the site’s About page) or inject blank PARM/UNIT/EQNS frames manually.
Why logging is wrapped in <logging>
In aprx 2.9.x, rflog, aprxlog, pidfile, and erlangfile must be inside a <logging>...</logging> block. Placing them at the top level produces “Unknown config keyword” errors.
7. Create log/state dirs
sudo install -d /var/log/aprx /var/run/aprx
sudo chmod 755 /var/log/aprx /var/run/aprx
8. Enable services for boot
sudo systemctl daemon-reload
sudo systemctl enable --now rfcomm-tnc.service
sudo systemctl enable --now aprx.service
Note: the aprx Debian/Ubuntu package installs the service disabled by default — you must enable it explicitly.
Verify:
systemctl is-enabled bluetooth rfcomm-tnc.service aprx.service
systemctl is-active bluetooth rfcomm-tnc.service aprx.service
# All should report enabled / active
9. Verify operation
# Bluetooth link to the TNC
hcitool con
# Expect: < ACL <TNC_MAC> handle 1 state 1 lm CENTRAL AUTH ENCRYPT
# RFCOMM tty exists
ls -la /dev/rfcomm0
# APRS-IS TCP is established
sudo ss -tnp state established | grep :14580
# aprx event log
sudo tail /var/log/aprx/aprx.log
# Expect: "TTY /dev/rfcomm0 opened" and "CONNECT APRSIS ..."
# Live RF traffic
sudo tail -f /var/log/aprx/aprx-rf.log
In the RF log:
<MYCALL> R ...= received from RF (will be gated to APRS-IS automatically after dedup)<MYCALL> T ...= transmitted on RF (own beacon, or an IS→RF gate)APRSIS R ...= received from APRS-IS (informational; not gated to RF unless it matches Tx-iGate criteria)
For an external sanity check, visit https://aprs.fi/info/a/<YOURCALL> — your station should appear within a minute or two and Device: will be detected as Aprx (igate, Linux/Unix).
10. Testing IS→RF gating
The IS→RF leg only fires for APRS :-type messages addressed to a station you’ve heard on RF within ~30 minutes. It will not gate position beacons, weather, telemetry, or status — that’s correct iGate behavior (otherwise IS would flood the channel).
To test:
- Have a separate station you control (handheld, mobile — using a different SSID from the iGate) beacon for a couple minutes within RF range
- From aprs.fi (logged in), use “Send APRS message” to send a message addressed to that other SSID
- Watch the iGate’s RF log:
sudo tail -F /var/log/aprx/aprx-rf.log | grep ' T '
You should see a T line with a third-party-wrapped payload within a few seconds. The handheld should receive it on RF and (if configured for messaging) auto-ACK.
Operations notes
Reboot
Everything comes back automatically. The Bluetooth bond is persistent under /var/lib/bluetooth/, the systemd services are enabled, and the rfcomm unit’s Restart=always handles the boot-time race where the TNC may not be reachable on the first attempt.
Logs
/var/log/aprx/aprx.log # connection events, beacons, errors
/var/log/aprx/aprx-rf.log # all RF traffic
Rotated by the logrotate config shipped with the aprx package on most distributions.
Reconfigure
Edit /etc/aprx.conf and:
sudo systemctl restart aprx.service
Validate the config without starting
sudo timeout 4 aprx -d -f /etc/aprx.conf
Debug output runs for a few seconds then exits. Look for ERROR: lines.
Beacon symbols
| Symbol | Meaning |
|---|---|
I& | Tx iGate (overlaid I on Gateway symbol) |
R& | RX-only iGate |
I# | Tx iGate + Digipeater |
/# | Digipeater (plain) |
T& | Tx-only beacon source |
Substitute in the <beacon> block to match what your station actually does.
File summary
| File | Purpose |
|---|---|
/etc/aprx.conf | aprx configuration |
/etc/systemd/system/rfcomm-tnc.service | holds Bluetooth RFCOMM link (BT setups only) |
/etc/systemd/system/aprx.service.d/override.conf | makes aprx depend on rfcomm + wait for device |
/var/log/aprx/aprx.log | daemon events |
/var/log/aprx/aprx-rf.log | RF + IS packet log |
/var/lib/bluetooth/<host>/<tnc>/info | persistent BT pairing/bonding |