@protoboardai/ext-hardware 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +13 -0
  3. package/checks/evidence.ts +63 -0
  4. package/evals/compose-probe-cable/case.json +160 -0
  5. package/evals/e2e-desk-rover/case.json +62 -0
  6. package/evals/e2e-desk-rover/fixture/AGENTS.md +9 -0
  7. package/evals/e2e-desk-rover/fixture/docs/brief.md +28 -0
  8. package/evals/e2e-desk-rover/fixture/docs/decisions.md +3 -0
  9. package/evals/e2e-desk-rover/fixture/package.json +1 -0
  10. package/evals/e2e-desk-rover/fixture/protoboard.config.ts +12 -0
  11. package/evals/e2e-desk-rover/fixture/src/.gitkeep +0 -0
  12. package/evals/e2e-desk-rover/solution/decisions.md +26 -0
  13. package/evals/e2e-desk-rover/solution/desk-rover.uhd.ts +60 -0
  14. package/evals/find-reversed-cell-leads/case.json +78 -0
  15. package/evals/fix-gnss-uart-swap/case.json +32 -0
  16. package/evals/fix-two-wiring-faults/case.json +97 -0
  17. package/evals/rough-draft-is-concept/case.json +36 -0
  18. package/evals/rough-draft-is-concept/fixture/AGENTS.md +9 -0
  19. package/evals/rough-draft-is-concept/fixture/docs/brief.md +8 -0
  20. package/evals/rough-draft-is-concept/fixture/docs/decisions.md +3 -0
  21. package/evals/rough-draft-is-concept/fixture/package.json +1 -0
  22. package/evals/rough-draft-is-concept/fixture/protoboard.config.ts +11 -0
  23. package/evals/rough-draft-is-concept/fixture/src/.gitkeep +0 -0
  24. package/evals/rough-draft-is-concept/solution/trail-light.uhd.ts +40 -0
  25. package/evals/tidy-the-diagram/case.json +30 -0
  26. package/evals/tidy-the-diagram/fixture/.protoboard/display.json +20 -0
  27. package/evals/tidy-the-diagram/fixture/AGENTS.md +9 -0
  28. package/evals/tidy-the-diagram/fixture/docs/brief.md +5 -0
  29. package/evals/tidy-the-diagram/fixture/package.json +1 -0
  30. package/evals/tidy-the-diagram/fixture/protoboard.config.ts +12 -0
  31. package/evals/tidy-the-diagram/fixture/src/bike-computer.uhd.ts +63 -0
  32. package/ops/bom.ts +22 -0
  33. package/package.json +30 -4
  34. package/protoboard-extension.json +65 -0
  35. package/skills/build-a-project/SKILL.md +562 -0
  36. package/skills/harness-author/SKILL.md +232 -0
  37. package/skills/harness-author/references/stack-up.md +70 -0
  38. package/skills/harness-author/references/wire-harnesses.md +35 -0
  39. package/skills/harness-author/scripts/stackup.ts +104 -0
  40. package/skills/harness-verify/SKILL.md +112 -0
  41. package/skills/system-compose/SKILL.md +269 -0
  42. package/skills/system-compose/references/diagnostic-codes.md +292 -0
  43. package/skills/system-compose/scripts/scenarios.ts +82 -0
  44. package/skills/system-review/SKILL.md +145 -0
  45. package/skills/test-and-measure/SKILL.md +181 -0
  46. package/skills/test-and-measure/references/procedure-template.md +45 -0
  47. package/skills/test-and-measure/references/test-plan.md +35 -0
  48. package/skills/test-and-measure/scripts/test-data.ts +94 -0
  49. package/README.md +0 -3
@@ -0,0 +1,232 @@
1
+ ---
2
+ name: harness-author
3
+ description: Author the physical carriers of a UHD system's links — wire harnesses (connector ends, conductors, lengths, gauges; soldered ends as link-scoped compositions) and mechanical joints as fastener stacks (stack-up arithmetic, hardware selection, nut-engagement check, tightening torque, supplier-cited hardware). Use after system-compose has the links, whenever a link has no carrier, a joint has no hardware, or UHD_ASSEMBLY / UHD_HARNESS_CONNECTOR / UHD_FASTENER_TORQUE fire; follow with harness-verify.
4
+ ---
5
+
6
+ # Harness author
7
+
8
+ In UHD a harness is a module (`kind: "harness"`) that physically carries
9
+ connections: a cable carries conductors between its connector ends, a set of
10
+ screws, spacers and nuts carries a mechanical joint. The BOM, the assembly
11
+ guide and the checks all see the physical parts.
12
+
13
+ Worked examples: `examples/quadcopter-5in/src/harnesses/harnesses.uhd.ts`
14
+ (the cables and six fastener stacks) and `hardware.uhd.ts` (the hardware
15
+ parts, each with a fetched supplier listing); how every cable was worked out,
16
+ modelled, routed and drawn: `examples/quadcopter-5in/docs/harnesses-and-cad.md`.
17
+ Model reference: `@deltarobotics/uhd` `docs/connectors-and-harnesses.md`.
18
+
19
+ ## Purpose
20
+
21
+ Give every link a physical carrier, so the BOM, the assembly guide, the work
22
+ order and the checks see the cables, screws, spacers and nuts that are
23
+ actually bought and fitted. Use it after **system-compose** has the links,
24
+ whenever a link has no carrier or a joint has no hardware, and when
25
+ `UHD_ASSEMBLY`, `UHD_HARNESS_CONNECTOR` or `UHD_FASTENER_TORQUE` fire. In
26
+ **build-a-project** it is part of stage 4; **harness-cad** (stage 7) then
27
+ gives each cable its routed geometry.
28
+
29
+ ## Inputs
30
+
31
+ | Input | Where | From |
32
+ | --- | --- | --- |
33
+ | The system's links and mates | the root system and sub-assemblies in `src/` | **system-compose** |
34
+ | Parts' sockets, pads and frames | the parts' connector composites and geometry (`npx protoboard inspect <instance>`) | **uhd-part-author**, catalog parts |
35
+ | Manuals' wiring orders, pinouts | `parts/evidence/<id>/`, catalog files in `parts/catalog/<id>/` | **uhd-part-research** |
36
+ | Peak currents | `npx protoboard check --format json` (`UHD_PROPULSION_CURRENT` details, supply budgets) | the checks |
37
+ | Supplier listings for hardware | fetched pages, dated | this skill, step 2 (item 3) |
38
+ | Structure thicknesses | plate modules' mechanical metadata | **frame-cad**, **enclosure-cad** |
39
+
40
+ ## Steps
41
+
42
+ ### 1. Wire harnesses (physical)
43
+
44
+ A cable is one physical thing, so it is modelled physically:
45
+
46
+ - each **end is a connector composite** (`Connector()`), positions in the
47
+ printed order;
48
+ - the **conductors are an internal link** between the ends (`self` ↔ `self`),
49
+ straight through unless `childLinks` state a crossover;
50
+ - the system **links each end** to the part's socket (a connector composite
51
+ on the part), or, for a **soldered end**, to a **link-scoped composition**
52
+ of the pads it lands on (`compose`).
53
+
54
+ UHD then derives the functional links (power, UART, I2C, S.Bus…) by tracing
55
+ the conductors, and every system check runs on them. Do not also store those
56
+ functional links.
57
+
58
+ ```ts
59
+ export const DJI_O4_3IN1_CABLE: ModuleDef = {
60
+ id: "dji-o4-3in1-cable", name: "DJI O4 3-in-1 cable", kind: "harness", topology: "wire",
61
+ manufacturer: "DJI",
62
+ interfaces: [
63
+ Connector({ id: "end_fc", connector: "dji_6pin", gender: "plug", pins: ["10V", "G", "T5", "R5", "G", "Sbus"] }),
64
+ Connector({ id: "end_o4", connector: "dji_3in1_cable_6pin", pins: ["VCC", "GND", "RX", "TX", "GND", "S.Bus"] }),
65
+ ],
66
+ links: [{ id: "wires", a: { self: true, interfaceId: "end_fc" }, b: { self: true, interfaceId: "end_o4" } }],
67
+ traits: [{ type: "usage_note", params: { note: "Wire order VCC, GND, RX, TX, GND, S.Bus (O4 user manual).", included_with: "dji-o4-air-unit" } }],
68
+ };
69
+
70
+ // in the system
71
+ link("dji_fc", "DJI socket", ["stack", "dji_socket"], ["dji_cable", "end_fc"]),
72
+ link("dji_o4", "O4 cable socket", ["dji_cable", "end_o4"], ["video", "fc_cable_socket"]),
73
+ // a soldered end: the FC has no connector there, so the link composes the pads
74
+ { id: "gnss_pads", a: { child: "gnss_lead", interfaceId: "end_fc" },
75
+ b: { child: "stack", interfaceId: "gnss_pads",
76
+ compose: { p1: "rail_4v5", p2: "fc:uart1_tx", p3: "fc:uart1_rx", p4: "fc:i2c1_scl", p5: "fc:i2c1_sda", p6: "gnd" } } },
77
+ ```
78
+
79
+ If a part has a socket but no connector composite for it, fix the part
80
+ (**uhd-part-author** rule), do not compose around it.
81
+
82
+ For every harness decide and record, each from a source or as an assumption:
83
+
84
+ | Fact | Where | Rule |
85
+ | --- | --- | --- |
86
+ | Topology | `topology: "wire" \| "bus" \| "split" \| "or"` | wire = point to point; bus = shared (all boards on the same bolts); split = one source, several drops. |
87
+ | Ends and connectors | one `Connector()` per end: type, gender, positions in printed order | The type must match the part's socket (`connector_mismatch`), genders must differ (`connector_gender`), counts must agree (`connector_positions`). A soldered end is `solder_leads` and links to a composition. |
88
+ | Conductors | the internal `self` link; `childLinks` only for a non 1:1 cable | Copy the order from the manual; straight-through (1:1) is a claim too. A wrong conductor inside a UART/I2C shows as `harness_wiring` on the derived link. |
89
+ | Length | `performance` trait `{ kind: "cable", length_mm }` | Measure along the route between the placed ends (assembly positions) plus a service loop; a bought cable's length is its listing's. **harness-cad** reports the shortest clear route. |
90
+ | Gauge | `performance` trait `wire_gauge_awg` | From the peak current the links carry (the system checks give it: `UHD_PROPULSION_CURRENT` details, supply budgets). Cite the ampacity table used; short silicone leads carrying bursts are an assumption to state, not a table value. |
91
+ | Supplied with a part | `usage_note` `included_with: "<part id>"` | Included cables need no separate sources; the part's evidence covers them. |
92
+
93
+ The harness is a child of the module that declares the links to its ends.
94
+ Power and its ground travel in the same harness.
95
+
96
+ Check the derived links:
97
+
98
+ ```bash
99
+ npx protoboard check --format json # UHD_LINK_STATE on derived links names the cable links they run through
100
+ ```
101
+
102
+ Every functional connection you expect through the cable must be there, in
103
+ state `configured` (`deriveLinks(system, lookup)` from `@deltarobotics/uhd`
104
+ lists them).
105
+
106
+ Give every cable its routed 3D geometry with **harness-cad**
107
+ (`@protoboardai/ext-harness-cad`): add a `cable_build` trait (colours, twisted
108
+ or flat, diameter when the gauge is unknown), a `cad.config.ts` in
109
+ `src/harnesses/<harness id>/`, run `npx protoboard run harness-cad`, and set
110
+ a cut-to-length cable's `length_mm` from the shortest route it reports.
111
+
112
+ See [wire harnesses](references/wire-harnesses.md) for the checklist and the quad's cables.
113
+
114
+ ### 2. Fastener stacks (mechanical joints)
115
+
116
+ A mechanical harness lists the hardware as children with quantities, one
117
+ `bolt_pattern` interface (role `component`) matching the joint, and a
118
+ `fastenerStack`: where each part sits along the normal of the joint's
119
+ **structure-side frame**, measured from the structure face (mm), repeated at
120
+ every hole of its bolt pattern.
121
+
122
+ ```ts
123
+ fastenerStack: [
124
+ { child: "screws", atMm: -5 }, // head under the 5 mm plate, shank up (+normal)
125
+ { child: "spacers", atMm: 0 }, // 6 mm nylon spacer 0–6
126
+ { child: "spacers", atMm: 10 }, // after the 4 mm ESC grommet (6–10): 10–16
127
+ { child: "nuts", atMm: 20, torque }, // after the FC grommet (16–20): 20–24
128
+ ],
129
+ ```
130
+
131
+ `atMm` is where the part's own origin sits (a screw's origin is under its
132
+ head, a nut's and spacer's at one face); `direction: -1` flips the part (a
133
+ screw inserted from above). `positions` overrides the hole list (the O4
134
+ camera: two holes per side plate).
135
+
136
+ 1. **Draw the stack-up** from the structure face outward: plate thickness,
137
+ each spacer/standoff, each board or grommet (the clamped thickness, not the
138
+ board's), the nut. Every thickness is a cited value or an `assumption`.
139
+ 2. **Choose lengths.** Screw length ≥ grip (everything between the head and
140
+ the nut's far face) + about 1–2 thread pitches beyond the nut; for a blind
141
+ thread (motor base, standoff, tapped hole, heat-set insert) ≤ the available
142
+ thread depth and ≥ ~1.5 × diameter engagement where the material allows
143
+ (derate for aluminium or plastic; honour a maker's maximum, e.g. DJI's
144
+ 3.10 mm for the O4 camera).
145
+ 3. **Select real hardware** with a supplier listing you fetched: standard,
146
+ size, length, material, part number, pack quantity. Put the listing on the
147
+ part as a `usage_note` with `source` (and packaging as a `performance`
148
+ trait). Dimensions the listing does not give (M2 nyloc height) are
149
+ nominal-standard assumptions, stated as such. A local hardware module also
150
+ needs the sizes the hardware CAD generator reads (**frame-cad** step 6):
151
+ `npx protoboard run hardware-cad --input module=<id>`.
152
+ 4. **Tightening torque** on the item you turn: `torque: { torqueNm, source }`
153
+ when a maker states it, else `{ torqueNm, assumption, reference: [...] }`
154
+ with the supplier table rows it was derated from (soft clamps, aluminium
155
+ threads and PCBs need far less than a steel-nut table value).
156
+ `UHD_FASTENER_TORQUE` warns on joints without one.
157
+ 5. **Check** with the stack-up script and `protoboard check`:
158
+
159
+ ```bash
160
+ npx tsx node_modules/@protoboardai/ext-hardware/skills/harness-author/scripts/stackup.ts # [harness-id ...] [--json]
161
+ npx protoboard check
162
+ ```
163
+
164
+ The script prints every item's span, the thread each screw leaves beyond each
165
+ nut's far face (negative: the screw stops inside the nut), and harness
166
+ quantities that differ from the positions the stack places. `assemble` makes
167
+ the same nut check (`UHD_ASSEMBLY: … ends 0.4 mm short of the far face of nuts`).
168
+
169
+ The quad's six stacks, worked through line by line: [stack-up worked examples](references/stack-up.md).
170
+
171
+ ### 3. After a structure change
172
+
173
+ A change in plate thickness, standoff height or pattern (e.g. via
174
+ **frame-cad** or **enclosure-cad**) invalidates every stack that references
175
+ that face: the checks catch a screw that no longer reaches its nut, but not a
176
+ screw head that now sits inside a thicker plate or a blind thread that is now
177
+ too short. Re-derive each affected stack and re-run the script.
178
+
179
+ ## Decision points
180
+
181
+ | Decision | Rule |
182
+ | --- | --- |
183
+ | Connector end or soldered end | A connector composite when the part has a socket; `solder_leads` and a link-scoped `compose` only when the part has bare pads. Never compose around a socket the part model lacks: fix the part. |
184
+ | Topology | `wire` point to point; `bus` when several boards share it; `split` when one source drops to several. |
185
+ | Bought or cut cable | A cable included with a part (`included_with`) or bought at a listed length keeps that length; a cut cable takes the routed length plus a service loop. |
186
+ | Gauge | From the peak current of the derived links and a cited ampacity table; otherwise an assumption. |
187
+ | Screw length | The shortest standard length that leaves 1–2 pitches beyond the nut, or that stays within a blind thread's depth and any maker maximum. |
188
+ | Torque | The maker's value when stated; otherwise derated from a supplier table, as an assumption with the rows it came from. |
189
+
190
+ ## Outputs
191
+
192
+ | Output | Where |
193
+ | --- | --- |
194
+ | Harness modules (cables, fastener stacks) | `src/harnesses/` (the quad: `harnesses.uhd.ts`) |
195
+ | Hardware parts with supplier listings | `src/harnesses/hardware.uhd.ts`, nominal CAD in `src/harnesses/cad/` |
196
+ | Per-cable provider directories | `src/harnesses/<harness id>/cad.config.ts` (routes by **harness-cad**) |
197
+
198
+ ## Verification
199
+
200
+ ```bash
201
+ npx protoboard check --format json # no UHD_ASSEMBLY, UHD_HARNESS_CONNECTOR, UHD_FASTENER_TORQUE, PB_UNKNOWN_HARNESS
202
+ npx tsx node_modules/@protoboardai/ext-hardware/skills/harness-author/scripts/stackup.ts --json # exit 0
203
+ ```
204
+
205
+ Every expected derived link is present and `configured`. Then
206
+ **harness-verify**, by a fresh agent.
207
+
208
+ ## Failure handling
209
+
210
+ | Code or symptom | Means | Do |
211
+ | --- | --- | --- |
212
+ | `UHD_LINK_STATE` `connector_mismatch` / `connector_gender` / `connector_positions` | The plug does not fit the socket | Fix the cable end's connector type, gender or position count from the manual |
213
+ | `UHD_LINK_STATE` `compose_unknown_slot` | A composition names a position the cable end lacks | Fix the composition |
214
+ | `UHD_LINK_STATE` `harness_wiring` | A conductor lands on the wrong pad inside a UART/I2C | Fix the conductor order or the composition |
215
+ | `UHD_HARNESS_CONNECTOR` | A harness end does not match the interface it mates | Fix the end or the connector choice |
216
+ | `UHD_ASSEMBLY` "… short of the far face of nuts" | The screw stops inside the nut | Choose a longer screw; never shorten the model's stack |
217
+ | `UHD_ASSEMBLY` harness quantity ≠ stack positions | Quantities and holes disagree | Fix the quantities |
218
+ | `UHD_FASTENER_TORQUE` | No torque, or one without source or assumption | Add it on the turned item |
219
+ | `PB_UNKNOWN_HARNESS` | A link names a harness that is not a child | Add the harness child, or fix the name |
220
+ | `HARDWARE_CAD_PARAMS` | A fastener module lacks a size | Add it from the listing |
221
+ | `stackup.ts` exits 1 | A screw stops inside a nut or none passes through one | As for `UHD_ASSEMBLY` |
222
+
223
+ Every other code: **system-compose** [diagnostic codes](../system-compose/references/diagnostic-codes.md).
224
+
225
+ ## Hard rules
226
+
227
+ 1. Every length and thickness in a stack is cited or an `assumption`.
228
+ 2. Never shorten the model's stack to make the nut check pass; change the
229
+ hardware.
230
+ 3. One harness per physical carrier; do not hide a cable inside a module.
231
+ 4. Hardware you buy is a part with a supplier listing; hardware included with
232
+ a part says `included_with`.
@@ -0,0 +1,70 @@
1
+ # Stack-up worked examples (quadcopter-5in)
2
+
3
+ All positions in mm along the structure-side frame's normal, from the
4
+ structure face (z = 0), as `fastenerStack` states them and as
5
+ `scripts/stackup.ts` prints them. Plate thicknesses: bottom plate 5, top plate
6
+ 2 (frame module metadata `plate_thickness_mm`).
7
+
8
+ ## Stack hardware — M3 × 30, spacers, grommets, nyloc (bus: ESC and FC share the bolts)
9
+
10
+ | z from … to | Item | Source of the thickness |
11
+ | --- | --- | --- |
12
+ | −5 … 0 | bottom plate (screw head under it) | frame design value |
13
+ | −5 … 25 | M3 × 30 screw (`atMm: -5`, up) | ISO 4762 length |
14
+ | 0 … 6 | 6 mm nylon spacer | Ettinger 005.83.060 listing |
15
+ | 6 … 10 | ESC in its M3 grommet (4 mm clamp) | assumption on the grommet clamp (DolphinRC stack) |
16
+ | 10 … 16 | 6 mm nylon spacer | Ettinger listing |
17
+ | 16 … 20 | FC in its grommet | as the ESC |
18
+ | 20 … 24 | M3 nyloc nut, 0.3 N·m (snug: soft grommets) | ISO 10511 nominal height |
19
+
20
+ Grip = 5 + 6 + 4 + 6 + 4 + 4 = 29 mm; the M3 × 30 ends at 25: **1 mm beyond
21
+ the nut's far face**. An M3 × 25 would stop 4 mm short: the nut check fails.
22
+ The mates follow the same numbers: the 1.6 mm ESC PCB sits centred in its
23
+ 4 mm grommet, so its underside is at 6 + (4 − 1.6) / 2 = 7.2 mm
24
+ (`gapMm: 7.2`), the FC's at 17.2.
25
+
26
+ ## GPS hardware — M2 × 12 down through the board into a nut under the top plate
27
+
28
+ | z | Item |
29
+ | --- | --- |
30
+ | −2 … 0 | top plate |
31
+ | −4.8 … −2 | M2 nyloc nut (`atMm: -2`, `direction: -1`) — 2.8 mm nominal (the listing gives no M2 dimensions: assumption) |
32
+ | 0 … 5 | 5 mm nylon spacer (the GH6P sockets stand 4.35 mm proud; vendor STEP) |
33
+ | 5 … 5.62 | GPS PCB, 0.62 mm |
34
+ | 5.62 … −6.38 | M2 × 12 screw, head on the board (`atMm: 5.62`, `direction: -1`) |
35
+
36
+ Grip 0.62 + 5 + 2 + 2.8 = 10.42 mm. M2 × 12 leaves **1.58 mm beyond the far
37
+ face**; M2 × 10 would stop 0.42 mm inside the nut (`UHD_ASSEMBLY … ends 0.4 mm
38
+ short`; the `short-gps-screws` fault scenario). Torque 0.1 N·m, an assumption
39
+ well under the M2 class 4.6 table value (0.17 N·m) so the PCB and spacers are
40
+ not crushed.
41
+
42
+ ## Air-unit hardware — M2 × 18 up through the plate, airflow spacers, module, nut
43
+
44
+ −5 … 0 plate · −5 … 13 M2 × 18 · 0 … 3 spacer · 3 … 9 module mounting ear
45
+ (6 mm) · 9 … 11.8 nut. Grip 16.8 mm: **1.2 mm beyond**. The 3 mm spacer is the
46
+ DJI airflow requirement (mate `gapMm: 3`).
47
+
48
+ ## Frame hardware — two blind threads in a 30 mm standoff
49
+
50
+ −5 … 3 M3 × 8 from below: 3 mm into the standoff's thread · 0 … 30 standoff ·
51
+ 30 … 32 top plate · 24 … 32 M3 × 8 from above (`direction: -1`): 6 mm into the
52
+ standoff. No nut: the check is the thread depth, which the standoff listing
53
+ must give. Torque 0.6 N·m, derated for the aluminium thread from the A2-70
54
+ steel-nut table value (0.95 N·m).
55
+
56
+ ## Motor hardware — M3 × 8 up into the motor base (included with the motor)
57
+
58
+ −5.5 … 2.5: 5 mm plate + 0.5 mm raised pad, then 2.5 mm into the motor base,
59
+ whose thread depth (3 mm) the part note records; longer screws reach the
60
+ windings. `included_with` the motor, so no separate listing.
61
+
62
+ ## Camera hardware — M2 × 5 into the O4 camera, both side plates
63
+
64
+ Side plates' outer faces at ±9 (plates ±7 … ±9 around the 14 mm camera),
65
+ screws at `atMm: ∓9` with `positions: [[0, -8], [0, 8]]`. A 5 mm screw through
66
+ a 2 mm plate leaves 3.0 mm for the camera's thread; DJI's maximum engagement
67
+ is 3.10 mm. The part's own note says "2.2 mm into the camera": reconcile the
68
+ two before building (washer? thread depth?). A 0.1 mm margin to a maker's
69
+ maximum is worth a second look — this is the kind of finding
70
+ **harness-verify** exists for.
@@ -0,0 +1,35 @@
1
+ # Wire harnesses
2
+
3
+ ## Checklist per cable
4
+
5
+ - [ ] `kind: "harness"`, `topology`, `manufacturer` (or "(custom)"), `description` naming both ends.
6
+ - [ ] One `Connector()` per end, positions in the printed order, `gender` when known.
7
+ Connector names are shared vocabulary: use the type the mating part's socket declares (`jst_sh_8`, `xt60`, `dji_6pin`); a soldered end is `solder_leads`.
8
+ - [ ] Conductors: an internal `self` ↔ `self` link between the ends; `childLinks` only when the cable is not 1:1. The order comes from the manual or listing (cited in a usage_note).
9
+ - [ ] `performance` trait `{ kind: "cable", length_mm, wire_gauge_awg }`, each cited or in an `assumption`.
10
+ - [ ] Current: the highest current any carried link sees (supply budgets, `UHD_PROPULSION_CURRENT` details `totalPeak`),
11
+ compared with the conductor's rating from a cited table or the cable maker. Burst-only margins are an `assumption` with the reason.
12
+ - [ ] System links: each end linked to the part's socket, or a soldered end to a link-scoped composition of the pads (`compose`). No stored functional links for what the cable carries: they are derived.
13
+ - [ ] Derived links (`deriveLinks`) list every expected connection, all `configured`; no `harness_wiring`.
14
+ - [ ] `included_with: "<part id>"` when the cable ships with a part; otherwise sources/verification like any purchased part.
15
+ - [ ] `protoboard check`: no link errors (`connector_*`, `compose_unknown_slot`, `harness_wiring` appear as `UHD_LINK_STATE`), no `PB_UNKNOWN_HARNESS`.
16
+
17
+ ## The quadcopter's cables
18
+
19
+ | Harness | Ends (system links) | Derived | Facts and sources |
20
+ | --- | --- | --- | --- |
21
+ | `dolphinrc-sh8-fc-esc-cable` | FC `esc_socket` ↔ ESC `fc_socket` (inside the stack) | `fc:esc_port ↔ esc:fc_port` | 50 mm SH1.0 8-pin, straight-through (BAT GND CUR VOID M1 M2 M3 M4); stack manual; included with the FC. |
22
+ | `dji-o4-3in1-cable` | FC `dji_socket` ↔ O4 `fc_cable_socket` | 10 V → VCC, GND ×2, UART5 ↔ MSP DisplayPort, S.Bus | Wire order VCC, GND, RX, TX, GND, S.Bus (O4 user manual); included with the O4. |
23
+ | `dolphinrc-xt60-battery-lead` | battery `xt60` ↔ ESC pads (composition `battery_pads`) | `battery_out → bat_in`, `battery_gnd → bat_neg` | 12 AWG, 120 mm, XT60 male (product page); included with the ESC. The bulk capacitor is soldered across the same pads (links without a harness: a solder joint at the pads). |
24
+ | `matek-gh6p-gnss-lead` | GNSS `gh6p_1` ↔ FC pads (composition `gnss_pads`) | UART1, I2C1, 4.5 V, GND | The M9N's included 20 cm GH6P lead, one plug cut off and soldered (the FC's GPS socket has no I2C). |
25
+
26
+ The receiver is soldered directly to FC pads with no separate lead in the
27
+ reference project; if a lead is added, model it like the GNSS lead.
28
+
29
+ ## Lengths from the model
30
+
31
+ The assembly places every module (`assemble(...).placements`), so the straight
32
+ distance between two linked interfaces is known once both ends have frames;
33
+ the route adds bends and a service loop. Record the length you chose and why
34
+ (`assumption`: "routed along the left arm, +20 mm service loop"). A length the
35
+ cable listing states wins over a computed one.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Fastener stack-up table for the project's mechanical harnesses.
3
+ *
4
+ * npx tsx <this file> [harness-id ...] [--json]
5
+ *
6
+ * For every harness with a `fastenerStack`: each item's span along the joint
7
+ * normal (from the structure-side face, mm), its torque, and for each nut
8
+ * the thread the screw leaves beyond the nut's far face (negative = the
9
+ * screw stops inside the nut; UHD's assemble reports that as an error).
10
+ * A nut on a threaded shaft (a prop nut) has the shaft as its screw.
11
+ * Harness quantities are compared with the number of positions the stack
12
+ * places. Positions are per hole of the structure's bolt pattern unless an
13
+ * item lists its own `positions`.
14
+ */
15
+ import type { ModuleDef } from "@deltarobotics/uhd";
16
+ import { loadProject } from "@protoboardai/core";
17
+
18
+ const args = process.argv.slice(2);
19
+ const json = args.includes("--json");
20
+ const only = args.filter((a) => !a.startsWith("--"));
21
+ const p = await loadProject(process.cwd(), { checks: false, extensions: false });
22
+
23
+ const lengthOf = (def: ModuleDef) =>
24
+ def.interfaces.flatMap((i) => i.parameters ?? []).find((x) => x.id === "length")?.value ?? 0;
25
+ const kindOf = (def: ModuleDef) => (["nut", "screw", "standoff", "spacer"] as const).find((k) => def.tags?.includes(k)) ?? "part";
26
+ const holesOf = (h: ModuleDef) => h.interfaces.flatMap((i) => i.parameters ?? []).find((x) => x.id === "hole_count")?.value ?? 4;
27
+
28
+ const harnesses = [...new Set(p.instances.map((i) => i.definition))]
29
+ .map((id) => p.lookup(id)!)
30
+ .filter((d) => d?.fastenerStack?.length && (!only.length || only.includes(d.id)))
31
+ .sort((a, b) => a.id.localeCompare(b.id));
32
+
33
+ /**
34
+ * The threaded shaft a harness's nut runs on (a prop nut on a motor shaft): the joint the harness carries
35
+ * is a shaft link whose structure end (a) states a thread in its `shaft` trait. UHD's assemble takes that
36
+ * shaft as the nut's screw; so does this table.
37
+ */
38
+ function threadedShaftOf(h: ModuleDef): string | undefined {
39
+ for (const rec of p.definitions.values()) {
40
+ const parent = rec.def;
41
+ for (const l of parent.links ?? []) {
42
+ const ref = parent.children?.find((c) => c.id === l.harness);
43
+ if (!ref || ref.moduleDefId !== h.id || !("child" in l.a)) continue;
44
+ const end = l.a as { child: string; interfaceId: string };
45
+ const child = parent.children?.find((c) => c.id === end.child);
46
+ const iface = child && p.lookup(child.moduleDefId)?.interfaces.find((i) => i.id === end.interfaceId);
47
+ const thread = iface?.protocols?.some((x) => x.type === "shaft") ? iface.traits?.find((t) => t.type === "shaft")?.params?.thread : undefined;
48
+ if (typeof thread === "string") return `${child!.moduleDefId}:${iface!.id} (${thread})`;
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ const report = harnesses.map((h) => {
55
+ const items = h.fastenerStack!.map((item) => {
56
+ const ref = h.children?.find((c) => c.id === item.child);
57
+ const def = ref ? p.lookup(ref.moduleDefId) : undefined;
58
+ const len = def ? lengthOf(def) : 0;
59
+ const dir = item.direction ?? 1;
60
+ return {
61
+ child: item.child,
62
+ part: def?.id ?? `(unknown child ${item.child})`,
63
+ kind: def ? kindOf(def) : "part",
64
+ from: Math.min(item.atMm, item.atMm + dir * len),
65
+ to: Math.max(item.atMm, item.atMm + dir * len),
66
+ direction: dir,
67
+ positions: item.positions?.length ?? holesOf(h),
68
+ torqueNm: item.torque?.torqueNm,
69
+ torqueBasis: item.torque ? (item.torque.source ? "source" : item.torque.assumption ? "assumption" : "unsupported") : undefined,
70
+ };
71
+ });
72
+ const nuts = items
73
+ .filter((x) => x.kind === "nut")
74
+ .map((nut) => {
75
+ const screw = items.find((s) => s.kind === "screw" && s.from <= nut.to && s.to >= nut.from);
76
+ const shaft = !screw && nut.from >= 0 ? threadedShaftOf(h) : undefined;
77
+ if (shaft) return { nut: nut.child, screw: `threaded shaft ${shaft}`, beyondFarFaceMm: null, onShaft: true };
78
+ if (!screw) return { nut: nut.child, screw: null, beyondFarFaceMm: null };
79
+ // the far face is the one the screw reaches last
80
+ const up = screw.direction === 1 ? screw.to - nut.to : nut.from - screw.from;
81
+ return { nut: nut.child, screw: screw.child, beyondFarFaceMm: Number(up.toFixed(2)) };
82
+ });
83
+ const placed = new Map<string, number>();
84
+ for (const x of items) placed.set(x.child, (placed.get(x.child) ?? 0) + x.positions);
85
+ const quantities = (h.children ?? []).map((c) => ({ child: c.id, listed: c.quantity ?? 1, placed: placed.get(c.id) ?? 0 }));
86
+ return { harness: h.id, name: h.name, items, nuts, quantities };
87
+ });
88
+
89
+ if (json) console.log(JSON.stringify(report, null, 2));
90
+ else {
91
+ for (const r of report) {
92
+ console.log(`\n${r.harness} ${r.name ?? ""}`);
93
+ for (const x of r.items) {
94
+ console.log(
95
+ ` ${x.child.padEnd(10)} ${x.part.padEnd(40)} ${x.from.toFixed(2).padStart(7)} … ${x.to.toFixed(2).padEnd(7)} ${x.direction === 1 ? "+" : "-"} ×${x.positions}${x.torqueNm !== undefined ? ` ${x.torqueNm} N·m (${x.torqueBasis})` : ""}`,
96
+ );
97
+ }
98
+ for (const n of r.nuts) {
99
+ console.log("onShaft" in n ? ` nut ${n.nut}: on the ${n.screw} (the shaft's thread length is the motor's fact; check it covers the nut)` : n.screw ? ` nut ${n.nut}: ${n.screw} ${n.beyondFarFaceMm! >= 0 ? `protrudes ${n.beyondFarFaceMm} mm beyond the far face` : `STOPS ${-n.beyondFarFaceMm!} mm SHORT of the far face`}` : ` nut ${n.nut}: NO SCREW passes through it`);
100
+ }
101
+ for (const q of r.quantities.filter((q) => q.listed !== q.placed)) console.log(` quantity ${q.child}: listed ${q.listed}, stack places ${q.placed}`);
102
+ }
103
+ }
104
+ process.exitCode = report.some((r) => r.nuts.some((n) => !("onShaft" in n) && (n.beyondFarFaceMm === null || n.beyondFarFaceMm < 0))) ? 1 : 0;
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: harness-verify
3
+ description: Independently audit a project's harnesses — re-derive every fastener stack-up from cited thicknesses, re-check thread engagement, torques and hardware listings, and every cable's connectors, conductor map, gauge and length against the sources. Run by a fresh agent after harness-author, and after any structure change; records findings, never edits the model.
4
+ ---
5
+
6
+ # Harness verify
7
+
8
+ The author's arithmetic is a claim. Re-derive it from the sources, not from
9
+ the model's own numbers.
10
+
11
+ ## Purpose
12
+
13
+ Prove that every joint will assemble and hold and every cable connects what
14
+ the manuals say, by re-deriving the facts independently. Use it after
15
+ **harness-author**, after any structure change (a plate, enclosure or
16
+ standoff that moved), and before a release. In **build-a-project** it closes
17
+ stage 4 and is repeated in stage 8. It records findings; it never edits the
18
+ model.
19
+
20
+ ## Inputs
21
+
22
+ | Input | Where | From |
23
+ | --- | --- | --- |
24
+ | Harness modules and hardware parts | `src/harnesses/` | **harness-author** |
25
+ | Structure thicknesses | plate and enclosure modules' metadata and `out/body.manifest.json` | **frame-cad**, **enclosure-cad** |
26
+ | Sources | supplier listings (`usage_note` sources), part datasheets and manuals (`parts/evidence/`, `parts/catalog/<id>/`) | **harness-author**, the part skills |
27
+ | Check results and the stack-up report | the commands in step 1 | the project |
28
+
29
+ ## Steps
30
+
31
+ ### 1. Automated
32
+
33
+ ```bash
34
+ npx protoboard check --format json # UHD_ASSEMBLY, UHD_HARNESS_CONNECTOR, UHD_FASTENER_TORQUE, PB_UNKNOWN_HARNESS
35
+ npx tsx node_modules/@protoboardai/ext-hardware/skills/harness-author/scripts/stackup.ts --json
36
+ ```
37
+
38
+ Both must be clean (the script exits 1 when a screw stops inside a nut or no
39
+ screw passes through one). Clean is necessary, not sufficient: the checks
40
+ compare the model with itself.
41
+
42
+ ### 2. Fastener stacks, per harness
43
+
44
+ 1. Rebuild the stack-up from the structure face using thicknesses taken from
45
+ the **sources** (plate thickness from the structure module, spacer and nut
46
+ heights from the listings, board thickness from the datasheet, grommet
47
+ clamp from the stack manual). Mark each as sourced or assumed.
48
+ 2. Compare with the model's `fastenerStack` positions. Any difference over
49
+ 0.1 mm is a finding.
50
+ 3. Thread: nut joints need the screw at least flush with the nut's far face
51
+ (the check) plus about a pitch; blind threads need engagement within the
52
+ thread depth and any maker maximum. Re-derive; note the margin.
53
+ 4. Heads: a screw head must bear on a face, not sit inside a plate or a pad
54
+ that grew (the checks do not see this).
55
+ 5. Torque: the value is on the turned item, cites a source or states an
56
+ assumption with reference rows, and is plausible for the weakest material
57
+ in the clamp.
58
+ 6. Hardware: open each supplier listing (the `usage_note` source); size,
59
+ length, material and part number match the part; quantities × packs cover
60
+ the harness quantities.
61
+
62
+ ### 3. Cables
63
+
64
+ Each end is a connector composite whose type, gender and position order match
65
+ the mating socket and the manual; a soldered end's composition (`compose`)
66
+ puts each wire on the pad the manual names. The conductors (internal link,
67
+ `childLinks` for a crossover) match the manual's pinout. List the derived
68
+ links (`deriveLinks` from `@deltarobotics/uhd`) and confirm each expected
69
+ connection is there and `configured`, and that nothing unexpected is (a wire
70
+ on the wrong pad shows up as an extra or incompatible derived link, or
71
+ `harness_wiring`). The gauge carries the peak current of the derived links
72
+ per a cited table or the maker; the length reaches between the placed ends
73
+ with a service loop. Where **harness-cad** has routed the cable, compare the
74
+ modelled length with `length_fit.route_mm` in its `out/body.manifest.json`.
75
+
76
+ ### 4. Write the findings
77
+
78
+ `docs/reviews/harness-verify-<YYYY-MM-DD>.md` with a table per harness:
79
+ `item | model | re-derived | source | margin | finding`, then the findings
80
+ list `{ id, severity, harness, finding, evidence, fix }`.
81
+
82
+ ## Decision points
83
+
84
+ | Decision | Rule |
85
+ | --- | --- |
86
+ | Severity | **blocker** for anything that would not assemble or would strip or crush; **major** for a claim not supported by a source; **minor** for traceability. |
87
+ | A difference between model and re-derivation | Over 0.1 mm in a stack, or any wrong conductor, is a finding. |
88
+ | A source and the model's own note disagree | A finding either way: name both. The quad's camera screws are a good calibration case: the model's positions leave 3.0 mm of thread in the camera against DJI's 3.10 mm maximum, while the part's note says 2.2 mm. |
89
+
90
+ ## Outputs
91
+
92
+ | Output | Where |
93
+ | --- | --- |
94
+ | The verification report | `docs/reviews/harness-verify-<YYYY-MM-DD>.md` |
95
+
96
+ ## Verification
97
+
98
+ The report has a table for every harness in `src/harnesses/`, every row
99
+ cites its source, and step 1's commands were clean when it was written (say
100
+ so, with their exit codes). No model file changed.
101
+
102
+ ## Failure handling
103
+
104
+ | Code or symptom | Means | Do |
105
+ | --- | --- | --- |
106
+ | `UHD_ASSEMBLY` or `stackup.ts` exit 1 | The model does not assemble | Stop; hand back to **harness-author** before auditing |
107
+ | `UHD_FASTENER_TORQUE` | A joint without a sourced or assumed torque | Finding (major) |
108
+ | `UHD_HARNESS_CONNECTOR`, `UHD_LINK_STATE` on a cable link | An end does not fit, or a conductor is on the wrong pad | Finding (blocker) |
109
+ | `HARNESS_ROUTE_LENGTH` | Modelled and routed length differ | Finding; the author regenerates or fixes the length (**harness-cad**) |
110
+ | A supplier listing no longer loads | The source is gone | Record it unverifiable, with the date; the author replaces the source |
111
+
112
+ Every other code: **system-compose** [diagnostic codes](../system-compose/references/diagnostic-codes.md).