tools/epro2/kicad: hierarchical export + global_label + 5-Voltage power ports

Three coupled changes so kicad-cli sch erc runs at the project level
(across all sheets of one schematic) instead of single-sheet:

1. (label) → (global_label (shape passive)). EPRO2 nets are
   project-global by construction (named rails span every page in the
   SCH and physically wire across PCBs); KiCad's local label is sheet-
   scoped and triggers `label_dangling` for any name not duplicated on
   the same page.

2. New root_sch_writer that groups SCH_PAGE docs by their parent SCH
   (META.schematic), emits one root .kicad_sch per group with one
   (sheet ...) entry per child, and threads the root-assigned uuid back
   into each child's (sheet_instances) so KiCad can bind them.
   --all-sch now defaults to this; --flat falls back to one-file-per-page.

3. EPRO2's "5-Voltage" placeholder COMPONENT (partId
   pid8a0e77bacb214e, 365 instances on ESP-VoCat) is the editor's power
   port. The rail name lives in the placement's `Global Net Name` ATTR,
   not in the PART. We now emit a (global_label "<rail>") at the
   placement coords whenever that attr is set (101/365 of them on
   ESP-VoCat — the rest are unconfigured drafts).

ESP-VoCat 5 hierarchical roots: 2325 → 2265 violations. Modest because
5 of 6 SCHs are single-page (no cross-sheet nets to resolve), and the
one 4-page schematic (CoreBoard) shares only a handful of names across
sheets — most net names are de-facto sheet-local. The remaining ~190
pin_not_connected are dominated by 0402-style passives whose pin tip
lies on a wire's interior, not at an endpoint; KiCad needs an explicit
(junction) at those points and we don't yet emit one. Marked as the
next follow-up in log.md.

47 → 52 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 00:05:47 +08:00
parent 54f0173947
commit ff5553fb06
6 changed files with 479 additions and 33 deletions

View File

@@ -0,0 +1,125 @@
"""Generate a root ``.kicad_sch`` that ties multiple SCH_PAGE files into one
hierarchical project.
EPRO2 multi-page schematics live as N standalone ``SCH_PAGE`` docs all
referencing the same ``SCH`` doc through ``META.schematic``. When we exported
each page as its own ``.kicad_sch`` and ran ``kicad-cli sch erc`` against it
in isolation, every cross-sheet net showed up as ``label_dangling`` /
``pin_not_connected`` because single-sheet ERC can't see the matching
endpoint on a sibling page.
Hierarchical mode fixes that:
- Root .kicad_sch carries one ``(sheet ...)`` block per child page,
each with a uuid the root assigns and a ``Sheetfile`` pointing at the
child filename in the same directory.
- Each child .kicad_sch declares its position in the hierarchy with
``(sheet_instances (path "/<assigned_uuid>" (page "<n>")))`` so KiCad
can bind it to the root.
- With both halves wired up, ``kicad-cli sch erc <root>`` walks the whole
project, resolving global_labels across pages.
Usage:
Build a list of (filename, page_title, assigned_uuid) tuples — one per
child — *before* writing the children, then pass it here to emit the
root. The same uuid you stored in this list goes into the child writer
via its ``sheet_path="/<uuid>"`` argument.
"""
from __future__ import annotations
import uuid as _uuid
from dataclasses import dataclass
from .sexpr import Sym, to_sexpr
KICAD_SCH_VERSION = 20231120
KICAD_GENERATOR = "facere-epro2"
@dataclass
class ChildSheet:
"""One child entry in a root sheet.
``filename`` is relative to the root .kicad_sch's directory.
``sheet_uuid`` is the uuid the root assigns to this child; the child's
own (sheet_instances) must echo it back as ``"/<sheet_uuid>"``.
"""
filename: str
title: str
sheet_uuid: str
def new_sheet_uuid() -> str:
return str(_uuid.uuid4())
def _font(size: float = 1.27) -> list:
return [Sym("effects"), [Sym("font"), [Sym("size"), size, size]],
[Sym("justify"), Sym("left"), Sym("bottom")]]
def _file_font(size: float = 1.27) -> list:
return [Sym("effects"), [Sym("font"), [Sym("size"), size, size]],
[Sym("justify"), Sym("left"), Sym("top")]]
def write_root_sheet(
title: str,
children: list[ChildSheet],
*,
project_name: str = "facere",
grid_origin_mm: tuple[float, float] = (38.1, 38.1),
box_size_mm: tuple[float, float] = (50.8, 25.4),
box_pitch_mm: tuple[float, float] = (63.5, 38.1),
columns: int = 3,
) -> str:
"""Render a root ``.kicad_sch`` listing ``children`` as ``(sheet ...)`` blocks.
The boxes are laid out on a simple ``columns``-wide grid so they don't
overlap on the canvas; visual layout is irrelevant to ERC but KiCad's
GUI needs non-zero positions/sizes for the sheet to be visible/editable.
"""
elements: list = []
bw, bh = box_size_mm
px, py = box_pitch_mm
ox, oy = grid_origin_mm
for idx, child in enumerate(children):
col = idx % columns
row = idx // columns
x = ox + col * px
y = oy + row * py
page_num = idx + 2 # root is page 1, children start at 2
elements.append([
Sym("sheet"),
[Sym("at"), x, y],
[Sym("size"), bw, bh],
[Sym("stroke"), [Sym("width"), 0.1524], [Sym("type"), Sym("solid")]],
[Sym("fill"), [Sym("color"), 0, 0, 0, 0.0]],
[Sym("uuid"), child.sheet_uuid],
[Sym("property"), "Sheetname", child.title,
[Sym("at"), x, y - 0.5, 0], _font()],
[Sym("property"), "Sheetfile", child.filename,
[Sym("at"), x, y + bh + 0.5, 0], _file_font()],
[Sym("instances"),
[Sym("project"), project_name,
[Sym("path"), "/", [Sym("page"), str(page_num)]]]],
])
sch: list = [
Sym("kicad_sch"),
[Sym("version"), KICAD_SCH_VERSION],
[Sym("generator"), KICAD_GENERATOR],
[Sym("uuid"), new_sheet_uuid()],
[Sym("paper"), "A4"],
[Sym("title_block"),
[Sym("title"), title],
[Sym("comment"), 1, f"hierarchical root for {len(children)} child sheet(s)"]],
[Sym("lib_symbols")],
*elements,
[Sym("sheet_instances"),
[Sym("path"), "/", [Sym("page"), "1"]]],
]
return to_sexpr(sch, pretty=True)