ミルク色の記録

やったこと、やってみたこと

Webプログラマが業務でIoTデバイス開発をして思ったこと

自分は本来Webプログラマのつもりなのだけれど、会社のやんごとなき事情により、今年はRaspberry PIやESP32を使ったいわゆるIoTデバイスアプリの開発をすることが多かった(あと、Kotlin使ってAndroidモバイルアプリ開発もやった)。

もともと、時々調査手伝ったり、プロトタイプ的なデバイスアプリ作ったりはしていたので、全く未経験ってわけではなかったけれど、自分一人で全部やって納品まで持っていったのは初めてだった。

少ないハードウェアリソースの中で専門的にやっている人たちに比べれば、自分のしたことはママゴトみたいなものなのだろうけど、開発を通して知ったこと・気づいたことがあったので記録として残しておく。

知ったこと・気づいたこと

ボード上で直接コードを変更しない

Raspberry PIやBeagle BoneでPython使ってアプリ作る時の話。ボードのOSがLinuxなので書こうと思えばSSHで入って直接書くことはできる。特にGPIOとかSPIとかI2Cみたいなボードの入出力使う必要がある場合、PCだとできない動作確認をする都合もあって、ついついソースコードを直接編集したくなる。

でも、ボードの方で編集しているとPCの方に持ってくるのを忘れてたり、うっかりボードのOS固有のファイルパスとかをハードコーディングしてしまって移植性が落ちるみたいなことになるので、「PCでコード書いて、ボードに転送して動作確認」を徹底したほうが良い。

自分はlsyncdを非デーモンモードで動かしてファイル変更したら自動転送して作っていた。

PythonPyPIモジュールはAPTで入れない

GPIO触るやつとか、OpenCVまわりとか、APTで直接OSに入れられるものもあるけど、requirements.txtに載っていない依存になったりして後々混乱するはめになるので、pipでインストールするほうが良い。最近はpiwheelsでも配布されていたりして、意外と大丈夫だったりする。

requirements.txtに載ってないけど、コード上はimportしているみたいなのはOSへのインストール忘れてたりして危なっかしくなる。

バイスOSの環境セットアップはスクリプトにしておく

Python3のvenvであったりPyPIモジュールが依存するOSの共有ライブラリなどのインストール、ちょっとした環境設定などはシェルスクリプトでセットアップスクリプトにしておくと良い。

ただし、どんなケースでも動くセットアップスクリプトを目指すと大変になるので、実行できる設定ドキュメントぐらいのノリで書いておくと良い。

shやbashで書くのが辛くなったら、いっそのことpython3で書いてしまえばいい。標準ライブラリの範囲内であればOSのプリインストール範囲内で結構できる。

国際化設定はちゃんと設定しておく

言語設定、タイムゾーン設定、キーボードレイアウト設定はデフォルトではen_GB環境になっているので、ちゃんと設定変えておいたほうが良い(en_GBな環境が対象ならばそのままでいいけど)。Wifi使うならWifiの地域設定も同様。後からキーボード繋いでちょっと調査...みたいなときに日本語キーボード繋いだらレイアウトあってなくて大混乱とかマジ大変。

いちいちraspi-configで設定するのが面倒ならば一通り設定するスクリプトを作っておけばいい。

raspi-configのコード見ればどのファイル編集すればいいか・コマンド叩けばいいかは判断できる。

OS(Raspbian)のリリース情報はこまめにチェックする

Raspbian、結構な頻度でバージョンアップするので公式のリリースはチェックして、チェンジログとか見ておいたほうが良い。rootじゃなくても実行できる権限増えてたり、ハードウェア乱数生成ツールがプリインストール対象にされていたりするし、OverlayFS利用したファイルシステムの読み込み専用化みたいな「コレもう標準でできるんじゃん」が結構出てきたりする。

ボード自体のアップデート(Raspberry PI 4のリリースとか)があると、旧バージョンのディストリだともう動かないとか出てくるので、最新リリースのディストリ(現在はBuster)と一つ前の(現在はStretch)は抑えて置くくらいの感覚でいたほうが良い。

ただし、ディストリの新バージョンが出た直後だとまだちょっと安定してない...?みたいな動作をする時があるので、先は見すえつつ採用は慎重にみたいな感じ。

ネットで見つかる環境設定方法などは鮮度に注意する

バイス開発に限った話じゃないけれど。

たとえばRTCの設定方法とか、「それもうdtoverlayでできるよ」みたいなのが見つかったり、自分でコマンド実装して叩いているやつがサービスとしてすでに入っていたりするので、ネットで見つけた情報は参考にしつつも公式の情報にないか調べてみると良い。

雑な見分け方として/etc/rc.localに実行コマンド足して何かしている系は、より良いやり方見つけられることが多い。

udevやsystemdなど活用してOSでできることはOSでやる

USBカメラをつないだら/dev/cameraシンボリックリンクができるようにudevルールを設定しておいて、/dev/video[0-9]+を直接探さなくても良いようにするとか、アプリケーションが動く前に確実にNTP同期が取れているようにsystemdの依存関係をうまく組み合わせて動かすとか。

アプリケーションに何でも仕事を持たせすぎずにOSでできることはOSに任せたほうが良い。

時計はマジで重要

HTTPSの接続とかWifiの証明書つかった認証とか、システムクロックがあっていないと途端にうまくいかなくなることがたくさんあるので、時刻があっていることを重視したほうが良い。

NTP頼みならば同期が確実に取れるまで待機させるとか、可能であればRTCの搭載を積極的に行うとか。とにかく時計は大事。

工夫すればテストは意外と書ける

以前はデバイスアプリの開発はどうしてもハードウェアへの依存が多くなるので、単体テストとか書きにくそうだなーと思っていた。 ただ、Webアプリケーションを使う場合でも、テストしにくいもの(データベースとか、ネットワーク越しの外部APIとか)を使うことになるモジュールをテストしやすくする方法はあるので、結局同じことかなーと思って工夫してみた。

UARTを使うpyserialや、SPI用のspidev、I2C用のsmbus-cffiとか、ビジネスロジックの中でなるべく直接使わずに、薄くラップしてインターフェイスを意識して利用することで分離し、ビジネスロジックをハードウェアの異存なしにテストできるようにした。

簡単な例で書くと説明しにくいのだけれど、例えば次のようにAppクラスのコンストラクタでシリアルコネクションを生成するとテストし難いコードになる。

import serial

class App(object):

    def __init__(self, serial_port):
        self._serial_conn = serial.Serial(serial_port, 115200, timeout=1)
        # その他初期化処理...

    def run(self):
        b = self._serial_conn.read(self._serial_conn.in_waiting or 1)
        if b:
            # 読み取りバッファを使った何らかの処理...

そこで、コンストラクタではシリアルコネクションを受け取って使うようにして、依存を外に追い出す。これだけでシリアルコネクションをモックに差し替えてテスト可能になる(実際はpyserialそのまま使わずに、もっと抽象化したものを使う)。

import serial

class App(object):

    def __init__(self, serial_conn):
        self._serial_conn = serial_conn
        # その他の初期化処理...

                                                                                                                                                                    
    def run(self):
        b = self._serial_conn.read(self._serial_conn.in_waiting or 1)
        if b:
            # 読み取りバッファを使った何らかの処理...

この辺りはRobert C.Martin先生のClean CodeとかClean Architectureとか読んだことが活かせた気がする(そういえばハードウェアを抽象化した話もあったような気がする)。

Pythonの場合だけではなくて、ESP32用にC++で書いていた時もAuniterでテストするにあたって同じようにやれた。

SDカードからイメージを作るときはサイズに注意

同じ8GBや16GBのSDカードだからといって、実際の細かいサイズまで同じとは限らない(メーカーの違いとかで変わる)。 8GBのSDカードに焼いたOSをddコマンドで取り出して、別のSDカードに焼こうとして容量不足で書けないとかザラにある。

SDカードの容量いっぱいまで使わないサイズにパーティションを意識して作成して、ddで取り出す時もcountオプションでパーティションの終わりまで切って取り出したほうが良い。

モバイルバッテリーで動かすときは消費電力に注意が必要

電源取れない場所で使うデバイスでモバイルバッテリー使うことになってわかったけど、一般のモバイルバッテリーにはオートパワーオフ機能がついていて、一定以上の電力消費が無いと供給をやめてしまう機能が付いている。

で、困ったことにこの基準となる消費電力のスペックがあまり公開されていない(Ankerの場合、日本法人のWebサイトにはなく、本家のWebサイトに載っていた)ので、満たせるかどうか綱渡り感がある。

とくにESP32は消費電力を抑えようとしてディープスリープ入れたりすると供給止められちゃうので要注意。

ESP32のディープスリープ時の時計は当てにならない

ESP32のディープスリープで起床時間を指定して眠りに入る機能があるけど、ディープスリープ時の時計が結構ずれやすく、長く寝させれば寝させるほど予定の時間に起きなかったりする。

しょうが無いのでRTCのDS3231を積んで、DS3231に付いているアラーム機能でWake upの信号を入れるようにして正しく起床させた。やはり時計は重要。

Arduinoの開発するならVSCodeがおすすめ

ESP32の開発はArduino IDEでできるんだけど、VS CodeArduino用拡張当てて使うほうが断然開発効率が良い。 シリアルでの入力だけはArduino IDEのほうがよい。

Arch Wikiは役にたつ

何かと行き着くArch Wiki。困った時に読むと情報載ってたりして役に立つ。 Raspberry PI用のページもあって非常に良い。

まとめ

他にもあった気がするけど、ざっとこんな感じ。

本業の方のWebプログラミングはさっぱり進歩がなくて、会社の他のメンバーのほうがやってたんじゃないかなーくらいな状態で、正直なところ焦ってる。将来を考えた上で非常にまずい感じなので、今後は「デバイス開発も素人程度にはできるWebプログラマ」になれるように努めていきたい。