原理图/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
505 行
25 KiB
Python
505 行
25 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""PhoneRail 的铺铜/平面层模型 —— 事实来源,改铜面改这里再 --push。
|
||
|
||
/c/Python314/python .claude/pcb_pour.py [--push] [--only L2|L3]
|
||
|
||
## 叠层(§10.1,layerStatus=1 的正好这四层)
|
||
|
||
| 设计文档 | EasyEDA 层 | id | 这里干什么 |
|
||
|---|---|---|---|
|
||
| L1 | Top Layer | 1 | 功率器件 + 信号,**D± 走这层,以 L2 为参考面** |
|
||
| L2 | Inner1-GND | 15 | **完整地平面,不许割断**(§10.1)|
|
||
| L3 | Inner2-PWR | 16 | 12V 母线 + 5V + 3V3 + 五路输出,**分割灌铜** |
|
||
| L4 | Bottom Layer | 2 | 控制信号 + ESP32 |
|
||
|
||
## 板框
|
||
|
||
160 × 125 mm,四角 R5(圆心正好落在四个 M3 定位孔的中心 (5,5)/(155,5)/(5,120)/(155,120))。
|
||
|
||
⚠️ **API 枚举不到那四段圆弧**:`pcb_PrimitiveArc.getAll()` 和 `getAllPrimitiveId()`
|
||
都返回 0,layer 11 上全工程只有 4 条直线(端点恰好是 R5 的切点)。
|
||
但渲染上角是实实在在的圆弧、闭合的。所以这里**不去读板框,按几何自己造多边形**。
|
||
(打样前记得用 Gerber 预览复核一次板框闭合。)
|
||
|
||
## 多边形不闭合就建不出来,而且 `create()` 成功了也会抛 ⚠️⚠️
|
||
|
||
两个坑叠在一起,实测(2026-09-03):
|
||
|
||
1. **`TPCB_PolygonSourceArray` 必须显式闭合** —— 末尾要把首点再写一遍。
|
||
文档写着「如果首尾不重合将会自动重合」,**对铺铜边框不成立**:
|
||
不闭合时 `pcb_PrimitivePour.create()` 直接不建,板上一块铜面都不多。
|
||
(`board_poly()` 一直能用,是因为它最后那段圆弧正好回到起点。)
|
||
2. **`create()` 抛异常不等于失败。** 闭合的多边形建得出来,
|
||
但调用照样抛 `错误:无法创建覆铜边框图元,可能是传入的参数不正确。`
|
||
—— 所以**返回值和 try/catch 都不能当判据**,只能建完 `getAll()` 按名字核对。
|
||
顺带一提:**铺铜名字会被 EasyEDA 转成大写**,比对时要 `toUpperCase()`。
|
||
|
||
一次分不清这两个的排查过程:不闭合的矩形(PA-OPEN)建完 `getAll()` 数量不变,
|
||
闭合的(PB-CLOSE)数量 +1 —— 两者都抛了一模一样的异常。
|
||
|
||
## 填充只能点菜单,`rebuildCopperRegion()` 是坏的 ⚠️
|
||
|
||
`pcb_PrimitivePour.create()` 建出来的只是**铺铜边框**(`nPour` 会 +1),
|
||
里头一片铜都没有,`pcb_PrimitivePoured.getAll()` 仍是 0,网络也不算连通。
|
||
|
||
`IPCB_PrimitivePour.rebuildCopperRegion()`(文档标 BETA)**必然抛**
|
||
`TypeError: Cannot read properties of undefined (reading 'length')`,约 157 ms。
|
||
四种写法都试过、都一样:`getAll()` 拿回来的对象、`toAsync()/done()` 异步式、
|
||
`createPolygon` 建的、`createComplexPolygon` 建的。
|
||
|
||
唯一能用的路子是 GUI:**工具 → 铺铜管理器... → 重建所有 → 确认**。
|
||
本文件的 `--push` 已经把这一串用 React 的 `onClick` 打进去了
|
||
(菜单项是 React 元素,用 `__reactProps$*.onClick`;`重建所有` 是原生 `<button>`,
|
||
两种都试,见 `drc_read.py` 的注释)。
|
||
|
||
**判据是 `nPoured`,不是 `nPour`。** 2026-09-03 实测:GND-L2 一块铜面重建耗时 < 2 s,
|
||
重建之后 DRC 的连接性错误从 1467 掉到 925。
|
||
|
||
## ⚠️ 本文件里的「优先级」那一列是**错的**,只是这块板上不要紧(2026-09-05 查清)
|
||
|
||
`plan()` 给 GND 1、给电源岛 5,隐含「数字大 = 优先」。**反了。**
|
||
铺铜管理器对话框自己写着「重建多个铺铜时,系统将按照优先级数字**从小到大**来执行铺铜」——
|
||
先铺的占地盘,所以 **GND 底填要给最大的数字**。
|
||
|
||
这块板上没出事,是因为这 19 块铜面**同层从不重叠**(`audit()` 的 MIN_GAP 就是在保这一条),
|
||
优先级压根没参与裁剪。**抄到载板去之前必须先把这一位翻过来**(载板 Inner1 是
|
||
分割电源面 + GND 底填,全靠优先级互相裁)。
|
||
|
||
顺带:`create()` 的第 7 个参数 `pourPriority` **不生效** —— 传进去的值被忽略,
|
||
实测这 14 块回读的是 1,2,3,4,4,5,5,6,7,8,9,10,11,12(基本是创建顺序)。
|
||
真要控制优先级只能建完再 `setState_PourPriority()` 逐块设并回读核对。见 skill §9j。
|
||
|
||
## ESP32 天线禁铺区
|
||
|
||
U20 在 (143, 46)、rot=270,本体框(rot=0)是 (-9.5, -9.35, 9.5, 16.75) ——
|
||
焊盘只在三面,**天线那一端(+Y)没有焊盘**,本体比焊盘框多伸 7.4 mm。
|
||
rot=270 之后天线朝 **+X**,落在 x≈152~160、y≈36.5~55.5。
|
||
模组 datasheet 要求**所有层**在天线下方都不铺铜,所以每一层的多边形都要挖这个缺口。
|
||
缺口开到右板边,不会把地平面割成两块(§10.1 那条「不许割断」仍然满足)。
|
||
"""
|
||
import io, json, os, sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
from eda import run
|
||
|
||
PCB = '7e9415a16a909c51'
|
||
MIL = 39.3701
|
||
|
||
L_TOP, L_GND, L_PWR, L_BOT = 1, 15, 16, 2
|
||
|
||
BOARD = (0.0, 0.0, 160.0, 125.0)
|
||
R_CORNER = 5.0
|
||
INSET = 0.5 # 铜面离板边
|
||
|
||
# 天线禁铺(含 1 mm 余量),x 一直开到板边
|
||
ANT = (151.0, 35.5, 160.0, 56.5)
|
||
|
||
|
||
def board_poly(inset=INSET, notch=ANT):
|
||
u"""板框内缩 inset 的圆角矩形,右边挖掉天线缺口。
|
||
|
||
返回 TPCB_PolygonSourceArray(mil):
|
||
x1 y1 'L' x2 y2 ... 'ARC' 角度 ex ey ...
|
||
ARC 的起点是「当前点」,角度正值逆时针(本工程 y 轴朝上)。
|
||
"""
|
||
x0, y0, x1, y1 = BOARD
|
||
d = inset
|
||
r = R_CORNER - d # 圆心不变,半径跟着缩
|
||
cx0, cy0, cx1, cy1 = x0 + R_CORNER, y0 + R_CORNER, x1 - R_CORNER, y1 - R_CORNER
|
||
ny0, ny1 = notch[1], notch[3]
|
||
nx = notch[0]
|
||
|
||
pts = [] # (mode, ...) 顺序即绘制顺序,逆时针
|
||
|
||
def L(x, y):
|
||
pts.append(('L', x, y))
|
||
|
||
def A(x, y):
|
||
pts.append(('ARC', 90.0, x, y))
|
||
|
||
start = (cx0, y0 + d) # 底边左端切点
|
||
L(cx1, y0 + d) # 底边 →
|
||
A(x1 - d, cy0) # 右下角圆弧
|
||
L(x1 - d, ny0) # 右边 ↑ 到缺口下沿
|
||
L(nx, ny0) # 缺口 ←
|
||
L(nx, ny1) # 缺口 ↑
|
||
L(x1 - d, ny1) # 缺口 →
|
||
L(x1 - d, cy1) # 右边 ↑
|
||
A(cx1, y1 - d) # 右上角
|
||
L(cx0, y1 - d) # 顶边 ←
|
||
A(x0 + d, cy1) # 左上角
|
||
L(x0 + d, cy0) # 左边 ↓
|
||
A(cx0, y0 + d) # 左下角,回到起点
|
||
|
||
src = [start[0] * MIL, start[1] * MIL]
|
||
mode = None
|
||
for p in pts:
|
||
if p[0] == 'L':
|
||
if mode != 'L':
|
||
src.append('L'); mode = 'L'
|
||
src += [p[1] * MIL, p[2] * MIL]
|
||
else:
|
||
src.append('ARC'); mode = 'ARC'
|
||
src += [p[1], p[2] * MIL, p[3] * MIL]
|
||
return src
|
||
|
||
|
||
|
||
def poly_from_pts(pts):
|
||
u"""任意直边多边形 -> TPCB_PolygonSourceArray(mil)。pts 是 [(x, y), ...] mm。
|
||
|
||
⚠️ **末尾必须把首点再写一遍**,见文件头「多边形不闭合就建不出来」。
|
||
"""
|
||
src = [pts[0][0] * MIL, pts[0][1] * MIL, 'L']
|
||
for x, y in list(pts[1:]) + [pts[0]]:
|
||
src += [x * MIL, y * MIL]
|
||
return src
|
||
|
||
|
||
def rect(x0, y0, x1, y1):
|
||
return poly_from_pts([(x0, y0), (x1, y0), (x1, y1), (x0, y1)])
|
||
|
||
|
||
def lshape(bx, by, tx, ty, ax, ay):
|
||
u"""主矩形 (bx,by)-(tx,ty) 左边再挂一块 (ax,ay)-(bx,ty) 的臂。"""
|
||
return poly_from_pts([(bx, by), (tx, by), (tx, ty), (ax, ty), (ax, ay), (bx, ay)])
|
||
|
||
|
||
# ── 要建的铜面 ─────────────────────────────────────────────────────────
|
||
# name, net, layer, 多边形, 优先级
|
||
#
|
||
# ══ L3(Inner2-PWR)══════════════════════════════════════════════════════
|
||
# 水平分层,和 L1 上器件的分带一一对应,过孔就都是最短的:
|
||
#
|
||
# y 110.0~116.2 VOn_P 五路输出到手机(在 **L4**,见下)
|
||
# y 82.5~ 99.5 VOn 电感 + 输出电容 + eFuse 输入侧(顺带给 eFuse 铺散热铜)
|
||
# y 67.5~ 81.9 +12V 五路 Buck 的输入母线(横贯整板)
|
||
# y 33.0~ 67.5 +12V band B 的支路 + 母线源头(x 38.5~95)
|
||
# y 49.0~ 66.5 $1N236 Q1 输出 → 体电容 → MLCC → Rs6 上游(x 14.4~35)
|
||
# y 49.5~ 56.5 $1N227 F1 → Q1 输入(x 14.6~19.0)
|
||
# y 45.5~ 55.5 $1N223 J1 → F1(x 3~14)
|
||
#
|
||
# ⚠️ 分界线的位置是**焊盘边**定的,不是整数好看:
|
||
# +12V 顶边 81.9 —— 再高就撞 C102 那一排 1210 的 +12V 焊盘(y 80.2~81.7);
|
||
# VOn 底边 82.3 —— C107 pin1 是 rot90 的 1210,焊盘竟然跨到 y 83.07~84.55,
|
||
# 比元件中心低 1.6 mm。按"元件在 y85.4"算会把它漏在两块铜面之间的缝里。
|
||
#
|
||
# ⚠️ **U8 / U13 三个 Kelvin 脚一律不进铜面**(MSOP-10,0.5 mm 间距)。
|
||
# U13.10 是 VIN+($1N236)、U13.9 是 VIN−(+12V),两脚相距 0.5 mm ——
|
||
# 任何一条分界线都塞不进去。它们走顶层细线,本来 Kelvin 就不该载流。
|
||
# 所以 $1N236 右边界停在 x35.0、+12V 左边界从 x38.5 起,中间 3.5 mm 全让给它们。
|
||
#
|
||
# ══ L4(Bottom)══════════════════════════════════════════════════════════
|
||
# 五路 VOn_P(Rs 下游 → J2)借用连接器带那条空走廊。这一层是 1 oz,
|
||
# 4 A 只要 1.34 mm,给了 6.2 mm 宽还带一条下伸的臂去接分流电阻。
|
||
# y 上限卡在 116.2 是被 (5,120)/(155,120) 那两个 M3 定位孔逼的:
|
||
# Φ6.0 焊盘 + 0.2 间距 = 铜面要离孔心 3.2 mm 以上,116.2 刚好剩 3.8 mm。
|
||
|
||
CH_X0, CH_PITCH = 1.0, 32.0 # 每路铜面的左边界与节距
|
||
CH_W_POUR = 30.0 # 铜面宽度(留 2 mm 路间隙)
|
||
|
||
|
||
def plan():
|
||
out = [
|
||
(u'GND-L2', 'GND', L_GND, board_poly(), 1),
|
||
# 8 A 输入链,三段各自一块(都很短,宽度按"横向电流的截面 = y 跨度"算)
|
||
(u'VIN-RAW-L3', '$1N223', L_PWR, rect(3.0, 45.5, 14.0, 55.5), 5),
|
||
(u'VIN-FUSED-L3', '$1N227', L_PWR, rect(14.6, 49.5, 19.0, 56.5), 5),
|
||
(u'VIN-SW-L3', '$1N236', L_PWR, lshape(19.5, 49.0, 35.0, 66.5, 14.4, 58.5), 5),
|
||
# +12V 母线:band A 横贯 + band B 一块(源头 Rs6 在 x40)
|
||
# 三段:下带 x14.6~95 y33~48.3(去接 U30 的 VIN,它在 x18.6~26.7)、
|
||
# 中带 x38.5~95 y48.3~67.5(源头 Rs6.2 在 x40.07)、顶带横贯全板 y67.5~81.9。
|
||
# 下带左边界 14.6 是被 $1N223 那块(x3~14)逼的,留 0.6 mm。
|
||
(u'12V-L3', '+12V', L_PWR,
|
||
poly_from_pts([(14.6, 33.0), (95.0, 33.0), (95.0, 67.5), (158.0, 67.5),
|
||
(158.0, 81.9), (1.0, 81.9), (1.0, 67.5), (38.5, 67.5),
|
||
(38.5, 48.3), (14.6, 48.3)]), 2),
|
||
# ── 三条低压轨(2026-09-04 加)─────────────────────────────────
|
||
# 加之前它们靠细线连,`+3V3` 8 条、`+3V3_HUB` 5 条自动布线器没布通。
|
||
# 都放 L3,塞进 12V 那几条带子中间的空地,互相与 12V 都留 >= 0.5 mm。
|
||
#
|
||
# +5V:U24~U28 五个 VBUS 负载开关排在 y11.5~12 那一行,最坏 2.8 A(§9.4)。
|
||
# 只做 y5.5~14 这一条横带;U30 输出那几个焊盘在 y41~45.5,
|
||
# 被 12V 下带(y33~48.3)占着,进不来,仍走顶层粗线下来接这条带。
|
||
(u'5V-L3', '+5V', L_PWR, rect(11.0, 5.5, 151.0, 14.0), 3),
|
||
# +3V3_HUB:USB2517 + U31 LDO 那一坨,x59~111 / y15~31 正好是它们的地盘。
|
||
# ⚠️ 与 +5V 在 y 上错开(14.0 / 15.0,净空 1 mm)—— 这一带两个网络的焊盘
|
||
# 是交错的(U31 的进/出脚挨着),靠 y 分层比靠 x 分区干净。
|
||
(u'3V3HUB-L3', '+3V3_HUB', L_PWR, rect(59.0, 15.0, 111.0, 31.0), 3),
|
||
# +3V3 分两块(同一个网络两块铜面,靠已有走线连起来):
|
||
# 控制区 —— 12V 下带/中带的右边(x>95)那块空地,ESP32 / PCA9555 / 595 / DAC 都在这里。
|
||
# ⚠️ 右边界 150.5 是被 ESP32 天线禁铺区(x>=151)逼的,不能再往右。
|
||
(u'3V3-CTRL-L3', '+3V3', L_PWR, rect(95.5, 33.5, 150.5, 67.0), 3),
|
||
# 通道区 —— VOn 铜面(y82.5~99.5)之上那一片,六片 INA226 的电源脚都在 y100~105。
|
||
(u'3V3-CH-L3', '+3V3', L_PWR, rect(5.0, 100.3, 155.0, 124.0), 3),
|
||
]
|
||
for n in range(1, 6):
|
||
x0 = CH_X0 + (n - 1) * CH_PITCH
|
||
x1 = x0 + CH_W_POUR
|
||
out.append((u'VO%d-L3' % n, 'VO%d' % n, L_PWR, rect(x0, 82.5, x1, 99.5), 4))
|
||
# VOn_P 在 L4:连接器带 + 一条下伸到分流电阻上游焊盘的臂
|
||
out.append((u'VO%d_P-L4' % n, 'VO%d_P' % n, L_BOT,
|
||
poly_from_pts([(x0, 104.5), (x0 + 3.5, 104.5), (x0 + 3.5, 110.0),
|
||
(x1, 110.0), (x1, 116.2), (x0, 116.2)]), 4))
|
||
return out
|
||
|
||
|
||
|
||
# ── 本地检查(不连 EasyEDA)───────────────────────────────────────────────
|
||
def _poly_pts(src):
|
||
u"""把 TPCB_PolygonSourceArray 还原成 mm 的顶点表。只处理直边(本文件的 L3/L4 都是)。"""
|
||
pts, i = [], 0
|
||
while i < len(src):
|
||
v = src[i]
|
||
if v == 'L':
|
||
i += 1; continue
|
||
if v == 'ARC':
|
||
i += 4; continue # 板框那块有弧,这里只用来算包围盒
|
||
pts.append((src[i] / MIL, src[i + 1] / MIL)); i += 2
|
||
return pts
|
||
|
||
|
||
def _inside(px, py, pts):
|
||
u"""射线法。边界上算在里面(本文件的分界线都留了 >= 0.4 mm 净空,不会踩边界)。"""
|
||
n, inside = len(pts), False
|
||
j = n - 1
|
||
for i in range(n):
|
||
xi, yi = pts[i]; xj, yj = pts[j]
|
||
if (yi > py) != (yj > py) and px < (xj - xi) * (py - yi) / (yj - yi + 1e-30) + xi:
|
||
inside = not inside
|
||
j = i
|
||
return inside
|
||
|
||
|
||
def _bbox(pts):
|
||
xs = [p[0] for p in pts]; ys = [p[1] for p in pts]
|
||
return min(xs), min(ys), max(xs), max(ys)
|
||
|
||
|
||
|
||
MIN_GAP = 0.4 # 同层两块铜面之间的最小净空(安全间距 0.2 的两倍,留余量)
|
||
|
||
|
||
def _seg_dist(p, q, r, t):
|
||
u"""两条线段的最短距离。相交返回 0。"""
|
||
def d_pt_seg(px, py, ax, ay, bx, by):
|
||
dx, dy = bx - ax, by - ay
|
||
L2 = dx * dx + dy * dy
|
||
s = 0.0 if L2 == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / L2))
|
||
return ((px - ax - s * dx) ** 2 + (py - ay - s * dy) ** 2) ** 0.5
|
||
|
||
def cross(o, a, b):
|
||
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
|
||
d1, d2 = cross(r, t, p), cross(r, t, q)
|
||
d3, d4 = cross(p, q, r), cross(p, q, t)
|
||
if ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0)):
|
||
return 0.0
|
||
return min(d_pt_seg(p[0], p[1], r[0], r[1], t[0], t[1]),
|
||
d_pt_seg(q[0], q[1], r[0], r[1], t[0], t[1]),
|
||
d_pt_seg(r[0], r[1], p[0], p[1], q[0], q[1]),
|
||
d_pt_seg(t[0], t[1], p[0], p[1], q[0], q[1]))
|
||
|
||
|
||
def _poly_gap(A, B):
|
||
u"""两个简单多边形的最近距离;一个包含另一个或相交时返回 0。"""
|
||
if any(_inside(x, y, B) for x, y in A) or any(_inside(x, y, A) for x, y in B):
|
||
return 0.0
|
||
best = 1e9
|
||
for i in range(len(A)):
|
||
for j in range(len(B)):
|
||
best = min(best, _seg_dist(A[i], A[(i + 1) % len(A)],
|
||
B[j], B[(j + 1) % len(B)]))
|
||
return best
|
||
|
||
|
||
# 故意不进铜面的焊盘(后缀匹配位号)。漏在这张表以外的都要查。
|
||
# U8/U13 的 0.5 mm 间距 Kelvin 脚 —— 分界线塞不进去,本来也不载流
|
||
# DZ1/C1 —— Q1 的栅极钳位,接的是 $1N227 但只有 µA 级
|
||
# R%01 —— VOn 的反馈分压抽头,落在 +12V 顶带里,走顶层细线
|
||
# R%19 —— 输出泄放 2512,只有放电脉冲电流
|
||
# 这两条轨的铜面**故意只盖一部分**:它们的焊盘散布全板,铜面只占 12V 带子之间的空地,
|
||
# 其余焊盘靠走线接过来。所以别把「漏掉」当错报,只报个数。
|
||
POUR_PARTIAL = ('+5V', '+3V3')
|
||
|
||
EXPECT_OUT = ('U8.', 'U9.', 'U10.', 'U11.', 'U12.', 'U13.', 'DZ1.', 'C1.1', 'R101.', 'R201.', 'R301.', 'R401.', 'R501.',
|
||
'R119.', 'R219.', 'R319.', 'R419.', 'R519.')
|
||
|
||
|
||
def audit():
|
||
u"""铜面之间不许挨太近;每个网络的焊盘要么在自己的铜面里,要么得记在案。"""
|
||
import collections
|
||
dump = os.path.join(HERE, 'pcb-copper.json')
|
||
pads = json.load(io.open(dump, encoding='utf-8'))['pads'] if os.path.exists(dump) else []
|
||
by_net = collections.defaultdict(list)
|
||
for q in pads:
|
||
by_net[q['net']].append(q)
|
||
|
||
ps = [(n, net, l, _poly_pts(src), pr) for n, net, l, src, pr in plan()]
|
||
bad = 0
|
||
|
||
# 1) 同层铜面不许挨得太近。
|
||
# ⚠️ 不能用包围盒 —— 12V-L3 是个横跨全板的 L 形,它的包围盒把 band B 全套进去,
|
||
# 四块输入链铜面全被误报成"重叠 -32 mm"。必须逐边算真距离。
|
||
for i in range(len(ps)):
|
||
for j in range(i + 1, len(ps)):
|
||
a, b = ps[i], ps[j]
|
||
if a[2] != b[2]:
|
||
continue
|
||
d = _poly_gap(a[3], b[3])
|
||
if d < MIN_GAP:
|
||
print(u' !! %s 与 %s 同在 layer %d,最近处只有 %.2f mm(要求 >= %.1f)'
|
||
% (a[0], b[0], a[2], d, MIN_GAP))
|
||
bad += 1
|
||
|
||
# 2) 每块铜面盖住了本网络的几个焊盘 / 漏了哪些
|
||
print(u'')
|
||
print(u'%-14s %-9s %s' % (u'铜面', u'网络', u'本网焊盘 在内/总数(漏掉的要走顶层线)'))
|
||
# ⚠️ 一个网络可能有**多块**铜面(+3V3 就是两块)。判「焊盘在不在铜面里」
|
||
# 必须拿同网络所有铜面做并集,否则每一块都会把另一块盖住的焊盘报成意外漏掉。
|
||
same_net = {}
|
||
for _n, _net, _l, _pts, _p in ps:
|
||
same_net.setdefault(_net, []).append(_pts)
|
||
for name, net, layer, pts, prio in ps:
|
||
if net == 'GND':
|
||
continue
|
||
mine = by_net.get(net, [])
|
||
ins = [q for q in mine
|
||
if any(_inside(q['x'], q['y'], t) for t in same_net[net])]
|
||
out = [q for q in mine if q not in ins]
|
||
tags = ['%s.%s' % (q['des'] or '?', q['num']) for q in out]
|
||
odd = ([] if net in POUR_PARTIAL else
|
||
[t for t in tags if not t.startswith(EXPECT_OUT) and not t.startswith('?.')])
|
||
note = u''
|
||
if tags:
|
||
note = (u' %d 个在铜面外,走线接(本网铜面只盖一部分)' % len(tags)
|
||
if net in POUR_PARTIAL else u' 漏 %d(预期之内)' % len(tags))
|
||
if odd:
|
||
note = u' !! 意外漏掉: ' + ' '.join(odd[:8])
|
||
bad += 1
|
||
print(u'%-14s %-9s %d / %d%s' % (name, net, len(ins), len(mine), note))
|
||
if not mine:
|
||
# ⚠️ 多半是 $1N223 这类**自动命名**的网络改了号(原理图一重新生成网表就会变)。
|
||
# pcb_fanout.py 的 TARGET 里也有同样三个名字,要一起改。
|
||
print(u' !! 板上根本没有这个网络 —— 自动命名的网络改号了?')
|
||
bad += 1
|
||
elif not ins:
|
||
print(u' !! 一个都没盖住 —— 网络名或坐标错了')
|
||
bad += 1
|
||
print(u'')
|
||
print(u'铜面 %d 块,问题 %d 条' % (len(ps), bad))
|
||
return bad
|
||
|
||
|
||
OPEN = u"""
|
||
await eda.dmt_EditorControl.openDocument('%s');
|
||
await new Promise(r => setTimeout(r, 1200));
|
||
""" % PCB
|
||
|
||
# ⚠️ 桥接单次请求 30 s 就超时(skill §9c)。15 块铜面 + 重建全部远超这个数,
|
||
# 所以拆成四步,每一步都短:建(分批)→ 点重建 → 轮询 → 确认+存盘。
|
||
JS_DEL = OPEN + u"""
|
||
let n = 0;
|
||
for (const old of await eda.pcb_PrimitivePour.getAll()) { await eda.pcb_PrimitivePour.delete(old.primitiveId); n++; }
|
||
return {deleted: n};
|
||
"""
|
||
|
||
JS_MAKE = OPEN + u"""
|
||
const PLAN = %s;
|
||
for (const p of PLAN) {
|
||
const poly = await eda.pcb_MathPolygon.createComplexPolygon([p.src]);
|
||
if (!poly) continue;
|
||
// ⚠️ create() **成功了也会抛**(见文件头),返回值和 try/catch 都不能当判据
|
||
try { await eda.pcb_PrimitivePour.create(p.net, p.layer, poly, 'solid', false, p.name, p.prio, 10); }
|
||
catch (e) {}
|
||
}
|
||
const made = (await eda.pcb_PrimitivePour.getAll()).map(x => String(x.pourName || '').toUpperCase());
|
||
return {made};
|
||
"""
|
||
|
||
# 工具 → 铺铜管理器... → 重建所有(React 元素用 __reactProps$*.onClick,按钮用原生 click)
|
||
JS_REBUILD = OPEN + 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 q=propsOf(t);
|
||
if(q&&typeof q.onClick==='function'){q.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; };
|
||
const leaf = (txt) => { for (const el of document.querySelectorAll('*'))
|
||
if (el.children.length===0 && (el.textContent||'').trim()===txt) return el; return null; };
|
||
const btn = (txt) => { for (const b of document.querySelectorAll('button'))
|
||
if ((b.innerText||'').trim()===txt && b.offsetParent!==null) return b; return null; };
|
||
if (!fire(leaf('工具'))) return {err: 'no 工具'};
|
||
await sleep(900);
|
||
if (!fire(leaf('铺铜管理器...'))) return {err: 'no 铺铜管理器'};
|
||
await sleep(2200);
|
||
const b = btn('重建所有');
|
||
if (!b) return {err: 'no 重建所有'};
|
||
return {fired: fire(b)};
|
||
"""
|
||
|
||
JS_POLL = u"return {poured: (await eda.pcb_PrimitivePoured.getAll()).length};"
|
||
|
||
JS_DONE = u"""
|
||
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 q=propsOf(t);
|
||
if(q&&typeof q.onClick==='function'){q.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; };
|
||
let ok = null;
|
||
for (const b of document.querySelectorAll('button'))
|
||
if ((b.innerText||'').trim()==='确认' && b.offsetParent!==null) { ok = b; break; }
|
||
if (ok) fire(ok);
|
||
await new Promise(r => setTimeout(r, 1200));
|
||
await eda.pcb_Document.save();
|
||
return {poured: (await eda.pcb_PrimitivePoured.getAll()).length,
|
||
pour: (await eda.pcb_PrimitivePour.getAll()).length};
|
||
"""
|
||
|
||
|
||
def main():
|
||
ps = plan()
|
||
print(u'计划建 %d 块铜面:' % len(ps))
|
||
for name, net, layer, src, prio in ps:
|
||
n_arc = sum(1 for v in src if v == 'ARC')
|
||
n_pt = sum(1 for v in src if isinstance(v, float)) // 2
|
||
print(u' %-10s net=%-6s layer=%-3d 多边形 %d 点 / %d 段弧'
|
||
% (name, net, layer, n_pt, n_arc))
|
||
audit()
|
||
if '--push' not in sys.argv and '--rebuild' not in sys.argv:
|
||
print(u'(未加 --push,没有下发)')
|
||
return
|
||
def call(js, timeout=60):
|
||
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']
|
||
|
||
print(u'')
|
||
if '--rebuild' in sys.argv:
|
||
# 铜面本身没变,只是扇出改了 -> 只重填,别重建边框
|
||
made = [n.upper() for n, _, _, _, _ in ps]
|
||
else:
|
||
print(u'清掉旧铜面:', call(JS_DEL)['deleted'], u'块')
|
||
for k in ([] if '--rebuild' in sys.argv else range(0, len(ps), 5)): # 5 块一批,稳在 30 s 以内
|
||
batch = [{'name': n, 'net': net, 'layer': l, 'src': src, 'prio': pr}
|
||
for n, net, l, src, pr in ps[k:k + 5]]
|
||
made = call(JS_MAKE % json.dumps(batch, ensure_ascii=False))['made']
|
||
print(u' 已建 %d / %d' % (len(made), len(ps)))
|
||
miss = [n for n, _, _, _, _ in ps if n.upper() not in made]
|
||
if miss:
|
||
print(u'!! 这几块没建出来:', ' '.join(miss)); sys.exit(1)
|
||
|
||
r = call(JS_REBUILD)
|
||
if r.get('err'):
|
||
print(u'!! 打不开铺铜管理器:', r['err']); sys.exit(1)
|
||
import time
|
||
for i in range(40): # 最多等 ~200 s
|
||
time.sleep(5)
|
||
n = call(JS_POLL, timeout=30)['poured']
|
||
print(u' 填充 %d / %d' % (n, len(ps)))
|
||
if n >= len(ps):
|
||
break
|
||
d = call(JS_DONE)
|
||
print(u'完成:铜面 %d 块,已填充 %d 块' % (d['pour'], d['poured']))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|