文件
phonerail/.claude/r2/pcb_base.py
T
zhou yang和Claude Opus 5 312089326e R2 底板:按用户布局重布风扇窗口 + L1 走廊重开 + 位号丝印重排
布线(窗口整体重布,已下发):
- 拆窗口内 43 段(用户手布的那批,用户选「拆掉重来」),补 139 段 / 7 孔,
  GND 回流孔 10 个;fan_check 对板上真实快照逐项净距达标(含「新孔 vs 内层线」)。
- ⛔ 根因:pcb_base.PD_L1 里的 L1 走廊是按**旧摆位**开的(注释坐标全是旧的),
  用户重摆后与件对不上 ⇒ 四条长途网必然布不通。重开六条走廊后未完成 4 → 2。
  走廊必须盖住焊盘正上方,只盖沿途不行。断面:几何 11.80 / 铺铜后实测 9.82(要 8.0)。
- 与拆线前逐网对比:没引入任何新的连通性问题(GND 未接 58→48、+3V3 2→0、
  两路 TACH 2/1→0、+12V 一字不差)。

定版:板上铜以**用户自己布好的那版**为准,不再改动。

丝印(silk_base.py,已下发并读回核对 150/150):
- 压焊盘 128→4、压过孔 48→8、越框 8→3、互相重叠 34 对→0;字号不变。
- 三条坑:rotation 是度且不归一化(900=180°);别拿「本体≥5mm」当保留白名单
  (会豁免掉 R9A/D3/U13 这些该重排的件);「离本体」必须设硬上限 + 降级策略。

新工具:silk_base.py(丝印重排)、l1_section.py(L1 12V 主块逐 y 断面核算)、
fan_rip.py(按窗口拆线,拆前后比对全板线/孔总数防连带删除)。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
2026-09-12 12:02:32 +08:00

615 行
29 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""R2 底板 PCB(M5,**4 层 98 × 98**,D15):板框 + 槽阵 + 背面分区 + 安装孔 + L1 的 12V 铜。
/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 # 读回校验
2 层版(110 × 125,12V 西干道 / 3V3 槽墙 / D± 竖通道 / 刀片投影限高)整份存档在
`archive_base2L/pcb_base.py`,那一套在这里**全部删掉**,下游引用到的会直接 AttributeError。
## 坐标系
**板左下角 = (0, 0),x 向右、y 向上**,两面**同一套坐标、都按从上往下看**。
x = 槽长方向,y = 五片刀片堆叠方向(J38 最南 … J34 最北)。
## 叠层(D15)
L1 插槽面 —— 只有 J34~J38 五个插槽(手焊)+ 西侧 12V 大铺铜 + 每排 12V 脚一根指
L2 GND —— 整层地,不布线(L1 信号的参考;2026-09-10 晚曾改 +3V3,同日深夜改回 GND,双地)
L3 GND —— 整层地,不布线(L4 上全部器件与 D± 的参考)
L4 器件面 —— 其余 133 件(工厂只贴这一面)+ 全部信号 + D±(参考 L3)+ 3V3/5V 局部铜
## ⛔ 板框必须是**折线**不是导线
`pcb_PrimitiveLine` 在嘉立创里是**导线(铜)**:渲染、DRC、几何校验全过,客户端就是不认板框
(方案 §27.3)。`push_outline()` 只用 `pcb_PrimitivePolyline`,并删掉层 11 上遗留的导线。
## 12V 指头的 x 为什么停在 34.95(不是分区草图写的 35.1)
槽针实测(`PIN_ROWS`,J36 读回):两排 12V 右边紧挨着的 x=35.5 是 **GND**,
焊盘左沿 35.15;35.1 只剩 0.05 mm。北排 12V 左边 x=32.5 也是 **GND**,
所以北排的指头只能从排外绕进来(L 形),不能像南排那样整块横穿。
"""
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
LAYERS = 4
TOP, BOTTOM = 1, 2 # EPCB_LayerId:器件只能在这两层
# ── 板框(mm,D15)─────────────────────────────────────────────────────
BW, BH = 98.0, 98.0 # 硬上限 98(100 正压嘉立创特价档边界,同 D14)
CORNER_R = 5.0
CORNER_SEG = 10
OUTLINE_W = 0.15
EDGE_KEEP = 0.5 # 铜 / 器件到板边
# ── 槽阵:五槽居中,长轴沿 x,rot 0 ───────────────────────────────────────
SLOT_PITCH = 20.32 # 刀片堆叠间距,机械定死
SLOT_N = 5
SLOT_ROT = 0
SLOT_CX = 49.0
ARRAY_CY = 49.0
SLOT_BODY_L = 42.0 # 槽体沿 x(`PCIE-64P11L` 焊盘外框)
SLOT_BODY_W = 9.2 # 槽体沿 y
PAD_ROW_DY = 3.6 # 两排焊盘 y = 槽心 ± 3.6
PAD_L, PAD_W = 0.7, 2.0 # 单盘 0.7(x) × 2.0(y),脚距 1.0
SHELL_U, SHELL_L, SHELL_W = 19.5, 3.0, 5.0 # 两端外壳 GND 盘:u = ±19.5,3(x) × 5(y)
SLOT_REFS = ('J34', 'J35', 'J36', 'J37', 'J38')
# 针位表(u = 距槽心,板上 x = SLOT_CX + u;2026-09-10 从 J36 实测焊盘读回)。
# S = 南排(焊盘 33~64,y = 槽心 − 3.6),N = 北排(焊盘 1~32,y = 槽心 + 3.6)。
# 每排 32 针:u −16.5 … −6.5(11 针)+ 键位缺口 + u −3.5 … +16.5(21 针)。
_U = [-16.5 + k for k in range(11)] + [-3.5 + k for k in range(21)]
_S = ('12V 12V 12V GND SCL SDA GND 3V3 NC NC INT '
'GND GND DP DM ' + 'GND ' * 15 + 'PRSNT GND').split()
_N = ('GND 12V 12V GND NC NC NC NC 3V3 3V3 EN '
'GND GND GND GND VDAC GND GND 5V ' + 'GND ' * 12 + '5V').split()
PIN_ROWS = {'S': dict(zip(_U, _S)), 'N': dict(zip(_U, _N))}
ROW_SIGN = {'S': -1, 'N': +1}
def pin_x(u):
return round(SLOT_CX + u if SLOT_ROT == 0 else SLOT_CX - u, 3)
def slot_y(i):
u"""槽 i(1 = 最南 = J38)的中心 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)
def pad_rect(i, row, u):
u"""槽 i 某排 u 处那只针的焊盘外框。"""
x, y = pin_x(u), slot_y(i) + ROW_SIGN[row] * PAD_ROW_DY
return (x - PAD_L / 2, y - PAD_W / 2, x + PAD_L / 2, y + PAD_W / 2)
def shell_rects(i):
y = slot_y(i)
return [(pin_x(s * SHELL_U) - SHELL_L / 2, y - SHELL_W / 2,
pin_x(s * SHELL_U) + SHELL_L / 2, y + SHELL_W / 2) for s in (-1, 1)]
# 逻辑 USB 号 → 插槽位号 / 物理槽位(南起 1..5)。USB1 = J34 = 最北。
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)}
# 2026-09-12 用户拍板:这几件挪到顶面(L1),理由是装配 / 接线都在插槽那一面。
# 五件的焊盘**全在封装 y = 0 那一排**(`_base_native_pads.json` 实测),而翻面只把
# 原生偏移的 y 取反(`place_base.pad_off`)⇒ 孔位、针序、铜**一根都不动**,
# 纯粹是装配面 + 丝印换面。⛔ 换成焊盘不在一条线上的件(贴片、双排针)不成立:
# 那种翻面会把铜挪到 L1,要重新布线。
TOP_EXTRA = ('C2', 'C3', # Φ8 470 µF 直立电解(12 mm 高)
'J26', 'J27', # 2510-4A 风扇座(11 mm 立式)
'J33') # 2.54 应急串口排针(11.5 mm 立式)
def side_of(ref):
u"""器件放哪一面:五个插槽 + TOP_EXTRA 在 L1(TOP),其余一律 L4(BOTTOM)。"""
return TOP if ref in SLOT_REFS or ref in TOP_EXTRA else BOTTOM
# ── 走廊:槽体之间那 11.12 mm,外加阵列两头 ────────────────────────────────
def corridor(k):
u"""走廊 k(0 = 阵列最南,5 = 最北),整板宽。"""
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)]
def usb_corridor(i):
u"""USB i 的 D± 所在走廊:D± 在每槽**南排**,所以是槽体南侧那条。"""
return corridor(USB_SLOT_POS[i] - 1)
# ── 背面 L4 分区(用户 2026-09-10 确认,见 M14-4L-floorplan.png)──────────────
# 槽体正下方(`slot_body` 投影)是过孔带:槽脚在 L1,GND 打孔下 L2/L3、信号打孔下 L4,不放件。
# 槽区 x 28~70 的走廊里只有 A4 两块,其余两条走廊留给布线。
# ⛔ 与草图不同:**Hub 在中间槽正南(走廊 2)、mux/DAC 在正北(走廊 3)**(2026-09-10 用户拍板)。
# U32 在底层只能 rot 270(口朝北),南去的几对要绕 U32 东侧;Hub 在走廊 3 时要绕三对、
# 最远 D± ≈ 80 mm,放走廊 2 只绕两对、最远 ≈ 58 mm。详见 place_base 文件头。
_C = corridors()
ZONES = {
'A1': (0.5, 0.5, 27.5, 56.0), # 输入链 J1 F1 Q1 D1 C2/C3 Rs6 U13
'A2': (0.5, 57.5, 27.5, 74.0), # 辅助降压 U19 U30 L2 L3
'A6': (0.5, 75.5, 27.5, 97.5), # 风扇 J26 J27 + 驱动
'A3': (70.5, 40.0, 97.5, 97.5), # 控制 U20 ESP32 J33 …
'A5': (70.5, 0.5, 97.5, 38.5), # 上行 J23 USB-C + U31 LDO
'A4H': (28.5, _C[2][1], 69.5, _C[2][3]), # Hub U32 + Y1 + 去耦(走廊 2)
'A4M': (28.5, _C[3][1], 69.5, _C[3][3]), # U35 mux + U6/U7 DAC(走廊 3)
}
H_BOTTOM = 14.0 # 背面限高:底板下 ≥ 15 mm 铜柱(C2/C3 直立电解 12 mm)
# D± 在 L4 沿 x≈48 竖向成束(参考 L3);I²C / VDAC 沿 x≈37。只是布线意图,这里不做禁区。
DP_TRUNK_X = 48.0
I2C_TRUNK_X = 37.0
def via_bands():
u"""槽体正下方的过孔带(背面摆位禁区)。"""
return [slot_body(i) for i in range(1, SLOT_N + 1)]
# ── ESP32 天线缺口(东板边)────────────────────────────────────────────────────
# 西沿 90.5 → 90.8(2026-09-10):U20 焊盘外框东沿正好在 90.5,地脚 1 / 40 离缺口边只有 0.155 mm,
# DRC「板框到贴片焊盘」要 0.3。东移 0.3 之后天线根部下面多留 0.3 mm 板材,影响可忽略。
ANT_NOTCH = (90.8, 58.0, BW, 80.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),
}
def keepout_rects():
u"""背面摆位禁区:过孔带 + 天线缺口(安装孔另按 HOLE_KEEP 判)。"""
return via_bands() + [ANT_NOTCH]
# ── L1 的 +12V 铜(9 A 干路,§6.4 断面 ≥ 8 mm)─────────────────────────────
# 主块贴西板边,东沿停在外壳 GND 盘(x 28~31)以西;每槽每排一根指头去够 12V 针。
# 南排 12V 在 u −16.5/−15.5/−14.5,左右都没有异网针挡着(西边是外壳盘,只要不碰它的 y 段)
# ⇒ 从主块横着伸过去,上沿停在外壳盘下沿以南 V12_CLR。
# 北排 12V 在 u −15.5/−14.5,左边 u −16.5 是 GND ⇒ 帽子在排外横过去,再从东边下一颗牙。
V12_CLR = 0.20 # 到异网铜(规则底线 0.15,留 0.05)
V12_OUT = 2.0 # 指头伸出针排外沿多少(进走廊)
V12_MAIN = (EDGE_KEEP, EDGE_KEEP, None, BH - EDGE_KEEP) # 东沿在 l1_12v_rects() 里算
def l1_12v_rects():
u"""L1 +12V 铜:[主块] + 每槽 [南指, 北帽, 北牙]。全部同网,重叠处由铺铜合并。"""
c = V12_CLR
shell_x0 = pin_x(-SHELL_U) - SHELL_L / 2 # 外壳盘西沿 28.0
x_main = round(shell_x0 - c, 3)
out = [(V12_MAIN[0], V12_MAIN[1], x_main, V12_MAIN[3])]
row_o = PAD_ROW_DY + PAD_W / 2 # 针排外沿 4.6
x_e = round(pin_x(-13.5) - PAD_L / 2 - c, 3) # 东界:u −13.5 那只 GND 左沿 − 净距
x_tooth = round(pin_x(-16.5) + PAD_L / 2 + c, 3) # 北牙西界:北排 u −16.5 GND 右沿 + 净距
for i in range(1, SLOT_N + 1):
cy = slot_y(i)
y_shell = SHELL_W / 2 + c # 外壳盘上下沿 + 净距
out += [
(x_main - 0.3, round(cy - row_o - V12_OUT, 3), x_e, round(cy - y_shell, 3)),
(x_main - 0.3, round(cy + row_o + c, 3), x_e, round(cy + row_o + V12_OUT, 3)),
(x_tooth, round(cy + y_shell, 3), x_e, round(cy + row_o + c + 0.05, 3)),
]
return out
# ── 下游脚本要的空壳 ─────────────────────────────────────────────────────
# ── D16:L1 12V 大铜上扣出来、只归个别网的窗口(route_base 挡线图 / pour_base 铺铜共用)──────────
# 用户 2026-09-11 选:① J1(USB-C)的 CC 夹在两组 VBUS 之间,VIN_RAW 一合流就出不来 ⇒ 在两组 VBUS 之间
# 打孔上 L1、东去 U36 再下来(CC2 在南、CC1 在北,两根不交叉;CC2 东段压低到 y ≤ 8.95 是为了让开
# J1 东侧外壳长槽在 L1 上的焊盘,它到 y 8.26);② U37 两侧 VIN 经 L1 上的 VIN_FUSED 小岛连通(TI 做法)。
# 12V 主块在小岛这一段还剩 x 0.5~18.6(18 mm),CC 两根只切 J38 那一个槽的供电路径。
PD_ISLAND = (18.9, 13.4, 22.9, 29.2)
PD_L1 = [('PD_CC2', (10.3, 8.1, 12.9, 9.35)), ('PD_CC2', (12.9, 8.1, 20.4, 8.95)),
('PD_CC1', (13.2, 9.1, 20.4, 10.2)),
('VIN_FUSED', PD_ISLAND),
# 2026-09-12(第三批,用户重摆整窝之后):风扇那一窝的长途网借 L1 横穿 A6。
# A6 那一带 L1 整片是 12V 主块(x 0.5~27.8)⇒ 过孔上得去但走不动,等于单层,而 L4 已被 22 件占满。
# ⛔ 前两批走廊是按**旧摆位**开的(注释里写的 `R748.1(18.2,91.0)` / `R757.2(27.2,93.0)` 都是旧坐标),
# 用户 2026-09-12 重摆之后件全变了位置,走廊与件对不上 ⇒ 四条长途网必然布不通。已整体替换。
# ⚠ 走廊必须**盖住焊盘正上方**:只盖"沿途"不行,那只脚得先在 L4 挪到走廊下方才能打孔上来,
# 实测 J27.4 / Q10.3 就是这么卡住的(v1 只盖沿途 ⇒ 未完成 3;v2 盖住焊盘 ⇒ 未完成 2)。
# ⚠ 只挡**导线图**;铺铜那边不用开窗(L1 的 12V 铺铜本来就绕开异网导线,CC 两根走的同一路子)。
# 断面(`l1_section.py`,§6.4 要 ≥ 8.0):逐 y 最窄 **11.80 mm**(y=76.10)。
# 走廊 x 一律 ≤ 27.5,而槽指头在 x ≥ 27.5 ⇒ 不碰任何一槽的供电路径。
('FAN_EN', (12.0, 76.10, 27.50, 77.10)), # R748.1(5.13,73.91) 西段走 L4,东段借 L1 到窗缘
('FAN_TACH_IN', (16.0, 75.15, 27.50, 76.05)), # 主干端点 (26.00,75.62) 在窗缘
('FAN_TACH_IN', (13.7, 76.05, 16.20, 90.40)), # 竖带盖住 Q9.3(14.21,89.92) / R753.2(15.62,89.97)
('FAN_TACH_OUT', (17.4, 80.70, 27.50, 81.70)), # 盖住 Q10.3(18.02,81.15)
('FAN_TACH_OUT', (17.4, 81.70, 18.80, 84.10)), # 竖段接 R757.2(18.11,83.57)
('FAN_PWM_L', (10.3, 83.90, 22.50, 87.30))] # 盖住 J27.4(10.92,84.44) 与 Q6.3(22.10,87.36) # R733.1(14.0,94.1) 往东出区
def rect_minus(r, holes):
u"""矩形 r 减去若干矩形 → 不重叠的矩形列表。"""
out = [tuple(r)]
for h in holes:
nxt = []
for a in out:
if not (a[0] < h[2] and h[0] < a[2] and a[1] < h[3] and h[1] < a[3]):
nxt.append(a)
continue
if a[0] < h[0]:
nxt.append((a[0], a[1], h[0], a[3]))
if h[2] < a[2]:
nxt.append((h[2], a[1], a[2], a[3]))
x0, x1 = max(a[0], h[0]), min(a[2], h[2])
if a[1] < h[1]:
nxt.append((x0, a[1], x1, h[1]))
if h[3] < a[3]:
nxt.append((x0, h[3], x1, a[3]))
out = nxt
return out
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(x, y):
for k, z in ZONES.items():
if z[0] <= x <= z[2] and z[1] <= y <= z[3]:
return k
return None
def height_ok(h):
u"""背面限高(正面只有插槽)。"""
return h <= H_BOTTOM
def _overlap(a, b):
return a[0] < b[2] and b[0] < a[2] and a[1] < b[3] and b[1] < a[3]
def _gap(a, b):
u"""两个矩形的最小距离(重叠为 0)。"""
dx = max(b[0] - a[2], a[0] - b[2], 0.0)
dy = max(b[1] - a[3], a[1] - b[3], 0.0)
return math.hypot(dx, dy)
def selfcheck():
bad = []
ys = slot_ys()
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)
for i in range(1, SLOT_N + 1):
b = slot_body(i)
if b[0] < 2 or b[1] < 2 or b[2] > BW - 2 or b[3] > BH - 2:
bad.append(u'槽 %d 槽体 %s 贴板边 < 2 mm' % (i, b))
# 针位表:每排 32 针,12V 共 5 针(南 3 北 2),D± 在南排
for row in 'SN':
if len(PIN_ROWS[row]) != 32:
bad.append(u'%s 排 %d 针 != 32' % (row, len(PIN_ROWS[row])))
n12 = [(r, u) for r in 'SN' for u, t in PIN_ROWS[r].items() if t == '12V']
if len(n12) != 5:
bad.append(u'12V 针 %d 只 != 5' % len(n12))
# 12V 铜:每只 12V 针都被某块指头盖到;到任何异网针 / 外壳盘 ≥ V12_CLR
rects = l1_12v_rects()
for i in range(1, SLOT_N + 1):
for row in 'SN':
for u, t in PIN_ROWS[row].items():
p = pad_rect(i, row, u)
if t == '12V':
if not any(_overlap(p, r) for r in rects):
bad.append(u'槽 %d %s 排 u=%.1f 的 12V 针没被铜盖到' % (i, row, u))
else:
g = min(_gap(p, r) for r in rects)
if g < V12_CLR - 1e-6:
bad.append(u'12V 铜离槽 %d %s 排 u=%.1f(%s)只有 %.3f'
% (i, row, u, t, g))
for s in shell_rects(i):
g = min(_gap(s, r) for r in rects)
if g < V12_CLR - 1e-6:
bad.append(u'12V 铜离槽 %d 外壳盘只有 %.3f' % (i, g))
main = rects[0]
if main[2] - main[0] < 8.0:
bad.append(u'L1 12V 主块只有 %.1f mm 宽,9 A 要 ≥ 8' % (main[2] - main[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))
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' % (k, w))
# 背面分区:在板内、两两不重叠、不压过孔带
zs = sorted(ZONES.items())
for k, z in zs:
if z[0] < EDGE_KEEP - 1e-6 or z[1] < EDGE_KEEP - 1e-6 or \
z[2] > BW - EDGE_KEEP + 1e-6 or z[3] > BH - EDGE_KEEP + 1e-6:
bad.append(u'分区 %s 出板: %s' % (k, z))
for j, vb in enumerate(via_bands(), 1):
if _overlap(z, vb):
bad.append(u'分区 %s 压到槽 %d 过孔带' % (k, j))
for n, (ka, za) in enumerate(zs):
for kb, zb in zs[n + 1:]:
if _overlap(za, zb):
bad.append(u'分区 %s / %s 重叠' % (ka, kb))
if [USB_SLOT_REF[i] for i in range(1, SLOT_N + 1)] != list(SLOT_REFS):
bad.append(u'USB -> 插槽位号映射被改坏')
# 安装孔:不出板 / 不压槽体 / 不被圆角切 / 不落天线缺口
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))
hr = (hx - HOLE_KEEP, hy - HOLE_KEEP, hx + HOLE_KEEP, hy + HOLE_KEEP)
for i in range(1, SLOT_N + 1):
if _overlap(hr, slot_body(i)):
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 _overlap(hr, ANT_NOTCH):
bad.append(u'安装孔 %s 落进天线缺口' % n)
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"""板框 = 一条闭合折线;顺带删掉层 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' % (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, layers: await eda.pcb_Layer.getTheNumberOfCopperLayers(),"
" comps: (await eda.pcb_PrimitiveComponent.getAll()).length};" % PCB,
timeout=180)
bad = []
if r['layers'] != LAYERS:
bad.append(u'铜层数 %s != %d' % (r['layers'], LAYERS))
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'铜层 %s;安装孔读回 %d 个;板上元件 %d 件' % (r['layers'], len(got), r['comps']))
if bad:
print(u'[X] 校验不过:')
for b in bad:
print(u' ' + b)
else:
print(u'[OK] 4 层、板框是一条闭合折线、外接矩形对上、安装孔齐')
return bad
if __name__ == '__main__':
selfcheck()
print(u'底板 %.0f x %.0f,%d 层,四角 R%.0f;PCB=%s' % (BW, BH, LAYERS, CORNER_R, PCB))
print(u'槽阵 5 槽 @%.2f(长轴沿 x,rot %d,槽心 x %.1f): y = %s'
% (SLOT_PITCH, SLOT_ROT, SLOT_CX, u' / '.join('%.2f' % y for y in slot_ys())))
print(u'走廊: %s' % u' | '.join('%.2f~%.2f' % (c[1], c[3]) for c in corridors()))
print(u'背面分区(限高 %.0f mm):' % H_BOTTOM)
for k, z in sorted(ZONES.items()):
print(u' %-4s (%5.1f, %5.1f)-(%5.1f, %5.1f) %6.0f mm²'
% (k, z[0], z[1], z[2], z[3], (z[2] - z[0]) * (z[3] - z[1])))
r = l1_12v_rects()
print(u'L1 +12V:主块 x %.2f~%.2f(宽 %.1f)+ %d 块指头,指头东界 x=%.2f'
% (r[0][0], r[0][2], r[0][2] - r[0][0], len(r) - 1, r[1][2]))
if '--outline' in sys.argv:
push_outline()
verify()
elif '--holes' in sys.argv:
push_holes()
verify()
elif '--verify' in sys.argv:
verify()