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