原理图/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
154 行
6.4 KiB
Python
154 行
6.4 KiB
Python
# -*- 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()
|