- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base) - 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base) - 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、 东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base) - 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面 - 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、 J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder 开关(route_base、remote_route.ps1);剩 37 个网待手工补 - 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base); 已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查) - 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
404 行
20 KiB
Python
404 行
20 KiB
Python
# -*- 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()
|