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 行删除
+170
查看文件
@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
u"""R2 底板:把 PCB 元件的 uniqueId 按位号对齐到原理图(skill §9o-2,照搬 `.claude/pcb1_uid_sync.py`)。
/c/Python314/python .claude/r2/uid_sync_base.py # 只对账,不动板
/c/Python314/python .claude/r2/uid_sync_base.py --push # 两阶段写回 + 存盘 + 回查
为什么要有:A4-HUB 页整页重画(`a4_hub.py --push`)后原理图元件 uuid / uniqueId 全新,
「从原理图导入变更」按 uniqueId 配对 ⇒ 那一页 31 件全变成「移除 + 增加」(2026-09-10 实测),
应用下去整窝 Hub 会被删掉重建、摆位归零。对齐之后对话框只剩真实差异(新增件 + 改网络)。
写之前把 PCB 侧旧 uniqueId 存到 `_base_uid_pre_sync.json`(只改这一个字段,这份就是快照)。
⚠ DRC / 几何 / 连通性都看不出这一步错没错,唯一验收是重开导入对话框看剩几条。
"""
import io
import json
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))
from eda import run # noqa: E402
U = json.load(io.open(os.path.join(HERE, 'r2-uuids.json'), encoding='utf-8'))
# `--blade` 对刀片做同一件事(2026-09-11:B4-EDGE / B3B-USB 整页重画后,导入对话框同样错开一格)
BOARD = 'BLADE' if '--blade' in sys.argv else 'BASE'
PCB = U['boards'][BOARD]['pcb']['uuid']
PAGES = U['boards'][BOARD]['pages']
SNAP = os.path.join(HERE, '_%s_uid_pre_sync.json' % BOARD.lower())
# 导入对话框若还开着(`pcb_import.py --probe` 不关它),先点「取消」再动板。
CLOSE_DLG = u"""
for (const b of document.querySelectorAll('button'))
if (b.offsetParent !== null && (b.innerText||'').trim() === '取消') {
const o={bubbles:true,cancelable:true,view:window,button:0};
for (const t of ['pointerdown','mousedown','pointerup','mouseup','click'])
b.dispatchEvent(new (t.startsWith('pointer')?PointerEvent:MouseEvent)(t,o));
await new Promise(r => setTimeout(r, 800));
}
"""
JS_PCB = CLOSE_DLG + u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r => setTimeout(r, 2000));
const o = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll())
o[c.getState_Designator()] = c.getState_UniqueId() || '';
return o;
""" % PCB
JS_SCH = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r => setTimeout(r, 1500));
const cur = await eda.dmt_Schematic.getCurrentSchematicPageInfo();
const o = {};
for (const c of await eda.sch_PrimitiveComponent.getAll()) {
if (c.getState_ComponentType() !== 'part') continue;
if (c.getState_AddIntoPcb && c.getState_AddIntoPcb() === false) continue;
o[c.getState_Designator()] = c.getState_UniqueId() || '';
}
return {page: cur.name, uids: o};
"""
def call(js, timeout=300):
r = run(js, timeout=timeout)
if not r.get('success'):
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:1200])
return r['result']
def collect():
pcb = call(JS_PCB)
sch = {}
for name, uuid in sorted(PAGES.items()):
r = call(JS_SCH % uuid)
if str(r['page']).upper() != name.upper():
raise RuntimeError(u'openDocument 打到了 %s(要 %s)' % (r['page'], name))
for d, u in r['uids'].items():
if d in sch:
print(u' ⚠️ 位号 %s 在多页出现(%s)' % (d, r['page']))
sch[d] = u
return pcb, sch
def main():
push = '--push' in sys.argv
pcb, sch = collect()
print(u'PCB %d 件 / 原理图 %d 件' % (len(pcb), len(sch)))
only_pcb = sorted(set(pcb) - set(sch))
only_sch = sorted(set(sch) - set(pcb))
if only_pcb:
print(u'⚠️ PCB 有原理图没有 %d: %s' % (len(only_pcb), only_pcb))
if only_sch:
print(u' 原理图有 PCB 没有 %d(等导入新增): %s' % (len(only_sch), ' '.join(only_sch)))
diff = {d: (pcb[d], sch[d]) for d in sorted(set(pcb) & set(sch)) if pcb[d] != sch[d]}
print(u'\n通道ID 对不上的:%d / %d' % (len(diff), len(set(pcb) & set(sch))))
for d, (a, b) in diff.items():
print(u' %-6s PCB=%-10s 原理图=%-10s' % (d, a or '(空)', b or '(空)'))
if not diff:
print(u'\n✅ 全部一致,导入对话框应当只报真实差异。')
return
tgt = {}
for d, (_a, b) in diff.items():
tgt.setdefault(b, []).append(d)
dup = {k: v for k, v in tgt.items() if len(v) > 1 or not k}
if dup:
sys.exit(u'⛔ 目标 uniqueId 撞车 / 为空,不能自动修:%s' % dup)
# 目标值不能和板上**不改的**件撞号。
# ⚠ 「PCB 有、原理图没有」的件(导入时要删的,如 2026-09-11 刀片的 J2)不算:它们占着的号
# 正是错位那一串要用的(J2 = gge74 = J4 的目标)。同一次写入里把它们挪到 `zzdel_<位号>`,
# 原理图里不可能有这个号,导入对话框照样把它们报成「移除元件」。
keep = {pcb[d]: d for d in pcb if d not in diff and d not in only_pcb and pcb[d]}
clash = {b: (d, keep[b]) for d, (_a, b) in diff.items() if b in keep}
if clash:
sys.exit(u'⛔ 目标 uniqueId 与板上不改的件撞号:%s' % clash)
if not push:
print(u'\n加 --push 写回(两阶段:先临时 ID 再目标 ID,避免中途重号)')
return
io.open(SNAP, 'w', encoding='utf-8').write(json.dumps(pcb, ensure_ascii=False, indent=0))
print(u'已存旧 uniqueId 快照 %s' % os.path.basename(SNAP))
plan = [[d, b] for d, (_a, b) in sorted(diff.items())]
plan += [[d, 'zzdel_' + d] for d in only_pcb]
r = call(CLOSE_DLG + u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r => setTimeout(r, 2000));
const PLAN = %s;
const by = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll()) by[c.getState_Designator()] = c;
let n1 = 0, n2 = 0;
for (let i = 0; i < PLAN.length; i++) {
const c = by[PLAN[i][0]];
if (!c) continue;
c.toAsync().setState_UniqueId('zzsync' + i).done();
n1++;
}
for (const [des, uid] of PLAN) {
const c = by[des];
if (!c) continue;
c.toAsync().setState_UniqueId(uid).done();
n2++;
}
await eda.pcb_Document.save();
return {n1, n2};
""" % (PCB, json.dumps(plan)))
print(u'\n两阶段写入:临时 %d / 目标 %d,已存盘' % (r['n1'], r['n2']))
pcb2, sch2 = collect()
left = {d: (pcb2[d], sch2[d]) for d in sorted(set(pcb2) & set(sch2)) if pcb2[d] != sch2[d]}
if left:
print(u'⛔ 回查还有 %d 个对不上:' % len(left))
for d, (a, b) in left.items():
print(u' %-6s PCB=%-10s 原理图=%s' % (d, a or '(空)', b or '(空)'))
sys.exit(1)
seen = {}
for d, u in pcb2.items():
if u:
seen.setdefault(u, []).append(d)
dup2 = {k: v for k, v in seen.items() if len(v) > 1}
if dup2:
sys.exit(u'⛔ PCB 上出现重号:%s' % dup2)
print(u' ✔ 回查:%d 个元件的通道ID 与原理图逐个一致,且无重号' % len(pcb2))
print(u'\n接着重开导入对话框核对:pcb_import.py --base --probe')
if __name__ == '__main__':
main()