@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,60 @@
1
+ import type { InterfaceLink, ModuleDef } from "@deltarobotics/uhd";
2
+
3
+ /**
4
+ * Desk rover sensor bench (docs/brief.md): an Arduino UNO R3 with the
5
+ * Adafruit Motor Shield V2 stacked on its headers, and a VL53L4CD breakout
6
+ * on the shared I2C bus, powered from the UNO's 5 V pin. Powered from the
7
+ * UNO's USB port (R5); the UNO's VIN and DC jack and the shield's VIN and
8
+ * motor terminal are marked unconnected on purpose (R5), so check reports
9
+ * them as info and the test plan does not power them.
10
+ */
11
+ const link = (id: string, name: string, a: [string, string], b: [string, string], extra: Partial<InterfaceLink> = {}): InterfaceLink => ({
12
+ id,
13
+ name,
14
+ a: { child: a[0], interfaceId: a[1] },
15
+ b: { child: b[0], interfaceId: b[1] },
16
+ ...extra,
17
+ });
18
+
19
+ export const DESK_ROVER: ModuleDef = {
20
+ id: "desk-rover",
21
+ name: "Desk rover sensor bench",
22
+ kind: "module",
23
+ version: "0.1.0",
24
+ description: "Arduino UNO R3 with an Adafruit Motor Shield V2 stacked on it and an Adafruit VL53L4CD time-of-flight breakout on the I2C bus; a desk rig for the rover's obstacle detection.",
25
+ interfaces: [],
26
+ children: [
27
+ {
28
+ id: "uno",
29
+ moduleDefId: "arduino-uno-rev3",
30
+ name: "Arduino UNO R3",
31
+ unconnected: [
32
+ { interfaceId: "vin", reason: "the bench runs from the UNO's USB port (R5)" },
33
+ { interfaceId: "dc_jack", reason: "the bench runs from the UNO's USB port (R5)" },
34
+ ],
35
+ },
36
+ {
37
+ id: "shield",
38
+ moduleDefId: "adafruit-1438-motor-shield-v2",
39
+ name: "Motor shield",
40
+ unconnected: [
41
+ { interfaceId: "arduino_vin", reason: "VIN jumper not fitted: the motors get their own supply" },
42
+ { interfaceId: "vmotor_in", reason: "stays unconnected until the rover's battery is chosen (R5)" },
43
+ ],
44
+ },
45
+ { id: "tof", moduleDefId: "adafruit-5396-vl53l4cd", name: "ToF sensor" },
46
+ ],
47
+ links: [
48
+ // R1: the shield stacks on the UNO's headers (11 mm stack height assumed, as in examples/uno-motor-bench)
49
+ link("shield_mount", "Shield on the UNO's headers", ["uno", "mount"], ["shield", "mount"], { mate: { gapMm: 11 } }),
50
+ link("shield_i2c", "Shield I2C", ["uno", "i2c"], ["shield", "i2c"]),
51
+ // R2: the shield's logic power from the UNO's headers
52
+ link("shield_5v", "Shield 5V", ["uno", "5v"], ["shield", "arduino_5v"]),
53
+ link("shield_ioref", "Shield IOREF", ["uno", "ioref"], ["shield", "arduino_ioref"]),
54
+ link("shield_gnd", "Shield GND", ["uno", "gnd_1"], ["shield", "arduino_gnd"]),
55
+ // R3: the sensor on the I2C bus, 5 V from the UNO, common ground
56
+ link("tof_i2c", "ToF I2C", ["uno", "i2c"], ["tof", "i2c"]),
57
+ link("tof_power", "ToF 5V", ["uno", "5v"], ["tof", "vin"]),
58
+ link("tof_gnd", "ToF GND", ["uno", "gnd_2"], ["tof", "gnd"]),
59
+ ],
60
+ };
@@ -0,0 +1,78 @@
1
+ {
2
+ "schema": "protoboard.eval-case/v0",
3
+ "title": "Find the cell leads soldered the wrong way round, with the scenario that describes it hidden",
4
+ "skills": ["system-compose", "system-review", "harness-author"],
5
+ "tags": ["fault", "system", "hidden-seed", "harder"],
6
+ "fixture": {
7
+ "from": "../../../../examples/smart-bottle",
8
+ "exclude": ["release", "docs"],
9
+ "steps": [
10
+ {
11
+ "note": "the seed is hidden: the scenario that describes it is taken out of src/scenarios.ts, and the datasheets (docs/, which list the scenarios) are not copied, so the agent has to find the fault, not match it to a scenario",
12
+ "replace": { "file": "src/scenarios.ts", "find": " {\n id: \"cell-leads-reversed\",\n label: \"Cell leads soldered the wrong way round\",\n description: \"The red lead goes to BAT- and the black lead to BAT+.\",\n expect: [\"UHD_LINK_STATE\"],\n system: recompose(SMART_BOTTLE, \"bat_pads\", { p1: \"bat_neg\", p2: \"bat_pos\" }),\n },\n", "with": "", "count": 1 }
13
+ },
14
+ {
15
+ "note": "the fault of the (removed) cell-leads-reversed scenario, applied to the system itself. With PB-869 merged check reports it as UHD_LINK_STATE on the battery pads, beside the battery leads' route being stale",
16
+ "replace": { "file": "src/bottle.uhd.ts", "find": "compose: { p1: \"bat_pos\", p2: \"bat_neg\" }", "with": "compose: { p1: \"bat_neg\", p2: \"bat_pos\" }", "count": 1 }
17
+ }
18
+ ]
19
+ },
20
+ "instruction": "During a rework of the smart bottle's cap, the cell's leads were unsoldered from the cap board and soldered back on, and the system model (src/bottle.uhd.ts) was updated to match the bench. Since then the cap does not power on. Find what is wrong in the system model and fix it so the model describes a cap that works; then say in one or two sentences what the fault was and how you found it. The board, the parts, the harness modules, their routed geometry and the scenarios are correct; do not change them.",
21
+ "tools": { "read": true, "edit": true, "commands": ["npx protoboard *", "npx tsx node_modules/@protoboardai/ext-hardware/skills/*"] },
22
+ "budget": { "turns": 30, "seconds": 900, "usd": 4 },
23
+ "graders": [
24
+ {
25
+ "id": "fault-fixed-and-named",
26
+ "kind": "fault",
27
+ "code": "UHD_LINK_STATE",
28
+ "subject": "bat",
29
+ "seeded": ["HARNESS_ROUTE_STALE"],
30
+ "caught": "(revers|swap|backward|wrong way|polarity|crossed|flipped)",
31
+ "description": "check reports reversed 2-pin pads (PB-869): the fault is UHD_LINK_STATE on the battery pads, with the battery leads' route stale (subject /bat/, so an unrelated link-state warning elsewhere does not count), and it must be gone. The answer names the reversal"
32
+ },
33
+ {
34
+ "id": "check-clean",
35
+ "kind": "check",
36
+ "maxErrors": 0,
37
+ "ran": ["uhd/checkSystem", "ext:@protoboardai/ext-harness-cad/routes"],
38
+ "diagnostics": [
39
+ { "code": "HARNESS_ROUTE_STALE", "subject": "battery", "present": false },
40
+ { "code": "UHD_LINK_STATE", "subject": "bat", "present": false }
41
+ ]
42
+ },
43
+ {
44
+ "id": "leads-restored",
45
+ "kind": "facts",
46
+ "definition": "smart-bottle",
47
+ "facts": [
48
+ { "path": "links[id=bat_pads].b.child", "equals": "board" },
49
+ { "path": "links[id=bat_pads].b.interfaceId", "equals": "bat_pads" },
50
+ { "path": "links[id=bat_pads].b.compose.p1", "equals": "bat_pos" },
51
+ { "path": "links[id=bat_pads].b.compose.p2", "equals": "bat_neg" }
52
+ ],
53
+ "description": "the red lead (p1) on BAT+, the black lead (p2) on BAT-, in the link the cell-leads-reversed scenario recomposes"
54
+ },
55
+ {
56
+ "id": "scenarios-hold",
57
+ "kind": "script",
58
+ "script": "skills/system-compose/scripts/scenarios.ts",
59
+ "args": ["--json"],
60
+ "facts": [
61
+ { "path": "[id=nominal].ok", "equals": true },
62
+ { "path": "[id=probe-dq-on-gnd-pad].ok", "equals": true },
63
+ { "path": "[id=module-on-vbus].ok", "equals": true }
64
+ ],
65
+ "description": "the nominal build is clean and the other scenarios are still caught (the exit code is not graded: the cell-leads-reversed scenario is taken out of the fixture)"
66
+ },
67
+ { "id": "route-untouched", "kind": "digest", "file": "src/harnesses/bottle-battery-leads/out/body.manifest.json", "changed": false, "description": "regenerating the route would hide the symptom, not fix the fault" },
68
+ { "id": "board-untouched", "kind": "digest", "file": "src/boards/bottle-cap/bottle-cap.uhd.ts", "changed": false },
69
+ { "id": "harnesses-untouched", "kind": "digest", "file": "src/harnesses/harnesses.uhd.ts", "changed": false },
70
+ { "id": "scenarios-untouched", "kind": "digest", "file": "src/scenarios.ts", "changed": false }
71
+ ],
72
+ "solution": [
73
+ { "protoboard": ["check"], "expect": { "exit": 1 } },
74
+ { "replace": { "file": "src/bottle.uhd.ts", "find": "compose: { p1: \"bat_neg\", p2: \"bat_pos\" }", "with": "compose: { p1: \"bat_pos\", p2: \"bat_neg\" }", "count": 1 } },
75
+ { "protoboard": ["check"] },
76
+ { "say": "The cell's leads were reversed on the board's BAT pads: link bat_pads put the red lead (p1) on bat_neg and the black lead (p2) on bat_pos. check reported UHD_LINK_STATE on bat_pads (no compatible protocol/role between cell:lead_pos and board:bat_neg) and that the battery leads' route was generated for different ends; the route was right and the compose was wrong, so I put p1 back on bat_pos and p2 on bat_neg, and the route is current again." }
77
+ ]
78
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "schema": "protoboard.eval-case/v0",
3
+ "title": "Find and fix a GNSS lead soldered with RX and TX swapped",
4
+ "skills": ["system-compose", "system-review"],
5
+ "tags": ["fault", "system"],
6
+ "fixture": {
7
+ "from": "../../../../examples/quadcopter-5in",
8
+ "exclude": ["docs"],
9
+ "steps": [
10
+ {
11
+ "note": "the gnss-rx-tx-swapped scenario (src/scenarios.ts), applied to the system itself",
12
+ "replace": { "file": "src/quadcopter.uhd.ts", "find": "p2: \"fc:uart1_tx\", p3: \"fc:uart1_rx\"", "with": "p2: \"fc:uart1_rx\", p3: \"fc:uart1_tx\"", "count": 1 }
13
+ }
14
+ ]
15
+ },
16
+ "instruction": "`npx protoboard check` reports an error on this quadcopter since the GNSS lead was re-soldered to the flight controller. Find what is wrong, fix the system model so that check reports no errors, and say in one or two sentences what the fault was. The purchased parts' definitions are correct; do not edit anything under parts/.",
17
+ "tools": { "read": true, "edit": true, "commands": ["npx protoboard *", "npx tsx node_modules/@protoboardai/ext-hardware/skills/*"] },
18
+ "budget": { "turns": 20, "seconds": 600, "usd": 2 },
19
+ "graders": [
20
+ { "id": "fault-fixed-and-named", "kind": "fault", "code": "UHD_LINK_STATE", "caught": "(rx|tx|receive|transmit)[^.]*(swap|revers|cross|wrong|backward)|(swap|revers|cross)[^.]*(rx|tx)" },
21
+ { "id": "check-clean", "kind": "check", "maxErrors": 0, "ran": ["uhd/checkSystem"] },
22
+ { "id": "pads-restored", "kind": "text", "file": "src/quadcopter.uhd.ts", "matches": ["p2:\\s*\"fc:uart1_tx\",\\s*p3:\\s*\"fc:uart1_rx\""], "description": "the soldered end is back on the pads the lead was built for: p2 on TX1, p3 on RX1" },
23
+ { "id": "gnss-part-untouched", "kind": "digest", "file": "parts/catalog/matek-m9n-5883.json", "changed": false },
24
+ { "id": "fc-part-untouched", "kind": "digest", "file": "parts/catalog/dolphinrc-f405-v3-flight-controller.json", "changed": false }
25
+ ],
26
+ "solution": [
27
+ { "protoboard": ["check"], "expect": { "exit": 1 } },
28
+ { "replace": { "file": "src/quadcopter.uhd.ts", "find": "p2: \"fc:uart1_rx\", p3: \"fc:uart1_tx\"", "with": "p2: \"fc:uart1_tx\", p3: \"fc:uart1_rx\"", "count": 1 } },
29
+ { "protoboard": ["check"] },
30
+ { "say": "The GNSS lead's soldered end had its RX and TX wires swapped on the FC's UART1 pads (p2 on RX1, p3 on TX1); I put p2 back on uart1_tx and p3 on uart1_rx, and check now reports no errors." }
31
+ ]
32
+ }
@@ -0,0 +1,97 @@
1
+ {
2
+ "schema": "protoboard.eval-case/v0",
3
+ "title": "Find and fix two wiring faults at once: the O4 powered from the pack, and the GNSS RX and TX swapped",
4
+ "skills": ["system-compose", "system-review", "harness-author"],
5
+ "tags": ["fault", "system", "two-faults", "harder"],
6
+ "fixture": {
7
+ "from": "../../../../examples/quadcopter-5in",
8
+ "exclude": ["docs"],
9
+ "steps": [
10
+ {
11
+ "note": "the seeds are hidden: the two scenarios that describe them are taken out of src/scenarios.ts (docs/, which lists them, is not copied), so the agent has to find the faults, not match them to scenarios",
12
+ "replace": { "file": "src/scenarios.ts", "find": " {\n id: \"o4-on-vbat\",\n label: \"O4 wired to raw 6S\",\n description: \"The DJI O4 power lead is soldered to the battery instead of the 10 V BEC.\",\n expect: [\"UHD_LINK_STATE\"],\n // The cable's red wire is cut off the DJI plug and soldered to the XT60\n // lead: positions 2-6 still mate, VCC now comes straight off the pack.\n system: {\n ...QUADCOPTER_5IN,\n links: [\n ...(QUADCOPTER_5IN.links ?? []).map((l) => (l.id === \"dji_fc\" ? { ...l, childLinks: [2, 3, 4, 5, 6].map((n) => ({ a: `p${n}`, b: `p${n}` })) } : l)),\n { id: \"video_power\", name: \"O4 VCC on the pack\", a: { child: \"battery\", interfaceId: \"battery_out\" }, b: { child: \"video\", interfaceId: \"vcc\" }, harness: \"dji_cable\" },\n ],\n },\n },\n", "with": "", "count": 1 }
13
+ },
14
+ { "replace": { "file": "src/scenarios.ts", "find": " {\n id: \"gnss-rx-tx-swapped\",\n label: \"GNSS lead with RX and TX swapped\",\n description: \"The soldered end of the GNSS lead has its RX and TX wires on the wrong pads (GNSS TX on TX1).\",\n expect: [\"UHD_LINK_STATE\"],\n system: recompose(QUADCOPTER_5IN, \"gnss_pads\", { p2: \"fc:uart1_rx\", p3: \"fc:uart1_tx\" }),\n },\n", "with": "", "count": 1 } },
15
+ {
16
+ "note": "the fault of the (removed) o4-on-vbat scenario on the system itself: the 3-in-1 cable's VCC position cut off the plug, the O4's VCC soldered to the pack",
17
+ "replace": {
18
+ "file": "src/quadcopter.uhd.ts",
19
+ "find": " link(\"dji_fc\", \"DJI socket\", [\"stack\", \"dji_socket\"], [\"dji_cable\", \"end_fc\"]),\n",
20
+ "with": " link(\"dji_fc\", \"DJI socket\", [\"stack\", \"dji_socket\"], [\"dji_cable\", \"end_fc\"], { childLinks: [2, 3, 4, 5, 6].map((n) => ({ a: `p${n}`, b: `p${n}` })) }),\n link(\"video_power\", \"O4 VCC\", [\"battery\", \"battery_out\"], [\"video\", \"vcc\"], { harness: \"dji_cable\" }),\n",
21
+ "count": 1
22
+ }
23
+ },
24
+ {
25
+ "note": "and the fault of the (removed) gnss-rx-tx-swapped scenario",
26
+ "replace": { "file": "src/quadcopter.uhd.ts", "find": "p2: \"fc:uart1_tx\", p3: \"fc:uart1_rx\"", "with": "p2: \"fc:uart1_rx\", p3: \"fc:uart1_tx\"", "count": 1 }
27
+ }
28
+ ]
29
+ },
30
+ "instruction": "This quadcopter came back from a bench repair: the GNSS lead was re-soldered to the flight controller, and the DJI O4's power lead was repaired. The system model (src/quadcopter.uhd.ts) was updated to match the bench, and since then `npx protoboard check` reports errors. Find everything that is wrong, fix the system model so that it describes the quad as it is meant to be built and check reports no errors, and say in a sentence or two per fault what each one was. The purchased parts' definitions and the harness modules are correct; do not edit anything under parts/ or src/harnesses/.",
31
+ "tools": { "read": true, "edit": true, "commands": ["npx protoboard *", "npx tsx node_modules/@protoboardai/ext-hardware/skills/*"] },
32
+ "budget": { "turns": 30, "seconds": 900, "usd": 4 },
33
+ "graders": [
34
+ {
35
+ "id": "o4-power-fixed-and-named",
36
+ "kind": "fault",
37
+ "code": "UHD_LINK_STATE",
38
+ "subject": "video_power|dji",
39
+ "caught": "(o4|air unit|video|vtx)[\\s\\S]*(battery|pack|vbat|6s|raw|xt60|25\\.2|unregulated)|(battery|pack|vbat|6s|raw|xt60)[\\s\\S]*(o4|air unit|video|vtx)"
40
+ },
41
+ {
42
+ "id": "gnss-fixed-and-named",
43
+ "kind": "fault",
44
+ "code": "UHD_LINK_STATE",
45
+ "subject": "gnss",
46
+ "caught": "(rx|tx|receive|transmit)[^.]*(swap|revers|cross|wrong|backward)|(swap|revers|cross)[^.]*(rx|tx)"
47
+ },
48
+ {
49
+ "id": "check-clean",
50
+ "kind": "check",
51
+ "maxErrors": 0,
52
+ "ran": ["uhd/checkSystem", "ext:@protoboardai/ext-harness-cad/routes"],
53
+ "diagnostics": [{ "code": "HARNESS_ROUTE_STALE", "present": false }, { "severity": "error", "message": "O4|GNSS|UART1", "present": false }]
54
+ },
55
+ {
56
+ "id": "wired-as-designed",
57
+ "kind": "facts",
58
+ "definition": "quadcopter-5in",
59
+ "facts": [
60
+ { "path": "links[id=video_power]", "exists": false },
61
+ { "path": "links[id=dji_fc].childLinks", "exists": false },
62
+ { "path": "links[id=dji_fc].b.child", "equals": "dji_cable" },
63
+ { "path": "links[id=dji_o4].b.child", "equals": "video" },
64
+ { "path": "links[id=gnss_pads].b.compose.p2", "equals": "fc:uart1_tx" },
65
+ { "path": "links[id=gnss_pads].b.compose.p3", "equals": "fc:uart1_rx" }
66
+ ],
67
+ "description": "the O4 takes its power through the 3-in-1 cable again (every position of the DJI socket, no lead to the pack), and the GNSS lead's p2 is on TX1, p3 on RX1. Re-pointing the O4 lead at another rail clears the error but is not the quad as designed"
68
+ },
69
+ {
70
+ "id": "scenarios-hold",
71
+ "kind": "script",
72
+ "script": "skills/system-compose/scripts/scenarios.ts",
73
+ "args": ["--json"],
74
+ "exit": 0,
75
+ "facts": [{ "path": "[id=nominal].ok", "equals": true }]
76
+ },
77
+ { "id": "harnesses-untouched", "kind": "digest", "file": "src/harnesses/harnesses.uhd.ts", "changed": false },
78
+ { "id": "scenarios-untouched", "kind": "digest", "file": "src/scenarios.ts", "changed": false },
79
+ { "id": "gnss-part-untouched", "kind": "digest", "file": "parts/catalog/matek-m9n-5883.json", "changed": false },
80
+ { "id": "o4-part-untouched", "kind": "digest", "file": "parts/catalog/dji-o4-air-unit.json", "changed": false }
81
+ ],
82
+ "solution": [
83
+ { "protoboard": ["check"], "expect": { "exit": 1 } },
84
+ {
85
+ "replace": {
86
+ "file": "src/quadcopter.uhd.ts",
87
+ "find": " link(\"dji_fc\", \"DJI socket\", [\"stack\", \"dji_socket\"], [\"dji_cable\", \"end_fc\"], { childLinks: [2, 3, 4, 5, 6].map((n) => ({ a: `p${n}`, b: `p${n}` })) }),\n link(\"video_power\", \"O4 VCC\", [\"battery\", \"battery_out\"], [\"video\", \"vcc\"], { harness: \"dji_cable\" }),\n",
88
+ "with": " link(\"dji_fc\", \"DJI socket\", [\"stack\", \"dji_socket\"], [\"dji_cable\", \"end_fc\"]),\n",
89
+ "count": 1
90
+ }
91
+ },
92
+ { "replace": { "file": "src/quadcopter.uhd.ts", "find": "p2: \"fc:uart1_rx\", p3: \"fc:uart1_tx\"", "with": "p2: \"fc:uart1_tx\", p3: \"fc:uart1_rx\"", "count": 1 } },
93
+ { "protoboard": ["check"] },
94
+ { "command": ["npx", "tsx", "node_modules/@protoboardai/ext-hardware/skills/system-compose/scripts/scenarios.ts"] },
95
+ { "say": "Two faults. 1) The O4's VCC was soldered to the battery (link video_power from battery_out, 18-25.2 V) with only positions 2-6 of the 3-in-1 cable mated, while the O4 takes 3.7-13.2 V: I removed that lead and mated every position of the DJI socket again, so the O4 is powered from the FC's 10 V BEC through the cable. 2) The GNSS lead's RX and TX were swapped on the FC's UART1 pads (p2 on RX1, p3 on TX1); I put p2 back on uart1_tx and p3 on uart1_rx. check reports no errors and every remaining fault scenario is caught." }
96
+ ]
97
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "schema": "protoboard.eval-case/v0",
3
+ "title": "A rough draft is the concept level: three to six blocks, and a question before going deeper",
4
+ "skills": ["build-a-project", "system-compose"],
5
+ "tags": ["system", "concept", "scoping"],
6
+ "fixture": { "from": "fixture" },
7
+ "instruction": "I have an idea for a product (docs/brief.md). Give me a rough draft of it in this Protoboard project so I can look at it as a diagram: the top-level module is `trail-light` (export TRAIL_LIGHT in src/trail-light.uhd.ts, as protoboard.config.ts expects). There is no network and no parts service. Follow the stack's build-a-project SOP. End by telling me what the draft shows and what you assumed.",
8
+ "tools": { "read": true, "edit": true, "commands": ["npx protoboard *"] },
9
+ "budget": { "turns": 30, "seconds": 900, "usd": 4 },
10
+ "graders": [
11
+ { "id": "root-composed", "kind": "fault", "code": "PB_ROOT_NOT_FOUND" },
12
+ { "id": "check-clean", "kind": "check", "maxErrors": 0, "absent": ["PB_INVALID_ENDPOINT", "PB_UNRESOLVED_DEFINITION"], "ran": ["uhd/checkSystem"] },
13
+ {
14
+ "id": "three-to-six-blocks",
15
+ "kind": "facts",
16
+ "definition": "trail-light",
17
+ "description": "The concept level: 3 to 6 top-level capability blocks, connected",
18
+ "facts": [{ "path": "children.2.id" }, { "path": "children.6", "exists": false }, { "path": "links.1.id" }]
19
+ },
20
+ { "id": "nothing-deeper", "kind": "files", "absent": ["src/boards", "src/harnesses", "src/enclosures", "docs/sourcing", "parts/local", "release"] },
21
+ { "id": "asks-before-going-deeper", "kind": "text", "matches": ["\\?", "deeper|next level|go further|break (it|them|each)|inside each"] },
22
+ { "id": "brief-untouched", "kind": "digest", "file": "docs/brief.md", "changed": false },
23
+ {
24
+ "id": "plain-and-honest",
25
+ "kind": "rubric",
26
+ "files": ["src/trail-light.uhd.ts"],
27
+ "rubric": "The person asked for a rough draft of a clip-on trail light from a four-line idea. 1. The model is a concept: three to six top-level blocks named for what they do (power, control, light, button, housing or similar), each with only the interfaces that join it to the others, and the links between them; no real part numbers, no circuit boards, no component-level detail, no file or folder per board. 2. Each block says it is a placeholder with no part chosen. 3. The final answer describes the draft in plain words with any abbreviation spelled out, states what was assumed (for example a single lithium cell, a supply voltage) as assumptions, and asks whether to go a level deeper instead of going there. 4. It does not claim the design is verified, sourced or ready to build."
28
+ }
29
+ ],
30
+ "solution": [
31
+ { "protoboard": ["check"], "expect": { "exit": 1 } },
32
+ { "write": { "file": "src/trail-light.uhd.ts", "from": "solution/trail-light.uhd.ts" } },
33
+ { "protoboard": ["check"] },
34
+ { "say": "Here is the rough draft, at concept level: four blocks in src/trail-light.uhd.ts. Power (a rechargeable cell and its charging from a phone charger) supplies Control and Light; Button tells Control when it is pressed; Control tells Light how bright to be. Every block is a placeholder: no part is chosen and no circuit board is drawn. Assumed, not decided: one lithium cell and a low-voltage controller. The housing and the clip are not in the draft yet. check: 0 errors. Do you want me to go a level deeper and break each block into what is inside it?" }
35
+ ]
36
+ }
@@ -0,0 +1,9 @@
1
+ # trail-light
2
+
3
+ A Protoboard hardware project. The system is described in UHD
4
+ (`@deltarobotics/uhd`): modules, interfaces, links, harnesses, artifacts.
5
+
6
+ - `npx protoboard inspect [instance]`, `npx protoboard check`, `npx protoboard build`
7
+ - `npx protoboard skills` lists the installed SOPs; start with **build-a-project**.
8
+ - The top-level module lives under `src/` (`src/trail-light.uhd.ts`, export `TRAIL_LIGHT`).
9
+ - Decisions go in `docs/decisions.md`; the idea is in `docs/brief.md`.
@@ -0,0 +1,8 @@
1
+ # Trail light: the idea
2
+
3
+ EVAL FIXTURE (an idea, not a specification).
4
+
5
+ A light that clips to a backpack strap for walking at dusk. It should run a
6
+ few evenings on one charge, charge from a phone charger, have one button
7
+ (on, dim, off), and survive rain. Nothing is chosen yet: no parts, no size,
8
+ no price.
@@ -0,0 +1,3 @@
1
+ # Decisions
2
+
3
+ One entry per engineering decision: context, options, choice, evidence.
@@ -0,0 +1 @@
1
+ { "name": "trail-light", "version": "0.1.0", "private": true, "type": "module" }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "@protoboardai/core";
2
+
3
+ // EVAL FIXTURE: a project set up with `protoboard init` and nothing in it yet:
4
+ // src/trail-light.uhd.ts does not exist. The person has an idea, not a design.
5
+ export default defineConfig({
6
+ name: "trail-light",
7
+ root: { source: "src/trail-light.uhd.ts", export: "TRAIL_LIGHT" },
8
+ sources: ["src/**/*.uhd.ts", "parts/local/**/*.uhd.ts"],
9
+ catalog: "parts/catalog",
10
+ extensions: ["@protoboardai/ext-hardware"],
11
+ });
@@ -0,0 +1,40 @@
1
+ import type { InterfaceDef, InterfaceLink, ModuleDef } from "@deltarobotics/uhd";
2
+
3
+ /**
4
+ * Trail light, concept level (docs/brief.md): four capability blocks and the
5
+ * connections between them. Every block is a placeholder: no part is chosen,
6
+ * no board is drawn. The voltages are working assumptions for a single
7
+ * lithium cell and a 3.3 V controller, to be settled at the next level.
8
+ */
9
+ const power = (id: string, name: string, role: "source" | "sink"): InterfaceDef => ({ id, name, domain: "electrical", exposed: true, protocols: [{ type: "power", roles: [role] }] });
10
+ const signal = (id: string, name: string, role: "output" | "input"): InterfaceDef => ({ id, name, domain: "electrical", exposed: true, protocols: [{ type: "gpio", roles: [role] }] });
11
+
12
+ const block = (id: string, name: string, description: string, interfaces: InterfaceDef[]): ModuleDef => ({ id, name, kind: "module", description: `Placeholder, no part chosen yet. ${description}`, interfaces, children: [], links: [] });
13
+
14
+ export const TRAIL_POWER = block("trail-light-power", "Power", "A rechargeable cell, its charging from a USB phone charger, and the supply for everything else.", [power("supply", "Supply out", "source")]);
15
+ export const TRAIL_CONTROL = block("trail-light-control", "Control", "Reads the button and sets the light: on, dim, off.", [power("supply", "Supply in", "sink"), signal("button", "Button in", "input"), signal("dim", "Brightness out", "output")]);
16
+ export const TRAIL_LIGHT_OUTPUT = block("trail-light-output", "Light", "The LED and what drives it at two brightness levels.", [power("supply", "Supply in", "sink"), signal("dim", "Brightness in", "input")]);
17
+ export const TRAIL_BUTTON = block("trail-light-button", "Button", "One push button the wearer can find with gloves on.", [signal("press", "Press out", "output")]);
18
+
19
+ const link = (id: string, name: string, a: [string, string], b: [string, string]): InterfaceLink => ({ id, name, a: { child: a[0], interfaceId: a[1] }, b: { child: b[0], interfaceId: b[1] } });
20
+
21
+ export const TRAIL_LIGHT: ModuleDef = {
22
+ id: "trail-light",
23
+ name: "Trail light",
24
+ kind: "module",
25
+ version: "0.1.0",
26
+ description: "Concept: a clip-on light for walking at dusk. Four blocks; nothing below them is decided.",
27
+ interfaces: [],
28
+ children: [
29
+ { id: "power", moduleDefId: "trail-light-power", name: "Power" },
30
+ { id: "control", moduleDefId: "trail-light-control", name: "Control" },
31
+ { id: "light", moduleDefId: "trail-light-output", name: "Light" },
32
+ { id: "button", moduleDefId: "trail-light-button", name: "Button" },
33
+ ],
34
+ links: [
35
+ link("control_supply", "Supply to the control", ["power", "supply"], ["control", "supply"]),
36
+ link("light_supply", "Supply to the light", ["power", "supply"], ["light", "supply"]),
37
+ link("button_press", "Button to the control", ["button", "press"], ["control", "button"]),
38
+ link("brightness", "Brightness", ["control", "dim"], ["light", "dim"]),
39
+ ],
40
+ };
@@ -0,0 +1,30 @@
1
+ {
2
+ "schema": "protoboard.eval-case/v0",
3
+ "title": "The diagram reads before the build is done: no strip of parts, nothing on top of anything, measured and looked at",
4
+ "skills": ["build-a-project"],
5
+ "tags": ["system", "diagram", "layout"],
6
+ "fixture": { "from": "fixture" },
7
+ "instruction": "Someone opened this project's diagram and said it is one long strip of parts, with two of them on top of each other. Make the diagram readable before you hand it back. Do not change what the system is (src/ stays as it is). There is no network and no parts service. Follow the stack's guidance (AGENTS.md, the build-a-project SOP). End by telling me what you changed and how the diagram measures now.",
8
+ "tools": { "read": true, "edit": true, "commands": ["npx protoboard *"] },
9
+ "budget": { "turns": 25, "seconds": 900, "usd": 3 },
10
+ "graders": [
11
+ { "id": "found-before", "kind": "fault", "code": "PB_DIAGRAM_LAYOUT" },
12
+ { "id": "layout-clean", "kind": "check", "maxErrors": 0, "absent": ["PB_DIAGRAM_LAYOUT"], "ran": ["protoboard/layout", "uhd/checkSystem"] },
13
+ { "id": "model-untouched", "kind": "digest", "file": "src/bike-computer.uhd.ts", "changed": false },
14
+ { "id": "layout-saved", "kind": "digest", "file": ".protoboard/display.json", "changed": true },
15
+ { "id": "says-the-numbers", "kind": "text", "matches": ["cross", "overlap|on top|strip|row"] },
16
+ {
17
+ "id": "laid-out-by-function",
18
+ "kind": "rubric",
19
+ "files": [".protoboard/display.json"],
20
+ "rubric": "The person asked for the bike computer's diagram to be made readable without changing the model. 1. The saved layout puts the controller near the middle with the blocks it talks to around it, the USB port and the battery on the feeding side, and no long row or column of blocks. 2. The agent measured the layout (protoboard layout or project_layout) after changing it and reports the numbers (crossings, overlaps, the longest row) honestly, including anything still above the limits. 3. The final answer says what changed in plain words and does not claim the model or design changed."
21
+ }
22
+ ],
23
+ "solution": [
24
+ { "protoboard": ["layout", "--check"], "expect": { "exit": 1 } },
25
+ { "protoboard": ["layout", "", "--apply"] },
26
+ { "protoboard": ["layout", "--check"] },
27
+ { "protoboard": ["check"] },
28
+ { "say": "The diagram was one row of nine blocks with the buzzer on top of the radio. I laid the top level out again with protoboard layout --apply: the controller in the middle, the USB port and the battery feeding it from the left, the sensors, screen, buzzer, buttons and radio around it. It now measures 0 overlaps, longest row 3 and few line crossings, and check no longer lists PB_DIAGRAM_LAYOUT. Only .protoboard/display.json changed; the model in src/ is as it was." }
29
+ ]
30
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schema": "uhd.display/v0",
3
+ "diagram": {
4
+ "layouts": {
5
+ "": {
6
+ "nodes": {
7
+ "screen": { "x": 60, "y": 140 },
8
+ "usb": { "x": 400, "y": 140 },
9
+ "gps": { "x": 740, "y": 140 },
10
+ "control": { "x": 1080, "y": 140 },
11
+ "buttons": { "x": 1420, "y": 140 },
12
+ "power": { "x": 1760, "y": 140 },
13
+ "motion": { "x": 2100, "y": 140 },
14
+ "radio": { "x": 2440, "y": 140 },
15
+ "buzzer": { "x": 2500, "y": 160 }
16
+ }
17
+ }
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,9 @@
1
+ # bike-computer
2
+
3
+ A Protoboard hardware project. The system is described in UHD
4
+ (`@deltarobotics/uhd`): modules, interfaces, links, harnesses, artifacts.
5
+
6
+ - `npx protoboard inspect [instance]`, `npx protoboard check`, `npx protoboard build`
7
+ - `npx protoboard skills` lists the installed SOPs; start with **build-a-project**.
8
+ - The top-level module lives under `src/` (`src/bike-computer.uhd.ts`, export `BIKE_COMPUTER`).
9
+ - The diagram's layout is saved in `.protoboard/display.json`, the one file under `.protoboard/` you may write.
@@ -0,0 +1,5 @@
1
+ # Bike computer
2
+
3
+ A handlebar computer for road bikes: speed, distance and the route on a small
4
+ screen, from GPS and a motion sensor; a buzzer for turns; two buttons; a phone
5
+ link over Bluetooth; charged from USB-C. Concept level only so far.
@@ -0,0 +1 @@
1
+ { "name": "bike-computer", "version": "0.1.0", "private": true, "type": "module" }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "@protoboardai/core";
2
+
3
+ // EVAL FIXTURE: a concept-level bike computer, nine blocks, whose diagram was
4
+ // saved as one long row of cards with two of them on top of each other
5
+ // (.protoboard/display.json). The model is right; the diagram is not.
6
+ export default defineConfig({
7
+ name: "bike-computer",
8
+ root: { source: "src/bike-computer.uhd.ts", export: "BIKE_COMPUTER" },
9
+ sources: ["src/**/*.uhd.ts", "parts/local/**/*.uhd.ts"],
10
+ catalog: "parts/catalog",
11
+ extensions: ["@protoboardai/ext-hardware"],
12
+ });
@@ -0,0 +1,63 @@
1
+ import type { InterfaceDef, InterfaceLink, ModuleDef } from "@deltarobotics/uhd";
2
+
3
+ /**
4
+ * Bike computer, concept level (docs/brief.md): nine capability blocks around
5
+ * the controller. Every block is a placeholder: no part is chosen.
6
+ */
7
+ const power = (id: string, name: string, role: "source" | "sink"): InterfaceDef => ({ id, name, domain: "electrical", exposed: true, protocols: [{ type: "power", roles: [role] }] });
8
+ const signal = (id: string, name: string, role: "output" | "input"): InterfaceDef => ({ id, name, domain: "electrical", exposed: true, protocols: [{ type: "gpio", roles: [role] }] });
9
+ const block = (id: string, name: string, description: string, interfaces: InterfaceDef[]): ModuleDef => ({ id, name, kind: "module", description: `Placeholder, no part chosen yet. ${description}`, interfaces, children: [], links: [] });
10
+
11
+ export const BIKE_USB = block("bike-usb", "USB-C port", "Charging input.", [power("vbus", "VBUS out", "source")]);
12
+ export const BIKE_POWER = block("bike-power", "Battery and charging", "A lithium cell, its charger and the 3.3 V supply.", [power("vbus", "VBUS in", "sink"), power("v3v3", "3.3 V out", "source")]);
13
+ export const BIKE_CONTROL = block("bike-control", "Controller", "Reads the sensors and buttons, draws the screen, talks to the phone.", [
14
+ power("v3v3", "3.3 V in", "sink"),
15
+ signal("gps", "GPS data in", "input"),
16
+ signal("motion", "Motion data in", "input"),
17
+ signal("screen", "Screen out", "output"),
18
+ signal("buzz", "Buzzer out", "output"),
19
+ signal("buttons", "Buttons in", "input"),
20
+ signal("radio", "Radio out", "output"),
21
+ ]);
22
+ export const BIKE_GPS = block("bike-gps", "GPS", "Position and speed.", [power("v3v3", "3.3 V in", "sink"), signal("data", "Data out", "output")]);
23
+ export const BIKE_MOTION = block("bike-motion", "Motion sensor", "Tilt and vibration, for climbs and crashes.", [power("v3v3", "3.3 V in", "sink"), signal("data", "Data out", "output")]);
24
+ export const BIKE_SCREEN = block("bike-screen", "Screen", "A small sunlight-readable display.", [power("v3v3", "3.3 V in", "sink"), signal("data", "Data in", "input")]);
25
+ export const BIKE_BUZZER = block("bike-buzzer", "Buzzer", "Beeps before a turn.", [signal("drive", "Drive in", "input")]);
26
+ export const BIKE_BUTTONS = block("bike-buttons", "Buttons", "Two buttons usable with gloves.", [signal("press", "Press out", "output")]);
27
+ export const BIKE_RADIO = block("bike-radio", "Bluetooth radio", "The link to the phone.", [power("v3v3", "3.3 V in", "sink"), signal("data", "Data in", "input")]);
28
+
29
+ const link = (id: string, name: string, a: [string, string], b: [string, string]): InterfaceLink => ({ id, name, a: { child: a[0], interfaceId: a[1] }, b: { child: b[0], interfaceId: b[1] } });
30
+
31
+ export const BIKE_COMPUTER: ModuleDef = {
32
+ id: "bike-computer",
33
+ name: "Bike computer",
34
+ kind: "module",
35
+ version: "0.1.0",
36
+ description: "Concept: a handlebar bike computer. Nine blocks; nothing below them is decided.",
37
+ interfaces: [],
38
+ children: [
39
+ { id: "usb", moduleDefId: "bike-usb", name: "USB-C port" },
40
+ { id: "power", moduleDefId: "bike-power", name: "Battery and charging" },
41
+ { id: "control", moduleDefId: "bike-control", name: "Controller" },
42
+ { id: "gps", moduleDefId: "bike-gps", name: "GPS" },
43
+ { id: "motion", moduleDefId: "bike-motion", name: "Motion sensor" },
44
+ { id: "screen", moduleDefId: "bike-screen", name: "Screen" },
45
+ { id: "buzzer", moduleDefId: "bike-buzzer", name: "Buzzer" },
46
+ { id: "buttons", moduleDefId: "bike-buttons", name: "Buttons" },
47
+ { id: "radio", moduleDefId: "bike-radio", name: "Bluetooth radio" },
48
+ ],
49
+ links: [
50
+ link("charge", "USB charging", ["usb", "vbus"], ["power", "vbus"]),
51
+ link("control_supply", "Supply to the controller", ["power", "v3v3"], ["control", "v3v3"]),
52
+ link("gps_supply", "Supply to the GPS", ["power", "v3v3"], ["gps", "v3v3"]),
53
+ link("motion_supply", "Supply to the motion sensor", ["power", "v3v3"], ["motion", "v3v3"]),
54
+ link("screen_supply", "Supply to the screen", ["power", "v3v3"], ["screen", "v3v3"]),
55
+ link("radio_supply", "Supply to the radio", ["power", "v3v3"], ["radio", "v3v3"]),
56
+ link("gps_data", "GPS data", ["gps", "data"], ["control", "gps"]),
57
+ link("motion_data", "Motion data", ["motion", "data"], ["control", "motion"]),
58
+ link("screen_data", "Screen data", ["control", "screen"], ["screen", "data"]),
59
+ link("buzz", "Buzzer drive", ["control", "buzz"], ["buzzer", "drive"]),
60
+ link("buttons", "Buttons", ["buttons", "press"], ["control", "buttons"]),
61
+ link("radio_data", "Radio data", ["control", "radio"], ["radio", "data"]),
62
+ ],
63
+ };
package/ops/bom.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { bom, type OperationFn } from "@protoboardai/core";
2
+
3
+ /** BOM export: quantities multiplied through the instance tree, one line per definition, with origin. */
4
+ export const bom_: OperationFn = (ctx) => {
5
+ const rows = bom(ctx.project);
6
+ const out = ctx.inputs.out ?? "docs";
7
+ const md = [
8
+ `# Bill of materials — ${ctx.project.name}`,
9
+ "",
10
+ "Generated by `protoboard run bom` from the resolved project. Do not edit by hand.",
11
+ "",
12
+ "| Qty | Part | Manufacturer | Part number | Origin |",
13
+ "| ---: | --- | --- | --- | --- |",
14
+ ...rows.map((r) => `| ${r.quantity} | ${r.name ?? r.definition} (\`${r.definition}\`) | ${r.manufacturer ?? ""} | ${r.partNumber ?? ""} | ${r.origin} |`),
15
+ "",
16
+ ].join("\n");
17
+ ctx.writeOutput(`${out}/bom.md`, md, "BOM (markdown)");
18
+ ctx.writeOutput(`${out}/bom.json`, JSON.stringify(rows, null, 2) + "\n", "BOM (JSON)");
19
+ const total = rows.reduce((s, r) => s + r.quantity, 0);
20
+ return { status: "succeeded", summary: `${rows.length} lines, ${total} items`, data: { lines: rows.length, items: total } };
21
+ };
22
+ export { bom_ as bom };