文件
phonerail/.claude/r2/board_check.py
T
zhou yang和Claude Opus 5 efcba30a0c R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base)
- 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base)
- 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、
  东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base)
- 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面
- 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、
  J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder
  开关(route_base、remote_route.ps1);剩 37 个网待手工补
- 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base);
  已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查)
- 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
2026-09-10 21:02:10 +08:00

211 行
9.2 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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()