321 行
15 KiB
Python
321 行
15 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""载板 PCB2:把 `CFG_SEL=110` 那次原理图变更(`c2_cfgsel.py`)在板上收尾。
|
||
|
||
/c/Python314/python .claude/pcb2_cfgsel.py --clean # 删掉变成异网/孤儿的旧铜
|
||
/c/Python314/python .claude/pcb2_cfgsel.py --refresh # 重抓障碍表(--clean 之后必须做)
|
||
/c/Python314/python .claude/pcb2_cfgsel.py # 算两条新走线 + 净距校核,不下发
|
||
/c/Python314/python .claude/pcb2_cfgsel.py --push # 只 create,不删任何东西
|
||
|
||
## 导入变更之后板上留下什么
|
||
|
||
`pcb1_import.py --board2 --apply` 只改焊盘的网络归属,**不动铜**。所以:
|
||
|
||
| 旧铜 | 现在的问题 |
|
||
|---|---|
|
||
| `U32.13` 往东那一小段 `GND` + 它的过孔 | pin 13 已经是 `+3V3_HUB` ⇒ **异网短路** |
|
||
| `R712.2` 往东那一小段 `GND` + 它的过孔 | R712.2 已经是 `+3V3_HUB` ⇒ **异网短路** |
|
||
| `PRTDIS6` / `PRTDIS7` 的 10 段线 | R714/R715 删了,网络已不存在 ⇒ 孤儿铜 |
|
||
|
||
⚠️ 这三类**净距校核一条都不报**(它按 `pad.net` 判同网跳过,而铜自己的 net 字段
|
||
还写着旧网名,两边对不上时反而被当成"同网"放过),只有 DRC 的连接性看得见。
|
||
所以删除这一步不能省,也不能等 DRC 来提醒。
|
||
|
||
## 为什么不用 `pcb2_route_sig.py --net +3V3_HUB`
|
||
|
||
`route_net()` 是**整网重布**:它从焊盘表重新长出生成树,而 `push_sig()` 下发前
|
||
先把该网络的铜全删。`+3V3_HUB` 有 33 个焊盘、几十段已经验过的铜,
|
||
为了接两个脚把整网推倒重来,风险远大于收益(板子现在停在 DRC 115 条的基线上)。
|
||
|
||
⇒ 这里只用布线器的**内核** `_connect()`("把两坨铜连起来"),
|
||
下发走**只 create 不 delete** 的路子(skill §9v:这种脚本必须自己记账,
|
||
所以新画的坐标存进 `_pcb2_cfgsel.json`,重跑 `--push` 先按坐标删掉自己上一次画的)。
|
||
|
||
## 两条连接
|
||
|
||
1. **`U32.13` → `+3V3_HUB`**:pin 13 在 QFN 西边 (95.55, 70.25)。
|
||
西边 x=96.325 那条走廊已经有一段本网铜(`(96.325,66.45)—(96.325,68.55)`,
|
||
U32.5↔U32.10 的内部连接),**顺着它往南延到 pin 13 的高度**即可,不用过孔。
|
||
2. **`R712.2` → `+3V3_HUB`**:R712 东边被 `HUB_RST`(y=66.45) 和 `CFGSEL0`(y=67.65)
|
||
两道横墙夹住,L2 上出不去,得翻到顶层。这一条交给 `_connect()` 找。
|
||
"""
|
||
import io
|
||
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 dp
|
||
import pcb2_route_sig as rs
|
||
|
||
PCB = dp.PCB
|
||
MIL = dp.MIL
|
||
LEDGER = os.path.join(HERE, '_pcb2_cfgsel.json')
|
||
|
||
# ---- 要删的旧铜。**按网络 + 坐标匹配**,不用 primitiveId(那个每次导入都会变)。
|
||
# 每一项 (net, layer, x0, y0, x1, y1);顺序无关,两端对调也算命中。
|
||
DEAD_LINES = [
|
||
('GND', 2, 95.926, 70.250, 96.501, 70.250), # U32.13 旧的对地出线
|
||
('GND', 2, 106.966, 67.050, 107.624, 67.050), # R712.2 旧的对地出线
|
||
('PRTDIS6', 2, 102.450, 62.133, 102.450, 62.716),
|
||
('PRTDIS6', 2, 102.450, 62.716, 101.250, 62.825),
|
||
('PRTDIS6', 2, 101.250, 63.550, 101.250, 62.825),
|
||
('PRTDIS7', 2, 101.360, 62.116, 101.322, 62.133),
|
||
('PRTDIS7', 2, 101.821, 62.133, 101.360, 62.116),
|
||
('PRTDIS7', 2, 101.322, 62.133, 100.593, 62.443),
|
||
('PRTDIS7', 2, 101.250, 62.133, 101.322, 62.133),
|
||
('PRTDIS7', 2, 101.322, 62.133, 101.821, 62.133),
|
||
('PRTDIS7', 2, 100.593, 62.443, 100.250, 62.825),
|
||
('PRTDIS7', 2, 100.250, 63.550, 100.250, 62.825),
|
||
# ⚠️ 2026-09-08 补:板上还有一条**跨整段的重叠图元**,上面那两条
|
||
# (101.250→101.322 与 101.322→101.821)盖不到它。当时 12/12 全删中、回查也说清零,
|
||
# 所以完全看不出来 —— 它跟着进了 `_pcb2_route_geom.json` 快照,
|
||
# 重建时又被重放回去,DRC 报「PRTDIS7 游离的导线」。
|
||
# ⇒ 教训:**删完要按「该网络还剩几条」回查,不能只数「删中几条」**。
|
||
('PRTDIS7', 2, 101.250, 62.133, 101.820, 62.133),
|
||
]
|
||
DEAD_VIAS = [
|
||
('GND', 96.501, 70.250), # U32.13 旧的对地过孔
|
||
('GND', 107.624, 67.050), # R712.2 旧的对地过孔
|
||
]
|
||
|
||
NET = '+3V3_HUB'
|
||
|
||
# ⚠️ 路径的 key 必须是 `网名` 或 `网名 + KSEP + 后缀` —— `dp.netof()` 拿第一段当网络名,
|
||
# `check()` 再用它判「同网跳过」。第一版把 key 写成位号('U32.13'),于是
|
||
# 「新铜搭上本网旧铜」这件**本来就该发生**的事被报成 8 条净距违规,
|
||
# 而真正的异网风险反倒没被检查到。
|
||
def _key(tag):
|
||
return NET + dp.KSEP + tag
|
||
|
||
|
||
# ---- 连接 1 手写(走廊已经被同网铜证明过,见文件头)。
|
||
# 语义见 skill §9r:第 i 点的 layer 是「从这一点走到下一点」那一段的层。
|
||
HAND = {
|
||
_key('U32.13'): [(95.926, 70.250, dp.L_BOT, 0.24),
|
||
(96.325, 70.250, dp.L_BOT, 0.24),
|
||
(96.325, 68.550, dp.L_BOT, 0.24)],
|
||
}
|
||
# ---- 连接 2 交给 _connect():(位号, 焊盘号, 线宽)
|
||
AUTO = [('R712', '2', 0.20)]
|
||
|
||
|
||
def call(js, what, timeout=600):
|
||
r = run(js, timeout=timeout)
|
||
if not r.get('success'):
|
||
sys.exit(u'%s 失败: %s' % (what, json.dumps(r, ensure_ascii=False)[:700]))
|
||
return r['result']
|
||
|
||
|
||
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
|
||
"await new Promise(r=>setTimeout(r,1800));" % PCB)
|
||
|
||
|
||
def clean():
|
||
js = OPEN + u"""
|
||
const MIL=%f, L=%s, V=%s, tol=0.02;
|
||
const hit=(a,b,c,d,p,q,r,s)=>
|
||
(Math.abs(a-p)<tol&&Math.abs(b-q)<tol&&Math.abs(c-r)<tol&&Math.abs(d-s)<tol)||
|
||
(Math.abs(a-r)<tol&&Math.abs(b-s)<tol&&Math.abs(c-p)<tol&&Math.abs(d-q)<tol);
|
||
let dl=0, dv=0; const missL=[], missV=[];
|
||
const seenL=new Set(), seenV=new Set();
|
||
for(const l of await eda.pcb_PrimitiveLine.getAll()){
|
||
const n=l.getState_Net()||'', lay=l.getState_Layer();
|
||
const a=l.getState_StartX()/MIL,b=l.getState_StartY()/MIL;
|
||
const c=l.getState_EndX()/MIL,d=l.getState_EndY()/MIL;
|
||
for(let i=0;i<L.length;i++){
|
||
const t=L[i];
|
||
if(n===t[0]&&lay===t[1]&&hit(a,b,c,d,t[2],t[3],t[4],t[5])){
|
||
await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++; seenL.add(i); break;}
|
||
}
|
||
}
|
||
for(const v of await eda.pcb_PrimitiveVia.getAll()){
|
||
const n=v.getState_Net()||'', x=v.getState_X()/MIL, y=v.getState_Y()/MIL;
|
||
for(let i=0;i<V.length;i++){
|
||
const t=V[i];
|
||
if(n===t[0]&&Math.abs(x-t[1])<tol&&Math.abs(y-t[2])<tol){
|
||
await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dv++; seenV.add(i); break;}
|
||
}
|
||
}
|
||
for(let i=0;i<L.length;i++) if(!seenL.has(i)) missL.push(L[i]);
|
||
for(let i=0;i<V.length;i++) if(!seenV.has(i)) missV.push(V[i]);
|
||
await eda.pcb_Document.save();
|
||
// skill §9ad:save() 的返回值不算数,回查一次还剩几条
|
||
let leftL=0, leftV=0;
|
||
for(const l of await eda.pcb_PrimitiveLine.getAll()){
|
||
const n=l.getState_Net()||'', lay=l.getState_Layer();
|
||
const a=l.getState_StartX()/MIL,b=l.getState_StartY()/MIL;
|
||
const c=l.getState_EndX()/MIL,d=l.getState_EndY()/MIL;
|
||
if(L.some(t=>n===t[0]&&lay===t[1]&&hit(a,b,c,d,t[2],t[3],t[4],t[5]))) leftL++;
|
||
}
|
||
for(const v of await eda.pcb_PrimitiveVia.getAll()){
|
||
const n=v.getState_Net()||'', x=v.getState_X()/MIL, y=v.getState_Y()/MIL;
|
||
if(V.some(t=>n===t[0]&&Math.abs(x-t[1])<tol&&Math.abs(y-t[2])<tol)) leftV++;
|
||
}
|
||
return {dl,dv,missL,missV,leftL,leftV};
|
||
""" % (MIL, json.dumps([list(x) for x in DEAD_LINES]), json.dumps([list(x) for x in DEAD_VIAS]))
|
||
r = call(js, u'删旧铜')
|
||
print(u'删除:导线 %d / %d,过孔 %d / %d' % (r['dl'], len(DEAD_LINES), r['dv'], len(DEAD_VIAS)))
|
||
if r['missL'] or r['missV']:
|
||
print(u'⚠️ 这些没找到(板上可能已经没有了,或坐标写错):')
|
||
for t in r['missL'] + r['missV']:
|
||
print(u' ' + json.dumps(t, ensure_ascii=False))
|
||
if r['leftL'] or r['leftV']:
|
||
sys.exit(u'回查还剩 %d 线 / %d 孔没删掉' % (r['leftL'], r['leftV']))
|
||
print(u'落盘后回查:目标铜已清零')
|
||
|
||
|
||
def build_routes():
|
||
u"""算出两条路径。返回 {key: path}。"""
|
||
pads, vias, lines = dp.geom()
|
||
rs.PADS_ALL = pads
|
||
obs = rs.shapes_of(pads, vias, lines)
|
||
|
||
routes = dict(HAND)
|
||
|
||
# 断言手写那条的起点确实落在 U32.13 的焊盘上(skill:手写坐标必须有断言兜底)
|
||
p13 = [q for q in pads if q['ref'] == 'U32' and q['num'] == '13']
|
||
if len(p13) != 1:
|
||
sys.exit(u'找不到 U32.13')
|
||
q = p13[0]
|
||
if q['net'] != NET:
|
||
sys.exit(u'U32.13 的网络是 %r,不是 %s —— 原理图变更没导进来?' % (q['net'], NET))
|
||
sx, sy = HAND[_key('U32.13')][0][0], HAND[_key('U32.13')][0][1]
|
||
if abs(sx - q['x']) > q['hw'] or abs(sy - q['y']) > q['hh']:
|
||
sys.exit(u'手写起点 (%.3f,%.3f) 不在 U32.13 焊盘内 (%.3f,%.3f ±%.3f/%.3f)'
|
||
% (sx, sy, q['x'], q['y'], q['hw'], q['hh']))
|
||
# 终点必须落在一段**本网**铜上,否则这条线是悬空的
|
||
ex, ey = HAND[_key('U32.13')][-1][0], HAND[_key('U32.13')][-1][1]
|
||
onnet = [l for l in lines if l['net'] == NET and l['lay'] == dp.L_BOT
|
||
and dp.seg_pt(l['x0'], l['y0'], l['x1'], l['y1'], ex, ey) < 0.02]
|
||
if not onnet:
|
||
sys.exit(u'手写终点 (%.3f,%.3f) 没搭在任何一段 %s 底层铜上' % (ex, ey, NET))
|
||
print(u'U32.13 手写路径:起点在焊盘内 ✓,终点搭在 %s 底层铜上 ✓' % NET)
|
||
|
||
# 手写那条立刻进障碍表,后面 _connect 才看得见它
|
||
for x0, y0, x1, y1, lay, w in dp.expand(HAND[_key('U32.13')])[0]:
|
||
obs.append((rs.sh_seg(x0, y0, x1, y1, w, lay), NET))
|
||
|
||
# 连接 2:把 R712.2 接到**已有的**本网铜上
|
||
have = []
|
||
for l in lines:
|
||
if l['net'] == NET:
|
||
have.append(rs.sh_seg(l['x0'], l['y0'], l['x1'], l['y1'], l['w'], l['lay']))
|
||
for v in vias:
|
||
if v['net'] == NET:
|
||
have.append(rs.sh_via(v['x'], v['y'], v['r']))
|
||
for ref, num, w in AUTO:
|
||
tgt = [q for q in pads if q['ref'] == ref and q['num'] == num]
|
||
if len(tgt) != 1:
|
||
sys.exit(u'找不到 %s.%s' % (ref, num))
|
||
q = tgt[0]
|
||
if q['net'] != NET:
|
||
sys.exit(u'%s.%s 的网络是 %r,不是 %s' % (ref, num, q['net'], NET))
|
||
lay = (dp.L_TOP, dp.L_BOT) if q['lay'] == 12 else (q['lay'],)
|
||
b = rs.Comp([rs.sh_rect(q['x'], q['y'], q['hw'], q['hh'], lay)],
|
||
(q['x'], q['y']), center=(q['x'], q['y']), layers=lay)
|
||
# `a` 用「离目标最近的那 40 段本网铜」,别把横跨全板的整个 +3V3_HUB 丢进去
|
||
# (skill / _connect 注释:窗口一大栅格就被放粗,QFN 边上的窄走廊会整片不可达)
|
||
near = sorted(have, key=lambda s: _dist_to(s, q['x'], q['y']))[:40]
|
||
a = rs.Comp(near, (q['x'], q['y']))
|
||
seg = None
|
||
for wtry in rs._widths(w, NET):
|
||
seg = rs._connect(NET, a, b, obs, wtry)
|
||
if seg is not None:
|
||
if wtry != w:
|
||
print(u'%s.%s 收窄到 %.2f 才通' % (ref, num, wtry))
|
||
break
|
||
if seg is None:
|
||
sys.exit(u'%s.%s 布不通' % (ref, num))
|
||
path, shp = seg
|
||
routes[_key('%s.%s' % (ref, num))] = path
|
||
obs.extend((s_, NET) for s_ in shp)
|
||
print(u'%s.%s 布通:%d 点 / %d 段 / %d 孔'
|
||
% (ref, num, len(path), len(dp.expand(path)[0]), len(dp.expand(path)[1])))
|
||
return routes, pads, vias, lines
|
||
|
||
|
||
def _dist_to(shape, x, y):
|
||
if shape[0] == 'seg':
|
||
return dp.seg_pt(shape[1], shape[2], shape[3], shape[4], x, y)
|
||
return math.hypot(shape[1] - x, shape[2] - y)
|
||
|
||
|
||
def push(routes):
|
||
u"""**只 create,不 delete**(除了本脚本上一次自己画的,按账本删)。"""
|
||
segs, vs = [], []
|
||
for key, path in routes.items():
|
||
s_, v_ = dp.expand(path)
|
||
for x0, y0, x1, y1, lay, w in s_:
|
||
segs.append([NET, lay, round(x0, 4), round(y0, 4), round(x1, 4), round(y1, 4), w])
|
||
for x, y in v_:
|
||
vs.append([NET, round(x, 4), round(y, 4)])
|
||
old = {'lines': [], 'vias': []}
|
||
if os.path.exists(LEDGER):
|
||
old = json.load(io.open(LEDGER, encoding='utf-8'))
|
||
js = OPEN + u"""
|
||
const MIL=%f, OLDL=%s, OLDV=%s, SEG=%s, VIA=%s, tol=0.02;
|
||
const hit=(a,b,c,d,p,q,r,s)=>
|
||
(Math.abs(a-p)<tol&&Math.abs(b-q)<tol&&Math.abs(c-r)<tol&&Math.abs(d-s)<tol)||
|
||
(Math.abs(a-r)<tol&&Math.abs(b-s)<tol&&Math.abs(c-p)<tol&&Math.abs(d-q)<tol);
|
||
let dl=0,dv=0;
|
||
for(const l of await eda.pcb_PrimitiveLine.getAll()){
|
||
const lay=l.getState_Layer();
|
||
const a=l.getState_StartX()/MIL,b=l.getState_StartY()/MIL;
|
||
const c=l.getState_EndX()/MIL,d=l.getState_EndY()/MIL;
|
||
if(OLDL.some(t=>lay===t[1]&&hit(a,b,c,d,t[2],t[3],t[4],t[5]))){
|
||
await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++;}
|
||
}
|
||
for(const v of await eda.pcb_PrimitiveVia.getAll()){
|
||
const x=v.getState_X()/MIL,y=v.getState_Y()/MIL;
|
||
if(OLDV.some(t=>Math.abs(x-t[1])<tol&&Math.abs(y-t[2])<tol)){
|
||
await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dv++;}
|
||
}
|
||
// ⚠ create() 的第一个参数就是**网络名**(和 pcb2_route_sig.JS_PUSH_LINES 一致)。
|
||
// 先 create 再 setState_Net 会被服务端拒掉:HTTP 500「对象参数不正确,无法应用到画布」。
|
||
let nl=0,nv=0;
|
||
for(const s of SEG){
|
||
await eda.pcb_PrimitiveLine.create(s[0], s[1], s[2]*MIL, s[3]*MIL, s[4]*MIL, s[5]*MIL, s[6]*MIL); nl++;
|
||
}
|
||
for(const v of VIA){
|
||
await eda.pcb_PrimitiveVia.create(v[0], v[1]*MIL, v[2]*MIL, %f*MIL, %f*MIL); nv++;
|
||
}
|
||
await eda.pcb_Document.save();
|
||
return {dl,dv,nl,nv};
|
||
""" % (MIL, json.dumps(old['lines']), json.dumps(old['vias']),
|
||
json.dumps(segs), json.dumps(vs), dp.VIA_H, dp.VIA_D)
|
||
r = call(js, u'下发')
|
||
print(u'下发:删自己上一版 %d 线 / %d 孔,新建 %d 线 / %d 孔'
|
||
% (r['dl'], r['dv'], r['nl'], r['nv']))
|
||
if r['nl'] != len(segs) or r['nv'] != len(vs):
|
||
sys.exit(u'建出来的数目对不上:线 %d/%d 孔 %d/%d'
|
||
% (r['nl'], len(segs), r['nv'], len(vs)))
|
||
json.dump({'lines': segs, 'vias': vs}, io.open(LEDGER, 'w', encoding='utf-8'))
|
||
print(u'账本已写 %s' % os.path.basename(LEDGER))
|
||
|
||
|
||
def main():
|
||
if '--clean' in sys.argv:
|
||
clean()
|
||
return
|
||
if '--refresh' in sys.argv:
|
||
dp.dump()
|
||
print(u'障碍表已刷新')
|
||
if '--push' not in sys.argv:
|
||
return
|
||
routes, pads, vias, lines = build_routes()
|
||
bad = dp.check(routes, pads, vias, lines)
|
||
if bad:
|
||
sys.exit(u'净距校核 %d 条,不下发' % len(bad))
|
||
print(u'净距校核通过')
|
||
if '--push' in sys.argv:
|
||
push(routes)
|
||
else:
|
||
print(u'加 --push 下发')
|
||
|
||
|
||
main()
|