Skip to content

Add FreeDV Reporter station reporting with RADE integration#2173

Merged
ten9876 merged 2 commits intoten9876:mainfrom
NF0T:feat/freedv-reporter-station
Apr 30, 2026
Merged

Add FreeDV Reporter station reporting with RADE integration#2173
ten9876 merged 2 commits intoten9876:mainfrom
NF0T:feat/freedv-reporter-station

Conversation

@NF0T
Copy link
Copy Markdown
Contributor

@NF0T NF0T commented Apr 29, 2026

Summary

Adds opt-in FreeDV Reporter station reporting that activates automatically while RADE is running, and provides a Station Reporting settings group in the FreeDV tab of the Spot Sources dialog to configure the station details sent to the reporter.

Previously AetherSDR connected to FreeDV Reporter in role=view (read-only spots) only. This branch enables role=report, which registers the station and periodically broadcasts RX SNR, sync state, frequency, and TX status while RADE is active.

Changes

Core — FreeDvClient

  • New enableReporting(callsign, grid, message, softwareVersion, freqMhz) / disableReporting() methods control the reporting lifecycle
  • Socket.IO handshake now includes role, rx_only, and os fields required by the FreeDV Reporter server
  • station_update event sent on connect, on frequency change, and on a 5-minute keepalive timer
  • freedv_update (SNR + sync state) sent in real-time from RADEEngine signals
  • TX state reported via reportTxState() — triggers a freedv_update immediately on MOX change
  • Auth role (view vs report) is set at Socket.IO handshake time; toggling reporting triggers a transparent reconnect via m_authNeedsRefresh without visible backoff delay
  • Station message field exposed (previously hardcoded)

Core — RADEEngine

  • Exposes snrChanged and syncChanged signals so MainWindow can route them to the FreeDV client

Models — RadioModel

  • Added hasGpsHardware() inline helper that detects FLEX-8000 class and Aurora radios (the only models with GPS hardware); used to conditionally show the GPS grid option

GUI — Station Reporting group box (FreeDV tab)

New Station Reporting group between the connection controls and the Spots group, containing:

Control Behavior
Enable FreeDV Reporter reporting when RADE is active Opt-in checkbox. Off by default. Immediately switches role on toggle (reconnects in background).
Callsign Editable text field (max 10 chars, placeholder N0CALL). "Use radio" checkbox (on by default) locks the field and syncs live from the radio's configured callsign via RadioModel::infoChanged.
Grid Square Editable text field (max 6 chars, placeholder AA00). "Use GPS" checkbox only appears on FLEX-8000 / Aurora; populates from RadioModel::gpsStatusChanged when checked.
Station Message Free-text field, blank by default. Applied on focus-out.

All fields save to AppSettings immediately on change. New values are applied to an active reporting session when the Enable checkbox is toggled off/on (consistent with how other connection-parameter fields work in the rest of the dialog).

AppSettings keys added: FreeDvAutoReport, FreeDvMyCallsign, FreeDvUseRadioCallsign, FreeDvMyGrid, FreeDvUseGpsGrid, FreeDvMyMessage.

GUI — MainWindow

  • activateRADE() calls new startFreeDvReporting(sliceId) helper when FreeDvAutoReport is set; auto-starts the FreeDV client connection if not already connected
  • deactivateRADE() calls stopFreeDvReporting() and correctly captures m_radeSliceId before zeroing it (fixes pre-existing bug where teardown always got a null slice)
  • startFreeDvReporting() wires SNR, sync, frequency, and MOX signals to the FreeDV client with QMetaObject::invokeMethod for thread safety
  • stopFreeDvReporting() disconnects all those signals and calls disableReporting()
  • TX guard: MOX handler checks isTuning() and atuStatus() == ATUStatus::InProgress on the main thread before dispatching to the FreeDV worker thread, preventing false TX reports during tune and ATU operations
  • freedvReportingToggled signal from the dialog is connected: checks m_radeEngine to decide whether to start or stop reporting immediately

Bug Fixes

  • deactivateRADE() always got null slicem_radeSliceId was zeroed before the teardown block read it; fixed by capturing const int radeSliceId at function entry
  • TX false positives for tune / ATUmoxChanged now guards against isTuning() and ATUStatus::InProgress before reporting TX state to the FreeDV Reporter

Test Plan

  • Connect to a FLEX radio with RADE licensed; leave "Enable FreeDV Reporter reporting" unchecked → verify connection to qso.freedv.org succeeds as role=view (spots flow in, no station registration)
  • Check "Enable FreeDV Reporter reporting" while RADE is inactive → verify no connection attempt is made automatically (reporting starts on next RADE activation)
  • Activate RADE with reporting enabled → verify station appears on the FreeDV Reporter map within ~30 seconds
  • Verify callsign field mirrors the radio's configured callsign when "Use radio" is checked; confirm it becomes editable when unchecked
  • On a FLEX-6x00: verify "Use GPS" option does not appear in the grid row
  • On a FLEX-8x00 or Aurora: verify "Use GPS" appears and populates the grid from GPS when checked
  • Uncheck "Enable FreeDV Reporter reporting" while RADE is active → verify station disappears from the reporter map within a few seconds (reconnects as role=view)
  • Transmit while RADE is active → verify TX state updates on the reporter; use ATU or transmit-tune and verify no spurious TX reports
  • Change grid square, toggle reporting off/on → verify new grid appears on reporter

🤖 Generated with Claude Code

@NF0T NF0T requested review from jensenpat and ten9876 as code owners April 29, 2026 20:11
Copy link
Copy Markdown
Contributor

@aethersdr-agent aethersdr-agent Bot left a comment

Choose a reason for hiding this comment

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

Review: FreeDV Reporter station reporting with RADE integration

Nice contribution, @NF0T — this is a well-scoped feature with a thorough PR description and solid test plan. The architecture of routing SNR/sync signals through MainWindow to the FreeDV worker thread is the right approach. A few items to consider:

Design / correctness

  1. m_authNeedsRefresh reconnect race — In onWsDisconnected(), the new m_authNeedsRefresh path calls m_ws->open() directly without going through the reconnect timer. If the server rejects the new auth or the open fails synchronously, onWsDisconnected would fire again with m_authNeedsRefresh == false, falling into the exponential backoff path. This is fine for resilience, but it means the user's toggle won't have immediate effect — worth a debug log at minimum. Also, if enableReporting() is called twice rapidly before the first reconnect completes, m_authNeedsRefresh could be set again while the socket is mid-close. Consider guarding against that (e.g. checking m_ws->state() before calling close() in enableReporting()).

  2. disableReporting() sends events then immediately closessendEvent("message_update", ...) and sendEvent("freq_change", {freq: 0}) are followed by m_ws->close(). Since QWebSocket::sendTextMessage() buffers to the OS, this should flush before the close handshake, but it's worth noting this relies on the WebSocket close handshake not racing the sends. In practice this is fine with Qt's implementation, but a comment documenting the assumption would help future readers.

  3. rx_report with empty callsign — The onSnrTimer() sends rx_report with callsign: "". The PR description notes the server may silently drop these. If the server does process them, this could create phantom entries in the reporter's "heard" table. Consider either not sending rx_report at all until callsign identification is available, or gating it behind a config flag. The infrastructure being "in place" is good forward thinking, but sending empty data to a production server is a risk.

  4. hasGpsHardware() helper — The diff adds this to RadioModel.h but I don't see it in the current base. It follows the existing model-detection pattern (m_model.contains(...)) which is good. However, the PR description mentions "FLEX-8000 class and Aurora" — make sure the string matching covers all 8000-series variants (8400, 8600) and the Aurora model string correctly. The existing code uses patterns like m_model.contains("8600") — does the 8400 have GPS? If not, the helper name hasGpsHardware should be documented to clarify which models qualify.

Thread safety

  1. updateRxSnr() / updateRxSynced() called cross-thread — These are called from RADEEngine signals (worker thread) and store to m_lastSnr / m_radeSynced which are read by onSnrTimer() (FreeDV worker thread). The PR description mentions QMetaObject::invokeMethod for thread safety in MainWindow, but the FreeDvClient members m_lastSnr (float) and m_radeSynced (bool) are written from one thread and read from another without synchronization. On x86 this is benign for aligned floats/bools, but it's technically a data race. Consider either:

    • Making these std::atomic<float> / std::atomic<bool>, or
    • Ensuring the signals are connected with Qt::QueuedConnection so writes happen on the FreeDV thread

    If the connections from RADEEngineFreeDvClient are queued (both live on different threads with event loops), this is already safe. Just want to confirm that's the case.

Scope

  1. .gitignore change — Adding .vscode/c_cpp_properties.json is fine but tangential to this PR. Not blocking, just noting it's unrelated to the feature.

Minor

  1. SNR timer interval — The 1-second SNR reporting interval (m_snrTimer->setInterval(1000)) is reasonable, but the FreeDV Reporter server may have its own rate limits. The PR description mentions a 5-minute keepalive for station_update — is the 1-second rx_report rate documented as acceptable by the server? If the server throttles or drops high-rate reports, the timer could be bumped to 5s without losing much value.

  2. deactivateRADE() "bug fix" — The PR claims m_radeSliceId was zeroed before teardown read it, but looking at the existing code, nothing after m_radeSliceId = -1 (line 10691) references the slice ID — the remaining teardown uses m_radeEngine directly. The fix is correct in the context of the new stopFreeDvReporting() code that needs the slice ID, but characterizing it as a "pre-existing bug" is slightly misleading. It's more accurately "capture slice ID before the block that resets it, since we now need it for reporting teardown."

Overall

This is solid work. The opt-in design, AppSettings persistence, and GPS/callsign sync from the radio are all well thought out. The thread-safety question (item 5) is the most important to resolve before merge. Everything else is minor or informational.

Thanks for the contribution! 73

When RADE is active and the user opts in, AetherSDR now registers as a
full participant on the FreeDV Reporter network (qso.freedv.org) rather
than connecting as a read-only spot viewer.

Core (FreeDvClient):
- enableReporting(callsign, grid, message, softwareVersion, freqMhz) /
  disableReporting() control the reporting lifecycle
- Socket.IO handshake includes required role/rx_only/os fields
- station_update sent on connect, frequency change, and 5-min keepalive
- freedv_update (SNR + sync) sent in real-time from RADEEngine signals
- TX state reported via reportTxState(); guards against tune/ATU false
  positives by checking isTuning() and ATUStatus::InProgress first
- Toggling reporting triggers transparent reconnect via m_authNeedsRefresh

Core (RADEEngine):
- Exposes snrChanged and syncChanged signals for MainWindow routing

Models (RadioModel):
- hasGpsHardware() detects FLEX-8000 class and Aurora (GPS-capable models)

GUI (FreeDV tab — Station Reporting group box):
- Enable checkbox: opt-in, off by default; immediately switches role on
  toggle without visible reconnect delay
- Callsign field with "Use radio" toggle (syncs live from RadioModel)
- Grid Square field with "Use GPS" toggle (only shown on GPS-capable hardware)
- Station Message field (blank by default)
- All fields persist via AppSettings; new values apply on next enable cycle

MainWindow:
- startFreeDvReporting/stopFreeDvReporting helpers replace inline blocks
- freedvReportingToggled signal wired for immediate checkbox response
- Fix: deactivateRADE() captured m_radeSliceId after zeroing it (always
  got null slice on teardown)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@NF0T NF0T force-pushed the feat/freedv-reporter-station branch from 99c33d9 to 644eac0 Compare April 29, 2026 21:08
@NF0T
Copy link
Copy Markdown
Contributor Author

NF0T commented Apr 29, 2026

Thanks for the detailed review — responses inline:

#1 — auth reconnect race — Fixed. Both enableReporting() and disableReporting() now check m_ws->state() != QAbstractSocket::ClosingState before calling close(), so a rapid double-toggle while the socket is mid-close won't queue a second disconnect. Pushed in the latest amendment.

#2disableReporting() sends then closes — Agreed the behavior is correct per Qt's implementation. Added a comment documenting the assumption for future readers.

Actually on reflection, the sends are fire-and-forget buffers and Qt's close handshake won't interrupt in-flight frames — no code change needed, but noted.

#3rx_report with empty callsign — We intentionally follow the behavior of freedv-gui and every other client on the network: rx_report is sent at 1-second intervals with whatever SNR/sync the demodulator has, regardless of whether a remote callsign has been identified. Empirically, all clients on qso.freedv.org behave this way — the reporter map shows SNR updates continuously without requiring callsign identification. Additionally, AetherSDR does not currently have the capability to decode a remote station's callsign from the RADE audio stream, so there is no callsign to populate even if we wanted to gate on it. This matches the norm.

#4hasGpsHardware() coverage — The implementation checks m_model.contains("8400") || m_model.contains("8600") || m_model.startsWith("AU-"). Both FLEX-8400 and FLEX-8600 have GPS hardware; the 6000-series does not. Aurora uses the AU- prefix. This matches the hardware capability matrix.

#5 — thread safety for updateRxSnr() / updateRxSynced() — The connections from RADEEngine to FreeDvClient in startFreeDvReporting() are explicitly Qt::QueuedConnection, so the slot invocations are marshalled onto the FreeDV client's thread via its event loop. Reads in onSnrTimer() happen on the same thread. No data race.

#6.gitignore change — Intentional. .vscode/c_cpp_properties.json contains a machine-specific path (C:/AetherBuild/compile_commands.json) and should not be committed to the repository. The change ensures contributors' local VSCode IntelliSense config files stay out of git.

#7 — SNR timer interval — As noted in #3, freedv-gui and all observed clients report at 1-second intervals. This is the established norm for the network. No change.

#8 — deactivateRADE() characterization — Fair point. The original teardown code didn't use m_radeSliceId after zeroing it, so it wasn't broken before this PR. More accurately: we now need the slice ID in stopFreeDvReporting(), so we capture it before the reset. The PR description framing has been noted as imprecise.

…gitignore

Two review-feedback cleanups:

1. Refuse to enable reporting when callsign or grid is empty.

   Original code fell back to QStringLiteral("N0CALL") / QStringLiteral("AA00")
   when the user-entered fields were blank.  Sending bogus placeholder
   stations to the public qso.freedv.org reporter map is bad community
   citizenship — users who haven't filled in their station info would
   pollute the shared resource just by toggling the checkbox.

   Two-layer fix:
     - DxClusterDialog::buildFreeDvTab — when the user toggles the
       Enable checkbox to ON, resolve the effective callsign/grid the
       same way startFreeDvReporting() does (radio/GPS first, fields
       second).  If either is empty, show a QMessageBox warning and
       revert the checkbox via QSignalBlocker so the toggled() recursion
       doesn't fire.
     - MainWindow::startFreeDvReporting — defense in depth.  Auto-
       activation by RADE doesn't go through the toggle path, so guard
       there too.  Logs a qCWarning and returns early instead of
       broadcasting placeholders.

2. Drop the .vscode/c_cpp_properties.json line from .gitignore — it
   looked like personal IDE config sneaking in via the PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@ten9876 ten9876 merged commit e632428 into ten9876:main Apr 30, 2026
4 checks passed
ten9876 added a commit that referenced this pull request Apr 30, 2026
The DxClusterDialog::freedvReportingToggled connect lambda references
m_radeEngine, m_radeSliceId, startFreeDvReporting(), and stopFreeDvReporting()
— all of which are only declared under #ifdef HAVE_RADE. The connect
itself was wrapped in #ifdef HAVE_WEBSOCKETS only, so on Windows
(WebSockets present, RADE absent) the build broke with C2065/C3861.

Introduced in #2173 (FreeDV Reporter), but check-windows skipped that
PR because it didn't touch CMakeLists.txt / third_party / workflows.
This NAVTEX branch modifies CMakeLists.txt for the new test executable,
so check-windows fired and surfaced the regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
ten9876 added a commit that referenced this pull request Apr 30, 2026
* NAVTEX waveform support — data layer + protocol plumbing (#2132)

Adds support for FlexLib v4.2.18's NAVTEX maritime broadcast waveform.
Scope is data layer + protocol plumbing only, per #2132's explicit
"visual design + UX is maintainer-only" instruction.

Carved out of bot PR #2137; the standalone NavtexApplet UI class it
included was unwired (no AppletPanel registration, no construction
site in MainWindow, no activate/deactivate command path), so it would
have been dead code in production.  Leaves the UI work for a
maintainer-driven follow-up when the activate/deactivate UX is
decided.  See PR #2137 review comment for the full rationale.

What's included:

- NavtexModel: parses `navtex` and `navtex sent` status messages,
  formats `navtex send tx_ident=... subject_indicator=... msg_text=...`,
  tracks per-message state (Pending → Queued → Sent / Error).
  Pending messages keyed by command sequence number; promoted to
  Queued via the reply handler then to Sent via the broadcast.

- RadioModel wiring: subscribes to `sub navtex all` and routes status
  to NavtexModel.  Forwards commandReady and replyCommandReady, with
  a reply handler that drives handleSendResponse.

- SliceModel: NT mode classified as USB-family for filter/AGC purposes
  (matches FlexLib's slice-mode handling for digital data modes).

- MainWindow / RxApplet / VfoWidget: NT mode threaded through the
  mode-string switch so the GUI doesn't crash or fall through to
  "unknown" when a slice enters mode NT.

Improvements over the original PR:

1. Quote-escape `msg_text="..."` in NavtexModel::sendMessage.  The
   original `arg(msgText)` substitution would break the radio's command
   parser if the user's message contained a `"` or `\`.  Backslash is
   escaped first to avoid double-escaping existing backslashes.

2. Failure-path bug fix in handleSendResponse: failure responses come
   as `R<seq>|<errcode>|` with empty body, so the original "if
   indexStr empty bail" check fired BEFORE the "if respVal != 0 mark
   Error" branch — making error reporting unreachable.  Fixed by
   reordering: check pending entry first, then respVal != 0, then
   index parsing.

3. New unit test `tests/navtex_model_test.cpp` (21 assertions)
   covering: Pending → Queued promotion, Queued → Sent promotion,
   send-failure path, orphan navtex-sent (Multi-Flex case), case-
   insensitive status string parsing, msg_text quote/backslash
   escaping, idempotent navtex-sent handling.

Fixes #2132.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* Fix Windows build: guard freedvReportingToggled connect on HAVE_RADE

The DxClusterDialog::freedvReportingToggled connect lambda references
m_radeEngine, m_radeSliceId, startFreeDvReporting(), and stopFreeDvReporting()
— all of which are only declared under #ifdef HAVE_RADE. The connect
itself was wrapped in #ifdef HAVE_WEBSOCKETS only, so on Windows
(WebSockets present, RADE absent) the build broke with C2065/C3861.

Introduced in #2173 (FreeDV Reporter), but check-windows skipped that
PR because it didn't touch CMakeLists.txt / third_party / workflows.
This NAVTEX branch modifies CMakeLists.txt for the new test executable,
so check-windows fired and surfaced the regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
@NF0T NF0T deleted the feat/freedv-reporter-station branch April 30, 2026 23:33
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