feat(icom): count and report TX packetiser drops instead of losing audio silently - #5162
feat(icom): count and report TX packetiser drops instead of losing audio silently#5162nigelfenton wants to merge 6 commits into
Conversation
…dio silently TxPacketizer::submit() discards the OLDEST bytes past its 250 ms cap. For voice that is correct and the comment at the drop site argues it well: latency must not grow, and the freshest audio is what matters. For a digital burst it is destructive in a way nothing reports -- the oldest bytes of an AX.25 transmission are the preamble and the opening flag, so what survives keys the radio, sounds exactly like packet on a receiver, and syncs on nothing. Nothing surfaced that. No error, no gap, no counter: a transmission that lost 63% of itself looked identical to one that did not. submit() now counts dropped bytes and overflow events; IcomSession::Stats carries them; onLinkTick reports new drops at WARNING with the running total and the pending/cap ratio, on the edge rather than per drop (one oversized submit sheds thousands of bytes in a single loop). Counting rather than logging inside IcomAudio.cpp keeps that file pure protocol -- no QObject, no Qt logging -- which is also what lets the unit test link it standalone. The test asserts the counter against the arithmetic (droppedBytes == submitted minus retained, from the queue's own cap) rather than trusting it, because an instrument that under-reports here would turn a lost preamble back into silence. The test was proven falsifiable before being trusted: with ++dropped commented out it fails 5 of 13 checks. Split out of aethersdr#5058 so the diagnostic can land independently of the tap-architecture discussion there. Context: aethersdr#5011 -- where the failure this makes visible (a burst losing its front while sounding intact) is the same symptom PTT timing produces by another route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Issue fit
Partially. There is no Fixes #NNNN — the commit message names #5011 as context and says this is split out of #5058, which reads correctly: #5011 explicitly asks for nothing to be fixed in AetherSDR and ranks "surface it at runtime" as option 2. This PR is that option, narrowed to one counter. Judged against its own stated intent — make a silent front-of-queue discard visible — the mechanism is sound and the unit test is honest. One defect stops it doing that reliably across a reconnect (Blocker 1).
One claim from #5011 worth putting back on the record, because this PR's comments lean on the opposite: the issue's own elimination table says "packetiser drops — the qCWarning that fires on any drop never fired across ~12 keyings." Whatever tree that warning lived in (there is none on main), the author's own measurement is that this overflow did not occur in the sessions that motivated it. That does not make the instrument unwarranted — it makes the in-code assertion that an oversized submit is "reachable in production, not just in theory" unevidenced. See the nit on IcomAudio.cpp:167; I did find a plausible reachable path, but it isn't the one the comment names.
Scope
| File / group | What it changes | Claimed by title/body | Verdict |
|---|---|---|---|
IcomAudio.{h,cpp} |
m_droppedBytes / m_dropEvents, accessors, resetDropCounters() |
yes — "count" | In scope |
IcomSession.{h,cpp} |
three new Stats fields, populated in stats() |
yes — carries the count to the owner | In scope |
IcomCivBackend.{h,cpp} |
edge-detected qCWarning in onLinkTick, m_lastTxDroppedBytes |
yes — "report" | In scope (see Blocker 1) |
tests/icom_tx_packetiser_drop_test.cpp, tests/tests.cmake |
new standalone test target | yes | In scope; the target is redundant — see Nits |
No unrelated files, no deleted guards, no CHANGELOG.md entry, no settings/persistence/capability surface, no UI or default changed, no new protocol verb. Stats is an internal struct, not third-party-visible. Nothing here is a personal-preference change: it adds a log line at an existing failure, it does not alter any behavior an operator can see. Everything in the diff is explained by the stated intent.
Blockers
1. m_lastTxDroppedBytes outlives the counter it tracks, so the warning goes silent after a reconnect. (inline: IcomCivBackend.cpp:4854)
TxPacketizer's counters are per-TxPacketizer, and m_tx is reassigned on every IcomSession::start() (IcomSession.cpp:66) — on top of which the whole IcomSession is a fresh make_unique on each connect (IcomCivBackend.cpp:606). m_lastTxDroppedBytes is a backend member and is reset nowhere: grep -rn m_lastTxDroppedBytes src/ returns only the declaration, the comparison, and the assignment.
Failure scenario, reasoned from the code (not reproduced — I have no build here): session A overflows and accumulates, say, 36 000 dropped bytes; m_lastTxDroppedBytes ends at 36 000. Operator reconnects. Session B's counter starts at 0. Session B then loses the preamble of an AX.25 burst — 10 000 bytes. 10000 > 36000 is false, so no warning is emitted at all, and none will be until session B exceeds session A's lifetime total. That is precisely the "no error, no gap, a transmission the operator can hear on a second receiver" failure the commit message is written to end.
The sibling in the same function already gets this right: m_schedulerTimeoutsReported is reset at IcomCivBackend.cpp:676 and :2781. Adding m_lastTxDroppedBytes = 0; next to the one at :676 matches the established pattern and closes it. (resetDropCounters() is added but never called from anywhere in src/ — resetting the backend-side tracker rather than the session-side counter is the fix that fits the existing shape.)
Nits (non-blocking)
tests/tests.cmake:422—Qt6::Coreis not needed.IcomAudio.cppincludes only<algorithm>/<cmath>plusIcomAudio.h, andIcomProtocol.hincludes no Qt header either. The pre-existingicom_audio_test(tests.cmake:439) compiles the sameIcomAudio.cppand links no Qt at all. The link line also mildly contradicts the target's own comment ("no QObject and no Qt logging").- The whole target is arguably redundant.
icom_audio_testalready exists to drive this exact class —IcomAudio.h:17says so in as many words. Fivecheck()blocks appended there would need no new target, no new CMake stanza, and no second compile ofIcomAudio.cpp. Maintainer's call; not worth a re-push on its own. - Neither test gates a merge.
ci.yml:385filters^icom_(civ|civ_scheduler|meters|backend)_test$, which excludes bothicom_audio_testand the new target. So "CI green" on this PR does not mean these assertions ran. If the counter is worth pinning, adding it to that regex is the cheap way to keep it pinned. IcomAudio.cpp:167cites a function that doesn't exist. There is noonTxPaceTickanywhere insrc/; the drain isIcomSession::onTxPump(IcomSession.cpp:728), and it doesn't "ship a larger chunk when a tick lands late" — it loopstakeFrame()until empty, which is the opposite shape. The reachable overflow path I did find is different and stronger:sendAudio()enqueues wheneverm_params.enableTx(IcomSession.cpp:766), whileonTxPump()returns early unlessm_audio && m_audio->isReady(). Producer with no drain ⇒ 250 ms of pending and then sustained front-drops. That's worth having in the comment instead.
What I tried to break
- The arithmetic. Cap 24 000; a 2.5× submit is 30 000 samples ⇒ 60 000 bytes;
while (size > cap)leaves exactly 24 000 and sheds 36 000 =submitted - cap. The test's assertion is against that arithmetic, not against the counter's own word, exactly as it claims. Survived. - The falsifiability claim. The commit says commenting out
++droppedfails 5 of 13 checks. I counted 13check()calls, and traced the mutation: because++m_dropEventsis nested underif (dropped > 0), both counters go dead, failing 2 in block 2, 2 in block 3, and 1 in block 4 = 5. The claim holds. A test that only asserteddroppedBytes > 0would have caught 3; this one is not self-confirming. - Termination and lifetime in the test.
while (!p.takeFrame().empty())terminates —takeFramereturns{}belowkAudioFrameBytes.submit(tone(n))builds aspan<const float>from a prvaluevector; legal (is_const_v<element_type>satisfies the range ctor) and the temporary outlives the call. No dangling. - The standalone link.
kAudioFrameBytes/kAudioSplitLargeareinline constexprinIcomProtocol.h(:463,:472), so compilingIcomAudio.cppwithoutIcomProtocol.cpplinks. The new target is not a build break. - Unsigned underflow in the log line.
s.txDroppedBytes - m_lastTxDroppedBytesis guarded by the>test, so a backwards-moving counter prints nothing rather than ~2^64. That guard is also what makes Blocker 1 silent instead of loud — correct arithmetic, wrong outcome. - Log volume.
kLinkTickMs = 1000(IcomCivBackend.cpp:70), so a sustained overflow warns once per second, not per drop or per 10 ms pump. Reasonable. - CodeGuard. All eight CG-PATH-001 hits are in
src/core/MidiSettings.cppandtests/midi_settings_test.cpp— neither file is in this diff. Nothing to fold in. - CI. All four checks pass on
ed1e248. Per the filter atci.yml:385, that green does not cover the new test; treat it as "the tree builds on three platforms", which it does.
Everything above is reasoned from the head checkout at /tmp/aetherclaude/pr-5162; I am headless and ran no build, no ctest, and no radio.
Recommendation
Request changes — for Blocker 1 only, which is a one-line fix at IcomCivBackend.cpp:676 alongside the reset that is already there. The rest is a well-shaped, well-tested diagnostic: correct seam (count in the Qt-free protocol file, report in the owner), edge-detected, honest test. Worth fixing before merge rather than after, because the defect is in the reporting path itself — it fails the same way it is designed to prevent, and would look exactly like "no drops occurred."
🤖 aethersdr-agent · cost: $6.4095 · model: claude-opus-5
| // once per drop: an oversized submit sheds thousands of bytes in one loop | ||
| // and a per-drop message would be unreadable. WARNING level because audio | ||
| // the client generated and then discarded is not a debug detail. | ||
| if (s.txDroppedBytes > m_lastTxDroppedBytes) { |
There was a problem hiding this comment.
Blocker — this goes silent after a reconnect.
m_lastTxDroppedBytes is a backend member that is never reset, but the counter it tracks is per-TxPacketizer: m_tx is reassigned on every IcomSession::start() (IcomSession.cpp:66), and the whole session is a fresh make_unique on each connect (IcomCivBackend.cpp:606). grep -rn m_lastTxDroppedBytes src/ finds only the declaration, this comparison, and the assignment below.
So: session A drops 36 000 bytes and leaves m_lastTxDroppedBytes = 36000. Reconnect. Session B loses the preamble of an AX.25 burst — 10 000 bytes. 10000 > 36000 is false ⇒ no warning, and none until B exceeds A's lifetime total. That is exactly the silent failure this PR exists to end.
The sibling tracker directly above gets this right: m_schedulerTimeoutsReported is reset at IcomCivBackend.cpp:676 and :2781. Adding one line beside the first of those matches the pattern:
m_schedulerTimeoutsReported = 0;
m_lastTxDroppedBytes = 0; // the session's counter restarts too(Reasoned from the checkout, not reproduced — I have no build here.)
| // The counter is cumulative, so reporting it directly would repeat the | ||
| // same total every tick; this holds the last value so only NEW drops are | ||
| // logged. | ||
| std::size_t m_lastTxDroppedBytes = 0; |
There was a problem hiding this comment.
Same issue as the inline on IcomCivBackend.cpp:4854: this member's initialiser is the only place it is ever set to 0, but the counter it mirrors restarts with each TxPacketizer / IcomSession. It needs a reset in the teardown block that already resets m_schedulerTimeoutsReported.
| // never is for voice: the oldest bytes of an AX.25 transmission are the | ||
| // preamble and the opening flag, so dropping from the front leaves a frame | ||
| // that still sounds like packet and syncs on nothing. A 596 ms burst is | ||
| // 2.4x this queue, and onTxPaceTick's catch-up pacing ships a larger chunk |
There was a problem hiding this comment.
Nit — the mechanism named here doesn't exist. There is no onTxPaceTick anywhere in src/; the drain is IcomSession::onTxPump (IcomSession.cpp:728), and it doesn't "ship a larger chunk whenever a tick lands late" — it loops takeFrame() until empty, which is the opposite shape and is what normally keeps m_pending under one frame.
The reachable overflow path I did find is stronger and worth having here instead: sendAudio() enqueues whenever m_params.enableTx (IcomSession.cpp:766), while onTxPump() returns early unless m_audio && m_audio->isReady(). Producer with no drain ⇒ 250 ms of backlog and then sustained front-drops.
Related: #5011's own elimination table records that a drop warning "never fired across ~12 keyings", so "reachable in production, not just in theory" is currently an assertion against the author's own measurement. Citing the isReady() gate would make it an argument.
| src/core/backends/icom/IcomAudio.cpp | ||
| ) | ||
| target_include_directories(icom_tx_packetiser_drop_test PRIVATE src) | ||
| target_link_libraries(icom_tx_packetiser_drop_test PRIVATE Qt6::Core) |
There was a problem hiding this comment.
Nit — Qt6::Core isn't needed. IcomAudio.cpp includes only <algorithm> and <cmath> beyond its own header, and IcomProtocol.h pulls in no Qt either. The pre-existing icom_audio_test at line 439 compiles the same IcomAudio.cpp and links nothing. The line also sits oddly against this target's own comment three lines up ("no QObject and no Qt logging").
| target_link_libraries(icom_tx_packetiser_drop_test PRIVATE Qt6::Core) | |
| add_test(NAME icom_tx_packetiser_drop_test COMMAND icom_tx_packetiser_drop_test) |
Broader (maintainer's call, not worth a re-push alone): icom_audio_test already exists to drive this class — IcomAudio.h:17 says so — so these five blocks could live there with no new target at all. Either way, note that ci.yml:385 filters ^icom_(civ|civ_scheduler|meters|backend)_test$, so neither this test nor icom_audio_test gates a merge today.
|
Cisco CodeGuard — static analysis of this PR (8 finding(s))
Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them. 🤖 aethersdr-agent · cost: $7.1716 · model: claude-opus-5 |
…adows Review catch on aethersdr#5162 (aethersdr-agent), and it fails exactly the way the counter exists to prevent: TxPacketizer dies with the session, but m_lastTxDroppedBytes lived on in the backend — so after a reconnect, a new session's drops stayed SILENT until they exceeded the old session's lifetime total. Reset it in the same teardown block that already resets m_schedulerTimeoutsReported, per the established pattern. Also from review: - IcomAudio.cpp cited a function that does not exist (onTxPaceTick) as the production path to overflow. The real, stronger path is now documented: sendAudio() enqueues whenever TX is enabled while onTxPump() drains nothing until the codec reports ready — a producer with no drain fills the cap and sheds silently. Verified against IcomSession.cpp before writing it down. - icom_tx_packetiser_drop_test no longer links Qt6::Core it never needed. - The test is added to the CI merge-gate regex, since a green gate that never ran the assertion is the trap this repo already knows about. Full gate set run locally: icom_civ, civ_scheduler, meters, backend, and the new test — all pass (backend flaked once under load on an assertion this change cannot reach, clean on rerun). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
All four review items addressed in
On the redundant-target nit: kept as its own target for now since it documents the standalone-linkability contract in its stanza, but zero attachment — fold it into Full gate set run locally (civ, civ_scheduler, meters, backend, new test): green. |
…er-drop-counters # Conflicts: # .github/workflows/ci.yml
Per @jensenpat: build-time CI is privileged (only Jeremy can merge a PR that touches it), and Jeremy has just been removing excess build-time tests. A PR adding its own gate takes itself out of the normal review flow for no good reason. This hunk was not a deliberate gate addition — it came from resolving a conflict during the rebase, where main had added `family` and `rf_gain_presentation` to the same regex I had earlier added `tx_packetiser_drop` to. Reverting the whole line to main's version; icom_tx_packetiser_drop_test stays registered in tests.cmake and still runs locally, it just isn't on the merge gate. If the gate is worth having, that's a maintainer's call to make separately, not something to smuggle in on a feature PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ten9876
left a comment
There was a problem hiding this comment.
Issue fit
No linked issue, and none is needed — GOVERNANCE.md exempts bug fixes with a clear root cause, and the root cause here is stated precisely: submit() drops the OLDEST bytes past a 250 ms cap, which is correct for voice and destructive for a digital burst, because the oldest bytes of an AX.25 transmission are the preamble and the opening flag. A frame that keys the radio, sounds like packet on a second receiver, and syncs on nothing is about the worst failure shape available, and nothing surfaced it.
The discipline of making the drop visible without changing the drop rule is the right call and the body says so plainly.
Verified empirically on macOS/arm64: clean build of the full app target; icom_tx_packetiser_drop_test (0.38 s) and icom_backend_test (81 s) both pass. I also mutation-tested the central claim — undercounting by half:
m_droppedBytes += dropped / 2;
→ FAIL: droppedBytes equals submitted minus retained
so the arithmetic assertion is real and not the counter marking its own homework, exactly as the body claims.
No blockers. Two nits.
Things I checked that turned out fine
- Threading. New non-atomic
std::size_tcounters read fromonLinkTick()looked like a cross-thread race, so I traced it:submit()is reached only viaIcomSession::sendAudio()fromIcomCivBackend, there is nomoveToThreadanywhere in the Icom backend, andonLinkTick()is aQTimeron the same object. The counters live on exactly the same call pathm_pendingalready did, so they add no exposure the deque did not already have. No finding. - The edge tracker.
m_lastTxDroppedBytes = s.txDroppedBytes;sits outside theif, so a single overflow warns once rather than every tick forever. a544f2ba. Resettingm_lastTxDroppedByteson disconnect is the subtle half of this, and the comment nails why: with a freshTxPacketizerper connect, a stale tracker keeps a new session's drops silent until they exceed the previous session's lifetime total — the warning failing in precisely the way it exists to prevent. That is the kind of thing that normally ships broken and is discovered a year later.
The decision to keep IcomAudio.cpp free of Qt logging so the unit test can link it standalone is also the right trade, and it is what made the mutation test above cheap for me to run.
Scope
Everything is explained by the stated defect. No CHANGELOG.md entry — correct. 92315135 ("ci: drop the test-gate edit from this PR") narrows rather than widens, which brings me to the first nit.
Nits
- The new test is on no CI gate. (inline:
tests/tests.cmake:417) Confirmed:grep -c icom_tx_packetiser_drop .github/workflows/ci.yml→0.92315135removed that edit deliberately, and if the reason was conflict avoidance it is a well-founded one — I reviewed two other open PRs today (#5244 and #5240) that both edit the samectest -Rregex line on the Icom gate and will conflict with each other. Worth saying so in the body, and worth a follow-up once those settle: this is a 0.38 s pure-arithmetic test with no Qt GUI, no event loop and no hardware, which is the exact profile the comments around those gates describe as worth failing a merge on. resetDropCounters()has no production caller. (inline:IcomAudio.h:101) Its only two uses are in the test. That is consistent with the design — the packetiser is fresh per connect, so nothing in production needs it — but it is public surface that exists for the test, and a future reader will reasonably assume something calls it. Either a one-line comment saying "test-only; production gets a fresh packetiser per session", or drop it and let the test construct a secondTxPacketizer.
Verified vs. read
Built and ran: the full app target, both test targets, and the undercount mutation above (reverted). Read: the threading trace, the edge-tracker placement, and the disconnect reset. Not verified: no IC-9700 or IC-705 here, so the production overflow path you describe — sendAudio() enqueuing while onTxPump() drains nothing until the codec reports ready — is reasoned from the code rather than observed. That path is the strongest part of the argument and it would be worth one line in the body saying whether you have seen it fire on hardware or inferred it, since it is what makes this a real-world bug rather than a theoretical one.
| add_executable(icom_tx_packetiser_drop_test | ||
| tests/icom_tx_packetiser_drop_test.cpp | ||
| src/core/backends/icom/IcomAudio.cpp | ||
| ) |
There was a problem hiding this comment.
Nit — registered with ctest, but on no CI filter.
$ grep -c 'icom_tx_packetiser_drop' .github/workflows/ci.yml
0
92315135 removed that edit on purpose, so this is a question rather than an objection: was it conflict avoidance? If so it was a good instinct. I reviewed two other open PRs today — #5244 and #5240 — that both add a target to the same ctest -R regex on the Icom gate (ci.yml:385), and they will conflict with each other on merge. A third PR joining that queue would have made it three-way.
Once those land, this test is close to ideal gate material: 0.38 s measured, links only IcomAudio.cpp + Qt6::Core, no Qt GUI, no event loop, no hardware, no display. And what it guards is invisible from every other angle — the whole premise of the PR is that a 63% audio loss looked identical to a clean transmission, so a silent regression here restores exactly the condition you set out to remove.
Worth a line in the body noting the gate was deliberately deferred and why, so it does not read as an oversight to whoever merges.
| // distinguishable. | ||
| [[nodiscard]] std::size_t droppedBytes() const noexcept { return m_droppedBytes; } | ||
| [[nodiscard]] std::size_t dropEvents() const noexcept { return m_dropEvents; } | ||
| void resetDropCounters() noexcept { m_droppedBytes = 0; m_dropEvents = 0; } |
There was a problem hiding this comment.
Nit — this has no production caller.
Both uses are in icom_tx_packetiser_drop_test.cpp:101-102. Nothing in src/ calls it.
That is consistent with your own design note — IcomCivBackend::disconnectRadio()'s comment says the packetiser's counter "dies with the session (fresh TxPacketizer per connect)", so production never needs a reset. The awkwardness is that a reader arriving at this header sees a public mutator and reasonably assumes some caller depends on it, which affects how freely they'd change it.
Two clean options:
| void resetDropCounters() noexcept { m_droppedBytes = 0; m_dropEvents = 0; } | |
| // Test-only: production gets a fresh TxPacketizer per session, so nothing | |
| // in src/ resets these — see IcomCivBackend::disconnectRadio(). | |
| void resetDropCounters() noexcept { m_droppedBytes = 0; m_dropEvents = 0; } |
or drop it entirely and have the test construct a second TxPacketizer for the "counters start at zero" case, which also proves the per-session freshness the production comment relies on rather than asserting it.
Addresses @ten9876's second nit on aethersdr#5162: resetDropCounters() has no production caller — its only two uses are in the test. That is consistent with the design (IcomSession builds a fresh TxPacketizer per connect, so the counters start at zero every session), but it is public surface that exists for the test and a future reader would reasonably assume something calls it. Now says so in place, rather than dropping it and making the test construct a second packetiser — the two-sequence assertion reads better with the reset. The first nit — the test is on no CI gate — is deliberate and stays that way in this PR. 9231513 dropped that edit because two other open PRs (aethersdr#5244 and aethersdr#5240) both edit the same `ctest -R` regex on the Icom gate and will conflict with each other; adding a third would make it worse. The reviewer is right that a 0.38 s pure-arithmetic test with no Qt, no event loop and no hardware is the exact profile those gates describe as worth failing a merge on, so it is worth adding once those settle — as a separate change, since build-time CI gates are maintainer-merge territory. Verified: icom_tx_packetiser_drop_test passes (all checks passed). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks — and the things you checked that turned out fine are as useful to me as the nits, particularly tracing the threading rather than assuming a race from the new non-atomic counters.
The CI gate stays off in this PR, deliberately. On your last point, which is the one that deserves a straight answer: the production overflow path is inferred from the code, not observed on hardware. I have no IC-9700 or IC-705 on this bench. What is observed is the counter mechanism itself: the unit test drives the overflow directly, and I mutation-tested the central claim the same way you did. |
|
Bump for review — this one is small, standalone, and is the blocker for shrinking #5058. What it is: This counts the drops and warns on the edge with a running total. Behaviour is otherwise unchanged — nothing new is dropped or kept, the loss is just no longer invisible. Why it is separate: split out of #5058 at @aethersdr-agent's suggestion, with its own falsifiability-proven test ( Verification: Windows/MSVC clean; the test observes a real 40960-byte drop event rather than asserting on an exit code. Same limits as its parent — Windows only, IC-9700 over RS-BA1, one radio. No rush if the queue is deep — flagging it mainly because #5058 cannot shed these commits until this lands. 73, Nigel G0JKN |
|
Bump for review — this one is small, standalone, and has already been through a clean review round. 221 additions across 8 files, one deletion. @ten9876 reviewed it on macOS/arm64 and found no blockers, having mutation-tested the central claim rather than taking it (
Why this one matters beyond itselfIt is the blocker for shrinking #5058. Commit
|
Bringing the branch current with main (3096621) ahead of review. MERGE, not rebase, deliberately. Replaying this branch's commits conflicts in .github/workflows/ci.yml: an earlier version of commit a544f2b added tx_packetiser_drop to the Icom test gate, and 9231513 exists to remove that edit again. main has since hardened the same block with a count-pin, so the naive rebase resolution silently discards upstream's newer gate in favour of my withdrawn one. Merging keeps ci.yml byte-identical to main, which is the correct end state -- this PR's net diff does not touch it.
|
Brought current with Merged, not rebased, and that was deliberate. As flagged in the bump above: replaying this branch conflicts in Verified after the merge:
Built and tested on Windows/MSVC at Still no radio on the bench for this push, so the caveats in the bump stand unchanged: the counter is pinned by the unit test and @ten9876's mutation run, not re-measured against a live IC-9700 — and |
CI is green on
|
|
Superceded by #5311 |
Summary
TxPacketizer::submit()discards the OLDEST bytes past its 250 ms cap. For voice that rule is right, and the comment at the drop site argues it well: latency must not grow, and the freshest audio is what matters. For a digital burst it is destructive in a way nothing reported — the oldest bytes of an AX.25 transmission are the preamble and the opening flag, so what survives keys the radio, sounds exactly like packet on a receiver, and syncs on nothing.Nothing surfaced that. No error, no gap, no counter: a transmission that lost 63% of itself looked identical to one that did not.
This PR makes the drop visible. It does not change the drop rule.
submit()counts dropped bytes and overflow events; one oversized submit is one event, so a single huge write and a thousand small ones are distinguishableIcomSession::StatscarriestxDroppedBytes/txDropEvents/txPendingBytesonLinkTickreports new drops at WARNING on the edge — running total plus pending/cap ratio — rather than once per dropped byte (an oversized submit sheds thousands of bytes in one loop)IcomAudio.cpppure protocol: no QObject, no Qt logging, which is also what lets the unit test link it standaloneTest
icom_tx_packetiser_drop_test— standalone, linksIcomAudio.cpp+Qt6::Coreonly. 13 checks:droppedBytesequals submitted minus retained — the arithmetic, not the counter's own word for itresetDropCounters()clears both countersThe test was proven falsifiable before being trusted: with
++droppedcommented out it fails 5 of 13 checks and exits non-zero. All 13 pass on this branch (Windows/MSVC, Qt 6.10.3).Origin and scope
Split out of #5058 per review feedback there — this piece is self-contained and independent of the tap-architecture discussion, so it can land on its own.
Context is #5011: the failure this instrument makes visible — a burst losing its front while still sounding intact — is the same symptom that PTT timing produces by another route (see the code trace in that issue). Distinguishing the two on a live transmission is exactly what this counter is for: if a truncated burst shows
txDroppedBytesadvancing, the packetiser did it; if the counter stays flat, the front was lost before the radio was keyed.This is diagnosis, not a fix: the drop rule is unchanged, and a burst that overflows still loses its front.
Not exercised here: the
onLinkTickWARNING path against a live RS-BA1 session — the unit test covers the counter arithmetic, and the reporter compiles and links, but no radio was on the bench for this branch. The standalone test is the proof boundary.🤖 Generated with Claude Code
Evidence: what is observed vs. what is inferred
Raised by @ten9876 in review, and it belongs in the body rather than a comment.
Observed. The counter mechanism itself.
icom_tx_packetiser_drop_testdrives the overflow directly and asserts the counts; the central claim was mutation-tested (undercounting by half fails the test).Inferred, not observed. The production path that makes this matter —
sendAudio()enqueuing whileonTxPump()drains nothing until the codec reports ready — is reasoned from the code. I have no IC-9700 or IC-705 on this bench and have not watched the overflow fire on real hardware.That distinction is load-bearing: it is the difference between "a silent front-of-queue discard is now visible" (demonstrated) and "this silently corrupts AX.25 bursts in the field" (argued from the code, and consistent with #5011, but not witnessed here).