297 行
14 KiB
Python
297 行
14 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""A3-CTRL:U20(ESP32-S3-WROOM-1)按**方案 B** 重分配 10 只脚 + J33 应急排针挪位(2026-09-11,用户选「落地方案 B」)。
|
||
|
||
/c/Python314/python .claude/r2/u20_remap_b.py # 只读:对账现状(改前 / 改后 / 都不是)
|
||
/c/Python314/python .claude/r2/u20_remap_b.py --push # 真改:删端口重建、删线重画、J33 挪位
|
||
|
||
## 方案 B(PROGRESS「U20 一簇诊断」;route_base.U20_REMAPS['B'],第 31 轮服务器试算离线断开 29 → 27)
|
||
|
||
| 模组脚 | IO | 原来 | 改成 |
|
||
|---|---|---|---|
|
||
| 4 / 5 | IO4 / IO5(ADC1)| FAN_TACH_OUT / FAN_TACH_IN | **NTC1 / NTC2**(去南边的热敏电阻)|
|
||
| 6 / 7 | IO6 / IO7 | FAN_PWM / FAN_EN | **UP_VBUS_DET / PRSNT5#** |
|
||
| 22 / 23 | IO14 / IO21 | PRSNT5# / UP_VBUS_DET | **FAN_TACH_OUT / FAN_TACH_IN** |
|
||
| 24 / 25 | IO47 / IO48 | 空 | **FAN_PWM / FAN_EN** |
|
||
| 38 / 39 | IO2 / IO1 | NTC2 / NTC1 | **空** |
|
||
|
||
引脚审计(`s3_pin_audit.audit()`,R8 八线严格口径)ERROR 0;WARN 与现分配相同(IO0 strapping、UART0 两脚,均有意为之)。
|
||
|
||
## J33 为什么要挪
|
||
|
||
24/25 在页上 y = 480 / 470,原来 J33 的地线 (410,475)→(340,475)→(340,415) 和四个端口的标签(x 303~370)
|
||
正好占着这两行 —— 25 脚一出脚就撞 TXD0 的标签。⇒ J33 挪到 (450,420):端口放 x 400(标签 333~400,
|
||
离 U20 地干线 x 320 留 13),地线直接接 U20 地干线已有的节点 (320,440),不再单挂地旗。
|
||
|
||
## 约定(skill easyeda-sch-geometry)
|
||
- 端口 / 旗**只能删了重建**,不能移动、不能改名(§3b);dir 从 `getState_Component().name` 读(§8b)。
|
||
- 坐标全是 5 的倍数(§3),端口引脚落在导线**端点**(§8a-2),导线横平竖直、非零长度(§6d)。
|
||
- 元件可以直接移动;挪完读回引脚坐标再画线(§6b/§8d)。
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, os.path.dirname(HERE))
|
||
sys.path.insert(0, HERE)
|
||
sys.stdout.reconfigure(encoding='utf-8')
|
||
from eda import run # noqa: E402
|
||
|
||
PAGE = '38327f806bc13133' # A3-CTRL(r2-uuids.json)
|
||
|
||
|
||
def rot_r(d):
|
||
u"""标签在引脚右侧:IN → 180,BI/OUT → 0(skill §8b)。"""
|
||
return 180 if d == 'IN' else 0
|
||
|
||
|
||
# U20 右侧(引脚 x = 295):(端口 x, y, 旧网, 旧 dir, 新网, 新 dir);新网 None = 删端口删线(改空脚)
|
||
U20_CHG = [
|
||
(470, 760, 'FAN_TACH_OUT', 'IN', 'NTC1', 'IN'), # 脚 4 IO4
|
||
(340, 750, 'FAN_TACH_IN', 'IN', 'NTC2', 'IN'), # 脚 5 IO5
|
||
(470, 740, 'FAN_PWM', 'OUT', 'UP_VBUS_DET', 'IN'), # 脚 6 IO6
|
||
(340, 730, 'FAN_EN', 'OUT', 'PRSNT5#', 'IN'), # 脚 7 IO7
|
||
(470, 660, 'PRSNT5#', 'IN', 'FAN_TACH_OUT', 'IN'), # 脚 22 IO14
|
||
(340, 590, 'UP_VBUS_DET', 'IN', 'FAN_TACH_IN', 'IN'), # 脚 23 IO21
|
||
(340, 780, 'NTC2', 'IN', None, None), # 脚 38 IO2 → 空
|
||
(470, 790, 'NTC1', 'IN', None, None), # 脚 39 IO1 → 空
|
||
]
|
||
U20_DEL_WIRES = [[(340, 780, 295, 780)], [(470, 790, 295, 790)]]
|
||
U20_NEW = [(340, 480, 'FAN_PWM', 'OUT'), # 脚 24 IO47
|
||
(340, 470, 'FAN_EN', 'OUT')] # 脚 25 IO48
|
||
U20_NEW_WIRES = [(295, 480, 340, 480), (295, 470, 340, 470)]
|
||
|
||
# J33:旧端口 / 旧地旗 / 旧导线 → 新位置
|
||
J33_OLD_XY, J33_NEW_XY = (430, 455), (450, 420)
|
||
J33_OLD_PORTS = [(370, 465, 'TXD0'), (370, 455, 'RXD0'), (370, 445, 'EN'), (370, 435, 'IO0')]
|
||
J33_OLD_FLAG = (340, 415, 'GND')
|
||
J33_OLD_WIRES = [[(340, 475, 410, 475), (340, 475, 340, 415)], [(410, 465, 370, 465)],
|
||
[(410, 455, 370, 455)], [(410, 445, 370, 445)], [(410, 435, 370, 435)]]
|
||
J33_PINS_NEW = {'1': (430, 440), '2': (430, 430), '3': (430, 420), '4': (430, 410), '5': (430, 400)}
|
||
J33_NEW_PORTS = [(400, 430, 'TXD0'), (400, 420, 'RXD0'), (400, 410, 'EN'), (400, 400, 'IO0')]
|
||
J33_NEW_WIRES = [(430, 430, 400, 430), (430, 420, 400, 420), (430, 410, 400, 410), (430, 400, 400, 400),
|
||
(430, 440, 320, 440)] # 1 脚 GND → U20 地干线的 (320,440) 节点
|
||
|
||
|
||
def selfcheck():
|
||
u"""下发前本地断言:5 的倍数、横平竖直非零长度、每个新端口都落在某条新线的端点上。"""
|
||
ws = U20_NEW_WIRES + J33_NEW_WIRES
|
||
for a, b, c, d in ws:
|
||
assert (a == c) != (b == d), u'不是横平竖直或零长度:%s' % ((a, b, c, d),)
|
||
assert all(v % 5 == 0 for v in (a, b, c, d)), u'不是 5 的倍数:%s' % ((a, b, c, d),)
|
||
ends = set()
|
||
for a, b, c, d in ws:
|
||
ends |= {(a, b), (c, d)}
|
||
ports = [(x, y) for x, y, _n, _d in U20_NEW] + [(x, y) for x, y, _n in J33_NEW_PORTS]
|
||
for p in ports:
|
||
assert p in ends, u'端口 %s 不在新导线的端点上' % (p,)
|
||
assert p[0] % 5 == 0 and p[1] % 5 == 0
|
||
for x, y, _o, _od, n, _nd in U20_CHG:
|
||
assert x % 5 == 0 and y % 5 == 0
|
||
for k, (x, y) in J33_PINS_NEW.items():
|
||
assert (x, y) in ends, u'J33.%s 新位置不在新导线端点上' % k
|
||
|
||
|
||
JS_READ = u"""
|
||
await eda.dmt_EditorControl.openDocument('%s');
|
||
await new Promise(r => setTimeout(r, 1500));
|
||
const cur = await eda.dmt_Schematic.getCurrentSchematicPageInfo();
|
||
const out = {page: cur && cur.name, ports: [], wires: [], j33: null};
|
||
for (const c of await eda.sch_PrimitiveComponent.getAll()) {
|
||
const t = c.getState_ComponentType();
|
||
if (t === 'netport' || t === 'netflag') {
|
||
let nm = ''; try { nm = (c.getState_Component() || {}).name || ''; } catch (e) {}
|
||
out.ports.push([c.getState_PrimitiveId(), t, String(c.getState_Net() || ''),
|
||
Math.round(c.getState_X()), Math.round(c.getState_Y()), c.getState_Rotation(), nm]);
|
||
} else if (t === 'part' && c.getState_Designator() === 'J33') {
|
||
const pins = (await c.getAllPins()).map(p => [String(p.getState_PinNumber()), Math.round(p.getState_X()), Math.round(p.getState_Y())]);
|
||
out.j33 = [c.getState_PrimitiveId(), Math.round(c.getState_X()), Math.round(c.getState_Y()), c.getState_Rotation(), pins];
|
||
}
|
||
}
|
||
for (const w of await eda.sch_PrimitiveWire.getAll()) {
|
||
const flat = [];
|
||
const push = v => { if (Array.isArray(v)) v.forEach(push); else flat.push(Math.round(Number(v))); };
|
||
push(w.getState_Line());
|
||
out.wires.push([w.getState_PrimitiveId(), flat]);
|
||
}
|
||
return out;
|
||
"""
|
||
|
||
|
||
def read():
|
||
r = run(JS_READ % PAGE, timeout=180)
|
||
assert r.get('success'), json.dumps(r, ensure_ascii=False)[:400]
|
||
d = r['result']
|
||
assert d['page'] == 'A3-CTRL', u'打开的不是 A3-CTRL:%s' % d['page']
|
||
return d
|
||
|
||
|
||
def pdir(nm):
|
||
return nm.replace('Netport-', '') if nm.startswith('Netport-') else nm
|
||
|
||
|
||
def segs_of(flat):
|
||
return [tuple(flat[i:i + 4]) for i in range(0, len(flat) - 3, 4)]
|
||
|
||
|
||
def norm(s):
|
||
a, b, c, d = s
|
||
return (a, b, c, d) if (a, b) <= (c, d) else (c, d, a, b)
|
||
|
||
|
||
def find_port(d, x, y, net, kind='netport'):
|
||
hit = [p for p in d['ports'] if p[1] == kind and p[3] == x and p[4] == y and p[2] == net]
|
||
return hit[0] if len(hit) == 1 else None
|
||
|
||
|
||
def find_wire(d, want):
|
||
u"""整条导线图元的段集合 == want(顺序 / 方向不论)。"""
|
||
w = set(norm(s) for s in want)
|
||
hit = [wid for wid, flat in d['wires'] if set(norm(s) for s in segs_of(flat)) == w]
|
||
return hit[0] if len(hit) == 1 else None
|
||
|
||
|
||
def has_seg(d, s):
|
||
s = norm(s)
|
||
for _wid, flat in d['wires']:
|
||
for t in segs_of(flat):
|
||
t = norm(t)
|
||
# 同一条直线上、覆盖住 s(EasyEDA 会把共线相接的段合并)
|
||
if t[0] == t[2] == s[0] == s[2] and min(t[1], t[3]) <= min(s[1], s[3]) and max(t[1], t[3]) >= max(s[1], s[3]):
|
||
return True
|
||
if t[1] == t[3] == s[1] == s[3] and min(t[0], t[2]) <= min(s[0], s[2]) and max(t[0], t[2]) >= max(s[0], s[2]):
|
||
return True
|
||
return False
|
||
|
||
|
||
def state(d):
|
||
u"""返回 (改前是否齐全, 改后是否齐全, 明细)。"""
|
||
bef, aft, notes = True, True, []
|
||
for x, y, on, od, nn, nd in U20_CHG:
|
||
p = find_port(d, x, y, on)
|
||
if not (p and pdir(p[6]) == od):
|
||
bef = False
|
||
if nn:
|
||
q = find_port(d, x, y, nn)
|
||
if not (q and pdir(q[6]) == nd and q[5] == rot_r(nd)):
|
||
aft = False
|
||
elif find_port(d, x, y, on):
|
||
aft = False
|
||
for x, y, n, dd in U20_NEW:
|
||
if find_port(d, x, y, n):
|
||
bef = False
|
||
q = find_port(d, x, y, n)
|
||
if not (q and pdir(q[6]) == dd):
|
||
aft = False
|
||
for ws in U20_DEL_WIRES:
|
||
if not find_wire(d, ws):
|
||
bef = False
|
||
for s in U20_NEW_WIRES:
|
||
if not has_seg(d, s):
|
||
aft = False
|
||
j = d['j33']
|
||
if not j:
|
||
return False, False, [u'页上找不到 J33']
|
||
if (j[1], j[2]) != J33_OLD_XY:
|
||
bef = False
|
||
if (j[1], j[2]) != J33_NEW_XY:
|
||
aft = False
|
||
for x, y, n in J33_OLD_PORTS:
|
||
if not find_port(d, x, y, n):
|
||
bef = False
|
||
for x, y, n in J33_NEW_PORTS:
|
||
q = find_port(d, x, y, n)
|
||
if not (q and pdir(q[6]) == 'BI' and q[5] == 180):
|
||
aft = False
|
||
if not find_port(d, J33_OLD_FLAG[0], J33_OLD_FLAG[1], J33_OLD_FLAG[2], 'netflag'):
|
||
bef = False
|
||
for ws in J33_OLD_WIRES:
|
||
if not find_wire(d, ws):
|
||
bef = False
|
||
for s in J33_NEW_WIRES:
|
||
if not has_seg(d, s):
|
||
aft = False
|
||
notes.append(u'J33 在 (%d,%d) rot %s,引脚 %s' % (j[1], j[2], j[3], sorted(j[4])))
|
||
return bef, aft, notes
|
||
|
||
|
||
def run_js(js, timeout=300):
|
||
r = run(js, timeout=timeout)
|
||
assert r.get('success'), json.dumps(r, ensure_ascii=False)[:500]
|
||
return r['result']
|
||
|
||
|
||
def push(d):
|
||
# ── 1. 删:端口 / 地旗 / 导线(id 在删之前全部认齐,认不齐一个都不删)──
|
||
del_ports, del_wires = [], []
|
||
for x, y, on, _od, _nn, _nd in U20_CHG:
|
||
del_ports.append(find_port(d, x, y, on)[0])
|
||
for x, y, n in J33_OLD_PORTS:
|
||
del_ports.append(find_port(d, x, y, n)[0])
|
||
del_ports.append(find_port(d, J33_OLD_FLAG[0], J33_OLD_FLAG[1], J33_OLD_FLAG[2], 'netflag')[0])
|
||
for ws in U20_DEL_WIRES + J33_OLD_WIRES:
|
||
del_wires.append(find_wire(d, ws))
|
||
assert all(del_ports) and all(del_wires), u'要删的图元没认齐,不动手'
|
||
js = (u"await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r => setTimeout(r, 1500));"
|
||
u"const P = %s, W = %s;"
|
||
u"await eda.sch_PrimitiveComponent.delete(P);"
|
||
u"for (const w of W) await eda.sch_PrimitiveWire.delete(w);"
|
||
u"return {p: P.length, w: W.length};") % (PAGE, json.dumps(del_ports), json.dumps(del_wires))
|
||
print(u' 删端口/旗 %(p)d、导线 %(w)d' % run_js(js))
|
||
# ── 2. J33 挪位,读回引脚 ──
|
||
js = (u"await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r => setTimeout(r, 1500));"
|
||
u"for (const c of await eda.sch_PrimitiveComponent.getAll()) {"
|
||
u" if (c.getState_ComponentType() !== 'part' || c.getState_Designator() !== 'J33') continue;"
|
||
u" await c.toAsync().setState_X(%d).setState_Y(%d).done(); }"
|
||
u"await new Promise(r => setTimeout(r, 500));"
|
||
u"for (const c of await eda.sch_PrimitiveComponent.getAll()) {"
|
||
u" if (c.getState_ComponentType() !== 'part' || c.getState_Designator() !== 'J33') continue;"
|
||
u" return (await c.getAllPins()).map(p => [String(p.getState_PinNumber()), Math.round(p.getState_X()), Math.round(p.getState_Y())]); }"
|
||
u"return null;") % (PAGE, J33_NEW_XY[0], J33_NEW_XY[1])
|
||
pins = dict((k, (x, y)) for k, x, y in run_js(js))
|
||
bad = [(k, pins.get(k), v) for k, v in J33_PINS_NEW.items() if pins.get(k) != v]
|
||
assert not bad, u'J33 挪位后引脚不在预期位置(不画线,先人工看):%s' % bad
|
||
print(u' J33 → (%d,%d),5 只引脚逐个对上' % J33_NEW_XY)
|
||
# ── 3. 画线,再建端口 ──
|
||
wires = U20_NEW_WIRES + J33_NEW_WIRES
|
||
ports = ([['IN' if nd == 'IN' else nd, nn, x, y, rot_r(nd)] for x, y, _on, _od, nn, nd in U20_CHG if nn] +
|
||
[[dd, n, x, y, rot_r(dd)] for x, y, n, dd in U20_NEW] +
|
||
[['BI', n, x, y, 180] for x, y, n in J33_NEW_PORTS])
|
||
js = (u"await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r => setTimeout(r, 1500));"
|
||
u"const W = %s, P = %s;"
|
||
u"for (const w of W) await eda.sch_PrimitiveWire.create(w);"
|
||
u"for (const p of P) await eda.sch_PrimitiveComponent.createNetPort(p[0], p[1], p[2], p[3], p[4], false);"
|
||
u"await eda.sch_Document.save(); await new Promise(r => setTimeout(r, 1000));"
|
||
u"return {w: W.length, p: P.length};") % (PAGE, json.dumps([list(w) for w in wires]),
|
||
json.dumps(ports, ensure_ascii=False))
|
||
print(u' 画线 %(w)d、建端口 %(p)d,已存盘' % run_js(js))
|
||
|
||
|
||
def main():
|
||
selfcheck()
|
||
d = read()
|
||
bef, aft, notes = state(d)
|
||
for n in notes:
|
||
print(u' ' + n)
|
||
if aft:
|
||
print(u'[OK] 已是方案 B(改后状态逐项对上),不用再改')
|
||
return
|
||
if not bef:
|
||
print(u'[X] 既不是改前也不是改后 —— 页面被别人动过或上次改到一半,不动手,先人工看')
|
||
sys.exit(1)
|
||
print(u'现状 = 改前(逐项对上)')
|
||
if '--push' not in sys.argv:
|
||
print(u'(没加 --push,没有下发)')
|
||
return
|
||
push(d)
|
||
bef2, aft2, notes2 = state(read())
|
||
for n in notes2:
|
||
print(u' ' + n)
|
||
print(u'[%s] 读回:%s' % ('OK' if aft2 else 'X', u'改后状态逐项对上' if aft2 else u'对不上,先人工看'))
|
||
if not aft2:
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|