IoTで家庭菜園。「ソロモン」を育てよう (2/3) – ソフトウェア編 –
2022年04月07日 木曜日
CONTENTS
ソロモンを育てよう、第 2 回ソフトウェア編です。
ソフトウェア編では、前回作成したハードウェアをただの箱から便利な箱へと進化させることにしましょう。
- 第1回「ハードウェア編」はこちら(ソロモンが何かも分かります)
ソフトウェアの設計
最近は Web で何でもできるようにすることが流行っているので、流行に乗って Web アプリケーションで作ることにします。
ソフトウェアの全体像はこんな感じです。
この構成は、Single Page Application (SPA) の典型的なパターンです。サーバのロジックは REST API サーバで行い、このサーバで各種周辺機器を制御できるようにします。そして利用者は、その API を呼び出すクライアントである Web UI を操作して使用します。この構成の特徴として、Web UI 以外のクライアントでも API も呼び出すことができるので、UI と API を合わせて設計でき API 開発の手間を減らせるメリットがあります。
ソフトウェアの実装は基本的に Node.js を使います。単に慣れているだけと言うのもありますが、JavaScript で書けるので Web UI とも共通にでき、必要最低限の学習コストで済むのが良いところです。
ほか工夫点として、裏方の gpiod を設けました。詳細は後ほど解説します。
カメラの制御は、ffmpeg や raspistill を使用することにします。Node.js の良い感じのライブラリが無かったので、ここは外部ツールのコマンド実行で妥協します。
大体の設計ができたのでサクッと実装していきます。
gpiod の作成
まずは、裏方のデーモン gpiod を作ります。gpiod は、名前の通り Raspberry Pi の GPIO 制御に使用するバックエンドです。実装は pigpio というライブラリを使用します。
あえて API サーバと分離しているのは、セキュリティのためです。具体的には、API サーバから root 権限を分けることが目的です。
もし仮に gpiod を作らなかった場合、API サーバで pigpio を使うため root 権限で実行されることになります。この状態では万が一 API サーバに攻撃された場合に、強い権限で被害が甚大になりやすい傾向にあります。この被害を低減するため、API サーバは一般ユーザで動作させ、別途 root 権限で動かす gpiod と API 通信で GPIO 制御を代行させます。この構成にすると API サーバから root 権限を分離でき、万が一侵入されても被害が少なく安全になります。
ちなみに、このシステムは次回に説明する IIJ IoT サービスの閉域ネットワークに接続されるため、基本的には外部からの侵入は考慮しなくてよいです。ただ安全に越したことはないので、利便性を損ねない範囲、かつ、少ない労力で実現できるので、実施しておいた方が損はありません。また、精神的にも安心できます。
gpiod で制御するのは、ポンプとライトのみです。他のセンサー類は、I2C/SPI で root 権限が無くとも通信できます。そのため、ポンプとライト以外のセンサーは gpiod に実装せず、API サーバで直接実装することにします。
API サーバと gpiod との API には、gRPC を使用します。gRPC にした理由は、IIJ IoT サービスで gRPC を使ったマイクロサービス化を進めているためです。ここでその事前学習をしておこうという戦略です。
ポンプの制御
ポンプの制御前回、ハードウェア編でポンプのパワーが強すぎて水が飛び散る問題が発生しました。その問題解決のため、パワー (回転数) をソフトウェアで制御します。使うのは PWM です。
PWM は、Pulse Width Modulation のことで、高速に ON/OFF をスイッチングする + ON 時間の割合 (Duty 比) を変えることで消費電力を調整する制御方式です。モーターのトルク減少も抑えられるので、ポンプの制御に最適です。もうこれは使う選択肢しかありません。
実装して色々試した結果、スイッチング周波数は、20 Hz で制御するのが最適でした。高すぎると Duty 比を変えてもあまりパワーが変わらなかったり、逆に低すぎると間欠動作になって制御できなくなりました。
最終的に、Duty 比を 80% 程度にしてほど良い感じになりました。
ライトの制御
せっかく RGB LED を搭載しているので、フルカラーで点灯させなければ損です。LED も PWM を使用してカラフルにしてやりましょう。
ただ、Raspberry Pi の PWM は 2ch しか対応していません。1 つはポンプで使用したので、残り 1 つです。 RGB 制御は 3 つ必要で足りません。
そこで、pigpio の ソフトウェア PWM 機能を使用します。ソフトウェア PWM は、ソフトウェアタイマーで ON/OFF を制御して擬似的に PWM 出力を得ます。ハードウェア PWM に比べて、CPU 負荷が高くなるデメリットがありますが、任意の GPIO ピンで使用できるメリットがあります。
実際にコーディングして、カラフルに点灯できるようになりました。写真はほぼ RGB ですけど、ちゃんとフルカラーで点灯できます。
あと、最近はゲーミング○○でカラフルにするのが流行っているらしいので、「ゲーミング家庭菜園モード」も実装しました。
色の滑らかな変化は、HSV 色空間を使用し、H (色相) を 0 から 360 まで変化させ、ぐるぐる回しています。LED の入力は RGB なので、最後に HSV から RGB へ変換すれば OK です。色空間の変換は、自前で変換式を書くか、Node.js だと color-convert というライブラリが使えます。
あと、LED は残光がないので単純に ON/OFF するとパッと切り替わってしまって安っぽくなります。高級感を与えるためにも、色がゆっくり変わるようにディマー(調光)機能を付けます。
ディマーはライトの明るさをゆっくり変化させていくだけです。変化のさせ方はいろいろな方法があるのですが、今回は PID 制御で明るさ値を変化させることにしました。
ライティングも良い感じに実装できました。
gRPC インタフェース設計
gRPC は名前の通り RPC なので、API 設計というよりもプログラム設計に近いです。クライアントから呼び出したい関数を定義するだけで作ることができます。
注意として、プログラム設計と違うところは、ステートレスである点だと思います。RPC で呼び出したときに、クライアントを識別する何かを用意しないと、他のクライアントのデータとごちゃ混ぜになってしまいます。要するにセッション管理を自前でやらないといけません。
今回の場合は、クライアントが 1 つしかないためセッション管理をする必要が無く簡単です。状態管理は、gpiod でグローバルに変数を持っておけばそれだけで OK です。
実際に作った関数の一部を書いておきます。
RPC 名
|
機能
|
引数
|
戻り値
|
---|---|---|---|
PumpController/On | ポンプを ON にする |
|
なし |
PumpController/GetStatus | ポンプの状態を取得する | なし |
|
LightController/Set | ライトを設定する |
|
なし |
REST API サーバの作成
カメラの排他制御
真っ先に考えないといけないことが一つあります。それはカメラの排他制御です。
今回、カメラを動画と静止画で分けて使おうと思いますが、この 2 つを同時には使用できません。どちらか一方を適切に使えるように制御、つまり排他制御を実装する必要があります。
単純にロックするよう排他制御しても良いのですが、より便利にするためにユースケースを考えます。
今回の利用用途は、静止画をタイムラプス動画の作成に使うことを想定しています。タイムラプス動画は、コマが欠けると再生スピードが変動して不自然になってしまいます。そのため、動画よりも静止画の方が優先度を高くする必要があります。
ここでは、動画使用中で静止画の撮影がリクエストされた場合に、処理に割り込んで静止画を撮影するということをやってみます。
このような設計は、状態遷移図を書くと頭の中を整理しやすいと思います。
青色の遷移が工夫点です。動画ストリーミング中に静止画の撮影をリクエストされた時に、ストリーミングを中断して静止画を撮影、その後再度ストリーミングを再開としています。
また今回は、使用中に別の動画・静止画リクエストされた場合、API でエラーを応答するようにします。エラーを応答することで、後から来たクライアントでもエラーを判定してリトライすれば使えます。
ちなみに他の対策としては、キューイングしたり、同じ撮影画像を返したりするなどが考えられます。また、動画と静止画を分離せず、動画使用中の静止画を動画のフレームから抜き出すといった方法も採れます。制御方式は色々あるので考えてみると結構楽しいですよ。
API 設計
REST API には一例として以下のようなメソッドを作りました。詳細は割愛します 😌
- GET /v1/sensors
・センサー値の取得
- PUT /v1/controls/pump
・ポンプの制御
・ { “power”: 100 } で ON (全力)、{ “power”: 0 } で OFF
・ { “power”: 100, “timeoutMs”: 10000 } で ON した後 10 秒後に OFF
- POST /v1/cameras/0/pictures
・静止画の撮影
・撮影すると GET /v1/cameras/0/pictures/[id] で JPG 画像を取得できるようになる
- POST /v1/cameras/0/streams
・ライブストリームの配信リクエスト
・リクエストすると HLS のストリームが 1 分間有効になる
・ /v1/cameras/0/streams/0 のリソースが作られ、1 分経つと消える
- PUT /v1/cameras/0/streams/0
・ライブストリームの時間延長
・空ボディでリクエストすると 1 分間タイムアウトが伸びる
- DELETE /v1/cameras/0/streams/0
・ライブストリームの終了
Web UI の作成
フレームワークは React と MUI を使用しました。
Web UI は、React で DOM を構築し、温度や湿度データなど必要になったタイミングで Fetch API を使用して REST API にデータを要求します。デザインは、最近 Material-UI から名前が変わった MUI を使用して Material Design を使います。こういったデザインフレームワークを使用すると HTML や CSS の詳細をある程度はカバーしてくれるのでありがたいです。それと MUI になってスタイルの書き方が変更になり、よりリーダブルなコードが書きやすくなった気がします 👍
一点、ライブ配信プロトコルの HLS に Chrome や Firefox が対応していないことに注意する必要があります。これは hls.js というライブラリを使用して対策します。hls.js は、JavaScript で HLS の m3u8 プレイリストをパースし、ストリーム分割された動画ファイルをブラウザ上で組み立てて再生してくれます。HLS に対応していないブラウザでも MediaStream API に対応しているブラウザであれば HLS を再生できるようになります。
画面は「見れれば良い!」と思って作りったので、見栄えはそんなにこだわってないです。そのうち本気出します。
ソフトウェアのデプロイ
実装が完了したらソフトウェアを Raspberry Pi にデプロイします。端的にはファイルをコピーして、サービスとして実行するだけです。
「IIJ IoT サービスを使用して楽したい!!」ところですが、このシステムの製作時点では IIJ IoT サービスにファイルコピーやデプロイを支援する機能がありませんでした。そのため、Raspberry Pi の SD カードに直接書き込むか、LAN を接続してファイルをコピーするなどの代替手段でデプロイする必要がありました。
この課題は、先日プレスリリースした「デバイスリンク」機能で解決できるようになりました。デバイスリンクは、インターネット経由でデバイスの TCP ポートに直接通信するプロキシ機能です。デバイスリンクを使用して Raspberry Pi に SSH 接続すれば、インターネット経由でファイルコピーができるようになり、このようなデプロイ作業も手軽に実施できるようになります。
次回、IoT サービス編
ハードウェア・ソフトウェアが共に準備できました。
次回は、IIJ IoT サービスを活用して、どこからでも使用できるようにしようと思います。