# -*- 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()