@tachikomagundam/abathur 0.2.7 → 0.2.8
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/dist/core/evolve/run-bench.js +7 -1
- package/dist/core/stats.js +7 -0
- package/dist/test/stats.test.js +17 -0
- package/docs/AGENTISM.md +58 -0
- package/docs/release/README.md +9 -0
- package/docs/release/TEMPLATE.md +10 -0
- package/docs/release/v0.2.7.md +7 -0
- package/docs/release/v0.2.8.md +8 -0
- package/graders/pcb-agent/bench-approver.py +77 -0
- package/graders/pcb-agent/grader.mjs +130 -0
- package/graders/pcb-agent/mutate.sh +165 -0
- package/graders/pcb-agent/r20-bench.sh +27 -0
- package/graders/pcb-agent/reset-sandbox.sh +7 -0
- package/graders/pcb-agent/run-scenario-r20.sh +77 -0
- package/graders/pcb-agent/run-scenario.sh +94 -0
- package/graders/pcb-agent/seed/esp32s3-r12-BRIEF.txt +51 -0
- package/graders/pcb-agent/seed/spec.json +129 -0
- package/graders/pcb-agent/seed/tools/board_author_pcb.py +237 -0
- package/graders/pcb-agent/seed/tools/board_author_sch.py +118 -0
- package/graders/pcb-agent/seed/tools/board_finalize.sh +23 -0
- package/graders/pcb-agent/seed/tools/board_gate.py +69 -0
- package/graders/pcb-agent/seed/tools/board_gate_pcb.py +23 -0
- package/graders/pcb-agent/seed/tools/board_route.py +324 -0
- package/graders/pcb-agent/seed/tools/board_zone_pour.py +312 -0
- package/graders/pcb-agent/seed/tools/claims_lint.py +417 -0
- package/graders/pcb-agent/seed-r20-arena.sh +54 -0
- package/graders/pcb-agent/seed-sandbox.sh +13 -0
- package/package.json +1 -1
- package/plugin/abathur.ts +2 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# run-scenario.sh (pcb-agent) — spawn the PCB TEAM headless on the scenario brief.
|
|
3
|
+
# The seat writes ZERO board artifacts here: this script only (1) seeds an isolated
|
|
4
|
+
# arena, (2) launches the internal-control orchestrator profile (isolated HOME/DB per
|
|
5
|
+
# L-isolation) with the brief, (3) records the transcript. All engineering is done by
|
|
6
|
+
# the team's own agents. Transcript = the evidence the grader reads.
|
|
7
|
+
#
|
|
8
|
+
# Contract mirrors historian: argv = <unitId> <scenario-file> [repoRoot]; last stdout
|
|
9
|
+
# line = {"tokensEst":N,"turns":M} for parseRunMeta; ABATHUR_TRANSCRIPT honored.
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
unit_id="${1:?usage: run-scenario.sh <unitId> <scenario-file> [repoRoot]}"
|
|
12
|
+
scenario_file="${2:?scenario file}"
|
|
13
|
+
repo_root="${3:-$(dirname "$(dirname "$scenario_file")")}"
|
|
14
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
CONTROL=/home/lab/workspace/pcb-control
|
|
16
|
+
ARENA_ROOT="${PCB_BENCH_ARENA:-/tmp/opencode/pcb-bench}"
|
|
17
|
+
ARENA="$ARENA_ROOT/$unit_id"
|
|
18
|
+
TRANSCRIPT="${ABATHUR_TRANSCRIPT:-$ARENA_ROOT/$unit_id.transcript.jsonl}"
|
|
19
|
+
mkdir -p "$ARENA_ROOT"
|
|
20
|
+
|
|
21
|
+
bash "$HERE/seed-sandbox.sh" "$ARENA" "$HERE/seed"
|
|
22
|
+
|
|
23
|
+
# Brief = the scenario file (whole thing; it is the task card, no solutions inside).
|
|
24
|
+
BRIEF="$(cat "$scenario_file")"
|
|
25
|
+
# run cards (S4 seam, historian precedent): candidate mutations ride to the team verbatim.
|
|
26
|
+
rc=""
|
|
27
|
+
if [ -d "$repo_root/abathur-notes" ]; then
|
|
28
|
+
for n in "$repo_root"/abathur-notes/*.md; do [ -e "$n" ] || continue
|
|
29
|
+
rc="$rc
|
|
30
|
+
## Run card
|
|
31
|
+
$(cat "$n")"; done
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
cd "$ARENA"
|
|
35
|
+
# per-unit isolated HOME/DB (never the human main DB; L-isolation law), seeded
|
|
36
|
+
# from the PROVISIONED internal home's provider config (local-qwen baseURL etc).
|
|
37
|
+
UHOME="$ARENA_ROOT/$unit_id.home"; rm -rf "$UHOME"; mkdir -p "$UHOME/.config" "$UHOME/.local/share/opencode"
|
|
38
|
+
cp -r /home/lab/workspace/pcb-control/internal-home/.config/opencode "$UHOME/.config/" 2>/dev/null || true
|
|
39
|
+
rm -rf "$UHOME/.config/opencode/node_modules" "$UHOME/.local/share/opencode/opencode.db" 2>/dev/null || true
|
|
40
|
+
DB="$UHOME/.local/share/opencode/opencode.db"
|
|
41
|
+
|
|
42
|
+
# THE TEAM IS THE SUBJECT: mount the PCB role tree into the arena cwd so
|
|
43
|
+
# `--agent pcb-orchestrator` resolves. Role provenance is the genome seam:
|
|
44
|
+
# if the candidate tree carries agents/*.md (a mutation to the team itself),
|
|
45
|
+
# those files WIN over the canonical internal-control copies.
|
|
46
|
+
mkdir -p "$ARENA/.opencode/agents"
|
|
47
|
+
cp /home/lab/workspace/pcb-control/internal-control/.opencode/agents/*.md "$ARENA/.opencode/agents/"
|
|
48
|
+
if [ -d "$repo_root/agents" ] && ls "$repo_root"/agents/*.md >/dev/null 2>&1; then
|
|
49
|
+
cp -f "$repo_root"/agents/*.md "$ARENA/.opencode/agents/"
|
|
50
|
+
echo "role-provenance: candidate-tree agents override canonical" >&2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
RUNENV=(env -i PATH="$PATH" LANG=C.UTF-8 TERM=xterm-256color HOME="$UHOME"
|
|
54
|
+
XDG_DATA_HOME="$UHOME/.local/share" XDG_CACHE_HOME="$UHOME/.cache" XDG_STATE_HOME="$UHOME/.local/state"
|
|
55
|
+
OPENCODE_DB="$DB" KICAD_SHARE_DIR=/home/lab/.local/AppDir/share/kicad)
|
|
56
|
+
|
|
57
|
+
# FAIL-CLOSED SELF-PROOF GATE: the named agent must actually resolve BEFORE we
|
|
58
|
+
# spend a rep. "agent not found. Falling back to default" voids the rep — that
|
|
59
|
+
# is precisely the channel-dead noise theater the lane forbids.
|
|
60
|
+
"${RUNENV[@]}" opencode agent list >"$ARENA/.agent-list.txt" 2>&1
|
|
61
|
+
grep -q "pcb-orchestrator" "$ARENA/.agent-list.txt" || {
|
|
62
|
+
echo "RUNNER_GATE_FAIL: pcb-orchestrator not in resolved agent inventory" >&2
|
|
63
|
+
tail -5 "$ARENA/.agent-list.txt" >&2
|
|
64
|
+
exit 1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
timeout "${PCB_RUN_TIMEOUT:-2400}" "${RUNENV[@]}" \
|
|
68
|
+
opencode run --agent pcb-orchestrator --format json \
|
|
69
|
+
--dir "$ARENA" -- "$BRIEF$rc" > "$TRANSCRIPT" 2>"$ARENA/.run-stderr" || true
|
|
70
|
+
|
|
71
|
+
if [ ! -s "$TRANSCRIPT" ]; then
|
|
72
|
+
echo "RUNNER_INFRA_FAIL: empty transcript (see $ARENA/.run-stderr)" >&2
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
# mechanical role attribution proof: the bench session in the isolated DB MUST
|
|
76
|
+
# be owned by pcb-orchestrator (transcript lines carry no agent field; the DB
|
|
77
|
+
# session table does). No proof = void rep, never a scored fallback run.
|
|
78
|
+
ROLE_OK="$(sqlite3 "$DB" "SELECT COUNT(*) FROM session WHERE agent='pcb-orchestrator';" 2>/dev/null || echo 0)"
|
|
79
|
+
[ "${ROLE_OK:-0}" -ge 1 ] || { echo "RUNNER_INFRA_FAIL: no pcb-orchestrator session in $DB (fallback-agent run would be noise theater)" >&2; exit 1; }
|
|
80
|
+
|
|
81
|
+
turns=$(grep -c '"type":"text"' "$TRANSCRIPT" 2>/dev/null || true); turns="${turns:-0}"
|
|
82
|
+
tokens=$(python3 - "$TRANSCRIPT" <<'EOF' 2>/dev/null | tail -1 || true
|
|
83
|
+
import json,sys
|
|
84
|
+
n=0
|
|
85
|
+
for l in open(sys.argv[1]):
|
|
86
|
+
try: d=json.loads(l)
|
|
87
|
+
except Exception: continue
|
|
88
|
+
p=d.get("part",{})
|
|
89
|
+
if p.get("type")=="text": n+=len(p.get("text",""))
|
|
90
|
+
print(n//4)
|
|
91
|
+
EOF
|
|
92
|
+
); tokens="${tokens:-0}"
|
|
93
|
+
echo "arena=$ARENA" > "$ARENA_ROOT/$unit_id.arena"
|
|
94
|
+
python3 -c "import json,re;t=int(re.sub(r'[^0-9]','',str('$tokens')) or 0);u=int(re.sub(r'[^0-9]','',str('$turns')) or 0);print(json.dumps({'tokensEst':t,'turns':u}))"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# MODULE BRIEF -- esp32s3-r12 (fixture-tier mini-board)
|
|
2
|
+
|
|
3
|
+
Mission dir: fixtures/board-lab/esp32s3-r12/ (root: /home/lab/workspace/pcb-control/fixtures)
|
|
4
|
+
Binding plan: 2ee2cfb309bea8552dd294db3eb087d74950f0e82e0e11524a142c9f8f217f4a / dispatch pcb-dispatch-r12a-plan1
|
|
5
|
+
|
|
6
|
+
## 0. Purpose
|
|
7
|
+
Six-module ESP32-S3 breakout: USB-C 5V in -> ME6211 3.3V LDO -> module power/decoupling, one status LED, one 1x4 debug header with UART. Fixtures only; never the real board.
|
|
8
|
+
|
|
9
|
+
## 1. Netlist (exactly 6 nets)
|
|
10
|
+
- +3V3 : U1.3V3, U2.VOUT, C1.1, C2.1, C3.1, D1.anode, H1.1 (L10: single merged name, no variants)
|
|
11
|
+
- +5V_IN: J1.VBUS, U2.VIN
|
|
12
|
+
- GND : J1.GND, U2.GND, C1.2, C2.2, C3.2, R1.2? NO -> R1.2 is +3V3 per LED string below.
|
|
13
|
+
Correction -- GND: J1.GND, U2.GND, C1.2, C2.2, C3.2, D1... see M5 for string orientation.
|
|
14
|
+
- GPIO2 : U1.GPIO2, R1.2
|
|
15
|
+
- GPIO4 : U1.GPIO4, H1.2 (label "TX" = board->host)
|
|
16
|
+
- GPIO5 : U1.GPIO5, H1.3 (label "RX" = host->board)
|
|
17
|
+
Final GND set: J1.GND, U2.GND, C1.2, C2.2, C3.2, H1.4.
|
|
18
|
+
All other U1 pins NC; flash/PSRAM GPIO26-32 MUST remain unconnected. U1.EN tied directly to +3V3 (hard-enable; accepted fixture deviation, flagged for DFM review).
|
|
19
|
+
|
|
20
|
+
## 2. Modules (6 max -- no extra refs permitted)
|
|
21
|
+
- M1 U1 ESP32-S3-WROOM-1 (module). Power pads 3V3/GND per library footprint; IO used: EN, GPIO2, GPIO4 (UART TX), GPIO5 (UART RX).
|
|
22
|
+
- M2 J1 USB-C receptacle, power-only. Connect VBUS(A4/A9/B4/B5 bussed)->+5V_IN and GND only. CC1/CC2 left NC -- fixture is bench/charger-fed; no PD negotiation. Deviation noted for reviewer.
|
|
23
|
+
- M3 U2 ME6211C33 LDO (SOT-23-3 class). Function pins VIN->+5V_IN, GND->GND, VOUT->+3V3. Pad-number resolution against the project library symbol is the schematic agent's job, verified by gate.
|
|
24
|
+
- M4 Decoupling: C1,C2 100nF 0402 across +3V3/GND placed at U1 3V3 pad(s); C3 10uF 0603 across +3V3/GND at U2 VOUT. ME6211 input cap omitted (fixture simplification, flagged).
|
|
25
|
+
- M5 Status: +3V3 -> D1 anode, D1 cathode -> R1 (1k) -> GPIO2 (sink-driven, active-low lit). GPIO2 is not an S3 strapping pin (straps are 0/3/45/46): safe.
|
|
26
|
+
- M6 H1 1x4 header, 2.54mm pitch: pin1 +3V3, pin2 GPIO4/TX, pin3 GPIO5/RX, pin4 GND.
|
|
27
|
+
|
|
28
|
+
## 3. Explicit rules -- binding on brief AND every downstream artifact
|
|
29
|
+
- L10 ONE merged power net named +3V3 for every 3.3V pad. Forbidden: +3V3A, 3V3_D, VDD_3V3, duplicate 3.3V names. Net-name scan of sch+pcb must find exactly "+3V3".
|
|
30
|
+
- L11 Every footprint pad pitch AND courtyard gap >= 0.25mm (class clearance 0.2mm + 0.05 margin). 0402 pads 1.0x0.5mm, rot-aware: clearance check applies after per-ref rotation. U1 side pads on >=1.5mm vertical grid rows.
|
|
31
|
+
- L16 Netlist uses KiCad schematic global-label grammar: every connection is a wire terminating in a label whose text equals the net name, placed at each pin. No hierarchical/local-only labels, no net-naming-by-wire.
|
|
32
|
+
- L1 pos.csv refs == bom.csv refs exactly (same set, same strings): {U1,U2,J1,H1,C1,C2,C3,D1,R1} -- 10 refs, no suffix drift, no extra/missing.
|
|
33
|
+
- Placement grid: 0.5mm snap for all component origins.
|
|
34
|
+
|
|
35
|
+
## 4. Artifact contract (mission dir)
|
|
36
|
+
- esp32s3-r12.kicad_sch -- 10 symbols, nets per section 1, grammar per L16.
|
|
37
|
+
- esp32s3-r12.kicad_pcb -- footprints from project lib; courtyard clearances per L11; board outline suggestion <= 55x35mm (target, non-binding).
|
|
38
|
+
- bom.csv / pos.csv -- per L1; pos.csv includes rotation column (feeds L11 rot-aware check).
|
|
39
|
+
- reports/ -- gate evidence JSON from tools/gate_runner.py.
|
|
40
|
+
|
|
41
|
+
## 5. Acceptance gates (deterministic, tools/gate_runner.py evidence only)
|
|
42
|
+
- g-net: parsed sch netlist == section 1 set exactly; net count == 6.
|
|
43
|
+
- g-l10: regex scan for 3.3V-name variants in sch+pcb yields zero hits outside "+3V3".
|
|
44
|
+
- g-l11: pad-pitch/courtyard matrix min gap >= 0.25mm; 0402 pad geom 1.0x0.5 present; U1 side-pad row pitch >= 1.5mm.
|
|
45
|
+
- g-l16: every net pin has wire+label; zero label-less pin wires.
|
|
46
|
+
- g-pos: set(bom.refs) == set(pos.refs), len == 10.
|
|
47
|
+
- g-grid: all placement origins on 0.5mm grid.
|
|
48
|
+
Fail on any gate -> revert to last checkpoint; 3 design-fails on same gate -> ESCALATE with options menu.
|
|
49
|
+
|
|
50
|
+
## 6. Out of scope
|
|
51
|
+
Antenna keepout tuning, ESD parts, USB data lines, battery/charge, strapping-button circuits, any board outside fixtures/.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"board": {
|
|
4
|
+
"name": "toy-m2",
|
|
5
|
+
"_provenance": "TOY-FIXTURE netlist from esp32s3-r12-BRIEF.txt + architect corrections (7 nets/9 refs). U1 pin-pad function mapping = FIXTURE CONVENTION (toy GPIOs on pads 4/5/9), NOT silicon pinout \u2014 HUMAN VERIFICATION REQUIRED before any real-order use. | C1/C2 0603 deviation per pad-corridor DRC class",
|
|
6
|
+
"project": "board-lab toy M2 full-cycle"
|
|
7
|
+
},
|
|
8
|
+
"components": [
|
|
9
|
+
{
|
|
10
|
+
"ref": "U1",
|
|
11
|
+
"mpn": "ESP32-S3-WROOM-1-N8R8",
|
|
12
|
+
"footprint": "RF_Module:ESP32-S3-WROOM-1",
|
|
13
|
+
"pins": {
|
|
14
|
+
"1": "GND",
|
|
15
|
+
"2": "+3V3",
|
|
16
|
+
"3": "+3V3",
|
|
17
|
+
"4": "GPIO4",
|
|
18
|
+
"5": "GPIO5",
|
|
19
|
+
"9": "GPIO2",
|
|
20
|
+
"21": "GND",
|
|
21
|
+
"6": "NC(pin unused)",
|
|
22
|
+
"7": "NC(pin unused)",
|
|
23
|
+
"8": "NC(pin unused)",
|
|
24
|
+
"10": "NC(pin unused)",
|
|
25
|
+
"11": "NC(pin unused)",
|
|
26
|
+
"12": "NC(pin unused)",
|
|
27
|
+
"13": "NC(pin unused)",
|
|
28
|
+
"14": "NC(pin unused)",
|
|
29
|
+
"15": "NC(pin unused)",
|
|
30
|
+
"16": "NC(pin unused)",
|
|
31
|
+
"17": "NC(pin unused)",
|
|
32
|
+
"18": "NC(pin unused)",
|
|
33
|
+
"19": "NC(pin unused)",
|
|
34
|
+
"20": "NC(pin unused)"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"ref": "U2",
|
|
39
|
+
"mpn": "ME6211C33",
|
|
40
|
+
"footprint": "Package_TO_SOT_SMD:SOT-23",
|
|
41
|
+
"pins": {
|
|
42
|
+
"1": "+5V_IN",
|
|
43
|
+
"2": "GND",
|
|
44
|
+
"3": "+3V3"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"ref": "J1",
|
|
49
|
+
"mpn": "USB4110",
|
|
50
|
+
"footprint": "Connector_USB:USB_C_Receptacle_GCT_USB4110",
|
|
51
|
+
"pins": {
|
|
52
|
+
"1": "GND",
|
|
53
|
+
"2": "NC(pin unused)",
|
|
54
|
+
"3": "NC(pin unused)",
|
|
55
|
+
"4": "+5V_IN",
|
|
56
|
+
"5": "NC(pin unused)",
|
|
57
|
+
"6": "+5V_IN",
|
|
58
|
+
"7": "NC(pin unused)",
|
|
59
|
+
"8": "GND",
|
|
60
|
+
"9": "GND",
|
|
61
|
+
"10": "NC(pin unused)",
|
|
62
|
+
"11": "NC(pin unused)",
|
|
63
|
+
"12": "NC(pin unused)",
|
|
64
|
+
"13": "NC(pin unused)",
|
|
65
|
+
"14": "NC(pin unused)",
|
|
66
|
+
"15": "NC(pin unused)",
|
|
67
|
+
"16": "GND"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"ref": "H1",
|
|
72
|
+
"mpn": "HDR1x4",
|
|
73
|
+
"footprint": "Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical",
|
|
74
|
+
"pins": {
|
|
75
|
+
"1": "+3V3",
|
|
76
|
+
"2": "GPIO4",
|
|
77
|
+
"3": "GPIO5",
|
|
78
|
+
"4": "GND"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"ref": "C1",
|
|
83
|
+
"mpn": "100nF",
|
|
84
|
+
"footprint": "Capacitor_SMD:C_0603_1608Metric",
|
|
85
|
+
"pins": {
|
|
86
|
+
"1": "+3V3",
|
|
87
|
+
"2": "GND"
|
|
88
|
+
},
|
|
89
|
+
"_dev": "0402->0603: pad-pair corridor (0.36mm) admits no 0.2mm track w/ 0.2 clr; fixture deviation logged"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"ref": "C2",
|
|
93
|
+
"mpn": "100nF",
|
|
94
|
+
"footprint": "Capacitor_SMD:C_0603_1608Metric",
|
|
95
|
+
"pins": {
|
|
96
|
+
"1": "+3V3",
|
|
97
|
+
"2": "GND"
|
|
98
|
+
},
|
|
99
|
+
"_dev": "0402->0603: pad-pair corridor (0.36mm) admits no 0.2mm track w/ 0.2 clr; fixture deviation logged"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"ref": "C3",
|
|
103
|
+
"mpn": "10uF",
|
|
104
|
+
"footprint": "Capacitor_SMD:C_0603_1608Metric",
|
|
105
|
+
"pins": {
|
|
106
|
+
"1": "+3V3",
|
|
107
|
+
"2": "GND"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"ref": "D1",
|
|
112
|
+
"mpn": "LED",
|
|
113
|
+
"footprint": "LED_SMD:LED_0603_1608Metric",
|
|
114
|
+
"pins": {
|
|
115
|
+
"1": "+3V3",
|
|
116
|
+
"2": "LED1"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"ref": "R1",
|
|
121
|
+
"mpn": "1k",
|
|
122
|
+
"footprint": "Resistor_SMD:R_0603_1608Metric",
|
|
123
|
+
"pins": {
|
|
124
|
+
"1": "LED1",
|
|
125
|
+
"2": "GPIO2"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""r17 deterministic PCB author: netlist.json + spec footprints -> placed .kicad_pcb.
|
|
3
|
+
RM-56 doctrine: no bytes through a model. Embeds real .kicad_mod libs from the
|
|
4
|
+
KiCad AppDir, injects (net N "NAME") per pad (name-match, else positional match
|
|
5
|
+
with WARN), shelf placement by footprint bbox + clearance, Edge.Cuts rect.
|
|
6
|
+
DRC gate (board_gate_pcb.py) evaluates placement-class errors; unconnected_items
|
|
7
|
+
are expected pre-routing and reported separately."""
|
|
8
|
+
import json, re, os, sys
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
|
|
11
|
+
ALIASES = {}
|
|
12
|
+
_ali = os.path.join(os.path.dirname(os.path.abspath(__file__)), "board_author_aliases.json")
|
|
13
|
+
if os.path.exists(_ali):
|
|
14
|
+
ALIASES = {tuple(k.split("|")): v for k, v in json.load(open(_ali)).items()}
|
|
15
|
+
|
|
16
|
+
FP_ROOT = "/home/lab/.local/AppDir/share/kicad/footprints"
|
|
17
|
+
PLACE_MARGIN = 3.0 # mm between footprint bboxes (courtyard-ish)
|
|
18
|
+
GRID = 0.1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def snap(v):
|
|
22
|
+
return round(v / GRID) * GRID
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def forms(s):
|
|
26
|
+
"""top-level s-expr blocks inside a ( ... ) wrapper string."""
|
|
27
|
+
out = []
|
|
28
|
+
i = 0
|
|
29
|
+
n = len(s)
|
|
30
|
+
while i < n:
|
|
31
|
+
if s[i] == "(":
|
|
32
|
+
depth = 0
|
|
33
|
+
j = i
|
|
34
|
+
instr = False
|
|
35
|
+
while j < n:
|
|
36
|
+
ch = s[j]
|
|
37
|
+
if ch == '"' and (j == 0 or s[j - 1] != "\\"):
|
|
38
|
+
instr = not instr
|
|
39
|
+
elif not instr:
|
|
40
|
+
if ch == "(":
|
|
41
|
+
depth += 1
|
|
42
|
+
elif ch == ")":
|
|
43
|
+
depth -= 1
|
|
44
|
+
if depth == 0:
|
|
45
|
+
break
|
|
46
|
+
j += 1
|
|
47
|
+
out.append(s[i:j + 1])
|
|
48
|
+
i = j + 1
|
|
49
|
+
else:
|
|
50
|
+
i += 1
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def sub_block(s, token):
|
|
55
|
+
"""return (block, start, end) of first block starting with token, else None"""
|
|
56
|
+
for b in forms(s):
|
|
57
|
+
if b.startswith(f"({token}"):
|
|
58
|
+
return b
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def bbox_of(km):
|
|
63
|
+
"""rough extent from pads + courtyard graphic lines/rects."""
|
|
64
|
+
xs, ys = [], []
|
|
65
|
+
for m in re.finditer(r'\(pad[^()]*?\(at ([-\d.]+)(?: ([-\d.]+))?\)(?:\s*\(size ([-\d.]+) ([-\d.]+))?', km):
|
|
66
|
+
x, y = float(m.group(1)), float(m.group(2) or 0)
|
|
67
|
+
w, h = (float(m.group(3)), float(m.group(4))) if m.group(3) else (1.0, 1.0)
|
|
68
|
+
xs += [x - w / 2, x + w / 2]
|
|
69
|
+
ys += [y - h / 2, y + h / 2]
|
|
70
|
+
for m in re.finditer(r'\((?:xy|start|end|at) ([-\d.]+) ([-\d.]+)', km):
|
|
71
|
+
xs.append(float(m.group(1)))
|
|
72
|
+
ys.append(float(m.group(2)))
|
|
73
|
+
if not xs:
|
|
74
|
+
return -1, -1, 2, 2
|
|
75
|
+
pad = 0.5
|
|
76
|
+
return min(xs) - pad, min(ys) - pad, max(xs) + pad, max(ys) + pad
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def resolve_footprint(spec_fp):
|
|
80
|
+
lib, name = spec_fp.split(":", 1)
|
|
81
|
+
p = os.path.join(FP_ROOT, f"{lib}.pretty", f"{name}.kicad_mod")
|
|
82
|
+
return p if os.path.exists(p) else None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def assign_nets(netlist):
|
|
86
|
+
nets = sorted({n for c in netlist["components"] for n in c["pins"].values()})
|
|
87
|
+
code = {n: i + 1 for i, n in enumerate(nets)}
|
|
88
|
+
return nets, code
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def author(netlist_path, out_path, rot_by_ref=None):
|
|
92
|
+
d = json.load(open(netlist_path))
|
|
93
|
+
comps = d["components"]
|
|
94
|
+
nets, ncode = assign_nets(d)
|
|
95
|
+
uuidn = [0]
|
|
96
|
+
|
|
97
|
+
def U():
|
|
98
|
+
uuidn[0] += 1
|
|
99
|
+
x = f"{uuidn[0]:032x}"
|
|
100
|
+
return f"{x[:8]}-{x[8:12]}-{x[12:16]}-{x[16:20]}-{x[20:32]}"
|
|
101
|
+
|
|
102
|
+
# ---- layout pass: shelf by bbox heights
|
|
103
|
+
items = []
|
|
104
|
+
warn = []
|
|
105
|
+
for c in sorted(comps, key=lambda c: (c["ref"][0], int(re.sub(r"\D", "", c["ref"]) or 0))):
|
|
106
|
+
fpp = resolve_footprint(c["footprint"])
|
|
107
|
+
if not fpp:
|
|
108
|
+
warn.append(f"NO-FOOTPRINT {c['ref']} {c['footprint']}")
|
|
109
|
+
continue
|
|
110
|
+
km = open(fpp).read()
|
|
111
|
+
x0, y0, x1, y1 = bbox_of(km)
|
|
112
|
+
items.append([c, km, snap(x1 - x0) + PLACE_MARGIN, snap(y1 - y0) + PLACE_MARGIN, (x0, y0)])
|
|
113
|
+
COL_W = 120.0
|
|
114
|
+
cx, cy, rowh = 10.0, 10.0, 0.0
|
|
115
|
+
placed = []
|
|
116
|
+
for it in items:
|
|
117
|
+
if cx + it[2] > COL_W:
|
|
118
|
+
cx = 10.0
|
|
119
|
+
cy += rowh
|
|
120
|
+
rowh = 0.0
|
|
121
|
+
placed.append((it, snap(cx), snap(cy)))
|
|
122
|
+
cx += it[2]
|
|
123
|
+
rowh = max(rowh, it[3])
|
|
124
|
+
board_w, board_h = snap(COL_W + 20), snap(cy + rowh + 20)
|
|
125
|
+
|
|
126
|
+
# ---- emit
|
|
127
|
+
L = ['(kicad_pcb (version 20260206) (generator "board_author_pcb") (generator_version "10.0")',
|
|
128
|
+
' (general (thickness 1.6) (legacy_teardrops no))',
|
|
129
|
+
' (paper "A3")',
|
|
130
|
+
' (layers (0 "F.Cu" signal) (31 "B.Cu" signal) (32 "B.Adhes" user "B.Adhesive") '
|
|
131
|
+
'(33 "F.Adhes" user "F.Adhesive") (34 "B.Paste" user) (35 "F.Paste" user) '
|
|
132
|
+
'(36 "B.SilkS" user "B.Silkscreen") (37 "F.SilkS" user "F.Silkscreen") '
|
|
133
|
+
'(38 "B.Mask" user) (39 "F.Mask" user) (40 "Dwgs.User" user "User.Drawings") '
|
|
134
|
+
'(44 "Edge.Cuts" user) (45 "Margin" user "BoardOutline") (46 "B.CrtYd" user "B.Courtyard") '
|
|
135
|
+
'(47 "F.CrtYd" user "F.Courtyard") (48 "B.Fab" user) (49 "F.Fab" user) (50 "User.1" user))',
|
|
136
|
+
' (setup (pad_to_mask_clearance 0))',
|
|
137
|
+
' (net_class "Default" "" (clearance 0.2) (trace_width 0.25) (via_dia 0.6) (via_drill 0.3) (uvia_dia 0.3) (uvia_drill 0.1)'
|
|
138
|
+
+ " " + "".join(f'(add_net "{esc(n)}")' for n in sorted(nets)) + ')']
|
|
139
|
+
L.append(' (net 0 "")')
|
|
140
|
+
for n in nets:
|
|
141
|
+
L.append(f' (net {ncode[n]} "{n.replace(chr(92), chr(92)*2).replace(chr(34), chr(92)+chr(34))}")')
|
|
142
|
+
# Edge.Cuts rectangle
|
|
143
|
+
pts = [(5, 5), (board_w, 5), (board_w, board_h - 5), (5, board_h - 5), (5, 5)]
|
|
144
|
+
for i in range(4):
|
|
145
|
+
L.append(f' (gr_line (start {pts[i][0]} {pts[i][1]}) (end {pts[i+1][0]} {pts[i+1][1]})'
|
|
146
|
+
f' (stroke (width 0.1) (type solid)) (layer "Edge.Cuts") (uuid "{U()}"))')
|
|
147
|
+
for (it, px, py) in placed:
|
|
148
|
+
c, km, _, _, (x0, y0) = it
|
|
149
|
+
ref = c["ref"]
|
|
150
|
+
pins = c["pins"]
|
|
151
|
+
# pad name -> net number string; match footprint pad names
|
|
152
|
+
fp_padnames = re.findall(r'\(pad "([^"]*)"', km) # file order; '' = np/mount hole
|
|
153
|
+
named = [n for n in fp_padnames if n]
|
|
154
|
+
mapping = {}
|
|
155
|
+
alias = ALIASES.get((ref, c["footprint"]))
|
|
156
|
+
if alias:
|
|
157
|
+
mapping = dict(alias)
|
|
158
|
+
elif set(pins) <= set(named):
|
|
159
|
+
pass # identity match on covered pins; extra pads (shell/np) stay net 0
|
|
160
|
+
elif len(named) == len(pins):
|
|
161
|
+
order_pins = sorted(pins, key=lambda pin: int(pin) if pin.isdigit() else 0)
|
|
162
|
+
for fpn, spp in zip(named, order_pins):
|
|
163
|
+
mapping.setdefault(fpn, spp)
|
|
164
|
+
warn.append(f"POSITIONAL-MAP {ref} {c['footprint']}: {len(named)} pads zipped to spec pins — NO authority table, verify datasheet")
|
|
165
|
+
else:
|
|
166
|
+
warn.append(f"NO-AUTHORITY-MAP {ref} {c['footprint']}: pads {sorted(set(named))[:8]} vs spec pins {sorted(pins)[:8]} -> design-data GAP, net 0 (refusing to guess)")
|
|
167
|
+
# origin so that bbox min lands at px,py
|
|
168
|
+
ox, oy = snap(px - x0), snap(py - y0)
|
|
169
|
+
head = f' (footprint "{esc(c["footprint"])}" (layer "F.Cu") (uuid "{U()}") (at {ox} {oy})'
|
|
170
|
+
head += (f' (property "Reference" "{esc(ref)}" (at 0 -1 0) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))'
|
|
171
|
+
f' (property "Value" "{esc(c.get("mpn", ref))}" (at 0 1 0) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))')
|
|
172
|
+
body = km[km.index("(attr"):] if "(attr" in km else km[km.rindex(")"):]
|
|
173
|
+
# inject nets into pad blocks
|
|
174
|
+
out_km = []
|
|
175
|
+
pos = 0
|
|
176
|
+
for pb in forms(body):
|
|
177
|
+
m = re.match(r'\(pad "([^"]+)"', pb)
|
|
178
|
+
s, e = body.find(pb, pos), body.find(pb, pos) + len(pb)
|
|
179
|
+
if m:
|
|
180
|
+
padname = m.group(1)
|
|
181
|
+
pin = None if padname == "" else mapping.get(padname, padname if padname in pins else None)
|
|
182
|
+
if pin is not None:
|
|
183
|
+
net = pins[pin]
|
|
184
|
+
ins = f' (net {ncode[net]} "{esc(net)}")'
|
|
185
|
+
pb = pb[:-1] + ins + ")"
|
|
186
|
+
pos = e
|
|
187
|
+
out_km.append((s, pb))
|
|
188
|
+
rebuilt = body
|
|
189
|
+
for s, pb in reversed(out_km):
|
|
190
|
+
e = s + None if False else s
|
|
191
|
+
# simpler: replace each original pad block string once
|
|
192
|
+
# (do textual replacement with count=1 on the ORIGINAL blocks)
|
|
193
|
+
for orig_pb in [b for b in forms(body) if b.startswith('(pad ')]:
|
|
194
|
+
new_pb = next(p for (s, p) in out_km if s == body.find(orig_pb))
|
|
195
|
+
rebuilt = rebuilt.replace(orig_pb, new_pb, 1)
|
|
196
|
+
rebuilt = rebuilt[:rebuilt.rindex(")")] # strip trailing close of fp file
|
|
197
|
+
L.append(head + "\n" + rebuilt + "\n )")
|
|
198
|
+
L.append(')\n')
|
|
199
|
+
open(out_path, "w").write("\n".join(x for x in L if x) + "\n")
|
|
200
|
+
pro = out_path[:-len(".kicad_pcb")] + ".kicad_pro"
|
|
201
|
+
import json as _j
|
|
202
|
+
_j.dump({
|
|
203
|
+
"board": {"design_settings": {
|
|
204
|
+
"defaults": {"board_outline_line_width": 0.1, "copper_line_width": 0.2,
|
|
205
|
+
"solder_mask_clearance": 0.0, "solder_mask_min_width": 0.0,
|
|
206
|
+
"allow_soldermask_bridges_in_footprints": True},
|
|
207
|
+
"drc_exclusions": [],
|
|
208
|
+
"rules": {"min_hole_to_hole": 0.15, "min_through_hole_diameter": 0.15,
|
|
209
|
+
"min_clearance": 0.2, "min_connection": 0.0,
|
|
210
|
+
"min_copper_edge_clearance": 0.1, "min_hole_clearance": 0.15,
|
|
211
|
+
"min_silk_clearance": 0.0, "min_text_height": 0.8,
|
|
212
|
+
"min_text_thickness": 0.12, "min_track_width": 0.15,
|
|
213
|
+
"min_via_annular_width": 0.1, "min_via_diameter": 0.45,
|
|
214
|
+
"solder_mask_to_copper_clearance": 0.0, "use_height_for_length_calcs": True}}},
|
|
215
|
+
"libraries": {"fp_files": []},
|
|
216
|
+
"meta": {"filename": pro.split("/")[-1], "version": 3},
|
|
217
|
+
"net_settings": {"classes": [{"name": "Default", "clearance": 0.2,
|
|
218
|
+
"trace_width": 0.25, "via_dia": 0.6, "via_drill": 0.2, "uvia_dia": 0.3,
|
|
219
|
+
"uvia_drill": 0.1, "allow_soldermask_bridges_in_footprints": True}],
|
|
220
|
+
"meta": {"version": 3}},
|
|
221
|
+
"pcbnew": {"last_paths": {}},
|
|
222
|
+
"sheets": [], "text_variables": {}},
|
|
223
|
+
open(pro, "w"), indent=1)
|
|
224
|
+
return nets, warn, board_w, board_h
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def esc(s):
|
|
228
|
+
return str(s).replace("\\", "\\\\").replace('"', '\\"')
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
nl = sys.argv[1]
|
|
233
|
+
out = sys.argv[2]
|
|
234
|
+
nets, warn, w, h = author(nl, out)
|
|
235
|
+
print(f"authored {out}: nets={len(nets)} board={w}x{h}mm")
|
|
236
|
+
for x in warn:
|
|
237
|
+
print("WARN", x)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""r17 deterministic schematic author (RM-56: netlist bytes NEVER through a model).
|
|
3
|
+
Topology: NO wires. Every pin gets one global label anchored EXACTLY on the pin
|
|
4
|
+
end, rotation 180 (text left, away from body). Anchor == pin end by construction;
|
|
5
|
+
no dangling labels (r13 D1 defect class), no collinear merges. Grid 0.635 mm.
|
|
6
|
+
Usage: board_author_sch.py netlist.json -o out.kicad_sch"""
|
|
7
|
+
import json, re, argparse
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
|
|
10
|
+
G = 0.635
|
|
11
|
+
COLPITCH = 99.06 # column pitch (41-pin U1 fans 52mm left of anchor col)
|
|
12
|
+
ROWPITCH = 76.2 # row pitch; keep > 2.54 * maxpins/2 stack of neighbours
|
|
13
|
+
PINPITCH = 2.54 # pin vertical spacing (label height 1.27 leaves 1.27 gap)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def snap(v):
|
|
17
|
+
return round(v / G) * G
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def esc(s):
|
|
21
|
+
return str(s).replace("\\", "\\\\").replace('"', '\\"')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Uuid:
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.n = 0
|
|
27
|
+
|
|
28
|
+
def __call__(self):
|
|
29
|
+
self.n += 1
|
|
30
|
+
x = f"{self.n:032x}"
|
|
31
|
+
return f"{x[:8]}-{x[8:12]}-{x[12:16]}-{x[16:20]}-{x[20:32]}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def pin_order(pins):
|
|
35
|
+
return sorted(pins, key=lambda p: int(p) if p.isdigit() else 0)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build(netlist_path, out_path):
|
|
39
|
+
d = json.load(open(netlist_path))
|
|
40
|
+
comps = d["components"]
|
|
41
|
+
u = Uuid()
|
|
42
|
+
order = sorted(comps, key=lambda c: (c["ref"][0], int(re.sub(r"\D", "", c["ref"]) or 0)))
|
|
43
|
+
pos = {}
|
|
44
|
+
for slot, c in enumerate(order):
|
|
45
|
+
row, col = divmod(slot, 4)
|
|
46
|
+
pos[c["ref"]] = (snap(60 + col * COLPITCH), snap(60 + row * ROWPITCH))
|
|
47
|
+
# symbol defs: single-sided fan-left, PINPITCH spacing
|
|
48
|
+
prefix_pins = {}
|
|
49
|
+
pinset = {}
|
|
50
|
+
for c in order:
|
|
51
|
+
ref = c["ref"]
|
|
52
|
+
ps = pin_order(c["pins"])
|
|
53
|
+
key = "SR_" + re.sub(r"\d+$", "", ref)
|
|
54
|
+
if key in prefix_pins and prefix_pins[key] != ps:
|
|
55
|
+
key = "SR_" + ref
|
|
56
|
+
prefix_pins.setdefault(key, ps)
|
|
57
|
+
c["_key"] = key
|
|
58
|
+
sx, sy = pos[ref]
|
|
59
|
+
n = len(ps)
|
|
60
|
+
# world y-down vs lib-symbol-local y-UP: the def emits oy=+off (up) per
|
|
61
|
+
# pin slot i; the label anchor in SHEET coords must therefore SUBTRACT.
|
|
62
|
+
pinset[ref] = [(p, snap(sx - 5.08), snap(sy - ((n - 1) / 2 - i) * PINPITCH))
|
|
63
|
+
for i, p in enumerate(ps)]
|
|
64
|
+
labels = []
|
|
65
|
+
for c in order:
|
|
66
|
+
for pnum, net in c["pins"].items():
|
|
67
|
+
px, py = next((ax, ay) for (pp, ax, ay) in pinset[c["ref"]] if pp == pnum)
|
|
68
|
+
labels.append((net, px, py))
|
|
69
|
+
L = ['(kicad_sch (version 20250114) (generator "board_author_sch") (generator_version "10.0.5")',
|
|
70
|
+
f' (uuid "{u()}")', ' (paper "A3")', ' (lib_symbols']
|
|
71
|
+
for key, ps in sorted(prefix_pins.items()):
|
|
72
|
+
n = len(ps)
|
|
73
|
+
L.append(f' (symbol "solar_ray:{esc(key)}"')
|
|
74
|
+
L.append(' (pin_numbers (hide yes))')
|
|
75
|
+
L.append(' (pin_names (offset 1.016))')
|
|
76
|
+
L.append(' (exclude_from_sim no) (in_bom yes) (on_board yes)')
|
|
77
|
+
L.append(' (property "Reference" "U" (at 0 0 0) (effects (font (size 1.27 1.27))))')
|
|
78
|
+
L.append(' (property "Value" "V" (at 0 0 0) (effects (font (size 1.27 1.27))))')
|
|
79
|
+
L.append(' (property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))')
|
|
80
|
+
L.append(' (property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))')
|
|
81
|
+
L.append(f' (symbol "{esc(key)}_1_1"')
|
|
82
|
+
for i, p in enumerate(ps):
|
|
83
|
+
oy = snap(((n - 1) / 2 - i) * PINPITCH)
|
|
84
|
+
L.append(f' (pin passive line (at {-5.08} {oy} 0) (length 5.08)'
|
|
85
|
+
f' (name "~" (effects (font (size 1.016 1.016))))'
|
|
86
|
+
f' (number "{esc(p)}" (effects (font (size 1.016 1.016)))))')
|
|
87
|
+
L.append(' )')
|
|
88
|
+
L.append(' (embedded_fonts no)')
|
|
89
|
+
L.append(' )')
|
|
90
|
+
L.append(' )')
|
|
91
|
+
for c in order:
|
|
92
|
+
ref = c["ref"]
|
|
93
|
+
sx, sy = pos[ref]
|
|
94
|
+
L.append(f' (symbol (lib_id "solar_ray:{esc(c["_key"])}") (at {sx} {sy} 0) (unit 1)')
|
|
95
|
+
L.append(' (exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (fields_autoplaced no)')
|
|
96
|
+
L.append(f' (uuid "{u()}")')
|
|
97
|
+
for k, v, xo in (("Reference", ref, -2.54), ("Value", c.get("mpn", ref), 2.54),
|
|
98
|
+
("Footprint", c.get("footprint", ""), 0.0), ("Datasheet", "", 0.0)):
|
|
99
|
+
L.append(f' (property "{k}" "{esc(v)}" (at {snap(sx+xo)} {snap(sy+5.08)} 0)'
|
|
100
|
+
f' (effects (font (size 1.27 1.27)) (hide yes)))')
|
|
101
|
+
for (p, _, _) in pinset[ref]:
|
|
102
|
+
L.append(f' (pin "{esc(p)}" (uuid "{u()}"))')
|
|
103
|
+
L.append(' )')
|
|
104
|
+
for net, x, y in labels:
|
|
105
|
+
L.append(f' (global_label "{esc(net)}" (shape passive) (at {x} {y} 180)'
|
|
106
|
+
f' (effects (font (size 1.27 1.27)) (justify left)) (uuid "{u()}"))')
|
|
107
|
+
L += [' (sheet_instances', ' (path "/" (page "1"))', ' )', ' (embedded_fonts no)', ')']
|
|
108
|
+
open(out_path, "w").write("\n".join(L) + "\n")
|
|
109
|
+
return len(order), len(labels)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
ap = argparse.ArgumentParser()
|
|
114
|
+
ap.add_argument("netlist")
|
|
115
|
+
ap.add_argument("-o", "--out", required=True)
|
|
116
|
+
a = ap.parse_args()
|
|
117
|
+
s, l = build(a.netlist, a.out)
|
|
118
|
+
print(f"emitted {a.out}: symbols={s} labels={l}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# r17 finalization pipeline: routed board -> pour -> native refill -> DRC gate -> fab exports
|
|
3
|
+
set -e
|
|
4
|
+
IN="${1:?routed kicad_pcb}"
|
|
5
|
+
OUT="${2:?output board pcb}"
|
|
6
|
+
RUN="${3:-evolution-round-17}"
|
|
7
|
+
cd /home/lab/workspace/pcb-control
|
|
8
|
+
python3 tools/board_zone_pour.py "$IN" "$OUT"
|
|
9
|
+
cp /tmp/sr-c4.kicad_pro "$(dirname "$OUT")/$(basename "$OUT" .kicad_pcb).kicad_pro"
|
|
10
|
+
D=/home/lab/workspace/pcb-control/reports/recovery/$RUN
|
|
11
|
+
kicad-cli pcb drc "$OUT" --refill-zones --save-board --format json --severity-error --output "$D/$(basename "$OUT").drc-refill.json" >/dev/null 2>&1
|
|
12
|
+
kicad-cli pcb drc "$OUT" --format json --severity-error --output "$D/$(basename "$OUT").drc-final.json" >/dev/null 2>&1
|
|
13
|
+
python3 - "$D/$(basename "$OUT").drc-final.json" <<'EOF'
|
|
14
|
+
import json, sys, collections
|
|
15
|
+
d = json.load(open(sys.argv[1]))
|
|
16
|
+
v = d.get('violations', []); u = d.get('unconnected_items', [])
|
|
17
|
+
print(f"GATE: errors={len(v)} {dict(collections.Counter(x.get('type','?') for x in v))} unconnected={len(u)}")
|
|
18
|
+
EOF
|
|
19
|
+
mkdir -p "$D/fab-out"
|
|
20
|
+
kicad-cli pcb export gerbers "$OUT" -o "$D/fab-out/gerbers.zip" >/dev/null
|
|
21
|
+
kicad-cli pcb export drill "$OUT" -o "$D/fab-out/" >/dev/null
|
|
22
|
+
kicad-cli pcb export pos "$OUT" -o "$D/fab-out/pos.pos" --units mm >/dev/null
|
|
23
|
+
echo "fab exports -> $D/fab-out/"; ls "$D/fab-out/" | head
|