{"id":37,"date":"2026-05-09T09:08:19","date_gmt":"2026-05-09T15:08:19","guid":{"rendered":"https:\/\/kg7okr.com\/?p=37"},"modified":"2026-05-09T09:13:55","modified_gmt":"2026-05-09T15:13:55","slug":"graywolf-aprs-igate-with-rtl-sdr-receive-only","status":"publish","type":"post","link":"https:\/\/kg7okr.com\/?p=37","title":{"rendered":"Graywolf APRS iGate with RTL-SDR (Receive-Only)"},"content":{"rendered":"\n<p>A complete guide for running <a href=\"https:\/\/github.com\/chrissnell\/graywolf\">graywolf<\/a> as a receive-only APRS iGate using an RTL-SDR dongle as the radio source, on Linux.<\/p>\n\n\n\n<p>Graywolf is designed to consume audio from a real radio via a USB sound card. It does not natively accept stdin or SDR input. This guide bridges an RTL-SDR to graywolf using an ALSA loopback device \u2014 the SDR&#8217;s demodulated audio is piped to one end of a virtual sound card, and graywolf reads from the other end.<\/p>\n\n\n\n<p><strong>Tested on:<\/strong> Debian 13 (Trixie), x86_64, with an RTL-SDR Blog V3 dongle.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What You&#8217;ll End Up With<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>An always-on receive-only APRS iGate that survives reboots<\/li>\n\n\n\n<li>Decoded APRS packets forwarded to APRS-IS under your callsign with <code>-10<\/code> SSID<\/li>\n\n\n\n<li>A web UI for monitoring and configuration<\/li>\n\n\n\n<li>Two systemd services with proper dependency ordering, no manual intervention required<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A licensed amateur radio callsign (required to run an iGate)<\/li>\n\n\n\n<li>An RTL-SDR dongle (V3 or similar)<\/li>\n\n\n\n<li>A 2m antenna \u2014 even a poor one (e.g. an ADS-B antenna) will hear strong local stations<\/li>\n\n\n\n<li>Linux box with at least one free USB port<\/li>\n\n\n\n<li><code>rtl-sdr<\/code> package and the SDR working with <code>rtl_test<\/code><\/li>\n\n\n\n<li>Internet connection for forwarding to APRS-IS<\/li>\n\n\n\n<li>Root\/sudo access<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Install ALSA Utilities<\/h2>\n\n\n\n<p>The ALSA tools provide <code>aplay<\/code>, which we use to feed audio into the loopback.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install alsa-utils psmisc<\/code><\/pre>\n\n\n\n<p><code>psmisc<\/code> provides <code>fuser<\/code>, useful for verifying which processes are using audio devices.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Enable the ALSA Loopback Module<\/h2>\n\n\n\n<p>The <code>snd-aloop<\/code> kernel module creates a virtual sound card that pairs a &#8220;playback&#8221; device with a &#8220;capture&#8221; device \u2014 anything written to one appears as a recording on the other.<\/p>\n\n\n\n<p>Load it now:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo modprobe snd-aloop<\/code><\/pre>\n\n\n\n<p>Make it persistent across reboots:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>echo \"snd-aloop\" | sudo tee \/etc\/modules-load.d\/snd-aloop.conf<\/code><\/pre>\n\n\n\n<p>Verify the loopback is present:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>aplay -l | grep Loopback\narecord -l | grep Loopback<\/code><\/pre>\n\n\n\n<p>You should see <code>Loopback<\/code> listed in both. Note the card&#8217;s name (<code>Loopback<\/code>) \u2014 we use the <strong>name<\/strong> rather than the card number because card numbers can shift between reboots, but names are stable.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Set a Recognizable Serial on the SDR (Optional but Recommended)<\/h2>\n\n\n\n<p>If you have multiple RTL-SDRs, set a unique serial on the one you&#8217;ll use for APRS so it&#8217;s easy to address by name.<\/p>\n\n\n\n<p>List devices:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>rtl_eeprom -d 0\nrtl_eeprom -d 1<\/code><\/pre>\n\n\n\n<p>Set a memorable serial on your APRS dongle (e.g., <code>00144390<\/code> for the APRS frequency 144.390 MHz):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>rtl_eeprom -d 0 -s 00144390<\/code><\/pre>\n\n\n\n<p>Confirm with <code>y<\/code>. <strong>Unplug and replug the SDR<\/strong> for the new serial to take effect, then verify:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>rtl_eeprom -d 0<\/code><\/pre>\n\n\n\n<p>You can now reference this dongle by serial (<code>-d 00144390<\/code>) in commands, which is more reliable than relying on device index when you have multiple SDRs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Test the Audio Chain Manually<\/h2>\n\n\n\n<p>Before involving graywolf, verify rtl_fm can demodulate APRS audio and feed it into the loopback:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>rtl_fm -d 00144390 -f 144.390M -s 44100 - | aplay -D plughw:Loopback,0,0 -r 44100 -f S16_LE -c 1 -t raw<\/code><\/pre>\n\n\n\n<p>Key flags:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>-d 00144390<\/code> \u2014 your SDR&#8217;s serial (or use <code>-d 0<\/code> for index)<\/li>\n\n\n\n<li><code>-f 144.390M<\/code> \u2014 North America APRS frequency. Use <code>144.800M<\/code> in Europe, <code>144.640M<\/code> in Australia.<\/li>\n\n\n\n<li><code>-s 44100<\/code> \u2014 sample rate. 44100 Hz works reliably with <code>plughw<\/code>. Lower rates like 22050 are theoretically more efficient but graywolf&#8217;s UI may not list them.<\/li>\n\n\n\n<li><code>plughw:Loopback,0,0<\/code> \u2014 the loopback&#8217;s playback side, addressed by name. The <code>plughw<\/code> plugin handles automatic format\/rate negotiation, which avoids <code>Channels count non available<\/code> errors.<\/li>\n\n\n\n<li><code>-c 1<\/code> \u2014 explicit mono. Required for <code>aplay<\/code> reliability.<\/li>\n<\/ul>\n\n\n\n<p>Expected output should include lines like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Tuned to 144643575 Hz.\nSampling at 1014300 S\/s.\nOutput at 44100 Hz.<\/code><\/pre>\n\n\n\n<p>(The &#8220;Tuned to&#8221; frequency may not match exactly \u2014 that&#8217;s normal IF offset behavior in <code>rtl_fm<\/code>.)<\/p>\n\n\n\n<p>If you have actual APRS activity in your area, you can verify decoding works by piping through <code>multimon-ng<\/code> instead:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install multimon-ng\nrtl_fm -d 00144390 -f 144.390M -s 22050 -g 42 - | multimon-ng -t raw -A -a AFSK1200 -<\/code><\/pre>\n\n\n\n<p>You should see decoded APRS packets appear after a minute or two. Press Ctrl-C when satisfied.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Install Graywolf<\/h2>\n\n\n\n<p>Download the latest <code>.deb<\/code> from the <a href=\"https:\/\/github.com\/chrissnell\/graywolf\/releases\">graywolf releases page<\/a>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd \/tmp\nwget https:\/\/github.com\/chrissnell\/graywolf\/releases\/latest\/download\/graywolf_linux_amd64.deb\nsudo apt install .\/graywolf_*.deb<\/code><\/pre>\n\n\n\n<p>(Filename may vary slightly. Check the releases page for the current naming.)<\/p>\n\n\n\n<p>The installer creates a <code>graywolf<\/code> system user with <code>audio<\/code> and <code>dialout<\/code> group membership, installs binaries to <code>\/usr\/bin\/graywolf<\/code> and <code>\/usr\/bin\/graywolf-modem<\/code>, places config at <code>\/var\/lib\/graywolf\/graywolf.db<\/code>, and registers a systemd service.<\/p>\n\n\n\n<p>Verify it started:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl status graywolf<\/code><\/pre>\n\n\n\n<p>The web UI should be available at <code>http:\/\/localhost:8080<\/code> (or <code>http:\/\/&lt;your-ip&gt;:8080<\/code> from another machine).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Initial Graywolf Configuration<\/h2>\n\n\n\n<p>Open the web UI and set an admin password. The login is required before exposing the UI on a network.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo graywolf auth set-password --user admin -config \/var\/lib\/graywolf\/graywolf.db<\/code><\/pre>\n\n\n\n<p>Navigate to <strong>Station Callsign<\/strong> and enter your callsign without an SSID.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 7: Configure the Audio Device<\/h2>\n\n\n\n<p>In the web UI, go to <strong>Settings \u2192 Audio Devices<\/strong> \u2192 <strong>Detect Devices<\/strong>.<\/p>\n\n\n\n<p>Among the detected devices you&#8217;ll see entries for <code>Loopback<\/code>. Select:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Loopback<\/strong> \u2014 <em>Recommended<\/em> \u2014 <strong>Input<\/strong> \u2014 <strong>Alsa<\/strong> \u2014 <code>plughw:CARD=Loopback,DEV=1<\/code><\/p>\n<\/blockquote>\n\n\n\n<p>Why <code>DEV=1<\/code> and not <code>DEV=0<\/code>: in ALSA loopback, audio written to <code>DEV=0<\/code> appears for capture at <code>DEV=1<\/code>. We pipe rtl_fm&#8217;s output to <code>DEV=0<\/code> (playback side); graywolf reads from <code>DEV=1<\/code> (capture side).<\/p>\n\n\n\n<p>Why <code>plughw:<\/code> and not <code>hw:<\/code>: the <code>plughw<\/code> plugin handles automatic format\/sample-rate\/channel conversion. Raw <code>hw:<\/code> requires exact format matching, which is fragile.<\/p>\n\n\n\n<p>In the device dialog:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Name<\/strong>: <code>Loopback<\/code> (or whatever)<\/li>\n\n\n\n<li><strong>Device Path<\/strong>: <code>plughw:CARD=Loopback,DEV=1<\/code> (auto-filled)<\/li>\n\n\n\n<li><strong>Direction<\/strong>: Input<\/li>\n\n\n\n<li><strong>Source Type<\/strong>: Sound Card<\/li>\n\n\n\n<li><strong>Sample Rate<\/strong>: <code>44100<\/code> (must match what rtl_fm outputs)<\/li>\n<\/ul>\n\n\n\n<p>Save.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 8: Configure the Radio Channel<\/h2>\n\n\n\n<p>Go to <strong>Settings \u2192 Channels<\/strong> \u2192 <strong>Add Channel<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Name<\/strong>: <code>APRS-RX<\/code> (or anything descriptive)<\/li>\n\n\n\n<li><strong>Modem Type<\/strong>: AFSK<\/li>\n\n\n\n<li><strong>Input Device<\/strong>: Loopback (the device you just added)<\/li>\n\n\n\n<li><strong>Input Channel<\/strong>: 0 (Left\/Mono)<\/li>\n\n\n\n<li><strong>Output Device<\/strong>: None (RX only)<\/li>\n\n\n\n<li><strong>Bit Rate<\/strong>: 1200<\/li>\n\n\n\n<li><strong>Mark Freq (Hz)<\/strong>: 1200<\/li>\n\n\n\n<li><strong>Space Freq (Hz)<\/strong>: 2200<\/li>\n<\/ul>\n\n\n\n<p>These are the standard APRS AFSK1200 parameters used worldwide.<\/p>\n\n\n\n<p>Save.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 9: Configure the iGate<\/h2>\n\n\n\n<p>Go to <strong>Operations \u2192 iGate<\/strong>. The page has two tabs.<\/p>\n\n\n\n<p><strong>Connection tab:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Enable iGate<\/strong>: toggled on<\/li>\n\n\n\n<li><strong>APRS-IS Server<\/strong>: <code>noam.aprs2.net<\/code> (North America), <code>euro.aprs2.net<\/code> (Europe), <code>aunz.aprs2.net<\/code> (Oceania), or <code>rotate.aprs2.net<\/code> (auto-routing)<\/li>\n\n\n\n<li><strong>Port<\/strong>: <code>14580<\/code><\/li>\n\n\n\n<li><strong>Callsign<\/strong>: your callsign with <code>-10<\/code> SSID (e.g., <code>WB7ABC-10<\/code>). The <code>-10<\/code> SSID is the standard convention for receive-only iGates.<\/li>\n\n\n\n<li><strong>Passcode<\/strong>: generated from your callsign (without SSID). Use any APRS passcode calculator \u2014 it&#8217;s a 5-digit number derived deterministically from your callsign.<\/li>\n<\/ul>\n\n\n\n<p>Save.<\/p>\n\n\n\n<p><strong>APRS-IS Feed &amp; TX Rules tab:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>APRS-IS Server Filter<\/strong>: <code>r\/&lt;lat&gt;\/&lt;lon&gt;\/100<\/code> \u2014 replace <code>&lt;lat&gt;<\/code> and <code>&lt;lon&gt;<\/code> with your station coordinates in decimal degrees, e.g. <code>r\/41.67\/-111.87\/100<\/code> for a 100 km radius. This tells the APRS-IS server which packets to send you for the live map.<\/li>\n\n\n\n<li><strong>TX Channel<\/strong>: leave at default. Since you have no IS\u2192RF rules, this value is unused.<\/li>\n\n\n\n<li><strong>IS \u2192 RF Transmit Rules<\/strong>: leave empty. You&#8217;re receive-only, so no rules are needed. Adding any rule here would cause graywolf to attempt RF transmission, which an Rx-only station should never do.<\/li>\n<\/ul>\n\n\n\n<p>Save.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 10: Restart Graywolf to Pick Up Configuration<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl restart graywolf<\/code><\/pre>\n\n\n\n<p>While the manual <code>rtl_fm | aplay<\/code> command from Step 4 is still running in your terminal, the dashboard should now show:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>RX Ready<\/code> indicator green<\/li>\n\n\n\n<li>Audio level around -10 to -3 dBFS<\/li>\n\n\n\n<li>Packet count climbing as APRS activity is heard<\/li>\n<\/ul>\n\n\n\n<p>Verify both processes are using the loopback:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo fuser -v \/dev\/snd\/*<\/code><\/pre>\n\n\n\n<p>You should see two entries \u2014 <code>aplay<\/code> on <code>pcmC&lt;N&gt;D0p<\/code> (the playback side) and <code>graywolf-modem<\/code> on <code>pcmC&lt;N&gt;D1c<\/code> (the capture side). The card number <code>&lt;N&gt;<\/code> may vary.<\/p>\n\n\n\n<p>Check the iGate connected successfully:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u graywolf | grep -i \"aprs-is connected\"<\/code><\/pre>\n\n\n\n<p>You should see a line like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\"aprs-is connected\" component=igate server=rotate.aprs2.net:14580 callsign=WB7ABC-10<\/code><\/pre>\n\n\n\n<p>You can now confirm your station appears at <code>https:\/\/aprs.fi\/info\/a\/&lt;YOUR-CALLSIGN&gt;-10<\/code>.<\/p>\n\n\n\n<p>Stop the manual <code>rtl_fm<\/code> command (Ctrl-C in that terminal) \u2014 we&#8217;ll replace it with a systemd service next.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 11: Persistent rtl_fm via Systemd<\/h2>\n\n\n\n<p>Create a service that runs the rtl_fm pipe automatically and restarts on failure.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo tee \/etc\/systemd\/system\/rtl_fm-aprs.service &gt; \/dev\/null &lt;&lt;'EOF'\n&#91;Unit]\nDescription=rtl_fm APRS audio bridge to ALSA loopback\nAfter=sound.target network.target\nBefore=graywolf.service\nRequires=sound.target\n\n&#91;Service]\nType=simple\nExecStartPre=\/bin\/sleep 5\nExecStart=\/bin\/bash -c '\/usr\/bin\/rtl_fm -d 00144390 -f 144.390M -s 44100 - | \/usr\/bin\/aplay -D plughw:Loopback,0,0 -r 44100 -f S16_LE -c 1 -t raw'\nRestart=on-failure\nRestartSec=10\nUser=root\n\n&#91;Install]\nWantedBy=multi-user.target\nEOF<\/code><\/pre>\n\n\n\n<p>Notes on the unit:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ExecStartPre=\/bin\/sleep 5<\/code> gives the system a moment to enumerate USB and load <code>snd-aloop<\/code> before rtl_fm tries to open the SDR<\/li>\n\n\n\n<li><code>Restart=on-failure<\/code> with <code>RestartSec=10<\/code> means transient failures recover automatically<\/li>\n\n\n\n<li><code>Before=graywolf.service<\/code> works in concert with the override below to ensure graywolf starts after audio is flowing<\/li>\n\n\n\n<li>Replace <code>00144390<\/code> with your SDR serial (or <code>0<\/code> for index)<\/li>\n<\/ul>\n\n\n\n<p>Enable and start it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl enable rtl_fm-aprs\nsudo systemctl start rtl_fm-aprs\nsudo systemctl status rtl_fm-aprs<\/code><\/pre>\n\n\n\n<p>You should see both <code>rtl_fm<\/code> and <code>aplay<\/code> listed in the cgroup tree under <code>Tasks:<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 12: Make Graywolf Wait for Audio<\/h2>\n\n\n\n<p>Tell graywolf it depends on the rtl_fm service, so they start in the right order at boot.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo mkdir -p \/etc\/systemd\/system\/graywolf.service.d\nsudo tee \/etc\/systemd\/system\/graywolf.service.d\/override.conf &gt; \/dev\/null &lt;&lt;'EOF'\n&#91;Unit]\nAfter=rtl_fm-aprs.service sound.target\nRequires=rtl_fm-aprs.service\nEOF\nsudo systemctl daemon-reload<\/code><\/pre>\n\n\n\n<p>This is functionally identical to <code>sudo systemctl edit graywolf<\/code> but doesn&#8217;t drop you into an interactive editor.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 13: Reboot Test<\/h2>\n\n\n\n<p>The moment of truth.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo reboot<\/code><\/pre>\n\n\n\n<p>After the system comes back up, verify:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl status rtl_fm-aprs graywolf\nsudo fuser -v \/dev\/snd\/*<\/code><\/pre>\n\n\n\n<p>Both services should be <code>active (running)<\/code>, and <code>fuser<\/code> should show <code>aplay<\/code> and <code>graywolf-modem<\/code> on the loopback. The web UI should be accessible and packets should be flowing.<\/p>\n\n\n\n<p>A single <code>underrun!!!<\/code> warning at startup is normal and harmless \u2014 it&#8217;s a sub-millisecond audio buffer hiccup that doesn&#8217;t affect packet decoding.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verification &amp; Troubleshooting<\/h2>\n\n\n\n<p><strong>No packets being received<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Confirm rtl_fm is tuning: <code>sudo journalctl -u rtl_fm-aprs | grep \"Tuned to\"<\/code> \u2014 should show ~144 MHz<\/li>\n\n\n\n<li>Confirm graywolf-modem is reading from the loopback: <code>sudo fuser -v \/dev\/snd\/*<\/code><\/li>\n\n\n\n<li>Test directly with <code>multimon-ng<\/code> to verify there&#8217;s actual APRS activity in your area<\/li>\n\n\n\n<li>Try adjusting gain: change <code>-g 49.6<\/code> (max gain) in the rtl_fm command if signals are weak<\/li>\n\n\n\n<li>Try PPM correction: add <code>-p &lt;N&gt;<\/code> to rtl_fm if your SDR is off-frequency. Typical RTL-SDR PPM offsets range from -50 to +50.<\/li>\n<\/ul>\n\n\n\n<p><strong><code>aplay: set_params:1398: Channels count non available<\/code><\/strong><\/p>\n\n\n\n<p>You&#8217;re using <code>hw:<\/code> instead of <code>plughw:<\/code>. Switch to <code>plughw:Loopback,0,0<\/code> in the rtl_fm-aprs service.<\/p>\n\n\n\n<p><strong>Card number changed after reboot<\/strong><\/p>\n\n\n\n<p>Use device names, not numbers. <code>plughw:Loopback,0,0<\/code> is stable; <code>plughw:2,0,0<\/code> is not.<\/p>\n\n\n\n<p><strong>iGate connects but iGated count stays at 0<\/strong><\/p>\n\n\n\n<p>The &#8220;Enable iGate&#8221; checkbox may not have saved. Re-toggle, save, restart graywolf:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl restart graywolf\nsudo journalctl -u graywolf -n 50 | grep -i \"aprs-is connected\"<\/code><\/pre>\n\n\n\n<p>You should see a successful login line. If absent, the iGate component isn&#8217;t actually trying to connect.<\/p>\n\n\n\n<p><strong>rtl_fm service exits with status 1<\/strong><\/p>\n\n\n\n<p>Check what aplay is complaining about:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u rtl_fm-aprs -n 50<\/code><\/pre>\n\n\n\n<p>Most issues are resolved by switching <code>hw:<\/code> to <code>plughw:<\/code> and explicitly setting <code>-c 1<\/code> for mono.<\/p>\n\n\n\n<p><strong>Two RTL-SDRs and the wrong one keeps getting selected<\/strong><\/p>\n\n\n\n<p>Set unique serials per Step 3 and reference them by serial in the service unit. Don&#8217;t rely on <code>-d 0<\/code> \/ <code>-d 1<\/code> \u2014 USB enumeration order can shift.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Operational Notes<\/h2>\n\n\n\n<p><strong>Disk usage<\/strong>: graywolf&#8217;s SQLite databases (<code>\/var\/lib\/graywolf\/graywolf.db<\/code> and <code>graywolf-history.db<\/code>) grow over time but are typically modest \u2014 single-digit MB to a few tens of MB depending on local APRS activity.<\/p>\n\n\n\n<p><strong>CPU usage<\/strong>: graywolf-modem uses ~5\u201310% of a modern CPU core for AFSK1200 decoding at 44100 Hz; rtl_fm uses similar. The ALSA loopback adds negligible overhead.<\/p>\n\n\n\n<p><strong>Memory<\/strong>: Total footprint is ~50\u201380 MB resident across graywolf, graywolf-modem, rtl_fm, and aplay.<\/p>\n\n\n\n<p><strong>Updates<\/strong>: download a new <code>.deb<\/code> from the releases page and reinstall:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd \/tmp\nwget https:\/\/github.com\/chrissnell\/graywolf\/releases\/latest\/download\/graywolf_linux_amd64.deb\nsudo apt install .\/graywolf_*.deb\nsudo systemctl restart graywolf<\/code><\/pre>\n\n\n\n<p>The override file in <code>\/etc\/systemd\/system\/graywolf.service.d\/<\/code> is preserved across package upgrades since it&#8217;s outside the package&#8217;s managed paths.<\/p>\n\n\n\n<p><strong>Adding TX capability later<\/strong>: the easiest upgrade path is replacing the SDR with a real radio + USB sound card interface (Digirig, AIOC, etc.). Graywolf was designed for this and the configuration is more straightforward than the SDR loopback approach. Your existing graywolf install remains usable; you&#8217;d just add a new audio device, channel, and PTT configuration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Approach Works<\/h2>\n\n\n\n<p>Graywolf expects an ALSA capture device. RTL-SDR produces audio on stdout. The ALSA loopback module is the bridge \u2014 it presents two paired ALSA devices that act as a virtual cable. <code>aplay<\/code> writes to one end; graywolf reads from the other.<\/p>\n\n\n\n<p>The <code>plughw:<\/code> plugin is the key piece that makes this robust. Raw <code>hw:<\/code> requires exact format\/rate\/channel agreement between writer and reader, which is brittle \u2014 <code>plughw:<\/code> performs automatic conversion, so as long as both sides specify <em>something<\/em> compatible with the loopback&#8217;s wide capability range, it works.<\/p>\n\n\n\n<p>The systemd dependency chain (<code>Requires=<\/code> + <code>Before=<\/code> + <code>After=<\/code>) ensures clean startup order: kernel module loaded \u2192 SDR enumerated \u2192 rtl_fm-aprs running \u2192 graywolf starts and finds audio waiting.<\/p>\n\n\n\n<p>This setup is, to my knowledge, not officially documented as a known-working configuration for graywolf. It&#8217;s been tested working on Debian 13 amd64. If you&#8217;ve replicated it on other distros or with other SDRs, consider contributing to the <a href=\"https:\/\/github.com\/chrissnell\/graywolf\">graywolf Known-Working Configurations<\/a> page.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A complete guide for running graywolf as a receive-only APRS iGate using an RTL-SDR dongle as the radio source, on Linux. Graywolf is designed to consume audio from a real radio via a USB sound card. It does not natively accept stdin or SDR input. This guide bridges an RTL-SDR to graywolf using an ALSA [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"set","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[8,9,11,10],"tags":[4],"class_list":["post-37","post","type-post","status-publish","format-standard","hentry","category-aprs","category-linux","category-sdr","category-software","tag-graywolf"],"_links":{"self":[{"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/posts\/37","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kg7okr.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=37"}],"version-history":[{"count":1,"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/posts\/37\/revisions"}],"predecessor-version":[{"id":38,"href":"https:\/\/kg7okr.com\/index.php?rest_route=\/wp\/v2\/posts\/37\/revisions\/38"}],"wp:attachment":[{"href":"https:\/\/kg7okr.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=37"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kg7okr.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=37"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kg7okr.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=37"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}