装 kicad 8.0.9 (apt PPA) 后跑 kicad-cli sch erc 校验我们 emit 的
.kicad_sch 文件,发现 9/9 sheets 一开始全部报 "Failed to load schematic
file" — 父节点解析就挂掉。Bisect 找到两个语法 bug:
1. **(pin_numbers (hide no)) 不被 KiCad 8 接受**
KiCad 8 lib_symbols 里 `pin_numbers` 是 token-form,不接受 (hide
yes/no) 子块。要么省略整个 block 默认 visible,要么 `(pin_numbers
hide)` 表示隐藏。原来的 `(hide no)` 风格是 KiCad 7 旧语法。
Fix: tools/epro2/kicad/sym_writer.py 删掉 (pin_numbers (hide no))
行;KiCad 默认 visible 行为正是我们想要的。
2. **String 里的字面 \n / \r / \t 让 KiCad 解析器中止**
ESP-VoCat 的 Overview sheet 有 TEXT "Battary\n3.7V 700mAH"(多行
电池标签),EPRO2 里以**字面 0x0a 字符**存储。我们把它原样 emit
成 "..." 包住的字符串 → KiCad reader 在 quoted string 内遇到 \n
就报 parse error 不给 message。
Fix: tools/epro2/kicad/sexpr.py 在 str escape 路径加 \n / \r / \t
转义;reader 加 \r 解码(roundtrip 用)。
修完后:
9/9 sheets parse OK in KiCad 8.0.9
ERC 跑通,9 个 sheet 共 2793 violations,分布:
1372 endpoint_off_grid (49%, cosmetic — 30-mil EPRO2 grid 不
snap KiCad 默认 50-mil grid)
571 lib_symbol_issues (20%, cosmetic — facere 库未注册到
user library table;库已 embed 在
.kicad_sch 内联可用)
444 wire_dangling (16%, real — wire 端点没精确对齐 pin)
406 pin_not_connected (15%, 同上的另一面)
Cosmetic 占 70%,real connectivity 30%,下个 phase 处理:
- grid 校准(把 coord 精确 round 到统一 grid 上)
- pin tip 端点匹配(KiCad 需要 wire 端点 == pin (at) 字段对应的
绝对坐标,浮点必须精确相等)
- 生成 sym-lib-table 注册 facere 库(消 lib_symbol_issues)
测试:
+ test_string_escapes_newlines_and_tabs
+ test_lib_symbol_omits_pin_numbers_block
reader 加 \r 解码
41/41 通过(39 旧 + 2 新)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
84 lines
2.8 KiB
Python
84 lines
2.8 KiB
Python
"""Hand-rolled S-expression emitter for KiCad files.
|
|
|
|
KiCad reads a Lisp-flavored S-expr; values are atoms (symbols / strings /
|
|
numbers) and lists. We emit lists as nested Python lists, with ``Sym`` marking
|
|
strings that should serialize as bare symbols (unquoted) — e.g. ``Sym("xy")``,
|
|
``Sym("kicad_sch")``, ``Sym("yes")``.
|
|
|
|
Plain ``str`` values get JSON-style double-quoting + backslash escaping.
|
|
Floats render with up to 6 decimals trimmed of trailing zeros (KiCad reads
|
|
either form, but trimmed is what kicad-cli emits).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import math
|
|
from typing import Any
|
|
|
|
|
|
class Sym(str):
|
|
"""Marker subclass: render as a bare S-expr symbol (unquoted)."""
|
|
|
|
__slots__ = ()
|
|
|
|
|
|
def _fmt_number(n: float | int) -> str:
|
|
if isinstance(n, bool): # bool is int subclass — guard first
|
|
return "yes" if n else "no"
|
|
if isinstance(n, int):
|
|
return str(n)
|
|
if math.isnan(n) or math.isinf(n):
|
|
raise ValueError(f"can't serialize non-finite number: {n}")
|
|
s = f"{n:.6f}".rstrip("0").rstrip(".")
|
|
return s if s else "0"
|
|
|
|
|
|
def _emit(value: Any, out: io.StringIO, indent: int, *, pretty: bool) -> None:
|
|
if isinstance(value, list):
|
|
out.write("(")
|
|
for i, item in enumerate(value):
|
|
if i > 0:
|
|
if pretty and isinstance(item, list) and len(item) > 1:
|
|
out.write("\n" + "\t" * (indent + 1))
|
|
else:
|
|
out.write(" ")
|
|
_emit(item, out, indent + 1, pretty=pretty)
|
|
out.write(")")
|
|
elif isinstance(value, Sym):
|
|
out.write(str(value))
|
|
elif value is True or value is False:
|
|
out.write("yes" if value else "no")
|
|
elif isinstance(value, (int, float)):
|
|
out.write(_fmt_number(value))
|
|
elif isinstance(value, str):
|
|
# KiCad's S-expr reader rejects literal newlines/CR/tabs inside
|
|
# quoted strings; we MUST escape them. Order matters: backslash first.
|
|
escaped = (
|
|
value.replace("\\", "\\\\")
|
|
.replace('"', '\\"')
|
|
.replace("\n", "\\n")
|
|
.replace("\r", "\\r")
|
|
.replace("\t", "\\t")
|
|
)
|
|
out.write(f'"{escaped}"')
|
|
elif value is None:
|
|
# rare but sometimes a slot is intentionally empty
|
|
out.write('""')
|
|
else:
|
|
raise TypeError(f"can't S-expr serialize {type(value).__name__}: {value!r}")
|
|
|
|
|
|
def to_sexpr(value: Any, *, pretty: bool = True) -> str:
|
|
"""Render a Python data structure as a KiCad-style S-expression.
|
|
|
|
Top-level value is usually a list whose first element is a ``Sym`` (the
|
|
block tag, e.g. ``Sym("kicad_sch")``). When ``pretty=True``, child lists
|
|
of length > 1 go onto their own indented line.
|
|
"""
|
|
buf = io.StringIO()
|
|
_emit(value, buf, indent=0, pretty=pretty)
|
|
if pretty:
|
|
buf.write("\n")
|
|
return buf.getvalue()
|