# -*- 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 # ══ M9 新坐标模型(2026-09-09 用户拍板)══════════════════════════════════ # ⛔ **M8 的分带式布局(槽阵横排 + ctrl 带在北)在两层板上是死的**, # 根因见方案 §29.1:单颗 Hub 扭出 5 路 D±,在扇出带里形成一道**横贯全板的墙** # (`USB5`/`USB1` 共用 y=41.4 那条道,两段拼起来 x 24.5~105.1), # 而墙以北没有横向通道(5 个槽体 + 每槽两端的屏蔽脚把槽阵横向堵死)。 # ⇒ 12V / 5V / 3V3 和 25 条槽上信号全都过不去,实测 173/314 条布不出来, # 四轮打平(173/174/175/173),换 `BOT_PEN` 无效(178,更差)。 # # ⇒ **槽阵转 90°**:槽体长轴沿 x,五片刀片沿 y 堆叠。于是 # **D± 走西缘、电源走东缘,走廊里全是平行车道,一个交叉都没有。** # # 为什么这条能成立(针位实测,见 §29.2):把槽转成 A1 端**朝东**之后, # 每只脚落在哪一列 x 是定死的 —— # x 71.5 +12V(5) ← 紧贴东缘,12V 铜条只进走廊 4.5 mm # x 66.5 SCL/SDA x 64.0 +3V3(3) x 61.5 EFUSE_EN / INT# # x 56.0 D± x 54.5 VDAC x 51.5 / 38.5 +5V # x 39.5 PRSNT# # **x > 69 只有 `+12V` 一个网** ⇒ 12V 的铜条可以在走廊东端做成「顶天立地」的一堵墙 # (同时够到上下两排焊盘),而不挡任何别的东西:其余全部从西边进来,最远只到 x=67。 SLOT_PITCH = 20.32 # 刀片堆叠间距(沿 y),= 手机+刀片+间隙,机械定死 SLOT_N = 5 SLOT_ROT = 0 # 2026-09-10 用户要求:五个插槽从 rot 180 再转 180° # ⚠ 槽体长轴中心**不在板中心**:西区要塞下 ESP32 的「天线 7.5 + 焊盘 19 = 26.5」, # 所以整条阵列往东挪 3 mm。刀片 98 沿 x ⇒ 盖住 x 9~107,两边各留 3 mm。 # ⛔ 再东移 2 mm(58→60):D± 通道要塞下**三条**并行车道。一对 1.2 mm 的线在占用图里 # 要 `1.41 + 2×0.81 = 3.03 mm`,三条 = 9.6,8 mm 的通道装不下 —— 现象是每对永远 # 只进得去一根(「DP 通、DM 出不来」,七对里六对)。刀片随之盖住 x 11~109。 # ⛔ **2026-09-09 用户拍板 A 方案**:通道从 10 加宽到 13.5,槽阵再东移 3.5(60→63.5)。 # 账:一对 1.0 mm 的 D± 在占用图里要 `1.21 + 2×0.71 = 2.63`;车道还要能被一根 # 0.25 的逃逸线横穿(吃掉 1.67)而仍装得下一对 ⇒ 车道 4.5,三条 = 13.5。 # 代价:刀片(98 宽)盖住 x 14.5~112.5,**伸出东板边 2.5 mm**(用户已确认)。 SLOT_CX = 46.5 # 插槽翻转后整体西移,东侧留 13.5 mm D± 通道 # ⚠ 阵列中心比板中心(62.5)**往北挪 1 mm**:南带要塞下 DC-005 的 14.5 mm 桶身,外加 0.6 净距, # 17.26 差 1 mm。北带少 1 mm 不影响(风扇块全是小件)。 # M13:整组北移 3 mm,为最南槽 USB5 与 9 A 输入铜之间留出独立顶层走廊。 # 槽距不变,并保留北侧辅助 Buck / 风扇服务带。 ARRAY_CY = 66.5 SLOT_BODY_L = 42.0 # 槽体沿 **x**(实测 `PCIE-64P11L` 焊盘外框) SLOT_BODY_W = 9.2 # 槽体沿 **y** # 两排焊盘:y = 槽心 ± 3.6;单盘 0.7(x) × 2.0(y),脚距 1.0 PAD_ROW_DY = 3.6 PAD_L, PAD_W = 0.7, 2.0 # 长轴上的针位(u = 距槽体中心,正 = 西)。A1 端朝东 ⇒ 板上 x = SLOT_CX − u。 PIN_U = {'+12V': (-16.5, -14.5), 'I2C': (-12.5, -11.5), '+3V3': (-9.5, -7.5), 'EN': (-6.5, -6.5), 'DIFF': (-1.5, -0.5), 'VDAC': (0.5, 0.5), '+5V': (3.5, 16.5), 'PRSNT': (15.5, 15.5)} def pin_x(u): u"""长轴坐标 u(封装)→ 板上 x。rot 0 与 rot 180 互为镜像。""" return round(SLOT_CX + u if SLOT_ROT == 0 else SLOT_CX - u, 3) def slot_y(i): u"""槽 i(1 = 最南)的中心 y。""" return round(ARRAY_CY + (i - (SLOT_N + 1) / 2.0) * SLOT_PITCH, 3) def slot_ys(): return [slot_y(i) for i in range(1, SLOT_N + 1)] def slot_body(i): y = slot_y(i) return (SLOT_CX - SLOT_BODY_L / 2, y - SLOT_BODY_W / 2, SLOT_CX + SLOT_BODY_L / 2, y + SLOT_BODY_W / 2) # ── 走廊:槽体之间那 11.12 mm,外加阵列两头 ──────────────────────────────── # 走廊 k(0 = 阵列最南那条,5 = 最北)。内部四条各服务上下两个槽的一排焊盘。 def corridor(k): y0 = 0.0 if k == 0 else slot_y(k) + SLOT_BODY_W / 2 y1 = BH if k == SLOT_N else slot_y(k + 1) - SLOT_BODY_W / 2 return (0.0, round(y0, 3), BW, round(y1, 3)) def corridors(): return [corridor(k) for k in range(SLOT_N + 1)] # ── 三条纵向分区(沿 x)─────────────────────────────────────────────────── # 西区:12V 干道;中区:5 个槽体;东区:D± 通道 + Hub / 控制器。 WEST = (0.0, 25.5) # 西侧 12V 服务区 MID = (SLOT_CX - SLOT_BODY_L / 2, SLOT_CX + SLOT_BODY_L / 2) # 34 ~ 76 EAST = (81.0, BW) # 东侧 Hub / ESP32 / mux / DAC 功能区 # 逻辑 USB 号 -> 插槽位号 / 物理槽位(南起 1..5)。 # 插槽翻转后物理顺序与 USB 号反向;混用会让五对 D± 全交叉。 USB_SLOT_REF = {1: 'J34', 2: 'J35', 3: 'J36', 4: 'J37', 5: 'J38'} USB_SLOT_POS = {i: SLOT_N + 1 - i for i in range(1, SLOT_N + 1)} def usb_corridor(i): u"""USB i 的 D± 所在插槽南侧走廊。""" return corridor(USB_SLOT_POS[i] - 1) # ── 刀片投影:每片刀片盖住 x 6~104,沿 y 占 ±7.35 ────────────────────────── # 保守起见整条阵列带按投影处理(限高 8 mm,方案 §6.6)。 BLADE_W = 98.0 # 刀片沿 x BLADE_X0 = round(SLOT_CX - BLADE_W / 2, 2) # 6.0 PROJ = (round(slot_y(1) - SLOT_BODY_W / 2, 2), round(slot_y(SLOT_N) + SLOT_BODY_W / 2, 2)) # 17.26 ~ 107.74 H_PROJ = 8.0 def blade_x_to_base_x(bx): u"""刀片自身坐标 x → 底板 x(刀片 98 沿 x 铺开,居中对齐槽体)。""" return round(BLADE_X0 + bx, 3) # ── 分区(y 方向只剩三条:南带 / 阵列带 / 北带)──────────────────────────── ZONES = { 'input': (0.0, 0.0, BW, PROJ[0]), # 投影外,可放高件:DC 座 / F1 / Q1 / Φ8 电解 'array': (0.0, PROJ[0], BW, PROJ[1]), # 槽体 + 走廊 + 西区器件 + 东区高速路 'fan': (0.0, PROJ[1], BW, BH), # 投影外:风扇座 ×2 / 驱动 / J33 应急口 } # ── 12V(9 A):南带进料 → 西侧高速路 → 每条走廊分段进槽 ──────────────── # M12 当前布局把 U35/U6/U7 放在 x=10~15;主干只能使用它们与槽体之间 # x=15.5~24.5 的连续通道。实测无异网焊盘侵入,9 mm 净宽仍高于 8 mm 要求。 # ⛔ 北端只到 y=110:再往北是风扇驱动 `Q6`/`Q7`(它们本来就该贴着风扇座), # 高速路铺过去会被它们的非 12V 脚掐到 **5.70 mm**(断面校核实测)。 # 那一段只带风扇 + 两颗 Buck(~3 A,5.7 mm 够),但校核按 9 A 一刀切 ⇒ # 与其放宽判据,不如让干道在 110 收口,北边的 12V 由走廊 5 那堵铜墙供。 # ⛔ 铜墙必须**伸进上下两个槽体 2.0 mm**:焊盘中心距槽心 3.6、盘高 2.0,也就是占了槽体边缘往里 0~2.0 那一段。 # 只画走廊本身的话**一只 12V 脚都碰不到**,而铺铜管理器 / DRC 都不会响 # (只有 `board_check` 从板上读回铜做并查集才看得见)。 RIB_OVER = 1.4 # 旧 BUS_S 与定稿后的 J1/F1/Q1 输入链重叠,会把 VIN_RAW 直接短到 +12V。 # 干道改从投影带南沿开始,Rs6 -> 干道由路由器单独连接。 # 南端避开 y=16.25 的 2 mm VIN_SW 引线;向东加宽后,扣除其净距仍超过 8 mm。 # 东界距 x=27、宽 3 mm 的槽外壳 GND 焊盘保留 0.25 mm 净距。 # RIB_X 保持从 x=24.5 起,与主干重叠 0.75 mm,并继续避开 x=33 的 GND 针。 BUS_W = (15.5, 18.75, 25.25, BH - 0.5) RIB_X = (24.5, 32.4) def rib_rects(): u"""五个插槽的 12V 分段铜墙。 下排是 x=30/31/32,上排是 x=31/32;上排 x=30 紧邻 GND,槽心 x=27 还有巨大的外壳 GND 盘。所以不能再用「整条走廊顶天立地」的矩形。 每排用窄指覆盖电源脚,再在排外用宽帽接回西侧干道。 """ out = [] c = 0.25 row_o = PAD_ROW_DY + PAD_W / 2 row_i = PAD_ROW_DY - PAD_W / 2 x_lower = round(pin_x(-16.5) - PAD_L / 2 - 0.05, 3) # x=30 焊盘左侧 x_upper = round(pin_x(-15.5) - PAD_L / 2 - 0.05, 3) # x=31 焊盘左侧 for i in range(1, SLOT_N + 1): cy = slot_y(i) cap_lo = round(cy - row_o - RIB_OVER, 3) cap_hi = round(cy - row_o - c, 3) # 最南宽帽直接接到主干,并停在 J38 外壳 GND 的净距边界以内。 if i == 1: cap_lo = BUS_W[1] top_hi = round(cy + row_o + RIB_OVER, 3) # 北移后的 J34 靠近 C734;北帽只需覆盖针排并接到主干,不再向北多伸。 if i == SLOT_N: top_hi = round(cy + row_o + 0.9, 3) out += [ (RIB_X[0], cap_lo, RIB_X[1], cap_hi), (x_lower, round(cy - row_o - c - 0.05, 3), RIB_X[1], round(cy - row_i + c + 0.05, 3)), (x_upper, round(cy + row_i - c - 0.05, 3), RIB_X[1], round(cy + row_o + c + 0.05, 3)), (RIB_X[0], round(cy + row_o + c, 3), RIB_X[1], top_hi), ] return out def bus_rects(): return [BUS_W] + rib_rects() # ── +3V3 铜墙(M9b,2026-09-10 用户拍板)─────────────────────────────────── # `+3V3` 是全板最大的网(47 只脚),A* 当一棵树布 ⇒ **整网崩**(M9 实测 46/47 没布通)。 # 办法与 12V 同:把脚位那一段直接做成铜,脚从 A* 里摘掉。 # # ⛔ 但 3V3 的墙**不能像 12V 那样顶天立地**,因为它不在东缘尽头,而在信号中间: # 南排 … EN(70) 3V3(71) 3V3(72) NC(73) NC(74) NC(75) NC(76) GND(77) 12V(78,79) … # 北排 … INT#(70) NC(71) NC(72) 3V3(73) GND(74) SDA(75) SCL(76) GND(77) 12V(78~80) # 每个槽的 `SDA_i`/`SCL_i` 在**北排 x 75/76**,要一路往西去 `U35`(x≈10~16) —— # 任何一条从 x>73 一直连到 x<70 的**顶层**南北向铜,都会把这十根线拦腰截断。 # ⇒ 墙只在**槽体那一段**(顶层,够着三只脚),走廊那一段**走底层桥**(0.8 mm + 两个过孔)。 # 代价是地平面上多五道 9.5 mm 长的窄缝(都在 x≈72,D± 全在 x<66,不碰 §6.2 第 1 条)。 # # ⛔ 墙的 x 是**逐排不同的**(同一个矩形盖不住两排): # 南排的 3V3 在 71/72、北排的在 73,而它们各自旁边就是**没有网的 NC 针** # (南排 73、北排 71/72)。一个 70.6~73.4 的方块会同时压上 NC 针 —— # 铺铜会绕开它们、把墙在排上切碎,而铺铜管理器和 DRC 都只说「有铜」。 # ⇒ 拆成「南窄 / 中宽 / 北窄」三段同网矩形(同网重叠是允许的,12V 那几块也是这么接的)。 W3V3_CLR = 0.25 # 到异网针的净距(规则底线 0.20,留 0.05) W3V3_OVER = 1.4 # 墙伸进走廊多少:给底层桥的过孔留地方 W3V3_BRIDGE_W = 0.8 # 走廊底层桥的线宽(3V3 全板 ~0.3 A,0.8 够 2 A) W3V3_VIA_DY = 5.4 # 过孔离槽心多远(针排外沿 4.6 + 0.55 环 + 余量) def wall3v3_rects(): u"""五块 `+3V3` 槽墙,每块三段(南窄 / 中宽 / 北窄)+ 两块走廊帽。 x 的来历(全部从 `PIN_U` 算,不手抄): 西边界 = `EN` 针右沿 + 净距;东边界 = `GND(74)` 针左沿 − 净距; 南段东界 = 南排 NC(73) 针左沿 − 净距;北段西界 = 北排 NC(72) 针右沿 − 净距。 """ c = W3V3_CLR # 新方向是旧铜墙相对槽心的水平镜像。 x_w = round(SLOT_CX - 9.9, 3) x_e = round(SLOT_CX - 7.1, 3) x_s_e = round(SLOT_CX - 9.1, 3) x_n_w = round(SLOT_CX - 8.9, 3) row_o = PAD_ROW_DY + PAD_W / 2 # 针排外沿 4.6 row_i = PAD_ROW_DY - PAD_W / 2 # 针排内沿 2.6 out = [] for i in range(1, SLOT_N + 1): cy = slot_y(i) out += [ # 走廊帽(排外,没有任何针):整宽,给桥的过孔一个圆整的落点 (x_w, round(cy - row_o - W3V3_OVER, 3), x_e, round(cy - row_o - c, 3)), # 南段:从走廊穿过南排(盖住 71/72)进到两排之间 (x_w, round(cy - row_o - c - 0.05, 3), x_s_e, round(cy - row_i + c + 0.05, 3)), # 中段:两排之间那 5.2 mm,整宽 (x_w, round(cy - row_i + c, 3), x_e, round(cy + row_i - c, 3)), # 北段:穿过北排(盖住 73) (x_n_w, round(cy + row_i - c - 0.05, 3), x_e, round(cy + row_o + c + 0.05, 3)), (x_w, round(cy + row_o + c, 3), x_e, round(cy + row_o + W3V3_OVER, 3)), ] return out def wall3v3_bridges(): u"""走廊里的**底层**桥:[(x, y0, y1, 宽)],以及两端的过孔 [(x, y)]。 只连相邻两块槽墙(4 段)。北端到 `U19`/`L2` 那一段交给 A*: 它是一条普通的短网,不像槽脚那样被十根 I²C 挡着。 """ x = round(SLOT_CX - 8.5, 3) # 38.0,新方向的 3V3 墙中心 segs, vias = [], [] for i in range(1, SLOT_N): y0 = round(slot_y(i) + W3V3_VIA_DY, 3) y1 = round(slot_y(i + 1) - W3V3_VIA_DY, 3) segs.append((x, y0, y1, W3V3_BRIDGE_W)) vias += [(x, y0), (x, y1)] return segs, vias # ── D± 竖向扇出通道(西区):`U32` 在西区中部,五对沿 y 散开再往东进走廊 ──── # ⛔ 这条带里**一件都不许放**(同 M8 的 FANOUT,理由一样:排布器会把去耦塞满, # 然后布线器报「某一对布了 0 段」,而摆位自检 / DRC / 几何验证一条都不响)。 DP_CHAN = (67.5, PROJ[0], 81.0, PROJ[1]) # Hub USB6 -> ESP32 原生 USB 的北向短通道。U20/U32 是端点,其他元件不得进入。 USB6_CHAN = (80.5, 78.0, 84.8, 107.2) # ⛔ 阵列**北边**还要留一条 4 mm 的过道:最北那个槽(槽 5)的 D± 焊盘在它的北排, # 北侧走廊就是风扇带本身。不留的话逃逸落点会掉进风扇块里 —— # 实测 `J38.launch` 起步格点 **0**,现象是「差分对 USB5 布了 0 段」。 DP_N_LANE = (MID[0], PROJ[1], BW, PROJ[1] + 4.0) # ── 走廊「喉口」:西区器件与 D± 通道之间那一段,**一件都不许放** ──────────── # ⛔ 2026-09-09 用户看出来的:`R605~R608` / `C9` / `C731` 那一窝落在走廊 2 的 # y 段、x 20~28 上 —— 那正是西区**所有**去往槽 2 的信号(含它那一对 D±) # 往东挤进通道的唯一喉口。堵在这儿的后果是布线器报「出不来 / 没布通」, # 而摆位自检、DRC、几何校验一条都不会响。 # ⇒ 每条内走廊在 x 17~29 这一段设成摆位禁区(器件退到槽体那几段 y 上去)。 # ⚠ 宽度是权衡出来的:17~29(12 mm)会把西区挤爆(去耦平均从 22.9 涨到 33.3 mm), # 而喉口只要够信号穿过去就行 —— 6 mm 在 0.5 间距下可过十根线,够一条走廊的量。 THROAT_X = (DP_CHAN[2], 87.0) # `U32` 东沿到 D± 通道之间那条**逃逸扇形带**:十根 0.15 的细线从 QFN 脚扇到车道, # 一件都不许放(放了就是 DRC「焊盘到导线」,实测 22 条压在 C602/C607 上)。 HUB_ESC = (DP_CHAN[2], 55.0, 86.0, 78.0) def throat_rects(): out = [] for k in range(1, SLOT_N): c = corridor(k) out.append((THROAT_X[0], c[1], THROAT_X[1], c[3])) return out def keepout_rects(): u"""摆位禁区(不是铺铜禁区):12V 高速路 + 铜墙 + 3V3 槽墙 + D± 竖向通道 + 天线缺口。""" return (bus_rects() + wall3v3_rects() + [DP_CHAN, USB6_CHAN, DP_N_LANE, ANT_NOTCH, HUB_ESC] + throat_rects()) # ── ESP32 天线缺口(西板边)──────────────────────────────────────────────── # 见 §28.14:`U20` 转 rot=90 天线朝西、天线那 7.5 mm 下方板材整块挖掉。 # 新布局里 `U20` 挪到西北(靠近风扇带的 FAN_*/NTC 和 J33),缺口跟着走。 ANT_NOTCH = (BW - 7.5, 86.0, BW, 108.0) def notch_rect(): return ANT_NOTCH # ── 安装孔(M3 尼龙立柱,NPTH)──────────────────────────────────────── HOLE_D = 3.2 HOLE_KEEP = 3.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"""两层都不许压的孔:四个 M3 安装孔(它们是 `pcb_PrimitivePad`,不在 pads.json 里)。""" return dict(MOUNT_HOLES) def mill_cuts(): 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, ANT_NOTCH[1]), (ANT_NOTCH[0], ANT_NOTCH[1]), (ANT_NOTCH[0], ANT_NOTCH[3]), (BW, ANT_NOTCH[3]), (BW, BH - r)] pts += corner(BW - r, BH - r, 0, 90) pts += [(r, BH)] pts += corner(r, BH - r, 90, 180) 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, x=None): u"""该位置允许 h mm 高的件吗(刀片投影是二维硬碰撞)。""" in_y = PROJ[0] <= y <= PROJ[1] in_x = x is None or BLADE_X0 <= x <= BLADE_X0 + BLADE_W return not (in_x and in_y and h > H_PROJ) def selfcheck(): bad = [] ys = slot_ys() # 阵列必须居中、间距是机械定死的 20.32 for a, b in zip(ys, ys[1:]): if abs((b - a) - SLOT_PITCH) > 1e-6: bad.append(u'槽间距 %.3f != %.2f' % (b - a, SLOT_PITCH)) if abs((ys[0] + ys[-1]) / 2.0 - ARRAY_CY) > 1e-6: bad.append(u'阵列没居中: %s' % ys) # 槽体不许出板边(南北两头) if ys[0] - SLOT_BODY_W / 2 < 2.0 or ys[-1] + SLOT_BODY_W / 2 > BH - 2.0: bad.append(u'槽体贴板边太近: %.2f / %.2f' % (ys[0] - SLOT_BODY_W / 2, ys[-1] + SLOT_BODY_W / 2)) # 槽体长轴不许出板边(东西两头),且要给东区高速路让出地方 if SLOT_CX - SLOT_BODY_L / 2 < WEST[1] - 1e-6 or SLOT_CX + SLOT_BODY_L / 2 > EAST[0] + 1e-6: bad.append(u'槽体 x %.1f~%.1f 越过了西/东区边界 %.1f / %.1f' % (SLOT_CX - SLOT_BODY_L / 2, SLOT_CX + SLOT_BODY_L / 2, WEST[1], EAST[0])) # 走廊必须首尾相接、铺满板 cs = corridors() for a, b in zip(cs, cs[1:]): if a[3] > b[1] + 1e-6: bad.append(u'走廊重叠: %s / %s' % (a, b)) if abs(cs[0][1]) > 1e-6 or abs(cs[-1][3] - BH) > 1e-6: bad.append(u'走廊没铺满 0~%.1f' % BH) for k in range(1, SLOT_N): w = cs[k][3] - cs[k][1] if abs(w - (SLOT_PITCH - SLOT_BODY_W)) > 1e-6: bad.append(u'走廊 %d 净宽 %.2f != %.2f' % (k, w, SLOT_PITCH - SLOT_BODY_W)) # 分区必须首尾相接 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) 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,)) # ⛔ 这条是新布局能成立的**判据本身**:x > RIB_X[0] 只许有 +12V。 # 别的网只要有一只脚落到铜墙的 x 区间里,那堵墙就不能顶天立地了。 for tag, (u0, u1) in PIN_U.items(): if tag == '+12V': continue for u in (u0, u1): if pin_x(u) < RIB_X[1] + 0.5: bad.append(u'%s 的针落在 x=%.1f,侵入 12V 铜墙区(x<%.1f 只许有 +12V)' % (tag, pin_x(u), RIB_X[1])) # 12V 铜墙要够得着 12V 针 if not (RIB_X[0] < pin_x(PIN_U['+12V'][0]) < RIB_X[1]): bad.append(u'12V 铜墙 x %.1f~%.1f 够不着 12V 针 x=%.1f' % (RIB_X[0], RIB_X[1], pin_x(PIN_U['+12V'][0]))) # 东缘高速路净宽(1 oz 外层 9 A ΔT20K 要 ≥8 mm) if BUS_W[2] - BUS_W[0] < 8.0: bad.append(u'西缘 12V 高速路只有 %.1f mm,9 A 要 ≥8' % (BUS_W[2] - BUS_W[0])) if [USB_SLOT_REF[i] for i in range(1, SLOT_N + 1)] != ['J34', 'J35', 'J36', 'J37', 'J38']: bad.append(u'USB -> 插槽位号映射被改坏') for i in range(1, SLOT_N + 1): c = usb_corridor(i) if c[0] >= c[2] or c[1] >= c[3]: bad.append(u'USB%d 的南侧走廊无效: %s' % (i, c)) # 安装孔:不许出板 / 压槽体 / 被圆角切 / 落进天线缺口 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 i in range(1, SLOT_N + 1): b0, b1, b2, b3 = slot_body(i) if b0 - HOLE_KEEP < hx < b2 + HOLE_KEEP and b1 - HOLE_KEEP < hy < b3 + HOLE_KEEP: bad.append(u'安装孔 %s 压到槽 %d' % (n, i)) 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)) 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)) sep = chr(10) + ' ' assert not bad, u'底板自检不过:' + sep + sep.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(沿 y 堆叠,长轴沿 x,rot %d): %s' % (SLOT_PITCH, SLOT_ROT, u' / '.join('%.2f' % y for y in slot_ys()))) print(u'槽体 x %.1f~%.1f;刀片投影 y %.1f~%.1f(带内元件 ≤ %.0f mm)' % (MID[0], MID[1], PROJ[0], PROJ[1], H_PROJ)) print(u'西区 x %.0f~%.0f | D± 通道 %.0f~%.0f | 中区 %.0f~%.0f | 东区 %.0f~%.0f' % (WEST[0], WEST[1], DP_CHAN[0], DP_CHAN[2], MID[0], MID[1], EAST[0], EAST[1])) print(u'12V:Rs6 单独接入 → 西缘高速路 %s → %d 堵铜墙' % (BUS_W, len(rib_rects()))) if '--outline' in sys.argv: push_outline() verify() elif '--holes' in sys.argv: push_holes() verify() elif '--verify' in sys.argv: verify()