R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""R900~R909 的地端扇出:每颗一条短线 + 一个过孔落到 Inner2 地平面。
|
||||
|
||||
/c/Python314/python .claude/pcb2_strap_gnd.py # 只规划
|
||||
/c/Python314/python .claude/pcb2_strap_gnd.py --push # 下发 + 存盘
|
||||
|
||||
为什么不跑 `pcb2_fanout.py`:那个是**整板**扇出,文件头写明「布局一动就要
|
||||
`--clean` → 重新 dump → `--push`」三步一起 —— 会把板上 415 个扇出孔全拆了重打,
|
||||
而现在只是多了十颗 0402。照 `pcb2_keypads.py --gnd` 的先例做局部扇出(那次是 5 个 GND 盘),
|
||||
净距判据直接复用它的 `_clear` / `_seg_clear`,和整板扇出是同一套。
|
||||
|
||||
⚠️ 元件在**底层**,所以短线画在 layer 2(keypads 那次的盘在顶层,画的是 layer 1)。
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
from eda import run
|
||||
import pcb2_route_dp as D
|
||||
import pcb2_keypads as K
|
||||
|
||||
PCB = '942ed1560d81c1e7'
|
||||
MIL = 39.3700787401575
|
||||
LAY = 2 # 底层
|
||||
W_STUB = K.W_STUB
|
||||
VIA_D, VIA_H = K.VIA_D, K.VIA_H
|
||||
REFS = ['R900', 'R901', 'R902', 'R903', 'R904', 'R905', 'R906', 'R907', 'R908', 'R909']
|
||||
|
||||
|
||||
def call(js, timeout=300):
|
||||
r = run("await eda.dmt_EditorControl.openDocument('%s');"
|
||||
"await new Promise(r=>setTimeout(r,2000));" % PCB + js, timeout=timeout)
|
||||
if not r.get('success'):
|
||||
sys.exit(u'桥接失败: ' + json.dumps(r, ensure_ascii=False)[:600])
|
||||
return r['result']
|
||||
|
||||
|
||||
def gnd_pads():
|
||||
u"""读回这十颗电阻的 GND 焊盘中心(mm)。"""
|
||||
return call("""
|
||||
const name = {};
|
||||
for (const c of await eda.pcb_PrimitiveComponent.getAll()) name[c.getState_PrimitiveId()] = c.getState_Designator();
|
||||
const ids = Object.keys(name).sort((a,b)=>b.length-a.length);
|
||||
const M = %f, out = {};
|
||||
for (const p of await eda.pcb_PrimitivePad.getAll()) {
|
||||
const pid = p.primitiveId || '';
|
||||
let hit = null;
|
||||
for (const id of ids) if (pid.length > id.length && pid.indexOf(id) === 0) { hit = name[id]; break; }
|
||||
if (!hit || !/^R9[0-9][0-9]$/.test(hit)) continue;
|
||||
if (p.getState_Net() !== 'GND') continue;
|
||||
out[hit] = [Math.round(p.getState_X()/M*1000)/1000, Math.round(p.getState_Y()/M*1000)/1000];
|
||||
}
|
||||
return out;""" % MIL)
|
||||
|
||||
|
||||
def plan(pads, vias, lines, gp):
|
||||
out, bad = {}, []
|
||||
for ref in REFS:
|
||||
x, y = gp[ref]
|
||||
# ⚠️ 第一版只试 5 个方向 x 5 个距离,四颗找不到位置。载板这一片太密,
|
||||
# 候选集必须放开:每 15 度一个方向、0.9~3.5 mm 每 0.1 一档,由近到远。
|
||||
cands = []
|
||||
for dist in [d / 10.0 for d in range(9, 36)]:
|
||||
for a in range(0, 360, 15):
|
||||
th = math.radians(a)
|
||||
cands.append((round(x + math.cos(th) * dist, 3), round(y + math.sin(th) * dist, 3)))
|
||||
chosen, why = None, []
|
||||
placed = [v['via'] for v in out.values()]
|
||||
for vx, vy in cands:
|
||||
# 本轮新打的过孔之间也要保钻孔间距(skill §9v)
|
||||
if any(math.hypot(vx - ax, vy - ay) < 0.33 + VIA_H for ax, ay in placed):
|
||||
continue
|
||||
ok, msg = K._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 = K._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:
|
||||
bad.append((ref, why[:6]))
|
||||
continue
|
||||
out[ref] = {'pad': (x, y), 'via': chosen}
|
||||
print(u' %-5s 盘 (%7.3f,%6.3f) -> 过孔 (%7.3f,%6.3f) 距 %.2f mm'
|
||||
% (ref, x, y, chosen[0], chosen[1], math.hypot(chosen[0] - x, chosen[1] - y)))
|
||||
# 新打的过孔彼此也要判钻孔间距(skill §9v 补的那一条)
|
||||
ks = list(out)
|
||||
for i in range(len(ks)):
|
||||
for j in range(i + 1, len(ks)):
|
||||
a, b = out[ks[i]]['via'], out[ks[j]]['via']
|
||||
if math.hypot(a[0] - b[0], a[1] - b[1]) < 0.33 + VIA_H:
|
||||
bad.append((ks[i] + '/' + ks[j], [u'两个新过孔钻孔间距不足']))
|
||||
if bad:
|
||||
for ref, why in bad:
|
||||
print(u'!! %s 找不到扇出位置:' % ref)
|
||||
for w in why:
|
||||
print(u' ' + w)
|
||||
sys.exit(1)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
D.dump()
|
||||
pads, vias, lines = D.geom()
|
||||
gp = gnd_pads()
|
||||
missing = [r for r in REFS if r not in gp]
|
||||
if missing:
|
||||
sys.exit(u'板上找不到这些 GND 焊盘: %s' % missing)
|
||||
p = plan(pads, vias, lines, gp)
|
||||
print(u'十条地端扇出全部找到位置(净距判据同整板扇出)')
|
||||
if '--push' not in sys.argv:
|
||||
print(u'加 --push 下发')
|
||||
return
|
||||
segs = [['GND', LAY, v['pad'][0], v['pad'][1], v['via'][0], v['via'][1], W_STUB] for v in p.values()]
|
||||
vs = [['GND', v['via'][0], v['via'][1]] for v in p.values()]
|
||||
r = call(u"""
|
||||
const M = %f, 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 {nl, nv};""" % (MIL, json.dumps(segs), json.dumps(vs), VIA_H, VIA_D))
|
||||
print(u'已下发:%d 条短线 / %d 个过孔' % (r['nl'], r['nv']))
|
||||
|
||||
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户