Skip to content

feat(transport): COBS+CRC16 serial framing for MCUboot raw serial recovery#122

Open
JPHutchins wants to merge 1 commit into
mainfrom
feat/cobs-raw-serial-transport
Open

feat(transport): COBS+CRC16 serial framing for MCUboot raw serial recovery#122
JPHutchins wants to merge 1 commit into
mainfrom
feat/cobs-raw-serial-transport

Conversation

@JPHutchins

Copy link
Copy Markdown
Collaborator

Adds COBS+CRC16 framing to SMPSerialRawTransport, so smpclient can drive MCUboot serial recovery built with the new self-synchronising raw protocol.

Upstream references

  • intercreate/mcuboot#5boot: boot_serial: add COBS framing for raw serial recovery (the CONFIG_BOOT_SERIAL_RAW_PROTOCOL_COBS this transport speaks; draft, layered on the raw protocol below).
  • mcu-tools/mcuboot#2755 — the raw (non-console) serial recovery protocol (BOOT_SERIAL_RAW_PROTOCOL) this builds on.
  • mcu-tools/mcuboot#2746 — the MCUmgr params command recovery advertises, so the buffer is negotiated.
  • intercreate/smp-server-fixtures#12 — the fixture this vendors, built (with west patch of mcuboot#5) and released as fa39f8c2.

On the wire

Each SMP message is framed as COBS(header ‖ payload ‖ CRC16) ‖ 0x00. The CRC is CRC-16/XMODEM over the message (reused from smp.packet); COBS byte-stuffing removes every 0x00, so the single trailing 0x00 is an unambiguous delimiter. The COBS codec matches MCUboot's boot/boot_serial/src/cobs.c byte-for-byte. Because the delimiter is a resync point and the CRC rejects damage, the decoder drops a corrupt or truncated frame and resyncs at the next delimiter rather than failing the stream.

Design

  • SerialFraming Protocol (transport/serial/framing/): encode / feed / take / reset — a wire framing plus a stateful per-connection decoder.
  • Cobs — a @dataclass(frozen=True, slots=True) that owns its reassembly buffer (field(default_factory=bytearray) → fresh per instance, mutated in place; frozen blocks rebinding, not mutation). One instance per transport.
  • SMPSerialRawTransport(framing=...)None (default) keeps the bare [header][payload] length-prefixed path unchanged; framing=Cobs() opts into COBS. receive() just pumps bytes through feed/take; reset() clears the buffer on (re)connect. max_unencoded_size is unchanged (COBS keeps the CRC in extra server-buffer space, so the cap is the advertised buf_size, same as raw).

COBS isn't auto-negotiated — there's no params bit for it (mcuboot#5: "the SMP client must be configured to use a matching COBS+CRC16 transport"), so it's an explicit opt-in.

Tests

  • Unit (tests/test_cobs.py, tests/test_smp_serial_raw_transport.py): COBS codec round-trip + run-length-boundary + malformed + deterministic fuzz; framing leftover-across-feeds, multi-frame-per-feed, byte-at-a-time, randomly-chunked stream; resync past bad-CRC / leading garbage / empty frames; lone-corrupt → None; empty-message frame dropped; delimiter-less buffer capped (64 KiB) + resync; per-instance buffer independence + reset(); and the transport's framed send/receive incl. a regression test that a non-stop non-framing stream still lets an outer timeout fire.
  • Integration (tests/integration/test_serial_recovery.py): the MCUboot recovery test is now one parameterized test (sum type, match/assert_never) over every framing — console Auto, console BufferSize, raw, and raw+COBS — each asserting the negotiated cap/wire size, uploading against real QEMU (mps2/an385), and confirming the recovery img group is coherent afterward. test_serial_recovery_raw.py is folded in.
  • DFU verification: test_image_management.py now asserts a completed upload's image (ImageInfo SHA256 TLV) actually appears as a slot hash, not merely that something landed.

ruff / pydoclint / mypy --strict / unit all green; recovery + DFU integration pass against QEMU.

The change was adversarially stress-tested by independent reviewers (crafted byte streams against the decoder + a diff review); confirmed findings — a busy-loop hang on a non-framing stream, an empty-message-frame acceptance, an unbounded buffer, and a vacuous resync test — were all fixed and have regression tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KKXy7L2Udjw929nie2c363

Add a pluggable `SerialFraming` to `SMPSerialRawTransport` plus a `Cobs`
framing that wraps each raw SMP packet as COBS(header || payload || CRC16)
|| 0x00 -- MCUboot's `BOOT_SERIAL_RAW_PROTOCOL_COBS` (intercreate/mcuboot#5,
layered on mcu-tools/mcuboot#2755). The trailing 0x00 makes the byte stream
self-synchronising and the CRC-16/XMODEM rejects damaged frames, so the
decoder drops a corrupt or truncated frame and resyncs at the next delimiter.

`Cobs` is a frozen, slotted dataclass that owns its connection's reassembly
buffer (the transport feeds/takes/resets it); `framing=None` leaves the bare
length-prefixed raw path unchanged.

Vendor the params-advertising `serial_recovery_raw_cobs` fixture from
smp-server-fixtures release fa39f8c2 (intercreate/smp-server-fixtures#12), and
unify the MCUboot serial-recovery integration test over every framing
(console Auto/BufferSize, raw, raw+COBS). Also strengthen the DFU upload test
to match the image's SHA256 TLV against the reported slot hash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKXy7L2Udjw929nie2c363
Copilot AI review requested due to automatic review settings June 25, 2026 21:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

2 participants