文件
phonerail/.claude/pcb2_rules.py
T
zhou yang和Claude Opus 5 2bfe308b30 初始导入:PhoneRail 五路手机直供电源板
原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。
这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。

纳入:
- 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本
- .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、
  pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源)
- 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾)
- firmware/ 的 A1 骨架与 site/ 文档站源码

.gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、
site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。

.gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存,
不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
2026-09-06 18:18:14 +08:00

473 行
23 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""载板 PCB2-CARRIER 设计规则 + D± 差分对(2026-09-05)。
做的事和下板的 `pcb_rules.py` 一模一样,只是网络表和对数不同 ——
**两个坑、两条陷阱全部照抄那边**,那边的文件头是唯一的说明书,改规则前先读它:
· `overwriteCurrentRuleConfiguration(x)` 要 config **本体**,不要 `{name,config}` 包装
· `getCurrentRuleConfiguration()` 是**嵌套形状**,`getRuleConfiguration(能力集)` 是**平形状**,
两者不能混用(混了会被静默吃掉)
· 下发之后**别再去 GUI 换“配置”下拉框**,换一次整套重置、网络指派全部悬空
· 写进去的 mm 会被换算成 mil 再换回来,`0.2` 回读是 `0.19999959999999997`,
任何几何比较都留 1e-4 容差
## 和下板不同的三处
1. **差分对 11 对,不是 7 对。** 载板上每一路 D± 被 ESD 器件 `U5n` 切成两段:
`USBn_DP/DM`(U32 → U5n,长段,走底层)与 `USBn_DPJ/DMJ`(U5n → J7n,约 5 mm,走顶层)。
两段都是同一条 90 Ω 链路,**两段都要建对**,否则短段会按普通信号走、
在 ESD 器件那头出现一截不受控阻抗。加上 `UPSTREAM` 共 11 对。
载板没有 `DEBUG` 那一对(ESP32 在下板)。
2. **几何 W0.28 / S0.18 直接照搬,不重算**(§25.6 ①)。嘉立创四层 1.6 mm
标准叠层上下对称(L1-L2 与 L3-L4 都是 0.2104 mm 介质),而载板的
层角色是镜像的(Inner1 = 电源面 / Inner2 = 完整地平面),
所以底层 D± 以 Inner2 为参考,介质厚度与下板顶层 D± 以 Inner1(地) 为参考完全相同。
⚠️ 顶层那 5 段 `*J` 的参考面是 **Inner1(分割的电源面)**,不是完整地。
段长只有 ~5 mm、又紧挨 ESD 器件,可以接受;但铺铜时要保证
每个 `U5n`↔`J7n` 下方的 Inner1 是**同一块**铜(VBUSn 岛),别让分割缝横穿这 5 mm。
3. **功率网络分三档**,不是两档:
| 规则 | 默认线宽 | 网络 | 依据 |
|---|---|---|---|
| `PWR4A` | 1.5 mm | `VO1_P`~`VO5_P` | 每路 4 A(IPC-2221 外层 1 oz,ΔT≈17 °C)|
| `PWR5V` | 1.2 mm | `+5V` | §9.4 最坏 2.8 A(1.2 mm 可载 3.6 A)|
| `PWR1A` | 0.6 mm | `VBUS1`~`5`、`+3V3_HUB` | 下行 VBUS 每口 0.5 A;Hub 3V3 最坏 0.42 A |
**最小值一律 0.25~0.3 mm**,理由同下板:规则是**按网络**生效的,
`+5V` / `VBUS` 上挂着 0402 去耦(焊盘才 0.57 mm 宽),
卡大线宽会让几十个焊盘扇不出去、刷出一屏假 DRC。
**载流靠灌铜保证,不靠 DRC**(§10.1)。
用法: /c/Python314/python .claude/pcb2_rules.py [--push | --check]
"""
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = 'f126ffb29c92f9b2' # PCB2-CARRIER
# ── 线宽规则(mm)──────────────────────────────────────────────────────
TRACK_RULES = {
'PWR4A': {'strokeWidthDefault': 1.5, 'strokeWidthMin': 0.3, 'strokeWidthMax': 6.0},
'PWR5V': {'strokeWidthDefault': 1.2, 'strokeWidthMin': 0.3, 'strokeWidthMax': 6.0},
# ⚠️ `strokeWidthMin` 2026-09-06 从 0.25 降到 **0.15**(和 DIFF 那次同一条道理):
# `+3V3_HUB` 要在 Hub 卫星圈 1.2 mm 网格之间的 0.659 mm 缝里穿过去,
# 0.25 的线要 0.65 + 量化余量,过不去;收到 0.15 才通。
# 载流上完全够:这一路最坏 420 mA,0.15 mm 外层 1 oz 在 ΔT=10 °C 下可载 ~0.8 A,
# 而且窄的只是几段几毫米长的颈,干路仍然是 0.35。
# **规则是按网络生效的**,最小值只能按最窄的那一段定 —— 同 §10.1。
'PWR1A': {'strokeWidthDefault': 0.6, 'strokeWidthMin': 0.15, 'strokeWidthMax': 3.0},
}
NET_TRACK = {}
for _n in range(1, 6):
NET_TRACK['VO%d_P' % _n] = 'PWR4A'
NET_TRACK['VBUS%d' % _n] = 'PWR1A'
NET_TRACK['+5V'] = 'PWR5V'
NET_TRACK['+3V3_HUB'] = 'PWR1A'
# ── 差分对几何(mm)── 与下板同一组数,理由见文件头第 2 条
# ⚠️ `strokeWidthMin` 2026-09-06 从 0.20 降到 **0.10**:BM24(0.35 间距)的 D± 出线
# 必须收窄到 0.11 mm 才能对相邻 GND 焊盘留住 0.20 的安全间距(算式见 pcb2_route_dp.W_ARR)。
# 和 §10.1 里 PWR4A/PWR12V 把最小值降到 0.3 是同一条道理:**规则是按网络生效的**,
# 一个网络上既有 0.28 的主干也有进细间距焊盘的颈,最小值只能按最窄的那一段定。
# 默认值仍是 0.28,交互布线画出来还是 0.28。
DIFF = {'strokeWidthDefault': 0.28, 'strokeWidthMin': 0.10, 'strokeWidthMax': 0.60,
'differentailPairSpacingDefault': 0.18, 'differentailPairSpacingMin': 0.15,
'differentailPairLenTolerMax': 5.0}
PAIR_RULE = 'USBPAIR'
PAIR_CLEARANCE = 0.15
PAIRS = [('UPSTREAM', 'UP_DP', 'UP_DM')]
PAIRS += [('USB%d' % n, 'USB%d_DP' % n, 'USB%d_DM' % n) for n in range(1, 6)]
PAIRS += [('USB%dJ' % n, 'USB%d_DPJ' % n, 'USB%d_DMJ' % n) for n in range(1, 6)]
MIN_CLEARANCE = 0.2
# ── 铺铜的焊盘连接方式(Plane / Copper Zone)────────────────────────────
# 默认条目 `copperRegion` 的 `connectMode` 是 **0 = 十字花**,辐条宽 0.254 mm。
# 2026-09-05 实测(改前/改后各铺一次、读回 `getState_PourFills()` 对比):
# connectMode 0 -> 每个本网通孔焊盘周围一圈反焊盘 + 4 条 `fill:false` 的描边辐条,
# VO4 那块铜面吐出 18 个 fill(1 个填充区 + 17 条辐条);
# connectMode 1 -> **全连接**,焊盘并进铜里,同一块铜面只剩 1 个 fill。
# 给五路 VOn_P 单独挂 `PLANE-DIRECT`,GND 留默认的十字花:
# · VOn_P 是 4 A 主路,每路在内层只有 5 个通孔焊盘(3 个排母针 + TP8n1 + C8n1.1),
# 而且只贴 Inner1 一层,全连接之后手焊仍可行,铜面几何也才量得准
# (十字花时铜面多边形把自己的焊盘挖空,`inner1_verify.py` 量到的断面会是 0.3 mm,
# 那是**度量假象**不是真断,但真假分不出来 —— 全连接之后这个歧义就没了);
# · GND 有 ~200 个通孔焊盘(光排母就 160 针),且同时贴 Inner1 底填和 Inner2 平面,
# 全连接会让手焊变得很难受;而回流被 26 根排母针分掉,辐条完全够 —— 见下。
#
# ⚠️ 「辐条太细扛不住 4 A」是个**看着对但错**的直觉:IPC-2221 的线宽表是给**长导体**的,
# 辐条长度只有 0.254 mm(就是净距那一段),两端都焊在大铜面上。
# 单条辐条 R = ρL/(Wt) = 1.72e-8 × 0.254e-3 / (0.254e-3 × 17.4e-6) ≈ 0.99 mΩ;
# 4 条并联 0.25 mΩ,4 A 上压降 1 mV / 发热 16 mW,
# 两端定温的均匀发热棒 ΔT ≈ PL/(8kA) ≈ **0.02 K**。所以十字花在电气上不是瓶颈,
# 这里改全连接是为了**几何可验证 + 少一处不确定**,不是因为十字花会烧。
PLANE_RULE = 'PLANE-DIRECT'
PLANE_CONNECT_DIRECT = 1 # 0 = 十字花(默认), 1 = 全连接(实测)
PLANE_NETS = ['VO%d_P' % _n for _n in range(1, 6)]
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,1500));" % PCB)
def call(body, timeout=240):
r = run(OPEN + body, timeout=timeout)
if not r.get('success'):
print(u'失败:', json.dumps(r, ensure_ascii=False)[:600])
sys.exit(1)
return r['result']
CAP_JS = u"""
const D = eda.pcb_Drc;
const cur = await D.getCurrentRuleConfiguration();
const c = JSON.parse(JSON.stringify(cur.config)); // 嵌套形状,原样送回
const multi = (await D.getRuleConfiguration(
'JLCPCB Capability(Multiple Layers Board)')).config; // 平形状,只当数据源
const TRACKS = %s, DW = %s, MINC = %s, PAIRRULE = %s, PAIRC = %s;
const PLANERULE = %s, PLANEMODE = %s;
const setTrack = (e, mn, df, mx) => {
for (const k of Object.keys(e.form.data))
Object.assign(e.form.data[k], {minValue: mn, defaultValue: df, maxValue: mx});
};
const T = c['Physics']['Track'], SS = c['Spacing']['Safe Spacing'];
// ── 1) 多层板能力集:三个放宽值 + 两条内层条目 ──
setTrack(T['copperThickness1oz'], 0.102, 0.254, 2.54);
if (!T['copperThickness0.5oz']) {
const e = JSON.parse(JSON.stringify(T['copperThickness1oz']));
e.editName = 'copperThickness0.5oz'; e.isSetDefault = false;
setTrack(e, 0.089, 0.254, 2.54);
T['copperThickness0.5oz'] = e;
}
Object.assign(c['Physics']['Via Size']['viaSize'].form,
{viaOuterdiameterMin: 0.3, viaInnerdiameterMin: 0.2});
if (!SS['copperThicknessInner0.5oz']) {
const e = JSON.parse(JSON.stringify(SS['copperThickness1oz']));
e.editName = 'copperThicknessInner0.5oz'; e.isSetDefault = false;
const src = multi['Spacing']['Safe Spacing']['copperThicknessInner0.5oz'].table;
for (const k of Object.keys(e.tables))
e.tables[k].content = JSON.parse(JSON.stringify(src));
SS['copperThicknessInner0.5oz'] = e;
}
// ── 2) 功率线宽规则 ──
for (const name of Object.keys(TRACKS)) {
const f = TRACKS[name];
const e = JSON.parse(JSON.stringify(T['copperThickness1oz']));
e.editName = name; e.isSetDefault = false; e.unit = 'mm';
setTrack(e, f.strokeWidthMin, f.strokeWidthDefault, f.strokeWidthMax);
T[name] = e;
}
// ── 3) 差分对几何 ──
const dp = c['Physics']['Differential Pair']['differentialPair'].form;
for (const k of Object.keys(dp.strokeWidthTables.data))
Object.assign(dp.strokeWidthTables.data[k],
{minValue: DW.strokeWidthMin, defaultValue: DW.strokeWidthDefault,
maxValue: DW.strokeWidthMax});
for (const k of Object.keys(dp.diffPairSpacingTables.data))
Object.assign(dp.diffPairSpacingTables.data[k],
{minValue: DW.differentailPairSpacingMin,
defaultValue: DW.differentailPairSpacingDefault});
dp.differentailPairLenTolerMax = DW.differentailPairLenTolerMax;
// ── 3b) 差分对对内间距条目(只给 net-by-net 用,不设为默认)──
if (!SS[PAIRRULE]) {
const e = JSON.parse(JSON.stringify(SS['copperThickness1oz']));
e.editName = PAIRRULE; e.isSetDefault = false;
for (const tk of Object.keys(e.tables || {}))
for (const row of (e.tables[tk].content || []))
for (let i = 0; i < row.length; i++)
if (typeof row[i] === 'number') row[i] = PAIRC;
SS[PAIRRULE] = e;
}
// ── 4) 安全间距全表抬到下限 ──
let bumped = 0;
for (const key of Object.keys(SS))
if (key !== PAIRRULE)
for (const tk of Object.keys(SS[key].tables || {}))
for (const row of (SS[key].tables[tk].content || []))
for (let i = 0; i < row.length; i++)
if (typeof row[i] === 'number' && row[i] < MINC) { row[i] = MINC; bumped++; }
// ── 5) 铺铜焊盘连接方式:给功率网络单独开一条「全连接」条目 ──
const CZ = c['Plane']['Copper Zone'];
if (!CZ[PLANERULE]) {
const e = JSON.parse(JSON.stringify(CZ['copperRegion']));
e.editName = PLANERULE; e.isSetDefault = false;
CZ[PLANERULE] = e;
}
for (const m of ['singleLayerPadModel', 'multiLayerPadModel'])
for (const k of Object.keys(CZ[PLANERULE].form[m].data))
CZ[PLANERULE].form[m].data[k].connectMode = PLANEMODE;
const ok = await D.overwriteCurrentRuleConfiguration(c); // ⚠️ 传本体
return {ok: ok === true, bumped: bumped, name: cur.name,
track: Object.keys(T), spacing: Object.keys(SS), plane: Object.keys(CZ)};
"""
def push_config():
r = call(CAP_JS % (json.dumps(TRACK_RULES), json.dumps(DIFF), MIN_CLEARANCE,
json.dumps(PAIR_RULE), PAIR_CLEARANCE,
json.dumps(PLANE_RULE), PLANE_CONNECT_DIRECT))
if not r.get('ok'):
print(u' [!!] overwriteCurrentRuleConfiguration 返回非 true,没生效')
sys.exit(1)
print(u' 规则配置已下发:线宽条目 %s' % ' '.join(r['track']))
print(u' 安全间距抬高 %d 格,间距条目 %s' % (r['bumped'], ' '.join(r['spacing'])))
print(u' 铺铜连接方式条目 %s' % ' '.join(r['plane']))
print(u' 配置名现在是 “%s”' % r['name'])
def caveat():
print(u'')
print(u'──── 一条必须记住的事 ────')
print(u'⚠️ 别去 GUI 的 设计 → 设计规则… 里换“配置”下拉框。')
print(u' 换一下整套重置,PWR4A / PWR5V / PWR1A 会被冲掉,')
print(u' %d 个网络的线宽指派立刻悬空(DRC 不报错,线宽也就不生效)。' % len(NET_TRACK))
print(u' 要改规则就改 pcb2_rules.py 再 --push。')
def _nums(o):
if isinstance(o, bool):
return []
if isinstance(o, (int, float)):
return [float(o)]
if isinstance(o, dict):
return [v for x in o.values() for v in _nums(x)]
if isinstance(o, list):
return [v for x in o for v in _nums(x)]
return []
def _find(o, key_sub):
if isinstance(o, dict):
for k, v in o.items():
if key_sub.lower() in str(k).lower() and isinstance(v, (int, float)) \
and not isinstance(v, bool):
return float(v)
for v in o.values():
r = _find(v, key_sub)
if r is not None:
return r
elif isinstance(o, list):
for v in o:
r = _find(v, key_sub)
if r is not None:
return r
return None
AUDIT_JS = """
await new Promise(r=>setTimeout(r,1500));
const c = await eda.pcb_Drc.getCurrentRuleConfiguration();
const rules = await eda.pcb_Drc.getNetRules();
const nbn = await eda.pcb_Drc.getNetByNetRules();
return {
cfgName: c && c.name,
layers: await eda.pcb_Layer.getTheNumberOfCopperLayers(),
track: Object.keys(c.config.Physics['Track']),
cap: {
track1oz: c.config.Physics['Track']['copperThickness1oz'],
via: c.config.Physics['Via Size']['viaSize'],
half: !!c.config.Physics['Track']['copperThickness0.5oz'],
inner: !!c.config.Spacing['Safe Spacing']['copperThicknessInner0.5oz'],
},
diff: c.config.Physics['Differential Pair'],
spacing: c.config.Spacing['Safe Spacing'],
pairs: (await eda.pcb_Drc.getAllDifferentialPairs()).map(d => d.name).sort(),
assigned: rules.filter(r => r['Track'] && r['Track'] !== 'default')
.map(r => [r.name, r['Track']]).sort(),
planeEntries: Object.keys(c.config['Plane']['Copper Zone']),
planeMode: ((((c.config['Plane']['Copper Zone'][%s] || {}).form || {})
.multiLayerPadModel || {}).data || {})['1'],
planeAssigned: rules.filter(r => r['Copper Zone'] && r['Copper Zone'] !== 'default')
.map(r => [r.name, r['Copper Zone']]).sort(),
nbn: ((nbn && nbn['Safe Spacing']) || []).length,
};
"""
def audit():
r = call(AUDIT_JS % json.dumps(PLANE_RULE))
bad = []
def chk(cond, good, msg):
print((u' [OK] ' if cond else u' [!!] ') + (good if cond else msg))
if not cond:
bad.append(msg)
print(u'──── 审计 ────')
chk(r['layers'] == 4, u'铜层数 4', u'铜层数是 %s,应为 4' % r['layers'])
cap = r['cap']
tmin = _find(cap['track1oz'], 'minValue')
if tmin is None:
tmin = _find(cap['track1oz'], 'strokeWidthMin')
vout = _find(cap['via'], 'viaOuterdiameterMin')
vin = _find(cap['via'], 'viaInnerdiameterMin')
okcap = (tmin is not None and abs(tmin - 0.102) < 5e-4
and vout is not None and abs(vout - 0.3) < 5e-4
and vin is not None and abs(vin - 0.2) < 5e-4
and cap['half'] and cap['inner'])
chk(okcap,
u'四层板能力集已就位(最小线宽 %.3f / 过孔 %.2f-%.2f,含 0.5oz 内层条目)'
% (tmin or -1, vout or -1, vin or -1),
u'还是两层板能力集:最小线宽 %.3f(应 0.102)/ 过孔 %.2f-%.2f(应 0.30-0.20)'
u' / 0.5oz 线宽条目 %s / 内层间距条目 %s'
% (tmin if tmin is not None else -1, vout if vout is not None else -1,
vin if vin is not None else -1,
u'有' if cap['half'] else u'无', u'有' if cap['inner'] else u'无'))
print(u' (配置名 “%s”)' % r['cfgName'])
for name in sorted(TRACK_RULES):
chk(name in r['track'], u'线宽规则 %s 存在' % name,
u'线宽规则 %s **不存在** —— 有 %d 个网络指着它,现在是悬空的'
% (name, sum(1 for _, t in r['assigned'] if t == name)))
w = _find(r['diff'], 'strokeWidthDefault')
if w is None:
w = _find(r['diff'], 'defaultValue')
sp = _find(r['diff'], 'PairSpacingDefault')
if sp is None:
d2 = (r['diff'] or {}).get('differentialPair', {}).get('form', {}) \
.get('diffPairSpacingTables')
sp = _find(d2, 'defaultValue')
chk(w is not None and abs(w - DIFF['strokeWidthDefault']) < 5e-3
and sp is not None and abs(sp - DIFF['differentailPairSpacingDefault']) < 5e-3,
u'差分对几何 W %.3f / S %.3f mm' % (w or -1, sp or -1),
u'差分对几何是 W %.3f / S %.3f,应为 W %.2f / S %.2f mm'
% (w if w is not None else -1, sp if sp is not None else -1,
DIFF['strokeWidthDefault'], DIFF['differentailPairSpacingDefault']))
# ⚠️ 容差留 1e-4:mm→mil→mm 往返,0.2 回读是 0.19999959999999997
vals = [v for v in _nums(r['spacing']) if 0 < v < 10]
mn = min(vals) if vals else -1
chk(mn >= PAIR_CLEARANCE - 1e-4,
u'安全间距下限 %.4f mm(对内那条 %.2f)' % (mn, PAIR_CLEARANCE),
u'安全间距下限只有 %.4f mm' % mn)
others = [v for k in r['spacing'] if k != PAIR_RULE
for v in _nums(r['spacing'][k]) if 0 < v < 10]
mno = min(others) if others else -1
chk(mno >= MIN_CLEARANCE - 1e-4, u'非差分对间距下限 %.4f mm' % mno,
u'非差分对间距下限只有 %.4f mm,应抬到 %.2f mm' % (mno, MIN_CLEARANCE))
want = sorted(n for n, _, _ in PAIRS)
chk(r['pairs'] == want, u'差分对 %d 对: %s' % (len(r['pairs']), ' '.join(r['pairs'])),
u'差分对对不上:有 %s,应为 %s' % (r['pairs'], want))
pm = r.get('planeMode') or {}
chk(PLANE_RULE in (r.get('planeEntries') or [])
and pm.get('connectMode') == PLANE_CONNECT_DIRECT,
u'铺铜连接方式条目 %s 在(connectMode=%s 全连接)' % (PLANE_RULE, pm.get('connectMode')),
u'没有 %s 条目或 connectMode 不是 %d —— 五路电源岛会退回十字花'
% (PLANE_RULE, PLANE_CONNECT_DIRECT))
pa = r.get('planeAssigned') or []
chk(sorted(n for n, _ in pa) == sorted(PLANE_NETS)
and all(v == PLANE_RULE for _, v in pa),
u'铺铜全连接指派 %d 个网络' % len(pa),
u'铺铜全连接指派是 %s,应为 %s 全挂 %s' % (pa, PLANE_NETS, PLANE_RULE))
chk(len(r['assigned']) == len(NET_TRACK),
u'线宽指派 %d 个网络' % len(r['assigned']),
u'线宽指派只有 %d 个,应为 %d' % (len(r['assigned']), len(NET_TRACK)))
chk(r['nbn'] == len(PAIRS), u'对内间距规则 %d 条' % r['nbn'],
u'对内间距规则 %d 条,应为 %d' % (r['nbn'], len(PAIRS)))
if bad:
caveat()
return not bad
def main():
print(u'载板 PCB2 规则:线宽 %d 条(%s),安全间距下限 %.2f mm,四层板能力集'
% (len(TRACK_RULES), ' '.join(sorted(TRACK_RULES)), MIN_CLEARANCE))
print(u'差分对:W %.2f / S %.2f / 线长容差 %.1f mm,共 %d 对'
% (DIFF['strokeWidthDefault'], DIFF['differentailPairSpacingDefault'],
DIFF['differentailPairLenTolerMax'], len(PAIRS)))
if '--check' in sys.argv:
sys.exit(0 if audit() else 1)
if '--push' not in sys.argv:
print(u'(未加 --push,没有下发;--check 审计当前状态)')
return
push_config()
n = call("""
const P = %s;
let k = 0;
for (const d of await eda.pcb_Drc.getAllDifferentialPairs())
await eda.pcb_Drc.deleteDifferentialPair(d.name);
for (const [name, p, m] of P) { if (await eda.pcb_Drc.createDifferentialPair(name, p, m)) k++; }
return k;
""" % json.dumps(PAIRS))
print(u' 差分对 %d / %d 对' % (n, len(PAIRS)))
if n != len(PAIRS):
print(u' [!!] 有对没建上 —— 多半是网络名在这块板上不存在,去 _pcb2_nets.json 核')
# ⚠️ `getNetRules()` 回来的是**整张表**,`overwriteNetRules` 整表覆盖 ——
# 线宽(Track)和铺铜连接方式(Copper Zone)必须在**同一次读改写**里做完,
# 分两次写的话后一次会拿着旧快照把前一次冲掉。
n = call("""
const T = %s, PN = %s, PR = %s;
const pset = new Set(PN);
const rules = await eda.pcb_Drc.getNetRules();
let k = 0, p = 0;
for (const r of rules) {
if (r.type !== 'net') continue;
if (T[r.name]) { r['Track'] = T[r.name]; k++; }
if (pset.has(r.name)) { r['Copper Zone'] = PR; p++; }
}
await eda.pcb_Drc.overwriteNetRules(rules);
return {track: k, plane: p};
""" % (json.dumps(NET_TRACK, ensure_ascii=False),
json.dumps(PLANE_NETS), json.dumps(PLANE_RULE)))
print(u' 线宽指派 %d / %d 个网络' % (n['track'], len(NET_TRACK)))
print(u' 铺铜全连接指派 %d / %d 个网络(%s)'
% (n['plane'], len(PLANE_NETS), ' '.join(PLANE_NETS)))
if n['plane'] != len(PLANE_NETS):
print(u' [!!] 有网络没指派上 —— 没指派 = 退回十字花,4 A 主路的铜面几何就量不准了')
# ⚠️ overwriteNetByNetRules 是整表覆盖,先读回再合并
n = call("""
const PAIRS = %s, RULE = %s;
const cur = await eda.pcb_Drc.getNetByNetRules();
const list = (cur && cur['Safe Spacing']) ? cur['Safe Spacing'].slice() : [];
const key = e => [e.net1.name, e.net2.name].sort().join('|');
const have = new Set(list.map(key));
let k = 0;
for (const [name, dp, dm] of PAIRS) {
const e = {net1: {type: 'net', name: dp}, net2: {type: 'net', name: dm},
rule: RULE, copperRuleName: null, planeRuleName: null};
if (have.has(key(e))) continue;
list.push(e); k++;
}
const ok = await eda.pcb_Drc.overwriteNetByNetRules(
{'Safe Spacing': list, 'Creepage Distance': (cur && cur['Creepage Distance']) || []});
const back = await eda.pcb_Drc.getNetByNetRules();
return {added: k, ok: ok === true, now: (back['Safe Spacing'] || []).length};
""" % (json.dumps(PAIRS), json.dumps(PAIR_RULE)))
print(u' 对内间距规则:新增 %d 条,现共 %d 条(%s = %.2f mm)'
% (n['added'], n['now'], PAIR_RULE, PAIR_CLEARANCE))
if n['now'] != len(PAIRS):
print(u' [!!] 应为 %d 条' % len(PAIRS))
call("await eda.pcb_Document.save(); return 1;")
print(u' 已保存')
caveat()
if __name__ == '__main__':
main()