R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-04 19:24:45 +08:00
共同撰写人 Claude Opus 5.5
父节点 cd01640c40
当前提交 cf7cc9cf90
共修改 759 个文件,包含 76962 行新增和 5299 行删除
+157
查看文件
@@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
u"""**在服务器上**扫「两只风扇座放哪里 + 这四个网能不能布通」—— 位置和可布性一起挑。
bash .claude/r2/remote_repair.sh --scan # 入口(本机)
DCDC_ROUTE_SERVER=1 pypy3 .claude/r2/fan_scan.py # 服务器上实际执行的
## 为什么非扫不可
J26 / J27 挪到顶面之后落在西北角,而那一带 **L1 整片是 `+12V` 大铜**
(`pcb_base.l1_12v_rects()` 主块 x 0.5~27.8 / y 0.5~97.5)⇒ 过孔上不去,
等于**单层(L4)布线**,而 A6 的 L4 挤满了风扇驱动的那一窝件。
于是「几何上放得下」不等于「线接得上」:第一版落位 (7.5, 81.1) / (11.5, 75.1)
几何全过,补线却有 4 条没布通。
⇒ 候选落位(`_base_fan_cands.json`,本机按几何 + 焊盘净距筛出来的)逐个真跑一遍补线,
按「未完成条数 → 总铜长」挑。单轮补线在 PyPy 上约 1.2 s,几十个候选一分钟就扫完。
输出 `_base_fan_scan.json`:每个候选的未完成数 / 总长,以及最优那个的完整段与孔。
"""
import copy
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_repair.sh --scan')
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_W=0.15`:见 repair_base.py —— 0402 两脚净距 0.30,默认宽度下 A* 找不到出口格点。
if os.environ.get('FAN_W'):
_w = float(os.environ['FAN_W'])
for _n in NETS:
RT.WIDTH[_n] = _w
_orig_rule_min = RT.rule_min
RT.rule_min = lambda net, auto: (_w if net.startswith('FAN_') else _orig_rule_min(net, auto))
print(u'线宽覆盖:FAN_* = %.2f mm' % _w)
LIMIT = int(os.environ.get('SCAN_LIMIT') or 0)
cp = json.load(io.open(os.path.join(HERE, '_base_fan_input.json'), encoding='utf-8'))
cands = json.load(io.open(os.path.join(HERE, '_base_fan_cands.json'), encoding='utf-8'))
if LIMIT:
cands = cands[:LIMIT]
base_pads = RB.load_pads()
nat = PL.native()
def moved_pads(place):
u"""把 J26 / J27 的焊盘搬到候选位置(只改这两件的脚,别的一只不动)。
⚠ 通孔盘在 `route_base.load_pads()` 里已经被拆成 L4 可布 + L1 障碍两份
(`TH_SUFFIX`),两份都要搬,否则 L1 那份还挡在老地方。
"""
out = []
for p in base_pads:
ref = p[0]
if ref not in ('J26', 'J27'):
out.append(p)
continue
cx, cy, rot = place[ref]
pn = str(p[1])[:-len(RB.TH_SUFFIX)] if str(p[1]).endswith(RB.TH_SUFFIX) else str(p[1])
src = [q for q in nat[ref] if str(q[0]) == pn]
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.TOP)
q = list(p)
q[2], q[3] = round(cx + dx, 4), round(cy + dy, 4)
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)}
BODY = (10.2, 5.0) # 2510-4A 本体(`place_base.BODY`)
def overlap(a, b, gap=0.6):
u"""⛔ 两只座的**本体**不许重叠(顶面同一面)。
2026-09-12 踩过:候选表只按「y 差 ≥ 5.6」过滤,J26 钉死在原位那一轮没带这条,
扫出来的「最优」是 J26=(13.5, 94.5) / J27=(12.0, 94.5) —— 两只座**摞在一起**,
布线代价当然最低。几何判据缺一条,搜索就会精准地钻这个空子。
"""
ax0, ax1 = a[0] - BODY[0] / 2 - gap / 2, a[0] + BODY[0] / 2 + gap / 2
ay0, ay1 = a[1] - BODY[1] / 2 - gap / 2, a[1] + BODY[1] / 2 + gap / 2
bx0, bx1 = b[0] - BODY[0] / 2 + gap / 2, b[0] + BODY[0] / 2 - gap / 2
by0, by1 = b[1] - BODY[1] / 2 + gap / 2, b[1] + BODY[1] / 2 - gap / 2
return not (ax1 <= bx0 or ax0 >= bx1 or ay1 <= by0 or ay0 >= by1)
rows = []
skipped = 0
t0 = time.time()
for k, (a, b) in enumerate(cands):
if overlap(a, b):
skipped += 1
continue
place = {'J26': (a[0], a[1], a[2]), 'J27': (b[0], b[1], b[2])}
try:
r = try_one(place)
except Exception as e: # noqa: BLE001
print(u' [%d] %s / %s 跑挂了: %s' % (k, a, b, e))
continue
rows.append({'J26': a, 'J27': b, 'open': len(r['logs']), 'split': len(r['unconnected']),
'len': r['len'], 'segs': r['segs'], 'vias': r['vias'],
'logs': r['logs'], 'unconnected': r['unconnected']})
if (k + 1) % 10 == 0:
best = min(rows, key=lambda q: (q['open'], q['split'], q['len']))
print(u' 扫了 %d/%d(%.0f s);目前最好:未完成 %d / 断 %d / %.1f mm J26=%s J27=%s'
% (k + 1, len(cands), time.time() - t0, best['open'], best['split'],
best['len'], best['J26'], best['J27']))
sys.stdout.flush()
rows.sort(key=lambda q: (q['open'], q['split'], q['len']))
print(u'\n扫完 %d 个候选,用时 %.0f s。前 8 名:' % (len(rows), time.time() - t0))
for q in rows[:8]:
print(u' 未完成 %d / 断 %d / %6.1f mm J26=%s J27=%s'
% (q['open'], q['split'], q['len'], q['J26'], q['J27']))
out = {'best': rows[0] if rows else None,
'top': [{k: v for k, v in q.items() if k not in ('segs', 'vias')} for q in rows[:30]],
'tried': len(rows)}
json.dump(out, io.open(os.path.join(HERE, '_base_fan_scan.json'), 'w', encoding='utf-8'),
ensure_ascii=False)
print(u'已写 _base_fan_scan.json(best 里带完整段 / 孔)')