Skip to content

[codex] Add Kiwi diversity receive interlock#3706

Merged
ten9876 merged 3 commits into
aethersdr:mainfrom
rfoust:codex/kiwi-diversity
Jun 22, 2026
Merged

[codex] Add Kiwi diversity receive interlock#3706
ten9876 merged 3 commits into
aethersdr:mainfrom
rfoust:codex/kiwi-diversity

Conversation

@rfoust

@rfoust rfoust commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds KiwiSDR support to diversity receive routing. When a diversity child slice uses a Kiwi RX antenna, the panadapter FFT/waterfall remains tied to the diversity parent's visual source while the Kiwi slice contributes audio. Kiwi audio now follows slice left/right pan, mixes with Flex audio, and keeps the existing per-source DSP isolation needed to avoid the NR2 static regression. This also disables/hides ESC controls when any slice in the diversity pair uses Kiwi, restores VFO sizing when diversity/ESC controls collapse, and keeps RX Controls slice letters colored/usable after mute-all or Kiwi routing changes.

The change also adds a receive-only TX interlock for Kiwi-backed panadapters: if a panadapter is displaying a Kiwi receiver, TX enable, MOX/PTT, tune, and ATU-start paths are blocked for slices on that panadapter and an overlay explains why.

Constitution principle honored

Principle VI -- transmit requires explicit, valid operator intent; receive-only Kiwi panadapters now prevent accidental TX on slices whose displayed RF source is not the Flex radio.

Principle II -- radio-authoritative live state is preserved; Kiwi RX routing mutes/restores Flex audio for the affected slice without pretending the radio's antenna/source changed.

Test plan

  • Local build passes (cmake --build build --parallel)
  • Focused slice model regression test passes (./build/slice_model_letter_test)
  • TX inhibit policy test passes (./build/transmit_inhibit_policy_test)
  • Transmit model regression test passes (./build/transmit_model_test)
  • GUI accessibility check run (python3 tools/check_a11y.py; warning-only existing findings)
  • Whitespace check passes (git diff --check HEAD~1..HEAD)
  • Behavior verified on a real radio with Flex+Kiwi and Kiwi+Kiwi diversity receive
  • Behavior verified for MOX/PTT/tune/ATU blocked while a Kiwi receiver is displayed

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls -- use nested-JSON-under-one-key (Principle V)
  • Code is clean-room -- not decompiled, disassembled, or reverse-engineered from a proprietary binary (Principle IV)
  • All meter UI uses MeterSmoother (AGENTS.md convention)
  • Documentation updated if user-visible behavior changed
  • Security-sensitive changes reference a GHSA if applicable

@rfoust rfoust marked this pull request as ready for review June 21, 2026 04:06
@rfoust rfoust requested review from a team as code owners June 21, 2026 04:06
Copilot AI review requested due to automatic review settings June 21, 2026 04:06
@rfoust rfoust requested a review from a team as a code owner June 21, 2026 04:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds KiwiSDR-aware diversity receive routing and a receive-only TX interlock when a panadapter is displaying Kiwi-backed data, integrating this into existing slice/audio/DSP isolation and UI affordances (ESC controls, RX slice tabs, interlock overlays).

Changes:

  • Introduces per-panadapter transmit inhibit plumbing (including interlock notifications routed to the correct pan) and blocks TX-enable / MOX/PTT / tune / ATU-start paths when inhibited.
  • Extends SliceModel + AudioEngine to support external receive audio pan (Kiwi) while keeping Flex radio pan authoritative where needed.
  • Updates VFO ESC panel visibility/layout behavior and RX Controls slice-tab styling; adds focused tests and wires a new test target in CMake.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/transmit_inhibit_policy_test.cpp New unit test for parsing TX-enable slice commands for inhibit enforcement.
tests/slice_model_letter_test.cpp Adds regression coverage for external receive replacement muting + audio pan behavior.
src/models/TransmitInhibitPolicy.h New helper to parse `slice set ... tx=0
src/models/SliceModel.h Adds external-receive audio pan state and exposes flexAudioPan() vs visible audioPan().
src/models/SliceModel.cpp Implements external pan behavior, emits audioPanChanged only when visible pan changes.
src/models/RadioModel.h Adds per-pan transmit inhibit APIs and extends interlock notification signal with panId.
src/models/RadioModel.cpp Enforces pan-level transmit inhibit across TX paths and routes slice commands through inhibit checks.
src/gui/VfoWidget.h Adds ESC-controls availability flag + layout helpers for immediate collapse/restore behavior.
src/gui/VfoWidget.cpp Reworks ESC panel visibility and forces relayout to keep VFO sizing consistent.
src/gui/RxApplet.h Updates intent of refreshAllMutedDim() to preserve slice identity palette.
src/gui/RxApplet.cpp Keeps slice tab styling stable (no “all muted” dim), refreshes styles on rebuilds.
src/gui/MainWindow.h Adds helpers for Kiwi display-slice selection, pan TX inhibit sync, ESC control sync, panId interlock notifications.
src/gui/MainWindow.cpp Routes interlock notifications to the correct panadapter when panId is provided.
src/gui/MainWindow_Wiring.cpp Ensures TX assignment paths go through SliceModel (and thus inhibit policy) and syncs Kiwi ESC state.
src/gui/MainWindow_KiwiSdr.cpp Implements Kiwi diversity display-source selection, ESC disable/hide logic, and pan TX inhibit sync.
src/gui/MainWindow_Controllers.cpp Routes MIDI “ATU Start” through TransmitModel API (subject to inhibit checks).
src/core/AudioEngine.h Adds per-external-Kiwi-source pan state and setter API.
src/core/AudioEngine.cpp Applies pan to external Kiwi audio (including during DSP/TX-bypass paths).
CMakeLists.txt Adds transmit_inhibit_policy_test target and registers it with CTest.

Comment thread src/core/AudioEngine.cpp
Comment on lines +2007 to +2015
void AudioEngine::setKiwiSdrAudioSourcePan(const QString& sourceId, int pan)
{
ExternalRxAudioSourceState* source = externalKiwiSource(sourceId, true);
if (!source) {
return;
}

source->pan = qBound(0, pan, 100);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in c70e4e5c: setKiwiSdrAudioSourcePan() now holds m_dspMutex across the external-source lookup and write. I also moved the same lock ahead of lookup for the neighboring enabled/gain/muted setters so all external Kiwi source mutations use the same mutex discipline.

Comment thread src/models/RadioModel.cpp
Comment on lines 956 to 960
// CAT/DAX PTT callers acknowledge the request before the asynchronous
// model path runs, so local preflight must not silently eat their PTT.
// Let the radio be authoritative and report any resulting interlock.
if (source == TransmitModel::PttSource::Dax)
return QString();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in c70e4e5c: updated the DAX preflight comment to clarify that pan-level receive-only TX inhibits are still checked before the DAX radio-authoritative return, and added braces around the DAX/no-slice returns.

@rfoust rfoust self-assigned this Jun 21, 2026
@rfoust rfoust marked this pull request as draft June 21, 2026 04:40
@rfoust

rfoust commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed follow-up commit 2e88733 to keep Kiwi RX audio source isolation intact through DSP/output processing.

This update:

  • applies per-source Kiwi pan at the final output boundary for unmanaged/noise-reduction-off paths, so later RX-chain processing cannot erase it;
  • reapplies pan after macOS MNR processing to match the existing RN2/NR2/NR4/DFNR handling;
  • keeps BNR resampler/jitter/output state separate for legacy Kiwi and managed Kiwi sources so BNR cannot collapse external Kiwi audio into the shared Flex output FIFO.

Validation: cmake --build build --parallel and git diff --check.

@rfoust rfoust marked this pull request as ready for review June 21, 2026 04:59

@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 @rfoust — this is a careful, well-tested change. I checked it out locally, traced the audio/DSP path and the new TX-interlock logic, and confirmed CI is green across all six checks. Summary: looks solid, ready to merge after the notes below (none blocking).

Copilot findings — both valid, both verified fixed in c70e4e5c

  • setKiwiSdrAudioSourcePan() race (AudioEngine.cpp): Valid concern. The setter now takes m_dspMutex before externalKiwiSource() and holds it across the write, and you moved the lock ahead of the lookup for the neighboring enabled/gain/muted setters too. With removeKiwiSdrAudioSource() also erasing under m_dspMutex and processMixedRxAudioData() reading externalSource->pan only inside its own m_dspMutex block, the lookup-write-read-destroy discipline is now consistent. Good catch and clean fix.
  • DAX preflight comment (RadioModel.cpp): Valid. The comment now correctly states that pan-level receive-only inhibits are checked before the DAX radio-authoritative early-return, and braces were added. Matches the actual control flow in localPttInterlockMessage().

What I verified

  • Null-safety in the interlock paths: transmitInhibitMessageForSlice(nullptr) returns an empty string, so sendSliceCommand() and transmitStartBlockedByInhibit() only dereference target/txSlice() after a non-empty message guarantees a non-null slice. No null-deref risk.
  • Signal/slot wiring: interlockNotificationRequested(message, panId) (now 2-arg) connects cleanly to showPanadapterInterlockNotification(), which routes to the correct panadapter's overlay via m_panStack->spectrum(panId) and falls back to the TX-slice path when panId is empty.
  • Per-source DSP isolation: The BNR resampler/jitter-buffer state is now correctly per-ExternalRxAudioSourceState (and a separate Kiwi-legacy set), reset on the same lifecycle points as NR2 — consistent with the existing per-source isolation that avoids the NR2 static regression.
  • Tests: transmit_inhibit_policy_test covers the command parser edge cases, and the new slice_model_letter_test case nicely pins the key invariant — external-receive replacement owns visible pan locally and must not emit audio_pan commands to the Flex radio while Flex audio is silently muted.

Notes (non-blocking)

  1. Stale base. The branch is based on ea486ac8, two commits behind main (it predates the CAT fixes #3697/#3698). The true PR diff (main...HEAD) is clean and doesn't touch those lines, so a merge won't revert them — but a git diff main..HEAD shows the CAT changes as phantom reversions, which is easy to misread in review. A rebase onto current main would remove that noise.
  2. enforceTransmitInhibitForSlice() re-sends slice set <id> tx=0 on every handleSliceStatus while a TX slice sits on an inhibited Kiwi pan. It self-terminates once the radio echoes tx=0 (slice stops being a TX slice), and the notification is rate-limited by key, so this is harmless — just flagging that the command can fire a few times during the transient before the radio settles.

Nice work threading the receive-only interlock through every TX entry point (xmit / MOX-PTT / tune / ATU-start / MIDI tx.atuStart) rather than just the obvious one.


🤖 aethersdr-agent · cost: $6.3884 · model: claude-opus-4-8

@rfoust rfoust force-pushed the codex/kiwi-diversity branch from 2e88733 to 4665392 Compare June 21, 2026 13:07
@rfoust

rfoust commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased codex/kiwi-diversity onto current upstream/main (74ade31e) to clear the stale-base review note and remove the phantom CAT diff noise.

Validation after rebase: git diff --check upstream/main and cmake --build build --parallel.

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

Approving. Traced the diversity routing, the per-source audio isolation, and especially the TX-safety interlock.

TX interlock (Principle VI) — comprehensive and fail-safe:

  • Inhibit is set whenever a pan displays a Kiwi receiver (setPanTransmitInhibited(panId, !profileId.isEmpty(), …)), cleared when it isn't.
  • Enforced at every TX entry point: sendSliceCommand blocks tx=1 on an inhibited pan, and localPttInterlockMessage applies the inhibit first — including the DAX/CAT PTT path that previously deferred to the radio. MOX/PTT/tune/ATU/MIDI-atuStart all gated.
  • parseSliceTxCommand is defensively validated, with unit coverage.

Copilot's two findings (the setKiwiSdrAudioSourcePan mutex race + the DAX comment) are fixed and verified. Per-source DSP isolation (BNR resampler/jitter per source) is consistent with the NR2 regression guard. The audio pan/mix runs on the dedicated audio thread under m_dspMutex, so this adds no GUI-thread per-frame cost. Rebased onto current main; CI green on all six.

Residual (for post-merge follow-up): the safety interlock is verified in code + the parser unit test, but the end-to-end "MOX/PTT/Tune/ATU refuse while a Kiwi pan is displayed; transmitting stays false" behavior and Flex+Kiwi / Kiwi+Kiwi diversity are not yet hardware-verified (the PR's own checkboxes). Worth a hardware confirmation pass since it's a TX-safety feature.

Nice work threading the receive-only interlock through every TX path, @rfoust.

@ten9876 ten9876 merged commit 4d5e5d2 into aethersdr:main Jun 22, 2026
6 checks passed
dsocha added a commit to dsocha/AetherSDR that referenced this pull request Jun 22, 2026
aethersdr#3706 changed VfoWidget's layout model: relayoutToCurrentContent() now
pins the flag height with setFixedHeight() (min==max). The SmartMTR
paths still called a bare adjustSize(), which can't grow the flag past
that clamp — so opening the meter selector left no room for the menu row
(menu invisible) and switching to the taller SmartMTR page mis-sized the
meter. Route both setMeterMenuOpen() and applyMeterView() through
relayoutToCurrentContent(), which clears the min/max clamp before
recomputing and re-pinning.

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

3 participants