many eda additions
This commit is contained in:
64
tools/install-kicad-toolkit
Executable file
64
tools/install-kicad-toolkit
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# install-kicad-toolkit — install circuit_toolkit into KiCad's bundled Python
|
||||
# so pcbnew-dependent builders (pcb.py) can import it.
|
||||
#
|
||||
# Usage: ./install-kicad-toolkit.sh
|
||||
#
|
||||
# This is a one-time setup. After KiCad updates, re-run to pick up changes.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
TOOLKIT_SRC="$PROJECT_ROOT/kicad-claude-toolkit/python/circuit_toolkit"
|
||||
|
||||
# Find KiCad Python
|
||||
KICAD_APP="/Applications/KiCad/KiCad.app"
|
||||
KICAD_PYTHON="${KICAD_APP}/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3"
|
||||
|
||||
if [ ! -x "$KICAD_PYTHON" ]; then
|
||||
echo "ERROR: KiCad Python not found at $KICAD_PYTHON" >&2
|
||||
echo "Install KiCad from kicad.org, then re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "KiCad Python: $KICAD_PYTHON"
|
||||
echo "circuit_toolkit source: $TOOLKIT_SRC"
|
||||
echo ""
|
||||
|
||||
# Verify pcbnew works
|
||||
echo "Checking pcbnew..."
|
||||
if ! "$KICAD_PYTHON" -c "import pcbnew" 2>/dev/null; then
|
||||
echo "ERROR: pcbnew not importable. Run KiCad.app at least once." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " pcbnew OK"
|
||||
|
||||
# Install circuit_toolkit in development mode
|
||||
echo ""
|
||||
echo "Installing circuit_toolkit (editable)..."
|
||||
pushd "$TOOLKIT_SRC" >/dev/null
|
||||
# KiCad ships Python 3.9; toolkit pyproject requires >=3.10. Use --no-build-isolation
|
||||
# to bypass the version check (the code only needs 3.9 features).
|
||||
"$KICAD_PYTHON" -m pip install --no-build-isolation -e . 2>&1
|
||||
echo " installed"
|
||||
|
||||
# Also install optional sim deps
|
||||
echo ""
|
||||
echo "Installing optional sim deps (matplotlib, numpy)..."
|
||||
"$KICAD_PYTHON" -m pip install matplotlib numpy 2>&1 || echo " (non-fatal: sim deps skipped)"
|
||||
|
||||
# Verify
|
||||
echo ""
|
||||
echo "Verifying..."
|
||||
"$KICAD_PYTHON" -c "
|
||||
import pcbnew
|
||||
import circuit_toolkit
|
||||
from circuit_toolkit.core.board import Board
|
||||
from circuit_toolkit.builders.pcb import build_pcb
|
||||
print(f' circuit_toolkit {circuit_toolkit.__version__} OK (pcbnew {pcbnew.__file__})')
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "Done. circuit_toolkit is now available inside KiCad's Python."
|
||||
echo "Use ./tools/kicad-python to run scripts with pcbnew access."
|
||||
256
tools/kicad-build
Executable file
256
tools/kicad-build
Executable file
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env bash
|
||||
# kicad-build — orchestrator for circuit_toolkit + kicad-cli.
|
||||
#
|
||||
# Usage:
|
||||
# ./kicad-build <board-dir> [--erc] [--drc] [--gerber] [--bom] [--schematic] [--sim] [--all]
|
||||
#
|
||||
# Steps (all are optional, --all runs everything):
|
||||
# 1. circuit_toolkit build (generates .kicad_sch + .kicad_pcb)
|
||||
# 2. kicad-cli sch erc (electrical rules check)
|
||||
# 3. kicad-cli pcb drc (design rules check)
|
||||
# 4. kicad-cli pcb export gerber/drill/position
|
||||
# 5. kicad-cli sch export bom/pdf/netlist
|
||||
# 6. SPICE simulation (if sim/ present)
|
||||
#
|
||||
# Requires:
|
||||
# - KiCad.app installed (for pcbnew)
|
||||
# - kicad-cli on PATH (homebrew: brew install kicad)
|
||||
# - ngspice on PATH (for --sim, brew install ngspice)
|
||||
# - netlistsvg on PATH (for --schematic SVG, npm install -g netlistsvg)
|
||||
# - circuit_toolkit installed into KiCad's Python (run install-kicad-toolkit.sh)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
KICAD_PYTHON="$SCRIPT_DIR/kicad-python"
|
||||
KICAD_CLI=$(command -v kicad-cli 2>/dev/null || echo "")
|
||||
|
||||
# circuit_toolkit paths
|
||||
TOOLKIT_SRC="$PROJECT_ROOT/kicad-claude-toolkit/python/circuit_toolkit"
|
||||
TOOLKIT_PY="$TOOLKIT_SRC/circuit_toolkit"
|
||||
|
||||
# Output directory (relative to board dir)
|
||||
OUTPUT_DIR=""
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <board-dir> [--erc] [--drc] [--gerber] [--bom] [--schematic] [--sim] [--all]"
|
||||
echo ""
|
||||
echo " --erc Run ERC on generated schematic"
|
||||
echo " --drc Run DRC on generated PCB"
|
||||
echo " --gerber Export Gerber + drill + position files"
|
||||
echo " --bom Generate BOM (flat + JLCPCB)"
|
||||
echo " --schematic Generate schematic SVG via netlistsvg"
|
||||
echo " --sim Run SPICE simulations (if sim/ present)"
|
||||
echo " --all Run all steps (default)"
|
||||
echo " --dry-run Print planned commands without executing"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse args
|
||||
BOARD_DIR=""
|
||||
DO_ERC=false
|
||||
DO_DRC=false
|
||||
DO_GERBER=false
|
||||
DO_BOM=false
|
||||
DO_SCHEMATIC=false
|
||||
DO_SIM=false
|
||||
DO_ALL=true
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--erc) DO_ERC=true; DO_ALL=false;;
|
||||
--drc) DO_DRC=true; DO_ALL=false;;
|
||||
--gerber) DO_GERBER=true; DO_ALL=false;;
|
||||
--bom) DO_BOM=true; DO_ALL=false;;
|
||||
--schematic) DO_SCHEMATIC=true; DO_ALL=false;;
|
||||
--sim) DO_SIM=true; DO_ALL=false;;
|
||||
--dry-run) DRY_RUN=true;;
|
||||
-h|--help) usage;;
|
||||
-*)
|
||||
echo "Unknown option: $arg" >&2
|
||||
usage;;
|
||||
*)
|
||||
if [ -z "$BOARD_DIR" ]; then
|
||||
BOARD_DIR="$arg"
|
||||
else
|
||||
echo "Unexpected argument: $arg" >&2
|
||||
usage
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$BOARD_DIR" ]; then
|
||||
echo "ERROR: board directory required" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
BOARD_DIR="$(cd "$BOARD_DIR" && pwd)"
|
||||
OUTPUT_DIR="$BOARD_DIR/output"
|
||||
|
||||
if $DO_ALL; then
|
||||
DO_ERC=true; DO_DRC=true; DO_GERBER=true; DO_BOM=true; DO_SCHEMATIC=true
|
||||
fi
|
||||
|
||||
# ── Pre-flight checks ──────────────────────────────────────────────────
|
||||
check_kicad_python() {
|
||||
if ! $DRY_RUN && ! bash "$KICAD_PYTHON" -c "import pcbnew" 2>/dev/null; then
|
||||
echo "ERROR: pcbnew not available. Run KiCad.app at least once, then retry." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_kicad_cli() {
|
||||
if [ -z "$KICAD_CLI" ]; then
|
||||
echo "ERROR: kicad-cli not on PATH. Install via: brew install kicad" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Step 1: circuit_toolkit build ──────────────────────────────────────
|
||||
step_build() {
|
||||
local build_script="$BOARD_DIR/build.py"
|
||||
if [ ! -f "$build_script" ]; then
|
||||
echo "SKIP build: no build.py in $BOARD_DIR"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 1: circuit_toolkit build ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] KiCad Python $KICAD_PYTHON"
|
||||
echo " [dry-run] cd $BOARD_DIR && $KICAD_PYTHON build.py"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
pushd "$BOARD_DIR" >/dev/null
|
||||
PYTHONPATH="$TOOLKIT_SRC:$PYTHONPATH" \
|
||||
bash "$KICAD_PYTHON" build.py
|
||||
popd >/dev/null
|
||||
echo " -> $OUTPUT_DIR/"
|
||||
}
|
||||
|
||||
# ── Step 2: ERC ────────────────────────────────────────────────────────
|
||||
step_erc() {
|
||||
local sch="$BOARD_DIR/*.kicad_sch"
|
||||
sch=$(ls $sch 2>/dev/null | head -1 || true)
|
||||
if [ -z "$sch" ]; then
|
||||
echo "SKIP ERC: no .kicad_sch found"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 2: ERC ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] $KICAD_CLI sch erc $sch"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR/erc"
|
||||
$KICAD_CLI sch erc "$sch" --output "$OUTPUT_DIR/erc/erc_report.txt" 2>&1 || true
|
||||
echo " -> $OUTPUT_DIR/erc/"
|
||||
}
|
||||
|
||||
# ── Step 3: DRC ────────────────────────────────────────────────────────
|
||||
step_drc() {
|
||||
local pcb="$BOARD_DIR/*.kicad_pcb"
|
||||
pcb=$(ls $pcb 2>/dev/null | head -1 || true)
|
||||
if [ -z "$pcb" ]; then
|
||||
echo "SKIP DRC: no .kicad_pcb found"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 3: DRC ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] $KICAD_CLI pcb drc $pcb"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR/drc"
|
||||
$KICAD_CLI pcb drc "$pcb" --output "$OUTPUT_DIR/drc/drc_report.html" 2>&1 || true
|
||||
echo " -> $OUTPUT_DIR/drc/"
|
||||
}
|
||||
|
||||
# ── Step 4: Gerber/Drill/Position export ──────────────────────────────
|
||||
step_gerber() {
|
||||
local pcb="$BOARD_DIR/*.kicad_pcb"
|
||||
pcb=$(ls $pcb 2>/dev/null | head -1 || true)
|
||||
if [ -z "$pcb" ]; then
|
||||
echo "SKIP gerber: no .kicad_pcb found"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 4: Gerber + Drill + Position export ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] $KICAD_CLI pcb export gerbers $pcb --output $OUTPUT_DIR/fab/gerber"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR/fab/gerber"
|
||||
$KICAD_CLI pcb export gerbers "$pcb" --output "$OUTPUT_DIR/fab/gerber" 2>&1
|
||||
$KICAD_CLI pcb export drill "$pcb" --output "$OUTPUT_DIR/fab/gerber" 2>&1
|
||||
$KICAD_CLI pcb export pos "$pcb" --output "$OUTPUT_DIR/fab/positions.csv" 2>&1
|
||||
echo " -> $OUTPUT_DIR/fab/"
|
||||
}
|
||||
|
||||
# ── Step 5: BOM + schematic PDF + netlist ─────────────────────────────
|
||||
step_bom() {
|
||||
local sch="$BOARD_DIR/*.kicad_sch"
|
||||
sch=$(ls $sch 2>/dev/null | head -1 || true)
|
||||
if [ -z "$sch" ]; then
|
||||
echo "SKIP bom: no .kicad_sch found"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 5: BOM + schematic PDF + netlist ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] $KICAD_CLI sch export bom $sch --output $OUTPUT_DIR/fab"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR/fab"
|
||||
$KICAD_CLI sch export bom "$sch" --output "$OUTPUT_DIR/fab" 2>&1
|
||||
$KICAD_CLI sch export pdf "$sch" --output "$OUTPUT_DIR/schematic.pdf" 2>&1
|
||||
$KICAD_CLI sch export "$sch" --format netlist --output "$OUTPUT_DIR/fab/netlist.xml" 2>&1
|
||||
echo " -> $OUTPUT_DIR/fab/"
|
||||
}
|
||||
|
||||
# ── Step 6: SPICE simulation ──────────────────────────────────────────
|
||||
step_sim() {
|
||||
if [ ! -d "$BOARD_DIR/sim" ]; then
|
||||
echo "SKIP sim: no sim/ directory"
|
||||
return 0
|
||||
fi
|
||||
echo "=== Step 6: SPICE simulation ==="
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] ngspice -b $BOARD_DIR/sim/*.cir"
|
||||
return 0
|
||||
fi
|
||||
if ! command -v ngspice &>/dev/null; then
|
||||
echo "SKIP sim: ngspice not on PATH (brew install ngspice)"
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$OUTPUT_DIR/sim"
|
||||
for cir in "$BOARD_DIR"/sim/*.cir; do
|
||||
echo " sim: $(basename "$cir")"
|
||||
ngspice -b -r "$OUTPUT_DIR/sim/$(basename "$cir" .cir).raw" "$cir" 2>&1 || true
|
||||
done
|
||||
echo " -> $OUTPUT_DIR/sim/"
|
||||
}
|
||||
|
||||
# ── Run ────────────────────────────────────────────────────────────────
|
||||
echo "kicad-build: $BOARD_DIR"
|
||||
echo " KiCad Python: $KICAD_PYTHON"
|
||||
echo " kicad-cli: $KICAD_CLI"
|
||||
echo " output: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
check_kicad_python
|
||||
if [ -n "$KICAD_CLI" ]; then
|
||||
: # ok
|
||||
else
|
||||
echo "WARNING: kicad-cli not found (ERC/DRC/gerber/bom will be skipped)"
|
||||
fi
|
||||
|
||||
step_build
|
||||
$DO_ERC && step_erc
|
||||
$DO_DRC && step_drc
|
||||
$DO_GERBER && step_gerber
|
||||
$DO_BOM && step_bom
|
||||
$DO_SIM && step_sim
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "Output: $OUTPUT_DIR/"
|
||||
ls -la "$OUTPUT_DIR/" 2>/dev/null || true
|
||||
44
tools/kicad-python
Executable file
44
tools/kicad-python
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# kicad-python — launcher for KiCad's bundled Python with pcbnew available.
|
||||
#
|
||||
# Usage:
|
||||
# ./kicad-python <script.py> [args...]
|
||||
# ./kicad-python -c "import pcbnew; ..."
|
||||
# ./kicad-python -m circuit_toolkit.build my-board/
|
||||
#
|
||||
# Detects KiCad.app on macOS, KiCad on Linux, or KiCad Flatpak.
|
||||
# Exits non-zero if KiCad Python is not found.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
KICAD_APP="/Applications/KiCad/KiCad.app"
|
||||
KICAD_PYTHON="${KICAD_APP}/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3"
|
||||
|
||||
# Try Flatpak install
|
||||
if [ -x "/var/lib/flatpak/app/org.kicad.KiCad" ] || command -v flatpak &>/dev/null; then
|
||||
FLATPAK_PYTHON=$(flatpak run --command=python3 org.kicad.KiCad 2>/dev/null || true)
|
||||
if [ -n "$FLATPAK_PYTHON" ]; then
|
||||
KICAD_PYTHON="$FLATPAK_PYTHON"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try Linux install
|
||||
if [ ! -x "$KICAD_PYTHON" ]; then
|
||||
for candidate in \
|
||||
/opt/kicad/bin/python3 \
|
||||
/usr/bin/kicad-python \
|
||||
/usr/bin/python3; do
|
||||
if "$candidate" -c "import pcbnew" 2>/dev/null; then
|
||||
KICAD_PYTHON="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ! -x "$KICAD_PYTHON" ]; then
|
||||
echo "ERROR: KiCad Python not found at $KICAD_PYTHON" >&2
|
||||
echo "Install KiCad (kicad.org) or a Flatpak, then re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$KICAD_PYTHON" "$@"
|
||||
266
tools/retire_block.py
Normal file
266
tools/retire_block.py
Normal file
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env python3
|
||||
"""retire_block.py — remove a circuit block from a CM5-carrier KiCad sheet cleanly.
|
||||
|
||||
For the diff-from-reference port (see .claude/skills/kicad-port). Given a sheet and a
|
||||
set of reference designators to drop plus the net-name globs they own, this:
|
||||
1. excises each named (symbol ...) block (paren-matched, by Reference property),
|
||||
2. floods the retired nets from their (label ...) coords through (wire ...) segments,
|
||||
3. removes those labels + every wire in the flooded nets,
|
||||
4. places (no_connect ...) markers on the exposed module pins that sat on those nets,
|
||||
5. writes back only if parentheses stay balanced.
|
||||
|
||||
Module pin coordinates are computed from the lib_symbol pin locals + the placement
|
||||
transform (validated abs = origin + (lx, -ly) for rot0/no-mirror; other orientations
|
||||
are rejected rather than guessed). Idempotent-ish: re-running finds nothing to do.
|
||||
|
||||
Usage:
|
||||
python3 tools/retire_block.py <sheet.kicad_sch> \
|
||||
--symbols J7,U18 --nets 'SD_*' \
|
||||
--module-lib CM5IO:ComputeModule5-CM5 [--apply]
|
||||
Without --apply it's a dry run (prints the plan, writes nothing).
|
||||
"""
|
||||
import re, sys, argparse, fnmatch, math, uuid as uuidlib
|
||||
|
||||
S = r'\s*'
|
||||
|
||||
def xform(px, py, rot, mirror, lx, ly):
|
||||
"""Schematic abs coord of a lib pin local (lx,ly) for a placement. Validated:
|
||||
mirror, then CCW rotate, then lib-Y-up -> schematic-Y-down flip."""
|
||||
X, Y = lx, ly
|
||||
if mirror == 'y': X = -X
|
||||
if mirror == 'x': Y = -Y
|
||||
r = math.radians(rot)
|
||||
rx = X * math.cos(r) - Y * math.sin(r)
|
||||
ry = X * math.sin(r) + Y * math.cos(r)
|
||||
return (round(px + rx, 2), round(py - ry, 2))
|
||||
|
||||
def paren_end(t, s):
|
||||
d = 0
|
||||
for i in range(s, len(t)):
|
||||
if t[i] == '(': d += 1
|
||||
elif t[i] == ')':
|
||||
d -= 1
|
||||
if d == 0: return i + 1
|
||||
raise ValueError("unbalanced from offset %d" % s)
|
||||
|
||||
def fnum(x): return round(float(x), 2)
|
||||
|
||||
def find_symbol_blocks(t):
|
||||
"""Yield (start,end,reference) for every placed (symbol (lib_id ...)) block."""
|
||||
for m in re.finditer(r'\(symbol' + S + r'\(lib_id' + S + r'"', t):
|
||||
s = m.start(); e = paren_end(t, s); seg = t[s:e]
|
||||
ref = re.search(r'\(property' + S + r'"Reference"' + S + r'"([^"]+)"', seg)
|
||||
if ref: yield (s, e, ref.group(1))
|
||||
|
||||
def module_pins_abs(t, module_lib):
|
||||
"""Compute {pin_number:(x,y)} for the placed module instance on this sheet."""
|
||||
inst = re.search(r'\(symbol' + S + r'\(lib_id' + S + r'"' + re.escape(module_lib) + r'"\)(.{0,500}?)\(unit' + S + r'(\d+)\)', t, re.S)
|
||||
if not inst: return {}, None
|
||||
body, unit = inst.group(1), inst.group(2)
|
||||
at = re.search(r'\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)' + S + r'([\d.-]+)\)', body)
|
||||
mir = re.search(r'\(mirror' + S + r'(\w+)\)', body)
|
||||
px, py, rot = float(at.group(1)), float(at.group(2)), float(at.group(3))
|
||||
if rot != 0 or mir:
|
||||
sys.exit(f"REFUSING: module instance has rot={rot} mirror={mir and mir.group(1)} — transform only validated for rot0/no-mirror.")
|
||||
libname = module_lib.split(':')[-1]
|
||||
pins = {}
|
||||
for pm in re.finditer(r'\(pin\b.*?\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)' + S + r'[\d.-]+\).*?\(number' + S + r'"([^"]+)"', t, re.S):
|
||||
lx, ly, num = float(pm.group(1)), float(pm.group(2)), pm.group(3)
|
||||
pins.setdefault(num, (round(px + lx, 2), round(py - ly, 2)))
|
||||
return pins, unit
|
||||
|
||||
def collect(t, pat):
|
||||
"""Return list of (start,end,coord,text) for top-level objects named `pat` token."""
|
||||
out = []
|
||||
for m in re.finditer(r'\(' + pat + r'\b', t):
|
||||
s = m.start(); e = paren_end(t, s); seg = t[s:e]
|
||||
at = re.search(r'\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)', seg)
|
||||
out.append((s, e, (fnum(at.group(1)), fnum(at.group(2))) if at else None, seg))
|
||||
return out
|
||||
|
||||
def wire_pts(seg):
|
||||
return [(fnum(a), fnum(b)) for a, b in re.findall(r'\(xy' + S + r'([\d.-]+)' + S + r'([\d.-]+)\)', seg)]
|
||||
|
||||
def lib_pins(t, lib_id):
|
||||
"""Local (lx,ly,angle) for every pin of a lib_symbol definition."""
|
||||
name = lib_id.split(':')[-1]
|
||||
lm = re.search(r'\(symbol' + S + r'"' + re.escape(lib_id) + r'"', t) or \
|
||||
re.search(r'\(symbol' + S + r'"' + re.escape(name) + r'"', t)
|
||||
if not lm: return []
|
||||
seg = t[lm.start():paren_end(t, lm.start())]
|
||||
return [(float(a), float(b), float(c)) for a, b, c in
|
||||
re.findall(r'\(pin\b.*?\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)' + S + r'([\d.-]+)\)', seg, re.S)]
|
||||
|
||||
def placed_symbols(t):
|
||||
"""Yield dict per placed symbol: start,end,ref,lib,at(px,py,rot),mirror."""
|
||||
for m in re.finditer(r'\(symbol' + S + r'\(lib_id' + S + r'"([^"]+)"\)', t):
|
||||
s = m.start(); e = paren_end(t, s); seg = t[s:e]
|
||||
ref = re.search(r'\(property' + S + r'"Reference"' + S + r'"([^"]+)"', seg)
|
||||
at = re.search(r'\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)' + S + r'([\d.-]+)\)', seg)
|
||||
mir = re.search(r'\(mirror' + S + r'(\w+)\)', seg)
|
||||
yield dict(s=s, e=e, ref=ref.group(1) if ref else None, lib=m.group(1),
|
||||
at=(float(at.group(1)), float(at.group(2)), float(at.group(3))) if at else None,
|
||||
mirror=mir.group(1) if mir else None)
|
||||
|
||||
def sym_pin_coords(t, sym):
|
||||
if not sym['at']: return []
|
||||
px, py, rot = sym['at']
|
||||
return [xform(px, py, rot, sym['mirror'], lx, ly) for lx, ly, _ in lib_pins(t, sym['lib'])]
|
||||
|
||||
def sheet_port_spans(t, names):
|
||||
"""On a ROOT sheet, find (pin "NAME" ...) hierarchical ports inside (sheet ...) blocks
|
||||
whose name is in `names`. Returns (pin spans to remove, their connection coords)."""
|
||||
spans, coords = [], set()
|
||||
for m in re.finditer(r'\(sheet\b', t):
|
||||
s = m.start(); e = paren_end(t, s)
|
||||
for pm in re.finditer(r'\(pin' + S + r'"([^"]+)"', t[s:e]):
|
||||
if pm.group(1) in names:
|
||||
ps = s + pm.start(); pe = paren_end(t, ps)
|
||||
at = re.search(r'\(at' + S + r'([\d.-]+)' + S + r'([\d.-]+)', t[ps:pe])
|
||||
spans.append((ps, pe))
|
||||
if at: coords.add((fnum(at.group(1)), fnum(at.group(2))))
|
||||
return spans, coords
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('sheet')
|
||||
ap.add_argument('--symbols', default='')
|
||||
ap.add_argument('--nets', default='')
|
||||
ap.add_argument('--nc-pins', default='', help='module pin NUMBERS to no-connect (from analyzer regressions)')
|
||||
ap.add_argument('--sheet-ports', default='', help='hierarchical net names: remove matching sheet-symbol pins + connecting root wires (run on the ROOT sheet)')
|
||||
ap.add_argument('--module-lib', default='CM5IO:ComputeModule5-CM5')
|
||||
ap.add_argument('--apply', action='store_true')
|
||||
a = ap.parse_args()
|
||||
drop_refs = set(filter(None, a.symbols.split(',')))
|
||||
net_globs = list(filter(None, a.nets.split(',')))
|
||||
nc_nums = list(filter(None, a.nc_pins.split(',')))
|
||||
port_names = set(filter(None, a.sheet_ports.split(',')))
|
||||
t = open(a.sheet).read()
|
||||
|
||||
modpins, unit = module_pins_abs(t, a.module_lib)
|
||||
pin_at = {xy: n for n, xy in modpins.items()}
|
||||
nc_pins = [(n, modpins[n]) for n in nc_nums if n in modpins]
|
||||
missing_nc = [n for n in nc_nums if n not in modpins]
|
||||
if missing_nc: sys.exit(f"NC pins not found on module: {missing_nc}")
|
||||
|
||||
# 1) symbols to remove + their abs pin coords (for stub cleanup)
|
||||
placed = list(placed_symbols(t))
|
||||
removed_syms = [sy for sy in placed if sy['ref'] in drop_refs]
|
||||
found_refs = {sy['ref'] for sy in removed_syms}
|
||||
sym_spans = [(sy['s'], sy['e']) for sy in removed_syms]
|
||||
removed_pin_coords = set()
|
||||
for sy in removed_syms:
|
||||
removed_pin_coords |= set(sym_pin_coords(t, sy))
|
||||
|
||||
# 2) retired labels by glob — match ANY label type (local / hierarchical / global)
|
||||
labels = collect(t, 'label') + collect(t, 'hierarchical_label') + collect(t, 'global_label')
|
||||
retired_labels = [(s, e, c) for s, e, c, seg in labels
|
||||
for nm in [re.search(r'"([^"]+)"', seg).group(1)]
|
||||
if any(fnmatch.fnmatch(nm, g) for g in net_globs)]
|
||||
retired_coords = {c for _, _, c in retired_labels}
|
||||
|
||||
# 3) anchor-aware wire removal via connected components.
|
||||
# A wire is removed only if its component (a) is orphaned by THIS edit (touches a
|
||||
# removed pin, retired label, or target NC pin) AND (b) reaches no KEPT anchor —
|
||||
# a kept non-power component pin or a kept label. Shared nets (e.g. a camera FFC's
|
||||
# I2C/power pins) stay anchored by the kept side, so they survive; pre-existing
|
||||
# floating wires aren't seeded, so they're untouched.
|
||||
wires = collect(t, 'wire')
|
||||
wpts = [(s, e, wire_pts(seg)) for s, e, _, seg in wires]
|
||||
parent = {}
|
||||
def find(x):
|
||||
parent.setdefault(x, x)
|
||||
root = x
|
||||
while parent[root] != root: root = parent[root]
|
||||
while parent[x] != root: parent[x], x = root, parent[x]
|
||||
return root
|
||||
def union(a, b): parent[find(a)] = find(b)
|
||||
for s, e, pts in wpts:
|
||||
for p in pts[1:]:
|
||||
union(pts[0], p)
|
||||
|
||||
nc_coords = {xy for _, xy in nc_pins}
|
||||
kept_label_coords = {c for s, e, c, seg in labels
|
||||
if not any(fnmatch.fnmatch(re.search(r'"([^"]+)"', seg).group(1), g)
|
||||
for g in net_globs)}
|
||||
kept_pin_coords = set()
|
||||
for sy in placed:
|
||||
if sy['ref'] in drop_refs or sy['lib'].startswith('power:'): continue
|
||||
kept_pin_coords |= set(sym_pin_coords(t, sy))
|
||||
kept_pin_coords -= nc_coords # pins we are NC'ing are being disconnected
|
||||
anchors = kept_pin_coords | kept_label_coords
|
||||
# sheet-symbol hierarchical ports to drop (root sheet) + their connection coords
|
||||
port_spans, port_coords = sheet_port_spans(t, port_names) if port_names else ([], set())
|
||||
seeds = set(removed_pin_coords) | set(retired_coords) | nc_coords | port_coords
|
||||
|
||||
anchored_roots, seeded_roots = set(), set()
|
||||
for c in anchors:
|
||||
if c in parent: anchored_roots.add(find(c))
|
||||
for c in seeds:
|
||||
if c in parent: seeded_roots.add(find(c))
|
||||
removable_roots = seeded_roots - anchored_roots
|
||||
wire_remove = [(s, e, pts) for s, e, pts in wpts if find(pts[0]) in removable_roots]
|
||||
wire_remove_spans = {(s, e) for s, e, _ in wire_remove}
|
||||
freed = set(removed_pin_coords)
|
||||
for _, _, pts in wire_remove:
|
||||
freed |= set(pts)
|
||||
kept_wire_pts = set()
|
||||
for s, e, pts in wpts:
|
||||
if (s, e) not in wire_remove_spans:
|
||||
kept_wire_pts |= set(pts)
|
||||
flooded_wires = wire_remove # naming compat for the summary print
|
||||
stub_wires = []
|
||||
|
||||
# 5) orphaned power flags (power:*): pin freed by my edit, no kept wire left on it
|
||||
power_spans = []
|
||||
for sy in placed:
|
||||
if sy['ref'] in drop_refs or not sy['lib'].startswith('power:'): continue
|
||||
pcs = sym_pin_coords(t, sy)
|
||||
if pcs and all(pc in freed and pc not in kept_wire_pts for pc in pcs):
|
||||
power_spans.append((sy['s'], sy['e']))
|
||||
|
||||
# 6) dangling no_connects sitting on a removed symbol's pin
|
||||
dangling_nc = [(s, e) for s, e, c, _ in collect(t, 'no_connect') if c in removed_pin_coords]
|
||||
|
||||
# 7) labels orphaned by my edit (any label type): coord freed, no kept wire left,
|
||||
# not already retired. Covers local (label), and cross-sheet (hierarchical_label /
|
||||
# global_label) that only fed a removed connector (e.g. a camera FFC's SCL0/CAM_GPIO).
|
||||
retired_label_spans = {(s, e) for s, e, _ in retired_labels}
|
||||
all_label_objs = collect(t, 'label') + collect(t, 'hierarchical_label') + collect(t, 'global_label')
|
||||
orphan_labels = [(s, e) for s, e, c, _ in all_label_objs
|
||||
if (s, e) not in retired_label_spans and c in freed and c not in kept_wire_pts]
|
||||
|
||||
nc_pins = sorted(nc_pins, key=lambda kv: int(re.sub(r'\D', '', kv[0]) or 0))
|
||||
print(f"sheet: {a.sheet} (module unit {unit}, {len(modpins)} pins)")
|
||||
print(f"symbols to remove: {sorted(found_refs)} (requested {sorted(drop_refs)}; missing {sorted(drop_refs-found_refs)})")
|
||||
print(f"retired-net labels: {len(retired_labels)} | wires removed (anchor-aware): {len(wire_remove)}")
|
||||
print(f"orphaned power flags: {len(power_spans)} | dangling NC removed: {len(dangling_nc)} | orphaned labels: {len(orphan_labels)}")
|
||||
if port_names: print(f"sheet-symbol ports removed: {len(port_spans)} ({sorted(port_names)})")
|
||||
print(f"module pins to NC ({len(nc_pins)}): " + ", ".join(f"{n}@{xy}" for n, xy in nc_pins))
|
||||
|
||||
# build new text: remove all spans, then append fresh no_connects on exposed module pins
|
||||
spans = sorted(set(sym_spans) | {(s, e) for s, e, _ in retired_labels}
|
||||
| wire_remove_spans | set(power_spans) | set(dangling_nc) | set(orphan_labels)
|
||||
| set(port_spans), key=lambda x: -x[0])
|
||||
nt = t
|
||||
for s, e in spans:
|
||||
p = s
|
||||
while p > 0 and nt[p-1] in '\t ': p -= 1
|
||||
if p > 0 and nt[p-1] == '\n': p -= 1
|
||||
nt = nt[:p] + nt[e:]
|
||||
# insert no_connect markers before the final closing paren of the sheet
|
||||
nc_block = "".join(
|
||||
f'\t(no_connect\n\t\t(at {xy[0]} {xy[1]})\n\t\t(uuid "{uuidlib.uuid5(uuidlib.NAMESPACE_DNS, a.sheet+n)}")\n\t)\n'
|
||||
for n, xy in nc_pins)
|
||||
last = nt.rstrip().rfind(')')
|
||||
nt = nt[:last] + nc_block + nt[last:]
|
||||
assert nt.count('(') == nt.count(')'), "UNBALANCED — aborting"
|
||||
|
||||
if a.apply:
|
||||
open(a.sheet, 'w').write(nt)
|
||||
print(f"APPLIED. parens balanced ({nt.count('(')}). +{len(nc_pins)} no_connect, -{len(spans)} objects.")
|
||||
else:
|
||||
print(f"DRY RUN ok. parens would balance ({nt.count('(')}). Re-run with --apply to write.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user