[codex] Add Kiwi diversity receive interlock#3706
Conversation
There was a problem hiding this comment.
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. |
| void AudioEngine::setKiwiSdrAudioSourcePan(const QString& sourceId, int pan) | ||
| { | ||
| ExternalRxAudioSourceState* source = externalKiwiSource(sourceId, true); | ||
| if (!source) { | ||
| return; | ||
| } | ||
|
|
||
| source->pan = qBound(0, pan, 100); | ||
| } |
There was a problem hiding this comment.
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.
| // 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(); |
There was a problem hiding this comment.
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.
|
Pushed follow-up commit 2e88733 to keep Kiwi RX audio source isolation intact through DSP/output processing. This update:
Validation: |
There was a problem hiding this comment.
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 takesm_dspMutexbeforeexternalKiwiSource()and holds it across the write, and you moved the lock ahead of the lookup for the neighboringenabled/gain/mutedsetters too. WithremoveKiwiSdrAudioSource()also erasing underm_dspMutexandprocessMixedRxAudioData()readingexternalSource->panonly inside its ownm_dspMutexblock, 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, sosendSliceCommand()andtransmitStartBlockedByInhibit()only dereferencetarget/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 toshowPanadapterInterlockNotification(), which routes to the correct panadapter's overlay viam_panStack->spectrum(panId)and falls back to the TX-slice path whenpanIdis 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_testcovers the command parser edge cases, and the newslice_model_letter_testcase nicely pins the key invariant — external-receive replacement owns visible pan locally and must not emitaudio_pancommands to the Flex radio while Flex audio is silently muted.
Notes (non-blocking)
- Stale base. The branch is based on
ea486ac8, two commits behindmain(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 agit diff main..HEADshows the CAT changes as phantom reversions, which is easy to misread in review. A rebase onto currentmainwould remove that noise. enforceTransmitInhibitForSlice()re-sendsslice set <id> tx=0on everyhandleSliceStatuswhile a TX slice sits on an inhibited Kiwi pan. It self-terminates once the radio echoestx=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
2e88733 to
4665392
Compare
|
Rebased Validation after rebase: |
ten9876
left a comment
There was a problem hiding this comment.
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:
sendSliceCommandblockstx=1on an inhibited pan, andlocalPttInterlockMessageapplies the inhibit first — including the DAX/CAT PTT path that previously deferred to the radio. MOX/PTT/tune/ATU/MIDI-atuStart all gated. parseSliceTxCommandis 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.
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>
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
cmake --build build --parallel)./build/slice_model_letter_test)./build/transmit_inhibit_policy_test)./build/transmit_model_test)python3 tools/check_a11y.py; warning-only existing findings)git diff --check HEAD~1..HEAD)Checklist
docs/COMMIT-SIGNING.md)AppSettingscalls -- use nested-JSON-under-one-key (Principle V)MeterSmoother(AGENTS.md convention)