R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+285
@@ -0,0 +1,285 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""P1-INPUT:把 12V 输入座 `J1` 从 XT30PW-M 换成**圆形 DC 电源座** DC-005-5.5*2.5。
|
||||
|
||||
/c/Python314/python .claude/p1_dcjack.py # 干跑,只打印计划
|
||||
/c/Python314/python .claude/p1_dcjack.py --push # 执行 + 存盘
|
||||
|
||||
用户 2026-09-08 拍板:换圆座、**不降载**,接受插座超额定运行。
|
||||
|
||||
## ⛔ 这颗件是全板唯一一处「明知超额定还要装」的地方
|
||||
|
||||
整机 12V 最坏工况 **9.99 A**(设计文档 §22.6),而 DC-005 家族的额定是
|
||||
**2~5 A**(立创同族 XKB `DC-005-2.5A-2.5` = C319098 官方标 2.5 A)。
|
||||
发热点在**插头内芯与插座弹片的接触面**,不在板上的铜 —— 板子这边再加强也救不了那一处。
|
||||
⇒ 这不是「算过没问题」,是**已知风险、用户明确接受**。设计文档 §1 / §22.6 要同步写明。
|
||||
|
||||
## 引脚定义(查过三处,两处互相印证)
|
||||
|
||||
| 脚 | 含义 | 本设计接法 |
|
||||
|---|---|---|
|
||||
| 1 | 中心极,**正** | `VIN_RAW` → F1.1 |
|
||||
| 2 | 负极**静触点**(无插头时与 3 短接)| GND |
|
||||
| 3 | 负极**动触点**(插头外壳压这一片)| GND |
|
||||
|
||||
**2 和 3 一起接地**,理由两条:
|
||||
|
||||
1. **安全**:不同厂家对「哪一片是常驻外壳极」的叫法不一致 —— 有的把 2 当外壳、
|
||||
有的把 3 当外壳。两个都接地就与这个分歧无关,且**两者都是负极侧**,
|
||||
无论如何接不出「插拔瞬间把适配器正极对地短路」这种事故。
|
||||
(反过来,若把 3 当成「开关信号」接到别处,一旦它其实是外壳极,回流路径就断了。)
|
||||
2. **正好帮到超额定**:两个负极触点**并联分流**,接触电阻减半、发热减半。
|
||||
这是本轮唯一一处能在板端缓解 10 A 问题的地方。
|
||||
|
||||
⚠️ 装配前用万用表核一次:无插头时 2-3 应导通、1 对 2/3 应断开。
|
||||
|
||||
## 版面(原理图单位 0.01 inch,Y 向上,坐标一律 5 的倍数)
|
||||
|
||||
元件放 `(110, 700)` rot 0 —— 该符号 rot 0 时三个脚都在**右侧** `+30`:
|
||||
|
||||
pin1 (+) (140, 710) → 复用原有导线 [250,710 → 140,710] 到 F1.1
|
||||
pin3 (−) (140, 700) → [140,700 → 185,700 → 185,665] → 地旗 (185,665)
|
||||
pin2 (−) (140, 690) → [140,690 → 140,655] → 原有地旗 (140,655)
|
||||
|
||||
⚠️ 原来那条地线 `[140,700 → 140,655]` **必须删掉**:新符号的 pin2 落在 (140,690)
|
||||
正好在它中间,而 §8b「导线在元件引脚处被截断」会把它劈成两段,
|
||||
接出来的东西看着连着、实际是断的。
|
||||
"""
|
||||
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' # P1-INPUT
|
||||
LIB = '0819f05c4eef4c71ace90d822a990e87'
|
||||
DEV = '66ed565d4c984ace901de00bbc83af82' # DC-005-5.5*2.5,立创 C9900017043
|
||||
|
||||
CX, CY = 110, 700 # 新 J1 的中心
|
||||
PIN = {'1': (140, 710), '3': (140, 700), '2': (140, 690)} # rot 0 时的绝对坐标
|
||||
|
||||
# 新画的导线(扁平数组,见 §4)
|
||||
WIRES = [
|
||||
[140, 690, 140, 655], # pin2 -> 原有地旗
|
||||
[140, 700, 185, 700, 185, 665], # pin3 -> 新地旗
|
||||
]
|
||||
# 新建的旗:(kind, net, x, y, rot)
|
||||
FLAGS = [('Ground', 'GND', 185, 665, 0)]
|
||||
|
||||
|
||||
def _seg(a, b, c, d):
|
||||
u"""一段导线的规范化键 —— 两端顺序无关(`getState_Line()` 会把两端反过来,§5)。"""
|
||||
return ','.join(str(v) for v in min((a, b, c, d), (c, d, a, b)))
|
||||
|
||||
|
||||
def _mine():
|
||||
u"""本脚本负责的那几段线(把折线拆成段)。"""
|
||||
out = set()
|
||||
for w in WIRES:
|
||||
for i in range(0, len(w) - 3, 2):
|
||||
out.add(_seg(w[i], w[i + 1], w[i + 2], w[i + 3]))
|
||||
return out
|
||||
|
||||
|
||||
def call(js, timeout=240):
|
||||
r = run(js, timeout=timeout)
|
||||
if not r.get('success'):
|
||||
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:1200])
|
||||
return r['result']
|
||||
|
||||
|
||||
def selfcheck():
|
||||
u"""下发前的本地几何自检(§6d / §8a-2b)。"""
|
||||
for w in WIRES:
|
||||
assert len(w) % 2 == 0 and len(w) >= 4, u'导线点数不对: %s' % (w,)
|
||||
for v in w:
|
||||
assert v % 5 == 0, u'导线坐标不是 5 的倍数: %s' % (w,)
|
||||
for i in range(0, len(w) - 2, 2):
|
||||
ax, ay, bx, by = w[i], w[i + 1], w[i + 2], w[i + 3]
|
||||
assert (ax == bx) != (ay == by), u'不是横平竖直或零长度: %s' % ((ax, ay, bx, by),)
|
||||
for _k, _n, x, y, _r in FLAGS:
|
||||
assert x % 5 == 0 and y % 5 == 0, u'旗坐标不是 5 的倍数'
|
||||
# 每条导线的起点都必须落在某个引脚上,否则就是算错了位置
|
||||
starts = {(w[0], w[1]) for w in WIRES}
|
||||
assert starts == {PIN['2'], PIN['3']}, u'导线起点和引脚对不上: %s' % (starts,)
|
||||
print(u'本地自检通过:%d 条导线 / %d 个旗,坐标全是 5 的倍数、横平竖直' % (len(WIRES), len(FLAGS)))
|
||||
|
||||
|
||||
JS_SURVEY = u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
const cur = await eda.dmt_Schematic.getCurrentSchematicPageInfo();
|
||||
if (cur.name !== 'P1-INPUT') return {err: 'wrong page: ' + cur.name};
|
||||
const out = {page: cur.name, parts: [], wires: [], flags: []};
|
||||
for (const c of await eda.sch_PrimitiveComponent.getAll()) {
|
||||
const t = c.getState_ComponentType();
|
||||
const pid = c.getState_PrimitiveId();
|
||||
const x = c.getState_X(), y = c.getState_Y();
|
||||
if (t === 'part') {
|
||||
const nm = (c.getState_Component() || {}).name || '';
|
||||
if (c.getState_Designator() === 'J1' || nm.indexOf('DC-005') === 0)
|
||||
out.parts.push({pid, des: c.getState_Designator(), nm, x, y, rot: c.getState_Rotation()});
|
||||
} else if (t === 'netflag' && x < 320 && y > 590 && y < 820) {
|
||||
out.flags.push({pid, net: c.getState_Net(), x, y, rot: c.getState_Rotation()});
|
||||
}
|
||||
}
|
||||
for (const w of await eda.sch_PrimitiveWire.getAll()) {
|
||||
const pts = w.getState_Line();
|
||||
const xs = pts.filter((_, i) => i %% 2 === 0), ys = pts.filter((_, i) => i %% 2 === 1);
|
||||
if (Math.min(...xs) > 320 || Math.max(...ys) < 590 || Math.min(...ys) > 820) continue;
|
||||
out.wires.push({pid: w.getState_PrimitiveId(), pts});
|
||||
}
|
||||
return out;
|
||||
""" % PAGE
|
||||
|
||||
|
||||
def survey():
|
||||
s = call(JS_SURVEY)
|
||||
if s.get('err'):
|
||||
sys.exit(s['err'])
|
||||
return s
|
||||
|
||||
|
||||
def main():
|
||||
push = '--push' in sys.argv
|
||||
selfcheck()
|
||||
s = survey()
|
||||
print(u'\n当前 P1-INPUT 上:')
|
||||
for p in s['parts']:
|
||||
print(u' 元件 %-4s %-18s (%d,%d) rot %s %s' % (p['des'], p['nm'], p['x'], p['y'], p['rot'], p['pid']))
|
||||
for f in s['flags']:
|
||||
print(u' 旗 %-6s (%d,%d) rot %s %s' % (f['net'], f['x'], f['y'], f['rot'], f['pid']))
|
||||
for w in s['wires']:
|
||||
print(u' 导线 %s %s' % (w['pid'], w['pts']))
|
||||
|
||||
old_j1 = [p for p in s['parts'] if p['des'] == 'J1' and p['nm'].find('DC-005') != 0]
|
||||
dcs = [p for p in s['parts'] if p['nm'].find('DC-005') == 0]
|
||||
# 那条要删的地线:起点/终点里同时出现 (140,700) 和 (140,655)
|
||||
gnd_old = [w for w in s['wires']
|
||||
if [140, 700] == w['pts'][:2] or [140, 700] == w['pts'][-2:]]
|
||||
|
||||
print(u'\n计划:')
|
||||
print(u' 删 旧 J1(XT30) %d 个' % len(old_j1))
|
||||
print(u' 删 旧地线 [140,700→140,655] %d 条' % len(gnd_old))
|
||||
print(u' 留 现有 DC-005 实例 %d 个(多余的删掉,留一个 rot 0 的改名 J1)' % len(dcs))
|
||||
print(u' 新建导线 %d 条 / 地旗 %d 个' % (len(WIRES), len(FLAGS)))
|
||||
if not push:
|
||||
print(u'\n加 --push 执行')
|
||||
return
|
||||
|
||||
keep = None
|
||||
for p in dcs:
|
||||
if p['rot'] == 0 and keep is None:
|
||||
keep = p
|
||||
if keep is None:
|
||||
sys.exit(u'⛔ 页面上没有 rot 0 的 DC-005 实例 —— 先手动 create 一个再跑')
|
||||
drop = [p['pid'] for p in dcs if p['pid'] != keep['pid']] + [p['pid'] for p in old_j1]
|
||||
|
||||
r = call(u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
const cur = await eda.dmt_Schematic.getCurrentSchematicPageInfo();
|
||||
if (cur.name !== 'P1-INPUT') return {err: 'wrong page: ' + cur.name};
|
||||
const DROP = %s, WDROP = %s;
|
||||
let nd = 0, nw = 0;
|
||||
for (const id of DROP) { try { await eda.sch_PrimitiveComponent.delete(id); nd++; } catch (e) {} }
|
||||
for (const id of WDROP) { try { await eda.sch_PrimitiveWire.delete(id); nw++; } catch (e) {} }
|
||||
// 留下来的那颗:挪到位、改位号。**只动 x/y,不动 rotation**(§6b)
|
||||
const all = await eda.sch_PrimitiveComponent.getAll();
|
||||
const k = all.find(c => c.getState_PrimitiveId() === '%s');
|
||||
if (!k) return {err: 'kept component vanished'};
|
||||
await k.toAsync().setState_X(%d).setState_Y(%d).setState_Designator('J1').done();
|
||||
return {nd, nw};
|
||||
""" % (PAGE, json.dumps(drop), json.dumps([w['pid'] for w in gnd_old]),
|
||||
keep['pid'], CX, CY))
|
||||
if r.get('err'):
|
||||
sys.exit(r['err'])
|
||||
print(u'\n删元件 %d / 删导线 %d,已把 %s 挪到 (%d,%d) 并改名 J1'
|
||||
% (r['nd'], r['nw'], keep['pid'], CX, CY))
|
||||
|
||||
# ⚠️ 挪完必须读回引脚,不能按偏移量推(§6c / §8d)
|
||||
pins = call(u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r => setTimeout(r, 1200));
|
||||
for (const c of await eda.sch_PrimitiveComponent.getAll()) {
|
||||
if (c.getState_ComponentType() !== 'part') continue;
|
||||
if (c.getState_Designator() !== 'J1') continue;
|
||||
const o = {};
|
||||
for (const p of await c.getAllPins())
|
||||
o[p.getState_PinNumber()] = [p.getState_X(), p.getState_Y()];
|
||||
return {x: c.getState_X(), y: c.getState_Y(), rot: c.getState_Rotation(),
|
||||
nm: (c.getState_Component() || {}).name, pins: o};
|
||||
}
|
||||
return null;""" % PAGE)
|
||||
print(u'回读 J1:%s (%s,%s) rot %s 引脚 %s'
|
||||
% (pins['nm'], pins['x'], pins['y'], pins['rot'], pins['pins']))
|
||||
for n, (ex, ey) in PIN.items():
|
||||
got = pins['pins'].get(n)
|
||||
assert got and got[0] == ex and got[1] == ey, \
|
||||
u'⛔ J1.%s 落在 %s,期望 (%d,%d) —— 方向或位置不对,导线先别画' % (n, got, ex, ey)
|
||||
print(u' ✔ 三个引脚坐标与计划逐个对上')
|
||||
|
||||
r2 = call(u"""
|
||||
await eda.dmt_EditorControl.openDocument('%s');
|
||||
await new Promise(r => setTimeout(r, 1200));
|
||||
// ⚠️ 幂等:重跑时先删掉本脚本画过的导线和地旗,否则会叠一层
|
||||
// (§9v:只加不删的脚本不幂等;重叠的细线还会被 EasyEDA 并进旧线里,看着像"没生效")
|
||||
// ⚠️⚠️ 比对必须按**线段集合**,不能按 getState_Line() 的原数组:
|
||||
// 它返回的是端点对(§5),而且两端顺序会反过来 —— 按数组比一条都匹配不上,
|
||||
// 于是"清理"静默落空、每跑一次多画一份。
|
||||
const W = %s, F = %s;
|
||||
const seg = (a, b, c, d) => [a, b, c, d] < [c, d, a, b] ? [a, b, c, d].join(',') : [c, d, a, b].join(',');
|
||||
const mine = new Set();
|
||||
for (const w of W)
|
||||
for (let i = 0; i + 3 < w.length; i += 2) mine.add(seg(w[i], w[i+1], w[i+2], w[i+3]));
|
||||
let nrm = 0;
|
||||
for (const w of await eda.sch_PrimitiveWire.getAll()) {
|
||||
const p = w.getState_Line();
|
||||
let all = p.length >= 4, any = false;
|
||||
for (let i = 0; i + 3 < p.length; i += 4) { // 端点对:每 4 个数一段
|
||||
const s = seg(p[i], p[i+1], p[i+2], p[i+3]);
|
||||
if (mine.has(s)) any = true; else all = false;
|
||||
}
|
||||
if (all && any) { await eda.sch_PrimitiveWire.delete(w.getState_PrimitiveId()); nrm++; }
|
||||
}
|
||||
for (const c of await eda.sch_PrimitiveComponent.getAll())
|
||||
if (c.getState_ComponentType() === 'netflag')
|
||||
for (const f of F)
|
||||
if (c.getState_X() === f[2] && c.getState_Y() === f[3]) {
|
||||
await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId()); nrm++;
|
||||
}
|
||||
let nw = 0, nf = 0;
|
||||
for (const w of W) { await eda.sch_PrimitiveWire.create(w); nw++; }
|
||||
for (const f of F) { await eda.sch_PrimitiveComponent.createNetFlag(f[0], f[1], f[2], f[3], f[4], false); nf++; }
|
||||
// 原理图存盘是 sch_Document.save(),不是 dmt_EditorControl.save()(后者不存在)
|
||||
const saved = await eda.sch_Document.save();
|
||||
return {nw, nf, nrm, saved};
|
||||
""" % (PAGE, json.dumps(WIRES), json.dumps(FLAGS)))
|
||||
print(u'清掉重跑残留 %d 个;新建导线 %d 条 / 地旗 %d 个,save() 返回 %s'
|
||||
% (r2['nrm'], r2['nw'], r2['nf'], r2['saved']))
|
||||
|
||||
# §9ad:save() 的返回值不算数,回读一遍才是落盘的证据。
|
||||
# ⚠️ 按**线段集合**比,不是按数组:getState_Line() 回来的是端点对,顺序还会反(§5)。
|
||||
back = survey()
|
||||
got = set()
|
||||
for w in back['wires']:
|
||||
p = w['pts']
|
||||
for i in range(0, len(p) - 3, 4):
|
||||
got.add(_seg(p[i], p[i + 1], p[i + 2], p[i + 3]))
|
||||
dup = sum(1 for w in back['wires']
|
||||
for i in range(0, len(w['pts']) - 3, 4)
|
||||
if _seg(*w['pts'][i:i + 4]) in _mine())
|
||||
for s in sorted(_mine()):
|
||||
assert s in got, u'⛔ 导线没落盘: %s' % (s,)
|
||||
assert dup == len(_mine()), u'⛔ 本脚本的线在板上有 %d 段,应该是 %d 段(重复了)' % (dup, len(_mine()))
|
||||
got_f = {(f['net'], f['x'], f['y']) for f in back['flags']}
|
||||
for _k, n, x, y, _r in FLAGS:
|
||||
assert (n, x, y) in got_f, u'⛔ 地旗没落盘: %s (%d,%d)' % (n, x, y)
|
||||
print(u' ✔ 回读确认:%d 段导线(无重复)+ %d 个地旗都在盘上' % (len(_mine()), len(FLAGS)))
|
||||
print(u'\n接着跑:verify_sch.py --nets → port_audit.py --board Board1 → xpage.py')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户