# -*- coding: utf-8 -*- u"""R2 刀片 **B1-POWER** 页:12V 入口保护 → Buck → eFuse → 检流 → `VO_P`。 /c/Python314/python .claude/r2/b1_power.py # 只算 + 本地自检 /c/Python314/python .claude/r2/b1_power.py --push # 清页 + 重画(幂等) ## 版图从哪来 **不是重新排的** —— 直接搬 R1 已经验证过六项的 `P2-CHANNEL` (`.claude/r2/dump_page.py` 读回的 `_p2_live.json`:40 元件 / 213 段导线 / 29 旗 / 12 端口),只做三件事: 1. **位号映射**(`DES`):`C1xx→Cx` / `R1xx→Rx` / `Rs101→Rs1` / `L101→L1` / `Q101→Q1` / `Q102→Q2`;芯片按 R2 刀片的编号 `U1` Buck / **`U8→U3`** INA226 / **`U14→U2`** eFuse。刀片只有一路,不再需要「通道号 = 位号百位」那套规则。 2. **网络名去掉通道后缀**(`NET`):`VO1→VO`、`VDAC1→VDAC`、`EFUSE_EN1→EFUSE_EN` … `GND` / `+3V3` / `BUCK_PG` / `INA_ALERT` / `SDA` / `SCL` 原样。 3. **新增 12V 入口保护块**(方案文档 D10):`+12V`(金手指) → `F1` 2A 慢断 → `+12V_F`(刀片内部 12V 轨)→ Buck;`D1` TVS 从 `+12V_F` 钳到地。 原来那两个 `+12V` 电源旗因此改叫 `+12V_F`。 ## ⚠️ 跨工程重放必须断言引脚坐标 skill §6b/§6c/§8d:`create()` 的 rotation 语义**同一个料号在不同页都能不一样**, 方向表一张都别信。所以放完之后逐个元件把 `getAllPins()` 读回来, 和 `_p2_live.json` 里 R1 的引脚坐标**逐脚比对**,差一个单位就终止 —— 坐标一致才能保证照搬过来的 213 段导线还落在引脚上。 ## ✅ T7 已结(2026-09-08):F1 = `F1206SB2000V063TM`(`C310998`) 原来这里写「库里搜不到 2A 的 1206 保险丝,`lib_Device.search` 只回 38 条」—— **那个结论是错的**:`search` 的完整签名是 `search(key, libraryUuid, classification, symbolType, itemsOfPage, page)`, **带上 `itemsOfPage` / `page` 就能翻页**(单次请求仍受桥接 30 s 限制,一个关键词最多翻 3 页)。 翻页之后 AEM 的 `F1206` 全系都在库里,型号规则是 `F1206` + 系列 + 电流 + `V` + 耐压 + `TM`: | 系列 | 含义 | 2A 那一颗 | |---|---|---| | **`SB`** | **Slow Blow(慢断)** | **`F1206SB2000V063TM` = `C310998`,63V / 分断 50A / I²t 0.63** ✅ 本页用它 | | `HI` | High Inrush(耐涌,也是延时型)| `F1206HI2000V063TM` = `C246768`,I²t 0.8 | | `FA` | Fast Acting(快断)| —— D10 要慢断,不能用 | | `HC` | High Current | 占位用过的 10A(`C310989`)、底板 F1 的 12A(`C310990`)就是这一系 | ⚠ **封装 uuid 和占位件不一样**(`bceff889…` vs `66116fae…`,10A/150A 分断那颗焊盘更大)。 刀片 PCB 还没布(M4),所以没有影响;原理图这一侧的导线本来就是**按读回的引脚坐标**算的,自动跟随。 ⚠ **裕量只有一档**:刀片满载约 1.45 A(16 W 手机 ÷ 92% ÷ 12 V)= 额定的 73%, 正压在保险丝「连续 ≤ 75%」的降额线上,机箱内 40~50 ℃ 还要再降。 要更大裕量就换 `F1206SB2500V032TM`(`C310999`,2.5A / 32V / I²t 0.9)—— 改 `FUSE` 一个常量即可。 """ import io import json import os import sys import time HERE = os.path.dirname(os.path.abspath(__file__)) # ⚠ `.claude` 下有和 r2 同名的 R1 脚本(pcb_pour / pcb_rules / pcb_place)。 # 只插父目录的话,**后面任何一个 `import pcb_rules` 都会拿到 R1 那一份**, # 而且不报错,只在调到 R2 专有的函数时才炸(2026-09-08 踩过)。 # 规矩:两行都要写,HERE 后插(在 sys.path 最前)。 sys.path.insert(0, os.path.dirname(HERE)) sys.path.insert(0, HERE) from eda import run # noqa: E402 PAGE = '5338949298dcff35' # BLADE / B1-POWER PAGE_NAME = 'B1-POWER' PROJ = '05a65c22f14846b8b95360ef8d3c1e2e' SRC = os.path.join(HERE, '_p2_live.json') PARTS = json.load(io.open(os.path.join(os.path.dirname(HERE), 'eda-parts.json'), encoding='utf-8'))['parts'] # ── 1. 位号映射 ─────────────────────────────────────────────────────── CHIP = {'U1': 'U1', 'U8': 'U3', 'U14': 'U2'} def des(d): if d in CHIP: return CHIP[d] pre = 'Rs' if d.startswith('Rs') else d[0] num = d[len(pre):] assert num.isdigit() and len(num) == 3 and num[0] == '1', d return '%s%d' % (pre, int(num[1:])) # ── 2. 网络名映射 ───────────────────────────────────────────────────── NET = { 'VO1': 'VO', 'VO1_P': 'VO_P', 'VDAC1': 'VDAC', 'EFUSE_EN1': 'EFUSE_EN', 'EFUSE_FLT1': 'EFUSE_FLT', 'EFUSE_PG1': 'EFUSE_PG', '+12V': '+12V_F', # Buck 侧吃的是保险丝之后那一段 } # ── 3. 新增:12V 入口保护块 ─────────────────────────────────────────── # 位置选在 y 200~340 / x 480~960 那片空地(R1 页上唯一一块连片的空区, # 标题栏占 x>455 且 y<192,所以主线放 y=250、TVS 朝上挂)。 F1_XY = (580, 250) D1_XY = (680, 290) IN_PORT = (520, 250) # '+12V' 端口引脚 NODE_X = 680 # +12V_F 节点的竖线 x FLAG_X = 760 # +12V_F 电源旗 GND_Y = 335 # TVS 上方的地旗(rot 180:符号在上、引脚在下) FUSE = 'C310998' # F1206SB2000V063TM:2A 慢断(AEM SB 系列),1206/63V,T7 已结 TVS = 'C163651' # SMBJ13A def lcsc_of(name): u"""器件名 -> 立创编号。R1 页上的名字带 `_C8545` 这种后缀,去掉再比。""" base = name.split('_C')[0] if '_C' in name else name for k, v in PARTS.items(): if v['name'] == name or v['name'] == base: return k raise SystemExit(u'eda-parts.json 里找不到器件 %r' % name) def plan(): src = json.load(io.open(SRC, encoding='utf-8')) pts = src['wires'] segs = [(tuple(pts[i]), tuple(pts[i + 1])) for i in range(0, len(pts), 2)] place, want_pins = [], {} for p in src['parts']: d0, name, _sub, x, y, rot, mir = p[0], p[1], p[2], p[3], p[4], p[5], p[6] k = lcsc_of(name) d = des(d0) place.append([d, PARTS[k]['uuid'], x, y, rot, PARTS[k]['lib'], bool(mir)]) want_pins[d] = {str(q[0]): (q[2], q[3]) for q in p[9]} # 新增两件 place.append(['F1', PARTS[FUSE]['uuid'], F1_XY[0], F1_XY[1], 0, PARTS[FUSE]['lib'], False]) # ⚠ create(90) 实测让 pin1(K) 落在上方 —— TVS 的 K 要朝下接 +12V_F,所以用 270。 # (entry_block() 里有硬断言,方向反了直接终止,见 skill §6c) place.append(['D1', PARTS[TVS]['uuid'], D1_XY[0], D1_XY[1], 270, PARTS[TVS]['lib'], False]) W = [[int(round(a[0])), int(round(a[1])), int(round(b[0])), int(round(b[1]))] for a, b in segs] # 旗的符号名读回来是 'Ground-GND' / 'Power-5V',而 createNetFlag 的第一个参数 # 只认 'Ground' / 'Power' —— 取前缀。('Power-5V' 里的 5V 只是符号默认网络名, # 见 skill §3b:移动电源旗会掉回这个默认值。) # ⚠️ rot=180 的旗/端口读回来带浮点尾巴(789.9999999999999),必须先取整 —— # 否则「坐标必须是 5 的倍数」和「必须落在导线端点」两道自检全部误报。 F = [[f[0].split('-')[0], NET.get(f[1], f[1]), int(round(f[2])), int(round(f[3])), f[4]] for f in src['flags']] PT = [[p[0].replace('Netport-', ''), NET.get(p[1], p[1]), int(round(p[2])), int(round(p[3])), p[4]] for p in src['ports']] return place, want_pins, W, F, PT def entry_block(pins, pnames): u"""按读回的真实引脚坐标补出入口保护块的导线 / 旗 / 端口。""" f = pins['F1'] assert len(f) == 2, u'F1 应该是 2 脚,实测 %s' % (f,) (fa, fb) = sorted(f.values()) # 左脚、右脚 assert fa[1] == fb[1] == F1_XY[1], u'F1 两脚必须同 y(横放),实测 %s' % (f,) d = pins['D1'] assert sorted(d) == ['1', '2'], u'D1 应该是 2 脚,实测 %s' % (d,) assert d['1'][0] == d['2'][0] == D1_XY[0], u'D1 两脚必须同 x(竖放),实测 %s' % (d,) # ⚠️ 极性件按**脚名**认,不按脚号(skill §6c:方向表一张都别信)。 # SMBJ13A 实测脚名是 'C'(阴极) / 'A'(阳极),阴极要朝下接 +12V_F。 nm = pnames['D1'] kk = [n for n in d if nm.get(n, '').startswith(('C', 'K'))] aa = [n for n in d if nm.get(n, '').startswith('A')] assert len(kk) == 1 and len(aa) == 1, u'D1 脚名认不出阴阳极:%s' % (nm,) dk, da = d[kk[0]], d[aa[0]] assert dk[1] < da[1], \ u'D1 是 TVS,阴极必须在下(接 +12V_F),实测 K=%s A=%s ⇒ 把 rot 翻 180 重放' % (dk, da) W = [ [IN_PORT[0], IN_PORT[1], fa[0], fa[1]], # 端口 → F1 左脚 [fb[0], fb[1], NODE_X, F1_XY[1]], # F1 右脚 → 节点 [NODE_X, F1_XY[1], FLAG_X, F1_XY[1]], # 节点 → +12V_F 旗 [NODE_X, F1_XY[1], dk[0], dk[1]], # 节点 → D1 阴极 [da[0], da[1], NODE_X, GND_Y], # D1 阳极 → 地旗 ] F = [['Power', '+12V_F', FLAG_X, F1_XY[1], 0], ['Ground', 'GND', NODE_X, GND_Y, 180]] PT = [['IN', '+12V', IN_PORT[0], IN_PORT[1], 0]] return W, F, PT TEXTS = [ (185, 785, u'B1-POWER 刀片功率级:12V 入口保护 → Buck → eFuse → 检流 → VO_P(5 片刀片完全相同)', 16), (480, 212, u'【12V 入口保护 · 方案 R2 §D10】金手指 +12V → F1 2A 慢断 → +12V_F 刀片内部轨;D1 TVS 钳位', 11), (60, 170, u'TPS54622: Vref 0.6V, fsw 480kHz(R3=100k), Vout = 4.222 - 0.2813*Vd', 11), (60, 150, u'TPS25940A: OVP 4.36V(R9/R10), ILIM 4.45A(R11), dVdT 33nF -> 0.36V/ms', 11), (60, 130, u'缓启动 11.5ms / 涌流 0.8A ; INA226 五片刀片同址 A1=A0=GND -> 0x40(靠底板 TCA9548A 分通道)', 11), (60, 110, u'放电互锁: EFUSE_EN 低 -> Q1 截止 -> Q2 导通 -> R19 10R 泄放手机侧 2200uF', 11), (60, 90, u'✅ T7 已结:F1 = F1206SB2000V063TM (C310998) 2A 慢断 1206/63V;满载 1.45A = 额定 73%,裕量只有一档', 11), (60, 70, u'⚠ EFUSE_EN 走金手指专用针直连底板 ESP32 GPIO,不经 PCA9555(安全链第三层)', 11), ] def selfcheck(W, F, PT, pins): bad = [] for a, b, c, e in W: if (a == c) == (b == e): bad.append(u'导线不是横平竖直或零长度: %s' % ((a, b, c, e),)) for v in (a, b, c, e): if v % 5: bad.append(u'导线坐标不是 5 的倍数: %s' % ((a, b, c, e),)) for it in F + PT: if it[2] % 5 or it[3] % 5: bad.append(u'旗/端口坐标不是 5 的倍数: %s' % (it,)) # 旗 / 端口的引脚必须落在某条导线段的**端点**(skill §8a-2) ends = set() for a, b, c, e in W: ends.add((a, b)); ends.add((c, e)) for it in F + PT: if (it[2], it[3]) not in ends: bad.append(u'旗/端口不在导线端点上: %s' % (it,)) # 每个元件引脚必须落在某条导线段上 def on(p, s): (px, py), (ax, ay, bx, by) = p, s if ax == bx: return px == ax and min(ay, by) <= py <= max(ay, by) return py == ay and min(ax, bx) <= px <= max(ax, bx) for d, ps in sorted(pins.items()): for n, xy in sorted(ps.items()): if not any(on(xy, s) for s in W): bad.append(u'悬空引脚 %s.%s @%s' % (d, n, (xy,))) return bad def call(js, timeout=180): r = run(js, timeout=timeout) if not r.get('success'): raise SystemExit(json.dumps(r, ensure_ascii=False)[:600]) return r['result'] READ_PINS_JS = """ const out = {}; for (const c of await eda.sch_PrimitiveComponent.getAll()) { if (c.getState_ComponentType() !== 'part') continue; out[c.getState_Designator()] = (await c.getAllPins()).map( p => [p.getState_PinNumber(), (p.getState_PinName ? p.getState_PinName() : ''), Math.round(p.getState_X()), Math.round(p.getState_Y())]); } return out; """ FLIP_JS = """ const P = %s; const by = {}; for (const c of await eda.sch_PrimitiveComponent.getAll()) if (c.getState_ComponentType() === 'part') by[c.getState_Designator()] = c; for (const [d, x, y, r] of P) { const c = by[d]; if (!c) continue; await c.toAsync().setState_Rotation(r).done(); await c.toAsync().setState_X(x).setState_Y(y).done(); } return P.length; """ OPEN = ("await eda.dmt_EditorControl.openDocument('%s');" "await new Promise(r=>setTimeout(r,1800));" "const cur=await eda.dmt_Schematic.getCurrentSchematicPageInfo();" "if(!cur||cur.name!=='%s') throw new Error('打错页了: '+(cur&&cur.name));" % (PAGE, PAGE_NAME)) def main(): push = '--push' in sys.argv place, want, W0, F0, PT0 = plan() if not push: print(u'元件 %d(含新增 F1/D1) 导线 %d 旗 %d 端口 %d 文字 %d' % (len(place), len(W0) + 5, len(F0) + 2, len(PT0) + 1, len(TEXTS))) nets = sorted({f[1] for f in F0} | {p[1] for p in PT0} | {'+12V', '+12V_F'}) print(u'网络名: ' + ' '.join(nets)) print(u'位号: ' + ' '.join(p[0] for p in place)) print(u'(只算不下发。要下发加 --push)') return call("await eda.dmt_Project.openProject('%s');" "await new Promise(r=>setTimeout(r,2500)); return 1;" % PROJ) # 清页(幂等) call(OPEN + """ for (const c of await eda.sch_PrimitiveComponent.getAll()) await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId()); for (const w of await eda.sch_PrimitiveWire.getAll()) await eda.sch_PrimitiveWire.delete(w.getState_PrimitiveId()); for (const t of await eda.sch_PrimitiveText.getAll()) await eda.sch_PrimitiveText.delete(t.getState_PrimitiveId()); return 1; """) for it in place: assert len(it) == 7 and it[1] and it[5], u'放置表字段不全: %s' % (it,) call(OPEN + """ globalThis.__pl = {done: 0, err: null, n: %d}; (async () => { for (const [d, uuid, x, y, rot, lib, mir] of %s) { try { await eda.sch_PrimitiveComponent.create({libraryUuid: lib, uuid}, x, y, undefined, rot, mir, true, true); } catch (e) { globalThis.__pl.err = d + ': ' + String(e).slice(0, 150); } globalThis.__pl.done++; } })(); return 'fired'; """ % (len(place), json.dumps(place))) for _ in range(120): st = call("return globalThis.__pl;") if st['err']: raise SystemExit(u'放置失败: %s' % st['err']) if st['done'] >= st['n']: break time.sleep(2) else: raise SystemExit(u'放置超时 %d/%d' % (st['done'], st['n'])) # ⚠️ `create()` 会把元件坐标吸附(实测吸到 5 的倍数)—— # R1 页上 679/639/654/589 这些位置放下去会差 1~2 个单位,而整页 213 段导线 # 是按 R1 的坐标算的,差 1 就全断。skill §3 只说了旗/端口会吸附,元件也会。 # ⇒ 认领位号按**就近**匹配(容差 3),认完再 setState_X/Y 拉回精确坐标。 # (移动元件是安全的;不安全的是移动旗/端口 §3b、和 setState_Rotation §6b。) named = call(""" const P = %s; let k = 0, moved = 0; const cs = (await eda.sch_PrimitiveComponent.getAll()).filter(c => c.getState_ComponentType() === 'part'); const used = new Set(); for (const [d, x, y] of P) { let best = null, bd = 4; for (const c of cs) { const id = c.getState_PrimitiveId(); if (used.has(id)) continue; const dd = Math.abs(Math.round(c.getState_X()) - x) + Math.abs(Math.round(c.getState_Y()) - y); if (dd < bd) { bd = dd; best = c; } } if (!best) continue; used.add(best.getState_PrimitiveId()); await best.toAsync().setState_Designator(d).done(); if (bd > 0) { await best.toAsync().setState_X(x).setState_Y(y).done(); moved++; } k++; } return k; """ % json.dumps([[p[0], p[2], p[3]] for p in place])) if named != len(place): raise SystemExit(u'只认领到 %d/%d 个位号' % (named, len(place))) raw = call(READ_PINS_JS) pins = {k: {str(n): (x, y) for n, _nm, x, y in v} for k, v in raw.items()} pnames = {k: {str(n): (_nm or '').upper() for n, _nm, x, y in v} for k, v in raw.items()} # ⚠️ 与 R1 逐脚比对(§6b/§6c:rotation 语义不可推,只能读回来断言) # 实测这一页 42 件里有 27 件的 rot=270 与 R1 语义相反: # · 25 件是两脚对称件(0402 R/C),只有引脚**编号**互换,坐标集相同; # · **C10 / C14 两颗 33nF 是 +20/−15 的不对称跨距**,翻过来坐标真的差 5, # 照 R1 的坐标画线会**断在那两颗上** —— 正是 skill §8d 那条「跨距同族也不统一」。 # ⇒ 对不上就 setState_Rotation(rot+180) 再回读,直到逐脚(含编号)与 R1 一致。 tgt = {q[0]: [q[2], q[3], q[4]] for q in place} for _round in range(4): miss = [d for d, wp in want.items() if pins.get(d) != {n: tuple(xy) for n, xy in wp.items()}] if not miss: break flip = [] for d in sorted(miss): tgt[d][2] = (tgt[d][2] + 180) % 360 flip.append([d, tgt[d][0], tgt[d][1], tgt[d][2]]) # ⚠️ 第一轮往往「看起来没生效」:create(270) 与 setState_Rotation(270) # 差 180°(skill §6b),所以要翻两次才落到 R1 的语义上。别以为卡住了。 print(u' 翻转 %d 件重试:%s' % (len(flip), ' '.join(f[0] for f in flip))) call(FLIP_JS % json.dumps(flip)) raw = call(READ_PINS_JS) pins = {k: {str(n): (x, y) for n, _nm, x, y in v} for k, v in raw.items()} pnames = {k: {str(n): (_nm or '').upper() for n, _nm, x, y in v} for k, v in raw.items()} bad = [] for d, wp in sorted(want.items()): got = pins.get(d) if not got: bad.append(u'%s 没放上' % d); continue for n, xy in sorted(wp.items()): if got.get(n) != tuple(xy): bad.append(u'%s.%s 期望 %s 实测 %s' % (d, n, tuple(xy), got.get(n))) if bad: raise SystemExit(u'引脚坐标与 R1 对不上(%d 处):\n ' % len(bad) + '\n '.join(bad[:20])) W1, F1_, PT1 = entry_block(pins, pnames) W, F, PT = W0 + W1, F0 + F1_, PT0 + PT1 err = selfcheck(W, F, PT, pins) if err: raise SystemExit(u'本地自检失败(%d 条):\n ' % len(err) + '\n '.join(err[:25])) def batch(items, size, js): for i in range(0, len(items), size): call(OPEN + js % json.dumps(items[i:i + size], ensure_ascii=False)) batch(W, 40, "for (const s of %s) await eda.sch_PrimitiveWire.create(s); return 1;") batch(PT, 25, "for (const p of %s) await eda.sch_PrimitiveComponent.createNetPort(p[0],p[1],p[2],p[3],p[4],false); return 1;") batch(F, 25, "for (const f of %s) await eda.sch_PrimitiveComponent.createNetFlag(f[0],f[1],f[2],f[3],f[4],false); return 1;") batch([list(t) for t in TEXTS], 15, "for (const t of %s) await eda.sch_PrimitiveText.create(t[0],t[1],t[2],0,null,null,t[3],false,false,false,0); return 1;") print(u'%s: 元件 %d 导线 %d 旗 %d 端口 %d 文字 %d' % (PAGE_NAME, len(place), len(W), len(F), len(PT), len(TEXTS))) if __name__ == '__main__': main()