fix(ptt,ui): Continuous mode no longer self-disables after 30 s; rename PTT label to Mic

Issue 1: in Continuous transmit mode the talk indicator turned
gray-out / mic disabled after ~30 s and could only be revived by
toggling mic mute. Root cause: SAD-079 MissedKeyUpWatchdog
subscribed to AudioTransmitGate.transmit_active and force-cleared
it after 30 s of true. In PTT mode this is correct (stuck key =
bug). In Continuous mode transmit_active is *supposed* to stay
true indefinitely; the watchdog assumption doesn't hold.

Fix: the watchdog now subscribes to a new ptt_held watch on the
TransmitModeSelector (the raw key-state input, not the resolved
gate). In Continuous mode ptt_held is never set true, so the
watchdog never fires. In PTT mode it still fires on a stuck
key-down as before. The session owns the watchdog (was on the
engine) so it survives engine restarts; it's spawned lazily on the
first start_audio.

MissedKeyUpWatchdog gains spawn_on_signal(rx, on_timeout, timeout)
alongside the existing spawn(gate, timeout) — old shape preserved
for backwards compat. run_watchdog generalised to take any
watch::Receiver<bool> + Box<dyn Fn() + Send + Sync>.

Two new tests:
  - watchdog_on_signal_does_not_fire_when_ptt_held_stays_false
    (the Continuous-mode regression test)
  - watchdog_on_signal_fires_when_signal_stays_true
    (the stuck-key case still fires)

Issue 2: the Voice Bar stats line said 'PTT on/off' even when the
user was in Continuous mode where no PTT key is involved. Renamed
to 'Mic on/off' (mode-neutral) and l10n-ised the on/off literal:
  - en: 'Mic on' / 'Mic off'
  - zh: '麦克风 开启' / '麦克风 关闭'

cargo test --workspace --lib: 80 passed / 0 failed / 1 ignored
(was 78, +2 watchdog tests).
flutter analyze: clean (6 pre-existing Radio.groupValue infos).
This commit is contained in:
EdisonJwa
2026-05-16 00:57:10 +08:00
parent 21945979a3
commit 6d4975bd6e
10 changed files with 229 additions and 37 deletions
+18 -8
View File
@@ -113,6 +113,17 @@ pub struct AudioEngine {
/// observes the gate directly. The platform input backend is
/// owned by `chanora_core::ptt::PttController` (SDD-088), not
/// by the engine.
///
/// In the post-rc.7 architecture this field is unused: the
/// missed-key-up watchdog now lives on the session and
/// subscribes to `TransmitModeSelector::subscribe_ptt_held`
/// rather than the gate. Watching the gate caused the watchdog
/// to fire in Continuous mode (where the gate is intentionally
/// pinned to `true`) which clearing surfaced as the bug
/// "Continuous transmission disabled after some time". The
/// field stays here as `None` for now to preserve the existing
/// engine-stop teardown flow; a follow-up commit can remove it
/// entirely.
ptt_watchdog: Option<crate::ptt::MissedKeyUpWatchdog>,
}
@@ -368,13 +379,12 @@ impl AudioEngine {
// responsible only for the cpal streams and the
// missed-key-up watchdog (SDD-092).
// Spawn the missed-key-up watchdog. The task aborts on
// Drop of `MissedKeyUpWatchdog`, so the engine's `stop`
// / Drop chain releases it without explicit cleanup.
let ptt_watchdog = crate::ptt::MissedKeyUpWatchdog::spawn(
transmit_gate.clone(),
crate::ptt::MissedKeyUpWatchdog::DEFAULT_TIMEOUT,
);
// The engine no longer spawns a watchdog against the
// gate (see comment on the `ptt_watchdog` field for the
// rationale). The session spawns the watchdog against the
// selector's `ptt_held` signal instead, so it never fires
// in Continuous mode.
let ptt_watchdog: Option<crate::ptt::MissedKeyUpWatchdog> = None;
Ok(Self {
transmit_gate,
@@ -386,7 +396,7 @@ impl AudioEngine {
_output_stream: Mutex::new(Some(output_stream)),
shutdown_tx: Some(shutdown_tx),
capture_active,
ptt_watchdog: Some(ptt_watchdog),
ptt_watchdog,
})
}