原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。 这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。 纳入: - 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本 - .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、 pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源) - 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾) - firmware/ 的 A1 骨架与 site/ 文档站源码 .gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、 site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。 .gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存, 不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
192 行
9.1 KiB
Python
192 行
9.1 KiB
Python
# -*- 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'
|
||
|
||
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('7e9415a16a909c51');
|
||
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;
|
||
"""
|
||
|
||
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 踩过)。
|
||
"""
|
||
pages = json.load(io.open(os.path.join(HERE, 'eda-parts.json'), encoding='utf-8'))['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, '_p1_sch_bom.json'), 'w', encoding='utf-8').write(
|
||
json.dumps(bom, ensure_ascii=False))
|
||
print(u'已写 _p1_sch_bom.json:%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, '_pcb1_comps.json'), 'w', encoding='utf-8').write(json.dumps(cs, ensure_ascii=False))
|
||
bom = json.load(io.open(os.path.join(HERE, '_p1_sch_bom.json'), 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, '_p1_import_rows.json'), '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__)
|