# -*- 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()