#!/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()