Skip to content

Key reassembly buffers by characteristic, validate sequence, drop truncated data#88

Open
kmatzen wants to merge 1 commit into
fix/correctness-and-test-gapsfrom
fix/packet-reassembly
Open

Key reassembly buffers by characteristic, validate sequence, drop truncated data#88
kmatzen wants to merge 1 commit into
fix/correctness-and-test-gapsfrom
fix/packet-reassembly

Conversation

@kmatzen

@kmatzen kmatzen commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Second of four stacked PRs. Base is fix/correctness-and-test-gaps (#87), not main.

Three defects in BLEPacketReconstructor. All of them corrupt camera state silently rather than failing visibly, which is why none showed up as a bug report.

Continuations routed by recency

Continuations went to whichever buffer for that peripheral was touched most recently, because nothing in the packet correlates it to a message.

The root cause is the buffer key. (peripheral, queryID) invents concurrency the protocol does not have, while ignoring the concurrency it does: query responses (0x0077) and settings responses (0x0075) are separate notify characteristics that stream independently, and both fed one buffer set. Buffers are now keyed by (peripheral, characteristic), matching the spec's per-characteristic accumulation, which makes routing deterministic.

Reproduced against the pre-fix code with the real ordering — start query 0x13, start settings 0x12, then 0x13's continuation arrives:

delivered as queryID 0x12
payload contains B's bytes (0xBB): true
payload contains A's bytes (0xDD): true
>>> CONTAMINATED: query 0x13's fragment delivered inside settings 0x12

Sequence counter never validated

The 4-bit counter documented in BLE_PROTOCOL.md:29 was parsed by nobody. A dropped or duplicated continuation produced a byte-shifted payload that still satisfied the length check and decoded as plausible-but-wrong TLV — wrong resolution, FPS, battery. It is now validated, and a gap discards the message in progress.

Worth being explicit: sequence validation alone would not have fixed the routing bug. Two messages interleaved at the same fragment position carry the same counter, so the check cannot tell them apart. Re-keying does that work; the counter catches drops and duplicates.

Truncated buffers broadcast to every camera

checkTimeouts returned (data, queryID) and discarded the peripheral half of the key, so BLEManager applied one camera's partial buffer to every connected camera — which then fed hasSettingsMismatch and could trigger a settings rewrite of the whole group. A dropout on one camera could rewrite the fleet.

Truncated buffers are now discarded. Nothing is delivered on timeout, so the broadcast loop is gone entirely.

Smaller fixes

  • clearBuffers(for:) matched peripherals by bare prefix, so clearing p1 also cleared p10.
  • A start packet arriving mid-message now discards the superseded one loudly instead of overwriting it silently.
  • A negative declared length is rejected.

Testing

128 tests pass, including five new ones covering sequence gaps, duplicates, cross-characteristic isolation, superseded messages, and timeout discard. Mutation-verified — reverting the key to peripheral-only fails the isolation test; disabling sequence validation fails the gap and duplicate tests.

Review notes

The design was checked with a TLA+ model (specs/PacketReassembly.tla, added in #90) before the code was written. Behaviour on real hardware is not verified — in particular, the counter starts at 0 for the first continuation, which matches this repo's existing tests and the Open GoPro convention, but is worth a sanity check against a camera.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AqfipPBp3eErDgZboooqxP

…ncated data

Three defects in BLEPacketReconstructor, all of which corrupt camera state
silently rather than failing visibly.

Continuations were routed to whichever buffer for that peripheral was touched
most recently, because nothing in the packet correlates it to a message. That
is not a tuning problem: buffers were keyed by (peripheral, queryID), which
invents concurrency the protocol does not have while ignoring the concurrency
it does. Query responses (0x0077) and settings responses (0x0075) are separate
notify characteristics that stream independently, and both fed one buffer set,
so a continuation could be appended to an unrelated message. Buffers are now
keyed by (peripheral, characteristic), which matches the spec's
per-characteristic accumulation and makes routing deterministic.

The 4-bit continuation sequence counter documented in BLE_PROTOCOL.md was never
parsed, so a dropped or duplicated packet produced a byte-shifted payload that
still satisfied the length check and decoded as plausible-but-wrong TLV. It is
now validated, and a gap discards the message in progress.

checkTimeouts force-completed truncated buffers and dropped the peripheral half
of the buffer key on the way out, so BLEManager applied one camera's partial
data to every connected camera — which then fed hasSettingsMismatch and could
trigger a settings rewrite of the whole group. Truncated buffers are now
discarded, so nothing is delivered on timeout and the broadcast loop is gone.

Also: clearBuffers(for:) matched peripherals by bare prefix, so clearing "p1"
would also clear "p10"; a start packet arriving mid-message now discards the
superseded one loudly instead of silently overwriting it; and a negative
declared length is rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqfipPBp3eErDgZboooqxP
@kmatzen
kmatzen force-pushed the fix/packet-reassembly branch from 372aafd to 2f8cee9 Compare July 19, 2026 04:45
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.

1 participant