原理图/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
244 行
11 KiB
Python
244 行
11 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""把 Inner1 上**真正铺出来的铜**读回来栅格化,量连通性与最窄断面。
|
||
|
||
/c/Python314/python .claude/inner1_verify.py [--grid 0.05]
|
||
|
||
## 为什么不能只看 `pcb2_pour.py --verify` 打印的「段数」
|
||
|
||
`getState_PourFills()` 把一块铜面吐成十几个 fill,**那多半是切块不是断开**:
|
||
相邻块共边。所以「17 段」既可能是一整块被切碎,也可能真碎成 17 座孤岛,
|
||
段数本身分辨不出来。唯一靠得住的判据是**把所有环画进同一张栅格再做连通域**。
|
||
|
||
`inner1_check.py` 是**下发前**按 ISLANDS 多边形 + 反焊盘自己算的(预测),
|
||
本脚本是**下发后**按 EDA 实际吐出来的铜算的(实测)。两个都要过。
|
||
|
||
⚠️ 环的格式是 `x1 y1 'L' x2 y2 ... 'ARC' 角度 ex ey ...`,
|
||
'ARC' 后面第一个数是**角度不是坐标**;这里按起点/终点/张角反解圆心,
|
||
再按 <=5° 细分成折线 —— 直接把弧当弦会把反焊盘算大、低估颈宽。
|
||
"""
|
||
import json, math, os, sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
from eda import run
|
||
from pcb2_pour import ISLANDS, PCB, MIL, body_bbox
|
||
|
||
U10MIL = 3.9370078740157477 # getState_PourFills() 的坐标单位:10 mil
|
||
NECK_MIN = 7.0
|
||
|
||
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) continue;
|
||
let f = []; try { f = q.getState_PourFills() || []; } catch (e) {}
|
||
const rings = [];
|
||
let spokes = 0;
|
||
for (const seg of f) {
|
||
const cp = (seg.path && seg.path.complexPolygon) || [];
|
||
// fill=false 的 seg 是**热焊盘十字花的辐条**(描边折线,lineWidth 一般 0.254),
|
||
// 不是填充区。出现它就说明这块铜面的焊盘连接方式退回了「十字花」。
|
||
if (seg.fill === false) { spokes += cp.length; continue; }
|
||
for (const r of cp) rings.push(r);
|
||
}
|
||
out[info[0]] = {net: info[1], layer: info[2], rings: rings, spokes: spokes, nfill: f.length};
|
||
}
|
||
return out;
|
||
""" % PCB
|
||
|
||
|
||
def ring_to_poly(ring):
|
||
u"""一个环 -> 折线点列(mm)。ARC 按 <=5° 细分。"""
|
||
pts, i = [], 0
|
||
cur = None
|
||
while i < len(ring):
|
||
v = ring[i]
|
||
if v == 'L':
|
||
i += 1
|
||
continue
|
||
if v == 'ARC':
|
||
i += 1
|
||
ang = ring[i]
|
||
ex, ey = ring[i + 1], ring[i + 2]
|
||
i += 3
|
||
x0, y0 = cur
|
||
th = math.radians(ang)
|
||
mx, my = (x0 + ex) / 2.0, (y0 + ey) / 2.0
|
||
dx, dy = ex - x0, ey - y0
|
||
c = math.hypot(dx, dy)
|
||
if c < 1e-9 or abs(math.sin(th / 2.0)) < 1e-9:
|
||
pts.append((ex, ey))
|
||
cur = (ex, ey)
|
||
continue
|
||
R = c / (2.0 * math.sin(th / 2.0)) # 带符号
|
||
d = math.sqrt(max(R * R - (c / 2.0) ** 2, 0.0)) * (1 if R > 0 else -1)
|
||
ox, oy = -dy / c, dx / c # 左法向
|
||
ccx, ccy = mx + ox * d, my + oy * d
|
||
a0 = math.atan2(y0 - ccy, x0 - ccx)
|
||
n = max(2, int(abs(ang) / 5.0) + 1)
|
||
for k in range(1, n + 1):
|
||
a = a0 + th * k / n
|
||
pts.append((ccx + abs(R) * math.cos(a), ccy + abs(R) * math.sin(a)))
|
||
cur = (ex, ey)
|
||
continue
|
||
x, y = ring[i], ring[i + 1]
|
||
i += 2
|
||
pts.append((x, y))
|
||
cur = (x, y)
|
||
return [(x / U10MIL, y / U10MIL) for x, y in pts]
|
||
|
||
|
||
def raster(polys, g):
|
||
u"""所有环画进一张栅格,奇偶填充(外环填、内环挖)。"""
|
||
xs = [p[0] for q in polys for p in q]
|
||
ys = [p[1] for q in polys for p in q]
|
||
x0, x1, y0, y1 = min(xs) - g, max(xs) + g, min(ys) - g, max(ys) + g
|
||
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 = []
|
||
for (ax, ay), (bx, by) in edges:
|
||
if (ay > y) != (by > y):
|
||
cr.append(ax + (bx - ax) * (y - ay) / (by - ay))
|
||
cr.sort()
|
||
row = grid[j]
|
||
for t in range(0, len(cr) - 1, 2): # 奇偶填充
|
||
i0 = max(0, int(math.ceil((cr[t] - x0) / g - 0.5)))
|
||
i1 = min(nx - 1, int(math.floor((cr[t + 1] - x0) / g - 0.5)))
|
||
for i in range(i0, i1 + 1):
|
||
row[i] = 1
|
||
return grid, x0, y0, nx, ny
|
||
|
||
|
||
def components(grid, nx, ny):
|
||
lab = [[0] * nx for _ in range(ny)]
|
||
k, sizes = 0, {}
|
||
for j in range(ny):
|
||
for i in range(nx):
|
||
if not grid[j][i] or lab[j][i]:
|
||
continue
|
||
k += 1
|
||
st = [(j, i)]
|
||
lab[j][i] = k
|
||
n = 0
|
||
while st:
|
||
a, b = st.pop()
|
||
n += 1
|
||
for da, db in ((1, 0), (-1, 0), (0, 1), (0, -1)):
|
||
p, q = a + da, b + db
|
||
if 0 <= p < ny and 0 <= q < nx and grid[p][q] and not lab[p][q]:
|
||
lab[p][q] = k
|
||
st.append((p, q))
|
||
sizes[k] = n
|
||
return lab, sizes
|
||
|
||
|
||
def main():
|
||
g = 0.05
|
||
if '--grid' in sys.argv:
|
||
g = float(sys.argv[sys.argv.index('--grid') + 1])
|
||
r = run(JS, timeout=240)
|
||
if not r.get('success'):
|
||
sys.exit(u'读回失败: ' + json.dumps(r, ensure_ascii=False)[:400])
|
||
data = r['result']
|
||
pads = json.load(open(os.path.join(HERE, '_pcb2_pads_full.json'), encoding='utf-8'))
|
||
vias = json.load(open(os.path.join(HERE, '_pcb2_vias.json'), encoding='utf-8'))
|
||
|
||
print(u'Inner1 实测铜面校核(栅格 %.2f mm,颈宽下限 %.1f mm)' % (g, NECK_MIN))
|
||
allok = True
|
||
for name, net, _m, _n in ISLANDS:
|
||
key = u'%s-INNER1' % name
|
||
d = data.get(key)
|
||
if not d:
|
||
print(u' %-12s [!!] 没读到这块铜面' % key)
|
||
allok = False
|
||
continue
|
||
if d.get('spokes'):
|
||
print(u' %-12s [!!] 有 %d 条十字花辐条 —— 这块铜面的焊盘连接方式不是「全连接」,'
|
||
u'4 A 只能从 0.254 mm 的辐条里过。去 pcb2_rules.py 看 PLANE-DIRECT 有没有指派上。'
|
||
% (key, d['spokes']))
|
||
allok = False
|
||
polys = [p for p in (ring_to_poly(rg) for rg in d['rings']) if len(p) >= 3]
|
||
grid, x0, y0, nx, ny = raster(polys, g)
|
||
lab, sizes = components(grid, nx, ny)
|
||
main_k = max(sizes, key=sizes.get) if sizes else 0
|
||
area = sizes.get(main_k, 0) * g * g
|
||
tot = sum(sizes.values()) * g * g
|
||
bad, own = [], 0
|
||
for ref, num, pnet, layer, px, py, shape, hole in pads:
|
||
if pnet != net or layer not in (12, 15):
|
||
continue
|
||
own += 1
|
||
px, py = px / MIL, py / MIL
|
||
rad = max(shape[1], shape[2] if len(shape) > 2 and isinstance(shape[2], (int, float)) else shape[1])
|
||
rr = int((rad / MIL / 2.0 + 0.35) / g) + 1
|
||
i, j, hit = int((px - x0) / g), int((py - y0) / g), False
|
||
for q in range(max(0, j - rr), min(ny, j + rr + 1)):
|
||
for p in range(max(0, i - rr), min(nx, i + rr + 1)):
|
||
if lab[q][p] == main_k:
|
||
hit = True
|
||
break
|
||
if hit:
|
||
break
|
||
if not hit:
|
||
bad.append('%s.%s' % (ref, num))
|
||
# ⚠️ 焊盘之外,**本网的过孔也必须落在主域里**:贴片电容全靠这一个孔接岛
|
||
# (`pcb2_vo_vias.py` 打的 25 个)。只查焊盘的话,孔掉进边角碎片里没人发现。
|
||
nvia = 0
|
||
for vnet, vx, vy, dia, _hole in vias:
|
||
if vnet != net:
|
||
continue
|
||
nvia += 1
|
||
vx, vy = vx / MIL, vy / MIL
|
||
rr = int((dia / MIL / 2.0 + 0.35) / g) + 1
|
||
i, j, hit = int((vx - x0) / g), int((vy - y0) / g), False
|
||
for q in range(max(0, j - rr), min(ny, j + rr + 1)):
|
||
for pp in range(max(0, i - rr), min(nx, i + rr + 1)):
|
||
if lab[q][pp] == main_k:
|
||
hit = True
|
||
break
|
||
if hit:
|
||
break
|
||
if not hit:
|
||
bad.append(u'过孔(%.2f,%.2f)' % (vx, vy))
|
||
# v2:颈宽只在走廊主体里量(穿筋的颈 3 mm 只走纹波电流,§25.16)
|
||
rx0, ry0, rx1, ry1 = body_bbox(_m)
|
||
# 边界行只剩铺铜内缩后的一丝铜(实测 2.8 = 穿筋的颈),各缩 0.15 再量
|
||
rx0, ry0, rx1, ry1 = rx0 + 0.15, ry0 + 0.15, rx1 - 0.15, ry1 - 0.15
|
||
rowsY = [t for t in ((sum(1 for i in range(nx) if lab[j][i] == main_k and rx0 <= x0 + (i + 0.5) * g <= rx1) * g,
|
||
y0 + (j + 0.5) * g)
|
||
for j in range(ny) if ry0 <= y0 + (j + 0.5) * g <= ry1) if t[0] > 0]
|
||
colsX = [t for t in ((sum(1 for j in range(ny) if lab[j][i] == main_k and ry0 <= y0 + (j + 0.5) * g <= ry1) * g,
|
||
x0 + (i + 0.5) * g)
|
||
for i in range(nx) if rx0 <= x0 + (i + 0.5) * g <= rx1) if t[0] > 0]
|
||
my, mx = min(rowsY), min(colsX)
|
||
ok = (not bad) and my[0] >= NECK_MIN and mx[0] >= NECK_MIN
|
||
allok = allok and ok
|
||
print(u' %-12s %-7s %d 环 / %d 连通域,主域 %.0f mm²(占本网铜的 %.1f%%)'
|
||
% (key, net, len(polys), len(sizes), area, 100.0 * area / tot if tot else 0))
|
||
print(u' %d 焊盘 + %d 过孔 %s 最窄横断面 %5.2f @ y=%.2f 最窄纵断面 %5.2f @ x=%.2f %s'
|
||
% (own, nvia, u'全在主域 ✔' if not bad else u'✘ 掉队: ' + ' '.join(bad),
|
||
my[0], my[1], mx[0], mx[1], u'✅' if ok else u'❌'))
|
||
for key in (u'GND-INNER1', u'GND-INNER2'):
|
||
d = data.get(key)
|
||
if d:
|
||
print(u' %-12s %-7s %d 环' % (key, d['net'], len(d['rings'])))
|
||
print(u'%s' % (u'实测全部通过。' if allok else u'!! 实测有不达标的岛。'))
|
||
return 0 if allok else 1
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|