文件

274 行
12 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""载板 PCB2 事故恢复:把电气数据从本地快照重放回文档。
/c/Python314/python .claude/pcb2_restore.py --survey # 只读:清点板上还剩什么
/c/Python314/python .claude/pcb2_restore.py --clean-orphans # 删掉已删元件留下的孤儿图形
/c/Python314/python .claude/pcb2_restore.py --copper # 干跑:从快照算出要建多少铜
/c/Python314/python .claude/pcb2_restore.py --copper --push # 真的建(分批 + 回查)
## 背景
2026-09-07:`pcb2_reload.py` 重载之后桥接与文档失同步(skill §9ae),铜层读回全 0,
而当时判断「重载成功」只看 `windowId` 变没变、**没有数图元**;紧接着 `pcb2_pour.py --push`
在那个状态下调了一次 `save()`,把「空」写回了文档。
结果:**元件 / 焊盘 / 导线 / 过孔 / 铺铜全没了,板框 / 挖槽 / 丝印等非铜层图形全在。**
(防呆已加进 `pcb2_reload.py:assert_alive()`,判据改成「图元数不为零」。)
## 为什么能重建
事故前一刻正好跑过 `pcb2_cfgsel.py --refresh`,它调 `pcb2_route_dp.dump()`
把整板几何写进了 `_pcb2_route_geom.json`:
| 内容 | 数量 |
|---|---:|
| 铜层导线(层 1/2,带网名和线宽)| **2033** |
| 过孔(带网名和孔径)| **894** |
| 焊盘(216 个元件 + 38 个自由焊盘)| 1000 |
⚠️ 这份快照是在 `--clean`(删 12 段异网旧铜)**之后**、`--push`(建 6 线 2 孔)**之前**抓的,
所以重放它 **再加上** `_pcb2_cfgsel.json` 账本里那 6 线 2 孔,正好等于事故前的最终状态。
元件本身不从快照恢复 —— 走原本的流程(从原理图导入变更 → `pcb2_place.py --push`),
那才是摆位的唯一事实来源;快照里的焊盘只用来**对账**(重放完回读焊盘坐标,和快照逐个比)。
## 顺序(每一步都回查,不许盲走)
1. --survey 清点,确认确实只剩非铜层图形
2. --clean-orphans 删孤儿丝印/锡膏/装配(**不碰板框 11 / 挖槽 12**)
3. pcb1_import.py --board2 --probe / --apply 216 件全部「增加元件」
4. pcb2_place.py --push 摆位
5. pcb2_place.py --mech / --outline 38 个自由焊盘 + 手机位外框丝印
6. pcb2_restore.py --copper --push 2033 线 + 894 孔 + 账本的 6 线 2 孔
7. pcb2_pour.py --push 7 块铺铜(**跑之前先确认第 6 步回查过**)
8. inner1_verify.py / drc_read.py --pcb2 对 DRC 115 条基线
"""
import io
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = '942ed1560d81c1e7'
MIL = 39.3700787401575
GEOM = os.path.join(HERE, '_pcb2_route_geom.json')
LEDGER = os.path.join(HERE, '_pcb2_cfgsel.json')
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,2500));" % PCB)
# 孤儿图形所在的层:元件封装自带的丝印 / 阻焊 / 锡膏 / 装配 / 引脚层。
# ⚠️ **绝不能碰**:11 板框、12 挖槽(跑道形穿线槽)、13 文档层。
# skill §9n 那次「删掉 layer 11 之外的全部导线」就是在载板上差点酿祸的写法。
ORPHAN_LAYERS = [3, 4, 5, 6, 7, 8, 9, 10, 48, 49, 50, 51]
KEEP_LAYERS = [11, 12, 13]
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']
SURVEY_JS = OPEN + u"""
const out = {doc: (await eda.dmt_Pcb.getCurrentPcbInfo() || {}).name};
const cnt = async (nm, get, layerOf) => {
try {
const by = {}; let n = 0;
for (const o of await get()) { n++; if (layerOf) { const L = layerOf(o); by[L] = (by[L]||0)+1; } }
out[nm] = n; if (layerOf) out[nm + '_byLayer'] = by;
} catch (e) { out[nm] = 'ERR ' + String(e).slice(0, 60); }
};
await cnt('component', () => eda.pcb_PrimitiveComponent.getAll());
await cnt('pad', () => eda.pcb_PrimitivePad.getAll());
await cnt('line', () => eda.pcb_PrimitiveLine.getAll(), o => o.getState_Layer());
await cnt('via', () => eda.pcb_PrimitiveVia.getAll());
await cnt('pour', () => eda.pcb_PrimitivePour.getAll());
await cnt('poured', () => eda.pcb_PrimitivePoured.getAll());
await cnt('polyline', () => eda.pcb_PrimitivePolyline.getAll(), o => o.getState_Layer());
await cnt('fill', () => eda.pcb_PrimitiveFill.getAll(), o => o.getState_Layer());
await cnt('arc', () => eda.pcb_PrimitiveArc.getAll(), o => o.getState_Layer());
await cnt('string', () => eda.pcb_PrimitiveString.getAll(), o => o.getState_Layer());
return out;
"""
def survey():
d = call(SURVEY_JS, u'清点')
print(u'文档: %s' % d.get('doc'))
for k in ('component', 'pad', 'line', 'via', 'pour', 'poured',
'polyline', 'fill', 'arc', 'string'):
v = d.get(k)
by = d.get(k + '_byLayer')
print(u' %-10s %-8s %s' % (k, v, (u'按层 ' + json.dumps(by)) if by else u''))
return d
# ⚠️ 一次请求 30 s 上限(skill §9c),2700 多个删除必须分批 —— 一批 200,从 Python 侧循环。
CLEAN_BATCH_JS = OPEN + u"""
const KILL = %s, KEEP = %s, LIMIT = %d, WHICH = '%s';
const api = (WHICH === 'polyline') ? eda.pcb_PrimitivePolyline : eda.pcb_PrimitiveFill;
const doomed = [];
for (const o of await api.getAll()) {
const L = o.getState_Layer();
if (KEEP.includes(L)) continue; // 板框 11 / 挖槽 12 / 文档 13,绝不碰
if (KILL.includes(L)) { doomed.push(o.getState_PrimitiveId()); if (doomed.length >= LIMIT) break; }
}
let n = 0;
for (const id of doomed) { await api.delete(id); n++; }
return {n, more: doomed.length >= LIMIT};
"""
LEFT_JS = OPEN + u"""
const left = {};
for (const o of await eda.pcb_PrimitivePolyline.getAll()) {
const L = o.getState_Layer(); left['pl' + L] = (left['pl' + L] || 0) + 1;
}
for (const o of await eda.pcb_PrimitiveFill.getAll()) {
const L = o.getState_Layer(); left['fi' + L] = (left['fi' + L] || 0) + 1;
}
return left;
"""
def clean_orphans():
before = survey()
if before.get('component') or before.get('pad'):
sys.exit(u'⛔ 板上还有 %s 个元件 / %s 个焊盘 —— 这一步只在「元件已经全没了」时才该跑。'
% (before.get('component'), before.get('pad')))
total = {}
for which in ('polyline', 'fill'):
n = 0
for _round in range(40): # skill §9x:删除写成循环,收尾断言剩 0
r = call(CLEAN_BATCH_JS % (json.dumps(ORPHAN_LAYERS), json.dumps(KEEP_LAYERS),
200, which), u'清 %s' % which)
n += r['n']
print(u' %s 已删 %d' % (which, n))
if not r['more']:
break
total[which] = n
call(OPEN + u'await eda.pcb_Document.save(); return 1;', u'存盘')
left = call(LEFT_JS, u'回查') # skill §9ad:save() 的返回值不算数
print(u'\n删除:多段线 %d / 填充 %d' % (total['polyline'], total['fill']))
print(u'落盘后剩余:%s' % json.dumps(left))
for k, v in left.items():
L = int(k[2:])
if L in ORPHAN_LAYERS:
sys.exit(u'⛔ 层 %d 还剩 %d 个没删掉' % (L, v))
if not left.get('pl11'):
sys.exit(u'⛔ 板框(层 11 多段线)不见了 —— 出大问题,立刻停手不要 save')
print(u'✅ 板框还在(层 11 多段线 %d 条),挖槽层 12 填充 %s 条'
% (left['pl11'], left.get('fi12')))
def copper_plan():
u"""从快照 + 账本算出要建的铜。返回 (segs, vias)。"""
g = json.load(io.open(GEOM, encoding='utf-8'))
segs = []
for _id, net, lay, x0, y0, x1, y1, w in g['lines']:
segs.append([net or '', lay, x0 / MIL, y0 / MIL, x1 / MIL, y1 / MIL, w / MIL])
vias = [[net or '', x / MIL, y / MIL, dm / MIL] for net, x, y, dm in g['vias']]
n_snap_l, n_snap_v = len(segs), len(vias)
# 快照是 `pcb2_cfgsel.py --push` **之前**抓的,所以要补上账本里那一批
add_l = add_v = 0
if os.path.exists(LEDGER):
led = json.load(io.open(LEDGER, encoding='utf-8'))
for net, lay, x0, y0, x1, y1, w in led['lines']:
segs.append([net, lay, x0, y0, x1, y1, w]); add_l += 1
for net, x, y in led['vias']:
vias.append([net, x, y, None]); add_v += 1 # 孔径用默认
print(u'快照 %d 线 / %d 孔 + cfgsel 账本 %d 线 / %d 孔 = %d 线 / %d 孔'
% (n_snap_l, n_snap_v, add_l, add_v, len(segs), len(vias)))
return segs, vias
PUSH_LINES_JS = OPEN + u"""
const SEGS = %s, MIL = %f; let n = 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); n++;
}
return n;
"""
PUSH_VIAS_JS = OPEN + u"""
const VIAS = %s, MIL = %f, DEF_H = %f, DEF_D = %f; let n = 0;
// 板上 894 个过孔外径**全部一样**(0.6096 mm = 24 mil,就是 VIA_D 的名义值),
// 所以统一用脚本默认的 VIA_H / VIA_D 建,和当初 push_sig 创建时逐位一致;
// 不去按快照里那个带 mil↔mm 舍入尾巴的读数反推孔径。
for (const [net, x, y, d] of VIAS) {
await eda.pcb_PrimitiveVia.create(net, x*MIL, y*MIL, DEF_H*MIL, DEF_D*MIL); n++;
}
return n;
"""
# ⚠️ 只数**铜层**(1 顶层 / 2 底层)。第一版数了全部 `pcb_PrimitiveLine`,
# 于是元件封装带进来的 605 条丝印/装配/文档层线把「板上必须没有铜」的守卫顶掉了。
COUNT_JS = OPEN + u"""
let nl = 0, nv = 0;
const by = {}, other = {};
for (const l of await eda.pcb_PrimitiveLine.getAll()) {
const L = l.getState_Layer();
if (L === 1 || L === 2) { nl++; by[L] = (by[L]||0)+1; }
else other[L] = (other[L]||0)+1;
}
for (const v of await eda.pcb_PrimitiveVia.getAll()) nv++;
return {nl, nv, by, other};
"""
def copper(push=False):
import pcb2_route_dp as dp
segs, vias = copper_plan()
by = {}
for s in segs:
by[s[1]] = by.get(s[1], 0) + 1
print(u'按层:%s' % json.dumps(by))
nets = sorted(set(s[0] for s in segs))
print(u'网络 %d 个;线宽 %.3f~%.3f mm'
% (len(nets), min(s[6] for s in segs), max(s[6] for s in segs)))
if not push:
print(u'\n加 --push 下发')
return
st0 = call(COUNT_JS, u'下发前清点')
print(u'下发前:铜层导线 %d / 过孔 %d(非铜层 %s,不算数)'
% (st0['nl'], st0['nv'], json.dumps(st0['other'])))
if st0['nl'] or st0['nv']:
sys.exit(u'⛔ 板上已经有铜了(%d 线 / %d 孔)—— 本脚本只在空板上重放,'
u'否则会画出两套。先确认状态。' % (st0['nl'], st0['nv']))
nl = 0
for i in range(0, len(segs), 250):
nl += call(PUSH_LINES_JS % (json.dumps(segs[i:i + 250]), MIL), u'建线 %d~' % i)
print(u' 线 %d / %d' % (nl, len(segs)))
nv = 0
for i in range(0, len(vias), 250):
nv += call(PUSH_VIAS_JS % (json.dumps(vias[i:i + 250]), MIL, dp.VIA_H, dp.VIA_D),
u'建孔 %d~' % i)
print(u' 孔 %d / %d' % (nv, len(vias)))
call(OPEN + u'await eda.pcb_Document.save(); return 1;', u'存盘')
st1 = call(COUNT_JS, u'下发后回查')
print(u'\n落盘后回查:导线 %d(按层 %s)/ 过孔 %d'
% (st1['nl'], json.dumps(st1['by']), st1['nv']))
if st1['nl'] != len(segs) or st1['nv'] != len(vias):
sys.exit(u'⛔ 回查对不上:期望 %d 线 / %d 孔,实得 %d / %d'
% (len(segs), len(vias), st1['nl'], st1['nv']))
print(u'✅ 铜层重放完成,数目逐项对上')
def main():
if '--survey' in sys.argv:
survey()
elif '--clean-orphans' in sys.argv:
clean_orphans()
elif '--copper' in sys.argv:
copper('--push' in sys.argv)
else:
print(__doc__)
main()