153 行
5.9 KiB
Python
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()
|