feat(protocol-v2): protocol core — COBS/CRC-16 framing, claims-gated slots, system commands, board profiles (native-tested, not yet wired)#581
Open
hongquanli wants to merge 14 commits into
Conversation
…-phase1 branch) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standard Cheshire & Baker COBS encode/decode, no heap. Removes all 0x00 bytes so the delimiter is unambiguous. Decode rejects embedded zeros, truncated input, code bytes past end, and output overflow. Round-trip verified for lengths 0/1/2/253/254/255/506 with zeros at head/middle/tail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single source of truth for protocol-v2 frame layout: packed little-endian structs (FrameHeader, Slot, RingEntry, AxisStateWire, SeqProgressWire, StandardResponse=158B, HelloPayload=16B, InfoPayload=22B, DiagPayload=40B, FaultEntryWire=8B), enums (FrameType/Flags/ResponseStatus/CommandType/ ErrorCode), sizing constants (kMaxFrame=512, kMaxPayload=506, kProtocolVersion=2), and resource-bit helpers. Compile-time static_asserts pin every size on both x86_64 and ARM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…and non-blocking TX RX accumulates bytes to a 0x00 delimiter, COBS-decodes, CRC-16 checks, and delivers header+payload (no CRC) to a FrameSink. Because 0x00 never appears inside an encoded frame, every delimiter is a clean resync point. TX appends CRC, COBS-encodes, and emits frame+0x00 to a ByteSink; drops (tx_drop++) and returns false when the sink is full — never blocks. Fixed 516-byte buffers, no heap. Observable counters: crc_err, resync, rx_overflow, tx_drop, frames_ok. Tests cover happy path, back-to-back frames, CRC-corruption recovery, truncation resync, garbage-burst recovery, oversize drop, non-blocking TX with round-trip, and a deterministic single-byte corruption sweep over every position of every 7th of 200 frames asserting at most one frame lost per corruption. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claims_table.h holds one authoritative row per command (static mask or a computed override for payload-dependent claims, used by the sequencer in Phase D); Phase B populates system commands only (0 claims). claims_for() looks up the production table; claims_for_in() takes an explicit table for tests and Phase D. claims_conflict() returns 0 when compatible, else the lowest-set conflicting resource bit index + 1. NOTE: claims_table.h is the security-critical resource map and needs Hongquan's explicit review (flagged in the PR description). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SlotManager gates concurrency across 5 slots: try_accept() dedups retries, rejects resource conflicts (RejectBusy + blocking resource id + holder cmd_id) and slot exhaustion (RejectNoSlots), or reserves a slot. complete() frees the slot and records the outcome in an 8-entry ring with an advancing head_seq (wraps keeping the newest 8). RETRY semantics: active cmd_id -> ActiveDuplicate (live state); completed cmd_id in ring -> CompletedDuplicate (replayed outcome, never re-executed); unknown cmd_id -> treated as new. A non-retry command reusing a recently-completed id is new, since cmd_ids recycle faster than the ring forgets. fill_response() emits the slots+ring wire section; reset() clears the session for protocol RESET / HELLO. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…antics, DIAG) build_response() turns a REQUEST frame into a RESPONSE frame: a StandardResponse (status/error + slots/ring from the SlotManager + machine state from an injected StateProvider) followed by any command-specific extra payload. Immediate system queries (GET_STATE/HELLO/GET_INFO/DIAG) answer synchronously without a slot; slotted commands route through SlotManager with claims_for gating, and a RETRY of a completed command is answered from the ring without re-invoking its handler. HELLO appends session info and resets the session (clears slots); DIAG page 0 returns counters, page >=1 returns fault-ring entries. Unknown cmd_type -> ERR_UNKNOWN_COMMAND, out-of-range payload -> ERR_BAD_LENGTH. The FrameSink glue sends the built frame through a bound Framer (set_framer breaks the ctor cycle). Verified: pio test -e native (134 cases) and pio run -e teensy41 both pass — the protocol core compiles into the ARM binary but is not yet wired to SerialUSB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The layout guarantees lived only in test_frames.cpp (native-only). Placing them in the header itself enforces them on every target that compiles the wire contract — native tests, teensy41, the future teensy41_boardv2, and CI — so ARM size drift breaks the build at the source of truth (design risk #3), not just the host test. test_frames.cpp keeps its copies as an explicit contract statement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-tested core + teensy41 binding)
Pure boot core (boot.{h,cpp}) drives the power-up sequence against an injected
BootHal: safe_state() FIRST, then reset-cause capture-once (+ register clear),
persistent boot-count increment, session-nonce mint (MurmurHash3 finalizer over
boot_count+cycle_counter — bijective, so distinct boots always differ and it is
never zero), and watchdog arm. A 16-entry fault ring lives in EEPROM and
survives reboots for post-mortem DIAG; loop/ISR timing tracks max-only.
boot_bind_teensy41.cpp implements BootHal on RT1062 (WDOG1 service sequence,
SRC_SRSR reset-cause mapping, EEPROM emulation, DWT CYCCNT) — compiled into the
teensy41 binary only, bench-verified in Phase C (design risk #1: RTWDOG vs
WDOG1; safe_state output inventory is a Phase C stub).
Verified: pio test -e native (143 cases) and pio run -e teensy41 both pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
board.h declares board_descriptor() (const InfoPayload&) plus board-scoped pin constants (v1 camera triggers 29-32, ready-line pin kBoardReadyLinePinV1 = TBD). board_squid_v1.cpp: 5 axes (drivers runtime-probed -> 0, Phase M), DAC80508 (8), 5 illum TTL, LED matrix, 4 triggers, 1 ready input, 16 program channels. board_squid_v2.cpp (stub): 8 triggers, 2+8 ready inputs, TMC2240 drivers. Exactly one board_*.cpp links per build: platformio env teensy41 (default v1) and new env teensy41_boardv2 (-DBOARD_SQUID_V2) each exclude the other via the src filter. CI firmware.yml now builds both envs. Native test_board / test_board_v2 assert per-board values and that counts fit their wire-response field widths. Verified: pio test -e native (145 cases), pio run -e teensy41 and -e teensy41_boardv2 all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
software/control/protocol_v2/: pure-Python mirror of the firmware codec — crc16 (CCITT-FALSE, table computed at import), cobs (Cheshire & Baker), frames (all wire constants + packed struct formats + encode_frame/decode_frame), and a Client over an injected Transport (RETRY flag, cmd_id correlation, timeout). Cross-language integrity is enforced three ways: - test_protocol_v2.py parses frames.h by regex (extending the FirmwareSimSerial pattern) and asserts every Python constant + struct size equals the header's (frames.h stays the single source of truth); - gen_protocol_golden.py emits protocol_v2_golden.json AND test_golden/ golden_cases.h from one definition of representative frames (system commands, 158B response, 506B max payload, all-zeros) using the Python codec as reference — deterministic, so CI regenerates and git-diffs it; - the C test_golden test and the Python golden test both reproduce every wire vector byte-for-byte (encode) and recover it (decode). Verified: pytest tests/control/test_protocol_v2.py (19 passed), pio test -e native (147 cases incl. test_golden), generator idempotent, black clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fuzz/fuzz_framer.cpp feeds arbitrary bytes through the full RX -> dispatch -> TX path (COBS decode, CRC, command dispatch, slots, COBS encode) with fake state, letting the input steer TX backpressure. Doubles as a libFuzzer entry (CI) and, under -DFUZZ_STANDALONE, an ASAN smoke driver for local runs (Apple clang ships no libFuzzer runtime). CI (firmware.yml) gains two jobs: static-analysis (cppcheck + clang-tidy over src/protocol, src/boot, src/hal — .clang-tidy pins a focused, high-signal check set with WarningsAsErrors) and fuzz (clang -fsanitize=address,fuzzer, 60s). Zero-initialize the Framer/Dispatcher fixed buffers (write-before-read by design) so cppcheck's uninitMemberVar is clean and reads are deterministic — negligible cost (singletons in production). Verified locally: cppcheck clean, clang-tidy clean (LLVM 22 via pip wheel), ASAN standalone driver clean over 100k random + structured/corrupted inputs, 147 native tests still green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add src/protocol/, src/boot/, src/hal/, fuzz/, and the new test dirs to the controller source tree, plus a "Protocol v2 (Phase B)" note stating the modules compile in but are not wired to SerialUSB — the v1 path stays live until Phase C's switchover — and pointing to the host codec and golden-vector generator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… comments) Reword the crc16 header/impl comments to present CRC-16/CCITT-FALSE by its standard parameters (poly 0x1021, init 0xFFFF, check 0x29B1) and its role as the wire-integrity contract, rather than as a copy of an earlier branch. The 256-entry table is the standard CCITT table (identical to what any correct generator emits and to the Python mirror crc16.py). Also replace the `while (length--)` loop with an explicit index loop: same computation, but avoids the decrement-in-condition idiom that some clang-tidy versions false-positive as bugprone-infinite-loop. No behavior change (0x29B1 check value and golden vectors unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase B of the firmware-v2 modernization: the protocol-v2 core as pure,
natively-tested modules, plus the mirrored Python codec with cross-language
golden vectors. Implements design doc
2026-07-04-firmware-v2-design.md§4(protocol) and §14 (robustness), decisions D1/D3/D5/D6/D8/D9.
Zero runtime behavior change. These modules compile into the firmware
binary but are NOT wired to
SerialUSB— the v1 protocol inserial_communication.cppremains the only live path. Phase C performs thesingle-PR switchover (D4: no dual-stack ships).
What's included
crc16), COBS codec (cobs), and a COBSframerwith a guaranteed ≤1-frame-loss resync and non-blocking TX(
tx_dropon backpressure). All counters observable — no silent drops.frames.his the single source of truth: packedlittle-endian structs with compile-time
static_asserts(
StandardResponse=158,HelloPayload=16,InfoPayload=22,DiagPayload=40) enforced on native and ARM builds.claimstable + conflict checker, and a 5-slotSlotManagerwith an 8-entry completion ring and RETRY dedup (retry of acompleted command is answered from the ring, never re-executed).
HELLO(session reset +nonce/boot-count/reset-cause),
GET_INFO,GET_STATE, andDIAG(page 0counters + page N fault ring); machine state read via an injected
StateProvider.boot.{h,cpp}pure core (safe-state-first, reset-causecapture, persistent boot count, session nonce, EEPROM fault ring, timing
watermarks) + a Teensy 4.1 binding (WDOG1 / SRC_SRSR / EEPROM / DWT).
GET_INFOdescriptors for Squidv1 and v2, with a new
teensy41_boardv2PlatformIO env.software/control/protocol_v2/mirrors the C codecbyte-for-byte; a
Clientover an injectedTransport.src/protocol|boot|hal,a 60-second libFuzzer run over the RX→dispatch→TX path, and a golden-vector
drift check (
git diff --exit-code).src/protocol/claims_table.hclaims_table.his the authoritative map of which hardware resources eachcommand claims — an incorrect row lets two commands touch the same hardware
concurrently. Phase B populates system commands only (0 claims); motion /
illumination / camera rows land with their handlers in Phase C/D. Please
review this file's contents explicitly.
Verification
pio test -e nativepio run -e teensy41pio run -e teensy41_boardv2pytest tests/control/test_protocol_v2.pyNotes / follow-ups for Phase C
expose RTWDOG in
imxrt.h). WDOG1_WCR encoding, SRC_SRSR reset-cause mapping,and
safe_state()output inventory are bench-verified in Phase C — thenative-tested core is binding-agnostic.
n_ready_inputs = 10exceeds the 8-bitcam_ready_mask; only the≤ kMaxCameras (8) camera-ready lines map to the mask (the count is
informational). Revisit when v2 hardware firms up.
kBoardReadyLinePinV1,marked TBD) — needed at Phase D.
Stats: 14 commits, 45 files, +4866/−17 on top of
ee7897c9.🤖 Generated with Claude Code