# -*- coding: utf-8 -*- u"""自建「PCI-E x4 金手指」封装 —— R2 刀片 `J1`(`PCIE-X4-EDGE-64P`)的板边本身。 /c/Python314/python .claude/r2/fp_edge.py # 只打印几何 + 自检 /c/Python314/python .claude/r2/fp_edge.py --push # 建/重画封装 + 挂到器件上 + 读回校验 ## 几何出处(**不是凭记忆写的**,M2d 的教训) 主源:KiCad 官方封装库 `Connector_PCBEdge.pretty/BUS_PCIexpress_x4.kicad_mod` (其 descr 注明摘自 PCI-E CEM 的 add-in card edge-finger 图)。逐条交叉核对过: | 项 | 本封装 | 交叉核对 | |---|---:|---| | 间距 | **1.00 mm** | CEM / 立创资料 / KiCad 一致 | | 手指宽 | **0.70 mm**(净距 0.30) | 同上 | | 手指长 | **4.30 mm** | 中文资料常写 4.0,KiCad 4.3,同量级 | | 指尖到板边 | **1.30 mm** | multi-cb 给 PCIe 推荐 1.4;另一处给 0.6~1.5 窗口 ⇒ 1.3 在窗口内 | | 缺口(key) | 11 与 12 号针之间,宽 ~~1.9~~ **1.6**(2026-09-11 改 CEM 值),深 ~~7.45~~ **8.3**(2026-09-12 加深,见 `NOTCH_D` 旁注),口部两个 0.5×45° 导入角 | KiCad x1 / x4 两个封装都是 1.9(到 11/12 号指只剩 0.2);深度不跟 KiCad,按防呆键筋全高 8.00 + 0.3 余量定 | | A1 / B31 短指 | 长 **3.2**,指尖再退 **1.10** | CEM 的 PRSNT# 语义:**最后接触**,插到底才认在位 | | A 排 / B 排 | A = **底层**,B = **顶层** | 同 KiCad | ⚠ **A1 / B31 必须是短指**:本方案的在位检测就是靠它俩(底板 A1=GND、B31=PRSNT{n}#, 刀片上短接)。做成等长指,刀片插一半就报「已插入」,而这一类错误功能测试测不出来。 ## ⛔ T3 的「20° 斜边」在嘉立创下不了单 嘉立创《金手指斜边设计注意事项》写明**只有 30° 和 45°,并建议优先 30°**。 20° 是 PCI/PCIe 的行业标准角(德系板厂 multi-cb 给的就是 20°),但**这一版要在嘉立创打**, ⇒ 方案文档 §5.3 / §21.3 的「20°/1.0 mm」改成 **30°**,深度由板厂按指长配。 指尖留了 1.30 mm 裸边,30° 斜边吃不到镀层(这正是那 1.3 mm 的用途)。 嘉立创另外两条硬要求,本封装已满足 / 已记: · **金手指处必须开通窗**(阻焊开窗)—— 焊盘自带开窗,且本封装不在指区放任何丝印。 · 斜边板单片长宽 ≥ 50 mm —— 刀片 98×76,过。 ## 板框:缺口画在封装里,刀片外框在 M4 的 PCB 里画 封装在 **BOARD_OUTLINE(11)** 层只出**缺口那一段**(两条竖线 + 顶部半圆 + 口部两个导入角), 刀片 98×76 的外框在 PCB 文档里画,**底边要在缺口口部断开**,两段拼成一个闭合外框。 ⇒ 缺口忘不掉(跟着封装走),而外框尺寸仍归 PCB。断点坐标从本模块的 `notch_mouth()` 取, 不要手抄。 ## 坐标系 原点 = **金手指阵列 X 中心 × 卡的底边**。手指在 **+Y 方向**(板边 y=0,往板内长)。 ⚠ 实测:EasyEDA PCB / 封装编辑器里 **y 向上为正**,所以 +Y 就是「板内」。截图为证(§M4a)。 `PIN_X[n]`(mm)是相对原点的 X;板边是 y=0。 """ import json import math import os import sys sys.stdout.reconfigure(encoding='utf-8') HERE = os.path.dirname(os.path.abspath(__file__)) # ⚠ `.claude` 下有和 r2 同名的 R1 脚本(pcb_pour / pcb_rules / pcb_place)。 # 只插父目录的话,**后面任何一个 `import pcb_rules` 都会拿到 R1 那一份**, # 而且不报错,只在调到 R2 专有的函数时才炸(2026-09-08 踩过)。 # 规矩:两行都要写,HERE 后插(在 sys.path 最前)。 sys.path.insert(0, os.path.dirname(HERE)) sys.path.insert(0, HERE) from eda import run # noqa: E402 LIB_ME = '543725e8f0454017a5691e7e570aeef6' # 个人库 FP_NAME = 'EDGE-64P-P1.00_PCIE-X4-GF' DEV_NAME = 'PCIE-X4-EDGE-64P' MIL = 39.3700787401575 # ── 几何常量(mm,出处见表头)──────────────────────────────────────── PITCH = 1.0 FW = 0.7 # 手指宽 LONG_L, SHORT_L = 4.3, 3.2 EDGE_CLR = 1.3 # 指尖到卡底边 SHORT_BACK = LONG_L - SHORT_L # 短指指尖再退 1.1 # ⛔ 2026-09-11 晚 用户拍板:缺口宽 1.9 → **1.6**(PCIe CEM 值)。1.9(KiCad 封装值)时缺口边到 11/12 号指铜边 # 只有 (3.0 − 1.9)/2 − 0.35 = 0.2 mm,DRC 报 4 条「板框到焊盘 < 0.3」(方案 §27.8);1.6 ⇒ 0.35。 # 代价:插槽防呆筋的配合余量少 0.15(CEM 的卡本来就是 1.6)。 # ⛔ 2026-09-12 晚 用户拍板:缺口深 7.45 → **8.3**。原因是 7.45 < 防呆键筋全高 8.00 ⇒ 卡往下插时 # 缺口封闭端先顶在键筋顶上,卡底停在 z=2.55、离槽底 2.000 差 0.55 ⇒ **卡是坐在一条塑料防呆筋上, # 不是坐在槽底**(电气无碍:触点 z 6.18~7.12 被完全盖住,纯粹是受力点)。8.3 = 模型槽底 8.00 再留 # 0.3 余量,防实测键筋高于标称。⚠ 8.00 是厂商 STEP 标称值、不含公差,卡尺实测仍是权威口径。 # 净空已实测:加深段 x 39.2~40.8 / y 7.45~8.30 不碰任何铜(最近的是 x=41.5 的 GND 孔,孔边离 # 侧壁 0.395;x=38.5 的 EFUSE_EN/INT# 竖线 0.60;A11/A12/B11/B12 四指 0.350 —— 加深只往 y 长、 # x 一动不动,这三个数都不变)。连带:pcb_pour.notch_cut() 的内层避让顶端 7.95 → 8.8,要重铺内层。 # ⛔⛔ 2026-09-12 深夜 用户拍板:缺口宽 **1.6 → 1.9**(回到 CEM / KiCad 标准值)。 # ## 为什么必须回去:1.6 那版**插不进插槽** # 厂商 STEP(SOFNG PCIE-64P11L)量出防呆键筋**根部宽 1.78**(竖壁 x −5.890/−4.110, # 中心 −5.000 与 NOTCH_CX 独立吻合;顶上 0.5 是 45° 导入角,故顶端 1.78−1.0 = 0.78, # 与文档记的「顶端 0.78」正好对上)。缺口 1.6 < 键筋 1.78 ⇒ 卡只能套住键筋顶端, # 往下插到 **z=9.59** 就被撑住,离槽底 2.000 差 **7.59 mm** —— 不是勉强,是根本插不进。 # ## 1.9 才是标准值,0.2 的铜边距是 PCIe 几何定死的 # KiCad 官方 x1/x4 两个封装缺口都是 1.9;方案 §27.8 原话:「1.0 间距 / 0.7 指宽 / 1.9 缺口宽 # ⇒ 缺口边到 11、12 号指的铜边只有 0.2 mm,**任何 PCIe 卡都是这个数**」。 # ⇒ 2026-09-11 那次把它改成 1.6 是**基于错误信息的决定**:当时写作「CEM 的 1.6」(CEM 其实是 1.9), # 且代价只估成「防呆筋配合余量少 0.15」,没意识到会导致插不进。 # ## 代价(用户已拍板接受) # 刀片 DRC 会多 **4 条**「板框到焊盘 < 0.3」,全部集中在这一处。 # ⇒ **下单时必须向嘉立创确认「金手指缺口处铜到板边 0.2 mm 可做」**(§27.8 给的就是这条路)。 # (另一条路是把 A11/A12/B11/B12 四根指改窄到 0.50 换回 DRC 0 条 —— 那四根是 # EFUSE_EN / INT# / GND×2,都不走电流,但会偏离 CEM 标准指宽。用户选了保标准指宽。) NOTCH_W, NOTCH_D = 1.9, 8.3 # 缺口宽 / 深(自板边算) LEAD_IN = 0.5 # 缺口口部 45° 导入角 # 封装丝印那一行的 y。⛔ 2026-09-12 深夜由写死的 `yb + 1.6`(=7.20) 改成跟着缺口走: # 丝印从 x 28.5 一路排到 ~44.1,**必然横穿缺口竖槽 x 39.20~40.80**,而缺口是铣穿的 # ⇒ 字会被切掉一段,偏偏这句是「别插进真 PCIe 插槽」的警告。7.45 时就已经重叠 0.75 # (不是加深引入的,加深只是把它恶化到 1.00)。抬到 NOTCH_D + 0.6 = 8.90 ⇒ 字带 8.40~9.40, # 比缺口顶高 0.10;上方仍远低于 ZONES['finger'] 顶 12.0 与外层铜下沿 TOP_POUR_Y 11.5。 SILK_Y = round(NOTCH_D + 0.6, 3) SILK_SIZE = 1.0 N_PER_SIDE = 32 SHORT_PINS = ('A1', 'B31') # PRSNT#:最后接触 GAP_AFTER = 11 # 11 号针之后空两格(缺口) ARC_SEG = 12 # 缺口顶部半圆用几段折线近似(见 outline_segments 的说明) def _x_raw(n): u"""第 n 号针在「以 1 号针为 0」的原始坐标(mm)。""" return (n - 1) + (2.0 if n > GAP_AFTER else 0.0) X_CENTER = (_x_raw(1) + _x_raw(N_PER_SIDE)) / 2.0 # 16.5 PIN_X = dict() for _n in range(1, N_PER_SIDE + 1): PIN_X['A%d' % _n] = PIN_X['B%d' % _n] = round(_x_raw(_n) - X_CENTER, 4) NOTCH_CX = round((_x_raw(GAP_AFTER) + _x_raw(GAP_AFTER + 1)) / 2.0 - X_CENTER, 4) # -5.0 LAYER = {'A': 2, 'B': 1} # A 排 = 底层,B 排 = 顶层 def pad_geom(pin): u"""(x, y_center, w, h) mm。**y 为正 = 往板内**(实测:PCB 编辑器里 y 向上为正, 见 §M4a 的截图判据;板边在 y=0,手指从板边往板内长)。""" h = SHORT_L if pin in SHORT_PINS else LONG_L tip = EDGE_CLR + (SHORT_BACK if pin in SHORT_PINS else 0.0) return PIN_X[pin], (tip + h / 2.0), FW, h def notch_mouth(): u"""缺口在板边(y=0)上的两个断点 x(mm)—— 刀片外框底边要在这两点断开。""" half = NOTCH_W / 2.0 + LEAD_IN return round(NOTCH_CX - half, 4), round(NOTCH_CX + half, 4) def outline_segments(): u"""缺口的板框线段 [(x1,y1,x2,y2), ...] + 半圆 (cx, cy, r)。""" hw = NOTCH_W / 2.0 ml, mr = notch_mouth() yl = LEAD_IN # 导入角结束的高度 top = NOTCH_D - hw # 顶部圆弧的圆心 y segs = [(ml, 0.0, NOTCH_CX - hw, yl), # 左导入角 (NOTCH_CX - hw, yl, NOTCH_CX - hw, top), (mr, 0.0, NOTCH_CX + hw, yl), # 右导入角 (NOTCH_CX + hw, yl, NOTCH_CX + hw, top)] # 顶部半圆用 ARC_SEG 段折线近似 —— **不用 pcb_PrimitiveArc**: # 实测它 create() 返回成功、画面上也画出来了,但 getAll()/getAllPrimitiveId() 一律 # 返回空 ⇒ 既读不回来校验、也删不掉(重画时会越堆越多)。折线弦高误差 # r*(1-cos(90/ARC_SEG deg)) < 0.01 mm,机械上无所谓,换来「可读回、可幂等重画」。 pts = [] for k in range(ARC_SEG + 1): a = math.pi * k / ARC_SEG # 0..pi,从右到左 pts.append((NOTCH_CX + hw * math.cos(a), top + hw * math.sin(a))) for k in range(ARC_SEG): segs.append((pts[k][0], pts[k][1], pts[k + 1][0], pts[k + 1][1])) return segs def selfcheck(): assert len(PIN_X) == 64, len(PIN_X) assert PIN_X['A1'] == -16.5 and PIN_X['A32'] == 16.5, (PIN_X['A1'], PIN_X['A32']) assert PIN_X['A11'] == -6.5 and PIN_X['A12'] == -3.5, u'11/12 之间必须空两格' assert NOTCH_CX == -5.0, NOTCH_CX ml, mr = notch_mouth() assert round(mr - ml, 4) == round(NOTCH_W + 2 * LEAD_IN, 4), (ml, mr) # 缺口的直段(不含口部导入角)到相邻两指的铜边距。口部的 45° 导入角在 y=0~+0.5, # 而指尖在 y=+1.3 才开始,两者在 Y 上错开 0.8 ⇒ 只需校核直段的 X 间距。 hw = NOTCH_W / 2.0 cl = round(NOTCH_CX - hw - (PIN_X['A11'] + FW / 2.0), 4) cr = round((PIN_X['A12'] - FW / 2.0) - (NOTCH_CX + hw), 4) # ⛔ 门槛 2026-09-12 深夜由 0.3 放到 **0.2**:0.2 是 PCIe 几何本身定死的 # (1.0 间距 / 0.7 指宽 / 1.9 缺口 ⇒ (3.0−1.9)/2 − 0.35 = 0.2),**任何 PCIe 卡都是这个数**, # KiCad 官方 x1/x4 封装同样是 0.2。0.3 是本项目自设的更严要求,而按它反推缺口只能 ≤1.70, # 装不下厂商键筋 1.78 ⇒ 窗口为空。⇒ 放回标准值,DRC 那 4 条下单时与嘉立创确认。 assert cl == cr >= 0.2, u'缺口到 11/12 号指的铜边距 %s / %s < 0.2(PCIe 几何下限)' % (cl, cr) assert EDGE_CLR - LEAD_IN >= 0.8, u'口部导入角会切进指尖' for p in SHORT_PINS: x, y, w, h = pad_geom(p) # 远端(板内那头)必须和长指齐平,指尖(板边那头)必须比长指退 SHORT_BACK assert abs((y + h / 2.0) - (EDGE_CLR + LONG_L)) < 1e-9, u'%s 的远端没对齐' % p assert abs((y - h / 2.0) - (EDGE_CLR + SHORT_BACK)) < 1e-9, p # 长指本身 x, y, w, h = pad_geom('B1') assert abs((y - h / 2.0) - EDGE_CLR) < 1e-9, u'长指指尖不在 1.3' # 缺口比手指深,否则插槽的防呆筋顶到板子 assert NOTCH_D > EDGE_CLR + LONG_L, u'缺口比手指还浅' # ⛔ 丝印不许跨缺口(2026-09-12 深夜补)。原来只断言了「不许落进指区 y<=5.6」, # 而丝印在 7.20、缺口顶 7.45 —— 两个数都过了指区那条断言,却**实际重叠 0.75**, # 几何验证全程不报,是截图才看出来的。这条按「字带下沿 vs 缺口顶」判。 assert SILK_Y - SILK_SIZE / 2.0 >= NOTCH_D, \ u'丝印带下沿 %.2f 低于缺口顶 %.2f ⇒ 会被铣掉一段' % (SILK_Y - SILK_SIZE / 2.0, NOTCH_D) return True def mil(v): return round(v * MIL, 4) 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 _find_once(name=FP_NAME): r = call("const r = await eda.lib_Footprint.search('%s', '%s', undefined, 20, 1);" "return (r||[]).map(f => [f.uuid, f.name]);" % (name, LIB_ME)) for u, n in r: if n == name: return u return None def find_fp(retries=6): import time for _i in range(retries): u = _find_once() if u: return u time.sleep(2) return None def body_js(): u"""画封装的 JS 语句串。""" js = [] # 64 个手指。助焊(锡膏)扩展给一个大负数,想让钢网在金手指区不开口。 # ⚠ 实测客户端把它**夹到 -1.37795 mil(-0.035 mm)**,关不死 ⇒ 光靠封装做不到, # 「金手指区钢网不开口」必须在下单时另外说明(同 fp_keypad.py 那颗接触盘)。 paste = "{topPasteMask:%s,bottomPasteMask:%s}" % (mil(-FW / 2.0), mil(-FW / 2.0)) for row in ('A', 'B'): for n in range(1, N_PER_SIDE + 1): pin = '%s%d' % (row, n) x, y, w, h = pad_geom(pin) js.append("await eda.pcb_PrimitivePad.create(%d,'%s',%s,%s,0,['RECT',%s,%s,0]," "undefined,null,undefined,undefined,undefined,undefined,undefined," "undefined,%s);" % (LAYER[row], pin, mil(x), mil(y), mil(w), mil(h), paste)) # 板框层:缺口(含顶部折线半圆) segs = outline_segments() for x1, y1, x2, y2 in segs: js.append("await eda.pcb_PrimitiveLine.create('',11,%s,%s,%s,%s,%s);" % (mil(x1), mil(y1), mil(x2), mil(y2), mil(0.15))) # 文档层:卡底边基准线(板框由 PCB 画,这里只给一条看得见的线) ml, mr = notch_mouth() js.append("await eda.pcb_PrimitiveLine.create('',13,%s,%s,%s,%s,%s);" % (mil(-X_CENTER - 2), 0, mil(ml), 0, mil(0.1))) js.append("await eda.pcb_PrimitiveLine.create('',13,%s,%s,%s,%s,%s);" % (mil(mr), 0, mil(X_CENTER + 2), 0, mil(0.1))) # 顶/底装配层:手指区外框,摆位时看得见 yb = EDGE_CLR + LONG_L for lay in (9, 10): for x1, y1, x2, y2 in [(-X_CENTER - FW / 2, yb, X_CENTER + FW / 2, yb), (-X_CENTER - FW / 2, yb, -X_CENTER - FW / 2, 0), (X_CENTER + FW / 2, yb, X_CENTER + FW / 2, 0)]: js.append("await eda.pcb_PrimitiveLine.create('',%d,%s,%s,%s,%s,%s);" % (lay, mil(x1), mil(y1), mil(x2), mil(y2), mil(0.1))) # 丝印:只准放在手指区**上方**(指区里一点丝印都不能有) # create(layer, x, y, text, fontFamily, fontSize, lineWidth, ...) js.append(silk_js()) return ''.join(js) SILK_TEXT = 'NOT PCIe - PhoneRail blade' def silk_js(): u"""丝印那一行的 JS(`body_js` 与 `push_silk` 共用,别写两份)。""" return ("await eda.pcb_PrimitiveString.create(3,%s,%s,'%s',undefined,%s,%s);" % (mil(-X_CENTER), mil(SILK_Y), SILK_TEXT, mil(SILK_SIZE), mil(0.15))) def push(): fp = find_fp(retries=2) if not fp: desc = (u'PCI-E x4 金手指(刀片板边,非采购件)。64 指 / 1.0mm / 0.7 宽 / 长指 4.3 / ' u'指尖离板边 1.3 / A1,B31 短指 3.2。缺口在板框层。' u'斜边 30 度(嘉立创只有 30/45)、电镀硬金、板厚 1.6。') call("await eda.lib_Footprint.create('%s','%s',undefined,%s);return 1;" % (LIB_ME, FP_NAME, json.dumps(desc, ensure_ascii=False))) fp = find_fp() assert fp, u'封装建了但搜不到' print(u'封装已建: %s %s' % (FP_NAME, fp)) else: print(u'封装已存在: %s %s,重画' % (FP_NAME, fp)) r = call("await eda.lib_Footprint.openInEditor('%s','%s');" "await new Promise(r=>setTimeout(r,2500));" "for (const k of ['pcb_PrimitivePad','pcb_PrimitiveLine','pcb_PrimitiveArc'," " 'pcb_PrimitiveString','pcb_PrimitiveRegion']) {" " const o = await eda[k].getAllPrimitiveId(); if (o && o.length) await eda[k].delete(o); }" "%s" "await eda.pcb_Document.save();" "return {pads:(await eda.pcb_PrimitivePad.getAllPrimitiveId()).length," " lines:(await eda.pcb_PrimitiveLine.getAllPrimitiveId()).length," " arcs:(await eda.pcb_PrimitiveArc.getAllPrimitiveId()||[]).length};" % (fp, LIB_ME, body_js()), timeout=300) print(u'已画: %s' % r) return fp def verify(fp=None): fp = fp or find_fp() assert fp, u'找不到封装' v = call("await eda.lib_Footprint.openInEditor('%s','%s');" "await new Promise(r=>setTimeout(r,2000));" "return {pads:(await eda.pcb_PrimitivePad.getAll()||[]).map(p=>({n:p.padNumber," " x:p.x,y:p.y,layer:p.layer,pad:p.pad,exp:p.solderMaskAndPasteMaskExpansion}))," " lines:(await eda.pcb_PrimitiveLine.getAll()||[]).map(l=>({layer:l.layer," " x:l.startX,y:l.startY,x2:l.endX,y2:l.endY}))," " arcs:(await eda.pcb_PrimitiveArc.getAll()||[]).map(a=>({layer:a.layer," " x:a.startX,y:a.startY,x2:a.endX,y2:a.endY,ang:a.arcAngle}))," " strs:(await eda.pcb_PrimitiveString.getAll()||[]).map(s=>({layer:s.layer," " x:s.x,y:s.y,t:s.text}))};" % (fp, LIB_ME), timeout=180) bad = [] got = {} for p in v['pads']: got[p['n']] = (round(p['x'] / MIL, 3), round(p['y'] / MIL, 3), p['layer'], p['pad']) if len(got) != 64: bad.append(u'焊盘数 %d != 64' % len(got)) for row in ('A', 'B'): for n in range(1, N_PER_SIDE + 1): pin = '%s%d' % (row, n) if pin not in got: bad.append(u'缺 %s' % pin) continue gx, gy, glay, gpad = got[pin] ex, ey, ew, eh = pad_geom(pin) if abs(gx - ex) > 0.01 or abs(gy - ey) > 0.01: bad.append(u'%s 坐标 (%.3f,%.3f) != (%.3f,%.3f)' % (pin, gx, gy, ex, ey)) if glay != LAYER[row]: bad.append(u'%s 在 layer %s,应为 %d' % (pin, glay, LAYER[row])) gw, gh = round(gpad[1] / MIL, 3), round(gpad[2] / MIL, 3) if abs(gw - ew) > 0.01 or abs(gh - eh) > 0.01: bad.append(u'%s 尺寸 %.2fx%.2f != %.2fx%.2f' % (pin, gw, gh, ew, eh)) nout = len([l for l in v['lines'] if l['layer'] == 11]) want = 4 + ARC_SEG if nout != want: bad.append(u'板框层线段 %d != %d' % (nout, want)) # 板框缺口必须是一条首尾相接的开口折线:只有口部两个端点落在 y=0 ends = {} for l in v['lines']: if l['layer'] != 11: continue for kx, ky in ((l['x'], l['y']), (l['x2'], l['y2'])): k = (round(kx / MIL, 3), round(ky / MIL, 3)) ends[k] = ends.get(k, 0) + 1 loose = sorted([k for k, c in ends.items() if c == 1]) ml, mr = notch_mouth() if len(loose) != 2 or abs(loose[0][0] - ml) > 0.01 or abs(loose[1][0] - mr) > 0.01 or abs(loose[0][1]) > 0.01 or abs(loose[1][1]) > 0.01: bad.append(u'缺口折线不闭合/端点不在板边: %s' % (loose,)) # 丝印一个字都不许落进指区(0 <= y <= EDGE_CLR+LONG_L 就是指区)。 # 封装名那一条是编辑器自带的属性串,不算丝印。 for st in v.get('strs', []): sy = st['y'] / MIL if st.get('t') in (FP_NAME, None): continue if st['layer'] in (3, 4) and -0.01 <= sy <= EDGE_CLR + LONG_L + 0.01: bad.append(u'丝印 "%s" 落进金手指区 (y=%.2f)' % (st.get('t'), sy)) ys = [got[p][1] for p in got] print(u'读回: %d 焊盘 / %d 线 / %d 弧 / %d 丝印串;y 范围 %.2f ~ %.2f' % (len(got), len(v['lines']), len(v['arcs']), len(v.get('strs', [])), min(ys), max(ys))) for st in v.get('strs', []): print(u' 串: layer=%s (%.2f,%.2f) "%s"' % (st['layer'], st['x'] / MIL, st['y'] / MIL, st.get('t'))) if bad: print(u'[X] 校验不过:') for b in bad[:20]: print(u' ' + b) else: print(u'[OK] 六十四指逐脚对上(坐标 / 层 / 尺寸),板框缺口 %d 段折线、端点落在板边' % want) return bad # 已知的两份封装:个人库原件 + **工程库副本**(PCB 上的 J1 引用的是后者,`getState_Footprint()` 读出来的)。 # ⚠ `lib_Footprint.search` 在 2026-09-11 晚的客户端上直接抛「o[r].match is not a function」⇒ 不靠搜索,按 uuid 开。 FP_COPIES = [('569096fb8fed4743b85c69f4b6581ead', '543725e8f0454017a5691e7e570aeef6'), # 个人库 ('a1ad3deadadbf758', '05a65c22f14846b8b95360ef8d3c1e2e')] # 工程库(J1 实例指向它) def push_notch(fp, lib): u"""**只重画板框层(11)的缺口线**,焊盘 / 丝印一个不动(J1 实例的焊盘网络按脚号挂着,别去碰)。""" segs = outline_segments() body = ''.join("await eda.pcb_PrimitiveLine.create('',11,%s,%s,%s,%s,%s);" % (mil(x1), mil(y1), mil(x2), mil(y2), mil(0.15)) for x1, y1, x2, y2 in segs) r = call("await eda.lib_Footprint.openInEditor('%s','%s');" "await new Promise(r=>setTimeout(r,2500));" "const old=(await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11).map(l=>l.primitiveId);" "if (old.length) await eda.pcb_PrimitiveLine.delete(old);" "%s" "await eda.pcb_Document.save();" "const L=(await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11);" "return {del: old.length, now: L.length, pads: (await eda.pcb_PrimitivePad.getAllPrimitiveId()).length," " xs: L.map(l=>[l.startX,l.endX]).flat()};" % (fp, lib, body), timeout=300) xs = [v / MIL for v in r['xs']] ok = r['now'] == len(segs) and r['pads'] == 64 and \ abs(min(xs) - notch_mouth()[0]) < 0.01 and abs(max(xs) - notch_mouth()[1]) < 0.01 print(u'%s @ %s:删 %d / 现 %d 段(应 %d),焊盘 %d,缺口 x %.3f~%.3f(应 %s)%s' % (fp[:8], lib[:8], r['del'], r['now'], len(segs), r['pads'], min(xs), max(xs), notch_mouth(), u' [OK]' if ok else u' [X]')) return ok def push_silk(fp, lib): u"""**只重画丝印层(3/4)的字符串**,焊盘 / 板框一个不动 —— 同 `push_notch` 的道理: `push()` 会把封装里所有图元删光重建,而 J1 实例的 64 个焊盘挂着网络,不能碰。 ⚠ 编辑器自带的**封装名属性串**也在字符串图元里(`verify()` 里按 `t in (FP_NAME, None)` 跳过它)。这里删的时候同样要避开,否则封装名会被一起删掉。 """ r = call("await eda.lib_Footprint.openInEditor('%s','%s');" "await new Promise(r=>setTimeout(r,2500));" "const all = await eda.pcb_PrimitiveString.getAll()||[];" "const old = all.filter(s=>[3,4].includes(s.layer) && s.text!=='%s')" ".map(s=>s.primitiveId);" "if (old.length) await eda.pcb_PrimitiveString.delete(old);" "%s" "await eda.pcb_Document.save();" "const now = (await eda.pcb_PrimitiveString.getAll()||[])" ".filter(s=>[3,4].includes(s.layer)).map(s=>[s.x,s.y,s.text]);" "return {del: old.length, now," " pads: (await eda.pcb_PrimitivePad.getAllPrimitiveId()).length," " lines: (await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>l.layer===11).length};" % (fp, lib, FP_NAME, silk_js()), timeout=300) got = [(round(x / MIL, 3), round(y / MIL, 3), t) for x, y, t in r['now'] if t != FP_NAME] ok = (len(got) == 1 and abs(got[0][1] - SILK_Y) < 0.01 and got[0][2] == SILK_TEXT and r['pads'] == 64 and r['lines'] == 4 + ARC_SEG) print(u'%s @ %s:删 %d / 现 %s;焊盘 %d(应 64)、板框 %d 段(应 %d)%s' % (fp[:8], lib[:8], r['del'], got, r['pads'], r['lines'], 4 + ARC_SEG, u' [OK]' if ok else u' [X]')) return ok def bind(): u"""把封装挂到器件 PCIE-X4-EDGE-64P 上。""" fp = find_fp() dev = call("const r = await eda.lib_Device.search('%s','%s',undefined,undefined,20,1);" "return (r||[]).map(d=>[d.uuid,d.name]);" % (DEV_NAME, LIB_ME)) du = None for u, n in dev: if n == DEV_NAME: du = u assert du, u'找不到器件 %s' % DEV_NAME # ⚠ modify 的位置参数是 (uuid, lib, name, classification, association, description, property) # —— 早先 dev_edge.py 把 association 写到了第 6 位(description),所以「改不动」。 call("await eda.lib_Device.modify('%s','%s',undefined,undefined," "{footprint:{uuid:'%s',libraryUuid:'%s'}});return 1;" % (du, LIB_ME, fp, LIB_ME), timeout=120) # 顺手把「进 BOM」关掉:property.addIntoBom 是 **boolean**,dev_edge.py 传的是字符串 'no' call("await eda.lib_Device.modify('%s','%s',undefined,undefined,undefined,undefined," "{addIntoBom:false,addIntoPcb:true});return 1;" % (du, LIB_ME), timeout=120) g = call("const d = await eda.lib_Device.get('%s','%s');" "return {fp:(d.association&&d.association.footprint)||null," " sym:(d.association&&d.association.symbol)||null," " bom:(d.property&&d.property.addIntoBom)};" % (du, LIB_ME)) print(u'器件关联: %s' % json.dumps(g, ensure_ascii=False)) return g if __name__ == '__main__': selfcheck() print(u'自检过。X 中心 %.2f,缺口中心 %.2f,口部断点 %s' % (X_CENTER, NOTCH_CX, notch_mouth())) for p in ('A1', 'B1', 'A11', 'A12', 'B31', 'A32'): print(u' %-4s x=%7.3f y=%7.3f %.2fx%.2f' % ((p,) + pad_geom(p))) print(u' 丝印 "%s" y=%.2f ⇒ 字带 %.2f~%.2f(缺口顶 %.2f)' % (SILK_TEXT, SILK_Y, SILK_Y - SILK_SIZE / 2, SILK_Y + SILK_SIZE / 2, NOTCH_D)) if '--notch' in sys.argv: # 2026-09-11 晚:缺口改 1.6 用这条,只动板框层 oks = [push_notch(f, l) for f, l in FP_COPIES] sys.exit(0 if all(oks) else 1) if '--silk' in sys.argv: # 2026-09-12 深夜:丝印抬过缺口顶,只动丝印层 oks = [push_silk(f, l) for f, l in FP_COPIES] sys.exit(0 if all(oks) else 1) if '--push' in sys.argv: fp = push() verify(fp) bind() elif '--verify' in sys.argv: verify()