文件
phonerail/.claude/r2/_pcb_base_m8.py.bak
zhou yang和Claude Opus 5 efcba30a0c 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
2026-09-10 21:02:10 +08:00

479 行
24 KiB
Plaintext

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""R2 底板 PCB(M8):板框 + 分区 + 插槽阵列 + 安装孔。
/c/Python314/python .claude/r2/pcb_base.py # 只算 + 自检
/c/Python314/python .claude/r2/pcb_base.py --outline # 画/重画板框(幂等)
/c/Python314/python .claude/r2/pcb_base.py --holes # 打安装孔
/c/Python314/python .claude/r2/pcb_base.py --verify # 读回校验
与 `pcb_blade.py` 是同一套接口(`PCB` / `BW` / `BH` / `ZONES` / `mill_cuts()` …),
下游的 `pcb_place` / `router` / `pcb_pour` 用 `--base` 切到这一份。
## 坐标系
**板左下角 = (0, 0),x 向右、y 向上**(EasyEDA PCB 里 y 向上为正,方案 §22.5)。
x = 槽距方向(5 槽并排),y = 槽长方向 / 板深。方案 §6.5 那张分区图可以直接抄。
## ⛔ 板框必须是**折线**不是导线
`pcb_PrimitiveLine` 在嘉立创里是**导线(铜)**。M4b~M6 的刀片板框就是这么画的:
渲染成品红、DRC 干净、四道几何校验全过,而客户端根本不认它是板框
(自动布线一路弹「请先绘制板框!」)。见方案 §27.3。
⇒ 板框只用 `pcb_PrimitivePolyline`。底板文档里现成那 4 条 `Line`(建工程时的默认框)
属于同一个坑,`push_outline()` 会把它们删掉。
## 刀片投影是硬碰撞,不是净距问题
插槽把刀片底边抬到底板上方 ~11 mm ⇒ **投影带内元件 ≤ 8 mm**(方案 §6.6)。
两颗 470 µF 直立电解、DC-005 座、TH 排针 / 风扇座都只能放到投影带**外面**
(`y < PROJ[0]` 或 `y > PROJ[1]`)。`zone_of()` / `height_ok()` 就是干这个的。
"""
import io
import json
import math
import os
import sys
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
# ⚠ 顺序不能反:`.claude` 下有同名的 R1 脚本,先插 .claude 会 import 到 R1 那一份且不报错。
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
from eda import run # noqa: E402
import fp_edge # noqa: E402
PCB = '7b9524b6aaf6f235' # BASE-PCB(r2-uuids.json)
MIL = fp_edge.MIL
# ── 板框(mm,出处 方案 §3.1)─────────────────────────────────────────
BW, BH = 110.0, 125.0
CORNER_R = 5.0 # 底板四角 R5(刀片是 R2)
CORNER_SEG = 10
OUTLINE_W = 0.15
# ── 插槽阵列(方案 §6.3;槽距 20.32 = T1 定稿)──────────────────────────
SLOT_PITCH = 20.32
SLOT_N = 5
SLOT_CX = BW / 2.0 # 阵列中心 = 板中心 = Hub 的 x
SLOT_CY = 64.5 # 槽体中心 y ⇒ 槽体 45.0~84.0(39 mm)
# ⚠ 39 是方案 §3.1 抄的「槽体长度」,**不是封装占的地方**。`PCIE-64P11L` 的焊盘外框
# 实测 **42.0 × 9.2**(含两端 3.0×5.0 的屏蔽脚 65),转 90° 之后沿 y 占 42、沿 x 占 9.2。
# 分区就按实测走,别按 39 —— 差的那 3 mm 正好会让槽体啃进 hub / ctrl 两带。
SLOT_BODY_L = 42.0 # 槽体沿 y(= 转 90° 后的封装长边,实测)
SLOT_BODY_W = 9.2 # 槽体沿 x(实测)
# 焊盘两列:中心 x = 槽心 ± 3.6,盘 2.0(x) × 0.7(y),即 x 占 ±2.6~±4.6。
# ⇒ 相邻槽之间的**布线走廊净宽 = 20.32 − 9.2 = 11.12**,两端各剩 9.76。
PAD_COL_DX = 3.6
PAD_L, PAD_W = 2.0, 0.7
# 刀片投影:刀片 98 宽沿 y 铺开,中心对齐槽体中心
BLADE_W = 98.0
PROJ = (round(SLOT_CY - BLADE_W / 2, 2), round(SLOT_CY + BLADE_W / 2, 2)) # 15.5 ~ 113.5
H_PROJ = 8.0 # 投影带内元件高度上限(方案 §6.6,已留 3 mm 余量)
# 刀片 x(刀片自身坐标)→ 底板 y 的映射:blade_x = 0 落在投影带下沿。
# ⇒ 刀片的**功率列**(blade x 72~98,最吵最热)落在 y 87.5~113.5,正对风墙;
# 手机主板(blade x 3~70)落在 y 18.5~85.6。
def blade_x_to_base_y(bx):
return round(PROJ[0] + bx, 3)
def slot_x(i):
u"""槽 i(1~5)的中心 x。"""
return round(SLOT_CX + (i - (SLOT_N + 1) / 2.0) * SLOT_PITCH, 3)
def slot_xs():
return [slot_x(i) for i in range(1, SLOT_N + 1)]
# ── 分区(方案 §6.5)──────────────────────────────────────────────────
# ⚠ 'slots' 带内除了 J34~J38 什么都不放:那 39×110 是 D± 走廊 + 槽体本身。
ZONES = {
'input': (0.0, 0.0, BW, 15.5), # 投影带外:DC 座 / F1 / Q1 / C2,C3 直立电解 / 泄放
'hub': (0.0, 15.5, BW, 43.5), # U32 居中 + Y1 + U31 + 5 个 D± 退路座
'slots': (0.0, 43.5, BW, 85.5), # 5 个插槽(42 mm)+ 走廊(L2 一刀不许割)
'ctrl': (0.0, 85.5, BW, 113.5), # ESP32 / mux / DAC / 3V3 / 5V(全部 ≤8 mm)
'fan': (0.0, 113.5, BW, BH), # 投影带外:风扇座 / J33 应急口(可高)
}
# ── 12V 母线(9 A):**梳子形**,一条脊 + 五根齿 ────────────────────────
# ⛔ **不能靠布线器的 1.5 mm 线送 9 A**。布线器把 `+12V` 当一个网布成一棵树,
# 树干那一段要背 ~7.5 A 而只有 1.5 mm(1 oz 外层 ΔT20K 只带 3.2 A),
# DRC / 连通性 / 几何四道判据一条都不会响。⇒ 母线做成铺铜,布线器的线与它并联。
#
# ⛔ **为什么不是「槽阵南沿一条横杠」**(第一版就是这么画的):
# 横杠沿 x 送电,而五对 D± 要从 U32 往北**竖着穿过它** —— 两层板上一根竖线
# 就把横杠**整个切断**,五处切口把它剁成五段,每段还得各自找电。
# 横杠上的「铜宽够不够」根本不是问题所在,问题是它压根不连通,
# **而铺铜管理器、DRC、断面校核三个都只会告诉你「有铜」。**
#
# ⇒ 改成梳子:
# · `SPINE` 横脊走**输入带最南那 5 mm**(y 0.8~5.8),从 `BUS_IN` 一路往西
# 钻到 `J1` 的桶身底下。那一条上只有 `J1` 的三只脚(全在 y≥8.2)和板边,
# 南北穿过它的东西一个都没有 —— 它南边就是板边。
# ⛔ **横脊原来放在 hub 带(y 16.2~24.2),那是错的**:hub 带一共只有 28 mm,
# 横脊一占,`U32` 北边的 D± 扇出带就只剩 8.6 mm,而
# **10 根 1.2 mm 的线要从 0.5 mm 间距的 QFN 脚上散开到 1.41 mm 间距,
# 光斜着散开就要 4.1 mm**,再加三条车道 8.1 —— 塞不下。
# 实测现象:内侧那三对(`USB2`/`USB3`/`USB4`)一段都布不出来。
# 挪到输入带之后 hub 带整条 28 mm 归 `U32`,扇出带 14.6 mm,宽裕。
# · 五根**支路**从脊往北接各槽的 +12V 针(y 48~50)。它们只有 **1.5 A**,
# 交给布线器画 1.5 mm 的线就够(1 oz 外层 ΔT20K 带 3.2 A),不必铺铜。
#
# ⛔ **齿曾经也是铺铜,画完才发现走不了**:槽的第 65 脚(屏蔽脚,3.0×5.0,接 GND)
# 正好坐在两列焊盘之间那条 5.2 mm 无盘带的**正中**(转 90° 后 y 43.5~46.5),
# 把齿从中间堵死 —— 断面校核量出来是 **0.00 mm**。
# 这是「按封装的焊盘外框想当然」的典型:外框里看着是空的,实际有一只大脚。
#
# ⚠ `BUS_IN` 西端必须**盖住 `Rs6` 的 +12V 焊盘**(x 63.2~65.2):
# 不盖住的话,9 A 要靠布线器那根 1.5 mm 的线从分流电阻爬到母线上。
# ⛔ 脊只能 **5 mm 高**,不是最初画的 8 —— hub 带一共只有 28 mm,要塞下
# 「脊 + U32(9.75) + 南边逃逸 + 北边 D± 扇出」。第一版 8 mm 的脊把 U32 逼到 y=37,
# 它北沿离插槽焊盘只剩 **1.6 mm**,五对 D± 根本扇不开(布线器实测 USB2 布了 0 段)。
# 载流上 5 mm 也够:脊上最大电流不是 9 A 而是 ~6 A(进料点在东段 x63~91,
# 往西只带 3 个槽 + 两颗 Buck),1 oz 外层 6 A ΔT20K 要 5.2 mm。
SPINE = (8.0, 0.8, 91.0, 5.8) # 西端 8.0:让开 H1 螺钉头(0.8~7.2)
BUS_IN = (63.0, 0.8, 91.0, 16.0) # 输入带里的 9 A 母线(15.2 mm 高)
def bus_rects():
return [BUS_IN, SPINE]
# ── D± 扇出带:`U32` 北沿到槽体南沿之间那条,**一件都不许放** ─────────────
# 五对 D± 全部在这里分东西两路(USB1/USB2 往东、USB4/USB5 往西、USB3 直上)。
# 带子 = `U32` 北沿(28.9) 到槽体南沿(43.5) = **14.6 mm**:
# 先要 4.1 mm 让 10 根线从 0.5 mm 的脚距斜着散到 1.41 mm 的线距,再要三条 2.7 的车道。
# ⛔ 不设这条禁区的实测后果:排布器把 `U32` 的去耦和退路座填满整条带,
# 布线器报「`USB2` 布了 0 段」,而摆位自检 / 朝向断言 / DRC **一条都不响**。
FANOUT = (2.0, 28.9, 108.0, 43.5)
# ── ESP32 天线缺口:西板边挖掉一块,天线整段悬空 ─────────────────────────
# 2026-09-09 用户拍板。**原来的做法是错的**:`U20` 摆在板中央 (17, 96, rot0)、
# 天线朝北埋在板里,净空只有 20 × 7.5 一小块 ⇒ 天线尖**正前方 1.0 mm 就是
# 重新铺起来的整块地平面**(再往北 3 mm 是风扇带的 RT601/RT602),
# 东西两侧净空也只比模组宽 0.5 mm。等于给天线加了块反射板。
# 而设计文档 §PCB 要点写的是「天线区域下方禁止铺铜、禁止走线,**模组放板边**」——
# 后半条从来没做到,前半条也只做了「下方那一小块」。
# ⇒ `U20` 转 **rot=90(天线朝西)**挪到西板边,天线那 7 mm 下方的**板材整块挖掉**。
#
# 尺寸依据(全部从 `_base_pads.json` 量出来,不是抄封装库):
# 模组本体 18.0 × 25.5;焊盘外框 19.0 × 19.0(三面出脚,每边比本体宽 0.5)
# ⇒ 天线段 = 25.5 − (19.0 − 0.5×2) = **7.5 mm**(本体 7.0 + 0.5 余量)。
# 缺口比模组两侧各宽 1.0 mm(y 85~107),深 7.5;`U20` 焊盘西沿 8.5,
# 离缺口边 1.0 mm ⇒ 焊盘还在板上、天线整段在板外,只有天线根部 1 mm 压着无铜的 FR4。
# ⚠ 内角是铣刀铣出来的,实际会带 Ø2 铣刀的圆角,不必在板框里画圆角。
ANT_NOTCH = (0.0, 85.0, 7.5, 107.0)
def notch_rect():
u"""天线缺口(板材挖掉的那一块)。板框在这里内凹,不走 `mill_cuts()`。"""
return ANT_NOTCH
def keepout_rects():
u"""摆位禁区(不是铺铜禁区):12V 母线 + D± 扇出带 + 天线缺口(那儿没板材)。"""
return bus_rects() + [FANOUT, ANT_NOTCH]
# ── 安装孔(M3 尼龙立柱,NPTH)────────────────────────────────────────
# **2026-09-09 用户拍板:只留四角 4 个**(原来还在东西两侧中点加了 H5/H6 当插拔力的支点,
# 用户决定省掉两颗立柱)。⇒ 5 片刀片一起插拔时板中部无支撑,装配时**从槽阵下方托住再插**。
HOLE_D = 3.2 # M3 过孔
HOLE_KEEP = 3.2 # 螺钉头 Ø6.0 /2 + 余量
HOLE_INSET = 4.0 # 孔心离板边
MOUNT_HOLES = {
'H1': (HOLE_INSET, HOLE_INSET),
'H2': (BW - HOLE_INSET, HOLE_INSET),
'H3': (HOLE_INSET, BH - HOLE_INSET),
'H4': (BW - HOLE_INSET, BH - HOLE_INSET),
}
# ── 下游脚本要的空壳(底板没有排线槽 / 手机定位孔 / 金手指缺口)──────────
FLEX_W = {}
SLOT_Y = None
PHONE_ORG = None
PHONE_WH = None
def phone_holes():
u"""下游(`router` / `pcb_place`)拿这个当「两层都不许压的孔」表。
刀片上是手机主板定位孔;底板上没有手机,但四个 M3 安装孔一样不许压 ——
它们是 `pcb_PrimitivePad` 不是元件引脚,**不在 `_base_pads.json` 里**,
不从这儿喂进去布线器就会直接从孔上压过去(而 DRC 只报 NPTH 到铜,很晚才看见)。
"""
return dict(MOUNT_HOLES)
def mill_cuts():
u"""板上铣穿的内槽:底板一个都没有。"""
return []
def notch_break():
return None
def corner(cx, cy, a0, a1):
pts = []
for k in range(CORNER_SEG + 1):
a = math.radians(a0 + (a1 - a0) * k / CORNER_SEG)
pts.append((cx + CORNER_R * math.cos(a), cy + CORNER_R * math.sin(a)))
return pts
def outline_pts():
u"""板框点列(逆时针,从底边左端 (r,0) 起,闭合回 (r,0))。
⚠ 相邻重合点必须去掉:零长度线段会被编辑器静默吃掉,读回条数对不上(刀片踩过)。
"""
r = CORNER_R
pts = [(r, 0.0), (BW - r, 0.0)]
pts += corner(BW - r, r, 270, 360)
pts += [(BW, BH - r)]
pts += corner(BW - r, BH - r, 0, 90)
pts += [(r, BH)]
pts += corner(r, BH - r, 90, 180)
# ESP32 天线缺口:西边从北往南走,走到 y=107 拐进板里 7.5,到 y=85 再拐回来
pts += [(0.0, ANT_NOTCH[3]), (ANT_NOTCH[2], ANT_NOTCH[3]),
(ANT_NOTCH[2], ANT_NOTCH[1]), (0.0, ANT_NOTCH[1])]
pts += [(0.0, r)]
pts += corner(r, r, 180, 270)
out = [pts[0]]
for q in pts[1:]:
if abs(q[0] - out[-1][0]) > 1e-9 or abs(q[1] - out[-1][1]) > 1e-9:
out.append(q)
assert abs(out[0][0] - out[-1][0]) < 1e-9 and abs(out[0][1] - out[-1][1]) < 1e-9, \
u'板框点列没有闭合'
return out
def outline_polygon():
return outline_pts()
def zone_of(y):
for k, z in ZONES.items():
if z[1] <= y < z[3]:
return k
return None
def height_ok(y, h):
u"""y 处允许 h mm 高的件吗(刀片投影是硬碰撞)。"""
return not (PROJ[0] <= y <= PROJ[1] and h > H_PROJ)
def selfcheck():
bad = []
xs = slot_xs()
if abs(xs[0] - 14.36) > 0.01 or abs(xs[-1] - 95.64) > 0.01:
bad.append(u'槽阵 x 与方案 §6.3 对不上: %s' % xs)
# 槽体不许出板边
if xs[0] - SLOT_BODY_W / 2 < 2.0 or xs[-1] + SLOT_BODY_W / 2 > BW - 2.0:
bad.append(u'槽体贴板边太近: %.2f / %.2f' % (xs[0] - SLOT_BODY_W / 2,
xs[-1] + SLOT_BODY_W / 2))
# 槽体必须整个落在 'slots' 带里
z = ZONES['slots']
if abs((SLOT_CY - SLOT_BODY_L / 2) - z[1]) > 0.01 or abs((SLOT_CY + SLOT_BODY_L / 2) - z[3]) > 0.01:
bad.append(u'槽体 y %.1f~%.1f 与 slots 带 %.1f~%.1f 对不上'
% (SLOT_CY - SLOT_BODY_L / 2, SLOT_CY + SLOT_BODY_L / 2, z[1], z[3]))
# 分区必须首尾相接、铺满板
ks = sorted(ZONES, key=lambda k: ZONES[k][1])
for a, b in zip(ks, ks[1:]):
if abs(ZONES[a][3] - ZONES[b][1]) > 1e-6:
bad.append(u'分区 %s 顶 %.1f != %s 底 %.1f' % (a, ZONES[a][3], b, ZONES[b][1]))
if abs(ZONES[ks[0]][1]) > 1e-6 or abs(ZONES[ks[-1]][3] - BH) > 1e-6:
bad.append(u'分区没铺满 0~%.1f' % BH)
# 投影带边界必须正好是 input 顶 / fan 底 —— 高件区就是投影带外那两条
if abs(PROJ[0] - ZONES['input'][3]) > 1e-6 or abs(PROJ[1] - ZONES['fan'][1]) > 1e-6:
bad.append(u'刀片投影 %s 与 input/fan 带边界对不上' % (PROJ,))
# 安装孔不许压在槽体上,也不许出板
for n, (hx, hy) in MOUNT_HOLES.items():
if not (HOLE_KEEP < hx < BW - HOLE_KEEP and HOLE_KEEP < hy < BH - HOLE_KEEP):
bad.append(u'安装孔 %s 出板或贴边: (%.1f, %.1f)' % (n, hx, hy))
for sx in slot_xs():
if abs(hx - sx) < SLOT_BODY_W / 2 + HOLE_KEEP and \
ZONES['slots'][1] - HOLE_KEEP < hy < ZONES['slots'][3] + HOLE_KEEP:
bad.append(u'安装孔 %s 压到槽 x=%.2f' % (n, sx))
# 四角圆角不许吃掉安装孔
for n, (hx, hy) in MOUNT_HOLES.items():
for cx, cy in ((CORNER_R, CORNER_R), (BW - CORNER_R, CORNER_R),
(CORNER_R, BH - CORNER_R), (BW - CORNER_R, BH - CORNER_R)):
out_x = hx < cx if cx < BW / 2 else hx > cx
out_y = hy < cy if cy < BH / 2 else hy > cy
if out_x and out_y and math.hypot(hx - cx, hy - cy) > CORNER_R - HOLE_KEEP:
bad.append(u'安装孔 %s 被 R%.0f 圆角切到' % (n, CORNER_R))
# 天线缺口:那儿没板材,孔和螺钉头都不许沾(H1/H3 在 x=4,只差 y 就撞上)
for n, (hx, hy) in MOUNT_HOLES.items():
if (ANT_NOTCH[0] - HOLE_KEEP < hx < ANT_NOTCH[2] + HOLE_KEEP and
ANT_NOTCH[1] - HOLE_KEEP < hy < ANT_NOTCH[3] + HOLE_KEEP):
bad.append(u'安装孔 %s 落进 ESP32 天线缺口 (%.1f, %.1f)' % (n, hx, hy))
assert not bad, u'底板自检不过:\n ' + u'\n '.join(bad)
return True
# ── 下发 ──────────────────────────────────────────────────────────────
def call(js, timeout=180):
r = run(js, timeout=timeout)
if not r.get('success'):
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:800])
return r['result']
def mil(v):
return round(v * MIL, 4)
def polygon_src(pts):
a = [mil(pts[0][0]), mil(pts[0][1])]
for q in pts[1:]:
a += ['L', mil(q[0]), mil(q[1])]
if pts[0] != pts[-1]:
a += ['L', mil(pts[0][0]), mil(pts[0][1])]
return a
def push_outline():
u"""板框 = 一条闭合折线;顺带删掉建工程时留下的 4 条层 11 **导线**(§27.3 的坑)。"""
src = polygon_src(outline_polygon())
js = ("await eda.dmt_EditorControl.openDocument('%s');" % PCB +
"await new Promise(r=>setTimeout(r,2500));" +
"const oldL = (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11)"
".map(l=>l.primitiveId);" +
"if (oldL.length) await eda.pcb_PrimitiveLine.delete(oldL);" +
"const oldP = (await eda.pcb_PrimitivePolyline.getAll()||[])"
".filter(p=>p.getState_Layer()===11).map(p=>p.getState_PrimitiveId());" +
"if (oldP.length) await eda.pcb_PrimitivePolyline.delete(oldP);" +
"const poly = await eda.pcb_MathPolygon.createPolygon(%s);" % json.dumps(src) +
"const r = await eda.pcb_PrimitivePolyline.create('', 11, poly, %s);" % mil(OUTLINE_W) +
"await eda.pcb_Document.save();" +
"return {delLine: oldL.length, delPoly: oldP.length, made: !!r,"
" now: (await eda.pcb_PrimitivePolyline.getAll()||[])"
".filter(p=>p.getState_Layer()===11).length,"
" line11: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11).length};")
r = call(js, timeout=300)
print(u'板框折线: %s(删旧导线 %d / 旧折线 %d);层 11 现有折线 %d / 导线 %d'
% (u'建好' if r['made'] else u'失败', r['delLine'], r['delPoly'], r['now'], r['line11']))
if not r['made'] or r['now'] != 1:
raise SystemExit(u'!! 板框没画成')
return r
PUSH_HOLES_JS = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,2500));
const H = %s, D = %s;
for (const p of await eda.pcb_PrimitivePad.getAll()) {
const num = p.getState_PadNumber();
if (typeof num === 'string' && /^H[1-9]$/.test(num))
await eda.pcb_PrimitivePad.delete(p.getState_PrimitiveId());
}
let k = 0;
for (const [name, x, y] of H) {
// layer 12 = 多层;末位 metallization=false ⇒ NPTH。参数位序照抄 pcb_blade.push_holes
await eda.pcb_PrimitivePad.create(12, name, x, y, 0, ['ELLIPSE', D, D], '',
['ROUND', D], 0, 0, 0, false);
k++;
}
await eda.pcb_Document.save();
return {made: k, now: (await eda.pcb_PrimitivePad.getAll())
.filter(p => /^H[1-9]$/.test(String(p.getState_PadNumber()))).length};
"""
def push_holes():
arr = [[k, mil(v[0]), mil(v[1])] for k, v in sorted(MOUNT_HOLES.items())]
r = call(PUSH_HOLES_JS % (PCB, json.dumps(arr), mil(HOLE_D)), timeout=300)
print(u'安装孔: 下发 %d / 读回 %d,NPTH Φ%.1f(M3 尼龙立柱)' % (r['made'], r['now'], HOLE_D))
for k, v in sorted(MOUNT_HOLES.items()):
print(u' %-3s (%6.2f, %6.2f)' % (k, v[0], v[1]))
return r
def verify():
r = call("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,2000));"
"const ps = (await eda.pcb_PrimitivePolyline.getAll()||[])"
".filter(p=>p.getState_Layer()===11);"
"const line11 = (await eda.pcb_PrimitiveLine.getAll()||[])"
".filter(l=>l.layer===11).length;"
"const holes = (await eda.pcb_PrimitivePad.getAll())"
".filter(p=>/^H[1-9]$/.test(String(p.getState_PadNumber())))"
".map(p=>[String(p.getState_PadNumber()), p.getState_X(), p.getState_Y()]);"
"return {n: ps.length, poly: ps.length ? ps[0].getState_Polygon() : null,"
" line11, holes, comps: (await eda.pcb_PrimitiveComponent.getAll()).length};" % PCB,
timeout=180)
bad = []
if r['n'] != 1:
bad.append(u'层 11 上有 %d 条折线(应为 1)' % r['n'])
if r['line11']:
bad.append(u'层 11 上还有 %d 条**导线**(板框画成铜的老坑,§27.3)' % r['line11'])
pts = []
src = r['poly']
if isinstance(src, dict):
src = src.get('pointArr') or src.get('points') or src.get('polygon') or src
if isinstance(src, list):
i = 0
while i < len(src):
if src[i] in ('L', 'l'):
i += 1
continue
if isinstance(src[i], (int, float)) and i + 1 < len(src) and isinstance(src[i + 1], (int, float)):
pts.append((src[i] / MIL, src[i + 1] / MIL))
i += 2
else:
i += 1
if pts:
xs = [p[0] for p in pts]
ys = [p[1] for p in pts]
if abs(min(xs)) > 0.01 or abs(max(xs) - BW) > 0.01 or \
abs(min(ys)) > 0.01 or abs(max(ys) - BH) > 0.01:
bad.append(u'外接矩形 %.2f~%.2f x %.2f~%.2f != 0~%.0f x 0~%.0f'
% (min(xs), max(xs), min(ys), max(ys), BW, BH))
if abs(pts[0][0] - pts[-1][0]) > 0.01 or abs(pts[0][1] - pts[-1][1]) > 0.01:
bad.append(u'折线首尾不重合: %s vs %s' % (pts[0], pts[-1]))
if abs(len(pts) - len(outline_polygon())) > 1:
bad.append(u'点数 %d != %d' % (len(pts), len(outline_polygon())))
print(u'读回板框折线 1 条 / %d 点,外接 %.2f~%.2f x %.2f~%.2f;层 11 遗留导线 %d'
% (len(pts), min(xs), max(xs), min(ys), max(ys), r['line11']))
else:
bad.append(u'读不回折线点列(原始值 %s)' % str(r['poly'])[:120])
got = {h[0]: (round(h[1] / MIL, 3), round(h[2] / MIL, 3)) for h in r['holes']}
for n, (hx, hy) in sorted(MOUNT_HOLES.items()):
if n not in got:
bad.append(u'安装孔 %s 没打' % n)
elif abs(got[n][0] - hx) > 0.02 or abs(got[n][1] - hy) > 0.02:
bad.append(u'安装孔 %s 位置 %s != (%.1f, %.1f)' % (n, got[n], hx, hy))
print(u'安装孔读回 %d 个;板上元件 %d 件' % (len(got), r['comps']))
if bad:
print(u'[X] 校验不过:')
for b in bad:
print(u' ' + b)
else:
print(u'[OK] 板框是一条闭合折线、外接矩形对上、安装孔齐')
return bad
if __name__ == '__main__':
selfcheck()
print(u'底板 %.0f x %.0f,四角 R%.0f;PCB=%s' % (BW, BH, CORNER_R, PCB))
print(u'槽阵 5 槽 @%.2f: %s' % (SLOT_PITCH, u' / '.join('%.2f' % x for x in slot_xs())))
print(u'槽体 y %.1f~%.1f;刀片投影 y %.1f~%.1f(带内元件 ≤ %.0f mm)'
% (SLOT_CY - SLOT_BODY_L / 2, SLOT_CY + SLOT_BODY_L / 2, PROJ[0], PROJ[1], H_PROJ))
for k in sorted(ZONES, key=lambda k: ZONES[k][1]):
z = ZONES[k]
print(u' %-7s y %6.2f ~ %6.2f (%5.1f mm)%s'
% (k, z[1], z[3], z[3] - z[1], u' ← 投影外,可放高件' if k in ('input', 'fan') else u''))
if '--outline' in sys.argv:
push_outline()
verify()
elif '--holes' in sys.argv:
push_holes()
verify()
elif '--verify' in sys.argv:
verify()