作って学ぶPhoenix、IoTサーバー

これはfukuoka.ex Elixir／Phoenix Advent Calendar 2019の15日の記事です。
昨日は@a_utsukiさんの「[「プログラミングElixir」の次にやること](https://qiita.com/a_utsuki/items/f3143db50fc5cbc2d62e)」でした。

今回は「[作って学ぶNerves、BBBでCO2計測](https://qiita.com/pojiro/items/ffc8f01fed5b02e3b17c)」の

> 取得した値をPhoenixへPOSTしWEBブラウザで見られるようにできたら、
> WEBからIoT端末までElixirで統一できてかっちょいいですね。

を実現できたので紹介します。:sunny:

# 結果

先に結果を示します。

<img width="100%" alt="Peek 2019-12-13 21-56.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369713/27b1d2c5-7c0e-a70f-76f9-9c665030e7df.gif">

見た目は地味で残念ですが、:sweat_smile:

- Current Measurementの直下に表示される値が5秒毎に更新されています。
- この値はNervesで計測したCO2の計測時刻（UTC）とその値(ppm)です。
- そして描画更新周期はCO2の計測周期です。**←ココポイント**

以下では、このシステムの構成とその処理を紹介します。

# 構成と処理

## 構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369713/17ed5dea-87c1-e93b-0880-bc1fd213f792.png)

Phoenixサーバーに２つのクライアントがアクセスします。
一つは「[作って学ぶNerves、BBBでCO2計測](https://qiita.com/pojiro/items/ffc8f01fed5b02e3b17c)」で作ったNerves、もう一つはWebブラウザです。

## Phoenix<-->Browser間の処理

1. WebブラウザはPhoenixのデータ参照ページにアクセスします。
1. Webブラウザは取得したjsファイルを実行し、WebSocketでPhoenixに接続をかけ、channelにjoinします。  
   このchannelには「"new_measurement"が来たら画面の値（時間とCO2濃度）を書き変える」イベントを登録しています。

PhoenixのChannelについては@nikuさんの「[Phoenix Framework - Channel 日本語翻訳](https://qiita.com/niku/items/e846c4cbb9f1d15830cc)」が把握しやすいです。

## Phoenix<-->Nerves間の処理

1. Nervesクライアントは計測したCO2データを5秒毎にPhoenixのJSON APIにPOSTします。
1. PhoenixはPOSTされたデータを永続データとしてPostgresに格納します。  
   また、同時にchannelに対し、"new_measurement"をbroadcastします。

このbroadcastを受け、Browserに登録したイベントがキックされ画面更新が起きます。
このため、この画面更新は即応性があります。

これはWebSocketを使った[サーバープッシュ](https://ja.wikipedia.org/wiki/Push%E6%8A%80%E8%A1%93)、通知機能等で用いられる技術です。

作っている中でPhoenixにはサーバープッシュに２つの方法があることが分かりました。

## 寄り道：Phoenixのサーバープッシュ

- chatのような場合は、Phoenix.Channel.broadcastを使う
- 今回の場合は、Phoenix.Endpoint.broadcastを使う

### chatのような場合

[PhoenixのChannelsのドキュメント](https://hexdocs.pm/phoenix/channels.html#tying-it-all-together)にあるchatの実装は、

1. Browser側は投稿ボタンのクリックに合わせてchannel.push("new_msg", {body: chatInput.value})し、
1. Phoenix側はhandle_in("new_msg", %{"body" => body}, socket)でBrowserのメッセージを受けて、  
   [Phoenix.Channel.broadcast](https://hexdocs.pm/phoenix/Phoenix.Channel.html#broadcast/3)を実行します。

~~~ elixir
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間の処理](https://qiita.com/drafts/ae53b46e4d9d66a4de69#phoenix-nerves%E9%96%93%E3%81%AE%E5%87%A6%E7%90%86)」で説明したとおり、データをDBに格納した直後にbroadcastします。
このとき、Phoenix.Channel.broadcastを使おうとすると、引数に渡すsocketに何を用いればよいか分かりません。
※ここでハマりました。

この場合に使えるのが、[Phoenix.Endpoint.broadcast](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#c:broadcast/3)です。
（@kanmoさんの「[Phoenix ChannelのAPIについて](https://qiita.com/kanmo/items/cf01d4d5dfb0da3dbb80#endpointbroadcast)」が参考になりました。）

[Phoenix.Endpoint.broadcast](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#c:broadcast/3)はトピックを指定して、broadcastすることができます。

~~~ measurement_controller.ex
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](https://github.com/dkarter/nerves_remote_led)」がありました。

ということで組み込んで試してみました。

![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369713/da9854a3-4d1c-ecf0-15b2-01fe23fcad1b.gif)

イェイ:v:

#### 突然の謝罪

[nerves_remote_led](https://github.com/dkarter/nerves_remote_led)が使っているライブラリは[phoenix_channel_client](https://hex.pm/packages/phoenix_channel_client)で、今回はそれを使いました。
**が**、[phoenix_channel_client](https://hex.pm/packages/phoenix_channel_client)は現在はretiredで、[PhoenixClient](https://hexdocs.pm/phoenix_client/readme.html)としてメンテンスされているようです。

このLED制御の取り組みは、
CO2計測の画面更新のネタだけだと「内容薄いな」と思い、無理やりねじ込んだので、
ちゃんと確認せずに古い[phoenix_channel_client](https://hex.pm/packages/phoenix_channel_client)で作ってしまいました。詳細がないのもそのためです。
ご:bow:め:bow:ん:bow:な:bow:さ:bow:い


**使う場合は[PhoenixClient](https://hexdocs.pm/phoenix_client/readme.html)を使うよう気をつけてください。**


# まとめ

- ElixirのNervesとPhoenixを使い、下から上までElixirで書けるIoTシステムを作ってみました。
- WebSocket(channel)を使うことで即応性のあるUIと制御が作れることを確認しました。

かけ足でPhoenixを学んでいるので、おかしい点はご指摘いただけると幸いです。

今回は以上です。では、また！

**「いいね」よろしくお願いします。**:wink:

明日は@zacky1972さんの「[どうやら Erlang をコンパイルした時のCコンパイラによって Elixir の性能がかなり異なるようだ](https://qiita.com/zacky1972/items/8ff9775d83062fd097be#_reference-ab62771593f1042efd6f)」です。

# 後記

Railsを知らないからなのか、Webフレームワークに慣れていないからなのか、
Phoenixの構成を理解するのに大分時間を費やしました。:sweat_smile:

英語が辛いのですが、構成の理解に「[Programming Phoenix](https://pragprog.com/book/phoenix14/programming-phoenix-1-4)」はとても良かったです。
Phoenixはほぼこれで勉強しました。
構成の理解だけでなく、そこどうするの？という「かゆいところに手が届く」点も良かったです。

今回の取り組みでPhoenixが分かってきたのでいろいろ作ってみます。:muscle: