ElixirでIoT#2.3：ラズパイの温湿度と超音波センサ値をPhoenixでサクッと？リアルタイム表示

(この記事は「[fukuoka.ex Elixir／Phoenix Advent Calendar 2018](https://qiita.com/advent-calendar/2018/fukuokaex)」の5日目です)
昨日の記事は @koga1020 さんの「[Phoenix1.4 でのvue環境構築メモ（Part1: 単一ファイルコンポーネントを使えるようにするまで）](https://qiita.com/koga1020/items/aa54f57cdd19b48187e6)」でした．

# はじめに

こんにちは
[fukuoka.ex](https://fukuokaex.fun/)ではIoT芸人をやっております．
福岡コミュニティに参加していながら実は京都在住なので，先日立ち上がった[kyoto.ex](https://www.meetup.com/ja-JP/kyoto-ex/)もばんばん盛り上げていきたいところです！！

さてこの記事では，もう半年も前の話しになってしまっていますが，「[fukuoka.ex#11：DB／データサイエンスにコネクトするElixir](https://fukuokaex.connpass.com/event/87241/)」にて披露したIoT芸のネタについて，Advent Calendarらしく2018年の総決算！として今さらながら紹介しようかと思います．
＃ 連載記事が滞ってしまっていてすみません^^;

fukuoka.ex#11の発表スライドとGitHubリポジトリは下記のとおりです．

- SlideShare: [ElixirでIoT 第2回「環境センシングとデータ表示をサクっと？やってみた」](https://www.slideshare.net/takasehideki/elixiriot-2)
- GitHub: https://github.com/takasehideki/fukuokaex11

ラズパイやIoTなお話しはこれまでの連載記事も合わせてご笑覧ください．

- Qiita記事: [ElixirでIoT：連載目次
](https://qiita.com/takasehideki/items/8f43f1853ce88cbbe82e)



# こんなの作りました

**ラズパイとElixirを使ってリアルタイムな環境センシングをサクッとやってみました！**

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ライブデモこれです。手ぶれ酷くてすんません。<br>温湿度センサと超音波センサの値をラズパイ3B＆GrovePi+で取得して、LCDに表示させつつWebページにもリアルタイム表示！もちろんすべてElixir/Phoenix！！ <a href="https://twitter.com/hashtag/fukuokaex?src=hash&amp;ref_src=twsrc%5Etfw">#fukuokaex</a> <a href="https://t.co/loGk3yZxzW">pic.twitter.com/loGk3yZxzW</a></p>&mdash; TAKASE hideki (@TAKASEhideki) <a href="https://twitter.com/TAKASEhideki/status/1010117388174422016?ref_src=twsrc%5Etfw">2018年6月22日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

登壇直前までデモが現地でまともに動かなくって，よっしゃギリで動いた！とりまバックアップで撮っておくぞ！！ってな焦りが手ブレから伝われば幸いです^^;
静止画の概念図だとこんな感じです．

![pic1.jpg](https://qiita-image-store.s3.amazonaws.com/0/240349/8ae78404-977b-bd28-f47a-544ed6bcc024.jpeg)

コレを作るまでに様々な要素技術を駆使して多くのTIPSを得られました．
ですが細かいトコロまで解説しているとキリがない，，，（そしてまとまりがつかない！）ので，この記事では各TIPSの紹介は概要レベルに留めます．

# 用意したモノ

まずは使ったモノを紹介します．

## ハードウェア

- ボード：[Raspberry Pi 3 Model B](https://www.raspberrypi.org/products/raspberry-pi-3-model-b/)
    - 使いやすさが自慢のIoT時代の風雲児です．とりあえず買ってみて動かしてみた方も多いかと思います．
    - 低価格なのに64-bit CPUが4-coreも載っていたり無線LANが標準搭載だったりで，下手なラップトップより性能が高くなるなんてこともあります．
- Groveシールド：[GrovePi+](https://www.seeedstudio.com/GrovePi%2B-p-2241.html)
    - [Grove](https://www.seeedstudio.com/grove.html)とは，各種センサなどのIoTデバイスを画一パッケージ化したモジュール群です．
    - 入出力ピンが4ピンのGroveコネクタで統一されており，簡単に付け替えできるのが特徴です．
    - このシールドはラズパイに適合していて，様々なIoTシステムの開発をラピッドに試すことができます:bullettrain_side::bullettrain_side::bullettrain_side:
- Grove：今回は下記のモジュールを使いました．
    - [Temperature&Humidity Sensor (DHT)](http://wiki.seeedstudio.com/Grove-TemperatureAndHumidity_Sensor/)：DHT11というデジタル温度湿度センサを搭載したモジュールです．
    - [Ultrasonic Ranger](http://wiki.seeedstudio.com/Grove-Ultrasonic_Ranger/)：超音波によって対象物との距離を測定できるモジュールです．
    - [LCD RGB Backlight](http://wiki.seeedstudio.com/Grove-LCD_RGB_Backlight/)：16x2の文字をLCD表示できます．RGBに光るバックライト付き．

今回はラズパイ上でのホスト開発です．
キーボードやディスプレイはよしなにご用意ください．SSHログインできるなら不要です．

## ソフトウェア


- カーネル：[Raspbian Stretch with Desktop 4.9](https://www.raspberrypi.org/downloads/raspbian/)
- Elixirバージョン：Elixir 1.6.5 (compiled with OTP 20)
    - Erlang/OTP 20 [erts-9.3] [source] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
- GrovePi+ライブラリ：[こちら](https://www.dexterindustries.com/GrovePi/get-started-with-the-grovepi/setting-software/)を参考にしてください．インストール不要だったかも．
- グラフ表示：[Chart.js](https://www.chartjs.org/)
    - Phoenixページでのhtmlグラフ表示に使用しました．簡単にカスタマイズできて便利です．バージョンは2.1.4を使いました．
    - Phoenixのhtmlページを整形している [`lib/home_weather_phx_web/templetes/page/index.html.eex`](https://github.com/takasehideki/fukuokaex11/blob/master/home_weather_phx/lib/home_weather_phx_web/templates/page/index.html.eex) にて使用しています．

なんかいろいろバージョン古くね！？ってのは，半年前ですしね，，，
それぞれ新しいものでも動くとは思います（誰か試して，，，

Elixir用のGroveライブラリは，下記を使用しました．

- [adkron/GrovePi: Use the GrovePi in Elixir](https://github.com/adkron/grovepi)

GrovePi+とGrovePi Zeroに対応しています．Groveモジュールの対応状況は[HexDocs](https://hexdocs.pm/grovepi/api-reference.html)をご参照ください．また，このライブラリでは，Erlang VMのNIF経由でラズパイのGPIO,I2C,SPIの各種デバドラにアクセスできる[elixir_ale](https://github.com/fhunleth/elixir_ale)が使われています．

# 動かしカタ

中身の説明に入る前に，デモの使い方を紹介します．

まずはハードウェアの準備としてラズパイのセットアップです．
GrovePi+をラズパイ3Bに刺して，下記の通りGroveモジュールを接続します．

|Groveモジュール|GrovePi+接続先|
|:--|:--|
|温湿度センサ|D7|
|超音波センサ|D4|
|LCD|I2C-1|

次はソフトウェア側の操作です．
まぁひとまずcloneします．

```shell-session
$ git clone https://github.com/takasehideki/fukuokaex11
```

今回のアプリは`home_weather_phx`です．

```shell-session
$ cd home_weather_phx/
```

他にも開発したものは[README.md](https://github.com/takasehideki/fukuokaex11/blob/master/README.md)をご参照ください．いろいろ悪戦苦闘の跡が感じ取れるかと．

本日の日時を`lib/home_weather_phx_web/templates/page/index.html.eex`の153行目の`today`に定義してください．（自動化したかったんですけどねぇ^^;

```js:lib/home_weather_phx_web/templates/page/index.html.eex
window.onload = function () {
  var csvData = csvToArray("dhtdata.csv");
  var today = "2018-12-04 ";
  ...
};
```

あとはこのディレクトリにて，Phoenixサーバとアプリを起動するだけです．

```shell-session
$ MIX_ENV=dev mix phx.server
```

ブラウザから`http://<IP>:4000/`にアクセスすれば，温湿度・超音波距離の取得値がグラフ表示されます．しかもリアルタイムに表示更新します！
ラズパイ内からlocalhostでも，同ネットワーク内のPCやスマホからIP直打ちでも閲覧可能です．

![pic2.jpg](https://qiita-image-store.s3.amazonaws.com/0/240349/5d15b108-a9e8-b0bf-9936-09bc6612607a.jpeg)

# 仕組みのナカミ

少しだけナカミを紹介します．
詳細は（また気が向いたら）連載の別記事にしたいと思います（たぶん）．

## mix.exs

まずは`mix.exs`の中身です．

```mix.exs
defmodule HomeWeatherPhx.Mixfile do
  use Mix.Project

  def project do
    [
      app: :home_weather_phx,
      version: "0.0.1",
      elixir: "~> 1.4",
      elixirc_paths: elixirc_paths(Mix.env),
      compilers: [:phoenix, :gettext] ++ Mix.compilers,
      build_embedded: Mix.env() == :prod,
      start_permanent: Mix.env == :prod,
      deps: deps()
    ]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [
      mod: {HomeWeatherPhx.Application, []},
      extra_applications: [:logger, :runtime_tools, :timex]
    ]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_),     do: ["lib"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
      {:phoenix, "~> 1.3.2"},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_html, "~> 2.10"},
      {:phoenix_live_reload, "~> 1.0", only: :dev},
      {:gettext, "~> 0.11"},
      {:cowboy, "~> 1.0"},
      {:timex, "~> 3.1"},
      {:grovepi, github: "adkron/grovepi", branch: "master"}
    ]
  end
end
```

`timex`は現在時刻の取得に，`phoenix_live_reload`はブラウザのリアルタイム表示に使用しています．
今回は`priv/static/dhtdata.csv`に取得データを書き込んでいくこととして，このCSVファイルが更新されたらPhoenixも更新されるようにしました．`dev.exs`のlive_reload対象にcsvを追加しています．

```config/dev.exs
# Watch static and templates for browser reloading.
config :home_weather_phx, HomeWeatherPhxWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg|csv)$},
      ~r{priv/gettext/.*(po)$},
      ~r{lib/home_weather_phx_web/views/.*(ex)$},
      ~r{lib/home_weather_phx_web/templates/.*(eex)$}
    ]
  ]
```

GrovePiライブラリがなかなか手癖がありまして，ReleaseされているHexパッケージでは不具合がありました．このため，GitHubから`master`ブランチを直接指定するようにしました．

```exs
      {:grovepi, github: "adkron/grovepi", branch: "master"}
```

実は`deps`はこんな書き方もできるのです．



## home_weather_phx.ex

アプリ本体の記述です．

```lib/home_weather_phx.ex
defmodule HomeWeatherPhx do
  @moduledoc false
  use GenServer
  use Timex
  require Logger

  defstruct [:dht]

  alias GrovePi.{RGBLCD, DHT}

  @us_pin 4 # Use port 4 for Ultrasonic

  def start_link(pin) do
    GenServer.start_link(__MODULE__, pin)
  end

  def init(dht_pin) do
    state = %HomeWeatherPhx{dht: dht_pin}

    {:ok, _pid} = GrovePi.Ultrasonic.start_link(@us_pin)

    RGBLCD.initialize()
    RGBLCD.set_text("Ready!")

    # Create CSV file
    File.write "priv/static/dhtdata.csv", ""

    DHT.subscribe(dht_pin, :changed)
    {:ok, state}
  end

  def handle_info({_pin, :changed, %{temp: temp, humidity: humidity}}, state) do
    # Measure distance
    distance = GrovePi.Ultrasonic.read_distance(@us_pin)
    distance = if distance >= 200, do: 0, else: distance
    
    # Get date
    date = Timex.now("Asia/Tokyo")
      |> Timex.format!( "%Y-%m-%d %H:%M:%S", :strftime )

    temp = format_temp(temp)
    humidity = format_humidity(humidity)
    distance = format_distance(distance)

    # Write data to CSV
    File.write "priv/static/dhtdata.csv", "#{date},#{temp},#{humidity},#{distance}\n", [:append]

    flash_rgb()

    RGBLCD.set_text(temp)
    RGBLCD.set_cursor(1, 0)
    RGBLCD.write_text(humidity)
    Logger.info temp <> " " <> humidity <> "" <> distance

    {:noreply, state}
  end

  def handle_info(_message, state) do
    {:noreply, state}
  end

  defp flash_rgb() do
    RGBLCD.set_rgb(255, 0, 0)
    Process.sleep(1000)
    RGBLCD.set_color_white()
  end

  defp format_temp(temp) do
    "Temp: #{Float.to_string(temp)} C"
  end

  defp format_humidity(humidity) do
    "Humidity: #{Float.to_string(humidity)}%"
  end

  defp format_distance(distance) do
    "distance: #{distance}cm"
  end
end
```

温湿度センサ`GrovePi.DHT`のライブラリは`GenServer`と`Supervisor`で動作する設計になっています．
`handle_info()`の第2引数が`:changed`しか選べないのがちょっと厄介．要するに温湿度センサの値が変更したときしか動作しません．
同じく`handle_info()`内では，超音波センサの値を取得したのちに，出力用にデータをフォーマットしてCSVファイル`priv/static/dhtdata.csv`に追加書き込みします．同時にLCDにもデータを表示させています．

`application.ex`の記述はこんな感じです．

```lib/home_weather_phx/application.ex
defmodule HomeWeatherPhx.Application do
  use Application

  # RGB LCD Screen should use the IC2-1 port
  @dht_pin 7 # Use port 7 for the DHT
  @dht_poll_interval 10_000 # poll every 10 second

  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    # Define workers and child supervisors to be supervised
    children = [
      # Start the endpoint when the application starts
      supervisor(HomeWeatherPhxWeb.Endpoint, []),
      # Start your own worker by calling: HomeWeatherPhx.Worker.start_link(arg1, arg2, arg3)
      # worker(HomeWeatherPhx.Worker, [arg1, arg2, arg3]),

      # Start the GrovePi sensor we want
      worker(GrovePi.DHT, [@dht_pin, [poll_interval: @dht_poll_interval]]),

      # Start the main app
      worker(HomeWeatherPhx, [@dht_pin]),
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: HomeWeatherPhx.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  def config_change(changed, _new, removed) do
    HomeWeatherPhxWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end
```

`GrovePi.DHT`のworkerを定義しています．

# おわりに

2018年はElixirに出会って，そして@piacere_exさんを始めとした[fukuoka.ex](https://fukuokaex.fun/)な多くの仲間に出会えた素晴らしい年でした．このデモアプリも皆さんのお力添えが無ければ完成することはできませんでした．
そしてそして，このような出会いのきっかけを作っていただいた@zacky1972先生には特に感謝感激雨霰でございます！！
（この辺りだけでけっこうなボリュームになりそなので，また技術ポエムにまとめますかね^^;

とはいえ2018年に挙げたネタは，所詮は他人のフンドシを借りていたに過ぎません．研究屋さんとしては新しい技術や概念をドンドン打ち出していくつもりです．脳内には構想や計画がもうたっぷりとあるのであります><;
2019年も**ElixirでIoT芸**に磨きを掛けて，研究を加速させていきまっす！！


明日の「[fukuoka.ex Elixir／Phoenix Advent Calendar 2018](https://qiita.com/advent-calendar/2018/fukuokaex)」の担当は @kobatako さんの
「[ソフトウェアルータでカオスエンジニアリング入門](https://qiita.com/kobatako/items/2d6cade69a8456c74391)」です！