文件
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

441 行
22 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""PCB1 设计规则 + D± 差分对。
做四件事:
1. **换掉规则配置**。转板时挂上的是 `JLCPCB Capability(Two Layers Board)` ——
这是块四层板,用两层板的能力集是错的。换成 `Multiple Layers Board`。
2. 加两条线宽规则并按网络指派:4A 通道输出、12V/5V 母线。
3. 差分对几何按 90 Ω 设:**W 0.28 / S 0.18 mm**(推导见下),线长容差 0.5 mm。
4. 建 7 对差分对:USB1~5(Hub 到五路出线座)、UPSTREAM(J23 到 Hub)、
DEBUG(J22 到 ESP32)。
## 线宽哪来的(IPC-2221,外层,ΔT=20 °C)
A = (I / (0.048 · ΔT^0.44))^(1/0.725) [mil²], W = A / 铜厚
4 A -> A = 72.5 mil² -> 1 oz(1.37 mil) 需 53 mil = 1.34 mm
8 A -> A = 188 mil² -> 1 oz 需 138 mil = 3.49 mm
外层已定 **1 oz**(§10.1)。4A 那条给 **1.5 mm**(温升 16.7 °C);
12V 分支给 **2.0 mm**,**总干那 8 A 走灌铜**,不当走线画。
## 90 Ω 是怎么算的 —— ⚠️ 必须拿板厂叠层复核
微带近似,按嘉立创四层 1.6 mm 标准叠层 L1-L2 介质 h=0.2104 mm、εr≈4.3、
**1 oz 外层(已定稿)**:
Z0 = 87/√(εr+1.41) · ln(5.98h / (0.8W+t))
Zdiff = 2·Z0·(1 − 0.48·e^(−0.96·S/h))
W=0.28 mm, S=0.18 mm -> Z0≈57.5 Ω, Zdiff≈90.5 Ω ✓
(回到 2 oz 的话同样几何只剩 83 Ω,W 要收到约 0.24 mm —— 铜厚进了 0.8W+t。)
叠层一变(板厚、prepreg 型号、内层铜厚)这两个数就得重算,
**打样前一定要用板厂的阻抗计算器复核**,别信这里的近似公式。
## 规则配置怎么下发(v3.2.149 实测,2026-09-03 摸清楚)
一度以为“只能手动”,**其实是参数形状给错了**。两个坑:
1. `overwriteCurrentRuleConfiguration(x)` 要的是 **config 本体**,
不是 `getRuleConfiguration()` 返回的 `{name, config}` 包装。
给包装它返回 `undefined` 且什么也不做;给本体返回 `true`。
2. **两种 config 形状不一样,不能混用。**
- `getRuleConfiguration('<系统能力集>')` → **平形状**:
`form.strokeWidthDefault` / `Safe Spacing[*].table`
- `getCurrentRuleConfiguration()` → **嵌套形状**(文档内部表示):
`form.data['1'].defaultValue` / `Safe Spacing[*].tables['1'].content`
把平形状丢进 `overwriteCurrentRuleConfiguration` 会被默默吃掉。
**正确做法:拿 `getCurrentRuleConfiguration().config` 改,原形状送回去。**
`saveRuleConfiguration(cfg, name, allowOverwrite)` 两种形状都返回 `false`,
真的不能用——但没关系,我们不需要存命名配置,只需要当前文档的规则对。
### “切到多层板能力集”实际就是五件事
把两个能力集逐值 diff 过:**只差 3 个数 + 2 条默认不启用的内层条目**,
其余 300 多个数值一模一样。所以直接把它们搬过来就行:
| 项 | Two Layers | Multiple Layers |
|---|---|---|
| `Track/copperThickness1oz` 最小线宽 | 0.127 | **0.102** |
| 过孔外径最小 | 0.5 | **0.3** |
| 过孔内径最小 | 0.3 | **0.2** |
| `Track/copperThickness0.5oz` | 无 | 有(min 0.089,不启用)|
| `Safe Spacing/copperThicknessInner0.5oz` | 无 | 有(不启用)|
⚠️ **下完之后别再去 GUI 里换“配置”下拉框** —— 换一下就是整套重置,
PWR4A / PWR12V 会被冲掉,12 个网络的指派又变成悬空的。
对话框里**显示的名字依旧是 `JLCPCB Capability(Two Layers Board)`**(名字改不了,
API 里没有 setCurrentRuleConfigurationName),**下发之后名字会自动变成 “自定义配置”** —— 这是对的,
说明它已经从系统能力集上摘下来了,以 `--check` 的实测值为准。
⚠️ 还有一条:**写进去的 mm 值会被 EasyEDA 换算成 mil 再换回来**,
0.2 回读是 `0.19999959999999997`。任何几何值都别做精确比较,容差留到 1e-4。
用法: /c/Python314/python .claude/pcb_rules.py [--push | --check]
"""
import io, json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
PCB = '7e9415a16a909c51'
# 多层板能力集只当**数据源**用(取内层 0.5 oz 那张间距表),不切换配置本身
BASE_CFG = 'JLCPCB Capability(Multiple Layers Board)'
# ── 线宽规则(mm)──────────────────────────────────────────────────────
# 外层 **1 oz**(§10.1,用户 2026-09-03 拍板)。
# ⚠️ PWR12V 的 2.0 mm 是给**分支**的:12V 总干那 8 A 走 2.0 mm 温升 35 °C,
# 不能当走线画,必须走灌铜(Inner2-PWR 0.5 oz 需 ≥18 mm 宽)。
# 最小值不敢设成 3.5 mm —— +12V 上还有一堆进 0402/SOT-23 焊盘的短支线,
# 卡 3.5 会刷出几百条假 DRC 错误,规则就没人看了。**载流靠灌铜保证,不靠 DRC。**
TRACK_RULES = {
# ⚠️ 最小值 1.0 → **0.3**(2026-09-03 扇出时改的,不是放松要求)。
# 规则是**按网络**生效的,`+12V` 上除了母线还挂着 R104 那种 0402 分压抽头:
# 0402 焊盘才 0.57 x 0.54 mm,1.0 mm 的线比焊盘还宽,根本引不出来 ——
# 实测卡 1.0 时有 29 个焊盘扇不出去,硬扇出去的又被判「线宽 11.8mil,应 >= 39.4mil」。
# 这正是 §10.1 已经写过的那条道理的延伸:**载流靠灌铜保证,不靠 DRC**。
# 默认值仍是 1.5 / 2.0,交互布线画出来的还是宽线;0.3 只是不再误伤短支线。
'PWR4A': {'strokeWidthDefault': 1.5, 'strokeWidthMin': 0.3, 'strokeWidthMax': 6.0},
'PWR12V': {'strokeWidthDefault': 2.0, 'strokeWidthMin': 0.3, 'strokeWidthMax': 10.0},
}
NET_TRACK = {}
for _n in range(1, 6):
NET_TRACK['VO%d' % _n] = 'PWR4A' # Buck 输出 -> eFuse
NET_TRACK['VO%d_P' % _n] = 'PWR4A' # 分流之后 -> 出线座
for _n in ('+12V', '+5V'):
NET_TRACK[_n] = 'PWR12V'
# ── 差分对几何(mm)────────────────────────────────────────────────────
# ⚠️ 长度容差 0.5 -> **5.0 mm**(2026-09-04 用户拍板)。
# USB 2.0 HS 的**对内**偏斜预算按 100 ps 算、FR4 上约 15 mm;5 mm ≈ 30 ps 仍有大余量。
# 卡 0.5 mm 时 USB1/USB2 被判违规(实测偏差 1.64 / 1.88 mm ≈ 10 ps)——
# 为了凑这个数去绕蛇形线,是拿真实的串扰与占板面积去换一个好看的 DRC 数字。
DIFF = {'strokeWidthDefault': 0.28, 'strokeWidthMin': 0.20, 'strokeWidthMax': 0.60,
'differentailPairSpacingDefault': 0.18, 'differentailPairSpacingMin': 0.15,
'differentailPairLenTolerMax': 5.0}
# ── 差分对**对内**间距规则(网络与网络之间)──────────────────────────
# 对内 0.28/0.18 是按 90 Ω 微带算的,紧耦合是设计意图;但 DP 与 DM 是两个不同的网络,
# 通用的 0.2 mm 净间距规则不认「这两个是一对」,在过孔换层处判了 4 条违规
# (实测 6~7 mil / 0.15~0.18 mm)。把线拉开阻抗就不是 90 Ω 了,所以**不能拉开**。
# 正确做法:建一条 0.15 mm 的安全间距条目,只挂在**每一对自己的两个网络之间**
# (`overwriteNetByNetRules`),对其它网络仍然是 0.2 mm。嘉立创四层工艺下限 0.1 mm。
PAIR_RULE = 'USBPAIR'
PAIR_CLEARANCE = 0.15
PAIRS = [('USB%d' % n, 'USB%d_DP' % n, 'USB%d_DM' % n) for n in range(1, 6)]
PAIRS += [('UPSTREAM', 'UP_DP', 'UP_DM'), ('DEBUG', 'USB_DP', 'USB_DM')]
# 安全间距的**下限**:嘉立创能力集给到 0.102 mm(4 mil),那是工艺极限不是设计目标。
# 全表抬到 0.2 mm,DRC 才拦得住无意中走太近的线。
MIN_CLEARANCE = 0.2
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;
// 嵌套形状里一条线宽规则是 form.data{'1':{minValue,defaultValue,maxValue}}
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) 两条功率线宽规则(名字必须和 NET_TRACK 里指的一模一样)──
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) // ⚠️ 对内那条 0.15 不能被抬回 0.2
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++; }
const ok = await D.overwriteCurrentRuleConfiguration(c); // ⚠️ 传本体,不传 {name,config}
return {ok: ok === true, bumped: bumped, name: cur.name,
track: Object.keys(T), spacing: Object.keys(SS)};
"""
def push_config():
u"""把规则配置整套下发到**当前文档**。见文件头的两个坑。"""
r = call(CAP_JS % (json.dumps(TRACK_RULES), json.dumps(DIFF), MIN_CLEARANCE,
json.dumps(PAIR_RULE), PAIR_CLEARANCE))
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”(下发前是系统能力集,改过就自动摘出来了)' % r['name'])
def caveat():
print(u'')
print(u'──── 一条必须记住的事 ────')
print(u'⚠️ 别再去 GUI 的 设计 → 设计规则… 里换“配置”下拉框。')
print(u' 换一下就是整套重置,PWR4A / PWR12V 两条线宽规则会被冲掉,')
print(u' 12 个功率网络的指派立刻变成悬空的(DRC 不报错,线宽也就不生效)。')
print(u' 要改规则就改这个脚本再 --push。')
print(u' 下发之后配置名会自动变成 “自定义配置” —— 这是对的,说明它不再挂在系统能力集上。')
def _nums(o):
u"""递归掏出一个子树里所有的数字。规则配置的结构**每个配置都不一样**:
两层板的差分对是 `form.strokeWidthTables.data['1'].defaultValue`,
多层板的是 `form.strokeWidthDefault`。别按固定路径取,会静默拿到 undefined。"""
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):
u"""按键名子串找第一个数字。"""
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
def audit():
JS = """
await new Promise(r=>setTimeout(r,2000));
const c = await eda.pcb_Drc.getCurrentRuleConfiguration();
const rules = await eda.pcb_Drc.getNetRules();
return {
cfgName: c && c.name,
track: Object.keys((c && c.config && c.config.Physics && 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 && c.config && c.config.Physics && c.config.Physics['Differential Pair'],
spacing: c && c.config && c.config.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(),
};
"""
r = call(JS)
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'──── 审计 ────')
# 配置的**名字**改不了(永远显示 Two Layers Board),所以查的是四层板能力集的数值。
# 两个能力集逐值 diff 只差这三个数 + 两条不启用的内层条目。
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, f in sorted(TRACK_RULES.items()):
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-9:写进去的 mm 会被 EasyEDA 换算成 mil 再换回来,
# 0.2 回读是 0.19999959999999997(差 4e-7 mm = 0.4 nm)。任何几何值都别做精确比较。
vals = [v for v in _nums(r['spacing']) if 0 < v < 10]
mn = min(vals) if vals else -1
chk(mn >= MIN_CLEARANCE - 1e-4, u'安全间距下限 %.4f mm' % mn,
u'安全间距下限只有 %.4f mm,应抬到 %.2f mm' % (mn, 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))
chk(len(r['assigned']) == len(NET_TRACK), u'线宽指派 %d 个网络' % len(r['assigned']),
u'线宽指派只有 %d 个,应为 %d' % (len(r['assigned']), len(NET_TRACK)))
if bad:
caveat()
return not bad
def main():
print(u'目标:线宽规则 %d 条(%s),安全间距下限 %.2f mm,四层板能力集'
% (len(TRACK_RULES), ' '.join(sorted(TRACK_RULES)), MIN_CLEARANCE))
print(u'差分对:W %.2f / S %.2f / 线长容差 %.2f 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)))
n = call("""
const T = %s;
const rules = await eda.pcb_Drc.getNetRules();
let k = 0;
for (const r of rules) {
if (r.type === 'net' && T[r.name]) { r['Track'] = T[r.name]; k++; }
}
await eda.pcb_Drc.overwriteNetRules(rules);
return k;
""" % json.dumps(NET_TRACK, ensure_ascii=False))
print(u' 线宽指派 %d / %d 个网络' % (n, len(NET_TRACK)))
# ── 差分对**对内**间距:只在每一对自己的两个网络之间挂 USBPAIR(0.15 mm)──
# 形状是从 pcb.js 的 getBetweenNetDataItem() 反出来的:
# {net1:{type:'net',name}, net2:{type:'net',name}, rule:<安全间距条目名>,
# copperRuleName:null, planeRuleName:null}
# ⚠️ 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()