KiCad pairs project files purely by basename + same directory: a folder
holding `Foo.kicad_pro`, `Foo.kicad_sch`, `Foo.kicad_pcb` opens as one
project on double-click of the .kicad_pro, with cross-tool navigation
(open footprint from schematic etc) wired up automatically.
- pro_writer.write_kicad_pro() renders the minimal KiCad 8 JSON we
need: meta.filename pinning the basename, sheets=[[<root_uuid>,
""]] binding the schematic root, and stub blocks for board /
schematic / net_settings / erc that KiCad expects to find on the
first GUI load.
- root_sch_writer.write_root_sheet() now accepts an optional
root_uuid so the caller can pass the same uuid into the .kicad_pro
and .kicad_sch (the binding fails silently with mismatched ids).
- CLI gains `--all`: groups SCH/PCB docs by their META.board uuid
(1:1 in EPRO2), strips SCH-/PCB- editor prefixes from titles to
derive a shared project basename, and emits one directory per
BOARD with paired files. BOARDs whose SCH is DELETE_DOC (LCD-BD on
ESP-VoCat) still get a .kicad_pro with sheets:[] + .kicad_pcb so
pcbnew opens cleanly.
ESP-VoCat smoke: 6 boards → 6 project dirs, all pairs validated by
kicad-cli sch erc / pcb export svg. The CoreBoard pro/sch/pcb trio
shares root uuid 366d3e53...c2fccbe4330b end-to-end.
68 → 71 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
"""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",
|
|
root_uuid: str | None = None,
|
|
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"), root_uuid or 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)
|