文件
phonerail/.claude/pcb2_npth.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

154 行
6.4 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""把**封装自带、API 读不出来的钻孔**从内层铺铜的反焊盘里反解出来。
/c/Python314/python .claude/pcb2_npth.py # 打印
/c/Python314/python .claude/pcb2_npth.py --write # 存 _pcb2_npth.json(布线脚本读它)
## 为什么要这么绕
J23(USB-C 母座)的封装里有两个 **Ø0.60 的非金属化定位孔**。它们不是焊盘:
`pcb_PrimitivePad.getAll()` 里没有,`pcb_PrimitiveRegion` / `pcb_PrimitiveFill` 是空的,
`pcb_Primitive.getPrimitiveByPrimitiveId('e55')` 也拿不到 —— 封装内部的图元 API 不给。
唯一能看见它们的地方是 **DRC**(「挖槽区域 到 导线」)和**内层铺铜给它们让出来的反焊盘**。
⇒ 本脚本把 Inner1/Inner2 的铺铜环读回来,找出里面的**圆弧**,
按「已知过孔的反焊盘半径 − 过孔半径」标定出铺铜净距,再反推出每个圆孔的真实半径;
最后把**对不上任何焊盘/过孔的那些圆**报出来 —— 那就是封装自带的孔。
2026-09-06 实测:J23 上两个 **Ø0.600 @ (213.660, 59.100) / (213.660, 64.900)**,
正好卡在 A1B12 / B1A12 两个 GND 焊盘的**外侧角上**,
所以「从 GND 焊盘往东拉一小段线打过孔」这条最自然的走法是走不通的。
DRC 报的「距离为 0mil」就是线正压在孔上。
⚠️ 这套反推**只对圆孔成立**。槽孔(SLOT)的反焊盘是跑道形,两端的圆弧半径
等于槽半宽,会被当成两个小圆报出来 —— 所以下面按「圆心和已知焊盘中心对得上」
把它们滤掉,剩下的才当未知孔。
"""
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from eda import run
import inner1_verify as IV
import pcb2_route_dp as R
OUT = os.path.join(HERE, '_pcb2_npth.json')
MINPTS = 10 # 至少这么多个连续点拟合同一个圆才算一段弧
TOL = 0.010 # 拟合残差上限(mm)
def circle3(a, b, c):
u"""三点定圆;共线返回 None。"""
ax, ay = a
bx, by = b
cx, cy = c
d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
if abs(d) < 1e-9:
return None
ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay)
+ (cx * cx + cy * cy) * (ay - by)) / d
uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx)
+ (cx * cx + cy * cy) * (bx - ax)) / d
return (ux, uy, math.hypot(ax - ux, ay - uy))
def arcs(poly):
u"""从一个环里挑出所有圆弧段 -> [(cx, cy, r), ...](贪心地往后延伸)。"""
out = []
n = len(poly)
i = 0
while i < n - 2:
c = circle3(poly[i], poly[i + 1], poly[i + 2])
if not c or c[2] > 5.0:
i += 1
continue
j = i + 3
while j < n and abs(math.hypot(poly[j][0] - c[0], poly[j][1] - c[1]) - c[2]) < TOL:
j += 1
if j - i >= MINPTS:
pts = poly[i:j]
sx = sum(p[0] for p in pts) / len(pts)
sy = sum(p[1] for p in pts) / len(pts)
# 用整段重新拟一次圆心(三点法对噪声敏感)
c2 = circle3(pts[0], pts[len(pts) // 2], pts[-1])
out.append(c2 or c)
i = j
else:
i += 1
return out
def main():
r = run(IV.JS, timeout=300)
if not r.get('success'):
sys.exit(u'读回铺铜失败: ' + json.dumps(r, ensure_ascii=False)[:400])
data = r['result']
found = []
for key, d in data.items():
if d['layer'] not in (15, 16):
continue
for rg in d['rings']:
pl = IV.ring_to_poly(rg)
if len(pl) >= 3:
found += arcs(pl)
# 去重(同一个孔在 Inner1 / Inner2 上各有一份)
uniq = {}
for cx, cy, rr in found:
uniq[(round(cx, 2), round(cy, 2), round(rr, 2))] = (cx, cy, rr)
circles = sorted(uniq.values(), key=lambda t: (t[1], t[0]))
print(u'铺铜环里一共拟出 %d 段不重复的圆弧' % len(circles))
pads, vias, _lines = R.geom()
# ── 标定铺铜净距:拿圆心和过孔中心对得上的那些 ──
clr = []
for cx, cy, rr in circles:
for v in vias:
if math.hypot(cx - v['x'], cy - v['y']) < 0.02:
clr.append(rr - v['r'])
break
if not clr:
sys.exit(u'一个过孔的反焊盘都没认出来,标定不了净距')
clr.sort()
C = clr[len(clr) // 2]
spread = clr[-1] - clr[0]
print(u'反焊盘净距标定:%d 个过孔,中位数 %.4f mm(离散度 %.4f)' % (len(clr), C, spread))
if spread > 0.02:
print(u' [!!] 离散度偏大,下面反推出来的孔径要打个问号')
# ── 认领:圆心对得上某个过孔 / 某个带孔焊盘的,就不是未知孔 ──
holed = [p for p in pads if p['lay'] == 12 or True]
unknown = []
for cx, cy, rr in circles:
if any(math.hypot(cx - v['x'], cy - v['y']) < 0.05 for v in vias):
continue
if any(math.hypot(cx - p['x'], cy - p['y']) < 0.05 for p in holed):
continue
# 槽孔两端的圆弧:圆心落在某个焊盘矩形里
if any(abs(cx - p['x']) <= p['hw'] + 1e-6 and abs(cy - p['y']) <= p['hh'] + 1e-6
for p in holed):
continue
hr = rr - C
# 剩下的圆弧大多是**几何噪声**,两类:
# · 铺铜多边形自己的凸角(铜面内缩 0.358 之后就是一段 r=0.358 的圆角)-> hr ≈ 0
# · 板框四角的 R5 -> hr ≈ 4.04
# 真正的孔在 0.1~3.0 mm 之间。
if 0.05 <= hr <= 1.5:
unknown.append((cx, cy, hr))
print(u'')
print(u'对不上任何焊盘/过孔、孔径又在 0.1~3.0 mm 的圆孔 %d 个'
u'(= 封装自带、API 读不出来的那种):' % len(unknown))
for cx, cy, hr in unknown:
print(u' (%8.3f, %8.3f) Ø%.3f mm' % (cx, cy, hr * 2))
if '--write' in sys.argv:
json.dump([[round(cx, 4), round(cy, 4), round(hr, 4)] for cx, cy, hr in unknown],
open(OUT, 'w', encoding='utf-8'))
print(u'已写入 %s' % os.path.basename(OUT))
if __name__ == '__main__':
main()