文件
phonerail/.claude/skills/easyeda-sch-geometry/scripts/local_netcheck.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

119 行
4.3 KiB
Python

# -*- coding: utf-8 -*-
"""本地布线检查器:在推送到 EasyEDA 之前先验证几何连通性,避免反复返工。"""
import json
from collections import defaultdict
PINS = json.load(open('pins.json'))
def pin(d, n):
x, y, _ = PINS[d][str(n)]
return (x, y)
SEGS = [] # [(p1, p2, net)]
FLAGS = [] # [(kind, net, x, y, rot)] kind: Power/Ground
PORTS = [] # [(dir, net, x, y, rot)]
TEXTS = [] # [(s, x, y, size)]
def w(net, *pts):
"""折线:逐点相连,拆成 2 点段。坐标必须是 5 的倍数。"""
for p in pts:
assert p[0] % 5 == 0 and p[1] % 5 == 0, f'{net}: {p} 不是 5 的倍数'
for a, b in zip(pts, pts[1:]):
assert a[0] == b[0] or a[1] == b[1], f'{net}: {a}->{b} 不是正交'
if a != b:
SEGS.append((a, b, net))
def gnd(x, y):
FLAGS.append(('Ground', 'GND', x, y, 0))
def pwr(net, x, y):
FLAGS.append(('Power', net, x, y, 0))
def port(net, x, y, rot, d='BI'):
PORTS.append((d, net, x, y, rot))
def txt(s, x, y, size=12):
TEXTS.append((s, x, y, size))
# ---------------- 连通性检查 ----------------
def _on(p, a, b, tol=0.6):
(px, py), (ax, ay), (bx, by) = p, a, b
if abs(ax - bx) < tol: # 竖直
return abs(px - ax) < tol and min(ay, by) - tol <= py <= max(ay, by) + tol
if abs(ay - by) < tol: # 水平
return abs(py - ay) < tol and min(ax, bx) - tol <= px <= max(ax, bx) + tol
return False
def check():
"""按几何反推网络,和声明的 net 名比对。返回 (错误列表, 网络表)。"""
errs = []
n = len(SEGS)
parent = list(range(n))
def find(i):
while parent[i] != i:
parent[i] = parent[parent[i]]; i = parent[i]
return i
def union(i, j):
a, b = find(i), find(j)
if a != b: parent[a] = b
for i in range(n):
for j in range(i + 1, n):
a1, a2, _ = SEGS[i]; b1, b2, _ = SEGS[j]
# 端点落在另一段上(含共享端点)才算连接;纯交叉不算
if _on(a1, b1, b2) or _on(a2, b1, b2) or _on(b1, a1, a2) or _on(b2, a1, a2):
union(i, j)
groups = defaultdict(list)
for i in range(n):
groups[find(i)].append(i)
nets = {}
for root, idxs in groups.items():
names = sorted({SEGS[i][2] for i in idxs})
if len(names) > 1:
detail = []
for i in idxs:
detail.append('%s%s-%s' % (SEGS[i][2], SEGS[i][0], SEGS[i][1]))
errs.append('短路: ' + ' + '.join(names) + ' :: ' + ' | '.join(detail))
nets[names[0]] = idxs
# 同名网络被拆成多块 = 漏连
seen = defaultdict(int)
for root, idxs in groups.items():
seen[SEGS[idxs[0]][2]] += 1
for name, cnt in seen.items():
if cnt > 1:
errs.append(f'网络 {name} 被拆成 {cnt} 块(漏连)')
# 每个元件引脚必须落在某条线段上
for d, pd in PINS.items():
for num, (x, y, nm) in pd.items():
if not any(_on((x, y), s[0], s[1]) for s in SEGS):
errs.append(f'悬空引脚: {d}.{num} ({nm}) @ ({x},{y})')
# 旗/端口引脚也必须落在线段上
for kind, net, x, y, rot in FLAGS:
if not any(_on((x, y), s[0], s[1]) for s in SEGS):
errs.append(f'悬空旗: {kind} {net} @ ({x},{y})')
for d, net, x, y, rot in PORTS:
if not any(_on((x, y), s[0], s[1]) for s in SEGS):
errs.append(f'悬空端口: {net} @ ({x},{y})')
return errs
def crossings():
"""列出所有交叉点(不相连),供人工确认可读性。"""
out = []
for i in range(len(SEGS)):
for j in range(i + 1, len(SEGS)):
a1, a2, na = SEGS[i]; b1, b2, nb = SEGS[j]
if na == nb: continue
hz_a = a1[1] == a2[1]; hz_b = b1[1] == b2[1]
if hz_a == hz_b: continue
h, v = ((a1, a2), (b1, b2)) if hz_a else ((b1, b2), (a1, a2))
y = h[0][1]; x = v[0][0]
if min(h[0][0], h[1][0]) < x < max(h[0][0], h[1][0]) and \
min(v[0][1], v[1][1]) < y < max(v[0][1], v[1][1]):
out.append((na, nb, x, y))
return out