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

228 行
10 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""P1-INPUT 的文字收拾(2026-09-06,跟在 `p1_r9f1.py` 换件之后)。
换完 R9A~R9E 截图才看出来两件事,一件是新的、一件是一直都在的:
1. **新的**:R9C/R9D/R9E 排在 y=300 的 x 860~1020 上,正好压住 Rs6/U13 那组说明文字
(x 850~1210 / y 261~300)。
2. **一直都在的**:`p1_add.py` 是「整页重画」的脚本,被推过好几次,
而它的清空那一步没把文字清干净 —— **66 个文字图元里只有 30 条是唯一的,
多出来的 36 个是同坐标同内容叠在一起的副本**(截图上表现为字发虚发粗)。
Rs6/U13 那一组还叠了**两代不同措辞**(x=850 的短版 + x=900 的长版)。
⚠️ 这类东西 `verify_sch.py` / `port_audit.py` / `xpage.py` 一个都不报 ——
文字没有电气含义,三道检查都不看它。**只有截图能发现。**
做三件事:
* **全页文字去重**:同 (x, y, 内容) 只留一个。
* **Rs6/U13 那一组**(两代共 7 条)整组删掉,在 R9 链**下方** (700, 206~245) 重建,
用措辞更全的那一代。x 700~1010 / y 206~245:离标题栏(x>455 且 y<192)还有 14,
离 GND 旗(1060,270)的标签在 x 上不重叠。
* **两条过期的说明**改掉:`F1 10A` → 12A;`R9 10R/5W` → R9A~R9E 5×2Ω。
用法: /c/Python314/python .claude/p1_text_fix.py [--push]
"""
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
PAGE = '9a6096762e036e23'
# 整组删掉的旧说明(两代措辞,按 (x, y) 认)
DROP_XY = [(850, 261), (850, 274), (850, 287), (850, 300),
(900, 269), (900, 282), (900, 295),
# ⬇ 更早的一整代说明文字。判据是 `p1-base.json`(整页几何底稿)只有 9 条、
# `p1_add.py` 的 ADD_TEXT 再加 5 条 —— 下面这些两边都不在,是残留。
# 它们各自压着一处:
(150, 760), (150, 780), (150, 795), (150, 810), # 旧【C】压在【A】上(页顶)
(150, 360), (150, 380), # 旧【B】压在【C】上
(140, 120), (140, 140), # 旧【A】,还写着 F1 10A
(520, 120), (520, 140), # 旧【A2】—— **印在标题栏里**
(900, 800), # 旧【D】压着 (850,800)
(150, 505)] # 旧注(内容已并进【B】标题)
# ⚠️ (60,537) 上叠着两条**不同内容**:新的「已复核 SLUSEF4A…」和旧的「…待复核」。
# 去重按 (x, y, 内容) 只能各留一条,所以旧的那条要按内容单独点名。
DROP_TEXT = [u'R2/R3 分压与 EN 接法待按 LMR51430 datasheet 的 Vref / EN 耐压复核']
# 收拾完页上应该剩下的坐标(9 条底稿 + 1 条【D】标题 + 4 条新说明块)。
# 逐条对账,多一条少一条都停 —— 「删掉名单外的一切」这种写法一旦名单错了就没救了。
CANON_XY = [(60, 800), (60, 789), (40, 730), (60, 548), (60, 537),
(60, 352), (60, 340), (60, 330), (60, 319), (850, 800)]
# 在 R9 链下方重建(x 700 起,宽的一条到 1010,不碰 GND 旗)
ADD_TEXT = [
(700, 245, 9, u'Rs6 5mΩ: 满板约 8A -> 40mV,INA226 满量程 81.92mV / 分辨率 0.5mA'),
(700, 232, 9, u'U13 地址 A1=GND、A0=SCL -> 0x43(地址表见 §8.2,不是顺排)'),
(700, 219, 9, u'体电容在 Rs6 上游;PCB 上 Rs6 放在体电容与五路分叉点之间,Kelvin 四线焊盘'),
(700, 206, 9, u'R9A~R9E: 5 × 2Ω 2512 串联 = 10Ω/10W,高 0.8mm(原立式水泥电阻站起来超高度预算)'),
]
# 就地改写的过期说明:(x, y) -> 新内容
REWRITE = {
(60, 789): u'12V/10A -> F1 12A -> Q1 AO4407 反接保护,栅极 RC 缓启动 tau = R1*C1 = 10ms',
(60, 319): u'泄放为突发额定:13.3V 下 1.33A / 17.7W,持续故障由固件关 VBUS 兜底',
}
# A4 可用区与标题栏(skill §8c / §12)
X_MIN, X_MAX, Y_MIN, Y_MAX = 20, 1145, 10, 812
TB_X, TB_Y = 455, 192
def width(s, sz):
u"""文字宽度 ≈ 字号 × (中日韩字数 + 0.5 × ASCII 字数)(skill §12 实测值)。"""
cjk = sum(1 for c in s if ord(c) > 0x2e80)
return sz * (cjk + 0.5 * (len(s) - cjk))
def selfcheck():
bad = []
for x, y, sz, s in ADD_TEXT:
w = width(s, sz)
if x + w > X_MAX:
bad.append(u'(%d,%d) 右边界 %.0f 超出可用区 %d:%s' % (x, y, x + w, X_MAX, s[:24]))
if y < Y_MIN or y > Y_MAX:
bad.append(u'(%d,%d) 越出可用区' % (x, y))
if x > TB_X and y < TB_Y:
bad.append(u'(%d,%d) 落进标题栏' % (x, y))
# R9 链的符号占 y 290~310,位号文字再往上 ⇒ 说明文字顶边不能高过 260
if y > 260:
bad.append(u'(%d,%d) 会撞上 R9 链' % (x, y))
# GND 旗 (1060,270) 的标签大致占 x 1040~1080 / y 240~258
if y > 235 and x + w > 1040:
bad.append(u'(%d,%d) 宽 %.0f 会压到 GND 旗的标签' % (x, y, w))
for (x, y), s in REWRITE.items():
if x + width(s, 9) > X_MAX:
bad.append(u'改写后的 (%d,%d) 超出右边界' % (x, y))
return bad
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,1500));" % PAGE)
def call(body, timeout=180):
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']
JS = u"""
const cur = (await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name;
if (cur !== 'P1-INPUT') return {err: '打错页了: ' + cur};
// 新说明块的坐标也进 DROP:那几条是「先删掉再按当前内容重建」,
// 否则这个脚本**不幂等** —— 第二次跑会在同一坐标上再叠一层
// (2026-09-06 实测:27 → 18 而不是 14,而对账因为坐标在名单内还报了 OK)。
const DROP = new Set(%(drop)s);
const DROPTXT = new Set(%(droptxt)s);
const CANON = new Set(%(canon)s);
const REW = %(rew)s; // [[x, y, 新内容], ...]
const rewKey = new Set(REW.map(r => r[0] + ',' + r[1]));
const all = [];
for (const t of await eda.sch_PrimitiveText.getAll())
all.push({id: t.getState_PrimitiveId(), x: Math.round(+t.x), y: Math.round(+t.y),
s: String(t.getState_Content ? t.getState_Content() : (t.content||'')),
sz: t.getState_FontSize ? t.getState_FontSize() : 9});
// ① 整组删掉的 + ② 去重(同 x/y/内容只留第一个)
const seen = new Set();
const kill = [], keep = [];
for (const t of all) {
const xy = t.x + ',' + t.y;
if (DROP.has(xy) || DROPTXT.has(t.s)) { kill.push(t.id); continue; }
const k = xy + '|' + t.s;
if (seen.has(k)) { kill.push(t.id); continue; }
seen.add(k); keep.push(t);
}
for (const id of kill) await eda.sch_PrimitiveText.delete(id);
// ③ 就地改写:删掉留下来的那一条,按新内容重建(文字没有「改内容」的 setState)
let rewrote = 0;
for (const t of keep) {
const k = t.x + ',' + t.y;
if (!rewKey.has(k)) continue;
const nw = REW.find(r => r[0] === t.x && r[1] === t.y);
await eda.sch_PrimitiveText.delete(t.id);
await eda.sch_PrimitiveText.create(t.x, t.y, nw[2], 0, null, null, t.sz);
rewrote++;
}
// ④ 新的说明块
let added = 0;
for (const a of %(add)s) { await eda.sch_PrimitiveText.create(a[0], a[1], a[3], 0, null, null, a[2]); added++; }
// ⑤ 逐条对账:剩下的必须正好是 CANON + 新说明块,多一条少一条都报出来
const left = [], stray = [];
for (const t of await eda.sch_PrimitiveText.getAll()) {
const xy = Math.round(+t.x) + ',' + Math.round(+t.y);
left.push(xy);
if (!CANON.has(xy)) stray.push(xy);
}
const missing = [...CANON].filter(k => !left.includes(k));
const dupLeft = left.filter((k, i) => left.indexOf(k) !== i);
return {before: all.length, killed: kill.length, rewrote, added,
after: left.length, want: CANON.size,
stray: [...new Set(stray)], missing, dup: [...new Set(dupLeft)]};
"""
def main():
bad = selfcheck()
print(u'P1-INPUT 文字收拾:去重 + Rs6/U13 说明块挪到 R9 链下方 + 两条过期说明改写')
for x, y, sz, s in ADD_TEXT:
print(u' 新块 (%4d,%3d) 宽 %4.0f -> x 到 %4.0f %s' % (x, y, width(s, sz), x + width(s, sz), s[:34]))
for (x, y), s in sorted(REWRITE.items()):
print(u' 改写 (%4d,%3d) %s' % (x, y, s[:44]))
if bad:
print(u'❌ 本地自检没过:')
for b in bad:
print(u' · ' + b)
sys.exit(1)
print(u' [OK] 本地自检:不出图框 / 不进标题栏 / 不撞 R9 链 / 不压 GND 旗标签')
if '--push' not in sys.argv:
print(u'\n加 --push 才真的改。')
return
canon = CANON_XY + [(x, y) for x, y, _sz, _s in ADD_TEXT]
d = call(JS % dict(
drop=json.dumps(['%d,%d' % xy for xy in DROP_XY]
+ ['%d,%d' % (x, y) for x, y, _sz, _s in ADD_TEXT]),
droptxt=json.dumps(DROP_TEXT, ensure_ascii=False),
canon=json.dumps(['%d,%d' % xy for xy in canon]),
rew=json.dumps([[x, y, s] for (x, y), s in REWRITE.items()], ensure_ascii=False),
add=json.dumps([[x, y, sz, s] for x, y, sz, s in ADD_TEXT], ensure_ascii=False)))
if d.get('err'):
sys.exit(u'❌ ' + d['err'])
print(u' 文字 %d -> %d(删 %d:旧的一整代 + 重复副本;改写 %d;新增 %d)'
% (d['before'], d['after'], d['killed'], d['rewrote'], d['added']))
ok = True
if d.get('stray'):
ok = False
print(u' [!!] 名单外还剩 %d 处坐标:%s' % (len(d['stray']), ' '.join(d['stray'])))
if d.get('missing'):
ok = False
print(u' [!!] 该留的没了 %d 处:%s' % (len(d['missing']), ' '.join(d['missing'])))
if d.get('dup'):
ok = False
print(u' [!!] 还有 %d 处坐标叠着两条以上:%s' % (len(d['dup']), ' '.join(d['dup'])))
if d['after'] != d['want']:
ok = False
print(u' [!!] 剩 %d 条,应该是 %d 条' % (d['after'], d['want']))
if ok:
print(u' [OK] 剩下的 %d 条正好等于「底稿 9 + 【D】标题 1 + 新说明块 4」,且无同坐标叠加'
% d['after'])
print(u'\n⚠️ 文字改动三道几何检查一个都不报 —— 必须截图人眼看。')
if not ok:
sys.exit(1)
if __name__ == '__main__':
main()