KiCAD Pcbnew マクロ Tips

KiCAD 5 の頃の内容です。

KiCAD 4.0.0 の Python マクロは Pcbnew のみサポートされており、boost Python ラッパーです。Eeschema でのマクロサポートはコードの大幅リファクタやファイルフォーマット変更後になるため、だいぶ先になるようです。

Pcbnew の Python コンソールで複数行のコードを実行するには、コンテキストメニューから Paste Plus を選択してコードを貼り付けます。関数としておいてペースト、関数を実行すると便利です。

ファイルフォーマット

KiCAD 4.0 では S式 (S-expression) 形式になっており、スクリプトで扱いやすくなっています。

コード実行

Python スクリプトは pcbnew モジュールを起点として以下の二種類のスクリプトとして実行できます。

  • Pcbnew - Tools - Scripting Console から実行
  • 外部の Python から実行

コンフィグパス

Preferences - Configure Paths で設定できるパスは環境変数として os.environ から読み込めます。以下のような変数が利用できます。

Python シェル

Pcbnew - Tools - Scripting Console で表示される Python シェルは wxPython の pyCrust のものを使用しています。
このシェルはいくつか機能があり、shell 変数から利用できます。詳細は以下参照。

便利なものをいくつか紹介します。

  • shell.run( COMMAND )
    • COMMAND を Python の式として実行します。
  • shell.runfile( FILE_PATH )
    • FILE_PATH で指定したファイルを読み込んで実行します。実行時のカレントディレクトリは KiCAD に依存するため、ローカルファイルを読み込んで実行すると、インポートしているファイルが見つからないことがあるかもしれません。import sys; sys.path.append( PATH ) などを使用してください。
  • shell.clear()
    • シェルの表示を空にします。

高機能シェル

import wx; f = wx.py.editor.EditorNotebookFrame(title="KiCad PCB"); f.Show()

https://lists.launchpad.net/kicad-developers/msg21851.html

KiCAD 4.0 がリリースされたあとの開発版ではシェルが高機能のものに置き換わっています。シェルの起動時に自動的にスクリプトを実行できます。実行されるスクリプトは Options - Startup - Edit Startup script から編集できます。ファイルは ~/.config/kicad/PyShell_pcbnew_startup.py に保存されます。

プラグインパス

Pythonスクリプトプラグインパスに配置しておくと自動的に読み込まれてモジュールの register() 関数が実行されます。

KICAD_PATH/scripting/plugins
# Linux の場合、以下も含まれます
~/.kicad_plugins
~/.kicad/scripting/plugins

コンソールから実行したい関数の入ったファイルを入れておくとインポートして簡単に実行できます。

位置やサイズについて

内部の座標や長さなどの値は nm 単位で管理されています。そのため、1 mm は 1,000,000 倍にして設定します。
取得した座標や長さの値は 1/1,000,000 で 1 mm になります。

pcbnew モジュールには数値変換用の関数などが用意されています。

  • wxPoint: 座標など内部形式
  • wxPointMM: mm 単位で生成
  • wxPointMils: mil 単位で生成
  • wxRect: サイズなど内部形式
  • wxRectMM: mm 単位
  • wxRectMils: mil 単位
  • wxSize: サイズなど内部形式
  • wxSizeMM: mm 単位
  • wxSizeMils: mil 単位
  • ToMM: mm に変換
  • ToMils: mil に変換
  • FromMM: mm から
  • FromMils: mil から

値と参照

APIから取得した値が wxPoint などの型の場合、内部の値への参照になっています。
たとえば、グリッドの原点を取得して後で元に戻したい場合は x, y の値をコピーしておく必要があります。

board = pcbnew.GetBoard()
origin = board.GetGridOrigin()
old_origin = pcbnew.wxPoint(origin.x, origin.y)
del origin
# ... ここでグリッドの原点が変更された場合、origin変数から取得できる値も変わる
board.SetGridOrigin(old_origin)

ソフトによっては構造体はコピーだったりしますが Pcbnew ではそうではない様子。

表示状態と PCB_VISIBLE 列挙型

ボード上で表示と非表示を切り替えられる要素の表示状態は以下のようにして取得、変更できます。
これらに使用する値は pcbnew からアクセスできる PCB_VISIBLE 列挙型のものです。PCB_VISIBLE の値はビットシフトの値です。
https://github.com/KiCad/kicad-source-mirror/blob/master/include/layers_id_colors_and_visibility.h#L403

board = pcbnew.GetBoard()
bits = board.GetVisibleElements()
bits &= ~(1 << pcbnew.RATSNEST_VISIBLE) # 未接続のライン表示をオフ
bits |= (1 << pcbnew.GRID_VISIBLE) # グリッドを表示
board.SetVisibleElements(bits)

網掛けの種類

ハッチの種類は CPolyLine クラスに定義されている列挙型です。

import pcbnew
pcbnew.CPolyLine.DIAGONAL_EDGE

# polygon/PolyLine.h
# enum HATCH_STYLE { NO_HATCH, DIAGONAL_FULL, DIAGONAL_EDGE

モジュールの取得

ボード上のモジュールは GetModules() メソッドでイテレータとして取得できます。

import pcbnew
for module in pcbnew.GetBoard().GetModules():
    module

特定のリファレンスを持つモジュールを取得するには FindFootprintByReference() メソッドを使用できます。

module = pcbnew.GetBoard().FindFootprintByReference("SW1")
if module:
    module.SetPosition(pcbnew.wxPointMM(100, 100))

以前は FindFootprintByReference メソッドは FindModuleByReference という名前でした。

現在のPcbnew画面に他の板のデータを読み込み

#import pcbnew
board = pcbnew.GetBoard()
path = "/mnt/hd/docs/kicad/Navi3_Main/data.kicad_pcb"
pcbnew.IO_MGR.Load(pcbnew.IO_MGR.KICAD, path, board)
#board.Save(path)

同じ場所に何かが配置されていると重なってしまいます。色々なボードをパネライズするのに使用できます。

この方法で読み込んでいくとファイル内部の Custom Track Widths のリストが重複したもので増大します。GUI から編集すると重複は取り除かれますが、コードで圧縮するには以下のようにします。

#import pcbnew
board = pcbnew.GetBoard()
tws = board.GetTrackWidthList()
ts = list(set(tws))
tws.resize(len(ts))
for n, t in enumerate(ts.sorted()):
    tws[n] = t

set を使用した方法では順番が保持されないためソートしています。元の順序を保つには工夫が必要です。

正規表現に一致する参照シルクの表示状態を変更

#import pcbnew
def set_visible(pattern, visibility=True):
    import re
    exp = re.compile(pattern)
    for module in pcbnew.GetBoard().GetModules():
        if exp.match(module.GetReference()):
            module.Reference().SetVisible(visibility)

SR1, SR2, ... SR10 の参照を非表示にするには、set_visible("SR\d*", False) などとします。

module.GetReference() はリファレンスを文字列で返します。一方、module.Reference() はリファレンステキストオブジェクトを返します。

正規表現に一致したモジュールのみを返すジェネレータを用意しておくと便利です。

def enumerate_matched_modules(board, exp):
    if isinstance(exp, str):
        import re
        exp = re.compile()
    for module in board.GetModules():
        if exp.match(module.GetReference()):
            yield module

これを使うと前出の set_visible 関数は以下のように書けます。

def set_visible(pattern, visibility=True):
    for module in enumerate_matched_modules(pcbnew.GetBoard(), pattern):
        module.Reference().SetVisible(visibility)

あまり複雑でない用途のために以下のような関数を作成しました。

import re

def apply_matched_modules(board, exp, func):
    exp = re.compile()
    for module in board.GetModules():
        if exp.match(module.GetReference()):
            func(module)

パターンにマッチするリファレンスのモジュールを引数として渡した関数 func を呼び出します。

func = lambda module: module.Reference().SetVisible(False)
apply_matched_modules(pcbnew.GetBoard(), "SR\d*", func)

レイヤーを変更

以下のコードでは指定したレイヤーの図形描写 (外形線など) を別のレイヤーに移動します。

import pcbnew

def iter_drawings_from_layer(board, layer, classes):
    for d in board.GetDrawings():
        if d.GetLayer() == layer and isinstance(d, classes):
            yield d


def move_drawings_to_layer(board, source_layer, dest_layer):
    for d in iter_drawings_from_layer(board, source_layer, pcbnew.DRAWSEGMENT):
        d.SetLayer(dest_layer)


board = pcbnew.GetBoard()
move_drawings_to_layer(board, pcbnew.Edge_Cuts, pcbnew.Eco2_User)
#move_drawings_to_layer(board, pcbnew.Eco2_User, pcbnew.Edge_Cuts)

ここでは、外形線を Edge.Cuts レイヤーから Eco2.User レイヤーに移動しています。

Dimensions - Text and Drawings

Dimensions - Text and Drawings の値の取得は以下のようにします。

import wx
board = pcbnew.GetBoard()
settings = board.GetDesignSettings()
wx.MessageBox(message=str(settings.m_ModuleTextWidth))

他の値は dir(settings) として一覧を参照してください。

フットプリントの読み込み

pcbnew モジュールには FootprintLoad 関数が用意されており、フットプリントをファイルから読み込めるように見えます。しかし、この関数は内部でレガシーフットプリント専用になっており、新しい形式のフットプリントファイル *.pretty/*.kicad_mod には使用できません。
新しい形式のフットプリントは以下のようにして自分で読み込みます。

def load_module(board, path, name):
    plug = pcbnew.IO_MGR.PluginFind(pcbnew.IO_MGR.KICAD) # pretty/kicad_mod 用
    module = plug.FootprintLoad(path, name)
    pcbnew.IO_MGR.PluginRelease(plug)
    if board:
        board.Add(module)
    return module

board = pcbnew.GetBoard()
path = "/home/hoge/kicad/mods/holes.pretty" # ライブラリパス
name = "M2.3" # 拡張子 .kicad_mod なしで
module = load_module(board, path, name)
module.SetPosition(pcbnew.wxRectMM(100, 100))

ボードの回転

ボードの Rotate メソッドは実装されていないため、呼び出すとエラーになります。自分でボード上のコンポーネントを回転させなければいけません。

def rotate(_board, _center, _angle):
    _angle = _angle * 10
    def _rotate(name):
        for i in getattr(_board, name)():
            i.Rotate(_center, _angle)
    map(_rotate, ("GetDrawings", "GetModules", "GetTracks"))
    for i in range(_board.GetAreaCount()):
        _board.GetArea(i).Rotate(_center, _angle)

board = pcbnew.GetBoard()
center = pcbnew.wxPointMM(100, 100)
angle = 30
rotate(board, center, angle)

回転中心を指定しなければいけません。角度は時計回りで指定します。負の値を渡すと半時計回りに回転します。

GUI

マクロ用の GUIwxPython を使うと簡単に作成できます。xwFormubuilder 3.4 以降ではウィンドウを作成する Python のコードを出力してくれるので、それを利用できます。
ダイアログなどを表示する際に wx.App の MainLoop を呼び出すと KiCAD が終了できなくなるので注意が必要です。

メッセージダイアログは以下のようにして表示できます。

import wx
wx.MessageBox("Message", "Caption")

wx.DirSelector や wx.FileSelector なども便利です。

wxPython で作成したダイアログを表示したまま KiCAD を終了してもダイアログは表示されたまま残ります。KiCAD や Pcbnew の終了を知るためのコールバックなどは無いようです(BZR 5980)。

ボードオブジェクトに弱参照を設定してオブジェクトの破棄でコールバックを受けるようにしてみました。しかし、内部でボードオブジェクトが頻繁に作り替えられているため、Pcbnew のウィンドウが表示されてからすぐにボードオブジェクトは破棄されてしまいます。

weakref で無理やり監視しようとすると以下のようになりましたが、いまいちです。

import weakref
import threading

class MacroWindowKiller:
    
    def __init__(self, dialog):
        self.dialog = dialog
        self.ref = None
        self.thread = threading.Timer(3.0, self.set_ref)
        self.thread.start()
    
    def set_ref(self):
        import pcbnew
        board = pcbnew.GetBoard()
        if board and not board.IsEmpty():
            self.ref = weakref.ref(pcbnew.GetBoard(), self.closed)
        else:
            self.close()
    
    def closed(self, ref):
        self.thread.cancel()
        self.thread = threading.Timer(2.0, self.set_ref)
        self.thread.start()
    
    def close(self):
        self.ref = None
        self.thread.cancel()
        self.dialog.OnClose(None)
        self.dialog = None