124 行
5.3 KiB
Python
124 行
5.3 KiB
Python
#!/usr/bin/env python3
|
||
"""对账 2:从几何重建网表,和 EasyEDA 权威网表逐网络比对。
|
||
|
||
为什么必须做这一步:KiCad 的连接性同样是从几何算出来的。只要「按同样坐标摆符号 +
|
||
画同样导线」重建出的网表能和 EasyEDA 编译结果对上,生成的 .kicad_sch 就能对上;
|
||
对不上就说明我对几何的理解有洞,必须先补。
|
||
|
||
规则(都来自 easyeda-sch-geometry skill 的实测):
|
||
- getState_Line() 是**端点对**:每 4 个数一段,不是折线路径。当折线解析会凭空造斜线。
|
||
- 坐标单位 0.01 inch,元件都吸附在 5 的倍数上;TOL=0.6 足够宽松又不会误连。
|
||
- netflag / netport 各带一个引脚,其 Net 字段就是网络名;**跨页靠同名 netport/netflag 合并**。
|
||
"""
|
||
import json, os, sys, collections
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
from k_netlist import parse_pads
|
||
|
||
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "eda_src")
|
||
TOL = 0.6
|
||
|
||
|
||
class DSU:
|
||
def __init__(self): self.p = {}
|
||
def find(self, x):
|
||
self.p.setdefault(x, x)
|
||
while self.p[x] != x:
|
||
self.p[x] = self.p[self.p[x]]; x = self.p[x]
|
||
return x
|
||
def union(self, a, b):
|
||
ra, rb = self.find(a), self.find(b)
|
||
if ra != rb: self.p[ra] = rb
|
||
|
||
|
||
def key(x, y):
|
||
"""吸附到 0.5 网格再取整 —— 坐标里有 539.9999999999999 这种浮点毛刺。"""
|
||
return (round(x * 2) / 2, round(y * 2) / 2)
|
||
|
||
|
||
def segments(line):
|
||
"""端点对 -> [((x1,y1),(x2,y2)), ...]"""
|
||
out = []
|
||
for i in range(0, len(line) - 3, 4):
|
||
a, b = key(line[i], line[i + 1]), key(line[i + 2], line[i + 3])
|
||
if a != b: # 零长度段丢掉(skill §8a 的坑,连接性上无意义)
|
||
out.append((a, b))
|
||
return out
|
||
|
||
|
||
def build(board):
|
||
tree = json.load(open(os.path.join(ROOT, "_tree.json"), encoding="utf-8"))
|
||
pages = next(x for x in tree["boards"] if x["board"] == board)["pages"]
|
||
dsu = DSU()
|
||
pinat = collections.defaultdict(list) # 坐标 -> ["R1.2", ...]
|
||
named = collections.defaultdict(list) # 网络名 -> [坐标, ...]
|
||
|
||
for p in pages:
|
||
g = json.load(open(os.path.join(ROOT, board, p["name"] + ".geom.json"), encoding="utf-8"))
|
||
pfx = p["name"]
|
||
for w in (g["prims"]["Wire"] or []):
|
||
for a, b in segments(w.get("Line") or []):
|
||
dsu.union((pfx, a), (pfx, b))
|
||
for c in (g["prims"]["Component"] or []):
|
||
t = c.get("ComponentType")
|
||
if t == "sheet":
|
||
continue
|
||
for q in (c.get("__pins") or []):
|
||
k = (pfx, key(q["X"], q["Y"]))
|
||
dsu.find(k)
|
||
if t == "part":
|
||
pinat[k].append(f'{c["Designator"]}.{q["PinNumber"]}')
|
||
else: # netflag / netport
|
||
n = (c.get("Net") or "").strip()
|
||
if n:
|
||
named[n].append(k)
|
||
# 同名 netflag/netport 跨页合并
|
||
for n, ks in named.items():
|
||
for k in ks[1:]:
|
||
dsu.union(ks[0], k)
|
||
|
||
nets = collections.defaultdict(set)
|
||
for k, refs in pinat.items():
|
||
nets[dsu.find(k)].update(refs)
|
||
label = {}
|
||
for n, ks in named.items():
|
||
label.setdefault(dsu.find(ks[0]), n)
|
||
# 单引脚的不算网络 —— 那是未连接引脚,EasyEDA 网表也不收录(KiCad 侧要靠 NC 标记处理)
|
||
out, nc = {}, []
|
||
for i, (r, v) in enumerate(nets.items()):
|
||
if len(v) >= 2:
|
||
out[label.get(r, f"@{i}")] = sorted(v)
|
||
elif v:
|
||
nc.extend(v)
|
||
return out, sorted(nc)
|
||
|
||
|
||
def compare(board):
|
||
mine, nc = build(board)
|
||
base = parse_pads(os.path.join(ROOT, board, "netlist.asc"))["nets"]
|
||
# 按引脚集合配对,网络名不参与(EasyEDA 的 $1N236 是它自己编的)
|
||
mset = {frozenset(v): k for k, v in mine.items()}
|
||
bset = {frozenset(v): k for k, v in base.items()}
|
||
only_b = [bset[s] for s in bset.keys() - mset.keys()]
|
||
only_m = [mset[s] for s in mset.keys() - bset.keys()]
|
||
print(f"[{board}] 重建 {len(mine)} 网 / 基准 {len(base)} 网;引脚集合完全一致的 {len(mset.keys() & bset.keys())}")
|
||
if not only_b and not only_m:
|
||
print(" ✔ 网表逐网络完全一致")
|
||
# 命名一致性另报(不影响连接性)
|
||
diff = [(bset[s], mset[s]) for s in bset.keys() & mset.keys()
|
||
if not mset[s].startswith("@") and bset[s] != mset[s] and not bset[s].startswith("$")]
|
||
if diff:
|
||
print(f" · 网络名不同但连接相同 {len(diff)}: {diff[:5]}")
|
||
print(f" · 未连接引脚 {len(nc)} 个(EasyEDA 网表不收录,KiCad 侧需打 NC): {nc[:6]}{' …' if len(nc)>6 else ''}")
|
||
return True
|
||
for nm in sorted(only_b)[:8]:
|
||
print(f" ⚠ 基准有、重建没有: {nm} = {base[nm]}")
|
||
for nm in sorted(only_m)[:8]:
|
||
print(f" ⚠ 重建有、基准没有: {nm} = {mine[nm]}")
|
||
print(f" (只在基准 {len(only_b)},只在重建 {len(only_m)})")
|
||
return False
|
||
|
||
|
||
if __name__ == "__main__":
|
||
ok = all([compare(b) for b in (sys.argv[1:] or ["Board1", "Board2"])])
|
||
sys.exit(0 if ok else 1)
|