Skip to content

fix(smartmtr): TX meter + audio gate engage on VOX-keyed transmit#3862

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
dsocha:fix/3861-smartmtr-vox-tx-meter
Jun 27, 2026
Merged

fix(smartmtr): TX meter + audio gate engage on VOX-keyed transmit#3862
ten9876 merged 1 commit into
aethersdr:mainfrom
dsocha:fix/3861-smartmtr-vox-tx-meter

Conversation

@dsocha

@dsocha dsocha commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Closes #3861

Problem

The SmartMtr TX meter (and the TX audio gate) did not engage when transmit was keyed via VOX, though it worked for MOX/PTT, hardware PTT (MIC/ACC/RCA), CW, and Tune.

Root cause

VOX keys the radio autonomously, so the client never calls setTransmit() and m_txRequested stays false. The interlock gate in RadioModel only treated m_txRequested, CW key, CWX, Tune, or hardware PTT sources (MIC/ACC/RCA) as legitimate owned-TX paths. A VOX-keyed state=TRANSMITTING matched none of them, so the force-off branch ran setTransmitting(false)moxChanged never fired, the SmartMtr stayed on the RX Signal scale, and the TX audio gate stayed closed.

The recognized sources (SW/MIC/ACC/RCA/TUNE) come from FlexLib ParsePTTSource (added in #2373 for the analogous hardware-PTT case); VOX was never added.

Fix

Extract the force-off decision into a pure helper RadioStatusOwnership::interlockKeepsLocalTxOn(...) and add VOX as an owned-TX path gated on TransmitModel::voxEnable().

Gating on voxEnable() (which the radio echoes via transmit ... vox_enable=1) rather than matching a source=="VOX" token is correct regardless of the interlock source string the radio emits, and cannot mis-fire:

  • still ANDed with m_txOwnedByUs, so foreign-TX force-off wins first;
  • non-TX interlock states still fall through to the existing release path, so VOX-enabled does not stick TX on — TX only engages on an actual state=TRANSMITTING.

Non-breaking

When VOX is off, the boolean is bit-identical to before, so MOX/PTT, hardware PTT, CW, CWX, Tune, and the SW optimistic local-unkey behaviour are unchanged.

Tests

Adds testInterlockTxGate() with VOX cases plus regression guards (SW unkey, foreign-owner force-off, hardware PTT, CW, Tune, MOX). The interlock gate had no unit coverage before; extracting the decision makes it testable.

  • New gate cases: 10/10 pass
  • Full app builds and links
  • Full suite: 50/50 pass (incl. transmit_model_test)

🤖 Generated with Claude Code

…thersdr#3861)

VOX keys the radio autonomously, so the client never calls setTransmit()
and m_txRequested stays false. The interlock gate only treated
m_txRequested, CW key, CWX, Tune, or hardware PTT sources (MIC/ACC/RCA)
as legitimate owned-TX paths, so a VOX-keyed state=TRANSMITTING fell into
the force-off branch — setTransmitting(false) kept the SmartMtr on the RX
Signal scale and the TX audio gate closed.

Extract the force-off decision into a pure helper
RadioStatusOwnership::interlockKeepsLocalTxOn and add VOX as an owned-TX
path gated on TransmitModel::voxEnable(). Gating on voxEnable() (which the
radio echoes via transmit ... vox_enable=1) rather than a source=="VOX"
token is correct regardless of the interlock source string and cannot
mis-fire — it is still ANDed with m_txOwnedByUs, and non-TX interlock
states still fall through to the existing release path, so VOX-enabled
does not stick TX on.

When VOX is off the boolean is bit-identical to before, so MOX/PTT,
hardware PTT, CW, CWX, Tune, and the SW optimistic local-unkey behaviour
are unchanged. Adds testInterlockTxGate() with VOX cases plus regression
guards (SW unkey, foreign-owner force-off, hardware PTT, CW, Tune, MOX).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dsocha dsocha requested review from a team as code owners June 27, 2026 06:55

@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 for the thorough writeup and tests, @dsocha — this is a clean fix and the root-cause analysis matches what I see in the code.

Verified

  • Faithful refactor. The extracted interlockKeepsLocalTxOn() is the exact De Morgan inverse of the old inline force-off condition at RadioModel.cpp:4995. With voxEnabled == false it is bit-identical, so MOX/PTT, hardware PTT (MIC/ACC/RCA), CW, CWX, Tune, and the SW optimistic-unkey path are all unchanged. The 6 regression-guard cases in the test confirm this.
  • voxEnable() is the right gate. Gating on the persistent voxEnable() flag is safe precisely because actual TX assertion still requires state == "TRANSMITTING" — when VOX is enabled but the radio isn't keyed, the keep-on result just routes into the final else, which still sets setTransmitting(false). So VOX-enabled cannot stick TX on, exactly as your description claims. Good reasoning.
  • Foreign-TX still wins. The early if (!txOwnedByUs) return false; preserves the foreign-owner force-off, and the test covers it.
  • Namespace/header wiring (AetherSDR::RadioStatusOwnership, caller's qualified call, test's using namespace) all resolve. Header is #pragma once, pure helper, no RAII/lifetime concerns.

One minor thing to be aware of (not a blocker)

When VOX is enabled and the user also keys via client MOX/PTT (SW source), the optimistic local-unkey path is slightly relaxed: after setTransmit(false) (so txRequested == false), a stale state=TRANSMITTING frame now keeps the gate on for that one frame (voxEnabled short-circuits the keep-on), instead of forcing off immediately. It self-corrects on the next non-TRANSMITTING status, and arguably it's more correct (with VOX on and live audio the radio may genuinely still be keyed) — but worth noting since the "bit-identical" guarantee only holds with VOX off. No change requested; just flagging for the record.

Nice that you added the first unit coverage for this gate. LGTM from a correctness standpoint.


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

@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. Did the full TX-safety analysis (Principle VI) on the interlock gate, not just the helper:

  • Foreign-TX force-off wins firstif (!txOwnedByUs) return false; is unconditional, before VOX. Multiflex safety preserved.
  • Bit-identical with VOX off — the extracted interlockKeepsLocalTxOn() is the exact De Morgan inverse of the old inline condition; the only addition is || voxEnabled. MOX/PTT, hardware PTT, CW, CWX, Tune, SW optimistic-unkey unchanged (6 regression-guard tests confirm).
  • VOX-enabled cannot stick TX on — actual TX only asserts via else if (state == "TRANSMITTING"); a VOX-enabled non-transmitting state (RECEIVE/READY) still falls to the final else and setTransmitting(false). No stuck TX / stuck-open audio gate. This is the key property and it holds.
  • Gated on the persistent voxEnable() flag (radio-echoed), ANDed with txOwnedByUs — robust to whatever interlock source string the radio emits.

First unit coverage for this gate (10 cases incl. foreign-owner force-off with VOX, VOX-off regressions). CI green on all 6. Thanks @dsocha — clean fix with exactly the right tests for a TX-interlock change.

@ten9876 ten9876 merged commit 4a9c38c into aethersdr:main Jun 27, 2026
6 checks passed
@dsocha dsocha deleted the fix/3861-smartmtr-vox-tx-meter branch June 27, 2026 21:08
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.

SmartMtr: TX meter does not trigger when transmitting via VOX

2 participants