- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base) - 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base) - 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、 东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base) - 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面 - 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、 J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder 开关(route_base、remote_route.ps1);剩 37 个网待手工补 - 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base); 已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查) - 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
916 行
43 KiB
Python
916 行
43 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""R2 底板 PCB 摆位(M5,4 层 98 × 98,D15):五个插槽在 L1,其余 133 件全部翻到 L4。
|
||
|
||
/c/Python314/python .claude/r2/place_base.py # 只算 + 自检 + 体检,不下发
|
||
/c/Python314/python .claude/r2/place_base.py --push # 下发(含翻面)+ 读回校验
|
||
/c/Python314/python .claude/r2/place_base.py --verify # 只读回校验
|
||
/c/Python314/python .claude/r2/place_base.py --dump # 重抓焊盘 _base_pads.json
|
||
/c/Python314/python .claude/r2/place_base.py --bbox # 重抓封装外框 _base_bbox.json
|
||
/c/Python314/python .claude/r2/place_base.py --native # 由板上焊盘 + 位姿重建 _base_native_pads.json
|
||
|
||
2 层版(南带 / 西区 / 北带、ROUTE_FIX / USER_INPUT)整份在 `archive_base2L/place_base.py`。
|
||
|
||
## 背面镜像(2026-09-10 实测:U13 翻面逐 rot 读焊盘)
|
||
|
||
封装原生偏移 (dx, dy) 在底层变成 **R(rot)·(dx, −dy)**:
|
||
rot 0 → (dx, −dy);90 → (dy, dx);180 → (−dx, dy);270 → (−dy, −dx)。
|
||
`pad_off()` 就按这一条算 —— 卫星件瞄的是**翻面之后真实的脚位**,不是芯片质心。
|
||
原生偏移表 `_base_native_pads.json` 由「板上焊盘 + 板上位姿」反推(`--native`),下发后要重建。
|
||
|
||
## 朝向(几何验证抓不到、错了要重画的那几条)—— `verify()` 用板上读回焊盘逐条断言
|
||
|
||
1. **U32 底层 rot 270**:下行口朝**北**,西 → 东 = USB1 … USB5;XTAL / 上行 / USB6 在**西**侧;
|
||
strap 在南;VDD18 在东。USB1/USB2 直接北上进 J34/J35;USB3~5 从北沿出来往东,
|
||
绕 U32 东侧(那一边除 VDD18 / 一只 3V3 外全是 NC)南下。
|
||
上行 / USB6 在西侧打孔上 L1,从 U32 顶上往东走(L1 走廊是空的),不和 D± 同层交叉。
|
||
⛔ 顶层那种「朝西、1 脚在北」**底层做不出来**:底层 rot 0 = 朝西但 1 脚在南(五对全交叉),
|
||
rot 180 = 朝东但晶振 / 上行在北沿,北沿到槽 J35 只剩 0.9 mm。
|
||
2. **U20 底层 rot 90**:天线(封装 +y 端)朝东伸进缺口;EFUSE_EN1~5 / PRSNT5# 那排朝西对着槽阵。
|
||
3. **J1 底层 rot 90** 桶口朝南;**J23 底层 rot 180** 口朝南、信号脚朝北。
|
||
4. **Q1 底层 rot 180**:VIN_FUSED 排在南(进)、VIN_SW 排在北(出),9 A 顺着 +y 走。
|
||
|
||
## D± 长度 → Hub 放走廊 2(2026-09-10 用户拍板)
|
||
|
||
分区草图原本 Hub 在走廊 3、mux 在走廊 2:USB3~5 三对都要绕 U32 东侧,最远 USB5 ≈ 80 mm。
|
||
对调之后只有 USB4/5 绕行,估算 45 / 23 / 3 / 40 / 58 mm。方案 §6.3 的 45 mm 只有把 U32
|
||
放进中间槽正下方的过孔带才做得到(没选);2 层版是 ~70 mm。`report()` 每次都打出估算值。
|
||
上面第 1 条写的「USB1/USB2 北上」是走廊 3 的情形;走廊 2 时北上的是 USB1~3,绕行的是 USB4/5。
|
||
"""
|
||
import io
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
sys.stdout.reconfigure(encoding='utf-8')
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, os.path.dirname(HERE))
|
||
sys.path.insert(0, HERE)
|
||
from eda import run # noqa: E402
|
||
import pcb_base as B # noqa: E402
|
||
|
||
PCB = B.PCB
|
||
MIL = B.MIL
|
||
BBOX = os.path.join(HERE, '_base_bbox.json')
|
||
NETS = os.path.join(HERE, '_base_pcb_nets.json')
|
||
PADS = os.path.join(HERE, '_base_pads.json')
|
||
NATIVE = os.path.join(HERE, '_base_native_pads.json')
|
||
POS_JSON = os.path.join(HERE, '_base_place.json')
|
||
ROT_JSON = os.path.join(HERE, '_base_rotations.json')
|
||
LIVE_POS_JSON = os.path.join(HERE, '_base_place_live.json')
|
||
|
||
GAP = 0.6 # 同面件间净距(装配下限,自检按它判)
|
||
# 卫星件(排布器摆的小件)之间的**排布**间距。0.6 只是装配下限,不给布线留余量:
|
||
# 第六 / 七轮剩下的信号网全卡在 U20 北沿那排上拉 / NTC 阻容、U32 西侧、mux 区 —— 0402 按 0.6 一字排开,
|
||
# 两只脚、它们的 GND 孔和进出的线全挤在一起(2026-09-10 用户选定:加大小件间距重排)。
|
||
# 排不下时退回 GAP(`plan()` 里第二遍),所以不会出现「排不下」。
|
||
SAT_GAP = 1.5
|
||
STEP = 0.25 # 候选格点 = 布线网格
|
||
ANCHOR_AREA = 9.0 # 焊盘外框面积 > 这个算「锚」(Y1 10.4 要算进来:负载电容贴晶振)
|
||
|
||
# ── 高度(mm):背面限高 B.H_BOTTOM ─────────────────────────────────────
|
||
HEIGHT = {
|
||
'J1': 11.0, # DC-005 桶身
|
||
'C2': 12.0, 'C3': 12.0, # Φ8 直立电解 470 µF
|
||
'J26': 11.0, 'J27': 11.0, # 2510 4P 立式
|
||
'J33': 11.5, # 2.54 排针立式
|
||
'J23': 3.3,
|
||
'U20': 3.1, 'L3': 4.5, 'L2': 4.0, 'F1': 2.0,
|
||
}
|
||
H_DEFAULT = 2.0
|
||
|
||
# 焊盘外框量不到本体的件:真实本体(mm,封装原生朝向)。
|
||
BODY = {
|
||
'J1': (14.5, 9.0), # DC-005 桶身(桶口朝封装 −x)
|
||
'C2': (8.0, 8.0), 'C3': (8.0, 8.0),
|
||
'J23': (9.0, 7.5),
|
||
'J26': (10.2, 5.0), 'J27': (10.2, 5.0),
|
||
'J33': (12.7, 2.54),
|
||
'L3': (7.6, 6.5),
|
||
'U20': (19.0, 19.0), # 焊盘框;天线 7.5 mm 另由 antenna_keepout 管
|
||
}
|
||
# 本体中心相对器件原点的偏移(封装原生坐标)。R1 实测 DC-005 本体 x −10.68~+3.92。
|
||
BODY_OFF = {'J1': (-3.38, 0.0), 'J23': (0.0, -1.5)}
|
||
# 本体越过 / 贴着板边才对的接口件:值 = 开口朝哪条边
|
||
EDGE_PARTS = {'J1': 'S', 'J23': 'S'}
|
||
|
||
# 南 → 北 = 物理槽位 1..5。USB1 = J34 = 最北(pcb_base.USB_SLOT_REF)。
|
||
SLOT_ORDER = ['J38', 'J37', 'J36', 'J35', 'J34']
|
||
|
||
# ── Hub 坐哪条走廊 ─────────────────────────────────────────────────────────
|
||
# 2 = **定稿**(2026-09-10 用户拍板,与分区草图相比 Hub / mux 对调):USB1~3 北上,
|
||
# 只有 USB4/5 绕 U32 东侧南下,D± 最远 ≈ 58 mm。
|
||
# 3 = 分区草图原样(`--hub3`,只留作对比):USB3~5 都要绕,最远 USB5 ≈ 80 mm。
|
||
# 两条走廊正好差一个槽距,对调 = Hub 那一窝整体 −20.32、mux 那一窝 +20.32。
|
||
HUB_K = 3 if '--hub3' in sys.argv else 2
|
||
MUX_K = 5 - HUB_K
|
||
_CH, _CM = B.corridor(HUB_K), B.corridor(MUX_K)
|
||
# x:走廊 3 时 USB2(−2.5)正对北邻槽 J35 的 D±(47.5/48.5)。
|
||
# 走廊 2 时 **东移 2.5 mm(49.0 → 51.5)**,让 USB3 的脚(50.25/50.75)落在 J36 D± 的东边:
|
||
# 北去三对极性都是反的(U32 北沿 DM 在西、槽上 D+ 在西),换位靠一次 U 形掉头(左右自然对调)。
|
||
# USB3 的目标槽就在正上方 1 mm,脚正对着焊盘就没有掉头的地方(第二轮服务器实测:
|
||
# 换位绕在北沿上把 USB1_DP / USB4_DM 封死)。东移后它从 J36 南排底下北上、在两排之间向西掉头、
|
||
# 打孔上 L1 回到焊盘 —— 见 route_base.usb3_uturn()。
|
||
HUB_CX = 50.5 if HUB_K == 3 else 51.5
|
||
# y:比走廊中心南移 0.26,北沿到北邻槽焊盘留 0.9 给那一对上 L1 的过孔
|
||
HUB_CY = round((_CH[1] + _CH[3]) / 2.0 - 0.26, 3)
|
||
MUX_CY = round((_CM[1] + _CM[3]) / 2.0, 3)
|
||
ZONES = dict(B.ZONES)
|
||
ZONES['A4H'] = (28.5, _CH[1], 69.5, _CH[3])
|
||
ZONES['A4M'] = (28.5, _CM[1], 69.5, _CM[3])
|
||
|
||
# ── 布线车道(摆位禁区,只在两条放件的走廊里)──────────────────────────────
|
||
# I²C:U35(mux 走廊西段)→ 另一侧那两槽的 SCL/SDA(x 36.5/37.5 在南排),要穿过 Hub 走廊。
|
||
# DP_H:绕 U32 东侧南下的那几对。DP_M:穿过 mux 走廊的那几对(回到 x≈48 附近)。
|
||
# U32_WVIA:上行 / USB6 在 U32 西侧打孔上 L1 的落点。
|
||
LANES = {
|
||
'I2C_H': (28.5, _CH[1], 31.5, _CH[3]),
|
||
'I2C_M': (28.5, _CM[1], 31.5, _CM[3]),
|
||
# U32 东侧焊盘外沿 + 1.2 扇出圈之后,再留 ~1.9 mm 给 VDD18 / 3V3_HUB 去耦(它们在 USB4/5 绕行环的**里面**)
|
||
'DP_H': (round(HUB_CX + 8.0, 3), _CH[1], round(HUB_CX + 12.0, 3), _CH[3]),
|
||
'DP_M': (46.5, _CM[1], 51.5, _CM[3]) if HUB_K == 3 else (44.0, _CM[1], 49.0, _CM[3]),
|
||
'U32_WVIA': (round(HUB_CX - 6.6, 3), round(HUB_CY - 2.3, 3),
|
||
round(HUB_CX - 5.4, 3), round(HUB_CY + 1.7, 3)),
|
||
}
|
||
# J23 针排北侧的出口带:D±(含两座同名脚的桥,route_base.j23_bridges)/ CC / VBUS 全从这里往北出。
|
||
# ⛔ 不留的话排布器会把 CC 下拉 R717 摆在针排正北 —— 布线静态校验实测 DM 桥离 R717 净距 −0.075。
|
||
# J23 底层 rot 180 在 (84.0, 5.25):信号针排 y = 7.72 ± 0.65(封装原生 +2.47),x 80.8~87.2。
|
||
J23_XY = (84.0, 5.25)
|
||
LANES['J23_ESC'] = (J23_XY[0] - 4.2, J23_XY[1] + 3.15, J23_XY[0] + 4.2, J23_XY[1] + 5.05)
|
||
# Rs6.2 西侧 9 A 上 L1 的过孔阵(route_base.V12_PATCH / V12_VIA_*):只禁过孔那一段,不含 Rs6 本体。
|
||
# ⛔ 以前只写在布线脚本里,摆位不知道要避开 —— 小件间距放大重排之后 R6(BLD_A)被挤进去,
|
||
# 布线静态校验实测净距 −0.55(v12_handoff 拦下,服务器那轮没跑)。
|
||
LANES['V12_VIAS'] = (5.3, 44.4, 10.55, 48.05)
|
||
|
||
|
||
# ── 扇出圈(2026-09-10 用户选定:芯片扇出上 L1 + U32 按构造画)─────────────────
|
||
# 器件全在背面,芯片的脚、它的去耦 / strap、这些件的 GND 孔全挤在 L4 同一圈里 ——
|
||
# 第三轮服务器 198 条里绝大多数是这几颗「脚出不去」。芯片正上方的 L1 是空的,
|
||
# 但要在脚边落得下过孔才上得去 ⇒ 焊盘外框外留一圈别的件不许进,给扇出过孔。
|
||
FAN_RING = {'U32': 1.2, 'U35': 1.6, 'U6': 1.6, 'U7': 1.6, 'U20': 1.6}
|
||
|
||
|
||
def fan_rings(sz, pos):
|
||
u"""[(主人, 矩形)]:主人自己不受它约束。"""
|
||
out = []
|
||
for ref, m in FAN_RING.items():
|
||
if ref in pos:
|
||
x, y, rot = pos[ref][:3]
|
||
r = body_rect(ref, x, y, rot, sz)
|
||
out.append((ref, (r[0] - m, r[1] - m, r[2] + m, r[3] + m)))
|
||
return out
|
||
|
||
|
||
def zone_at(x, y):
|
||
for k, z in ZONES.items():
|
||
if z[0] <= x <= z[2] and z[1] <= y <= z[3]:
|
||
return k
|
||
return None
|
||
|
||
# ── 分区:原理图页 → 背面分区;个别件按功能改派 ─────────────────────────────
|
||
PAGE_ZONE = {'A1-INPUT': 'A1', 'A2-AUX': 'A2', 'A3-CTRL': 'A3', 'A4-HUB': 'A4H',
|
||
'A6-FAN': 'A6', 'A5-SLOTS': None, 'A5B-SLOTS': None}
|
||
ZONE_OVERRIDE = {}
|
||
for _r in ('U35', 'U6', 'U7', 'C604', 'C607', 'C608', 'C609', 'C610', 'C611', 'C612',
|
||
'R614', 'R615', 'R616', 'R617'):
|
||
ZONE_OVERRIDE[_r] = 'A4M' # mux / DAC 与各自的去耦、参考 RC
|
||
for _r in ('J23', 'U31', 'C737', 'C752', 'C753', 'R717', 'R718', 'R721', 'R722', 'RT601'):
|
||
ZONE_OVERRIDE[_r] = 'A5' # 上行口 + Hub LDO(RT601 监测 U31)
|
||
ZONE_OVERRIDE['RT602'] = 'A1' # 监测 DC-005 接触热
|
||
for _r in ('C736', 'C739', 'R660', 'R661'):
|
||
ZONE_OVERRIDE[_r] = 'A3' # ESP32 ADC 的 NTC 分压 / 滤波
|
||
ZONE_ORDER = ['A4H', 'A4M', 'A3', 'A5', 'A1', 'A2', 'A6']
|
||
|
||
BIG_NETS = ('+3V3', '+12V', '+5V', '+3V3_HUB') # 遍布全板,只当次选目标
|
||
|
||
|
||
# ── 几何:镜像 + 旋转 ────────────────────────────────────────────────────
|
||
def rot_vec(rot, a, b):
|
||
r = int(round(rot)) % 360
|
||
return {0: (a, b), 90: (-b, a), 180: (-a, -b), 270: (b, -a)}[r]
|
||
|
||
|
||
def layer_of(ref):
|
||
return B.side_of(ref)
|
||
|
||
|
||
def pad_off(nx, ny, rot, layer):
|
||
u"""封装原生偏移 → 板上偏移。底层先 y 取反再转(实测,见文件头)。"""
|
||
return rot_vec(rot, nx, -ny if layer == B.BOTTOM else ny)
|
||
|
||
|
||
def native():
|
||
return json.load(io.open(NATIVE, encoding='utf-8'))
|
||
|
||
|
||
def pads_at(nat, ref, x, y, rot):
|
||
u"""[(脚号, 网, 板 x, 板 y)]。"""
|
||
out = []
|
||
for pn, nx, ny, _w, _h, net, _sh in nat.get(ref, []):
|
||
dx, dy = pad_off(nx, ny, rot, layer_of(ref))
|
||
out.append((pn, net, round(x + dx, 4), round(y + dy, 4)))
|
||
return out
|
||
|
||
|
||
def sizes():
|
||
d = json.load(io.open(BBOX, encoding='utf-8'))
|
||
out = {}
|
||
for k, v in d.items():
|
||
bw, bh = BODY.get(k, (0.0, 0.0))
|
||
out[k] = (max(v['w'], bw, 0.6), max(v['h'], bh, 0.6))
|
||
return out
|
||
|
||
|
||
def body_rect(ref, x, y, rot, sz, pad=0.0):
|
||
w, h = sz[ref]
|
||
if int(round(rot)) % 180:
|
||
w, h = h, w
|
||
ox, oy = BODY_OFF.get(ref, (0.0, 0.0))
|
||
dx, dy = pad_off(ox, oy, rot, layer_of(ref))
|
||
cx, cy = x + dx, y + dy
|
||
return (cx - w / 2 - pad, cy - h / 2 - pad, cx + w / 2 + pad, cy + h / 2 + pad)
|
||
|
||
|
||
def height(ref):
|
||
return HEIGHT.get(ref, H_DEFAULT)
|
||
|
||
|
||
def _hit(r, q):
|
||
return r[0] < q[2] and q[0] < r[2] and r[1] < q[3] and q[1] < r[3]
|
||
|
||
|
||
def _rect_dist(px, py, r):
|
||
dx = max(r[0] - px, px - r[2], 0.0)
|
||
dy = max(r[1] - py, py - r[3], 0.0)
|
||
return math.hypot(dx, dy)
|
||
|
||
|
||
# ── 固定障碍(背面)─────────────────────────────────────────────────────
|
||
ANT_LEN = 7.5 # 天线段伸出焊盘框
|
||
ANT_SIDE = 0.5
|
||
|
||
|
||
def antenna_keepout(sz, pos):
|
||
u"""U20 天线净空:天线在封装 +y 端,方向 = pad_off(0, 1)。**跟着 rot 和翻面走**。"""
|
||
ax, ay, rot = pos['U20']
|
||
w, h = sz['U20']
|
||
if int(round(rot)) % 180:
|
||
w, h = h, w
|
||
dx, dy = pad_off(0, 1, rot, layer_of('U20'))
|
||
if dx > 0.5:
|
||
return (ax + w / 2, ay - h / 2 - ANT_SIDE, ax + w / 2 + ANT_LEN, ay + h / 2 + ANT_SIDE)
|
||
if dx < -0.5:
|
||
return (ax - w / 2 - ANT_LEN, ay - h / 2 - ANT_SIDE, ax - w / 2, ay + h / 2 + ANT_SIDE)
|
||
if dy > 0.5:
|
||
return (ax - w / 2 - ANT_SIDE, ay + h / 2, ax + w / 2 + ANT_SIDE, ay + h / 2 + ANT_LEN)
|
||
return (ax - w / 2 - ANT_SIDE, ay - h / 2 - ANT_LEN, ax + w / 2 + ANT_SIDE, ay - h / 2)
|
||
|
||
|
||
def blockers(sz, pos):
|
||
out = []
|
||
k = B.HOLE_KEEP
|
||
for _n, (hx, hy) in B.MOUNT_HOLES.items():
|
||
out.append((hx - k, hy - k, hx + k, hy + k))
|
||
n = B.notch_rect()
|
||
out.append((n[0] - 0.5, n[1] - 0.5, n[2], n[3] + 0.5))
|
||
out.append(antenna_keepout(sz, pos))
|
||
out += B.via_bands()
|
||
out += list(LANES.values())
|
||
return out
|
||
|
||
|
||
# ── 手排件 ───────────────────────────────────────────────────────────────
|
||
def pick_rot(nat, ref, x, y, net, point, cands=(0, 90, 180, 270)):
|
||
u"""cands 里挑一个 rot,让 ref 上网为 net 的那只脚离 point 最近。"""
|
||
best = None
|
||
for r in cands:
|
||
for pn, n, px, py in pads_at(nat, ref, x, y, r):
|
||
if n == net:
|
||
d = math.hypot(px - point[0], py - point[1])
|
||
if best is None or d < best[0] - 1e-6:
|
||
best = (d, r)
|
||
return best[1]
|
||
|
||
|
||
def hand(nat=None):
|
||
u"""{位号: (x, y, rot)}。层由 `pcb_base.side_of` 定(插槽 L1,其余 L4)。"""
|
||
nat = nat or native()
|
||
H = {}
|
||
for i, ref in enumerate(SLOT_ORDER, start=1):
|
||
H[ref] = (B.SLOT_CX, B.slot_y(i), B.SLOT_ROT)
|
||
|
||
# ── A4H:Hub(走廊 HUB_K;x / y 的来历见 HUB_CX / HUB_CY)───────────────
|
||
# 走廊 3 时焊盘 54.0~63.8,北沿到 J35 焊盘(64.72)留 0.9 给 USB2 那对过孔。
|
||
H['U32'] = (HUB_CX, HUB_CY, 270)
|
||
# 晶振在 U32 西侧(XTAL1/2 在西沿北段),抬 3.1 让开 上行/USB6 的过孔带。
|
||
H['Y1'] = (round(HUB_CX - 8.7, 3), round(HUB_CY + 3.1, 3), 270)
|
||
|
||
# ── A4M:mux 在西段、两颗 DAC 在东段(中间 DP_M 车道) ─────────────────────
|
||
# U35 底层 rot 0:原生南排(MUX_RST#、SDA1~SCL4)翻到北边。
|
||
H['U35'] = (39.0, MUX_CY, 0)
|
||
H['U6'] = (56.0, MUX_CY, 0)
|
||
H['U7'] = (64.0, MUX_CY, 0)
|
||
|
||
# ── A3:ESP32 天线朝东伸进缺口(58~80)──────────────────────────────────
|
||
# 焊盘框 71.5~90.5 × 59.5~78.5,天线段 90.5~98。
|
||
H['U20'] = (81.0, 69.0, 90)
|
||
H['J33'] = (80.0, 95.5, 0) # 应急串口,北板边内侧(让开 H4)
|
||
|
||
# ── A5:上行 USB-C 在南板边,LDO 在它北边 ────────────────────────────────
|
||
H['J23'] = (J23_XY[0], J23_XY[1], 180)
|
||
H['U31'] = (80.0, 26.0, 180) # 三只脚朝西(朝板中),散热片朝东
|
||
H['RT601'] = (86.0, 26.0, 0) # 贴 U31 散热片(东侧),测 Hub LDO 最坏 ~0.7 W
|
||
# ⚠ 不能交给排布器:它的同网脚是 U20.39(NTC1),会被拉到 A5 北沿
|
||
|
||
# ── A1:输入链自南向北,电流顺 +y ────────────────────────────────────────
|
||
# J1 桶身 9 × 14.5,本体 x 8~17 / y 0~14.5(让开 H1 的 3.2 圈)。
|
||
H['J1'] = (12.5, 10.63, 90)
|
||
H['F1'] = (12.5, 17.75, 90) # VIN_RAW 在南、VIN_FUSED 在北
|
||
H['Q1'] = (12.5, 24.5, 180)
|
||
H['D1'] = (12.5, 34.0, 90) # TVS 竖放在两颗电解中间,VIN_SW 脚朝南
|
||
H['C2'] = (5.5, 35.0, 180) # VIN_SW 脚朝东(朝 D1 / Q1)
|
||
H['C3'] = (19.5, 35.0, 0) # VIN_SW 脚朝西
|
||
H['Rs6'] = (12.5, 43.5, 90) # VIN_SW 在南、+12V 在北 —— +12V 在这打孔阵上 L1 大铺铜
|
||
H['U13'] = (19.5, 43.5, 270) # INA226 Kelvin 脚(原生北排)朝西贴 Rs6
|
||
H['RT602'] = (19.5, 8.0, 0) # 贴 J1 东侧,测 DC-005 接触热
|
||
# 泄放链:Q3 在西头,R9A→R9E 自西向东一字排,每颗在 90/270 里挑,让相邻两颗的公共网脚在同一侧
|
||
H['Q3'] = (2.95, 52.0, 180) # 漏极排($1N114)朝北
|
||
prev = ('Q3', '$1N114')
|
||
for k, (ref, x) in enumerate((('R9A', 7.6), ('R9B', 11.95), ('R9C', 16.3),
|
||
('R9D', 20.65), ('R9E', 25.0))):
|
||
px, py = H[prev[0]][0], H[prev[0]][1]
|
||
pts = [(q[2], q[3]) for q in pads_at(nat, prev[0], px, py, H[prev[0]][2])
|
||
if q[1] == prev[1]]
|
||
tgt = (sum(p[0] for p in pts) / len(pts), sum(p[1] for p in pts) / len(pts))
|
||
H[ref] = (x, 52.0, pick_rot(nat, ref, x, 52.0, prev[1], tgt, (90, 270)))
|
||
prev = (ref, [q[5] for q in nat[ref] if q[5] != prev[1]][0])
|
||
|
||
# ── A2:两颗 Buck 一字排,IC → 电感紧邻(SW 最短)─────────────────────────
|
||
H['U19'] = (5.0, 68.5, 0) # SW 脚(原生东)朝东
|
||
H['L2'] = (11.0, 68.5, 0) # SW 脚(原生西)朝西
|
||
H['U30'] = (17.0, 68.5, 0)
|
||
H['L3'] = (23.5, 68.5, 0)
|
||
|
||
# ── A6:两只风扇座贴北板边内侧 ─────────────────────────────────────────
|
||
H['J26'] = (13.5, 94.5, 0)
|
||
H['J27'] = (13.5, 88.0, 0)
|
||
return H
|
||
|
||
|
||
# ── 排布 ─────────────────────────────────────────────────────────────────
|
||
def page_of():
|
||
bom = json.load(io.open(os.path.join(HERE, '_base_sch_bom.json'), encoding='utf-8'))
|
||
return {r['d']: pg for pg, rows in bom.items() for r in rows}
|
||
|
||
|
||
def zone_of_ref(ref, pg):
|
||
return ZONE_OVERRIDE.get(ref) or PAGE_ZONE[pg[ref]]
|
||
|
||
|
||
def sat_target(ref, nat, pos, sz, zone, claims):
|
||
u"""目标 = 已摆的锚上**同网的那只脚**(翻面后的真实坐标)。
|
||
|
||
排序:**分区内的脚优先** → 非大网优先 → 离分区近的优先 → 被别的去耦认领得少的优先 → 锚小的优先
|
||
(晶振负载电容认 Y1 而不是 U32;八颗 3V3_HUB 去耦分摊到 U32 的八只电源脚上)。
|
||
⚠ 「分区内优先」必须排第一:风扇驱动的同网脚一头在 J26/J27(A6 内)、一头在 U20(A3),
|
||
按「锚大优先」会全体瞄 60 mm 外的 U20,挤到 A6 的东南角。
|
||
返回 (x, y, 本件用来对准的网, 认领键) 或 None。
|
||
"""
|
||
nets = [q[5] for q in nat.get(ref, []) if q[5] and q[5] != 'GND']
|
||
if not nets:
|
||
return None
|
||
small = [n for n in nets if n not in BIG_NETS]
|
||
want = set(small or nets)
|
||
zr = ZONES[zone]
|
||
best = None
|
||
for other, (ox, oy, orot) in pos.items():
|
||
if other == ref:
|
||
continue
|
||
area = sz[other][0] * sz[other][1]
|
||
if area <= ANCHOR_AREA and small:
|
||
pri0 = 1
|
||
elif area <= ANCHOR_AREA:
|
||
continue
|
||
else:
|
||
pri0 = 0
|
||
for pn, n, px, py in pads_at(nat, other, ox, oy, orot):
|
||
if n not in want:
|
||
continue
|
||
zd = round(_rect_dist(px, py, zr), 1)
|
||
key = (zd > 0, pri0 + (2 if n in BIG_NETS else 0), zd,
|
||
claims.get((other, pn), 0), area, other, pn)
|
||
if best is None or key < best[0]:
|
||
best = (key, px, py, n, (other, pn))
|
||
if best is None:
|
||
return None
|
||
return best[1], best[2], best[3], best[4]
|
||
|
||
|
||
def _cands(zr, sz_ref, t):
|
||
u"""分区内格点,按离目标由近到远。"""
|
||
m = max(sz_ref) / 2.0
|
||
xs, ys = [], []
|
||
v = math.ceil(zr[0] / STEP) * STEP
|
||
while v <= zr[2] + 1e-9:
|
||
xs.append(round(v, 3))
|
||
v += STEP
|
||
v = math.ceil(zr[1] / STEP) * STEP
|
||
while v <= zr[3] + 1e-9:
|
||
ys.append(round(v, 3))
|
||
v += STEP
|
||
pts = [(x, y) for x in xs for y in ys
|
||
if zr[0] - m <= x <= zr[2] + m and zr[1] - m <= y <= zr[3] + m]
|
||
pts.sort(key=lambda p: (math.hypot(p[0] - t[0], p[1] - t[1]), p[1], p[0]))
|
||
return pts
|
||
|
||
|
||
TIGHT = [] # 上一次 plan() 里退回 GAP 的件(报告用)
|
||
POWER_NETS = ('+3V3', '+3V3_HUB', '+5V', '+12V', 'VDD18', 'VDD18PLL', 'VIN_SW')
|
||
_NAT = [None]
|
||
|
||
|
||
def is_decap(ref):
|
||
u"""两只脚、一只 GND 一只电源 ⇒ 去耦 / 储能。"""
|
||
if _NAT[0] is None:
|
||
_NAT[0] = native()
|
||
nets = set(q[5] for q in _NAT[0].get(ref, []))
|
||
return len(_NAT[0].get(ref, [])) == 2 and 'GND' in nets and bool(nets & set(POWER_NETS))
|
||
|
||
|
||
def _inside(r, zr):
|
||
return zr[0] - 1e-6 <= r[0] and r[2] <= zr[2] + 1e-6 and zr[1] - 1e-6 <= r[1] and r[3] <= zr[3] + 1e-6
|
||
|
||
|
||
def plan():
|
||
sz = sizes()
|
||
nat = native()
|
||
pg = page_of()
|
||
pos = dict(hand(nat))
|
||
fixed = list(blockers(sz, pos)) + [r for _o, r in fan_rings(sz, pos)]
|
||
placed = [] # (ref, 带 GAP/2 的本体框),只收背面件
|
||
for ref, (x, y, r) in pos.items():
|
||
if layer_of(ref) == B.BOTTOM:
|
||
placed.append((ref, body_rect(ref, x, y, r, sz, GAP / 2)))
|
||
todo = {}
|
||
for ref in sz:
|
||
if ref not in pos:
|
||
todo.setdefault(zone_of_ref(ref, pg), []).append(ref)
|
||
claims = {}
|
||
unplaced = []
|
||
aim = {}
|
||
tight = [] # 只能按装配下限 GAP 塞进去的件
|
||
TIGHT[:] = tight
|
||
for zone in ZONE_ORDER:
|
||
zr = ZONES[zone]
|
||
zfixed = [q for q in fixed if _hit(q, zr)]
|
||
|
||
def _ord(r):
|
||
a = sz[r][0] * sz[r][1]
|
||
nn = len(nat.get(r, []))
|
||
# 去耦排在其它两脚件前面:它们必须贴脚,先占近处(第一版 SAT_GAP 把 C746 挤到 U32 脚 20 mm 外)
|
||
return (0 if a > ANCHOR_AREA else 1, 0 if nn > 2 else (1 if is_decap(r) else 2), -a, r)
|
||
|
||
for ref in sorted(todo.get(zone, []), key=_ord):
|
||
t = sat_target(ref, nat, pos, sz, zone, claims)
|
||
tgt = (t[0], t[1]) if t else ((zr[0] + zr[2]) / 2.0, (zr[1] + zr[3]) / 2.0)
|
||
net = t[2] if t else None
|
||
done = None
|
||
zplaced = [q for _r, q in placed if _hit(q, (zr[0] - 12, zr[1] - 12,
|
||
zr[2] + 12, zr[3] + 12))]
|
||
# 第一遍按 SAT_GAP 找;整个分区都找不到才按装配下限 GAP 再找一遍。
|
||
# ⚠ 去耦不放宽:它的价值全在「贴着脚」,放宽之后近处的位子被上拉 / 阻容占走,去耦被挤到十几毫米外。
|
||
gaps = (GAP / 2,) if is_decap(ref) else (SAT_GAP / 2, GAP / 2)
|
||
for mc in gaps:
|
||
for (x, y) in _cands(zr, sz[ref], tgt):
|
||
ok = []
|
||
for rot in (0, 90, 180, 270):
|
||
body = body_rect(ref, x, y, rot, sz)
|
||
if not _inside(body, zr):
|
||
continue
|
||
rg = body_rect(ref, x, y, rot, sz, mc)
|
||
if any(_hit(rg, q) for q in zfixed) or any(_hit(rg, q) for q in zplaced):
|
||
continue
|
||
if net:
|
||
d = min(math.hypot(px - tgt[0], py - tgt[1])
|
||
for _pn, n, px, py in pads_at(nat, ref, x, y, rot) if n == net)
|
||
else:
|
||
d = math.hypot(x - tgt[0], y - tgt[1])
|
||
ok.append((d, rot))
|
||
if ok:
|
||
d, rot = min(ok)
|
||
done = (x, y, rot, d)
|
||
break
|
||
if done is not None:
|
||
if mc < SAT_GAP / 2 and not is_decap(ref):
|
||
tight.append(ref)
|
||
break
|
||
if done is None:
|
||
unplaced.append((ref, zone))
|
||
continue
|
||
x, y, rot, d = done
|
||
pos[ref] = (x, y, rot)
|
||
# 按 SAT_GAP 的一半存:后来的件离它也至少 SAT_GAP(它自己退回 GAP 时也一样)。
|
||
# 去耦按 GAP 存:别的去耦可以紧挨着它(一只脚两颗去耦的常见排法)。
|
||
placed.append((ref, body_rect(ref, x, y, rot, sz, (GAP if is_decap(ref) else SAT_GAP) / 2)))
|
||
if t:
|
||
claims[t[3]] = claims.get(t[3], 0) + 1
|
||
aim[ref] = (t[3][0], t[3][1], net, round(d, 2))
|
||
return pos, unplaced, sz, aim
|
||
|
||
|
||
# ── 自检 ─────────────────────────────────────────────────────────────────
|
||
def selfcheck(pos, unplaced, sz):
|
||
bad = []
|
||
if unplaced:
|
||
bad.append(u'排不下 %d 件: %s' % (len(unplaced), ' '.join('%s(%s)' % u for u in unplaced)))
|
||
miss = sorted(set(sz) - set(pos))
|
||
if miss:
|
||
bad.append(u'没摆: %s' % ' '.join(miss))
|
||
fixed = blockers(sz, pos)
|
||
rings = fan_rings(sz, pos)
|
||
items = sorted(pos)
|
||
for i, a in enumerate(items):
|
||
x, y, r = pos[a]
|
||
br = body_rect(a, x, y, r, sz)
|
||
lay = layer_of(a)
|
||
if a in EDGE_PARTS:
|
||
if abs(br[1]) > 1.0:
|
||
bad.append(u'%s 是接口件,本体南沿 y=%.2f 没贴板边' % (a, br[1]))
|
||
if br[0] < 0.25 or br[2] > B.BW - 0.25:
|
||
bad.append(u'%s 东西向出板' % a)
|
||
elif br[0] < 0.25 or br[1] < 0.25 or br[2] > B.BW - 0.25 or br[3] > B.BH - 0.25:
|
||
bad.append(u'%s 出板或贴边: %s' % (a, tuple(round(v, 2) for v in br)))
|
||
if lay == B.BOTTOM:
|
||
if not B.height_ok(height(a)):
|
||
bad.append(u'%s 高 %.1f 超背面限高 %.0f' % (a, height(a), B.H_BOTTOM))
|
||
if a != 'U20':
|
||
for q in fixed:
|
||
if _hit(br, q):
|
||
bad.append(u'%s 压到禁区 %s' % (a, tuple(round(v, 2) for v in q)))
|
||
break
|
||
for o, q in rings:
|
||
if o != a and _hit(br, q):
|
||
bad.append(u'%s 进了 %s 的扇出圈' % (a, o))
|
||
for b in items[i + 1:]:
|
||
if layer_of(b) != lay:
|
||
continue
|
||
rb = body_rect(b, pos[b][0], pos[b][1], pos[b][2], sz)
|
||
if _hit((br[0] - GAP + 1e-6, br[1] - GAP + 1e-6, br[2] + GAP - 1e-6, br[3] + GAP - 1e-6), rb):
|
||
bad.append(u'%s 与 %s 挨太近(< %.1f)' % (a, b, GAP))
|
||
return bad
|
||
|
||
|
||
# ── 体检 ─────────────────────────────────────────────────────────────────
|
||
def report(pos, sz, aim):
|
||
nat = native()
|
||
pg = page_of()
|
||
print(u'')
|
||
print(u'分区占用(本体面积 / 分区面积):')
|
||
cnt = {}
|
||
for ref, (x, y, _r) in pos.items():
|
||
if layer_of(ref) == B.BOTTOM:
|
||
cnt.setdefault(zone_at(x, y) or '?', []).append(ref)
|
||
for k in ZONE_ORDER + ['?']:
|
||
v = cnt.get(k, [])
|
||
if not v:
|
||
continue
|
||
if k == '?':
|
||
print(u' 不在任何分区: %s' % ' '.join(sorted(v)))
|
||
continue
|
||
z = ZONES[k]
|
||
area = (z[2] - z[0]) * (z[3] - z[1])
|
||
used = sum(sz[r][0] * sz[r][1] for r in v)
|
||
print(u' %-4s %3d 件 %5.0f / %5.0f mm² (%2.0f%%)' % (k, len(v), used, area, 100.0 * used / area))
|
||
wrong = [r for r, (x, y, _r) in pos.items() if layer_of(r) == B.BOTTOM and r in pg
|
||
and zone_at(x, y) not in (zone_of_ref(r, pg), None)]
|
||
if wrong:
|
||
print(u' ⚠ 落到别的分区: %s' % ' '.join(sorted(wrong)))
|
||
print(u'')
|
||
far = sorted(((v[3], r, v[0], v[1], v[2]) for r, v in aim.items()), reverse=True)
|
||
print(u'卫星件 → 目标脚的距离:平均 %.1f mm;最远 8 件:' % (sum(f[0] for f in far) / max(len(far), 1)))
|
||
for d, r, o, pn, net in far[:8]:
|
||
print(u' %-6s → %s.%s (%s) %.1f mm' % (r, o, pn, net, d))
|
||
print(u'')
|
||
print(u'D± 估算(曼哈顿:U32 北沿脚 → 槽南排 D± 脚;南去的另加绕 U32 东侧)— Hub 在走廊 %d:' % HUB_K)
|
||
L = dp_lengths(pos, nat)
|
||
for i, ref, v in L:
|
||
print(u' USB%d → %s ≈ %.0f mm' % (i, ref, v))
|
||
print(u' 最远 ≈ %.0f mm(M5 验收线 45)' % max(v for _i, _r, v in L))
|
||
|
||
|
||
def dp_lengths(pos, nat):
|
||
u"""[(USB 号, 槽位号, 估算长度 mm)]。北去的直走;南去的先上到北邻槽底下、向东进 DP_H、再南下。"""
|
||
ux, uy, ur = pos['U32']
|
||
pp = {pn: (px, py) for pn, _n, px, py in pads_at(nat, 'U32', ux, uy, ur)}
|
||
up = _CH[3] + 1.3
|
||
east = LANES['DP_H'][0] + 1.0
|
||
out = []
|
||
for i in range(1, B.SLOT_N + 1):
|
||
ref = B.USB_SLOT_REF[i]
|
||
pn = {1: '2', 2: '4', 3: '7', 4: '9', 5: '12'}[i]
|
||
sx, sy = B.pin_x(-1.5), pos[ref][1] - B.PAD_ROW_DY
|
||
px, py = pp[pn]
|
||
if sy > py:
|
||
v = abs(sx - px) + abs(sy - py)
|
||
else:
|
||
v = (up - py) + (east - px) + (up - sy) + abs(east - sx)
|
||
out.append((i, ref, round(v, 1)))
|
||
return out
|
||
|
||
|
||
# ── 下发 / 读回 ───────────────────────────────────────────────────────────
|
||
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
|
||
"await new Promise(r=>setTimeout(r,2500));" % PCB)
|
||
|
||
|
||
def call(js, timeout=300):
|
||
r = run(OPEN + js, timeout=timeout)
|
||
if not r.get('success'):
|
||
print(u'失败: ' + json.dumps(r, ensure_ascii=False)[:700])
|
||
sys.exit(1)
|
||
return r['result']
|
||
|
||
|
||
def push(pos):
|
||
u"""layer + x + y + rotation 一次 modify。⚠ rotation 0 会被当成「未传」,传 360。"""
|
||
arr = [[k, layer_of(k), round(v[0] * MIL, 4), round(v[1] * MIL, 4),
|
||
int(v[2]) % 360 or 360] for k, v in sorted(pos.items())]
|
||
r = call("""
|
||
const P = %s;
|
||
const by = {};
|
||
for (const c of await eda.pcb_PrimitiveComponent.getAll()) by[c.designator] = c;
|
||
let k = 0, miss = [];
|
||
for (const [d, lay, x, y, rot] of P) {
|
||
const c = by[d];
|
||
if (!c) { miss.push(d); continue; }
|
||
await eda.pcb_PrimitiveComponent.modify(c.getState_PrimitiveId(),
|
||
{layer: lay, x: x, y: y, rotation: rot});
|
||
k++;
|
||
}
|
||
await eda.pcb_Document.save();
|
||
return {moved: k, miss: miss};
|
||
""" % json.dumps(arr), timeout=900)
|
||
print(u'摆位下发 %d 件%s' % (r['moved'], (u';板上没有: %s' % r['miss']) if r['miss'] else u''))
|
||
return r
|
||
|
||
|
||
BBOX_JS = u"""
|
||
const MIL = %s;
|
||
const out = {};
|
||
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
|
||
const ps = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId());
|
||
let x0=1e9, y0=1e9, x1=-1e9, y1=-1e9;
|
||
for (const p of ps) {
|
||
const pad = p.pad || [];
|
||
let w = (typeof pad[1]==='number')?pad[1]:0, h = (typeof pad[2]==='number')?pad[2]:0;
|
||
let poly = false;
|
||
if (String(pad[0])==='POLYGON' && Array.isArray(pad[1])) {
|
||
poly = true;
|
||
const nums = pad[1].filter(v=>typeof v==='number');
|
||
let mnx=1e9,mny=1e9,mxx=-1e9,mxy=-1e9;
|
||
for (let k=0;k+1<nums.length;k+=2){mnx=Math.min(mnx,nums[k]);mxx=Math.max(mxx,nums[k]);
|
||
mny=Math.min(mny,nums[k+1]);mxy=Math.max(mxy,nums[k+1]);}
|
||
w = mxx-mnx; h = mxy-mny;
|
||
}
|
||
// p.x/p.y 是板坐标,pad[1]/[2] 是封装坐标下的宽高:件转 90° 之后要换过来
|
||
const prot = (((p.rotation||0)%%180)+180)%%180;
|
||
if (!poly && prot > 45 && prot < 135) { const t=w; w=h; h=t; }
|
||
x0=Math.min(x0,p.x-w/2); x1=Math.max(x1,p.x+w/2);
|
||
y0=Math.min(y0,p.y-h/2); y1=Math.max(y1,p.y+h/2);
|
||
}
|
||
out[c.designator] = {n: ps.length, w: (x1-x0)/MIL, h: (y1-y0)/MIL, rot: c.rotation,
|
||
fp: (c.footprint && c.footprint.name) ? c.footprint.name : c.footprint};
|
||
}
|
||
return out;
|
||
""" % MIL
|
||
|
||
|
||
def dump_bbox():
|
||
u"""封装外框 → `_base_bbox.json`,**按 rot 反转回封装朝向**(翻面不换长宽)。"""
|
||
d = call(BBOX_JS, timeout=600)
|
||
for k, v in d.items():
|
||
if int(v.get('rot') or 0) % 180 == 90:
|
||
v['w'], v['h'] = v['h'], v['w']
|
||
v['w'] = round(v['w'], 3)
|
||
v['h'] = round(v['h'], 3)
|
||
io.open(BBOX, 'w', encoding='utf-8').write(json.dumps(d, ensure_ascii=False, indent=0))
|
||
print(u'已写 _base_bbox.json:%d 件' % len(d))
|
||
return d
|
||
|
||
|
||
DUMP_JS = u"""
|
||
const MIL = %s;
|
||
const out = [];
|
||
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
|
||
const ps = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId());
|
||
for (const p of ps) {
|
||
const pad = p.pad || [];
|
||
let w = (typeof pad[1]==='number')?pad[1]:0, h = (typeof pad[2]==='number')?pad[2]:0;
|
||
let cx = p.x, cy = p.y, poly = false;
|
||
if (String(pad[0]) === 'POLYGON' && Array.isArray(pad[1])) {
|
||
poly = true;
|
||
const nums = pad[1].filter(v => typeof v === 'number');
|
||
const pts = [];
|
||
for (let k=0; k+1<nums.length; k+=2) pts.push([nums[k], nums[k+1]]);
|
||
const ys = [...new Set(pts.map(q=>q[1]))].sort((a,b)=>a-b);
|
||
for (let b2=0; b2+1<ys.length; b2++) {
|
||
const ym = (ys[b2]+ys[b2+1])/2, xs = [];
|
||
for (let k=0; k<pts.length; k++) {
|
||
const a2=pts[k], b3=pts[(k+1)%%pts.length];
|
||
if (a2[0]!==b3[0]) continue;
|
||
if (Math.min(a2[1],b3[1]) < ym && ym < Math.max(a2[1],b3[1])) xs.push(a2[0]);
|
||
}
|
||
xs.sort((a,b)=>a-b);
|
||
for (let k=0; k+1<xs.length; k+=2) {
|
||
if (xs[k+1]-xs[k] < 1e-6) continue;
|
||
out.push([c.designator, String(p.padNumber),
|
||
Math.round((xs[k]+xs[k+1])/2/MIL*1000)/1000,
|
||
Math.round(ym/MIL*1000)/1000,
|
||
Math.round((xs[k+1]-xs[k])/MIL*1000)/1000,
|
||
Math.round((ys[b2+1]-ys[b2])/MIL*1000)/1000,
|
||
p.layer, p.net||'', 'POLYRECT']);
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
const rot = (((p.rotation||0)%%180)+180)%%180;
|
||
if (!poly && rot > 45 && rot < 135) { const t=w; w=h; h=t; }
|
||
out.push([c.designator, String(p.padNumber), Math.round(cx/MIL*1000)/1000,
|
||
Math.round(cy/MIL*1000)/1000, Math.round(w/MIL*1000)/1000,
|
||
Math.round(h/MIL*1000)/1000, p.layer, p.net||'', String(pad[0])]);
|
||
}
|
||
}
|
||
return out;
|
||
""" % MIL
|
||
|
||
|
||
def dump_pads():
|
||
d = call(DUMP_JS, timeout=600)
|
||
io.open(PADS, 'w', encoding='utf-8').write(json.dumps(d, ensure_ascii=False))
|
||
live = dump_live_positions()
|
||
io.open(ROT_JSON, 'w', encoding='utf-8').write(
|
||
json.dumps({k: v[2] for k, v in live.items()}, ensure_ascii=False, sort_keys=True))
|
||
print(u'已写 _base_pads.json:%d 个焊盘' % len(d))
|
||
return d
|
||
|
||
|
||
def dump_live_positions():
|
||
d = call("const o={}; for(const c of await eda.pcb_PrimitiveComponent.getAll())"
|
||
" o[c.designator]=[c.x/%s,c.y/%s,(c.rotation||0)%%360,c.getState_Layer()]; return o;"
|
||
% (MIL, MIL))
|
||
if len(d) != 138:
|
||
raise SystemExit(u'板上读到 %d 件,应为 138;拒绝覆盖 LIVE 快照' % len(d))
|
||
out = {ref: [round(float(v[0]), 4), round(float(v[1]), 4), int(round(v[2])) % 360, v[3]]
|
||
for ref, v in sorted(d.items())}
|
||
io.open(LIVE_POS_JSON, 'w', encoding='utf-8').write(json.dumps(out, ensure_ascii=False, indent=2))
|
||
return out
|
||
|
||
|
||
def rebuild_native():
|
||
u"""`_base_pads.json` + `_base_place_live.json` → 封装原生偏移(反用镜像规则)。"""
|
||
P = json.load(io.open(PADS, encoding='utf-8'))
|
||
L = json.load(io.open(LIVE_POS_JSON, encoding='utf-8'))
|
||
nat = {}
|
||
for p in P:
|
||
x0, y0, rot, lay = L[p[0]]
|
||
a, b = rot_vec((360 - rot) % 360, p[2] - x0, p[3] - y0)
|
||
if lay == B.BOTTOM:
|
||
b = -b
|
||
w, h = (p[4], p[5]) if rot % 180 == 0 else (p[5], p[4])
|
||
nat.setdefault(p[0], []).append([p[1], round(a, 4), round(b, 4), round(w, 4),
|
||
round(h, 4), p[7], p[8]])
|
||
io.open(NATIVE, 'w', encoding='utf-8').write(json.dumps(nat, ensure_ascii=False))
|
||
print(u'已重建 _base_native_pads.json:%d 件' % len(nat))
|
||
return nat
|
||
|
||
|
||
def verify(pos, sz):
|
||
u"""读回位姿逐件对账 + 朝向断言(用**板上读回的焊盘**,不信离线模型)。"""
|
||
d = dump_pads()
|
||
live = json.load(io.open(LIVE_POS_JSON, encoding='utf-8'))
|
||
got, allp = {}, {}
|
||
for ref, pn, x, y, _w, _h, _lay, net, _sh in d:
|
||
got.setdefault(ref, {}).setdefault(pn, (x, y, net))
|
||
# ⚠ 脚号会重复:插槽两个外壳盘都叫 65(u = ±19.5),J23 的 A1B12 / A4B9 各有好几块
|
||
allp.setdefault((ref, pn), []).append((x, y))
|
||
bad = []
|
||
for ref, (x, y, rot) in sorted(pos.items()):
|
||
g = live.get(ref)
|
||
if not g:
|
||
bad.append(u'%s 板上没有' % ref)
|
||
continue
|
||
if abs(g[0] - x) > 0.02 or abs(g[1] - y) > 0.02 or g[2] % 360 != int(rot) % 360 \
|
||
or g[3] != layer_of(ref):
|
||
bad.append(u'%s 读回 (%.2f,%.2f,%s°,L%s) != (%.2f,%.2f,%s°,L%s)'
|
||
% (ref, g[0], g[1], g[2], g[3], x, y, rot, layer_of(ref)))
|
||
|
||
def pad(ref, pn):
|
||
return got.get(ref, {}).get(pn)
|
||
|
||
# 1. 离线镜像模型 == 板上焊盘(全部件逐脚)
|
||
nat = native()
|
||
worst = (0.0, '')
|
||
for ref, (x, y, rot) in pos.items():
|
||
for pn, _n, px, py in pads_at(nat, ref, x, y, rot):
|
||
gs = allp.get((ref, pn))
|
||
if gs:
|
||
e = min(math.hypot(gx - px, gy - py) for gx, gy in gs)
|
||
if e > worst[0]:
|
||
worst = (e, '%s.%s' % (ref, pn))
|
||
if worst[0] > 0.05:
|
||
bad.append(u'离线镜像模型和板上焊盘差 %.3f mm(%s)⇒ pad_off 的规则不对' % worst)
|
||
# 2. U32 下行口朝北、USB1 在西
|
||
ux, uy = pos['U32'][0], pos['U32'][1]
|
||
p2, p12 = pad('U32', '2'), pad('U32', '12')
|
||
if not p2 or not p12:
|
||
bad.append(u'U32 的 2/12 脚没读到')
|
||
elif not (p2[1] > uy + 3.5 and p12[1] > uy + 3.5 and p2[0] < p12[0]):
|
||
bad.append(u'U32 下行口不对:USB1_DP (%.2f,%.2f) USB5_DP (%.2f,%.2f),中心 y=%.2f'
|
||
% (p2[0], p2[1], p12[0], p12[1], uy))
|
||
x61 = pad('U32', '61')
|
||
if x61 and x61[0] > ux - 3.5:
|
||
bad.append(u'U32 的 XTAL1 不在西侧(x=%.2f)' % x61[0])
|
||
# 3. 插槽:+12V 在西、D± 在南排 x 47.5/48.5
|
||
for i, ref in enumerate(SLOT_ORDER, start=1):
|
||
p2, p46, p47 = pad(ref, '2'), pad(ref, '46'), pad(ref, '47')
|
||
cy = B.slot_y(i)
|
||
if not (p2 and p46 and p47):
|
||
bad.append(u'%s 的 2/46/47 脚没读到' % ref)
|
||
continue
|
||
if p2[2] != '+12V' or p2[0] > B.SLOT_CX:
|
||
bad.append(u'%s 的 2 脚 %s 在 x=%.2f,应为西侧 +12V' % (ref, p2[2], p2[0]))
|
||
if abs(p46[0] - B.pin_x(-1.5)) > 0.05 or abs(p46[1] - (cy - B.PAD_ROW_DY)) > 0.05:
|
||
bad.append(u'%s 的 D+ 在 (%.2f,%.2f),应在 (%.1f,%.2f)'
|
||
% (ref, p46[0], p46[1], B.pin_x(-1.5), cy - B.PAD_ROW_DY))
|
||
# 4. U20 天线朝东、尖落在缺口里
|
||
pa, pb = pad('U20', '1'), pad('U20', '40')
|
||
if not pa or not pb:
|
||
bad.append(u'U20 的 1/40 脚没读到')
|
||
else:
|
||
mx, my = (pa[0] + pb[0]) / 2.0, (pa[1] + pb[1]) / 2.0
|
||
nk = B.notch_rect()
|
||
cx = pos['U20'][0]
|
||
if mx < cx + 5.0 or not (nk[1] <= my <= nk[3]) or not (nk[0] - 1.5 <= mx <= nk[0] + 0.5):
|
||
bad.append(u'U20 天线端(pin1/40 中点 %.2f,%.2f)没朝东对着缺口 x %.1f / y %.1f~%.1f'
|
||
% (mx, my, nk[0], nk[1], nk[3]))
|
||
# 5. 接口 / 输入链朝向
|
||
j1, j1c = pad('J1', '1'), pos['J1']
|
||
if j1 and j1[1] <= j1c[1]:
|
||
bad.append(u'J1 的 VIN_RAW 脚在南(y=%.2f),桶口朝向不对' % j1[1])
|
||
a6 = pad('J23', 'A6')
|
||
if a6 and a6[1] <= pos['J23'][1]:
|
||
bad.append(u'J23 的信号脚在南(y=%.2f),口朝向不对' % a6[1])
|
||
qs = [pad('Q1', n) for n in ('1', '2', '3', '5', '6', '7', '8')]
|
||
if all(qs) and not (sum(q[1] for q in qs[3:]) / 4 > sum(q[1] for q in qs[:3]) / 3):
|
||
bad.append(u'Q1 的 VIN_SW 排不在 VIN_FUSED 排北边')
|
||
if bad:
|
||
print(u'[X] 读回校验 %d 条不过:' % len(bad))
|
||
for t in bad[:25]:
|
||
print(u' ' + t)
|
||
else:
|
||
print(u'[OK] %d 件位姿 / 层逐件对上;离线镜像模型与板上焊盘最大差 %.3f mm;'
|
||
u'U32 口朝北 USB1 在西、插槽 12V 在西 D± 在南排、U20 天线朝东入缺口、J1/J23/Q1 朝向全对'
|
||
% (len(pos), worst[0]))
|
||
return bad
|
||
|
||
|
||
def route_fixed_pads(pads):
|
||
u"""2 层版遗留接口:4 层版没有 ROUTE_FIX,恒等变换。"""
|
||
return pads, []
|
||
|
||
|
||
if __name__ == '__main__':
|
||
if '--bbox' in sys.argv:
|
||
dump_bbox()
|
||
sys.exit(0)
|
||
if '--dump' in sys.argv:
|
||
dump_pads()
|
||
sys.exit(0)
|
||
if '--native' in sys.argv:
|
||
dump_pads()
|
||
rebuild_native()
|
||
sys.exit(0)
|
||
pos, unplaced, sz, aim = plan()
|
||
bad = selfcheck(pos, unplaced, sz)
|
||
nh = len(hand())
|
||
print(u'底板摆位:%d 件(手排 %d + 排布 %d);L1 %d 件 / L4 %d 件'
|
||
% (len(pos), nh, len(pos) - nh, sum(1 for r in pos if layer_of(r) == B.TOP),
|
||
sum(1 for r in pos if layer_of(r) == B.BOTTOM)))
|
||
if bad:
|
||
print(u'[X] 自检 %d 条不过:' % len(bad))
|
||
for t in bad[:30]:
|
||
print(u' ' + t)
|
||
else:
|
||
print(u'[OK] 自检全过(全摆上 / 不出板 / 同面不重叠 / 背面限高 / 螺钉 / 过孔带 / 车道 / 天线净空)')
|
||
report(pos, sz, aim)
|
||
io.open(POS_JSON, 'w', encoding='utf-8').write(
|
||
json.dumps({k: list(v) + [layer_of(k)] for k, v in pos.items()}, ensure_ascii=False, indent=0))
|
||
if '--push' in sys.argv:
|
||
if bad:
|
||
print(u'!! 自检不过,不下发')
|
||
sys.exit(1)
|
||
push(pos)
|
||
verify(pos, sz)
|
||
elif '--verify' in sys.argv:
|
||
verify(pos, sz)
|