{"id":40,"date":"2026-05-12T16:16:28","date_gmt":"2026-05-12T22:16:28","guid":{"rendered":"https:\/\/kg7okr.com\/?p=40"},"modified":"2026-05-12T16:20:49","modified_gmt":"2026-05-12T22:20:49","slug":"a-working-recipe-for-a-headless-bidirectional-aprs-fill-in-igate-using","status":"publish","type":"post","link":"https:\/\/kg7okr.com\/?p=40","title":{"rendered":"A working recipe for a headless, bidirectional APRS fill-in iGate using:"},"content":{"rendered":"\n<p>A working recipe for a headless, bidirectional APRS fill-in iGate using:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A modern systemd-based Linux distribution (Debian\/Ubuntu, Fedora, Arch, etc.)<\/li>\n\n\n\n<li>A <strong>Bluetooth KISS TNC<\/strong> as the hardware modem (this guide is written against the Mobilinkd TNC3 but applies to any BT-SPP KISS TNC \u2014 see <em>Device variations<\/em> below)<\/li>\n\n\n\n<li><code>aprx<\/code> as the iGate daemon<\/li>\n\n\n\n<li><code>systemd<\/code> to manage the RFCOMM link and aprx itself<\/li>\n<\/ul>\n\n\n\n<p>The configuration is bidirectional: RF\u2192APRS-IS for every received packet, APRS-IS\u2192RF for messages addressed to stations heard on RF within ~30 minutes. No RF\u2192RF digipeating.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What you need before starting<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><\/th><th>Example<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td>Callsign + SSID<\/td><td><code>N0CALL-10<\/code><\/td><td><code>-10<\/code> is conventional for iGates; <code>-15<\/code> for fixed\/internet stations also seen<\/td><\/tr><tr><td>APRS-IS passcode<\/td><td><code>12345<\/code><\/td><td>Generated from your callsign via the standard algorithm (search &#8220;APRS-IS passcode generator&#8221;)<\/td><\/tr><tr><td>Position<\/td><td><code>40.000000, -100.000000<\/code><\/td><td>Decimal degrees; will be converted to APRS DMM format below<\/td><\/tr><tr><td>Beacon comment<\/td><td><code>Linux aprx Fill-in iGate<\/code><\/td><td>Any string, ~36 chars recommended<\/td><\/tr><tr><td>BT MAC of your TNC<\/td><td><code>AA:BB:CC:11:22:33<\/code><\/td><td>Get via <code>bluetoothctl scan on<\/code><\/td><\/tr><tr><td>TNC RFCOMM channel<\/td><td><code>6<\/code><\/td><td><strong>Device-specific.<\/strong> See <em>Device variations<\/em> below<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Throughout this guide, <strong>substitute your own values<\/strong> wherever the examples appear.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">1. Install packages<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Debian \/ Ubuntu<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt-get install -y bluez bluez-tools aprx expect<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Fedora \/ RHEL<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf install -y bluez bluez-tools aprx expect<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Arch<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo pacman -S --needed bluez bluez-utils aprx expect<\/code><\/pre>\n\n\n\n<p>Then in all cases:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl enable --now bluetooth\nhciconfig -a hci0    # confirm: UP RUNNING<\/code><\/pre>\n\n\n\n<p>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 <code>dmesg | grep -i bluetooth<\/code> after plugging in.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. Pair the TNC over Bluetooth<\/h2>\n\n\n\n<p>Power the TNC on. Find its Bluetooth MAC by scanning:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo bluetoothctl\n&#91;bluetooth]# scan on\n# wait until your TNC's name appears, note its MAC\n&#91;bluetooth]# scan off\n&#91;bluetooth]# quit<\/code><\/pre>\n\n\n\n<p>Then pair it. The trick is that BlueZ drops devices from its discovery cache shortly after <code>scan off<\/code>, so a separate <code>pair<\/code> command often gets &#8220;Device not available&#8221;. This expect script keeps scan active until the device is in cache, then pairs in the same session.<\/p>\n\n\n\n<p>Save as <code>\/tmp\/pair.exp<\/code> (substitute <code>&lt;TNC_MAC&gt;<\/code> and your TNC&#8217;s advertised name like <code>Mobilinkd<\/code>, <code>TNC4<\/code>, etc., in the <code>expect \"...\"<\/code> line):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/expect -f\nlog_user 1\nset timeout 60\nspawn bluetoothctl\nexpect \"#\"\nsend \"power on\\r\"; expect \"#\"\nsend \"agent NoInputNoOutput\\r\"; expect \"#\"\nsend \"default-agent\\r\"; expect \"#\"\nsend \"scan on\\r\"\nexpect \"Mobilinkd\"\nsleep 2\nsend \"scan off\\r\"\nsleep 1\nsend \"pair &lt;TNC_MAC&gt;\\r\"\nexpect {\n  -re \"Pairing successful|already.*paired|AlreadyExists\" {\n    send \"trust &lt;TNC_MAC&gt;\\r\"; expect \"#\"\n  }\n  \"Failed\" { }\n  timeout { }\n}\nsend \"info &lt;TNC_MAC&gt;\\r\"; expect \"#\"\nsend \"quit\\r\"\nexpect eof<\/code><\/pre>\n\n\n\n<p>Run it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo chmod +x \/tmp\/pair.exp\nsudo \/tmp\/pair.exp<\/code><\/pre>\n\n\n\n<p>Verify success \u2014 <code>bluetoothctl info &lt;TNC_MAC&gt;<\/code> should show:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Paired: yes\nBonded: yes\nTrusted: yes<\/code><\/pre>\n\n\n\n<p>The bond is persistent (stored under <code>\/var\/lib\/bluetooth\/<\/code>) and survives reboots, so this is a one-time step per TNC.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">TNCs that require a PIN<\/h3>\n\n\n\n<p>Some older BT TNCs prompt for a numeric PIN (often <code>0000<\/code> or <code>1234<\/code>). Change the agent from <code>NoInputNoOutput<\/code> to <code>KeyboardOnly<\/code> in the expect script and add a clause:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>expect -re \"PIN code|Passkey\"\nsend \"0000\\r\"<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. Find the TNC&#8217;s RFCOMM channel<\/h2>\n\n\n\n<p>Standard SPP (Serial Port Profile) is on RFCOMM channel <strong>1<\/strong> by default, but many TNCs use a non-default channel. Confirm with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo sdptool browse &lt;TNC_MAC&gt; | grep -A2 -B1 \"Serial Port\"<\/code><\/pre>\n\n\n\n<p>Look for the <code>Channel:<\/code> line under the <code>\"Serial Port\" (0x1101)<\/code> entry. <strong>Use this number<\/strong> in the rfcomm unit below.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Device variations<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Device<\/th><th>RFCOMM channel<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td><strong>Mobilinkd TNC3 \/ TNC4<\/strong><\/td><td>6<\/td><td>Confirmed via SDP<\/td><\/tr><tr><td><strong>Generic SPP BT modules (HC-05, BBT-1)<\/strong><\/td><td>1<\/td><td>Default SPP<\/td><\/tr><tr><td><strong>NinoTNC + BT bridge<\/strong><\/td><td>depends on bridge config<\/td><td>Browse with sdptool<\/td><\/tr><tr><td><strong>TinyTrak4 BT<\/strong><\/td><td>varies by firmware<\/td><td>Browse with sdptool<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4. systemd unit to hold the RFCOMM link<\/h2>\n\n\n\n<p>Substitute <code>&lt;TNC_MAC&gt;<\/code> and the channel number (<code>6<\/code> here is the Mobilinkd value \u2014 use what SDP showed you).<\/p>\n\n\n\n<p>Create <code>\/etc\/systemd\/system\/rfcomm-tnc.service<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Unit]\nDescription=Hold Bluetooth RFCOMM link to KISS TNC\nAfter=bluetooth.service\nRequires=bluetooth.service\n\n&#91;Service]\nType=simple\nExecStartPre=-\/usr\/bin\/rfcomm release 0\nExecStartPre=\/bin\/sleep 5\nExecStart=\/usr\/bin\/rfcomm connect 0 &lt;TNC_MAC&gt; 6\nRestart=always\nRestartSec=5s\nStartLimitIntervalSec=300\nStartLimitBurst=30\nKillMode=process\n\n&#91;Install]\nWantedBy=multi-user.target<\/code><\/pre>\n\n\n\n<p>Key points:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The <code>sleep 5<\/code> ExecStartPre absorbs the boot-time race where <code>bluetooth.service<\/code> reports active before <code>hci0<\/code> is actually ready to make outbound connections<\/li>\n\n\n\n<li><code>Restart=always<\/code> (not <code>on-failure<\/code>) \u2014 <code>rfcomm connect<\/code> exits 0 on clean disconnect, and we want it to come right back<\/li>\n\n\n\n<li><code>rfcomm release 0<\/code> as a no-fail pre-step ensures a stale binding doesn&#8217;t block a fresh open<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">USB-serial TNCs instead<\/h3>\n\n\n\n<p>If your TNC is wired USB or RS-232 (not Bluetooth), <strong>skip this entire section<\/strong> and skip the BT pairing too. In aprx.conf you&#8217;ll point <code>serial-device<\/code> directly at <code>\/dev\/ttyUSB0<\/code> (or whatever your device enumerates as). Consider adding a <code>udev<\/code> rule so the same physical TNC always gets the same <code>\/dev\/serial\/by-id\/...<\/code> name \u2014 it makes the config stable across reboots and re-plugs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">KISS-over-TCP instead<\/h3>\n\n\n\n<p>If your &#8220;TNC&#8221; 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:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;interface&gt;\n   tcp-device   192.0.2.10  8001  KISS\n   callsign     $mycall\n   tx-ok        true\n&lt;\/interface&gt;<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">5. systemd override for aprx<\/h2>\n\n\n\n<p>Make aprx depend on the RFCOMM link and wait for <code>\/dev\/rfcomm0<\/code> to appear before starting:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo mkdir -p \/etc\/systemd\/system\/aprx.service.d<\/code><\/pre>\n\n\n\n<p>Create <code>\/etc\/systemd\/system\/aprx.service.d\/override.conf<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Unit]\nAfter=rfcomm-tnc.service network-online.target\nRequires=rfcomm-tnc.service\nExecStartPre=\/bin\/sh -c 'for i in $(seq 1 20); do &#91; -e \/dev\/rfcomm0 ] &amp;&amp; exit 0; sleep 1; done; exit 1'<\/code><\/pre>\n\n\n\n<p>The pre-start loop waits up to 20 seconds for <code>\/dev\/rfcomm0<\/code> before starting aprx, avoiding errors if the BT link is still establishing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Non-Bluetooth setups<\/h3>\n\n\n\n<p>If you&#8217;re using a wired serial TNC, the device file already exists at boot \u2014 drop the <code>Requires=<\/code>\/<code>After=rfcomm-tnc.service<\/code> lines and the <code>ExecStartPre<\/code> loop. The standard aprx.service shipped with the package is enough.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">6. aprx configuration<\/h2>\n\n\n\n<p>The lat\/lon must be in APRS <strong>DMM<\/strong> format (<code>DDMM.mmN<\/code> \/ <code>DDDMM.mmW<\/code>). To convert from decimal degrees:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>40.500000  \u2192   40\u00b0 + 0.500000 \u00d7 60 =  4030.00 N\n100.250000 \u2192  100\u00b0 + 0.250000 \u00d7 60 = 10015.00 W<\/code><\/pre>\n\n\n\n<p>Latitude positive = North, negative = South. Longitude positive = East, negative = West.<\/p>\n\n\n\n<p>Replace <code>\/etc\/aprx.conf<\/code> with the following \u2014 substitute your values:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># \/etc\/aprx.conf -- APRX Fill-in iGate\n\nmycall          N0CALL-10\nmyloc lat 4030.00N lon 10015.00W\n\n&lt;aprsis&gt;\n   passcode     12345\n   server       noam.aprs2.net     # see \"Regional servers\" below\n&lt;\/aprsis&gt;\n\n&lt;logging&gt;\n   pidfile      \/var\/run\/aprx.pid\n   rflog        \/var\/log\/aprx\/aprx-rf.log\n   aprxlog      \/var\/log\/aprx\/aprx.log\n&lt;\/logging&gt;\n\n# Bluetooth KISS TNC on \/dev\/rfcomm0\n# For a wired serial TNC, replace with e.g.\n#   serial-device \/dev\/ttyUSB0 9600 8n1 KISS\n# For a network KISS TNC, replace with e.g.\n#   tcp-device 192.0.2.10 8001 KISS\n&lt;interface&gt;\n   serial-device \/dev\/rfcomm0  9600  8n1  KISS\n   callsign      $mycall\n   tx-ok         true\n   telem-to-is   false\n&lt;\/interface&gt;\n\n# Position beacon: \"I&amp;\" = Tx-iGate symbol on the primary table.\n# Use \"R&amp;\" for an RX-only iGate, \"I#\" for Tx-iGate + Digipeater.\n# 10-minute cycle satisfies FCC \u00a797.119 (US) station-ID-every-10-minutes.\n&lt;beacon&gt;\n   beaconmode   both\n   cycle-size   10m\n\n   beacon  symbol \"I&amp;\" $myloc \\\n           comment \"Linux aprx Fill-in iGate\"\n&lt;\/beacon&gt;\n\n# Tx-iGate only -- no RF-&gt;RF digipeating.\n# Heard-list is fed automatically by interface RX; no local-RF &lt;source&gt;\n# is needed (and adding one would enable digipeating by default).\n&lt;digipeater&gt;\n   transmitter  $mycall\n   ratelimit    60 120\n\n   &lt;source&gt;\n      source        APRSIS\n      relay-type    third-party\n      viscous-delay 5\n      ratelimit     60 120\n   &lt;\/source&gt;\n&lt;\/digipeater&gt;<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Regional APRS-IS servers<\/h3>\n\n\n\n<p>Pick the closest to you:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Server<\/th><th>Region<\/th><\/tr><\/thead><tbody><tr><td><code>noam.aprs2.net<\/code><\/td><td>North America<\/td><\/tr><tr><td><code>soam.aprs2.net<\/code><\/td><td>South America<\/td><\/tr><tr><td><code>euro.aprs2.net<\/code><\/td><td>Europe \/ Africa<\/td><\/tr><tr><td><code>asia.aprs2.net<\/code><\/td><td>Asia<\/td><\/tr><tr><td><code>aunz.aprs2.net<\/code><\/td><td>Oceania<\/td><\/tr><tr><td><code>rotate.aprs2.net<\/code><\/td><td>DNS round-robin global default<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Why no local-RF <code>&lt;source&gt;<\/code> block<\/h3>\n\n\n\n<p>This is the easiest thing to get wrong. aprx&#8217;s <code>&lt;digipeater&gt;<\/code> block contains <code>&lt;source&gt;<\/code> subblocks that default to <code>relay-type digipeated<\/code>. If you add a <code>&lt;source&gt; source $mycall &lt;\/source&gt;<\/code> block (as the shipped sample config shows), you are configuring aprx as an <strong>active RF\u2192RF digipeater<\/strong> \u2014 it will retransmit every packet it hears with your callsign inserted in the path.<\/p>\n\n\n\n<p>For a pure iGate you do NOT need a local-RF source. The Tx-iGate heard-list is populated automatically by <code>&lt;interface&gt;<\/code> RX, independent of the digipeater block. The only <code>&lt;source&gt;<\/code> you need is <code>APRSIS<\/code> for IS\u2192RF gating.<\/p>\n\n\n\n<p>If you DO want to also be a digipeater (most fill-in iGates do not), see the aprx sample config under <code>\/etc\/aprx.conf.dist<\/code> (or wherever your distribution put the example) for the correct settings \u2014 typically you want <code>viscous-delay<\/code> set, careful filters, and a clear understanding of what your area needs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why <code>telem-to-is false<\/code><\/h3>\n\n\n\n<p>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&#8217;t useful. Set <code>telem-to-is false<\/code> on the <code>&lt;interface&gt;<\/code> to disable.<\/p>\n\n\n\n<p>If you leave this enabled and later want it off, note that <strong>aprs.fi caches the telemetry channel definitions<\/strong> \u2014 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&#8217;s About page) or inject blank PARM\/UNIT\/EQNS frames manually.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why logging is wrapped in <code>&lt;logging&gt;<\/code><\/h3>\n\n\n\n<p>In aprx 2.9.x, <code>rflog<\/code>, <code>aprxlog<\/code>, <code>pidfile<\/code>, and <code>erlangfile<\/code> must be inside a <code>&lt;logging&gt;...&lt;\/logging&gt;<\/code> block. Placing them at the top level produces &#8220;Unknown config keyword&#8221; errors.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">7. Create log\/state dirs<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo install -d \/var\/log\/aprx \/var\/run\/aprx\nsudo chmod 755 \/var\/log\/aprx \/var\/run\/aprx<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">8. Enable services for boot<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl enable --now rfcomm-tnc.service\nsudo systemctl enable --now aprx.service<\/code><\/pre>\n\n\n\n<p>Note: the <code>aprx<\/code> Debian\/Ubuntu package installs the service <strong>disabled<\/strong> by default \u2014 you must <code>enable<\/code> it explicitly.<\/p>\n\n\n\n<p>Verify:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl is-enabled bluetooth rfcomm-tnc.service aprx.service\nsystemctl is-active  bluetooth rfcomm-tnc.service aprx.service\n# All should report enabled \/ active<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">9. Verify operation<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code># Bluetooth link to the TNC\nhcitool con\n# Expect: &lt; ACL &lt;TNC_MAC&gt; handle 1 state 1 lm CENTRAL AUTH ENCRYPT\n\n# RFCOMM tty exists\nls -la \/dev\/rfcomm0\n\n# APRS-IS TCP is established\nsudo ss -tnp state established | grep :14580\n\n# aprx event log\nsudo tail \/var\/log\/aprx\/aprx.log\n# Expect: \"TTY \/dev\/rfcomm0 opened\" and \"CONNECT APRSIS ...\"\n\n# Live RF traffic\nsudo tail -f \/var\/log\/aprx\/aprx-rf.log<\/code><\/pre>\n\n\n\n<p>In the RF log:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>&lt;MYCALL&gt; R ...<\/code> = received from RF (will be gated to APRS-IS automatically after dedup)<\/li>\n\n\n\n<li><code>&lt;MYCALL&gt; T ...<\/code> = transmitted on RF (own beacon, or an IS\u2192RF gate)<\/li>\n\n\n\n<li><code>APRSIS    R ...<\/code> = received from APRS-IS (informational; not gated to RF unless it matches Tx-iGate criteria)<\/li>\n<\/ul>\n\n\n\n<p>For an external sanity check, visit <code>https:\/\/aprs.fi\/info\/a\/&lt;YOURCALL&gt;<\/code> \u2014 your station should appear within a minute or two and <code>Device:<\/code> will be detected as <code>Aprx (igate, Linux\/Unix)<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">10. Testing IS\u2192RF gating<\/h2>\n\n\n\n<p>The IS\u2192RF leg only fires for APRS <code>:<\/code>-type <strong>messages<\/strong> addressed to a station you&#8217;ve heard on RF within ~30 minutes. It will not gate position beacons, weather, telemetry, or status \u2014 that&#8217;s correct iGate behavior (otherwise IS would flood the channel).<\/p>\n\n\n\n<p>To test:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Have a separate station you control (handheld, mobile \u2014 using a different SSID from the iGate) beacon for a couple minutes within RF range<\/li>\n\n\n\n<li>From aprs.fi (logged in), use &#8220;Send APRS message&#8221; to send a message addressed to that other SSID<\/li>\n\n\n\n<li>Watch the iGate&#8217;s RF log:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo tail -F \/var\/log\/aprx\/aprx-rf.log | grep ' T '<\/code><\/pre>\n\n\n\n<p>You should see a <code>T<\/code> 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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Operations notes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Reboot<\/h3>\n\n\n\n<p>Everything comes back automatically. The Bluetooth bond is persistent under <code>\/var\/lib\/bluetooth\/<\/code>, the systemd services are enabled, and the rfcomm unit&#8217;s <code>Restart=always<\/code> handles the boot-time race where the TNC may not be reachable on the first attempt.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Logs<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/var\/log\/aprx\/aprx.log     # connection events, beacons, errors\n\/var\/log\/aprx\/aprx-rf.log  # all RF traffic<\/code><\/pre>\n\n\n\n<p>Rotated by the logrotate config shipped with the aprx package on most distributions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Reconfigure<\/h3>\n\n\n\n<p>Edit <code>\/etc\/aprx.conf<\/code> and:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl restart aprx.service<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Validate the config without starting<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo timeout 4 aprx -d -f \/etc\/aprx.conf<\/code><\/pre>\n\n\n\n<p>Debug output runs for a few seconds then exits. Look for <code>ERROR:<\/code> lines.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Beacon symbols<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Symbol<\/th><th>Meaning<\/th><\/tr><\/thead><tbody><tr><td><code>I&amp;<\/code><\/td><td>Tx iGate (overlaid <code>I<\/code> on Gateway symbol)<\/td><\/tr><tr><td><code>R&amp;<\/code><\/td><td>RX-only iGate<\/td><\/tr><tr><td><code>I#<\/code><\/td><td>Tx iGate + Digipeater<\/td><\/tr><tr><td><code>\/#<\/code><\/td><td>Digipeater (plain)<\/td><\/tr><tr><td><code>T&amp;<\/code><\/td><td>Tx-only beacon source<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Substitute in the <code>&lt;beacon&gt;<\/code> block to match what your station actually does.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">File summary<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>File<\/th><th>Purpose<\/th><\/tr><\/thead><tbody><tr><td><code>\/etc\/aprx.conf<\/code><\/td><td>aprx configuration<\/td><\/tr><tr><td><code>\/etc\/systemd\/system\/rfcomm-tnc.service<\/code><\/td><td>holds Bluetooth RFCOMM link (BT setups only)<\/td><\/tr><tr><td><code>\/etc\/systemd\/system\/aprx.service.d\/override.conf<\/code><\/td><td>makes aprx depend on rfcomm + wait for device<\/td><\/tr><tr><td><code>\/var\/log\/aprx\/aprx.log<\/code><\/td><td>daemon events<\/td><\/tr><tr><td><code>\/var\/log\/aprx\/aprx-rf.log<\/code><\/td><td>RF + IS packet log<\/td><\/tr><tr><td><code>\/var\/lib\/bluetooth\/&lt;host&gt;\/&lt;tnc&gt;\/info<\/code><\/td><td>persistent BT pairing\/bonding<\/td><\/tr><\/tbody><\/table><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>A working recipe for a headless, bidirectional APRS fill-in iGate using: The configuration is bidirectional: RF\u2192APRS-IS for every received packet, APRS-IS\u2192RF for messages addressed to stations heard on RF within ~30 minutes. No RF\u2192RF digipeating. What you need before starting Example Notes Callsign + SSID N0CALL-10 -10 is conventional for iGates; -15 for fixed\/internet stations [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"set","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[1],"tags":[],"class_list":["post-40","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/posts\/40","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kg7okr.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=40"}],"version-history":[{"count":4,"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/posts\/40\/revisions"}],"predecessor-version":[{"id":45,"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/posts\/40\/revisions\/45"}],"wp:attachment":[{"href":"https:\/\/kg7okr.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=40"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kg7okr.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=40"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kg7okr.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=40"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}