文件
phonerail/.claude/pcb2_place.py
T
zhou yang和Claude Opus 5 efcba30a0c R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 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
2026-09-10 21:02:10 +08:00

1019 行
60 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""载板 PCB2-CARRIER 布局 —— **v2:元件全部在底层,排线穿槽(2026-09-06 用户拍板)**。
上一版(顶层服务带 + 底层 Hub)存档在 `obsolete/pcb2_place_topside_2026-09-06.py`,
当时的全部铜/元件状态在 `_pcb2_snap_topside.json`(`pcb2_snapshot.py` 抓的)。
板框 **220 × 135 mm**,四角 R5。5 块小米8 主板 **3+2 双排面对面**,第二排居中:
┌──────────────────────────────────────────────┐ 135
│ ┌───CH1───┐ ┌───CH2───┐ ┌───CH3───┐ │
│ │ 主板正面朝天,反面贴 3.0 立柱 │ │ y 85.5~130.16
│ └═════════┘ └═════════┘ └═════════┘ │ ═ = 两条穿线槽(贴着接口边外侧)
│ 中央走廊:底层排 TP/J7/U5 两行 + J31/J32 + Hub │ y 49.66~85.5
│ ┌═════════┐ ┌═════════┐ │
│ │ CH4 │ │ CH5 │ │ y 5~49.66
│ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────┘ 220
## v2 的四条硬决定(2026-09-06)
1. **顶层只剩 20 根顶针**(它们必须顶着主板反面的侧键触点,别无选择)。
其余 141 件全部 layer 2。顶面成为一张干净的「床」——主板压板、风道都好做。
2. **每站两条 3.0 mm 穿线槽**(`SLOTS`,本体坐标;按 2026-09-06 实测排线尺寸定):
电池槽 u 18.8~28.8 / v −4.2~−1.2(接口边外侧,正对 J600,排线 8×38);
**尾插槽 u 45~65 / v 1.4~4.4,在 J1401 正下方(v4,用户拍板)**,排线 18×55:
从 J1401 出线朝接口边 → 绕过接口边折回主板下方 → 正下方下穿 → **沿载板背面往 +v 走 ~34 mm** 到 J7n(v 37)。
EDL 两根飞线跟着尾插排线走。
槽在 EasyEDA 里是 **layer 12 (MULTI) 的填充 = 挖槽区域**(`pcb_PrimitiveFill`)。
3. **C8n1 换成矮封装 SMD 电解**(2026-09-06 用户拍板):红宝石 6.3TPV2200M10X10.5,C3828985,
D10×L10.5,罐体 10.3 见方(`CAN_BODY`)。两板净空 13 mm ⇒ **下板在这五块投影下只能放 ≤ 1.5 mm 的件**
(`--heights` 会把底层高度图导出给下板用)。
4. **底层元件离 35 个定位孔中心 ≥ 2.8 mm、离 6 个 M3 孔中心 ≥ 3.5 mm**:
尼龙立柱从底面用螺钉锁,螺钉头要地方。
## 底层排法(本体坐标,v<0 = 接口边外侧 = 中央走廊;v>0 = 主板投影下方)
(v4,全部在主板投影下方;`BAND` 是事实来源)
v −4.2~−1.2 ═ 电池槽 u 18.8~28.8 ═ 接口边外侧
v −4.2~−1.2 ═ 电池槽 u 18.8~28.8 ═ ═ 尾插槽 u 45~65 ═ (v4c:两条槽同一条线,都在接口边外侧)
v 0~13 │ 电池排线车道 u 18.8~27.8 │ C8n1(34.3,11.3)
v 15 C8n2 C8n6 J8n(排线直焊, 23.8) │
v 18.5 C8n3 C8n4 C8n5 R78n R79n │ 尾插排线车道 u 44.6~62.6
v 8.5 行 B C8n4 C8n5 C8n1(33,11) │
v 21 行 C R74n U4n C76n │ 尾插排线车道 u 44.6~62.6(背面,不放元件)
v 26 行 D C70n U2n C71n C72n R70n │
v 35 行 F J7n(52.5, rot 0) ← 排线从南边来
v 37~39.5 行 E U5n R76n R77n J6n(62.5,41) (插头背后)
走廊里两站面对面各占 v −4.2 ~ −13.7,中间 y 63.4~71.8 那条 8.4 mm 的带子放
J31/J32(y 65.4~69.6);Hub 簇从 (110,67) 挪到 **(100,68)**,避开 CH5 的行 B 和
CH2 的行 A(原位置 C749/C742 会压在 CH2 的 C8n4 上)。U31 组、U34/C760 跟着重排,见 `GLOBAL`。
## 底层镜像语义(2026-09-06 实测,U51/J71/R761 三件翻到底层读焊盘)
**底层 = 先把封装在自己的本地系里沿 x 轴翻(ly → −ly),再按 rot 逆时针转。**
后果:
· 两脚件(0402/0603/1206/电解)焊盘在本地 y=0 上,**pin1 方向和顶层完全一样**;
· rot 0 的 SOT23-6 / BM24:**南北两排对调**(J7n 奇数排从北变到南);
· rot 90 时东西对调(顶层 rot 90 引脚在东,底层 rot 90 在西)。
所以 J23 在底层要用 **rot 270** 才能保持口朝 +X,R717/R718 的 y 要对调。
⚠️ **`--verify` 会回读 J23/R717/R718/J7n 的焊盘坐标核这一条**,别靠推。
用法:
/c/Python314/python .claude/pcb2_place.py # 只算,本地检查
/c/Python314/python .claude/pcb2_place.py --clean-copper # 删 1/2/15/16 层导线 + 全部过孔 + 铺铜(换版必做)
/c/Python314/python .claude/pcb2_place.py --outline # 板框 + 两类槽 + 载板自己的 M3 孔
/c/Python314/python .claude/pcb2_place.py --mech # 35 定位孔 + 5 个手机位丝印
/c/Python314/python .claude/pcb2_place.py --push # 摆元件(含层),并导出排母焊盘坐标
/c/Python314/python .claude/pcb2_place.py --verify # 回读焊盘坐标核镜像语义
/c/Python314/python .claude/pcb2_place.py --heights # 导出底层高度图(给下板布局避让)
/c/Python314/python .claude/pcb2_place.py --layers # 切 4 层(只需跑一次,已跑过)
"""
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = '942ed1560d81c1e7' # PCB2-CARRIER
MIL = 39.3700787401575
BW, BH = 220.0, 135.0 # 板框 mm
CORNER_R = 5.0
# 载板自己的 M3 安装孔(来历见 obsolete/pcb2_place_topside_2026-09-06.py 文件头,位置没动)。
MH = [(8.0, 8.0), (212.0, 8.0), (7.0, 60.0), (213.0, 45.0),
(76.6, 79.5), (148.1, 79.5), # v3:中跨孔随上排下移 0.6(焊盘顶 82.5 < 83.0)
# 2026-09-06 用户指令「两板尺寸取大对齐、四角定位孔对齐」:下板改成同样的 220×135、
# 四角孔与本板重合。底部两角本来就有 (8,8)/(212,8);顶部两角被上排主板(y ≤ 127.66)和
# 尾插槽(site1 x 8.09~27.09,见 SLOTS 注释)夹住,只剩板角 4×4 那一块:
# 孔心 (4,131)/(216,131),焊盘 Ø6 → 离板边 1.0、离主板边 0.34(主板架在 3 mm 立柱上,螺钉头 ≤ 2)、离槽 1.09。
(4.0, 131.0), (216.0, 131.0)]
MH_HOLE, MH_PAD = 3.2, 6.0
MH_KEEP = 3.5 # 底层元件离 M3 孔中心的最小距离(螺钉头 Ø6)
# ---- 小米8 主板(顶视,板左下角原点,mm)----
# 2026-09-06 实测替换(旧值的 7 孔是**从照片认的**,最大错 4.8 mm)。
# 新值来源:测量底板(四基准点 Φ6,圆心矩形 160x110)上描轮廓 + 每孔戳一点 -> 扫描 ->
# `pcb2_measure_read.py` 单应反算。**再拿真主板平板扫描件做了一次独立复核**:
# 用这 7 个孔去套扫描件上的孔,**7/7 命中、平均残差 0.38 mm**(`_fit.png`)。
# ⚠️ 编号整体重排过,别拿旧 H 号对新 H 号。
# ⚠️ 板长 67 = **裸板**。用户描轮廓时 SIM 卡托还插着,卡托在 u 大的那一端伸出约 3.3 mm,
# 所以描出来是 70.3。他早先给的 u 值是拿 66 / 70.3 混着换算的,才有那 3~4 mm 的怪差。
PW, PH = 67.0, 44.0
HOLES = {'H1': (1.4, 9.9), 'H2': (0.7, 27.3), 'H3': (1.7, 38.7), 'H4': (41.9, 1.9),
'H5': (57.4, 41.2), 'H6': (64.6, 28.1), 'H7': (64.8, 32.4)}
HOLE_D = 1.8 # 载板上的定位孔,NPTH。2026-09-06 用户卡尺:**主板孔 Φ2.0**
# (先前口头的 Φ1.5 作废)。Φ2.0 是手机自己的过孔 ⇒ 配 **M1.6 尼龙立柱**
# (M2 螺杆 Φ2.0 塞 Φ2.0 没余量,不选);载板这侧是螺钉过孔,取 M1.6 的通孔 Φ1.8。
# 原来整套按 Φ2.5 / M2.5 做的,是错的。
HOLE_KEEP = 2.2 # 底层元件离定位孔中心的最小距离(M1.6 尼龙螺钉头 Ø3.2 + 余量)
# 2026-09-07:**H5 不打**。J61~J65(EDL 座)离 P*H5 只有 0.92(要 2.2),二选一里「不打 H5」代价为零:
# 每台还有 6 颗螺钉,H5 在 u 大 / v 大那个角(离侧键触点最远的角),那里没有需要压紧的东西。
# 模型里 HOLES 保留 7 个(它是实测数据),只在下发孔 / 螺钉头避让时跳过这里列的。
SKIP_HOLES = {'H5'}
# 侧键触点。旧值(§22.3b 那四个点)是从照片认的,**内部排布是错的**,见下。
#
# ---- 已经问清楚的(可信)----
# 触点是 **4 个 Φ1.35 的圆盘,排成菱形**:相邻 1.87 mm、对角 2.65 mm。
# 来源:中框(配合件)上那四个金色焊盘的平板扫描,边界干净,k-means 分开后直接量。
# ⚠️ **顶针本体直径必须 < 1.87 mm**,常规 Φ2 以上的排不下 —— 选型硬约束。
#
# ---- 位置:**中框螺柱配准**那一版(已被卡尺判定为正确)----
# 2026-09-06:用户卡尺实测 **H1 孔心到四触点菱形中心 = 5 mm**,判死了两条路之争:
# 中框配准版 (3.42, 5.07) -> 离 H1 5.24 mm ✔ 采信
# 红漆配准版 (0.81, 7.08) -> 离 H1 2.88 mm �’ 弃
# 红漆那条当时的论据是「同图内量到最近孔 2.7 mm,不经配准也不吃手性」,**论据是错的**:
# 那个「最近的孔」是环形滤波器几百个候选里的一个,多半是过孔/焊盘误报,不是真孔。
# ⚠️ 教训:环形滤波器在这块板上误报率极高,**任何"离最近的孔多少"都不能直接用**,
# 必须先确认那个候选真的是 7 孔之一。
# 旁证:中框版四个点全在板内(红漆版算出 GND 在板边外 0.5 mm,与用户「GND 离短板边最近」矛盾),
# 而且和旧 §22.3b 的簇心 (3.34, 5.12) 只差 0.1 mm —— 旧数据的**簇心本来就是对的,错的是内部排布**。
# 下面四个坐标直接取中框配准的实测值(不是理想菱形),残差 0.57 mm。
POGO_XY = [(4.72, 5.16), # PWR 电源 u+
(3.42, 6.37), # VUP 音量+ v+
(3.49, 3.74), # VDN 音量- v-
(2.05, 4.99)] # GND u-
POGO_TAG = ['J603 PWR', 'J601 VUP', 'J602 VDN', 'J604 GND']
# ---- 2026-09-06 用户改方案:**顶针取消,改成和中框一样的焊盘** ----
# 装配方式跟着改:不再靠顶针弹力,而是**用螺丝把主板压在载板上**,
# 由主板自己的侧键弹片直接压在载板的焊盘上 —— 完全复刻中框的接触方式。
# 好处:顶针那个「本体必须 < 1.87 mm」的硬卡点直接消失(现有 POGO-SMD1P_D1.9 的 Ø1.9
# 在真实 1.78 mm 最小间距下是负净距,正是那 20 条重叠的来源)。
#
# 焊盘定径:四点实测最小间距 **1.78 mm**(不是理想菱形的 1.87)⇒
# 净距 0.20 -> 最大 Ø1.58 · 净距 0.25 -> Ø1.53 · 净距 0.30 -> Ø1.48
# 中框原盘 Ø1.35。取 **Ø1.5**:净距 0.28(好做),容许位置误差 0.50 mm。
POGO_PAD_D = 1.5
# ⚠️ **位置不确定度 ±0.6 mm 已经吃掉全部余量**(Ø1.5 只容许 0.50)。
# 打样前必须把触点位置收到 ±0.3:办法是「**一次装夹**」——
# 主板放测量底板上,先戳 7 个孔、再把弹片涂红盖印,孔和触点落进同一套基准圆,一次量完。
# ⚠️ **立柱高度 3.0 -> 1.0 mm**:2026-09-06 用户实测中框「螺柱支撑面 到 金焊盘面 = 1 mm」,
# 这就是载板面到主板贴合面该有的间隙。原来是 3.0 mm 立柱 + 顶针压缩 0.5,整套作废。
# ✅ **不用挖空**:2026-09-06 用户实测「贴合面上元件全都低于 1 mm」(屏蔽罩、SIM 卡座都算过),
# 所以载板**保持平板**,1.0 mm 间隙直接成立。曾担心的「挖空会和底层 BAND 那组元件打架」不存在了。
# ✅ 排线反而更宽裕(绕边下落 5 -> 3 mm):
# 尾插 2.9 + 3 + 2.9 + 1.6 + 34.1 ≈ 44.5 ≤ 50(原 46.5)
# 电池 4.6 + 3 + ... ≈ 29.6 ≤ 35(原 31.6)
# ⇒ §25.16 那串长度核算只会更松,不用改路径。
# ⚠️ **1 mm 立柱不好买**:M1.6 尼龙立柱通常从 3 mm 起。可行做法是
# 「螺钉从上穿过手机孔 + 1 mm 尼龙垫片 + 穿过载板 Φ1.8 孔 + 底面尼龙螺母」。垫片可叠。
# ⚠️ 顶角那两个 M3 孔(4,131)/(216,131) 的旧注释写着「主板架在 3 mm 立柱上,螺钉头 ≤ 2」——
# 现在只剩 1 mm,那两个孔真要补的话螺钉头必须 ≤ 1 或者挪开。(它们本来就没打到板上。)
POGO_STANDOFF_H = 1.0
# ---- 五个手机位:本体左下角 + 旋转(0 = 接口边朝下,180 = 接口边朝上;to_board 里 rot 180 是绕本体中心转,位置矩形不变)----
# v3(2026-09-06 用户拍板):穿线槽挪到**主板投影外侧、靠板边**那一条,排线(按 6 cm 算)
# 跨过主板正面到远端边、下穿、再折回底面。上排下移 / 下排上移各 2.5 mm,让板边那条留出
# 3.5 mm 的筋(主板顶边 127.66 → 槽 128.5~131.5 → 板边 135)。走廊 y 52.16~83.0(30.8 mm)。
# v5(2026-09-06 用户拍板):**五块主板连槽整体转 180°,接口边一律朝板边**。上排接口边在 y 127.66(槽 128.86~131.86,
# 离板边 3.14),下排接口边在 y 7.5(槽 3.3~6.3)。两条排线都从板边翻下来、在背面往板中心走;走廊(y 52.16~83)完全空出来,
# 只剩 J31/J32 / Hub / U31 组 / U34 / J23。顶针(侧键触点,v 2.5~7)随之到板边一侧。
SITES = {
1: (5.0, 83.0, 180),
2: (76.5, 83.0, 180),
3: (148.0, 83.0, 180),
4: (40.75, 7.5, 0),
5: (112.25, 7.5, 0),
}
# ---- 穿线槽(本体坐标):(u0, u1, v0, v1) ----
# 2026-09-06 实测排线:电池排线 8 宽 × 38 长,尾插排线 18 宽 × 55 长。
# · 电池排线 38 mm 够不到远端边(J600 v=4.6 到远端边就要 40),改回**接口边外侧**开 10 × 3 的小槽,
# P+/P− 焊盘放主板投影下方贴着接口边(排线下穿后折回 ~6 mm);
# · 尾插排线 55 mm 走远端边:J1401(v 2.86) → 远端边 41.8 → 斜下到槽 ~4.2 → 穿板 1.6 → 折回到 J7n ~3.5 ≈ 51,
# 前提是 **J7n 贴着槽放(v 42.5)**,且槽按 18 mm 宽的排线开 20 mm 长。
# 2026-09-06 v4(用户拍板):**尾插槽挪到 J1401 正下方**(J1401 本体 u 47.5~59.7 / v 1.7~4.0),
# 排线从 J1401 出线朝接口边、绕过接口边折回主板下方、在 J1401 正下方下穿,
# 然后**沿载板背面往 +v 走**到远处的 J7n(用户给的排线可达长度 50 mm)。
# 长度核算:J1401→接口边 2.9 + 绕边下落(插头 1 + 主板 1 + 立柱 3)5 + 折回到槽心 2.9 + 穿板 1.6 + 背面 34.1 ≈ 46.5 ≤ 50 ✔
# 槽长 20(排线 18 宽,中心 u 53.6,实际覆盖 44.6~62.6);西端 45.0 离 H7 (42.6,3.7) 2.4 ≥ 2.25。
SLOTS = {
'bat': (18.8, 28.8, -4.2, -1.2), # 电池排线:接口边外侧,正对 J600(u≈23.8)
# 2026-09-06 v4c(用户拍板):尾插槽挪到**和电池槽同一条线**(接口边外侧 v −4.2~−1.2),排线翻过接口边直接下穿,
# 不再从主板反面 3.0 mm 的缝里钻。背面到 J7n 变长 5.6,J7n 从 v 37 收到 **v 35**:
# 2.86 + 绕边 5 + 到槽心 2.7 + 穿板 1.6 + 背面 37.7 ≈ 49.9 ≤ 50 ✔(实长 55)。
# 2026-09-06 顶角 M3 孔 (4,131):site1 rot 180 下槽的 x = 72.09 − u,u=65 → x 7.09 会压到 Ø6 焊盘(边 7.0)。
# 东端 65 → 64(x 8.09,离焊盘 1.09)。排线 18 宽盖 u 44.6~62.6,64 端还余 1.4。
'tail': (45.0, 64.0, -4.2, -1.2), # 尾插排线:接口边外侧,与电池槽同线,正对 J1401 (u 47.5~59.7)
}
# 排线在背面占的车道(本体坐标)。**车道里不放任何元件**(排线要平贴着走到插头 / 焊盘,0402 也别放):
# 尾插排线 u 44.6~62.6,v 4.4 ~ J7n 北沿(EDL 组 / VBUS 组因此整体西移到 u ≤ 44);
# 电池排线 u 18.8~27.8(8 mm 宽,对准 J600,到 J8n 偏西 1 mm),v −1.2(槽)~ 13(J8n 南沿)。
TAIL_LANE = (44.6, 62.6, -1.2, 36.3)
BAT_LANE = (18.8, 27.8, -1.2, 13.0) # 槽中心 23.8、J8n 中心 22.8,车道取两者并集
LANES = {'tail': (TAIL_LANE, 'j7'), 'bat': (BAT_LANE, 'j8')}
SLOT_KEEP = 0.5 # 铜/元件离槽边
# 上游 USB-C 的中心 X:壳体前端面与**右**板边齐平(中心离前端面 5.134 mm,§25.5 #1)
J23_X = BW - 5.134
# ---- 服务带(全部底层):本体坐标,v>0 = 主板投影内 ----
BAND = {
# ── v3b:电池侧靠接口边、尾插侧靠远端边,全部在主板投影下方 ──
# ── 电池侧(2026-09-06 v4b,用户拍板):TP8n1/TP8n2 换成排线直焊座 J8n(个人库 FPC-4P-P2.0_MI8-BAT,
# 4 焊盘 1.1×4.0 间距 2.0,fp_batfpc.py),并且**放到主板下方远处、35 mm 排线够得到的地方**(v 15)。
# 路径:J600(v 4.6) → 接口边 → 下落 ~5 → 电池槽(v −2.7) → 穿板 1.6 → 背面往 +v 走 17.7 到 J8n ≈ 31.6 ≤ 35 ✔(实长 38)。
# 排线车道 `BAT_LANE` u 18.8~27.8 / v −1.2~13 不放元件;中心 22.8(J600 在 23.77,偏 1 mm 让开 C8n1 焊盘框),焊盘 u 19.8/21.8/23.8/25.8。
# ⚠️ rot 0 = pin1(P+) 在 −u(西);rot 180 = P+ 在 +u。**手性待用户实测**(设计文档 §13.5 末尾),封装上下对称所以只改这一个 rot。
'j8': (22.8, 15.0, 0), # J8n 排线直焊座(框 u 19.25~26.35 / v 13~17;C8n1 的焊盘框跨 26.55~39.45,比罐体宽,留 0.2)
'c6': (17.5, 15.0, 0), # C8n6 100nF 0402,贴着 J8n 西侧
'c2': (13.5, 15.0, 0), # C8n2 100µF 1206(框 11.16~15.84)
'c3': (13.0, 18.5, 0), # C8n3 上一行西端
'c4': (18.0, 18.5, 0), # C8n4 22µF 1206
'c5': (23.0, 18.5, 0), # C8n5 J8n 正北(框 20.66~25.34 / 17.64~19.36)
'r78': (29.5, 18.5, 0), # R78n 10k TEMP→GND(代替保护板 NTC),C8n1 罐体北边
'r79': (31.5, 18.5, 0), # R79n 68k ID→GND
'c1': (34.3, 11.3, 0), # C8n1 2200µF/6.3V SMD 10×10.5(焊盘跨 27.85~40.75,罐体 28.9~40.0 × 5.9~16.7;v4b 东移 1.3 让开电池排线车道 27.8、上移 0.3 让 H7 (42.6,3.7) 的螺钉头 ≥ 2.8)
# 行 C(v=21):EDL 开关组
'r74': (34.5, 21.0, 0), # R74n 100k EDL_EN 下拉
'u4': (38.5, 21.0, 0), # U4n TS5A3157
'c76': (42.0, 21.0, 0), # C76n 100nF
# 行 D(v=26):VBUS 负载开关组(v4:整体西移 4.5,让开尾插排线车道 u ≥ 44.6)
'c70': (30.0, 26.0, 0), # C70n 2.2µF 0805 输入
'u2': (34.5, 26.0, 0), # U24~28 TPS22918
'c71': (38.5, 26.0, 0), # C71n 100nF 输出
'c72': (41.0, 26.0, 0), # C72n 100pF CT
'r70': (43.5, 26.0, 0), # R70n 100k 下拉(东沿 44.2 < 车道 44.6)
# 行 F(v=35,v4c):J7n 在排线车道尽头、主板投影下方远处。中心 u 52.5(J1401 53.58,偏西 1 mm 让开 H3 螺钉头)。
# 框 u 46.2~58.8 / v 33.7~36.3:离 H3 (61.9,33.7) 3.1、离 H1 (54.2,42.3) 6.0(都 ≥ 2.8)。
# ⚠️ 底层 **rot 0**(v3b 是 rot 180,改掉了)—— 由几何推出来的,理由见设计文档 §25.16 v4:
# 排线只绕 u 轴弯折(不在面内转),J7n 的触点图案 = J1401 的触点图案平移 + 翻面;
# J1401 的 pin1/2 端在 +u(用户实测 G1 在右端),所以 J7n 的 pin1/2 端必须在 −u,
# 而封装 pin51(= pin1/2 端)在本地 −X ⇒ 底层 rot 0(rot 180 会把 VBUS 端放到东边,与 §24.3 矛盾)。
# rot 0 底层:奇数排南、偶数排北;.6=DPJ 在 u 50.0、.4=DMJ 在 49.65(北排,朝 U5n);pin51=VBUS 西端。
# 镜像位 2026-09-05 已实测(J1401 靠接口边那一排是奇数排 = fp_bm24.ODD_ROW_TOP=True),rot 0 回读与推导一致 ⇒ 不用再量;装好后两点通断复核(设计文档 §12)。
'j7': (52.5, 35.0, 0), # J7n BM24-50DS
# 行 E(v≈37~39.5):ESD + CC 电阻在 J7n **北边**(插头背后,排线车道之外)
'u5': (48.8, 39.5, 180), # U5n USBLC6,rot 180 南排 1/2/3 朝 J7n(.3=DPJ 47.85 / .1=DMJ 49.75,与 J7n 次序相反,DMJ 走顶层交叉)
# 2026-09-06(第十三轮):两颗从 v 37.15 挪到 **v 38.0**。理由是 J7n 前排的逃逸带在 v5 里只剩 0.58 mm:
# J7n 前排焊盘外沿 36.30 ←→ R76n/R77n 焊盘下沿 36.88。0.20 的线要 0.58 才过得去,等于一根都不许错位;
# 而 `THERM_n`(J7n.14,u 50.40)要一路东行到 R77n.1(u 54.37),`CC1_n`(J7n.22,u 51.80)要往北进 R76n.1(u 51.37)
# —— 一个往东一个往北,**在 u≈51.6 处必然交叉**,同层无解;想让 THERM 翻顶层又打不下过孔
# (孔要离 J7n 前排 0.5 ⇒ v ≥ 36.80,又要离 R76n 焊盘 0.5 ⇒ v ≤ 36.38)。
# 挪到 38.0 之后逃逸带变成 1.43 mm,THERM 在 (50.90, 36.90) / (54.37, 36.90) 打两个孔走顶层跨过去,
# CC1 全程底层斜进 R76n.1,两者不再打架。北边 U5n 的焊盘在 u ≤ 50.015,离 R76n 西沿 51.084 还有 1.07。
# 中心 52.233 是算出来的:0603 两焊盘中心距 0.866,要让 **pin1 正好落在 u 51.800**
# = J7n.22 (CC1) 的正北,CC1 就是一根直上的竖线,不用斜、也不会蹭到 THERM 的换层孔。
'r76': (52.233, 38.0, 0), # R76n 56k CC1→VBUS,pin1 (51.800) = J7n.22 正北
'r77': (54.8, 38.0, 0), # R77n 100k,pin1 (54.367) 收 THERM
'j6': (62.5, 41.0, 0), # J6n EDL 1.25mm 2P,J7n 东北、H1 keep-out 东侧;EDL 飞线随尾插排线穿槽走背面 ~40 mm
# ── 按键 MOS 三列(2026-09-06 随 74HC595 从下板搬来,§26.23):顶针(顶层,u 2.1~5.4 / v 2.5~7)背面那片空地。
# 列距 3.5;SOT-23 框 ±1.5×±1.27,0402 框 ±0.72×±0.27(竖放 ±0.27×±0.72)。
# 东沿 15.5+1.5 = 17.0 < 电池排线车道 18.8;离 H6 (4,10.8) 最近的是 R%21 (8.5,10):4.6 > 2.8;
# 漏极→顶针焊盘要换层(顶针是顶层单面焊盘),过孔由布线器自己打。
'q3': (8.5, 4.0, 0), # Q%03 PWR
'q4': (12.0, 4.0, 0), # Q%04 VUP
'q5': (15.5, 4.0, 0), # Q%05 VDN
'r20': (8.5, 7.5, 90), # R%20 1k 栅阻
'r22': (12.0, 7.5, 90),
'r24': (15.5, 7.5, 90),
'r21': (8.5, 10.0, 90), # R%21 100k 栅极下拉
'r23': (12.0, 10.0, 90),
'r25': (15.5, 10.0, 90),
}
# SMD 罐体 keep-out(相对元件原点):10.3 见方 + 单边 0.25
CAN_BODY = (-5.4, -5.4, 5.4, 5.4)
def desig(n):
u"""第 n 个手机位用到的全部位号。"""
return {
'pogo': ['J%d' % (41 + (n - 1) * 4 + i) for i in range(4)],
'j8': 'J8%d' % n, 'r78': 'R78%d' % n, 'r79': 'R79%d' % n,
'c1': 'C8%d1' % (n - 1), 'c2': 'C8%d2' % (n - 1), 'c3': 'C8%d3' % (n - 1),
'c4': 'C8%d4' % (n - 1), 'c5': 'C8%d5' % (n - 1), 'c6': 'C8%d6' % (n - 1),
'j6': 'J6%d' % n, 'u4': 'U4%d' % n, 'r74': 'R74%d' % n, 'c76': 'C76%d' % n,
'j7': 'J7%d' % n, 'u5': 'U5%d' % n, 'r76': 'R76%d' % n, 'r77': 'R77%d' % n,
'u2': 'U%d' % (23 + n), 'c70': 'C70%d' % n, 'c71': 'C71%d' % n,
'c72': 'C72%d' % n, 'r70': 'R70%d' % n,
'q3': 'Q%d03' % n, 'q4': 'Q%d04' % n, 'q5': 'Q%d05' % n,
'r20': 'R%d20' % n, 'r21': 'R%d21' % n, 'r22': 'R%d22' % n,
'r23': 'R%d23' % n, 'r24': 'R%d24' % n, 'r25': 'R%d25' % n,
}
def to_board(n, u, v, rot=0):
u"""手机本体坐标 (u,v) -> 板坐标。rot=180 时整位绕本体中心转半圈。"""
x0, y0, rho = SITES[n]
if rho == 0:
return (x0 + u, y0 + v, rot)
return (x0 + PW - u, y0 + PH - v, (rot + 180) % 360)
def slot_rects():
u"""全部 10 条槽的板坐标矩形 [(name, x0, y0, x1, y1)]。"""
out = []
for n in SITES:
for key, (u0, u1, v0, v1) in SLOTS.items():
ax, ay, _ = to_board(n, u0, v0)
bx, by, _ = to_board(n, u1, v1)
out.append(('CH%d-%s' % (n, key), min(ax, bx), min(ay, by), max(ax, bx), max(ay, by)))
return out
# ═══════════════ 全局块(全部底层)═══════════════
GLOBAL = {
# 上游 USB-C,口朝 +X、壳体前端面与右板边齐平。**底层要 rot 270**(镜像语义见文件头)。
'J23': (J23_X, 62.0, 270),
# CC1/CC2 各 5.1k 下拉,贴着 J23 的 A5/B5 焊盘。底层 rot 270 之后 A5 在北、B5 在南,
# 所以和顶层那版相比 y 对调(--verify 会核)。0603 的 pin1 方向不受镜像影响,仍朝东。
# v3 布线:UP_DP/DM 从西边 y 61.25~62.75 进 J23,两颗 CC 电阻退到 x 209、上下各让开 1.5
'R717': (209.0, 64.5, 180), # pin1 = CC1 -> J23.A5 (212.39, 63.25)
'R718': (209.0, 59.5, 180), # pin1 = CC2 -> J23.B5 (212.39, 60.25)
# AMS1117-3.3 组:J31 东端与 Hub 西侧之间(+5V 从 J31 来,3V3 就近喂 Hub)
# v3 布线:U31 组挪到 J31 东端**南**侧(走廊北带 y 70~80 留给差分对上顶层北上)
'U31': (87.5, 60.5, 0), # 底边 57.65 让开 CH4 电池槽(y ≤ 56.36 + 0.5);USB1 车道改走 y 64.0/64.46
'C736': (80.5, 58.5, 0), # +5V 输入 0805(U31 西侧)
# ── 2026-09-07:U32 的 PRT_SWP1~5 五个 strap 脚的 100k 下拉(R900~R904,见 §21.6a)──
# BOOST/GANG_EN/SWP6-7 那五颗用户拍板不装(采到 1 也无功能后果)。
# ⚠️ 第一版把十颗全摆在 U32 北边 y=79 那一排(那一片唯一的整块空地),
# `pcb2_route_sig` **10 条只布通 3 条** —— 南边(49/50/51)、西边(15/17)、东南(47/48)
# 那七个脚要绕过整颗 QFN 和一圈去耦,绕不出去。改成**每颗贴着自己的引脚**。
# 坐标是 `pcb2_strap_place.py` 按布线器的障碍表搜出来的(以引脚为心一圈圈往外找,
# 判据:两个焊盘离异网铜 >= 0.20 且不落进任何已有元件的本体投影)。改摆位请重跑它。
'R904': (103.458, 73.632, 0), # U32.31 PRT_SWP5,离脚 1.2 mm
'R903': (105.632, 71.958, 90), # U32.33 PRT_SWP4,离脚 1.2 mm
'R902': (106.635, 62.145, 0), # U32.47 PRT_SWP3,离脚 3.4 mm
'R901': (103.021, 59.414, 0), # U32.49 PRT_SWP2,离脚 4.2 mm
'R900': (102.750, 58.550, 0), # U32.51 PRT_SWP1,离脚 5.0 mm
# 2026-09-07:C737 由 1206 陶瓷换成 KEMET T491A226K010AT 固态钽(AMS1117 手册 §Stability
# 要求的 22uF 固态钽)。钽是极性件且 1 脚 = 正极,而板上 +3V3_HUB 的铜在东、GND 在西,
# 所以必须 rot 180;再东移 0.17 mm,让 1 脚(+3V3_HUB)西沿离那颗 GND 过孔 0.265 mm。
# 改这两个数之前先读 §26.25:0.17 是算出来的净距,不是随手摆的。
'C737': (93.17, 54.0, 180), # +3V3 输出,CASE-A_3216 固态钽
# LM75BD 测温 + 100nF(SCL/SDA 在 J31.21/23 = x 63.3/65.8,北排;走线从排母针缝里穿到南边)
# v2 第二版:挪到 J31 **南**边(走廊北带 y 70~76 要留给差分高速路)。CH4 行 B 只剩 U54(48.3)/R764/R774,这里是空的。
# v3 布线:J31 **南**边(USB1 那对在底层 y 62.9/63.6 从排母南边一路西行,U34 顶边 62.2)
'U34': (58.0, 60.0, 90),
'C760': (62.5, 60.0, 90),
# 上游 VBUS 分压 -> UP_VBUS_DET -> J31 pin36 (81.049, 65.100),跟着高阻端走
'R721': (80.0, 61.5, 180), # pin1 = VBUS_UP(v3:让开 U31 与 USB1 车道)
'R722': (78.0, 61.5, 180), # pin1 = UP_VBUS_DET, pin2 = GND
# ── 三片 74HC595(2026-09-06 从下板搬来,§26.23)。SOIC-16 框 9.5 × 7.44(rot 90 后 7.44 宽 × 9.5 高)。
# 走廊中段 x 113~132 里能用的其实只有 **y 60.8~75.26 那条带**:UP_DP/UP_DM(底层 y 60.1/60.6)从 x 99 一路横穿到 205,
# 下面 y 52.5/53.0 是 USB5,上面 y 75.4/77.0 是 USB3(第一版只查了端点落在框里的线,把 UP 这对漏了,
# U21 rot 0 摆在 y 58.5 直接压在 UP 上;再上一版 rot 90 面对面缝 1.56,16 个脚的出线口互相堵死)。
# U21 (117,66.5) / U22 (128,66.5) rot 90:x 113.28~120.72 / 124.28~131.72(缝 3.56,出线段各 1.3 还剩 1 mm),
# y 61.75~71.25:离 UP_DM 线边 60.74 有 1.0,离 USB3_DP 线边 75.26 有 4.0;离 USB5 竖段 111.44 有 1.84、离 J32 焊盘边 133.0 有 1.28。
# U33 单独放 **J32 东侧**(x 183~208 / y 52~86 只有 UP 那对在 y 60.1/60.6 横穿):(193.5,68.5) rot 90 → y 63.75~73.25,离 UP_DM 3.0。
# 四根控制线从 J32 北排 3~6 行(x 138.95~146.57)进来,正好夹在两处中间。
# 2026-09-07 第三十轮:rot 90 的两片各 8 个西排脚全从 x 113~114 / 124 那两条竖缝出线,8 个网络布不通
# (EDL_EN2/3/5、K5_VDN 等,§26.23 落地末尾)。改成 **rot 0 / 180:引脚排朝南北**,16 根脚沿 9 mm 宽的边出线:
# 北带 y 70.23~75.26(5 mm,C604/R609/C605 在 y 73)、南带 y 60.74~62.77(2 mm,够打一排换层孔到顶层)。
# 底层 rot 0:脚 1~8(K/EDL 输出)在**北**排、9~16(控制/+3V3)在南排;rot 180 反过来。
# U21 rot 0:K1 去 site1(西北)、EDL_EN2/3 去 site2/3(北),输出朝北顺路;控制脚南排沿 y≈63 东行去 J32。
# U22 rot 180:K4/K5 去 site4/5(南),输出朝南,从南带换层到顶层跨过 UP 差分;控制脚北排。
# U21 (116.5,66.5):焊盘 x 111.56~121.44,离 USB5 竖段线边 111.4 有 0.5(≥0.2);
# U22 (127.5,66.5):焊盘 x 122.56~132.44,离 J32.1 盘边 ~133.1 有 0.6;两片焊盘之间 1.1。
'U21': (116.5, 66.5, 0),
'U22': (127.5, 66.5, 180),
'U33': (193.5, 68.5, 90),
# 去耦 / OE# 上拉都放两片北边那条带(y 72.73~73.27,离芯片上沿 71.25 有 1.5、离 USB3_DP 有 2.0)
'C604': (117.0, 73.0, 0),
'C605': (128.0, 73.0, 0),
'R609': (122.5, 73.0, 0),
'C739': (199.5, 68.5, 90),
}
HDR = {'J31': (62.0, 67.5, 0), 'J32': (158.0, 67.5, 0)}
HUB_CENTER = (100.0, 68.0) # v1 是 (110, 67):西移 10 让开 CH2 行 A / CH5 行 B,北移 1 让 C741 离开 CH4 电池槽
HUB_PARTS = (['U32', 'Y1'] + ['C7%d' % i for i in range(40, 52)]
+ ['R7%02d' % i for i in (8, 9, 10, 11, 12, 14, 15, 16, 19, 20)])
def th_parts():
u"""直插件:引脚穿板,**顶层和底层都占位**。只剩 J31 / J32(C8n1 换 SMD、TP8n1/2 换 J8n 之后)。"""
return set(HDR)
def hub_layout():
u"""Hub 的 24 个卫星件,逐脚对位手排(几何同 v1,只是整簇随 HUB_CENTER 平移)。
通道:西侧 x < −4.45 留给五对下游 D± 扇出;南侧竖井 x −2.4~−0.2 给 XTAL + UP_DP/DM;
晶振窝 (−4.2, −9.2)。RB=8.9 是 2026-09-06 拍板的 B 圈半径。"""
hx, hy = HUB_CENTER
RA, RB = 6.3, 8.9
out = {'U32': (hx, hy, 0)}
for dsg, dx in (('C744', -4.45), ('R708', -3.45), ('C743', -2.65),
('R715', 1.25), ('R714', 2.45), ('C746', 3.65)):
out[dsg] = (hx + dx, hy - RA, 270)
# ⚠️ C745 **不在 A 圈上**(用户 2026-09-06 拍板)。它原来在 (hx+0.05, hy−RA),
# 那个位置的 GND 脚是全板唯一扇不出去的焊盘 —— 四面都被堵死:
# 南 `UP_DM` 东行段 y=60.60(孔要 0.64,只有 0.41)
# 北 U32 南排焊盘 y=63.55 与 A 圈之间的 0.73 mm 死带(孔要 1.0)
# 东 R715 pin2(缝 0.667) 西 `UP_DP`/`UP_DM` 两条竖段(相距 0.5,塞不下 Ø0.6)
# 往南跨过 UP 那一对就宽敞了。GND 脚 (100.0, 58.60) / +3V3 脚 (100.0, 59.44),
# 过孔打在正南 (100.0, 57.83)。代价是离 Hub 远了 2.7 mm ——
# 它只是 33 个 +3V3_HUB 焊盘里的一颗去耦,A 圈上还有 C746、北边还有 C742/C749。
out['C745'] = (hx + 0.00, hy - 8.98, 270)
out['Y1'] = (hx - 4.2, hy - 9.2, 0)
out['C740'] = (hx - 7.2, hy - 8.35, 180)
out['C741'] = (hx - 3.1, hy - 12.3, 270)
out['C749'] = (hx - 0.75, hy + RA, 90)
out['C742'] = (hx + 0.75, hy + RA, 90)
# v2:10 µF 总储能挪到西北角横放 —— 原 (hx+3, hy+7) 竖放会压 CH2 的 TP812 / C702
out['C751'] = (hx - 4.0, hy + 6.3, 0)
# v3 布线:pin5/pin10 的 +3V3 电容不再贴西侧 —— 西侧 x 93.8~87.7 是十条 D± 车道扇开 + 换层过孔的地方。
# 两颗放到扇开后各自车道的尽头(pin5 车道 y 65.875 → 拐到 66.6;pin10 车道 y 69.625)。
# v5 布线(2026-09-06):pin5/pin10 的 +3V3 改走**顶层**(西缘 12 条车道里只有它俩能翻层),
# 两颗电容挪到 U31 pin2(+3V3) 东侧的 +3V3 主干上、竖放;x 88~90 那一带让给 USB2 北上车道。
out['C747'] = (88.2, 71.5, 90) # J31 最东列 (86.98) 与 USB2 北上车道 (89.27) 之间,pin5 从顶层绕过来
out['C748'] = (92.5, 71.2, 90) # USB3 东车道 (91.53) 与 USB5 西车道 (93.47) 之间,pin10 直接进
out['R719'] = (hx + RA, hy + 0.25, 0)
out['R712'] = (hx + RA, hy - 0.95, 0)
out['R709'] = (hx + RA, hy - 2.15, 180)
out['R710'] = (hx + RA, hy - 3.35, 180)
out['C750'] = (hx + RA, hy - 4.55, 0)
out['R711'] = (hx + RB, hy - 0.35, 0)
out['R716'] = (hx + RB, hy - 1.55, 0)
out['R720'] = (hx + RB, hy - 2.75, 0)
missing = set(HUB_PARTS) - set(out)
extra = set(out) - set(HUB_PARTS)
if missing or extra:
raise SystemExit(u'Hub 手排对不上 HUB_PARTS:漏 %s / 多 %s' % (sorted(missing), sorted(extra)))
return out
def plan():
u"""汇总所有元件 -> {位号: (x_mm, y_mm, rot, layer)}。顶层只有顶针。"""
pos = {}
for n in SITES:
d = desig(n)
for i, (u, v) in enumerate(POGO_XY):
x, y, r = to_board(n, u, v, 0)
pos[d['pogo'][i]] = (x, y, r, 1)
for key, (u, v, r0) in BAND.items():
x, y, r = to_board(n, u, v, r0)
pos[d[key]] = (x, y, r, 2)
for dsg, (x, y, r) in list(GLOBAL.items()) + list(HDR.items()) + list(hub_layout().items()):
pos[dsg] = (x, y, r, 2)
return pos
# ═══════════════ 本地检查 ═══════════════
def load_bb():
u"""从 pcb2-parts.json 读每个元件的焊盘包围盒(相对元件原点、rot 0 本地系,mm)。"""
raw = json.load(open(os.path.join(HERE, 'pcb2-parts.json'), encoding='utf-8'))
bb = {}
for c in raw:
if not c['bb']:
bb[c['d']] = (-0.5, -0.5, 0.5, 0.5)
continue
x, y = c['x'], c['y']
b = c['bb']
bb[c['d']] = ((b[0] - x) / MIL, (b[1] - y) / MIL, (b[2] - x) / MIL, (b[3] - y) / MIL)
return bb
CAN = None # 延迟算:五个 Ø10 电解的位号
def rot_box(b, rot):
u"""本地包围盒按 rot(0/90/180/270,逆时针)转到板系(仍相对元件原点)。"""
x0, y0, x1, y1 = b
r = rot % 360
if r == 90:
return (-y1, x0, -y0, x1)
if r == 180:
return (-x1, -y1, -x0, -y0)
if r == 270:
return (y0, -x1, y1, -x0)
return b
def box(d, rot, bb, layer=1):
u"""元件在 `layer` 上占的框(相对元件原点,板系)。底层先把本地 y 翻一次(镜像语义)。"""
global CAN
if CAN is None:
CAN = {desig(n)['c1'] for n in SITES}
b = bb[d]
if layer == 2: # 焊盘包围盒:底层本地 y 翻一次
b = (b[0], -b[3], b[2], -b[1])
if d in CAN and layer == 2: # SMD 罐体(底层)比焊盘框大得多,按 CAN_BODY 留
x0, y0, x1, y1 = b
b = (min(x0, CAN_BODY[0]), min(y0, CAN_BODY[1]), max(x1, CAN_BODY[2]), max(y1, CAN_BODY[3]))
return rot_box(b, rot)
def _overlap(a, b, eps=0.02):
return (a[0] < b[2] - eps and b[0] < a[2] - eps and a[1] < b[3] - eps and b[1] < a[3] - eps)
def _dist_box_pt(b, px, py):
dx = max(b[0] - px, 0.0, px - b[2])
dy = max(b[1] - py, 0.0, py - b[3])
return math.hypot(dx, dy)
def check(pos):
bb = load_bb()
errs = []
known = set(bb)
for d in pos:
if d not in known:
errs.append(u'PCB 上没有这个位号: %s' % d)
missing = known - set(pos)
if missing:
errs.append(u'没安排位置的元件 %d 个: %s' % (len(missing), sorted(missing)[:12]))
TH = th_parts()
def layers(d, lay):
return (1, 2) if d in TH else (lay,)
def abs_box(d, L):
x, y, r, lay = pos[d]
b = box(d, r, bb, L)
return (x + b[0], y + b[1], x + b[2], y + b[3])
items = [d for d in pos if d in known]
# 越框
for d in items:
for L in layers(d, pos[d][3]):
b = abs_box(d, L)
if b[0] < 1.0 or b[2] > BW - 1.0 or b[1] < 1.0 or b[3] > BH - 1.0:
errs.append(u'越框: %s @ (%.1f, %.1f)' % (d, pos[d][0], pos[d][1]))
# 同层重叠
# 侧键触点盘(J41~J60)是 Ø1.5 的圆盘、盘心距最小 1.78 ⇒ 净距 0.28。
# 包围盒判会把菱形里相邻的两个盘报成重叠(dx 1.30 < 1.50),所以这一组按**圆**判:
# 盘心距 >= POGO_PAD_D + 0.20(DRC 净距)。
pogo = set()
for n in SITES:
pogo |= set(desig(n)['pogo'])
for i in range(len(items)):
for j in range(i + 1, len(items)):
a, b_ = items[i], items[j]
if a in pogo and b_ in pogo:
dd = math.hypot(pos[a][0] - pos[b_][0], pos[a][1] - pos[b_][1])
if dd < POGO_PAD_D + 0.20:
errs.append(u'触点盘太近: %s x %s 盘心距 %.2f(要 >= %.2f)' % (a, b_, dd, POGO_PAD_D + 0.20))
continue
shared = set(layers(a, pos[a][3])) & set(layers(b_, pos[b_][3]))
for L in sorted(shared):
if _overlap(abs_box(a, L), abs_box(b_, L)):
errs.append(u'重叠(L%d): %s(%.1f,%.1f) x %s(%.1f,%.1f)'
% (L, a, pos[a][0], pos[a][1], b_, pos[b_][0], pos[b_][1]))
# 槽:任何层的任何元件都不许压进槽(含 0.5 余量)
slots = slot_rects()
for d in items:
for L in layers(d, pos[d][3]):
b = abs_box(d, L)
for name, x0, y0, x1, y1 in slots:
if _overlap(b, (x0 - SLOT_KEEP, y0 - SLOT_KEEP, x1 + SLOT_KEEP, y1 + SLOT_KEEP), 0.0):
errs.append(u'%s (L%d) 压在槽 %s 上' % (d, L, name))
# 槽不许碰定位孔 / M3 孔
for name, x0, y0, x1, y1 in slots:
for n in SITES:
for hn, (u_, v_) in HOLES.items():
hx, hy, _ = to_board(n, u_, v_)
if _dist_box_pt((x0, y0, x1, y1), hx, hy) < HOLE_D / 2 + 1.0:
errs.append(u'槽 %s 离定位孔 P%d%s 太近' % (name, n, hn))
for i, (mx, my) in enumerate(MH, 1):
if _dist_box_pt((x0, y0, x1, y1), mx, my) < MH_PAD / 2 + 1.0:
errs.append(u'槽 %s 离 M3 孔 MH%d 太近' % (name, i))
# 排线在背面的车道里不许有任何底层元件(排线要平贴到插头 / 焊盘),车道尽头那个座子自己除外
for d in items:
if 2 not in layers(d, pos[d][3]):
continue
b = abs_box(d, 2)
for n in SITES:
for lname, ((u0, u1, v0, v1), owner) in LANES.items():
if d == desig(n)[owner]:
continue
ax, ay, _ = to_board(n, u0, v0)
bx, by, _ = to_board(n, u1, v1)
lane = (min(ax, bx), min(ay, by), max(ax, bx), max(ay, by))
if _overlap(b, lane, 0.0):
errs.append(u'%s 压在 CH%d 的%s排线车道上' % (d, n, u'尾插' if lname == 'tail' else u'电池'))
# 顶层只许有顶针
allow = set()
for n in SITES:
allow |= set(desig(n)['pogo'])
for d in items:
if pos[d][3] == 1 and d not in allow:
errs.append(u'%s 还在顶层' % d)
# 底层元件离定位孔 / M3 孔(螺钉头)
for d in items:
if 2 not in layers(d, pos[d][3]):
continue
b = abs_box(d, 2)
for n in SITES:
for hn, (u_, v_) in HOLES.items():
if hn in SKIP_HOLES:
continue
hx, hy, _ = to_board(n, u_, v_)
dd = _dist_box_pt(b, hx, hy)
if dd < HOLE_KEEP:
errs.append(u'%s 离定位孔 P%d%s 只有 %.2f(要 >= %.1f)' % (d, n, hn, dd, HOLE_KEEP))
for i, (mx, my) in enumerate(MH, 1):
dd = _dist_box_pt(b, mx, my)
if dd < MH_KEEP:
errs.append(u'%s 离 M3 孔 MH%d 只有 %.2f(要 >= %.1f)' % (d, i, dd, MH_KEEP))
# 载板自己的 M3 孔不许落进手机本体投影,也不许和定位孔挨太近
for i, (mx, my) in enumerate(MH, 1):
for n, (x0, y0, rho) in SITES.items():
if x0 - MH_PAD / 2 < mx < x0 + PW + MH_PAD / 2 and y0 - MH_PAD / 2 < my < y0 + PH + MH_PAD / 2:
errs.append(u'载板 M3 孔 MH%d (%.1f,%.1f) 落在 CH%d 主板投影里' % (i, mx, my, n))
for n in SITES:
for hn, (u_, v_) in HOLES.items():
hx, hy, _ = to_board(n, u_, v_)
dd = ((mx - hx) ** 2 + (my - hy) ** 2) ** 0.5
need = MH_PAD / 2 + HOLE_D / 2 + 0.2
if dd < need:
errs.append(u'载板 M3 孔 MH%d 离定位孔 P%d%s 只有 %.2f mm(要 >= %.2f)'
% (i, n, hn, dd, need))
return errs
# ═══════════════ 下发 ═══════════════
def call(body, timeout=300):
r = run("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,1500));" % PCB + body, timeout=timeout)
if not r.get('success'):
print(u'下发失败:', json.dumps(r, ensure_ascii=False)[:600])
sys.exit(1)
return r['result']
def push_layers():
r = call("""
const before = await eda.pcb_Layer.getTheNumberOfCopperLayers();
await eda.pcb_Layer.setTheNumberOfCopperLayers(4);
const after = await eda.pcb_Layer.getTheNumberOfCopperLayers();
await eda.pcb_Document.save();
return {before, after};
""")
print(u'铜层数 %s -> %s' % (r['before'], r['after']))
def clean_copper():
u"""换版:删信号层导线(白名单 1/2/15/16)、全部过孔、全部铺铜。丝印/装配/文档层不动(§9n)。"""
r = call("""
let dl = 0, dv = 0, dp = 0;
// ⚠️ 一遍删不干净(实测 1416 条删完回读还剩 16 条),循环到剩 0(skill §9x)
for (let pass = 0; pass < 4; pass++) {
const todo = (await eda.pcb_PrimitiveLine.getAll()).filter(l => [1, 2, 15, 16].includes(l.getState_Layer()));
if (!todo.length) break;
for (const l of todo) { await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); dl++; }
}
for (const v of await eda.pcb_PrimitiveVia.getAll()) { await eda.pcb_PrimitiveVia.delete(v.getState_PrimitiveId()); dv++; }
for (const p of await eda.pcb_PrimitivePour.getAll()) { await eda.pcb_PrimitivePour.delete(p.getState_PrimitiveId()); dp++; }
const left = {};
for (const l of await eda.pcb_PrimitiveLine.getAll()) { const L = l.getState_Layer(); left[L] = (left[L] || 0) + 1; }
const vias = (await eda.pcb_PrimitiveVia.getAll()).length;
const pours = (await eda.pcb_PrimitivePour.getAll()).length;
await eda.pcb_Document.save();
return {dl, dv, dp, left, vias, pours};
""", timeout=600)
print(u'清铜: 删导线 %d / 过孔 %d / 铺铜 %d;剩余导线按层 %s,过孔 %d,铺铜 %d'
% (r['dl'], r['dv'], r['dp'], r['left'], r['vias'], r['pours']))
for L, want in ((3, 50), (9, 100), (13, 20)):
if r['left'].get(str(L), 0) < want:
print(u'!! 层 %d 只剩 %d 条,丝印/装配/文档被误删?' % (L, r['left'].get(str(L), 0)))
def _polysrc(start, pts):
src, mode = list(start), None
for p in pts:
if p[0] == 'L':
if mode != 'L':
src.append('L'); mode = 'L'
src += [round(p[1] * MIL, 4), round(p[2] * MIL, 4)]
else:
src.append('ARC'); mode = 'ARC'
src += [p[1], round(p[2] * MIL, 4), round(p[3] * MIL, 4)]
return src
def outline_poly():
u"""板框:一条闭合多段线(4 直边 + 4 个 R5 圆角),逆时针、正角弧。"""
R = CORNER_R
pts = []
L = lambda x, y: pts.append(('L', x, y))
A = lambda x, y: pts.append(('ARC', 90.0, x, y))
L(BW - R, 0.0); A(BW, R)
L(BW, BH - R); A(BW - R, BH)
L(R, BH); A(0.0, BH - R)
L(0.0, R); A(R, 0.0)
return _polysrc([round(R * MIL, 4), 0.0], pts)
def slot_poly(x0, y0, x1, y1):
u"""跑道形槽:两端半圆(半径 = 槽宽/2)。逆时针,每端两段 90° 弧。"""
r = (y1 - y0) / 2.0
cy = (y0 + y1) / 2.0
pts = [('L', x1 - r, y0), ('ARC', 90.0, x1, cy), ('ARC', 90.0, x1 - r, y1),
('L', x0 + r, y1), ('ARC', 90.0, x0, cy), ('ARC', 90.0, x0 + r, y0)]
return _polysrc([round((x0 + r) * MIL, 4), round(y0 * MIL, 4)], pts)
def push_outline():
n = call("""
let del = 0;
for (const l of await eda.pcb_PrimitiveLine.getAll())
if (l.getState_Layer() === 11) { await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); del++; }
for (const p of await eda.pcb_PrimitivePolyline.getAll())
if (p.getState_Layer() === 11) { await eda.pcb_PrimitivePolyline.delete(p.getState_PrimitiveId()); del++; }
const poly = await eda.pcb_MathPolygon.createPolygon(%s);
const r = await eda.pcb_PrimitivePolyline.create('', 11, poly, 5);
const left = (await eda.pcb_PrimitivePolyline.getAll()).filter(p => p.layer === 11).length;
return {del, made: r ? r.primitiveId : null, left};
""" % json.dumps(outline_poly()))
if n['left'] != 1:
print(u'!! layer 11 上有 %d 条多段线,应为 1' % n['left'])
sys.exit(1)
print(u'板框: %.0f × %.0f mm,一条闭合多段线(R%.0f 圆角)' % (BW, BH, CORNER_R))
# 槽:layer 12 (MULTI) 的填充 = 挖槽区域。先删旧的再建。
slots = slot_rects()
s = call("""
const S = %s;
let del = 0;
for (const f of await eda.pcb_PrimitiveFill.getAll())
if (f.getState_Layer() === 12) { await eda.pcb_PrimitiveFill.delete(f.getState_PrimitiveId()); del++; }
let made = 0, fail = [];
for (const [name, src] of S) {
const poly = await eda.pcb_MathPolygon.createPolygon(src);
const r = await eda.pcb_PrimitiveFill.create(12, poly);
if (r) made++; else fail.push(name);
}
const fills = (await eda.pcb_PrimitiveFill.getAll()).map(f => f.getState_Layer());
return {del, made, fail, fills};
""" % json.dumps([[name, slot_poly(x0, y0, x1, y1)] for name, x0, y0, x1, y1 in slots]))
print(u'穿线槽: 建 %d / 失败 %s(删旧 %d);板上 layer12 填充 %d 个'
% (s['made'], s['fail'], s['del'], sum(1 for L in s['fills'] if L == 12)))
if s['made'] != len(slots):
sys.exit(u'!! 槽没建全')
for name, x0, y0, x1, y1 in slots:
print(u' %-9s x %.2f~%.2f y %.2f~%.2f (%.1f × %.1f)' % (name, x0, x1, y0, y1, x1 - x0, y1 - y0))
k = call("""
const MH = %s;
for (const p of await eda.pcb_PrimitivePad.getAll()) {
const num = p.getState_PadNumber();
if (typeof num === 'string' && num.indexOf('MH') === 0) await eda.pcb_PrimitivePad.delete(p.getState_PrimitiveId());
}
let k = 0;
for (let i = 0; i < MH.length; i++) {
await eda.pcb_PrimitivePad.create(12, 'MH' + (i + 1), MH[i][0], MH[i][1], 0,
['ELLIPSE', %.2f, %.2f], 'GND', ['ROUND', %.4f], 0, 0, 0, true);
k++;
}
return k;
""" % (json.dumps([[round(x * MIL, 2), round(y * MIL, 2)] for x, y in MH]),
MH_PAD * MIL, MH_PAD * MIL, MH_HOLE * MIL))
print(u'载板自己的 M3 定位孔: %d 个(Φ%.1f / 焊盘 Φ%.1f / 接 GND)' % (k, MH_HOLE, MH_PAD))
call("await eda.pcb_Document.save(); return 1;")
def mech_items():
u"""手机定位孔(NPTH,7 孔模型减去 SKIP_HOLES ⇒ 30 个)+ 5 个手机位的丝印外框与标签。"""
holes, lines, texts = [], [], []
for n in SITES:
x0, y0, rho = SITES[n]
for hn, (u, v) in HOLES.items():
if hn in SKIP_HOLES: # 2026-09-07:H5 不打(push_mech 会先删掉板上全部 P*H* 再建)
continue
x, y, _ = to_board(n, u, v)
holes.append(['P%d%s' % (n, hn), round(x * MIL, 2), round(y * MIL, 2)])
c = [(x0, y0), (x0 + PW, y0), (x0 + PW, y0 + PH), (x0, y0 + PH)]
for i in range(4):
a, b = c[i], c[(i + 1) % 4]
lines.append([round(a[0] * MIL, 2), round(a[1] * MIL, 2),
round(b[0] * MIL, 2), round(b[1] * MIL, 2)])
ey = y0 if rho == 0 else y0 + PH
lines.append([round((x0 + 2) * MIL, 2), round((ey + (1.5 if rho == 0 else -1.5)) * MIL, 2),
round((x0 + PW - 2) * MIL, 2), round((ey + (1.5 if rho == 0 else -1.5)) * MIL, 2)])
texts.append([round((x0 + PW / 2 - 6) * MIL, 2), round((y0 + PH / 2) * MIL, 2),
u'CH%d 主板' % n])
return holes, lines, texts
def push_mech():
holes, lines, texts = mech_items()
n = call("""
const H = %s;
for (const p of await eda.pcb_PrimitivePad.getAll()) {
const num = p.getState_PadNumber();
if (typeof num === 'string' && /^P[1-5]H[1-7]$/.test(num)) await eda.pcb_PrimitivePad.delete(p.getState_PrimitiveId());
}
let k = 0;
for (const [name, x, y] of H) {
await eda.pcb_PrimitivePad.create(12, name, x, y, 0,
['ELLIPSE', %.4f, %.4f], '', ['ROUND', %.4f], 0, 0, 0, false);
k++;
}
return k;
""" % (json.dumps(holes), HOLE_D * MIL, HOLE_D * MIL, HOLE_D * MIL))
print(u'手机定位孔: %d 个 NPTH Φ%.1f(⚠ 必须配尼龙立柱)' % (n, HOLE_D))
m = call("""
const L = %s, T = %s;
let del = 0;
for (const l of await eda.pcb_PrimitiveLine.getAll())
if (l.getState_Layer() === 3) { await eda.pcb_PrimitiveLine.delete(l.getState_PrimitiveId()); del++; }
for (const s of await eda.pcb_PrimitiveString.getAll())
if (s.getState_Layer() === 3) { await eda.pcb_PrimitiveString.delete(s.getState_PrimitiveId()); del++; }
let k = 0;
for (const [x1,y1,x2,y2] of L) { await eda.pcb_PrimitiveLine.create('', 3, x1, y1, x2, y2, 6); k++; }
for (const [x,y,t] of T) { await eda.pcb_PrimitiveString.create(3, x, y, t, 'NotoSansSC-Regular', 60, 6, 0, 0, false, 0, false, false); }
return {del, k, t: T.length};
""" % (json.dumps(lines), json.dumps(texts, ensure_ascii=False)))
print(u'手机位丝印: %d 条线 + %d 个标签(顺带删掉 %d 个旧图元)' % (m['k'], m['t'], m['del']))
call("await eda.pcb_Document.save(); return 1;")
def push_parts(pos):
plan_list = [[d, round(x * MIL, 2), round(y * MIL, 2), r, lay]
for d, (x, y, r, lay) in sorted(pos.items())]
total = 0
for k in range(0, len(plan_list), 50):
total += call("""
const P = %s;
const map = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll()) map[c.getState_Designator()] = c;
let n = 0;
for (const [d, x, y, rot, lay] of P) {
const c = map[d]; if (!c) continue;
await eda.pcb_PrimitiveComponent.modify(c.getState_PrimitiveId(), {layer: lay});
await eda.pcb_PrimitiveComponent.modify(c.getState_PrimitiveId(), {x, y, rotation: rot});
n++;
}
return n;
""" % json.dumps(plan_list[k:k + 50]))
print(u' 已摆 %d / %d' % (total, len(plan_list)))
call("await eda.pcb_Document.save(); return 1;")
print(u'布局已下发并保存:%d 个元件' % total)
def verify(pos):
u"""回读关键件的焊盘坐标,核镜像语义(J23 口朝向、R717/R718 对 A5/B5、J7n 排向、层)。"""
r = call("""
const M = %s;
const want = %s;
const out = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
const d = c.getState_Designator();
if (!want.includes(d)) continue;
const pins = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId());
out[d] = {x: c.getState_X()/M, y: c.getState_Y()/M, rot: c.getState_Rotation(), layer: c.getState_Layer(),
pins: Object.fromEntries(pins.map(p => [p.padNumber, [+(p.x/M).toFixed(3), +(p.y/M).toFixed(3), p.layer]]))};
}
const layers = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll()) { const L = c.getState_Layer(); layers[L] = (layers[L]||0)+1; }
out._layers = layers;
return out;
""" % (MIL, json.dumps(['J23', 'R717', 'R718', 'J71', 'U51', 'C801', 'J31', 'R761', 'U24'])))
print(u'元件按层: %s' % r.pop('_layers'))
ok = True
j23 = r['J23']['pins']
sig_x = [v[0] for k, v in j23.items() if k in ('A5', 'B5', 'A6', 'B6', 'A7', 'B7', 'A8', 'B8')]
if max(sig_x) < r['J23']['x']:
print(u'✔ J23 信号焊盘在中心西侧 (x≈%.2f),口朝 +X' % sig_x[0])
else:
print(u'✘ J23 信号焊盘在东侧,口朝板内!'); ok = False
for rr, pad in (('R717', 'A5'), ('R718', 'B5')):
p1 = r[rr]['pins']['1']; tgt = j23[pad]
dd = math.hypot(p1[0] - tgt[0], p1[1] - tgt[1])
print(u'%s %s.pin1 (%.2f,%.2f) -> J23.%s (%.2f,%.2f) 距 %.2f' % (u'✔' if dd < 4 else u'✘', rr, p1[0], p1[1], pad, tgt[0], tgt[1], dd))
ok &= dd < 4
j71 = r['J71']['pins']
print(u'J71 (L%d) pin1 %s / pin2 %s / pin49 %s / pin50 %s' % (r['J71']['layer'], j71['1'], j71['2'], j71.get('49'), j71.get('50')))
print(u'J71 pin4(DMJ) %s / pin6(DPJ) %s / pin51(VBUS) %s / pin52(GND) %s' % (j71.get('4'), j71.get('6'), j71.get('51'), j71.get('52')))
# 按**本体坐标**判(站可能整体转了 180°):pin51(VBUS) 与 pin6(DPJ) 都要在 −u 侧
def _u(x):
x0, _, rho = SITES[1]
return (x - x0) if rho == 0 else (x0 + PW - x)
cu = _u(r['J71']['x'])
if j71.get('51') and _u(j71['51'][0]) < cu and _u(j71['6'][0]) < cu:
print(u'✔ J71 pin51(VBUS)/pin6(DPJ) 在本体 −u 侧 —— 与 §24.3「VBUS 落到 −X 端」一致(底层 rot 0 + 站向)')
else:
print(u'✘ J71 pin51 不在本体 −u 端!rot/镜像语义不对'); ok = False
print(u'U51 (L%d) pin1 %s pin6 %s' % (r['U51']['layer'], r['U51']['pins']['1'], r['U51']['pins']['6']))
print(u'C801 (L%d) pin1 %s pin2 %s' % (r['C801']['layer'], r['C801']['pins']['1'], r['C801']['pins']['2']))
print(u'R761 (L%d) pin1 %s pin2 %s' % (r['R761']['layer'], r['R761']['pins']['1'], r['R761']['pins']['2']))
print(u'U24 (L%d) pin1 %s pin4 %s' % (r['U24']['layer'], r['U24']['pins']['1'], r['U24']['pins']['4']))
print(u'J31 (L%d) pin1 %s pin2 %s' % (r['J31']['layer'], r['J31']['pins']['1'], r['J31']['pins']['2']))
return ok
def header_pads():
r = call("""
const out = [];
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
const d = c.getState_Designator();
if (d !== 'J31' && d !== 'J32') continue;
const pins = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId());
for (const p of pins) out.push([d, p.padNumber, p.x, p.y, c.getState_Layer()]);
}
return out;
""")
data = {'frame': u'载板坐标系,mm;下板原点 = 载板 (30, 5),下板侧目标 = 本坐标 − (30,5)',
'pads': [{'ref': a, 'pad': b, 'x': round(x / MIL, 3), 'y': round(y / MIL, 3),
'layer': lay} for a, b, x, y, lay in r]}
out = os.path.join(HERE, 'pcb2-header-pads.json')
json.dump(data, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
print(u'排母焊盘坐标已导出: %s(%d 个)' % (out, len(r)))
# 底层元件高度(mm,离载板底面),给下板布局避让用。没查到规格书的按同类估。
HEIGHT = {
'J31': 8.5, 'J32': 8.5, # 直插排母本体(C5307344)
'J23': 3.2, 'U31': 1.8, 'U32': 0.9, 'Y1': 1.2, 'U34': 1.6,
'C737': 1.8, # KEMET A 壳 EIA-3216-18,最大高 1.8(不是 1206 的 1.7)
}
def part_height(d):
if d in HEIGHT:
return HEIGHT[d]
if d in CAN:
return 10.5 # SMD 10×10.5 罐
if d.startswith('TP'):
return 0.0
if d.startswith('J7'):
return 1.0 # BM24-50DS 母座 + 插头(对插高 0.6~1.0)
if d.startswith('J8'):
return 0.5 # 电池排线直焊:排线 0.2 + 焊锡
if d.startswith('J6'):
return 3.6 # 1.25 mm 2P 卧式座
if d.startswith('U'):
return 1.2 # SOT23-6
if d.startswith('R'):
return 0.5
if d.startswith('C'):
return 1.7 # 1206 电容取最大
return 2.0
def heights(pos):
global CAN
bb = load_bb()
CAN = {desig(n)['c1'] for n in SITES}
out = []
for d, (x, y, r, lay) in sorted(pos.items()):
if lay != 2 and d not in th_parts():
continue
b = box(d, r, bb, 2)
out.append({'ref': d, 'x0': round(x + b[0], 2), 'y0': round(y + b[1], 2),
'x1': round(x + b[2], 2), 'y1': round(y + b[3], 2), 'h': part_height(d)})
slots = [{'name': n, 'x0': x0, 'y0': y0, 'x1': x1, 'y1': y1} for n, x0, y0, x1, y1 in slot_rects()]
data = {'frame': u'载板坐标系 mm,h = 元件离载板底面的高度;两板净空 13 mm ⇒ 下板同一 (x,y) 上元件高 ≤ 13 − h − 1',
'parts': out, 'slots': slots}
p = os.path.join(HERE, 'pcb2-bottom-heights.json')
json.dump(data, open(p, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
tall = [o for o in out if o['h'] >= 3.0]
print(u'底层高度图已导出: %s(%d 件,其中 ≥3 mm 的 %d 件)' % (p, len(out), len(tall)))
for o in tall:
print(u' %-6s h=%.1f x %.1f~%.1f y %.1f~%.1f' % (o['ref'], o['h'], o['x0'], o['x1'], o['y0'], o['y1']))
if __name__ == '__main__':
pos = plan()
errs = check(pos)
print(u'安排了 %d 个元件(顶层 %d / 底层 %d)'
% (len(pos), sum(1 for v in pos.values() if v[3] == 1),
sum(1 for v in pos.values() if v[3] == 2)))
if errs:
print(u'\n!! %d 个问题:' % len(errs))
for e in errs[:40]:
print(' ', e)
if len(errs) > 40:
print(u' ...还有 %d 条' % (len(errs) - 40))
# --force:只下发 --mech / --outline(孔、槽、丝印)时,元件级的报错不该挡路 ——
# 那些是「元件 vs 孔」「元件 vs 元件」,和要写的机械图元本身无关。
# ⚠️ 但 --push(重摆元件)绝不允许 force,摆错了会连着布线一起烂。
if '--force' in sys.argv and '--push' not in sys.argv:
print(u'--force:以上按警告处理,继续下发机械图元(元件没动)')
else:
sys.exit(1)
else:
print(u'本地检查通过:无重叠、无越框、没人压槽、顶层只有顶针、底层件避开了螺钉头')
if '--layers' in sys.argv:
push_layers()
if '--clean-copper' in sys.argv:
clean_copper()
if '--outline' in sys.argv:
push_outline()
if '--mech' in sys.argv:
push_mech()
if '--push' in sys.argv:
push_parts(pos)
header_pads()
if '--verify' in sys.argv:
if not verify(pos):
sys.exit(1)
if '--heights' in sys.argv:
heights(pos)