tools/epro2/kicad: --all emits paired .kicad_pro + .kicad_sch + .kicad_pcb per BOARD
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>
This commit is contained in:
78
log.md
78
log.md
@@ -4,6 +4,84 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-04-29 02:30 KiCad 工程文件 + `--all` 一键导:双击 .kicad_pro 打开 GUI
|
||||||
|
|
||||||
|
**Claude 会话**
|
||||||
|
|
||||||
|
接 `adc5dc5`。两件小事一起做:发 `.kicad_pro` + CLI 加 `--all`。目标是让消费者(咱们的 ML 训练代码 / 下游同学)双击就能在 KiCad GUI 里同时打开 schematic + PCB 配对。
|
||||||
|
|
||||||
|
### 1. `tools/epro2/kicad/pro_writer.py`
|
||||||
|
|
||||||
|
KiCad 8 的 .kicad_pro 是 JSON。最小可用集:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"meta": {"filename": "<basename>.kicad_pro", "version": 1},
|
||||||
|
"sheets": [["<root_sheet_uuid>", ""]], // 绑 schematic 根 sheet
|
||||||
|
"board": {...}, "schematic": {...}, "net_settings": {...}, ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
只要 .kicad_pro / .kicad_sch / .kicad_pcb **三个文件同名同目录**,KiCad 自动配对——双击 .kicad_pro 同时弹两个编辑器。`sheets` 数组里的 uuid 必须和 .kicad_sch 的 `(uuid ...)` 对得上,所以 `write_root_sheet` 加了 `root_uuid` 参数让调用方注入确定值。
|
||||||
|
|
||||||
|
### 2. CLI `--all`
|
||||||
|
|
||||||
|
按 BOARD uuid 分组 SCH 和 PCB(两边的 META 都带 `board: <uuid>`,自动 1:1 对应),每个 BOARD 一个目录:
|
||||||
|
|
||||||
|
```
|
||||||
|
out/
|
||||||
|
├── EchoEar-BaseBoard-V1_0/
|
||||||
|
│ ├── EchoEar-BaseBoard-V1_0.kicad_pro
|
||||||
|
│ ├── EchoEar-BaseBoard-V1_0.kicad_sch (root)
|
||||||
|
│ ├── EchoEar-BaseBoard-V1_0.kicad_pcb
|
||||||
|
│ └── P1_45092758.kicad_sch (子页)
|
||||||
|
├── EchoEar-CoreBoard-V1_0/
|
||||||
|
│ ├── EchoEar-CoreBoard-V1_0.kicad_pro
|
||||||
|
│ ├── EchoEar-CoreBoard-V1_0.kicad_sch
|
||||||
|
│ ├── EchoEar-CoreBoard-V1_0.kicad_pcb
|
||||||
|
│ ├── Overview_dc13d6d2.kicad_sch (4 个子页)
|
||||||
|
│ ├── MCU_510cff33.kicad_sch
|
||||||
|
│ ├── Interface_b336a7c7.kicad_sch
|
||||||
|
│ └── codec_0b0163fa.kicad_sch
|
||||||
|
... (5 boards total)
|
||||||
|
```
|
||||||
|
|
||||||
|
basename 从 SCH/PCB title 剥掉 `SCH-`/`PCB-` 前缀(`SCH-EchoEar-CoreBoard-V1_0` → `EchoEar-CoreBoard-V1_0`),schematic 和 PCB 自然 collapse 到一个项目。
|
||||||
|
|
||||||
|
### ESP-VoCat 实测
|
||||||
|
|
||||||
|
6 个 BOARD(含 LCD-BD 那个 SCH 被删的——只有 PCB,照样生成 `.kicad_pro` 和 `.kicad_pcb`,sheets:[]):
|
||||||
|
|
||||||
|
| 项目 | sch 解析 | pcb 渲染 |
|
||||||
|
|---|:-:|:-:|
|
||||||
|
| EchoEar-BaseBoard-V1_0 | ✓ ERC 485 | ✓ |
|
||||||
|
| EchoEar-CoreBoard-V1_0 | ✓ ERC 1205 | ✓ |
|
||||||
|
| EchoEar-Rotating-Base-LCD-BD-V1_0 | (no sch) | ✓ |
|
||||||
|
| EchoEar-Rotating-Base-Mainboard-V1_0 | ✓ ERC 412 | ✓ |
|
||||||
|
| ESP-VoCat-MicBoard-V1_0 | ✓ ERC 133 | ✓ |
|
||||||
|
| ESP-VoCat-Rotating-Base-Sub-board-V1_0 | ✓ ERC 30 | ✓ |
|
||||||
|
|
||||||
|
`pro.sheets[0][0]` 跟 `.kicad_sch` 第一行 `(uuid ...)` 字符串相等(验证过 CoreBoard:`366d3e53-5167-4e17-9325-c2fccbe4330b`)。
|
||||||
|
|
||||||
|
### 决策(Why)
|
||||||
|
|
||||||
|
- **basename 对齐 = KiCad pairing 的全部**:KiCad 不读 .kicad_pro 里的 `boards`/`schematic` 字段去找文件,纯靠**同目录同 basename**自动配对。其它字段都是项目 settings,缺了 KiCad 用 default 兜底。
|
||||||
|
- **`sheets: []` 兜底 LCD-BD**:SCH 被 DELETE_DOC 的板子没 schematic root,pro 里空数组兜底,KiCad 打开时只显示 pcbnew,不会卡。
|
||||||
|
- **`--all` 不替代 `--all-sch` / `--all-pcb`**:保留两个细粒度命令——只想看 schematic 或只想 batch-export PCB 时不必生成 .kicad_pro 噪音。
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
68 → 71 单测全过:pro_writer 3 个(filename + root uuid 绑定 / sheets 空数组 fallback / KiCad 8 顶层 key 完整性)。
|
||||||
|
|
||||||
|
### 下游交付
|
||||||
|
|
||||||
|
下游同学的 Wokwi pipeline 不吃的 5 个 Pro 项目(3 块 EPRO2 AES + 2 块 Pro 2.x)现在一条命令就能转:
|
||||||
|
```
|
||||||
|
uv run python -m tools.epro2.kicad data/raw/oshwhub/<uuid> --all --out <dst>
|
||||||
|
```
|
||||||
|
每个项目目录拷贝到他们 corpus 即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-04-29 02:00 PCB Phase-2:POUR → KiCad zone,CoreBoard unconnected -43%
|
## 2026-04-29 02:00 PCB Phase-2:POUR → KiCad zone,CoreBoard unconnected -43%
|
||||||
|
|
||||||
**Claude 会话**
|
**Claude 会话**
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from pathlib import Path
|
|||||||
from ..project_relations import ProjectRelations
|
from ..project_relations import ProjectRelations
|
||||||
from ..replay import Document, Project, replay_project
|
from ..replay import Document, Project, replay_project
|
||||||
from .pcb_writer import write_pcb
|
from .pcb_writer import write_pcb
|
||||||
|
from .pro_writer import write_kicad_pro
|
||||||
from .root_sch_writer import ChildSheet, new_sheet_uuid, write_root_sheet
|
from .root_sch_writer import ChildSheet, new_sheet_uuid, write_root_sheet
|
||||||
from .sch_writer import write_sch_page
|
from .sch_writer import write_sch_page
|
||||||
|
|
||||||
@@ -154,6 +155,138 @@ def _convert_hierarchical(
|
|||||||
return root_count
|
return root_count
|
||||||
|
|
||||||
|
|
||||||
|
def _project_basename(title: str) -> str:
|
||||||
|
"""Strip the SCH-/PCB- editor prefix so schematic and PCB collapse to
|
||||||
|
one project basename. EPRO2 names its docs ``SCH-<board>-Vx`` /
|
||||||
|
``PCB-<board>-Vx``; without stripping we'd end up with two separate
|
||||||
|
projects per physical board."""
|
||||||
|
s = title
|
||||||
|
for prefix in ("SCH-", "PCB-", "sch-", "pcb-"):
|
||||||
|
if s.startswith(prefix):
|
||||||
|
s = s[len(prefix):]
|
||||||
|
break
|
||||||
|
return _safe_filename(s) or "project"
|
||||||
|
|
||||||
|
|
||||||
|
def _group_by_board(proj: Project) -> dict[str, dict]:
|
||||||
|
"""Group SCH (collection) and PCB docs by their META.board uuid.
|
||||||
|
|
||||||
|
Returns ``{board_uuid: {"sch": <SCH doc or None>,
|
||||||
|
"pcb": <PCB doc or None>,
|
||||||
|
"title": <board name>}}``.
|
||||||
|
Skips boards whose SCH is DELETE_DOC (LCD-BD on ESP-VoCat).
|
||||||
|
"""
|
||||||
|
deleted_schs = {
|
||||||
|
u for u, d in proj.documents.items()
|
||||||
|
if d.doc_type == "SCH" and (d.objects.get("DELETE_DOC") or {}).get("isDelete")
|
||||||
|
}
|
||||||
|
out: dict[str, dict] = {}
|
||||||
|
for u, d in proj.documents.items():
|
||||||
|
if d.doc_type not in ("SCH", "PCB"):
|
||||||
|
continue
|
||||||
|
meta = d.objects.get("META") or {}
|
||||||
|
board_id = str(meta.get("board") or "")
|
||||||
|
if not board_id:
|
||||||
|
continue
|
||||||
|
if d.doc_type == "SCH" and u in deleted_schs:
|
||||||
|
continue
|
||||||
|
slot = out.setdefault(board_id, {"sch": None, "pcb": None, "title": None})
|
||||||
|
if d.doc_type == "SCH":
|
||||||
|
slot["sch"] = d
|
||||||
|
else:
|
||||||
|
slot["pcb"] = d
|
||||||
|
# Prefer SCH title for the project name (PCB title is just a mirror).
|
||||||
|
if slot["title"] is None or d.doc_type == "SCH":
|
||||||
|
slot["title"] = (meta.get("title") or "").strip()
|
||||||
|
# Drop boards where neither SCH nor PCB survived (deleted SCH + no PCB).
|
||||||
|
return {b: s for b, s in out.items() if s["sch"] or s["pcb"]}
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_all_projects(
|
||||||
|
proj: Project,
|
||||||
|
out_dir: Path,
|
||||||
|
pr: ProjectRelations,
|
||||||
|
) -> int:
|
||||||
|
"""One directory per BOARD: <out>/<basename>/{<basename>.kicad_pro,
|
||||||
|
<basename>.kicad_sch (root), <basename>.kicad_pcb, <child pages>...}.
|
||||||
|
|
||||||
|
KiCad pairs files by basename, so giving the .kicad_pro / .kicad_sch
|
||||||
|
root / .kicad_pcb the same stem makes "double-click .kicad_pro open in
|
||||||
|
GUI with both editors" work without any extra plumbing.
|
||||||
|
"""
|
||||||
|
boards = _group_by_board(proj)
|
||||||
|
if not boards:
|
||||||
|
print("no BOARDs in this project", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
page_groups = _group_pages_by_sch(proj) # SCH-uuid → [page-uuid, ...]
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
for board_id, slot in boards.items():
|
||||||
|
title = slot["title"] or board_id[:12]
|
||||||
|
basename = _project_basename(title)
|
||||||
|
proj_dir = out_dir / basename
|
||||||
|
proj_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
sch_doc = slot["sch"]
|
||||||
|
pcb_doc = slot["pcb"]
|
||||||
|
print(f"Board '{title}' → {proj_dir}")
|
||||||
|
|
||||||
|
root_uuid: str | None = None
|
||||||
|
# ---- schematic + root sheet
|
||||||
|
if sch_doc is not None:
|
||||||
|
page_uuids = page_groups.get(sch_doc.doc_uuid, [])
|
||||||
|
children: list[ChildSheet] = []
|
||||||
|
for page_uuid in page_uuids:
|
||||||
|
page_title = _page_title(proj.documents[page_uuid])
|
||||||
|
children.append(ChildSheet(
|
||||||
|
filename=f"{_safe_filename(page_title)}_{page_uuid[:8]}.kicad_sch",
|
||||||
|
title=page_title,
|
||||||
|
sheet_uuid=new_sheet_uuid(),
|
||||||
|
))
|
||||||
|
for idx, (page_uuid, child) in enumerate(zip(page_uuids, children)):
|
||||||
|
page_doc = proj.documents[page_uuid]
|
||||||
|
text = write_sch_page(
|
||||||
|
page_doc,
|
||||||
|
project_relations=pr,
|
||||||
|
sheet_path=f"/{child.sheet_uuid}",
|
||||||
|
page_num=idx + 2,
|
||||||
|
)
|
||||||
|
(proj_dir / child.filename).write_text(text, encoding="utf-8")
|
||||||
|
_print_stats(child.filename)
|
||||||
|
|
||||||
|
root_uuid = new_sheet_uuid()
|
||||||
|
root_text = write_root_sheet(
|
||||||
|
title, children,
|
||||||
|
project_name=basename,
|
||||||
|
root_uuid=root_uuid,
|
||||||
|
)
|
||||||
|
(proj_dir / f"{basename}.kicad_sch").write_text(root_text, encoding="utf-8")
|
||||||
|
print(f" SCH ROOT: {basename}.kicad_sch ({len(children)} page(s))")
|
||||||
|
|
||||||
|
# ---- pcb
|
||||||
|
if pcb_doc is not None:
|
||||||
|
try:
|
||||||
|
pcb_text = write_pcb(pcb_doc, project_relations=pr)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f" PCB FAIL {pcb_doc.doc_uuid[:12]}: {e}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
(proj_dir / f"{basename}.kicad_pcb").write_text(pcb_text, encoding="utf-8")
|
||||||
|
pcb_stats = getattr(write_pcb, "last_stats", None)
|
||||||
|
if pcb_stats:
|
||||||
|
print(
|
||||||
|
f" PCB: {basename}.kicad_pcb nets={pcb_stats.nets} "
|
||||||
|
f"fps={pcb_stats.footprints} segments={pcb_stats.segments} "
|
||||||
|
f"vias={pcb_stats.vias} zones={pcb_stats.zones}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- project file
|
||||||
|
pro_text = write_kicad_pro(basename, root_sheet_uuid=root_uuid)
|
||||||
|
(proj_dir / f"{basename}.kicad_pro").write_text(pro_text, encoding="utf-8")
|
||||||
|
print(f" PRO: {basename}.kicad_pro")
|
||||||
|
n += 1
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
def _convert_all_pcb(proj: Project, out_dir: Path, pr: ProjectRelations) -> int:
|
def _convert_all_pcb(proj: Project, out_dir: Path, pr: ProjectRelations) -> int:
|
||||||
"""Emit one .kicad_pcb per PCB doc. Each is named after its META.title
|
"""Emit one .kicad_pcb per PCB doc. Each is named after its META.title
|
||||||
and dropped into a sibling directory of the matching SCH for parity
|
and dropped into a sibling directory of the matching SCH for parity
|
||||||
@@ -191,8 +324,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
ap.add_argument("project_dir", type=Path)
|
ap.add_argument("project_dir", type=Path)
|
||||||
g = ap.add_mutually_exclusive_group(required=True)
|
g = ap.add_mutually_exclusive_group(required=True)
|
||||||
g.add_argument("--doc", help="SCH_PAGE doc uuid (or unique prefix) to convert")
|
g.add_argument("--doc", help="SCH_PAGE doc uuid (or unique prefix) to convert")
|
||||||
g.add_argument("--all-sch", action="store_true", help="convert every SCH_PAGE")
|
g.add_argument("--all-sch", action="store_true", help="convert every SCH_PAGE (hierarchical, no PCB)")
|
||||||
g.add_argument("--all-pcb", action="store_true", help="convert every PCB doc to .kicad_pcb")
|
g.add_argument("--all-pcb", action="store_true", help="convert every PCB doc to .kicad_pcb")
|
||||||
|
g.add_argument("--all", action="store_true",
|
||||||
|
help="emit a complete KiCad project per BOARD: paired .kicad_pro / .kicad_sch / .kicad_pcb")
|
||||||
ap.add_argument("--out", type=Path, default=Path("data/processed/kicad_sch"))
|
ap.add_argument("--out", type=Path, default=Path("data/processed/kicad_sch"))
|
||||||
ap.add_argument(
|
ap.add_argument(
|
||||||
"--no-lib-symbols",
|
"--no-lib-symbols",
|
||||||
@@ -216,6 +351,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
_convert_all_pcb(proj, args.out, pr=pr)
|
_convert_all_pcb(proj, args.out, pr=pr)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
if args.all:
|
||||||
|
if pr is None:
|
||||||
|
pr = ProjectRelations.build(proj)
|
||||||
|
_convert_all_projects(proj, args.out, pr=pr)
|
||||||
|
return 0
|
||||||
|
|
||||||
if args.doc:
|
if args.doc:
|
||||||
_convert_one_flat(proj, args.doc, args.out, pr=pr)
|
_convert_one_flat(proj, args.doc, args.out, pr=pr)
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
96
tools/epro2/kicad/pro_writer.py
Normal file
96
tools/epro2/kicad/pro_writer.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
"""Emit a minimal ``.kicad_pro`` so a sibling ``.kicad_sch`` + ``.kicad_pcb``
|
||||||
|
pair opens as one project in the KiCad GUI (double-click).
|
||||||
|
|
||||||
|
KiCad pairs files by basename: a directory containing
|
||||||
|
``EchoEar-CoreBoard.kicad_pro`` + ``EchoEar-CoreBoard.kicad_sch`` +
|
||||||
|
``EchoEar-CoreBoard.kicad_pcb`` is treated as one project, and opening the
|
||||||
|
.kicad_pro launches both editors with cross-navigation working. Without
|
||||||
|
this file each .kicad_sch / .kicad_pcb opens as a one-off and KiCad puts
|
||||||
|
up a "create project file?" dialog every time.
|
||||||
|
|
||||||
|
The .kicad_pro is JSON. KiCad will fill in defaults for anything missing
|
||||||
|
the next time the GUI opens it; we only emit the keys that meaningfully
|
||||||
|
identify the project (``meta``, ``sheets`` for hierarchy root binding,
|
||||||
|
empty stubs for ``board`` / ``schematic`` / ``net_settings`` so the
|
||||||
|
top-level keys exist in the form KiCad expects).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def write_kicad_pro(
|
||||||
|
project_basename: str,
|
||||||
|
*,
|
||||||
|
root_sheet_uuid: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Render a JSON .kicad_pro string for one project.
|
||||||
|
|
||||||
|
``project_basename`` is the filename without extension — e.g.
|
||||||
|
``"EchoEar-CoreBoard"``. ``root_sheet_uuid`` is the uuid at the top of
|
||||||
|
the sibling .kicad_sch's ``(uuid ...)`` form; KiCad uses it to bind
|
||||||
|
the root sheet to the project's hierarchy. If omitted, KiCad
|
||||||
|
generates one on first open (works but causes a "save changes?"
|
||||||
|
prompt the first time).
|
||||||
|
"""
|
||||||
|
sheets: list = []
|
||||||
|
if root_sheet_uuid:
|
||||||
|
sheets.append([root_sheet_uuid, ""])
|
||||||
|
|
||||||
|
pro: dict = {
|
||||||
|
"board": {
|
||||||
|
"design_settings": {
|
||||||
|
"defaults": {},
|
||||||
|
"rules": {},
|
||||||
|
},
|
||||||
|
"layer_presets": [],
|
||||||
|
"viewports": [],
|
||||||
|
},
|
||||||
|
"boards": [],
|
||||||
|
"cvpcb": {"equivalence_files": []},
|
||||||
|
"erc": {
|
||||||
|
"erc_exclusions": [],
|
||||||
|
"meta": {"version": 0},
|
||||||
|
"pin_map": [],
|
||||||
|
"rule_severities": {},
|
||||||
|
"rule_severitieslegacy": {},
|
||||||
|
},
|
||||||
|
"libraries": {"pinned_footprint_libs": [], "pinned_symbol_libs": []},
|
||||||
|
"meta": {
|
||||||
|
"filename": f"{project_basename}.kicad_pro",
|
||||||
|
"version": 1,
|
||||||
|
},
|
||||||
|
"net_settings": {
|
||||||
|
"classes": [],
|
||||||
|
"meta": {"version": 3},
|
||||||
|
"net_colors": None,
|
||||||
|
"netclass_assignments": None,
|
||||||
|
"netclass_patterns": [],
|
||||||
|
},
|
||||||
|
"pcbnew": {
|
||||||
|
"last_paths": {},
|
||||||
|
"page_layout_descr_file": "",
|
||||||
|
},
|
||||||
|
"schematic": {
|
||||||
|
"annotate_start_num": 0,
|
||||||
|
"bom_export_filename": "",
|
||||||
|
"bom_fmt_presets": [],
|
||||||
|
"bom_fmt_settings": {},
|
||||||
|
"bom_presets": [],
|
||||||
|
"bom_settings": {},
|
||||||
|
"drawing": {},
|
||||||
|
"legacy_lib_dir": "",
|
||||||
|
"legacy_lib_list": [],
|
||||||
|
"meta": {"version": 1},
|
||||||
|
"net_format_name": "",
|
||||||
|
"page_layout_descr_file": "",
|
||||||
|
"plot_directory": "",
|
||||||
|
"spice_external_command": "spice \"%I\"",
|
||||||
|
"subpart_first_id": 65,
|
||||||
|
"subpart_id_separator": 0,
|
||||||
|
},
|
||||||
|
"sheets": sheets,
|
||||||
|
"text_variables": {},
|
||||||
|
}
|
||||||
|
return json.dumps(pro, indent=2)
|
||||||
@@ -69,6 +69,7 @@ def write_root_sheet(
|
|||||||
children: list[ChildSheet],
|
children: list[ChildSheet],
|
||||||
*,
|
*,
|
||||||
project_name: str = "facere",
|
project_name: str = "facere",
|
||||||
|
root_uuid: str | None = None,
|
||||||
grid_origin_mm: tuple[float, float] = (38.1, 38.1),
|
grid_origin_mm: tuple[float, float] = (38.1, 38.1),
|
||||||
box_size_mm: tuple[float, float] = (50.8, 25.4),
|
box_size_mm: tuple[float, float] = (50.8, 25.4),
|
||||||
box_pitch_mm: tuple[float, float] = (63.5, 38.1),
|
box_pitch_mm: tuple[float, float] = (63.5, 38.1),
|
||||||
@@ -112,7 +113,7 @@ def write_root_sheet(
|
|||||||
Sym("kicad_sch"),
|
Sym("kicad_sch"),
|
||||||
[Sym("version"), KICAD_SCH_VERSION],
|
[Sym("version"), KICAD_SCH_VERSION],
|
||||||
[Sym("generator"), KICAD_GENERATOR],
|
[Sym("generator"), KICAD_GENERATOR],
|
||||||
[Sym("uuid"), new_sheet_uuid()],
|
[Sym("uuid"), root_uuid or new_sheet_uuid()],
|
||||||
[Sym("paper"), "A4"],
|
[Sym("paper"), "A4"],
|
||||||
[Sym("title_block"),
|
[Sym("title_block"),
|
||||||
[Sym("title"), title],
|
[Sym("title"), title],
|
||||||
|
|||||||
38
tools/epro2/tests/test_pro_writer.py
Normal file
38
tools/epro2/tests/test_pro_writer.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""Project file emitter regression."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from tools.epro2.kicad.pro_writer import write_kicad_pro
|
||||||
|
|
||||||
|
|
||||||
|
def test_pro_carries_filename_and_root_sheet_uuid():
|
||||||
|
"""The .kicad_pro must record (a) its own filename so KiCad confirms
|
||||||
|
the basename matches the .kicad_sch / .kicad_pcb siblings, and (b)
|
||||||
|
the root sheet uuid in the `sheets` array — that's how KiCad binds
|
||||||
|
the project to the schematic root. A `sheets: []` pro file works
|
||||||
|
for "open a one-off PCB" but loses cross-tool navigation in the
|
||||||
|
schematic editor."""
|
||||||
|
text = write_kicad_pro("EchoEar-CoreBoard", root_sheet_uuid="abc-123")
|
||||||
|
j = json.loads(text)
|
||||||
|
assert j["meta"]["filename"] == "EchoEar-CoreBoard.kicad_pro"
|
||||||
|
assert j["sheets"] == [["abc-123", ""]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_pro_without_root_uuid_emits_empty_sheets_array():
|
||||||
|
"""When called for a board that has only a PCB (the SCH was
|
||||||
|
DELETE_DOC), there's no root sheet uuid. Emit `sheets: []` so
|
||||||
|
the project file still parses — KiCad will simply not show a
|
||||||
|
schematic editor on open."""
|
||||||
|
text = write_kicad_pro("OnlyPcb", root_sheet_uuid=None)
|
||||||
|
j = json.loads(text)
|
||||||
|
assert j["sheets"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_pro_top_level_keys_present_for_kicad_8():
|
||||||
|
"""KiCad 8 expects certain top-level keys to exist (it'll
|
||||||
|
backfill missing ones but with a "save changes?" prompt every
|
||||||
|
open). Smoke-test: assert the keys that GUI reads at startup."""
|
||||||
|
text = write_kicad_pro("X")
|
||||||
|
j = json.loads(text)
|
||||||
|
for key in ("board", "meta", "schematic", "sheets", "net_settings"):
|
||||||
|
assert key in j, f"missing top-level key: {key}"
|
||||||
Reference in New Issue
Block a user