@protoboardai/ext-hardware 0.0.0-stage → 0.2.1
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.
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +10 -2
- package/checks/evidence.ts +63 -0
- package/evals/compose-probe-cable/case.json +160 -0
- package/evals/e2e-desk-rover/case.json +62 -0
- package/evals/e2e-desk-rover/fixture/AGENTS.md +9 -0
- package/evals/e2e-desk-rover/fixture/docs/brief.md +28 -0
- package/evals/e2e-desk-rover/fixture/docs/decisions.md +3 -0
- package/evals/e2e-desk-rover/fixture/package.json +1 -0
- package/evals/e2e-desk-rover/fixture/protoboard.config.ts +12 -0
- package/evals/e2e-desk-rover/fixture/src/.gitkeep +0 -0
- package/evals/e2e-desk-rover/solution/decisions.md +26 -0
- package/evals/e2e-desk-rover/solution/desk-rover.uhd.ts +60 -0
- package/evals/find-reversed-cell-leads/case.json +78 -0
- package/evals/fix-gnss-uart-swap/case.json +32 -0
- package/evals/fix-two-wiring-faults/case.json +97 -0
- package/evals/rough-draft-is-concept/case.json +36 -0
- package/evals/rough-draft-is-concept/fixture/AGENTS.md +9 -0
- package/evals/rough-draft-is-concept/fixture/docs/brief.md +8 -0
- package/evals/rough-draft-is-concept/fixture/docs/decisions.md +3 -0
- package/evals/rough-draft-is-concept/fixture/package.json +1 -0
- package/evals/rough-draft-is-concept/fixture/protoboard.config.ts +11 -0
- package/evals/rough-draft-is-concept/fixture/src/.gitkeep +0 -0
- package/evals/rough-draft-is-concept/solution/trail-light.uhd.ts +40 -0
- package/evals/tidy-the-diagram/case.json +30 -0
- package/evals/tidy-the-diagram/fixture/.protoboard/display.json +20 -0
- package/evals/tidy-the-diagram/fixture/AGENTS.md +9 -0
- package/evals/tidy-the-diagram/fixture/docs/brief.md +5 -0
- package/evals/tidy-the-diagram/fixture/package.json +1 -0
- package/evals/tidy-the-diagram/fixture/protoboard.config.ts +12 -0
- package/evals/tidy-the-diagram/fixture/src/bike-computer.uhd.ts +63 -0
- package/ops/bom.ts +22 -0
- package/package.json +41 -4
- package/protoboard-extension.json +65 -0
- package/skills/build-a-project/SKILL.md +562 -0
- package/skills/harness-author/SKILL.md +232 -0
- package/skills/harness-author/references/stack-up.md +70 -0
- package/skills/harness-author/references/wire-harnesses.md +35 -0
- package/skills/harness-author/scripts/stackup.ts +104 -0
- package/skills/harness-verify/SKILL.md +112 -0
- package/skills/system-compose/SKILL.md +269 -0
- package/skills/system-compose/references/diagnostic-codes.md +292 -0
- package/skills/system-compose/scripts/scenarios.ts +82 -0
- package/skills/system-review/SKILL.md +145 -0
- package/skills/test-and-measure/SKILL.md +181 -0
- package/skills/test-and-measure/references/procedure-template.md +45 -0
- package/skills/test-and-measure/references/test-plan.md +35 -0
- package/skills/test-and-measure/scripts/test-data.ts +94 -0
|
@@ -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).
|