KiCad用のPythonスクリプト ～ ほぼ回路図の配置通りにフットプリントを予備配置する

# Pythonの勉強を兼ねてKiCad用のスクリプトを作成してみる

KiCadの4.0版以降では、PcbnewがPythonスクリプトに対応し、基板から情報を取得したり、属性の一括変更や作業の自動化が出来るようになったので、Pythonの勉強を兼ねて、このスクリプトを作成しました。回路図を描く時に、せっかく部品配置を考慮した配置をしても、基板CADにネットを取り込んでみると、そんな情報が消し飛んでしまうのは勿体ない、と言うわけです。

商用のCADには部品配置だけでなく、配線順序や一点アースのポイントなど反映できるものも有りますが、概略配置が反映されるだけでも随分と楽になります。

　[スクリプトの実行動画(Youtube)](https://www.youtube.com/watch?v=3axJR0ErkFw)

## 今回参考にさせて頂いたサイト、参考資料

 [KiCadのPythonスクリプトリファレンス](http://ci.kicad-pcb.org/job/kicad-doxygen/ws/build/pcbnew/doxygen-python/html/index.html)

 [hanyaさんのブログ](http://d.hatena.ne.jp/hanya_orz/20140507/p1)

 [東京工業大学 ロボット技術研究会](http://titech-ssr.blog.jp/archives/1047505890.html)

※2016-01-24 追記
Eeschema ファイルフォーマット日本語版　http://wiki.kicad.jp/Schematic_file_format_JA

こちらは、以前[kicad.jp](http://kicad.jp)の有志の皆さんが翻訳されたEeschemaのファイルフォーマットのドキュメントですが、4.0.1版でもBZR4022版と回路図ファイルの構造は同じです。

KiCadの各種フォーマットについて、以前のバージョンではインストールディレクトリ内にドキュメントがありましたが、v4.0以降では見当たらず、launchpad上にpdfが公開されています。
http://bazaar.launchpad.net/~stambaughw/kicad/doc-read-only/download/head:/1115%4016bec504-3128-0410-b3e8-8e38c2123bca:trunk%252Fkicad-doc%252Fdoc%252Fhelp%252Ffile_formats%252Ffile_formats.pdf/file_formats.pdf

## スクリプトの説明
スクリプトは以下の通りです。基本的な流れは

1. 基板情報を取得
2. 回路図ファイルを読み込む
3. "$Comp" セクション（個々の回路図シンボルの情報）を抽出、リストに格納
4. GNDなど電源シンボルを除外
5. 1番目のユニットを抽出
6. リファレンスと座標を抽出
7. リファレンスを基に部品配置（絶対座標値の変更）

です。

※2016-03-21 追記 スケーリングと原点指定に対応しました。
※2016-01-24 追記 Githubにもソースを上げてみました。
https://github.com/silvermoon-hiro/kicad_python


```py:ki_pre_place.py
# coding: utf-8
# KiCad footprint pre place tool (2016-01-30)
# 
# This script places footprints based on the XY coordinate
# of Eeschema's schematic components.


# import sys
import codecs
import pcbnew

def input_num():
    while True:
        try:
            v = float(sys.stdin.readline())
            return v
        except ValueError:
            print 'Value Error!'

comp_flg    = False    # For Detect $Comp section
qty         = -1       # Comp counter
zuwaku      = []       # Sch sheet size
sch_comp    = []       # All Comp info (expect PWR-Symbol)
sch_comp_xy = []       # Comp Location List
pcb_comp_xy = []       # pcb location

scl_size  = float(0.4)  # Scaling for pcb
offset_x  = float(2000) # PCB Place offset X(mils)
offset_y  = float(2000) #                  Y(mils)

pcb      = pcbnew.GetBoard()        # Get pcb all info
pcb_name = pcb.GetFileName()        # current pcb file name
sch_name = pcb_name.replace('.kicad_pcb','.sch') # Generate Sch file name

org_aux  = pcb.GetAuxOrigin()  # fab. origin(gerber etc.) and conv string (non used)
org_grid = pcb.GetGridOrigin() # grid origin and conv string (non used)

#Start Sch operation
f   = codecs.open(sch_name,'r','utf-8') # Sch file reading (utf-8)
sch = f.readlines()
f.close()

for line in sch: # reading each sch lines...

    line = line.lstrip().rstrip()  # Del \t and \n
        
    if line.startswith('$Descr') : # Detect $Descr section...
        
        zuwaku    = line.replace('$Descr ','').split(' ')
        zuwaku[1] = float(zuwaku[1]) # X size
        zuwaku[2] = float(zuwaku[2]) # Y size
                
    if line.startswith('$Comp') : # Detect $Comp section...
        comp_flg = True           # flg on
        qty += 1                  # Component count
        sch_comp.append([])       # ex) sch_comp[0] = []  , sch_comp[1] = [] ....

    elif line.startswith('$EndComp') : # $EndComp -> flg off
        comp_flg = False

    if comp_flg :   # from $Comp to $EndComp
        
        if not line.startswith('$Comp') :  # except 1st line($Comp)
            sch_comp[qty].append(line)     # add comp all info
                        
            # ex) sch_comp[0][0] <- 'L 74AC04 U2'
            #             [0][1] <- 'U 1 1 512E0139'
            #             [0][2] <- 'P 4050 6950'
            #     ....



# Get Ref.No and XY
for c_line in sch_comp:

    if  '#' not in c_line[0] :          # except PWR SYMBOL (GND,Vcc,PWR_FLG etc.)
        ref      = c_line[0].split(' ')
        unit_num = c_line[1].split(' ')
        xy       = c_line[2].split(' ')
        angle    = c_line[-1]           # orientation matrix (angle and mirror)
        
        if unit_num[1] == '1': # detect 1st unit ( for OPAMP , logic IC etc.)
            
            xy_raw = [ref[2],float(xy[1]),float(xy[2]),angle] # make Ref,x,y list
            sch_comp_xy.append(xy_raw)
            
            # ex) sch_comp_xy[0][0] <- R1
            #                [0][1] <- X(mils)
            #                [0][2] <- Y(mils)
            #                [0][3] <- orientation matrix
            #
            #                [1][0] <- C1
            #                        .....

# End Sch operation

# print sch_comp     <- all comp list
# print sch_comp_xy  <- comp xy list
# print zuwaku       <- Sch paper size

# Start PCB operation

place_flg = 'n'
judge = 'n'

while place_flg == 'n' :

    if judge != 'n' :
        pass
        
    else :
        print 'scale = ' + str(scl_size)
        print 'offset x,y(mil) = ' + '(' +str(offset_x) + ',' + str(offset_y) + ')'
        
        for posi in sch_comp_xy:
        
            pcb_x = int(posi[1] * scl_size) # convert xy Sch --> PCB (scaling)
            pcb_y = int(posi[2] * scl_size)
        
            module = pcb.FindModuleByReference(posi[0])           # Get Footprint info
            module.SetPosition(pcbnew.wxPointMils(pcb_x , pcb_y)) # Move abs x,y(mils)
            module.Move(pcbnew.wxPointMils(offset_x , offset_y))  # Move inc(offset from origin, mils)
        
        print '''
    Place Finished , Press[F3] Key(Redraw).
'''
        
        
        
    print 'Is this floor plan all right(y/n,a:abort)?' # Layout check...
    
    judge = sys.stdin.readline()
    
    if judge == 'y':          # Layout OK
        print 'Place Completed.'
        place_flg = 'y'
        
    elif judge == 'n':        # Change place scale and origin
        print 'place scale = '
        scl_size  = input_num()
        print 'offset x(mil) = '
        offset_x  = input_num()
        print 'offset y(mil) = '
        offset_y  = input_num()
    
    elif judge == 'a':        # abort
        print 'Aborted.'
        place_flg = 'a'
    
    else :
        pass
```

### 回路図シンボル（コンポーネント）の座標抽出
Eeschemaが吐き出す中間ネットリストが使えたらラッキー！と思いましたが、当然というか残念というか、シンボルの座標情報はありません。そこで、今開いている基板ファイルの名前から回路図ファイル名を作成して、回路図ファイルを頭から読み込んで必要な部分を抽出しています。当方の環境はWin7の64bitですが、EeschemaのファイルはUTF-8でしたので、一応文字コード指定しています。

\#で始まる部品名は、GNDシンボルやPWR_FLGなので、除外します。また、オペアンプやロジックICなど、1パッケージに複数のユニットがある場合には、1番目のユニットだけを抽出しています。

未使用ですが、一応、各シンボルの配置角度とミラー情報を"angle"（orientation matrix）に格納しています。角度は後で回転させることが多いと思いますし、マトリックスの判定が結構面倒だったので配列のまま格納しています。
また、回路図のシートサイズも格納しています。

### 基板内への配置
リファレンスと座標を抽出したら、取得した基板情報を使いfor文を座標値リストで回して基板に絶対座標で配置（移動）します。A4で原寸配置だと広がりすぎるので、座標値にスケールをかけてから配置しています。

PCBNewから基板サイズを取得して自動的にスケーリングする事も考えましたが、現状はスクリプトで概略基板サイズを取得出来ないようなので、スケール値を入力する方法にしています。Eeschemaのグリッドがデフォルトでは50milなので、スケール値0.25～0.5くらいがちょうど良いようです。

またスクリプトを修正して配置原点を選択できるようにグリッド原点と実装原点を抽出しています。~~（22、23行目でコメントアウトしています）~~

### 使い方

スクリプトを実行する前に、

1. ネットリストの作成
2. フットプリントの作成と割り付け
3. ネットリストの基板への反映
4. フットプリントの配置（フットプリントモードでグローバル移動/配置→全てのフットプリントを展開

をして、下図の状態にします。

![kicad_pre_place_1.png](https://qiita-image-store.s3.amazonaws.com/0/104354/cb36ba22-4fa2-1b0b-343a-9673a50b1d7a.png)


適当な場所にスクリプトを置いて、Pcbnewのメニューバーから、Pythonスクリプトのウィンドウから

execfile(’絶対パス￥ki_pre_place.py’)

として実行し、配置原点、スケールを入力し、F3キーで再表示すると下のように配置されます。

![kicad_pre_place_2.png](https://qiita-image-store.s3.amazonaws.com/0/104354/0ee24dfb-8aeb-6acf-0ec4-edf0b625e328.png)


## 注意点

- 当方の環境だけかもしれませんが、**上記4の手順を飛ばすと配置してくれません。**スクリプト実行前にフットプリントを基板上に置いておく必要があるようです。
- **配置後の再表示をしてくれない**ので、手動でお願いします。スクリプトでの再表示が可能かどうか調査中です。
- **回路図の階層ファイルには対応していません。**
- 実行後のUndoはできますが、上記4の状態で一度ファイル保存し、再度開いて直後に実行した場合、Undoできないようです。Pcbnew側の問題かもしれませんが不明です。実害は少ないと思いますがご注意ください。
- Win7上で動作確認しています。
- エラー処理は特にしていないのでご注意下さい。


## 今後の改善予定

- ~~配置原点やスケールなどを初期値として与えていますが、フットプリント配置後にスクリプトで再表示する手段があれば、結果を見てからスケールや配置原点を変えて再実行するような、対話入力に変更したいです。~~

- 階層ファイルへの対応も、$Schセクションに階層ファイル名と座標がありますので、何とかなりそうですが、上記の実現が優先でしょうか。。。
- 不具合などありましたらご指摘ください。

2016-03-21 追記
スケーリングと原点指定、そしてリトライに対応しましたが、やはり便利です。

## スクリプトを作成した感想
Pythonの勉強を兼ねてやってみましたが、欲しかった機能を実現できるのは感動です。デベロッパーの方々、スクリプトの情報提供をして下さった方々に感謝です。

ベタ書きのスクリプトですが、回路図情報を抽出する部分は使い回しできるようにしたいと思っています。"$Comp"セクション内の情報を一旦全てリストに格納しているので、中間ネットリストからBomを作成するより、リストから引っ張り出す方が簡単かもしれません。Eeschemaがスクリプト対応するまでの間とは思いますが。。

余談ですが、同じくOSSの3D CAD「FreeCAD」もPythonスクリプト機能が実装されているようです。ライブラリーや基板外形を相互に連携させたり、干渉チェックなど、便利に使えそうです。