文件
phonerail/.claude/pcb1_env.py
T
zhou yang和Claude Opus 5 2bfe308b30 初始导入:PhoneRail 五路手机直供电源板
原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。
这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。

纳入:
- 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本
- .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、
  pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源)
- 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾)
- firmware/ 的 A1 骨架与 site/ 文档站源码

.gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、
site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。

.gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存,
不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
2026-09-06 18:18:14 +08:00

176 行
7.5 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""下板 PCB1 的**环境约束图**:把载板 PCB2 底面的东西折算到下板坐标系。
下板 160×125 居中在载板 220×135 下方 ⇒ **下板坐标 = 载板坐标 − (30, 5)**
(事实来源 `pcb2-header-pads.json` 的 frame 字段 / 设计文档 §25.1)。
两板净空 **13 mm**(§22.3f:公头塑高 2.5 + 母座塑高 10.5)。
产出 `pcb1-env.json`:
| 字段 | 含义 |
|---|---|
| `anchors` | J24 / J25 的 40 个焊盘在下板坐标下的目标位置(**对位的唯一依据**,别按引脚号推)|
| `zones` | 一堆矩形,每个带 `hmax` = 下板在这块地方允许的最大元件高度 = 13 − h载板 − 1 |
| `lanes` | 排线在载板背面占的车道(下板坐标)。排线要平贴着走,这里 `hmax` 取 11.0 |
| `slots` | 载板的 10 条穿线槽投影(下板边缘 y<1.3 / y>123.9 两条带)|
用法: /c/Python314/python .claude/pcb1_env.py
"""
import io, json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__)); sys.path.insert(0, HERE)
import pcb2_place as P2
from eda import run
DX, DY = 30.0, 5.0 # 下板原点在载板坐标里的位置
GAP = 13.0 # 两板净空
MARGIN = 1.0 # 装配余量
BW, BH = 160.0, 125.0
LANE_H = 1.0 # 排线(0.2 厚)+ 走线余量,按 1 mm 算
def shift(x0, y0, x1, y1):
return [round(x0 - DX, 2), round(y0 - DY, 2), round(x1 - DX, 2), round(y1 - DY, 2)]
def clip(r):
u"""裁到下板板框内;完全在框外返回 None。"""
x0 = max(r[0], 0.0); y0 = max(r[1], 0.0)
x1 = min(r[2], BW); y1 = min(r[3], BH)
if x1 <= x0 or y1 <= y0:
return None
return [round(x0, 2), round(y0, 2), round(x1, 2), round(y1, 2)]
def header_anchors():
u"""载板 J31/J32 的 80 个焊盘 -> 下板坐标 + **网络名**。
⚠️ 对插的判据是**网络**,不是针号。两颗封装的焊盘编号同手性、母座又在载板底层,
所以下板 pin(2r−1) 正对载板 pin(2r)(设计文档 §26.5)。针号一定对不上,网络必须对上。
坐标来自 `pcb2-header-pads.json`(`pcb2_place --push` 导出),网络当场从载板读。
"""
pads = json.load(io.open(os.path.join(HERE, 'pcb2-header-pads.json'), encoding='utf-8'))['pads']
nets = {}
r = run("""
await eda.dmt_EditorControl.openDocument('f126ffb29c92f9b2');
await new Promise(r=>setTimeout(r,2000));
const out = {};
for (const c of await eda.pcb_PrimitiveComponent.getAll()) {
const d = c.getState_Designator();
if (d !== 'J31' && d !== 'J32') continue;
const ps = await eda.pcb_PrimitiveComponent.getAllPinsByPrimitiveId(c.getState_PrimitiveId());
out[d] = ps.map(p => [p.padNumber, p.net]);
}
return out;
""", timeout=300)
if not r.get('success'):
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:400])
for ref, lst in r['result'].items():
for pad, net in lst:
nets[(ref, pad)] = net
out = {}
for q in pads:
out.setdefault(q['ref'], {})[q['pad']] = [round(q['x'] - DX, 3), round(q['y'] - DY, 3),
nets.get((q['ref'], q['pad']))]
# J31 -> J24,J32 -> J25
return {'J24': out['J31'], 'J25': out['J32']}
def build():
pos = P2.plan()
bb = P2.load_bb()
P2.CAN = {P2.desig(n)['c1'] for n in P2.SITES}
zones = []
for d, (x, y, r, lay) in sorted(pos.items()):
if lay != 2 and d not in P2.th_parts():
continue
h = P2.part_height(d)
b = P2.box(d, r, bb, 2)
rect = clip(shift(x + b[0], y + b[1], x + b[2], y + b[3]))
if not rect:
continue
zones.append({'ref': d, 'rect': rect, 'h': h,
'hmax': round(GAP - h - MARGIN, 2)})
lanes = []
for n in P2.SITES:
for lname, ((u0, u1, v0, v1), owner) in P2.LANES.items():
ax, ay, _ = P2.to_board(n, u0, v0)
bx, by, _ = P2.to_board(n, u1, v1)
rect = clip(shift(min(ax, bx), min(ay, by), max(ax, bx), max(ay, by)))
if rect:
lanes.append({'ref': 'CH%d-%s-lane' % (n, lname), 'rect': rect,
'h': LANE_H, 'hmax': round(GAP - LANE_H - MARGIN, 2)})
slots = []
for name, x0, y0, x1, y1 in P2.slot_rects():
rect = clip(shift(x0, y0, x1, y1))
if rect:
slots.append({'ref': name, 'rect': rect})
anchors = header_anchors()
data = {
'frame': u'下板 PCB1 坐标系 mm,原点左下角,Y 向上。下板坐标 = 载板坐标 − (30,5)。'
u'hmax = 该矩形内下板元件允许的最大高度 = 13(净空) − h(载板底面元件) − 1(余量)',
'gap': GAP, 'board': [BW, BH],
'anchors': anchors, 'zones': zones, 'lanes': lanes, 'slots': slots,
}
out = os.path.join(HERE, 'pcb1-env.json')
json.dump(data, io.open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
return data, out
def report(data, out):
print(u'已写 %s' % out)
print(u' 锚点 J24/J25 各 %d / %d 个焊盘(含载板侧网络名)'
% (len(data['anchors']['J24']), len(data['anchors']['J25'])))
print(u' 受限区 %d 块 / 排线车道 %d 条 / 槽投影 %d 条'
% (len(data['zones']), len(data['lanes']), len(data['slots'])))
tight = sorted([z for z in data['zones'] if z['hmax'] < 10.0], key=lambda z: z['hmax'])
print(u'\n 下板高度受限的地方(hmax < 10 mm):')
for z in tight:
r = z['rect']
print(u' %-6s hmax=%5.2f x %6.2f~%6.2f y %6.2f~%6.2f' % (z['ref'], z['hmax'], r[0], r[2], r[1], r[3]))
print(u'\n 排线车道(下板这些地方元件 ≤ %.1f mm):' % data['lanes'][0]['hmax'] if data['lanes'] else '')
for z in data['lanes']:
r = z['rect']
print(u' %-14s x %6.2f~%6.2f y %6.2f~%6.2f' % (z['ref'], r[0], r[2], r[1], r[3]))
print(u'\n 槽在下板上的投影(排线从这里下穿,别放高件):')
for z in data['slots']:
r = z['rect']
print(u' %-12s x %6.2f~%6.2f y %6.2f~%6.2f' % (z['ref'], r[0], r[2], r[1], r[3]))
if __name__ == '__main__':
d, o = build()
report(d, o)
# ═════════════════ 高度地图(给人看的)═════════════════
def hmap(cell=2.0):
u"""把 zones/lanes 栅格化成一张 hmax 地图,字符越大越矮。"""
d = json.load(io.open(os.path.join(HERE, 'pcb1-env.json'), encoding='utf-8'))
nx, ny = int(BW / cell), int(BH / cell)
g = [[GAP - MARGIN] * nx for _ in range(ny)] # 12.0 = 载板那边是光板
for z in d['zones'] + d['lanes']:
x0, y0, x1, y1 = z['rect']
for j in range(max(0, int(y0 / cell)), min(ny, int((y1 - 1e-6) / cell) + 1)):
for i in range(max(0, int(x0 / cell)), min(nx, int((x1 - 1e-6) / cell) + 1)):
g[j][i] = min(g[j][i], z['hmax'])
def ch(h):
if h >= 12.0: return '.'
if h >= 11.0: return ','
if h >= 10.0: return ':'
if h >= 8.0: return '='
if h >= 3.5: return '#'
return '@'
print(u'\n高度地图(cell=%.0f mm,左下角是原点;. ≥12 , ≥11 : ≥10 = ≥8 # ≥3.5 @ <3.5)' % cell)
print(u' ' + ''.join(str(int(i * cell / 10) % 10) for i in range(nx)))
for j in range(ny - 1, -1, -1):
print(u'%4.0f ' % (j * cell) + ''.join(ch(v) for v in g[j]))
return g