初始导入:PhoneRail 五路手机直供电源板
原理图/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
这个提交包含在:
+167
@@ -0,0 +1,167 @@
|
||||
# -*- 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()
|
||||
在新工单中引用
屏蔽一个用户