Skip to content

feat(aprs): Add a lightweight APRS client to AetherModem AX.25 tab#3530

Merged
ten9876 merged 7 commits into
aethersdr:mainfrom
jensenpat:feat/aprs-client
Jun 14, 2026
Merged

feat(aprs): Add a lightweight APRS client to AetherModem AX.25 tab#3530
ten9876 merged 7 commits into
aethersdr:mainfrom
jensenpat:feat/aprs-client

Conversation

@jensenpat

@jensenpat jensenpat commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator
image

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)

Class What it does
AprsPacket APRS 1.0.1 info-field codec. Parses uncompressed, compressed (base-91) and Mic-E positions (course/speed, /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.
AprsStationList Station roster: merges position/movement/free-text/weather across packet types per station; suppresses digipeated duplicates (same source+info within 30 s) so packet counts reflect originated traffic; persists to tnc/aprs-stations.json with coalesced saves.
AprsMessenger Messaging engine: outgoing messages get a sequential {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.
AprsBeacon Interval position beacon fed by the radio's onboard GPS (RadioModel::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.
AprsSettings Nested-JSON config blob under a single AppSettings key (AetherModemAprs), per Constitution Principle V.

APRS tab UI

Station table (scrolls, newest-first by default):

  • Columns: local-time TIME, station, symbol, ticking AGE, packets, grid, distance + bearing from our own position, course/speed, status/comment.
  • Sortable headers with numeric sort keys for TIME / AGE / PKTS / DIST / CRS/SPD; unknown distances sort to the bottom; the chosen sort survives rebuilds.
  • Age-fade rows: full brightness ≤ 5 min, linear fade to 50% at 30 min — active stations pop, the overnight tail recedes without disappearing.
  • Wireframe symbol icons (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.
  • Right-click: Send Message…, Station Info… (full-detail popup), View on aprs.fi, and Clear All Stations… (confirmation; also clears the saved roster). Double-click prefills the message destination.

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

  • Distance/bearing computed from your own (GPS or manual) position right in the table.
  • Proper messaging semantics: retries with message numbers, auto-ack + re-ack on sender retry, duplicate suppression.
  • Digipeat de-duplication keeps station packet counts honest on busy WIDE2 channels.
  • Weather stations read as weather — decoded summary and condition icons, not raw token soup.
  • One TX queue shared with KISS/PMS/terminal — no PTT contention by construction.

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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

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 failed

Two checks failed on this commit, and they're the same root cause:

  • build (Linux) — failed at the Build step (run 27382328496)
  • analyze (cpp) (CodeQL) — also failed at its Build step (run 27382328502); the analysis itself was skipped because the compile didn't finish

check-macos and check-windows both passed.

Root cause: QTimeZone::UTC requires Qt 6.5, but Linux CI builds against Qt 6.4.2

Both failing jobs build inside the ghcr.io/ten9876/aethersdr-ci container, which is Ubuntu 24.04 (.github/docker/Dockerfile) — its qt6-base-dev is Qt 6.4.2. The lightweight time-zone enum QTimeZone::UTC (i.e. QTimeZone::Initialization::UTC) was only added in Qt 6.5, so this line doesn't compile there:

item->data(Qt::UserRole).toLongLong(), QTimeZone::UTC);

It's the only occurrence in the diff. macOS (Homebrew Qt ~6.10) and Windows (Qt 6.8.3 per ci.yml) both have the enum, which is why they're green — and why your local macOS build was clean.

This also explains why your earlier fix (4a791e8, adding #include <QTimeZone>) didn't clear it: the include turned an "incomplete type" error into a "no such member" error, because on Qt 6.4 the enum simply doesn't exist regardless of headers. (The project's floor is find_package(Qt6 6.2 ...) in CMakeLists.txt:62, so this is a real compatibility issue rather than CI infrastructure flakiness.)

Suggested fix (one line)

Use the function form, which has been around since Qt 5 and is the existing convention in this codebase (src/gui/SpectrumWidget.cpp:1769 does exactly this):

const QDateTime lastHeard = QDateTime::fromMSecsSinceEpoch(
    item->data(Qt::UserRole).toLongLong(), QTimeZone::utc());

(Qt::UTC as in src/models/SpotModel.cpp:40 would also work, but QTimeZone::utc() avoids the deprecated-ish Qt::TimeSpec overload.) The #include <QTimeZone> you added can stay — utc() needs it too.

One small caveat for transparency: the CI log API only exposed step-level status to me, so the diagnosis above is derived from the diff, the container's Qt version, and the pass/fail pattern across platforms rather than a quoted compiler line. If the next push still fails after this change, ping me and I'll dig deeper.

Everything else in the PR builds cleanly on the two newer-Qt platforms, so this one-liner should get you fully green. Thanks again for volunteering the time on this — looking forward to seeing the EKG trace in action! 73


🤖 aethersdr-agent · cost: $18.3563 · model: claude-fable-5

@jensenpat jensenpat marked this pull request as ready for review June 12, 2026 00:49
@jensenpat jensenpat requested review from a team as code owners June 12, 2026 00:49

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: enqueueAprsTx re-checks m_enableDecode and 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::changedrefreshAprsStationTable() rebuilds all rows plus resizeColumnsToContents() 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

@ten9876

ten9876 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Heads-up — main moved under the #3351 MainWindow decomposition while this PR was open, so it'll need a rebase before merge. Trial-merge against current main shows a conflict in src/gui/MainWindow.cpp and CMakeLists.txt (the CMake conflict is the new-TU source-list ordering — keep both your entry and the MainWindow_*.cpp entries).

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 (MainWindow_Session.cpp, _Wiring.cpp, _DigitalModes.cpp, _Menus.cpp, _Controllers.cpp, _Shortcuts.cpp, _SwrSweep.cpp, _Spots.cpp, _DspApplets.cpp) — put the change there rather than back in MainWindow.cpp. Most likely your hunk is untouched and git rebase origin/main resolves cleanly.

Happy to take the rebase off your hands if you'd prefer — just say so.

jensenpat and others added 6 commits June 13, 2026 17:35
…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>
@ten9876 ten9876 force-pushed the feat/aprs-client branch from a295196 to 84c4b94 Compare June 14, 2026 00:40
@ten9876

ten9876 commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

@jensenpat — I took the rebase onto current main (offered earlier) and force-pushed to feat/aprs-client (head 84c4b944). Your six commits are preserved with your authorship; I signed the rebased commits with the maintainer key. Please git fetch before any further local work.

The rebase resolved one conflict, and it's the textbook #3351 case worth calling out: your modem-autostart commit edited startKissTncOnStartupIfConfigured() in MainWindow.cpp, but that method moved to MainWindow_DigitalModes.cpp (Phase 1e — it owns AX.25/AetherModem/KISS startup). So per the new TU architecture:

  • The AprsSettings::modemAutostart() condition (+ the comment update) now lives in MainWindow_DigitalModes.cpp::startKissTncOnStartupIfConfigured(), next to the KISS-startup logic it extends.
  • The #include "core/aprs/AprsSettings.h" moved there too (it was the only user).
  • MainWindow.cpp now has zero net change vs main — all your APRS wiring is in the sibling TU where it belongs.

Everything else (the src/core/aprs/ layer, the GUI dialogs, CMakeLists.txt, tests) applied cleanly. Verified locally: clean build, and all six APRS/AX.25 ctests pass (aprs_packet_test, aprs_messenger_test, hdlc_codec_test, tnc_terminal_test, + the ax25 suite).

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 docs/architecture/mainwindow-decomposition.md — pending in #3564 — documents exactly this "hunk fell in a relocated region → put it in the sibling TU" rule.)

@jensenpat jensenpat changed the title feat(aprs): turn the AetherModem AX.25 tab into a lightweight APRS client feat(aprs): Add a lightweight APRS client to AetherModem AX.25 tab Jun 14, 2026
…, 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>
@ten9876

ten9876 commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

@jensenpat — pushed a fix-up commit (e64f76a, fast-forward on top of the rebase) addressing all four review findings. The APRS layer is genuinely nice work; these are small, contained fixes.

  1. Numbered-message dedupe (bot PHONE applet: DEXP commands rejected on firmware v1.4.0.0 #1) — now bounded by text + a 30-min window. The old unbounded msgNo match would silently drop a genuinely-new message that reused a number we stored long ago (while still auto-acking it, so neither side noticed). Bounding on text too means an immediate msgNo reuse with different text is still accepted.
  2. Beacon 60.00 minutes (bot build(deps): Bump github/codeql-action from 3 to 4 #2) — encodeUncompressedPosition now rounds minutes and carries the overflow into degrees, so 38.999999° emits 3900.00N, not the invalid 3860.00N. (Verified aprs_packet_test still green.)
  3. Beacon timer churn (bot build(deps): Bump actions/checkout from 4 to 6 #3) — setEnabled()/setIntervalMinutes() now no-op when unchanged, so applyAprsConfigFromUi() firing on every dialog interaction can't defer a timed beacon.
  4. Atomic persistence (Principle XIV) — AprsStationList + AprsMessenger now persist via QSaveFile (temp + atomic rename) instead of QFile+Truncate. The PR's XIV claim covered the AppSettings blob but not these two sidecar JSON files; an unclean shutdown mid-write could've truncated the roster / message history.

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 git fetch before further local work. Still needs a non-author CODEOWNER approval to merge.

@ten9876 ten9876 merged commit 0d35e26 into aethersdr:main Jun 14, 2026
6 checks passed
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants