Skip to content

[core] Add interlock notification system#2586

Merged
ten9876 merged 8 commits into
aethersdr:mainfrom
jensenpat:aether/interlock-notifications
May 12, 2026
Merged

[core] Add interlock notification system#2586
ten9876 merged 8 commits into
aethersdr:mainfrom
jensenpat:aether/interlock-notifications

Conversation

@jensenpat

@jensenpat jensenpat commented May 11, 2026

Copy link
Copy Markdown
Collaborator
Untitled 3

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

  • Adds a SpectrumWidget alert label centered over the panadapter for five seconds.
  • Blocks local voice MOX/PTT in DIGU/DIGL before sending xmit 1.
  • Exempts TCI, RADE digital voice, and TUNE from the local DIGU/DIGL voice warning.
  • Defers rigctl/DAX CAT PTT preflight to the radio so external apps are not locally ACKed and then silently blocked.
  • Allows TUNE in DIGU/DIGL for SWR checks while still letting radio-side frequency interlocks report back.
  • Detects TX audio filter overlap with current band edges for local voice transmit attempts.
  • Maps Flex interlock reason/state values into operator-facing messages with the raw interlock name appended in parentheses.
  • Suppresses passive radio interlock chatter unless a local TX/TUNE/ATU attempt armed the notification path.
  • Merges the audio pipeline docs from PR [docs] Document AetherSDR audio pipeline #2571 and documents the PTT-source vs audio-route boundary for interlock handling.

Interlock Notification Table

Trigger Notification
Local MOX/PTT in DIGU or DIGL You cannot transmit voice in DIGU/DIGL mode.
Local MOX/PTT/TUNE with no TX slice No transmit slice is assigned.
Local voice TX filter would exceed band edge Your TX filter overlaps your frequency limits.
Radio reason OUT_OF_BAND You cannot transmit on this frequency. (OUT_OF_BAND)
Radio reason TUNED_TOO_FAR You cannot transmit on this frequency. (TUNED_TOO_FAR)
Radio reason OUT_OF_PA_RANGE You cannot transmit on this frequency. (OUT_OF_PA_RANGE)
Radio reason XVTR_RX_ONLY You cannot transmit on this frequency. (XVTR_RX_ONLY)
Radio reason BAD_MODE on local non-RADE DIGU/DIGL voice TX You cannot transmit voice in DIGU/DIGL mode. (BAD_MODE)
Radio reason BAD_MODE otherwise You cannot transmit in this mode. (BAD_MODE)
Radio reason CLIENT_TX_INHIBIT Transmit is inhibited for this band. (CLIENT_TX_INHIBIT)
Radio reason NO_TX_ASSIGNED No transmit slice is assigned. (NO_TX_ASSIGNED)
Radio reason RCA_TXREQ External RCA TX request is holding transmit. (RCA_TXREQ)
Radio reason ACC_TXREQ External ACC TX request is holding transmit. (ACC_TXREQ)
Radio reason AMP:TG Amplifier interlock is blocking transmit. (AMP:TG)
Radio reason containing PG-XL Amplifier interlock is blocking transmit. (<reason>)
Radio state TIMEOUT Transmit timed out. (TIMEOUT)
Radio state STUCK_INPUT PTT input is stuck active. (STUCK_INPUT)
Radio state TX_FAULT Transmit 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 -j8
  • ctest --test-dir build --output-on-failure
  • ./build/transmit_model_test
  • git diff --check

👨🏼‍💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat

@jensenpat jensenpat changed the title Add interlock panadapter notifications [core] Add interlock notification system May 11, 2026
@jensenpat jensenpat marked this pull request as ready for review May 11, 2026 14:04

@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 @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 cmdSetPtt returns 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::TuneDax 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.

@jensenpat jensenpat force-pushed the aether/interlock-notifications branch from d08c882 to 9cef27c Compare May 11, 2026 14:33
@jensenpat jensenpat marked this pull request as draft May 11, 2026 14:33
@jensenpat jensenpat marked this pull request as ready for review May 11, 2026 17:18
…fications

# Conflicts:
#	docs/audio-pipeline.md
#	src/gui/SpectrumWidget.cpp

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

Claude here, reviewing on Jeremy's behalf.

Status

Pushed merge commit 80ea17a0 to your branch resolving conflicts with current main. Two conflicts resolved:

  1. src/gui/SpectrumWidget.cpp — single content conflict at resizeEvent. Both #2578 (FPS meter labels) and this PR added a positioning call right after positionZoomButtons(). Resolution: kept both. Order is positionFpsMeterLabels() then positionInterlockNotification().

  2. 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 routes section 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 new tune preflight blocks tune command and two-tone preflight blocks setup and tune commands cases 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 TCI trx PTT 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

  1. Five-second hardcoded duration. Reasonable for most cases, but for STUCK_INPUT or TX_FAULT the operator might benefit from a longer-lived display (the condition isn't transient). Not blocking, just worth noting for a possible follow-up.
  2. AMP:TG and PG-XL reason-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.
  3. 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:

  1. Verifies CI re-runs green on the rebased branch
  2. 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 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.

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:

  1. 5-second hardcoded notification duration — fine for transient cases, possibly worth longer for `STUCK_INPUT`/`TX_FAULT` follow-up
  2. `AMP:TG`/`PG-XL` reason substring matching is firmware-tied — worth a comment near the substring match noting the firmware coupling
  3. 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 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.

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

  1. Local MOX/PTT in DIGU mode → "You cannot transmit voice in DIGU/DIGL mode."
  2. Tune outside band edges → radio reports OUT_OF_BAND, notification shows
  3. Rigctl PTT from external app (e.g. WSJT-X or VARAC) → no local preflight; radio interlock surfaces if conditions warrant
  4. 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 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.

Approved — three rebase passes complete, all recent PRs coexist, CI green.

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.

2 participants