[core] Add interlock notification system#2586
Conversation
There was a problem hiding this comment.
Thanks @jensenpat — this is a thoughtful piece of work. The two-stage design (TransmitModel preflight callback + RadioModel armed window for radio-originated notifications) cleanly separates the GUI button path from the CAT/DAX entry path, the dedup key + 5 s window in emitInterlockNotification avoids spam, and onDisconnected resets all the notification bookkeeping. The unit test additions for the preflight callback are nice.
A few things worth a look before merging:
1. CAT/DAX PTT can be silently blocked by setTransmit (src/models/RadioModel.cpp:setTransmit).
RigctlProtocol::cmdSetPtt and TciProtocol::cmdTrx now call m_model->setTransmit(tx, PttSource::Dax). When localPttInterlockMessage returns a non-empty string (no TX slice, or TX filter overlaps band edges), setTransmit emits the panadapter notification, calls m_transmitModel.setTransmitting(false), and returns — but the CAT handler has already returned rprt(0)/OK to the external client. WSJT-X / VARA / N1MM will think PTT succeeded while the radio never keys, and the only feedback is the local panadapter overlay which the external operator may not be watching. Consider either:
- Letting these local-preflight checks not apply to
Dax(treat the radio's own interlock as authoritative for CAT paths), or - Plumbing the failure back so
cmdSetPttreturns a non-zero rigctl error code when the local preflight blocks.
2. txFilterFrequencyLimitMessage runs for Dax source too (src/models/RadioModel.cpp:localPttInterlockMessage).
The PR description says "Detects TX audio filter overlap with current band edges for local voice transmit attempts.", but localPttInterlockMessage only short-circuits the filter check for PttSource::Tune — Dax falls through and gets the filter overlap check. Combined with point (1), this means a DIGU TX from an external digital app near a band edge gets blocked locally with no signal back to the app. If the intent really is voice-only, gating the filter check on !nonVoiceSource (or at least excluding Dax) would match the table in the PR body.
3. Minor: a few magic timeouts that should probably be named.
armInterlockNotification uses a 6000 ms window, emitInterlockNotification dedups within 5000 ms, and the panadapter overlay shows for 5000 ms. These are close but not identical; named constants (e.g. kInterlockArmedWindowMs, kInterlockDedupWindowMs, kInterlockOverlayDurationMs) at the top of RadioModel.cpp / SpectrumWidget.cpp would make the relationship between them explicit and easier to tune later.
Nothing blocking — the architecture is sound. Points (1) and (2) are the ones I'd most like clarified, since they change behavior for external digital-mode users in a way that's not visible to them.
d08c882 to
9cef27c
Compare
…npat/AetherSDR into aether/interlock-notifications
…fications # Conflicts: # docs/audio-pipeline.md # src/gui/SpectrumWidget.cpp
ten9876
left a comment
There was a problem hiding this comment.
Claude here, reviewing on Jeremy's behalf.
Status
Pushed merge commit 80ea17a0 to your branch resolving conflicts with current main. Two conflicts resolved:
-
src/gui/SpectrumWidget.cpp— single content conflict atresizeEvent. Both #2578 (FPS meter labels) and this PR added a positioning call right afterpositionZoomButtons(). Resolution: kept both. Order ispositionFpsMeterLabels()thenpositionInterlockNotification(). -
docs/audio-pipeline.md— 18 add/add regions, same pattern as #2583. Took main's version (which has the post-#2572 corrected canonicalization description), then manually re-inserted your new## Transmit interlocks and audio routessection in the correct spot (between the DAX route section and the metering section).
Post-merge sanity (all recent PRs preserved)
TxMicChannelNormalizer in AudioEngine.cpp: 13 (#2572 ✓)
isTxSlice guards in RigctlProtocol: 4 (#2568 ✓)
FramelessMoveHelper in TitleBar.cpp: 6 (#2576 ✓)
FPS labels in SpectrumWidget.cpp: 13 (#2578 ✓)
autoStartTci log refs in RadioModel.cpp: 2 (#2557 ✓)
Build + tests
cmake --build build -j$(nproc)— clean, only pre-existing unrelated warnings./build/tx_mic_channel_normalizer_test— 17/17 OK./build/transmit_model_test— all OK, including the newtune preflight blocks tune commandandtwo-tone preflight blocks setup and tune commandscases that this PR adds
Verdict on the feature itself
Strong design choices worth calling out:
- Local preflight vs. radio-authoritative interlock split. PC mic voice MOX/PTT in DIGU/DIGL gets blocked locally before
xmit 1, but rigctl CAT PTT and TCItrxPTT bypass local preflight and let the radio be authoritative. That's the right architecture — external apps shouldn't get locally ACKed and then silently blocked, and the radio's interlock state machine is the ground truth for frequency/PA/XVTR reasons. - Arm-on-local-intent for radio-originated notifications. Passive startup or tuning interlock chatter is suppressed unless the operator just attempted TX/TUNE/ATU. Eliminates the false-positive "your radio just told you about an interlock" notifications that don't correspond to anything the operator did.
- TUNE exempt from DIGU/DIGL voice warning. Operators legitimately tune in digital modes for SWR checks. Local preflight allowing TUNE while still letting radio-side frequency interlocks come through is the right policy.
- The interlock notification table in the PR description is canonical — worth copy-pasting into the user-facing docs (the LU5DX manual or aetherclaude/help) at some point.
The five-second panadapter-centered alert is a much better UX than a status-bar message or modal dialog for transient interlock state. Visible without being intrusive.
Concerns / questions
- Five-second hardcoded duration. Reasonable for most cases, but for
STUCK_INPUTorTX_FAULTthe operator might benefit from a longer-lived display (the condition isn't transient). Not blocking, just worth noting for a possible follow-up. AMP:TGandPG-XLreason-substring matching at the message map level — these strings come from Flex firmware. A version bump on the radio side could change the wire format and break the mapping. Not blocking — the fallback message includes the raw reason in parentheses, so worst case is a less-friendly message. Worth a comment near the substring match noting it's firmware-tied.- Local TX-filter-overlap check — the PR description says "Detects TX audio filter overlap with current band edges." Worth verifying the band-edge data is current for all bands the user might be on (especially with XVTR setups where the band edge is computed from
xvtr_freq + slice_freq).
CI
All 5 checks green on the pre-merge branch state. CI will re-run against 80ea17a0 after the push.
Recommendation
Merge after Jeremy:
- Verifies CI re-runs green on the rebased branch
- Spot-tests at least three of the interlock scenarios on his FLEX-8600 — recommended:
- Local MOX in DIGU mode (should show "You cannot transmit voice in DIGU/DIGL mode.")
- Tune outside band edges (should show "OUT_OF_BAND" with raw reason in parens)
- Rigctl PTT from an external app (should NOT show local preflight — radio interlock should pop up if conditions warrant)
Thanks Pat — the PTT-source-vs-audio-route distinction documented in the new section is exactly the right mental model to make explicit. Future contributors touching the PTT coordination paths now have something to grep for when the question "which path should this take?" comes up.
73, Jeremy KK7GWY & Claude (AI dev partner)
ten9876
left a comment
There was a problem hiding this comment.
Claude here, follow-up review after second rebase.
Status
Pushed merge commit `9c78eb2a` to your branch syncing in #2583 (audio device hotplug) which landed since my last rebase. No conflicts this round — clean auto-merge.
Post-merge audit
All recent PRs preserved:
| Check | Count |
|---|---|
| `TxMicChannelNormalizer` in AudioEngine.cpp (#2572) | 13 ✓ |
| `AudioDeviceChangeDialog` in MainWindow.cpp (#2583, just landed) | 5 ✓ |
| `updatePcAudioTooltip` in MainWindow.cpp (#2587) | 4 ✓ |
| `isTxSlice()` guards in RigctlProtocol.cpp (#2568) | 4 ✓ |
| `positionInterlockNotification` in SpectrumWidget.cpp (this PR) | 3 ✓ |
| Interlock implementation in SpectrumWidget.{h,cpp} (this PR) | 32 refs ✓ |
Local build clean. `transmit_model_test` passes (including the new tune-preflight / two-tone-preflight cases). `tx_mic_channel_normalizer_test` passes 17/17.
CI
Four checks green on the pre-merge commit (`build`, `analyze (cpp)`, `check-paths`, CodeQL); `check-windows` was skipped. The new merge will trigger a re-run.
Recommendation
Should be ready to land once CI re-runs green. The architectural review from my previous comment stands — local preflight vs. radio-authoritative interlock split is the right design, the arm-on-local-intent suppression eliminates passive startup chatter, and the new `## Transmit interlocks and audio routes` doc section captures the PTT-source-vs-audio-route distinction explicitly.
The earlier non-blocking notes still apply:
- 5-second hardcoded notification duration — fine for transient cases, possibly worth longer for `STUCK_INPUT`/`TX_FAULT` follow-up
- `AMP:TG`/`PG-XL` reason substring matching is firmware-tied — worth a comment near the substring match noting the firmware coupling
- XVTR band-edge data freshness for the TX-filter-overlap check
None blocking. Same testing recommendations as before — verify local MOX in DIGU, tune outside band, rigctl PTT from external app on the FLEX-8600.
73, Jeremy KK7GWY & Claude (AI dev partner)
ten9876
left a comment
There was a problem hiding this comment.
Claude here, third pass — pushed merge commit `1cd2e29a` syncing in #2593 (dBm drag race fix) which landed since my last rebase.
Clean auto-merge — no conflicts this round
Just one new commit on main (#2593 — the dBm scale drag coordination work) and the auto-merge resolved cleanly. The dBm drag fix and this PR's interlock notifications touch overlapping files (SpectrumWidget, MainWindow) but in non-overlapping regions.
Post-merge audit
All recent PRs preserved:
| Check | Count |
|---|---|
| `TxMicChannelNormalizer` (#2572) | 13 |
| `AudioDeviceChangeDialog` (#2583) | 5 |
| `updatePcAudioTooltip` (#2587) | 4 |
| FPS labels (#2578) — `positionFpsMeterLabels` | 5 |
| dBm drag fix (#2593) — pendingDbm/setStreamDbmRange/m_resetFftSmoothingOnNextFrame | 26 across SpectrumWidget+MainWindow |
| This PR's interlock content — InterlockAlert/positionInterlockNotification | 36 across SpectrumWidget |
Both major features (dBm drag race fix + interlock notifications) are now intact and coexisting on the branch.
Build + tests
- `cmake --build build -j$(nproc)` — clean, only pre-existing unrelated warnings
- `./build/transmit_model_test` — all tests pass, including the new `tune preflight` and `two-tone preflight` cases this PR added
Recommendation unchanged
Same as before — ready to land once CI re-runs green. The architectural review from my earlier passes still stands:
- Local preflight vs. radio-authoritative interlock split is the right design (local for DIGU/DIGL voice-block + TX filter overlap; radio for frequency/PA/XVTR/amplifier reasons)
- Arm-on-local-intent suppression for radio-originated notifications eliminates passive startup chatter
- Five-second hardcoded notification duration — fine for transient cases, possibly worth longer for `STUCK_INPUT`/`TX_FAULT` as a follow-up
- `AMP:TG`/`PG-XL` reason substring matching is firmware-tied — worth a comment near the substring match noting that
Suggested validation on the FLEX-8600
- Local MOX/PTT in DIGU mode → "You cannot transmit voice in DIGU/DIGL mode."
- Tune outside band edges → radio reports OUT_OF_BAND, notification shows
- Rigctl PTT from external app (e.g. WSJT-X or VARAC) → no local preflight; radio interlock surfaces if conditions warrant
- Verify the interlock notification doesn't interact poorly with the dBm drag overlay (both render onto SpectrumWidget) — drag the dBm scale while an interlock notification is showing; expect both to render correctly
73, Jeremy KK7GWY & Claude (AI dev partner)
ten9876
left a comment
There was a problem hiding this comment.
Approved — three rebase passes complete, all recent PRs coexist, CI green.
Summary
Adds a panadapter-centered interlock notification overlay and wires it into both local TX preflight checks and FlexRadio interlock status handling.
The overlay shows a square-bordered alert for five seconds on the TX panadapter, using the AetherSDR title-label blue for the border. Local preflight catches operator-facing cases before keying where possible, while radio-originated interlock notifications are only shown after a local TX/TUNE/ATU attempt so passive startup or tuning status does not surprise the operator.
Why
Operators were not getting clear feedback when TX was blocked by a local mode mismatch, a radio interlock reason, or a TX filter/frequency-limit overlap. The radio already reports detailed interlock state and reason fields, but AetherSDR was not surfacing them in the panadapter at the moment of the failed TX attempt.
Behavior
SpectrumWidgetalert label centered over the panadapter for five seconds.xmit 1.Interlock Notification Table
DIGUorDIGLYou cannot transmit voice in DIGU/DIGL mode.No transmit slice is assigned.Your TX filter overlaps your frequency limits.OUT_OF_BANDYou cannot transmit on this frequency. (OUT_OF_BAND)TUNED_TOO_FARYou cannot transmit on this frequency. (TUNED_TOO_FAR)OUT_OF_PA_RANGEYou cannot transmit on this frequency. (OUT_OF_PA_RANGE)XVTR_RX_ONLYYou cannot transmit on this frequency. (XVTR_RX_ONLY)BAD_MODEon local non-RADEDIGU/DIGLvoice TXYou cannot transmit voice in DIGU/DIGL mode. (BAD_MODE)BAD_MODEotherwiseYou cannot transmit in this mode. (BAD_MODE)CLIENT_TX_INHIBITTransmit is inhibited for this band. (CLIENT_TX_INHIBIT)NO_TX_ASSIGNEDNo transmit slice is assigned. (NO_TX_ASSIGNED)RCA_TXREQExternal RCA TX request is holding transmit. (RCA_TXREQ)ACC_TXREQExternal ACC TX request is holding transmit. (ACC_TXREQ)AMP:TGAmplifier interlock is blocking transmit. (AMP:TG)PG-XLAmplifier interlock is blocking transmit. (<reason>)TIMEOUTTransmit timed out. (TIMEOUT)STUCK_INPUTPTT input is stuck active. (STUCK_INPUT)TX_FAULTTransmit interlock fault. (TX_FAULT)Radio-originated notifications only display after a local TX/TUNE/ATU attempt arms the alert window. Passive startup/tuning interlock status is ignored. Local DIGU/DIGL blocking exempts TCI, RADE, and TUNE. Rigctl/DAX CAT PTT bypasses local preflight and lets the radio report any resulting interlock.
Validation
cmake --build build -j8ctest --test-dir build --output-on-failure./build/transmit_model_testgit diff --check👨🏼💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat