diff --git a/hardware/rev2/HOWTO-module-pcb.md b/hardware/rev2/HOWTO-module-pcb.md index 0476790..e534d2a 100644 --- a/hardware/rev2/HOWTO-module-pcb.md +++ b/hardware/rev2/HOWTO-module-pcb.md @@ -20,9 +20,9 @@ Only layout is done twice (module boards now, integrated board later). ## 1. Preflight — blockers to clear before starting -1. **Four symbols still have no footprint.** `Update PCB from Schematic` (F8) - refuses to run until every symbol has one. Names verified against the - KiCad 10 libraries on disk 2026-08-12: +1. ~~Four symbols still have no footprint.~~ **DONE 2026-08-12** — all 172 + symbol instances carry a footprint (`Update PCB from Schematic` runs). + Kept for the record; names verified against the KiCad 10 libraries: | Symbol | Sheet | Footprint | |---|---|---| @@ -36,9 +36,9 @@ Only layout is done twice (module boards now, integrated board later). Assign these in the block sheets (they belong to the design, not to the prototype) so stage B inherits them. -2. **Off-LCSC parts** — the LCSC order will NOT cover these; source separately: - Weidmüller 1715010000 ×4 (J2 battery, J3 pump, J8/J9 panel), and the generic - pin headers. See §9. +2. **Off-LCSC parts** — the LCSC order does not cover Weidmüller 1715010000 ×4 + (J2 battery, J3 pump, J8/J9 panel) nor the 2.54 mm pin headers: **both are + already in Paul's stock**, nothing to buy. See §9. 3. Close KiCad before any scripted edit; stale `~*.lck` files are gitignored but a second KiCad instance on the same sheet file will fight you. @@ -155,6 +155,14 @@ both ends (return + keying): `mod-mcu` keeps `U0TXD`/`U0RXD`/`EN`/`BOOT` internal (blocks 2+3 are on the same board) and exposes J7 expansion + J6 JTAG as their own headers, as designed. +**Generated views of this contract:** `python3 hardware/rev2/tools/gen-module-canvas.py` +renders the six module cards, the current-path strip and the net→pin bench +matrix to `hardware/rev2/export/module-canvas/module-interconnect.html` +(open in a browser). The script cross-checks its data against the J101 table +above and refuses to run on a mismatch — edit the table here first, then the +script, then regenerate. The same run also emits the artboards for the +editable Claude Design canvas (published from a Claude Code session). + **Silkscreen every header pin with its net name.** This is the whole self-documenting-bundle idea from `09-prototype-modules.md`. diff --git a/hardware/rev2/tools/gen-module-canvas.py b/hardware/rev2/tools/gen-module-canvas.py new file mode 100644 index 0000000..ce65191 --- /dev/null +++ b/hardware/rev2/tools/gen-module-canvas.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +"""Generate the stage-A module-interconnect views from the pin contract. + +Source of truth is hardware/rev2/HOWTO-module-pcb.md (§4 modules, §6 header +pinouts, §7 current paths). The MODULES block below mirrors those tables and +is CROSS-CHECKED against the §6 J101 table on every run: a mismatch aborts, +so this script can never quietly draw something the HOWTO does not say. + +Run: python3 hardware/rev2/tools/gen-module-canvas.py +Out: hardware/rev2/export/module-canvas/ (gitignored) + module-interconnect.html <- open in any browser (standalone preview) + Main.dc.html, NetMatrix.dc.html, canvas.json <- artboards for the + editable Claude Design canvas (published from a Claude Code session) + +No dependencies beyond Python 3. +""" +import html, os, re, sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +REV2 = os.path.dirname(HERE) +HOWTO = os.path.join(REV2, "HOWTO-module-pcb.md") +OUT = os.path.join(REV2, "export", "module-canvas") +os.makedirs(OUT, exist_ok=True) + +RAIL = ["VBAT", "GND", "+3V3", "GND", "SENS_12V", "GND"] # J100 fixed pinout +MODULES = [ # (name, blocks, J100 pins used, J101 nets, field connectors, notes) + ("mod-power", "block 1", {1,2,3,4,5,6}, + ["GND","SENS_GATE","VBAT_SENSE","PWR_PG","I2C_SDA","I2C_SCL","GND"], + [("J2","battery in","VBAT_IN · GND"),("J8","panel +","PANEL_IN · PANEL_OUT"),("J9","panel −","PANEL_RTN in · out (isolated)")], + "SOURCE of all rails · PWR_FLAGs live here"), + ("mod-mcu", "blocks 2+3+8", {3,4}, + ["GND","VBAT_SENSE","PWR_PG","SENS_PWR_EN","RS485_TX","RS485_RX","PUMP_EN","RESERVOIR_LOW_LEVEL","RESERVOIR_HIGH_LEVEL","I2C_SDA","I2C_SCL","GND"], + [("J1","USB-C","prog / power island"),("J6","JTAG 1×6","IO12–15 (DNP)"),("J7","expansion 1×8","IO18 SCK · 19 MISO · 23 MOSI · 4 CS · 27 IRQ")], + "U0TXD/U0RXD/EN/BOOT stay on-board"), + ("mod-rs485", "block 4", {1,2,3,4,5,6}, + ["GND","RS485_TX","RS485_RX","SENS_PWR_EN","SENS_GATE","GND"], + [("J4","NPK sensor 4-pin","A · B · SENS_12V · GND")], + "THVD1426 auto-direction · Q60 drives SENS_GATE"), + ("mod-pump", "block 5", {3,4}, + ["GND","PUMP_EN","I2C_SDA","I2C_SCL","GND"], + [("J3","pump out","PUMP_P · PUMP_N"),("—","VBAT/GND feed","screw terminal ← mod-power (≤8 A)")], + "VBAT NEVER via J100 pin 1 — current path"), + ("mod-level", "block 6", {3,4,5,6}, + ["GND","RESERVOIR_LOW_LEVEL","RESERVOIR_HIGH_LEVEL","GND"], + [("J5","XKC-Y26 LOW","VCC · OUT · GND · MODE"),("J10","XKC-Y26 HIGH","VCC · OUT · GND · MODE")], + "active-LOW via 2N7002 · JP1/JP2 MODE"), + ("mod-i2c-env","block 7", {3,4}, + ["GND","I2C_SDA","I2C_SCL","GND"], + [], + "owns the ONLY I²C pull-ups (R90/R91)"), +] + + +# ---------------- cross-check against HOWTO §6 (tables are the truth) --------- +def howto_j101(): + """Parse the '| Module | J101 pinout (pin 1 → n) |' table from the HOWTO.""" + text = open(HOWTO, encoding="utf-8").read() + m = re.search(r"\| Module \| J101 pinout[^\n]*\n\|[-| ]+\n((?:\|[^\n]*\n)+)", text) + if not m: + sys.exit("gen-module-canvas: could not find the J101 table in HOWTO-module-pcb.md §6") + table = {} + for line in m.group(1).strip().splitlines(): + cells = [c.strip() for c in line.strip().strip("|").split("|")] + name = cells[0].strip("`") + nets = [p.strip().strip("`") for p in cells[1].split("·")] + table[name] = nets + return table + +expected = howto_j101() +for name, _, _, j101, _, _ in MODULES: + if name not in expected: + sys.exit(f"gen-module-canvas: {name} missing from HOWTO §6 J101 table") + if expected[name] != j101: + sys.exit(f"gen-module-canvas: J101 mismatch for {name}\n HOWTO : {expected[name]}\n script: {j101}\nFix the script data to match the HOWTO (the HOWTO is the source of truth).") +extra = set(expected) - {m[0] for m in MODULES} +if extra: + sys.exit(f"gen-module-canvas: HOWTO lists modules the script does not know: {sorted(extra)}") + +# --- KiCad classic palette -------------------------------------------------- +C = dict(group="#0000c8", body="#840000", fill="#ffffc2", wire="#008400", + net="#101010", hier="#848400", muted="#8a8a8a", paper="#fdfdfa", warn="#b00020") +FONT = "'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif" +MONO = "'IBM Plex Mono', Menlo, Consolas, monospace" + +def pin_strip(ref, nets, used=None, title=""): + rows = [] + for i, net in enumerate(nets, 1): + on = (used is None) or (i in used) + col = C["net"] if on else C["muted"] + label = net if on else "n/c" + rows.append( + f'
| net | {ths}
|---|