A working recipe for a headless, bidirectional APRS fill-in iGate using:

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)
  • aprx as the iGate daemon
  • systemd to 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

ExampleNotes
Callsign + SSIDN0CALL-10-10 is conventional for iGates; -15 for fixed/internet stations also seen
APRS-IS passcode12345Generated from your callsign via the standard algorithm (search “APRS-IS passcode generator”)
Position40.000000, -100.000000Decimal degrees; will be converted to APRS DMM format below
Beacon commentLinux aprx Fill-in iGateAny string, ~36 chars recommended
BT MAC of your TNCAA:BB:CC:11:22:33Get via bluetoothctl scan on
TNC RFCOMM channel6Device-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

DeviceRFCOMM channelNotes
Mobilinkd TNC3 / TNC46Confirmed via SDP
Generic SPP BT modules (HC-05, BBT-1)1Default SPP
NinoTNC + BT bridgedepends on bridge configBrowse with sdptool
TinyTrak4 BTvaries by firmwareBrowse 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 5 ExecStartPre absorbs the boot-time race where bluetooth.service reports active before hci0 is actually ready to make outbound connections
  • Restart=always (not on-failure) — rfcomm connect exits 0 on clean disconnect, and we want it to come right back
  • rfcomm release 0 as 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:

ServerRegion
noam.aprs2.netNorth America
soam.aprs2.netSouth America
euro.aprs2.netEurope / Africa
asia.aprs2.netAsia
aunz.aprs2.netOceania
rotate.aprs2.netDNS 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:

  1. Have a separate station you control (handheld, mobile — using a different SSID from the iGate) beacon for a couple minutes within RF range
  2. From aprs.fi (logged in), use “Send APRS message” to send a message addressed to that other SSID
  3. 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

SymbolMeaning
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

FilePurpose
/etc/aprx.confaprx configuration
/etc/systemd/system/rfcomm-tnc.serviceholds Bluetooth RFCOMM link (BT setups only)
/etc/systemd/system/aprx.service.d/override.confmakes aprx depend on rfcomm + wait for device
/var/log/aprx/aprx.logdaemon events
/var/log/aprx/aprx-rf.logRF + IS packet log
/var/lib/bluetooth/<host>/<tnc>/infopersistent BT pairing/bonding

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top