feat(mqtt): publish CW decoded text to aethersdr/cw/decode#3216
Conversation
Connects both the RX and TX CW decoders to a new
publishCwDecodeMqtt() helper that fires whenever the decoder
emits a character. Payload is compact JSON:
{"text":"K","freq":14.025,"rx":true}
rx:false marks text from the TX sidetone decoder. freq is the
active slice frequency in MHz; omitted if no slice is active.
Publishing is a no-op when MQTT is disconnected.
This makes the decoded text stream available to any MQTT subscriber
(e.g. a contest logger) without adding a new AetherSDR interface.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The decoder emits every character with a confidence cost value. PanadapterApplet already gates display on m_cwCostThreshold; the MQTT publish now checks the same threshold so the browser feed matches the decoder window exactly without any separate setting. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bail when m_cwDecoderApplet is null (no panel active = nothing displayed = nothing to publish); previous logic inverted the guard - Drop redundant isConnected() check — MqttClient::publish() already no-ops silently when disconnected - Mirror PanadapterApplet text normalization: replace \n with space, and drop whitespace-only TX chunks (matching appendCwTextTx:394) - Publish the normalized text rather than the raw ggmorse emission Addresses feedback from aethersdr#3215 triage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@ten9876 An FYI, didn't find another way to tell you: I've developed a minimal FlexRadio emulator. I can connect to it from AetherSDR, just like my 6500 on my network (discovery works). I'm currently building it out as a contest simulator (it generates CW contest QSO's for practice). But, I plan to eventually populate all the API functions with stubs that return something sane. If you are interested in using it for automated testing that doesn't rely on a real FlexRadio, drop me a line. |
There was a problem hiding this comment.
Nice clean addition — thanks @K5PTB. Verified against the surrounding code:
Looks good
- New
connectcalls andpublishCwDecodeMqttdefinition both live inside theHAVE_MQTTblock, matching the header declaration. - Threshold +
\n→space+ drop-empty-TX exactly mirrorPanadapterApplet::appendCwText/appendCwTextTx, so the MQTT feed and panel can't drift. - Dropping the
isConnected()check is safe —MqttClient::publish()already short-circuits on!m_connected || !m_mosq(src/core/MqttClient.cpp:235). SliceModel::frequency()is already MHz (src/models/SliceModel.h:38), so the documented"freq":14.025payload shape is correct without unit conversion.m_cwDecoderAppletis aQPointer<PanadapterApplet>, so the!m_cwDecoderAppletguard correctly handles a destroyed panel as well as never-set.m_cwDecoder/m_cwDecoderTxare direct MainWindow members, sotextDecodedfires on the GUI thread — no cross-thread/QoS concerns for the publish path.
Minor observation (not blocking)
- One asymmetry worth noting: the topic name
aethersdr/cw/decodeis hardcoded at the call site. If a future PR adds a second decode source (e.g. RTTY) it'll want to follow the same pattern, so factoring out akCwDecodeTopicconstant later wouldn't hurt — but not worth churn now for a single call site.
Happy with this as a contained, well-tested addition.
🤖 aethersdr-agent · cost: $13.0835 · model: claude-opus-4-7
…nfo panel Addresses the minor observation from the aethersdr#3216 review — factors out "aethersdr/cw/decode" into an inline constexpr QLatin1String in MqttSettings.h so the call site in MainWindow and the new settings dialog info panel share the same definition and can't drift. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nfo panel Addresses the minor observation from the aethersdr#3216 review — factors out "aethersdr/cw/decode" into an inline constexpr QLatin1String in MqttSettings.h so the call site in MainWindow and the new settings dialog info panel share the same definition and can't drift. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tab (#3251) Closes #3249. ## Summary The Subscriptions tab already has an **Internal AetherSDR Topics** group box listing topics AetherSDR subscribes to automatically. The Publish Buttons tab had no equivalent, leaving `aethersdr/cw/decode` — published for every decoded CW character since #3216 — undiscoverable without reading source. - Adds a matching read-only group box to the bottom of the Publish Buttons tab; label text is mouse-selectable for copy-paste into subscriber configs - Adds `internalMqttPublishTopics()` to `MqttSettings.h/.cpp` alongside the existing `internalMqttSubscriptionTopics()` for a machine-readable topic list - Extracts the hardcoded `"aethersdr/cw/decode"` string into `inline constexpr QLatin1String kCwDecodeTopic` in `MqttSettings.h`; the publish call in `MainWindow` and the new settings panel now share the same definition — addressing the minor observation from the #3216 review ## Files changed | File | Change | |---|---| | `src/core/MqttSettings.h` | `kCwDecodeTopic` constant + `internalMqttPublishTopics()` declaration | | `src/core/MqttSettings.cpp` | `internalMqttPublishTopics()` implementation | | `src/gui/MqttSettingsDialog.cpp` | Group box added to Publish Buttons tab | | `src/gui/MainWindow.cpp` | Publish call uses `kCwDecodeTopic` | ## Test plan - [x] Build clean (macOS, no warnings from changed files) - [x] MQTT settings dialog → Publish Buttons tab shows **Internal AetherSDR Topics** group box with `aethersdr/cw/decode` - [x] Label text is mouse-selectable - [x] Subscriptions tab unchanged - [x] No behavior change — UI only 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.com>
…gging (#3460) ## Motivation Addresses the feature requests and concerns raised in #3388. The existing MQTT implementation published only antenna/VFO topics with no user control over which topics were active. This PR adds opt-in control for all new topics, extends the CW decode payload, and adds bidirectional MQTT support for the CW keyer, radio state, and AX.25 packet radio. ## Changes ### Per-topic opt-in control A new `InternalMqttTopicDef` struct serves as the single source of truth for the topic registry, replacing the previous ad-hoc approach. Each topic carries a `gateable` flag: - **Non-gateable topics** (the existing antenna alias topics) remain always-on and cannot be disabled, preserving the original behavior. They appear checked and greyed-out in the settings dialog. - **`aethersdr/cw/decode`** is gateable but **defaults to enabled** (`defaultEnabled=true`), preserving the behavior of existing relay scripts and loggers without requiring any user action after upgrade. - **All other new topics** — `aethersdr/cw/transmit`, `aethersdr/radio/state`, `aethersdr/ax25/rx`, `aethersdr/ax25/tx` — are gateable and default to `false` (disabled). Users explicitly opt in via the MQTT settings dialog. `isMqttTopicEnabled()` / `setMqttTopicEnabled()` are backed by `QSettings`. ### `aethersdr/cw/decode` — payload additions and gating change `pitch_hz` and `speed_wpm` are now included in the payload when their values are non-zero, allowing subscribers to mirror the operator's keying characteristics in replies. > **Note for existing users:** `aethersdr/cw/decode` was introduced in v26.6.1 (#3216) as an always-active publish topic. This PR brings it under the same per-topic opt-in mechanism as all other gateable topics, but it **defaults to enabled** for backward compatibility. Users relying on this topic do not need to take any action after upgrading. > The `kCwDecodeTopic` string constant was consolidated per the feedback in the #3216 review; the topic path itself is unchanged. ### `aethersdr/cw/transmit` — subscribe (opt-in) AetherSDR subscribes to this topic and feeds received text to the CWX keyer. Payload: `{"text": "K5PTB DE W1ABC", "speed_wpm": 28, "pitch_hz": 600}` `speed_wpm` and `pitch_hz` are optional; omitting them uses the current keyer settings. ### `aethersdr/radio/state` — publish (opt-in) Published on any frequency, mode, or TX state change: ```json {"slice": "A", "freq": 14.025, "mode": "CW", "tx": false} ``` ### `aethersdr/ax25/rx` — publish (opt-in) Each decoded AX.25 frame is published: ```json { "from": "K5PTB", "to": "APRS", "via": ["WIDE1-1", "WIDE2-1"], "payload": ">Running AetherSDR", "freq": 144.390, "confidence": 0.87 } ``` ### `aethersdr/ax25/tx` — subscribe (opt-in) AetherSDR subscribes and queues received frames for 1200-baud Bell 202 transmission. Payload: TNC2 string — `K5PTB>APRS,WIDE1-1:>payload` > **Initialization note:** The AX.25 MQTT handler lives in the AetherModem dialog, which is lazy-initialized. The `ax25/tx` topic will not be active until the dialog has been initialized — either by opening the AetherModem window, or by enabling **Start KISS TNC on startup** in the AetherModem KISS TNC settings. With that option enabled, the dialog is created silently at launch (no window appears) and `ax25/tx` is available immediately without any user interaction. ### Debug logging All MQTT publish and receive events are written to the AetherSDR debug console when the debug logging option is enabled. No applet-level display is added for these topics. ## Design decisions (re: #3388) **On TCI overlap (`radio/state`):** TCI and MQTT carry overlapping information, but they serve different clients. A subscriber using only MQTT for external tool integration (contest loggers, automation scripts) should not need a second TCI connection. The ordering concern — CW decode characters arriving before a `tx=false` state update — is not a practical issue: both originate from the same internal event bus, so MQTT message ordering between them is deterministic for a single publisher. **On keeping AX.25 topics:** The `ax25/rx` and `ax25/tx` topics are not debug conveniences; they are the primary path for external tools (APRS clients, packet BBS clients, automation scripts) to consume decoded frames without maintaining a persistent KISS TCP connection. They have been in live use for multi-day AX.25 packet decode testing. **On PR structure:** The per-topic gating infrastructure (`InternalMqttTopicDef`, `isMqttTopicEnabled`) is shared across all the new topics. Splitting would require duplicating or pre-landing that infrastructure separately. Happy to split if preferred — the topics are otherwise independent. ## What does not change - Existing antenna/VFO alias topics remain always-on and non-disableable - `aethersdr/cw/decode` topic path is unchanged - No changes to KISS TNC, TCI server, or CAT/rigctld paths ## Testing Verified end-to-end on all new topics: publish events received by a Mosquitto subscriber, inbound `cw/transmit` messages keyed through CWX, `radio/state` updates on band change, and `ax25/rx` frames from live 144.390 MHz traffic. `ax25/tx` (transmit path) has not been exercised on-air pending transverter calibration. The AX.25 MQTT path has been in continuous use for multi-day packet decode comparison testing on a Raspberry Pi 5 (2,000+ frames across 12-hour runs, three-decoder comparison against Graywolf and Direwolf). The `cw/transmit` and `radio/state` topics were used in live contest operating on a MacBook Air M2 during the 2026 Atlantic Canada QSO Party. Screenshots of the updated MQTT settings dialog are in #3388. --- Built and smoke tested on: Windows 11 on Intel i3 (integrated GPU) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…#3216) ## Summary - Connects both the RX and TX CW decoders to a new `publishCwDecodeMqtt()` helper in `MainWindow` that fires on every `textDecoded` emission - Applies the active panel's `m_cwCostThreshold` so the MQTT feed matches exactly what the CW decoder window displays - Mirrors `PanadapterApplet` text normalization: `\n` → space, drop whitespace-only TX chunks - Bails silently when no CW panel is active or MQTT is disconnected ## Payload format ```json {"text":"K","freq":14.025,"rx":true} ``` - `text` — normalized decoded character(s) - `freq` — active slice frequency in MHz (omitted if no slice active) - `rx` — `false` for TX sidetone decoder output ## Commit breakdown 1. `feat`: core wiring — `publishCwDecodeMqtt()` + two `connect` calls in the `HAVE_MQTT` block 2. `fix`: add `cwCostThreshold()` public getter on `PanadapterApplet`; apply threshold in the publisher 3. `fix`: triage feedback — invert null-applet guard, drop redundant `isConnected()` check, add text normalization ## Test plan - [ ] Enable MQTT in AetherSDR (Settings → MQTT → localhost:1883) - [ ] Tune to a CW signal with the decoder running - [ ] Confirm `mosquitto_sub -t aethersdr/cw/decode -v` shows the same characters as the decoder panel - [ ] Adjust sensitivity slider — confirm MQTT feed changes in lockstep - [ ] Disable MQTT — confirm no errors logged - [ ] No CW panel open — confirm nothing published ## Related Closes aethersdr#3215 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…tab (aethersdr#3251) Closes aethersdr#3249. ## Summary The Subscriptions tab already has an **Internal AetherSDR Topics** group box listing topics AetherSDR subscribes to automatically. The Publish Buttons tab had no equivalent, leaving `aethersdr/cw/decode` — published for every decoded CW character since aethersdr#3216 — undiscoverable without reading source. - Adds a matching read-only group box to the bottom of the Publish Buttons tab; label text is mouse-selectable for copy-paste into subscriber configs - Adds `internalMqttPublishTopics()` to `MqttSettings.h/.cpp` alongside the existing `internalMqttSubscriptionTopics()` for a machine-readable topic list - Extracts the hardcoded `"aethersdr/cw/decode"` string into `inline constexpr QLatin1String kCwDecodeTopic` in `MqttSettings.h`; the publish call in `MainWindow` and the new settings panel now share the same definition — addressing the minor observation from the aethersdr#3216 review ## Files changed | File | Change | |---|---| | `src/core/MqttSettings.h` | `kCwDecodeTopic` constant + `internalMqttPublishTopics()` declaration | | `src/core/MqttSettings.cpp` | `internalMqttPublishTopics()` implementation | | `src/gui/MqttSettingsDialog.cpp` | Group box added to Publish Buttons tab | | `src/gui/MainWindow.cpp` | Publish call uses `kCwDecodeTopic` | ## Test plan - [x] Build clean (macOS, no warnings from changed files) - [x] MQTT settings dialog → Publish Buttons tab shows **Internal AetherSDR Topics** group box with `aethersdr/cw/decode` - [x] Label text is mouse-selectable - [x] Subscriptions tab unchanged - [x] No behavior change — UI only 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.com>
…gging (aethersdr#3460) ## Motivation Addresses the feature requests and concerns raised in aethersdr#3388. The existing MQTT implementation published only antenna/VFO topics with no user control over which topics were active. This PR adds opt-in control for all new topics, extends the CW decode payload, and adds bidirectional MQTT support for the CW keyer, radio state, and AX.25 packet radio. ## Changes ### Per-topic opt-in control A new `InternalMqttTopicDef` struct serves as the single source of truth for the topic registry, replacing the previous ad-hoc approach. Each topic carries a `gateable` flag: - **Non-gateable topics** (the existing antenna alias topics) remain always-on and cannot be disabled, preserving the original behavior. They appear checked and greyed-out in the settings dialog. - **`aethersdr/cw/decode`** is gateable but **defaults to enabled** (`defaultEnabled=true`), preserving the behavior of existing relay scripts and loggers without requiring any user action after upgrade. - **All other new topics** — `aethersdr/cw/transmit`, `aethersdr/radio/state`, `aethersdr/ax25/rx`, `aethersdr/ax25/tx` — are gateable and default to `false` (disabled). Users explicitly opt in via the MQTT settings dialog. `isMqttTopicEnabled()` / `setMqttTopicEnabled()` are backed by `QSettings`. ### `aethersdr/cw/decode` — payload additions and gating change `pitch_hz` and `speed_wpm` are now included in the payload when their values are non-zero, allowing subscribers to mirror the operator's keying characteristics in replies. > **Note for existing users:** `aethersdr/cw/decode` was introduced in v26.6.1 (aethersdr#3216) as an always-active publish topic. This PR brings it under the same per-topic opt-in mechanism as all other gateable topics, but it **defaults to enabled** for backward compatibility. Users relying on this topic do not need to take any action after upgrading. > The `kCwDecodeTopic` string constant was consolidated per the feedback in the aethersdr#3216 review; the topic path itself is unchanged. ### `aethersdr/cw/transmit` — subscribe (opt-in) AetherSDR subscribes to this topic and feeds received text to the CWX keyer. Payload: `{"text": "K5PTB DE W1ABC", "speed_wpm": 28, "pitch_hz": 600}` `speed_wpm` and `pitch_hz` are optional; omitting them uses the current keyer settings. ### `aethersdr/radio/state` — publish (opt-in) Published on any frequency, mode, or TX state change: ```json {"slice": "A", "freq": 14.025, "mode": "CW", "tx": false} ``` ### `aethersdr/ax25/rx` — publish (opt-in) Each decoded AX.25 frame is published: ```json { "from": "K5PTB", "to": "APRS", "via": ["WIDE1-1", "WIDE2-1"], "payload": ">Running AetherSDR", "freq": 144.390, "confidence": 0.87 } ``` ### `aethersdr/ax25/tx` — subscribe (opt-in) AetherSDR subscribes and queues received frames for 1200-baud Bell 202 transmission. Payload: TNC2 string — `K5PTB>APRS,WIDE1-1:>payload` > **Initialization note:** The AX.25 MQTT handler lives in the AetherModem dialog, which is lazy-initialized. The `ax25/tx` topic will not be active until the dialog has been initialized — either by opening the AetherModem window, or by enabling **Start KISS TNC on startup** in the AetherModem KISS TNC settings. With that option enabled, the dialog is created silently at launch (no window appears) and `ax25/tx` is available immediately without any user interaction. ### Debug logging All MQTT publish and receive events are written to the AetherSDR debug console when the debug logging option is enabled. No applet-level display is added for these topics. ## Design decisions (re: aethersdr#3388) **On TCI overlap (`radio/state`):** TCI and MQTT carry overlapping information, but they serve different clients. A subscriber using only MQTT for external tool integration (contest loggers, automation scripts) should not need a second TCI connection. The ordering concern — CW decode characters arriving before a `tx=false` state update — is not a practical issue: both originate from the same internal event bus, so MQTT message ordering between them is deterministic for a single publisher. **On keeping AX.25 topics:** The `ax25/rx` and `ax25/tx` topics are not debug conveniences; they are the primary path for external tools (APRS clients, packet BBS clients, automation scripts) to consume decoded frames without maintaining a persistent KISS TCP connection. They have been in live use for multi-day AX.25 packet decode testing. **On PR structure:** The per-topic gating infrastructure (`InternalMqttTopicDef`, `isMqttTopicEnabled`) is shared across all the new topics. Splitting would require duplicating or pre-landing that infrastructure separately. Happy to split if preferred — the topics are otherwise independent. ## What does not change - Existing antenna/VFO alias topics remain always-on and non-disableable - `aethersdr/cw/decode` topic path is unchanged - No changes to KISS TNC, TCI server, or CAT/rigctld paths ## Testing Verified end-to-end on all new topics: publish events received by a Mosquitto subscriber, inbound `cw/transmit` messages keyed through CWX, `radio/state` updates on band change, and `ax25/rx` frames from live 144.390 MHz traffic. `ax25/tx` (transmit path) has not been exercised on-air pending transverter calibration. The AX.25 MQTT path has been in continuous use for multi-day packet decode comparison testing on a Raspberry Pi 5 (2,000+ frames across 12-hour runs, three-decoder comparison against Graywolf and Direwolf). The `cw/transmit` and `radio/state` topics were used in live contest operating on a MacBook Air M2 during the 2026 Atlantic Canada QSO Party. Screenshots of the updated MQTT settings dialog are in aethersdr#3388. --- Built and smoke tested on: Windows 11 on Intel i3 (integrated GPU) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
publishCwDecodeMqtt()helper inMainWindowthat fires on everytextDecodedemissionm_cwCostThresholdso the MQTT feedmatches exactly what the CW decoder window displays
PanadapterApplettext normalization:\n→ space, dropwhitespace-only TX chunks
Payload format
{"text":"K","freq":14.025,"rx":true}text— normalized decoded character(s)freq— active slice frequency in MHz (omitted if no slice active)rx—falsefor TX sidetone decoder outputCommit breakdown
feat: core wiring —publishCwDecodeMqtt()+ twoconnectcallsin the
HAVE_MQTTblockfix: addcwCostThreshold()public getter onPanadapterApplet;apply threshold in the publisher
fix: triage feedback — invert null-applet guard, drop redundantisConnected()check, add text normalizationTest plan
mosquitto_sub -t aethersdr/cw/decode -vshows the samecharacters as the decoder panel
Related
Closes #3215