これはfukuoka.ex Elixir/Phoenix Advent Calendar 2019の15日の記事です。
昨日は@a_utsukiさんの「「プログラミングElixir」の次にやること」でした。
今回は「作って学ぶNerves、BBBでCO2計測」の
取得した値をPhoenixへPOSTしWEBブラウザで見られるようにできたら、
WEBからIoT端末までElixirで統一できてかっちょいいですね。
を実現できたので紹介します。
結果
先に結果を示します。
見た目は地味で残念ですが、
- Current Measurementの直下に表示される値が5秒毎に更新されています。
- この値はNervesで計測したCO2の計測時刻(UTC)とその値(ppm)です。
- そして描画更新周期はCO2の計測周期です。←ココポイント
以下では、このシステムの構成とその処理を紹介します。
構成と処理
構成
Phoenixサーバーに2つのクライアントがアクセスします。
一つは「作って学ぶNerves、BBBでCO2計測」で作ったNerves、もう一つはWebブラウザです。
Phoenix<-->Browser間の処理
- WebブラウザはPhoenixのデータ参照ページにアクセスします。
- Webブラウザは取得したjsファイルを実行し、WebSocketでPhoenixに接続をかけ、channelにjoinします。
このchannelには「"new_measurement"が来たら画面の値(時間とCO2濃度)を書き変える」イベントを登録しています。
PhoenixのChannelについては@nikuさんの「Phoenix Framework - Channel 日本語翻訳」が把握しやすいです。
Phoenix<-->Nerves間の処理
- Nervesクライアントは計測したCO2データを5秒毎にPhoenixのJSON APIにPOSTします。
- PhoenixはPOSTされたデータを永続データとしてPostgresに格納します。
また、同時にchannelに対し、"new_measurement"をbroadcastします。
このbroadcastを受け、Browserに登録したイベントがキックされ画面更新が起きます。
このため、この画面更新は即応性があります。
これはWebSocketを使ったサーバープッシュ、通知機能等で用いられる技術です。
作っている中でPhoenixにはサーバープッシュに2つの方法があることが分かりました。
寄り道:Phoenixのサーバープッシュ
- chatのような場合は、Phoenix.Channel.broadcastを使う
- 今回の場合は、Phoenix.Endpoint.broadcastを使う
chatのような場合
PhoenixのChannelsのドキュメントにあるchatの実装は、
- Browser側は投稿ボタンのクリックに合わせてchannel.push("new_msg", {body: chatInput.value})し、
- Phoenix側はhandle_in("new_msg", %{"body" => body}, socket)でBrowserのメッセージを受けて、
Phoenix.Channel.broadcastを実行します。
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast!(socket, "new_msg", %{body: body}) # Phoenix.Channel.broadcast! を使っている
{:noreply, socket}
end
handle_in関数はsocketに流れて来るeventとmessageを受けます。
このときhandle_in関数の引数にはsocketが渡ってくるため、broadcast先はそのsocketとして知ることができます。
今回の場合
今回の場合は「Phoenix<-->Nerves間の処理」で説明したとおり、データをDBに格納した直後にbroadcastします。
このとき、Phoenix.Channel.broadcastを使おうとすると、引数に渡すsocketに何を用いればよいか分かりません。
※ここでハマりました。
この場合に使えるのが、Phoenix.Endpoint.broadcastです。
(@kanmoさんの「Phoenix ChannelのAPIについて」が参考になりました。)
Phoenix.Endpoint.broadcastはトピックを指定して、broadcastすることができます。
def create(conn, %{"data" => measurement}=params) do
with {:ok, %Measurements.Data{} = datum} <- Measurements.create_data(measurement) do
EasyIotServerWeb.Endpoint.broadcast!( # Phoenix.Endpoint.broadcast! を使っている
"topic:subtopic",
"new_measurement",
%{
time: datum.utc_datetime_usec,
value: datum.value,
}
)
conn
|> put_status(:ok)
|> render("show.json", datum: datum)
end
end
このbroadcastによって、サーバープッシュでブラウザの画面更新を行うことができました。
さらなるアイデア
Phoenix<-->Browser間はchannelによって、計測に対して即応性のある画面更新ができるようになりました。
では、Phoenix<-->Nerves間でchannelを用いて、即応性のある動作をさせることはできないかと思いました。
イメージはブラウザに表示されたボタンの押下により、即応性の高い制御をすることです。
もうすでにチャレンジしているリポジトリ、「nerves_remote_led」がありました。
ということで組み込んで試してみました。
イェイ
突然の謝罪
nerves_remote_ledが使っているライブラリはphoenix_channel_clientで、今回はそれを使いました。
が、phoenix_channel_clientは現在はretiredで、PhoenixClientとしてメンテンスされているようです。
このLED制御の取り組みは、
CO2計測の画面更新のネタだけだと「内容薄いな」と思い、無理やりねじ込んだので、
ちゃんと確認せずに古いphoenix_channel_clientで作ってしまいました。詳細がないのもそのためです。
ごめんなさい
使う場合はPhoenixClientを使うよう気をつけてください。
まとめ
- ElixirのNervesとPhoenixを使い、下から上までElixirで書けるIoTシステムを作ってみました。
- WebSocket(channel)を使うことで即応性のあるUIと制御が作れることを確認しました。
かけ足でPhoenixを学んでいるので、おかしい点はご指摘いただけると幸いです。
今回は以上です。では、また!
「いいね」よろしくお願いします。
明日は@zacky1972さんの「どうやら Erlang をコンパイルした時のCコンパイラによって Elixir の性能がかなり異なるようだ」です。
後記
Railsを知らないからなのか、Webフレームワークに慣れていないからなのか、
Phoenixの構成を理解するのに大分時間を費やしました。
英語が辛いのですが、構成の理解に「Programming Phoenix」はとても良かったです。
Phoenixはほぼこれで勉強しました。
構成の理解だけでなく、そこどうするの?という「かゆいところに手が届く」点も良かったです。
今回の取り組みでPhoenixが分かってきたのでいろいろ作ってみます。