# -*- coding: utf-8 -*- u"""下板 PCB1:把元件的**通道ID(uniqueId)**与原理图重新对齐。 /c/Python314/python .claude/pcb1_uid_sync.py # 只对账,不动板 /c/Python314/python .claude/pcb1_uid_sync.py --push # 写回 + 存盘 + 回查 ## 修的是什么 「设计 → 从原理图导入变更」是**按 uniqueId 配对**原理图元件与 PCB 元件的, **不是按位号**。2026-09-08 实测下板上这个 ID 与原理图**整体错了一格**: 位号 PCB 原理图 J26 gge418 gge419 J27 gge419 gge420 ← 板上 J27 拿着原理图 J26 的 ID Q9 gge423 gge424 R753 gge413 gge414 于是 EasyEDA 把**原理图的 J26 配到了板上的 J27** 头上,进而提出一整套 「改位号 / 改封装 / 改网络」的重映射。**那份清单看着像在修错,其实会把一块 本来正确的板改坏** —— 同期按四项(位号 / 封装 / 器件值 / 焊盘网络)逐个对账, 风扇块 23 件全部一致。 错位的来源是 skill §9o:某一页被整页重画过,元件 uuid 全新, EasyEDA 重编 uniqueId 时和 PCB 侧对不上了。 ## 判据(不要拿导入对话框当判据) 对话框是**结果**不是**原因**:它按错位的配对去算差异,算出来的东西自然也是错位的。 真正的判据是这里做的事 —— **按位号把两边的 uniqueId 摆在一起比**。 ## 为什么要两阶段写 这是一个**排列**(J27 的 ID 要给 J26、J26 的给别人),直接按目标值挨个写会在 中途出现**两个元件同 ID**。所以先把所有要改的写成一批临时 ID(`zzsync`), 再写成目标值。 ## ⚠️ 写之前先存盘快照 `_pcb1_snapshot.py` 存一份。这个脚本改的是元件身份,出错了看不出来 —— DRC / 几何检查 / 连通性一条都不会报,只有下次开导入对话框才发现。 """ 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 = '7e9415a16a909c51' CACHE = os.path.join(HERE, 'eda-parts.json') 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 pages(): c = json.load(io.open(CACHE, encoding='utf-8')) return c['pages'] # Board1 的页表就在顶层(`--board Board2` 才走 schematic_boards) JS_PCB = 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; // 离板器件不上 PCB o[c.getState_Designator()] = c.getState_UniqueId() || ''; } return {page: cur.name, uids: o}; """ def collect(): pcb = call(JS_PCB) sch = {} for name, uuid in sorted(pages().items()): r = call(JS_SCH % uuid) 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), 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%s' % (d, a or '(空)', b or '(空)', u' ← 新建件,本来就没有' if not a else '')) if not diff: print(u'\n✅ 全部一致,导入对话框应当只报真实差异。') return # 目标值有没有撞车(同一个 uniqueId 被两个位号要) 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} if dup: sys.exit(u'⛔ 目标 uniqueId 撞车,不能自动修:%s' % dup) if not push: print(u'\n加 --push 写回(两阶段:先临时 ID 再目标 ID,避免中途重号)') return plan = [[d, b] for d, (_a, b) in sorted(diff.items())] r = call(u""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r => setTimeout(r, 2000)); const PLAN = %s; const all = await eda.pcb_PrimitiveComponent.getAll(); const by = {}; for (const c of all) by[c.getState_Designator()] = c; // 第一阶段:全部写成临时 ID,解开排列里的环 let n1 = 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++; } // 第二阶段:写成目标 ID let n2 = 0; 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'])) # §9ad:save() 的返回值不算数,重新读一遍 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接着开一次「从原理图导入变更」核对:p1_import_rows2.py') if __name__ == '__main__': main()