文件
phonerail/.claude/k_verify.py
T

153 行
5.9 KiB
Python

#!/usr/bin/env python3
"""对账 3(终检):kicad-cli 编译出的网表 vs EasyEDA 权威网表,逐元件、逐网络比对。
这是整个转换的验收判据。网络**名**允许不同(EasyEDA 的匿名网 $1N236 和 KiCad 的
自动命名本来就不一样),所以按「引脚集合」配对;单引脚网络两边都不算网络。
用法: python k_verify.py [Board1 Board2]
退出码 0 = 全过
"""
import json, os, re, subprocess, sys, collections
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from k_netlist import parse_pads
ROOT = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(ROOT, "eda_src")
KI = os.path.join(os.path.dirname(ROOT), "kicad")
CLI = r"D:\KiCad\bin\kicad-cli.exe"
PROJ = {"Board1": "PhoneRail-Board1", "Board2": "PhoneRail-Board2"}
def export(board, out):
p = os.path.join(KI, PROJ[board], PROJ[board] + ".kicad_sch")
r = subprocess.run([CLI, "sch", "export", "netlist", "--format", "kicadsexpr",
"-o", out, p], capture_output=True, text=True, encoding="utf-8", errors="replace")
warn = (r.stdout or "") + (r.stderr or "")
return warn.strip()
def sexp(text):
"""极简 S-expression 解析 -> 嵌套 list(字符串保留引号内原文)"""
out, stack, i, n = [], [], 0, len(text)
while i < n:
ch = text[i]
if ch == "(":
new = []
(stack[-1] if stack else out).append(new)
stack.append(new); i += 1
elif ch == ")":
stack.pop(); i += 1
elif ch == '"':
j = i + 1; buf = []
while j < n:
if text[j] == "\\":
buf.append(text[j + 1]); j += 2
elif text[j] == '"':
break
else:
buf.append(text[j]); j += 1
(stack[-1] if stack else out).append("".join(buf)); i = j + 1
elif ch.isspace():
i += 1
else:
j = i
while j < n and not text[j].isspace() and text[j] not in '()"':
j += 1
(stack[-1] if stack else out).append(text[i:j]); i = j
return out[0] if len(out) == 1 else out
def _get(node, key):
for x in node:
if isinstance(x, list) and x and x[0] == key:
return x
return None
def parse_kicad(path):
"""-> parts {ref: footprint}, nets {name: [ref.pin]}
用 S-expression 遍历而不是正则 —— 正则版曾经稳定漏掉每块板的**最后一个** comp
(表现为元件数恰好少 1),排查时很容易误判成转换丢件。
"""
root = sexp(open(path, encoding="utf-8").read())
parts, nets = {}, {}
for node in root:
if not isinstance(node, list) or not node:
continue
if node[0] == "components":
for c in node[1:]:
if not isinstance(c, list) or c[0] != "comp":
continue
ref = _get(c, "ref")[1]
fp = _get(c, "footprint")
parts[ref] = (fp[1] if fp and len(fp) > 1 else "").split(":")[-1]
elif node[0] == "nets":
for nt in node[1:]:
if not isinstance(nt, list) or nt[0] != "net":
continue
nm = _get(nt, "name")[1] if _get(nt, "name") else ""
pins = []
for nd in nt[1:]:
if isinstance(nd, list) and nd[0] == "node":
pins.append(f'{_get(nd, "ref")[1]}.{_get(nd, "pin")[1]}')
nets[nm] = pins
return parts, nets
def check(board, workdir):
out = os.path.join(workdir, board + ".net")
warn = export(board, out)
if not os.path.exists(out):
print(f"[{board}] ✘ 导出失败: {warn[:300]}"); return False
kparts, knets = parse_kicad(out)
base = parse_pads(os.path.join(SRC, board, "netlist.asc"))
bparts, bnets = base["parts"], base["nets"]
ok = True
print(f"[{board}]" + (f" ⚠ kicad-cli: {warn.splitlines()[0][:60]}" if warn else ""))
# 元件
only_k = sorted(set(kparts) - set(bparts) - {r for r in kparts if r.startswith("#PWR")})
only_b = sorted(set(bparts) - set(kparts))
real_k = {r for r in kparts if not r.startswith("#PWR")}
print(f" 元件 KiCad {len(real_k)} / 基准 {len(bparts)}", end="")
if only_k or only_b:
print(f" ✘ 只在 KiCad {only_k[:6]} / 只在基准 {only_b[:6]}"); ok = False
else:
print(" ✔")
bad_fp = [(r, kparts[r], bparts[r]) for r in real_k if r in bparts and kparts[r] != bparts[r]]
print(f" 封装 {'✔ 全部一致' if not bad_fp else f'✘ {len(bad_fp)} 处不符 {bad_fp[:4]}'}")
ok &= not bad_fp
# 网络:按引脚集合配对,单引脚不算网络
def norm(d):
return {frozenset(v): k for k, v in d.items() if len(set(v)) >= 2}
K, B = norm(knets), norm(bnets)
nc = [k for k, v in knets.items() if len(set(v)) < 2]
same = K.keys() & B.keys()
print(f" 网络 KiCad {len(K)} / 基准 {len(B)} 引脚集合一致 {len(same)}"
f" (另有 {len(nc)} 个单引脚网络 = 未连接引脚)")
if len(same) == len(K) == len(B):
print(" ✔ 逐网络完全一致")
else:
ok = False
for s in list(B.keys() - K.keys())[:5]:
print(f" ✘ 基准有、KiCad 没有: {B[s]} = {sorted(s)[:8]}")
for s in list(K.keys() - B.keys())[:5]:
print(f" ✘ KiCad 有、基准没有: {K[s]} = {sorted(s)[:8]}")
return ok
def main():
wd = os.path.join(os.environ.get("TEMP", "."), "phonerail_verify")
os.makedirs(wd, exist_ok=True)
boards = sys.argv[1:] or ["Board1", "Board2"]
good = all([check(b, wd) for b in boards])
print("\n" + ("全部通过 ✔" if good else "存在不一致 ✘"))
sys.exit(0 if good else 1)
if __name__ == "__main__":
main()