# -*- coding: utf-8 -*- u"""载板 C4-SITE1 ~ C4-SITE5:每台手机一页。 一页里三块: 【A】电池排线 4 焊盘直焊座 J8n(P+/P−/TEMP/ID,2026-09-06 取代 TP8n1/2)+ TEMP 10k / ID 68k + §6.3 电容组(六颗) 【B】四根侧键顶针(PWR / VUP / VDN + J604 三键公共地) 【C】EDL:1.25mm 2P 座 + TS5A3157 模拟开关 + 100k 下拉 + 去耦 用法: /c/Python314/python .claude/c4_site.py [--push] [--only N] 所有引脚坐标都是**读回来再算线**的(skill §6b:create 的 rotation 与 setState_Rotation 差 180 度,电容跨距也各不相同)。EXPECT 里断言不过就直接终止。 """ import json import time import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from eda import run SCH = '302e115180aaa8fc' # phonerail-carrier LIB = '0819f05c4eef4c71ace90d822a990e87' LIB_POGO = '543725e8f0454017a5691e7e570aeef6' # 个人库,顶针自建封装版在这里 P = { # 2026-09-06 换矮封装(用户拍板):红宝石 6.3TPV2200M10X10.5,C3828985,SMD D10×L10.5, # 液态铝电解 ESR 60mΩ@100kHz(§6.2 要 40~100,非聚合物)。立创库里这颗器件挂的是直插封装, # 所以在个人库里建了「6.3TPV2200M10X10.5 SMD」:符号复制自立创,封装 = CAP-SMD_BD10.0-L10.3-W10.3-LS11.2-FD_H10.5 # 的副本(焊盘 1 = +,渲染图核过)。旧件 C49256788(10×16 插件)留作历史。 'C2200U': '4bb2e55e90d9441eb935a4b4af5beeed', # C3828985 2200uF/6.3V SMD 10x10.5(个人库 LIB_POGO) 'C2200U_OLD': '1477290f70a341ce8e1943e3cd679351', # C49256788 2200uF/10V 插件(作废) 'C100U': 'a110bb2c7fd046cdb078d7f64c0f9f04', # C15008 100uF/6.3V 1206 'C22U': '0de9d28bbcd34ca3bf2a4d78faaa50bb', # C12891 22uF/25V 1206 'C100N': '87bb635d0b2f489a9f60e7cd225beb3c', # C1525 100nF 0402 'TPRED': '17b5ed105c5b4fe180eee3ffb4702963', # C9900007423 红色测试点(2026-09-06 起不用) 'TPBLK': 'cf8b789614c04ee491fd17b89f81a8ab', # C9900007426 黑色测试点(2026-09-06 起不用) # 2026-09-06 用户拍板:电池排线在保护板焊点处剪断,4 个焊盘直焊载板(设计文档 §13.5 末尾)。 # 个人库自建器件 MI8-BAT-FPC-4P(fp_batfpc.py):1=P+ 2=P− 3=TEMP 4=ID;保护板上的 NTC(10k) / ID(68k) 用固定电阻复制。 'BATFPC': '22b0c3130f0b4ff6baab529761dab3e4', # MI8-BAT-FPC-4P(个人库 LIB_POGO) 'R10K': 'c3b9baa5ef2e4070a4c0f9e9cd04fe6e', # C25744 10k 0402 R78n TEMP 'R68K': '94441cf3b58f4f16af17200285ebaf08', # C36871 68k 0402 R79n ID # ⚠ 2026-09-04 改用**个人库自建封装版**:立创原封装 SMD-1_BD2.0-H3.6 的焊盘是 Ø2.5, # 而小米8 四个侧键触点最近的一对(J601↔J604)只隔 2.217 mm —— 两个焊盘会叠在一起短路。 # 顶针法兰本身只有 Ø2.0,所以是立创把焊盘开大了。自建封装 POGO-SMD1P_D1.9 缩到 Ø1.9 # (净间距 0.317 mm,过 0.2 mm 规则)。见 .claude/pcb2_place.py 文件头。 'POGO': '1c8e0ea1e66142a8860d6812b1dbe356', # YTC1P-2510-01 D1.9(个人库,LIB_POGO) 'POGO_LCSC': '43b0bd715d444e1fab4580cb95b3686d', # 作废:C5221288 原件(焊盘 Ø2.5) 'CONN2P': '6461e6e5749d421bb997627984866425', # C225104 A1251WV-S-2P 'SW': '5acafd8eb5a641fcaf05190902930635', # C353034 TS5A3157DBVR 'R100K': 'acc294b6661948399f609b41c77ed19f', # C25741 100k 0402 } # ---- 版面常量 ---- Y_VO, Y_GND = 720, 540 # 【A】两条母线 Y_CAP = 630 # 电容中心 CAP_X = [250, 350, 450, 550, 650, 750] X_RAIL0, X_RAIL1 = 90, 940 # 2026-09-06 母线西端 150→90:给 J8n 的四条竖走廊让地方 J8 = (225, 630) # 排线直焊座:符号 4 脚朝**左**(实测 pin 在 cx−20)、1 在最上,放在第一颗电容左边 # 四脚各自的竖走廊:1(P+) 往上接 VO 母线,走最远的 100(它的竖线在所有横线之上,不会被穿); # 往下走的 2/3/4 按「越低的越靠近引脚」排:2→130 落 GND 母线,3→170 进 R78n,4→190 进 R79n。 COL = {'1': 100, '2': 130, '3': 170, '4': 190} R78, R79 = (170, 575), (190, 575) # TEMP / ID 电阻(竖放),下端直接落在 GND 母线上 POGO_X = [220, 320, 420, 520] # 【B】四根顶针的元件中心 Y_POGO = 380 POGO_PORT_Y = [330, 300, 270, 240] J6, U4 = (660, 400), (880, 400) # 【C】 R74, C76 = (975, 370), (1060, 370) CAPS = [('C2200U', u'2200µF/6.3V'), ('C100U', u'100µF'), ('C100U', u'100µF'), ('C22U', u'22µF'), ('C22U', u'22µF'), ('C100N', u'100nF')] def call(js, timeout=180): r = run(js, timeout=timeout) if not r.get('success'): raise RuntimeError(json.dumps(r, ensure_ascii=False)[:800]) return r['result'] def names(n): base = 801 + (n - 1) * 10 return { 'caps': ['C%d' % (base + i) for i in range(6)], 'j8': 'J8%d' % n, 'r78': 'R78%d' % n, 'r79': 'R79%d' % n, 'pogo': ['J%d' % (41 + (n - 1) * 4 + i) for i in range(4)], 'j6': 'J6%d' % n, 'u4': 'U4%d' % n, 'r74': 'R74%d' % n, 'c76': 'C76%d' % n, } def placement(n): d = names(n) out = [[d['j8'], P['BATFPC'], J8[0], J8[1], 0, LIB_POGO], [d['r78'], P['R10K'], R78[0], R78[1], 270, LIB], [d['r79'], P['R68K'], R79[0], R79[1], 270, LIB]] for i, (key, _v) in enumerate(CAPS): # C80x1 是电解,pin1 = 正极,必须朝上接 VOn_P。 # 实测这颗 2200µF(C49256788)在 create() 里传 270 得到 pin1 在**下**, # 和 skill §6 那张表相反 —— 所以传 90。下面 EXPECT 会再断言一次。 rot = 90 if key == 'C2200U' else 270 out.append([d['caps'][i], P[key], CAP_X[i], Y_CAP, rot, LIB_POGO if key == 'C2200U' else LIB]) for i, x in enumerate(POGO_X): out.append([d['pogo'][i], P['POGO'], x, Y_POGO, 0, LIB_POGO]) out += [[d['j6'], P['CONN2P'], J6[0], J6[1], 0, LIB], [d['u4'], P['SW'], U4[0], U4[1], 0, LIB], [d['r74'], P['R100K'], R74[0], R74[1], 270, LIB], [d['c76'], P['C100N'], C76[0], C76[1], 270, LIB]] # ⚠️ 每条必须是 6 项。少了 lib 那一项 -> `create({libraryUuid: undefined, uuid})` # **永远不 resolve、也不抛错**,表现成「桥接 30 秒超时」,极难往这里想。 for it in out: assert len(it) == 6 and it[5], u'placement 少了 libraryUuid: %s' % (it,) return out def wiring(n, pins): u"""pins: {位号: {引脚号: (x, y)}},全部是读回来的真实坐标。""" d = names(n) W, F, PT, TX = [], [], [], [] def w(*p): for i in range(0, len(p) - 2, 2): W.append([p[i], p[i + 1], p[i + 2], p[i + 3]]) # ---------- 【A】P+/P- 与电容组 ---------- w(X_RAIL0, Y_VO, X_RAIL1, Y_VO) PT.append(['BI', 'VO%d_P' % n, X_RAIL1, Y_VO, 0]) w(X_RAIL0, Y_GND, 900, Y_GND, 900, 510) F.append(['Ground', 'GND', 900, 510, 0]) # 排线直焊座 J8n:4 脚朝左、1 在最上(build_page 里断言过)。每脚先横走到自己的竖走廊再上下落(COL 的注释)。 jp = pins[d['j8']] for k in ('1', '2', '3', '4'): px, py = jp[k] assert px > max(COL.values()), u'%s.%s 引脚 x=%d 太靠左(走廊到 %d)' % (d['j8'], k, px, max(COL.values())) w(px, py, COL[k], py) w(COL['1'], jp['1'][1], COL['1'], Y_VO) w(COL['2'], jp['2'][1], COL['2'], Y_GND) for k, rn in (('3', d['r78']), ('4', d['r79'])): rp = pins[rn] rtop = max(rp.values(), key=lambda v: v[1]) rbot = min(rp.values(), key=lambda v: v[1]) assert rtop[0] == COL[k], u'%s 不在走廊 x=%d 上(实测 %s)' % (rn, COL[k], rtop) w(COL[k], jp[k][1], COL[k], rtop[1]) w(rbot[0], rbot[1], rbot[0], Y_GND) # 直接落在 GND 母线上(T 型连接) for i in range(6): pm = pins[d['caps'][i]] top = max(pm.values(), key=lambda v: v[1]) bot = min(pm.values(), key=lambda v: v[1]) if i == 0: # 电解:pin1 = 正极,必须是接 VOn_P 的那一头(换了符号 rot 语义可能变,读回来断言) assert pm['1'][1] > pm['2'][1], u'%s 的 pin1(+) 不在上面: %s' % (d['caps'][0], pm) w(top[0], top[1], top[0], Y_VO) w(bot[0], bot[1], bot[0], Y_GND) # ---------- 【B】侧键顶针 ---------- labels = ['POGO%d_PWR' % n, 'POGO%d_VUP' % n, 'POGO%d_VDN' % n, None] for i in range(4): px, py = pins[d['pogo'][i]]['1'] yy = POGO_PORT_Y[i] w(px, py, px, yy) if labels[i]: PT.append(['BI', labels[i], px, yy, 180]) else: F.append(['Ground', 'GND', px, yy, 0]) # ---------- 【C】EDL ---------- j, u = pins[d['j6']], pins[d['u4']] no, gnd, com = u['1'], u['2'], u['4'] inn, vp = u['6'], u['5'] s1, s2, t3, t4 = j['1'], j['2'], j['3'], j['4'] # 模拟开关的两个通路端分别在符号两侧,只能一上一下绕,两条走廊错开且互不交叉 w(no[0], no[1], no[0], 490, 530, 490, 530, s1[1], s1[0], s1[1]) w(com[0], com[1], com[0], 300, 560, 300, 560, s2[1], s2[0], s2[1]) w(gnd[0], gnd[1], 825, gnd[1], 825, 350) F.append(['Ground', 'GND', 825, 350, 0]) # 1.25mm 座的两个固定脚接地(和 C3-USB 上的 J17 一致) w(t3[0], t3[1], 600, t3[1], 600, 445) F.append(['Ground', 'GND', 600, 445, 180]) w(t4[0], t4[1], 600, t4[1], 600, 355) F.append(['Ground', 'GND', 600, 355, 0]) # V+ 往下走,避开 IN 那条横线 w(vp[0], vp[1], 945, vp[1], 945, 340) F.append(['Power', '+3V3_HUB', 945, 340, 180]) # IN <- EDL_ENn,中途接 100k 下拉 w(inn[0], inn[1], 1000, inn[1]) PT.append(['BI', 'EDL_EN%d' % n, 1000, inn[1], 0]) rp = pins[d['r74']] rtop = max(rp.values(), key=lambda v: v[1]) rbot = min(rp.values(), key=lambda v: v[1]) w(rtop[0], inn[1], rtop[0], rtop[1]) w(rbot[0], rbot[1], rbot[0], 320) F.append(['Ground', 'GND', rbot[0], 320, 0]) cp = pins[d['c76']] ctop = max(cp.values(), key=lambda v: v[1]) cbot = min(cp.values(), key=lambda v: v[1]) w(ctop[0], ctop[1], ctop[0], 420) F.append(['Power', '+3V3_HUB', ctop[0], 420, 0]) w(cbot[0], cbot[1], cbot[0], 320) F.append(['Ground', 'GND', cbot[0], 320, 0]) TX += [ (45, 806, u'【CH%d 手机位】P+/P− 焊盘 + 电容组 + 侧键顶针 + EDL' % n, 13), (45, 788, u'【A】%s:原厂电池排线在保护板焊点处剪断,4 个焊盘直焊(1=P+ 2=P− 3=TEMP 4=ID,间距 2.0)。保护板整块不要(§13.5 末尾)。' % names(n)['j8'], 10), (45, 772, u' %s 10k 代替保护板上的 NTC(模拟 25 °C),%s 68k 照抄保护板上的 ID 电阻,都对 P−。' % (names(n)['r78'], names(n)['r79']), 10), (45, 470, u'【B】四根顶针顶主板反面的 J601~J604(§13.3)。J604 是三键公共地,', 10), (45, 454, u' 直接接本路的电源返回地。驱动 MOS 在下板,这里只有顶针。', 10), (45, 438, u'顶针 = YTC1P-2510-01(立创 C5221288):自由高 3.5 / 行程 0.8 / 压到底 2.7 mm。', 10), (45, 422, u' 配 3.0mm 立柱 → 压缩 0.5mm(62% 行程,约 62g),屏蔽罩净空 1.6mm。', 10), (45, 406, u' 冠径只有 Ø0.9 球头,±1mm 落点公差靠首件实测校正(§22.3c / §22.3e)。', 10), (45, 205, u'【C】%s 是 1.25mm 2P 座,两根线焊到主板正面的 TP1204 / TP1205(§22.4)。' % names(n)['j6'], 10), (45, 189, u' %s 导通即短接两点 → 手机进 9008。串了 1.8k,短路电流只有 1mA。' % names(n)['u4'], 10), (45, 173, u' %s 100k 下拉紧挨开关放:ESP32 复位期间 74HC595 输出高阻,' % names(n)['r74'], 10), (45, 157, u' 靠它保证手机不会被误拉进 9008(§8.1 复位安全)。', 10), ] # 所有导线必须是水平或垂直、且非零长度 —— 斜线会被 create() 直接拒掉, # 零长度会变成 skill §8a 那种「看着连上、重画就断」的隐形漏接。 # 另外:旗/端口的坐标会被吸附到 5 的倍数,导线端点不会。两边差 2 个单位, # 旗就落在导线中段 = 真断线,而 verify_sch 和 DRC 都看不出来 # (C1-CONN 的 GND 旗写成 y=362 就是这么断的,只有 port_audit 抓得到)。 for a, b, c, e in W: assert (a == c) != (b == e), u'CH%d 有一段不是横平竖直:%s' % (n, (a, b, c, e)) for v in (a, b, c, e): assert v % 5 == 0, u'CH%d 导线坐标 %d 不是 5 的倍数:%s' % (n, v, (a, b, c, e)) for kind, lst in (('flag', F), ('port', PT)): for it in lst: assert it[2] % 5 == 0 and it[3] % 5 == 0, u'CH%d 的 %s %s 坐标 (%d,%d) 不是 5 的倍数' % (n, kind, it[1], it[2], it[3]) return W, F, PT, TX def build_page(n, push): d = names(n) pname = 'C4-SITE%d' % n if not push: return page = call(""" const info = await eda.dmt_Schematic.getSchematicInfo('%s'); for (const p of (info.page||[])) if (p.name==='%s') return p.uuid; const u = await eda.dmt_Schematic.createSchematicPage('%s'); await eda.dmt_Schematic.modifySchematicPageName(u,'%s'); return u; """ % (SCH, pname.lower(), SCH, pname)) call(""" await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,900)); const n=(await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name; if (n!=='%s') throw new Error('page='+n); for (const c of await eda.sch_PrimitiveComponent.getAll()){ if (c.getState_ComponentType()==='sheet') continue; 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; """ % (page, pname)) place = placement(n) # 分两段:创建走「甩出去 + 轮询」绕开桥接的 30 秒单次上限(16 个件逐个联网取 # 库时会撑爆),认领位号留在正常的 await 请求里(快,且要读回全部元件比对坐标)。 # ⚠️ 这个模式只对「慢」有效。2026-09-04 曾经卡在 done=0 不动,真因是 # `placement()` 里那两个测试点漏了 libraryUuid —— `create({libraryUuid: undefined})` # **永远不 resolve 也不报错**,轮询看起来和卡死一样。见 skill §9c-2 与上面的断言。 call(""" globalThis.__pl = {done: 0, err: null, n: %d}; (async () => { for (const [d, uuid, x, y, rot, lib] of %s) { try { await eda.sch_PrimitiveComponent.create({libraryUuid: lib, uuid}, x, y, undefined, rot, false, 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(150): st = call("return globalThis.__pl;") if st['err']: raise SystemExit(u'%s 放置失败: %s' % (pname, st['err'])) if st['done'] >= st['n']: break time.sleep(2) else: raise SystemExit(u'%s 放置超时,只完成 %d/%d' % (pname, st['done'], st['n'])) # 按坐标认领位号(本页每个件的 (x,y) 都是唯一的) named = call(""" const P = %s; let k = 0; const cs = (await eda.sch_PrimitiveComponent.getAll()).filter(c => c.getState_ComponentType() === 'part'); for (const [d, uuid, x, y] of P) { for (const c of cs) { if (Math.round(c.getState_X()) !== x || Math.round(c.getState_Y()) !== y) continue; await c.toAsync().setState_Designator(d).done(); k++; break; } } return k; """ % json.dumps([[d, u, x, y] for d, u, x, y, r, l in place])) if named != len(place): raise SystemExit(u'%s 只认领到 %d/%d 个位号' % (pname, named, len(place))) raw = call(""" 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; """) pins = {k: {p: (x, y) for p, _n, x, y in v} for k, v in raw.items()} pnames = {k: {p: _n for p, _n, x, y in v} for k, v in raw.items()} bad = [] jp = pins[d['j8']] if sorted(jp) != ['1', '2', '3', '4']: bad.append(u'%s 引脚号 %s 不是 1..4' % (d['j8'], sorted(jp))) elif not (jp['1'][1] > jp['2'][1] > jp['3'][1] > jp['4'][1]) or len({v[0] for v in jp.values()}) != 1 or jp['1'][0] >= J8[0]: bad.append(u'%s 引脚要 4 脚朝左、1 在最上,实测 %s' % (d['j8'], jp)) # 电解电容 pin1 = 正极,必须在上(接 VOn_P) c801 = pins[d['caps'][0]] if c801['1'][1] <= c801['2'][1]: bad.append(u'%s 是电解电容,pin1(正极) 必须在上,实测 %s' % (d['caps'][0], c801)) # 模拟开关引脚名要对上 NO/GND/NC/COM/V+/IN want = {'1': 'NO', '2': 'GND', '3': 'NC', '4': 'COM', '5': 'V+', '6': 'IN'} got = pnames[d['u4']] for k, v in want.items(): if got.get(k, '').upper().replace('VCC', 'V+') != v: bad.append(u'%s.%s 引脚名期望 %s 实测 %s' % (d['u4'], k, v, got.get(k))) if bad: raise SystemExit(pname + u' 引脚核对失败:\n ' + '\n '.join(bad)) W, F, PT, TX = wiring(n, pins) def batch(items, size, js): for i in range(0, len(items), size): call(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 TX], 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 (%s)' % (pname, len(place), len(W), len(F), len(PT), len(TX), page)) return page def main(): push = '--push' in sys.argv only = None if '--only' in sys.argv: only = int(sys.argv[sys.argv.index('--only') + 1]) sites = [only] if only else [1, 2, 3, 4, 5] if not push: for n in sites: print(u'C4-SITE%d 元件 %d 个' % (n, len(placement(n)))) print(u'(未推送,加 --push)') return nc = {} for n in sites: build_page(n, True) nc['U4%d' % n] = ['3'] # 模拟开关的 NC 端不用 ncf = os.path.join(HERE, 'c4-nets-nc.json') old = json.load(open(ncf, encoding='utf-8')) if os.path.exists(ncf) else {} old.update(nc) json.dump(old, open(ncf, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) if __name__ == '__main__': main()