R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,249 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""载板侧键触点盘 J41~J60 的**定点重摆 + GND 盘扇出**(2026-09-07,顶针改中框式焊盘之后)。
|
||||
|
||||
/c/Python314/python .claude/pcb2_keypads.py # 只算:目标坐标 + GND 扇出候选
|
||||
/c/Python314/python .claude/pcb2_keypads.py --push # 只摆这 20 个盘(先删 GND 盘上的旧短线/过孔),读回核对
|
||||
/c/Python314/python .claude/pcb2_keypads.py --gnd # 重抓障碍表 -> 给 5 个 GND 盘各打一个过孔 + 短线(净距校核过才下发)
|
||||
|
||||
## 为什么不用 `pcb2_place.py --push`
|
||||
|
||||
它会重摆全部 218 个元件;导入变更后只有这 20 个盘换了封装、坐标要按新 `POGO_XY` 挪,
|
||||
其它元件一个都不该碰(碰了就要整板重布)。坐标仍从 `pcb2_place.plan()` 取 —— 事实来源不变。
|
||||
|
||||
## GND 盘的扇出
|
||||
|
||||
15 根 POGO*_* 信号由 `pcb2_route_sig.py --net ...` 重布(它整网删掉重画);
|
||||
GND 是 `PRE_ONLY`,布线器不给它拉线,所以 5 个 GND 盘(J44/J48/J52/J56/J60)在这里手工扇出:
|
||||
盘心 -> 0.30 mm 顶层短线 -> Ø0.61/0.305 过孔,落到 Inner2 地平面。
|
||||
过孔放在盘的 **u− 侧**(触点菱形里 GND 本来就在最靠机身侧边那一角,那一侧没有别的盘),
|
||||
候选位置逐个过净距校核(对焊盘 / 过孔 / 两层导线,判据同 `pcb2_route_dp.CLR`)。
|
||||
"""
|
||||
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, HERE)
|
||||
from eda import run
|
||||
import pcb2_place as P
|
||||
import pcb2_route_dp as D
|
||||
|
||||
MIL = P.MIL
|
||||
PCB = D.PCB
|
||||
W_STUB = 0.30
|
||||
VIA_D, VIA_H, CLR = D.VIA_D, D.VIA_H, D.CLR
|
||||
HOLE2HOLE = 0.30
|
||||
STATE = os.path.join(HERE, '_pcb2_keypads_state.json')
|
||||
|
||||
|
||||
def targets():
|
||||
pos = P.plan()
|
||||
out = {}
|
||||
for n in P.SITES:
|
||||
d = P.desig(n)
|
||||
for i, ref in enumerate(d['pogo']):
|
||||
out[ref] = {'xy': pos[ref][:2], 'rot': pos[ref][2], 'site': n, 'tag': P.POGO_TAG[i]}
|
||||
return out
|
||||
|
||||
|
||||
def gnd_refs():
|
||||
return [P.desig(n)['pogo'][3] for n in P.SITES] # POGO_XY[3] = GND
|
||||
|
||||
|
||||
def call(js, timeout=300):
|
||||
r = run("await eda.dmt_EditorControl.openDocument('%s');"
|
||||
"await new Promise(r=>setTimeout(r,1500));" % PCB + js, timeout=timeout)
|
||||
if not r.get('success'):
|
||||
sys.exit(u'桥接失败: ' + json.dumps(r, ensure_ascii=False)[:600])
|
||||
return r['result']
|
||||
|
||||
|
||||
JS_STUB_DEL = u"""
|
||||
// 删掉挂在 C(盘心表)上的 GND 顶层短线(端点离盘心 <= 1.05)以及短线另一端的 GND 过孔。调用方先声明 M / C。
|
||||
let dl = 0, dv = 0;
|
||||
const vias = await eda.pcb_PrimitiveVia.getAll();
|
||||
for (const l of await eda.pcb_PrimitiveLine.getAll()) {
|
||||
if (l.getState_Layer() !== 1 || l.getState_Net() !== 'GND') continue;
|
||||
const e = [[l.getState_StartX()/M, l.getState_StartY()/M], [l.getState_EndX()/M, l.getState_EndY()/M]];
|
||||
let hit = -1;
|
||||
for (const [cx, cy] of C) for (let k = 0; k < 2; k++) if (Math.hypot(e[k][0]-cx, e[k][1]-cy) <= 1.05) hit = 1 - k;
|
||||
if (hit < 0) continue;
|
||||
for (const v of vias) if (v.getState_Net() === 'GND' && Math.hypot(v.getState_X()/M - e[hit][0], v.getState_Y()/M - e[hit][1]) < 0.05) { await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dv++; }
|
||||
await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def push_pos():
|
||||
T = targets()
|
||||
plan = [[ref, round(t['xy'][0] * MIL, 2), round(t['xy'][1] * MIL, 2), t['rot']] for ref, t in sorted(T.items())]
|
||||
r = call(u"""
|
||||
const M = %f, PLAN = %s, GREF = %s;
|
||||
const map = {};
|
||||
for (const c of await eda.pcb_PrimitiveComponent.getAll()) map[c.getState_Designator()] = c;
|
||||
const old = {};
|
||||
for (const [d] of PLAN) { const c = map[d]; if (c) old[d] = [c.getState_X()/M, c.getState_Y()/M]; }
|
||||
const C = GREF.map(d => old[d]).filter(Boolean);
|
||||
""" % (MIL, json.dumps(plan), json.dumps(gnd_refs())) + JS_STUB_DEL + u"""
|
||||
let n = 0;
|
||||
for (const [d, x, y, rot] of PLAN) {
|
||||
const c = map[d]; if (!c) continue;
|
||||
await eda.pcb_PrimitiveComponent.modify(c.getState_PrimitiveId(), {layer: 1});
|
||||
await eda.pcb_PrimitiveComponent.modify(c.getState_PrimitiveId(), {x, y, rotation: rot});
|
||||
n++;
|
||||
}
|
||||
await eda.pcb_Document.save();
|
||||
return {n, old, dl, dv};
|
||||
""")
|
||||
print(u'已摆 %d 个盘;删掉旧 GND 短线 %d 条 / 过孔 %d 个' % (r['n'], r['dl'], r['dv']))
|
||||
json.dump(r['old'], io.open(STATE, 'w', encoding='utf-8'))
|
||||
verify()
|
||||
|
||||
|
||||
def verify():
|
||||
T = targets()
|
||||
r = call(u"""
|
||||
const M = %f, W = %s, out = {};
|
||||
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
|
||||
const d = c.getState_Designator(); if (!W.includes(d)) continue;
|
||||
const pins = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId());
|
||||
const p = pins[0];
|
||||
out[d] = [c.getState_X()/M, c.getState_Y()/M, c.getState_Layer(), p.x/M, p.y/M, p.net, +p.pad[1]/M];
|
||||
}
|
||||
return out;
|
||||
""" % (MIL, json.dumps(sorted(T))))
|
||||
bad = 0
|
||||
for ref, t in sorted(T.items()):
|
||||
x, y, lay, px, py, net, dia = r[ref]
|
||||
dd = math.hypot(px - t['xy'][0], py - t['xy'][1])
|
||||
ok = dd < 0.01 and lay == 1 and abs(dia - P.POGO_PAD_D) < 0.02
|
||||
bad += not ok
|
||||
print(u' %s %-4s site%d %-8s 焊盘 (%.3f, %.3f) 目标 (%.3f, %.3f) 差 %.4f L%d Ø%.2f %s'
|
||||
% (u'✔' if ok else u'✘', ref, t['site'], t['tag'], px, py, t['xy'][0], t['xy'][1], dd, lay, dia, net))
|
||||
if bad:
|
||||
sys.exit(u'!! %d 个盘没摆到位' % bad)
|
||||
print(u'20 个盘全部到位,Ø%.1f 顶层' % P.POGO_PAD_D)
|
||||
|
||||
|
||||
# ═══════════════ GND 扇出 ═══════════════
|
||||
def _clear(x, y, r, lay, pads, vias, lines, skip_ref):
|
||||
u"""圆 (x,y,r) 在 lay 层(12 = 双层)离所有异网铜是否 >= CLR;同时对任何孔保 HOLE2HOLE。"""
|
||||
lays = {1, 2} if lay == 12 else {lay}
|
||||
for q in pads:
|
||||
if q['ref'] == skip_ref:
|
||||
continue
|
||||
ql = {1, 2} if q['lay'] == 12 else {q['lay']}
|
||||
if not (ql & lays):
|
||||
continue
|
||||
d = D.seg_rect((x, y), (x, y), q['x'], q['y'], q['hw'], q['hh'])
|
||||
need = r + CLR if q['net'] != 'GND' else 0.0
|
||||
if q['lay'] == 12: # 通孔:还要孔到孔
|
||||
need = max(need, r + HOLE2HOLE) if lay == 12 else need
|
||||
if d < need:
|
||||
return False, u'焊盘 %s.%s(%s) %.2f' % (q['ref'], q['num'], q['net'], d)
|
||||
for v in vias:
|
||||
d = math.hypot(v['x'] - x, v['y'] - y)
|
||||
need = r + v['r'] + CLR if v['net'] != 'GND' else 0.0
|
||||
if lay == 12:
|
||||
need = max(need, r + v['r'] + HOLE2HOLE)
|
||||
if d < need:
|
||||
return False, u'过孔(%s) %.2f' % (v['net'], d)
|
||||
for l in lines:
|
||||
if l['lay'] not in lays or l['net'] == 'GND':
|
||||
continue
|
||||
d = D.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], x, y)
|
||||
if d < r + l['w'] / 2 + CLR:
|
||||
return False, u'导线(%s L%d) %.2f' % (l['net'], l['lay'], d)
|
||||
return True, ''
|
||||
|
||||
|
||||
def _seg_clear(a, b, w, pads, vias, lines, skip_ref):
|
||||
for q in pads:
|
||||
if q['ref'] == skip_ref or q['net'] == 'GND':
|
||||
continue
|
||||
if q['lay'] not in (1, 12):
|
||||
continue
|
||||
if D.seg_rect(a, b, q['x'], q['y'], q['hw'], q['hh']) < w / 2 + CLR:
|
||||
return False, u'焊盘 %s.%s(%s)' % (q['ref'], q['num'], q['net'])
|
||||
for v in vias:
|
||||
if v['net'] == 'GND':
|
||||
continue
|
||||
if D.seg_pt(a[0], a[1], b[0], b[1], v['x'], v['y']) < w / 2 + v['r'] + CLR:
|
||||
return False, u'过孔(%s)' % v['net']
|
||||
for l in lines:
|
||||
if l['lay'] != 1 or l['net'] == 'GND':
|
||||
continue
|
||||
if D.seg_seg(a, b, (l['x0'], l['y0']), (l['x1'], l['y1'])) < w / 2 + l['w'] / 2 + CLR:
|
||||
return False, u'导线(%s)' % l['net']
|
||||
return True, ''
|
||||
|
||||
|
||||
def plan_gnd(pads, vias, lines):
|
||||
T = targets()
|
||||
out = {}
|
||||
for ref in gnd_refs():
|
||||
t = T[ref]
|
||||
x, y = t['xy']
|
||||
rho = P.SITES[t['site']][2]
|
||||
ux = -1.0 if rho == 0 else 1.0 # 本体 u− 方向在板系里的 x 分量
|
||||
cands = []
|
||||
for dist in (1.85, 2.0, 2.2, 2.4):
|
||||
for (dx, dy) in ((ux, 0.0), (ux * 0.7071, 0.7071), (ux * 0.7071, -0.7071), (0.0, 1.0), (0.0, -1.0)):
|
||||
cands.append((x + dx * dist, y + dy * dist))
|
||||
chosen = None
|
||||
why = []
|
||||
for vx, vy in cands:
|
||||
vx, vy = round(vx, 3), round(vy, 3)
|
||||
ok, msg = _clear(vx, vy, VIA_D / 2, 12, pads, vias, lines, ref)
|
||||
if not ok:
|
||||
why.append(u'(%.2f,%.2f) 孔撞 %s' % (vx, vy, msg)); continue
|
||||
ok, msg = _seg_clear((x, y), (vx, vy), W_STUB, pads, vias, lines, ref)
|
||||
if not ok:
|
||||
why.append(u'(%.2f,%.2f) 线撞 %s' % (vx, vy, msg)); continue
|
||||
chosen = (vx, vy)
|
||||
break
|
||||
if chosen is None:
|
||||
print(u'!! %s 找不到扇出位置:' % ref)
|
||||
for w in why[:8]:
|
||||
print(u' ' + w)
|
||||
sys.exit(1)
|
||||
out[ref] = {'pad': (x, y), 'via': chosen}
|
||||
print(u' %s site%d 盘 (%.2f, %.2f) -> 过孔 (%.2f, %.2f) 距 %.2f' % (ref, t['site'], x, y, chosen[0], chosen[1], math.hypot(chosen[0] - x, chosen[1] - y)))
|
||||
return out
|
||||
|
||||
|
||||
def push_gnd():
|
||||
D.dump()
|
||||
pads, vias, lines = D.geom()
|
||||
plan = plan_gnd(pads, vias, lines)
|
||||
centers = [list(v['pad']) for v in plan.values()]
|
||||
segs = [['GND', 1, v['pad'][0], v['pad'][1], v['via'][0], v['via'][1], W_STUB] for v in plan.values()]
|
||||
vs = [['GND', v['via'][0], v['via'][1]] for v in plan.values()]
|
||||
r = call(u"const M = %f; const C = %s;" % (MIL, json.dumps(centers)) + JS_STUB_DEL + u"""
|
||||
const SEGS = %s, VIAS = %s;
|
||||
let nl = 0, nv = 0;
|
||||
for (const [net, lay, x0, y0, x1, y1, w] of SEGS) { await eda.pcb_PrimitiveLine.create(net, lay, x0 * M, y0 * M, x1 * M, y1 * M, w * M); nl++; }
|
||||
for (const [net, x, y] of VIAS) { await eda.pcb_PrimitiveVia.create(net, x * M, y * M, %f * M, %f * M); nv++; }
|
||||
await eda.pcb_Document.save();
|
||||
return {dl, dv, nl, nv};
|
||||
""" % (json.dumps(segs), json.dumps(vs), VIA_H, VIA_D))
|
||||
print(u'GND 扇出下发:删旧 %d 线 / %d 孔,新画 %d 线 / %d 孔' % (r['dl'], r['dv'], r['nl'], r['nv']))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
T = targets()
|
||||
for ref, t in sorted(T.items()):
|
||||
print(u' %-4s site%d %-8s -> (%.3f, %.3f) rot %d' % (ref, t['site'], t['tag'], t['xy'][0], t['xy'][1], t['rot']))
|
||||
if '--push' in sys.argv:
|
||||
push_pos()
|
||||
elif '--verify' in sys.argv:
|
||||
verify()
|
||||
if '--gnd' in sys.argv:
|
||||
push_gnd()
|
||||
elif '--push' not in sys.argv and '--verify' not in sys.argv:
|
||||
if os.path.exists(D.GEOM):
|
||||
print(u'\nGND 扇出候选(按现有障碍表,不下发):')
|
||||
plan_gnd(*D.geom())
|
||||
在新工单中引用
屏蔽一个用户