feat(safety): refuse pre-Rev-2.2 shields for VPP-on-line-11 chips (CAP-02) - #45
Merged
Conversation
CAP-02 host side. Chips whose bus-config routes VPP to bus line 11 (socket
pin 21 on the DIP24_2716 / DIP24_2532 pinouts) need the 3-position JP4 header
introduced on RURP shield Rev 2.2. Driving them on an earlier shield is a
chip-damage path. The host now refuses at connect time, before the firmware
engages the VPP regulator.
Retires the CMD_FW_VERSION pre-probe
--------------------------------------
_probe_port used to send a dedicated CMD_FW_VERSION command and consume two
acks purely to read the version, THEN send the user's command. The firmware
now carries both the version and the effective hardware revision in the
MSG_OK_READY ack of whatever command it is answering, so the user's command
goes first and both gates run off its ack. One fewer command exchange per
connect.
Sending the real command before validating is safe by construction, not by
luck: init_programmer_framed does run configure_memory before emitting
MSG_OK_READY, but every configure_* handler is pure (function-pointer
assignment and pulse defaults only). The VPP regulator is not engaged until
firestarter_operation_init, which blocks on op_wait_for_ack(). Raising here
means that ack is never sent, so the operation never starts and the rail
stays down.
The gate is an ALLOWLIST, not a comparison
------------------------------------------
The REVISION_* bytes are not a version-ordered scale. REVISION_UNKNOWN is
0xFE -- numerically ABOVE REVISION_2_2 (0x04) -- so the obvious
`detected >= REVISION_2_2` spelling would admit precisely the boards whose
revision could not be determined. Membership in {REVISION_2_2, REVISION_2_3}
fails closed for 0xFE, for the 0xFF override-absent sentinel, for the
REVISION_2_0 broad bucket, and for None (pre-CAP-02 firmware). A planted
`>=` was confirmed to turn the two trap tests red before the allowlist went
back in.
ADC detection collapses Rev 2.0/2.1/2.2 into REVISION_2_0, so a genuine
Rev 2.2 is refused until the operator writes the EEPROM override. That is
the design: the operator must look at the physical header and assert it, and
that assertion is the safety mechanism. The refusal message names the exact
byte to write, because `config --rev` casts through int() and '--rev 2.2'
silently truncates to the Rev 2.0 bucket.
Collateral fixed, not papered over
----------------------------------
The Phase-133 SerialError census gate (test_chip_test_sdp_leg.py) fired on
the new subclass and surfaced a genuine defect: chip_test.py::_run_step's
re-raise clause named only two subclasses, so a shield-revision refusal would
have degraded to a BAD step per remaining operation -- reporting a
damaged-looking chip when the real cause was the wrong shield. _run_step now
re-raises it; the census is updated to four with the reasoning recorded.
firmware_identity / hw_revision are declared at CLASS level because several
suites patch __init__ to a no-op and conftest's make_comm builds instances
via __new__; instance-only attributes would AttributeError into _probe_port's
broad `except Exception` and degrade to "no programmer found".
No catalog change: MSG_OK_READY is already declared `params = [{type =
"bytes"}]`, so codegen drift stays clean (verified).
Tests: 1532 pass (was 1508). 22 new in test_hw_revision_gate.py covering the
policy, the extended/legacy/truncated ack decode, and the gate's coupling to
the real pinout data. ruff clean. The mypy watermark gate cannot run in this
devcontainer (py3.12 vs the pinned CI version -- numpy stubs fail to parse);
that failure is pre-existing and identical with these changes stashed.
Co-Authored-By: Claude Opus 5 (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.
Host half of the CAP-02 pair. Must land together with henols/firestarter#49 — this host refuses firmware that predates that change.
The safety change
Chips whose bus-config routes VPP to bus line 11 (socket pin 21 on the
DIP24_2716/DIP24_2532pinouts) need the 3-position JP4 header introduced on RURP shield Rev 2.2. Driving them on an earlier shield is a chip-damage path. The host now refuses at connect time, before the firmware engages the VPP regulator.Retires the CMD_FW_VERSION pre-probe
_probe_portused to send a dedicatedCMD_FW_VERSIONcommand and consume two acks purely to read the version, then send the user's command. Firmware now carries the version and the effective hardware revision in theMSG_OK_READYack of whatever command it is answering, so the user's command goes first and both gates run off its ack. One fewer command exchange per connect.Sending the real command before validating is safe by construction, not by luck:
init_programmer_frameddoes runconfigure_memorybefore emittingMSG_OK_READY, but everyconfigure_*handler is pure — function-pointer assignment and pulse defaults only. The VPP regulator is not engaged untilfirestarter_operation_init, which blocks onop_wait_for_ack(). Raising here means that ack is never sent, so the operation never starts and the rail stays down.The gate is an allowlist, not a comparison
The
REVISION_*bytes are not a version-ordered scale:detected >= REVISION_2_2would admit precisely the boards whose revision could not be determined. Membership in{REVISION_2_2, REVISION_2_3}fails closed for0xFE, for the0xFFoverride-absent sentinel, for theREVISION_2_0broad bucket, and forNone(pre-CAP-02 firmware).A
>=was planted and confirmed to turn exactly the two trap tests red before the allowlist went back in — the tests are not vacuously green.The override requirement is the safety mechanism
ADC detection collapses Rev 2.0/2.1/2.2 into
REVISION_2_0, so a genuine Rev 2.2 is refused until the operator writes the EEPROM override. That is the design, not a gap: the operator has to look at the physical header and assert it.The refusal message names the exact byte to write, because
config --revcasts throughint()and--rev 2.2silently truncates to2— selecting the Rev 2.0 bucket, the opposite of what the operator intends. That truncation is a live trap inconfigand is not fixed here.Collateral fixed, not papered over
The Phase-133
SerialErrorcensus gate fired on the new subclass and pointed at a genuine defect:chip_test.py::_run_step's re-raise clause named only two subclasses, so a shield-revision refusal would have degraded to a BAD step per remaining operation — reporting a damaged-looking chip when the real cause was the wrong shield._run_stepnow re-raises it; the census is updated to four with the reasoning recorded.firmware_identity/hw_revisionare declared at class level because several suites patch__init__to a no-op andconftest'smake_commbuilds instances via__new__. Instance-only attributes wouldAttributeErrorinto_probe_port's broadexcept Exceptionand degrade to "no programmer found".Compatibility — breaking, by design
len == 2test → falls to the 512 chunk floor. Correct, halves Leonardo throughput.FirmwareOutdatedErrorLockstep host+firmware upgrade, consistent with PROJECT.md's stated constraint.
Tests
1532 pass (baseline 1508). 22 new in
tests/test_hw_revision_gate.pycovering the policy, the extended/legacy/truncated ack decode, and the gate's coupling to the real pinout data —test_exactly_two_pinouts_emit_the_gated_vpp_lineasserts{DIP24_2716, DIP24_2532}against the live database, so a pinout edit that moves chips in or out of the gate's scope fails loudly.ruff lint + format clean. The mypy watermark gate could not be run locally — it exits 2 on numpy stubs under the devcontainer's py3.12 versus the pinned CI version, identically with these changes stashed. CI runs it on the pinned version; that result is the one that counts. Direct mypy over the 8 strict modules is clean apart from a pre-existing
submit.py:666error.Reviewer attention, please
🤖 Generated with Claude Code