初始导入: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
这个提交包含在:
zhou yang
2026-09-06 18:18:14 +08:00
共同撰写人 Claude Opus 5
当前提交 2bfe308b30
共修改 526 个文件,包含 80633 行新增和 0 行删除
+135
查看文件
@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
u"""从器件的 **3D Model Transform** 直接读出**本体框** —— 不用再截图量。
/c/Python314/python .claude/fp_body.py --scan KF2510 # 按关键词列候选
/c/Python314/python .claude/fp_body.py --scan XT30 --3p # 只看 3 脚 2.54 的
/c/Python314/python .claude/fp_body.py --check # 和 pcb1_place.FP_BODY 对账
## 为什么能这么读(2026-09-06 验证)
§26.6 写过「板边连接器的本体框,除了实测没有别的来源」——
那一条是**错的**,只是当时没找到路子。`lib_Device.search()` 回来的
`otherProperty['3D Model Transform']` 是一串逗号分隔的数:
v[0], v[1] = 3D 本体的 X / Y 尺寸(mil)
v[6], v[7] = 本体中心相对**封装原点**的偏移(mil)
⇒ 本体框 = (ox − sx/2, oy − sy/2, ox + sx/2, oy + sy/2)。
**六个已经用放大截图实测过的封装全部对上**(误差 ≤ 0.31 mm,就是截图标定的精度):
| 封装 | 3D 算出的 y 向框 | 截图实测 |
|---|---|---|
| `CONN-TH_KF2510-3A-1`(弯针,§26.6 栽过一次)| −0.31 .. +11.70 | −0.20 .. +11.70 |
| `CONN-TH_P5.08_KF128-5.08-2P` | −5.31 .. +5.38 | −5.20 .. +5.20 |
| `USB-C_SMD-TYPE-C-31-M-12_1` | −5.05 .. +2.85 | −5.20 .. +3.12 |
| `WIRELM-SMD_ESP32-S3-WROOM-1` | −9.10 .. +16.40 | −9.35 .. +16.75 |
| `IND-SMD_L7.1-W6.5` | −3.25 .. +3.25 | −3.35 .. +3.35 |
| `CAP-TH_BD8.0-P3.50-D0.6-FD` | −4.00 .. +4.00 | −4.10 .. +4.10 |
⚠️ 它给的是 **3D 模型**的框,不是丝印框,也不含公差。当成「本体大致占多少」够用
(这正是摆位要的),要卡到 0.1 mm 的净距还是得看焊盘。**没有 3D 模型的器件读不出来**,
那种还是只能截图。
## 顺带:立式 / 弯针一眼就分得出
同一个 3P 2.54 端子座:
* **立式(直针)**:本体 ≈ 7.6 × 6,偏移 ≈ 0 —— 本体骑在焊盘上
* **弯针(90°)**:本体 ≈ 7.6 × 12,偏移 ≈ ±5.7 —— 本体整个甩到一边
⚠️ **名字完全不可靠**:`KF2510-3A`(C2940511) 名字像直针,3D 说它是弯针(+5.69);
`KF2510-3AW`(C457341) 也是弯针,只是甩向 −Y。要立式的得看数,不能看后缀。
"""
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
MIL = 39.3700787401575
def parse(tr):
u"""'3D Model Transform' -> (本体 w, h, 框 x0,y0,x1,y1),读不出来返回 None。"""
if not tr:
return None
try:
v = [float(x) for x in str(tr).split(',')]
except ValueError:
return None
if len(v) < 8:
return None
sx, sy = v[0] / MIL, v[1] / MIL
ox, oy = v[6] / MIL, v[7] / MIL
if sx <= 0 or sy <= 0:
return None
return (sx, sy, ox - sx / 2, oy - sy / 2, ox + sx / 2, oy + sy / 2)
def search(kw, n=40):
js = ("const r = await eda.lib_Device.search(%s, undefined, undefined, undefined, %d, 1);"
"return (r||[]).map(d => [d.name||'', d.footprintName||'', d.supplierId||'',"
" (d.otherProperty&&d.otherProperty['3D Model Transform'])||'', d.uuid, d.libraryUuid]);"
% (json.dumps(kw), n))
r = run(js, timeout=180)
if not r.get('success'):
sys.exit(u'搜索失败: ' + json.dumps(r, ensure_ascii=False)[:400])
return r['result']
def show(rows, only=None):
print(u'%-30s %-32s %-11s %-15s %s'
% (u'器件', u'封装', u'立创', u'本体 w x h', u'框 y0..y1 判断'))
for name, fp, lcsc, tr, _u, _l in rows:
if only and only.lower() not in (name + fp).lower():
continue
b = parse(tr)
if b is None:
print(u'%-30s %-32s %-11s —— 没有 3D 模型,读不出来' % (name[:30], fp[:32], lcsc))
continue
w, h, x0, y0, x1, y1 = b
# 立式 = 本体基本骑在焊盘上;弯针 = 整个甩到一边
kind = u'立式' if abs((y0 + y1) / 2) < h * 0.25 else u'弯针/偏置'
print(u'%-30s %-32s %-11s %6.2f x %6.2f %+6.2f..%+6.2f %s'
% (name[:30], fp[:32], lcsc, w, h, y0, y1, kind))
def check():
u"""拿 `pcb1_place.FP_BODY` 的手工实测值和 3D 模型对账。"""
import pcb1_place as P
hit = {}
for fp in P.FP_BODY:
kw = fp.split('_')[-1] or fp
for row in search(kw, 40):
if row[1] == fp and row[3]:
hit[fp] = row
break
print(u'%-34s %-22s %-22s %s' % (u'封装', u'3D 框 y', u'FP_BODY 框 y', u''))
bad = 0
for fp, want in sorted(P.FP_BODY.items()):
row = hit.get(fp)
if not row:
print(u'%-34s —— 没搜到带 3D 的器件,跳过' % fp[:34])
continue
b = parse(row[3])
ok = abs(b[3] - want[1]) < 0.6 and abs(b[5] - want[3]) < 0.6
bad += 0 if ok else 1
print(u'%-34s %+7.2f..%+7.2f %+7.2f..%+7.2f %s'
% (fp[:34], b[3], b[5], want[1], want[3], u'✔' if ok else u'✘ 对不上'))
print(u'\n对不上的 %d 个' % bad)
return bad
if __name__ == '__main__':
argv = sys.argv[1:]
if '--check' in argv:
sys.exit(1 if check() else 0)
if '--scan' in argv:
kw = argv[argv.index('--scan') + 1]
only = argv[argv.index('--only') + 1] if '--only' in argv else None
show(search(kw, 40), only)
else:
sys.exit(__doc__)