feat(aprs): Add a lightweight APRS client to AetherModem AX.25 tab#3530
Conversation
|
Hi @jensenpat — thanks for this PR, the APRS client is a really nice addition (the messaging retry/re-ack semantics and the digipeat dedupe are details a lot of clients get wrong 👏). Here's what's going on with CI on c98ad6b: What failedTwo checks failed on this commit, and they're the same root cause:
Root cause:
|
There was a problem hiding this comment.
Reviewed the full branch locally (core layer, GUI, tests, CMake). This is a well-structured PR: the src/core/aprs/ layer is genuinely Qt-Core-only and unit-tested, settings go through a single nested-JSON AppSettings key (Principle V), ownership is consistently parent-based with QPointer for the child dialog, TX rides the existing shared keying queue, and the head commit already sidesteps the Qt 6.5 QTimeZone::UTC constant for the Linux CI image. The tests are real protocol tests (wrong-station acks, re-ack on retry, loopback rejection), not smoke tests. Nice work.
Three issues worth addressing, in descending priority:
1. Incoming-message dedupe never expires for numbered messages — new messages can be silently dropped (AprsMessenger::onPacket)
The duplicate check for a numbered message scans the entire stored history (up to 500 messages, persisted across sessions) for the same {msgNo} from the same station, with no time bound:
if (!packet.messageNo.isEmpty()) {
if (m.msgNo == packet.messageNo)
return;
}Real-world APRS clients reuse small message numbers freely (many cycle {1–{99, some restart numbering per session). A station that sent you {1 last week will have today's genuinely new {1 acked (so their client shows it delivered) but never stored or surfaced — the worst failure mode, since neither side notices. Suggest bounding the numbered-message dedupe by time the way the unnumbered path already is (e.g. same msgNo within ~30–60 min), or additionally comparing the text.
2. encodeUncompressedPosition can emit invalid "60.00" minutes (AprsPacket.cpp)
out += QString::asprintf("%02d%05.2f%c", latDeg, latMin, latHemi);For a latitude like 38.999999°, latMin is 59.99999 and %.2f rounds it to 60.00, producing 3860.00N — which your own parseLatLon (and other stations' parsers) reject as min >= 60. With a GPS-fed beacon this will eventually fire near minute boundaries. Round the minutes to 2 decimals first and carry overflow into the degrees (same for longitude).
3. Any config interaction defers the next timed beacon by a full interval
applyAprsConfigFromUi() runs on every editingFinished / symbol change / message send / Beacon Now, and it unconditionally calls m_aprsBeacon->setEnabled(checked) (and setIntervalMinutes), both of which restart m_timer. An operator who keeps touching the dialog can push a 30-minute beacon out indefinitely. Making setEnabled/setIntervalMinutes no-ops when the value hasn't changed would fix it without touching the UI wiring.
Two observations, no change requested:
- Modem auto-re-enable on retry:
enqueueAprsTxre-checksm_enableDecodeand turns the modem back on if a message retry or armed beacon fires after the operator disabled it. It's logged, and retries cap at ~2 minutes, so this seems acceptable — but it's the kind of "the radio keyed after I turned it off" behavior worth being deliberate about. Pausing retries while the modem is disabled would be the conservative alternative. - Full table rebuild per packet:
AprsStationList::changed→refreshAprsStationTable()rebuilds all rows plusresizeColumnsToContents()on every accepted packet. Fine at 1200-baud rates with a 500-station cap; if a busier source (e.g. a future IGate feed) ever drives this, coalescing rebuilds through a short single-shot timer like the saves would be the fix.
Scope is clean — every changed file serves the stated feature, the removed placeholder row was dead UI, and the MainWindow hook mirrors the existing KISS start-on-startup path. Persistence error handling matches the established HeardList pattern.
Couldn't compile here (no toolchain on this review box), so leaning on CI for the build/test gate. Thanks @jensenpat — items 1 and 2 are small, contained fixes and the rest of this is in good shape.
🤖 aethersdr-agent · cost: $20.4607 · model: claude-fable-5
|
Heads-up — Good news: this is a mechanical rebase, no code changes required. I checked the diff — it doesn't reference any of the members or methods that the decomposition relocated, so the conflict is pure line-churn from MainWindow.cpp shrinking (~19.5k → ~8.1k lines across #3508–#3545). One thing to watch when you resolve it: if your conflict hunk falls in a region that moved out of MainWindow.cpp, the new home is a sibling TU ( Happy to take the rebase off your hands if you'd prefer — just say so. |
…ient. Principle V.
The AX.25 tab becomes an APRS tab: a live station table, a timed GPS
position beacon, and two-way APRS messaging, all riding the existing
AFSK modem and the shared one-at-a-time TX keying/pacing queue.
New core layer (src/core/aprs/, Qt Core only, unit-tested standalone):
- AprsPacket — APRS 1.0.1 info-field codec. Parses uncompressed,
compressed (base-91) and Mic-E position reports (course/speed,
/A= and Mic-E altitude, position ambiguity), status, messages,
acks/rejs, objects, items, positionless weather. Encoders for the
position beacon, messages, acks, status; Maidenhead grid, haversine
distance/bearing, and symbol-name helpers.
- AprsStationList — station roster behind the table. Merges position,
movement and free-text across packet types per station, suppresses
digipeated duplicates (same source+info inside 30 s), persists to
tnc/aprs-stations.json with HeardList-style coalesced saves.
- AprsMessenger — messaging engine: outgoing messages get sequential
{msgNo} and retry every 30 s (4 tries) until acked; incoming
messages addressed to us are stored unread, auto-acked (and
re-acked when the sender retries), and de-duplicated; history
persists to tnc/aprs-messages.json.
- AprsBeacon — interval position beacon fed by the radio's onboard
GPS (gpsStatusChanged; used when status reports a lock) with a
manual lat/lon fallback for radios without GPS hardware. Enabling
arms the timer only; "Beacon Now" is the explicit immediate send.
- AprsSettings — nested-JSON config blob under one AppSettings key
("AetherModemAprs"), per Constitution Principle V.
APRS tab UI (Ax25HfPacketDecodeDialog):
- Scrolling station table: callsign, symbol, age (ticks every second),
packet count, grid, distance/bearing from our own position, course/
speed, status/comment. Right-click: Send Message, Station Info
(full detail popup), View on aprs.fi. Double-click prefills the
message destination.
- Beacon row: callsign, symbol picker, digi path, enable + interval,
status text, Beacon Now, and a live position-source readout
(GPS grid / manual / none).
- Message row: destination + text + Send, plus an envelope button
with an unread badge that opens the new APRS Messages window
(delivery states, reply on double-click). Replaces the dead
"Send SMS" / "Send APRS Msg..." placeholder action row.
- TX frames enqueue on the shared KISS/PMS/terminal TX queue, so
beacon, messages, mailbox and KISS clients can't double-key.
Tests: aprs_packet_test (codec vectors incl. hand-derived Mic-E and
compressed-position references), aprs_messenger_test (ack matching,
auto-ack/re-ack, duplicate suppression, unread tracking, roster
digipeat dedupe).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
GCC on the Linux CI image rejects the nested-name use with only the forward declaration that qdatetime.h provides; Homebrew Qt on macOS happened to pull the full header transitively. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
Three APRS-tab usability changes from on-air testing: - The raw decode log and the raw AX.25 UI-frame TX row are debug chrome now: visible only while Packet Activity Debug is on (click the activity trace to toggle). The station table gets the full viewport in normal operation. KISS/Mailbox tabs keep the shared log; Terminal keeps its full-window transcript. - "Autostart at launch" checkbox next to Enable Modem (persisted in the AetherModemAprs blob). MainWindow's existing KISS start-on-startup hook now also constructs the hidden AetherModem dialog when modem autostart is set, so APRS decode runs from app launch without opening the window. - The packet activity bar graph becomes an EKG-style sweep trace: every decoded frame draws a QRS-like green heartbeat, FCS failures draw smaller amber bumps, an open receive gate lifts/agitates the baseline, and the trail fades like phosphor afterglow. One spike = one packet — countable at a glance at APRS rates, where the old per-second bars made 1-3 pkt/s nearly unreadable. The sweep animation self-stops ~3 s after diagnostics stop flowing, so an idle or closed modem costs no repaints. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
Window real-estate and polish pass on the APRS tab: - Tab bar slimmed to status-bar height (40 px / 16 pt buttons → 20 px min-height / 13 pt with tight padding). - New AprsSymbolIcons module: programmatic 1.5 px wireframe line-art icons (house, car, jeep, trucks, van, RV, motorcycle, bicycle, jogger, boats, aircraft, helicopter, balloon, tower, repeater, yagi, weather, ambulance, fire, shield, bus, train, phone) drawn at 2x for retina and cached per code+color; unknown codes fall back to a rounded badge showing the raw symbol character. Used in the station table's SYMBOL column and the beacon symbol dropdown. - Station table: new first column with the last-heard date/time in the operator's local time zone; AGE keeps ticking beside it. Columns get +18 px breathing room after resizeColumnsToContents, item padding, alternating row colors, and a fixed 24 px row height. - Capture 3m / Clear Log join the raw log and TX row behind the Packet Activity Debug toggle — they are diagnostics tooling, and hiding them leaves BAUD + MODEM as the only standing controls row. - Modem/baud controls row trimmed (margins and spacing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
- APRS weather decode (APRS 1.0.1 ch. 12): positionless '_' reports (MDHM timestamp, cDDDsSSS wind, token stream) and position reports carrying the WX symbol (wind from the CSE/SPD extension in knots, tokens in the comment). Temp, wind/gust, humidity (h00 = 100%), barometric pressure (shown as inHg), and rain (hr / 24h) land in the station's comment as a triple-spaced readable summary, e.g. "77°F Wind 220° 4 mph (gust 5) Hum 50% 29.23 inHg". Wind is scrubbed from course/speed so weather stations don't appear to be driving. Weather rows get condition-matched wireframe icons in the same style/size as the symbol set: cloud, cloud-with-rain, or cloud-with-wind-streaks (rain beats wind beats cloud). - Age-fade rows: full brightness for 5 minutes, linear fade to 50% at 30 minutes, floor at 50%. Applied per-second with the age tick, quantized to 5% steps so only rows whose bucket changed repaint; wireframe icons fade with their row via the per-color icon cache. - Sortable headers: numeric sort keys for TIME, AGE, PKTS, DIST and CRS/SPD (text columns sort lexically); unknown distances sort to the bottom. Default order stays most-recent-first; the rebuild re-applies whatever column/order the operator chose. - Right-click menu gains "Clear All Stations..." (confirmation dialog; wipes the persisted roster too), available even from an empty area of the table; station actions still require a row. Tests: weather vectors (positionless, position-form knots→mph conversion, sparse "..." sensors, negative temperature, h00=100%). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
QTimeZone::UTC (the lightweight constant) only exists since Qt 6.5; the CI container's Qt rejects it. QTimeZone::utc() is equivalent here and available everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
a295196 to
84c4b94
Compare
|
@jensenpat — I took the rebase onto current The rebase resolved one conflict, and it's the textbook #3351 case worth calling out: your modem-autostart commit edited
Everything else (the The PR should drop from CONFLICTING to mergeable once CI re-runs. It still needs a non-author CODEOWNER review/approval before merge. (Ref: the new |
…, atomic persistence (aethersdr#3530 review) Maintainer fix-up for the four findings on aethersdr#3530 (the three from the aethersdr-agent review, plus one persistence issue): 1. AprsMessenger: bound the numbered-message dedupe by text AND a 30-min window. The old unbounded msgNo match would silently drop a genuinely-new message that reused a number seen long ago — while still auto-acking it, so neither side noticed. APRS clients cycle small msg numbers freely. 2. AprsPacket::encodeUncompressedPosition: round minutes and carry a 60.00 overflow into the degrees. 38.999999° was formatting as "3860.00N" (min>=60, rejected by every parser, incl. ours) — a GPS-fed beacon hits this near minute boundaries. 3. AprsBeacon: setEnabled()/setIntervalMinutes() now no-op when unchanged. applyAprsConfigFromUi() calls them on every dialog interaction, so the unconditional timer restart let an operator defer a timed beacon indefinitely. 4. AprsStationList + AprsMessenger: persist via QSaveFile (temp + atomic rename) instead of QFile+Truncate, so an unclean shutdown can't truncate the roster / message history (Constitution Principle XIV — the PR's XIV claim covered only the AppSettings blob, not these two sidecar files). Verified: clean build; all six APRS/AX.25 ctests pass (aprs_packet_test, aprs_messenger_test, hdlc_codec_test, tnc_terminal_test + ax25 suite). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@jensenpat — pushed a fix-up commit (
Verified locally: clean build, all six APRS/AX.25 ctests pass. The two non-blocking observations (modem auto-re-enable on retry; full table rebuild per packet) I left as-is — your call. Please |
…ient (aethersdr#3530) Adds an APRS client on the existing AFSK modem: live station table with wireframe symbol icons + weather decode, timed GPS position beacon, and two-way messaging (numbered acks, retries, auto-ack + re-ack, dedupe), all on the shared one-at-a-time TX keying queue. New Qt-Core-only src/core/aprs/ layer (AprsPacket/StationList/Messenger/Beacon/Settings), unit-tested. Settings under a single nested-JSON key (Principle V). Authored by @jensenpat with a maintainer rebase + review fix-up (msg-dedupe bound, beacon minute-overflow + timer-churn, atomic QSaveFile persistence). Co-authored-by: jensenpat <patjensen@gmail.com>
Summary
The AetherModem AX.25 tab becomes a basic, lightweight APRS client: a live station table with wireframe symbol icons and weather decode, a timed GPS position beacon, and two-way APRS messaging — all riding the existing AFSK modem (300/1200 baud) and the shared one-at-a-time TX keying/pacing queue, so APRS traffic can never double-key against KISS clients, the PMS mailbox, or the terminal.
New core layer —
src/core/aprs/(Qt Core only, unit-tested standalone)AprsPacket/A=+ Mic-E altitude, position ambiguity), status, messages, acks/rejs, objects, items, and weather reports (see below). Encoders for the beacon, messages, acks. Maidenhead grid, haversine distance/bearing, symbol names.AprsStationListtnc/aprs-stations.jsonwith coalesced saves.AprsMessenger{msgNo}and retry every 30 s (max 4) until acked; incoming messages addressed to us are stored unread, auto-acked (and re-acked when the sender retries — a duplicate means our ack was lost), de-duplicated; persisted history.AprsBeaconRadioModel::gpsStatusChanged, used when the status reports a lock — FLEX-8000-class/Aurora) with a manual lat/lon fallback. Enabling only arms the timer; "Beacon Now" is the explicit immediate send — nothing transmits by itself on startup restore.AprsSettingsAetherModemAprs), per Constitution Principle V.APRS tab UI
Station table (scrolls, newest-first by default):
AprsSymbolIcons): ~25 programmatic 1.5 px line-art icons (house, vehicles, boats, aircraft, balloon, towers, yagi, weather, services...) drawn at 2× for retina and cached per code+color so they fade with their row; unknown codes fall back to a badge with the raw symbol character. Same icons in the beacon symbol dropdown.Weather decode (APRS 1.0.1 ch. 12): positionless
_reports and position reports with the WX symbol (wind from the CSE/SPD extension, converted knots → mph). Temp, wind/gust, humidity (h00= 100%), pressure (inHg) and rain land in the comment as a readable triple-spaced summary —77°F Wind 220° 4 mph (gust 5) Hum 50% 29.23 inHg. Weather rows get condition-matched wireframe icons (cloud / rain / wind streaks), and wind is scrubbed from course/speed so WX stations don't appear to be driving.Beacon row: my callsign, symbol picker, digi path (default
WIDE1-1,WIDE2-1), enable + interval (1 min–24 h), status text, Beacon Now, and a live position-source readout (GPS DN18rg …/Manual …/none).Message row: destination + text + Send APRS Msg, plus an envelope button with an unread badge opening the APRS Messages window (delivery states — sent (try n) / ✓ acked / ✗ no ack; opening marks read; double-click to reply).
Modem controls: new Autostart at launch checkbox — MainWindow constructs the hidden AetherModem dialog at app launch (same hook as KISS start-on-startup) so APRS decode runs from startup without opening the window.
EKG activity trace: the per-second activity bar graph is replaced by a heart-monitor-style sweep — every decoded frame draws a sharp green QRS heartbeat, FCS failures draw smaller amber bumps, an open receive gate lifts/agitates the baseline, and the trail fades like phosphor afterglow. One spike = one packet, individually countable at APRS rates. The sweep self-stops a few seconds after diagnostics stop flowing (zero idle repaint cost).
Decluttered chrome: the raw decode log, raw AX.25 UI-frame TX row, and Capture 3m / Clear Log buttons are diagnostics tooling — visible only while Packet Activity Debug is on (click the activity trace to toggle). With debug off the station table gets the full viewport. Tab bar slimmed to status-bar height; columns padded; alternating row colors; the dead "Send SMS" / "Send APRS Msg..." / "Connect BBS" placeholder row is removed. KISS/Mailbox tabs keep the shared log; Terminal is unchanged.
What makes it nicer than a bare APRS client
Future ideas (out of scope here): map view, SmartBeaconing (speed/corner-pegging adaptive rate), digipeater mode, APRS-IS IGate, bulletins/announcements, azimuth radar plot of heard stations.
Tests
aprs_packet_test— codec vectors incl. a hand-derived Mic-E reference (33° 25.64′ N / 112° 07.74′ W, 20 kt, 251°), the APRS 1.0.1 compressed-position example, encoder round-trips, grid/distance/bearing, and weather vectors (positionless, position-form knots→mph, sparse...sensors, negative temperature,h00= 100%).aprs_messenger_test— ack matching (wrong-station acks ignored), auto-ack/re-ack, duplicate suppression, unread tracking, own-frame loopback rejection, roster digipeat dedupe.All AX.25/PMS/APRS ctests pass; macOS build is clean. Tested on the air against live 144.390 MHz traffic alongside the Direwolf-derived demod from #3527.
💻 Generated with Claude Code (Fable 5.0) with architecture by @jensenpat