R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""**在服务器上**比较几套风扇区布局的可布性:每套布局真跑一遍 14 个网的补线,按未完成条数挑。
|
||||
|
||||
bash .claude/r2/remote_fan_layout.sh # 入口(本机)
|
||||
DCDC_ROUTE_SERVER=1 pypy3 .claude/r2/fan_layout_scan.py
|
||||
|
||||
## 为什么要扫布局而不是只扫落位
|
||||
|
||||
`fan_scan.py` 扫的是**两只座**放哪;这一份扫的是**外围 22 件**怎么排。
|
||||
2026-09-12 用户把两只座挪成 rot90 竖放之后,外围按 `SAT_GAP=1.5` 重排,
|
||||
几何全过(真实焊盘三向 0 冲突),14 个网却有 10~11 条布不通 —— 而失败的脚
|
||||
周围往往只有一两个障碍、出口格点也合法 ⇒ 不是「出不来」,是那一带 L4 真没路了
|
||||
(L1 整片是 12V 大铜,上不去,等于单层)。
|
||||
|
||||
摊开排(加大 `SAT_GAP`)未必更好:链会被拉长(实测 `$6N64` 从 25.2 → 23.8、
|
||||
`$6N74` 13.5 → 21.6,有增有减)。**光看几何分不出优劣,只能让布线器实测。**
|
||||
|
||||
输入 `_base_fan_layouts.json`:`{布局名: {位号: [x, y, rot]}}`(本机 `place_base` 按不同
|
||||
`--fan-gap` 算好的几套)。输出 `_base_fan_layout_scan.json`:每套的未完成 / 断 / 总长,
|
||||
以及最优那套的完整段与孔。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.dirname(HERE))
|
||||
sys.path.insert(0, HERE)
|
||||
|
||||
if not os.environ.get('DCDC_ROUTE_SERVER'):
|
||||
sys.exit(u'⛔ 布线搜索只在 10.5.5.13 上跑(CLAUDE.md):bash .claude/r2/remote_fan_layout.sh')
|
||||
|
||||
import router as RT # noqa: E402
|
||||
import route_base as RB # noqa: E402
|
||||
import place_base as PL # noqa: E402
|
||||
import pcb_base as B # noqa: E402
|
||||
|
||||
NETS = ['FAN_12V_SW', 'FAN_IN_TACH_RAW', 'FAN_OUT_TACH_RAW', 'FAN_PWM_L',
|
||||
'FAN_PWM', 'FAN_EN', 'FAN_TACH_IN', 'FAN_TACH_OUT',
|
||||
'$6N45', '$6N48', '$6N49', '$6N55', '$6N64', '$6N74']
|
||||
cp = json.load(io.open(os.path.join(HERE, '_base_fan_input.json'), encoding='utf-8'))
|
||||
layouts = json.load(io.open(os.path.join(HERE, '_base_fan_layouts.json'), encoding='utf-8'))
|
||||
base_pads = RB.load_pads()
|
||||
nat = PL.native()
|
||||
FAN = PL.FAN_REFS
|
||||
|
||||
|
||||
def moved_pads(place):
|
||||
u"""把这 22 件的焊盘搬到该布局的位置(都是贴片件,没有通孔副本要跟着搬)。"""
|
||||
out = []
|
||||
for p in base_pads:
|
||||
ref = p[0]
|
||||
if ref not in FAN or ref not in place:
|
||||
out.append(p)
|
||||
continue
|
||||
cx, cy, rot = place[ref]
|
||||
src = [q for q in nat.get(ref, []) if str(q[0]) == str(p[1])]
|
||||
if not src:
|
||||
out.append(p)
|
||||
continue
|
||||
_pn, nx, ny, w, h, _net, _sh = src[0]
|
||||
dx, dy = PL.pad_off(nx, ny, rot, B.BOTTOM)
|
||||
q = list(p)
|
||||
q[2], q[3] = round(cx + dx, 4), round(cy + dy, 4)
|
||||
if int(rot) % 180: # 转 90/270:焊盘的宽高也要换过来
|
||||
q[4], q[5] = h, w
|
||||
else:
|
||||
q[4], q[5] = w, h
|
||||
out.append(q)
|
||||
return out
|
||||
|
||||
|
||||
def try_one(place):
|
||||
pads = moved_pads(place)
|
||||
segs = [tuple(s) for s in cp['segs']]
|
||||
vias = [tuple(v) for v in cp['vias']]
|
||||
board_vias = [tuple(v) for v in cp['board_vias']]
|
||||
bypad = {}
|
||||
for p in pads:
|
||||
if not p[7] or p[7] not in NETS or str(p[1]).endswith(RB.TH_SUFFIX):
|
||||
continue
|
||||
bypad.setdefault(p[7], []).append(p)
|
||||
n0, v0 = len(segs), len(vias)
|
||||
RT.AUTO_CACHE.clear()
|
||||
logs = RT.route_groups(bypad, pads, segs, vias, board_vias, {},
|
||||
priority=tuple(NETS), only=set(NETS))
|
||||
chk = [q for q in RB.check_pads(pads) if q[7] in set(NETS)]
|
||||
left = RT.check_connected(chk, segs, vias)
|
||||
new_s = [list(s) for s in segs[n0:]]
|
||||
new_v = [list(v) for v in vias[v0:]]
|
||||
length = sum(((s[3] - s[1]) ** 2 + (s[4] - s[2]) ** 2) ** 0.5 for s in new_s)
|
||||
return {'logs': list(logs), 'unconnected': list(left), 'segs': new_s, 'vias': new_v,
|
||||
'len': round(length, 2)}
|
||||
|
||||
|
||||
rows = []
|
||||
t0 = time.time()
|
||||
for name in sorted(layouts):
|
||||
place = {k: tuple(v) for k, v in layouts[name].items()}
|
||||
try:
|
||||
r = try_one(place)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(u' %s 跑挂了: %s' % (name, e))
|
||||
continue
|
||||
rows.append({'name': name, 'open': len(r['logs']), 'split': len(r['unconnected']),
|
||||
'len': r['len'], 'segs': r['segs'], 'vias': r['vias'],
|
||||
'logs': r['logs'], 'unconnected': r['unconnected'], 'place': layouts[name]})
|
||||
print(u' %-8s 未完成 %2d / 断 %2d / %6.1f mm / %d 段 %d 孔(%.0f s)'
|
||||
% (name, rows[-1]['open'], rows[-1]['split'], rows[-1]['len'],
|
||||
len(r['segs']), len(r['vias']), time.time() - t0))
|
||||
for m in r['logs'][:6]:
|
||||
print(u' [!] ' + m)
|
||||
sys.stdout.flush()
|
||||
|
||||
rows.sort(key=lambda q: (q['open'], q['split'], q['len']))
|
||||
print(u'\n排名:')
|
||||
for q in rows:
|
||||
print(u' %-8s 未完成 %2d / 断 %2d / %6.1f mm' % (q['name'], q['open'], q['split'], q['len']))
|
||||
out = {'best': rows[0] if rows else None,
|
||||
'all': [{k: v for k, v in q.items() if k not in ('segs', 'vias', 'place')} for q in rows]}
|
||||
json.dump(out, io.open(os.path.join(HERE, '_base_fan_layout_scan.json'), 'w', encoding='utf-8'),
|
||||
ensure_ascii=False)
|
||||
print(u'已写 _base_fan_layout_scan.json(best 里带布局 + 完整段 / 孔)')
|
||||
在新工单中引用
屏蔽一个用户