Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

はじめに

こんにちは
fukuoka.exではIoT芸人をやっております.
福岡コミュニティに参加していながら実は京都在住なので,先日立ち上がったkyoto.exもばんばん盛り上げていきたいところです!!

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

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

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

こんなの作りました

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

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

pic1.jpg

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

用意したモノ

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

ハードウェア

  • ボード:Raspberry Pi 3 Model B
    • 使いやすさが自慢のIoT時代の風雲児です.とりあえず買ってみて動かしてみた方も多いかと思います.
    • 低価格なのに64-bit CPUが4-coreも載っていたり無線LANが標準搭載だったりで,下手なラップトップより性能が高くなるなんてこともあります.
  • Groveシールド:GrovePi+
    • Groveとは,各種センサなどのIoTデバイスを画一パッケージ化したモジュール群です.
    • 入出力ピンが4ピンのGroveコネクタで統一されており,簡単に付け替えできるのが特徴です.
    • このシールドはラズパイに適合していて,様々なIoTシステムの開発をラピッドに試すことができます:bullettrain_side::bullettrain_side::bullettrain_side:
  • Grove:今回は下記のモジュールを使いました.

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

ソフトウェア

  • カーネル:Raspbian Stretch with Desktop 4.9
  • 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+ライブラリ:こちらを参考にしてください.インストール不要だったかも.
  • グラフ表示:Chart.js
    • Phoenixページでのhtmlグラフ表示に使用しました.簡単にカスタマイズできて便利です.バージョンは2.1.4を使いました.
    • Phoenixのhtmlページを整形している lib/home_weather_phx_web/templetes/page/index.html.eex にて使用しています.

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

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

GrovePi+とGrovePi Zeroに対応しています.Groveモジュールの対応状況はHexDocsをご参照ください.また,このライブラリでは,Erlang VMのNIF経由でラズパイのGPIO,I2C,SPIの各種デバドラにアクセスできるelixir_aleが使われています.

動かしカタ

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

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

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

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

$ git clone https://github.com/takasehideki/fukuokaex11

今回のアプリはhome_weather_phxです.

$ cd home_weather_phx/

他にも開発したものはREADME.mdをご参照ください.いろいろ悪戦苦闘の跡が感じ取れるかと.

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

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

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

$ MIX_ENV=dev mix phx.server

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

pic2.jpg

仕組みのナカミ

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

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ブランチを直接指定するようにしました.

      {: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のライブラリはGenServerSupervisorで動作する設計になっています.
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な多くの仲間に出会えた素晴らしい年でした.このデモアプリも皆さんのお力添えが無ければ完成することはできませんでした.
そしてそして,このような出会いのきっかけを作っていただいた@zacky1972先生には特に感謝感激雨霰でございます!!
(この辺りだけでけっこうなボリュームになりそなので,また技術ポエムにまとめますかね^^;

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

明日の「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の担当は @kobatako さんの
ソフトウェアルータでカオスエンジニアリング入門」です!

takasehideki
Asst. Prof. at Kyoto Univ. a.k.a 組込マー
fukuokaex
エンジニア/企業向けにElixirプロダクト開発・SI案件開発を支援する福岡のコミュニティ
https://fukuokaex.fun/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした