R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-04 19:24:45 +08:00
共同撰写人 Claude Opus 5.5
父节点 cd01640c40
当前提交 cf7cc9cf90
共修改 759 个文件,包含 76962 行新增和 5299 行删除
+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()