文件
phonerail/.claude/p1p6_conn.py
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

260 行
12 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""两处接口换件(2026-09-06 用户拍板):
1. **J1 12V 输入**:`KF128-5.08-2P` 螺钉端子 → **`XT30PW-M`**(C53373776,90° 板装公头)。
理由:用户要插拔式。⚠️ 但 **12V 输入是 8 A**(五路 4A×4.2V ÷ 0.92 ÷ 12V ≈ 7.6 A,
加风扇 0.7 A),而「3.5 mm DC」那一类(DC-002,外径 3.5 / 内径 1.35)**额定只有 0.5~1 A**,
差了近一个数量级 —— 接上去会烧插座。XT30 主脚额定 **20 A**(温升 <85 °C),余量充足。
选 2 脚版而不是 (2+2):(2+2) 多出来的两脚是 **1 A 信号脚**,我们用不上,
而且它的 pin 编号在库里读不出语义;2 脚版的符号**引脚名直接写着 `+` / `−`**,没有歧义。
2. **J26~J30 风扇座**:`KF2510-3A`(C2940511)→ **`2510-3A(直针)`**(C2803349)。
现在这颗名字像直针,**实测是弯针**:本体 7.62 × 12.01,整个甩到 +Y 一侧,进深 11.7 mm
(§26.6 当年是拿放大截图量出来的)。换成真直针之后本体 7.62 × **5.81**、基本骑在焊盘上,
**每个座省 6.2 mm 进深**,五个座(左板边 2 / 右板边 2 / 顶板边 1)一起把板边让出来。
## 为什么这一轮**一根导线都不用动**
| | 旧件引脚(rot 0)| 新件引脚(rot 0)| 做法 |
|---|---|---|---|
| J1 | p1(−20,+5) p2(−20,−5) | p1"−"(−20,+5) p2"+"(−20,−5) | 原位 (120,705) rot 180,**p1/p2 位置完全重合** |
| J26~J30 | p1(−15,+10) p2(−15,0) p3(−15,−10) | p1(−20,+10) p2(−20,0) p3(−20,−10) | 元件**东移 5** 到 x=1085 ⇒ 引脚绝对坐标不变 |
极性也天然对上:板上实测 **J1.1 = GND、J1.2 = 12V 进线**,而新符号正好 p1 = `−`、p2 = `+`。
⚠️ **J1 新件多出 p3/p4 两个固定脚**(rot 0 时在 (+5,+25) / (+5,−25),rot 180 后落在
(115,680) / (115,730))。它们是机械固定孔,**不接网络** —— 下发后要把它们加进 NC 清单,
否则 `verify_sch` 会报未连接。
## 按 skill 的四条硬规矩
* **先 create 后 delete**(§9b):create 参数写错时删除已经生效,页面会凭空少件。
* **坐标 5 的倍数**(§8a-2b);1085 / 705 都是。
* **引脚跨距读回来断言**(§8d),对不上就停手 —— 这一版的判据更硬:
新件的每一个引脚都必须**落在旧件某个引脚的同一坐标上**(J1 的 p3/p4 除外)。
* 换件不动版面:导线是独立图元,旧件删掉之后新件引脚落在同一坐标就自动接上。
用法: /c/Python314/python .claude/p1p6_conn.py [--push]
下发完必跑:verify_sch.py --nets(两页)/ port_audit.py / xpage.py
再 pcb1_import.py --bom → --verify
"""
import io
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run # noqa: E402
LIB = '0819f05c4eef4c71ace90d822a990e87'
P1 = '9a6096762e036e23' # P1-INPUT
P6 = 'fd00f13e59336392' # P6-FAN-EDL
# ── 新器件(按立创编号在库里当场查 uuid,不硬编码)──────────────────────
NEW_J1 = ('XT30PW-M', 'C53373776') # fp CONN-TH_XT30PW-M 本体 13.30 x 13.95
NEW_FAN = ('2510-3A(直针)', 'C2803349') # fp CONN-TH_2510-3A 本体 7.62 x 5.81
# ── 旧件(primitiveId 是 2026-09-06 从页上读回来的,下发前逐个核存在)──
OLD = {
'J1': ('163ed00ced8619b9', P1, (120, 705), 180),
'J26': ('adc7843724c9f781', P6, (1080, 700), 0),
'J27': ('4a1abe4be255fb93', P6, (1080, 620), 0),
'J28': ('1d15c744cd52913d', P6, (1080, 540), 0),
'J29': ('1202c39a84e9c4fb', P6, (1080, 460), 0),
'J30': ('68ab7d950e494c11', P6, (1080, 380), 0),
}
# 新件放哪(东移 5 抵消引脚偏移;J1 原位不动)
NEW_XY = {
'J1': (120, 705, 180),
'J26': (1085, 700, 0), 'J27': (1085, 620, 0), 'J28': (1085, 540, 0),
'J29': (1085, 460, 0), 'J30': (1085, 380, 0),
}
# 旧件引脚的绝对坐标(p6-pins.json + 本轮从 P1 页读回来的)
OLD_PINS = {
'J1': {'1': (140, 700), '2': (140, 710)},
'J26': {'1': (1065, 710), '2': (1065, 700), '3': (1065, 690)},
'J27': {'1': (1065, 630), '2': (1065, 620), '3': (1065, 610)},
'J28': {'1': (1065, 550), '2': (1065, 540), '3': (1065, 530)},
'J29': {'1': (1065, 470), '2': (1065, 460), '3': (1065, 450)},
'J30': {'1': (1065, 390), '2': (1065, 380), '3': (1065, 370)},
}
# J1 新件多出来的固定脚,不接网络
J1_NC = ('3', '4')
def selfcheck():
bad = []
for des, (x, y, rot) in NEW_XY.items():
if x % 5 or y % 5:
bad.append(u'%s 的坐标 (%d,%d) 不是 5 的倍数' % (des, x, y))
if rot not in (0, 90, 180, 270):
bad.append(u'%s 的旋转角 %s 不合法' % (des, rot))
if set(NEW_XY) != set(OLD) or set(OLD_PINS) != set(OLD):
bad.append(u'三张表的位号对不齐')
return bad
def dev(name, lcsc):
r = run("const r = await eda.lib_Device.search(%s, undefined, undefined, undefined, 40, 1);"
"return (r||[]).map(d => [d.name, d.supplierId||'', d.uuid, d.libraryUuid,"
" d.footprintName||'']);" % json.dumps(name), timeout=180)
if not r.get('success'):
sys.exit(u'查器件失败: ' + json.dumps(r, ensure_ascii=False)[:300])
for n, s, u, l, fp in r['result']:
if s == lcsc:
print(u' 器件 %-16s %-11s uuid=%s fp=%s' % (n, s, u, fp))
return u, l, fp
sys.exit(u'库里找不到 %s (%s)' % (name, lcsc))
JS_CREATE = u"""
await eda.dmt_EditorControl.openDocument('%(page)s');
await new Promise(r => setTimeout(r, 1800));
const cur = (await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name;
if (cur !== '%(pname)s') return {err: '打错页了: ' + cur};
// 要删的旧件必须都还在 —— 不在就说明这个脚本跑过一次了
const have = new Set();
for (const c of await eda.sch_PrimitiveComponent.getAll()) have.add(c.getState_PrimitiveId());
const missing = %(need)s.filter(id => !have.has(id));
if (missing.length) return {err: '要删的旧件不在页上(已经跑过一次?): ' + missing.join(' ')};
const made = [];
for (const [des, uuid, lib, x, y, rot] of %(plan)s) {
const c = await eda.sch_PrimitiveComponent.create({libraryUuid: lib, uuid: uuid},
x, y, undefined, rot, false);
const pins = [];
for (const p of await c.getAllPins())
pins.push([String(p.pinNumber||''), String(p.pinName||''), +p.x, +p.y]);
made.push({des: des, id: c.getState_PrimitiveId(), pins: pins});
}
return {made: made};
"""
JS_FINISH = u"""
await eda.dmt_EditorControl.openDocument('%(page)s');
await new Promise(r => setTimeout(r, 1800));
const cur = (await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name;
if (cur !== '%(pname)s') return {err: '打错页了: ' + cur};
const out = {deleted: [], named: []};
for (const id of %(dele)s) { await eda.sch_PrimitiveComponent.delete(id); out.deleted.push(id); }
const byId = {};
for (const c of await eda.sch_PrimitiveComponent.getAll()) byId[c.getState_PrimitiveId()] = c;
for (const [id, des] of %(names)s) {
const c = byId[id];
if (!c) { out.err = '新建的元件不见了: ' + des; return out; }
await c.toAsync().setState_Designator(des).done();
out.named.push(des);
}
return out;
"""
JS_DROP = u"""
await eda.dmt_EditorControl.openDocument('%(page)s');
await new Promise(r => setTimeout(r, 1500));
const cur = (await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name;
if (cur !== '%(pname)s') return {err: '打错页了: ' + cur};
let n = 0;
for (const id of %(ids)s) { await eda.sch_PrimitiveComponent.delete(id); n++; }
return {n: n};
"""
def do_page(page, pname, deslist, devmap, push):
plan, need, dele, expect = [], [], [], {}
for des in deslist:
x, y, rot = NEW_XY[des]
u, l, _fp = devmap[des]
plan.append([des, u, l, x, y, rot])
need.append(OLD[des][0])
dele.append(OLD[des][0])
expect[des] = OLD_PINS[des]
r = run(JS_CREATE % dict(page=page, pname=pname, need=json.dumps(need),
plan=json.dumps(plan)), timeout=400)
if not r.get('success'):
sys.exit(u'建新件失败: ' + json.dumps(r, ensure_ascii=False)[:500])
d = r['result']
if d.get('err'):
sys.exit(u'⛔ ' + d['err'])
# ── 硬断言:新件每个引脚都要落在旧件同号引脚的坐标上 ──
bad, names = [], []
for m in d['made']:
des = m['des']
got = {p[0]: (p[2], p[3]) for p in m['pins']}
for num, xy in expect[des].items():
if num not in got:
bad.append(u'%s 少了 p%s' % (des, num))
elif got[num] != tuple(xy):
bad.append(u'%s p%s 在 %s,旧件在 %s' % (des, num, got[num], tuple(xy)))
extra = sorted(set(got) - set(expect[des]))
if des == 'J1':
if extra != list(J1_NC):
bad.append(u'J1 多出来的脚是 %s,预期 %s' % (extra, list(J1_NC)))
elif extra:
bad.append(u'%s 多出来的脚 %s(旧件没有)' % (des, extra))
names.append([m['id'], des])
for m in d['made']:
print(u' 新件 %-4s id=%s %s' % (m['des'], m['id'],
' '.join(u'p%s"%s"@%d,%d' % tuple(p) for p in m['pins'])))
if bad:
print(u'\n⛔ 引脚对不上,**新件已经建出来了但没删旧件** —— 请手工撤销:')
for b in bad:
print(u' ' + b)
sys.exit(1)
print(u' ✔ %d 个新件的引脚逐个落在旧件同号引脚的坐标上' % len(d['made']))
if not push:
# ⚠️ **试跑必须自己收尾。** 「先 create 后 delete」(skill §9b)意味着
# 断言那一步已经把新件建到页上了 —— 不删掉的话,一次「只看看」的试跑
# 就在页面上留下 6 个和旧件完全重叠的元件,而且位号还是自动分配的,
# 下一次跑又会再建一批。2026-09-06 第一次跑就是这么留下 6 个的。
r3 = run(JS_DROP % dict(page=page, pname=pname,
ids=json.dumps([m['id'] for m in d['made']])), timeout=300)
if not r3.get('success'):
sys.exit(u'⛔ 试跑之后没能删掉试建的新件,页面现在是重叠状态,手工清理: '
+ json.dumps(r3, ensure_ascii=False)[:400])
print(u' (试跑:断言过了,刚建的 %d 个新件已经删回去,页面原样未动)'
% r3['result'].get('n', 0))
return d
r2 = run(JS_FINISH % dict(page=page, pname=pname, dele=json.dumps(dele),
names=json.dumps(names)), timeout=400)
if not r2.get('success'):
sys.exit(u'删旧件/改位号失败: ' + json.dumps(r2, ensure_ascii=False)[:500])
x = r2['result']
if x.get('err'):
sys.exit(u'⛔ ' + x['err'])
print(u' 删掉旧件 %d 个,位号设好 %s' % (len(x['deleted']), ' '.join(x['named'])))
return d
def main():
push = '--push' in sys.argv[1:]
bad = selfcheck()
if bad:
for b in bad:
print(u'⛔ ' + b)
sys.exit(1)
print(u'两处接口换件:J1 → XT30PW-M(20 A) / J26~J30 → 2510-3A 直针座')
u1 = dev(*NEW_J1)
u6 = dev(*NEW_FAN)
devmap = {'J1': u1}
for d in ('J26', 'J27', 'J28', 'J29', 'J30'):
devmap[d] = u6
print(u'\n[P1-INPUT] J1')
do_page(P1, 'P1-INPUT', ['J1'], devmap, push)
print(u'\n[P6-FAN-EDL] J26~J30')
do_page(P6, 'P6-FAN-EDL', ['J26', 'J27', 'J28', 'J29', 'J30'], devmap, push)
if push:
print(u'\n下一步(缺一不可):')
print(u' 1. J1 的 p3/p4 是机械固定脚,加进 P1-INPUT 的 NC 清单')
print(u' 2. verify_sch.py --nets(两页)/ port_audit.py / xpage.py')
print(u' 3. pcb1_import.py --bom → --verify(封装换了,pcb1-parts.json 必须重抓)')
print(u' 4. pcb1_place.FP_BODY / H_FP 加两条新封装,再重推摆位')
return 0
if __name__ == '__main__':
sys.exit(main())