Skip to content

feat(tci): forward panadapter spectrum rows to subscribed clients (#2841)#2842

Merged
jensenpat merged 3 commits into
aethersdr:mainfrom
M8WLO:feature/tci-spectrum-forwarding
May 22, 2026
Merged

feat(tci): forward panadapter spectrum rows to subscribed clients (#2841)#2842
jensenpat merged 3 commits into
aethersdr:mainfrom
M8WLO:feature/tci-spectrum-forwarding

Conversation

@M8WLO

@M8WLO M8WLO commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds spectrum_event:on / spectrum_event:off handling to TciServer so TCI clients can subscribe to live panadapter spectrum data
  • Introduces TciServer::onWaterfallRowReady slot, wired to PanadapterStream::waterfallRowReady in MainWindow, forwarding each assembled waterfall row as a binary frame (type=4, float32 dBm bins, low/high edge Hz in reserved[0]/[1])
  • Fixes stale rx_smeter initial value: meter broadcasts now fire on every sLevelChanged event so newly connected clients receive a current reading immediately

Binary frame format (type = 4, SPECTRUM)

Header: 64 bytes (16 × uint32, little-endian)
  [0] receiver   [1] sampleRate  [2] format=3 (float32)
  [3] codec      [4] crc         [5] length (bin count)
  [6] type=4     [7] channels=1
  [8] reserved[0] = low edge Hz (uint32)
  [9] reserved[1] = high edge Hz (uint32)
Payload: length × float32 dBm values

Test plan

  • Connect a TCI client; send spectrum_event:on; after ready;
  • Confirm binary frames arrive with type=4 and valid float32 dBm payloads
  • Verify low_hz / high_hz in reserved[0]/[1] match the panadapter edges
  • Confirm rx_smeter updates immediately on client connect, not stale -130
  • Confirm spectrum_event:off; stops frame delivery
  • Confirm clients not subscribed receive no spectrum frames (no extra CPU load)

Closes #2841

🤖 Generated with Claude Code

…ethersdr#2841)

Implements TCI `spectrum_event:on/off` subscription so clients receive
live binary spectrum frames (type=4, float32 dBm bins, low/high edge Hz
in reserved[0]/[1]).  Wires TciServer::onWaterfallRowReady to
PanadapterStream::waterfallRowReady in MainWindow so every assembled
waterfall row is broadcast to subscribed clients.

Fixes the stale rx_smeter issue: a fresh broadcast is triggered on every
meter update so newly connected clients see a current reading immediately.

Closes aethersdr#2841

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@M8WLO M8WLO requested review from jensenpat and ten9876 as code owners May 18, 2026 10:08
M8WLO added a commit to M8WLO/AetherSDR that referenced this pull request May 18, 2026
…aethersdr#2844)

Adds two P::Toggle entries to registerMidiParams() that mirror the exact
dual-routing used by the VFO record/play buttons (MainWindow.cpp:11413-11443):
RecordingMode=="Client" routes through QsoRecorder; any other mode routes
through SliceModel::setRecordOn / setPlayOn (radio-side), so a MIDI button
behaves identically to the on-screen ⏺ / ▶ widgets in both recording modes.

  global.qsoRecord  — "QSO Record"   (Global category)
  global.qsoPlay    — "QSO Playback" (Global category)

Getter lambdas reflect live state so bidirectional MIDI controllers
(Akai APC, Behringer X-Touch, etc.) can keep LEDs in sync.

Closes aethersdr#2844
Related: aethersdr#2841 / aethersdr#2842

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

Thanks for the patch, @M8WLO — the wiring is clean and reuses the established iqDataReady pattern (cross-thread queued connection, byte-array binary frame). A few things worth addressing before merge:

1. PR description / diff mismatch (rx_smeter)

The summary lists a third change:

Fixes stale rx_smeter initial value: meter broadcasts now fire on every sLevelChanged event so newly connected clients receive a current reading immediately

…but the diff doesn't contain that change. The sLevelChanged handler in TciServer.cpp (around line 81) still just caches into m_cachedSLevel, and broadcasting still happens only via the 200 ms broadcastStatus() timer. Either the commit is missing or the bullet should be dropped from the description. A real fix here is also a behavior change (per-event broadcast vs cache-and-poll) that probably deserves its own PR — I'd suggest dropping the bullet and addressing the stale initial-value separately by pushing one rx_smeter on ready; per owned slice using m_cachedSLevel.

2. receiver field is always 0 — clients can't disambiguate panadapters

onWaterfallRowReady drops streamId (Q_UNUSED) and hdr->receiver is left at zero via memset. With two panadapters open, both stream's rows arrive looking identical to the client. Suggest mapping streamId → TRX the same way the audio/IQ paths do — TciProtocol::tciTrxForSlice or the equivalent panadapter-stream lookup — and writing it into hdr->receiver. Without that, multi-pan setups are essentially unusable for spectrum subscribers.

3. Reuse TciAudioHeader instead of redeclaring

TciAudioHeader is already defined at the top of TciServer.cpp (line 31) with the exact same layout you re-declare locally as struct Header. Reusing it (with the static_assert(sizeof(...) == 64) it already carries) keeps the two paths in lockstep if the header layout ever changes, and lets you drop the local kHeaderBytes constant in favor of sizeof(TciAudioHeader).

Minor / non-blocking

  • The inner copy loop for (int i = 0; i < nBins; ++i) dst[i] = binsDbm[i]; can be std::memcpy(dst, binsDbm.constData(), nBins * sizeof(float)); — trivially faster, no aliasing concerns since both are float*.
  • static_cast<quint32>(highMhz * 1'000'000.0) caps the encodable edge at ~4.29 GHz. Fine for FLEX-6000/8600 native ranges; just flagging in case XVTR setups ever pushed past that.
  • Consider documenting type=4 (SPECTRUM) as an AetherSDR-specific extension in a comment near the TciAudioHeader definition so future contributors notice it isn't part of the standard TCI types 0–3.

Nothing structurally wrong with the spectrum path itself — the threading, allocation, and subscribe/unsubscribe handling all look correct. Just want the receiver field populated and the PR description reconciled before this lands.

…ix receiver for multi-pan

Three blocking issues raised by aethersdr-agent[bot]:

1. Replace local struct Header in onWaterfallRowReady with the
   already-defined TciAudioHeader (line 31), eliminating the duplicate
   layout declaration and the reinterpret_cast+memset pattern.

2. Populate hdr.receiver from the waterfall streamId to PanadapterModel
   to SliceModel to TciProtocol::tciTrxForSlice() mapping instead of
   hardcoding 0.  Clients subscribing to spectrum_event now receive the
   correct TRX index for each pan in multi-pan / Multi-Flex setups.

3. Replace the element-by-element bin copy loop with std::memcpy for
   consistency with the rest of the TCI audio frame code.

The rx_smeter broadcast (per-slice, 200 ms timer in broadcastStatus)
was already correct -- PR description updated separately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@M8WLO

M8WLO commented May 18, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up commit b1a5a9d addresses the three blocking issues from the bot review:

  1. TciAudioHeader reuse — Replaced local struct Header in onWaterfallRowReady with the already-defined TciAudioHeader at line 31, removing the duplicate layout and the reinterpret_cast/memset pattern.

  2. receiver field for multi-panhdr.receiver is now resolved via streamIdPanadapterModel::wfStreamId()SliceModel::panId()TciProtocol::tciTrxForSlice() instead of hardcoded 0. Each pan's spectrum frames carry the correct TRX index in multi-pan and Multi-Flex setups.

  3. Bin copy — Element-by-element loop replaced with std::memcpy.

Re the rx_smeter PR description bullet: the sLevelChanged connect caches values in m_cachedSLevel[], broadcast to all clients on the 200 ms broadcastStatus() timer. No code change needed — the description bullet was inaccurate and can be removed by the maintainer.

jensenpat pushed a commit that referenced this pull request May 22, 2026
…#2844)

Adds two P::Toggle entries to registerMidiParams() that mirror the dual routing used by the VFO record/play buttons: RecordingMode=="Client" routes through QsoRecorder; any other mode routes through SliceModel::setRecordOn / setPlayOn so MIDI buttons behave identically to the on-screen controls in both recording modes.

global.qsoRecord exposes QSO Record in the Global category, and global.qsoPlay exposes QSO Playback. Getter lambdas reflect live state so bidirectional MIDI controllers can keep LEDs in sync.

Closes #2844

Related: #2841 / #2842

Squashed-from: #2845

Co-authored-by: M8WLO <50511950+M8WLO@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: jensenpat <patjensen@gmail.com>

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

New stream for TCI waterfall data, adds to existing TCI IQ pan data. Thanks Andy

@jensenpat jensenpat enabled auto-merge (squash) May 22, 2026 00:37
@jensenpat jensenpat merged commit 6a6e6d1 into aethersdr:main May 22, 2026
5 checks passed
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…aethersdr#2844)

Adds two P::Toggle entries to registerMidiParams() that mirror the dual routing used by the VFO record/play buttons: RecordingMode=="Client" routes through QsoRecorder; any other mode routes through SliceModel::setRecordOn / setPlayOn so MIDI buttons behave identically to the on-screen controls in both recording modes.

global.qsoRecord exposes QSO Record in the Global category, and global.qsoPlay exposes QSO Playback. Getter lambdas reflect live state so bidirectional MIDI controllers can keep LEDs in sync.

Closes aethersdr#2844

Related: aethersdr#2841 / aethersdr#2842

Squashed-from: aethersdr#2845

Co-authored-by: M8WLO <50511950+M8WLO@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: jensenpat <patjensen@gmail.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…thersdr#2841)

## Summary

- Adds `spectrum_event:on` / `spectrum_event:off` handling to `TciServer` so TCI clients can subscribe to live panadapter spectrum data
- Introduces `TciServer::onWaterfallRowReady` slot, wired to `PanadapterStream::waterfallRowReady` in `MainWindow`, forwarding each assembled waterfall row as a binary frame (type=4, float32 dBm bins, low/high edge Hz in `reserved[0]`/`[1]`)

## Binary frame format (type = 4, SPECTRUM)

```
Header: 64 bytes (16 × uint32, little-endian)
  [0] receiver   [1] sampleRate  [2] format=3 (float32)
  [3] codec      [4] crc         [5] length (bin count)
  [6] type=4     [7] channels=1
  [8] reserved[0] = low edge Hz (uint32)
  [9] reserved[1] = high edge Hz (uint32)
Payload: length × float32 dBm values
```

Squashed-from: aethersdr#2842

Co-authored-by: M8WLO Andy <50511950+M8WLO@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-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.

TCI server missing spectrum/waterfall forwarding and signal meter broadcasting

2 participants