R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发

- 规则:铜层 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
这个提交包含在:
zhou yang
2026-09-10 21:02:10 +08:00
共同撰写人 Claude Opus 5
父节点 18d3d69856
当前提交 efcba30a0c
共修改 280 个文件,包含 92007 行新增和 15053 行删除
+200 -191
查看文件
@@ -1,191 +1,200 @@
# -*- 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__)
# -*- 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__)