185 行
8.8 KiB
Python
185 行
8.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""跑 PCB 的 DRC 并把结果读回来。
|
||
|
||
/c/Python314/python .claude/drc_read.py [--no-check] # 下板
|
||
/c/Python314/python .claude/drc_read.py --pcb2 [--no-check] # 载板
|
||
|
||
⚠️ **不加 `--pcb2` 跑的是下板** —— 2026-09-06 在这上面白查过半天
|
||
(读到 649 条,里面全是 `KEY_OE#` / `SDA` 这些下板才有的网络)。
|
||
|
||
⚠️⚠️ **「连接性错误」这一支在程序化建线 / 建过孔之后是陈旧的,
|
||
必须 `location.reload()` 之后再跑才准。** 2026-09-06 实测:
|
||
下发完 12 个差分网络后读到 403(每个网络还报 2~7 条「与其他相同网络的对象断开了连接」),
|
||
而按实际铜自算(`pcb2_net_conn.py`)是 22/22 全连通、`pcb_Net.getNetLength()`
|
||
也等于算出来的长度;重载客户端后立刻变成 338(比布线前少 26,正合预期)。
|
||
「间距错误」那一支倒是即时更新的。
|
||
|
||
## 为什么要有这个脚本
|
||
|
||
`pcb_Drc.check()` **只返回一个 boolean**,`pcb_Drc` 的 40 多个方法里
|
||
一个结果 getter 都没有,也没有 DRC 标记层可读 —— 一度以为结果拿不回来。
|
||
|
||
**但 DRC 面板在 DOM 里是挂载着的**(和设计规则对话框不一样,那个窗口不前台就不挂载)。
|
||
两个关键手法:
|
||
|
||
1. **左边的分类树要用 React 的 onClick 点,`el.click()` 不管用。**
|
||
从元素身上找 `__reactProps$*`,一路往上找到带 `onClick` 的那层,直接调。
|
||
不点就不选中,右边明细区一直显示「空列表」。
|
||
2. **明细表的 innerText 尾巴上粘着一大段 CSS**(分页组件的 `<style>`)。
|
||
碰到 `input::-webkit` 就截断。
|
||
|
||
## 顺带一提:画布上那一片黄叉是 check() 画出来的
|
||
|
||
跑完 `check()`,每个未布线焊盘都会盖一个黄叉。没布线时 1200+ 个,
|
||
看着像出了大事,其实全是「连接性错误」。**要看的是「间距错误」那一支。**
|
||
清掉:DRC 面板上的「清除错误」按钮。
|
||
|
||
⚠️ 写这个文件不要用 bash heredoc —— 实测反斜杠会被吃掉一层,正则和 JS 全废。
|
||
"""
|
||
import json, os, re, sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
from eda import run
|
||
|
||
# 默认下板;载板加参数:/c/Python314/python .claude/drc_read.py --pcb2
|
||
# 2026-09-08:加 `--blade` / `--base` 跑 R2 的两块板(UUID 从 .claude/r2/r2-uuids.json 取)
|
||
if '--blade' in sys.argv or '--base' in sys.argv:
|
||
import json as _j, io as _io, os as _os
|
||
_u = _j.load(_io.open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)),
|
||
'r2', 'r2-uuids.json'), encoding='utf-8'))
|
||
PCB = _u['boards']['BASE' if '--base' in sys.argv else 'BLADE']['pcb']['uuid']
|
||
if '--pcb' in sys.argv: # 2026-09-14:直接给 uuid(BLADE-PCB_1 = c575a2e854057614 不在 r2-uuids 里)
|
||
PCB = sys.argv[sys.argv.index('--pcb') + 1]
|
||
else:
|
||
PCB = '942ed1560d81c1e7' if '--pcb2' in sys.argv else '7e9415a16a909c51'
|
||
|
||
JS_TREE = u"""
|
||
await eda.dmt_EditorControl.openDocument('%s');
|
||
await new Promise(r => setTimeout(r, 1500));
|
||
%s
|
||
await new Promise(r => setTimeout(r, 2500));
|
||
let node = null;
|
||
// 锚点不能用「间距错误」—— 那一支修干净之后整个节点就没了
|
||
for (const el of document.querySelectorAll('*'))
|
||
if (el.children.length === 0 && (el.textContent || '').trim().startsWith('全部 (')) { node = el; break; }
|
||
if (!node) return {err: 'DRC 面板没找到 —— 面板是不是收起来了?还是 DRC 没跑过?'};
|
||
let root = node;
|
||
for (let i = 0; i < 10 && root.parentElement; i++) {
|
||
root = root.parentElement;
|
||
if ((root.innerText || '').length > 30) break;
|
||
}
|
||
return {tree: (root.innerText || '').split('\\n').join('')};
|
||
"""
|
||
|
||
JS_DETAIL = 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;
|
||
};
|
||
let leaf = null;
|
||
for (const el of document.querySelectorAll('*'))
|
||
if (el.children.length === 0 && (el.textContent || '').trim() === %s) { leaf = el; break; }
|
||
if (!leaf) return {err: 'leaf not found'};
|
||
// ⚠️ el.click() 不触发 React 的 onClick,必须从 fiber 上取 handler 直接调
|
||
let t = leaf, fired = false;
|
||
for (let i = 0; i < 6 && t && !fired; i++) {
|
||
const p = propsOf(t);
|
||
if (p && typeof p.onClick === 'function') {
|
||
p.onClick({target: t, currentTarget: t, stopPropagation(){}, preventDefault(){},
|
||
nativeEvent: {}, button: 0, detail: 1});
|
||
fired = true;
|
||
}
|
||
t = t.parentElement;
|
||
}
|
||
if (!fired) return {err: 'no onClick in ancestor chain'};
|
||
await sleep(1500);
|
||
let best = null;
|
||
for (const el of document.querySelectorAll('div,table,tbody')) {
|
||
const x = el.innerText || '';
|
||
// ⚠️ 光判「含错误对象」会选中表头那个小格子(innerText 就四个字)。
|
||
// 必须同时要求里面真的有一条明细行,行首长这样:`1间距错误…` / `3物理错误…`
|
||
// ⚠️ 行首类别还有「差分对错误」「同封装内间距错误」—— 只认四种的话这两类只剩标题(2026-09-10 底板 18 条被吞)
|
||
if (/错误对象/.test(x) && /[0-9](间距|物理|制造|连接性|差分对|同封装内间距)错误/.test(x)
|
||
&& (!best || x.length < best.length)) best = x;
|
||
}
|
||
return {detail: best || ''};
|
||
"""
|
||
|
||
|
||
def call(js, timeout=300):
|
||
r = run(js, timeout=timeout)
|
||
if not r.get('success'):
|
||
print(u'失败:', json.dumps(r, ensure_ascii=False)[:400])
|
||
sys.exit(1)
|
||
return r['result']
|
||
|
||
|
||
def main():
|
||
check = u'' if '--no-check' in sys.argv else u'await eda.pcb_Drc.check();'
|
||
r = call(JS_TREE % (PCB, check))
|
||
if r.get('err'):
|
||
print(r['err'])
|
||
sys.exit(1)
|
||
tree = r['tree']
|
||
|
||
# 树是一整条文本:全部 (1273)间距错误 (9)贴片焊盘 到 贴片焊盘 (5)...连接性错误 (1264)...
|
||
seg = tree.split(u'间距错误', 1)
|
||
has_sp = len(seg) > 1
|
||
spacing_part = seg[1].split(u'连接性错误', 1)[0] if has_sp else u''
|
||
total = re.search(r'全部 \((\d+)\)', tree)
|
||
n_sp = re.match(r'\s*\((\d+)\)', spacing_part)
|
||
n_conn = re.search(r'连接性错误 \((\d+)\)', tree)
|
||
|
||
print(u'全部 %s 条 = 间距错误 %s + 连接性错误 %s(连接性 = 还没布线,预期之内)'
|
||
% (total.group(1) if total else u'?',
|
||
n_sp.group(1) if n_sp else (u'?' if has_sp else u'0'),
|
||
n_conn.group(1) if n_conn else u'?'))
|
||
|
||
cats = re.findall(r'([^()\d][^()]*?) \((\d+)\)', spacing_part)
|
||
# 物理错误那一支也要列出来(线宽违规、过孔尺寸违规都在这儿)
|
||
phys = tree.split(u'物理错误', 1)
|
||
if len(phys) > 1:
|
||
tail = phys[1].split(u'连接性错误', 1)[0].split(u'间距错误', 1)[0]
|
||
cats += [(u'物理错误·' + n.strip(), c)
|
||
for n, c in re.findall(r'([^()\d][^()]*?) \((\d+)\)', tail)]
|
||
# `--conn`:连接性错误那一支也逐条列出来(2026-09-11:刀片 90×68 要确认 39 条里有没有被走线围住、
|
||
# 铺铜够不着的 GND 焊盘 —— board_check 读不了铺铜,只有 DRC 这一支看得到)
|
||
if '--conn' in sys.argv:
|
||
conn = tree.split(u'连接性错误', 1)
|
||
if len(conn) > 1:
|
||
cats += [(u'连接性错误·' + n.strip(), c)
|
||
for n, c in re.findall(r'([^()\d][^()]*?) \((\d+)\)', conn[1])]
|
||
if not cats:
|
||
print(u'')
|
||
print(u'✅ 没有间距错误。')
|
||
return
|
||
print(u'')
|
||
bad = 0
|
||
for name, n in cats:
|
||
name = name.strip()
|
||
bad += int(n)
|
||
print(u'── %s (%s) ──' % (name, n))
|
||
# ⚠️ 树节点的 textContent 是「名字 (计数)」,光用名字匹配不上
|
||
leafname = name.split(u'·')[-1]
|
||
d = call(JS_DETAIL % json.dumps(u'%s (%s)' % (leafname, n), ensure_ascii=False))
|
||
if d.get('err'):
|
||
print(u' 读不到明细: %s' % d['err'])
|
||
continue
|
||
txt = d['detail'].split(u'input::-webkit')[0] # 尾巴粘着分页组件的 CSS
|
||
# ⚠️ 行首的类别名不止「间距错误」—— 还有「物理错误」「制造错误」等。
|
||
# 只匹配间距错误会让另外那些类别只剩一个标题、一条明细都看不到。
|
||
kinds = u'间距错误|物理错误|制造错误|连接性错误|差分对错误|同封装内间距错误'
|
||
rows = re.findall(r'(\d+)(%s)(.+?)(?=\d+(?:%s)|$)' % (kinds, kinds), txt)
|
||
for idx, kind, body in rows:
|
||
print(u' %s. [%s] %s' % (idx, kind, body.strip()[:190]))
|
||
if not rows and txt.strip():
|
||
print(u' (明细没解析出来,原文前 300 字)%s' % txt.strip()[:300])
|
||
print(u'')
|
||
sys.exit(1 if bad else 0)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|