2021年2月28日日曜日

KiCadの基板の大きさをpythonで測る


背景

KiCadで設計した基板を発注する際、zipのアップロードの他に基板の大きさの入力を求められることが多いです。
設計データから自動で大きさを読み取ってくれるのは自分が知っている中ではOSC Parkのみです。
Fusion PCBは時々それらしい挙動になることがありますが、基本大きさの手動入力が必要です。

KiCadのpcbnewの物差し機能を使えば計れはするのですが毎回やるのが手間に思えてきたので、プログラムで測定する方法を調べてみました。
試行錯誤の末に成功したので、備忘録を兼ねて書き方を共有します。

使ったもの

KiCad 5.1.9

下記のページからダウンロードできます。

https://kicad.org/download/

KiCadのpcbnewでのpythonの動かし方

pcbnewの ツール -> スクリプトコンソール を選ぶと入力画面を表示できます。


この画面にpythonのプログラムを入力します。


関数名は自動補完が効くことがあります。
for文の中など階層が深くなると出てきません。


自分の使うKiCadで動いているpythonのバージョンは3.8.5でした。


参考にしたKiCadの資料

非公式ですが、機能一覧は下記のページが探した中で最も分かりやすかったです。
大まかな使い方はgistで公開されているコードなどを参考にしつつ、細部は下記の資料を参考にしました。

Welcome to KiCad’s Python API documentation!

コード貼り付け実行時の注意

この記事で紹介しているコードをコピーしてKiCadのコンソールで実行すると、pcbnewの読み込みで下記のようなエラーが発生することがあります。
  File "/usr/lib/python3.8/codeop.py", line 141, in __call__
codeob = compile(source, filename, symbol, self.flags, 1)
File "<input>", line 1
import pcbnew
^
SyntaxError: multiple statements found while compiling a single statement

このエラーはコピーする内容に不要な情報が含まれるために発生するようです。
ペースト時にCtrl + v ではなく、Ctrl + Shift + v とすると、不要な情報が取り除かれるのか自分の環境では成功するようになりました。

直線だけなら最大値と最小値の差から大きさを取得

切断線の起点と終点は下記のコードで取得できます。
import pcbnew

pcb = pcbnew.GetBoard()

for draw in pcb.GetDrawings():
if draw.GetLayerName() == "Edge.Cuts" and draw.GetShapeStr() == "Line":
draw.GetStart()
draw.GetEnd()

横10.2mm縦15.3mmの基盤のpcbnewで上記のスクリプトを実行してみます。


下記の結果を得ました。
wxPoint(125900000, 63900000)
wxPoint(115700000, 63900000)
wxPoint(125900000, 79200000)
wxPoint(125900000, 63900000)
wxPoint(115700000, 79200000)
wxPoint(125900000, 79200000)
wxPoint(115700000, 63900000)
wxPoint(115700000, 79200000)

wxPointの第一項の最大値は1259*10^5、最小値は1157*10^5、その差は102*10^5です。
10^6が1mmに対応するようなので 102*10^5/10^6=10.2となり、横の長さである10.2mmを得られます。
第二項の最大値792*10^5と最小値639*10^5に対して同様の計算をすることで、縦の長さである15.3mmを得られます。

その考えをコードにしました。
import pcbnew

class MinMax1DimHolder:
min = None
max = None

def updateMinMax(self, v):
self.max = v if self.max is None else max(v, self.max)
self.min = v if self.min is None else min(v, self.min)

def getDistance(self):
return self.max - self.min

def getDistanceMm(self):
return self.getDistance() / 1000000

class MinMax2DimHolder:
x = MinMax1DimHolder()
y = MinMax1DimHolder()

def updateMinMax(self, point):
self.x.updateMinMax(point[0])
self.y.updateMinMax(point[1])

pointMinMax = MinMax2DimHolder()
pcb = pcbnew.GetBoard()

for draw in pcb.GetDrawings():
if draw.GetLayerName() == "Edge.Cuts":
pointMinMax.updateMinMax(draw.GetStart())
pointMinMax.updateMinMax(draw.GetEnd())

pointMinMax.x.getDistanceMm()
pointMinMax.y.getDistanceMm()

実行して期待する結果を得られました。

pointMinMax.x.getDistanceMm()
10.2
pointMinMax.y.getDistanceMm()
15.3

円は半径(と中心)から算出

円の場合は要素が1つなので、半径からの大きさを算出できます。
import pcbnew

pcb = pcbnew.GetBoard()

for draw in pcb.GetDrawings():
if draw.GetLayerName() == "Edge.Cuts" and draw.GetShapeStr() == "Circle":
        draw.GetCenter()
draw.GetRadius()

直径10.872の円形の切断線に対して実行してみます。


下記の結果を得ました。
wxPoint(157734000, 88646000)
5435837

大きさは5435837*2/10^6=10.871674mmと分かります。
(KiCadの距離表示は、小数点第三位で丸めれるようです。)

切断線が円だけならこれで問題ないのですが、円の内側に切り抜きがあったり、四角の基盤の内側に円形の切り抜きがあったり、複数の切断線が利用されている場合があります。
先ほど共有した直線だけの判別に円形切断線の座標の最大値と最小値を組み合わせることで、そのような複数の種類が利用された基盤の大きさも判別可能になります。

中心+半径で座標の最大値、中心-半径で座標の最小値が分かります。



半円は、中心、開始点、終了点、半径、開始角、描画角から算出

今回説明する中で最もややこしい形状です。
import pcbnew

pcb = pcbnew.GetBoard()

for draw in pcb.GetDrawings():
if draw.GetLayerName() == "Edge.Cuts" and draw.GetShapeStr() == "Arc":
        draw.GetCenter()
        draw.GetArcStart()
        draw.GetArcEnd()
draw.GetRadius()
    
    draw.GetArcAngleStart()
        draw.GetAngle()

縦の長さが約6.35mmの半円に対して実行してみます。


下記の結果を得られました。
wxPoint(153378052, 85121623)
wxPoint(147574000, 82804000)
wxPoint(148602337, 89152868)
6249672
2017.6737983301603
-619.3553998

pcbnewの要素編集画面を見ると(位が異なるものの)中心点、開始点、終了点、角度を確認でき、pythonのプログラムだとそれに加えて半径と開始角を得られます。


この条件で縦(y)と横(x)の最大値と最小値を求めます。

基本: xとyの最大値と最小値は、開始点と終了点の最大値と最小値。
角度が0度の描画がある: yの最小値は中心y座標-半径
角度が90度の描画がある: xの最小値は中心x座標-半径
角度が180度の描画がある: yの最大値は中心y座標+半径
角度が270度の描画がある: xの最大値は中心x座標+半径

pcbnewはx軸が右向きに、y軸が下向きに伸びているので、今回の半円に対して補助線を付けて角度を表すと下記の図の関係になります。
angleは正で時計回り、負で半時計周りです。
180度の描画があるため横軸xの最小値がその点になり、他の点最大値と最小値は開始点と終了点を見れば良いのが分かります。


xの最大値は開始点と終了点の大きいほうがそれになり、今回は終了点の方がxが大きいので148576924です。
xの最小値は180度の点なので 153378052-6249672 = 147128380 です。
よって、この半円の描画される横幅はxの 148602337-147128380 = 1473957となり10^6で割ってmmで表すと約1.474mmとなります。

縦幅は90度と270度の描画がないので、開始点と終了点が最大値と最小値を持ち、その差は89152868−82804000 = 6348868なので、約6.348mmとなります。
(図の縦の長さが合っていないのは、他の図形と異なりKiCadの頂点を合わせてくれる補助機能が使えない形なため、目視配置だからです。)

縦の長さに対して横が1/4弱の長さとなり、見た目と同じ印象なので計算が合っていそうです。

その考えをコードにしました。
MinMax1DimHolderとMinMax2DimHolderの実装は直線だけの場合と同じです。
GetAngle系の関数で取れる角度はなぜか10倍されているので、10で割って利用しています。
import pcbnew

class MinMax1DimHolder:
min = None
max = None

def updateMinMax(self, v):
self.max = v if self.max is None else max(v, self.max)
self.min = v if self.min is None else min(v, self.min)

def getDistance(self):
return self.max - self.min

def getDistanceMm(self):
return self.getDistance() / 1000000

class MinMax2DimHolder:
x = MinMax1DimHolder()
y = MinMax1DimHolder()

def updateMinMax(self, point):
self.x.updateMinMax(point[0])
self.y.updateMinMax(point[1])

def hasLineOnDegree(targetDegree, angleDegree, angleDegreeStart):
angleDegreeEnd = angleDegreeStart + angleDegree
result = False
if angleDegree > 0:
result = (angleDegreeStart <= targetDegree and targetDegree <= angleDegreeEnd) or (angleDegreeStart - 360 <= targetDegree and targetDegree <= angleDegreeEnd - 360)
else:
result = (angleDegreeEnd <= targetDegree and targetDegree <= angleDegreeStart ) or (angleDegreeEnd + 360 <= targetDegree and targetDegree <= angleDegreeStart + 360)
# print('check', targetDegree, result)
# print(angleDegree, angleDegreeStart, angleDegreeEnd)
return result

def getArcMinMaxPoints(draw):
pointCenter = draw.GetCenter()
pointStart = draw.GetArcStart()
pointEnd = draw.GetArcEnd()
points = [pointStart, pointEnd]
radius = draw.GetRadius()
angleDegreeStart = draw.GetArcAngleStart() / 10
angleDegree = draw.GetAngle() / 10
if hasLineOnDegree(0, angleDegree, angleDegreeStart):
points.append(pcbnew.wxPoint(pointCenter[0]+radius, pointCenter[1]))
if hasLineOnDegree(90, angleDegree, angleDegreeStart):
points.append(pcbnew.wxPoint(pointCenter[0], pointCenter[1]+radius))
if hasLineOnDegree(180, angleDegree, angleDegreeStart):
points.append(pcbnew.wxPoint(pointCenter[0]-radius, pointCenter[1]))
if hasLineOnDegree(270, angleDegree, angleDegreeStart):
points.append(pcbnew.wxPoint(pointCenter[0], pointCenter[1]-radius))
return points

pointMinMax = MinMax2DimHolder()
pcb = pcbnew.GetBoard()

for draw in pcb.GetDrawings():
if draw.GetLayerName() == "Edge.Cuts" and draw.GetShapeStr() == "Arc":
for point in getArcMinMaxPoints(draw):
pointMinMax.updateMinMax(point)

pointMinMax.x.getDistanceMm()
pointMinMax.y.getDistanceMm()

実行して期待する結果を得られました。


pointMinMax.x.getDistanceMm()
1.473957
pointMinMax.y.getDistanceMm()
6.348868

全部まとめる

直線、円、半円、全てに対応したコードがこちらです。
import pcbnew

class MinMax1DimHolder:
min = None
max = None

def updateMinMax(self, v):
self.max = v if self.max is None else max(v, self.max)
self.min = v if self.min is None else min(v, self.min)

def getDistance(self):
return self.max - self.min

def getDistanceMm(self):
return self.getDistance() / 1000000

class MinMax2DimHolder:
x = MinMax1DimHolder()
y = MinMax1DimHolder()

def updateMinMax(self, point):
self.x.updateMinMax(point[0])
self.y.updateMinMax(point[1])

def hasLineOnDegree(targetDegree, angleDegree, angleDegreeStart):
angleDegreeEnd = angleDegreeStart + angleDegree
result = False
if angleDegree > 0:
result = (angleDegreeStart <= targetDegree and targetDegree <= angleDegreeEnd) or (angleDegreeStart - 360 <= targetDegree and targetDegree <= angleDegreeEnd - 360)
else:
result = (angleDegreeEnd <= targetDegree and targetDegree <= angleDegreeStart ) or (angleDegreeEnd + 360 <= targetDegree and targetDegree <= angleDegreeStart + 360)
# print('check', targetDegree, result)
# print(angleDegree, angleDegreeStart, angleDegreeEnd)
return result

def getArcMinMaxPoints(draw):
pointCenter = draw.GetCenter()
pointStart = draw.GetArcStart()
pointEnd = draw.GetArcEnd()
points = [pointStart, pointEnd]
radius = draw.GetRadius()
angleDegreeStart = draw.GetArcAngleStart() / 10
angleDegree = draw.GetAngle() / 10
if hasLineOnDegree(0, angleDegree, angleDegreeStart):
points.append(pcbnew.wxPoint(pointCenter[0]+radius, pointCenter[1]))
if hasLineOnDegree(90, angleDegree, angleDegreeStart):
points.append(pcbnew.wxPoint(pointCenter[0], pointCenter[1]+radius))
if hasLineOnDegree(180, angleDegree, angleDegreeStart):
points.append(pcbnew.wxPoint(pointCenter[0]-radius, pointCenter[1]))
if hasLineOnDegree(270, angleDegree, angleDegreeStart):
points.append(pcbnew.wxPoint(pointCenter[0], pointCenter[1]-radius))
return points

pointMinMax = MinMax2DimHolder()
pcb = pcbnew.GetBoard()

for draw in pcb.GetDrawings():
if draw.GetLayerName() == "Edge.Cuts":
if draw.GetShapeStr() == "Arc":
for point in getArcMinMaxPoints(draw):
pointMinMax.updateMinMax(point)
elif draw.GetShapeStr() == "Circle":
r = draw.GetRadius()
center = draw.GetCenter()
x = center[0]
y = center[1]
pointMinMax.updateMinMax(pcbnew.wxPoint(x + r, y + r))
pointMinMax.updateMinMax(pcbnew.wxPoint(x - r, y - r))
else:
pointMinMax.updateMinMax(draw.GetStart())
pointMinMax.updateMinMax(draw.GetEnd())

pointMinMax.x.getDistanceMm()
pointMinMax.y.getDistanceMm()

切断線として直線、半円、円が使われた下記の基板に対して実行してみます。


期待する結果を得られました。

pointMinMax.x.getDistanceMm()
7.900406
pointMinMax.y.getDistanceMm()
10.063605

コピペせずに使いたい

自分が作ったgerber_to_orderというガーバーファイルをzipでまとめるプラグインに大きさ測定機能を追加しました。

https://github.com/asukiaaa/gerber_to_order/
PCB製造サービス向けのガーバーデータとzipを作るKiCadのプラグインを作ってみた

このプラグインを実行することで、大きさ情報を名前に含んだガーバーファイル入のzipファイルを生成します。



良かったらご利用ください。

まとめ

KiCadのpcbnewのpython実行機能を利用して、基盤の切断線から縦と横の大きさを取得できました。
gerber_to_orderに機能を取り込んだので、それを使えば発注時に利用するzipファイルを見れば縦横の大きさが分かります。

変更履歴

2021/03/01
「全部まとめる」と「コピペせずに使いたい」を追加しました。

0 件のコメント :