Summary
MetisClient::setBandFilter() queues a copy of m_ccConfig into m_oneShot. That one-shot is redundant, and because it is a snapshot, it can put a stale config on the wire.
Found during review of #4503 (not a regression from it — the one-shot is added by that PR, but the effect is bounded and was not worth blocking on).
Why it is redundant
buildNextControlPacket() builds every EP2 frame as:
auto pkt = ep2Packet(m_txSeq++, withMox(m_ccConfig, keyed), withMox(b, keyed));
Bank A is live m_ccConfig on every single frame. The one-shot only ever populates bank B. So the stated purpose — "a band change moves the NCO and the filter in the same gesture, don't wait for the round robin" — is already satisfied by bank A without the push.
Why it can be stale
Bank B is applied after bank A, so when the two disagree the snapshot wins for that frame:
setBandFilter(X) pushes snapshot S = ccConfig(rate_old, numRx, X).
setSampleRate(rate_new) rebuilds m_ccConfig = ccConfig(rate_new, numRx, X).
- Next frame: bank A =
rate_new, bank B = S = rate_old. The radio ends on rate_old.
Bank A re-asserts the truth on the following frame, so the visible effect is one EP2 frame (~1 ms) of stale DDC rate. Small, but it is a real transient the code does not intend.
Additionally, m_oneShot is not cleared in start() (it is cleared nowhere). Hl2Backend::applyBandFilter() has no m_connected guard and m_metis outlives a disconnect, so a snapshot queued while disconnected can replay on the next session's first frames.
Suggested fix
Delete the m_oneShot.push_back(m_ccConfig) in setBandFilter(). Bank A already does the job, and removing the push eliminates the staleness with it.
If the push is kept for some reason not visible here, then m_oneShot.clear() belongs in start() alongside the other per-session resets (m_txSeq, m_roundRobin, m_haveRxSeq, m_drops, m_linkUp).
Summary
MetisClient::setBandFilter()queues a copy ofm_ccConfigintom_oneShot. That one-shot is redundant, and because it is a snapshot, it can put a stale config on the wire.Found during review of #4503 (not a regression from it — the one-shot is added by that PR, but the effect is bounded and was not worth blocking on).
Why it is redundant
buildNextControlPacket()builds every EP2 frame as:auto pkt = ep2Packet(m_txSeq++, withMox(m_ccConfig, keyed), withMox(b, keyed));Bank A is live
m_ccConfigon every single frame. The one-shot only ever populates bank B. So the stated purpose — "a band change moves the NCO and the filter in the same gesture, don't wait for the round robin" — is already satisfied by bank A without the push.Why it can be stale
Bank B is applied after bank A, so when the two disagree the snapshot wins for that frame:
setBandFilter(X)pushes snapshotS = ccConfig(rate_old, numRx, X).setSampleRate(rate_new)rebuildsm_ccConfig = ccConfig(rate_new, numRx, X).rate_new, bank B =S=rate_old. The radio ends onrate_old.Bank A re-asserts the truth on the following frame, so the visible effect is one EP2 frame (~1 ms) of stale DDC rate. Small, but it is a real transient the code does not intend.
Additionally,
m_oneShotis not cleared instart()(it is cleared nowhere).Hl2Backend::applyBandFilter()has nom_connectedguard andm_metisoutlives a disconnect, so a snapshot queued while disconnected can replay on the next session's first frames.Suggested fix
Delete the
m_oneShot.push_back(m_ccConfig)insetBandFilter(). Bank A already does the job, and removing the push eliminates the staleness with it.If the push is kept for some reason not visible here, then
m_oneShot.clear()belongs instart()alongside the other per-session resets (m_txSeq,m_roundRobin,m_haveRxSeq,m_drops,m_linkUp).