原理图/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
168 行
7.5 KiB
Python
168 行
7.5 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
|
|
PCB = 'f126ffb29c92f9b2' 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物理错误…`
|
|
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)]
|
|
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
|
|
# ⚠️ 行首的类别名不止「间距错误」—— 还有「物理错误」「制造错误」等。
|
|
# 只匹配间距错误会让另外那些类别只剩一个标题、一条明细都看不到。
|
|
rows = re.findall(r'(\d+)(间距错误|物理错误|制造错误|连接性错误)(.+?)'
|
|
r'(?=\d+(?:间距错误|物理错误|制造错误|连接性错误)|$)', 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()
|