装 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>
89 lines
2.4 KiB
Python
89 lines
2.4 KiB
Python
"""Tiny S-expression parser used by tests for round-trip validation.
|
|
|
|
Not a full KiCad reader — just enough to confirm our writer output is
|
|
syntactically valid Lisp-flavored S-expr (balanced parens, quoted strings,
|
|
numbers, symbols).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Iterator
|
|
|
|
|
|
def tokens(src: str) -> Iterator[str]:
|
|
i, n = 0, len(src)
|
|
while i < n:
|
|
c = src[i]
|
|
if c.isspace():
|
|
i += 1
|
|
continue
|
|
if c == "(":
|
|
yield "("
|
|
i += 1
|
|
continue
|
|
if c == ")":
|
|
yield ")"
|
|
i += 1
|
|
continue
|
|
if c == '"':
|
|
j = i + 1
|
|
buf = []
|
|
while j < n:
|
|
ch = src[j]
|
|
if ch == "\\" and j + 1 < n:
|
|
nxt = src[j + 1]
|
|
buf.append({"\\": "\\", '"': '"', "n": "\n", "t": "\t", "r": "\r"}.get(nxt, nxt))
|
|
j += 2
|
|
continue
|
|
if ch == '"':
|
|
break
|
|
buf.append(ch)
|
|
j += 1
|
|
if j >= n:
|
|
raise SyntaxError("unterminated string")
|
|
yield '"' + "".join(buf) + '"'
|
|
i = j + 1
|
|
continue
|
|
# bare token until whitespace or paren
|
|
j = i
|
|
while j < n and not src[j].isspace() and src[j] not in "()":
|
|
j += 1
|
|
yield src[i:j]
|
|
i = j
|
|
|
|
|
|
def parse(src: str):
|
|
it = iter(tokens(src))
|
|
def _read(tok):
|
|
if tok == "(":
|
|
out = []
|
|
while True:
|
|
try:
|
|
nxt = next(it)
|
|
except StopIteration:
|
|
raise SyntaxError("unterminated list")
|
|
if nxt == ")":
|
|
return out
|
|
out.append(_read(nxt))
|
|
if tok == ")":
|
|
raise SyntaxError("unexpected )")
|
|
if tok.startswith('"') and tok.endswith('"'):
|
|
return tok[1:-1]
|
|
try:
|
|
return int(tok)
|
|
except ValueError:
|
|
try:
|
|
return float(tok)
|
|
except ValueError:
|
|
return tok # symbol
|
|
|
|
try:
|
|
first = next(it)
|
|
except StopIteration:
|
|
raise SyntaxError("empty input")
|
|
val = _read(first)
|
|
# Expect EOF
|
|
for extra in it:
|
|
raise SyntaxError(f"unexpected trailing token: {extra!r}")
|
|
return val
|