- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base) - 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base) - 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、 东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base) - 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面 - 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、 J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder 开关(route_base、remote_route.ps1);剩 37 个网待手工补 - 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base); 已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查) - 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
201 行
9.7 KiB
Python
201 行
9.7 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'
|
||
# 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__)
|