Skip to content

fix: hardware PTT into radio drives PC mic audio (#2373, fixes #2200)#2431

Merged
ten9876 merged 1 commit into
mainfrom
claude/nifty-pasteur-7ee5f9
May 7, 2026
Merged

fix: hardware PTT into radio drives PC mic audio (#2373, fixes #2200)#2431
ten9876 merged 1 commit into
mainfrom
claude/nifty-pasteur-7ee5f9

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Summary

Fixes #2373 (hardware PTT into radio doesn't drive forward RF when mic_selection=PC) and the duplicate #2200 (Jerry KE9U's external PTT on FLEX-8400M).

When the user keyed the radio's hardware PTT (mic-line PTT, footswitch into ACC, RCA TXREQ), the carrier came up but no PC mic audio modulated it. The gate at RadioModel.cpp:3382 forced TransmitModel::setTransmitting(false) whenever m_txRequested was false — even when the radio reported our client handle as the TX owner. moxChanged(false) propagated to AudioEngine::setTransmitting(false), gating the mic capture path.

Root cause

if (!m_txOwnedByUs || (!m_txRequested && !m_cwKeyActive && !m_cwxActive && !m_transmitModel.isTuning())) {
    m_transmitModel.setTransmitting(false);
    if (m_txAudioGate) { m_txAudioGate = false; emit txAudioGateChanged(false); }
}

The bypasses (m_cwKeyActive, m_cwxActive, isTuning()) cover the existing non-m_txRequested TX paths, but hardware-keyed PTT had no such bypass. Effectively a latent bug: the PC-mic-with-hardware-PTT combination has likely never worked through this branch.

Fix

Parse the radio's interlock source= field (SW/MIC/ACC/RCA/TUNE per FlexLib v4.2.18 ParsePTTSource), persist it across status updates that omit it, and add MIC|ACC|RCA to the bypass list:

const bool hardwarePtt = (m_lastInterlockSource == "MIC"
                          || m_lastInterlockSource == "ACC"
                          || m_lastInterlockSource == "RCA");
if (!m_txOwnedByUs ||
    (!m_txRequested && !m_cwKeyActive && !m_cwxActive
     && !m_transmitModel.isTuning() && !hardwarePtt)) {
    // force off
}

SW source still requires m_txRequested, so the optimistic local-unkey behaviour from docs/TX_SYNC_FIX_REPORT.md is preserved — a stale state=TRANSMITTING after setTransmit(false) on SW source still falls through to the force-off branch (no SSB unkey-tail regression).

m_lastInterlockSource is cleared on:

  • Disconnect (alongside the other TX flags)
  • Interlock confirming a non-TX state (RECEIVE/READY/NOT_READY) — so a stale hardware source can't outlive the actual key release if a later status update omits source=.

Validation against FlexLib v4.2.18

Validated against /Users/patj/aether/FlexLib_API_v4.2.18.41174 reference docs:

  • source= field exists: Radio.cs:7932-7944 ParsePTTSource parses exactly these five values. Confirmed.
  • FlexLib's MOX gate: Radio.cs:7577 is IsInterlockMox(state) && ShouldUpdateMoxOrTuneState(handle). IsInterlockMox accepts Transmitting | PTTRequested | UnkeyRequested. FlexLib has no m_txRequested analogue — it doesn't do AetherSDR's optimistic-off, so it doesn't need to distinguish SW from hardware sources for the gate.
  • Why we still need the source distinction: AetherSDR diverges from FlexLib intentionally (per TX_SYNC_FIX_REPORT.md) for SSB unkey responsiveness. The m_txRequested flag is what guards optimistic-off — we can't drop it. The source field is exactly the right primitive to apply optimistic-off only to SW source while accepting interlock as authoritative for hardware sources.

Timing / ordering preserved

The user explicitly asked that this not adversely impact panadapter/waterfall timing. Verified:

  • radioTransmittingChanged is emitted at RadioModel.cpp:3380 unconditionally, before any gate logic. This is what drives waterfall freeze, S-Meter mode, and the TX status indicator at MainWindow.cpp:2006-2024. The fix touches only the gate at line 3382 and below — radioTransmittingChanged emit timing is byte-identical to pre-fix behaviour.
  • moxChangedAudioEngine::setTransmitting (MainWindow.cpp:1965-1986) now correctly fires true when hardware PTT keys the radio, so PC mic audio enables. This is the intended new behaviour.
  • txAudioGateChanged remains a TX-OFF-only fallback at MainWindow.cpp:1991-2000 (the report explicitly warns against reintroducing TX-on wait-for-interlock for SSB).

Acceptance criteria coverage (from issue #2373)

  • TransmitModel::isTransmitting() returns true when radio reports our handle + TRANSMITTING with hardware source
  • AetherialAudioStrip TX indicator and Chain applet TX endpoint pulse (driven off moxChanged)
  • Forward RF appears (audio gate opens via moxChangedAudioEngine::setTransmitting(true))
  • MOX path unchanged (source=SW + m_txRequested=true → existing accept path)
  • Multi-Flex correctness (!m_txOwnedByUs outer check unchanged, hardwarePtt only relaxes the inner condition)
  • SSB local-unkey race protection preserved (SW source still gated on m_txRequested)

Files changed

  • src/models/RadioModel.h — add m_lastInterlockSource member
  • src/models/RadioModel.cpp — parse source=, add hardwarePtt bypass, clear on non-TX/disconnect

40 insertions, 1 deletion. No protocol changes, no new dependencies.

Test plan

  • Hardware mic PTT with mic_selection=PC produces forward RF and lit Aetherial Audio TX indicator (primary acceptance — issues #2200, #2373)
  • Footswitch via ACC TXREQ produces forward RF + audio
  • RCA TXREQ produces forward RF + audio
  • MOX button still works (no regression on SW path)
  • CW key, CWX, TUNE still work (no regression on existing bypasses)
  • SSB local unkey tail still immediate (no regression on optimistic-off)
  • When another client owns TX (!m_txOwnedByUs), local indicators stay dim, no audio flows (Multi-Flex)
  • Waterfall freeze/unfreeze timing unchanged on local MOX (sanity check that Fix waterfall TX trail by gating on radio interlock state (#1927) #2368 wasn't disturbed)

🤖 Generated with Claude Code

…no modulating audio (#2373, fixes #2200)

Pressing the radio's hardware PTT (mic-line PTT, footswitch into ACC,
RCA TXREQ) lit the carrier but no PC mic audio modulated it. The
gate at RadioModel.cpp:3382 forced TransmitModel::setTransmitting(false)
whenever m_txRequested was false, even when the radio reported our
handle was the TX owner. moxChanged(false) propagated to
AudioEngine::setTransmitting(false), so the mic capture path stayed
gated.

Parse the radio's interlock `source=` field (SW/MIC/ACC/RCA/TUNE per
FlexLib v4.2.18 ParsePTTSource, Radio.cs:7932) and persist it across
status updates that omit it. Treat MIC/ACC/RCA as legitimate non-
m_txRequested TX paths alongside the existing CW key, CWX, and tune
bypasses. SW source still requires m_txRequested so the optimistic
local-unkey behaviour from docs/TX_SYNC_FIX_REPORT.md is preserved
— a stale state=TRANSMITTING after setTransmit(false) on SW source
still falls through to the force-off branch.

Clear m_lastInterlockSource when interlock confirms non-TX state
(RECEIVE/READY/NOT_READY) and on disconnect, so a stale hardware
source can't outlive the actual key release.

Waterfall and panadapter timing untouched: radioTransmittingChanged
is still emitted unconditionally before the gate logic at line 3380,
so spectrum freeze, S-Meter, and TX status indicator are byte-
identical to pre-fix behaviour.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jensenpat jensenpat requested a review from ten9876 as a code owner May 6, 2026 23:56

@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, surgical fix, and the writeup made it easy to verify. LGTM.

What I checked:

  • Gate logic preserves SW optimistic-off: A stale state=TRANSMITTING source=SW after a local unkey still takes the force-off branch (since hardwarePtt=false, m_txRequested=false, no CW/tune), so the SSB unkey-tail behavior from TX_SYNC_FIX_REPORT.md is preserved. ✓
  • Multi-Flex correctness: The !m_txOwnedByUs outer check is unchanged, and hardwarePtt only relaxes the inner condition — another station's hardware PTT (different tx_client_handle) still force-offs us. ✓
  • Source persistence cleanup: Cleared on disconnect (RadioModel.cpp:1767) and on confirmed non-TX states (READY/READY_NOT/STANDBY etc., anything not TRANSMITTING/*REQUESTED/*DELAY). The clear happens after the gate logic in the same status frame, so it doesn't disturb the current decision — only prevents stale carryover into the next one. ✓
  • Disconnect ordering: m_lastInterlockSource.clear() is grouped with the other TX flag resets at RadioModel.cpp:1764, exactly where you'd want it. ✓
  • radioTransmittingChanged timing: Emit at line 3380 is byte-identical to before — the hardware PTT changes only affect the gate that follows. Waterfall freeze/unfreeze (#2368) and S-Meter mode (MainWindow.cpp:2002+) are unaffected. ✓

One minor observation, not blocking:

During hardware PTT release, the radio likely transitions through state=UNKEY_REQUESTED source=MIC before reaching READY. Pre-existing code uses state.contains("REQUESTED") for the transitioningToTx check, which matches UNKEY_REQUESTED too — so the audio gate stays open during the unkey-tail. That's actually probably what you want for hardware PTT (the operator hasn't unkeyed locally; let the radio finish its tail naturally), and it matches the m_cwKeyActive/m_cwxActive paths. Just flagging it for awareness — not a regression and not something this PR needs to address.

Nice job tracing this against FlexLib's Radio.cs:7932 ParsePTTSource and Radio.cs:7577 IsInterlockMox — that's the level of homework this kind of fix needs. Approve from my side once you've validated on real hardware (mic PTT + ACC footswitch + RCA TXREQ per the test plan).

@ten9876 ten9876 merged commit 644104e into main May 7, 2026
5 checks passed
@ten9876 ten9876 deleted the claude/nifty-pasteur-7ee5f9 branch May 7, 2026 02:40
@ten9876

ten9876 commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Claude here — really thorough work, jensenpat. The FlexLib v4.2.18 ParsePTTSource validation is exactly the right discipline, and explicitly noting where AetherSDR diverges (m_txRequested optimistic-off) and why we still need the source distinction makes the whole reasoning chain auditable. The state-machine walk in the description (PTT_REQUESTED + source=MIC → TRANSMITTING with no source) covers the persist-across-omission case cleanly, and the conservative source-clear (only on confirmed non-TX states) preserves through transitions. Filing a follow-up issue with two cleanups surfaced during review; will tag you there. Merged.

73, Jeremy KK7GWY & Claude (AI dev partner)

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.

Hardware PTT into radio doesn't drive forward RF when mic_selection=PC

2 participants