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
这个提交包含在:
@@ -1362,3 +1362,447 @@ DRC 照样报旧线的错 —— 看上去就是「改了没生效」。
|
||||
|
||||
顺带:把多个库 API 塞进同一个 `/execute` 里跑,其中一个挂住(本次是 `lib_LibrariesList` / `lib_Classification`)
|
||||
整条请求 30 s 超时,**但前面已经执行的 create/copy 仍然生效** —— 超时后先去库里搜一遍有没有留下半成品再重试。
|
||||
|
||||
## 9z. 改库里的器件**不会传到原理图实例** —— 换封装要走「封装管理器」的 DOM(2026-09-07)⚠️⚠️
|
||||
|
||||
**现象**:`lib_Device.modify()` 把器件 `1c8e0ea1…` 的封装换成 `PAD-D1.5_MI8-KEY` 之后,
|
||||
原理图里 20 个 J41~J60 的 `footprint` 仍是 `POGO-SMD1P_D1.9`,`component.uuid` 甚至指向一个
|
||||
`lib_Device.get()` 已经查不到的旧 uuid(`7904eff7…`)—— 实例是放置时的快照,和库脱钩。
|
||||
于是「从原理图导入变更」一行都不出。
|
||||
|
||||
**API 改不动**:`sch_PrimitiveComponent.modify()` 的签名里没有 `footprint` / `component`,
|
||||
塞进去被静默忽略(实测读回不变)。`更新工程库...` 菜单只弹出一个「封装需要关联器件」的提示框。
|
||||
|
||||
**能用的路**:原理图页 **工具 → 封装管理器...**(`lc_modal_dialog_box_*` 模态框),全程 DOM:
|
||||
|
||||
1. 左表 `input[placeholder="过滤"]` 用 React 的 value setter + `input` 事件填旧封装名,只剩要换的行;
|
||||
表是**虚拟滚动**的(`lc-table-body_*`,一次只渲染 ~12 行),要边 `scrollTop += 60` 边找 `J41…J60` 的叶子元素;
|
||||
点**叶子文本元素**(不是 `<tr>`)才选中(`lc-table-row--selected_*`),`ctrlKey: true` 累加多选,20 个逐个点。
|
||||
2. 右侧点「个人」页签(`div.tree-content_*`),`input[placeholder^="搜索"]` 填封装名 + Enter,
|
||||
结果表里点封装那一格的叶子。
|
||||
3. ⚠️ **「更新」按钮要等右下「检查封装尺寸」表出现焊盘尺寸(`1|1.5 * 1.5`)才会去掉 `*_disabled_*` 类**
|
||||
—— 之前 `onClick` 调了也没反应、模态还在、封装没变。等 1~2 s 再点,toast「更新成功!」,
|
||||
左表按旧名过滤会变空(因为都换掉了)。
|
||||
4. 读回:`c.footprint.name` 全变新名(uuid 是**工程库副本**的新 uuid,不是个人库那个),`component` 仍指旧器件 —— 不影响导入。
|
||||
|
||||
**BOM 字段在实例上**:`sch_PrimitiveComponent.modify(id, {addIntoBom: false})` 生效;
|
||||
`manufacturer / manufacturerId / supplier / supplierId` 传 `''` 或 `null` 都被忽略 —— 清不掉,靠 addIntoBom=false 把它挡在 BOM 外。
|
||||
|
||||
**顺带**:`lib_Device.modify` 的 `property`(Add into BOM 等)同样被忽略(§上一轮),所以这两处都不要指望 API。
|
||||
|
||||
⚠️ 一个卡了半小时的假象:之前留在页面上的「封装需要关联器件使用…/ 关联器件 / 取消」提示框
|
||||
(`lc-tips-modal-*`,`fp_keypad.py` 的 `openInEditor` 留下的)会挡住后面所有菜单动作,
|
||||
菜单探测脚本第一件事先把可见的「取消」按钮点一遍。
|
||||
|
||||
## 9aa. PCB 收尾顺序:reload 会丢铺铜填充,API 新建的铜要 reload 之后 DRC 才认(2026-09-07)⚠️⚠️
|
||||
|
||||
第三十轮实测(载板):
|
||||
|
||||
- `pcb2_reload.py`(关对话框 → 存盘 → 页面 reload)之后 `getState_PourFills` 回读 **0 块** —— 填充没了,
|
||||
只剩铺铜边框。此时跑 DRC 会把所有只靠内层铜面连通的对象(GND 397 条、VOn_P 65 条)全报「断开」。
|
||||
- 反过来,用 API 新建了几百段导线之后**不 reload 直接 DRC**,刚建的那几个网络成片报「与其他相同网络的对象断开」
|
||||
(171 条),reload 之后同一份数据 DRC 归零 —— 连接性索引不跟着 API 创建更新。
|
||||
- ⇒ **固定顺序:`pcb2_reload.py` → `pcb2_pour.py --push` → `inner1_verify.py` → `drc_read.py --pcb2`**。
|
||||
pour 之后不许再 reload;DRC 之前必须 reload 过一次。
|
||||
|
||||
DRC 面板里的图元 ID(`e16831` 这种)**不是** `getState_PrimitiveId()`,`pcb_PrimitiveLine.get('e16831')` 返回空数组;
|
||||
要定位只能按网络 + 坐标自己找(`_drc_conn.py` 读分类明细,再用几何去对)。
|
||||
|
||||
## 9ab. 「挖槽区域」= 封装自带的 NPTH,不是 layer 12 填充(2026-09-07)
|
||||
|
||||
DRC 报「挖槽区域 到 过孔 0mil」的 `e56`,在 `pcb_PrimitiveFill.getAll()`(只有 10 条穿线槽)和 `pcb_PrimitiveRegion`(空)里都找不到 ——
|
||||
它是 J23 封装自带的 Ø0.6 非金属化孔(§9u 说过 API 读不出来,`pcb2_npth.py --write` 反解出的 `_pcb2_npth.json`)。
|
||||
布线脚本要把这份清单当无网络的通孔盘加进障碍表(`pcb2_route_dp.geom()` 已加)。
|
||||
|
||||
## 9ac. `pcb_PrimitiveLine.create()` 建一条穿过本网焊盘的线,会在焊盘中心截断(2026-09-07)⚠️
|
||||
|
||||
实测(载板 KEY_RCLK 穿 U22.12):路径 (126.896,66.7)→(126.865,70.521) 从焊盘中心 (126.865,69.236) 旁 0.01 mm 穿过去接出线段末端,
|
||||
建完板上只剩 (66.7→69.236) 那一半,后半段和同位置的出线段一起没了,DRC 报焊盘 + 一个过孔「断开」。
|
||||
而两条**完全重合**的线 create 两次只留一条(后建的替换先建的)。
|
||||
⇒ 下发前把离本网焊盘中心 < 0.15 mm 的线段在中心处拆成两段(`pcb2_route_sig.push_sig` 已做),别让任何一段"穿过"焊盘。
|
||||
|
||||
## 6e. SOT-23 想要「G 左 / S 上 / D 下」只能靠 **rot + mirror**(2026-09-07)⚠️
|
||||
|
||||
画高边 P-MOS(源极接 +12V 在上、漏极接负载在下、栅极朝左)时,
|
||||
**单靠 rotation 做不出来**。四种组合实测(AO3401A `be56508f…`,偏移是相对元件中心):
|
||||
|
||||
| create 参数 | G | S | D | 用在 |
|
||||
|---|---|---|---|---|
|
||||
| `rot 0, mirror false` | 左 (−20,0) | **下** (+10,−20) | 上 (+10,+20) | 低边 N-MOS(源极接地)|
|
||||
| `rot 0, mirror true` | **右** (+20,0) | 下 (−10,−20) | 上 (−10,+20) | **栅极朝右接收信号**(信号从右往左流的接收电路)|
|
||||
| `rot 180, mirror false` | 右 (+20,0) | 上 (−10,+20) | 下 (−10,−20) | —— |
|
||||
| **`rot 180, mirror true`** | **左 (−20,0)** | **上 (+10,+20)** | **下 (+10,−20)** | **高边 P-MOS** |
|
||||
|
||||
`mirror` 是**水平镜像**(只把 G 换到另一侧),rot 180 才上下翻。两个都要才凑得出第四行。
|
||||
|
||||
P6 的两路 TACH 接收就是靠第二行:原始信号从右边的风扇座过来,
|
||||
栅极朝右接住,漏极向上接 3V3 上拉、源极向下接地 —— 一根绕线都不用。
|
||||
|
||||
⚠️ `mirror` 只影响原理图符号,不镜像 PCB 封装。但**放完照样要 `getAllPins()` 断言**(§6c)。
|
||||
|
||||
## 2b. `createNetFlag('Power', …)` 的网络名可以是任意串(2026-09-07)
|
||||
|
||||
不限于 `+3V3` / `+12V` / `+5V`。实测 `createNetFlag('Power', 'FAN_12V_SW', x, y, 0, false)`
|
||||
读回来就是 `FAN_12V_SW`,rot 0 / 180 都一样。
|
||||
|
||||
⇒ **开关后的电源轨(本项目的 `FAN_12V_SW`)用电源旗最省事**:
|
||||
7 个接点各挂一个旗,不用画母线,也不会像 netport 那样被 `xpage.py` 当成漏接
|
||||
(旗有 7 个实例)。真正只有一个实例的局部网络才需要进 `xpage.NAMING_ONLY`。
|
||||
|
||||
⚠️ 但 §3b 那条不变:**旗建好之后不能移动、不能改名**,只能删了重建。
|
||||
|
||||
## 12a. 地旗的 `GND` 字样掉在符号下方约 15 个单位(2026-09-07)
|
||||
|
||||
`createNetFlag('Ground','GND',x,y,0)` 的符号占 `y−20 ~ y`,但**文字还在更下面**,
|
||||
大约 `y−28 ~ y−15`。所以「旗底 y−20 以下就安全」是错的,实际要留到 **y−30**。
|
||||
|
||||
实测踩到:P6 的地旗放在 `(250,225)`,把 `y=190` 那一行标题压掉了一半。
|
||||
`verify_sch.py` / `port_audit.py` / 本地几何自检**四道全过** —— 这类问题只有截图看得见。
|
||||
|
||||
⇒ 本地自检里给旗留净空按 **30** 算,不是 20。同理端口标签的纵向净空按 ±10 算。
|
||||
|
||||
## 8a-3. 旗放在「自己分两段建的母线」中点上 —— 照样不连(2026-09-07)⚠️⚠️
|
||||
|
||||
§8a-2 的第三种成因,比前两种更容易上当,因为**看起来是端点**。
|
||||
|
||||
给一排电阻做共地母线时的自然写法:把母线拆成两段,让旗的位置正好是两段的公共端点。
|
||||
|
||||
```python
|
||||
w('GND', (190, 250), (230, 250)) # 以为 (230,250) 是端点
|
||||
w('GND', (230, 250), (310, 250))
|
||||
F.append(('Ground', 'GND', 230, 250, 0))
|
||||
```
|
||||
|
||||
**EasyEDA 会把共线的两段合并成一条** `(190,250)→(310,250)`,
|
||||
于是旗又落回中段,判定不相连。
|
||||
|
||||
实测(C2-HUB 的 R903~R906 共地母线):旗放在 x=250(几颗电阻分支点之间的空档)时,
|
||||
PCB 网表里那四颗的地端是 **`$16N236` 而不是 `GND`**;
|
||||
而右边那条母线的旗恰好放在 x=820 = R907 的分支点上,就正常。
|
||||
|
||||
**判据:旗/端口的坐标必须是某条「真的从别处接进来」的导线的端点** ——
|
||||
也就是那里得有一根**垂直方向**的支线(元件的引出线)落进来,光靠把水平母线切两段没用。
|
||||
|
||||
两种可靠写法:
|
||||
|
||||
1. **把旗放在某颗元件的分支点上**(本项目采用:`LBUS = (190, 310, 230)`,230 是 R905 的分支 x);
|
||||
2. 给旗单独接一根**拐弯的**短线:`w('GND', (250,250), (250,225))` 再把旗放 `(250,225)`。
|
||||
|
||||
⚠️ **`verify_sch.py` 抓不到**(它的判据是「落在某条线段上」),
|
||||
`port_audit.py` 在旗被合并线包住时也可能放过。
|
||||
**真正的判据是 PCB 那边的网表** —— 从原理图导入变更之后,
|
||||
把关键网络的焊盘网名读回来对一遍(`pcb_PrimitivePad.getState_Net()`),
|
||||
共地/共电源的那一组尤其要看。本项目就是靠这一步抓到的。
|
||||
|
||||
---
|
||||
|
||||
## 9o-2. 「从原理图导入变更」按 **uniqueId** 配对,不是按位号 —— 错位时它会提出一份**看着像在修错、实则会改坏板子**的清单(2026-09-08)⚠️⚠️⚠️
|
||||
|
||||
§9o 说整页重画会让元件 uuid 全新。**后果比"摆位归零"严重得多**:
|
||||
重画那一页的元件 `uniqueId` 会整体重编,而 PCB 侧还是旧的一套。
|
||||
导入对话框按 uniqueId 把两边配对,于是**整块区域错开一格**。
|
||||
|
||||
下板 2026-09-08 实测(P6-FAN-EDL 重画过):
|
||||
|
||||
位号 PCB 原理图
|
||||
J26 gge418 gge419
|
||||
J27 gge419 gge420 ← 板上 J27 拿着原理图 J26 的 ID
|
||||
Q9 gge423 gge424
|
||||
R753 gge413 gge414
|
||||
|
||||
全板 301 件里错了 20 个,**全在那一页,整齐地差 +1**。
|
||||
对话框据此提出:`J27→J26`、`Q10→Q9`、`R753→R752`、`Q7` 换成 4 脚座封装、
|
||||
`R747` 换成 DC 座封装…… 一大串「改位号 / 改封装 / 改网络」。
|
||||
|
||||
### ⛔ 最贵的一点:那份清单**看起来像在修真错误**
|
||||
|
||||
它会说 `修改网络 J26:3 FAN_OUT_TACH_RAW → FAN_IN_TACH_RAW`,
|
||||
而 J26 确实是进风扇、确实该是 `IN` —— 于是很容易得出「板子接反了,导入正好修好它」。
|
||||
**实际是反的**:板上 J26.3 本来就是 `FAN_IN_TACH_RAW`,
|
||||
对话框说的那个「J26」是它配错的那个对象(板上的 J27)。应用下去才真的接反。
|
||||
|
||||
### 判据:不要拿对话框当判据,按**四项**直接对账
|
||||
|
||||
对话框是**结果**不是**原因** —— 它用错位的配对去算差异,算出来的自然也是错位的。
|
||||
|
||||
| 对账项 | 怎么查 | 能不能单独定案 |
|
||||
|---|---|---|
|
||||
| 位号 | `pcb1_import.py --verify` | ❌ 光这个不够 |
|
||||
| 封装名 | 同上 | ❌ **同封装之间的错位它看不见**(J26/J27 都是 `CONN-TH_4P`,Q9/Q10 都是 SOT-23-3)|
|
||||
| **器件值** | 两边 `getState_Component().name` 按位号比 | ❌ 同型号之间也看不见 |
|
||||
| **焊盘网络** | PCB `pad.getState_Net()` vs 原理图连通性 | ✅ 这一项才有判别力 |
|
||||
|
||||
四项全对 = 板子是对的,对话框在胡说。任何一项对不上 = 先查 uniqueId 再说。
|
||||
|
||||
### 修法:把 PCB 的 uniqueId 按位号写成原理图的那一套
|
||||
|
||||
`pcb_PrimitiveComponent` 上**有 `setState_UniqueId()`**(`setState_Footprint` 没有,
|
||||
所以换封装只能删了重建,见 §9z 一节的 PCB 版)。
|
||||
|
||||
⚠️ **必须两阶段写**:这是一个**排列**(J27 的 ID 要给 J26),
|
||||
直接按目标值挨个写会在中途出现两个元件同 ID。
|
||||
先全部写成临时值(`zzsync<n>`),再写成目标值。
|
||||
|
||||
⚠️ 改的是**元件身份**,`DRC / 几何检查 / 连通性一条都不会报` ——
|
||||
唯一的验收是「重开导入对话框看它还剩几条」。写之前先存快照。
|
||||
|
||||
`.claude/pcb1_uid_sync.py` 就是这件事:对账 → 两阶段写 → 回查逐个一致且无重号。
|
||||
下板实测:20 处对齐之后,变更清单从一大串塌成**一行**
|
||||
(`J1:通道ID (空) → $1I219`,那是 API 新建的元件本来就没有通道ID),
|
||||
应用之后再开对话框 **0 条变更**,而风扇块 20 件的焊盘网络逐条未变。
|
||||
|
||||
> 💡 `getState_UniqueId()` 返回的 `ggeNNN` 和对话框里显示的「通道ID」`$17I175`
|
||||
> 不是同一个字段,但**配对用的是前者** —— 对齐 `ggeNNN` 之后错位就消失了。
|
||||
|
||||
## 9o-3. 读导入对话框:**别用滚动容器的 `innerText`**(2026-09-08)
|
||||
|
||||
那张表是**多列分开渲染**的,`offsetTop` 在列之间碰撞。
|
||||
按 `innerText` 拼接读出来,表头(位号)和明细行会被拼错,
|
||||
读出来的清单似是而非 —— 同一份数据能读出两种互相矛盾的「错位方向」。
|
||||
|
||||
逐 `<tr>` 读、每行自带单元格(`.claude/p1_import_rows2.py`)才是对的。
|
||||
**但即使读对了,结论仍以上面那四项对账为准。**
|
||||
|
||||
|
||||
## 9ah. 「假板框」不止坑摆位 —— 校核脚本的**栅格原点**同样会把负坐标静默吃掉(2026-09-08)⚠️⚠️
|
||||
|
||||
本项目的板框在两板对齐那一轮从 `160×125 @(0,0)` 改成了 `220×135 @(−30,−5)`。
|
||||
`pcb1_place.py` 当时改了,**`pcb1_pour.py` 没改** —— 它的栅格一直是
|
||||
`nx, ny = int(BW/G), int(BH/G)` 加 `int(x / G)`,从 (0,0) 起索引。
|
||||
|
||||
平时看不出来:所有铜都在第一象限。直到把元件挪到 x<0 —— 三块顶层铺铜
|
||||
当场报「碎成 0 块」「断面 0.00 mm」。**板上的铜其实没问题**
|
||||
(多边形是原样下发给 EasyEDA 的,由它去填),坏的只是本地校核。
|
||||
|
||||
⇒ 判据:**校核报 0 而实测回读正常时,先怀疑校核的坐标系,别去改设计。**
|
||||
两边对不上,信实测那一边(`--verify` 读 `getState_PourFills()`)。
|
||||
|
||||
修法是把毫米↔栅格收进四个函数(`gi/gj/gx/gy`),**禁止再直接写 `int(x / G)`**。
|
||||
|
||||
### 9ah-2. ⛔ 但改共享常量会**震到别的模块**,而且是静默的
|
||||
|
||||
`pcb1_netlist.py` 里写着 `G = PO.G`,还自己用 `int(x / G)` 判「焊盘/过孔落没落在铺铜上」。
|
||||
`pcb1_pour.BW/BH` 一改,它的栅格尺寸跟着变大、原点却还在 (0,0)
|
||||
⇒ **坐标整体偏 30 mm**。现象是「靠平面连通的网络(+12V / GND / +3V3 / VOn_P)
|
||||
忽然从 4 个碎片炸成 23 个」,而同一时刻 `pcb1_pour --verify` 回读的实测铺铜
|
||||
「+12V 一个连通域、58 个焊盘没着落 0」完全健康。
|
||||
|
||||
**代价是真的**:`pcb1_orphan_cu.py` 走的就是这套栅格,据此把 12 个过孔判成悬空删掉了
|
||||
(所幸那些碎片本来就不含焊盘 —— 那一半判据不依赖栅格 —— 所以电气上无损)。
|
||||
|
||||
⇒ 两条规矩:
|
||||
1. **改共享的几何常量之后,把 `import` 了它的模块全部跑一遍**,不能只验改动的那个;
|
||||
2. 「连通性突然大面积变差」和「实测回读健康」同时出现时,**先怀疑度量,再怀疑板子**。
|
||||
|
||||
|
||||
## 3c. `create()` 会把**元件**坐标也吸附到 5 的倍数(2026-09-08)⚠️⚠️
|
||||
|
||||
§3 说「元件坐标不受此限」——那是对**移动**(`setState_X/Y`)说的。
|
||||
**`sch_PrimitiveComponent.create()` 会吸附**:R1 页上 679 / 639 / 654 / 589 这些
|
||||
非 5 倍数的位置(都是当年 create 完再拖出来的),照搬到新工程里放下去会落在
|
||||
680 / 640 / 655 / 590,差 1~2 个单位。
|
||||
|
||||
后果:整页导线是按原坐标算的,差 1 就全断;而「按 (x,y) 精确匹配认领位号」的
|
||||
惯用写法会**一个都认不上**(本项目实测 42 件只认到 28 件,另外 14 件停在 `U?` / `C?`)。
|
||||
|
||||
⇒ 跨工程重放版图的固定写法:**就近认领(容差 3)→ 认完立刻 `setState_X/Y` 拉回精确坐标**。
|
||||
|
||||
## 6f. 跨工程重放:**27/42 件的 `create(rot)` 语义与源页相反**(2026-09-08)
|
||||
|
||||
§6b/§6c 说「方向表一张都别信」,本轮给出了量级:把 R1 一页 40 个件原样搬到另一个工程,
|
||||
**27 件的 rot=270 得到的引脚顺序与源页相反**。其中
|
||||
|
||||
- 25 件是两脚对称件(0402 R/C),只有引脚**编号**互换,坐标集相同 ⇒ 连线不受影响;
|
||||
- **2 件致命**:33nF(`C13594`)是 **+20 / −15 的不对称跨距**,翻过来两个脚的**坐标真的差 5**,
|
||||
照源页坐标画线会断在这两颗上。这正是 §8d「跨距同族也不统一」的后果被放大。
|
||||
|
||||
⇒ 做法:把源页的**引脚坐标连同脚号**一起 dump 下来当基准,放完逐脚比对,
|
||||
对不上的 `setState_Rotation(rot+180)` 再回读,循环到全对。
|
||||
|
||||
⚠️ **第一轮往往「看起来没生效」**:`create(270)` 与 `setState_Rotation(270)` 差 180°(§6b),
|
||||
所以要翻**两次**才落到源页的语义上。别以为脚本卡住了 —— 本项目日志里就是同一批 27 件
|
||||
连打两行「翻转 27 件重试」,第三轮才空。
|
||||
|
||||
### 6f-2. ✅ 那两圈空转可以直接省掉:**放完立刻用同一个角度再写一次**(2026-09-08)
|
||||
|
||||
受控实验(A2-AUX 空页,0402 电容,`create(..., 90)`):
|
||||
|
||||
| | pin1 | pin2 |
|
||||
|---|---|---|
|
||||
| `create(90)` 之后第一次读回 | (0, **+20**) | (0, −20) |
|
||||
| 紧接着 `setState_Rotation(90)`(**同一个角度**)再读 | (0, **−20**) | (0, +20) |
|
||||
|
||||
两者差 180°,就是 §6b 那条。**后者才是最终落盘的语义。**
|
||||
⇒ `place()` 里在 create 之后加一遍「按 placement 表把每件的 rotation 用同一个角度再写一次」,
|
||||
第一次读回就是真的:R2 底板 A1-INPUT 那 14 件 rot 90/270 的件,
|
||||
从「翻转 14 件重试 ×2 轮」变成**一轮直接通过**。已并入 `.claude/r2/schcheck.py` 的 `place()`。
|
||||
|
||||
> 这不只是省时间。**盲目翻转循环有可能替一个真放错的件「碰」出一个假的通过** ——
|
||||
> 循环最多转 4 轮,rot+180 / +360 / +540 里总有一个可能与错误的期望值凑上。
|
||||
> 先把语义刷正,再让逐脚断言去判,判据才是干净的。
|
||||
|
||||
## 3e. 读回来的旗/端口坐标带浮点尾巴(2026-09-08)
|
||||
|
||||
`rot=180` 的旗和端口,`getState_X()` 读回来是 `789.9999999999999` / `520.0000000000001`。
|
||||
直接拿去做「必须是 5 的倍数」「必须落在导线端点」两道自检 ⇒ **全部误报**。
|
||||
⇒ dump 出来先 `int(round(...))`。(导线的 `getState_Line()` 没有这个问题。)
|
||||
|
||||
顺带:**旗的符号名读回来是 `Ground-GND` / `Power-5V`**,而 `createNetFlag()` 的第一个参数
|
||||
只认 `Ground` / `Power` —— 重建时取 `name.split('-')[0]`。
|
||||
(`Power-5V` 里的 `5V` 只是符号默认网络名,正是 §3b 里「移动电源旗会掉回 `+5V`」的那个默认值。)
|
||||
|
||||
|
||||
# 附录 A:外部来源的四条 PCB 陷阱(2026-09-07 并入)
|
||||
|
||||
⚠️ **本节与全文其余部分性质不同**:以上每一条都是本项目亲手踩出来的,
|
||||
下面这四条来自 [`v0id-byte/easyeda-pro-claude-skill`](https://github.com/v0id-byte/easyeda-pro-claude-skill)
|
||||
(MIT)§10,是**别人**在一块密集四层板上的实测记录,**本项目尚未复现**。
|
||||
按「可信但待验证」对待:判据照做,但别把它当成本项目已确认的事实。
|
||||
|
||||
> 同一份文档里有一条**我们已经证伪**的说法:它称
|
||||
> `overwriteCurrentRuleConfiguration()` 是 BETA、调用会永久 hang、只能走 GUI 改规则。
|
||||
> 本项目 `.claude/pcb1_rules.py:315` 是跑通的 —— 失败原因在参数:
|
||||
> **要传 config 本体,不要传 `{name, config}` 包装**(见该文件头第 10 行)。
|
||||
> ⇒ 外部 skill 的结论要逐条对着本项目的实测过一遍,不要整段照抄。
|
||||
|
||||
## 9ad. `save()` 返回 true 也可能没落盘 —— 判据是回查图元数 ⚠️⚠️
|
||||
|
||||
文档上下文过期时,API `save()` **返回 `true` 但盘上文件仍是旧状态**。
|
||||
对方因此丢了一整版布线:界面上线都在,重开文件只剩铺铜。
|
||||
|
||||
**判据**:`save()` 的返回值不算数。落盘的证据是**重新读一次图元计数**——
|
||||
```python
|
||||
before = count_tracks() # 下发前
|
||||
... # 下发
|
||||
call('await eda.pcb_Document.save()')
|
||||
after = count_tracks() # 必须等于 before + 本次新增
|
||||
```
|
||||
本项目 30 多个脚本(`pcb1_place.py` / `pcb2_route_sig.py` / …)目前**都只看 `save()` 的返回值**,
|
||||
没有一个做回查。新写的下发脚本按上面这个形状补。
|
||||
|
||||
> 与 §9p 是同一类判据:「跑完了 ≠ 生效了,重开数一次才算」。
|
||||
> §9p 已经在自动布线上踩过,这条是它在 `save()` 上的同构版本。
|
||||
|
||||
## 9ae. 关掉再打开 PCB 标签页 → 桥接失去同步(API 读到 0 图元)⚠️⚠️
|
||||
|
||||
关闭 PCB 文档标签页再重新打开之后,API 会读回 `0 tracks` / 陈旧上下文,
|
||||
**而画面上明明有线**。此时任何「先读回再增量下发」的脚本都会做出错误决策
|
||||
(把有线的板子当空板重画)。
|
||||
|
||||
**恢复**:EDA 里 `API Gateway → 重新连接`。
|
||||
**判据**:脚本开头读回的图元数如果是 0,但你知道板子非空,**先怀疑桥接而不是怀疑板子**——
|
||||
这正是 §9v「只加不删的脚本必须自己记账」最怕的输入。
|
||||
|
||||
顺带:一个工程里常有多个 PCB 副本(`PCB1` / `PCB1_1`…),重开时确认打开的是对的那一个。
|
||||
|
||||
### 9ae-2. ⚠️⚠️⚠️ 失同步状态下**再 save() 一次,会把「空」写回文档**(2026-09-07,真丢了一块板)
|
||||
|
||||
上面那条说的是「读到 0 会让脚本做出错误决策」。**比这严重得多的是写。**
|
||||
|
||||
实际发生的一次(载板 PCB2,223 元件 / 2649 导线 / 896 过孔):
|
||||
|
||||
```
|
||||
pcb2_cfgsel.py --push # 建 6 线 2 孔 + save(),一切正常
|
||||
pcb2_reload.py # 删铺铜边框 → save() → location.reload(),报告「已重载」
|
||||
pcb2_pour.py --push # 建 7 块铺铜边框 → 填充一直 0/7 → 中途 save() ← 就是这一下
|
||||
```
|
||||
|
||||
第二步重载回来之后,**铜层已经读回全 0**(就是 §9ae 那个陈旧上下文),
|
||||
但当时的判据只有「windowId 变了 = 重载成功」,没人去数图元,于是直接往下走。
|
||||
第三步在那个状态里调了 `save()`,把客户端内存里那个「没有电气对象」的模型写回了文档。
|
||||
|
||||
**事后的样子非常有迷惑性**:完整重启客户端之后,
|
||||
|
||||
| 层 | 读数 |
|
||||
|---|---:|
|
||||
| 铜层(1/2/15/16):元件 / 导线 / 过孔 / 焊盘 / 铺铜 | **全 0** |
|
||||
| 板框层 11 | 1 条 ✅ |
|
||||
| 底层丝印 4 / 底层锡膏 8 / 元件外形 48 / 元件标识 49 | 1321 / 216 / 185 / 186 ✅ |
|
||||
| 挖槽 12 / 文档层 13 | 12 / 39 ✅ |
|
||||
|
||||
也就是说**丝印、锡膏、装配、板框这些非铜层图形一条不少,只有电气对象没了** ——
|
||||
第一眼很像「API 又失同步了」,其实是数据真没了。
|
||||
`pcb_PrimitiveComponent.get('U32')` 甚至还能返回一个对象,但它所有 `getState_*` 都是 `null`
|
||||
(**空壳,别把它当成「找到了」**)。同一工程里的另一块板(PCB1,301 件)读起来完全正常,
|
||||
所以「换块板读读看」是区分「客户端坏了」和「这块板坏了」最快的判据。
|
||||
|
||||
**规矩三条**
|
||||
|
||||
1. **「重载成功」的判据不是 `windowId` 变了,是重载后数一遍图元、数字不为零。**
|
||||
`pcb2_reload.py` 已经把这条做成硬断言(`assert_alive()`),不过关直接 `sys.exit`。
|
||||
2. **读回 0 的时候,绝对不许再 save()。** 先 `API Gateway → 重新连接`,
|
||||
不行就重启客户端,确认数字回来了再动。任何「建完东西顺手 save」的脚本
|
||||
在这个状态下都是破坏性的。
|
||||
3. 真丢了就走 **`文件 → 工程历史...`**(另有 `版本控制` / `回收站...`)恢复到写入之前,
|
||||
再把这一轮的下发按脚本重放一遍 —— 所以**每一步都要能用脚本重放**,
|
||||
这也是本项目坚持「改动写成脚本而不是手工操作」的理由之一。
|
||||
|
||||
⚠️ 顺带:这个状态下 `getCurrentRenderedAreaImage()` / `zoomToAllPrimitives()` 会**挂到超时**
|
||||
(不是 §9t 那种 88 字节空 PNG)。截图挂住 + 另一块板正常 = 这块板的编辑器实例有问题,别再往下写。
|
||||
|
||||
### 9ae-3. 结局与更正:**毁数据的是失同步后的 save,不是 `reload()` 本身**(2026-09-08)
|
||||
|
||||
上一节读起来容易得出「别再用 `location.reload()`」的结论。**那是错的**,同日的收尾证伪了它:
|
||||
|
||||
- 损坏的文档最终**连删都删不掉**(右键删除,服务端直接回 `delete failed !`),
|
||||
只能靠 `文件 → 工程历史...` 还原到事故前的版本把它移除,再新建一块 PCB 按快照重建。
|
||||
- ⚠️ 顺带更正:曾以为「工程历史点不开」,其实是**当时的活动文档正是卡死的那块板**。
|
||||
换个健康文档再点就开了。⇒ **某个 GUI 功能点不开时,先怀疑当前活动文档卡住。**
|
||||
- 在**新建的健康文档**上,`pcb2_reload.py`(内含 `location.reload()`)**完全正常**:
|
||||
重载后回查 216 元件 / 2644 线 / 896 孔 / 1000 焊盘全对。
|
||||
而且它是**唯一能清掉「卡在 0% 的后台任务」的办法** —— 不清掉,铺铜「重建所有」永远填 0 块。
|
||||
|
||||
⇒ 正确的规矩不是「不许重载」,而是:
|
||||
|
||||
1. **重载前先 `save()` 并回读验证**(图元数对得上再重载);
|
||||
2. **重载后必须数一遍图元**(`pcb2_reload.py:assert_alive()`),为 0 就停手、**绝不 save**;
|
||||
3. 读到 0 时先 `API Gateway → 重新连接`,不行就整个重启 EasyEDA;
|
||||
**如果重启之后某块板仍然读 0 而另一块正常,那就是这块板的文档坏了**,修不了,只能换。
|
||||
|
||||
### 9ae-4. 铺铜「重建所有」填 0 块的另一个成因:飞线还在算
|
||||
|
||||
§9k 说填 0 块 = 客户端卡在后台任务上,判据是底部那条 0% 进度条。
|
||||
2026-09-08 补一条**具体是什么任务**:刚导入几百个元件 + 两千段铜之后,
|
||||
`pcb_Document.getCalculatingRatlineStatus()` 返回 **`active`** —— 飞线在算。
|
||||
|
||||
⚠️ 但 `stopCalculatingRatline()` 停掉它(`active` → `inactive`)**并不能**让铺铜通,
|
||||
0% 进度条也不消失。**最后还是要重载。** ⇒ 这个 API 可以用来*确认*后台确实有任务在跑,
|
||||
但别指望它能解开死结。
|
||||
|
||||
## 9af. 布线质量的客观判据:**绕行比** = 走线长 ÷ 焊盘跨距
|
||||
|
||||
不用截图就能给走线打分:
|
||||
|
||||
- 每网络走线总长 `L = Σ hypot(endX-startX, endY-startY)`(`pcb_PrimitiveLine`,只数铜层)
|
||||
- 焊盘跨距 `S = max` 两两焊盘距离
|
||||
- **绕行比 = L / S**
|
||||
|
||||
判据(两脚网络):**1.0 ~ 1.5 = 直连;> 2 = 绕远路**。
|
||||
对方靠这一个数抓出一条 13 个过孔 / 2.2× 的 PWM 网络,全程没开截图。
|
||||
|
||||
⚠️ 长度本身没有判别力(跨距大的网络本来就长),**要判的是路径不是长度**。
|
||||
|
||||
⇒ 这条正好补上本项目 CLAUDE.md 里那句「几何验证证明不了看着对」的一部分:
|
||||
绕行比能把「连上了但绕了一圈」从人眼截图挪进脚本。建议加进 §10 验证流程的 PCB 侧。
|
||||
|
||||
## 9ag. 自动布线忽略 GND → DRC 连通性开路,而**飞线不显示** ⚠️⚠️
|
||||
|
||||
把 GND 加进自动布线的「忽略网络」(指望铺铜兜住),
|
||||
则**没有被 GND 铜实际盖住的 GND 焊盘会判为连通性开路**——对方那块板 62 处。
|
||||
|
||||
**最坑的是飞线数远小于 DRC 数**:GND 的飞线被部分抑制,看飞线会以为没事。
|
||||
**判据:信 DRC 的连通性计数,不信飞线。**
|
||||
|
||||
两条出路,二选一:
|
||||
1. 把 GND 也交给布线器(代价:顶/底层拥挤 + 几处过孔间距吃紧);
|
||||
2. 给每个开路的 GND 焊盘**打一个过孔下到内层 GND 平面**。
|
||||
|
||||
本项目走的是 2(`pcb2_strap_gnd.py` / `pcb2_vo_vias.py`),
|
||||
但**开路的判定来源**要按这条改:以 DRC 连通性计数为准,不要拿飞线数收工。
|
||||
|
||||
### 顺带:对方在 Freerouting 上的结论(本项目没走这条路,存档备查)
|
||||
DSN→jar→SES 往返**跑得通但结果不可制造**,两个死因:
|
||||
① 铺铜导出成 `plane 0`(= 没导出),布线器看不见铜,会把 GND/电源当普通线布;
|
||||
② **逐对象类型的间距矩阵在 DSN 里塌成单一 class 间距** —— track-via 6mil / track-copper 10mil 全丢,
|
||||
只剩 track-track。导入后 track-过孔间隙落在 0~4 mil,**改 DSN 间距重布结果一模一样**(属 SES 导入的产物)。
|
||||
⇒ 用 EasyEDA 自带的自动布线器(本项目 §9p 走的就是这条),别在 Freerouting 上耗时间。
|
||||
|
||||
在新工单中引用
屏蔽一个用户