KiCad Schematic Agent Generate ERC-clean KiCad 8/9 schematics by writing Python scripts that use computed pin positions — never guess coordinates. Also fix ERC errors on existing schematics and handle KiCad 8→9 migration. Critical Principle The #1 cause of broken schematics is guessed pin positions. When connecting labels to IC pins, you MUST compute exact coordinates using the symbol definition's pin positions and the coordinate transform formula. The helper library in does this automatically. The Y-axis Trap (Most Common Bug) Symbol libraries (.kicad sym) use Y-up (math convention). Schemat…

, m.group(1)):\n sym_name = m.group(1)\n depth = line.count('(') - line.count(')')\n block_lines = [line]\n j = i + 1\n while j \u003c len(lines) and depth > 0:\n l = lines[j].strip()\n depth += l.count('(') - l.count(')')\n block_lines.append(l)\n j += 1\n\n block = '\\n'.join(block_lines)\n pins = []\n for pm in pin_pattern.finditer(block):\n pins.append(PinDef(\n name=pm.group(6), number=pm.group(7),\n x=float(pm.group(2)), y=float(pm.group(3)),\n angle=int(pm.group(4)), length=float(pm.group(5)),\n pin_type=pm.group(1),\n ))\n if pins:\n self.symbols[sym_name] = SymbolDef(name=sym_name, pins=pins)\n i = j\n else:\n i += 1\n\n def get(self, name: str) -> Optional[SymbolDef]:\n \"\"\"Get symbol by name (tries with and without library prefix).\"\"\"\n if name in self.symbols:\n return self.symbols[name]\n if ':' in name:\n short = name.split(':', 1)[1]\n if short in self.symbols:\n return self.symbols[short]\n return None\n\n\n# =============================================================================\n# lib_symbols template generators\n# =============================================================================\n\ndef lib_sym_2pin(lib_id: str, ref_prefix: str, default_val: str,\n pin1_name: str = \"1\", pin2_name: str = \"2\",\n pin1_type: str = \"passive\", pin2_type: str = \"passive\",\n body: str = \"rect\") -> str:\n \"\"\"\n Generate lib_symbol for a 2-pin component.\n\n IMPORTANT: Sub-symbol names use ONLY the symbol name, not the library prefix.\n This is handled automatically by this function.\n\n Standard 2-pin pin positions:\n Pin 1: (0, 2.54) pointing down (angle 270) — TOP in schematic\n Pin 2: (0, -2.54) pointing up (angle 90) — BOTTOM in schematic\n \"\"\"\n sym_name = lib_id.split(':')[-1] if ':' in lib_id else lib_id\n\n bodies = {\n \"rect\": \"\"\" (rectangle (start -1.016 1.27) (end 1.016 -1.27)\n (stroke (width 0.254) (type default)) (fill (type none)))\"\"\",\n \"cap\": \"\"\" (polyline (pts (xy -1.27 0.508) (xy 1.27 0.508))\n (stroke (width 0.254) (type default)) (fill (type none)))\n (polyline (pts (xy -1.27 -0.508) (xy 1.27 -0.508))\n (stroke (width 0.254) (type default)) (fill (type none)))\"\"\",\n \"inductor\": \"\"\" (arc (start 0 -1.27) (mid 0.635 -0.635) (end 0 0)\n (stroke (width 0.254) (type default)) (fill (type none)))\n (arc (start 0 0) (mid 0.635 0.635) (end 0 1.27)\n (stroke (width 0.254) (type default)) (fill (type none)))\"\"\",\n \"diode\": \"\"\" (polyline (pts (xy -1.27 1.016) (xy -1.27 -1.016) (xy 1.27 0) (xy -1.27 1.016))\n (stroke (width 0.254) (type default)) (fill (type none)))\n (polyline (pts (xy 1.27 1.016) (xy 1.27 -1.016))\n (stroke (width 0.254) (type default)) (fill (type none)))\"\"\",\n \"led\": \"\"\" (polyline (pts (xy -1.27 1.016) (xy -1.27 -1.016) (xy 1.27 0) (xy -1.27 1.016))\n (stroke (width 0.254) (type default)) (fill (type none)))\n (polyline (pts (xy 1.27 1.016) (xy 1.27 -1.016))\n (stroke (width 0.254) (type default)) (fill (type none)))\"\"\",\n }\n drawing = bodies.get(body, bodies[\"rect\"])\n\n return f\"\"\" (symbol \"{lib_id}\"\n (pin_numbers hide) (pin_names hide) (in_bom yes) (on_board yes)\n (property \"Reference\" \"{ref_prefix}\" (at 2.54 0.508 0)\n (effects (font (size 1.27 1.27)) (justify left)))\n (property \"Value\" \"{default_val}\" (at 2.54 -1.016 0)\n (effects (font (size 1.27 1.27)) (justify left)))\n (property \"Footprint\" \"\" (at 0 0 0)\n (effects (font (size 1.27 1.27)) hide))\n (symbol \"{sym_name}_0_1\"\n{drawing}\n )\n (symbol \"{sym_name}_1_1\"\n (pin {pin1_type} line (at 0 2.54 270) (length 1.27)\n (name \"{pin1_name}\" (effects (font (size 1.0 1.0))))\n (number \"1\" (effects (font (size 1.0 1.0)))))\n (pin {pin2_type} line (at 0 -2.54 90) (length 1.27)\n (name \"{pin2_name}\" (effects (font (size 1.0 1.0))))\n (number \"2\" (effects (font (size 1.0 1.0)))))\n )\n )\"\"\"\n\n\ndef lib_sym_power(name: str, net_name: str) -> str:\n \"\"\"Generate a power symbol definition (GND, +3.3V, etc.).\"\"\"\n sym_name = name.split(':')[-1] if ':' in name else name\n if \"GND\" in name:\n drawing = \"\"\" (polyline (pts (xy 0 0) (xy 0 -1.27) (xy -1.27 -1.27) (xy 0 -2.54) (xy 1.27 -1.27) (xy 0 -1.27))\n (stroke (width 0) (type default)) (fill (type none)))\"\"\"\n pin_at = \"(at 0 0 0)\"\n else:\n drawing = \"\"\" (polyline (pts (xy -0.762 1.27) (xy 0.762 1.27))\n (stroke (width 0.254) (type default)) (fill (type none)))\n (polyline (pts (xy 0 0) (xy 0 1.27))\n (stroke (width 0) (type default)) (fill (type none)))\"\"\"\n pin_at = \"(at 0 0 90)\"\n\n return f\"\"\" (symbol \"{name}\"\n (power) (pin_numbers hide) (pin_names hide) (in_bom no) (on_board yes)\n (property \"Reference\" \"#PWR\" (at 0 2.54 0)\n (effects (font (size 1.27 1.27)) hide))\n (property \"Value\" \"{net_name}\" (at 0 3.81 0)\n (effects (font (size 1.0 1.0))))\n (property \"Footprint\" \"\" (at 0 0 0)\n (effects (font (size 1.27 1.27)) hide))\n (symbol \"{sym_name}_0_1\"\n{drawing}\n )\n (symbol \"{sym_name}_1_1\"\n (pin power_in line {pin_at} (length 0)\n (name \"{net_name}\" (effects (font (size 1.0 1.0))))\n (number \"1\" (effects (font (size 1.0 1.0)))))\n )\n )\"\"\"\n\n\ndef lib_sym_pwr_flag() -> str:\n \"\"\"Generate PWR_FLAG symbol. Place on every power output net to avoid\n 'power_pin_not_driven' ERC errors.\"\"\"\n return \"\"\" (symbol \"power:PWR_FLAG\"\n (power) (pin_numbers hide) (pin_names hide) (in_bom no) (on_board yes)\n (property \"Reference\" \"#FLG\" (at 0 2.54 0)\n (effects (font (size 1.27 1.27)) hide))\n (property \"Value\" \"PWR_FLAG\" (at 0 3.81 0)\n (effects (font (size 1.0 1.0))))\n (property \"Footprint\" \"\" (at 0 0 0)\n (effects (font (size 1.27 1.27)) hide))\n (symbol \"PWR_FLAG_0_1\"\n (polyline (pts (xy 0 0) (xy 0 1.27) (xy -1.016 2.032) (xy 0 2.794) (xy 1.016 2.032) (xy 0 1.27))\n (stroke (width 0) (type default)) (fill (type none)))\n )\n (symbol \"PWR_FLAG_1_1\"\n (pin power_out line (at 0 0 90) (length 0)\n (name \"pwr\" (effects (font (size 1.0 1.0))))\n (number \"1\" (effects (font (size 1.0 1.0)))))\n )\n )\"\"\"\n\n\n# =============================================================================\n# Sub-symbol name fixer (post-processing)\n# =============================================================================\n\ndef fix_subsymbol_names(content: str) -> str:\n \"\"\"\n Fix sub-symbol names in lib_symbols section.\n\n KiCad REQUIRES that sub-symbols (those with _N_N suffix) do NOT include\n the library prefix. This is the most common cause of \"Invalid symbol unit\n name prefix\" errors when opening generated schematics.\n\n Examples of what this fixes:\n \"Device:R_0_1\" → \"R_0_1\"\n \"Device:C_Polarized_0_1\" → \"C_Polarized_0_1\"\n \"CubeSat_SDR:AD9363ABCZ_1_1\" → \"AD9363ABCZ_1_1\"\n \"Connector:Barrel_Jack_0_1\" → \"Barrel_Jack_0_1\"\n \"\"\"\n def fix_match(m):\n full_name = m.group(1)\n suffix = m.group(2)\n if ':' in full_name:\n name = full_name.split(':', 1)[1]\n return f'(symbol \"{name}{suffix}\"'\n return m.group(0)\n\n return re.sub(r'\\(symbol \"([^\"]+?)(_\\d+_\\d+)\"', fix_match, content)\n\n\n# =============================================================================\n# Schematic builder\n# =============================================================================\n\n@dataclass\nclass PlacedComponent:\n \"\"\"A component placed in the schematic.\"\"\"\n lib_id: str\n ref: str\n value: str\n x: float\n y: float\n rotation: int\n footprint: str\n lcsc: str\n mirror_y: bool\n unit: int\n uuid: str\n\n\nclass SchematicBuilder:\n \"\"\"\n Build a KiCad 8 schematic with guaranteed pin-label connectivity.\n\n All coordinates are automatically grid-snapped.\n Use connect_pin() for IC pins — it computes exact positions.\n Use place_2pin_vertical/horizontal for passive components.\n \"\"\"\n\n def __init__(self, symbol_lib: SymbolLibrary = None, project_name: str = \"project\"):\n self.symbol_lib = symbol_lib\n self.project_name = project_name\n self.root_uuid = uid()\n self.components: list = []\n self.placed: dict = {} # ref -> PlacedComponent\n self.wires: list = []\n self.labels: list = []\n self.no_connects: list = []\n self.text_notes: list = []\n self.pwr_sym_counter = 0\n self.flg_counter = 0\n self._lib_symbols_content = \"\"\n\n def set_symbol_library(self, lib: SymbolLibrary):\n \"\"\"Set the symbol library for pin position lookups.\"\"\"\n self.symbol_lib = lib\n\n def set_lib_symbols(self, content: str):\n \"\"\"Set raw lib_symbols S-expression content.\"\"\"\n self._lib_symbols_content = content\n\n def place(self, lib_id: str, ref: str, value: str, x: float, y: float,\n rotation: int = 0, footprint: str = \"\", lcsc: str = \"\",\n mirror_y: bool = False, unit: int = 1) -> PlacedComponent:\n \"\"\"Place a component at grid-snapped coordinates.\"\"\"\n x, y = snap(x), snap(y)\n u = uid()\n ms = \"(mirror y)\" if mirror_y else \"\"\n\n self.components.append(f\"\"\" (symbol (lib_id \"{lib_id}\") (at {x:.2f} {y:.2f} {rotation}) {ms}\n (uuid \"{u}\")\n (property \"Reference\" \"{ref}\" (at {x:.2f} {y - 3.81:.2f} 0)\n (effects (font (size 1.27 1.27))))\n (property \"Value\" \"{value}\" (at {x:.2f} {y + 3.81:.2f} 0)\n (effects (font (size 1.0 1.0))))\n (property \"Footprint\" \"{footprint}\" (at {x:.2f} {y + 5.08:.2f} 0)\n (effects (font (size 1.27 1.27)) hide))\n (property \"LCSC\" \"{lcsc}\" (at {x:.2f} {y + 6.35:.2f} 0)\n (effects (font (size 1.27 1.27)) hide))\n (instances\n (project \"{self.project_name}\"\n (path \"/{self.root_uuid}\" (reference \"{ref}\") (unit {unit}))\n )\n )\n )\"\"\")\n\n comp = PlacedComponent(\n lib_id=lib_id, ref=ref, value=value,\n x=x, y=y, rotation=rotation,\n footprint=footprint, lcsc=lcsc,\n mirror_y=mirror_y, unit=unit, uuid=u\n )\n self.placed[ref] = comp\n return comp\n\n def place_power(self, lib_id: str, value: str, x: float, y: float, rotation: int = 0):\n \"\"\"Place a power symbol (GND, VCC, etc.).\"\"\"\n x, y = snap(x), snap(y)\n self.pwr_sym_counter += 1\n ref = f\"#PWR{self.pwr_sym_counter:03d}\"\n self.components.append(f\"\"\" (symbol (lib_id \"{lib_id}\") (at {x:.2f} {y:.2f} {rotation})\n (uuid \"{uid()}\")\n (property \"Reference\" \"{ref}\" (at {x:.2f} {y + 2.54:.2f} 0)\n (effects (font (size 1.27 1.27)) hide))\n (property \"Value\" \"{value}\" (at {x:.2f} {y + 3.81:.2f} 0)\n (effects (font (size 0.8 0.8))))\n (property \"Footprint\" \"\" (at {x:.2f} {y:.2f} 0)\n (effects (font (size 1.27 1.27)) hide))\n (instances\n (project \"{self.project_name}\"\n (path \"/{self.root_uuid}\" (reference \"{ref}\") (unit 1))\n )\n )\n )\"\"\")\n\n def place_pwr_flag(self, x: float, y: float, net_name: str):\n \"\"\"Place a PWR_FLAG on a power net. Essential for regulator outputs.\"\"\"\n x, y = snap(x), snap(y)\n self.flg_counter += 1\n ref = f\"#FLG{self.flg_counter:03d}\"\n self.components.append(f\"\"\" (symbol (lib_id \"power:PWR_FLAG\") (at {x:.2f} {y:.2f} 0)\n (uuid \"{uid()}\")\n (property \"Reference\" \"{ref}\" (at {x:.2f} {y + 2.54:.2f} 0)\n (effects (font (size 1.27 1.27)) hide))\n (property \"Value\" \"PWR_FLAG\" (at {x:.2f} {y + 3.81:.2f} 0)\n (effects (font (size 0.8 0.8))))\n (property \"Footprint\" \"\" (at {x:.2f} {y:.2f} 0)\n (effects (font (size 1.27 1.27)) hide))\n (instances\n (project \"{self.project_name}\"\n (path \"/{self.root_uuid}\" (reference \"{ref}\") (unit 1))\n )\n )\n )\"\"\")\n self.label(net_name, x, y)\n\n def connect_pin(self, ref: str, pin_name: str, net_label: str,\n wire_dx: float = 0, wire_dy: float = 0,\n label_angle: int = 0, by_number: bool = False):\n \"\"\"\n THE key method. Connect a component's pin to a net label with exact\n computed coordinates and an optional wire stub.\n\n This eliminates dangling label and unconnected pin ERC errors.\n\n Args:\n ref: Component reference (e.g., \"U1\")\n pin_name: Pin name (or number if by_number=True)\n net_label: Net label text\n wire_dx, wire_dy: Wire extension from pin for routing room\n label_angle: Label rotation (0, 90, 180, 270)\n by_number: Look up pin by number instead of name\n \"\"\"\n comp = self.placed.get(ref)\n if not comp:\n print(f\"WARNING: Component {ref} not found\", file=sys.stderr)\n return\n\n if not self.symbol_lib:\n print(f\"WARNING: No symbol library set. Pass symbol_lib to constructor \"\n f\"or call set_symbol_library() first.\", file=sys.stderr)\n return\n\n sym_def = self.symbol_lib.get(comp.lib_id)\n if not sym_def:\n print(f\"WARNING: Symbol {comp.lib_id} not in library\", file=sys.stderr)\n return\n\n pin = (sym_def.get_pin_by_number(pin_name) if by_number\n else sym_def.get_pin(pin_name))\n if not pin:\n print(f\"WARNING: Pin '{pin_name}' not found on {comp.lib_id}\",\n file=sys.stderr)\n return\n\n abs_x, abs_y = pin_abs(comp.x, comp.y, pin.x, pin.y,\n comp.rotation, comp.mirror_y)\n end_x = snap(abs_x + wire_dx)\n end_y = snap(abs_y + wire_dy)\n\n if wire_dx != 0 or wire_dy != 0:\n self.wire(abs_x, abs_y, end_x, end_y)\n self.label(net_label, end_x, end_y, label_angle)\n else:\n self.label(net_label, abs_x, abs_y, label_angle)\n\n def connect_pin_noconnect(self, ref: str, pin_name: str, by_number: bool = False):\n \"\"\"Place a no-connect flag on an unused pin.\"\"\"\n comp = self.placed.get(ref)\n if not comp or not self.symbol_lib:\n return\n sym_def = self.symbol_lib.get(comp.lib_id)\n if not sym_def:\n return\n pin = (sym_def.get_pin_by_number(pin_name) if by_number\n else sym_def.get_pin(pin_name))\n if not pin:\n return\n abs_x, abs_y = pin_abs(comp.x, comp.y, pin.x, pin.y,\n comp.rotation, comp.mirror_y)\n self.no_connect(abs_x, abs_y)\n\n # Alias for backward compatibility\n connect_pin_nc = connect_pin_noconnect\n\n def wire(self, x1: float, y1: float, x2: float, y2: float):\n \"\"\"Draw a wire (auto-snapped). Skips zero-length wires.\"\"\"\n x1, y1, x2, y2 = snap(x1), snap(y1), snap(x2), snap(y2)\n if x1 == x2 and y1 == y2:\n return\n self.wires.append(f\"\"\" (wire (pts (xy {x1:.2f} {y1:.2f}) (xy {x2:.2f} {y2:.2f}))\n (stroke (width 0) (type default))\n (uuid \"{uid()}\")\n )\"\"\")\n\n # Short alias\n w = wire\n\n def label(self, name: str, x: float, y: float, angle: int = 0):\n \"\"\"Place a net label (auto-snapped).\"\"\"\n x, y = snap(x), snap(y)\n self.labels.append(f\"\"\" (label \"{name}\" (at {x:.2f} {y:.2f} {angle})\n (effects (font (size 1.27 1.27)) (justify left))\n (uuid \"{uid()}\")\n )\"\"\")\n\n def no_connect(self, x: float, y: float):\n \"\"\"Place a no-connect flag (auto-snapped).\"\"\"\n x, y = snap(x), snap(y)\n self.no_connects.append(f\"\"\" (no_connect (at {x:.2f} {y:.2f})\n (uuid \"{uid()}\")\n )\"\"\")\n\n # Short alias\n nc = no_connect\n\n def text_note(self, text: str, x: float, y: float, size: float = 2.54):\n \"\"\"Add a text annotation.\"\"\"\n self.text_notes.append(f\"\"\" (text \"{text}\" (at {x:.2f} {y:.2f} 0)\n (effects (font (size {size} {size})) (justify left))\n (uuid \"{uid()}\")\n )\"\"\")\n\n def build(self, title: str = \"Schematic\", date: str = \"2026-01-01\",\n rev: str = \"1.0\", paper: str = \"A1\", comments: list = None) -> str:\n \"\"\"Generate the complete .kicad_sch file.\n IMPORTANT: Always run fix_subsymbol_names() on the output!\"\"\"\n comment_lines = \"\"\n if comments:\n for i, c in enumerate(comments, 1):\n comment_lines += f' (comment {i} \"{c}\")\\n'\n\n header = f\"\"\"(kicad_sch\n (version 20231120)\n (generator \"kicad_sch_agent\")\n (generator_version \"8.0\")\n (uuid \"{self.root_uuid}\")\n (paper \"{paper}\")\n (title_block\n (title \"{title}\")\n (date \"{date}\")\n (rev \"{rev}\")\n{comment_lines} )\"\"\"\n\n all_items = (self.components + self.wires + self.labels +\n self.no_connects + self.text_notes)\n\n return f\"\"\"{header}\n\n (lib_symbols\n{self._lib_symbols_content}\n )\n\n{chr(10).join(all_items)}\n\n (sheet_instances\n (path \"/\"\n (page \"1\")\n )\n )\n)\"\"\"\n\n\n# =============================================================================\n# Convenience helpers for 2-pin components\n# =============================================================================\n\ndef place_2pin_vertical(builder: SchematicBuilder, lib_id: str, ref: str,\n value: str, x: float, y: float,\n top_net: str, bottom_net: str,\n footprint: str = \"\", lcsc: str = \"\",\n wire_ext: float = 3.81):\n \"\"\"\n Place a 2-pin component vertically and wire both pins to net labels.\n\n Pin layout (rotation 0):\n Pin 1 at lib (0, 2.54) -> schematic TOP -> connects to top_net\n Pin 2 at lib (0, -2.54) -> schematic BOTTOM -> connects to bottom_net\n\n Wire stubs extend wire_ext mm from each pin.\n \"\"\"\n x, y = snap(x), snap(y)\n builder.place(lib_id, ref, value, x, y, footprint=footprint, lcsc=lcsc)\n p1y = snap(y - 2.54) # Pin 1 in schematic (Y negated)\n p2y = snap(y + 2.54) # Pin 2 in schematic\n builder.wire(x, p1y, x, snap(p1y - wire_ext))\n builder.label(top_net, x, snap(p1y - wire_ext))\n builder.wire(x, p2y, x, snap(p2y + wire_ext))\n builder.label(bottom_net, x, snap(p2y + wire_ext))\n\n\ndef place_2pin_horizontal(builder: SchematicBuilder, lib_id: str, ref: str,\n value: str, x: float, y: float,\n left_net: str, right_net: str,\n footprint: str = \"\", lcsc: str = \"\",\n wire_ext: float = 3.81):\n \"\"\"\n Place a 2-pin component horizontally (rotation=90) and wire both pins.\n\n Pin layout (rotation 90):\n Pin 1 at lib (0, 2.54) -> schematic RIGHT -> connects to right_net\n Pin 2 at lib (0, -2.54) -> schematic LEFT -> connects to left_net\n \"\"\"\n x, y = snap(x), snap(y)\n builder.place(lib_id, ref, value, x, y, rotation=90,\n footprint=footprint, lcsc=lcsc)\n p1x = snap(x + 2.54) # Pin 1 in schematic (rotation 90)\n p2x = snap(x - 2.54) # Pin 2\n builder.wire(p1x, y, snap(p1x + wire_ext), y)\n builder.label(right_net, snap(p1x + wire_ext), y)\n builder.wire(p2x, y, snap(p2x - wire_ext), y)\n builder.label(left_net, snap(p2x - wire_ext), y)\n\n\n# =============================================================================\n# ERC validation\n# =============================================================================\n\ndef run_erc(schematic_path: str, output_path: str = None,\n kicad_cli: str = \"kicad-cli\",\n env_vars: dict = None) -> dict:\n \"\"\"\n Run KiCad ERC check via kicad-cli and return structured results.\n\n Args:\n schematic_path: Path to the .kicad_sch file\n output_path: Where to write JSON results (default: alongside schematic)\n kicad_cli: Path to kicad-cli executable\n env_vars: Extra environment variables (e.g., KICAD9_SYMBOL_DIR,\n KICAD9_FOOTPRINT_DIR for macOS KiCad 9)\n\n Returns:\n dict with: success (bool), errors (int), warnings (int),\n error_types (dict), details (list)\n\n Note: JSON output uses sheets[].violations[] format, not top-level violations.\n This function handles both formats automatically.\n \"\"\"\n import os as _os\n\n if output_path is None:\n output_path = str(Path(schematic_path).with_suffix('.erc.json'))\n\n # Build environment with optional extra vars (needed for macOS KiCad 9)\n run_env = _os.environ.copy()\n if env_vars:\n run_env.update(env_vars)\n\n try:\n result = subprocess.run(\n [kicad_cli, \"sch\", \"erc\",\n \"--output\", output_path, \"--format\", \"json\",\n \"--severity-all\", schematic_path],\n capture_output=True, text=True, timeout=60,\n env=run_env\n )\n except FileNotFoundError:\n # Try to auto-discover kicad-cli\n found = find_kicad_cli()\n if found:\n print(f\"WARNING: kicad-cli not on PATH but found at: {found}\",\n file=sys.stderr)\n suggest_kicad_cli_symlink()\n try:\n result = subprocess.run(\n [found, \"sch\", \"erc\",\n \"--output\", output_path, \"--format\", \"json\",\n \"--severity-all\", schematic_path],\n capture_output=True, text=True, timeout=60,\n env=run_env\n )\n except Exception as e:\n return {\"success\": False, \"errors\": -1, \"warnings\": -1,\n \"total\": -1, \"details\": [],\n \"raw\": f\"kicad-cli found at {found} but failed: {e}\"}\n else:\n suggest_kicad_cli_symlink()\n return {\"success\": False, \"errors\": -1, \"warnings\": -1,\n \"total\": -1, \"details\": [], \"raw\": \"kicad-cli not found\"}\n except subprocess.TimeoutExpired:\n return {\"success\": False, \"errors\": -1, \"warnings\": -1,\n \"total\": -1, \"details\": [], \"raw\": \"timeout\"}\n\n try:\n with open(output_path) as f:\n report = json.load(f)\n except (json.JSONDecodeError, FileNotFoundError):\n return _parse_text_erc(result.stdout + result.stderr)\n\n # Handle both KiCad 8 (top-level violations) and KiCad 9 (sheets[].violations[])\n all_violations = []\n if \"violations\" in report:\n all_violations = report[\"violations\"]\n elif \"sheets\" in report:\n for sheet in report[\"sheets\"]:\n all_violations.extend(sheet.get(\"violations\", []))\n\n errors = [v for v in all_violations if v.get(\"severity\") == \"error\"]\n warnings = [v for v in all_violations if v.get(\"severity\") == \"warning\"]\n\n return {\n \"success\": len(errors) == 0,\n \"errors\": len(errors), \"warnings\": len(warnings),\n \"total\": len(errors) + len(warnings),\n \"details\": all_violations,\n \"error_types\": _categorize(errors),\n \"warning_types\": _categorize(warnings),\n }\n\n\ndef validate_and_fix_loop(schematic_path: str, fix_callback,\n max_iterations: int = 5,\n kicad_cli: str = \"kicad-cli\") -> dict:\n \"\"\"\n Automated generate -> validate -> fix loop.\n\n Args:\n schematic_path: Path to the schematic file\n fix_callback: Function(erc_result, iteration) -> bool\n Returns True if fixes were applied, False to stop\n max_iterations: Maximum fix attempts\n kicad_cli: Path to kicad-cli\n\n Returns:\n Final ERC result dict\n \"\"\"\n for i in range(max_iterations):\n print(f\"\\n=== ERC Validation Iteration {i+1}/{max_iterations} ===\")\n result = run_erc(schematic_path, kicad_cli=kicad_cli)\n print(f\"Errors: {result['errors']}, Warnings: {result['warnings']}\")\n\n if result[\"errors\"] == 0:\n print(\"No ERC errors!\")\n return result\n\n if not fix_callback(result, i):\n print(\"Fix callback returned False, stopping.\")\n return result\n\n print(f\"Reached max iterations ({max_iterations})\")\n return result\n\n\ndef _categorize(violations):\n cats = {}\n for v in violations:\n cats[v.get(\"type\", \"unknown\")] = cats.get(v.get(\"type\", \"unknown\"), 0) + 1\n return cats\n\n\ndef _parse_text_erc(text):\n errors = len(re.findall(r';\\s*error', text))\n warnings = len(re.findall(r';\\s*warning', text))\n return {\"success\": errors == 0, \"errors\": errors, \"warnings\": warnings,\n \"total\": errors + warnings, \"details\": [], \"raw\": text}\n\n\n# =============================================================================\n# kicad-cli discovery and symlink helper\n# =============================================================================\n\ndef find_kicad_cli() -> Optional[str]:\n \"\"\"\n Locate kicad-cli on the system. Returns the path if found, None otherwise.\n Checks PATH first, then common installation directories per OS.\n \"\"\"\n import shutil\n import platform\n\n found = shutil.which(\"kicad-cli\")\n if found:\n return found\n\n system = platform.system()\n\n candidates = []\n if system == \"Darwin\":\n candidates = [\n \"/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli\",\n \"/Applications/KiCad 9.0/KiCad.app/Contents/MacOS/kicad-cli\",\n \"/Applications/KiCad 8.0/KiCad.app/Contents/MacOS/kicad-cli\",\n Path.home() / \"Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli\",\n ]\n elif system == \"Linux\":\n candidates = [\n \"/usr/bin/kicad-cli\",\n \"/usr/local/bin/kicad-cli\",\n \"/snap/kicad/current/bin/kicad-cli\",\n Path.home() / \".local/bin/kicad-cli\",\n ]\n elif system == \"Windows\":\n candidates = [\n Path(r\"C:\\Program Files\\KiCad\\9.0\\bin\\kicad-cli.exe\"),\n Path(r\"C:\\Program Files\\KiCad\\8.0\\bin\\kicad-cli.exe\"),\n Path(r\"C:\\Program Files\\KiCad\\bin\\kicad-cli.exe\"),\n Path(r\"C:\\Program Files (x86)\\KiCad\\8.0\\bin\\kicad-cli.exe\"),\n ]\n\n for candidate in candidates:\n if Path(candidate).is_file():\n return str(candidate)\n\n return None\n\n\ndef suggest_kicad_cli_symlink() -> Optional[str]:\n \"\"\"\n Find kicad-cli and print instructions to make it available on PATH.\n Returns the found path, or None if not installed.\n \"\"\"\n import platform\n\n found = find_kicad_cli()\n if not found:\n system = platform.system()\n urls = {\n \"Darwin\": \"https://www.kicad.org/download/macos/\",\n \"Linux\": \"https://www.kicad.org/download/linux/\",\n \"Windows\": \"https://www.kicad.org/download/windows/\",\n }\n url = urls.get(system, \"https://www.kicad.org/download/\")\n print(f\"kicad-cli not found. Install KiCad 8 from: {url}\", file=sys.stderr)\n return None\n\n import shutil\n if shutil.which(\"kicad-cli\"):\n return found\n\n system = platform.system()\n if system in (\"Darwin\", \"Linux\"):\n print(f\"Found kicad-cli at: {found}\", file=sys.stderr)\n print(f\"To add to PATH, run:\", file=sys.stderr)\n print(f\" sudo ln -sf '{found}' /usr/local/bin/kicad-cli\", file=sys.stderr)\n elif system == \"Windows\":\n bin_dir = str(Path(found).parent)\n print(f\"Found kicad-cli at: {found}\", file=sys.stderr)\n print(f\"To add to PATH, run in PowerShell (as admin):\", file=sys.stderr)\n print(f' [Environment]::SetEnvironmentVariable(\"PATH\", $env:PATH + \";{bin_dir}\", \"User\")',\n file=sys.stderr)\n\n return found\n\n\n# =============================================================================\n# ERC fixing utilities — for modifying existing schematics\n# =============================================================================\n\ndef find_block(content: str, start_pos: int) -> tuple:\n \"\"\"\n Find a balanced parenthesized block starting at start_pos.\n\n Handles quoted strings correctly (parentheses inside quotes are ignored).\n\n Args:\n content: The full file content\n start_pos: Position of the opening '('\n\n Returns:\n (block_text, end_pos) where end_pos is the position after the closing ')'\n\n Raises:\n ValueError: If no '(' at start_pos or parentheses are unbalanced\n\n Example:\n >>> content = '(symbol \"Device:R\" (pin passive line))'\n >>> text, end = find_block(content, 0)\n >>> text\n '(symbol \"Device:R\" (pin passive line))'\n \"\"\"\n if content[start_pos] != '(':\n raise ValueError(f\"Expected '(' at position {start_pos}, got '{content[start_pos]}'\")\n depth = 0\n i = start_pos\n in_string = False\n while i \u003c len(content):\n c = content[i]\n if c == '\"' and (i == 0 or content[i-1] != '\\\\'):\n in_string = not in_string\n elif not in_string:\n if c == '(':\n depth += 1\n elif c == ')':\n depth -= 1\n if depth == 0:\n return content[start_pos:i+1], i + 1\n i += 1\n raise ValueError(f\"Unbalanced parentheses starting at {start_pos}\")\n\n\ndef remove_block_with_whitespace(content: str, block_start: int, block_end: int) -> str:\n \"\"\"\n Remove a block and its surrounding whitespace/newlines cleanly.\n\n Looks backwards for preceding whitespace/tabs and a newline,\n and forward for trailing whitespace and a newline. Removes all of it\n so no blank lines are left behind.\n\n Args:\n content: The full file content\n block_start: Start position of the block (the opening paren)\n block_end: End position of the block (after the closing paren)\n\n Returns:\n Modified content with the block and surrounding whitespace removed\n \"\"\"\n start = block_start\n while start > 0 and content[start-1] in ' \\t':\n start -= 1\n if start > 0 and content[start-1] == '\\n':\n start -= 1\n end = block_end\n while end \u003c len(content) and content[end] in ' \\t':\n end += 1\n if end \u003c len(content) and content[end] == '\\n':\n end += 1\n return content[:start] + content[end:]\n\n\ndef extract_embedded_symbol(content: str, symbol_name: str) -> Optional[str]:\n \"\"\"\n Extract an embedded lib_symbol block by its full name.\n\n Searches the lib_symbols section of a .kicad_sch file for a symbol\n with the given name (e.g., 'Connector:Conn_01x04' or 'CubeSat_SDR:AMS1117')\n and returns the complete s-expression block.\n\n Args:\n content: The full .kicad_sch file content\n symbol_name: Full prefixed symbol name (e.g., 'CubeSat_SDR:AMS1117')\n\n Returns:\n The symbol block text, or None if not found\n \"\"\"\n pattern = f'(symbol \"{symbol_name}\"'\n pos = content.find(pattern)\n if pos == -1:\n return None\n block_text, _ = find_block(content, pos)\n return block_text\n\n\ndef convert_embedded_to_library(block_text: str, old_prefix: str, new_name: str) -> str:\n \"\"\"\n Convert an embedded lib_symbol to standalone library format.\n\n In embedded format: top-level is (symbol \"Prefix:Name\" ...)\n In library format: top-level is (symbol \"Name\" ...)\n Sub-symbols (Name_0_1, Name_1_1) remain unchanged in both formats.\n\n Args:\n block_text: The extracted symbol block\n old_prefix: The library prefix to remove (e.g., \"Connector\", \"CubeSat_SDR\")\n new_name: The symbol name without prefix (e.g., \"Conn_01x04\")\n\n Returns:\n The converted block suitable for a .kicad_sym library file\n \"\"\"\n return block_text.replace(\n f'(symbol \"{old_prefix}:{new_name}\"',\n f'(symbol \"{new_name}\"',\n 1\n )\n\n\ndef find_by_uuid(content: str, uuid: str) -> Optional[int]:\n \"\"\"\n Find the position of a UUID string in the content.\n\n Args:\n content: The full file content\n uuid: The UUID to search for\n\n Returns:\n Position of the UUID marker, or None if not found\n \"\"\"\n marker = f'(uuid \"{uuid}\")'\n pos = content.find(marker)\n return pos if pos != -1 else None\n\n\ndef remove_by_uuid(content: str, uuid: str, element_type: str) -> str:\n \"\"\"\n Remove an element (symbol, wire, no_connect, label) by its UUID.\n\n Searches backwards from the UUID to find the containing block of the\n specified type, then removes it with surrounding whitespace.\n\n Args:\n content: The full file content\n uuid: The UUID of the element to remove\n element_type: The s-expression type ('symbol', 'wire', 'no_connect', 'label')\n\n Returns:\n Modified content with the element removed\n\n Raises:\n ValueError: If UUID not found or parent block not found\n\n Example:\n >>> content = remove_by_uuid(content, \"ac2d9711-...\", \"symbol\")\n \"\"\"\n marker = f'(uuid \"{uuid}\")'\n pos = content.find(marker)\n if pos == -1:\n raise ValueError(f\"UUID '{uuid}' not found in content\")\n\n # Search backwards for the containing element\n search_term = f'({element_type}'\n block_start = content.rfind(search_term, 0, pos)\n if block_start == -1:\n raise ValueError(f\"Could not find parent ({element_type} block for UUID '{uuid}'\")\n\n block_text, block_end = find_block(content, block_start)\n if uuid not in block_text:\n raise ValueError(f\"Found ({element_type} block but UUID '{uuid}' not inside it\")\n\n return remove_block_with_whitespace(content, block_start, block_end)\n\n\ndef replace_lib_id(content: str, old_id: str, new_id: str) -> tuple:\n \"\"\"\n Replace a lib_id across all symbol instances and embedded lib_symbols.\n\n Updates both:\n - (lib_id \"old_id\") in placed symbol instances\n - (symbol \"old_id\" ...) in the embedded lib_symbols section\n\n Args:\n content: The full .kicad_sch file content\n old_id: The old lib_id (e.g., \"Connector:Conn_01x04\")\n new_id: The new lib_id (e.g., \"CubeSat_SDR:Conn_01x04\")\n\n Returns:\n (modified_content, count) where count is total replacements made\n\n Example:\n >>> content, n = replace_lib_id(content, \"Connector:SMA\", \"CubeSat_SDR:SMA\")\n >>> print(f\"Replaced {n} occurrences\")\n \"\"\"\n count = 0\n\n # Replace in placed symbol instances\n old_str = f'(lib_id \"{old_id}\")'\n new_str = f'(lib_id \"{new_id}\")'\n c = content.count(old_str)\n content = content.replace(old_str, new_str)\n count += c\n\n # Replace in embedded lib_symbol key\n old_sym = f'(symbol \"{old_id}\"'\n new_sym = f'(symbol \"{new_id}\"'\n if old_sym in content:\n content = content.replace(old_sym, new_sym)\n count += 1\n\n return content, count\n\n\ndef replace_footprint(content: str, old_fp: str, new_fp: str) -> tuple:\n \"\"\"\n Replace a footprint reference across all symbol instances.\n\n Args:\n content: The full .kicad_sch file content\n old_fp: The old footprint (e.g., \"Button_Switch_SMD:SW_Push_1P1T_NO_6x3.5mm\")\n new_fp: The new footprint (e.g., \"Button_Switch_SMD:SW_Push_1P1T_NO_CK_PTS125Sx43SMTR\")\n\n Returns:\n (modified_content, count) where count is number of replacements\n\n Example:\n >>> content, n = replace_footprint(content,\n ... \"Connector_Coaxial:SMA_Amphenol_901-143_Vertical\",\n ... \"Connector_Coaxial:SMA_Amphenol_901-144_Vertical\")\n \"\"\"\n old_str = f'\"Footprint\" \"{old_fp}\"'\n new_str = f'\"Footprint\" \"{new_fp}\"'\n count = content.count(old_str)\n content = content.replace(old_str, new_str)\n return content, count\n\n\ndef fix_annotation_suffixes(content: str) -> tuple:\n \"\"\"\n Ensure all reference designators end with a digit (KiCad 9 requirement).\n\n KiCad 9's GUI requires all references to end with a number. References\n like 'C_RX1B_N' or 'J_PWR' cause \"Item not annotated\" errors. This\n function appends '1' to any reference that doesn't end with a digit.\n\n Handles both:\n - (property \"Reference\" \"C_RX1B_N\" ...) in symbol properties\n - (reference \"C_RX1B_N\") in instance paths\n\n Args:\n content: The full .kicad_sch file content\n\n Returns:\n (modified_content, fixed_refs) where fixed_refs is list of refs that were fixed\n\n Example:\n >>> content, refs = fix_annotation_suffixes(content)\n >>> print(f\"Fixed {len(refs)} references: {refs}\")\n \"\"\"\n # Find all instance references (excluding hidden #FLG, #PWR)\n refs = re.findall(r'\\(reference \"([^\"]+)\"\\)', content)\n visible_refs = [r for r in refs if not r.startswith('#')]\n no_digit = sorted(set(r for r in visible_refs if r and not r[-1].isdigit()))\n\n for ref in no_digit:\n new_ref = ref + \"1\"\n # Replace in property \"Reference\"\n old_prop = f'\"Reference\" \"{ref}\"'\n new_prop = f'\"Reference\" \"{new_ref}\"'\n content = content.replace(old_prop, new_prop)\n\n # Replace in instance (reference ...)\n old_inst = f'(reference \"{ref}\")'\n new_inst = f'(reference \"{new_ref}\")'\n content = content.replace(old_inst, new_inst)\n\n return content, no_digit\n\n\ndef create_pwr_flag_block(x: float, y: float, ref_num: int,\n project_name: str, root_uuid: str) -> str:\n \"\"\"\n Generate a PWR_FLAG symbol s-expression block for insertion into a schematic.\n\n Use this to fix 'power_pin_not_driven' ERC errors. Place the PWR_FLAG\n on a wire connected to the power input pin.\n\n Args:\n x, y: Position in schematic coordinates (should be on a wire)\n ref_num: Reference number (e.g., 7 for #FLG07)\n project_name: Project name for the instances section\n root_uuid: Root sheet UUID for the instances path\n\n Returns:\n Complete s-expression block ready to insert into the schematic\n\n Example:\n >>> block = create_pwr_flag_block(34.29, 77.47, 7, \"cubesat_sdr\",\n ... \"5fb33c66-7637-43ae-9eef-34b4f23f6cfb\")\n \"\"\"\n sym_uuid = uid()\n pin_uuid = uid()\n ref = f\"#FLG{ref_num:02d}\"\n\n return f\"\"\"\\t(symbol\n\\t\\t(lib_id \"power:PWR_FLAG\")\n\\t\\t(at {x} {y} 0)\n\\t\\t(unit 1)\n\\t\\t(exclude_from_sim no)\n\\t\\t(in_bom yes)\n\\t\\t(on_board yes)\n\\t\\t(dnp no)\n\\t\\t(uuid \"{sym_uuid}\")\n\\t\\t(property \"Reference\" \"{ref}\"\n\\t\\t\\t(at {x} {y - 2.54} 0)\n\\t\\t\\t(effects\n\\t\\t\\t\\t(font\n\\t\\t\\t\\t\\t(size 1.27 1.27)\n\\t\\t\\t\\t)\n\\t\\t\\t\\t(hide yes)\n\\t\\t\\t)\n\\t\\t)\n\\t\\t(property \"Value\" \"PWR_FLAG\"\n\\t\\t\\t(at {x} {y - 3.81} 0)\n\\t\\t\\t(effects\n\\t\\t\\t\\t(font\n\\t\\t\\t\\t\\t(size 0.8 0.8)\n\\t\\t\\t\\t)\n\\t\\t\\t)\n\\t\\t)\n\\t\\t(property \"Footprint\" \"\"\n\\t\\t\\t(at {x} {y} 0)\n\\t\\t\\t(effects\n\\t\\t\\t\\t(font\n\\t\\t\\t\\t\\t(size 1.27 1.27)\n\\t\\t\\t\\t)\n\\t\\t\\t\\t(hide yes)\n\\t\\t\\t)\n\\t\\t)\n\\t\\t(property \"Datasheet\" \"\"\n\\t\\t\\t(at {x} {y} 0)\n\\t\\t\\t(effects\n\\t\\t\\t\\t(font\n\\t\\t\\t\\t\\t(size 1.27 1.27)\n\\t\\t\\t\\t)\n\\t\\t\\t)\n\\t\\t)\n\\t\\t(property \"Description\" \"\"\n\\t\\t\\t(at {x} {y} 0)\n\\t\\t\\t(effects\n\\t\\t\\t\\t(font\n\\t\\t\\t\\t\\t(size 1.27 1.27)\n\\t\\t\\t\\t)\n\\t\\t\\t)\n\\t\\t)\n\\t\\t(pin \"1\"\n\\t\\t\\t(uuid \"{pin_uuid}\")\n\\t\\t)\n\\t\\t(instances\n\\t\\t\\t(project \"{project_name}\"\n\\t\\t\\t\\t(path \"/{root_uuid}\"\n\\t\\t\\t\\t\\t(reference \"{ref}\")\n\\t\\t\\t\\t\\t(unit 1)\n\\t\\t\\t\\t)\n\\t\\t\\t)\n\\t\\t)\n\\t)\"\"\"\n\n\ndef suppress_erc_warning(pro_path: str, rule_name: str) -> None:\n \"\"\"\n Suppress an ERC warning type in the .kicad_pro file.\n\n Only use for warnings that are known-safe (e.g., lib_symbol_mismatch\n during KiCad 8→9 migration). Never suppress errors.\n\n Args:\n pro_path: Path to the .kicad_pro file\n rule_name: The rule to suppress (e.g., 'lib_symbol_mismatch')\n \"\"\"\n with open(pro_path) as f:\n pro = json.load(f)\n\n if 'erc' not in pro:\n pro['erc'] = {}\n if 'rule_severities' not in pro['erc']:\n pro['erc']['rule_severities'] = {}\n\n pro['erc']['rule_severities'][rule_name] = 'ignore'\n\n with open(pro_path, 'w') as f:\n json.dump(pro, f, indent=2)\n f.write('\\n')\n\n\nif __name__ == \"__main__\":\n print(\"KiCad Schematic Helper Library v3\")\n print(f\"Grid: {GRID} mm\")\n print()\n\n # Test coordinate utilities\n print(\"=== Coordinate Utilities ===\")\n print(f\"snap(42.5) = {snap(42.5)}\")\n print(f\"pin_abs(320, 200, -17.78, 25.40, rotation=0) = \"\n f\"{pin_abs(320, 200, -17.78, 25.40, rotation=0)}\")\n print(f\"pin_abs(320, 200, 0, 2.54, rotation=90) = \"\n f\"{pin_abs(320, 200, 0, 2.54, rotation=90)}\")\n print()\n\n # Test ERC fixing utilities\n print(\"=== ERC Fixing Utilities ===\")\n test_content = '(symbol (lib_id \"test\") (at 0 0 0) (uuid \"abc-123\"))'\n block, end = find_block(test_content, 0)\n print(f\"find_block: found block of {len(block)} chars, end at {end}\")\n\n test_sch = 'before\\n\\t(wire (pts (xy 0 0) (xy 1 1))\\n\\t\\t(uuid \"wire-1\")\\n\\t)\\nafter'\n cleaned = remove_block_with_whitespace(test_sch, 8, test_sch.index(')') + 1)\n print(f\"remove_block_with_whitespace: '{test_sch[:20]}...' -> '{cleaned[:20]}...'\")\n\n # Test annotation suffix fixing\n test_refs = '(reference \"C_RX1B_N\")(reference \"R1\")(reference \"J_PWR\")'\n fixed, refs = fix_annotation_suffixes(test_refs)\n print(f\"fix_annotation_suffixes: fixed {len(refs)} refs: {refs}\")\n print()\n\n # Check kicad-cli\n print(\"=== kicad-cli ===\")\n cli_path = find_kicad_cli()\n if cli_path:\n print(f\"kicad-cli found: {cli_path}\")\n else:\n suggest_kicad_cli_symlink()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":48928,"content_sha256":"0e919f7a09c6276815a00de4c90a937a1d69532cfaa914b07456c6ac454c901b"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"KiCad Schematic Agent","type":"text"}]},{"type":"paragraph","content":[{"text":"Generate ERC-clean KiCad 8/9 schematics by writing Python scripts that use computed pin positions — never guess coordinates. Also fix ERC errors on existing schematics and handle KiCad 8→9 migration.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Critical Principle","type":"text"}]},{"type":"paragraph","content":[{"text":"The #1 cause of broken schematics is guessed pin positions.","type":"text","marks":[{"type":"strong"}]},{"text":" When connecting labels to IC pins, you MUST compute exact coordinates using the symbol definition's pin positions and the coordinate transform formula. The helper library in ","type":"text"},{"text":"scripts/kicad_sch_helpers.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" does this automatically.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"The Y-axis Trap (Most Common Bug)","type":"text"}]},{"type":"paragraph","content":[{"text":"Symbol libraries (.kicad_sym) use ","type":"text"},{"text":"Y-up","type":"text","marks":[{"type":"strong"}]},{"text":" (math convention). Schematics (.kicad_sch) use ","type":"text"},{"text":"Y-down","type":"text","marks":[{"type":"strong"}]},{"text":" (screen convention). This means you MUST negate the Y coordinate when transforming from library to schematic space. Forgetting this will place labels 10-50mm away from their pins, causing massive pin_not_connected and label_dangling errors.","type":"text"}]},{"type":"paragraph","content":[{"text":"Transform formula","type":"text","marks":[{"type":"strong"}]},{"text":" — pin at library (px, py), symbol placed at schematic (sx, sy) with rotation R:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rotation 0","type":"text","marks":[{"type":"strong"}]},{"text":": schematic position = (sx + px, sy ","type":"text"},{"text":"-","type":"text","marks":[{"type":"strong"}]},{"text":" py)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rotation 90","type":"text","marks":[{"type":"strong"}]},{"text":": schematic position = (sx + py, sy + px)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rotation 180","type":"text","marks":[{"type":"strong"}]},{"text":": schematic position = (sx - px, sy ","type":"text"},{"text":"+","type":"text","marks":[{"type":"strong"}]},{"text":" py)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rotation 270","type":"text","marks":[{"type":"strong"}]},{"text":": schematic position = (sx - py, sy - px)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Always use ","type":"text"},{"text":"pin_abs()","type":"text","marks":[{"type":"code_inline"}]},{"text":" from the helper library — never compute these by hand.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Architecture","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"User describes circuit\n |\nRead symbol libraries (.kicad_sym) to get pin positions\n |\nBuild pin position dictionaries for every multi-pin IC\n |\nWrite Python script using SchematicBuilder (from helper library)\n - Use connect_pin() for IC pins (computes positions automatically)\n - Use place_2pin_vertical() for passives (knows pin 1/2 positions)\n |\nGenerate .kicad_sch file\n |\nPost-process with fix_subsymbol_names()\n |\nRun ERC validation: kicad-cli sch erc --format json\n |\nParse errors -> fix script -> regenerate -> repeat (max 5 iterations)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step-by-step Workflow","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"0. Ensure kicad-cli is Available","type":"text"}]},{"type":"paragraph","content":[{"text":"Before running any ERC validation, verify that ","type":"text"},{"text":"kicad-cli","type":"text","marks":[{"type":"code_inline"}]},{"text":" is on the system PATH. Run:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"which kicad-cli 2>/dev/null || where kicad-cli 2>/dev/null","type":"text"}]},{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"not found","type":"text","marks":[{"type":"strong"}]},{"text":", check for a local KiCad installation and offer to create a symlink:","type":"text"}]},{"type":"paragraph","content":[{"text":"macOS:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Check if KiCad is installed as an app\nKICAD_CLI=\"/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli\"\nif [ -f \"$KICAD_CLI\" ]; then\n echo \"Found kicad-cli inside KiCad.app. Creating symlink...\"\n sudo ln -sf \"$KICAD_CLI\" /usr/local/bin/kicad-cli\n echo \"Done! kicad-cli is now available on PATH.\"\nelse\n echo \"KiCad not found. Install from https://www.kicad.org/download/macos/\"\nfi","type":"text"}]},{"type":"paragraph","content":[{"text":"Linux:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# kicad-cli is typically installed alongside KiCad via package manager\n# Check common locations\nfor p in /usr/bin/kicad-cli /usr/local/bin/kicad-cli /snap/kicad/current/bin/kicad-cli; do\n if [ -f \"$p\" ]; then\n echo \"Found kicad-cli at $p\"\n # If not on PATH, symlink it\n if ! command -v kicad-cli &>/dev/null; then\n sudo ln -sf \"$p\" /usr/local/bin/kicad-cli\n fi\n break\n fi\ndone\n# If still not found:\n# Ubuntu/Debian: sudo apt install kicad\n# Fedora: sudo dnf install kicad\n# Arch: sudo pacman -S kicad\n# Or install from https://www.kicad.org/download/linux/","type":"text"}]},{"type":"paragraph","content":[{"text":"Windows:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"powershell"},"content":[{"text":"# Check standard install path\n$kicadCli = \"C:\\Program Files\\KiCad\\8.0\\bin\\kicad-cli.exe\"\nif (Test-Path $kicadCli) {\n Write-Host \"Found kicad-cli. Add to PATH:\"\n Write-Host ' [Environment]::SetEnvironmentVariable(\"PATH\", $env:PATH + \";C:\\Program Files\\KiCad\\8.0\\bin\", \"User\")'\n} else {\n Write-Host \"KiCad not found. Install from https://www.kicad.org/download/windows/\"\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"Tell the user what you found and ask for confirmation before creating any symlinks. If kicad-cli is truly not installed, provide the download link for their OS and stop — ERC validation requires it.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Understand the Circuit","type":"text"}]},{"type":"paragraph","content":[{"text":"Before writing any code, gather:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Component list with specific part numbers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Power architecture (voltage rails, regulators)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Signal connections (which pins connect to which)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Symbol libraries needed (standard KiCad libs + any custom .kicad_sym files)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Read Symbol Libraries (NON-NEGOTIABLE)","type":"text"}]},{"type":"paragraph","content":[{"text":"For ","type":"text"},{"text":"every IC and multi-pin component","type":"text","marks":[{"type":"strong"}]},{"text":", read its .kicad_sym definition to get exact pin names, numbers, positions, and types. You cannot connect pins correctly without this data.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from kicad_sch_helpers import SymbolLibrary\n\nlib = SymbolLibrary()\nlib.load_from_kicad_sym(\"path/to/library.kicad_sym\")\n\n# Now you know exact pin positions\nad9363 = lib.get(\"AD9363ABCZ\")\nfor pin in ad9363.pins:\n print(f\"{pin.name} ({pin.number}): at ({pin.x}, {pin.y}), type={pin.pin_type}\")","type":"text"}]},{"type":"paragraph","content":[{"text":"For manual/inline approaches","type":"text","marks":[{"type":"strong"}]},{"text":", build a pin dictionary from the library:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# Extract from .kicad_sym file — these are LIBRARY coordinates (Y-up)\nAD_PINS = {\n 'TX1A_P': (-17.78, 25.40),\n 'TX1A_N': (-17.78, 22.86),\n 'SPI_CLK': (-17.78, -10.16),\n # ... all pins\n}\n\n# SOT-23-5 packages (common for LDOs like AP2112K, ME6211):\nSOT5_PINS = {\n 'VIN': (-7.62, 2.54), # Pin 1 - top left\n 'GND': ( 0.00, -7.62), # Pin 2 - bottom center\n 'EN': (-7.62, -2.54), # Pin 3 - bottom left\n 'NC': ( 7.62, -2.54), # Pin 4 - bottom right \u003c- NOT VOUT!\n 'VOUT': ( 7.62, 2.54), # Pin 5 - top right \u003c- THIS is VOUT!\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"WARNING — SOT-23-5 pin trap:","type":"text","marks":[{"type":"strong"}]},{"text":" VOUT is at (7.62, ","type":"text"},{"text":"+2.54","type":"text","marks":[{"type":"strong"}]},{"text":") and NC is at (7.62, ","type":"text"},{"text":"-2.54","type":"text","marks":[{"type":"strong"}]},{"text":"). These are only 5.08mm apart. Confusing them means your LDO output goes nowhere. Always verify from the actual library file.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Write the Generator Script","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"SchematicBuilder","type":"text","marks":[{"type":"code_inline"}]},{"text":" for all schematic construction. The key method is ","type":"text"},{"text":"connect_pin()","type":"text","marks":[{"type":"code_inline"}]},{"text":" which computes exact pin positions automatically:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from kicad_sch_helpers import SchematicBuilder, SymbolLibrary, snap\n\nlib = SymbolLibrary()\nlib.load_from_kicad_sym(\"custom_symbols.kicad_sym\")\n\nsch = SchematicBuilder(symbol_lib=lib, project_name=\"my_project\")\nsch.set_lib_symbols(lib_symbols_content) # Raw S-expression for embedded symbols\n\n# Place an IC\nsch.place(\"CubeSat_SDR:AD9363ABCZ\", \"U1\", \"AD9363ABCZ\",\n x=320, y=200, footprint=\"CubeSat_SDR:AD9363_BGA144\")\n\n# Connect pins by NAME — coordinates computed automatically\nsch.connect_pin(\"U1\", \"TX1A_P\", \"TX1A_P\", wire_dx=-5.08)\nsch.connect_pin(\"U1\", \"SPI_CLK\", \"SPI_CLK\", wire_dy=-5.08)\nsch.connect_pin(\"U1\", \"GND\", \"GND\", wire_dy=5.08)\n\n# For unused pins, add no-connect flags\nsch.connect_pin_noconnect(\"U1\", \"AUXDAC1\")\n\n# For 2-pin passives, use convenience helpers\nfrom kicad_sch_helpers import place_2pin_vertical\nplace_2pin_vertical(sch, \"Device:C\", \"C1\", \"100nF\",\n x=snap(230), y=snap(155),\n top_net=\"VCC_3V3\", bottom_net=\"GND\",\n footprint=\"Capacitor_SMD:C_0402_1005Metric\")","type":"text"}]},{"type":"paragraph","content":[{"text":"If using inline pin dictionaries","type":"text","marks":[{"type":"strong"}]},{"text":" (without SymbolLibrary), use ","type":"text"},{"text":"pin_abs()","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from kicad_sch_helpers import pin_abs, snap\n\nGRID = 1.27\n\ndef wl(sch, sx, sy, pin_name, pins_dict, net, dx=0, dy=0, rot=0, label_angle=0):\n \"\"\"Wire + Label: connect an IC pin to a net label.\"\"\"\n px, py = pin_abs(sx, sy, pins_dict[pin_name][0], pins_dict[pin_name][1], rot)\n ex, ey = snap(px + dx), snap(py + dy)\n if dx != 0 or dy != 0:\n sch.w(px, py, ex, ey)\n sch.label(net, ex, ey, label_angle)\n\n# Usage:\nwl(sch, 320, 200, 'TX1A_P', AD_PINS, \"TX1A_P\", dx=-7.62)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Handle the lib_symbols Section","type":"text"}]},{"type":"paragraph","content":[{"text":"Every symbol referenced must be embedded in the schematic's lib_symbols. Three critical rules:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Parent symbols","type":"text","marks":[{"type":"strong"}]},{"text":" use the full lib_id: ","type":"text"},{"text":"(symbol \"Device:R\" ...)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sub-symbols","type":"text","marks":[{"type":"strong"}]},{"text":" must NOT have the library prefix: ","type":"text"},{"text":"(symbol \"R_0_1\" ...)","type":"text","marks":[{"type":"code_inline"}]},{"text":" not ","type":"text"},{"text":"(symbol \"Device:R_0_1\" ...)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Always post-process","type":"text","marks":[{"type":"strong"}]},{"text":" with ","type":"text"},{"text":"fix_subsymbol_names()","type":"text","marks":[{"type":"code_inline"}]},{"text":" to catch any mistakes","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"fix_subsymbol_names()","type":"text","marks":[{"type":"code_inline"}]},{"text":" as a post-processing step:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from kicad_sch_helpers import fix_subsymbol_names\n\ncontent = sch.build(title=\"My Schematic\")\ncontent = fix_subsymbol_names(content)","type":"text"}]},{"type":"paragraph","content":[{"text":"The regex-based fixer handles nested sub-symbols at any depth and any library prefix format.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Grid Snapping (Prevents 90% of Warnings)","type":"text"}]},{"type":"paragraph","content":[{"text":"Every coordinate","type":"text","marks":[{"type":"strong"}]},{"text":" in the schematic must be a multiple of 1.27mm. Use ","type":"text"},{"text":"snap()","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from kicad_sch_helpers import snap\n\n# snap() rounds to nearest 1.27mm grid point\nx = snap(123.45) # -> 124.46 (nearest multiple of 1.27)","type":"text"}]},{"type":"paragraph","content":[{"text":"Apply ","type":"text"},{"text":"snap()","type":"text","marks":[{"type":"code_inline"}]},{"text":" to: component positions, wire endpoints, label positions, no-connect positions, and PWR_FLAG positions. The ","type":"text"},{"text":"connect_pin()","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"pin_abs()","type":"text","marks":[{"type":"code_inline"}]},{"text":" functions do this automatically.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. Add PWR_FLAG Symbols","type":"text"}]},{"type":"paragraph","content":[{"text":"For every power net that originates from a voltage regulator (not a power symbol), add a PWR_FLAG to prevent \"power_pin_not_driven\" errors:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# Define PWR_FLAG in lib_symbols (see references/kicad_sexpression_format.md)\n# Then place on each power net:\nsch.place_pwr_flag(x=70, y=78, net_name=\"VCC_3V3A\")","type":"text"}]},{"type":"paragraph","content":[{"text":"Rule of thumb","type":"text","marks":[{"type":"strong"}]},{"text":": If a net is driven by a component whose output pin type is ","type":"text"},{"text":"passive","type":"text","marks":[{"type":"code_inline"}]},{"text":" (not ","type":"text"},{"text":"power_out","type":"text","marks":[{"type":"code_inline"}]},{"text":"), that net needs a PWR_FLAG. This includes most LDO regulators.","type":"text"}]},{"type":"paragraph","content":[{"text":"Also place PWR_FLAG on GND nets that don't have a dedicated GND power symbol driving them.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"7. No-Connect Flags","type":"text"}]},{"type":"paragraph","content":[{"text":"Every unused pin on every IC MUST have a no-connect flag. Missing no-connect flags cause ","type":"text"},{"text":"pin_not_connected","type":"text","marks":[{"type":"code_inline"}]},{"text":" errors.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# Using SchematicBuilder:\nsch.connect_pin_noconnect(\"U1\", \"AUXDAC1\")\n\n# Or manually with pin_abs:\npx, py = pin_abs(sx, sy, pin_x, pin_y, rotation)\nsch.nc(px, py)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"8. Validate with kicad-cli","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from kicad_sch_helpers import run_erc\n\nresult = run_erc(\"output/schematic.kicad_sch\")\nprint(f\"Errors: {result['errors']}, Warnings: {result['warnings']}\")\n\nif result['errors'] > 0:\n for detail in result['details']:\n if detail.get('severity') == 'error':\n print(f\" {detail['type']}: {detail.get('description', '')}\")","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"9. Automated Fix Loop","type":"text"}]},{"type":"paragraph","content":[{"text":"For complex schematics, use the validation loop:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from kicad_sch_helpers import validate_and_fix_loop\n\ndef my_fixer(erc_result, iteration):\n \"\"\"Analyze ERC errors and apply fixes. Return True if fixes applied.\"\"\"\n error_types = erc_result.get('error_types', {})\n\n if 'pin_not_connected' in error_types:\n # Read the schematic, find unconnected pins, add connections\n # ... fix logic ...\n return True\n\n if 'label_dangling' in error_types:\n # Move labels to correct pin positions\n # ... fix logic ...\n return True\n\n return False # No fixable errors found\n\nfinal = validate_and_fix_loop(\"output/schematic.kicad_sch\", my_fixer)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Fixing ERC on Existing Schematics","type":"text"}]},{"type":"paragraph","content":[{"text":"When the user has an existing ","type":"text"},{"text":".kicad_sch","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ERC errors (not generating a new schematic), use this workflow.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1: Run ERC with JSON Output","type":"text"}]},{"type":"paragraph","content":[{"text":"Always","type":"text","marks":[{"type":"strong"}]},{"text":" use ","type":"text"},{"text":"--format json -o file.json --severity-all","type":"text","marks":[{"type":"code_inline"}]},{"text":". Never pipe kicad-cli output to stdout — it writes JSON to the file specified by ","type":"text"},{"text":"-o","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"On macOS, kicad-cli needs environment variables to find the standard libraries:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"KICAD9_SYMBOL_DIR=\"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols\" \\\nKICAD9_FOOTPRINT_DIR=\"/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints\" \\\n/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli sch erc \\\n --format json --severity-all -o /tmp/erc_result.json schematic.kicad_sch","type":"text"}]},{"type":"paragraph","content":[{"text":"Or use the helper:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from kicad_sch_helpers import run_erc\nresult = run_erc(\"schematic.kicad_sch\", env_vars={\n \"KICAD9_SYMBOL_DIR\": \"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols\",\n \"KICAD9_FOOTPRINT_DIR\": \"/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints\",\n})","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2: Parse and Categorize Violations","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import json\nwith open(\"/tmp/erc_result.json\") as f:\n report = json.load(f)\n\nviolations = []\nfor sheet in report.get(\"sheets\", []):\n violations.extend(sheet.get(\"violations\", []))\n\n# Categorize by severity and type\nerrors = [v for v in violations if v.get(\"severity\") == \"error\"]\nwarnings = [v for v in violations if v.get(\"severity\") == \"warning\"]\nby_type = {}\nfor v in violations:\n t = v.get(\"type\", \"unknown\")\n by_type.setdefault(t, []).append(v)\n\nprint(f\"Errors: {len(errors)}, Warnings: {len(warnings)}\")\nfor t, items in sorted(by_type.items()):\n print(f\" {t}: {len(items)}\")","type":"text"}]},{"type":"paragraph","content":[{"text":"Note:","type":"text","marks":[{"type":"strong"}]},{"text":" The JSON format nests violations under ","type":"text"},{"text":"sheets[].violations[]","type":"text","marks":[{"type":"code_inline"}]},{"text":", not at the top level.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3: Write Targeted Python Fix Scripts","type":"text"}]},{"type":"paragraph","content":[{"text":"Never manually edit ","type":"text","marks":[{"type":"strong"}]},{"text":".kicad_sch","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" files","type":"text","marks":[{"type":"strong"}]},{"text":" — always write Python scripts. The s-expression format is sensitive to whitespace and parenthesis balance.","type":"text"}]},{"type":"paragraph","content":[{"text":"Key utility functions (all in ","type":"text"},{"text":"scripts/kicad_sch_helpers.py","type":"text","marks":[{"type":"code_inline"}]},{"text":"):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from kicad_sch_helpers import (\n find_block, # Find balanced parenthesized block\n remove_block_with_whitespace, # Clean removal preserving formatting\n extract_embedded_symbol, # Extract from lib_symbols section\n convert_embedded_to_library, # Embedded → standalone library format\n find_by_uuid, # Locate element by UUID\n remove_by_uuid, # Remove element by UUID\n replace_lib_id, # Bulk lib_id replacement\n replace_footprint, # Bulk footprint replacement\n fix_annotation_suffixes, # Add numeric suffixes to bare refs\n create_pwr_flag_block, # Generate PWR_FLAG s-expression\n)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 4: Common Fix Patterns","type":"text"}]},{"type":"paragraph","content":[{"text":"Remove a symbol by UUID:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"content = remove_by_uuid(content, \"uuid-string\", \"symbol\")","type":"text"}]},{"type":"paragraph","content":[{"text":"Replace a lib_id across all instances:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"content = replace_lib_id(content, \"Connector:Conn_01x04\", \"CubeSat_SDR:Conn_01x04\")","type":"text"}]},{"type":"paragraph","content":[{"text":"Add PWR_FLAG to fix power_pin_not_driven:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"pwr_block = create_pwr_flag_block(\n x=34.29, y=77.47, ref_num=7,\n project_name=\"my_project\",\n root_uuid=\"5fb33c66-7637-43ae-9eef-34b4f23f6cfb\"\n)\n# Insert before the final closing paren\nlast_close = content.rstrip().rfind(')')\ncontent = content[:last_close] + '\\n' + pwr_block + '\\n' + content[last_close:]","type":"text"}]},{"type":"paragraph","content":[{"text":"Suppress warnings in .kicad_pro:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import json\nwith open(\"project.kicad_pro\") as f:\n pro = json.load(f)\npro[\"erc\"][\"rule_severities\"][\"lib_symbol_mismatch\"] = \"ignore\"\nwith open(\"project.kicad_pro\", \"w\") as f:\n json.dump(pro, f, indent=2)\n f.write('\\n')","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 5: Iterative Fix-Verify Loop","type":"text"}]},{"type":"paragraph","content":[{"text":"Always run ERC after each batch of fixes. Some fixes expose new issues:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ERC → parse results → categorize","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fix the highest-priority errors first (see error priority in references)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ERC again → verify error count dropped","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Repeat until 0 errors (warnings may be suppressed if justified)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Back up the schematic before running fix scripts.","type":"text","marks":[{"type":"strong"}]},{"text":" Use ","type":"text"},{"text":"shutil.copy2()","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"KiCad 8→9 Migration","type":"text"}]},{"type":"paragraph","content":[{"text":"When validating a KiCad 8 schematic with KiCad 9, expect these categories of issues:","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Symbol Renames (lib_symbol_issues)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"KiCad 8 Name","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"KiCad 9 Name","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Notes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Connector:Conn_01x04","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Connector:Conn_01x04_Pin","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"All single-row connectors renamed","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Connector:Conn_01x06","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Connector:Conn_01x06_Pin","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Same pattern","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Connector:Conn_02x20","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Connector:Conn_02x20_Pin","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"All dual-row connectors too","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Connector:SMA","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Removed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use custom library or Connector:Coaxial","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Connector:TestPoint","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Connector:TestPoint","type":"text","marks":[{"type":"code_inline"}]},{"text":" (moved)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"May have different pin layout","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Regulator_Linear:AMS1117","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Has 4 pins (added ADJ)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3-pin schematic won't match","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Fix strategy:","type":"text","marks":[{"type":"strong"}]},{"text":" Create a project-level custom library with KiCad 8 symbol versions. Change lib_ids to point to the custom library. Do NOT try to update to KiCad 9 versions — pin positions differ and will break wire connections.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Pin Position Changes (lib_symbol_mismatch)","type":"text"}]},{"type":"paragraph","content":[{"text":"CRITICAL: Do NOT replace embedded Device:C/R/L symbols with KiCad 9 versions.","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"KiCad 9 changed passive pin positions:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"KiCad 8: ","type":"text"},{"text":"Device:C","type":"text","marks":[{"type":"code_inline"}]},{"text":" pins at (0, ±2.54)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"KiCad 9: ","type":"text"},{"text":"Device:C","type":"text","marks":[{"type":"code_inline"}]},{"text":" pins at (0, ±3.81)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Replacing embedded symbols breaks every wire connected to every capacitor, resistor, and inductor. Instead, suppress ","type":"text"},{"text":"lib_symbol_mismatch","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":".kicad_pro","type":"text","marks":[{"type":"code_inline"}]},{"text":" — the embedded KiCad 8 symbols work fine.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Footprint Renames (footprint_link_issues)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"KiCad 8 Footprint","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"KiCad 9 Footprint","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SW_Push_1P1T_NO_6x3.5mm","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SW_Push_1P1T_NO_CK_PTS125Sx43SMTR","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SMA_Amphenol_901-143_Vertical","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SMA_Amphenol_901-144_Vertical","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"replace_footprint()","type":"text","marks":[{"type":"code_inline"}]},{"text":" for bulk updates.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Annotation Requirements","type":"text"}]},{"type":"paragraph","content":[{"text":"KiCad 9 requires all reference designators to end with a digit.","type":"text","marks":[{"type":"strong"}]},{"text":" References like ","type":"text"},{"text":"C_RX1B_N","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"J_PWR","type":"text","marks":[{"type":"code_inline"}]},{"text":" will cause \"Item not annotated\" errors in the GUI (but NOT in CLI ERC).","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from kicad_sch_helpers import fix_annotation_suffixes\ncontent = fix_annotation_suffixes(content) # Adds \"1\" suffix to bare refs","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Migration Workflow","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ERC → categorize violations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create custom library with KiCad 8 symbol versions (extract from embedded ","type":"text"},{"text":"lib_symbols","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Update lib_ids from standard libraries to custom library","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Update renamed footprints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fix annotation suffixes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add PWR_FLAG for any new power_pin_not_driven errors","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Suppress ","type":"text"},{"text":"lib_symbol_mismatch","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":".kicad_pro","type":"text","marks":[{"type":"code_inline"}]},{"text":" (safe — embedded symbols still work)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Suppress ","type":"text"},{"text":"multiple_net_names","type":"text","marks":[{"type":"code_inline"}]},{"text":" if intentional dual-naming exists","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ERC again → verify 0 errors","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Custom Library Management","type":"text"}]},{"type":"paragraph","content":[{"text":"When standard KiCad libraries change between versions, create project-level libraries to preserve compatibility.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Creating Project Library Tables","type":"text"}]},{"type":"paragraph","content":[{"text":"sym-lib-table","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" (in project root):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"(sym_lib_table\n (version 7)\n (lib (name \"CubeSat_SDR\")(type \"KiCad\")(uri \"${KIPRJMOD}/libraries/cubesat_sdr.kicad_sym\")(options \"\")(descr \"Project custom symbols\"))\n)","type":"text"}]},{"type":"paragraph","content":[{"text":"fp-lib-table","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" (in project root):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"(fp_lib_table\n (version 7)\n (lib (name \"CubeSat_SDR\")(type \"KiCad\")(uri \"${KIPRJMOD}/libraries/cubesat_sdr.pretty\")(options \"\")(descr \"Project custom footprints\"))\n)","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"${KIPRJMOD}","type":"text","marks":[{"type":"code_inline"}]},{"text":" for portable paths — it resolves to the project directory.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Extracting Embedded Symbols to Library","type":"text"}]},{"type":"paragraph","content":[{"text":"When migrating from standard library symbols to custom ones:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from kicad_sch_helpers import extract_embedded_symbol, convert_embedded_to_library\n\n# Read schematic\nwith open(\"schematic.kicad_sch\") as f:\n sch = f.read()\n\n# Extract a symbol from the embedded lib_symbols section\nblock = extract_embedded_symbol(sch, \"Connector:Conn_01x04\")\n\n# Convert from embedded format (prefix:Name) to library format (Name)\nlib_block = convert_embedded_to_library(block, \"Connector\", \"Conn_01x04\")\n\n# Append to custom library file (before the final closing paren)\nwith open(\"libraries/custom.kicad_sym\") as f:\n lib = f.read()\nclose_pos = lib.rstrip().rfind(')')\nlib = lib[:close_pos] + '\\t' + lib_block + '\\n' + lib[close_pos:]\nwith open(\"libraries/custom.kicad_sym\", \"w\") as f:\n f.write(lib)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Embedded vs Library Format","type":"text"}]},{"type":"paragraph","content":[{"text":"In ","type":"text"},{"text":".kicad_sch","type":"text","marks":[{"type":"code_inline"}]},{"text":" embedded ","type":"text"},{"text":"lib_symbols","type":"text","marks":[{"type":"code_inline"}]},{"text":": top-level is ","type":"text"},{"text":"(symbol \"Library:Name\" ...)","type":"text","marks":[{"type":"code_inline"}]},{"text":", sub-symbols use ","type":"text"},{"text":"(symbol \"Name_0_1\" ...)","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"In ","type":"text"},{"text":".kicad_sym","type":"text","marks":[{"type":"code_inline"}]},{"text":" library files: top-level is ","type":"text"},{"text":"(symbol \"Name\" ...)","type":"text","marks":[{"type":"code_inline"}]},{"text":", sub-symbols use ","type":"text"},{"text":"(symbol \"Name_0_1\" ...)","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"The only difference is the top-level name loses its library prefix.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Decoupling Capacitor Array","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"for i, (ref, val) in enumerate(zip(refs, values)):\n place_2pin_vertical(sch, \"Device:C\", ref, val,\n x=snap(start_x + i * 8), y=snap(cap_y),\n top_net=power_net, bottom_net=\"GND\",\n footprint=f\"Capacitor_SMD:{fp}\")","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2-Pin Passives (R, C, L)","type":"text"}]},{"type":"paragraph","content":[{"text":"Standard KiCad 2-pin passive symbols have:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pin 1 at library (0, 2.54) — top when vertical","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pin 2 at library (0, -2.54) — bottom when vertical","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"With rotation 0 at schematic (sx, sy):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pin 1 schematic position: (sx, sy - 2.54)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pin 2 schematic position: (sx, sy + 2.54)","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"place_2pin_vertical(sch, \"Device:C\", \"C1\", \"100nF\",\n x=snap(100), y=snap(150),\n top_net=\"VCC_3V3\", bottom_net=\"GND\",\n footprint=\"Capacitor_SMD:C_0402_1005Metric\")","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Multi-pin IC Connection","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# Always use connect_pin — never compute positions manually\nsignal_map = {\n \"TX1A_P\": \"TX1A_P\",\n \"TX1A_N\": \"TX1A_N\",\n \"SPI_CLK\": \"SPI_CLK\",\n # ... all signal pins\n}\nfor pin_name, net_name in signal_map.items():\n sch.connect_pin(\"U1\", pin_name, net_name, wire_dx=-7.62)\n\n# Power pins\nfor pin_name in [\"VDDD1P3\", \"VDDA1P3\", \"VDDD1P8\"]:\n sch.connect_pin(\"U1\", pin_name, f\"VCC_{pin_name}\", wire_dy=5.08)\n\n# Unused pins — EVERY unused pin needs this\nfor pin_name in [\"AUXDAC1\", \"AUXDAC2\", \"AUXADC\", \"TEMP_SENS\"]:\n sch.connect_pin_noconnect(\"U1\", pin_name)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Power Regulator with PWR_FLAG","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"sch.place(\"CubeSat_SDR:AP2112K\", \"U4\", \"AP2112K-3.3\",\n x=snap(100), y=snap(180),\n footprint=\"Package_TO_SOT_SMD:SOT-23-5\")\nwl(sch, 100, 180, 'VIN', SOT5_PINS, \"VCC_5V\", dx=-7.62)\nwl(sch, 100, 180, 'EN', SOT5_PINS, \"VCC_5V\", dx=-7.62)\nwl(sch, 100, 180, 'GND', SOT5_PINS, \"GND\", dy=5.08)\nwl(sch, 100, 180, 'VOUT', SOT5_PINS, \"VCC_3V3\", dx=7.62)\n# NC pin — no-connect flag, NOT a label\nnc_x, nc_y = pin_abs(100, 180, *SOT5_PINS['NC'])\nsch.nc(nc_x, nc_y)\n# PWR_FLAG on output net\nsch.place_pwr_flag(x=snap(115), y=snap(178), net_name=\"VCC_3V3\")","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Lessons Learned (Battle-Tested)","type":"text"}]},{"type":"paragraph","content":[{"text":"These lessons came from debugging a real 119-component CubeSat SDR schematic:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never guess pin positions.","type":"text","marks":[{"type":"strong"}]},{"text":" Even being off by 1.27mm (one grid unit) causes ERC errors. Always read the .kicad_sym file and use computed positions.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The Y-axis flip is the #1 source of bugs.","type":"text","marks":[{"type":"strong"}]},{"text":" Library Y-up vs schematic Y-down means you must negate Y. A pin at library (0, 25.4) is at schematic (sx, sy - 25.4) — NOT (sx, sy + 25.4). Getting this wrong places labels 50mm from their pins.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SOT-23-5 VOUT vs NC confusion","type":"text","marks":[{"type":"strong"}]},{"text":" silently breaks LDO circuits. VOUT=(7.62, 2.54), NC=(7.62, -2.54). They differ only in Y sign. After the Y-flip, VOUT is above NC in the schematic. Always verify against the library.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sub-symbol naming breaks KiCad silently.","type":"text","marks":[{"type":"strong"}]},{"text":" If you write ","type":"text"},{"text":"(symbol \"Device:R_0_1\" ...)","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead of ","type":"text"},{"text":"(symbol \"R_0_1\" ...)","type":"text","marks":[{"type":"code_inline"}]},{"text":", KiCad may open the file but all symbols appear broken. Always run ","type":"text"},{"text":"fix_subsymbol_names()","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PWR_FLAG is needed more often than you think.","type":"text","marks":[{"type":"strong"}]},{"text":" Any net driven by a regulator with passive-type output pins needs one. Also add one on GND. Missing PWR_FLAGs cause power_pin_not_driven errors on every component on that net.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Grid snapping prevents hundreds of warnings.","type":"text","marks":[{"type":"strong"}]},{"text":" A single off-grid component cascades into off-grid warnings for all connected wires and labels. Snap everything from the start.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Account for every pin.","type":"text","marks":[{"type":"strong"}]},{"text":" Go through the pin list systematically. Every pin must be either: connected via wire+label, connected to a power symbol, or flagged with no_connect. Missing even one pin produces an error.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Parenthesis balance check.","type":"text","marks":[{"type":"strong"}]},{"text":" KiCad S-expressions must have perfectly balanced parentheses. Add a check at the end of generation:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"depth = sum(1 if c == '(' else -1 if c == ')' else 0 for c in content)\nassert depth == 0, f\"Parenthesis imbalance: depth={depth}\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Don't replace embedded Device:C/R/L with KiCad 9 versions.","type":"text","marks":[{"type":"strong"}]},{"text":" KiCad 9 changed passive pin positions from ±2.54 to ±3.81. Replacing embedded symbols breaks every wire connection. Suppress ","type":"text"},{"text":"lib_symbol_mismatch","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"kicad-cli JSON goes to file, not stdout.","type":"text","marks":[{"type":"strong"}]},{"text":" Always use ","type":"text"},{"text":"-o /tmp/result.json","type":"text","marks":[{"type":"code_inline"}]},{"text":". Piping to python gives empty stdin. The JSON is nested under ","type":"text"},{"text":"sheets[].violations[]","type":"text","marks":[{"type":"code_inline"}]},{"text":", not at the top level.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CLI ERC doesn't check annotations.","type":"text","marks":[{"type":"strong"}]},{"text":" The GUI flags \"Item not annotated\" for references not ending with a digit (e.g., ","type":"text"},{"text":"C_RX1B_N","type":"text","marks":[{"type":"code_inline"}]},{"text":"), but CLI ERC silently passes. Always run ","type":"text"},{"text":"fix_annotation_suffixes()","type":"text","marks":[{"type":"code_inline"}]},{"text":" when migrating.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"macOS kicad-cli needs environment variables.","type":"text","marks":[{"type":"strong"}]},{"text":" Without ","type":"text"},{"text":"KICAD9_SYMBOL_DIR","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"KICAD9_FOOTPRINT_DIR","type":"text","marks":[{"type":"code_inline"}]},{"text":", kicad-cli can't find the global libraries and produces false ","type":"text"},{"text":"lib_symbol_issues","type":"text","marks":[{"type":"code_inline"}]},{"text":" warnings.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Don't use ","type":"text","marks":[{"type":"strong"}]},{"text":"grep -P","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" on macOS.","type":"text","marks":[{"type":"strong"}]},{"text":" PCRE mode is not supported. Use Python regex for all pattern matching on schematic files.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create project-level library tables for custom symbols.","type":"text","marks":[{"type":"strong"}]},{"text":" Use ","type":"text"},{"text":"${KIPRJMOD}","type":"text","marks":[{"type":"code_inline"}]},{"text":" for portable paths. Extract embedded symbols to populate the library — don't rewrite them from scratch.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Back up before running fix scripts.","type":"text","marks":[{"type":"strong"}]},{"text":" A broken parenthesis balance renders the schematic unloadable. Use ","type":"text"},{"text":"shutil.copy2()","type":"text","marks":[{"type":"code_inline"}]},{"text":" before any modifications.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Don't suppress ERC errors, only warnings.","type":"text","marks":[{"type":"strong"}]},{"text":" Suppressing ","type":"text"},{"text":"lib_symbol_mismatch","type":"text","marks":[{"type":"code_inline"}]},{"text":" (warnings) is safe for KiCad 8→9 migration. Never suppress actual errors like ","type":"text"},{"text":"pin_not_connected","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"power_pin_not_driven","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Reference Files","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/kicad_sch_helpers.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Python helper library (always use this)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/kicad_sexpression_format.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — KiCad S-expression format specification, coordinate system, common ERC errors and fixes","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"references/kicad_sexpression_format.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" before generating any schematic to understand the coordinate system, sub-symbol naming rules, and PWR_FLAG requirements.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Checklist Before Delivery","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"New Schematic Generation","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"All coordinates snapped to 1.27mm grid via ","type":"text"},{"text":"snap()","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Every IC pin either connected via ","type":"text"},{"text":"connect_pin()","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"wl()","type":"text","marks":[{"type":"code_inline"}]},{"text":" or flagged with ","type":"text"},{"text":"connect_pin_noconnect()","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"nc()","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sub-symbol names fixed with ","type":"text"},{"text":"fix_subsymbol_names()","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PWR_FLAG on every voltage regulator output net AND on GND","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ERC validation run (0 errors target, warnings acceptable)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Parenthesis balance verified (depth 0 at end of file)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pin positions verified against .kicad_sym library (not guessed)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SOT-23-5 VOUT vs NC positions double-checked for all LDOs","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Existing Schematic Fixing / KiCad 9 Migration","type":"text"}]},{"type":"ordered_list","attrs":{"order":9,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"All reference designators end with a digit (","type":"text"},{"text":"fix_annotation_suffixes()","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Renamed/missing symbols handled via project-level custom library","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Embedded Device:C/R/L symbols NOT replaced (suppress ","type":"text"},{"text":"lib_symbol_mismatch","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Renamed footprints updated (","type":"text"},{"text":"replace_footprint()","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PWR_FLAG added on all power input nets including external power (barrel jack, USB VBUS)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Schematic backed up before running any fix scripts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Environment variables set for kicad-cli on macOS (","type":"text"},{"text":"KICAD9_SYMBOL_DIR","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"KICAD9_FOOTPRINT_DIR","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ERC run with ","type":"text"},{"text":"--severity-all","type":"text","marks":[{"type":"code_inline"}]},{"text":" to catch all warning types","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"kicad-schematic","author":"@skillopedia","source":{"stars":1,"repo_name":"kicad-schematic","origin_url":"https://github.com/kenchangh/kicad-schematic/blob/HEAD/SKILL.md","repo_owner":"kenchangh","body_sha256":"76b8c171ff41991a8bd42c16d3de6f4242896ba2dc78e3b133fae1665679a867","cluster_key":"1f3b55d376c012bd8663ca532e65a6dbff20cb476440c2c5df6bc59e68e01490","clean_bundle":{"format":"clean-skill-bundle-v1","source":"kenchangh/kicad-schematic/SKILL.md","attachments":[{"id":"7cb4ed25-db0a-5ae7-a1b9-4d52711863a7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7cb4ed25-db0a-5ae7-a1b9-4d52711863a7/attachment","path":".gitignore","size":126,"sha256":"01fbdaa59fa61d2b8d9d67dda8a6470a57a711e5c860bffe7aed9398dc835d95","contentType":"text/plain; charset=utf-8"},{"id":"e93b6d99-f2a7-5b36-8a86-4c54c2204275","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e93b6d99-f2a7-5b36-8a86-4c54c2204275/attachment.md","path":"README.md","size":2194,"sha256":"e9c29887f39ad74d3e89f0d3c9dc97f95d49938745be69e353e882d9d4f02887","contentType":"text/markdown; charset=utf-8"},{"id":"aae381e4-bdae-52ad-ac1d-35725b7a6797","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aae381e4-bdae-52ad-ac1d-35725b7a6797/attachment.md","path":"references/kicad_sexpression_format.md","size":16701,"sha256":"3ae7276c717302d2b5ff7b356311796a0142363b0973ae8a61f6231bb2cb9e9f","contentType":"text/markdown; charset=utf-8"},{"id":"baabfe30-4cbd-5904-95e7-ac5e8f3beff6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/baabfe30-4cbd-5904-95e7-ac5e8f3beff6/attachment.py","path":"scripts/kicad_sch_helpers.py","size":48928,"sha256":"0e919f7a09c6276815a00de4c90a937a1d69532cfaa914b07456c6ac454c901b","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"f8a3195e42cef75e7422f8def2cd32e95d11b166e5610e7ed7dc875fe96bb6b0","attachment_count":4,"text_attachments":4,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"web-development","import_tag":"clean-skills-v1","description":"Generate, validate, and fix KiCad 8/9 schematic files (.kicad_sch) programmatically. Use this skill whenever the user wants to create, modify, or fix KiCad schematics, generate netlists from circuit descriptions, fix ERC errors, or migrate schematics between KiCad versions. Triggers on: KiCad, schematic, .kicad_sch, ERC, electrical rules check, circuit design, PCB schematic, netlist generation, S-expression schematic, KiCad migration."}},"renderedAt":1782987380568}

KiCad Schematic Agent Generate ERC-clean KiCad 8/9 schematics by writing Python scripts that use computed pin positions — never guess coordinates. Also fix ERC errors on existing schematics and handle KiCad 8→9 migration. Critical Principle The #1 cause of broken schematics is guessed pin positions. When connecting labels to IC pins, you MUST compute exact coordinates using the symbol definition's pin positions and the coordinate transform formula. The helper library in does this automatically. The Y-axis Trap (Most Common Bug) Symbol libraries (.kicad sym) use Y-up (math convention). Schemat…