R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,425 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""混合布线(2026-09-11):脚本预布关键铜并锁定 → EasyEDA 自带自动布线只补其余(只开 L1/L4)。
|
||||
|
||||
/c/Python314/python .claude/r2/ar_hybrid.py --clear # 删四层全部导线、全部过孔、全部铜面(铜面由 pour_base --push 按计划重建)
|
||||
/c/Python314/python .claude/r2/ar_hybrid.py --push # 下发 _base_preroute_0911.json,线 / 孔全部锁定
|
||||
/c/Python314/python .claude/r2/ar_hybrid.py --verify # 读回:预布的每段 / 每孔都在、都锁着
|
||||
/c/Python314/python .claude/r2/ar_hybrid.py --route # EasyEDA autoRouting:只开 L1/L4、保留已有、忽略平面网 / 入口链
|
||||
|
||||
⚠ `--route` 在客户端 3.2.149 上用不了:`eda.pcb_Document.autoRouting is not a function`(文档里标 beta,PCB 端没开放;
|
||||
枚举全部 `eda.*` 只有 `sch_Document.autoRouting`,PCB 端只有 import/getAutoRouteJsonFile / clearRouting)⇒ 自动布线让用户在客户端手动点,
|
||||
设置与 IGNORE 相同:布线层只勾顶层 / 底层、保留已有导线和过孔、忽略 GND / +12V / VIN_RAW / VIN_FUSED。
|
||||
|
||||
预布子集由 r34-v0 挑出:GND / +12V / 入口链 / Kelvin / 晶振 RBIAS VDD18(PLL) / 两颗 Buck 反馈 / +3V3_HUB / USB1~6 整网,
|
||||
UP 两端到第一个孔(中段交给自动布线)。为什么这么分,见 PROGRESS「EasyEDA 自动布线评审」。
|
||||
⚠ `--clear` 前先备份板上现状(`_base_board_autoroute_0911.json` 就是 09-11 那版自动布线)。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
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)
|
||||
from eda import run # noqa: E402
|
||||
import pcb_base as B # noqa: E402
|
||||
|
||||
MIL = B.MIL
|
||||
# 基准铜(strip / verify 以它为准):0911 = r34 挑出的预布;0911b = 再去掉 +3V3_HUB 远程线、加 C4.2 改接和服务器中心线版 UP
|
||||
SUBSET = os.path.join(HERE, '_base_preroute_0911b.json')
|
||||
VIA_D, VIA_HOLE = 0.6, 0.3 # 与 router / 缓存一致
|
||||
IGNORE = ['GND', '+12V', 'VIN_RAW', 'VIN_FUSED'] # 平面网 + 入口链(靠铜面)
|
||||
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
|
||||
"await new Promise(r=>setTimeout(r,2500));" % B.PCB)
|
||||
|
||||
|
||||
def call(js, timeout=600):
|
||||
r = run(OPEN + js, timeout=timeout)
|
||||
if not r.get('success'):
|
||||
sys.exit(u'!! ' + json.dumps(r, ensure_ascii=False)[:600])
|
||||
return r['result']
|
||||
|
||||
|
||||
def clear():
|
||||
r = call(u"""
|
||||
const out = {lines: 0, vias: 0, pours: 0, rounds: 0};
|
||||
for (let k = 0; k < 10; k++) {
|
||||
const ls = (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer)).map(l=>l.primitiveId);
|
||||
const vs = (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>v.primitiveId);
|
||||
const ps = (await eda.pcb_PrimitivePour.getAll()||[]).map(p=>p.primitiveId);
|
||||
if (!ls.length && !vs.length && !ps.length) break;
|
||||
if (ls.length) await eda.pcb_PrimitiveLine.delete(ls);
|
||||
if (vs.length) await eda.pcb_PrimitiveVia.delete(vs);
|
||||
for (const p of ps) await eda.pcb_PrimitivePour.delete(p);
|
||||
out.lines += ls.length; out.vias += vs.length; out.pours += ps.length; out.rounds++;
|
||||
}
|
||||
await eda.pcb_Document.save();
|
||||
out.left = {lines: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer)).length,
|
||||
vias: (await eda.pcb_PrimitiveVia.getAll()||[]).length,
|
||||
pours: (await eda.pcb_PrimitivePour.getAll()||[]).length,
|
||||
poured: (await eda.pcb_PrimitivePoured.getAll()||[]).length};
|
||||
return out;""", timeout=900)
|
||||
print(u'清板:删导线 %d / 过孔 %d / 铜面 %d(%d 轮);剩 %s' % (r['lines'], r['vias'], r['pours'], r['rounds'], r['left']))
|
||||
if any(r['left'].values()):
|
||||
sys.exit(u'!! 没清干净')
|
||||
|
||||
|
||||
PUSH_JS = u"""
|
||||
const MIL = %s, S = %s, V = %s, H = %s, D = %s;
|
||||
let ls = 0, vs = 0, bad = [];
|
||||
for (const s of S) {
|
||||
try { const o = await eda.pcb_PrimitiveLine.create(s[6], s[0], s[1]*MIL, s[2]*MIL, s[3]*MIL, s[4]*MIL, s[5]*MIL, true);
|
||||
if (o) ls++; else bad.push(s[6]); } catch (e) { bad.push(s[6] + ' ' + e); }
|
||||
}
|
||||
for (const v of V) {
|
||||
try { const o = await eda.pcb_PrimitiveVia.create(v[2], v[0]*MIL, v[1]*MIL, H*MIL, D*MIL, undefined, null, null, true);
|
||||
if (o) vs++; else bad.push('via ' + v[2]); } catch (e) { bad.push('via ' + v[2] + ' ' + e); }
|
||||
}
|
||||
return {ls, vs, bad: bad.slice(0, 10), nbad: bad.length};
|
||||
"""
|
||||
|
||||
|
||||
def push():
|
||||
c = json.load(io.open(SUBSET, encoding='utf-8'))
|
||||
segs, vias = c['segs'], c['vias']
|
||||
nl = nv = nb = 0
|
||||
for k in range(0, max(len(segs), 1), 120):
|
||||
r = call(PUSH_JS % (MIL, json.dumps(segs[k:k + 120]), json.dumps(vias if k == 0 else []), VIA_HOLE, VIA_D),
|
||||
timeout=900)
|
||||
nl, nv, nb = nl + r['ls'], nv + r['vs'], nb + r['nbad']
|
||||
if r['bad']:
|
||||
print(u' ⚠ 失败样例: %s' % r['bad'])
|
||||
call(u'await eda.pcb_Document.save(); return 1;')
|
||||
print(u'下发(锁定):导线 %d / %d,过孔 %d / %d,失败 %d' % (nl, len(segs), nv, len(vias), nb))
|
||||
if nb or nl != len(segs) or nv != len(vias):
|
||||
sys.exit(u'!! 下发不全')
|
||||
|
||||
|
||||
def readback():
|
||||
r = call(u"""
|
||||
return {L: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer))
|
||||
.map(l=>[l.layer,l.startX,l.startY,l.endX,l.endY,l.lineWidth,l.net||'',!!l.primitiveLock]),
|
||||
V: (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.x,v.y,v.net||'',v.diameter,v.holeDiameter,!!v.primitiveLock])};""")
|
||||
L = [[a[0], a[1] / MIL, a[2] / MIL, a[3] / MIL, a[4] / MIL, a[5] / MIL, a[6], a[7]] for a in r['L']]
|
||||
V = [[a[0] / MIL, a[1] / MIL, a[2], a[3] / MIL, a[4] / MIL, a[5]] for a in r['V']]
|
||||
return L, V
|
||||
|
||||
|
||||
def verify():
|
||||
c = json.load(io.open(SUBSET, encoding='utf-8'))
|
||||
L, V = readback()
|
||||
|
||||
# ⚠ EasyEDA 会把首尾相接的共线段合并成一段(下发 599 读回 572),端点精确匹配对不上 ⇒
|
||||
# 判据改成「预布段的两端 + 中点都落在板上同层 / 同网 / 同宽、且锁定的某段线上」
|
||||
def pd(px, py, s):
|
||||
x1, y1, x2, y2 = s[1], s[2], s[3], s[4]
|
||||
dx, dy = x2 - x1, y2 - y1
|
||||
L2 = dx * dx + dy * dy
|
||||
t = 0.0 if L2 < 1e-12 else max(0.0, min(1.0, ((px - x1) * dx + (py - y1) * dy) / L2))
|
||||
return math.hypot(px - (x1 + t * dx), py - (y1 + t * dy))
|
||||
|
||||
def covered(s, locked_only):
|
||||
pts = ((s[1], s[2]), (s[3], s[4]), ((s[1] + s[3]) / 2, (s[2] + s[4]) / 2))
|
||||
cand = [b for b in L if b[0] == s[0] and b[6] == s[6] and abs(b[5] - s[5]) < 0.01 and (b[7] or not locked_only)]
|
||||
return all(any(pd(x, y, b) < 0.01 for b in cand) for x, y in pts)
|
||||
miss = [s for s in c['segs'] if not covered(s, False)]
|
||||
unl = [s for s in c['segs'] if s not in miss and not covered(s, True)]
|
||||
vm = [v for v in c['vias'] if not any(w[2] == v[2] and math.hypot(w[0] - v[0], w[1] - v[1]) < 0.01 and w[5] for w in V)]
|
||||
print(u'读回:导线 %d(锁定 %d)/ 过孔 %d(锁定 %d)' % (len(L), sum(1 for s in L if s[7]), len(V), sum(1 for v in V if v[5])))
|
||||
print(u'预布 %d 段:没被盖住 %d,盖住但没锁 %d;预布 %d 孔:缺或没锁 %d' % (len(c['segs']), len(miss), len(unl), len(c['vias']), len(vm)))
|
||||
for s in miss[:8]:
|
||||
print(u' 缺段 %s' % s)
|
||||
for v in vm[:8]:
|
||||
print(u' 缺孔 %s' % v)
|
||||
return not miss and not vm
|
||||
|
||||
|
||||
def route():
|
||||
u"""桥接服务端每个请求硬上限 30 s(bridge-server.mjs REQUEST_TIMEOUT_MS),自动布线远超这个数 ⇒
|
||||
发起后立刻返回,结果挂在 globalThis.__AR 上,再每 15 s 轮询一次(每次轮询都是短请求)。"""
|
||||
import time
|
||||
r = call(u"""
|
||||
const T = (typeof EPCB_LayerId !== 'undefined') ? EPCB_LayerId.TOP : 1;
|
||||
const Bt = (typeof EPCB_LayerId !== 'undefined') ? EPCB_LayerId.BOTTOM : 2;
|
||||
const KEEP = (typeof EPCB_AutoRoutingExistingPrimitiveMode !== 'undefined') ? EPCB_AutoRoutingExistingPrimitiveMode.KEEP : 'keep';
|
||||
const t0 = Date.now();
|
||||
globalThis.__AR = {state: 'running', t0};
|
||||
eda.pcb_Document.autoRouting({layers: [T, Bt], ignoreNets: %s, existingPrimitiveMode: KEEP})
|
||||
.then(async res => { globalThis.__AR = {state: 'done', res, ms: Date.now() - t0};
|
||||
try { await eda.pcb_Document.save(); globalThis.__AR.saved = true; } catch (e) { globalThis.__AR.saveErr = String(e); } })
|
||||
.catch(e => { globalThis.__AR = {state: 'error', err: String(e && (e.stack || e.message) || e), ms: Date.now() - t0}; });
|
||||
return {started: true, layers: [T, Bt], keep: KEEP};""" % json.dumps(IGNORE), timeout=60)
|
||||
print(u'发起:%s' % json.dumps(r, ensure_ascii=False))
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < 3600:
|
||||
time.sleep(15)
|
||||
try:
|
||||
s = run(u"const a = globalThis.__AR || null;"
|
||||
u"const n = (await eda.pcb_PrimitiveLine.getAll()||[]).length;"
|
||||
u"return {a: a && a.state === 'running' ? {state: 'running', sec: (Date.now() - a.t0) / 1000} : a, lines: n};",
|
||||
timeout=60)
|
||||
except Exception as e: # 客户端忙着布线时请求可能超时,下一轮再问
|
||||
print(u' 轮询异常:%s' % e)
|
||||
continue
|
||||
res = s.get('result') or {}
|
||||
a = res.get('a') or {}
|
||||
print(u' %4.0f s:%s,板上导线 %s' % (time.time() - t0, a.get('state'), res.get('lines')))
|
||||
sys.stdout.flush()
|
||||
if a.get('state') in ('done', 'error'):
|
||||
print(json.dumps(a, ensure_ascii=False)[:3000])
|
||||
return
|
||||
print(u'!! 60 分钟还没结束')
|
||||
|
||||
|
||||
def set_lock(flag):
|
||||
u"""全部导线(四层)+ 过孔改锁定状态;分块改(桥接每个请求 30 s 上限)。
|
||||
2026-09-11:锁着 + 「保留已有」时用户点自动布线毫无反应 ⇒ 解锁做对照。"""
|
||||
ids = call(u"return {L: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer)).map(l=>l.primitiveId),"
|
||||
u" V: (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>v.primitiveId)};")
|
||||
done = 0
|
||||
for kind, api in (('L', 'pcb_PrimitiveLine'), ('V', 'pcb_PrimitiveVia')):
|
||||
arr = ids[kind]
|
||||
for k in range(0, len(arr), 150):
|
||||
if kind == 'V' and not flag:
|
||||
# ⚠ 过孔的 modify(id, {primitiveLock:false}) 返回成功但**不生效**(3.2.149 实测,读回仍锁),
|
||||
# toAsync().setState_PrimitiveLock() 连新建的普通孔也报「对象参数不正确」⇒ 锁着的孔删掉按 5 参数原位重建(不锁)
|
||||
r = call(u"let n = 0; for (const id of %s) { const v = await eda.pcb_PrimitiveVia.get(id);"
|
||||
u" if (!v || !v.primitiveLock) continue;"
|
||||
u" const a = [v.net, v.x, v.y, v.holeDiameter, v.diameter];"
|
||||
u" if (await eda.pcb_PrimitiveVia.delete(id) && await eda.pcb_PrimitiveVia.create(...a)) n++; }"
|
||||
u" return n;" % json.dumps(arr[k:k + 100]), timeout=120)
|
||||
if len(arr) > k + 100:
|
||||
r += call(u"let n = 0; for (const id of %s) { const v = await eda.pcb_PrimitiveVia.get(id);"
|
||||
u" if (!v || !v.primitiveLock) continue;"
|
||||
u" const a = [v.net, v.x, v.y, v.holeDiameter, v.diameter];"
|
||||
u" if (await eda.pcb_PrimitiveVia.delete(id) && await eda.pcb_PrimitiveVia.create(...a)) n++; }"
|
||||
u" return n;" % json.dumps(arr[k + 100:k + 150]), timeout=120)
|
||||
else:
|
||||
r = call(u"let n = 0; for (const id of %s) { try { if (await eda.%s.modify(id, {primitiveLock: %s})) n++; } catch (e) {} }"
|
||||
u" return n;" % (json.dumps(arr[k:k + 150]), api, 'true' if flag else 'false'), timeout=120)
|
||||
done += r
|
||||
call(u'await eda.pcb_Document.save(); return 1;')
|
||||
L, V = readback()
|
||||
print(u'%s:改了 %d / %d;读回 导线 %d(锁定 %d)/ 过孔 %d(锁定 %d)'
|
||||
% (u'锁定' if flag else u'解锁', done, len(ids['L']) + len(ids['V']),
|
||||
len(L), sum(1 for s in L if s[7]), len(V), sum(1 for v in V if v[5])))
|
||||
|
||||
|
||||
KO_NAME = 'AR-NOWIRE-%s'
|
||||
|
||||
|
||||
def keepout(add):
|
||||
u"""L2 / L3 整板「禁止布线」区域(EPCB_PrimitiveRegionRuleType.NO_WIRES = 5;API 没有「禁止过孔」类型)。
|
||||
2026-09-11 用户实测:EasyEDA 自动布线**不勾内层就不跑**(进度一闪、零产出)⇒ 对话框四层都勾,用这两块区域把导线挡在内层外。
|
||||
`add=False` 只删这两块。⚠ create 传的 regionName 读回是空(3.2.149 实测)⇒ 按「层 15/16 + ruleType 恰为 [5]」认。"""
|
||||
src = B.polygon_src(B.outline_polygon())
|
||||
r = call(u"""
|
||||
const mine = g => [15, 16].includes(g.layer) && JSON.stringify(g.ruleType) === '[5]';
|
||||
const old = (await eda.pcb_PrimitiveRegion.getAll()||[]).filter(mine);
|
||||
for (const g of old) await eda.pcb_PrimitiveRegion.delete(g.primitiveId);
|
||||
const made = [];
|
||||
if (%s) for (const [lay, tag] of [[15, 'L2'], [16, 'L3']]) {
|
||||
const poly = await eda.pcb_MathPolygon.createPolygon(%s);
|
||||
const g = await eda.pcb_PrimitiveRegion.create(lay, poly, [5], '%s'.replace('%%s', tag));
|
||||
made.push(g ? [g.layer, g.regionName, g.ruleType] : null);
|
||||
}
|
||||
await eda.pcb_Document.save();
|
||||
const now = (await eda.pcb_PrimitiveRegion.getAll()||[]).filter(mine).map(g => [g.layer, g.ruleType, g.primitiveId]);
|
||||
return {deleted: old.length, made, now};""" % ('true' if add else 'false', json.dumps(src), KO_NAME))
|
||||
print(u'内层禁布区:删旧 %d,新建 %s;板上现有 %s' % (r['deleted'], r['made'], r['now']))
|
||||
|
||||
|
||||
ZONES = os.path.join(HERE, '_base_inner_keepout_0911.json')
|
||||
|
||||
|
||||
def keepout_zones(add):
|
||||
u"""L2 + L3 **关键区**「禁止布线」区域(`_base_inner_keepout_0911.json`:USB1~6 在 L1/L4 上的 D± 段各外扩 0.9 +
|
||||
晶振 / 两颗 Buck / 入口功率链矩形,shapely 合并)。2026-09-11 用户选「关键区禁布 + 四层全勾」:
|
||||
布线器只有四层全勾才跑、且把禁布区当连孔也禁 ⇒ 只在这些区域里不许它走内层、不许它打孔,其余随它。
|
||||
认法与 `keepout()` 相同:层 15/16 + ruleType 恰为 [5](regionName 存不住)。"""
|
||||
zs = json.load(io.open(ZONES, encoding='utf-8')) if add else []
|
||||
|
||||
def src(pts):
|
||||
a = [round(pts[0][0] * MIL, 4), round(pts[0][1] * MIL, 4)]
|
||||
for x, y in pts[1:]:
|
||||
a += ['L', round(x * MIL, 4), round(y * MIL, 4)]
|
||||
return a
|
||||
rings = [[src(z['pts'])] + [src(h) for h in z['holes']] for z in zs]
|
||||
r = call(u"""
|
||||
const mine = g => [15, 16].includes(g.layer) && JSON.stringify(g.ruleType) === '[5]';
|
||||
const old = (await eda.pcb_PrimitiveRegion.getAll()||[]).filter(mine);
|
||||
for (const g of old) await eda.pcb_PrimitiveRegion.delete(g.primitiveId);
|
||||
const R = %s, made = [], bad = [];
|
||||
for (const lay of [15, 16]) for (let i = 0; i < R.length; i++) {
|
||||
const poly = R[i].length > 1 ? eda.pcb_MathPolygon.createComplexPolygon(R[i]) : eda.pcb_MathPolygon.createPolygon(R[i][0]);
|
||||
const p = (poly && typeof poly.then === 'function') ? await poly : poly;
|
||||
if (!p) { bad.push([lay, i, 'poly']); continue; }
|
||||
try { const g = await eda.pcb_PrimitiveRegion.create(lay, p, [5]); if (g) made.push([lay, i]); else bad.push([lay, i, 'create']); }
|
||||
catch (e) { bad.push([lay, i, String(e).slice(0, 80)]); }
|
||||
}
|
||||
await eda.pcb_Document.save();
|
||||
const now = (await eda.pcb_PrimitiveRegion.getAll()||[]).filter(mine).map(g => g.layer);
|
||||
return {deleted: old.length, made: made.length, bad, now: now.length, byLayer: {L2: now.filter(l => l === 15).length, L3: now.filter(l => l === 16).length}};"""
|
||||
% json.dumps(rings), timeout=120)
|
||||
print(u'内层关键区禁布:删旧 %d,新建 %d(失败 %s);板上现有 %d %s'
|
||||
% (r['deleted'], r['made'], r['bad'], r['now'], r['byLayer']))
|
||||
|
||||
|
||||
def _pd(px, py, s):
|
||||
x1, y1, x2, y2 = s[1], s[2], s[3], s[4]
|
||||
dx, dy = x2 - x1, y2 - y1
|
||||
L2 = dx * dx + dy * dy
|
||||
t = 0.0 if L2 < 1e-12 else max(0.0, min(1.0, ((px - x1) * dx + (py - y1) * dy) / L2))
|
||||
return math.hypot(px - (x1 + t * dx), py - (y1 + t * dy))
|
||||
|
||||
|
||||
def _on(seg, pool):
|
||||
u"""seg 的两端 + 中点都落在 pool 里同层 / 同网 / 同宽的某段上(EasyEDA 会合并共线段,所以按「被盖住」判)。"""
|
||||
cand = [b for b in pool if b[0] == seg[0] and b[6] == seg[6] and abs(b[5] - seg[5]) < 0.01]
|
||||
pts = ((seg[1], seg[2]), (seg[3], seg[4]), ((seg[1] + seg[3]) / 2, (seg[2] + seg[4]) / 2))
|
||||
return all(any(_pd(x, y, b) < 0.01 for b in cand) for x, y in pts)
|
||||
|
||||
|
||||
def strip(nets=None):
|
||||
u"""删掉**不属于预布子集**的导线 / 过孔(= 自动布线加的),再把被连带删掉的预布段补回来。
|
||||
`nets` 给了就只动这些网(2026-09-11:自动布线在已完整的 USB3 / USB6 上又并了一条,只删它们的新增部分)。"""
|
||||
c = json.load(io.open(SUBSET, encoding='utf-8'))
|
||||
r = call(u"return {L: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer))"
|
||||
u".map(l=>[l.primitiveId,l.layer,l.startX,l.startY,l.endX,l.endY,l.lineWidth,l.net||'']),"
|
||||
u" V: (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.primitiveId,v.x,v.y,v.net||''])};")
|
||||
L = [(a[0], [a[1], a[2] / MIL, a[3] / MIL, a[4] / MIL, a[5] / MIL, a[6] / MIL, a[7]]) for a in r['L']]
|
||||
V = [(a[0], a[1] / MIL, a[2] / MIL, a[3]) for a in r['V']]
|
||||
if nets:
|
||||
L = [(i, s) for i, s in L if s[6] in nets]
|
||||
V = [v for v in V if v[3] in nets]
|
||||
kill_l = [i for i, s in L if not _on(s, c['segs'])]
|
||||
kill_v = [i for i, x, y, n in V if not any(v[2] == n and math.hypot(v[0] - x, v[1] - y) < 0.01 for v in c['vias'])]
|
||||
for k in range(0, len(kill_l), 300):
|
||||
call(u"await eda.pcb_PrimitiveLine.delete(%s); return 1;" % json.dumps(kill_l[k:k + 300]), timeout=120)
|
||||
if kill_v:
|
||||
call(u"await eda.pcb_PrimitiveVia.delete(%s); return 1;" % json.dumps(kill_v), timeout=120)
|
||||
call(u'await eda.pcb_Document.save(); return 1;')
|
||||
print(u'清掉非预布:导线 %d / %d,过孔 %d / %d' % (len(kill_l), len(L), len(kill_v), len(V)))
|
||||
Lb, _Vb = readback()
|
||||
# ⚠ 指定了 nets 就只补这些网的预布段(2026-09-11 踩过:apply_up 删了 +3V3_HUB 远程线,随后 strip(UP) 把它们全补了回来,压在新 UP 上)
|
||||
lost = [s for s in c['segs'] if not _on(s, Lb) and (not nets or s[6] in nets)]
|
||||
if lost:
|
||||
r = call(PUSH_JS % (MIL, json.dumps(lost), json.dumps([]), VIA_HOLE, VIA_D), timeout=300)
|
||||
call(u'await eda.pcb_Document.save(); return 1;')
|
||||
print(u'补回被合并连带删掉的预布段:%d / %d(失败 %d)' % (r['ls'], len(lost), r['nbad']))
|
||||
Lb, Vb = readback()
|
||||
lost = [s for s in c['segs'] if not _on(s, Lb)]
|
||||
vl = [v for v in c['vias'] if not any(w[2] == v[2] and math.hypot(w[0] - v[0], w[1] - v[1]) < 0.01 for w in Vb)]
|
||||
print(u'现在:导线 %d / 过孔 %d;预布没盖住 %d 段、缺孔 %d' % (len(Lb), len(Vb), len(lost), len(vl)))
|
||||
for s in lost[:6]:
|
||||
print(u' 没盖住 %s' % s)
|
||||
|
||||
|
||||
# C4.2(VIN_SW 去耦)被 U13.10 → Rs6.2 的 +12V Kelvin 线挡在 VIN_SW 铜北边,L4 上接不过去;
|
||||
# 自动布线绕 L1 18 mm 穿 12V 大铜(L1 12V 主块最窄 13.20 → 8.95)。2026-09-11 服务器扫描(c4_scan,孔不进焊盘、只接宽功率铜 / Rs6.1)
|
||||
# 用户选「接功率铜」:孔 A 在 C4.2 西南外侧,L1 4.5 mm 到孔 B,再 L4 短接 Rs6.1 旁 2.0 宽的 VIN_SW 铜。
|
||||
C4_FIX_SEGS = [[2, 16.5, 46.0, 15.5, 45.125, 0.3, 'VIN_SW'],
|
||||
[1, 15.5, 45.125, 13.688, 41.0, 0.3, 'VIN_SW'],
|
||||
[2, 13.688, 41.0, 13.625, 40.875, 0.3, 'VIN_SW']]
|
||||
C4_FIX_VIAS = [[15.5, 45.125, 'VIN_SW'], [13.688, 41.0, 'VIN_SW']]
|
||||
|
||||
|
||||
def fix_c4():
|
||||
u"""删自动布线在 VIN_SW 上加的铜(= 去 C4.2 那条 L1 绕线),下发 C4_FIX_*(不锁),读回核对。"""
|
||||
strip({'VIN_SW'})
|
||||
r = call(PUSH_JS.replace(', true);', ');').replace('null, null, true)', 'null, null, false)')
|
||||
% (MIL, json.dumps(C4_FIX_SEGS), json.dumps(C4_FIX_VIAS), VIA_HOLE, VIA_D), timeout=120)
|
||||
call(u'await eda.pcb_Document.save(); return 1;')
|
||||
L, V = readback()
|
||||
miss = [s for s in C4_FIX_SEGS if not _on(s, L)]
|
||||
vm = [v for v in C4_FIX_VIAS if not any(w[2] == v[2] and math.hypot(w[0] - v[0], w[1] - v[1]) < 0.01 for w in V)]
|
||||
print(u'C4.2 改接:下发线 %d / 孔 %d(失败 %d);读回缺线 %d、缺孔 %d;VIN_SW 现在 %d 段 / %d 孔'
|
||||
% (r['ls'], r['vs'], r['nbad'], len(miss), len(vm), sum(1 for s in L if s[6] == 'VIN_SW'),
|
||||
sum(1 for v in V if v[2] == 'VIN_SW')))
|
||||
|
||||
|
||||
UP_ROUTE = os.path.join(HERE, '_up_route.cl_b52.json')
|
||||
UP_RIP = ('+5V', 'EFUSE_EN4', 'HUB_RST') # 挡 UP 中心线的自动布线网:整网删掉,之后「所选网络」重布
|
||||
U32_C = (51.5, 38.6)
|
||||
|
||||
|
||||
def apply_up(with_ret=True):
|
||||
u"""下发服务器中心线版 UP(up_route.py --centerline --pre-only --free-3v3hub --pitch 0.52):
|
||||
① 删 UP_RIP 三个网的全部铜 + +3V3_HUB 离 U32 > 8 mm 的远程线 / 孔(它们最后「所选网络」重布);
|
||||
② UP 恢复成只剩预布两端段(strip);③ 下发 UP 新线 + GND 回流孔(不锁);④ 读回核对。"""
|
||||
u = json.load(io.open(UP_ROUTE, encoding='utf-8'))
|
||||
far = lambda x, y: math.hypot(x - U32_C[0], y - U32_C[1]) > 8.0 # noqa: E731
|
||||
r = call(u"return {L: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer))"
|
||||
u".map(l=>[l.primitiveId,l.startX,l.startY,l.endX,l.endY,l.net||'']),"
|
||||
u" V: (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.primitiveId,v.x,v.y,v.net||''])};")
|
||||
kl = [a[0] for a in r['L'] if a[5] in UP_RIP or
|
||||
(a[5] == '+3V3_HUB' and far(a[1] / MIL, a[2] / MIL) and far(a[3] / MIL, a[4] / MIL))]
|
||||
kv = [a[0] for a in r['V'] if a[3] in UP_RIP or (a[3] == '+3V3_HUB' and far(a[1] / MIL, a[2] / MIL))]
|
||||
for k in range(0, len(kl), 300):
|
||||
call(u"await eda.pcb_PrimitiveLine.delete(%s); return 1;" % json.dumps(kl[k:k + 300]), timeout=120)
|
||||
if kv:
|
||||
call(u"await eda.pcb_PrimitiveVia.delete(%s); return 1;" % json.dumps(kv), timeout=120)
|
||||
call(u'await eda.pcb_Document.save(); return 1;')
|
||||
print(u'腾位置:删线 %d / 孔 %d(%s + +3V3_HUB 远程线)' % (len(kl), len(kv), ' '.join(UP_RIP)))
|
||||
strip({'UP_DP', 'UP_DM'})
|
||||
vias = [list(v) for v in u['ret']] if with_ret else []
|
||||
js = PUSH_JS.replace(', true);', ');').replace('null, null, true)', 'null, null, false)')
|
||||
n = 0
|
||||
for k in range(0, len(u['segs']), 120):
|
||||
res = call(js % (MIL, json.dumps(u['segs'][k:k + 120]), json.dumps(vias if k == 0 else []), VIA_HOLE, VIA_D),
|
||||
timeout=300)
|
||||
n += res['ls']
|
||||
if res['nbad']:
|
||||
print(u' ⚠ 失败 %d:%s' % (res['nbad'], res['bad']))
|
||||
call(u'await eda.pcb_Document.save(); return 1;')
|
||||
Lb, Vb = readback()
|
||||
miss = [s for s in u['segs'] if not _on(list(s), Lb)]
|
||||
vm = [v for v in vias if not any(w[2] == v[2] and math.hypot(w[0] - v[0], w[1] - v[1]) < 0.01 for w in Vb)]
|
||||
print(u'UP 下发:线 %d / %d,回流孔 %d;读回缺线 %d、缺孔 %d;板上 UP_DP %d 段 / UP_DM %d 段'
|
||||
% (n, len(u['segs']), len(vias), len(miss), len(vm),
|
||||
sum(1 for s in Lb if s[6] == 'UP_DP'), sum(1 for s in Lb if s[6] == 'UP_DM')))
|
||||
|
||||
|
||||
def inner_type(t):
|
||||
u"""L2 / L3 层类型:'PLANE'(内电层,负片)或 'SIGNAL'。
|
||||
2026-09-11:EasyEDA 自动布线不勾内层不跑;勾内层 + 整板禁布区又不敢打孔 ⇒ 试把内层改内电层。"""
|
||||
r = call(u"""
|
||||
const T = (typeof EPCB_LayerType !== 'undefined') ? (%s ? EPCB_LayerType.INTERNAL_ELECTRICAL : EPCB_LayerType.SIGNAL) : '%s';
|
||||
const a = await eda.pcb_Layer.modifyLayer(15, {type: T});
|
||||
const b = await eda.pcb_Layer.modifyLayer(16, {type: T});
|
||||
await eda.pcb_Document.save();
|
||||
return {T, a, b, now: (await eda.pcb_Layer.getAllLayers()||[]).filter(l => [1,2,15,16].includes(l.id)).map(l => [l.id, l.name, l.type])};"""
|
||||
% ('true' if t == 'PLANE' else 'false', t))
|
||||
print(u'内层类型 → %s:modify %s / %s;读回 %s' % (r['T'], r['a'], r['b'], r['now']))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if '--strip' in sys.argv:
|
||||
strip()
|
||||
if '--keepout' in sys.argv:
|
||||
keepout(True)
|
||||
if '--unkeepout' in sys.argv:
|
||||
keepout(False)
|
||||
if '--zones' in sys.argv:
|
||||
keepout_zones(True)
|
||||
if '--unzones' in sys.argv:
|
||||
keepout_zones(False)
|
||||
if '--plane' in sys.argv:
|
||||
inner_type('PLANE')
|
||||
if '--signal' in sys.argv:
|
||||
inner_type('SIGNAL')
|
||||
if '--unlock' in sys.argv:
|
||||
set_lock(False)
|
||||
if '--lock' in sys.argv:
|
||||
set_lock(True)
|
||||
if '--clear' in sys.argv:
|
||||
clear()
|
||||
if '--push' in sys.argv:
|
||||
push()
|
||||
if '--verify' in sys.argv:
|
||||
sys.exit(0 if verify() else 1)
|
||||
if '--route' in sys.argv:
|
||||
route()
|
||||
在新工单中引用
屏蔽一个用户