# -*- 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` 靠铜面 + 缝合过孔成网,铜面不在这个检查里(读不回填充多边形), 所以它们只报「有几只脚一根铜都不挨」。 ⛔ **但「一根铜都不挨」在刀片上几乎全是假阳性**(2026-09-12 晚踩了一次,白查半天): 摘掉「被计划中的铜面盖住」那些脚的是 `covered()`,而它第一行就是 `if '--base' not in sys.argv: return False` ⇒ **只对底板生效**。刀片下一只脚都不摘,于是凡是「只靠铺铜盖住、不单独拉线」的 去耦件地脚(`C*.2` / `R*.1` / `U*.2` 那一票)全被报成孤立 —— 刀片实测固定 **49 只**。 ⇒ 判据:**跟重铺前的板面快照逐只比对**。2026-09-12 晚 `NOTCH_D` 加深后重铺,前后两边 `49 == 49` 且逐只同名、L1/2/15/16 的铜都是 899 段(铺铜只动填充多边形、不动导线) ⇒ 与重铺无关。别看见这一长串就以为把地铺断了。 ⛔ **底板(`--base`)下 `covered()` 也有两种假阳性**,2026-09-12 晚查实,5 只脚全是假的: ① **通孔脚被内层整层盖住,但 `covered()` 只看外层**:第 119 行是 `if lay in (1, 2)`, 把 `GND-L2-4`/`GND-L3-4`(L15/L16 整层 GND,x 6.20~90.30 / y 0.50~97.50)滤掉了。 而 `C3.2` / `J26.1` / `J1.2` / `J1.3` 是 layer 12 的**通孔**焊盘,穿透所有层, 正是被那两块内层盖住的 ⇒ 报「一根铜都不挨」,其实盖得好好的。 ② **贴片脚跨在两块同网瓦片的接缝上**:`U29.3` 焊盘框 x 1.71~2.36,而 `GND-L4-1` 到 1.80、 `GND-L4-4` 从 1.80 起 —— 铜面实际连续(瓦片只是切分),但 `covered()` 要求**单块完全包住**, 跨缝就不认。 ⚠ **没有放宽判据**:放宽(改成"多块并集包住"或"含内层")会把真漏一起摘掉,代价更大。 遇到这类报告,按上面两条**手工排除**,或跟重铺前快照比对。 """ 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 # ⚠ 底板 2026-09-10 深夜起 L2 改整层 GND,+3V3 回到布线网 ⇒ 底板上 +3V3 按普通网查连通(刀片不变) # ⛔ 2026-09-11 更正:原来是 `+ (() if '--base' else ('+3V3',))` —— 「+3V3 铜面网」是**底板** M9b 的槽墙, # 刀片上从来没有 +3V3 铜面(pcb_pour 只铺 GND / VO / V12),却一直按铜面网只查「脚挨不挨铜」, # 刀片 +3V3 的真实连通性等于没验过。两块板现在都按普通布线网查。 PLANE = ('GND', 'VO_P', '+12V', '+12V_F') # ⚠ `+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()