初始导入:PhoneRail 五路手机直供电源板
原理图/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
这个提交包含在:
@@ -0,0 +1,255 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""补上 §25.8 遗留的**悬空 GND 焊盘** —— 自己找出来,自己打孔。
|
||||
|
||||
/c/Python314/python .claude/pcb2_route_dp.py --dump # 1. 先刷障碍表
|
||||
/c/Python314/python .claude/pcb2_gnd_patch.py # 2. 找悬空 + 选孔位 + 净距校核
|
||||
/c/Python314/python .claude/pcb2_gnd_patch.py --check # 只问「现在板上还有没有悬空的」
|
||||
/c/Python314/python .claude/pcb2_gnd_patch.py --push # 3. 下发(**只加不删**)
|
||||
/c/Python314/python .claude/pcb2_pour.py --push # 4. 必须重铺
|
||||
|
||||
## 「悬空」是自己算的,不是抄 DRC
|
||||
|
||||
把 GND 网络的**焊盘 / 过孔 / 导线**当图,两两相触连边(判据同 `pcb2_net_conn.py`),
|
||||
一个连通域里只要有**一个过孔或一个通孔焊盘**,它就落到 Inner1/Inner2 的地平面上 = 连通。
|
||||
**一个都没有的连通域就是悬空的** —— 那是纯顶层/底层的一小块孤铜。
|
||||
|
||||
⇒ 这个判据是幂等的:补完孔再跑一次,找到 0 个。
|
||||
|
||||
## 和 `pcb2_fanout.py` 的分工
|
||||
|
||||
`pcb_fanout` 是**批量**扇出(210 个孔),它对每个焊盘只试固定的几个方位,
|
||||
挤不下就跳过 —— §25.8 里剩下的这几个就是被它跳过的。
|
||||
这里换成和 `pcb2_vo_vias.py` 同一套搜索:8 个方向 × 距离 × 线宽阶梯,逐个试到过为止。
|
||||
|
||||
## 下发**不能**用 `pcb2_route_dp.push()`
|
||||
|
||||
那个函数会先把 net 上**所有**线和过孔删掉再重画 —— 对 GND 就是把 210 个扇出孔
|
||||
连同 BM24 的两条梳子全部抹掉。这里自己写一段只动**本脚本自己那几个对象**的 JS。
|
||||
|
||||
## ⚠️ 幂等靠 `_pcb2_gnd_patch.json` 这份「我上次画了啥」的清单
|
||||
|
||||
GND 上有两百多个别人的过孔,没法按网络整网重画,所以本脚本把自己下发的
|
||||
过孔/导线坐标记在 `_pcb2_gnd_patch.json` 里,下次 `--push` 先按坐标删掉它们再重画。
|
||||
|
||||
**不记账的代价实测过一次**:改完选点逻辑直接再 `--push`,板上就多出一对
|
||||
旧坐标的过孔(425 而不是 423),而且新画的短线被 EasyEDA 并进了旧的粗线里 ——
|
||||
DRC 照样报错,看上去像"改了没生效"。
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import pcb2_route_dp as R
|
||||
from pcb2_vo_vias import holes, local, place
|
||||
|
||||
NET = 'GND'
|
||||
STATE = os.path.join(HERE, '_pcb2_gnd_patch.json')
|
||||
W_STEPS = [0.40, 0.30, 0.25, 0.20] # GND 没有专门的线宽规则,扇出历来用 0.30
|
||||
EPS = 1e-6
|
||||
|
||||
|
||||
def clusters(pads, vias, lines):
|
||||
u"""GND 的连通域;返回**悬空的**那些(域里一个过孔/通孔焊盘都没有)。"""
|
||||
items = []
|
||||
for p in pads:
|
||||
if p['net'] == NET:
|
||||
items.append(('pad', set((1, 2)) if p['lay'] == 12 else set((p['lay'],)), p))
|
||||
for v in vias:
|
||||
if v['net'] == NET:
|
||||
items.append(('via', set((1, 2)), v))
|
||||
for l in lines:
|
||||
if l['net'] == NET:
|
||||
items.append(('line', set((l['lay'],)), l))
|
||||
n = len(items)
|
||||
par = list(range(n))
|
||||
|
||||
def find(i):
|
||||
while par[i] != i:
|
||||
par[i] = par[par[i]]
|
||||
i = par[i]
|
||||
return i
|
||||
|
||||
def touch(a, b):
|
||||
(ka, la, oa), (kb, lb, ob) = a, b
|
||||
if not (la & lb):
|
||||
return False
|
||||
if ka == 'line' and kb == 'line':
|
||||
return R.seg_seg((oa['x0'], oa['y0']), (oa['x1'], oa['y1']),
|
||||
(ob['x0'], ob['y0']), (ob['x1'], ob['y1'])) <= (oa['w'] + ob['w']) / 2 + EPS
|
||||
if ka == 'line' and kb == 'pad':
|
||||
return R.seg_rect((oa['x0'], oa['y0']), (oa['x1'], oa['y1']),
|
||||
ob['x'], ob['y'], ob['hw'], ob['hh']) <= oa['w'] / 2 + EPS
|
||||
if ka == 'line' and kb == 'via':
|
||||
return R.seg_pt(oa['x0'], oa['y0'], oa['x1'], oa['y1'],
|
||||
ob['x'], ob['y']) <= oa['w'] / 2 + ob['r'] + EPS
|
||||
if ka == 'via' and kb == 'via':
|
||||
return math.hypot(oa['x'] - ob['x'], oa['y'] - ob['y']) <= oa['r'] + ob['r'] + EPS
|
||||
if ka == 'via' and kb == 'pad':
|
||||
return R.seg_rect((oa['x'], oa['y']), (oa['x'], oa['y']),
|
||||
ob['x'], ob['y'], ob['hw'], ob['hh']) <= oa['r'] + EPS
|
||||
if ka == 'pad' and kb == 'pad':
|
||||
return False
|
||||
return touch(b, a)
|
||||
|
||||
for i in range(n):
|
||||
for j in range(i + 1, n):
|
||||
if touch(items[i], items[j]):
|
||||
a, b = find(i), find(j)
|
||||
if a != b:
|
||||
par[a] = b
|
||||
comp = {}
|
||||
for i in range(n):
|
||||
comp.setdefault(find(i), []).append(i)
|
||||
out = []
|
||||
for _k, idx in comp.items():
|
||||
if any(items[i][0] == 'via' or (items[i][0] == 'pad' and items[i][2]['lay'] == 12)
|
||||
for i in idx):
|
||||
continue
|
||||
out.append([items[i] for i in idx])
|
||||
return out
|
||||
|
||||
|
||||
JS_ADD = u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r=>setTimeout(r,1800));
|
||||
const SEGS = %s, VIAS = %s, MIL = %f, OLD = %s;
|
||||
// 先按坐标删掉**上一次本脚本画的**那几个(清单在 _pcb2_gnd_patch.json)
|
||||
const hit = (x, y) => OLD.some(t => Math.hypot(x/MIL - t[0], y/MIL - t[1]) < 0.02);
|
||||
let dv = 0, dl = 0;
|
||||
for (const v of await eda.pcb_PrimitiveVia.getAll())
|
||||
if (v.getState_Net() === 'GND' && hit(v.getState_X(), v.getState_Y())) {
|
||||
await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dv++;
|
||||
}
|
||||
for (const l of await eda.pcb_PrimitiveLine.getAll())
|
||||
if (l.getState_Net() === 'GND'
|
||||
&& (hit(l.getState_StartX(), l.getState_StartY()) || hit(l.getState_EndX(), l.getState_EndY()))) {
|
||||
await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++;
|
||||
}
|
||||
let nl = 0, nv = 0;
|
||||
for (const [net, lay, x0, y0, x1, y1, w] of SEGS) {
|
||||
await eda.pcb_PrimitiveLine.create(net, lay, x0*MIL, y0*MIL, x1*MIL, y1*MIL, w*MIL); nl++;
|
||||
}
|
||||
for (const [net, x, y] of VIAS) {
|
||||
await eda.pcb_PrimitiveVia.create(net, x*MIL, y*MIL, %f*MIL, %f*MIL); nv++;
|
||||
}
|
||||
await eda.pcb_Document.save();
|
||||
await new Promise(r=>setTimeout(r,1500));
|
||||
return {lines: nl, vias: nv, delVia: dv, delLine: dl,
|
||||
totVia: (await eda.pcb_PrimitiveVia.getAll()).length};
|
||||
"""
|
||||
|
||||
|
||||
def load_state():
|
||||
u"""上一轮本脚本下发的坐标(过孔中心 + 每段线的两端)。"""
|
||||
try:
|
||||
return json.load(open(STATE, encoding='utf-8'))
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def drop_mine(pads, vias, lines):
|
||||
u"""算之前先把**自己上一轮的产物**从障碍表里摘掉 —— 否则悬空焊盘已经不悬空了,
|
||||
脚本会以为没活干(而实际上那几个对象马上就要被删掉重画)。"""
|
||||
old = load_state()
|
||||
|
||||
def mine(x, y):
|
||||
return any(math.hypot(x - a, y - b) < 0.02 for a, b in old)
|
||||
vias = [v for v in vias if not mine(v['x'], v['y'])]
|
||||
lines = [l for l in lines
|
||||
if not (mine(l['x0'], l['y0']) or mine(l['x1'], l['y1'])) or l['net'] != NET]
|
||||
return pads, vias, lines
|
||||
|
||||
|
||||
def push_add(routes):
|
||||
u"""**只 create、不 delete** —— GND 上有 210 个扇出孔和 BM24 的梳子,一删就完。"""
|
||||
segs, vias = [], []
|
||||
for key, path in routes.items():
|
||||
net = R.netof(key)
|
||||
s, v = R.expand(path)
|
||||
for x0, y0, x1, y1, lay, w in s:
|
||||
segs.append([net, lay, x0, y0, x1, y1, w])
|
||||
for x, y in v:
|
||||
vias.append([net, x, y])
|
||||
old = load_state()
|
||||
r = R.run(JS_ADD % (R.PCB, json.dumps(segs), json.dumps(vias), R.MIL,
|
||||
json.dumps(old), R.VIA_H, R.VIA_D), timeout=600)
|
||||
if not r.get('success'):
|
||||
sys.exit(u'下发失败: ' + json.dumps(r, ensure_ascii=False)[:500])
|
||||
d = r['result']
|
||||
pts = [[v[1], v[2]] for v in vias]
|
||||
pts += [[s_[2], s_[3]] for s_ in segs] + [[s_[4], s_[5]] for s_ in segs]
|
||||
json.dump(pts, open(STATE, 'w', encoding='utf-8'))
|
||||
print(u'下发完成:先删掉自己上一轮的 %d 孔 / %d 线,再画 %d 线 / %d 孔,板上过孔共 %d 个'
|
||||
% (d['delVia'], d['delLine'], d['lines'], d['vias'], d['totVia']))
|
||||
|
||||
|
||||
def main():
|
||||
if '--check' in sys.argv:
|
||||
# ⚠️ 不摘自己上一轮的产物 —— 这一支回答的是「板子现在还有没有悬空 GND」,
|
||||
# 默认那一支回答的是「要画成什么样」(所以必须先摘掉自己画的再算)。
|
||||
left = clusters(*R.geom())
|
||||
print(u'板上还悬空的 GND 连通域 %d 个' % len(left))
|
||||
for cl in left:
|
||||
print(u' %s' % ' + '.join('%s.%s' % (o['ref'], o['num'])
|
||||
for k, _l, o in cl if k == 'pad'))
|
||||
sys.exit(1 if left else 0)
|
||||
pads, vias, lines = drop_mine(*R.geom())
|
||||
floats = clusters(pads, vias, lines)
|
||||
print(u'悬空的 GND 连通域 %d 个:' % len(floats))
|
||||
if not floats:
|
||||
print(u' 没有 —— 不用补。')
|
||||
return
|
||||
|
||||
hl = holes(pads)
|
||||
routes, mine, mvias, fail = {}, [], [], 0
|
||||
for k, cl in enumerate(floats):
|
||||
cands = [o for kind, _l, o in cl if kind == 'pad']
|
||||
desc = ' + '.join('%s.%s' % (o['ref'], o['num']) for o in cands)
|
||||
best = None
|
||||
for pad in cands: # 域里哪个焊盘都行,取第一个打得下孔的
|
||||
lp, lv, ll, lh = local(pads, vias, lines, pad['x'], pad['y'], hl)
|
||||
lay = R.L_TOP if pad['lay'] == 1 else R.L_BOT
|
||||
got = place(pad, lay, W_STEPS, lp, lv, ll, lh, mine, mvias)
|
||||
if got:
|
||||
best = (pad, lay) + got
|
||||
break
|
||||
if not best:
|
||||
print(u' [!!] %-28s 找不到能落孔的位置' % desc)
|
||||
fail += 1
|
||||
continue
|
||||
pad, lay, w, segs, (vx, vy) = best
|
||||
other = R.L_BOT if lay == R.L_TOP else R.L_TOP
|
||||
pts = [(segs[0][1], segs[0][2], lay, w)] + [(x1, y1, lay, w) for _l, _a, _b, x1, y1, _w in segs]
|
||||
routes[u'%s#patch%d' % (NET, k)] = pts + [(vx, vy, other, w), (vx, vy, other, w)]
|
||||
for l_, x0, y0, x1, y1, w_ in segs:
|
||||
mine.append((NET, l_, x0, y0, x1, y1, w_))
|
||||
mvias.append((NET, vx, vy))
|
||||
print(u' %-28s 从 %s.%s 出,过孔 (%8.3f,%8.3f) %d 段 线宽 %.2f'
|
||||
% (desc, pad['ref'], pad['num'], vx, vy, len(segs), w))
|
||||
|
||||
segs = sum(len(R.expand(p)[0]) for p in routes.values())
|
||||
nv = sum(len(R.expand(p)[1]) for p in routes.values())
|
||||
print(u'')
|
||||
print(u'%d/%d 个域搞定:%d 段导线 / %d 个过孔' % (len(routes), len(floats), segs, nv))
|
||||
if nv != len(routes):
|
||||
sys.exit(u'!! 过孔数和域数对不上 —— expand() 又把末端过孔吞了')
|
||||
n_sharp = len(R.sharp_corners(routes))
|
||||
print(u'直角拐点 %d(拐弯只许 45°,应当是 0)' % n_sharp)
|
||||
print(u'')
|
||||
print(u'净距校核(一般 %.2f mm):' % R.CLR)
|
||||
bad = R.check(routes, pads, vias, lines)
|
||||
if not bad:
|
||||
print(u' 全部通过 ✔')
|
||||
|
||||
if '--push' in sys.argv:
|
||||
if bad or fail or n_sharp:
|
||||
sys.exit(u'\n!! 有未解决的问题,不下发。')
|
||||
push_add(routes)
|
||||
print(u'下一步必须重铺:/c/Python314/python .claude/pcb2_pour.py --push')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户