文件
phonerail/.claude/inner1_diffref.py
zhou yang和Claude Opus 5 2bfe308b30 初始导入:PhoneRail 五路手机直供电源板
原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。
这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。

纳入:
- 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本
- .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、
  pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源)
- 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾)
- firmware/ 的 A1 骨架与 site/ 文档站源码

.gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、
site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。

.gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存,
不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
2026-09-06 18:18:14 +08:00

220 行
9.4 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""§10.1 的参考面检查:顶层那 5 段 `*J` 差分(U5n→J7n)下方的 Inner1 是不是**一整块同网络的铜**。
/c/Python314/python .claude/inner1_diffref.py [--grid 0.05] [--margin 1.0]
/c/Python314/python .claude/inner1_diffref.py --routes # 沿**实际走线**逐点查(长差分对用这个)
载板的层角色是镜像的(Inner1 = 分割电源面,Inner2 = 完整地),
所以**底层**的长距离 D± 参考 Inner2(完整地,没问题),
而**顶层**这 5 段 5 mm 的短差分参考的是 Inner1 —— 那一层是被五块 `VOn_P` 岛切开的。
段长只有 ~5 mm、又紧挨 ESD 器件,可以接受,
但前提是**每段下方不能横穿分割缝**(§10.1)。
判据:把两根线的包围盒外扩 `margin`,在这个窗口里
① 只能有**一个**网络的铜面覆盖;② 覆盖必须是**一个连通域**;③ 覆盖率 100%。
"""
import json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
from pcb2_pour import PCB, MIL
from inner1_verify import ring_to_poly, components
L_PWR = 15
JS = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,1800));
const byId = {};
for (const p of await eda.pcb_PrimitivePour.getAll())
byId[p.getState_PrimitiveId()] = [String(p.getState_PourName()||''), p.getState_Net(), p.getState_Layer()];
const out = {};
for (const q of await eda.pcb_PrimitivePoured.getAll()) {
const info = byId[q.getState_PourPrimitiveId()]; if (!info || info[2] !== %d) continue;
let f = []; try { f = q.getState_PourFills() || []; } catch (e) {}
const rings = [];
for (const seg of f) {
if (seg.fill === false) continue;
for (const r of ((seg.path && seg.path.complexPolygon) || [])) rings.push(r);
}
out[info[0]] = {net: info[1], rings: rings};
}
return out;
""" % (PCB, L_PWR)
def cover(polys, x0, y0, x1, y1, g):
u"""窗口内被这些环覆盖的栅格(奇偶填充,逐环按所属 fill 分组已在调用方做)。"""
nx = int((x1 - x0) / g) + 1
ny = int((y1 - y0) / g) + 1
edges = []
for q in polys:
for k in range(len(q)):
a, b = q[k], q[(k + 1) % len(q)]
if a[1] != b[1]:
edges.append((a, b))
grid = [bytearray(nx) for _ in range(ny)]
for j in range(ny):
y = y0 + (j + 0.5) * g
cr = sorted(ax + (bx - ax) * (y - ay) / (by - ay)
for (ax, ay), (bx, by) in edges if (ay > y) != (by > y))
row = grid[j]
for t in range(0, len(cr) - 1, 2):
i0 = max(0, int((cr[t] - x0) / g))
i1 = min(nx - 1, int((cr[t + 1] - x0) / g))
for i in range(i0, i1 + 1):
row[i] = 1
return grid, nx, ny
JS_TOPLINES = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,1800));
const out = [];
for (const o of await eda.pcb_PrimitiveLine.getAll()) {
if (o.getState_Layer() !== 1) continue;
const n = o.getState_Net() || '';
if (!/^(USB[0-9]_D[PM]J?|UP_D[PM])$/.test(n)) continue;
out.push([n, o.getState_StartX(), o.getState_StartY(), o.getState_EndX(), o.getState_EndY()]);
}
return out;
""" % PCB
def pt_in(polys, x, y):
u"""奇偶填充的点包含判定(所有环一起算,孔自然被挖掉)。"""
c = False
for q in polys:
for k in range(len(q)):
(ax, ay), (bx, by) = q[k], q[(k + 1) % len(q)]
if (ay > y) != (by > y) and x < ax + (bx - ax) * (y - ay) / (by - ay):
c = not c
return c
def by_routes(pours, step=0.1):
u"""沿顶层**实际走线**逐点查参考面。
⚠ 判据是「**每一条连通的顶层走线**下方只能有一种铜」,不是「每个网络」——
一个网络在板上可能有好几段互不相连的顶层走线(比如 pair5:过隘口那一段
压在 `VO2_P` 岛上,进 U55 焊盘那一小段压在 GND 底填上),
各自压在各自的一整块铜上就没问题,**没有哪一段横穿了缝**。
⚠ 「裸露」点不算违规:那是过孔 / 焊盘的反焊盘(Ø≈1.0),线穿过它天经地义。
"""
r = run(JS_TOPLINES, timeout=240)
if not r.get('success'):
sys.exit(u'读顶层导线失败: ' + json.dumps(r, ensure_ascii=False)[:400])
segs = {}
for n, x0, y0, x1, y1 in r['result']:
segs.setdefault(n, []).append((x0 / MIL, y0 / MIL, x1 / MIL, y1 / MIL))
def runs(lst):
u"""按端点把线段并成若干条连通走线(并查集,端点取 0.01 mm 容差)。"""
par = list(range(len(lst)))
def find(i):
while par[i] != i:
par[i] = par[par[i]]; i = par[i]
return i
key = {}
for i, (x0, y0, x1, y1) in enumerate(lst):
for k in ((round(x0, 2), round(y0, 2)), (round(x1, 2), round(y1, 2))):
if k in key:
a, b = find(key[k]), find(i)
if a != b:
par[a] = b
else:
key[k] = i
out = {}
for i, seg in enumerate(lst):
out.setdefault(find(i), []).append(seg)
return list(out.values())
print(u'§10.1 参考面检查(沿实际走线,步长 %.2f mm):顶层每一条连通走线下方的 Inner1' % step)
allok = True
for net in sorted(segs):
for run_i, run_segs in enumerate(runs(segs[net])):
seen, bare, tot = {}, 0, 0
xs, ys = [], []
for x0, y0, x1, y1 in run_segs:
xs += [x0, x1]; ys += [y0, y1]
L = ((x1 - x0) ** 2 + (y1 - y0) ** 2) ** 0.5
m = max(2, int(L / step) + 1)
for i in range(m + 1):
t = i / float(m)
x, y = x0 + (x1 - x0) * t, y0 + (y1 - y0) * t
tot += 1
hit = [d['net'] for d in pours.values() if pt_in(d['polys'], x, y)]
if not hit:
bare += 1
for h in hit:
seen[h] = seen.get(h, 0) + 1
ok = len(seen) <= 1
allok = allok and ok
print(u' %-10s #%d x %6.1f~%6.1f y %5.1f~%5.1f %2d 段/%4d 点 -> %-28s 反焊盘 %3d %s'
% (net, run_i, min(xs), max(xs), min(ys), max(ys), len(run_segs), tot,
u' + '.join(u'%s %.0f%%' % (k, 100.0 * v / tot) for k, v in sorted(seen.items()))
or u'(无铜)', bare, u'OK' if ok else u'!! 跨缝'))
print(u'%s' % (u'每一条顶层走线都只压在一块铜上。' if allok
else u'!! 有走线横穿了 Inner1 的分割缝 —— 违反 §10.1。'))
return 0 if allok else 1
def main():
g, margin = 0.05, 1.0
if '--grid' in sys.argv:
g = float(sys.argv[sys.argv.index('--grid') + 1])
if '--margin' in sys.argv:
margin = float(sys.argv[sys.argv.index('--margin') + 1])
pads = json.load(open(os.path.join(HERE, '_pcb2_pads_full.json'), encoding='utf-8'))
r = run(JS, timeout=240)
if not r.get('success'):
sys.exit(u'读回失败: ' + json.dumps(r, ensure_ascii=False)[:400])
pours = dict((k, {'net': v['net'],
'polys': [p for p in (ring_to_poly(rg) for rg in v['rings']) if len(p) >= 3]})
for k, v in r['result'].items())
if '--routes' in sys.argv:
return by_routes(pours)
print(u'§10.1 参考面检查:顶层 5 段 `*J` 差分下方的 Inner1(窗口 = 包围盒外扩 %.1f mm)' % margin)
allok = True
for n in range(1, 6):
pts = [(p[4] / MIL, p[5] / MIL) for p in pads
if p[2] in ('USB%d_DPJ' % n, 'USB%d_DMJ' % n) and p[3] == 1]
if len(pts) != 4:
print(u' CH%d [!!] 只找到 %d 个焊盘,应为 4' % (n, len(pts)))
allok = False
continue
x0 = min(p[0] for p in pts) - margin
x1 = max(p[0] for p in pts) + margin
y0 = min(p[1] for p in pts) - margin
y1 = max(p[1] for p in pts) + margin
hits = []
for name, d in pours.items():
grid, nx, ny = cover(d['polys'], x0, y0, x1, y1, g)
cells = sum(sum(row) for row in grid)
if not cells:
continue
lab, sizes = components(grid, nx, ny)
hits.append((name, d['net'], cells, nx * ny, len(sizes)))
tot = nx * ny
if len(hits) == 1 and hits[0][4] == 1 and hits[0][2] == tot:
print(u' CH%d x %.2f~%.2f y %.2f~%.2f -> %s (%s) 一整块盖满 ✅'
% (n, x0, x1, y0, y1, hits[0][0], hits[0][1]))
else:
allok = False
print(u' CH%d x %.2f~%.2f y %.2f~%.2f -> ❌ %s'
% (n, x0, x1, y0, y1,
u' / '.join(u'%s(%s) 盖 %.1f%% %d 域'
% (a, b, 100.0 * c / t, k) for a, b, c, t, k in hits)
or u'下方没有任何 Inner1 铜'))
print(u'%s' % (u'五段全部落在同一块连续铜上。' if allok
else u'!! 有段下方是分割缝或多块铜 —— 违反 §10.1,要挪岛或挪线。'))
return 0 if allok else 1
if __name__ == '__main__':
sys.exit(main())