# -*- coding: utf-8 -*- u"""下板 PCB1「从原理图导入变更」。 用法: python .claude/pcb1_import.py --bom 逐页重抓原理图 BOM,存 _p1_sch_bom.json(对账的基准) python .claude/pcb1_import.py --probe 只开对话框、把全部变更行读出来存 _p1_import_rows.json python .claude/pcb1_import.py --apply 读完之后真的点「应用修改」(先自动取消勾选「包含设计规则」) python .claude/pcb1_import.py --verify 拿 PCB 上的元件表和 _p1_sch_bom.json 对账 ⚠️ **改过原理图就要先 `--bom` 再 `--verify`**,否则是拿新 PCB 和旧原理图对账 —— 刚改的那几个位号会被报成「PCB 有原理图没有」,而真正的错混在里面看不出来。 (2026-09-06 之前 `_p1_sch_bom.json` 在仓库里根本没有生产者,只能手工维护。) ⚠️ skill §9o:整页重画过的页(P5-BTB / P6-FAN-EDL)元件 UUID 全是新的, EasyEDA 会把 PCB 上**将被删掉的**元件复用成新元件(改封装 + 改位号), 所以导入后必须跑 --verify 核对「位号 ↔ 封装」是不是和原理图一致。 """ import io, json, os, re, sys, time HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE) from eda import run PCB = '7e9415a16a909c51' # 2026-09-06:加 `--board2` 就对载板 PCB2 做同一套事(BOM / 对话框 / 对账),产出文件带 _p2_ 前缀 BOARD2 = '--board2' in sys.argv if BOARD2: PCB = '942ed1560d81c1e7' sys.argv = [a for a in sys.argv if a != '--board2'] BOM_FILE = '_p2_sch_bom.json' if BOARD2 else '_p1_sch_bom.json' COMPS_FILE = '_pcb2_comps.json' if BOARD2 else '_pcb1_comps.json' ROWS_FILE = '_p2_import_rows.json' if BOARD2 else '_p1_import_rows.json' HELPERS = u""" const sleep = ms => new Promise(r => setTimeout(r, ms)); const propsOf = el => { for (const k of Object.keys(el)) if (k.startsWith('__reactProps$')) return el[k]; return null; }; const fire = (el) => { let t = el; for (let i=0;i<8&&t;i++){const p=propsOf(t); if(p&&typeof p.onClick==='function'){p.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; }; const nat = b => { 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)); }; const leaf = (txt) => { for (const el of document.querySelectorAll('*')) if (el.children.length===0 && (el.textContent||'').trim()===txt && el.offsetParent!==null) return el; return null; }; """ JS_PROBE = HELPERS + u""" await eda.dmt_EditorControl.openDocument('%s'); await sleep(1800); const out = {}; fire(leaf('设计')); await sleep(900); out.opened = fire(leaf('从原理图导入变更')); await sleep(9000); let sc = null; for (const el of document.querySelectorAll('div')) if (el.scrollHeight > el.clientHeight + 20 && /移除元件|增加元件|修改/.test(el.innerText || '')) if (!sc || el.innerText.length < sc.innerText.length) sc = el; const seq = []; if (sc) { sc.scrollTop = 0; await sleep(400); let last = -1; for (let i = 0; i < 200; i++) { seq.push({top: sc.scrollTop, txt: sc.innerText}); if (sc.scrollTop === last) break; last = sc.scrollTop; if (sc.scrollTop + sc.clientHeight >= sc.scrollHeight - 2) break; sc.scrollTop += Math.max(40, sc.clientHeight / 2); await sleep(260); } } out.seq = seq; out.buttons = [...document.querySelectorAll('button')].filter(b => b.offsetParent !== null).map(b => (b.innerText||'').trim()).filter(Boolean); return out; """ % PCB JS_APPLY = HELPERS + u""" const out = {}; let ruleBox = null, ruleTxt = null; for (const el of document.querySelectorAll('*')) { if (el.children.length === 0 && /设计规则/.test((el.textContent||'').trim()) && el.offsetParent !== null) { ruleTxt = (el.textContent||'').trim(); let r = el; for (let i = 0; i < 6 && r; i++) { const cb = r.querySelector && r.querySelector('input[type=checkbox]'); if (cb) { ruleBox = cb; break; } r = r.parentElement; } break; } } out.ruleTxt = ruleTxt; out.ruleChecked = ruleBox ? ruleBox.checked : null; if (ruleBox && ruleBox.checked) { nat(ruleBox); await sleep(500); out.ruleAfter = ruleBox.checked; } const btn = [...document.querySelectorAll('button')].find(b => b.offsetParent !== null && (b.innerText||'').trim() === '应用修改'); if (!btn) return Object.assign(out, {err: 'no 应用修改'}); nat(btn); await sleep(15000); for (const b of document.querySelectorAll('button')) if (b.offsetParent !== null && /^(确定|确认|是)$/.test((b.innerText||'').trim())) { nat(b); await sleep(4000); out.confirmed = (b.innerText||'').trim(); } await sleep(4000); const comps = await eda.pcb_PrimitiveComponent.getAll(); out.n = comps.length; out.buttons = [...document.querySelectorAll('button')].filter(b => b.offsetParent !== null).map(b => (b.innerText||'').trim()).filter(Boolean).slice(0,20); return out; """ JS_DUMP = u""" // ⚠️ skill §9l:每段会读/改文档的 JS 都要自己 openDocument。 // 少了这一行,紧跟在 --bom(最后停在某张原理图上)之后跑 --verify, // `pcb_PrimitiveComponent.getAll()` 在原理图文档上直接抛 // `Cannot read properties of null (reading 'map')` —— 报错信息完全指不到根因。 await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r => setTimeout(r, 1800)); const cs = await eda.pcb_PrimitiveComponent.getAll(); const out = []; for (const c of cs) out.push({d: c.designator, x: c.x, y: c.y, rot: c.rotation, lay: c.layer, fp: (c.footprint && c.footprint.name) ? c.footprint.name : (c.footprint || null)}); return out; """ % PCB JS_BOM = u""" const sleep = ms => new Promise(r => setTimeout(r, ms)); await eda.dmt_EditorControl.openDocument('%s'); await sleep(1300); const cur = String((await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name || ''); if (cur.toUpperCase() !== '%s') return {err: 'openDocument 打到了 ' + cur}; const list = []; for (const c of await eda.sch_PrimitiveComponent.getAll()) { if (c.getState_ComponentType() !== 'part') continue; let fp = null; try { const f = c.getState_Footprint(); if (f) fp = {name: String(f.name || f)}; } catch (e) {} list.push({d: String(c.getState_Designator ? c.getState_Designator() : ''), uuid: c.getState_PrimitiveId(), fp: fp}); } return {list: list}; """ def refresh_bom(): u"""逐页重抓原理图 BOM。 ⚠️ **一页一个请求**,不要一次抓 11 页 —— `eda.py` 把返回值截到 20000 字符再打印, 整本 BOM 装不下,截断之后 `json.load` 直接炸在中间(2026-09-06 踩过)。 """ ep = json.load(io.open(os.path.join(HERE, 'eda-parts.json'), encoding='utf-8')) pages = ep['schematic_boards']['Board2']['pages'] if BOARD2 else ep['pages'] bom, bad = {}, [] for name, uuid in pages.items(): r = run(JS_BOM % (uuid, name.upper()), timeout=300) if not r.get('success'): bad.append(u'%s: %s' % (name, json.dumps(r, ensure_ascii=False)[:160])) continue res = r['result'] if res.get('err'): bad.append(u'%s: %s' % (name, res['err'])) continue bom[name] = res['list'] print(u' %-12s %3d 件' % (name, len(res['list']))) if bad: print(u'❌ 这几页没抓到,**没有写缓存**:') for b in bad: print(u' · ' + b) sys.exit(1) io.open(os.path.join(HERE, BOM_FILE), 'w', encoding='utf-8').write( json.dumps(bom, ensure_ascii=False)) print(u'已写 ' + BOM_FILE + u':%d 页 / %d 件' % (len(bom), sum(len(v) for v in bom.values()))) def dump(): r = run(JS_DUMP, timeout=300) if not r.get('success'): sys.exit(json.dumps(r, ensure_ascii=False)[:600]) return r['result'] def verify(): cs = dump() io.open(os.path.join(HERE, COMPS_FILE), 'w', encoding='utf-8').write(json.dumps(cs, ensure_ascii=False)) bom = json.load(io.open(os.path.join(HERE, BOM_FILE), encoding='utf-8')) sch = {} for pg, v in bom.items(): for c in v: d = c.get('d') if d and re.match(r'^[A-Za-z]+\d', d): sch[d] = ((c.get('fp') or {}).get('name'), pg) pcb = {c['d']: c for c in cs} miss = sorted(set(sch) - set(pcb)); extra = sorted(set(pcb) - set(sch)) bad = [(d, pcb[d]['fp'], sch[d][0]) for d in sorted(set(sch) & set(pcb)) if pcb[d]['fp'] != sch[d][0]] print(u'PCB %d 件 / 原理图 %d 件' % (len(cs), len(sch))) print(u'原理图有 PCB 没有 %d: %s' % (len(miss), miss[:40])) print(u'PCB 有原理图没有 %d: %s' % (len(extra), extra[:40])) print(u'封装对不上 %d:' % len(bad)) for t in bad[:40]: print(u' %s PCB=%s SCH=%s' % t) return not (miss or extra or bad) if __name__ == '__main__': a = sys.argv[1] if len(sys.argv) > 1 else '--verify' if a == '--bom': refresh_bom() elif a == '--probe': r = run(JS_PROBE, timeout=400) if not r.get('success'): sys.exit(json.dumps(r, ensure_ascii=False)[:800]) res = r['result'] io.open(os.path.join(HERE, ROWS_FILE), 'w', encoding='utf-8').write(json.dumps(res, ensure_ascii=False, indent=1)) print(u'opened=%s 快照 %d 屏' % (res.get('opened'), len(res.get('seq') or []))) print(res.get('buttons')) elif a == '--apply': r = run(JS_APPLY, timeout=400) print(json.dumps(r, ensure_ascii=False)[:1200]) elif a == '--verify': sys.exit(0 if verify() else 1) else: sys.exit(__doc__)