{"id":1086,"date":"2026-11-04T09:46:56","date_gmt":"2026-11-04T14:46:56","guid":{"rendered":"https:\/\/www.we8chz.org\/?p=1086"},"modified":"2026-11-04T10:04:24","modified_gmt":"2026-11-04T15:04:24","slug":"how-to-all-linux-winlink-and-aprs-gateway-with-dire-wolf-xara-and-linbpq","status":"publish","type":"post","link":"https:\/\/www.we8chz.org\/?p=1086","title":{"rendered":"How To All-Linux Winlink and APRS Gateway With Dire Wolf, VARA (XARA), and LinBPQ"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">This article details how to build a Winlink RMS gateway and APRS digipeater\/iGate on Linux with native software only. Dire Wolf handles 1200 baud and 9600 baud packet, XARA handles VARA FM, and LinBPQ connects all three modes to the Winlink system. The setup uses no Wine.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The finished station answers Winlink connections on a single frequency in three modes at the same time: 1200 baud packet, 9600 baud packet, and VARA FM. One radio, one USB sound interface, and one PTT line serve all three modems. The same LinBPQ node also runs an APRS digipeater and iGate, and the entire station starts on its own at boot.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>XARA is open source.<\/strong> VARA is a commercial program, and XARA is an independent, MIT-licensed, VARA-compatible modem for Linux, macOS, and FreeBSD. The project reports testing against VARA FM 4.4.0 and VARA HF 4.9.0. Holders of a registered VARA license enable the higher registered speeds by building XARA with the registered feature and starting it with the registered option. Both steps appear in the XARA section below.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">VARA FM is a VHF\/UHF protocol, so this build targets 2 meters or 70 cm. The last sections cover changing the mode XARA uses for HF and other frequencies. The reference interface keys the radio through a CM108 GPIO pin, and a dedicated section covers interfaces that key through a serial port (such as the DigiRig) and interfaces that key by VOX (such as the Tigertronics SignaLink USB).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The article begins after the operating system is installed. A bare-metal installation and a virtual machine behave the same once the USB sound interface is visible to the operating system. The reference build is Ubuntu 26.04 LTS Server in a Proxmox virtual machine.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Definitions<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Winlink: the worldwide radio email system.<\/li>\n\n\n\n<li>RMS gateway: a radio station that connects Winlink clients on the air to the Winlink system over the internet.<\/li>\n\n\n\n<li>CMS: the Winlink Common Message Server. The RMS gateway relays each radio session to a CMS.<\/li>\n\n\n\n<li>LinBPQ: the Linux version of the BPQ32 packet switch by John Wiseman, G8BPQ. In this build it is the RMS gateway, the packet node, and the APRS digipeater\/iGate.<\/li>\n\n\n\n<li>Dire Wolf: a software TNC (sound card modem) for AX.25 packet radio. It decodes and generates 1200 baud AFSK and 9600 baud G3RUH signals.<\/li>\n\n\n\n<li>XARA: a VARA-compatible modem for Linux. It provides the VARA FM mode.<\/li>\n\n\n\n<li>VARA FM: the FM-radio mode of the VARA protocol, available as narrow and wide.<\/li>\n\n\n\n<li>KISS: the simple framing protocol that packet software uses to talk to a TNC. Dire Wolf offers it over TCP.<\/li>\n\n\n\n<li>PTT: push-to-talk, the line that keys the transmitter.<\/li>\n\n\n\n<li>RTS and DTR: control lines of a serial port. Many interfaces key the radio by switching one of them.<\/li>\n\n\n\n<li>VOX: voice-operated transmit. The interface keys the radio when audio arrives from the computer, and no PTT line is involved.<\/li>\n\n\n\n<li>CM108: a USB audio chip that includes general-purpose I\/O pins. Many USB radio interfaces use it and key the radio by switching one of those pins (GPIO 3 on the interface used here).<\/li>\n\n\n\n<li>ALSA: the Linux sound system. Its dsnoop plugin lets several programs share one capture stream, and its dmix plugin mixes several playback streams into one.<\/li>\n\n\n\n<li>APRS: the Automatic Packet Reporting System.<\/li>\n\n\n\n<li>iGate: an APRS station that passes packets between the radio channel and the APRS-IS internet network.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Expectations<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The station is built from four cooperating parts. The radio connects to a CM108 USB interface. ALSA shares the interface audio between Dire Wolf and XARA. A small program called the PTT arbiter combines the PTT requests of all three modems into the single GPIO line. LinBPQ sits on top and talks to the modems over local TCP ports.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code> Radio (data port, 145.050 MHz)\n        |\n CM108 USB interface\n     |  audio                     |  PTT (GPIO 3)\n     |                            |\n ALSA dsnoop (one capture)     PTT arbiter\n ALSA dmix   (mixed playback)   ^     ^     ^\n     |                          |     |     |\n     +--&gt; Dire Wolf ch 0: 1200 baud -+     |\n     +--&gt; Dire Wolf ch 1: 9600 baud -+     |     (KISS, TCP 8001)\n     +--&gt; XARA: VARA FM -------------------+     (VARA, TCP 8300\/8301)\n                     |\n                  LinBPQ ----&gt; Winlink CMS (internet)\n                          ----&gt; APRS-IS    (internet)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The reference station was built and tested with the following versions.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Item<\/th><th>Version<\/th><\/tr><\/thead><tbody><tr><td>Operating system<\/td><td>Ubuntu 26.04.1 LTS Server (kernel 7.0)<\/td><\/tr><tr><td>Dire Wolf<\/td><td>1.8.1 (Ubuntu package)<\/td><\/tr><tr><td>XARA<\/td><td>0.1.3 (commit 09a9201)<\/td><\/tr><tr><td>LinBPQ<\/td><td>6.0.26.1 (linbpq64)<\/td><\/tr><tr><td>Radio<\/td><td>Yaesu FT-7800, 9600 baud data port<\/td><\/tr><tr><td>Interface<\/td><td>Allscan URI 125 (CM108)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Terminal output in this article was captured on the working station. Callsigns and names are replaced with the placeholders listed below.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Hardware Needed<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A computer or virtual machine running a recent Debian or Ubuntu based Linux. The reference virtual machine has 4 CPU cores, 1.6 GB of RAM, and 31 GB of disk.<\/li>\n\n\n\n<li>A VHF\/UHF FM radio with a data port. The 9600 baud packet mode and VARA FM wide use the flat-audio 9600 baud data port. The reference radio is a Yaesu FT-7800 with its data speed set to 9600.<\/li>\n\n\n\n<li>A USB sound interface that can key the radio. The reference interface is an Allscan URI 125 (CM108 chip, GPIO PTT). Interfaces that key through a serial port line (such as the DigiRig) and interfaces that key by VOX (such as the Tigertronics SignaLink USB) work with the same build, and the section on other PTT interfaces covers the differences.<\/li>\n\n\n\n<li>A cable between the radio data port and the interface.<\/li>\n\n\n\n<li>An internet connection with outbound access to the Winlink servers and to APRS-IS.<\/li>\n\n\n\n<li>An amateur radio license for the callsign used by the station.<\/li>\n\n\n\n<li>Winlink gateway credentials: the gateway password for the callsign, obtained through Winlink&#8217;s RMS gateway registration at <a href=\"https:\/\/winlink.org\">https:\/\/winlink.org<\/a>.<\/li>\n\n\n\n<li>An APRS-IS passcode for the callsign (needed only for the iGate).<\/li>\n\n\n\n<li>A second receiver tuned to the station frequency. It allows the transmit audio of each mode to be heard during testing.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">A virtual machine receives the interface through USB passthrough. In Proxmox VE the device is added under &#8220;Hardware&#8221;, &#8220;Add&#8221;, &#8220;USB Device&#8221;, using the vendor\/device ID so the assignment follows the interface itself. The interface then appears inside the guest exactly as it would on bare metal.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Software To Download<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Dire Wolf, installed from the distribution package (version 1.8.1 on the reference station).<\/li>\n\n\n\n<li>XARA, built from source: <a href=\"https:\/\/github.com\/x0rro\/xara\">https:\/\/github.com\/x0rro\/xara<\/a> (reference commit 09a9201, version 0.1.3).<\/li>\n\n\n\n<li>LinBPQ, the 64-bit binary: <a href=\"https:\/\/www.cantab.net\/users\/john.wiseman\/Downloads\/Beta\/linbpq64\">https:\/\/www.cantab.net\/users\/john.wiseman\/Downloads\/Beta\/linbpq64<\/a><\/li>\n\n\n\n<li>The LinBPQ web page set: <a href=\"https:\/\/www.cantab.net\/users\/john.wiseman\/Downloads\/Beta\/HTMLPages.zip\">https:\/\/www.cantab.net\/users\/john.wiseman\/Downloads\/Beta\/HTMLPages.zip<\/a><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The PTT arbiter, the ALSA configuration, and the test scripts appear in full in this article.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Placeholders<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every command and file below uses the following placeholders. Each one is replaced with the real value when the file is created.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Placeholder<\/th><th>Meaning<\/th><\/tr><\/thead><tbody><tr><td>N0CALL<\/td><td>the base station callsign<\/td><\/tr><tr><td>N0CALL-10<\/td><td>the RMS gateway callsign (SSID 10 is the usual choice)<\/td><\/tr><tr><td>N0CALL-7<\/td><td>the packet node callsign<\/td><\/tr><tr><td>N0CALL-5<\/td><td>the APRS iGate callsign<\/td><\/tr><tr><td>AA00aa<\/td><td>the six-character Maidenhead grid square<\/td><\/tr><tr><td>GATEWAY_PASSWORD<\/td><td>the Winlink gateway password<\/td><\/tr><tr><td>SYSOP_USER, SYSOP_PASSWORD<\/td><td>the login for the LinBPQ web interface and node sysop<\/td><\/tr><tr><td>APRS_PASSCODE<\/td><td>the APRS-IS passcode for N0CALL<\/td><\/tr><tr><td>DDMM.mmN, DDDMM.mmW<\/td><td>the station latitude and longitude in APRS format<\/td><\/tr><tr><td>145050000<\/td><td>the station frequency in Hz<\/td><\/tr><tr><td>USERNAME<\/td><td>the Linux administrator account<\/td><\/tr><tr><td>SERIAL_PORT<\/td><td>the serial port of the interface under \/dev\/serial\/by-id\/ (serial PTT interfaces)<\/td><\/tr><tr><td>CARDNAME<\/td><td>the ALSA card name of the sound interface, when it differs from Device<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Preparing the Operating System<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The system starts from a fresh install with network access and a user with sudo rights. The package index is refreshed and the installed packages are brought up to date first.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update\nsudo apt upgrade -y<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Winlink sessions and APRS-IS logins depend on an accurate clock. Ubuntu enables time synchronization by default, and the state is confirmed with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>timedatectl<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>               Local time: Wed 2026-11-04 13:40:12 UTC\n           Universal time: Wed 2026-11-04 13:40:12 UTC\n                 RTC time: Wed 2026-11-04 13:40:12\n                Time zone: Etc\/UTC (UTC, +0000)\nSystem clock synchronized: yes\n              NTP service: active\n          RTC in local TZ: no<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The packages for every part of the build install in one command. The list covers the compiler and audio libraries needed to build XARA, Dire Wolf itself, the ALSA tools, and the helpers used later.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y git build-essential pkg-config rustc cargo \\\n  libasound2-dev libpulse-dev alsa-utils direwolf usbutils \\\n  unzip curl ufw python3<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Ubuntu 26.04 Rust toolchain (1.93) satisfies XARA&#8217;s minimum version of 1.85.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Identifying the USB Interface<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">With the interface connected, the operating system lists it as a USB device and as a sound card.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>lsusb | grep -i c-media<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Bus 002 Device 002: ID 0d8c:0012 C-Media Electronics, Inc. USB Audio Device<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The vendor ID 0d8c and product ID 0012 identify this interface. Other CM108\/CM119 interfaces report different product IDs, and the udev rule later in this article uses whichever pair <code>lsusb<\/code> shows.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cat \/proc\/asound\/cards<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code> 0 &#91;Device         ]: USB-Audio - USB Audio Device\n                      C-Media Electronics Inc. USB Audio Device at usb-0000:02:1b.0-1, full speed<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The card is named <code>Device<\/code> in square brackets. That name appears in the ALSA configuration, so it stays valid even if the card number changes.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>arecord -l\naplay -l<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>**** List of CAPTURE Hardware Devices ****\ncard 0: Device &#91;USB Audio Device], device 0: USB Audio &#91;USB Audio]\n  Subdevices: 1\/1\n  Subdevice #0: subdevice #0\n**** List of PLAYBACK Hardware Devices ****\ncard 0: Device &#91;USB Audio Device], device 0: USB Audio &#91;USB Audio]\n  Subdevices: 1\/1\n  Subdevice #0: subdevice #0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The interface also exposes a HID node. That node carries the GPIO that keys the radio. In a virtual machine the emulated USB tablet creates a HID node as well, so the vendor and product IDs identify the right one.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>for h in \/dev\/hidraw*; do echo -n \"$h: \"; udevadm info -q property -n $h | grep -E '^ID_(VENDOR|MODEL)_ID' | tr '\\n' ' '; echo; done<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/dev\/hidraw0: ID_MODEL_ID=0001 ID_VENDOR_ID=0627\n\/dev\/hidraw1: ID_MODEL_ID=0012 ID_VENDOR_ID=0d8c<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Here <code>\/dev\/hidraw1<\/code> is the interface. The number can change between boots, so the udev rule in the PTT arbiter section creates a stable name. Interfaces that key through a serial port or by VOX have no HID node, and the section on other PTT interfaces covers them.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Service Accounts<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Each program runs under its own unprivileged account. The accounts share the <code>audio<\/code> group so that all of them can open the same sound devices. The Dire Wolf package already creates a <code>direwolf<\/code> account.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo usermod -aG audio direwolf\nsudo useradd --system --no-create-home --shell \/usr\/sbin\/nologin --user-group pttmux\nsudo usermod -aG audio,plugdev pttmux\nsudo useradd --system --home-dir \/opt\/linbpq --no-create-home --shell \/usr\/sbin\/nologin --user-group linbpq<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>xara<\/code> account is created by the XARA installer, and the administrator account joins the <code>audio<\/code> group so that it can run the audio tests.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo usermod -aG audio USERNAME<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The group change applies to new login sessions, so a fresh SSH login follows this step.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Sharing the Radio Audio Between Modems (ALSA dsnoop and dmix)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A USB sound card opened directly by one program is unavailable to the next program. Three modems on one radio need the opposite: every modem must hear the same received audio, and every modem must be able to transmit through the same output. ALSA provides both with two plugins.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The dsnoop plugin takes the single capture stream from the interface and hands a copy to every program that opens it.<\/li>\n\n\n\n<li>The dmix plugin accepts playback streams from several programs and mixes them into the single stream that goes to the interface.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Programs open the shared devices by name instead of opening the hardware. The names are defined in <code>\/etc\/asound.conf<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/etc\/asound.conf<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The file contains the following.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Shared access to the Allscan URI 125 (C-Media CM108) for several modems at once.\n#\n#   capture : one hardware stream fanned out to every reader   (dsnoop)\n#   playback: every writer mixed into one hardware stream      (dmix)\n#\n# ipc_perm\/ipc_gid let different service users (direwolf, xara, ...) share the\n# same dsnoop\/dmix instance; they must all be in group \"audio\".\n#\n# IMPORTANT: keep ipc_key values well apart (&gt;= 10).  dmix also uses key+1 for its\n# sum buffer and dsnoop's segment lands at a nearby key; adjacent keys (e.g. 1 and 2)\n# collide and dmix then fails with \"unable to initialize sum ring buffer\".\n#\n# RX\/TX audio levels: set with alsamixer\/amixer on card \"Device\" (AGC must be off).\n\npcm.uri_capture {\n    type dsnoop\n    ipc_key 584410\n    ipc_perm 0660\n    ipc_gid audio\n    slave {\n        pcm \"hw:CARD=Device,DEV=0\"\n        channels 1\n        rate 48000\n        format S16_LE\n        period_size 480\n        buffer_size 4800\n    }\n}\n\npcm.uri_playback {\n    type dmix\n    ipc_key 584420\n    ipc_perm 0660\n    ipc_gid audio\n    slave {\n        pcm \"hw:CARD=Device,DEV=0\"\n        channels 2\n        rate 48000\n        format S16_LE\n        period_size 480\n        # 200 ms: Xara insists the playback device buffer &gt;= 4x its tx-buffer-ms (40 ms\n        # default -&gt; 160 ms; its minimum setting of 30 ms still needs 120 ms).\n        buffer_size 9600\n    }\n    bindings {\n        0 0\n        1 1\n    }\n}\n\n# --- Dire Wolf: stereo pair = two modems (left=1200 baud, right=9600 baud) -----\n# RX: the single mono radio audio is copied to both channels.\npcm.dw_in {\n    type plug\n    slave.pcm {\n        type route\n        slave.pcm \"uri_capture\"\n        slave.channels 1\n        ttable.0.0 1\n        ttable.1.0 1\n    }\n}\n# TX: left and right are summed onto both radio channels.\npcm.dw_out {\n    type plug\n    slave.pcm {\n        type route\n        slave.pcm \"uri_playback\"\n        slave.channels 2\n        ttable.0.0 1\n        ttable.0.1 1\n        ttable.1.0 1\n        ttable.1.1 1\n    }\n}\n\n# --- Xara (mono modem) ---------------------------------------------------------\npcm.xara_in {\n    type plug\n    slave.pcm \"uri_capture\"\n}\npcm.xara_out {\n    type plug\n    slave.pcm {\n        type route\n        slave.pcm \"uri_playback\"\n        slave.channels 2\n        ttable.0.0 1\n        ttable.0.1 1\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The file defines six names, and each one has a specific job.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>uri_capture<\/code> is the dsnoop device. It reads the interface once, as mono at 48000 Hz.<\/li>\n\n\n\n<li><code>uri_playback<\/code> is the dmix device. It writes to the interface as stereo at 48000 Hz.<\/li>\n\n\n\n<li><code>dw_in<\/code> gives Dire Wolf a stereo capture stream. The mono radio audio is copied to both channels, so the 1200 baud modem on the left channel and the 9600 baud modem on the right channel both receive everything the radio hears.<\/li>\n\n\n\n<li><code>dw_out<\/code> accepts Dire Wolf&#8217;s stereo output. The left channel (1200 baud modem) and the right channel (9600 baud modem) are both summed onto both channels of the interface, so either modem reaches the radio.<\/li>\n\n\n\n<li><code>xara_in<\/code> and <code>xara_out<\/code> give XARA a mono capture and playback path on the same shared devices.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Two numbers in the file matter for the rest of the build.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>ipc_key<\/code> values are 584410 and 584420. ALSA keeps shared state in System V shared memory, and dmix uses the key plus one for its mixing buffer, while dsnoop&#8217;s segment lands at a nearby key. Keys at least ten apart keep the two plugins from colliding.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The dmix <code>buffer_size<\/code> is 9600 frames, which is 200 ms at 48000 Hz. XARA requires the playback device to buffer at least four times its transmit buffer setting (40 ms by default, so 160 ms). The same buffer also determines the transmit tail time set later in the Dire Wolf and LinBPQ configuration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>ipc_perm 0660<\/code> and <code>ipc_gid audio<\/code> lines let accounts in the <code>audio<\/code> group share the same dsnoop and dmix instances.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ALSA reads the file whenever a program opens a device. The new names appear in the device list.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>aplay -L | grep -E '^(uri_|dw_|xara_)'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>uri_capture\nuri_playback\ndw_in\ndw_out\nxara_in\nxara_out<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Verifying the shared devices<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The following script runs two captures and two playbacks at the same time under two different accounts. The playback is digital silence and PTT stays unkeyed, so nothing is transmitted.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>nano ~\/audiotest.py<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/env python3\n\"\"\"Shared-audio check for the virtual ALSA devices.\n\nTwo different users capture the same radio audio at once and play digital\nsilence into the shared mixer at once.  PTT is never keyed, so nothing is\ntransmitted.  Run with:  python3 audiotest.py\n\"\"\"\nimport array\nimport getpass\nimport math\nimport subprocess\nimport sys\nimport threading\n\nresults = {}\n\n\ndef rms(samples):\n    return math.sqrt(sum(v * v for v in samples) \/ max(len(samples), 1))\n\n\ndef capture(user, device, channels):\n    p = subprocess.run(\n        &#91;\"sudo\", \"-u\", user, \"arecord\", \"-q\", \"-D\", device, \"-f\", \"S16_LE\", \"-r\", \"48000\",\n         \"-c\", str(channels), \"-d\", \"4\", \"-t\", \"raw\"], capture_output=True)\n    data = array.array(\"h\")\n    data.frombytes(p.stdout&#91;: len(p.stdout) \/\/ 2 * 2])\n    left = data&#91;0::channels]\n    right = data&#91;1::channels] if channels &gt; 1 else left\n    results&#91;f\"RX {user:10s} {device:12s}\"] = (\n        p.returncode, f\"frames={len(data) \/\/ channels} rmsL={rms(left):.1f} rmsR={rms(right):.1f}\"\n        + (f\"  {p.stderr.decode().strip()}\" if p.stderr else \"\"))\n\n\ndef play(user, device, channels):\n    p = subprocess.run(\n        &#91;\"sudo\", \"-u\", user, \"aplay\", \"-q\", \"-D\", device, \"-f\", \"S16_LE\", \"-r\", \"48000\",\n         \"-c\", str(channels), \"-d\", \"4\", \"\/dev\/zero\"], capture_output=True)\n    results&#91;f\"TX {user:10s} {device:12s}\"] = (\n        p.returncode, \"played 4 s of silence\" + (f\"  {p.stderr.decode().strip()}\" if p.stderr else \"\"))\n\n\nme = getpass.getuser()\njobs = &#91;\n    (capture, (me, \"uri_capture\", 1)),\n    (capture, (\"direwolf\", \"dw_in\", 2)),\n    (play, (me, \"uri_playback\", 2)),\n    (play, (\"direwolf\", \"dw_out\", 2)),\n]\nthreads = &#91;threading.Thread(target=fn, args=args) for fn, args in jobs]\nfor t in threads:\n    t.start()\nfor t in threads:\n    t.join()\n\nfailed = 0\nfor key in sorted(results):\n    code, message = results&#91;key]\n    print((\"OK   \" if code == 0 else \"FAIL \") + key + \"  \" + message)\n    failed += code != 0\nsys.exit(1 if failed else 0)<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>python3 -I ~\/audiotest.py<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>OK   RX direwolf   dw_in         frames=192000 rmsL=3444.5 rmsR=3444.5\nOK   RX admin      uri_capture   frames=192000 rmsL=3444.4 rmsR=3444.4\nOK   TX direwolf   dw_out        played 4 s of silence\nOK   TX admin      uri_playback  played 4 s of silence<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Four <code>OK<\/code> lines confirm that two accounts read and write the interface simultaneously. Identical <code>rmsL<\/code> and <code>rmsR<\/code> values on the <code>dw_in<\/code> line confirm that the mono radio audio reaches both modem channels. A non-zero RMS value shows that the radio data port is delivering audio to the interface.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Mixer Levels<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The interface exposes three mixer controls.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>amixer -c 0 scontrols<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Simple mixer control 'Speaker',0\nSimple mixer control 'Mic',0\nSimple mixer control 'Auto Gain Control',0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Data modems need a fixed, predictable audio path, so automatic gain control is switched off. The Speaker control sets the transmit audio level into the radio. The reference station settled on step 21 of 37 (-16 dB), which is a good starting point for the transmit level adjustment later in this article.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>amixer -c 0 sset 'Auto Gain Control' off\namixer -c 0 sset Speaker 21\nsudo alsactl store<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>amixer -c 0 sget Speaker | grep -E 'Limits|Front'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>  Limits: Playback 0 - 37\n  Front Left: Playback 21 &#91;57%] &#91;-16.00dB] &#91;on]\n  Front Right: Playback 21 &#91;57%] &#91;-16.00dB] &#91;on]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>alsactl store<\/code> command saves the state, and the ALSA restore service reapplies it at every boot.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">PTT Arbiter<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The problem<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Dire Wolf keys PTT for the 1200 baud and 9600 baud channels independently, and XARA keys PTT for VARA FM. All three drive the one GPIO line of the interface. With all three modems driving the line directly, whichever modem finishes first releases PTT while another modem is still transmitting, and the end of that transmission is lost.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The solution<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each modem keys a named pipe (FIFO) instead of the real interface. A small program, the PTT arbiter, reads all the pipes and drives the interface itself. PTT is asserted while any modem wants to transmit and is released only after every modem has released.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Dire Wolf accepts any file path in its <code>PTT CM108<\/code> setting, and XARA accepts any path in <code>ptt = cm108:PATH:GPIO<\/code>. Both write the same five-byte CM108 HID report to that path: <code>[0, 0, data, mask, 0]<\/code>, where the data bit for the GPIO is 1 for transmit and 0 for receive.<\/li>\n\n\n\n<li>The output side of the arbiter is selectable. The default writes the CM108 HID report to the real interface, and <code>--output rts<\/code> or <code>--output dtr<\/code> drives a serial port line instead, as covered in the section on other PTT interfaces.<\/li>\n\n\n\n<li>The arbiter keeps one state per modem, ignores reports for other GPIO pins, and logs the moments when two modems key at the same time as <code>OVERLAP<\/code>.<\/li>\n\n\n\n<li>PTT is forced off when the arbiter starts and when it exits. A per-modem watchdog releases a modem that holds PTT for more than 120 seconds.<\/li>\n\n\n\n<li>The FIFOs survive an arbiter restart, because XARA holds its pipe open for as long as it runs. Stale bytes left in a pipe are discarded at start-up so that only fresh transmit requests key the radio.<\/li>\n\n\n\n<li>The <code>--dry-run<\/code> option logs the actions and leaves the interface untouched.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">The udev rule<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The HID node number can change between boots. A udev rule creates a stable name, <code>\/dev\/uri125-ptt<\/code>, from the vendor and product IDs found earlier with <code>lsusb<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/etc\/udev\/rules.d\/61-uri125.rules<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Stable name for the Allscan URI 125 (CM108) PTT HID node, independent of\n# \/dev\/hidrawN numbering.  Used by pttmux.\nSUBSYSTEM==\"hidraw\", ATTRS{idVendor}==\"0d8c\", ATTRS{idProduct}==\"0012\", SYMLINK+=\"uri125-ptt\"<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo udevadm control --reload\nsudo udevadm trigger --subsystem-match=hidraw --action=change\nls -l \/dev\/uri125-ptt<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>lrwxrwxrwx 1 root root 7 Nov  4 13:35 \/dev\/uri125-ptt -&gt; hidraw1<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Dire Wolf package installs a second udev rule that gives the <code>audio<\/code> group read and write access to CM108 HID nodes. The <code>pttmux<\/code> account is a member of that group.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ls -l \/dev\/hidraw1<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>crw-rw---- 1 root audio 240, 1 Nov  4 13:35 \/dev\/hidraw1<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">The arbiter program<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The script uses only the Python standard library. It is saved as <code>\/usr\/local\/bin\/pttmux<\/code> and made executable.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/usr\/local\/bin\/pttmux\nsudo chmod 755 \/usr\/local\/bin\/pttmux<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/env python3\n\"\"\"pttmux - merge PTT requests from several modems into one PTT line.\n\nSeveral modems (Dire Wolf 1200\/9600, XARA) share a single radio.  Each modem is\npointed at its own FIFO, which looks like a \"CM108 hidraw device\" to it.  A modem\nkeys the radio by writing the usual 5-byte CM108 HID output report\n\n    &#91;0, 0, data, mask, 0]        data bit (gpio-1) = 1 -&gt; PTT on\n\nto its FIFO.  This daemon tracks one state per client and drives the real PTT\nline so that PTT is asserted while ANY client wants to transmit and released\nonly when ALL have released.  Without this, whichever modem finishes first would\ndrop PTT in the middle of another modem's transmission.\n\nThe real PTT line is chosen with --output:\n\n    cm108   GPIO of a CM108\/CM119 interface, through --device \/dev\/hidrawN (default)\n    rts     RTS of the serial port --device; DTR is held low\n    dtr     DTR of the serial port --device; RTS is held low\n\n--invert reverses the polarity of a serial line (keyed = line low).\n\nSafety: PTT is forced off at start-up and shutdown, and a per-client watchdog\nreleases a client that has held PTT longer than --max-on seconds.\n\nOverlapping requests (two modems transmitting at once) are logged; audio from the\nmodems is mixed by ALSA dmix, so an overlap is a collision on the air, exactly as\non any shared packet channel.\n\"\"\"\nimport argparse\nimport fcntl\nimport grp\nimport os\nimport select\nimport signal\nimport stat\nimport struct\nimport sys\nimport termios\nimport time\n\n\ndef log(msg):\n    print(msg, flush=True)  # journald adds the timestamp\n\n\nclass Cm108Output:\n    \"\"\"CM108\/CM119 GPIO, driven with a HID output report.\"\"\"\n\n    def __init__(self, device, bit):\n        self.device, self.bit = device, bit\n        self.fd = os.open(device, os.O_WRONLY)\n\n    def describe(self):\n        return f\"CM108 GPIO bit {self.bit:#04x} on {self.device}\"\n\n    def set(self, on):\n        os.write(self.fd, bytes(&#91;0, 0, self.bit if on else 0, self.bit, 0]))\n\n    def close(self):\n        os.close(self.fd)\n\n\nclass SerialOutput:\n    \"\"\"RTS or DTR of a serial port; the other line is held low.\"\"\"\n\n    def __init__(self, device, line, invert):\n        self.device, self.line, self.invert = device, line, invert\n        self.fd = os.open(device, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)\n        try:\n            if not os.isatty(self.fd):\n                raise OSError(f\"{device} is not a serial port\")\n            # Opening a port raises RTS and DTR; both are cleared at once.\n            self._modem(termios.TIOCMBIC, termios.TIOCM_RTS | termios.TIOCM_DTR)\n            # Keep the lines as they are when the port closes (the default would\n            # drop them, which keys an inverted interface).\n            try:\n                attrs = termios.tcgetattr(self.fd)\n                attrs&#91;2] &amp;= ~termios.HUPCL\n                termios.tcsetattr(self.fd, termios.TCSANOW, attrs)\n            except termios.error:\n                if invert:\n                    raise\n            self.set(False)\n        except BaseException:\n            os.close(self.fd)\n            raise\n\n    def describe(self):\n        name = \"RTS\" if self.line == termios.TIOCM_RTS else \"DTR\"\n        pol = \"inverted\" if self.invert else \"normal\"\n        return f\"{name} ({pol} polarity, other line held low) on {self.device}\"\n\n    def _modem(self, request, bits):\n        fcntl.ioctl(self.fd, request, struct.pack(\"I\", bits))\n\n    def _lines(self):\n        return struct.unpack(\"I\", fcntl.ioctl(self.fd, termios.TIOCMGET, struct.pack(\"I\", 0)))&#91;0]\n\n    def set(self, on):\n        high = on != self.invert\n        self._modem(termios.TIOCMBIS if high else termios.TIOCMBIC, self.line)\n        if bool(self._lines() &amp; self.line) != high:\n            raise OSError(f\"{self.device}: modem line did not follow\")\n\n    def close(self):\n        os.close(self.fd)\n\n\nclass Mux:\n    def __init__(self, a):\n        self.args = a\n        self.bit = 1 &lt;&lt; (a.gpio - 1)\n        self.dry = a.dry_run\n        self.max_on = a.max_on\n        self.stop = False\n        self.out = None\n        self.hw_on = None  # unknown until first write\n        self.last_err = (\"\", 0.0)\n        self.fd = {}       # client -&gt; fifo fd\n        self.name = {}     # fifo fd -&gt; client\n        self.on_since = {}  # client -&gt; monotonic time PTT went on, or None\n        self.buf = {}\n        gid = grp.getgrnam(a.group).gr_gid if a.group else -1\n        os.makedirs(a.dir, exist_ok=True)\n        for c in a.client:\n            p = os.path.join(a.dir, c)\n            # Reuse an existing FIFO: a modem (xara) keeps its end open for its whole\n            # life, so replacing the inode on a pttmux restart would orphan it.\n            try:\n                if not stat.S_ISFIFO(os.stat(p).st_mode):\n                    os.unlink(p)\n                    raise FileNotFoundError\n            except FileNotFoundError:\n                os.mkfifo(p, 0o660)\n            os.chmod(p, 0o660)\n            if gid != -1:\n                os.chown(p, -1, gid)\n            # O_RDWR: we are always a reader (so a modem's open() never blocks)\n            # and a writer (so we never see EOF when modems come and go).\n            fd = os.open(p, os.O_RDWR | os.O_NONBLOCK)\n            self.fd&#91;c] = fd\n            self.name&#91;fd] = c\n            self.on_since&#91;c] = None\n            self.buf&#91;c] = b\"\"\n            log(f\"client {c}: {p}\")\n\n    # ---- real PTT line ----------------------------------------------------\n    def open_output(self):\n        a = self.args\n        if a.output == \"cm108\":\n            out = Cm108Output(a.device, self.bit)\n        else:\n            line = termios.TIOCM_RTS if a.output == \"rts\" else termios.TIOCM_DTR\n            out = SerialOutput(a.device, line, a.invert)\n        log(\"output: \" + out.describe())\n        return out\n\n    def drop_output(self):\n        if self.out is not None:\n            try:\n                self.out.close()\n            except OSError:\n                pass\n            self.out = None\n\n    def hw(self, on):\n        if self.dry:\n            self.hw_on = on\n            log(f\"&#91;dry-run] PTT line would be {'ON' if on else 'OFF'}\")\n            return\n        try:\n            if self.out is None:\n                self.out = self.open_output()\n            self.out.set(on)\n            self.hw_on = on\n        except OSError as e:\n            msg, when = self.last_err\n            now = time.monotonic()\n            if str(e) != msg or now - when &gt;= 30:  # one line per 30 s, not one per retry\n                log(f\"ERROR on {self.args.device}: {e}\")\n                self.last_err = (str(e), now)\n            self.drop_output()\n\n    def want(self):\n        return any(t is not None for t in self.on_since.values())\n\n    def reconcile(self):\n        w = self.want()\n        if w != self.hw_on:\n            self.hw(w)\n            if self.hw_on == w:\n                log(\"PTT \" + (\"ON\" if w else \"OFF\"))\n\n    # ---- client reports ---------------------------------------------------\n    def report(self, c, rep):\n        data, mask = rep&#91;2], rep&#91;3]\n        if not mask &amp; self.bit:\n            return  # not about our GPIO\n        on = bool(data &amp; self.bit)\n        was = self.on_since&#91;c] is not None\n        if on and not was:\n            others = &#91;o for o, t in self.on_since.items() if t is not None and o != c]\n            self.on_since&#91;c] = time.monotonic()\n            log(f\"{c} keyed\" + (f\"  ** OVERLAP with {','.join(others)} **\" if others else \"\"))\n        elif not on and was:\n            held = time.monotonic() - self.on_since&#91;c]\n            self.on_since&#91;c] = None\n            log(f\"{c} released after {held:.1f}s\")\n\n    def feed(self, c, data):\n        b = self.buf&#91;c] + data\n        while len(b) &gt;= 5:\n            rep = b&#91;:5]\n            if rep&#91;0] != 0 or rep&#91;4] != 0:  # not a CM108 report: resync\n                b = b&#91;1:]\n                continue\n            b = b&#91;5:]\n            self.report(c, rep)\n        self.buf&#91;c] = b\n\n    def watchdog(self):\n        now = time.monotonic()\n        for c, t in self.on_since.items():\n            if t is not None and now - t &gt; self.max_on:\n                log(f\"WATCHDOG: {c} held PTT &gt; {self.max_on}s, forcing release\")\n                self.on_since&#91;c] = None\n\n    # ---- main loop --------------------------------------------------------\n    def run(self):\n        p = select.poll()\n        for fd in self.name:\n            p.register(fd, select.POLLIN)\n        self.hw(False)  # known-safe start\n        # A reused FIFO may hold stale reports (e.g. an old PTT-ON written just before we\n        # were restarted).  Never replay those: discard everything, fail safe = PTT off.\n        for fd, c in self.name.items():\n            drained = 0\n            while True:\n                try:\n                    d = os.read(fd, 4096)\n                except BlockingIOError:\n                    break\n                if not d:\n                    break\n                drained += len(d)\n            if drained:\n                log(f\"discarded {drained} stale byte(s) from {c}\")\n        log(\"ready; PTT off\")\n        while not self.stop:\n            try:\n                events = p.poll(500)\n            except InterruptedError:\n                continue\n            for fd, _ in events:\n                try:\n                    data = os.read(fd, 4096)\n                except BlockingIOError:\n                    continue\n                if data:\n                    self.feed(self.name&#91;fd], data)\n            self.watchdog()\n            self.reconcile()\n\n    def shutdown(self):\n        for c in self.on_since:\n            self.on_since&#91;c] = None\n        self.hw(False)\n        self.drop_output()\n        log(\"PTT forced OFF; exiting\")\n\n\ndef main():\n    ap = argparse.ArgumentParser(description=__doc__.split(\"\\n\\n\")&#91;0])\n    ap.add_argument(\"--output\", choices=(\"cm108\", \"rts\", \"dtr\"), default=\"cm108\",\n                    help=\"real PTT line: CM108 GPIO (default) or serial RTS\/DTR\")\n    ap.add_argument(\"--device\", default=\"\/dev\/uri125-ptt\",\n                    help=\"hidraw node (cm108) or serial port (rts, dtr)\")\n    ap.add_argument(\"--gpio\", type=int, default=3, choices=range(1, 9), metavar=\"1-8\",\n                    help=\"GPIO number the modems key in their CM108 reports (default 3)\")\n    ap.add_argument(\"--invert\", action=\"store_true\", help=\"serial output: keyed = line low\")\n    ap.add_argument(\"--dir\", default=\"\/run\/pttmux\", help=\"directory for client FIFOs\")\n    ap.add_argument(\"--group\", default=\"audio\", help=\"group given access to the FIFOs\")\n    ap.add_argument(\"--client\", action=\"append\", required=True, help=\"client name (repeatable)\")\n    ap.add_argument(\"--max-on\", type=float, default=120.0, help=\"per-client watchdog, seconds\")\n    ap.add_argument(\"--dry-run\", action=\"store_true\", help=\"never touch the PTT line; log only\")\n    a = ap.parse_args()\n    if a.invert and a.output == \"cm108\":\n        ap.error(\"--invert applies to the serial outputs (rts, dtr)\")\n\n    m = Mux(a)\n\n    def bye(*_):\n        m.stop = True\n\n    signal.signal(signal.SIGTERM, bye)\n    signal.signal(signal.SIGINT, bye)\n    try:\n        m.run()\n    finally:\n        m.shutdown()\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">The service<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The service runs the arbiter as the <code>pttmux<\/code> account with the three FIFO names <code>dw1200<\/code>, <code>dw9600<\/code>, and <code>xara<\/code>. The <code>RuntimeDirectory<\/code> setting creates <code>\/run\/pttmux<\/code> at start. The default <code>--output cm108<\/code> writes to the HID node named by <code>--device<\/code>. The <code>--gpio 3<\/code> option names the GPIO number that the modems key in their pipes and that the CM108 output drives, which is 3 on the URI 125 and on most CM108 interfaces.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/etc\/systemd\/system\/pttmux.service<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Unit]\nDescription=PTT multiplexer (shared by Dire Wolf and XARA)\nAfter=sound.target\n\n&#91;Service]\nType=simple\nUser=pttmux\nGroup=pttmux\nSupplementaryGroups=audio plugdev\nRuntimeDirectory=pttmux\nRuntimeDirectoryMode=0755\nRuntimeDirectoryPreserve=restart\nExecStart=\/usr\/local\/bin\/pttmux --device \/dev\/uri125-ptt --gpio 3 --dir \/run\/pttmux --client dw1200 --client dw9600 --client xara\nRestart=always\nRestartSec=2\n\nNoNewPrivileges=yes\nProtectSystem=strict\nProtectHome=yes\nPrivateTmp=yes\nProtectKernelTunables=yes\nProtectKernelModules=yes\nProtectControlGroups=yes\nRestrictNamespaces=yes\nRestrictSUIDSGID=yes\nLockPersonality=yes\nRestrictAddressFamilies=AF_UNIX\nCapabilityBoundingSet=\nDevicePolicy=closed\nDeviceAllow=char-hidraw rw\nUMask=0007\n\n&#91;Install]\nWantedBy=multi-user.target<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl enable --now pttmux<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Created symlink '\/etc\/systemd\/system\/multi-user.target.wants\/pttmux.service' \u2192 '\/etc\/systemd\/system\/pttmux.service'.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The service state, the three pipes, and the start-up log confirm the arbiter.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl is-active pttmux\nls -l \/run\/pttmux\/\nsudo journalctl -u pttmux -n 6 --no-pager -o cat<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>active\ntotal 0\nprw-rw---- 1 pttmux audio 0 Nov  4 13:35 dw1200\nprw-rw---- 1 pttmux audio 0 Nov  4 13:35 dw9600\nprw-rw---- 1 pttmux audio 0 Nov  4 13:35 xara\nStarted pttmux.service - PTT multiplexer (shared by Dire Wolf and XARA).\nclient dw1200: \/run\/pttmux\/dw1200\nclient dw9600: \/run\/pttmux\/dw9600\nclient xara: \/run\/pttmux\/xara\noutput: CM108 GPIO bit 0x04 on \/dev\/uri125-ptt\nready; PTT off<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Keying the radio by hand<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A short manual key-down confirms that GPIO 3 is the right pin for the interface and that the radio transmits. The test is run into a dummy load. Writing the report bytes to a pipe acts exactly like a modem keying PTT.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo sh -c 'printf \"\\000\\000\\004\\004\\000\" &gt; \/run\/pttmux\/xara'\nsleep 2\nsudo sh -c 'printf \"\\000\\000\\000\\004\\000\" &gt; \/run\/pttmux\/xara'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first command sends data <code>04<\/code> with mask <code>04<\/code> (GPIO 3 on), and the second sends data <code>00<\/code> with the same mask (GPIO 3 off). The radio keys for two seconds, and the arbiter logs the event.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u pttmux -n 4 --no-pager -o cat<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>xara keyed\nPTT ON\nxara released after 2.0s\nPTT OFF<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">An interface that keys on a different GPIO number takes that number in the <code>--gpio<\/code> option of the service and in the Dire Wolf and XARA PTT settings that follow. For GPIO number n, the data and mask bytes of the manual test are both 2 raised to the power n minus 1 (4 for GPIO 3, 8 for GPIO 4).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Other PTT Interfaces: Serial Port and VOX<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The CM108 GPIO is one of three common ways for a sound interface to key a radio. Interfaces such as the DigiRig key the radio with a control line of a USB serial port. Interfaces such as the Tigertronics SignaLink USB key the radio from the audio itself (VOX). Each method fits the same station design with a small change.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Interface type<\/th><th>Example<\/th><th>How the radio is keyed<\/th><th>PTT arbiter<\/th><th>Dire Wolf PTT line<\/th><th>XARA ptt setting<\/th><\/tr><\/thead><tbody><tr><td>CM108 GPIO<\/td><td>Allscan URI 125<\/td><td>GPIO pin of the sound chip<\/td><td><code>--output cm108<\/code><\/td><td><code>PTT CM108 3 \/run\/pttmux\/NAME<\/code><\/td><td><code>cm108:\/run\/pttmux\/xara:3<\/code><\/td><\/tr><tr><td>Serial port<\/td><td>DigiRig<\/td><td>RTS (or DTR) of the interface&#8217;s serial port<\/td><td><code>--output rts<\/code> (or <code>dtr<\/code>)<\/td><td>the same pipe line as CM108<\/td><td>the same pipe setting as CM108<\/td><\/tr><tr><td>VOX<\/td><td>SignaLink USB<\/td><td>audio from the computer trips a voice-operated circuit<\/td><td>not used<\/td><td>no PTT line<\/td><td><code>vox<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Serial Port PTT (DigiRig and Similar)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The DigiRig Mobile pairs a CM108-class sound codec with a CP2102 USB serial port. The product page describes its PTT as an open-collector switch controlled by the RTS signal of the COM port. The radio is keyed by raising RTS, and the sound card carries only audio.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The modems keep keying their pipes exactly as in the CM108 build, and the Dire Wolf and XARA PTT settings stay unchanged. The arbiter performs the last step: it turns the combined PTT state into RTS (or DTR) on the serial port, using the same line sequence as XARA&#8217;s own serial PTT. RTS or DTR is the keyed line, the other line is held low, and both lines are cleared within milliseconds of the port opening, because Linux raises them at open. When the arbiter exits, the lines stay in their released state.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The serial port appears under <code>\/dev\/serial\/by-id\/<\/code> with a name that stays the same between boots and across USB ports.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ls -l \/dev\/serial\/by-id\/<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>lrwxrwxrwx 1 root root 13 Nov  4 13:35 usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_SERIAL-if00-port0 -&gt; ..\/..\/ttyUSB0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The example shows the naming of a CP210x device. The serial number part of the name differs for every device. The full path, <code>\/dev\/serial\/by-id\/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_SERIAL-if00-port0<\/code> in this example, is the <code>SERIAL_PORT<\/code> value used below.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Serial devices belong to the <code>dialout<\/code> group, so the arbiter account joins that group.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo usermod -aG dialout pttmux<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The sound card of the interface is named in <code>\/etc\/asound.conf<\/code>. A CM108-class codec captures in mono, so the file from the earlier section applies with the card name changed. The section on sound card changes at the end of this section covers the card name and the channel count.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Three settings in <code>\/etc\/systemd\/system\/pttmux.service<\/code> change. <code>SupplementaryGroups<\/code> adds the <code>dialout<\/code> group, <code>ExecStart<\/code> selects the serial output and names the port, and <code>DeviceAllow<\/code> permits the serial device classes in place of the HID class.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SupplementaryGroups=audio dialout\nExecStart=\/usr\/local\/bin\/pttmux --output rts --device \/dev\/serial\/by-id\/SERIAL_PORT --dir \/run\/pttmux --client dw1200 --client dw9600 --client xara\nDeviceAllow=char-ttyUSB rw\nDeviceAllow=char-ttyACM rw<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The original <code>SupplementaryGroups=audio plugdev<\/code>, <code>ExecStart<\/code>, and <code>DeviceAllow=char-hidraw rw<\/code> lines are replaced by the lines above. The udev rule for the CM108 HID node is unnecessary for a serial interface.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The options of the serial output are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>--output rts<\/code> keys with RTS and holds DTR low. <code>--output dtr<\/code> keys with DTR and holds RTS low.<\/li>\n\n\n\n<li><code>--invert<\/code> reverses the polarity for interfaces that key when the line is low. The idle state is then a high line, and the arbiter keeps it high when it exits.<\/li>\n\n\n\n<li><code>--device<\/code> names the serial port. <code>--gpio<\/code> keeps its default of 3, because it names the GPIO number that the modems key in their pipes.<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl restart pttmux\nsudo journalctl -u pttmux -n 6 --no-pager -o cat<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Started pttmux.service - PTT multiplexer (shared by Dire Wolf and XARA).\nclient dw1200: \/run\/pttmux\/dw1200\nclient dw9600: \/run\/pttmux\/dw9600\nclient xara: \/run\/pttmux\/xara\noutput: RTS (normal polarity, other line held low) on \/dev\/serial\/by-id\/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_SERIAL-if00-port0\nready; PTT off<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>output:<\/code> line names the line, the polarity, and the port. The manual key test from the arbiter section then confirms the interface. The radio keys for two seconds into a dummy load, and the log shows the same four lines as before.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo sh -c 'printf \"\\000\\000\\004\\004\\000\" &gt; \/run\/pttmux\/xara'\nsleep 2\nsudo sh -c 'printf \"\\000\\000\\000\\004\\000\" &gt; \/run\/pttmux\/xara'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A station that runs a single modem can key the serial port directly. Dire Wolf takes the port and the line in its <code>PTT<\/code> setting, and a leading minus sign inverts the polarity.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PTT \/dev\/serial\/by-id\/SERIAL_PORT RTS<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">XARA takes the same information in its <code>ptt<\/code> setting, with <code>dtr:<\/code> available for DTR interfaces.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ptt = rts:\/dev\/serial\/by-id\/SERIAL_PORT<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">VOX PTT (SignaLink and Similar)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A VOX interface keys the radio from the audio. The SignaLink USB keys the radio automatically (&#8220;Auto-PTT&#8221;) through a relay-based keying circuit when audio arrives from the computer, and Tigertronics specifies an adjustable transmit hang time of 30 ms to 3 seconds. The interface needs no PTT line, so the PTT arbiter has no job in a VOX station.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The shared audio design already provides what a VOX station needs. The dmix plugin mixes the output of all three modems into the one stream that reaches the interface, and any modem that plays audio keys the radio. Overlapping transmissions mix in the stream, just as they do on a shared channel. Digital silence between transmissions leaves the VOX circuit idle.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A VOX station skips the PTT arbiter section, the udev rule for the HID node, and the <code>pttmux<\/code> account. Three configuration changes replace them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Dire Wolf.<\/strong> The <code>PTT<\/code> lines are removed from <code>packet.conf<\/code>, and the unit file drops the dependency on the arbiter. Dire Wolf keys the radio only on channels that have a <code>PTT<\/code> line.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Dire Wolf: two modems on one stereo pair, VOX interface (no PTT lines)\nADEVICE dw_in dw_out\nARATE 48000\nACHANNELS 2\n\nCHANNEL 0\nMYCALL N0CALL-10\nMODEM 1200\nTXDELAY 50\nTXTAIL 30\n\nCHANNEL 1\nMYCALL N0CALL-10\nMODEM 9600\nTXDELAY 30\nTXTAIL 30\n\nKISSPORT 8001\nAGWPORT 0<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Unit]\nDescription=Dire Wolf soundmodem - 1200 and 9600 baud AX.25\nAfter=sound.target network.target<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>[Unit]<\/code> lines above replace the <code>After=<\/code> and <code>Requires=<\/code> lines of <code>direwolf-packet.service<\/code>, and the rest of the unit stays the same. At start-up Dire Wolf confirms the setup and states its VOX requirement.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Channel 0: 1200 baud, AFSK 1200 &amp; 2200 Hz, A+, 48000 sample rate, Tx AX.25.\nChannel 1: 9600 baud, K9NG\/G3RUH, +, 48000 sample rate x 3, Tx AX.25.\nThe ratio of audio samples per sec (48000) to data rate in baud (9600) is 5.0\nIncreasing the sample rate should improve decoder performance.\n\nNote: PTT not configured for channel 0. (OK if using VOX.)\nWhen using VOX, ensure that it adds very little delay (e.g. 10-20) milliseconds\nbetween the time that transmit audio ends and PTT is deactivated.\nFor example, if using a SignaLink USB, turn the DLY control all the way\ncounter clockwise.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dire Wolf transmits its <code>TXDELAY<\/code> and <code>TXTAIL<\/code> periods as flag audio around the data. The VOX circuit therefore keys on the first flags and stays keyed through the tail, and the radio&#8217;s key-up time is absorbed by the <code>TXDELAY<\/code> preamble. The SignaLink&#8217;s delay control (<code>DLY<\/code> in Dire Wolf&#8217;s note) is turned fully counter-clockwise, as the note says, so the radio unkeys promptly after the last flag. The <code>TXDELAY<\/code> values of this build, 500 ms for 1200 baud and 300 ms for 9600 baud, are a starting point, and longer values give a slower radio more time to key up.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>XARA.<\/strong> The <code>ptt<\/code> setting selects VOX, and the lead and tail lines are left out.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># XARA: VARA FM on the shared interface, VOX\ncall = N0CALL-10\nfm = wide\nregistered = on\naudio = alsa\ncapture = xara_in\nplayback = xara_out\nradio = on\ndrive-db = -4\nptt = vox\nkiss-port = off\naddr = 127.0.0.1\nport = 8300<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The start-up log names the PTT method.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>xara 0.1.3: N0CALL-10 FM Wide, cmd 127.0.0.1:8300 data 127.0.0.1:8301, xara_in -&gt; xara_out\nxara: RADIO MODE: transmitting into \"xara_out\", PTT VOX (no PTT line), lead 0 ms, tail 100ms, watchdog 30s<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The drop-in file for the XARA service lists only Dire Wolf for ordering.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Unit]\nAfter=direwolf-packet.service\nStartLimitIntervalSec=0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>LinBPQ.<\/strong> The LinBPQ configuration is identical to the CM108 build, including the <code>TXDELAY<\/code> and <code>TXTAIL<\/code> values on the two packet ports.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The audio levels follow the same procedure as the other interfaces (see the adjustment section). On a SignaLink the TX and RX knobs on the front panel set the levels at the interface, and the ALSA mixer sets the level from the computer.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Sound Card Changes for Other Interfaces<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The card name and the capture channel count are the two interface-specific values in <code>\/etc\/asound.conf<\/code>. The card name appears in square brackets in the card list, and the stream listing shows the channels and sample rates of both directions.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cat \/proc\/asound\/cards\ncat \/proc\/asound\/card0\/stream0<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code> 0 &#91;Device         ]: USB-Audio - USB Audio Device\n                      C-Media Electronics Inc. USB Audio Device at usb-0000:02:1b.0-1, full speed\nC-Media Electronics Inc. USB Audio Device at usb-0000:02:1b.0-1, full speed : USB Audio\n\nPlayback:\n  Status: Stop\n  Interface 1\n    Altset 1\n    Format: S16_LE\n    Channels: 2\n    Endpoint: 0x01 (1 OUT) (ADAPTIVE)\n    Rates: 48000, 44100\n    Bits: 16\n    Channel map: FL FR\n\nCapture:\n  Status: Stop\n  Interface 2\n    Altset 1\n    Format: S16_LE\n    Channels: 1\n    Endpoint: 0x82 (2 IN) (SYNC)\n    Rates: 48000, 44100\n    Bits: 16\n    Channel map: MONO<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The example shows the CM108 interface used throughout this article: stereo playback, mono capture, and 48000 Hz available in both directions. An interface with the same layout takes only the card name. Both <code>pcm \"hw:CARD=Device,DEV=0\"<\/code> lines in <code>uri_capture<\/code> and <code>uri_playback<\/code> change to the card name shown in brackets for the new interface, for example <code>pcm \"hw:CARD=CARDNAME,DEV=0\"<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">An interface that captures in stereo takes three additional edits. The <code>dsnoop<\/code> slave in <code>uri_capture<\/code> changes to <code>channels 2<\/code>, and the two capture routes (<code>dw_in<\/code> and <code>xara_in<\/code>) take their audio from the left channel of the stereo stream. The following definitions replace <code>uri_capture<\/code>, <code>dw_in<\/code>, and <code>xara_in<\/code>. The rest of the file stays the same.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pcm.uri_capture {\n    type dsnoop\n    ipc_key 584410\n    ipc_perm 0660\n    ipc_gid audio\n    slave {\n        pcm \"hw:CARD=CARDNAME,DEV=0\"\n        channels 2\n        rate 48000\n        format S16_LE\n        period_size 480\n        buffer_size 4800\n    }\n}\n\npcm.dw_in {\n    type plug\n    slave.pcm {\n        type route\n        slave.pcm \"uri_capture\"\n        slave.channels 2\n        ttable.0.0 1\n        ttable.1.0 1\n    }\n}\n\npcm.xara_in {\n    type plug\n    slave.pcm {\n        type route\n        slave.pcm \"uri_capture\"\n        slave.channels 2\n        ttable.0.0 1\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In these routes, <code>ttable.0.0 1<\/code> copies the left channel of the stream to the first output channel, and <code>ttable.1.0 1<\/code> copies the same left channel to the second output channel of <code>dw_in<\/code>. A test with a left-channel tone confirms the mapping: the tone appears on both channels of <code>dw_in<\/code> and on the single channel of <code>xara_in<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>arecord -D dw_in -f S16_LE -r 48000 -c 2 -d 3 -t raw | wc -c<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>576000<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Three seconds of 48000 Hz, 16-bit, two-channel audio is 576000 bytes, which shows that the new devices open and deliver samples. The <code>audiotest.py<\/code> script from the audio section runs unchanged after the edit and reports the RMS level of both channels.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Interfaces whose mixer controls have other names are adjusted with the names that <code>amixer -c CARDNUMBER scontrols<\/code> lists. Automatic gain control, where the interface has it, is switched off in the same way as on the CM108 interface.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Dire Wolf: 1200 and 9600 Baud Packet<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">One program, two modems<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A single Dire Wolf instance runs both packet speeds. It reads the stereo <code>dw_in<\/code> device and treats the left channel as radio channel 0 (1200 baud AFSK) and the right channel as radio channel 1 (9600 baud G3RUH). Both channels share one KISS TCP port, and LinBPQ addresses them as KISS channels A and B.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo install -d \/etc\/direwolf\nsudo nano \/etc\/direwolf\/packet.conf<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Dire Wolf: two modems on one stereo pair\n#   channel 0 (left)  = 1200 baud AFSK\n#   channel 1 (right) = 9600 baud G3RUH\nADEVICE dw_in dw_out\nARATE 48000\nACHANNELS 2\n\nCHANNEL 0\nMYCALL N0CALL-10\nMODEM 1200\nTXDELAY 50\nTXTAIL 30\nPTT CM108 3 \/run\/pttmux\/dw1200\n\nCHANNEL 1\nMYCALL N0CALL-10\nMODEM 9600\nTXDELAY 30\nTXTAIL 30\nPTT CM108 3 \/run\/pttmux\/dw9600\n\n# LinBPQ attaches here: KISS channel A = radio channel 0, B = radio channel 1\nKISSPORT 8001\nAGWPORT 0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each setting has a specific purpose.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ADEVICE dw_in dw_out<\/code> names the shared ALSA capture and playback devices from <code>\/etc\/asound.conf<\/code>.<\/li>\n\n\n\n<li><code>ARATE 48000<\/code> and <code>ACHANNELS 2<\/code> match the shared devices. 48000 Hz is the highest sample rate the CM108 offers.<\/li>\n\n\n\n<li><code>MODEM 1200<\/code> and <code>MODEM 9600<\/code> select the modem type for each channel.<\/li>\n\n\n\n<li><code>PTT CM108 3 \/run\/pttmux\/dw1200<\/code> keys GPIO 3 through the arbiter pipe of that channel. Each channel has its own pipe. Serial-port interfaces use the same line, and VOX interfaces omit it, as described in the section on other PTT interfaces.<\/li>\n\n\n\n<li><code>TXDELAY<\/code> is the length of the flag sequence Dire Wolf transmits between key-up and the first data, in units of 10 ms. The 500 ms (value 50) on 1200 baud and 300 ms (value 30) on 9600 baud give the radio and the receiving stations time to settle before data starts.<\/li>\n\n\n\n<li><code>TXTAIL<\/code> is the length of the flag sequence Dire Wolf keeps transmitting after the data, in units of 10 ms, and PTT stays down for that time. The 300 ms (value 30) exceeds the 200 ms dmix buffer, so the final frame of every transmission has left the sound card before the radio unkeys.<\/li>\n\n\n\n<li><code>KISSPORT 8001<\/code> opens the KISS TCP port, and <code>AGWPORT 0<\/code> turns the AGW port off.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">LinBPQ sends its own TXDELAY and TXTAIL values over KISS after it connects, with a delay of up to about a minute and a half. The matching values in this file apply from the first second after every Dire Wolf start, so the correct timing is in force during that interval as well.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The service<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/etc\/systemd\/system\/direwolf-packet.service<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Unit]\nDescription=Dire Wolf soundmodem - 1200 and 9600 baud AX.25\nAfter=sound.target pttmux.service network.target\nRequires=pttmux.service\n\n&#91;Service]\nType=simple\nUser=direwolf\nGroup=direwolf\nSupplementaryGroups=audio\nExecStart=\/usr\/bin\/direwolf -c \/etc\/direwolf\/packet.conf -t 0\nRestart=on-failure\nRestartSec=10\nNice=-5\n\nNoNewPrivileges=yes\nProtectSystem=strict\nProtectHome=yes\nPrivateTmp=yes\nProtectKernelTunables=yes\nProtectKernelModules=yes\nProtectControlGroups=yes\nRestrictNamespaces=yes\nRestrictSUIDSGID=yes\nLockPersonality=yes\nCapabilityBoundingSet=\nDevicePolicy=closed\nDeviceAllow=char-alsa rw\n\n&#91;Install]\nWantedBy=multi-user.target<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>Requires=pttmux.service<\/code> line ties Dire Wolf to the arbiter, and <code>-t 0<\/code> turns off colored output so the journal stays readable.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl enable --now direwolf-packet\nsudo journalctl -u direwolf-packet -b --no-pager -o cat | head -20<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Started direwolf-packet.service - Dire Wolf soundmodem - 1200 and 9600 baud AX.25.\nDire Wolf Release 1.8.1, November 2025\nIncludes optional support for:  gpsd hamlib cm108-ptt\nReading config file \/etc\/direwolf\/packet.conf\nCould not extract card number or name from dw_out\nCan't automatically find matching HID for PTT.\nCould not extract card number or name from dw_out\nCan't automatically find matching HID for PTT.\nAudio input device for receive: dw_in  (channels 0 &amp; 1)\nAudio out device for transmit: dw_out  (channels 0 &amp; 1)\nChannel 0: 1200 baud, AFSK 1200 &amp; 2200 Hz, A+, 48000 sample rate, Tx AX.25.\nChannel 1: 9600 baud, K9NG\/G3RUH, +, 48000 sample rate x 3, Tx AX.25.\nThe ratio of audio samples per sec (48000) to data rate in baud (9600) is 5.0\nIncreasing the sample rate should improve decoder performance.\nUsing \/run\/pttmux\/dw1200 GPIO 3 for channel 0 PTT control.\nUsing \/run\/pttmux\/dw9600 GPIO 3 for channel 1 PTT control.\nDisabled AGW network client port.\nReady to accept KISS TCP client application 0 on port 8001 ...<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The two &#8220;Could not extract card number&#8221; lines come from Dire Wolf&#8217;s automatic PTT detection, which runs before it applies the explicit pipe paths. The <code>Using ... for channel 0 PTT control<\/code> lines confirm that the explicit paths took effect. The sample-rate message is informational, because 48000 Hz is the interface maximum.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Reading the receive log<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Received frames appear in the journal with a line for the audio level and a line for the decoded frame.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u direwolf-packet -f -o cat<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>W1ABC-6 audio level = 25(7\/6)    |||||||__\n&#91;0.3] W1ABC-6&gt;APBPQ1,WIDE1-1,WIDE2-1:!0000.00N\/00000.00W&amp;Example BPQ node<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The number after <code>audio level =<\/code> is the received level, and the pair in parentheses holds the mark and space tone peaks. Received levels between about 25 and 40 decoded cleanly on the reference station. The bar graph visualizes the same level. In <code>[0.3]<\/code>, the first number is the radio channel and the digit after the dot identifies which of the parallel decoders produced the frame. The prefix <code>[0L]<\/code> marks a frame that the station transmitted on channel 0, and <code>[1L]<\/code> marks one on channel 1.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The receive level is adjusted with the <code>Mic<\/code> capture control of the interface (<code>amixer -c 0 sget Mic<\/code> shows its range).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">XARA: VARA FM<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Building<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">XARA is built from source with the Rust toolchain installed earlier. The reference build uses commit 09a9201 (version 0.1.3) of the project.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p ~\/src &amp;&amp; cd ~\/src\ngit clone https:\/\/github.com\/x0rro\/xara.git\ncd xara\ngit checkout 09a9201<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Registered VARA users enable the registered speeds with a build feature.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cargo build --release --locked -p xara --features registered<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">An unregistered build omits the feature.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cargo build --release --locked -p xara<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>   Compiling pkg-config v0.3.34\n   Compiling adler2 v2.0.1\n   Compiling num-traits v0.2.19\n   Compiling miniz_oxide v0.8.9\n   Compiling alsa-sys v0.3.1\n   Compiling xara v0.1.3 (\/home\/USERNAME\/src\/xara\/crates\/xara)\n   ...\n   Compiling xara-ui v0.1.3 (\/home\/USERNAME\/src\/xara\/crates\/xara-ui)\n    Finished `release` profile &#91;optimized] target(s) in 44.00s<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The build takes about a minute on four cores. A registered build accepts the <code>registered<\/code> option at run time, and the registered option is rejected by a build that omits the feature. The two binaries, <code>xara<\/code> and <code>xara-monitor<\/code>, land in <code>target\/release<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Installing<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The project&#8217;s installer copies the binaries, the systemd template unit, the udev rule, and the service account definition into place.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo .\/dist\/install.sh target\/release\nsudo systemd-sysusers \/usr\/lib\/sysusers.d\/xara.conf\nsudo udevadm control --reload\nid xara<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Creating group 'xara' with GID 982.\nCreating user 'xara' (xara modem) with UID 982 and GID 982.\nuid=982(xara) gid=982(xara) groups=982(xara)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The installer provides a systemd template named <code>xara@.service<\/code>. An instance named <code>gateway<\/code> reads its settings from <code>\/etc\/xara\/gateway.conf<\/code>. The template gives the service account the <code>audio<\/code>, <code>dialout<\/code>, and <code>plugdev<\/code> groups.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Configuration<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/etc\/xara\/gateway.conf\nsudo chown root:xara \/etc\/xara\/gateway.conf\nsudo chmod 640 \/etc\/xara\/gateway.conf<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># XARA: VARA FM on the shared radio\ncall = N0CALL-10\nfm = wide\n\n# registered VARA users add this line (requires the registered build)\nregistered = on\n\n# shared ALSA devices from \/etc\/asound.conf\naudio = alsa\ncapture = xara_in\nplayback = xara_out\n\nradio = on\ndrive-db = -4\n\n# PTT through the arbiter pipe, GPIO 3\nptt = cm108:\/run\/pttmux\/xara:3\nptt-lead-ms = 0\nptt-tail-ms = 100\n\n# LinBPQ uses the VARA command and data ports\nkiss-port = off\naddr = 127.0.0.1\nport = 8300<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The settings work as follows.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>call<\/code> is the callsign XARA answers to. Using the RMS gateway callsign (SSID 10) keeps the on-air identity identical in every mode.<\/li>\n\n\n\n<li><code>fm = wide<\/code> or <code>fm = narrow<\/code> selects the VARA FM mode. The HF alternative is covered in the last sections.<\/li>\n\n\n\n<li><code>registered = on<\/code> enables the registered speeds. It is a line for registered VARA users and requires the registered build.<\/li>\n\n\n\n<li><code>capture<\/code> and <code>playback<\/code> name the shared ALSA devices.<\/li>\n\n\n\n<li><code>radio = on<\/code> enables transmitting, and <code>drive-db = -4<\/code> sets the transmit drive in dB (zero or lower). The level is tuned with the audio adjustment procedure later in this article.<\/li>\n\n\n\n<li><code>ptt = cm108:\/run\/pttmux\/xara:3<\/code> keys GPIO 3 through the arbiter pipe. XARA also offers <code>rts:DEVICE<\/code>, <code>dtr:DEVICE<\/code>, <code>vox<\/code>, and <code>rigctld:HOST:PORT<\/code> for other kinds of interfaces, and the section on other PTT interfaces covers the serial and VOX cases.<\/li>\n\n\n\n<li><code>kiss-port = off<\/code> turns XARA&#8217;s own KISS port off, because LinBPQ uses the VARA ports.<\/li>\n\n\n\n<li><code>port = 8300<\/code> sets the command port, and the data port is always the next number, 8301.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The service template needs two ordering additions, kept in a drop-in file. The first makes XARA depend on the PTT arbiter and start after Dire Wolf. The second lifts the start limit, so systemd keeps retrying if the sound interface appears late at boot.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo mkdir -p \/etc\/systemd\/system\/xara@.service.d\nsudo nano \/etc\/systemd\/system\/xara@.service.d\/override.conf<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Unit]\nRequires=pttmux.service\nAfter=pttmux.service direwolf-packet.service\nStartLimitIntervalSec=0<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl enable --now xara@gateway\nsudo journalctl -u xara@gateway -b --no-pager -o cat | head -10<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Started xara@gateway.service - xara VARA-compatible modem (gateway).\nxara: config \/etc\/xara\/gateway.conf\nxara: alsa playback \"xara_out\": S16_LE, 1 channel(s), 48000 Hz, period 480, buffer 7680 frames\nxara: alsa capture \"xara_in\": S16_LE, 1 channel(s), 48000 Hz, period 480, buffer 4800 frames\nxara: settings in \/var\/lib\/xara\/gateway\/settings\nxara: audio: TX path 40 ms = capture 0 + backlog 0 + lead 0 + keying 0 + queue 40 + interp 0.2\nxara 0.1.3: N0CALL-10 FM Wide, cmd 127.0.0.1:8300 data 127.0.0.1:8301, xara_in -&gt; xara_out\nxara: warning: \/run\/pttmux\/xara is not a hidraw device; CM108 PTT may not work\nxara: RADIO MODE: transmitting into \"xara_out\", PTT CM108 GPIO3 on \/run\/pttmux\/xara, lead 0 ms, tail 100ms, watchdog 30s\n     0.000   expect &#91;0] x1<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>N0CALL-10 FM Wide<\/code> banner shows the mode and ports, and the <code>RADIO MODE<\/code> line confirms the PTT path. The hidraw warning is informational, because the pipe stands in for the hidraw device by design.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Checking registration<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">XARA answers the VARA command protocol on port 8300. A registered build started with the registered option reports <code>REGISTERED<\/code> after a <code>MYCALL<\/code> command. The check is run before LinBPQ is connected, because XARA serves its command port to one program at a time.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python3 - &lt;&lt;'EOF'\nimport socket, time\ns = socket.create_connection((\"127.0.0.1\", 8300), timeout=3)\ns.settimeout(1.0)\ns.sendall(b\"MYCALL N0CALL-10\\r\")\nt0, buf = time.time(), b\"\"\nwhile time.time() - t0 &lt; 8:\n    try:\n        data = s.recv(4096)\n        if not data:\n            break\n        buf += data\n    except socket.timeout:\n        pass\nprint(buf.decode().replace(\"\\r\", \"\\n\").strip())\nEOF<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>BUSY ON\nOK\nBUSY OFF\nREGISTERED N0CALL-10\nENCRYPTION DISABLED<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>REGISTERED N0CALL-10<\/code> line confirms the registered mode. The <code>BUSY ON<\/code> and <code>BUSY OFF<\/code> lines show XARA&#8217;s busy-channel detector reacting to activity on the audio input.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">LinBPQ: The Winlink Gateway and Packet Node<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Installing<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">LinBPQ is a single 64-bit binary plus a set of web pages. Both are downloaded from the author&#8217;s site and placed in <code>\/opt\/linbpq<\/code>, which belongs to the <code>linbpq<\/code> account created earlier.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo install -d -o linbpq -g linbpq -m 0750 \/opt\/linbpq \/opt\/linbpq\/HTML\ncd ~\ncurl -fLO https:\/\/www.cantab.net\/users\/john.wiseman\/Downloads\/Beta\/linbpq64\ncurl -fLO https:\/\/www.cantab.net\/users\/john.wiseman\/Downloads\/Beta\/HTMLPages.zip\nsha256sum linbpq64 HTMLPages.zip<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>2f4f9326d84ea4cdae6ae8594baa071af83a2767b293cccc56d362061eb0dd08  linbpq64\n773f997fe3255098aed7518d53d94f62a1a7354e7485dbf25bcfb36982224e2b  HTMLPages.zip<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The checksums above belong to the downloads used for the reference build (LinBPQ 6.0.26.1). The author publishes updated binaries over time, so the values change with each release. Recording the values of the installed copy gives a reference for later comparison.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo install -o linbpq -g linbpq -m 0750 linbpq64 \/opt\/linbpq\/linbpq\nsudo unzip -o -q HTMLPages.zip -d \/opt\/linbpq\/HTML\nsudo chown -R linbpq:linbpq \/opt\/linbpq<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The binary runs on a default Ubuntu Server installation, because the libraries it needs (zlib, D-Bus, and systemd) are part of the base system. When the standard output is not a terminal, LinBPQ disables its console and runs in the foreground, which is the mode systemd uses.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The configuration file<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">LinBPQ reads <code>bpq32.cfg<\/code> from its working directory. The file holds the Winlink gateway password, so it is created readable by the <code>linbpq<\/code> account only.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/opt\/linbpq\/bpq32.cfg\nsudo chown linbpq:linbpq \/opt\/linbpq\/bpq32.cfg\nsudo chmod 600 \/opt\/linbpq\/bpq32.cfg<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The file is assembled from the blocks below, in this order: node identity, the APRS block (covered in its own section), the four ports, and the application line. The first block sets the identity of the node.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>; N0CALL-10 Winlink RMS gateway and N0CALL-7 packet node\n;  Port 1  Telnet, web administration, and Winlink CMS access\n;  Port 2  1200 baud packet  (Dire Wolf channel 0 = KISS channel A)\n;  Port 3  9600 baud packet  (Dire Wolf channel 1 = KISS channel B)\n;  Port 4  VARA FM           (XARA command port 8300, data port 8301)\n\nSIMPLE\nNODECALL=N0CALL-7\nNODEALIAS=ALIAS\nLOCATOR=AA00aa\nMAPCOMMENT=BPQ32 NODE BY N0CALL, CITY STATE 145.050\nPASSWORD=SYSOP_PASSWORD\nIDINTERVAL=0\nBTINTERVAL=0\n\nIDMSG:\n145.050 MHz N0CALL-7 BPQ NODE. RMS N0CALL-10\n***\n\nINFOMSG:\nWelcome to the N0CALL packet\/VARA FM node, CITY STATE.  AA00aa.\n\nWinlink RMS N0CALL-10 is available on 145.050 MHz over 1200 and 9600 baud\npacket and VARA FM Wide.\n***\n\nCTEXT:\nWelcome to the N0CALL-7 BPQ Node.\nType ? for help.\nALIAS:N0CALL} CONNECT BYE INFO NODES ROUTES PORTS RMS USERS MHEARD\n***\n\nFULL_CTEXT=1\nMAXLINKS=100\nMAXNODES=300\nMAXCIRCUITS=150\nIDLETIME=1200\nENABLE_LINKED=A<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>SIMPLE<\/code> applies a set of sensible defaults for the node.<\/li>\n\n\n\n<li><code>NODECALL<\/code> is the callsign of the node. <code>NODEALIAS<\/code> is a short name of up to six characters.<\/li>\n\n\n\n<li><code>LOCATOR<\/code> is the grid square. It also enables position reporting to the BPQ32 node map, and <code>MAPCOMMENT<\/code> supplies the text shown there.<\/li>\n\n\n\n<li><code>PASSWORD<\/code> sets the node sysop password.<\/li>\n\n\n\n<li><code>IDINTERVAL=0<\/code> and <code>BTINTERVAL=0<\/code> turn off the periodic ID and beacon transmissions of the node itself. The APRS beacon is separate and has its own interval.<\/li>\n\n\n\n<li><code>IDMSG:<\/code>, <code>INFOMSG:<\/code>, and <code>CTEXT:<\/code> hold the text blocks users see. Each block ends with <code>***<\/code>.<\/li>\n\n\n\n<li><code>MAXLINKS<\/code>, <code>MAXNODES<\/code>, <code>MAXCIRCUITS<\/code>, and <code>IDLETIME<\/code> set the capacity and idle timeout of the node. <code>ENABLE_LINKED=A<\/code> restricts the <code>*** LINKED<\/code> command to application programs.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The first port carries Telnet, the web interface, and the connection to the Winlink CMS.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PORT\n PORTNUM=1\n ID=Telnet\n DRIVER=TELNET\n QUALITY=0\n CONFIG\n LOGGING=1\n CMS=1\n CMSCALL=N0CALL\n CMSPASS=GATEWAY_PASSWORD\n CloseOnDisconnect=1\n HTTPPORT=8080\n TCPPORT=8010\n FBBPORT=8011\n MAXSESSIONS=15\n USER=SYSOP_USER,SYSOP_PASSWORD,N0CALL,\"\",SYSOP\n\nENDPORT<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>CMS=1<\/code> turns the port into the gateway connection to the Winlink CMS.<\/li>\n\n\n\n<li><code>CMSCALL<\/code> is the callsign registered with Winlink, normally the base callsign. <code>CMSPASS<\/code> is the Winlink gateway password. Together they form the secure login to the CMS.<\/li>\n\n\n\n<li><code>HTTPPORT<\/code>, <code>TCPPORT<\/code>, and <code>FBBPORT<\/code> are the web interface (8080), the Telnet port (8010), and a monitoring port for BPQTermTCP (8011).<\/li>\n\n\n\n<li><code>USER<\/code> defines a login. The fields are the user name, the password, the callsign, an optional application, and <code>SYSOP<\/code> to grant sysop rights.<\/li>\n\n\n\n<li><code>QUALITY=0<\/code> keeps routing traffic off this port.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The two packet ports connect to the same Dire Wolf KISS port. LinBPQ chains ports that share an address and TCP port onto one connection and routes the frames by KISS channel.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PORT\n PORTNUM=2\n ID=145.050 MHz 1200 bps\n TYPE=ASYNC\n PROTOCOL=KISS\n IPADDR=127.0.0.1\n TCPPORT=8001\n CHANNEL=A\n PACLEN=80\n TXDELAY=500\n TXTAIL=300\n SLOTTIME=100\n PERSIST=63\n FULLDUP=0\n WL2KREPORT PUBLIC, api.winlink.org, 80, N0CALL-10, AA00aa, 00-23, 145050000, PKT1200, 50, 30, 11, 0\nENDPORT\n\nPORT\n PORTNUM=3\n ID=145.050 MHz 9600 bps\n TYPE=ASYNC\n PROTOCOL=KISS\n IPADDR=127.0.0.1\n TCPPORT=8001\n CHANNEL=B\n PACLEN=80\n TXDELAY=300\n TXTAIL=300\n SLOTTIME=100\n PERSIST=63\n FULLDUP=0\n WL2KREPORT PUBLIC, api.winlink.org, 80, N0CALL-10, AA00aa, 00-23, 145050000, PKT9600, 50, 30, 11, 0\nENDPORT<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>TYPE=ASYNC<\/code>, <code>PROTOCOL=KISS<\/code>, <code>IPADDR<\/code>, and <code>TCPPORT<\/code> describe a KISS-over-TCP link to Dire Wolf.<\/li>\n\n\n\n<li><code>CHANNEL=A<\/code> selects Dire Wolf radio channel 0 (1200 baud), and <code>CHANNEL=B<\/code> selects radio channel 1 (9600 baud).<\/li>\n\n\n\n<li><code>PACLEN=80<\/code> sets the maximum packet length.<\/li>\n\n\n\n<li><code>TXDELAY<\/code> and <code>TXTAIL<\/code> are in milliseconds here, and they match the values in the Dire Wolf configuration (which uses units of 10 ms). The 300 ms tail exceeds the 200 ms audio buffer so that the last frame of a transmission plays out completely.<\/li>\n\n\n\n<li><code>SLOTTIME<\/code> and <code>PERSIST<\/code> are the CSMA parameters, and <code>FULLDUP=0<\/code> selects half duplex.<\/li>\n\n\n\n<li><code>WL2KREPORT<\/code> publishes the channel to the Winlink channel list. Its fields are covered below.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The VARA FM port connects to XARA.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PORT\n PORTNUM=4\n ID=145.050 MHz VARA FM Wide\n DRIVER=VARA\n CONFIG\n ADDR 127.0.0.1 8300\n FM9600\n WL2KREPORT PUBLIC, api.winlink.org, 80, N0CALL-10, AA00aa, 00-23, 145050000, VARAFM96, 50, 30, 11, 0\nENDPORT\n\nAPPLICATION 1,RMS,C 1 CMS,N0CALL-10,GBRMS<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>DRIVER=VARA<\/code> loads the VARA driver. <code>ADDR 127.0.0.1 8300<\/code> names the XARA command port, and the driver uses the next port number, 8301, for data.<\/li>\n\n\n\n<li><code>FM9600<\/code> marks the port as VARA FM wide for the Winlink channel list. <code>FM1200<\/code> marks VARA FM narrow.<\/li>\n\n\n\n<li><code>APPLICATION 1,RMS,C 1 CMS,N0CALL-10,GBRMS<\/code> creates the RMS application. When a station connects to <code>N0CALL-10<\/code> (or the alias <code>GBRMS<\/code>) on any port, LinBPQ runs <code>C 1 CMS<\/code>, which connects through port 1 to the Winlink CMS. The <code>1<\/code> in the command is the number of the Telnet port.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">The Winlink channel report<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each <code>WL2KREPORT<\/code> line adds one channel to the Winlink gateway list. LinBPQ reports shortly after it starts and then every two hours. The fields appear in this order.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Field<\/th><th>Example<\/th><th>Meaning<\/th><\/tr><\/thead><tbody><tr><td>Service<\/td><td>PUBLIC<\/td><td>the service code; PUBLIC is the default<\/td><\/tr><tr><td>Host, Port<\/td><td>api.winlink.org, 80<\/td><td>the Winlink reporting server<\/td><\/tr><tr><td>Callsign<\/td><td>N0CALL-10<\/td><td>the gateway callsign with SSID<\/td><\/tr><tr><td>Locator<\/td><td>AA00aa<\/td><td>the grid square<\/td><\/tr><tr><td>Hours<\/td><td>00-23<\/td><td>the hours of operation; 00-23 means continuous<\/td><\/tr><tr><td>Frequency<\/td><td>145050000<\/td><td>the center frequency in Hz<\/td><\/tr><tr><td>Mode<\/td><td>PKT1200<\/td><td>the mode code, listed below<\/td><\/tr><tr><td>Power<\/td><td>50<\/td><td>transmitter power in watts<\/td><\/tr><tr><td>Antenna height<\/td><td>30<\/td><td>in feet<\/td><\/tr><tr><td>Antenna gain<\/td><td>11<\/td><td>in dB<\/td><\/tr><tr><td>Antenna direction<\/td><td>0<\/td><td>in degrees; 0 means omnidirectional<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The mode codes used in this build are <code>PKT1200<\/code>, <code>PKT9600<\/code>, and <code>VARAFM96<\/code> (VARA FM wide). The complete mode table, including the other VARA modes, appears in the section on changing bands and modes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The service<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/etc\/systemd\/system\/linbpq.service<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;Unit]\nDescription=LinBPQ packet node and Winlink RMS gateway\nAfter=network-online.target direwolf-packet.service xara@gateway.service\nWants=network-online.target direwolf-packet.service xara@gateway.service\n\n&#91;Service]\nType=simple\nUser=linbpq\nGroup=linbpq\nWorkingDirectory=\/opt\/linbpq\nExecStart=\/opt\/linbpq\/linbpq\nRestart=on-failure\nRestartSec=15\n\nNoNewPrivileges=yes\nProtectSystem=strict\nReadWritePaths=\/opt\/linbpq\nProtectHome=yes\nPrivateTmp=yes\nPrivateDevices=yes\nProtectKernelTunables=yes\nProtectKernelModules=yes\nProtectControlGroups=yes\nRestrictNamespaces=yes\nRestrictSUIDSGID=yes\nLockPersonality=yes\nCapabilityBoundingSet=\nUMask=0077\n\n&#91;Install]\nWantedBy=multi-user.target<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>After<\/code> and <code>Wants<\/code> lines start LinBPQ after both modems. LinBPQ reconnects by itself whenever a modem restarts.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl enable --now linbpq\nsudo journalctl -u linbpq -b --no-pager -o cat | grep -E 'Initialising|Connected to VARA'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Initialising Port 01     Telnet Server\nInitialising Port 02     TCPKISS IP 127.0.0.1 Port 8001 Chan A\nInitialising Port 03     TCPKISS IP 127.0.0.1 Port 8001 Chan B\nInitialising Port 04     VARA Host 127.0.0.1 8300\nConnected to VARA TNC Port 4<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dire Wolf logs the matching connection.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u direwolf-packet -b --no-pager -o cat | grep -E 'Attached|KISS protocol set TX'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Attached to KISS TCP client application 0 on port 8001 ...\nKISS protocol set TXDELAY = 50 (*10mS units = 500 mS), chan 0\nKISS protocol set TXtail = 30 (*10mS units = 300 mS), chan 0\nKISS protocol set TXDELAY = 30 (*10mS units = 300 mS), chan 1\nKISS protocol set TXtail = 30 (*10mS units = 300 mS), chan 1<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>KISS protocol set<\/code> lines show LinBPQ delivering its timing values to Dire Wolf. They appear after a delay.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Testing the connection to the Winlink CMS<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The LinBPQ web interface listens on port 8080. An SSH tunnel from the administrator&#8217;s computer brings it to a local address.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh -L 8080:127.0.0.1:8080 USERNAME@GATEWAY<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The page <code>http:\/\/127.0.0.1:8080<\/code> then opens in a browser on that computer. According to the LinBPQ documentation, connections from the local machine log in automatically, and the &#8220;SYSOP Signin&#8221; link accepts the <code>USER<\/code> credentials from the configuration file. On the &#8220;Terminal&#8221; page, the command <code>RMS<\/code> opens a connection to the CMS, and the command <code>B<\/code> closes it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The same test runs from a shell on the gateway with a Telnet client.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y telnet\ntelnet 127.0.0.1 8010<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Trying 127.0.0.1...\nConnected to 127.0.0.1.\nEscape character is '^]'.\nuser:SYSOP_USER\npassword:SYSOP_PASSWORD\nConnected to N0CALL-7's Telnet Server\nRMS\nTrying ec2-52-21-40-82.compute-1.amazonaws.com\n*** N0CALL Connected to CMS\n&#91;WL2K-5.0-B2FWIHJM$]\n;PQ: 86860125\nCMS via N0CALL &gt;\nB\nDisconnecting...<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The lines <code>*** N0CALL Connected to CMS<\/code> and <code>[WL2K-5.0-B2FWIHJM$]<\/code> confirm that the gateway callsign and password are accepted by Winlink. The <code>;PQ:<\/code> line is the secure-login challenge.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">APRS Digipeater and iGate<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">APRS runs inside LinBPQ. The <code>APRSDIGI<\/code> block of <code>bpq32.cfg<\/code> configures it, and no separate program is involved. The optional BPQAPRS map and messaging client is a separate application, and the <code>RUN BPQAPRS.exe<\/code> line found in Windows configurations belongs to that client. The block is placed above the first <code>PORT<\/code> line.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>APRSDIGI\n   APRSCALL N0CALL-5\n   APRSPATH 2=APRS,WIDE1-1,WIDE2-1\n   DIGICALLS=N0CALL\n   STATUSMSG=145.050 MHz N0CALL-7 BPQ\/APRS NODE. RMS N0CALL-10, APRS 5\n   SYMBOL=&amp;\n   SYMSET=\\\n   BeaconInterval=60\n   TraceCalls=WIDE,TRACE\n   FloodCalls=USA\n   MAXTRACEHOPS=2\n   MAXFLOODHOPS=2\n   LAT=DDMM.mmN\n   LON=DDDMM.mmW\n   ISHOST=noam.aprs2.net\n   ISPORT=14580\n   ISPASSCODE=APRS_PASSCODE\n   ISFILTER=m\/50\n***<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>APRSCALL<\/code> is the callsign of the APRS station (the iGate).<\/li>\n\n\n\n<li><code>APRSPATH 2=APRS,WIDE1-1,WIDE2-1<\/code> selects LinBPQ port 2, the 1200 baud port, as the APRS RF port and sets the destination and digipeater path of its beacons. Only ports that have an <code>APRSPATH<\/code> line take part in APRS.<\/li>\n\n\n\n<li><code>DIGICALLS<\/code>, <code>TraceCalls<\/code>, and <code>FloodCalls<\/code> define which path entries the digipeater answers. <code>MAXTRACEHOPS<\/code> and <code>MAXFLOODHOPS<\/code> cap the hop count it accepts.<\/li>\n\n\n\n<li><code>STATUSMSG<\/code> is the text of the status beacon. <code>SYMBOL=&amp;<\/code> with <code>SYMSET=\\<\/code> selects the gateway icon.<\/li>\n\n\n\n<li><code>BeaconInterval=60<\/code> sends the position beacon every 60 minutes.<\/li>\n\n\n\n<li><code>LAT<\/code> and <code>LON<\/code> give the position in APRS format: degrees and decimal minutes, for example <code>4015.50N<\/code> and <code>07500.25W<\/code>.<\/li>\n\n\n\n<li><code>ISHOST<\/code>, <code>ISPORT<\/code>, and <code>ISPASSCODE<\/code> define the connection to the APRS-IS network. Port 14580 is the filtered feed, and the passcode comes from the callsign.<\/li>\n\n\n\n<li><code>ISFILTER=m\/50<\/code> asks the server for stations within 50 km. LinBPQ sends this filter at login when the BPQAPRS client is not running, and the value is adjusted to the coverage wanted.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">LinBPQ reads the APRS block when it starts.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl restart linbpq\nsudo journalctl -u linbpq -b --no-pager -o cat | grep APRS<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Using Shared Memory \/BPQAPRSSharedMem\nMap APRS Shared Memory Allocated at 0x43000000\nAPRS Digi\/Gateway Enabled<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The connection to APRS-IS appears in the socket list.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ss -tnp | grep 14580<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>ESTAB 0      0        192.168.1.50:57784 192.104.234.219:14580 users:((\"linbpq\",pid=13436,fd=10))<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first beacon goes out on the radio shortly after the start. A neighboring digipeater that repeats it returns a copy with its own callsign added to the path, which confirms the RF path in both directions.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u direwolf-packet -b --no-pager -o cat | grep -E 'N0CALL-5'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;0L] N0CALL-5&gt;APBPQ1,WIDE1-1,WIDE2-1:!DDMM.mmN\\DDDMM.mmW&amp;145.050 MHz N0CALL-7 BPQ\/APRS NODE. RMS N0CALL-10, APRS 5\n&#91;0.3] N0CALL-5&gt;APBPQ1,W1ABC-6,WIDE1*,WIDE2-1:!DDMM.mmN\\DDDMM.mmW&amp;145.050 MHz N0CALL-7 BPQ\/APRS NODE. RMS N0CALL-10, APRS 5<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The station also appears on <a href=\"https:\/\/aprs.fi\/info\/a\/N0CALL-5\">https:\/\/aprs.fi\/info\/a\/N0CALL-5<\/a> after its first beacon reaches APRS-IS.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Firewall<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dire Wolf, LinBPQ, and XARA open TCP ports for each other. Dire Wolf and LinBPQ listen on all addresses, so the firewall limits inbound traffic to SSH, and the web interface is reached through the SSH tunnel shown above.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ufw default deny incoming\nsudo ufw default allow outgoing\nsudo ufw allow 22\/tcp comment ssh\nsudo ufw --force enable\nsudo ufw status<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Firewall is active and enabled on system startup\nStatus: active\n\nTo                         Action      From\n--                         ------      ----\n22\/tcp                     ALLOW       Anywhere                   # ssh\n22\/tcp (v6)                ALLOW       Anywhere (v6)              # ssh<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The ports in use are:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Port<\/th><th>Program<\/th><th>Purpose<\/th><th>Reachable from<\/th><\/tr><\/thead><tbody><tr><td>22<\/td><td>sshd<\/td><td>administration<\/td><td>the network<\/td><\/tr><tr><td>8001<\/td><td>Dire Wolf<\/td><td>KISS for LinBPQ<\/td><td>the gateway itself<\/td><\/tr><tr><td>8010, 8011, 8080<\/td><td>LinBPQ<\/td><td>Telnet, monitoring, web interface<\/td><td>the gateway itself, or an SSH tunnel<\/td><\/tr><tr><td>8300, 8301<\/td><td>XARA<\/td><td>VARA command and data<\/td><td>the gateway itself (bound to 127.0.0.1)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The firewall leaves outbound connections open, which covers the Winlink servers and APRS-IS.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Starting Everything at Boot<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every piece of the station is a systemd service, and each one is enabled so that the whole chain starts by itself.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl enable pttmux direwolf-packet xara@gateway linbpq ufw<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The services start in a fixed order, which systemd builds from the <code>Requires<\/code>, <code>Wants<\/code>, and <code>After<\/code> lines written earlier.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Service<\/th><th>Starts after<\/th><th>Relationship<\/th><\/tr><\/thead><tbody><tr><td>pttmux<\/td><td>sound.target<\/td><td>creates the PTT pipes in \/run\/pttmux<\/td><\/tr><tr><td>direwolf-packet<\/td><td>pttmux<\/td><td>requires pttmux<\/td><\/tr><tr><td>xara@gateway<\/td><td>pttmux, direwolf-packet<\/td><td>requires pttmux; the start limit is lifted, so systemd keeps retrying<\/td><\/tr><tr><td>linbpq<\/td><td>network-online, direwolf-packet, xara@gateway<\/td><td>wants both modems and reconnects whenever either one restarts<\/td><\/tr><tr><td>ufw<\/td><td>early boot<\/td><td>applies the firewall rules<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The remaining pieces persist by themselves. The ALSA configuration in <code>\/etc\/asound.conf<\/code> and the udev rule load at boot, the mixer state saved with <code>alsactl store<\/code> is restored by the ALSA restore service when the sound card appears, and the XARA, Dire Wolf, and LinBPQ configuration files stay in place.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A virtual machine returns after a host restart when its &#8220;Start at boot&#8221; option is enabled in the hypervisor, and the USB interface follows the machine when it is attached by vendor and device ID.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Verifying a reboot<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A full reboot confirms the whole chain.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl reboot<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">After the machine returns, the services, the boot time, and any failed units are checked.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl is-active pttmux direwolf-packet xara@gateway linbpq ufw\nsystemd-analyze\nsystemctl --failed --no-legend<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>active\nactive\nactive\nactive\nactive\nStartup finished in 1.042s (kernel) + 1.725s (initrd) + 3.131s (userspace) = 5.899s<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The empty output of <code>systemctl --failed<\/code> means that no unit failed. The interface, the PTT name, and the saved mixer level are checked next.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>lsusb | grep -i c-media\nls -l \/dev\/uri125-ptt\namixer -c 0 sget Speaker | grep Front<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>Bus 002 Device 002: ID 0d8c:0012 C-Media Electronics, Inc. USB Audio Device\nlrwxrwxrwx 1 root root 7 Nov  4 13:35 \/dev\/uri125-ptt -&gt; hidraw1\n  Front Left: Playback 21 &#91;57%] &#91;-16.00dB] &#91;on]\n  Front Right: Playback 21 &#91;57%] &#91;-16.00dB] &#91;on]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The modem and gateway logs show the connections made by themselves during start-up.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u xara@gateway -b --no-pager -o cat | grep -E 'Mycall|Listen|REGISTERED'\nsudo journalctl -u linbpq -b --no-pager -o cat | grep -E 'Connected to VARA|APRS Digi'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>     3.930 &gt; Mycall(Ok(&#91;\"N0CALL-7\", \"N0CALL-10\"]))\n     3.930 &gt; Listen(true)\n     8.930 &lt; REGISTERED N0CALL-7 N0CALL-10\nConnected to VARA TNC Port 4\nAPRS Digi\/Gateway Enabled<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>Mycall<\/code> line shows LinBPQ handing both of its callsigns to XARA, and the <code>REGISTERED<\/code> line shows XARA reporting the registered mode for both. On the reference station all five services were active five seconds into boot, and the first APRS beacon followed about half a minute after the LinBPQ service started.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing the Gateway<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Receive<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Received packets from other stations on the channel appear in the Dire Wolf log as soon as the station is running. A decoded APRS beacon from a neighboring station, as shown earlier, proves the receive chain from the antenna to the decoder.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Transmit<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Test transmissions go through the same path as real sessions: the modem, the shared mixer, the PTT arbiter, the interface, and the radio. A small script sends identified UI frames through Dire Wolf&#8217;s KISS port. The source address of every frame is the station callsign, so every transmission carries identification.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>nano ~\/kisstest.py<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/env python3\n\"\"\"Send identified AX.25 UI test frames to Dire Wolf through its KISS TCP port.\n\nusage: kisstest.py CALLSIGN-SSID CHANNEL &#91;BURSTS] &#91;FRAMES_PER_BURST]\n\n  CHANNEL            0 = 1200 baud, 1 = 9600 baud\n  BURSTS             number of transmissions, 6 seconds apart (default 3)\n  FRAMES_PER_BURST   frames queued together in each transmission (default 3)\n\nEvery frame carries CALLSIGN-SSID as its source address, so each transmission\nis identified.\n\"\"\"\nimport socket\nimport sys\nimport time\n\nFEND, FESC, TFEND, TFESC = 0xC0, 0xDB, 0xDC, 0xDD\n\n\ndef address(call, ssid, last):\n    field = bytes((ord(c) &lt;&lt; 1) for c in call.ljust(6))\n    return field + bytes(&#91;0x60 | (ssid &lt;&lt; 1) | (1 if last else 0)])\n\n\ndef ui_frame(source, text):\n    call, _, ssid = source.partition(\"-\")\n    return (address(\"TEST\", 0, False)\n            + address(call, int(ssid or 0), True)\n            + bytes(&#91;0x03, 0xF0])          # UI control, no layer 3\n            + text.encode())\n\n\ndef kiss(channel, frame):\n    out = bytearray(&#91;FEND, channel &lt;&lt; 4])\n    for b in frame:\n        if b == FEND:\n            out += bytes(&#91;FESC, TFEND])\n        elif b == FESC:\n            out += bytes(&#91;FESC, TFESC])\n        else:\n            out.append(b)\n    out.append(FEND)\n    return bytes(out)\n\n\ndef main():\n    if len(sys.argv) &lt; 3:\n        sys.exit(__doc__)\n    source, channel = sys.argv&#91;1].upper(), int(sys.argv&#91;2])\n    bursts = int(sys.argv&#91;3]) if len(sys.argv) &gt; 3 else 3\n    per_burst = int(sys.argv&#91;4]) if len(sys.argv) &gt; 4 else 3\n    pad = (\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\" * 4)&#91;:100]\n    mode = {0: \"1200 baud\", 1: \"9600 baud\"}.get(channel, f\"channel {channel}\")\n    sock = socket.create_connection((\"127.0.0.1\", 8001), timeout=5)\n    for n in range(1, bursts + 1):\n        data = b\"\".join(\n            kiss(channel, ui_frame(source, f\"{source} {mode} test burst {n}\/{bursts} frame {k}\/{per_burst} {pad}\"))\n            for k in range(1, per_burst + 1))\n        print(time.strftime(\"%H:%M:%S UTC\", time.gmtime()), f\"burst {n}\/{bursts}: {per_burst} frame(s), {mode}\", flush=True)\n        sock.sendall(data)\n        time.sleep(6)\n    sock.close()\n\n\nif __name__ == \"__main__\":\n    main()<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The arguments are the callsign, the channel (0 for 1200 baud, 1 for 9600 baud), the number of bursts, and the number of frames per burst. The second receiver tuned to the station frequency carries the audio. A burst of two or three frames lasts long enough to hear clearly on a voice receiver, and 9600 baud sounds like a short rush of noise.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>python3 ~\/kisstest.py N0CALL-10 0 3 2\npython3 ~\/kisstest.py N0CALL-10 1 3 3<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>14:12:01 UTC burst 1\/3: 2 frame(s), 1200 baud\n14:12:07 UTC burst 2\/3: 2 frame(s), 1200 baud\n14:12:13 UTC burst 3\/3: 2 frame(s), 1200 baud\n14:12:31 UTC burst 1\/3: 3 frame(s), 9600 baud\n14:12:37 UTC burst 2\/3: 3 frame(s), 9600 baud\n14:12:43 UTC burst 3\/3: 3 frame(s), 9600 baud<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The PTT arbiter log shows how long the radio was keyed for each burst. The durations vary with the frame length, the TXDELAY, and the TXTAIL.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u pttmux --since \"-3min\" --no-pager -o cat | grep -E 'keyed|released'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>dw1200 keyed\ndw1200 released after 3.1s\ndw1200 keyed\ndw1200 released after 3.1s\ndw1200 keyed\ndw1200 released after 3.1s\ndw9600 keyed\ndw9600 released after 1.1s\ndw9600 keyed\ndw9600 released after 1.1s\ndw9600 keyed\ndw9600 released after 1.1s<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The same log marks any moment when two modems key at once with an <code>OVERLAP<\/code> note.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The VARA FM path<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">LinBPQ can place a VARA connection request itself, which exercises LinBPQ, XARA, the mixer, and the radio together. A VARA port is attached first with <code>ATTACH<\/code> and then called. The commands are typed in the web interface terminal or in a Telnet session to port 8010.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ATTACH 4\nC N0CALL-9<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The XARA log shows the connect request and the PTT cycles. Closing the session ends the attempt, and XARA returns to listening.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u xara@gateway --since \"-1min\" --no-pager -o cat | grep -E 'Connect|PTT|Listen'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>   131.000 &gt; Connect { mycall: \"N0CALL\", remote: \"N0CALL-9\" }\n   131.010 &lt; PTT ON\n   132.230 &lt; PTT OFF\n   134.910 &lt; PTT ON\n   136.130 &lt; PTT OFF\n   140.340 &gt; Disconnect\n   142.700 &gt; Listen(true)<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">A client session<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A real client confirms everything at once. Winlink Express, WoAD, and similar programs connect to the RMS gateway callsign, in any of the modes, as they would to any other RMS station. The Dire Wolf log records every frame. The abridged trace below comes from a packet session in which the client delivered one message.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u direwolf-packet -f -o cat | grep -v 'audio level'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;0.2] W1ABC&gt;N0CALL-10:(SABM cmd, p=1)\n&#91;0L] N0CALL-10&gt;W1ABC:(UA res, f=1)\n&#91;0L] N0CALL-10&gt;W1ABC:(I cmd, n(s)=0, n(r)=0, p=1, pid=0xf0)Trying ec2-98-82-103-158.compute-1.amazonaws.com&lt;0x0d&gt;\n&#91;0.2] W1ABC&gt;N0CALL-10:(RR res, n(r)=1, f=1)\n&#91;0L] N0CALL-10&gt;W1ABC:(I cmd, n(s)=1, n(r)=0, p=0, pid=0xf0)*** N0CALL Connected to CMS&lt;0x0d&gt;\n&#91;0L] N0CALL-10&gt;W1ABC:(I cmd, n(s)=2, n(r)=0, p=0, pid=0xf0)&#91;WL2K-5.0-B2FWIHJM$]&lt;0x0d&gt;\n&#91;0L] N0CALL-10&gt;W1ABC:(I cmd, n(s)=3, n(r)=0, p=0, pid=0xf0);PQ: 69988343&lt;0x0d&gt;\n&#91;0L] N0CALL-10&gt;W1ABC:(I cmd, n(s)=4, n(r)=0, p=1, pid=0xf0)CMS via N0CALL &gt;&lt;0x0d&gt;\n&#91;0.2] W1ABC&gt;N0CALL-10:(I cmd, n(s)=0, n(r)=5, p=0, pid=0xf0);FW: W1ABC&lt;0x0d&gt;&#91;WoAD-1.5.22-B2FHM$]&lt;0x0d&gt;;PR: 35146639&lt;0x0d&gt;; WL2K DE W1ABC (AA00aa)&lt;0x0d&gt;\n&#91;0.2] W1ABC&gt;N0CALL-10:(I cmd, n(s)=1, n(r)=5, p=0, pid=0xf0)FC EM C2AILA3HBI2P 299 256 0&lt;0x0d&gt;F&gt; B3&lt;0x0d&gt;\n&#91;0L] N0CALL-10&gt;W1ABC:(I cmd, n(s)=5, n(r)=2, p=1, pid=0xf0)FS Y&lt;0x0d&gt;\n&#91;0.2] W1ABC&gt;N0CALL-10:(I cmd, n(s)=2, n(r)=6, p=0, pid=0xf0)... message data ...\n&#91;0L] N0CALL-10&gt;W1ABC:(I cmd, n(s)=6, n(r)=4, p=1, pid=0xf0)FF&lt;0x0d&gt;\n&#91;0.2] W1ABC&gt;N0CALL-10:(I cmd, n(s)=4, n(r)=7, p=0, pid=0xf0)FQ&lt;0x0d&gt;\n&#91;0.2] W1ABC&gt;N0CALL-10:(DISC cmd, p=1)\n&#91;0L] N0CALL-10&gt;W1ABC:(UA res, f=1)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The trace reads as follows.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>SABM<\/code> and <code>UA<\/code> open the AX.25 connection to <code>N0CALL-10<\/code>.<\/li>\n\n\n\n<li>The <code>Trying ...<\/code> and <code>*** N0CALL Connected to CMS<\/code> lines show LinBPQ opening the secure connection to the Winlink CMS. <code>[WL2K-...]<\/code> and <code>;PQ:<\/code> are the CMS greeting and login challenge.<\/li>\n\n\n\n<li>The client answers with its own greeting (<code>;FW:<\/code>, <code>[WoAD-...]<\/code>, <code>;PR:<\/code>).<\/li>\n\n\n\n<li><code>FC EM ...<\/code> is the client proposing one message, and <code>FS Y<\/code> is the CMS accepting it. The binary message data follows.<\/li>\n\n\n\n<li><code>FF<\/code> from the CMS means no mail waits for the client, <code>FQ<\/code> from the client ends the exchange, and <code>DISC<\/code> with <code>UA<\/code> closes the link.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The message appearing in the recipient&#8217;s Winlink mailbox confirms the delivery from the other side.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Winlink channel list<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">LinBPQ reports the three channels shortly after it starts, and then every two hours. It does not log the result. The Winlink gateway status pages list the channels of the callsign, with the frequency, the mode, the grid square, and the power, height, and gain values from the <code>WL2KREPORT<\/code> lines.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Adjusting Transmit Audio<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The transmit level decides how strongly the radio is modulated. Too little level weakens the signal, and too much level distorts it. Three settings control the level.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The ALSA <code>Speaker<\/code> control sets the audio that reaches the radio for all three modes. Steps of 3 dB are applied with <code>amixer -c 0 sset Speaker 3dB+<\/code> or <code>3dB-<\/code>, and <code>sudo alsactl store<\/code> saves the result.<\/li>\n\n\n\n<li>The XARA <code>drive-db<\/code> value trims VARA FM relative to that level.<\/li>\n\n\n\n<li>The data port input level and the trim control of the interface set the sensitivity on the radio side.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The adjustment proceeds mode by mode. The second receiver carries the test bursts from <code>kisstest.py<\/code> and the VARA connect attempts, and a successful client session in each mode confirms the result. The reference station settled on a <code>Speaker<\/code> setting of -16 dB and a XARA <code>drive-db<\/code> of -4, and successful sessions with a client radio confirmed those values. A service monitor or a deviation meter gives a calibrated adjustment when one is available.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">XARA&#8217;s <code>drive-db<\/code> takes effect after a service restart.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl restart xara@gateway<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Changing Bands and Modes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">2 meters and 70 cm<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The same build works on either VHF\/UHF band. The frequency in the three <code>WL2KREPORT<\/code> lines, the radio, and the antenna change. The software configuration stays the same.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">VARA FM narrow<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">VARA FM offers a narrow and a wide mode. Three settings keep XARA, LinBPQ, and the Winlink channel list consistent.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Mode<\/th><th>XARA setting<\/th><th>LinBPQ VARA port line<\/th><th>WL2KREPORT mode<\/th><\/tr><\/thead><tbody><tr><td>VARA FM wide<\/td><td><code>fm = wide<\/code><\/td><td><code>FM9600<\/code><\/td><td><code>VARAFM96<\/code><\/td><\/tr><tr><td>VARA FM narrow<\/td><td><code>fm = narrow<\/code><\/td><td><code>FM1200<\/code><\/td><td><code>VARAFM<\/code> (<code>VARAFM12<\/code> is also accepted)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">After the change, XARA and LinBPQ restart.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl restart xara@gateway linbpq<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">HF<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">XARA&#8217;s HF mode works with the same programs and the same LinBPQ driver. The mode is set in <code>\/etc\/xara\/gateway.conf<\/code> by replacing the <code>fm<\/code> line with an <code>hf<\/code> line holding the bandwidth in Hz: 500, 2300, or 2750.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Mode<\/th><th>XARA setting<\/th><th>LinBPQ VARA port line<\/th><th>WL2KREPORT mode<\/th><\/tr><\/thead><tbody><tr><td>VARA HF 500<\/td><td><code>hf = 500<\/code><\/td><td><code>BW500<\/code><\/td><td><code>VARA500<\/code><\/td><\/tr><tr><td>VARA HF 2300<\/td><td><code>hf = 2300<\/code><\/td><td><code>BW2300<\/code><\/td><td><code>VARA<\/code><\/td><\/tr><tr><td>VARA HF 2750<\/td><td><code>hf = 2750<\/code><\/td><td><code>BW2750<\/code><\/td><td><code>VARA2750<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The HF station needs a few other changes.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Packet modems are a VHF\/UHF feature, so an HF-only gateway runs XARA and LinBPQ. The two KISS ports are removed from <code>bpq32.cfg<\/code>, the <code>direwolf-packet<\/code> service is dropped from the <code>Wants<\/code> and <code>After<\/code> lines of <code>linbpq.service<\/code> and from the XARA drop-in, and the VARA port moves up in the port numbering. The <code>C 1 CMS<\/code> command in the <code>APPLICATION<\/code> line keeps pointing to the Telnet port, which stays port 1.<\/li>\n\n\n\n<li>HF radios key through CAT control or a serial line more often than through a CM108 GPIO. XARA accepts <code>rts:DEVICE<\/code>, <code>dtr:DEVICE<\/code>, and <code>rigctld:HOST:PORT<\/code>, and the section on other PTT interfaces covers the serial case. Hamlib&#8217;s <code>rigctld<\/code> provides CAT control, and the stable <code>\/dev\/serial\/by-id\/<\/code> device names keep the serial port assignment fixed between boots.<\/li>\n\n\n\n<li>With XARA as the only modem, PTT can key a CM108 interface directly with <code>ptt = cm108:\/dev\/uri125-ptt:3<\/code>, and the PTT arbiter service becomes optional.<\/li>\n\n\n\n<li>The Winlink channel frequency is the center frequency of the signal in Hz. For upper sideband, the center is the dial frequency plus the audio center of the signal, usually 1500 Hz.<\/li>\n\n\n\n<li>The HF audio levels follow the same procedure as the FM levels, with a lower <code>drive-db<\/code> as the usual starting point.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Hamlib&#8217;s <code>rigctld<\/code> installs from the distribution package.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y libhamlib-utils\nrigctl -l | grep -i 'IC-7300'\nrigctld -m 3073 -r \/dev\/serial\/by-id\/usb-EXAMPLE-if00 -s 19200<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>rigctl -l<\/code> listing shows the model number of the radio, which is the number passed to <code>-m<\/code>. A <code>rigctld<\/code> systemd service keeps the daemon running at boot.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">An HF configuration for XARA follows.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>call = N0CALL-10\nhf = 2300\nregistered = on\naudio = alsa\ncapture = xara_in\nplayback = xara_out\nradio = on\ndrive-db = -10\nptt = rigctld:127.0.0.1:4532\nkiss-port = off\naddr = 127.0.0.1\nport = 8300<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The matching LinBPQ port replaces the VARA FM port.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PORT\n PORTNUM=2\n ID=HF VARA 2300\n DRIVER=VARA\n CONFIG\n ADDR 127.0.0.1 8300\n BW2300\n WL2KREPORT PUBLIC, api.winlink.org, 80, N0CALL-10, AA00aa, 00-23, CENTER_FREQUENCY_HZ, VARA, 50, 30, 11, 0\nENDPORT<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">XARA also ships <code>xara-monitor<\/code>, a passive, receive-only VARA HF monitor. It reads the same capture device and is handy for watching band activity before a gateway goes on the air.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most of the symptoms below appeared while this station was built. The last five rows list standard checks that the build did not require.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Symptom<\/th><th>Cause<\/th><th>Fix<\/th><\/tr><\/thead><tbody><tr><td><code>unable to initialize sum ring buffer<\/code> when a program opens <code>uri_playback<\/code><\/td><td>the <code>ipc_key<\/code> values of dsnoop and dmix are too close together<\/td><td>Set the keys at least 10 apart (584410 and 584420 in this build), clear stale shared-memory segments with <code>ipcs -m<\/code> and <code>sudo ipcrm -m ID<\/code>, and restart the services.<\/td><\/tr><tr><td>XARA exits with <code>the device buffers at most 50 ms, but --tx-buffer-ms 40 needs 160 ms<\/code><\/td><td>the dmix buffer is too small<\/td><td>Set <code>buffer_size 9600<\/code> in <code>uri_playback<\/code>, then restart Dire Wolf and XARA so the shared mixer is rebuilt.<\/td><\/tr><tr><td>A client receives the connect acknowledgement (<code>UA<\/code>) but the session stalls, and only the first frame of each transmission arrives<\/td><td>PTT releases before the audio buffer finishes playing, which truncates the last frame<\/td><td>Set <code>TXTAIL<\/code> to at least 300 ms in both <code>packet.conf<\/code> (value 30) and <code>bpq32.cfg<\/code> (value 300).<\/td><\/tr><tr><td>Short transmissions are inaudible on the monitor receiver, or the start of a burst is lost<\/td><td>the radio and the receivers need longer to settle<\/td><td>Raise <code>TXDELAY<\/code>: 500 ms for 1200 baud and 300 ms for 9600 baud worked on the reference station.<\/td><\/tr><tr><td>Sessions stall for the first minute or two after Dire Wolf restarts<\/td><td>Dire Wolf runs on its own defaults until LinBPQ delivers the timing values<\/td><td>Keep <code>TXDELAY<\/code> and <code>TXTAIL<\/code> in <code>packet.conf<\/code> as shown, so the correct values apply from the first second.<\/td><\/tr><tr><td>New dmix or dsnoop settings seem ignored<\/td><td>the old shared instance stays alive while any program holds the device<\/td><td>Stop Dire Wolf, XARA, and LinBPQ, clear the segments with <code>ipcrm<\/code>, and start the services again.<\/td><\/tr><tr><td>A connection to XARA&#8217;s port 8300 is refused while LinBPQ runs<\/td><td>XARA serves its command port to one program at a time, and LinBPQ holds it<\/td><td>Read the XARA log instead, or stop <code>linbpq<\/code> for the duration of a manual test.<\/td><\/tr><tr><td>The Dire Wolf log shows <code>is not a supported device type<\/code> for the PTT pipes<\/td><td>Dire Wolf checks for a real USB HID device and finds a pipe<\/td><td>These lines are informational, and the transmissions work.<\/td><\/tr><tr><td><code>Permission denied<\/code> on <code>\/dev\/snd\/*<\/code> or on a PTT pipe<\/td><td>the service account is missing from the <code>audio<\/code> group<\/td><td>Add the account with <code>sudo usermod -aG audio ACCOUNT<\/code> and restart its service.<\/td><\/tr><tr><td>A serial-PTT radio keys when it should be idle, and releases when it should be keyed<\/td><td>the interface keys on the opposite line level<\/td><td>Add <code>--invert<\/code> to the <code>pttmux<\/code> command line, restart the service, and repeat the manual key test.<\/td><\/tr><tr><td><code>Permission denied<\/code> on the serial port in the <code>pttmux<\/code> log<\/td><td>the <code>pttmux<\/code> account is missing from the <code>dialout<\/code> group<\/td><td>Run <code>sudo usermod -aG dialout pttmux<\/code>, confirm that <code>SupplementaryGroups<\/code> in the unit lists <code>dialout<\/code>, and restart the service.<\/td><\/tr><tr><td>VOX station: the radio stays keyed after the last frame, or the start of a frame is lost<\/td><td>the interface&#8217;s delay is longer than needed, or the preamble is shorter than the radio&#8217;s key-up time<\/td><td>Turn the interface delay (<code>DLY<\/code> on a SignaLink) fully counter-clockwise, and raise <code>TXDELAY<\/code> for the affected channel.<\/td><\/tr><tr><td>No frames decode<\/td><td>the receive audio is missing or too low<\/td><td>Check the radio data port speed setting, run the <code>audiotest.py<\/code> check for a non-zero RMS value, and adjust the <code>Mic<\/code> capture control.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Maintenance<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Service logs are read with <code>journalctl<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u linbpq -f\nsudo journalctl -u direwolf-packet -f\nsudo journalctl -u xara@gateway -f\nsudo journalctl -u pttmux -f<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">LinBPQ also writes its own logs under <code>\/opt\/linbpq\/logs<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A new LinBPQ release replaces the binary. The configuration file stays as it is.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl stop linbpq\nsudo install -o linbpq -g linbpq -m 0750 ~\/linbpq64 \/opt\/linbpq\/linbpq\nsudo systemctl start linbpq<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A new XARA version is built and installed the same way as the first one.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd ~\/src\/xara\ngit pull\ncargo build --release --locked -p xara --features registered\nsudo .\/dist\/install.sh target\/release\nsudo systemctl restart xara@gateway<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dire Wolf updates arrive with the regular package updates (<code>sudo apt upgrade<\/code>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One archive holds every file of the station, which makes a restore or a second build quick. The archive contains the gateway password, so it is stored somewhere private.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo tar czf ~\/gateway-config-$(date +%F).tar.gz \\\n  \/etc\/asound.conf \/etc\/direwolf \/etc\/xara \\\n  \/etc\/udev\/rules.d\/61-uri125.rules \\\n  \/etc\/systemd\/system\/pttmux.service \\\n  \/etc\/systemd\/system\/direwolf-packet.service \\\n  \/etc\/systemd\/system\/linbpq.service \\\n  \/etc\/systemd\/system\/xara@.service.d \\\n  \/usr\/local\/bin\/pttmux \/opt\/linbpq\/bpq32.cfg \/var\/lib\/alsa\/asound.state<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Finished<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The station is complete. One radio and one USB interface now serve a Winlink RMS gateway in three modes at once, 1200 baud packet, 9600 baud packet, and VARA FM, with an APRS digipeater and iGate on the same node. ALSA&#8217;s dsnoop and dmix share the audio, the PTT arbiter keeps the transmitter keyed until the last modem is finished, and every service returns by itself after a reboot.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The same layout carries over to other bands. The frequency changes for 70 cm, and the XARA mode and the LinBPQ port line change for HF.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">References<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>XARA: <a href=\"https:\/\/github.com\/x0rro\/xara\">https:\/\/github.com\/x0rro\/xara<\/a><\/li>\n\n\n\n<li>Dire Wolf: <a href=\"https:\/\/github.com\/wb2osz\/direwolf\">https:\/\/github.com\/wb2osz\/direwolf<\/a><\/li>\n\n\n\n<li>LinBPQ installation guide: <a href=\"https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/InstallingLINBPQ.html\">https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/InstallingLINBPQ.html<\/a><\/li>\n\n\n\n<li>LinBPQ Winlink RMS gateway guide: <a href=\"https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/LinBPQ_RMSGateway.html\">https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/LinBPQ_RMSGateway.html<\/a><\/li>\n\n\n\n<li>BPQ32\/LinBPQ configuration file reference: <a href=\"https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/BPQCFGFile.html\">https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/BPQCFGFile.html<\/a><\/li>\n\n\n\n<li>Winlink channel reporting (WL2KREPORT): <a href=\"https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/WL2KReporting.html\">https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/WL2KReporting.html<\/a><\/li>\n\n\n\n<li>VARA ports in BPQ32\/LinBPQ: <a href=\"https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/VARA.html\">https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/VARA.html<\/a><\/li>\n\n\n\n<li>APRS digipeater and iGate: <a href=\"https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/APRSDigiGate.html\">https:\/\/www.cantab.net\/users\/john.wiseman\/Documents\/APRSDigiGate.html<\/a><\/li>\n\n\n\n<li>Winlink: <a href=\"https:\/\/winlink.org\">https:\/\/winlink.org<\/a><\/li>\n\n\n\n<li>APRS-IS: <a href=\"http:\/\/www.aprs-is.net\/\">http:\/\/www.aprs-is.net\/<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>This article details how to build a Winlink RMS gateway and APRS digipeater\/iGate on Linux with native software only. Dire Wolf handles 1200 baud and 9600 baud packet, XARA handles VARA FM, and LinBPQ connects all three modes to the Winlink system. The setup uses no Wine. The finished station answers Winlink connections on a [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":1087,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1086","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"rttpg_featured_image_url":{"full":["https:\/\/www.we8chz.org\/wp-content\/uploads\/2026\/11\/wl2klogosm400x167_0-4264154065.png",400,167,false],"landscape":["https:\/\/www.we8chz.org\/wp-content\/uploads\/2026\/11\/wl2klogosm400x167_0-4264154065.png",400,167,false],"portraits":["https:\/\/www.we8chz.org\/wp-content\/uploads\/2026\/11\/wl2klogosm400x167_0-4264154065.png",400,167,false],"thumbnail":["https:\/\/www.we8chz.org\/wp-content\/uploads\/2026\/11\/wl2klogosm400x167_0-4264154065-150x150.png",150,150,true],"medium":["https:\/\/www.we8chz.org\/wp-content\/uploads\/2026\/11\/wl2klogosm400x167_0-4264154065-300x125.png",300,125,true],"large":["https:\/\/www.we8chz.org\/wp-content\/uploads\/2026\/11\/wl2klogosm400x167_0-4264154065.png",400,167,false],"1536x1536":["https:\/\/www.we8chz.org\/wp-content\/uploads\/2026\/11\/wl2klogosm400x167_0-4264154065.png",400,167,false],"2048x2048":["https:\/\/www.we8chz.org\/wp-content\/uploads\/2026\/11\/wl2klogosm400x167_0-4264154065.png",400,167,false]},"rttpg_author":{"display_name":"Jay NT0Y","author_link":"https:\/\/www.we8chz.org\/?author=4"},"rttpg_comment":0,"rttpg_category":"<a href=\"https:\/\/www.we8chz.org\/?cat=1\" rel=\"category\">Uncategorized<\/a>","rttpg_excerpt":"This article details how to build a Winlink RMS gateway and APRS digipeater\/iGate on Linux with native software only. Dire Wolf handles 1200 baud and 9600 baud packet, XARA handles VARA FM, and LinBPQ connects all three modes to the Winlink system. The setup uses no Wine. The finished station answers Winlink connections on a&hellip;","_links":{"self":[{"href":"https:\/\/www.we8chz.org\/index.php?rest_route=\/wp\/v2\/posts\/1086","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.we8chz.org\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.we8chz.org\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.we8chz.org\/index.php?rest_route=\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.we8chz.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1086"}],"version-history":[{"count":3,"href":"https:\/\/www.we8chz.org\/index.php?rest_route=\/wp\/v2\/posts\/1086\/revisions"}],"predecessor-version":[{"id":1090,"href":"https:\/\/www.we8chz.org\/index.php?rest_route=\/wp\/v2\/posts\/1086\/revisions\/1090"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.we8chz.org\/index.php?rest_route=\/wp\/v2\/media\/1087"}],"wp:attachment":[{"href":"https:\/\/www.we8chz.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1086"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.we8chz.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1086"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.we8chz.org\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1086"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}