[logging] Add default TX capture health summaries for TCI handoffs#4233
[logging] Add default TX capture health summaries for TCI handoffs#4233jensenpat wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Nicely scoped diagnostic PR. The TxCaptureHealthTracker is a clean hardware-free state machine, the conservative saturation signature (Active→Idle + suppressed callbacks + unread bytes + TCI fresh) is well-reasoned, the anomaly rate-limiting is correct, and the state-machine test pins each class (initial-Idle-is-healthy, saturation, DAX exclusion, foreign-owner exclusion, one-log-per-lifecycle). Threading care is evident — recordTxCaptureLocalTxAttempt is marshaled onto the owning thread and the stateChanged lambda guards against a stale source. All files are in scope, no AppSettings/QSettings misuse, no new deps, commit is signed. Default-on + bounded + no per-callback logging matches the stated field-evidence goal.
Non-blocking notes
audioEndpointDiagnostics()reads the tracker (andm_micDevice->bytesAvailable()) from the caller's thread while the tracker mutates on the audio thread — inline comment. Consistent with the existing pattern in that function, so not a blocker.
Thanks for the thorough writeup and the field-validation boundary being called out honestly — that's exactly the right framing for a diagnostic-only change.
🤖 aethersdr-agent · cost: $4.9911 · model: claude-opus-4-8
| const TxCaptureHealthTracker::Snapshot txHealth = | ||
| m_txCaptureHealth.snapshot(txCaptureNowMs()); | ||
| tx["buffer_bytes_available"] = static_cast<double>(txCaptureBufferedBytes()); |
There was a problem hiding this comment.
audioEndpointDiagnostics() is const and invoked from AutomationServer / DeviceDiagnostics on the main thread, but m_txCaptureHealth and m_micDevice are owned and mutated on the audio thread (onTxAudioReady, the queued stateChanged lambda, recordMicRead). So snapshot() here — and txCaptureBufferedBytes()'s m_micDevice->bytesAvailable() call one line below — race the audio thread's writes on non-atomic members.
This matches the pre-existing pattern in this same function (it already reads m_audioSink->state(), m_micDevice->isOpen(), etc. cross-thread), so it's not a regression and the torn reads are diagnostic-only. Worth flagging only because you deliberately marshaled recordLocalTxAttempt onto the owning thread "so diagnostics cannot race readyRead/stateChanged" — the read side of the same data isn't marshaled. Fine to leave as-is for a support-log snapshot; just noting the asymmetry.
|
Reviewed this alongside #4251 (which stacks on top of this one). Two findings that are specifically this PR's own — reproducible with no drain/Linux-specific code involved at all: 1. Cross-thread read of 2. Both are independent of #4251's Linux drain — they reproduce in this PR alone. I posted two different, drain-interaction-specific findings on #4251 itself, since those only manifest once the drain commit is layered on top of this one. |
Summary
Adds default-on, bounded TX microphone capture health summaries for #4230 without changing audio behavior.
The #4230 field log has now confirmed the failure signature: while TCI audio keeps the 200 ms suppression window fresh,
AudioEngine::onTxAudioReady()returns without consuming the localQAudioSource. The Linux/PipeWire capture ring reached12000/12000bytes while Qt still reportedActive/NoError; the last successful microphone read was 14,173 ms old. This PR's diagnostic now detects that full-while-Active condition directly, while retaining Active-to-Idle with unread bytes as a fallback when capacity is unavailable.Merge order and follow-up fix
Merge this diagnostic PR before #4251. The Linux-only behavioral fix in #4251 is intentionally stacked on this branch. After this PR lands, #4251 will reduce to its single behavior commit and the default-on diagnostics here will remain available to validate the fix in affected PipeWire systems.
What is recorded
AetherSDR's own local TX remains tracked normally when the radio is operating in Multi-Flex mode. Only TX owned by another client is excluded, because that remote transmission does not consume this AetherSDR process's microphone capture stream and must not be counted as a local capture failure.
Anomalies use the existing
aether.audio.summarycategory, whose info summaries are force-enabled for support logs. Logging is bounded to one warning for each anomaly class per source lifecycle, plus one aggregate info summary when a relevant source lifecycle ends. No per-callback logging is added.The existing
get audioautomation snapshot exposes the same TX endpoint evidence:buffer_bytes_availablebuffer_capacity_bytessource_was_activesaturation_observedtci_suppressed_callbacksidle_during_tci_transitionsfull_buffer_during_tci_observationspost_tci_local_tx_while_saturatedlast_mic_read_age_msRelated PipeWire / Linux audio tracking
These references are the working set of PipeWire/Linux audio failure and lifecycle reports relevant to field correlation. They are not asserted to share one root cause; the diagnostics distinguish local
QAudioSourcecapture saturation from DAX routing, stream ownership, clocking, and device-change failures.Current reports:
dax_txstream is recreated.Prior PipeWire/Linux audio lifecycle and quality reports:
Issues that only concern packaging, PipeWire node naming, or channel-pair selection are intentionally not listed as capture-stall candidates.
Validation
tools/check_engine_boundary.py --strict: zero blocking findings.git diff --check: clean.AETHER_AUTOMATION_NO_AUTOCONNECT=1;pingandget audiopassed and exposed the corrected fields. No radio connection or TX was performed.Field-validation result
The first affected PipeWire support log supplied the decisive evidence this PR was designed to collect: the ring was full while the Qt source remained Active. #4251 applies the narrow Linux-only drain/discard fix; these summaries remain enabled to verify that affected systems no longer reach saturation.
Screenshots
Not applicable: this is support-log and diagnostics instrumentation with no visual UI change.
👨🏼💻 Generated with OpenAI Codex (GPT-5.6 Sol) and tested by @jensenpat