R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+152
@@ -0,0 +1,152 @@
|
||||
#!/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()
|
||||
在新工单中引用
屏蔽一个用户