# -*- coding: utf-8 -*- u"""**从板上读回铜**,按真实几何判每个网通没通(M7)。 /c/Python314/python .claude/r2/board_check.py # 全部网络 /c/Python314/python .claude/r2/board_check.py --net SDA # 只看一个 ## 为什么不能用现成的那几个 | 判据 | 问题 | |---|---| | `router.py` 的连通性 | 判的是**缓存里的几何**,手工改过板(挪过孔、改线)之后就过期了 | | `router_check.py` | 同上,而且只判净距不判连通 | | `pcb_route.py --check`(`getNetLength`)| 客户端重载之后一律返回 0,读不出来 | | EasyEDA 的连接性 DRC | 计数在重载前后能差 5 倍(52 → 237),而且读不回明细 | ⇒ 这里只信一件事:**板上读回来的线段 / 过孔 / 焊盘**,按「铜挨着铜就是连着」 做并查集。判据是「同一个网的所有焊盘落在同一块铜里」。 ## 平面网另算 `GND` / `VO_P` / `+12V` 靠铜面 + 缝合过孔成网,铜面不在这个检查里(读不回填充多边形), 所以它们只报「有几只脚一根铜都不挨」——那种才是真漏。 """ import io import json import math import os import sys sys.stdout.reconfigure(encoding='utf-8') HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.dirname(HERE)) sys.path.insert(0, HERE) from eda import run # noqa: E402 # ⚠ `--base` 切到底板(`pcb_base` + `_base_pads.json`);不带就是刀片,行为一字未变。 if '--base' in sys.argv: import pcb_base as B # noqa: E402 PADS_JSON = os.path.join(HERE, '_base_pads.json') else: import pcb_blade as B # noqa: E402 PADS_JSON = os.path.join(HERE, '_blade_pads.json') MIL = B.MIL PLANE = ('GND', 'VO_P', '+12V', '+12V_F', '+3V3') # ⚠ `+3V3` 从 M9b 起也是**铜面网**(五块槽墙,方案 §30):15 只槽脚一根线都不接, # 靠铺铜盖住。⇒ 它和 12V 一样,只能判「脚是不是至少挨着一根铜」, # 而**被计划中的铜面盖住的脚要先摘掉**(下面 `covered()`),否则每次都刷一堆假漏。 # 槽脚真正的判据是 `pour_base.wall_check()`(盖没盖住)+ DRC + 截图。 TOUCH = 0.02 # 判「挨着」的容差(mm) JS = u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,2500)); const L=(await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer)) .map(l=>[l.net,l.layer,l.startX,l.startY,l.endX,l.endY,l.lineWidth]); const V=(await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.net,v.x,v.y,v.diameter]); return {L, V}; """ % B.PCB def seg_dist(px, py, x1, y1, x2, y2): dx, dy = x2 - x1, y2 - y1 L2 = dx * dx + dy * dy if L2 < 1e-12: return math.hypot(px - x1, py - y1) t = max(0.0, min(1.0, ((px - x1) * dx + (py - y1) * dy) / L2)) return math.hypot(px - (x1 + t * dx), py - (y1 + t * dy)) def seg_seg(a, b): u"""两段的最近距离(够用的近似:四个端点到对方线段 + 相交判定)。""" (x1, y1, x2, y2), (x3, y3, x4, y4) = a, b d1 = (x2 - x1, y2 - y1) d2 = (x4 - x3, y4 - y3) den = d1[0] * d2[1] - d1[1] * d2[0] if abs(den) > 1e-12: t = ((x3 - x1) * d2[1] - (y3 - y1) * d2[0]) / den s = ((x3 - x1) * d1[1] - (y3 - y1) * d1[0]) / den if -1e-9 <= t <= 1 + 1e-9 and -1e-9 <= s <= 1 + 1e-9: return 0.0 return min(seg_dist(x1, y1, x3, y3, x4, y4), seg_dist(x2, y2, x3, y3, x4, y4), seg_dist(x3, y3, x1, y1, x2, y2), seg_dist(x4, y4, x1, y1, x2, y2)) class UF(object): def __init__(self): self.p = {} def find(self, a): self.p.setdefault(a, a) while self.p[a] != a: self.p[a] = self.p[self.p[a]] a = self.p[a] return a def union(self, a, b): ra, rb = self.find(a), self.find(b) if ra != rb: self.p[ra] = rb _PLAN = [None] def covered(net, ref): u"""这只脚是不是整只落在**计划中的同网铜面**里(12V 铜墙 / 3V3 槽墙那种)。 ⛔ 填出来的多边形读不回来(见文件头那张表),所以这里只能拿**计划**当依据; 计划与板上对不对得上,由 `pour_base --push` 之后的 DRC + 截图判。 """ if '--base' not in sys.argv: return False if _PLAN[0] is None: import pour_base as PB _PLAN[0] = [(n, r) for (_nm, n, lay, r, _p) in PB.plan() if lay in (1, 2)] des, pin = ref.split('.', 1) for p in json.load(io.open(PADS_JSON, encoding='utf-8')): if p[0] != des or str(p[1]) != pin: continue x0, y0 = p[2] - p[4] / 2, p[3] - p[5] / 2 x1, y1 = p[2] + p[4] / 2, p[3] + p[5] / 2 return any(n == net and r[0] <= x0 and x1 <= r[2] and r[1] <= y0 and y1 <= r[3] for n, r in _PLAN[0]) return False def main(): only = None if '--net' in sys.argv: only = sys.argv[sys.argv.index('--net') + 1] r = run(JS, timeout=300) if not r.get('success'): sys.exit(json.dumps(r, ensure_ascii=False)[:400]) lines = [(a[0], a[1], a[2] / MIL, a[3] / MIL, a[4] / MIL, a[5] / MIL, a[6] / MIL) for a in r['result']['L']] vias = [(a[0], a[1] / MIL, a[2] / MIL, a[3] / MIL) for a in r['result']['V']] pads = json.load(io.open(PADS_JSON, encoding='utf-8')) bad, ok = [], 0 nets = sorted(set(p[7] for p in pads if p[7])) for net in nets: if only and net != only: continue np_ = [p for p in pads if p[7] == net] if len(np_) < 2: continue segs = [l for l in lines if l[0] == net] vs = [v for v in vias if v[0] == net] uf = UF() items = [] # (kind, layer, geom) for i, l in enumerate(segs): items.append(('s', l[1], (l[2], l[3], l[4], l[5]), l[6] / 2.0, ('s', i))) for i, v in enumerate(vs): # 过孔穿两层,层记 None items.append(('v', None, (v[1], v[2]), v[3] / 2.0, ('v', i))) for i, p in enumerate(np_): # ⚠ 通孔焊盘(layer 12)两层都有铜,和过孔一样层记 None —— 不这么记的话, # 接在 L4 上的线被判成「不挨着」(底板 J1 / C2 / C3 / J26 / J27 / J33 全部误报孤立) items.append(('p', None if p[6] == 12 else p[6], (p[2], p[3], p[4], p[5]), 0.0, ('p', i))) for i in range(len(items)): for j in range(i + 1, len(items)): a, b = items[i], items[j] if a[1] is not None and b[1] is not None and a[1] != b[1]: continue d = None if a[0] == 's' and b[0] == 's': d = seg_seg(a[2], b[2]) - a[3] - b[3] elif a[0] == 's' and b[0] in 'vp' or b[0] == 's' and a[0] in 'vp': s, o = (a, b) if a[0] == 's' else (b, a) if o[0] == 'v': d = seg_dist(o[2][0], o[2][1], *s[2]) - s[3] - o[3] else: # 焊盘:按矩形近似,取盘心到线段的距离减半对角 d = (seg_dist(o[2][0], o[2][1], *s[2]) - s[3] - max(o[2][2], o[2][3]) / 2.0) else: # 过孔/焊盘之间 ax = a[2][0] if a[0] == 'v' else a[2][0] ay = a[2][1] if a[0] == 'v' else a[2][1] bx, by = b[2][0], b[2][1] ra = a[3] if a[0] == 'v' else max(a[2][2], a[2][3]) / 2.0 rb = b[3] if b[0] == 'v' else max(b[2][2], b[2][3]) / 2.0 d = math.hypot(ax - bx, ay - by) - ra - rb if d is not None and d <= TOUCH: uf.union(a[4], b[4]) roots = set(uf.find(('p', i)) for i in range(len(np_))) alone = [np_[i][0] + '.' + np_[i][1] for i in range(len(np_)) if uf.find(('p', i)) == ('p', i) and not any(uf.find(x) == ('p', i) for x in uf.p if x != ('p', i))] if net in PLANE: alone = [a for a in alone if not covered(net, a)] if alone: bad.append((net, u'靠铜面成网,但这些脚一根铜都不挨:' + ' '.join(alone))) else: ok += 1 elif len(roots) > 1: bad.append((net, u'铜分成 %d 块(%d 只脚)%s' % (len(roots), len(np_), u';孤立脚: ' + ' '.join(alone) if alone else ''))) else: ok += 1 print(u'板上读回:%d 段铜 / %d 过孔;判了 %d 个网' % (len(lines), len(vias), ok + len(bad))) print(u'[OK] 全连通 %d 个' % ok) if bad: print(u'[X] 没连通 %d 个:' % len(bad)) for n, m in bad: print(u' %-10s %s' % (n, m)) else: print(u'[OK] **所有网络在板上都是一整块铜**') if __name__ == '__main__': main()