257 lines
8.6 KiB
Plaintext
257 lines
8.6 KiB
Plaintext
|
|
#!/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
|