diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ed4ec4e..bb0cac5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -54,6 +54,12 @@ are in [AGENTS.md](AGENTS.md).
- Do not change E-stop, brake, contactor, watchdog, motor-enable or charge-inhibit logic
unless the platform lead has agreed it in the issue; such changes need two human reviewers.
+**Hardware and procurement.** Every part in a BOM or procurement list must be available from
+stock at two or more independent suppliers. A single-source part may appear only as a named
+option next to a second-sourced alternative, and a part with no stock is not selected. The rule
+is recorded as COMPONENT-AVAILABILITY in [decisions.yaml](decisions.yaml) (P-03 rev20.2 item
+26). The automated check reads wording only; the reviewer checks the supplier columns.
+
## 4. Open a draft pull request
Open the PR as a **draft** and fill in every section of the template: work package,
diff --git a/STATE.md b/STATE.md
new file mode 100644
index 0000000..1f6b990
--- /dev/null
+++ b/STATE.md
@@ -0,0 +1,39 @@
+# STATE
+
+Read this file before any work in this repository and update it in the same PR as the work.
+The PR evidence check fails when STATE.md exists and a PR neither updates it nor says
+"no change" with a reason. Keep it under 60 lines; facts only, each with a date or SHA.
+
+## Repository
+
+- Type and lifecycle: governance, active
+- Readiness: not recorded
+- Owner role: platform-lead (decisions.yaml); ci-owner (tools/, workflows)
+- Verification: `bash rollout/verify.sh`, `python3 tools/check_decisions.py --decisions decisions.yaml --maintainers maintainers.yaml --validate-only`, `python3 tools/check_agent_rules.py --canonical agent-rules/SHARED_RULES.md --file AGENTS.md`; last PASS on main not recorded here (file added 2026-11-01; branch rebased on ed98001)
+
+## Register in force
+
+- `in_force`: P-03-rev20.2, 2026-11-01 (change request #77); on main ed98001 (#73 merged) it is P-03-rev20.1, 2026-10-30
+- Canonical BOM: Issue 7.14 (BOM-ISSUE-IN-FORCE), from 2026-11-01
+
+## Current work
+
+| Work package | Branch or PR | Status | Next step |
+|---|---|---|---|
+| #77 | #78 (claude/optimistic-sagan-oqc2lu) | draft, rebased on ed98001 | owner review of the P-03 rev20.2 item 26 values and the OPERATOR-PANEL portrait change |
+| P-03 rev20.1 | #73 | merged as ed98001 (2026-11-01) | none; its OPERATOR-PANEL portrait check is narrowed in #78 |
+| PLATFORM-PIN | #74 | open, not merged (2026-11-01) | touches decisions.yaml; rebase after whichever merges first |
+
+## Decisions this repository implements
+
+This repository holds the register (decisions.yaml) and its checkers; it implements no robot values.
+
+## Known gaps
+
+- Safety-path register updates (precharge fitted, K1/K2 decision 2 Nov 2026) left to the electrical owner; SAFETY-PROCUREMENT and SAFETY-CHAIN-IO unchanged since 71c439e
+- "Chest E-stop" wording retirement (P-03 rev20.2 item 26) not recorded; it belongs to SAFETY-PROCUREMENT
+
+## Not verified
+
+- Register values against the P-03 rev20.2 source document (held outside any repository), since 2026-11-01
+- Scans of consumer repositories against the new patterns, since 2026-11-01
diff --git a/WATCHDOG.md b/WATCHDOG.md
index 839100d..6deae9c 100644
--- a/WATCHDOG.md
+++ b/WATCHDOG.md
@@ -121,19 +121,19 @@ identical to the register.
| Decision | Status | Accepted on the same line (any one; case does not matter) |
|---|---|---|
-| BOM-ISSUE-IN-FORCE | recorded | *BOM Issue 7.10 is the canonical hardware BOM*: `supersed (superseded, supersedes)`, `replaced`, `previous`, `earlier`
*B-01 is superseded*: `supersed (superseded, supersedes)`
*BOM Issue 7.10 is the canonical hardware BOM*: `supersed (superseded, supersedes)`, `replaced`, `previous`, `earlier`, `historical`
*superseded source still cited (P-03-rev18.1 line 7); cite P-03-rev19.6*: no label; correct the line or add a decision-allow marker |
+| BOM-ISSUE-IN-FORCE | recorded | *BOM Issue 7.14 is the canonical hardware BOM*: `supersed (superseded, supersedes)`, `replaced`, `previous`, `earlier`
*B-01 is superseded*: `supersed (superseded, supersedes)`
*BOM Issue 7.14 is the canonical hardware BOM; Issue 7 to 7.13 (including 7.10 and 7.11) are retired*: `supersed (superseded, supersedes)`, `replaced`, `previous`, `earlier`, `historical`
*superseded source still cited (P-03-rev18.1 line 7); cite P-03-rev20.2*: no label; correct the line or add a decision-allow marker |
| MAST-INSTALL-HEIGHT | superseded | `supersed (superseded, supersedes)`, `legacy`, `historical`, `earlier`, `previous`, `rev 18.1 to rev 18.6`, `lift`, `L1350`, `extended`, `full stroke` |
| MAST-POSITIONS | superseded | `supersed (superseded, supersedes)`, `legacy`, `historical`, `earlier`, `previous`, `rev 18.1 to rev 18.6` |
| MAST-TOP-HEIGHT | superseded | `supersed (superseded, supersedes)`, `legacy`, `historical`, `earlier`, `previous`, `rev 18.1 to rev 18.6` |
| MAX-ASSEMBLED-HEIGHT | recorded | *maximum assembled height is 1700 mm*: no label; correct the line or add a decision-allow marker
*1700 mm is the assembled-height envelope, not a shoulder height or mast position*: `max`, `envelope`, `not shoulder, not the shoulder`, `higher`, `supersed (superseded, supersedes)` |
| DATUM-HEIGHT-STACK | recorded | *the steel chassis deck top is 294 mm above the floor*: `supersed (superseded, supersedes)`, `historical`, `legacy`, `earlier`, `previous`
*the lift base-plate top face is 304 mm above the floor, the reference for lift and shoulder heights*: `supersed (superseded, supersedes)`, `historical`, `legacy`, `earlier`, `previous` |
-| FRAMES-REP105 | recorded | *base_link is at the drive-axle midpoint and axle height; base_footprint is on the floor*: `base_footprint`, `not`, `never`, `supersed (superseded, supersedes)`, `historical`, `legacy`
*base_footprint is on the floor under the drive-axle midpoint*: `not`, `never`, `supersed (superseded, supersedes)`, `historical`, `legacy`
*imu_link sits on the centreline away from motor magnetic fields*: `away`, `not`, `never`, `supersed (superseded, supersedes)`, `historical`, `legacy` |
+| FRAMES-REP105 | recorded | *base_link is at the drive-axle midpoint and axle height; base_footprint is on the floor*: `base_footprint`, `not`, `never`, `supersed (superseded, supersedes)`, `historical`, `legacy`
*base_footprint is on the floor under the drive-axle midpoint*: `not`, `never`, `supersed (superseded, supersedes)`, `historical`, `legacy`
*imu_link sits on the centreline away from motor magnetic fields*: `away`, `not`, `never`, `supersed (superseded, supersedes)`, `historical`, `legacy`
*the imu_link board is the TDK EV_ICM-42688-P; MIKROE-4237 is superseded*: `supersed (superseded, supersedes)`, `replac`, `historical`, `legacy`, `earlier`, `previous`, `no longer`, `was`, `instead of` |
| NEAR-FIELD-FRAMES | recorded | *the ToF cliff pitch is 10 degrees down, not 20*: `supersed (superseded, supersedes)`, `replac`, `previous`, `earlier`, `historical`, `instead of`, `was 20`, `from 20`
*the 10 degree ToF pitch is confirmed only by the bench test*: `bench`, `until` |
| BATTERY-PLACEMENT | recorded | `supersed (superseded, supersedes)`, `historical`, `previous`, `earlier`, `rev 18.1 to rev 18.6` |
| SPEED-CEILING | recorded | *1.5 m/s is a command ceiling, not an operating limit*: `ceiling`, `analytical`
*1.5 m/s is not an accepted operating speed*: `ceiling`, `analytical`, `not` |
-| DRIVETRAIN | recorded | *the selected motor variant is ZLLG80ASM250-L-B*: no label; correct the line or add a decision-allow marker
*ZLTECH is the selected 2.0 drivetrain, not an option*: no label; correct the line or add a decision-allow marker
*brake fail-safe function and ratings are open supplier-evidence gates*: `pending`, `F2A`, `verif`, `not yet`, `unverified` |
+| DRIVETRAIN | recorded | *the selected motor variant is ZLLG80ASM250-L-B*: no label; correct the line or add a decision-allow marker
*ZLTECH is the selected 2.0 drivetrain, not an option*: no label; correct the line or add a decision-allow marker
*brake ratings are recorded (ZLTECH 18 Oct 2026); the fail-safe behaviour still needs verification evidence*: `pending`, `F2A`, `verif`, `not yet`, `unverified` |
| RS485-NOT-IN-2-0 | recorded | *RS485 is not provisioned in 2.0*: `not used`, `no RS485`, `not provisioned`, `without`, `legacy`, `Gate A`, `removed`, `not in 2.0`
*CAN1 and CAN2 are dedicated; no upper-body serial link to the base*: no label; correct the line or add a decision-allow marker |
-| BASE-CONTROLLER-GATES | recorded | *Teensy is the Gate A controller and the release fallback, not a bench target*: no label; correct the line or add a decision-allow marker
*the Gate A IMU is the MPU6500*: `would return`, `labeled`, `labelled`, `assumed`, `not a real`, `instead of`, `USE_MPU6050`, `driver` |
+| BASE-CONTROLLER-GATES | recorded | *Teensy is the Gate A controller and the release fallback, not a bench target*: no label; correct the line or add a decision-allow marker
*the Gate A IMU is the MPU6500*: `would return`, `labeled`, `labelled`, `assumed`, `not a real`, `instead of`, `USE_MPU6050`, `driver`
*the 2.0 IMU board is the TDK EV_ICM-42688-P on SPI4; MIKROE-4237 is superseded*: `supersed (superseded, supersedes)`, `replac`, `historical`, `legacy`, `earlier`, `previous`, `no longer`, `was`, `instead of` |
| BASE-CONTROLLER-IO | recorded | *the base controller is the STM32H723ZG on the NUCLEO-H723ZG; label STM32H743 / NUCLEO-H743ZI2 material as superseded*: `supersed (superseded, supersedes)`, `legacy`, `historical`, `replaced`
*the ultrasonic sensors are four MaxBotix MB7060 on dedicated STM32 UARTs at 9600 8N1; no sensor I2C*: `supersed (superseded, supersedes)`, `legacy`, `historical`, `replaced`
*micro-ROS runs over UDP on Ethernet between MCU and Jetson; USB is for the bench only*: `bench`, `legacy`, `Gate A`, `Teensy`, `supersed (superseded, supersedes)`, `historical`
*the base has four MaxBotix MB7060 (edge holes) and two TF-Luna ToF cliff sensors (centre holes)*: `supersed (superseded, supersedes)`, `legacy`, `historical` |
| IMU-TOPIC-OWNERSHIP | recorded | *firmware publishes /imu/data_raw; /imu/data belongs to the host filter*: `host`, `filter (not unfiltered)`, `EKF`, `madgwick`, `must not`, `never`, `outdated`, `older revision(s)`
*firmware must not publish filtered /imu/data*: no label; correct the line or add a decision-allow marker |
| CAMERAS | recorded | *the base camera is the Orbbec Gemini 336L*: `legacy`, `historical`, `previous`, `not used`, `replaced`
*the base camera tilts about 10 degrees up*: no label; correct the line or add a decision-allow marker
*the 2.0 base camera is the Orbbec Gemini 336L*: `legacy`, `Gate A`, `historical`
*the Gemini 336L up-tilt positions are 5, 10 and 15 degrees, baseline 10*: `supersed (superseded, supersedes)`, `historical`, `legacy`
*the base camera is not at 243 mm; base_camera_link is (0.387, 0, 0.046) from base_link*: `supersed (superseded, supersedes)`, `historical` |
@@ -163,8 +163,10 @@ identical to the register.
| UI-MANUAL-VELOCITY-INPUT | open | not scanned until the decision is taken |
| STATUS-VOCABULARY | recorded | `not`, `never`, `only`, `hardwired`, `STOPPED \(SOFTWARE\)` |
| STOP-SCOPE | recorded | *E-stop means only the hardwired buttons; the UI control is Stop (software), a functional stop*: `not`, `never`, `no`, `supersed (superseded, supersedes)`, `historical`
*Stop (software) covers base motion only today and is labelled base-only*: `target`, `planned`, `future`, `will`, `not`, `base[- ]only`
*Stop (software) is a functional stop, not a safety function*: `not`, `never`, `functional` |
-| OPERATOR-PANEL | recorded | *the operator panel is the Getac ZX80, 8 inch, 1920 x 1200, landscape*: `supersed (superseded, supersedes)`, `replac`, `historical`, `earlier`, `previous`, `was`
*the ZX80 operator panel is used in landscape*: `not`, `never` |
+| OPERATOR-PANEL | recorded | *the operator panel is the Getac ZX80, 8 inch, 1920 x 1200*: `supersed (superseded, supersedes)`, `replac`, `historical`, `earlier`, `previous`, `was`
*the ZX80 is landscape by default; portrait is the second locked position*: `supersed (superseded, supersedes)`, `historical`, `not`, `was`, `landscape \(?(?:by )?default` |
| UI-GATE-A | recorded | `partial`, `A1`, `supersed (superseded, supersedes)`, `historical`, `was`, `6 Nov` |
+| COMPONENT-AVAILABILITY | recorded | `option`, `alternative`, `second[- ]source`, `not`, `never`, `supersed (superseded, supersedes)`, `historical` |
+| TABLET-MOUNT | recorded | *the tablet mount default is landscape; portrait is the second locked position*: `supersed (superseded, supersedes)`, `historical`, `not`, `was`, `landscape \(?(?:by )?default`
*the tablet position is a mount setting, not a configuration ID*: `not`, `never`, `supersed (superseded, supersedes)`, `historical` |
## Approved public contacts
diff --git a/decisions.yaml b/decisions.yaml
index 61d5c1c..40d53a2 100644
--- a/decisions.yaml
+++ b/decisions.yaml
@@ -53,12 +53,18 @@
schema_version: 1
in_force:
- source: P-03-rev20.1
- date: 2026-10-30
+ source: P-03-rev20.2
+ date: 2026-11-01
sources:
+ P-03-rev20.2:
+ title: "OpenAMRobot 2.0 Decision Addendum rev20.2, 1 November 2026 (the addendum in force; item 26: BOM Issue 7.14, component availability, tablet mount, SAFE 24 V converter, EV_ICM-42688-P IMU board, hub-motor brake data, DOLD holding confirmation)"
+ evidence: plan-set document, not held in any repository; provenance only, CI never reads it; values entered from the owner's summary of item 26 (change request openAMRobot/.github#77)
+ BOM-Issue-7.14:
+ title: OpenAMRobot 2.0 Detailed Hardware BOM, Issue 7.14, 27 October 2026, with the 1 November 2026 changes (canonical per P-03 rev20.2)
+ evidence: not held in any repository; provenance only, CI never reads it
P-03-rev20.1:
- title: "OpenAMRobot 2.0 Decision Addendum rev20.1, with the platform lead's decisions of 30 October 2026 (the addendum in force; carries the rev20.0 items 15, 17 and 18 head-camera values; item 24: whole chest layout, including the ZX80 operator panel correction of 28 October 2026)"
+ title: "OpenAMRobot 2.0 Decision Addendum rev20.1, with the platform lead's decisions of 30 October 2026 (earlier revision; superseded by rev20.2 as the addendum in force; carries the rev20.0 items 15, 17 and 18 head-camera values; item 24: whole chest layout, including the ZX80 operator panel correction of 28 October 2026)"
evidence: plan-set document, not held in any repository; provenance only, CI never reads it
Alex decision 2026-10-30:
title: Platform decision of 30 October 2026 (UI manual velocity input and stop lock, status vocabulary, stop scope, UI Gate A date); change request openAMRobot/.github#72
@@ -92,7 +98,7 @@ sources:
title: P-03 Decision Addendum, revision 18.4 (earlier revision)
evidence: plan-set document, not held in any repository; cited by item
P-03-rev19.6:
- title: "OpenAMRobot 2.0 Decision Addendum rev19.6, 18 October 2026 (earlier revision; superseded by rev20.1 as the addendum in force; item 22: Gigavac GX11CAC contactors and the item 22 controller I/O)"
+ title: "OpenAMRobot 2.0 Decision Addendum rev19.6, 18 October 2026 (earlier revision; item 22: Gigavac GX11CAC contactors and the item 22 controller I/O)"
evidence: plan-set document, not held in any repository; cited by item
E-01-rev1.3:
title: E-01 Safety and Power Distribution Baseline rev1.3, 18 October 2026 (detail for P-03 rev19.6 item 22)
@@ -134,7 +140,7 @@ sources:
title: P-00 Master Coordination Plan, revision 18.1
evidence: plan-set document, not held in any repository; cited by text line
BOM-Issue-7.10:
- title: OpenAMRobot 2.0 Detailed Hardware BOM, Issue 7.10, 18 October 2026 (canonical per P-03 rev19.6)
+ title: OpenAMRobot 2.0 Detailed Hardware BOM, Issue 7.10, 18 October 2026 (earlier issue; canonical per P-03 rev19.6)
evidence: not held in any repository; provenance only, CI never reads it
BOM-Issue-7.8:
title: OpenAMRobot 2.0 Detailed Hardware BOM, Issue 7.8, 15 October 2026 (earlier issue; canonical per P-03 rev19.2)
@@ -170,16 +176,18 @@ exclude:
decisions:
- id: BOM-ISSUE-IN-FORCE
title: Canonical hardware BOM issue for OpenAMRobot 2.0
- summary: "BOM Issue 7.10 is the canonical OpenAMRobot 2.0 hardware BOM."
- fix_hint: "Name Issue 7.10 as canonical, or label older issues (Issue 6, Issue 7 to 7.9, B-01) as superseded or earlier."
+ summary: "BOM Issue 7.14 is the canonical OpenAMRobot 2.0 hardware BOM."
+ fix_hint: "Name Issue 7.14 as canonical, or label older issues (Issue 6, Issue 7 to 7.13, B-01) as superseded or earlier."
kind: value
status: recorded
- value: OpenAMRobot 2.0 Detailed Hardware BOM, Issue 7.10
+ value: OpenAMRobot 2.0 Detailed Hardware BOM, Issue 7.14 (27 October 2026, with the 1 November 2026 changes)
unit: none
- date: 2026-10-18
- review_by: 2026-11-29
- source: {document: P-03-rev19.6, item: "BOM issue in force (BOM Issue 7.10)"}
+ date: 2026-11-01
+ review_by: 2026-12-13
+ source: {document: P-03-rev20.2, item: "item 26 (BOM Issue 7.14)"}
supersedes:
+ - {value: Issue 7.11, source: "earlier issue; replaced by Issue 7.14 per P-03-rev20.2 item 26"}
+ - {value: Issue 7.10, source: "P-03-rev19.6 BOM issue in force (BOM Issue 7.10)"}
- {value: Issue 7.8, source: "P-03-rev19.2 BOM issue in force (BOM Issue 7.8)"}
- {value: Issue 7.7, source: "P-03-rev19.0 BOM issue in force (BOM Issue 7.7)"}
- {value: Issue 7.6, source: "P-03-rev18.9 item 16 (BOM Issue 7.6 carries the change)"}
@@ -193,15 +201,15 @@ decisions:
check:
- pattern: '(?P(?:canonical|in force|of record)[^\n]{0,40}\bIssue\s*6\b|\bIssue\s*6\b[^\n]{0,40}\b(?:canonical|in force|of record))'
unless: 'supersed|replaced|previous|earlier'
- message: BOM Issue 7.10 is the canonical hardware BOM (P-03 rev19.6)
+ message: BOM Issue 7.14 is the canonical hardware BOM (P-03 rev20.2 item 26)
- pattern: '(?PB-01[^\n]{0,60}\b(?:canonical|current))'
unless: 'supersed'
message: B-01 is superseded
- - pattern: '(?P(?:canonical|in force|of record)[^\n]{0,40}\bIssue\s*7(?!\.10\b)(?:\.\d+)?\b|\bIssue\s*7(?!\.10\b)(?:\.\d+)?\b[^\n]{0,40}\b(?:canonical|in force|of record))'
+ - pattern: '(?P(?:canonical|in force|of record)[^\n]{0,40}\bIssue\s*7(?!\.14\b)(?:\.\d+)?\b|\bIssue\s*7(?!\.14\b)(?:\.\d+)?\b[^\n]{0,40}\b(?:canonical|in force|of record))'
unless: 'supersed|replaced|previous|earlier|historical'
- message: BOM Issue 7.10 is the canonical hardware BOM (P-03 rev19.6)
+ message: BOM Issue 7.14 is the canonical hardware BOM; Issue 7 to 7.13 (including 7.10 and 7.11) are retired (P-03 rev20.2 item 26)
verification:
- machine: Issue 6, Issue 7 to 7.9 or B-01 presented as the canonical BOM; citations of P-03 rev18.1 line 7
+ machine: Issue 6, Issue 7 to 7.13 (including 7.10 and 7.11) or B-01 presented as the canonical BOM; citations of P-03 rev18.1 line 7
human: {reviewer: platform-lead, evidence: the BOM file header and issue number in the released BOM}
owner: platform-lead
@@ -358,13 +366,14 @@ decisions:
- base_link at the drive-axle midpoint and axle height; x forward, z up
- base_link 0.098 m above the floor (axle height; moved here from CAMERAS on 20 Oct 2026, value unchanged, P-03 rev19.4 item 19 D3)
- imu_link on the centreline, away from motor magnetic fields
- - "imu_link (0.0544, 0, 0.1210) m from base_link, rpy (0, 0, -pi/2), yaw -90 deg: ICM-42688-P on MIKROE-4237 on the electronics bracket MMP.04.00.00.002, centreline, 54.4 mm ahead of the drive axle, chip centre 165.043 mm above the base-plate top, 219.04 mm above the floor"
+ - "imu_link (0.0544, 0, 0.1210) m from base_link, rpy (0, 0, -pi/2), yaw -90 deg (unchanged): ICM-42688-P on the TDK EV_ICM-42688-P board (replaces MIKROE-4237, P-03 rev20.2 item 26), chip mounted +Y forward, on the electronics bracket MMP.04.00.00.002, centreline, 54.4 mm ahead of the drive axle, chip centre 165.043 mm above the base-plate top, 219.04 mm above the floor"
- "the IMU publishes raw sensor-frame values; the axis mapping is in the URDF (sensor X to base -y, Y to +x, Z to +z); no firmware remap"
unit: none
- date: 2026-10-16
- review_by: 2026-11-27
- source: {document: P-03-rev19.4, item: "item 19 D5 update (Alex decision 16 Oct 2026, platform-sw #70)"}
+ date: 2026-11-01
+ review_by: 2026-12-13
+ source: {document: P-03-rev20.2, item: "item 26 (IMU board TDK EV_ICM-42688-P, chip +Y forward; pose unchanged); imu_link pose per P-03-rev19.4 item 19 D5 update (Alex decision 16 Oct 2026, platform-sw #70)"}
supersedes:
+ - {value: "imu_link (0.0544, 0, 0.1210), yaw -90 deg: ICM-42688-P on MIKROE-4237", source: "P-03-rev19.4 item 19 D5 update; board replaced by P-03-rev20.2 item 26"}
- {value: "(0.0544, 0, 0.133), rpy 0", source: "measured against a displaced base plate in the CAD; superseded by Alex decision 16 Oct 2026 (platform-sw #70)"}
- {value: "(0.195, 0, -0.044) plus board stack", source: "Alex decision 12 Oct 2026, superseded by Alex decision 16 Oct 2026 (CAD Rev B, platform-hw #20)"}
applies_to: {repositories: ["*"], files: ["**/*.md", "**/*.yaml", "**/*.yml", "**/*.html", "**/*.xacro", "**/*.urdf", "**/*.launch.py"]}
@@ -378,8 +387,13 @@ decisions:
- pattern: '(?P\bimu_link\b[^\n]{0,40}\b(?:next to|beside|on|near|above) the (?:drive |hub |wheel )?motors?\b)'
unless: '\baway\b|\bnot\b|never|supersed|historical|legacy'
message: imu_link sits on the centreline away from motor magnetic fields (P-03 rev18.7 item 15)
+ # Robot-description files only; BASE-CONTROLLER-GATES carries the same check for text files.
+ - pattern: '(?P\bMIKROE-?4237\b)'
+ files: ["**/*.xacro", "**/*.urdf", "**/*.launch.py"]
+ unless: 'supersed|replac|historical|legacy|earlier|previous|no longer|\bwas\b|instead of'
+ message: the imu_link board is the TDK EV_ICM-42688-P; MIKROE-4237 is superseded (P-03 rev20.2 item 26)
verification:
- machine: base_link placed on the floor, base_footprint at axle height, or imu_link next to a motor
+ machine: base_link placed on the floor, base_footprint at axle height, imu_link next to a motor, or MIKROE-4237 presented as current in a robot-description file
human: {reviewer: robot-description-owner, evidence: URDF frame tree and real TF on the robot matching REP 105}
owner: platform-lead
@@ -468,14 +482,18 @@ decisions:
- id: DRIVETRAIN
title: Drivetrain
summary: "Two ZLTECH ZLLG80ASM250-L-B hub motors with brakes, one ZLAC8015D driver on CAN1."
- fix_hint: "Name the ZLLG80ASM250-L-B as selected; mark brake ratings as pending supplier evidence."
+ fix_hint: "Name the ZLLG80ASM250-L-B; brakes 24 V, 12.5 W, 6 Nm holding (ZLTECH 18 Oct 2026); fail-safe needs evidence."
kind: value
status: recorded
- value: Two ZLTECH ZLLG80ASM250-L-B hub motors with brakes, one ZLAC8015D V4.2 driver on CAN1 (CANopen), 200 mm wheels
+ values:
+ - Two ZLTECH ZLLG80ASM250-L-B hub motors with brakes, one ZLAC8015D V4.2 driver on CAN1 (CANopen), 200 mm wheels
+ - "hub-motor brakes (ZLTECH 18 Oct 2026): 24 V, 12.5 W per motor, 6 Nm holding, release at most 100 ms, engagement at most 30 ms without diode"
unit: none
- date: 2026-09-21
- review_by: 2026-11-02
- source: {document: P-00-rev18.1, item: txt 16}
+ date: 2026-11-01
+ review_by: 2026-12-13
+ source: {document: P-03-rev20.2, item: "item 26 (hub-motor brake data, ZLTECH 18 Oct 2026); motors and driver per P-00-rev18.1 txt 16"}
+ supersedes:
+ - {value: "brake ratings pending supplier evidence", source: "P-00-rev18.1 txt 16 (register fix_hint until P-03-rev20.2 item 26)"}
applies_to:
repositories: ["openamr-platform-*", "openamr-upperbody-*", "openamrobot-docs", "openamrobot-release", ".github"]
files: ["**/*.md", "**/*.yaml", "**/*.yml", "**/*.html", "**/*.xacro", "**/*.urdf"]
@@ -486,10 +504,10 @@ decisions:
message: ZLTECH is the selected 2.0 drivetrain, not an option
- pattern: '(?Pfail-safe brakes?)'
unless: 'pending|F2A|verif|not yet|unverified'
- message: brake fail-safe function and ratings are open supplier-evidence gates
+ message: brake ratings are recorded (ZLTECH 18 Oct 2026); the fail-safe behaviour still needs verification evidence
verification:
machine: the unbraked motor variant, ZLTECH described as optional, brakes described as fail-safe
- human: {reviewer: platform-lead, evidence: supplier datasheets for the brake rating and fail-safe behaviour}
+ human: {reviewer: platform-lead, evidence: ZLTECH brake data of 18 Oct 2026 and supplier evidence of the fail-safe behaviour}
owner: platform-lead
- id: RS485-NOT-IN-2-0
@@ -519,20 +537,22 @@ decisions:
- id: BASE-CONTROLLER-GATES
title: Base-controller gates and the 2.0 IMU
- summary: "Gate A (Teensy, MPU6500) and Gate B (STM32H723ZG) stay distinct; the ICM-42688-P is the 2.0 IMU."
- fix_hint: "Call the Teensy and MPU6500 Gate A (legacy); the 2.0 IMU is the ICM-42688-P (MIKROE-4237, SPI)."
+ summary: "Gate A (Teensy, MPU6500) and Gate B (STM32H723ZG) stay distinct; the 2.0 IMU is the ICM-42688-P on EV_ICM-42688-P."
+ fix_hint: "Call Teensy and MPU6500 Gate A (legacy); 2.0 IMU board is TDK EV_ICM-42688-P on SPI4; MIKROE-4237 is superseded."
kind: distinction
status: recorded
values:
- Gate A, Jetson with the existing Teensy, ZBLD/PWM drivetrain and MPU6500 (legacy test configuration)
- Gate B, Jetson with the STM32H723ZG (NUCLEO-H723ZG bench board) and the ZLTECH drivers
- - the ICM-42688-P (MIKROE-4237, SPI) is the OpenAMRobot 2.0 IMU, WHO_AM_I 0x47 at boot; the MPU6500 stays the Gate A (legacy) IMU
+ - the ICM-42688-P on the TDK EV_ICM-42688-P board (SPI4, Mode 0) is the OpenAMRobot 2.0 IMU, WHO_AM_I 0x47 at boot; it replaces the MIKROE-4237 board (same sensor); the MPU6500 stays the Gate A (legacy) IMU
+ - no click shield on the NUCLEO-H723ZG; the IMU board and the CAN isolator are wired directly to SPI4 and FDCAN2
- STM32 go/no-go 6 November 2026; fallback is the validated legacy Teensy/PWM build behind I8
unit: none
- date: 2026-10-12
- review_by: 2026-11-23
- source: {document: P-03-rev19.0, item: "item 19 (base-controller IMU)"}
+ date: 2026-11-01
+ review_by: 2026-12-13
+ source: {document: P-03-rev20.2, item: "item 26 (IMU board TDK EV_ICM-42688-P, no click shield); Gate A/B distinction and IMU per P-03-rev19.0 item 19 (base-controller IMU)"}
supersedes:
+ - {value: "the ICM-42688-P (MIKROE-4237, SPI) is the OpenAMRobot 2.0 IMU", source: "P-03-rev19.0 item 19 (base-controller IMU); board replaced by P-03-rev20.2 item 26"}
- {value: "ICM-42688-P enters the manufacturing BOM only after side-by-side robot evidence; adoption decision 20 November 2026", source: "P-00-rev18.1 txt 63, 101, 106, 344; I8-WP adoption gate"}
applies_to: {repositories: ["*"], files: ["**/*.md", "**/*.yaml", "**/*.yml", "**/*.html"]}
check:
@@ -541,9 +561,12 @@ decisions:
- pattern: '\b(?PMPU6050)\b'
unless: 'would return|labeled|labelled|assumed|not a real|instead of|USE_MPU6050|driver'
message: the Gate A IMU is the MPU6500
+ - pattern: '(?P\bMIKROE-?4237\b)'
+ unless: 'supersed|replac|historical|legacy|earlier|previous|no longer|\bwas\b|instead of'
+ message: the 2.0 IMU board is the TDK EV_ICM-42688-P on SPI4; MIKROE-4237 is superseded (P-03 rev20.2 item 26)
verification:
- machine: Teensy called a bench target, MPU6050 named as the IMU
- human: {reviewer: platform-lead, evidence: "Gate A test log, STM32 go/no-go record, ICM-42688-P WHO_AM_I 0x47 boot log"}
+ machine: Teensy called a bench target, MPU6050 named as the IMU, MIKROE-4237 presented as the current IMU board
+ human: {reviewer: platform-lead, evidence: "Gate A test log, STM32 go/no-go record, ICM-42688-P WHO_AM_I 0x47 boot log on the EV_ICM-42688-P over SPI4"}
owner: platform-lead
- id: BASE-CONTROLLER-IO
@@ -701,11 +724,12 @@ decisions:
- main battery bus 25.6 V nominal (not a regulated 24 V rail)
- regulated 24 V branch (safety relay, peripherals); the brake-release supply comes from the actuator side, see ESTOP-SCOPE
- regulated 5 V (includes the navigation LiDAR)
+ - "SAFE 24 V converter for lift and brake release: MEAN WELL DDR-240B-24 (16.8 to 33.6 V in, 24 V 10 A, 240 W) behind F-LIFT 15 A, 2.5 mm2 (P-03 rev20.2 item 26)"
- no 12 V rail
unit: V
date: null
- review_by: 2026-11-18 # six weeks after this register update (2026-10-07)
- source: {document: P-03-rev18.1, item: "item 2 line 10; P-03-rev18.4 item 13; brake-release supply per P-03-rev19.2 item 20"}
+ review_by: 2026-12-13 # six weeks after this register update (2026-11-01)
+ source: {document: P-03-rev18.1, item: "item 2 line 10; P-03-rev18.4 item 13; brake-release supply per P-03-rev19.2 item 20; SAFE 24 V converter per P-03-rev20.2 item 26"}
supersedes:
- {value: "regulated 24 V branch (safety relay, brake coils, peripherals)", source: "P-03-rev18.4 item 13"}
applies_to:
@@ -720,7 +744,7 @@ decisions:
message: there is no 12 V rail in 2.0
verification:
machine: a 12 V rail or a 24 V to 5/12 V converter in text
- human: {reviewer: platform-lead, evidence: power distribution schematic}
+ human: {reviewer: platform-lead, evidence: "power distribution schematic, including the DDR-240B-24 SAFE 24 V branch behind F-LIFT"}
owner: platform-lead
- id: DOCK-NO-CONTACTS
@@ -1199,12 +1223,14 @@ decisions:
- lift base plate fore-aft positions centre and +50 mm (bp000, bp050)
- lift motion only in the stowed or carry safe pose with the base stopped
- hold-and-move limits 0.05 m/s (drawer reversal) and 0.3 m/s (carrying)
- - "release gates: written DOLD holding on E-stop and power loss, supplier CAD, weighed mass, the CAN3 lift interface and the F2S rerun"
+ - "DOLD written confirmation received 14 Oct 2026: 800 N held with power off"
+ - "release gates: the holding test on the delivered unit after an E-stop during motion and after power loss, supplier CAD, weighed mass, the CAN3 lift interface and the F2S rerun"
unit: none
- date: 2026-10-09
- review_by: 2026-11-20
- source: {document: P-03-rev18.9, item: "items 16 and 18; motion rules item 15; release gates items 6, 8, 14, 15, 16"}
+ date: 2026-11-01
+ review_by: 2026-12-13
+ source: {document: P-03-rev20.2, item: "item 26 (DOLD confirmation of 14 Oct 2026; holding-test release gate); lift, motion rules and other release gates per P-03-rev18.9 items 6, 8, 14, 15, 16 and 18"}
supersedes:
+ - {value: "release gate: written DOLD holding on E-stop and power loss", source: "P-03-rev18.9 items 6, 8, 14, 15, 16"}
- {value: "Lift removed from OpenAMRobot 2.0; fixed mast; lift on the 3.0 roadmap (former entry LIFT-REMOVED)", source: P-00-rev18.1 txt 18}
- {value: "DOLD Hexalift V1 350 mm primary", source: P-03-rev18.7 item 15}
- {value: "base plate positions centre, +50, +100, +150 and +200 mm", source: P-03-rev18.7 item 15}
@@ -1227,7 +1253,7 @@ decisions:
message: the lift base plate has two fore-aft positions, centre and +50 mm (P-03 rev18.9 item 18)
verification:
machine: text saying 2.0 has no lift, presenting a fixed mast as current, deferring the lift to 3.0, naming Hexalift V1 as current, or base-plate positions beyond +50 mm
- human: {reviewer: platform-lead, evidence: "P-03 rev18.9 items 16 and 18, the option B drawing set and the release-gate records: DOLD holding on E-stop and power loss, supplier CAD, weighed mass, CAN3 lift interface, F2S rerun"}
+ human: {reviewer: platform-lead, evidence: "P-03 rev18.9 items 16 and 18, the option B drawing set and the release-gate records: DOLD confirmation of 14 Oct 2026, the holding-test record on the delivered unit after an E-stop during motion and after power loss, supplier CAD, weighed mass, CAN3 lift interface, F2S rerun"}
owner: platform-lead
- id: ARM-MOUNT
@@ -1280,6 +1306,7 @@ decisions:
date: 2026-10-28
review_by: 2026-12-09
source: {document: P-03-rev20.1, item: "item 17 (head_pitch baseline hp30, value of the P-03-rev20.0 28 Oct 2026 corrections, unchanged); column, base_plate and base_tilt per P-03-rev18.9 items 17 and 18"}
+ note: The tablet position (landscape or portrait, TABLET-MOUNT) is a mount setting, not a configuration ID (P-03 rev20.2 item 26).
supersedes:
- {value: "head_pitch hp15, hp20, hp25, hp30, hp35 (baseline hp25)", source: P-03-rev18.9 item 17}
- {value: "mast_id (mast_1300 to mast_1450)", source: P-03-rev18.2 shoulder height}
@@ -1431,16 +1458,17 @@ decisions:
- id: OPERATOR-PANEL
title: Operator panel
- summary: "Operator panel: Getac ZX80, 8 inch, 1920 x 1200, landscape."
- fix_hint: "Name the Getac ZX80 (8 inch, 1920 x 1200, landscape); label ZX10 material as superseded."
+ summary: "Operator panel: Getac ZX80, 8 inch, 1920 x 1200; rotatable mount, landscape by default, portrait second (TABLET-MOUNT)."
+ fix_hint: "Name the Getac ZX80 (8 inch, 1920 x 1200), landscape by default, portrait second; label ZX10 as superseded."
kind: value
status: recorded
- value: Getac ZX80, 8 inch, 1920 x 1200, landscape
+ value: Getac ZX80, 8 inch, 1920 x 1200, rotatable mount, landscape by default with a portrait second position (see TABLET-MOUNT, P-03 rev20.2 item 26)
unit: none
- date: 2026-10-28
- review_by: 2026-12-09
- source: {document: P-03-rev20.1, item: "item 24 (whole chest layout, including the ZX80 correction of 28 Oct 2026)"}
+ date: 2026-11-01
+ review_by: 2026-12-13
+ source: {document: P-03-rev20.1, item: "item 24 (whole chest layout, including the ZX80 correction of 28 Oct 2026); mount positions per P-03-rev20.2 item 26 (TABLET-MOUNT)"}
supersedes:
+ - {value: "Getac ZX80, 8 inch, 1920 x 1200, landscape (no portrait position)", source: "P-03-rev20.1 item 24; portrait second position added by P-03-rev20.2 item 26"}
- {value: Getac ZX10, source: "earlier panel selection, not held in this register; replaced by P-03-rev20.1 item 24"}
applies_to:
repositories: ["openamrobot-ui", "openamrobot-docs"]
@@ -1448,13 +1476,16 @@ decisions:
check:
- pattern: '(?P\bZX10\b)'
unless: 'supersed|replac|historical|earlier|previous|\bwas\b'
- message: the operator panel is the Getac ZX80, 8 inch, 1920 x 1200, landscape (P-03 rev20.1 item 24)
- - pattern: '(?P\bZX80\b[^\n]{0,60}\bportrait\b|\bportrait\b[^\n]{0,60}\bZX80\b)'
- unless: '\bnot\b|never'
- message: the ZX80 operator panel is used in landscape (P-03 rev20.1 item 24)
+ message: the operator panel is the Getac ZX80, 8 inch, 1920 x 1200 (P-03 rev20.1 item 24)
+ # Portrait is the approved second locked position (P-03 rev20.2 item 26); only portrait as the
+ # default is a finding. UI code files only: TABLET-MOUNT carries the same check for text files.
+ - pattern: '(?P\b(?:ZX80|tablet)\b[^\n]{0,60}\bportrait\b[^\n]{0,20}\b(?:by|as (?:the)?|is (?:the)?) ?default\b|\b(?:ZX80|tablet)\b[^\n]{0,60}\bdefault\b[^\n]{0,20}\bportrait\b|\bportrait\b[^\n]{0,20}\b(?:is |as )?(?:the )?default\b[^\n]{0,60}\b(?:ZX80|tablet)\b)'
+ files: ["**/*.json", "**/*.ts", "**/*.tsx", "**/*.css"]
+ unless: 'supersed|historical|\bnot\b|\bwas\b|landscape \(?(?:by )?default'
+ message: the ZX80 is landscape by default; portrait is the second locked position (P-03 rev20.2 item 26, TABLET-MOUNT)
verification:
- machine: ZX10 without a superseded label; the ZX80 in portrait
- human: {reviewer: software-lead, evidence: UI layout screenshots on the ZX80 at 1920 x 1200 landscape}
+ machine: ZX10 without a superseded label; portrait presented as the ZX80 default in UI code
+ human: {reviewer: software-lead, evidence: "UI layout screenshots on the ZX80 at 1920 x 1200 in landscape (default) and portrait"}
owner: platform-lead
- id: UI-GATE-A
@@ -1482,3 +1513,60 @@ decisions:
machine: the full UI Gate A dated 30 October without a partial or A1 label
human: {reviewer: software-lead, evidence: UI Gate A test record dated 6 November 2026 and the A1 partial result of 30 October 2026}
owner: platform-lead
+
+ - id: COMPONENT-AVAILABILITY
+ title: Component availability rule for BOM and procurement
+ summary: "Every part in stock at 2+ independent suppliers; single-source only as an option; no-stock parts not selected."
+ fix_hint: "Give a second-sourced alternative next to a single-source part, or list it as a named option, not the selection."
+ kind: limit
+ status: recorded
+ values:
+ - every part is available from stock at two or more independent suppliers
+ - a single-source part appears only as a named option next to a second-sourced alternative
+ - parts with no stock are not selected
+ unit: none
+ date: 2026-11-01
+ review_by: 2026-12-13
+ source: {document: P-03-rev20.2, item: "item 26 (component availability rule)"}
+ applies_to:
+ repositories: ["*"]
+ files: ["**/*BOM*.md", "**/*bom*.md", "**/*BOM*.yaml", "**/*bom*.yaml", "**/*BOM*.yml", "**/*bom*.yml",
+ "**/procurement/**/*.md", "**/procurement/**/*.yaml", "**/procurement/**/*.yml",
+ "**/*procurement*.md", "**/*procurement*.yaml", "**/*procurement*.yml"]
+ check:
+ # Wording only: the scan cannot see supplier stock or count suppliers.
+ - pattern: '(?P\bsingle[- ]source[ds]?\b[^\n]{0,40}\b(?:selected|chosen|baseline|primary)\b|\b(?:selected|chosen|baseline|primary)\b[^\n]{0,40}\bsingle[- ]source[ds]?\b)'
+ unless: 'option|alternative|second[- ]source|\bnot\b|never|supersed|historical'
+ message: a single-source part appears only as a named option next to a second-sourced alternative (P-03 rev20.2 item 26)
+ verification:
+ machine: "wording only: a single-source part presented as selected, chosen, baseline or primary without an option or alternative label; supplier stock and supplier count are not checked"
+ human: {reviewer: platform-lead, evidence: "BOM Issue 7.14 supplier columns showing stock at two or more independent suppliers per line"}
+ owner: platform-lead
+
+ - id: TABLET-MOUNT
+ title: Operator tablet on a rotatable mount with two locked positions
+ summary: "Getac ZX80 on a rotatable mount, two locked positions: landscape (default, 234 x 149.8 mm) and portrait."
+ fix_hint: "Describe landscape as the default position and portrait as the second locked position; it is not a configuration ID."
+ kind: configuration
+ status: recorded
+ values:
+ - Getac ZX80 tablet on a rotatable mount with two locked positions
+ - landscape, default, 234 x 149.8 mm
+ - portrait, 149.8 x 234 mm
+ - the position is a mount setting, not a configuration ID (see CONFIG-IDS)
+ unit: mm
+ date: 2026-11-01
+ review_by: 2026-12-13
+ source: {document: P-03-rev20.2, item: "item 26 (tablet mount)"}
+ applies_to: {repositories: ["*"], files: ["**/*.md", "**/*.yaml", "**/*.yml", "**/*.html"]}
+ check:
+ - pattern: '(?P\b(?:ZX80|tablet)\b[^\n]{0,60}\bportrait\b[^\n]{0,20}\b(?:by|as (?:the)?|is (?:the)?) ?default\b|\b(?:ZX80|tablet)\b[^\n]{0,60}\bdefault\b[^\n]{0,20}\bportrait\b|\bportrait\b[^\n]{0,20}\b(?:is |as )?(?:the )?default\b[^\n]{0,60}\b(?:ZX80|tablet)\b)'
+ unless: 'supersed|historical|\bnot\b|\bwas\b|landscape \(?(?:by )?default'
+ message: the tablet mount default is landscape; portrait is the second locked position (P-03 rev20.2 item 26)
+ - pattern: '(?P\btablet[ _](?:position|orientation)\b[^\n]{0,40}\bconfiguration[ _]ID\b)'
+ unless: '\bnot\b|never|supersed|historical'
+ message: the tablet position is a mount setting, not a configuration ID (P-03 rev20.2 item 26)
+ verification:
+ machine: portrait presented as the default tablet position; the tablet position presented as a configuration ID
+ human: {reviewer: platform-lead, evidence: "tablet mount drawing with the two locked positions and the ZX80 outline dimensions"}
+ owner: platform-lead
diff --git a/tests/test_check_decisions.py b/tests/test_check_decisions.py
index ab7fd58..d60d1e6 100644
--- a/tests/test_check_decisions.py
+++ b/tests/test_check_decisions.py
@@ -427,13 +427,20 @@ def test_bom_issue_in_force(self):
self.assertEqual(self.ids("docs/a.md", "BOM per P-03 rev18.1 line 7.\n", "x"), ["BOM-ISSUE-IN-FORCE"])
self.assertEqual(self.ids("docs/a.md", "Issue 7.6 is canonical; Issue 6 is superseded.\n", "x"), [])
- def test_bom_issue_7_10_in_force(self):
+ # P-03 rev20.2 item 26: BOM Issue 7.14 is canonical; Issue 7.10 and 7.11 are retired.
+ def test_bom_issue_7_14_in_force(self):
for text in ("The canonical BOM is Issue 7.\n", "BOM Issue 7.1 is the BOM of record.\n",
- "BOM Issue 7.8 is canonical.\n", "BOM Issue 7.9 is canonical.\n"):
+ "BOM Issue 7.8 is canonical.\n", "BOM Issue 7.9 is canonical.\n",
+ "BOM Issue 7.10 is canonical.\n", "BOM Issue 7.11 is the BOM in force.\n",
+ "The canonical BOM is Issue 7.11.\n"):
self.assertEqual(self.ids("docs/a.md", text, "x"), ["BOM-ISSUE-IN-FORCE"], text)
- for text in ("BOM Issue 7.10 is canonical.\n", "Issue 7.8 was canonical until Issue 7.10 superseded it.\n",
- "The earlier canonical BOM was Issue 7.8.\n"):
+ for text in ("BOM Issue 7.14 is canonical.\n", "Issue 7.10 was canonical until Issue 7.14 superseded it.\n",
+ "The earlier canonical BOM was Issue 7.11.\n", "The canonical BOM is Issue 7.14.\n"):
self.assertEqual(self.ids("docs/a.md", text, "x"), [], text)
+ by_id = {d["id"]: d for d in self.decisions}
+ retired = [s["value"] for s in by_id["BOM-ISSUE-IN-FORCE"]["supersedes"]]
+ self.assertIn("Issue 7.10", retired)
+ self.assertIn("Issue 7.11", retired)
# P-03 rev19.2 item 20 (E-01 rev1.1): each check flags the stale wording and accepts a
# sentence that labels it as superseded, removed or legacy.
@@ -644,10 +651,63 @@ def test_base_camera_height_243_superseded(self):
def test_icm42688p_is_the_2_0_imu(self):
for text in ("The ICM-42688-P is selected as the OpenAMRobot 2.0 IMU.\n",
- "ICM-42688-P (MIKROE-4237, SPI) is the baseline IMU; WHO_AM_I 0x47 at boot.\n"):
+ "ICM-42688-P (TDK EV_ICM-42688-P, SPI4) is the baseline IMU; WHO_AM_I 0x47 at boot.\n"):
self.assertEqual(self.ids("docs/a.md", text, "x"), [], text)
self.assertEqual(self.ids("docs/a.md", "The Gate A IMU is the MPU6050.\n", "x"), ["BASE-CONTROLLER-GATES"])
+ # P-03 rev20.2 item 26: the TDK EV_ICM-42688-P replaces the MIKROE-4237 board.
+ def test_mikroe_4237_is_superseded(self):
+ for text in ("ICM-42688-P (MIKROE-4237, SPI) is the baseline IMU.\n", "IMU board: MIKROE-4237 on SPI4.\n"):
+ self.assertEqual(self.ids("docs/a.md", text, "x"), ["BASE-CONTROLLER-GATES"], text)
+ for text in ("MIKROE-4237 is superseded by the TDK EV_ICM-42688-P.\n",
+ "The EV_ICM-42688-P replaces the MIKROE-4237 (same sensor).\n",
+ "Historical: the IMU was on a MIKROE-4237.\n"):
+ self.assertEqual(self.ids("docs/a.md", text, "x"), [], text)
+ # Robot-description files are covered by FRAMES-REP105, without a duplicate finding.
+ self.assertEqual(self.ids("urdf/base.urdf.xacro", "\n"),
+ ["FRAMES-REP105"])
+ self.assertEqual(self.ids("urdf/base.urdf.xacro", "\n"), [])
+ by_id = {d["id"]: d for d in self.decisions}
+ self.assertTrue(any("EV_ICM-42688-P" in v for v in by_id["FRAMES-REP105"]["values"]))
+ self.assertTrue(any("SPI4" in v and "EV_ICM-42688-P" in v for v in by_id["BASE-CONTROLLER-GATES"]["values"]))
+
+ # P-03 rev20.2 item 26: values recorded without a new pattern.
+ def test_rev20_2_values_in_force(self):
+ data = yaml.safe_load((ROOT / "decisions.yaml").read_text(encoding="utf-8"))
+ self.assertEqual(data["in_force"], {"source": "P-03-rev20.2", "date": cd.as_date("2026-11-01")})
+ by_id = {d["id"]: d for d in self.decisions}
+ self.assertTrue(any("DDR-240B-24" in v and "F-LIFT 15 A" in v for v in by_id["POWER-RAILS"]["values"]))
+ self.assertTrue(any("6 Nm holding" in v for v in by_id["DRIVETRAIN"]["values"]))
+ self.assertNotIn("pending", by_id["DRIVETRAIN"]["fix_hint"])
+ self.assertTrue(any("holding test on the delivered unit" in v for v in by_id["LIFT"]["values"]))
+ self.assertIn("not a configuration ID", by_id["CONFIG-IDS"]["note"])
+ for did in ("BOM-ISSUE-IN-FORCE", "FRAMES-REP105", "BASE-CONTROLLER-GATES", "DRIVETRAIN", "LIFT"):
+ self.assertTrue(by_id[did]["supersedes"], did)
+ self.assertEqual((by_id["COMPONENT-AVAILABILITY"]["kind"], by_id["COMPONENT-AVAILABILITY"]["owner"]),
+ ("limit", "platform-lead"))
+ self.assertEqual(by_id["TABLET-MOUNT"]["kind"], "configuration")
+
+ def test_component_availability(self):
+ for name in ("docs/BOM.md", "hw/procurement/parts.yaml", "bom/bom_2_0.yml"):
+ self.assertEqual(self.ids(name, "PSU-01 single-source part, selected.\n", "x"),
+ ["COMPONENT-AVAILABILITY"], name)
+ for text in ("PSU-01 single-source part, named option next to PSU-02 (second-sourced).\n",
+ "Selected: PSU-02; single-source alternative PSU-01 as an option.\n",
+ "PSU-01 is single-source and not selected.\n"):
+ self.assertEqual(self.ids("docs/BOM.md", text, "x"), [], text)
+ # Outside BOM and procurement files the rule is not scanned.
+ self.assertEqual(self.ids("docs/a.md", "PSU-01 single-source part, selected.\n", "x"), [])
+
+ def test_tablet_mount(self):
+ for text in ("The ZX80 tablet default position is portrait.\n",
+ "Portrait is the default for the tablet mount.\n",
+ "The tablet position is a configuration ID.\n"):
+ self.assertEqual(self.ids("docs/a.md", text, "x"), ["TABLET-MOUNT"], text)
+ for text in ("The ZX80 tablet default position is landscape; portrait is the second locked position.\n",
+ "Tablet mount: landscape (default, 234 x 149.8 mm) or portrait (149.8 x 234 mm).\n",
+ "The tablet position is a mount setting, not a configuration ID.\n"):
+ self.assertEqual(self.ids("docs/a.md", text, "x"), [], text)
+
def test_mpu6050_explanatory_gate_a_text_is_accepted(self):
# Text explaining why the Gate A chip is not an MPU6050 is not a finding.
for text in ("| `WHO_AM_I` (reg 0x75) | **0x70** (an MPU6050 would return 0x68; Gate A unit) |\n",
@@ -763,10 +823,11 @@ def test_second_estop_is_not_the_openarm_kit_button(self):
"Historical: the OpenArm kit chest button.\n"):
self.assertEqual(self.ids("docs/a.md", text, repo), [], text)
- def test_p03_rev20_1_is_in_force_and_cited(self):
- # Owner decision 30 Oct 2026: P-03 rev20.1 is in force; head-camera values stay 30 degrees and hp30.
+ def test_p03_rev20_1_values_are_cited(self):
+ # Owner decision 30 Oct 2026: P-03 rev20.1 values stay cited (30 degrees, hp30, ZX80 panel);
+ # P-03 rev20.2 is in force from 1 Nov 2026 (test_rev20_2_values_in_force).
data = yaml.safe_load((ROOT / "decisions.yaml").read_text(encoding="utf-8"))
- self.assertEqual(data["in_force"]["source"], "P-03-rev20.1")
+ self.assertEqual(data["in_force"]["source"], "P-03-rev20.2")
self.assertIn("P-03-rev20.1", data["sources"])
by_id = {d["id"]: d for d in self.decisions}
for did in ("HEAD-CAMERA-IDENTITY", "CONFIG-IDS", "OPERATOR-PANEL"):
@@ -825,14 +886,32 @@ def test_software_stop_is_base_only_and_not_an_estop(self):
self.assertEqual(self.ids("docs/a.md", text, repo), [], text)
def test_operator_panel_is_the_getac_zx80(self):
- # P-03 rev20.1 item 24: Getac ZX80, 8 inch, 1920 x 1200, landscape; supersedes ZX10.
+ # P-03 rev20.1 item 24: Getac ZX80, 8 inch, 1920 x 1200; supersedes ZX10.
repo = "openamrobot-ui"
- for text in ("Operator panel: Getac ZX10.\n", "Layout for the ZX80 in portrait.\n"):
- self.assertEqual(self.ids("docs/a.md", text, repo), ["OPERATOR-PANEL"], text)
+ self.assertEqual(self.ids("docs/a.md", "Operator panel: Getac ZX10.\n", repo), ["OPERATOR-PANEL"])
for text in ("Operator panel: Getac ZX80, 8 inch, 1920 x 1200, landscape.\n",
"The ZX10 was replaced by the ZX80.\n",
"Getac ZX10 (superseded).\n"):
self.assertEqual(self.ids("docs/a.md", text, repo), [], text)
+ by_id = {d["id"]: d for d in self.decisions}
+ self.assertIn("landscape by default with a portrait second position", by_id["OPERATOR-PANEL"]["value"])
+
+ def test_zx80_portrait_second_position_versus_default(self):
+ # P-03 rev20.2 item 26: portrait is the second locked position; only portrait as the default is flagged.
+ repo = "openamrobot-ui"
+ second = ("Layout for the ZX80 in portrait.\n",
+ "ZX80: landscape by default; portrait is the second locked position.\n",
+ "The ZX80 rotates to portrait (second locked position, 149.8 x 234 mm).\n")
+ default = ("The ZX80 default orientation is portrait.\n",
+ "Layout for the ZX80 in portrait by default.\n",
+ "Portrait is the default for the ZX80.\n")
+ for text in second:
+ self.assertEqual(self.ids("docs/a.md", text, repo), [], text)
+ self.assertEqual(self.ids("src/layout.ts", "// " + text, repo), [], text)
+ for text in default:
+ # One finding per line: TABLET-MOUNT in text files, OPERATOR-PANEL in UI code.
+ self.assertEqual(self.ids("docs/a.md", text, repo), ["TABLET-MOUNT"], text)
+ self.assertEqual(self.ids("src/layout.ts", "// " + text, repo), ["OPERATOR-PANEL"], text)
def test_ui_gate_a_is_6_november(self):
# Alex decision 30 Oct 2026: full UI Gate A on 6 November 2026; 30 October was partial (A1).