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
这个提交包含在:
@@ -0,0 +1,203 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""R2「从原理图导入变更」(刀片 / 底板通用)。
|
||||
|
||||
/c/Python314/python .claude/r2/pcb_import.py --bom 逐页抓原理图 BOM(对账基准)
|
||||
/c/Python314/python .claude/r2/pcb_import.py --probe 开对话框、把变更行读出来
|
||||
/c/Python314/python .claude/r2/pcb_import.py --apply 真的点「应用修改」(先取消「包含设计规则」)
|
||||
/c/Python314/python .claude/r2/pcb_import.py --verify PCB 元件表 ↔ 原理图 BOM 逐件对账
|
||||
加 --base 就对底板做同一套事(默认是刀片)。
|
||||
|
||||
做法照搬 R1 的 `.claude/pcb1_import.py`(那一份是唯一的说明书,两个坑都在它头上):
|
||||
**没有可用的 API**,只能驱动 GUI。⚠ `pcb_Document.importChanges()` 这个 API 在本机
|
||||
客户端上**一律返回 false、对话框都不弹**(2026-09-08 实测,刀片 PCB 上试过带/不带
|
||||
schematic uuid 两种),别再浪费时间试它。
|
||||
|
||||
⚠ 改过原理图就要**先 `--bom` 再 `--verify`**,否则是拿新 PCB 和旧 BOM 对账。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
sys.path.insert(0, os.path.dirname(HERE))
|
||||
from eda import run # noqa: E402
|
||||
|
||||
U = json.load(io.open(os.path.join(HERE, 'r2-uuids.json'), encoding='utf-8'))
|
||||
BASE = '--base' in sys.argv
|
||||
BOARD = 'BASE' if BASE else 'BLADE'
|
||||
PCB = U['boards'][BOARD]['pcb']['uuid']
|
||||
PAGES = U['boards'][BOARD]['pages']
|
||||
TAG = 'base' if BASE else 'blade'
|
||||
BOM_FILE = '_%s_sch_bom.json' % TAG
|
||||
COMPS_FILE = '_%s_pcb_comps.json' % TAG
|
||||
ROWS_FILE = '_%s_import_rows.json' % TAG
|
||||
|
||||
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(2000);
|
||||
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(5000);
|
||||
await eda.pcb_Document.save();
|
||||
out.n = (await eda.pcb_PrimitiveComponent.getAll()||[]).length;
|
||||
return out;
|
||||
"""
|
||||
|
||||
JS_DUMP = u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
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(1500);
|
||||
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 call(js, timeout=300):
|
||||
r = run(js, timeout=timeout)
|
||||
if not r.get('success'):
|
||||
sys.exit(json.dumps(r, ensure_ascii=False)[:800])
|
||||
return r['result']
|
||||
|
||||
|
||||
def refresh_bom():
|
||||
u"""一页一个请求 —— eda.py 会把返回截到 20000 字符,整本 BOM 装不下。"""
|
||||
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'[X] 这几页没抓到,**没有写缓存**:')
|
||||
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'已写 %s:%d 页 / %d 件' % (BOM_FILE, len(bom), sum(len(v) for v in bom.values())))
|
||||
|
||||
|
||||
def verify():
|
||||
cs = call(JS_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'原理图 %d 件 / PCB %d 件' % (len(sch), len(pcb)))
|
||||
if miss:
|
||||
print(u'[X] 原理图有、PCB 没有 (%d): %s' % (len(miss), ' '.join(miss[:30])))
|
||||
if extra:
|
||||
print(u'[X] PCB 有、原理图没有 (%d): %s' % (len(extra), ' '.join(extra[:30])))
|
||||
for d, a, b in bad[:20]:
|
||||
print(u'[X] %s 封装不一致: PCB=%s 原理图=%s' % (d, a, b))
|
||||
if not (miss or extra or bad):
|
||||
print(u'[OK] %d 件逐件对上(位号 + 封装)' % len(sch))
|
||||
return miss, extra, bad
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(u'板: %s PCB=%s %d 页' % (BOARD, PCB, len(PAGES)))
|
||||
if '--bom' in sys.argv:
|
||||
refresh_bom()
|
||||
if '--probe' in sys.argv:
|
||||
r = call(JS_PROBE, timeout=420)
|
||||
io.open(os.path.join(HERE, ROWS_FILE), 'w', encoding='utf-8').write(
|
||||
json.dumps(r, ensure_ascii=False, indent=1))
|
||||
txt = '\n'.join(s['txt'] for s in r.get('seq', []))
|
||||
print(u'对话框: opened=%s 按钮=%s' % (r.get('opened'), r.get('buttons')))
|
||||
print(txt[:3000])
|
||||
if '--apply' in sys.argv:
|
||||
print(call(JS_APPLY, timeout=420))
|
||||
if '--verify' in sys.argv:
|
||||
verify()
|
||||
在新工单中引用
屏蔽一个用户