見出し画像

KiCADでフレキシブル基板用の螺旋状Neopixelをpythonで配置する方法


はじめに

備忘録も兼ねてメモ.

球体デバイス

以前から球体デバイスを作成しています.

いろいろ球体デバイスを作成しているのですが,最近はIsolation Sphereというガジェットを作成しています.

このような球体LEDを作成する場合,どのようにLEDを配置するかは非常に重要なテーマです.

LEDの配置問題

上のIsolation Sphereの場合には球面上に均等にLEDが配置されるようなシミュレーションを行い均等配置したものを正12面体上にプロットしています.そのため作成したLED用基板にはほぼランダムにLEDが配置されています.

正五角形Neopixel基板

この基板にWS2812-2020を実装して3Dプリンタで印刷した外殻に接続することで球体ディスプレイにすることができます.

これを12枚組み合わせることで球体を構成することができます.

NeoPixelとはシリアルにLEDを接続して各LEDのRGB値をそれぞれ制御できるLEDであるため,基板上で全て一筆書きで配線する必要があります.

平面基板の場合には,配線の自由度が高いのでこのような配置をしても一筆書きで繋げることは問題ありません.写真からも配線できていることがわかると思います.
しかし,この正12面体を用いる方法には二つの欠点があります

正12面体基板の欠点

球体の表面にLEDを配置したい要望に対して平面の基板を用いることの問題点には次の二つがあります.

  • 正12面体は球より体積が小さいので内部に配置できる容量が小さい

  • 球面に対して平面の五角形を配置するので,五角形の辺に向かうほど歪みが大きくなる

1つめについては,大量のLED(600個)を配置するため,電源の確保が重要な課題となり,大きい電池を収納したくなります(外殻の外側は全てLEDなのですべて内包したい)
しかし,正12面体では球の体積の約66%しかないのです.なるべく大きな電池を配置したい.

2つめは,平面の5角形基板から平面の球体の表面までは3Dプリンタで基板の端と中央で距離が異なることです.

CAD画面
基板と外殻
基板を嵌め込んだ状態

上の動画を見るとわかりますが,真上から見ている時は問題ないのですが,五角形を傾けて斜めから見てみると中央付近の色が見えづらくなっています.(外殻が白いので反射して見えてはいますがLEDからの直接光が観察できなくなっています)
この結果,外殻を組み合わせて球体を構成しても五角形の中央付近と周辺付近でLEDの見え方が異なるので,見え方に偏りが発生してしまいます.
さらに,基板平面からLEDが放射する光の方向はすべて基板平面に垂直な面になることから,同様に外殻の角度によって見え方が異なってしまいます.
これはこれで問題ないと考えて球体の開発を進めていますが,同時に上の課題を解決した方式も検討しようと思っています.

フレキシブル基板の設計

そこで外殻の内側も球面にした均等型外殻を設計し,フレキシブル基板を使って曲面に沿わせるように配置することで,LEDから導光する穴が球表面に直交し,各LEDと外殻表面までの距離も均等になるため,組み立てた後に球体を回転させても継ぎ目を判別することはできないでしょう.

均等型外殻

そこで,このような局面に配置するためのフレキシブル基板を設計します.しかし,上でも述べたようにneopixelはシリアル接続のLEDのために一筆書きで配線しないといけません.しかし均等な螺旋状配置を手で行うことはできません(というか無限にめんどくさい)
そこで螺旋状にLEDを配置したフレキシブル基板を設計するために,KiCADでpythonを用いてプログラム的に配置したことに対するメモとなります.

LEDの配置方法検討

そもそも螺旋状に物体を配置するにはどうしたらよいでしょうか?

アルキメデス螺旋

今回の目的に適合するのは「アルキメデス螺旋」になります.
アルキメデス螺旋とは次のような式で表されるようです.

  $${X(θ)=aθcos(θ)}$$
  $${Y(θ)=aθsin(θ)  ただし a>0}$$
この螺旋をもとに均等に点を打つプログラムを作成します.

spiral.py

上記のリンクを参考に螺旋を描くプログラムをまず書きます.
この方法では$${θ}$$を少しづつ変化させながら$${X(θ), Y(θ)}$$をプロットしていく方法を採用しました.
アルキメデス螺旋の座標値を返す関数 ArchimedeanSpiral は次のようになります.$${θ}$$を与えるとその時の座標を返します.

def ArchimedeanSpiral(theta):
    r = a * theta
    x = r * math.cos(theta)
    y = r * math.sin(theta)
    return (x, y)

プログラムは以下のようになります.

import plotly.figure_factory as ff
import plotly.graph_objects as go
import numpy as np
import pandas as pd
import json
import math

// θを打つ点の数:ここは調整
num_points = 2400
// 螺旋度?
a = 4.5
// θのステップ
STEP = 2 * math.pi * 0.004

// 空のdataframeを定義
df = pd.DataFrame(columns=['x', 'y', 'theta', 'distance'])

theta = 0.0
for n in range(num_points):
    pos = ArchimedeanSpiral(theta)
    print(n, theta, pos)

    df = pd.concat([df, pd.DataFrame([{'x': pos[0], 'y': pos[1], 'theta': theta}])], ignore_index=True)
    theta = theta + STEP

こうすることで,螺旋上の点がdfにnum_pointsの数だけ確保されます.
num_poinots, a, STEP を調整することで任意のアルキメデス螺旋を描くことができます.

螺旋の均等分割

次は螺旋上に均等に点を配置する処理です.最初は距離が均等になるθを求めようとしたのですが,めんどくさそうなので下に示すプログラムで全長としてlengthをdfの各点の距離の合計とし,各点の距離をdfに再格納します.

length = 0.0

for i, d1 in df.iterrows():
    if i == 0:
        print('zero')
        dist = 0.0
    else:
        d2 = df.iloc[[i-1],:]
        vec = d2-d1
        print('vec', vec)
        dist = math.sqrt(vec['x']*vec['x'] + vec['y']*vec['y'])
    df.at[i, 'distance'] = dist
    length = length + dist

例えば,この螺旋を150分割したい場合,pitch=length/150として計算しその点にneopixelを配置しposとします.
さらにLEDとLEDの中間にキャパシタも配置しましょう.capとします.

pos = pd.DataFrame(columns=['x', 'y', 'theta', 'deg', 'distance'])
cap = pd.DataFrame(columns=['x', 'y', 'theta', 'deg', 'distance'])

この二つの座標を次のようなプログラムで算出していきます.dfの点と点のdf.distanceを加算していき,pitchを超えたら次のLED 点としてposに追加していきます.同様にcapも追加します.

dist = 0.0
cap_dist = pitch/2.0

for i, d in df.iterrows():
    if i == 0:
        pass
    else:
        dist = dist + d['distance']
        cap_dist = cap_dist + d['distance']
        
        print(i,dist, pitch,  '-----\n', d)
        if dist > pitch:
            deg = 180.0 * d['theta'] / (2.0 * math.pi)
            # pos = pd.concat([pos, pd.DataFrame([{'x': d['x'], 'y': d['y'], 'theta': d['theta'], 'distance': dist}])], ignore_index=True)
            pos = pd.concat([pos, pd.DataFrame([{'x': d['x'], 'y': d['y'], 'theta': d['theta'], 'deg': deg, 'distance': dist}])], ignore_index=True)
            dist = 0.0
        if cap_dist > pitch:
            deg = 180.0 * d['theta'] / (2.0 * math.pi)
            # pos = pd.concat([pos, pd.DataFrame([{'x': d['x'], 'y': d['y'], 'theta': d['theta'], 'distance': dist}])], ignore_index=True)
            cap = pd.concat([cap, pd.DataFrame([{'x': d['x'], 'y': d['y'], 'theta': d['theta'], 'deg': deg, 'distance': cap_dist}])], ignore_index=True)
            cap_dist = 0.0

これらをcsvとして保存します.

print(pos.shape)
pos.to_csv('spiral.csv')
print(cap.shape)
cap.to_csv('spiral-capacitor.csv')

最後にグラフを書きましょう.
螺旋dfをsとして描きます.
ss0はLEDの位置を線で結びます.
ss1はLEDの位置にマーカーを置きます.
ss2はキャパシタの位置にマーカーを置きます.

s = go.Scatter(x=df['x'], y=df['y'])
ss0 = go.Scatter(x=pos['x'], y=pos['y'])
ss1 = go.Scatter(x=pos['x'], y=pos['y'], mode = 'markers')
ss2 = go.Scatter(x=cap['x'], y=cap['y'], mode = 'markers')

plot = [s, ss0, ss1, ss2]
fig = go.Figure(data=plot) 
fig.update_layout(
      width=1000,
      height=1000)
fig.show()
150点均等分布アルキメデス螺旋

最後に,最初の点を不採用にするなどcsvを直接調整してもよいと思います.

KiCADでの描画

最初は,自分で手で配置していました.

KiCADをpythonで操作する

きっかけはこのサイト

こんな方法あるんだ,とびっくりしました.複雑な形状はDXFをimportして使うものだと思っていたので,これができると分かった時には狂喜乱舞しました.
それまでは全部手作業かDXFだったので,位置の精度や配置の角度を計算するのが本当にめんどくさかったのです.
それがcsvに記録した値で配置できてしまうなんて!


実装

まず回路図エディタで必要なLEDとcapacitor を配置・配線

回路図エディタ

最初に,このツール(kicad_tools.py)を導入しておきます.https://gist.github.com/idt12312/a20f17893c3a1936b036

spiral.csv, spiral_capacitor.csv とkicad_tools.py を*.kicad_pcbと同じディレクトリにいれてpcbnewのコンソールからimport kicad_toolsと打つと使えます。(がちょっと作業が必要なので,下の方で書きます)
この状態で,次のようなソースコードを作成します.

import importlib
import pcbnew
from io import open
import csv
import math

import kicad_tools

(
    HORIZON_THEN_VERTICAL,
    VERTICAL_THEN_HORIZON 
) = range(0,2)

filename = "/path/to/csv/spiral.csv"

print('spiral')
with open(filename, "r", newline="") as f:
    board = pcbnew.GetBoard()
    csvreader = csv.reader(f)
    for i, row in enumerate(csvreader):
        if i > 9:
            x = float(row[1])*0.1
            y = float(row[2])*0.1

            ref = "D" + str(i-9)
            l = kicad_tools.findModulesByStrings([ref])
            radian = math.atan2(y, x)
            degree = -radian * (180 / math.pi) - 90.0

            print(i-9, ref, degree, row)
            l[0].SetOrientationDegrees(degree)

            point = pcbnew.wxPoint(x,y)
            l[0].SetPosition(pcbnew.VECTOR2I(pcbnew.wxPointMM(x, y)))

filenameでcsvを指定し,csvファイルを読み込みます.(最初の9個は使わないので読み飛ばし)
LEDである”D*"の付いたパーツを読み込み,SetOrientationDegreesで向きを,wxPointで位置をcsvから読み込んだ値で配置します.

このソースコードをpcbnewで反映させます.

このソースコードを実行する方法がこれが正しいかどうかわかりませんが,とりあえずできたので共有.

スクリプト実行

ツールから「スクリプトコントロール」を開きます.

スクリプトコントロール

するとKiPython が開きます.

KiPython

下のブログでファイルを直接読み込む方法を試してみます.

ここではファイルを読み込んで実行する方法として
execfileを使う方法が紹介されていますが,試してみるとうまくいかない.いろいろよく調べてみるとpythonが3.9.3になりexecfileが使えないことがわかりました.代替としてexec()があるのですが,うまくいかない...

ファイルを読み込む

ファイルを読み込んでみたのですが,実行することはできない...
そこで,直接実行する方法を選択.

import os
os.chdir("/path/to/kicad/files/")

を実行してカレントディレクトリを変更します.

そしてソースコードを全てコピーして,貼り付けても下のエラーが発生

  File "<input>", line 1
    import importlib
                    ^
SyntaxError: multiple statements found while compiling a single statement

結局うまくいかないので,
import部分はひとつづつ実行してimportします.

(
    HORIZON_THEN_VERTICAL,
    VERTICAL_THEN_HORIZON 
) = range(0,2)
filename = "/path/to/file/spiral.csv"

ここまで個別に実行した後,

with open(filename, "r", newline="") as f:
      .......

の文を実行することで目的を達成できました.
一文づつしか実行できないみたい...
他にいい方法を知っている方がいらっしゃれば教えてください.

配置完了&配線(途中)

実行結果がこちら↑
LEDのデザインに一工夫

フットプリントの編集で,Edge-Cutsの線を設置しておきます.

WS2812 footprint

このパーツをスクリプトで配置することによって,edge-cutsが部分的に設置されます.それを手作業で配線時に繋げることで完全に閉じた形状をつくれる,というわけ.

配置例:edge-cuts

作業で気をつけること

言い忘れていました.
上の工程を実際にやってみると,KiPython上でプログラムが完了しても,結果が反映されません.

一度保存して,pcbnew を閉じて開き直すとプログラムの結果が反映されます.先ほどひさしぶりに同じ作業をやり直して「結果が反映されない!!!」と一人大騒ぎしてしまいました.

600LEDの配置(角度は手修正中)

ファイルから実装できたり,もうちょっと正しい手順があるんだろうとは思いますが,引き続き調査していきます.

さいごに

と,ここまで書いておきながら,実は円形の螺旋ではなく正五角形の螺旋構造に変更しました.

正五角形螺旋基板

このような基板を作成し,配線.
JLCPCBに発注しました.

発注画面

届きました
境界線にはミシン目のようなものが入ってました.

届いた!

点灯試験! ぽち!

ということで,うまく動かすことができました.
これを外殻に貼り付けていく話はまた今度.

しかし,結局pcbnewでスクリプトをファイル単位で動かすことができませんでした.
どなたか詳しい方とかいらっしゃったら,アドバイスください.

あと,外殻は白か黒か,いまだに悩んでますw

この記事が気に入ったらサポートをしてみませんか?