Skip to content

feat(mqtt): publish CW decoded text to aethersdr/cw/decode#3216

Merged
ten9876 merged 3 commits into
aethersdr:mainfrom
K5PTB:feat/mqtt-cw-decode-publish
May 27, 2026
Merged

feat(mqtt): publish CW decoded text to aethersdr/cw/decode#3216
ten9876 merged 3 commits into
aethersdr:mainfrom
K5PTB:feat/mqtt-cw-decode-publish

Conversation

@K5PTB

@K5PTB K5PTB commented May 27, 2026

Copy link
Copy Markdown
Contributor

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

{"text":"K","freq":14.025,"rx":true}
  • text — normalized decoded character(s)
  • freq — active slice frequency in MHz (omitted if no slice active)
  • rxfalse 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 #3215

K5PTB and others added 3 commits May 27, 2026 05:52
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>
@K5PTB

K5PTB commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

@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.

@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.

Nice clean addition — thanks @K5PTB. Verified against the surrounding code:

Looks good

  • New connect calls and publishCwDecodeMqtt definition both live inside the HAVE_MQTT block, matching the header declaration.
  • Threshold + \n→space + drop-empty-TX exactly mirror PanadapterApplet::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.025 payload shape is correct without unit conversion.
  • m_cwDecoderApplet is a QPointer<PanadapterApplet>, so the !m_cwDecoderApplet guard correctly handles a destroyed panel as well as never-set.
  • m_cwDecoder / m_cwDecoderTx are direct MainWindow members, so textDecoded fires 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/decode is 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 a kCwDecodeTopic constant 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

@ten9876 ten9876 merged commit c058627 into aethersdr:main May 27, 2026
5 checks passed
@K5PTB K5PTB deleted the feat/mqtt-cw-decode-publish branch May 27, 2026 22:59
K5PTB added a commit to K5PTB/AetherSDR that referenced this pull request May 28, 2026
…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>
K5PTB added a commit to K5PTB/AetherSDR that referenced this pull request May 28, 2026
…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>
ten9876 added a commit that referenced this pull request May 29, 2026
…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>
jensenpat pushed a commit that referenced this pull request Jun 9, 2026
…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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…#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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
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.

Publish CW decoded text to MQTT topic aethersdr/cw/decode

2 participants