- 板上与布线缓存同步删掉 J1.2 / J1.3 里误打的 12 个 GND 过孔(TH_KEYS 改名 bug 已在上一提交修) - 天线缺口西沿 90.5 → 90.8:U20 地脚 1/40 到缺口边 0.155 → 0.454 mm,板框已重画并校验 - PROGRESS 顶部加「下次从这里开始」:填铜、DRC 挖槽区域、37 个网手工补线的顺序 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
557 行
25 KiB
Python
557 行
25 KiB
Python
# -*- 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 +3V3 —— 整层 +3V3,不布线(2026-09-10 由 GND 改,D15)
|
||
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)}
|
||
|
||
|
||
def side_of(ref):
|
||
u"""器件放哪一面:五个插槽在 L1(TOP),其余一律 L4(BOTTOM)。"""
|
||
return TOP if ref in SLOT_REFS 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
|
||
|
||
|
||
# ── 下游脚本要的空壳 ─────────────────────────────────────────────────────
|
||
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()
|