250 行
11 KiB
Python
250 行
11 KiB
Python
# -*- 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())
|