1.はじめに
Elixir言語で組み込み機器を開発するフレームワーク Nerves を使って、実際の開発をする中で、下記のような状況に遭遇しました。
- Nervesを実行する機器が複数ある
- CPU同じ。入出力周りの構造もおおむね同じ
- ハードウェア回りのコードは 共通で使いまわしたい
- それぞれの機器が相互に連携する構成のため、機器ごとに動作内容が違う
- 1台目は全体統括する機能を持つので、それ用のGenServerを起動したい
- 2台目以降は、1台目の指示を受けて動作するので、それ用のGenServerを起動したい
こういった場合、それぞれの機器ごとにプロジェクトを分けて作ることになりますが、開発中に下記のような失敗をしてしまいました。
- 共通で使いまわすコードの修正をした際、別のプロジェクトへのコードの同期を忘れてしまった
-
mix upload
するときに、間違ったデバイスにファームウェアを転送してしまった
こういった起こりえるトラブルのポカヨケの方法として、1つのプロジェクトで複数のNerves機器の動作機能を使い分ける仕組みづくりを試行してみました。
2.環境
下記のような構成を想定します。
汎用機能はそれぞれ共通で使うものとして、機器A・機器Bの違いで、最初のSupervisorの起動対象を切り替えることで、挙動を変えます。
3.目標
今回は、環境変数に、対象のIPアドレスを代入することで、挙動の切り替えをする仕組みを作ってみます。
考え方
mixコマンドの例
# ターゲットデバイスを環境変数に指定
$ export MIX_TARGET=rpi4
# ビルドとアップロードを一つのコマンドで実行
$ export MIX_TARGET_HOST=192.168.1.23 && mix firmware && mix upload ${MIX_TARGET_HOST}
*** > target host : "192.168.1.23"
*** > target settings : {"hosta", "192.168.1.23", :hosta, :role_hoge} / "passphrase"
==> nerves
==> projectname
A new version of Nerves bootstrap is available(1.11.2 < 1.11.3), You can update by running
mix local.nerves
Nerves environment
MIX_TARGET: rpi4
MIX_ENV: dev
(・・・省略・・・)
4.コード
ここでは、Elixirのプロジェクト名はprojectname
とします。
最初に、config/config.exs
を細工します。
渡された環境変数によって、動的に設定情報を切り替えて、Application.get_env
関数から引き出せるようにします。
(・・・省略・・・)
target = Application.get_env(:projectname, :target)
# 環境変数[MIX_TARGET_HOST]から書き込み先IPを取得
target_ip = System.get_env("MIX_TARGET_HOST")
# 確認用のログ・環境変数で渡されたIPアドレスを表示
IO.puts(" *** > target host : #{inspect(target_ip)}")
# 環境変数で指定のIPアドレスに対応した
# ホスト名や役割の紐づけるためのリストを
# あらかじめ用意しておく
node_host_setting_list = [
# デバイスA用
# 左:環境変数に対応する"KEY"、右:設定情報 {ノード名、(今回は未使用)、”役割”}
"192.168.1.23": {"hosta", :hosta, :role_hoge},
# デバイスB用
"192.168.1.45": {"hostb", :hostb, :role_fuga}
# (リストの項目はいくつでも増やせます)
]
# 上記のリストから、環境変数に与えられたIPアドレスを使って、
# 該当する設定情報を引き出す
node_host_setting =
case node_host_setting_list[:"#{target_ip}"] do
{nodename, b, role} ->
# 見つかったら、設定情報の部分を返す
{nodename, target_ip, b, role}
_ ->
# 見つからなかったら、デフォルト値を返す
{"none", "127.0.0.1", nil, nil} end
# アプリケーションの中から、現在の設定情報を参照できるようにする
config :projectname, hostselect: node_host_setting
# パスフレーズを指定する
node_host_cookie = "passphrase"
config :projectname, hostcookie: node_host_cookie
# 確認用のログ・選択中の設定内容を表示
IO.puts(" *** > target settings : #{inspect(node_host_setting)} / #{inspect(node_host_cookie)}")
(・・・省略・・・)
次に、lib/projectname/application.ex
を細工します。
起動時の環境変数で選択された設定情報を使って、GenServerの起動対象や、ノード名の設定を動的に切り替えます。
(・・・省略・・・)
def children(_target) do
# 選択中の設定情報を読み込み
{nodename, ipaddr, role} =
case Application.get_env(:projectname, :hostselect) do
{a, b, _, d} -> {a, b, d}
_ -> {"hostname", "127.0.0.1", false}
end
# 選択中の”役割”を表示
IO.puts("App role: #{role}")
role_nodes =
case role do
:role_hoge ->
# 役割hoge
[
# 役割hogeで起動したいGenServer
{ProjectName.FuncA, nil}
{ProjectName.FuncB, nil}
]
:role_fuga ->
# 役割fuga
[
# 役割fugaで起動したいGenServer
{ProjectName.FuncY, nil}
{ProjectName.FuncZ, nil}
]
_ ->
# 役割が未定義の時はエラー
IO.puts("Role error")
nil
end
# 選択中のノード名・IPアドレスでノード初期化
Node.start(:"#{nodename}@#{ipaddr}")
# cookieを設定
Node.set_cookie(:"#{Application.get_env(:projectname, :hostcookie)}")
(・・・省略・・・)
実行方法は、前述の「mixコマンドの例」を参照ください。
5.まとめ
1つのプロジェクトで複数のNerves機器の動作機能を使い分ける仕組みとして、
mixを実行するときに与える環境変数を使って、挙動を変える方法を試行してみました。
これを活用する場面は限定的かとは思いますが、ご参考になる方があれば幸いです。