Add routed PSU carrier board layout (rev 0.1)

- Placement script (layout_pcb.py): LV left, HV right, panel connector on the
  boundary, terminal blocks on the edges, heatsink space for the TO-220s
- Freerouting-based routing script (route_pcb.py) that routes with oversized
  tracks/vias and shrinks them, giving 3 mm HV-to-LV and 1.3 mm HV-to-HV
- Custom DRC rules (plasma_dmd_psu.kicad_dru) with documented exceptions
- Wide TO-92 footprints; schematic generator now keeps symbol UUIDs
- DRC: 0 violations, 0 unconnected pads, schematic parity OK

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-24 02:58:35 +02:00
1 parent dbc1f0041f
commit 28c666506e
11 files changed
+30816 -422

No files matched your search

+9
View File
@@ -17,3 +17,12 @@ __pycache__/
# KiCad generated reports
hardware/kicad/erc.rpt
*.net
# Local tools (Freerouting jar)
tools/
# KiCad local history
.history/
# Freerouting work files
hardware/kicad/route/
+3 -2
View File
@@ -20,7 +20,7 @@ Stern/Sega display PSU (520-5138-00). The original transformer and AC front end
|---|---|
| [docs/power-supply.md](docs/power-supply.md) | PSU design: block diagram, analysis of the original board, changes |
| [hardware/psu-schematic.svg](hardware/psu-schematic.svg) | PSU schematic (generated by [hardware/psu_schematic.py](hardware/psu_schematic.py)) |
| [hardware/kicad/](hardware/kicad/) | KiCad 10 project for the PSU carrier board (schematic + net classes, PCB not started) |
| [hardware/kicad/](hardware/kicad/) | KiCad 10 project for the PSU carrier board: schematic and routed PCB (rev 0.1) |
| [docs/safety.md](docs/safety.md) | Safety design (HV watchdog, fusing, layout, bring-up procedure) |
| [docs/esp32-interface.md](docs/esp32-interface.md) | ESP32 pin plan, 74HCT245 level shifter, panel signals |
| [docs/bom.md](docs/bom.md) | Bill of materials |
@@ -32,7 +32,8 @@ Stern/Sega display PSU (520-5138-00). The original transformer and AC front end
- [ ] Identify panel: manufacturer/part number from its label
- [ ] Map the panel's power connector pins (WPC panel ≠ Stern CN2 pinout) and check if +12 V is used
- [ ] Map the panel's 14-pin data connector
- [ ] Lay out the PSU PCB in KiCad ([hardware/kicad/](hardware/kicad/README.md))
- [x] Lay out the PSU PCB in KiCad ([hardware/kicad/](hardware/kicad/README.md))
- [ ] Review the PCB and work through the [before ordering](hardware/kicad/README.md#before-ordering) checklist
- [ ] Order parts ([docs/bom.md](docs/bom.md))
- [ ] Build and bench-test the PSU **without the panel** ([bring-up](docs/safety.md#bring-up-procedure))
- [ ] Build HV watchdog and verify that the relay drops when toggling stops
+57 -10
View File
@@ -42,24 +42,71 @@ stop using the generator once you start editing the schematic in KiCad.
| Class | Nets | Clearance | Track |
|---|---|---|---|
| HV | +90V_RAW, +65V, −130V_RAW, −112V, −100V, HVP/HVN_MOD and all regulator internals | **3.0 mm** | 0.6 mm |
| HV | +90V_RAW, +65V, −130V_RAW, −112V, −100V, HVP/HVN_MOD and all regulator internals | 1.3 mm (3.0 mm to LV via custom rules) | 0.6 mm |
| Power | GND, +12V*, +5V, +12V_PANEL | 0.3 mm | 1.0 mm |
| Default | Watchdog signals | 0.25 mm | 0.4 mm |
| Default | Watchdog signals, LED cathode/anode nodes | 0.25 mm | 0.4 mm |
## Making the PCB
## PCB (rev 0.1)
1. Open the PCB editor and choose **Tools → Update PCB from Schematic** (F8). All footprints arrive with ratsnest.
2. Draw the board outline on `Edge.Cuts`.
3. Placement tips:
- Keep the HV area (J5/J7, F4/F5, regulator, J2) on one side and the 12 V/5 V/watchdog on the other.
- Leave room for TO-220 heatsinks on Q3, Q5 and Q6. R13 (5 W) runs warm.
- Consider routed slots between the HV and low-voltage areas.
4. Run DRC. The HV class enforces 3 mm to everything.
![Top view](pcb-top.png)
The board is 170 × 125 mm, 2 layers, with 4 × M3 holes. It is fully routed with a bottom-layer GND pour.
DRC is clean: 0 violations, 0 unconnected pads, schematic parity OK.
- **Left: low voltage.** 12 V input, Mini560, ESP32 watchdog, relay, F1–F3.
- **Right: high voltage.** +65 V regulator at the top, −112/−100 V regulator at the bottom.
A dashed silkscreen line marks the boundary, with DANGER text on both sides.
- **J2 (panel)** straddles the boundary. Its LV pins (GND, +5V, +12V) leave towards the left, its HV pins towards the right.
- **Terminal blocks** sit on the top and bottom edges, with wire entries facing outwards.
- **Heatsink space** is left above Q3, Q5 and Q6.
> ⚠️ **The TO-220 tabs are live.** Q3's tab is at +90 V, Q5's at −130 V and Q6's at Q6_C.
> Give each transistor its own heatsink, keep the heatsinks apart, and don't let them touch anything.
### Clearance rules
The classes are in the project, and the custom rules are in `plasma_dmd_psu.kicad_dru`. Later rules win.
| Between | Clearance | Why |
|---|---|---|
| HV and LV (GND, 12 V, 5 V, watchdog) | **3.0 mm** | Keeps HV well away from the touchable side |
| HV and HV | 1.3 mm | IPC-2221 B2 needs 1.25 mm up to 300 V |
| HV and LV, next to a connector pin | 1.8 mm | Fixed pin pitch of J2/J5/J7 |
| HV and HV, next to a transistor pin | 1.0 mm | TO-220 pin escape; same-polarity nets only |
| Pins inside one connector / transistor / HV electrolytic | 0.75 / 0.6 / 2.5 mm | Fixed footprint pitch |
### Regenerating the board
The layout is produced by two scripts, run with KiCad's Python while KiCad is closed:
```bash
"C:/Users/patri/AppData/Local/Programs/KiCad/10.0/bin/python.exe" hardware/kicad/layout_pcb.py
"C:/Users/patri/AppData/Local/Programs/KiCad/10.0/bin/python.exe" hardware/kicad/route_pcb.py
```
- `layout_pcb.py` places every footprint from its `PLACE` table and draws the outline, holes and silkscreen.
It also swaps in any footprint that changed in the schematic. **It deletes all tracks and zones.**
- `route_pcb.py` autoroutes with Freerouting (`tools/freerouting/`, needs Java 25):
- It routes with oversized tracks and vias, then shrinks them to their real width. This turns
the router's smaller clearances into 3 mm for HV to LV and 1.3 mm for HV to HV.
- It adds fixed escape stubs on J2 and the GND pour.
- Run DRC afterwards: `kicad-cli pcb drc --schematic-parity --refill-zones hardware/kicad/plasma_dmd_psu.kicad_pcb`.
Once you start hand-editing the board in KiCad, stop running these scripts. They would overwrite your edits.
### Before ordering
- [ ] Check every footprint against the parts you actually buy: fuse holders, the 12.5 mm caps, the R13 5 W size, and the terminal block pitch.
- [ ] Map J2 to your panel's power connector. The current order follows the Stern CN2 connector.
- [ ] Check heatsink size and clearance for Q3/Q5/Q6.
- [ ] Consider conformal coating on the HV side.
- [ ] Get a second pair of eyes on the HV layout.
## Verified so far
- The schematic loads in KiCad 10.0.6, and ERC reports 0 violations.
- `check_netlist.py`: all 122 pins sit on the intended nets (31 nets).
- PCB DRC with schematic parity: 0 violations, 0 unconnected pads.
- All symbols and footprints come from the KiCad 10 standard libraries.
- Transistor pinouts match the footprints: MJE15030/31 are B-C-E (TO-220); MPSA42/92 are E-B-C (TO-92); 2N7000 is S-G-D.
+25 -6
View File
@@ -23,7 +23,7 @@ FP_D35 = "Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal"
FP_D41 = "Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal"
FP_D15 = "Diode_THT:D_DO-15_P12.70mm_Horizontal"
FP_LED = "LED_THT:LED_D5.0mm"
FP_TO92 = "Package_TO_SOT_THT:TO-92_Inline"
FP_TO92 = "Package_TO_SOT_THT:TO-92_Inline_Wide" # 2.54 mm pitch for HV clearance
FP_TO220 = "Package_TO_SOT_THT:TO-220-3_Vertical"
FP_FUSE = "Fuse:Fuseholder_Cylinder-5x20mm_Schurter_0031_8201_Horizontal_Open"
FP_CFILM = "Capacitor_THT:C_Rect_L18.0mm_W5.0mm_P15.00mm_FKS3_FKP3"
@@ -213,8 +213,9 @@ def snap(v):
return round(round(v / 1.27) * 1.27, 2)
HV_NETS = ["+90V_RAW", "+65V", "HVP_MOD", "Q2_E", "REF_P", "Q3_B", "N_LED1",
"-130V_RAW", "-112V", "-100V", "HVN_MOD", "Q4_E", "REF_N", "Q5_B", "REF_M", "Q6_B", "Q6_C", "N_LED2"]
# N_LED1/N_LED2 sit a LED drop away from GND, so they stay in Default.
HV_NETS = ["+90V_RAW", "+65V", "HVP_MOD", "Q2_E", "REF_P", "Q3_B",
"-130V_RAW", "-112V", "-100V", "HVN_MOD", "Q4_E", "REF_N", "Q5_B", "REF_M", "Q6_B", "Q6_C"]
POWER_NETS = ["GND", "+12V", "+12V_IN", "+12V_HV", "+12V_HVP", "+12V_HVN", "+5V", "+12V_PANEL"]
@@ -236,7 +237,7 @@ def write_project():
"classes": [
netclass("Default", 0.25, 0.4, 2147483647),
netclass("Power", 0.3, 1.0, 1),
netclass("HV", 3.0, 0.6, 0),
netclass("HV", 1.3, 0.6, 0) # HV <-> LV 3 mm comes from plasma_dmd_psu.kicad_dru,
],
"meta": {"version": 5},
"net_colors": None,
@@ -247,8 +248,26 @@ def write_project():
pro.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
def existing_uuids():
path = HERE / f"{PROJECT}.kicad_sch"
if not path.exists():
return None, {}
text = path.read_text(encoding="utf-8")
root = re.search(r'^\(kicad_sch.*?\(uuid "([^"]+)"\)', text, re.S).group(1)
syms = {}
for m in re.finditer(r'^ \(symbol \(lib_id', text, re.M):
blk = read_sexpr_block(text, m.start() + 2)
u = re.search(r'\(dnp no\) \(uuid "([^"]+)"\)', blk)
r = re.search(r'\(property "Reference" "([^"]+)"', blk)
if u and r:
syms[r.group(1)] = u.group(1)
return root, syms
def main():
root = uid()
# Reuse UUIDs from an existing schematic so the PCB keeps its links to the symbols.
old_root, old_sym = existing_uuids()
root = old_root or uid()
lib_blocks, pin_cache = {}, {}
for p in PARTS:
if p[1] not in lib_blocks:
@@ -273,7 +292,7 @@ def main():
rx, ry, vx, vy, just = snap(x + 5.08), snap(y - 1.27), snap(x + 5.08), snap(y + 1.27), " (justify left)"
pin_uuid = "\n".join(f' (pin {q(num)} (uuid {q(uid())}))' for num, *_ in pins)
items.append(f""" (symbol (lib_id {q(lib_id)}) (at {x} {y} 0) (unit 1)
(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (uuid {q(uid())})
(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (uuid {q(old_sym.get(ref) or uid())})
(property "Reference" {q(ref)} (at {rx} {ry} 0) (effects (font (size 1.27 1.27)){just}))
(property "Value" {q(value)} (at {vx} {vy} 0) (effects (font (size 1.27 1.27)){just}))
(property "Footprint" {q(fp)} (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))
+225
View File
@@ -0,0 +1,225 @@
"""Place footprints, draw the outline and silkscreen for the PSU carrier board.
Run with KiCad's bundled Python (KiCad must not have the board open):
"C:/Users/patri/AppData/Local/Programs/KiCad/10.0/bin/python.exe" hardware/kicad/layout_pcb.py
Coordinates below are board-local mm (origin = top-left corner of the board, y down).
Positions are the footprint origin (pad 1). Axial parts run left to right, pad 1 on the left.
"""
from pathlib import Path
import pcbnew
HERE = Path(__file__).parent
BOARD = HERE / "plasma_dmd_psu.kicad_pcb"
OX, OY = 100.0, 50.0 # board origin on the page
W, H = 170.0, 125.0 # board size
HV_X = 61.0 # LV | HV boundary (silkscreen)
# ref: (x, y, rotation_deg)
PLACE = {
# Phoenix MKDS terminals: wire entry is on the footprint's +y side, so top-edge blocks are
# rotated 180 (entry up) and bottom-edge blocks stay at 0 (entry down).
# ---- low-voltage side (left) -------------------------------------------------
"J1": (16.08, 6.5, 180), # 12 V in
"J3": (39.24, 6.5, 180), # Mini560
"J8": (57.16, 6.5, 180), # ESP32
"F1": (8.0, 22.0, 0),
"D1": (8.0, 31.0, 0),
"C1": (28.0, 35.0, 0),
"C2": (40.0, 22.0, 0),
"C3": (37.0, 34.0, 0),
"D3": (37.0, 41.0, 0),
"D2": (37.0, 47.0, 0),
"C4": (50.0, 41.0, 0),
"R1": (37.0, 53.0, 0),
"Q1": (44.0, 60.0, 0),
"JP1": (50.0, 72.0, 0),
"F2": (8.0, 76.0, 0),
"F3": (8.0, 90.0, 0),
"J4": (11.0, 118.5, 0), # relay module
"J6": (40.0, 118.5, 0), # isolated DC-DC input
# ---- positive HV (top right) -------------------------------------------------
"J5": (76.16, 6.5, 180), # NCH6100HV #1
"F4": (66.0, 20.0, 0),
"C5": (96.0, 20.0, 0),
"R2": (110.0, 20.0, 0),
"R3": (127.0, 20.0, 0),
"LED1": (148.0, 20.0, 0),
"D4": (68.0, 32.0, 0),
"R5": (80.0, 32.0, 0),
"R4": (102.0, 32.0, 0),
"Q2": (117.0, 33.0, 0),
"D5": (127.0, 32.0, 0),
"Q3": (81.0, 52.0, 0), # heatsink space above (tab side)
"C6": (98.0, 50.0, 0),
"R6": (119.0, 50.0, 0),
# ---- panel connector: on the LV/HV boundary, LV pins 4-7 face left, HV pins 1,2,8 right
"J2": (61.0, 78.0, 90),
# ---- negative HV (bottom right) ----------------------------------------------
"Q5": (81.0, 70.0, 0), # heatsink space above (tab side)
"Q6": (99.0, 70.0, 0),
"R13": (114.0, 62.0, 0), # 5 W, runs warm: keep clear of caps
"D7": (74.0, 80.5, 0),
"D8": (90.0, 79.0, 0),
"R12": (106.0, 79.0, 0),
"C8": (122.0, 79.0, 0),
"D6": (68.0, 89.0, 0),
"R10": (82.0, 89.0, 0),
"R9": (105.0, 89.0, 0),
"Q4": (120.0, 90.0, 0),
"R11": (130.0, 89.0, 0),
"C9": (68.0, 99.0, 0),
"C7": (92.0, 101.0, 0),
"R7": (106.0, 99.0, 0),
"R8": (122.0, 99.0, 0),
"LED2": (144.0, 99.0, 0),
"F5": (120.0, 111.0, 0),
"J7": (70.0, 118.5, 0), # NCH6100HV #2 output
# ---- mounting holes ------------------------------------------------------------
"H1": (4.0, 4.0, 0),
"H2": (W - 4.0, 4.0, 0),
"H3": (4.0, H - 4.0, 0),
"H4": (W - 4.0, H - 4.0, 0),
}
SILK = [
# (text, x, y, size, layer)
("PLASMA DMD PSU rev 0.1", 24.0, 102.0, 1.5, "F.SilkS"),
("J1 12V IN", 13.5, 13.6, 1.0, "F.SilkS"),
("J3 MINI560", 31.6, 13.6, 1.0, "F.SilkS"),
("J8 ESP32", 52.1, 13.6, 1.0, "F.SilkS"),
("J5 HV MOD 1", 71.1, 13.6, 1.0, "F.SilkS"),
("J4 RELAY MODULE", 21.2, 111.2, 1.0, "F.SilkS"),
("J6 ISO DC-DC IN", 41.5, 111.2, 1.0, "F.SilkS"),
("J7 HV MOD 2 OUT", 72.5, 111.2, 1.0, "F.SilkS"),
("J2 PANEL", 61.0, 44.0, 1.0, "F.SilkS"),
("DANGER", 158.0, 38.0, 2.0, "F.SilkS"),
("HIGH VOLTAGE", 158.0, 42.0, 1.6, "F.SilkS"),
("up to 220V", 158.0, 45.5, 1.2, "F.SilkS"),
("DANGER HIGH VOLTAGE", 152.0, 70.0, 1.6, "B.SilkS"),
]
def mm(v):
return pcbnew.FromMM(v)
def pt(x, y):
return pcbnew.VECTOR2I(mm(OX + x), mm(OY + y))
FP_DIR = Path(pcbnew.__file__).parents[3] / "share/kicad/footprints" # <KiCad>/10.0/share/...
def schematic_footprints():
"""ref -> footprint lib id, read from the schematic."""
import re
text = (HERE / "plasma_dmd_psu.kicad_sch").read_text(encoding="utf-8")
out = {}
for m in re.finditer(r'\(property "Reference" "([^"]+)".*?\(property "Footprint" "([^"]*)"', text, re.S):
out[m.group(1)] = m.group(2)
return out
def sync_footprints(board):
"""Swap footprints whose library id no longer matches the schematic (keeps ref, value, nets, link)."""
wanted = schematic_footprints()
for old in list(board.GetFootprints()):
ref = old.GetReference()
fpid = wanted.get(ref)
if not fpid or fpid == old.GetFPIDAsString():
continue
lib, name = fpid.split(":")
new = pcbnew.FootprintLoad(str(FP_DIR / f"{lib}.pretty"), name)
new.SetFPID(pcbnew.LIB_ID(lib, name))
new.SetReference(ref)
new.SetValue(old.GetValue())
new.SetPath(old.GetPath())
new.SetSheetname(old.GetSheetname())
new.SetSheetfile(old.GetSheetfile())
nets = {p.GetNumber(): p.GetNet() for p in old.Pads()}
board.Add(new)
for p in new.Pads():
if p.GetNumber() in nets:
p.SetNet(nets[p.GetNumber()])
new.SetPosition(old.GetPosition())
board.Delete(old)
print("swapped", ref, "->", fpid)
def main():
board = pcbnew.LoadBoard(str(BOARD))
sync_footprints(board)
# clean previous layout output (outline, our silkscreen, tracks, vias, zones)
for d in list(board.GetDrawings()):
if d.GetLayer() in (pcbnew.Edge_Cuts, pcbnew.F_SilkS, pcbnew.B_SilkS):
board.Delete(d)
for getter in (board.GetTracks, board.Zones):
try:
items = list(getter())
except TypeError: # SWIG returns a bare object when the list is empty
items = []
for it in items:
board.Delete(it)
fps = {f.GetReference(): f for f in board.GetFootprints()}
missing = set(fps) - set(PLACE)
extra = set(PLACE) - set(fps)
if missing or extra:
raise SystemExit(f"placement table mismatch: missing {missing}, unknown {extra}")
for ref, (x, y, rot) in PLACE.items():
f = fps[ref]
if f.IsFlipped():
f.Flip(f.GetPosition(), False)
f.SetOrientationDegrees(rot)
f.SetPosition(pt(x, y))
f.SetLocked(ref.startswith(("J", "H")))
if ref.startswith(("J", "H")):
f.Reference().SetVisible(False) # connectors are labelled by the SILK texts
# board outline with 2 mm corner radius
r = 2.0
def seg(x1, y1, x2, y2):
s = pcbnew.PCB_SHAPE(board, pcbnew.SHAPE_T_SEGMENT)
s.SetStart(pt(x1, y1)); s.SetEnd(pt(x2, y2))
s.SetLayer(pcbnew.Edge_Cuts); s.SetWidth(mm(0.1)); board.Add(s)
def arc(cx, cy, sx, sy, ang):
a = pcbnew.PCB_SHAPE(board, pcbnew.SHAPE_T_ARC)
a.SetCenter(pt(cx, cy)); a.SetStart(pt(sx, sy)); a.SetArcAngleAndEnd(pcbnew.EDA_ANGLE(ang, pcbnew.DEGREES_T))
a.SetLayer(pcbnew.Edge_Cuts); a.SetWidth(mm(0.1)); board.Add(a)
seg(r, 0, W - r, 0); seg(W, r, W, H - r); seg(W - r, H, r, H); seg(0, H - r, 0, r)
arc(r, r, 0, r, 90); arc(W - r, r, W - r, 0, 90); arc(W - r, H - r, W, H - r, 90); arc(r, H - r, r, H, 90)
# HV boundary: dashed line on silkscreen (gap where the panel connector J2 straddles it)
y = 15.0
while y < H - 3:
if 42.0 < y < 84.0:
y += 3.5
continue
s = pcbnew.PCB_SHAPE(board, pcbnew.SHAPE_T_SEGMENT)
s.SetStart(pt(HV_X, y)); s.SetEnd(pt(HV_X, min(y + 2.0, H - 3)))
s.SetLayer(pcbnew.F_SilkS); s.SetWidth(mm(0.3)); board.Add(s)
y += 3.5
for text, x, ty, size, layer in SILK:
t = pcbnew.PCB_TEXT(board)
t.SetText(text); t.SetPosition(pt(x, ty))
t.SetTextSize(pcbnew.VECTOR2I(mm(size), mm(size))); t.SetTextThickness(mm(size * 0.15))
t.SetLayer(board.GetLayerID(layer))
if layer.startswith("B."):
t.SetMirrored(True)
board.Add(t)
board.Save(str(BOARD))
# report pad positions of rotated connectors so orientation can be checked
for ref in ("J2", "J4", "J6", "J7"):
f = fps[ref]
print(ref, [(p.GetNumber(), round(pcbnew.ToMM(p.GetPosition().x) - OX, 2),
round(pcbnew.ToMM(p.GetPosition().y) - OY, 2)) for p in f.Pads()])
print("placed", len(PLACE), "footprints")
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 941 KiB

+35
View File
@@ -0,0 +1,35 @@
(version 1)
# Clearance policy for the PSU carrier board (later rules take priority).
# IPC-2221 B2 (external, uncoated) needs 1.25 mm up to 300 V; we add margin:
# - HV <-> anything else: 3.0 mm (keeps HV well away from the 12 V / 5 V / ESP32 side)
# - HV <-> HV: 1.3 mm (netclass default; up to about 220 V between +90 V and -130 V)
# - HV <-> LV next to a connector pin: 1.8 mm (tracks leaving J2/J5/J7 pins that sit next to other-class pins)
# - inside connectors J*: 0.75 mm (fixed pin pitch; adjacent HV/LV pins differ by <= 53 V at J2, J5/J7 have 2.4 mm)
# - inside the HV electrolytics: 2.5 mm (5 mm pin pitch, 90/130 V)
# - HV <-> HV next to a transistor pin: 1.0 mm (TO-220 pin escape; each transistor only has same-polarity nets around it)
# - inside transistors Q*: 0.6 mm (TO-92 / TO-220 pitch; pins of one transistor differ by < 100 V)
(rule "HV to low voltage"
(condition "A.hasNetclass('HV') && !B.hasNetclass('HV')")
(constraint clearance (min 3.0mm)))
(rule "HV next to connector pins"
(condition "A.hasNetclass('HV') && !B.hasNetclass('HV') && (A.memberOfFootprint('J*') || B.memberOfFootprint('J*'))")
(constraint clearance (min 1.8mm)))
(rule "HV next to transistor pins"
(condition "A.hasNetclass('HV') && B.hasNetclass('HV') && (A.memberOfFootprint('Q*') || B.memberOfFootprint('Q*'))")
(constraint clearance (min 1.0mm)))
(rule "HV inside connectors"
(condition "A.hasNetclass('HV') && A.memberOfFootprint('J*') && B.memberOfFootprint('J*')")
(constraint clearance (min 0.75mm)))
(rule "HV inside transistors"
(condition "A.hasNetclass('HV') && B.hasNetclass('HV') && A.memberOfFootprint('Q*') && B.memberOfFootprint('Q*')")
(constraint clearance (min 0.6mm)))
(rule "HV electrolytic pins"
(condition "(A.memberOfFootprint('C5') && B.memberOfFootprint('C5')) || (A.memberOfFootprint('C7') && B.memberOfFootprint('C7'))")
(constraint clearance (min 2.5mm)))
File diff suppressed because it is too large. Load diff
+659 -27
View File
@@ -1,4 +1,492 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_barcodes": false,
"apply_defaults_to_fp_dimensions": false,
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.8,
"height": 1.27,
"width": 1.27
},
"silk_line_width": 0.1,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.1,
"silk_text_upright": false,
"zones": {
"border_display_style": 2,
"border_hatch_pitch": 0.5,
"corner_radius": 0.0,
"corner_smoothing": 0,
"fill_mode": 0,
"hatch_gap": 1.5,
"hatch_orientation": 0.0,
"hatch_smoothing_level": 0,
"hatch_smoothing_value": 0.1,
"hatch_thickness": 1.0,
"min_clearance": 0.5,
"min_island_area": 10.0,
"min_thickness": 0.25,
"pad_connection": 1,
"remove_islands": 0,
"thermal_relief_gap": 0.5,
"thermal_relief_spoke_width": 0.5
}
},
"diff_pair_dimensions": [],
"drc_exclusions": [],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_field_mismatch": "warning",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_to_hole": "warning",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"missing_tuning_profile": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "error",
"padstack": "warning",
"pth_inside_courtyard": "error",
"shorting_items": "error",
"silk_edge_clearance": "warning",
"silk_over_copper": "warning",
"silk_overlap": "warning",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "error",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_not_centered_on_via": "ignore",
"track_on_post_machined_layer": "error",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"tuning_profile_track_geometries": "ignore",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.0,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.5,
"min_groove_width": 0.0,
"min_hole_clearance": 0.25,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.3,
"min_track_width": 0.2,
"min_via_annular_width": 0.1,
"min_via_diameter": 0.5,
"solder_mask_to_copper_clearance": 0.0,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [],
"zones_allow_external_fillets": false
},
"ipc2581": {
"bom_rev": "",
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": "",
"sch_revision": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"component_class_settings": {
"assignments": [],
"meta": {
"version": 0
},
"sheet_component_classes": {
"enabled": false
}
},
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"field_name_whitespace": "warning",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"ground_pin_not_ground": "warning",
"hier_label_mismatch": "error",
"isolated_pin_label": "warning",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"stacked_pin_name": "warning",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "plasma_dmd_psu.kicad_pro",
"version": 3
@@ -24,6 +512,25 @@
"via_drill": 0.5,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 1.3,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "HV",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 0,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.6,
"tuning_profile": "",
"via_diameter": 1.0,
"via_drill": 0.5,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 0.3,
@@ -42,25 +549,6 @@
"via_diameter": 1.0,
"via_drill": 0.5,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 3.0,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "HV",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 0,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.6,
"tuning_profile": "",
"via_diameter": 1.0,
"via_drill": 0.5,
"wire_width": 6
}
],
"meta": {
@@ -93,10 +581,6 @@
"netclass": "HV",
"pattern": "/Q3_B"
},
{
"netclass": "HV",
"pattern": "/N_LED1"
},
{
"netclass": "HV",
"pattern": "/-130V_RAW"
@@ -137,10 +621,6 @@
"netclass": "HV",
"pattern": "/Q6_C"
},
{
"netclass": "HV",
"pattern": "/N_LED2"
},
{
"netclass": "Power",
"pattern": "/GND"
@@ -174,5 +654,157 @@
"pattern": "/+12V_PANEL"
}
]
},
"pcbnew": {
"last_paths": {
"idf": "",
"netlist": "",
"plot": "",
"specctra_dsn": "",
"step": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"annotation": {
"method": 0,
"sort_order": 0
},
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Simulation",
"name": "${EXCLUDE_FROM_SIM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Position Files",
"name": "${EXCLUDE_FROM_POS_FILES}",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "Default Editing",
"sort_asc": true,
"sort_field": "Reference"
},
"bus_aliases": {},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"hop_over_size_choice": 0,
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"page_layout_descr_file": "",
"plot_directory": "",
"reuse_designators": true,
"subpart_first_id": 65,
"subpart_id_separator": 0,
"top_level_sheets": [
{
"filename": "plasma_dmd_psu.kicad_sch",
"name": "plasma_dmd_psu",
"uuid": "00000000-0000-0000-0000-000000000000"
}
],
"used_designators": "",
"variants": []
},
"sheets": [],
"text_variables": {},
"tuning_profiles": {
"meta": {
"version": 0
},
"tuning_profiles_impedance_geometric": []
}
}
File diff suppressed because it is too large. Load diff
+210
View File
@@ -0,0 +1,210 @@
"""Autoroute the PSU carrier board with Freerouting.
Run with KiCad's bundled Python (KiCad must not have the board open):
"C:/Users/patri/AppData/Local/Programs/KiCad/10.0/bin/python.exe" hardware/kicad/route_pcb.py
Exports a Specctra DSN, adds class-to-class clearance (HV <-> everything else 3 mm, matching
plasma_dmd_psu.kicad_dru, which Freerouting does not read), routes, and imports the session.
Existing tracks are removed first, so the result always comes from the current placement.
"""
import re
import subprocess
import sys
from pathlib import Path
import pcbnew
HERE = Path(__file__).parent
ROOT = HERE.parents[1]
BOARD = HERE / "plasma_dmd_psu.kicad_pcb"
WORK = HERE / "route"
JAVA = Path("C:/Program Files/Eclipse Adoptium/jre-25.0.4.101-hotspot/bin/java.exe")
JAR = ROOT / "tools/freerouting/freerouting-2.5.0.jar"
PASSES = int(sys.argv[1]) if len(sys.argv) > 1 else 100
# Route with oversized tracks, then shrink them to the real width: every track then keeps
# (routing clearance + half the width difference) from its neighbours. This gets 3 mm HV <-> LV
# and 1.3 mm HV <-> HV while tracks can still squeeze out between transistor pins.
# class: (routing width um, routing clearance um, final width mm)
ROUTE = {"kicad_default": (1800, 250, 0.4), "Power": (2400, 300, 1.0), "HV": (2100, 650, 0.6)}
VIA_ROUTE_UM, VIA_FINAL_MM, VIA_DRILL_MM = 2300, 1.0, 0.5 # same trick for vias
HV_TO_LV_UM = 2450 # + (2.1 - 0.6) / 2 after shrinking = 3.2 mm
def add_gnd_plane(board):
"""Bottom-layer GND pour over the whole board (custom rules keep it 3 mm from HV)."""
for z in list(board.Zones()):
board.Delete(z)
bb = board.GetBoardEdgesBoundingBox()
inset = pcbnew.FromMM(0.5)
x0, y0 = bb.GetX() + inset, bb.GetY() + inset
x1, y1 = bb.GetRight() - inset, bb.GetBottom() - inset
z = pcbnew.ZONE(board)
z.SetLayer(pcbnew.B_Cu)
z.SetNet(board.FindNet("/GND"))
z.SetLocalClearance(pcbnew.FromMM(0.5))
z.SetMinThickness(pcbnew.FromMM(0.5))
z.SetPadConnection(pcbnew.ZONE_CONNECTION_THERMAL)
z.SetThermalReliefGap(pcbnew.FromMM(0.5))
z.SetThermalReliefSpokeWidth(pcbnew.FromMM(0.8))
ol = z.Outline()
ol.NewOutline()
for x, y in ((x0, y0), (x1, y0), (x1, y1), (x0, y1)):
ol.Append(x, y)
board.Add(z)
def add_connector_stubs(board, ref="J2", length=5.0):
"""Fixed escape stubs on the panel connector: LV pins exit left, HV pins exit right.
Its 3.96 mm pitch is tighter than the router's HV <-> LV clearance, so the router could not
leave those pins by itself. GND pins connect to the bottom plane and need no stub.
"""
fp = board.FindFootprintByReference(ref)
# final widths: a router track must end on the stub copper itself, which still holds after shrinking
widths = {"HV": ROUTE["HV"][2], "Power": ROUTE["Power"][2]}
for pad in fp.Pads():
net = pad.GetNet()
name = net.GetNetname() if net else ""
if not name or name == "/GND" or name.startswith("unconnected"):
continue
cls = pad.GetNetClassName()
sign = 1 if cls == "HV" else -1
p = pad.GetPosition()
t = pcbnew.PCB_TRACK(board)
t.SetStart(p)
t.SetEnd(pcbnew.VECTOR2I(p.x + sign * pcbnew.FromMM(length), p.y))
t.SetLayer(pcbnew.F_Cu)
t.SetWidth(pcbnew.FromMM(widths.get(cls, ROUTE["kicad_default"][2])))
t.SetNet(net)
t.SetLocked(True)
board.Add(t)
def unconnected(board):
board.BuildConnectivity()
return board.GetConnectivity().GetUnconnectedCount(False)
def remove_dangling_stubs(board):
"""Drop duplicate escape stubs (the SES brings them back), then delete each stub whose
removal does not leave anything unconnected (the router may join a stub anywhere along it)."""
seen = set()
for t in [t for t in board.GetTracks() if t.Type() == pcbnew.PCB_TRACE_T and t.IsLocked()]:
key = (t.GetNetCode(), t.GetLayer(), t.GetStart().x, t.GetStart().y, t.GetEnd().x, t.GetEnd().y)
if key in seen:
board.Delete(t)
seen.add(key)
base = unconnected(board)
for t in [t for t in board.GetTracks() if t.Type() == pcbnew.PCB_TRACE_T and t.IsLocked()]:
s, e = t.GetStart(), t.GetEnd() # copy values: t is freed by Delete
keep = (s.x, s.y, e.x, e.y, t.GetLayer(), t.GetWidth(), t.GetNetCode())
board.Delete(t)
if unconnected(board) > base: # it was needed: put it back
n = pcbnew.PCB_TRACK(board)
n.SetStart(pcbnew.VECTOR2I(keep[0], keep[1])); n.SetEnd(pcbnew.VECTOR2I(keep[2], keep[3]))
n.SetLayer(keep[4]); n.SetWidth(keep[5])
board.Add(n)
n.SetNet(board.FindNet(keep[6])); n.SetLocked(True)
def patch_via_padstacks(text):
"""Enlarge the via pads in the DSN library (vias are set back to their real size after import)."""
out = []
in_via = False
for line in text.splitlines(keepends=True):
if line.lstrip().startswith('(padstack "Via'):
in_via = True
elif in_via and line.lstrip().startswith("(shape (circle"):
line = re.sub(r"(\(circle \S+ )[\d.]+", lambda m: m.group(1) + str(VIA_ROUTE_UM), line)
elif in_via and line.strip() == ")":
in_via = False
out.append(line)
return "".join(out)
def patch_class_rules(text):
"""Replace each class's width/clearance in the DSN with the routing values."""
out, pos = [], 0
for m in re.finditer(r"\(class (\S+) ", text):
name = m.group(1)
start = m.start()
rule = text.index("(rule", start)
end = text.index(")", text.index("(clearance", rule)) + 1
width, clear, _ = ROUTE[name]
block = text[rule:end]
block = re.sub(r"\(width [\d.]+\)", f"(width {width})", block)
block = re.sub(r"\(clearance [\d.]+\)", f"(clearance {clear})", block, count=1)
out.append(text[pos:rule] + block)
pos = end
out.append(text[pos:])
return "".join(out)
def shrink_tracks(board):
final = {("Default" if k == "kicad_default" else k): pcbnew.FromMM(v[2]) for k, v in ROUTE.items()}
for t in board.GetTracks():
if t.Type() == pcbnew.PCB_VIA_T:
t.SetWidth(pcbnew.FromMM(VIA_FINAL_MM))
t.SetDrill(pcbnew.FromMM(VIA_DRILL_MM))
elif t.Type() == pcbnew.PCB_TRACE_T:
name = t.GetNetClassName()
t.SetWidth(final.get(name, final["Default"]))
def main():
WORK.mkdir(exist_ok=True)
dsn, ses = WORK / "board.dsn", WORK / "board.ses"
board = pcbnew.LoadBoard(str(BOARD))
try:
tracks = list(board.GetTracks())
except TypeError:
tracks = []
for t in tracks:
board.Delete(t)
for z in list(board.Zones()):
board.Delete(z) # GND is routed as tracks; the pour is added afterwards
add_connector_stubs(board)
if not pcbnew.ExportSpecctraDSN(board, str(dsn)):
raise SystemExit("DSN export failed")
text = dsn.read_text(encoding="utf-8")
classes = re.findall(r"\(class (\S+) ", text)
text = patch_class_rules(text)
text = patch_via_padstacks(text)
extra = "".join(
f" (class_class (classes HV {c})\n (rule (clearance {HV_TO_LV_UM}))\n )\n"
for c in classes if c != "HV")
# insert after the last (class ...) block inside (network ...)
last = text.rfind(" (class ")
depth, i = 0, last
while True:
if text[i] == "(":
depth += 1
elif text[i] == ")":
depth -= 1
if depth == 0:
break
i += 1
text = text[:i + 1] + "\n" + extra + text[i + 1:]
dsn.write_text(text, encoding="utf-8")
ses.unlink(missing_ok=True)
subprocess.run([str(JAVA), "-jar", str(JAR), "-de", str(dsn), "-do", str(ses),
"-mp", str(PASSES), "-l", "en"], check=True, cwd=WORK) # logs stay in route/
if not ses.exists():
raise SystemExit("Freerouting produced no session file")
if not pcbnew.ImportSpecctraSES(board, str(ses)):
raise SystemExit("SES import failed")
shrink_tracks(board)
remove_dangling_stubs(board)
add_gnd_plane(board)
pcbnew.ZONE_FILLER(board).Fill(board.Zones())
board.Save(str(BOARD))
print("routed and saved", BOARD.name)
if __name__ == "__main__":
main()