Claude here on Jeremy's behalf.
Summary
Add a native software TNC (Terminal Node Controller) inside AetherSDR so packet radio, KISS, and APRS become first-class features — no Direwolf bridge, no virtual audio cables, no external hardware TNC required.
AetherSDR already has every piece needed: per-slice 24 kHz post-demod audio, TX audio injection (used by WAVE / PUDU monitor), PTT control, settings infrastructure, applets, and the PersistentDialog pattern. A soft-TNC turns APRS into a one-click feature.
This issue tracks the umbrella plan. Subtasks below will be filed as separate issues once Phase 1 design is locked.
Why native (not a Direwolf bridge)
| Option |
Pros |
Cons |
| Native soft-TNC (this plan) |
Tight PTT/audio timing, single integrated UI, no audio routing pain, no external deps |
~3 weeks of DSP work |
| Direwolf bridge |
Ships in a weekend, inherits Direwolf's tuning |
Runtime dep, PipeWire/PulseAudio routing, looser PTT timing, two UIs to learn |
We're going native because packet TX timing matters (the radio's PTT-attack window is unforgiving) and because keeping the user experience inside one app is the project's stated goal.
Scope for v1
- Modulation: Bell 202 1200-baud AFSK (VHF APRS, ~99% of traffic) + 300-baud HF AFSK (30 m HF APRS — same demod, different constants).
- Layer 2: AX.25 v2.0 UI-frame TX/RX (APRS uses UI almost exclusively).
- KISS server: TCP on port 8001, multi-client, so Xastir / YAAC / kissattach / APRSdroid (via reverse-tunnel) work as external apps.
- APRS layer: Position/Message/Status/Object/Weather/Mic-E parser + builder.
- Native UI: applet tile, map dialog (QtLocation/OSM), message log, station list, settings dialog.
- Smart Beaconing: HamHUD-style variable-rate scheduler from day one.
- Auto-create FM slice when TNC is first enabled (144.39 MHz, 15 kHz BW, named
APRS).
- Non-destructive audio tap: TNC copies slice audio, RX continues to speakers normally.
- APRS-IS uplink/downlink: optional Tier-2 server connection (rotate.aprs2.net:14580) so AetherSDR acts as an i-gate.
Architecture
```
RadioModel / SliceModel (existing)
↓ 24 kHz post-demod audio per slice
AudioEngine
↓ tap @ TNC slice
ClientTnc (lock-free atomics, AudioEngine thread)
AFSK demod → HDLC bits → AX.25 frame
AX.25 frame → HDLC bits → AFSK mod (TX)
↓ queued signal
KissTcpServer + AprsModel
↓
AprsApplet / AprsMapDialog / AprsMessagesDialog / AprsStationListDialog
```
DSP runs on the existing AudioEngine thread alongside RX audio processing — no new threads. KISS TCP server is a parallel integration path so users who already use Xastir or YAAC keep their workflow.
Files (v1)
| File |
Change |
| `src/core/ClientTnc.{h,cpp}` |
NEW — AFSK demod/mod + HDLC + AX.25 frame in/out |
| `src/core/Ax25Frame.{h,cpp}` |
NEW — frame parser/builder, address/control/FCS |
| `src/core/HdlcCodec.{h,cpp}` |
NEW — NRZI, bit-stuff, flag-sync, FCS-16 |
| `src/core/KissProtocol.{h,cpp}` |
NEW — KISS frame encode/decode (no I/O) |
| `src/core/KissTcpServer.{h,cpp}` |
NEW — QTcpServer wrapping KissProtocol |
| `src/core/AprsParser.{h,cpp}` |
NEW — info-field parser (position, message, weather, Mic-E…) |
| `src/models/AprsModel.{h,cpp}` |
NEW — station registry, message log, beacon scheduler |
| `src/core/AprsIsClient.{h,cpp}` |
NEW — optional uplink to APRS-IS Tier 2 |
| `src/core/AudioEngine.{h,cpp}` |
Wire ClientTnc tap into per-slice audio path |
| `src/gui/AprsApplet.{h,cpp}` |
NEW — applet tile |
| `src/gui/AprsMapDialog.{h,cpp}` |
NEW — QtLocation map with station markers |
| `src/gui/AprsMessagesDialog.{h,cpp}` |
NEW — chat-style message log |
| `src/gui/AprsStationListDialog.{h,cpp}` |
NEW — heard-stations table |
| `src/gui/AprsSettingsDialog.{h,cpp}` |
NEW — callsign, path, beacon, KISS, APRS-IS |
| `tests/ax25_frame_test.cpp` |
NEW |
| `tests/hdlc_codec_test.cpp` |
NEW |
| `tests/kiss_protocol_test.cpp` |
NEW |
| `tests/aprs_parser_test.cpp` |
NEW |
| `tests/client_tnc_test.cpp` |
NEW — loopback: frame → mod → demod → parse |
Phasing — three shippable releases
v26.6.0 (Phases 1–3) — AetherSDR is a KISS TNC
- Phase 1: Protocol core (HDLC + AX.25 + KISS as a pure library, fully unit-tested, no audio).
- Phase 2: AFSK demodulator + RX path tap; decoded frames in a log.
- Phase 3: KISS TCP server; Xastir / YAAC / kissattach work as external clients.
v26.7.0 (Phase 4) — TX works
- AFSK modulator, TX state machine (KeyUp → TxDelay → Frame → TxTail → KeyDown), CSMA collision avoidance, PTT integration via existing TX path.
- External KISS clients can beacon and send messages through AetherSDR.
v26.8.0 (Phases 5–7) — Full native APRS
- APRS info-field parser (including Mic-E).
- Native UI: applet, map, message log, station list, beacon scheduler.
- APRS-IS uplink/downlink with passcode and filter expression.
Implementation details
Phase 1 — Protocol core
Pure C++, no Qt except `QByteArray`. Fully testable in CI without audio.
HDLC gotchas (these are 2-hour debugging sessions if wrong):
- Bit order on the wire: LSB first within each byte.
- NRZI: 0 = transition, 1 = no transition.
- Bit stuffing: insert 0 after five consecutive 1s in payload — but NOT inside flag bytes.
- FCS: CRC-16-CCITT, init 0xFFFF, polynomial 0x1021, LSB-first, byte-reversed, complemented. Lots of wrong CRC code online; copy from Direwolf's `fcs_calc.c` and verify against canonical test vectors.
AX.25 address gotchas:
- Each callsign byte left-shifted 1 bit.
- 7th byte is SSID + bits: `0bCxxxSSIDL` (C = command/response, L = last address).
- "Has been digipeated" bit (H-bit) at top of each digipeater's SSID byte.
50+ unit tests across `ax25_frame_test`, `hdlc_codec_test`, `kiss_protocol_test`. Round-trip: build frame → toWire → HDLC encode → HDLC decode → parse → assert structural equality.
Phase 2 — AFSK demod
Bell 202: mark 1200 Hz, space 2200 Hz, 1200 bps, 24000/1200 = 20 samples/symbol at AetherSDR's native audio rate.
Demod approach: correlator-difference (most accurate; ~20-sample window of cos/sin references at 1200/2200 Hz, output bit metric = `|markCorr| - |spaceCorr|`). Symbol clock recovery via early-late gate, locks in 3-5 symbols (well within the flag preamble before every frame).
Phase 3 — KISS TCP server
`QTcpServer` on localhost:8001 by default (loopback only, LAN opt-in). Multi-client. Each client gets its own streaming KISS decoder. `KissTcpServer::txRequested` → queued signal → `ClientTnc::queueTx`.
Phase 4 — AFSK mod + TX path
Phase-continuous sin generation (discontinuous phase produces splatter outside the channel and breaks narrow-band filters). TX state machine: `Idle → KeyUp → TxDelay (flags) → Frame → TxTail → KeyDown`. PTT timing: assert PTT before audio starts, hold until after audio stops, reuse the existing interlock-confirmation path used by SSB/CW.
CSMA: p-persistent collision avoidance — when ready to TX and DCD clear, draw `rand(0..255)`; if < persistence (default 63), TX; else wait `slotTime` ms (default 100) and retry.
Phase 5 — APRS parser
Data-type byte selects format: `!`/`=` position, `/`/`@` position+timestamp, `>` status, `:` message, `;` object, `)` item, `_` weather, `T` telemetry, `?` query, `<` capabilities, `}` third-party.
Coordinate formats: standard `DDMM.MM[N/S]`, compressed base91, and Mic-E (lat encoded in destination callsign, lon in info field — ~30% of mobile traffic).
Phase 6 — Native UI
QtLocation for the map (approved dependency). PersistentDialog base for all popouts. Frameless mode respected.
Marker layer: bright icon for stations heard in last 30 min, dimmed for 24h. Click → station detail popup (last position, comment, path, packet count). Filters: All / Direct only / Via specific digi.
Message dialog: bidirectional chat-style log. Beacon scheduler implements full Smart Beaconing (see Maintainer Decisions §3) — variable rate by speed + corner-pegging on heading change. Fixed stations degrade naturally to a slow fixed rate.
Phase 7 — APRS-IS
Tier-2 connection (`rotate.aprs2.net:14580`). Standard login string with callsign-derived passcode (open algorithm, ref `aprslib.passcode()`). Filter expression like `r/47.6/-122.3/200` for 200 km area.
Uplink: local RF decodes forwarded to APRS-IS (i-gate function), q-construct `,qAR,N0CALL-1` appended.
Downlink: filtered packets back, map populates with stations beyond RF range.
Settings persistence
Per `AppSettings` conventions (PascalCase keys, `AetherSDR.settings` XML):
```
AprsTncEnabled, AprsTncSliceId, AprsCallsign, AprsSymbolTable, AprsSymbolCode,
AprsPath, AprsTxDelayMs, AprsTxTailMs, AprsSquelchDb,
AprsBeaconEnabled, AprsBeaconPeriodSec, AprsBeaconCommentText, AprsBeaconLat, AprsBeaconLon,
AprsBeaconPositionSrc (Fixed|RadioGps|Gpsd), AprsGpsdHost, AprsGpsdPort,
AprsSmartBeaconEnabled, AprsSmartBeaconRateMinSec, AprsSmartBeaconRateMaxSec,
AprsSmartBeaconLowSpeed, AprsSmartBeaconHighSpeed,
AprsSmartBeaconTurnMinDeg, AprsSmartBeaconTurnTimeSec, AprsSmartBeaconTurnSlope,
AprsKissEnabled, AprsKissPort, AprsKissBindAddr,
AprsIsEnabled, AprsIsServer, AprsIsPort, AprsIsFilter
```
Edge cases
- Slice mode: APRS needs FM with flat audio response. Force the TNC slice into FM (or PKT mode if available on the radio). Warn user if they switch out.
- DCD: prefer "valid AFSK lock" over RSSI threshold for carrier detection — more accurate.
- TX while another stage is keyed: ClientTnc must check existing TX-busy state before requesting PTT. Queue frame until channel is free.
- APRS-IS lost connection: exponential backoff (1s, 2s, 4s, … capped at 60s).
- No callsign: TNC enabled but `AprsCallsign` blank → log warning, refuse to TX. RX still works.
- Multi-radio: TNC tied to a specific slice on a specific radio. Settings key should be radio-scoped.
Verification (full system, v26.8.0)
- Tune to 144.39 MHz (US) or local APRS freq; slice = FM, 15 kHz BW.
- Enable TNC on that slice; set callsign.
- Within minutes, AprsMessagesDialog shows live decoded stations.
- AprsMapDialog populates with markers.
- Enable beacon at 5-min period; aprs.fi shows your callsign within ~6 min.
- Send a directed message to a known callsign; verify ACK in the log.
- Enable APRS-IS uplink; aprs.fi shows AetherSDR as the receiving i-gate for nearby packets.
- `kissattach /dev/ptmx ax0 ax25.conf` pointed at `localhost:8001`; `axlisten -ar` shows decoded frames.
- Point Xastir at "Networked KISS TNC, localhost:8001"; it decodes independently.
Estimated effort
- Phase 1 (protocol core + tests): ~3-4 days
- Phase 2 (AFSK demod + integration): ~2-3 days
- Phase 3 (KISS server): ~1 day
- Phase 4 (AFSK mod + TX): ~2-3 days
- Phase 5 (APRS parser, inc. Mic-E): ~2 days
- Phase 6 (UI): ~4-5 days
- Phase 7 (APRS-IS): ~1-2 days
- Integration, polish, docs: ~2 days
Total: ~17-22 working days, split across three releases.
Forward-looking enhancements (v2+, post-26.8.0)
Filing these here so they don't get lost. None are blocking v1; each could be a standalone issue when prioritised.
Modulation modes
- 9600-baud G3RUH — flat-FM-discriminator scrambled NRZI. Needs a "raw FM audio" tap separate from the audio-channel filtering that 1200 AFSK uses. Doubles VHF/UHF packet throughput. Standard on satellites (AO-91, AO-92, ISS digipeater).
- 300-baud HF AFSK — narrow-shift HF packet, see dedicated section below.
- Robust Packet Radio (RPR) — high-value HF mode, see dedicated section below.
- IL2P — Improved Layer 2 Protocol. Reed-Solomon FEC wrapper around AX.25 frames. Direwolf supports IL2P; FX.25 too. Significant range/reliability improvement on noisy channels.
- IL2P-X4 — heavy FEC mode for emergency comms.
- PSK-APRS — PSK63/PSK125 carrying APRS frames. Niche but used on HF.
- Codec2 voice-over-packet — FreeDV-style digital voice carried in AX.25 UI frames.
Robust Packet Radio (RPR) — HF packet for poor conditions
RPR deserves its own subsection because it solves the biggest weakness of 1200 AFSK and 9600 G3RUH on HF: both fall apart below ~10 dB SNR, exactly the conditions where HF APRS operators actually need them. RPR is the de-facto HF packet standard for emergency comms in Europe, growing in North America, and is on the wishlist for almost every soft-modem project.
What it is
- Developed by SCS GmbH (Special Communications Systems, Germany) — the same vendor as PACTOR.
- 8-tone OFDM, 500 Hz total occupied bandwidth, ~200/600 bps gross with FEC.
- Two speeds: RPR-200 (rock-solid below 0 dB SNR) and RPR-600 (faster, needs ~+3 dB).
- Strong Reed-Solomon + Viterbi FEC; works under selective fading where 300-baud AFSK fails.
- Compatible with the existing AX.25 / KISS layer — RPR replaces the PHY only. UI frames, addressing, FCS unchanged.
Hardware landscape today
- SCS Tracker / DSP-TNC: ~€450, the canonical hardware modem.
- SCS P4dragon (HF data modem): ~€1500, overkill for APRS.
- Direwolf does NOT support RPR. Open feature request for years. This is the gap a native AetherSDR implementation closes for the whole soft-modem community.
- A few research GNU Radio flowgraphs exist (DK7XE published reference designs); no production-quality open soft-modem.
Why AetherSDR is well-positioned
- We already have per-slice 24 kHz audio, FFT infrastructure, Qt models for visualisation, and an OFDM-adjacent DSP mindset (the existing waterfall + filter code is in the same neighbourhood).
- 8-tone OFDM at 50 baud per tone is computationally trivial vs the AFSK demod (smaller FFT, longer integration window — symbol rate is much slower).
- Native PTT timing matters even more on RPR than on 1200 AFSK because the symbol period is ~20 ms; a 30 ms PTT-attack delay eats an entire symbol.
Frequencies (HF APRS RPR)
- 10.147.6 MHz USB (30 m)
- 14.103.0 MHz USB (20 m, most active)
- 18.106.0 MHz USB (17 m)
- 21.103.0 MHz USB (15 m)
- 28.123.0 MHz USB (10 m)
(Note: USB dial; the 1500 Hz centre puts the RPR signal at the listed frequency + 1.5 kHz.)
Implementation sketch (when v2 work begins)
- New
ClientRpr DSP stage parallel to ClientTnc. Shares the audio tap point and KISS framing layer; the radio just sees a different PHY.
- 8-FFT bin demod, hard or soft-decision per bin.
- Viterbi decoder (free implementations available — Phil Karn's libfec is the canonical reference, MIT-licensed).
- Reed-Solomon outer code (libfec again).
- Synchroniser uses the unique-word preamble per the SCS spec.
Effort estimate
- Modem PHY (demod + sync + FEC): ~5-7 days. The FEC libraries do the heavy lifting; the trick is sync and equalisation under fading.
- Modulator: ~2-3 days. OFDM TX is conceptually simpler than the demod.
- AetherSDR integration (slice settings, applet mode toggle, settings): ~1-2 days, since the AX.25/KISS/APRS layers above already exist by then.
- Field testing across HF bands and propagation conditions: open-ended. Plan on 2-3 weeks of operator-feedback iteration before declaring v1 RPR stable.
Total: ~2-3 weeks of focused work, post-v26.8.0 (after the AFSK/AX.25/KISS/APRS foundation is in place).
Spec / reference material
- SCS Robust Packet documentation (download from
scs-ptc.com).
- Patrick Lindecker's (F6CTE) RPR analysis articles.
- DK7XE GNU Radio reference flowgraphs.
- Phil Karn's libfec for Viterbi + Reed-Solomon.
Open question: should RPR be a v2.0 milestone (post the v1 APRS releases) or interleaved into v26.8.0? The HF emergency-comms community would benefit from it sooner, but it's a much bigger DSP undertaking than 1200 AFSK and would push the v1 timeline.
300-baud HF AFSK — the classic 30 m HF APRS mode
If RPR is the "emergency-comms heavyweight" HF mode, 300 baud AFSK is the workhorse — it's been the standard HF APRS modulation since the 1990s, every existing hardware TNC (KAM, KPC, PK-232, MFJ-1278, TNC-X) supports it, and there's an active worldwide HF APRS i-gate network using it on 30 m specifically. Native AetherSDR 300-baud support brings the world's most popular HF APRS network online with effectively zero additional DSP code over what Phase 2 already builds.
Why 30 m is the canonical band
- Quiet, narrow, regulated for digital-only above 10.140 MHz (no SSB QRM).
- Propagates well day and night, low absorption, modest QSB.
- The single HF band where APRS has continuous activity globally — North America, Europe, Australia, South America all converge on 30 m.
Frequencies (HF APRS 300-baud)
- 10.151.00 MHz LSB — North America / VK (most active worldwide, this is where i-gates live).
- 10.147.30 MHz USB — Europe (Region 1).
- 7.040.00 MHz LSB — 40 m, secondary (sparse activity).
- 14.105.00 MHz LSB — 20 m, regional/experimental.
LSB vs USB matters because the mark/space tones are at audio frequencies (1600/1800 Hz typical) and flipping sideband flips which audio tone maps to mark vs space — get it wrong and the radio decodes nothing.
Modulation details
- 300 bps AFSK, Bell 103-derived.
- Mark 1600 Hz, space 1800 Hz (200 Hz shift — fits HF's narrow channel).
- Same AX.25 / HDLC / KISS upper layers as VHF 1200 AFSK — only the PHY changes.
- 24000 / 300 = 80 samples per symbol — 4× the integration window of 1200 AFSK, which means better noise performance for free.
Why it's almost free given the v1 codebase
The v1 `ClientTnc` already builds a correlator-difference Bell 202 demod. 300-baud HF is the same demod with three constants changed:
| Constant |
1200 AFSK (Bell 202) |
300 AFSK (Bell 103-style HF) |
| Mark freq |
1200 Hz |
1600 Hz |
| Space freq |
2200 Hz |
1800 Hz |
| Samples/symbol |
20 |
80 |
| Shift |
1000 Hz |
200 Hz |
Strategy: extend `ClientTnc::prepare()` to accept a "PHY profile" enum (`Bell202_1200`, `Hf300`, and later `G3RUH_9600`); the rest of the modem stays identical.
TX considerations
- HF SSB transmitters have ~3 kHz audio bandwidth — both tones fit easily, no concern.
- Phase-continuous mod is still required.
- TXDelay should be longer than VHF (HF ALC / amplifier settle times) — recommend 500-800 ms vs 300 ms on VHF. Make it per-PHY in settings.
- HF QSB / fading: TX retransmission strategy (send the frame 2-3× back to back) is common practice — should be a togglable option.
Effort estimate
- PHY profile enum + constant swap: ~half a day.
- Audio-bandwidth filter retuning (the AFSK bandpass needs to centre on the new tones): ~half a day.
- HF-specific TX timing tuning + settings UI: ~1 day.
- Field testing on 30 m: a few evenings of i-gate audibility tests.
Total: ~2 days of focused work, layered on top of the v1 `ClientTnc`. Realistically lands in v26.7.0 or v26.8.0 alongside the rest of the APRS stack — no good reason to defer to v2.
Slice configuration for HF
- Mode: LSB (or USB depending on band/region).
- Filter: 500 Hz tight filter is ideal (narrows out QRM).
- Centre audio tone target: ~1700 Hz (midway between 1600/1800).
- The Settings dialog should ship preset buttons: "30 m HF APRS (NA)", "30 m HF APRS (EU)", "144.39 VHF APRS" — one-click sideband + tone + freq + filter + TXDelay setup.
Why this matters in the v1 phasing
Adding 300 baud unlocks HF APRS for every AetherSDR user with an HF-capable Flex (which is all of them — even the 6300/6400/6500 series cover HF). The user base for HF APRS is small but passionate (Pacific Northwest fire-net ops, sailing cruisers, off-grid operators), and it's an obvious differentiator vs Direwolf-bridge setups that struggle with sideband-dependent audio routing.
Layer 2 / connected mode
- AX.25 v2.2 connected mode — SABM/UA/I-frames/RR/REJ/SREJ + T1/T2/T3 timers. Required for Winlink BBS, packet BBS (DXSpider, F6FBV, fBB), and any "connect to a remote node" workflow. ~2-3 weeks of additional work (state machine is non-trivial; v2.2 vs v2.0 differences are tricky).
- NET/ROM — packet routing layer above AX.25. Old but still in use by mesh nets.
- ROSE / FlexNet — alternative L3 routing protocols.
Multi-port / multi-radio
- Multi-port TNC — simultaneous decode on multiple slices. HF APRS on 30 m + VHF APRS on 144 + UHF on 440 all at once. Each port gets its own KISS sub-channel (KISS supports 16 ports natively).
- Cross-band repeating — RX on one slice, retransmit on another.
- Multi-radio TNC — one TNC instance spanning multiple connected radios.
Other TNC protocols
- AGW Packet Engine (AGWPE) compatibility — many older APRS clients (UI-View32, APRSpoint, some Winlink versions) speak AGWPE, not KISS. Adding an AGWPE server alongside the KISS server picks up that audience.
- AGW Express / Async Packet — variants in the wild.
- BPQ / Linux AX.25 kernel modules — direct integration with the in-kernel AX.25 stack instead of going through KISS.
Internet bridges
- Winlink integration — VARA HF/FM modem bridge or RMS Express compatibility. Big lift but high-value for emcomm operators.
- JS8Call APRS bridge — JS8Call has an APRS bridge mode; we could speak its API natively.
- DMR APRS bridge — relay APRS packets to/from BrandMeister APRS network.
- Yaesu System Fusion APRS — C4FM APRS messages over Wires-X.
- D-STAR DPRS gateway — DPRS is APRS-over-D-STAR; relay to APRS-IS.
- LoRa-APRS — uplink/downlink via separate LoRa hardware (RAK831, Heltec).
- APRS-via-ISS — Doppler-aware beacon scheduler for the ISS digipeater (145.825 MHz, ±2.5 kHz Doppler over a pass).
- APRS over Reticulum / mesh — bridge to mesh networking stacks (Reticulum, NextNet).
- CWOP integration — Citizens Weather Observer Program submission (weather-only APRS-IS server).
APRS feature depth
- Telemetry decoder + grapher — APRS T-type packets (5 analog + 8 digital channels). Plot history.
- Weather station integration — read Davis/Acu-Rite/etc. via existing weatherd/wview, format as APRS WX packets.
- Bulletin / announcement support — BLN1..BLN9, NWS weather bulletins, general announcements.
- Object/Item tracking — moving objects, `?APRSO` query response.
- GPS integration — connect to gpsd or serial NMEA for live position beaconing.
- Position privacy / fuzzing — round coordinates to N km for privacy-conscious users.
- Custom symbol overlays — extended symbol table characters, custom icons.
- Position ambiguity — APRS spec ambiguity levels (10s, 1°, 0.1°, exact).
- Net coordinator mode — track checked-in stations during a net by APRS position.
- Auto-reply / auto-ack canned messages — "off the grid", "busy", etc.
- Heard-list export — CSV / ADIF / JSON of all heard stations.
UI enhancements
- OpenStreetMap offline tiles — packaged tile bundles for off-grid / portable operation.
- Map themes — dark mode tiles, terrain, satellite imagery (where licensed).
- Trail history — draw past-N-hours track for moving stations.
- Distance / bearing widget — to selected station from operator's QTH.
- Heatmap overlay — coverage analysis over time.
- Replay mode — scrub backward through the message/position log.
- Audio waveform of decoded packet — debug aid; play back the captured audio.
- Symbol picker — visual grid of all APRS symbols (replaces typing a code).
- Path visualizer — show the digipeater chain for each decoded frame graphically.
- Streaming APRS feed — websocket export so external dashboards / visualizations can subscribe.
- Stream Deck tile — beacon-now button, new-message indicator (Jeremy's hardware supports this; see existing Stream Deck plugin work).
- MIDI mapping — map MIDI controllers to APRS actions (beacon, send canned msg).
- Voice / TTS announcements — speak callsign of new heard station, alert on directed message.
- Hot stations notification — desktop notification when specific callsigns are heard.
- Logbook integration — drop APRS-spotted stations into AetherSDR's ADIF log as informal QSOs (similar to existing DX cluster integration).
Protocol / standards work
- APRS Spec 1.2 features — current spec compliance is 1.0.1; 1.2 adds Base-91 telemetry, extended addresses, etc.
- OpenAPRS / OGN protocol — Open Glider Network protocol for aviation tracking (overlap with APRS).
- AIS bridge — Automatic Identification System (marine) — same UI works, different L2.
Other digital modes adjacent to packet
- MT63 — robust HF emergency comms mode (1000 Hz / 2000 Hz variants).
- Olivia — 8/250, 32/1000, etc. — popular emcomm digital mode.
- Throb / MFSK — exotic but lightweight modes.
- RTTY / SITOR-B / NAVTEX — old-school FSK modes (could share the AFSK demod scaffolding).
- FT4/FT8 APRS gateway — bridge JS8/FT8 grid-square spots into APRS positions.
- PACTOR — proprietary but documented; reverse-engineered modems exist.
Hardware / driver
- Hardware TNC passthrough — for users who want to keep their KPC-3+ or TNC-X, expose AetherSDR's audio path as a serial-equivalent so the hardware TNC can still drive it.
- Soundmodem compatibility — old Linux soundmodem driver compatibility layer.
Maintainer decisions (locked in)
All five open questions answered. These decisions are load-bearing — re-read this section before starting Phase 1.
1. Default TNC slice — auto-create FM slice
When the user enables TNC for the first time and no AprsTncSliceId exists, AetherSDR creates a new slice on the radio: mode FM, 144.39 MHz (US default — region-configurable later), 15 kHz bandwidth, name APRS. The returned slice ID is saved as AprsTncSliceId.
RadioModel already exposes a slice-creation path (used by the "+ slice" toolbar) — hook into it.
- On TNC disable, leave the slice alone. User removes it explicitly via SLICE menu. Auto-destroying surprises users.
- If the saved slice ID no longer exists on the radio on next connect (user manually closed it), AprsApplet shows "TNC slice missing — re-enable to create". Don't auto-recreate on reconnect.
- HF 300-baud presets ("30 m HF APRS NA / EU") override the auto-created slice's freq + mode + sideband when activated.
2. Map UI — QtLocation approved
find_package(Qt6 COMPONENTS Location Positioning REQUIRED) added to CMakeLists.txt in Phase 6.
.github/docker/Dockerfile adds qt6-location-dev plus runtime qml-module-qtlocation / qml-module-qtpositioning. Rebuild the CI image and wait for docker-ci-image.yml (~3 min) before the next CI run.
- OSM tile plugin is the default; no API key needed.
- Adds ~30 MB to runtime image; accepted.
3. Beacon scheduler — Smart Beaconing in v1
HamHUD-style variable-rate scheduler from day one, not deferred.
| Setting |
Default |
Meaning |
AprsSmartBeaconEnabled |
True |
Master toggle |
AprsSmartBeaconRateMinSec |
60 |
Fastest interval (high speed) |
AprsSmartBeaconRateMaxSec |
1800 |
Slowest interval (stopped) |
AprsSmartBeaconLowSpeed |
5 kn |
Below: use max-sec |
AprsSmartBeaconHighSpeed |
60 kn |
Above: use min-sec |
AprsSmartBeaconTurnMinDeg |
30 |
Min heading change for corner peg |
AprsSmartBeaconTurnTimeSec |
15 |
Deadband after corner peg |
AprsSmartBeaconTurnSlope |
240 |
kn × deg for turn threshold |
Algorithm (1 Hz against position source):
speed < LowSpeed → beacon every RateMaxSec.
speed > HighSpeed → beacon every RateMinSec.
- Else: linear interp between min and max by speed.
- ALSO: if heading changed >
TurnMinDeg + TurnSlope/speed since last beacon AND last beacon > TurnTimeSec ago → beacon immediately (corner peg).
Fixed stations (no GPS, speed=0): degrades to RateMaxSec (~30 min) — exactly what fixed users want. No special case needed.
Position source priority:
- Radio GPS slice (Maestro / 6700-series).
gpsd on localhost:2947 (if reachable).
- Static fixed lat/lon from settings.
4. Tile cache — always fetch
QtLocation's OSM plugin caches tiles internally in ~/.cache/QtProject/.... No AetherSDR-managed tile storage. Document QtLocation's cache path in user docs so off-grid users can pre-warm before going offline.
5. Audio tap — always copy
ClientTnc::processRx() takes a const pointer to the slice's audio buffer. Slice's normal audio path (DSP chain → speakers, WAVE recording, etc.) is unaffected. Users hear AFSK bursts through speakers while the TNC decodes in parallel. No "mute while TNC enabled" option.
Related
- Plan file: `~/.claude/plans/native-soft-tnc-aprs.md` (this issue is the public version of that plan).
- Existing TX DSP stage pattern: PUDU / Reverb plans (reference for atomics + applet structure).
73, Jeremy KK7GWY & Claude (AI dev partner)
Claude here on Jeremy's behalf.
Summary
Add a native software TNC (Terminal Node Controller) inside AetherSDR so packet radio, KISS, and APRS become first-class features — no Direwolf bridge, no virtual audio cables, no external hardware TNC required.
AetherSDR already has every piece needed: per-slice 24 kHz post-demod audio, TX audio injection (used by WAVE / PUDU monitor), PTT control, settings infrastructure, applets, and the PersistentDialog pattern. A soft-TNC turns APRS into a one-click feature.
This issue tracks the umbrella plan. Subtasks below will be filed as separate issues once Phase 1 design is locked.
Why native (not a Direwolf bridge)
We're going native because packet TX timing matters (the radio's PTT-attack window is unforgiving) and because keeping the user experience inside one app is the project's stated goal.
Scope for v1
APRS).Architecture
```
RadioModel / SliceModel (existing)
↓ 24 kHz post-demod audio per slice
AudioEngine
↓ tap @ TNC slice
ClientTnc (lock-free atomics, AudioEngine thread)
AFSK demod → HDLC bits → AX.25 frame
AX.25 frame → HDLC bits → AFSK mod (TX)
↓ queued signal
KissTcpServer + AprsModel
↓
AprsApplet / AprsMapDialog / AprsMessagesDialog / AprsStationListDialog
```
DSP runs on the existing AudioEngine thread alongside RX audio processing — no new threads. KISS TCP server is a parallel integration path so users who already use Xastir or YAAC keep their workflow.
Files (v1)
Phasing — three shippable releases
v26.6.0 (Phases 1–3) — AetherSDR is a KISS TNC
v26.7.0 (Phase 4) — TX works
v26.8.0 (Phases 5–7) — Full native APRS
Implementation details
Phase 1 — Protocol core
Pure C++, no Qt except `QByteArray`. Fully testable in CI without audio.
HDLC gotchas (these are 2-hour debugging sessions if wrong):
AX.25 address gotchas:
50+ unit tests across `ax25_frame_test`, `hdlc_codec_test`, `kiss_protocol_test`. Round-trip: build frame → toWire → HDLC encode → HDLC decode → parse → assert structural equality.
Phase 2 — AFSK demod
Bell 202: mark 1200 Hz, space 2200 Hz, 1200 bps, 24000/1200 = 20 samples/symbol at AetherSDR's native audio rate.
Demod approach: correlator-difference (most accurate; ~20-sample window of cos/sin references at 1200/2200 Hz, output bit metric = `|markCorr| - |spaceCorr|`). Symbol clock recovery via early-late gate, locks in 3-5 symbols (well within the flag preamble before every frame).
Phase 3 — KISS TCP server
`QTcpServer` on localhost:8001 by default (loopback only, LAN opt-in). Multi-client. Each client gets its own streaming KISS decoder. `KissTcpServer::txRequested` → queued signal → `ClientTnc::queueTx`.
Phase 4 — AFSK mod + TX path
Phase-continuous sin generation (discontinuous phase produces splatter outside the channel and breaks narrow-band filters). TX state machine: `Idle → KeyUp → TxDelay (flags) → Frame → TxTail → KeyDown`. PTT timing: assert PTT before audio starts, hold until after audio stops, reuse the existing interlock-confirmation path used by SSB/CW.
CSMA: p-persistent collision avoidance — when ready to TX and DCD clear, draw `rand(0..255)`; if < persistence (default 63), TX; else wait `slotTime` ms (default 100) and retry.
Phase 5 — APRS parser
Data-type byte selects format: `!`/`=` position, `/`/`@` position+timestamp, `>` status, `:` message, `;` object, `)` item, `_` weather, `T` telemetry, `?` query, `<` capabilities, `}` third-party.
Coordinate formats: standard `DDMM.MM[N/S]`, compressed base91, and Mic-E (lat encoded in destination callsign, lon in info field — ~30% of mobile traffic).
Phase 6 — Native UI
QtLocation for the map (approved dependency). PersistentDialog base for all popouts. Frameless mode respected.
Marker layer: bright icon for stations heard in last 30 min, dimmed for 24h. Click → station detail popup (last position, comment, path, packet count). Filters: All / Direct only / Via specific digi.
Message dialog: bidirectional chat-style log. Beacon scheduler implements full Smart Beaconing (see Maintainer Decisions §3) — variable rate by speed + corner-pegging on heading change. Fixed stations degrade naturally to a slow fixed rate.
Phase 7 — APRS-IS
Tier-2 connection (`rotate.aprs2.net:14580`). Standard login string with callsign-derived passcode (open algorithm, ref `aprslib.passcode()`). Filter expression like `r/47.6/-122.3/200` for 200 km area.
Uplink: local RF decodes forwarded to APRS-IS (i-gate function), q-construct `,qAR,N0CALL-1` appended.
Downlink: filtered packets back, map populates with stations beyond RF range.
Settings persistence
Per `AppSettings` conventions (PascalCase keys, `AetherSDR.settings` XML):
```
AprsTncEnabled, AprsTncSliceId, AprsCallsign, AprsSymbolTable, AprsSymbolCode,
AprsPath, AprsTxDelayMs, AprsTxTailMs, AprsSquelchDb,
AprsBeaconEnabled, AprsBeaconPeriodSec, AprsBeaconCommentText, AprsBeaconLat, AprsBeaconLon,
AprsBeaconPositionSrc (Fixed|RadioGps|Gpsd), AprsGpsdHost, AprsGpsdPort,
AprsSmartBeaconEnabled, AprsSmartBeaconRateMinSec, AprsSmartBeaconRateMaxSec,
AprsSmartBeaconLowSpeed, AprsSmartBeaconHighSpeed,
AprsSmartBeaconTurnMinDeg, AprsSmartBeaconTurnTimeSec, AprsSmartBeaconTurnSlope,
AprsKissEnabled, AprsKissPort, AprsKissBindAddr,
AprsIsEnabled, AprsIsServer, AprsIsPort, AprsIsFilter
```
Edge cases
Verification (full system, v26.8.0)
Estimated effort
Total: ~17-22 working days, split across three releases.
Forward-looking enhancements (v2+, post-26.8.0)
Filing these here so they don't get lost. None are blocking v1; each could be a standalone issue when prioritised.
Modulation modes
Robust Packet Radio (RPR) — HF packet for poor conditions
RPR deserves its own subsection because it solves the biggest weakness of 1200 AFSK and 9600 G3RUH on HF: both fall apart below ~10 dB SNR, exactly the conditions where HF APRS operators actually need them. RPR is the de-facto HF packet standard for emergency comms in Europe, growing in North America, and is on the wishlist for almost every soft-modem project.
What it is
Hardware landscape today
Why AetherSDR is well-positioned
Frequencies (HF APRS RPR)
(Note: USB dial; the 1500 Hz centre puts the RPR signal at the listed frequency + 1.5 kHz.)
Implementation sketch (when v2 work begins)
ClientRprDSP stage parallel toClientTnc. Shares the audio tap point and KISS framing layer; the radio just sees a different PHY.Effort estimate
Total: ~2-3 weeks of focused work, post-v26.8.0 (after the AFSK/AX.25/KISS/APRS foundation is in place).
Spec / reference material
scs-ptc.com).Open question: should RPR be a v2.0 milestone (post the v1 APRS releases) or interleaved into v26.8.0? The HF emergency-comms community would benefit from it sooner, but it's a much bigger DSP undertaking than 1200 AFSK and would push the v1 timeline.
300-baud HF AFSK — the classic 30 m HF APRS mode
If RPR is the "emergency-comms heavyweight" HF mode, 300 baud AFSK is the workhorse — it's been the standard HF APRS modulation since the 1990s, every existing hardware TNC (KAM, KPC, PK-232, MFJ-1278, TNC-X) supports it, and there's an active worldwide HF APRS i-gate network using it on 30 m specifically. Native AetherSDR 300-baud support brings the world's most popular HF APRS network online with effectively zero additional DSP code over what Phase 2 already builds.
Why 30 m is the canonical band
Frequencies (HF APRS 300-baud)
LSB vs USB matters because the mark/space tones are at audio frequencies (1600/1800 Hz typical) and flipping sideband flips which audio tone maps to mark vs space — get it wrong and the radio decodes nothing.
Modulation details
Why it's almost free given the v1 codebase
The v1 `ClientTnc` already builds a correlator-difference Bell 202 demod. 300-baud HF is the same demod with three constants changed:
Strategy: extend `ClientTnc::prepare()` to accept a "PHY profile" enum (`Bell202_1200`, `Hf300`, and later `G3RUH_9600`); the rest of the modem stays identical.
TX considerations
Effort estimate
Total: ~2 days of focused work, layered on top of the v1 `ClientTnc`. Realistically lands in v26.7.0 or v26.8.0 alongside the rest of the APRS stack — no good reason to defer to v2.
Slice configuration for HF
Why this matters in the v1 phasing
Adding 300 baud unlocks HF APRS for every AetherSDR user with an HF-capable Flex (which is all of them — even the 6300/6400/6500 series cover HF). The user base for HF APRS is small but passionate (Pacific Northwest fire-net ops, sailing cruisers, off-grid operators), and it's an obvious differentiator vs Direwolf-bridge setups that struggle with sideband-dependent audio routing.
Layer 2 / connected mode
Multi-port / multi-radio
Other TNC protocols
Internet bridges
APRS feature depth
UI enhancements
Protocol / standards work
Other digital modes adjacent to packet
Hardware / driver
Maintainer decisions (locked in)
All five open questions answered. These decisions are load-bearing — re-read this section before starting Phase 1.
1. Default TNC slice — auto-create FM slice
When the user enables TNC for the first time and no
AprsTncSliceIdexists, AetherSDR creates a new slice on the radio: mode FM, 144.39 MHz (US default — region-configurable later), 15 kHz bandwidth, nameAPRS. The returned slice ID is saved asAprsTncSliceId.RadioModelalready exposes a slice-creation path (used by the "+ slice" toolbar) — hook into it.2. Map UI — QtLocation approved
find_package(Qt6 COMPONENTS Location Positioning REQUIRED)added toCMakeLists.txtin Phase 6..github/docker/Dockerfileaddsqt6-location-devplus runtimeqml-module-qtlocation/qml-module-qtpositioning. Rebuild the CI image and wait fordocker-ci-image.yml(~3 min) before the next CI run.3. Beacon scheduler — Smart Beaconing in v1
HamHUD-style variable-rate scheduler from day one, not deferred.
AprsSmartBeaconEnabledAprsSmartBeaconRateMinSecAprsSmartBeaconRateMaxSecAprsSmartBeaconLowSpeedAprsSmartBeaconHighSpeedAprsSmartBeaconTurnMinDegAprsSmartBeaconTurnTimeSecAprsSmartBeaconTurnSlopeAlgorithm (1 Hz against position source):
speed < LowSpeed→ beacon everyRateMaxSec.speed > HighSpeed→ beacon everyRateMinSec.TurnMinDeg + TurnSlope/speedsince last beacon AND last beacon >TurnTimeSecago → beacon immediately (corner peg).Fixed stations (no GPS, speed=0): degrades to
RateMaxSec(~30 min) — exactly what fixed users want. No special case needed.Position source priority:
gpsdonlocalhost:2947(if reachable).4. Tile cache — always fetch
QtLocation's OSM plugin caches tiles internally in
~/.cache/QtProject/.... No AetherSDR-managed tile storage. Document QtLocation's cache path in user docs so off-grid users can pre-warm before going offline.5. Audio tap — always copy
ClientTnc::processRx()takes aconstpointer to the slice's audio buffer. Slice's normal audio path (DSP chain → speakers, WAVE recording, etc.) is unaffected. Users hear AFSK bursts through speakers while the TNC decodes in parallel. No "mute while TNC enabled" option.Related
73, Jeremy KK7GWY & Claude (AI dev partner)