Nervesでのアプリケーション自動起動の手順(Version1.5系の場合)


以下の環境をベースにしています。

  • OS : macOS Mojave
  • Elixir : 1.9.0
  • Nerves : 1.5.3
  • Nerves Bootstrap: 1.6.2
  • Nervesのターゲットマシン: Raspberry Pi Zero WH

前説

Nervesとは、プログラミング言語Elixirを使って組込みソフトウェア開発を行うためのフレームワークです。

非常に軽量なLinuxブートローダー+ErlangVM+Elixir実行環境を提供してくれる優れものでもあります。加えて、mixだけで「IoTを操作するためのNervesのプロジェクト」が簡単に生成できてしまいます。

これによって、IoTの操作をするために、ラズパイ用のOSの環境設定をしたり慣れないC言語をさわったりといった煩わしさから解放されます。

また、2019年にはNervesJPのアドベントカレンダーもできており、全て埋まっています。このように、一部では非常に注目を浴びているんです。

まったくもって、NervesはElixirのIoTでナウでヤングなcoolなすごいヤツですね!

本題

Nerves Examplesのサンプルのファイル構成と、2020年1月時点でmix nerves.new <プロジェクト名>を実行した際のファイル構成にギャップがあったので整理してみました。

具体的な違いとしては、lib/<プロジェクト名>/application.exの存在です。
Nerves Examplesのプロジェクトにはlib/<プロジェクト名>/application.exはありません。
一方で、Nervesの1.5系で作成したNervesプロジェクトにはlib/<プロジェクト名>/application.exファイルがあり、かつmix.exsdef application do中のmod:で定義されているので最初に実行されます。

やりたいこと

Nervesを組み込んだSDカードを刺したIoT機器上に対して、電源を入れたら何のトリガーも必要とせず自動的にプロセスが動くこと、です。

したがって、mix nerves.newをした後に自動生成されているlib/<プロジェクト名>/application.exから、どのようにして自分で作成したモジュールや関数を呼び出すか、が課題となります。

対応方法:

通常のスーパーバイザーやGenServerの仕組みと同じように用意すれば良いです。

箇条書きにすると、次のようになります。

  1. lib/<プロジェクト名>/application.exchildrenの要素に、スーパーバイザーのワーカーとなるモジュールを設定します。
  2. 前述の1.で設定した、実行するモジュールのファイルを用意します。そのモジュール内では、以下の設定/関数を用意します。
    1. use GenServerでGenServerの振る舞い(ビヘイビア)を利用できるようにします。
    2. start_link/1関数を用意し、lib/<プロジェクト名>/application.ex側から起動されるようにします。
    3. init/1関数を用意し、初期化処理を記述します。関数の戻り値には、{:ok, state}のようにタプルの第一項目に実行結果、第二項目に"状態"を設定して返します。1
    4. init/1関数の中で、メインの処理になる関数を呼び出します。2

具体例

Raspberry Pi Zero WH(以降、rpi0)に対して、一定の間隔で以下を繰り返すようなプログラムを書きます。

  • 自身のLEDを点滅させる
  • GPIOを操作して出力のon/offを切り替える

プロジェクト名はnerves_gpio_sampleとします。
コンソールにmix nerves.new nerves_gpio_sampleと入力し、nerves_gpio_sampleのプロジェクトを作成します。

コード修正

以下、3点のコードの修正をします。

  1. ライブラリとして利用する「Circuits.GPIO3や「Nerves.Leds4を、mix.exsに設定(追記)します。
  2. lib/nerves_gpio_sample/application.exに対して、ワーカーになるモジュールNervesGpioSample.Ledを設定(追記)します。5
  3. lib/nerves_gpio_sample/led_raspi.exを新規作成します。その中で、GenServerの振る舞いとワーカー対応用のコールバック関数、およびLチカ用に処理を記述します。6
mix.exs(抜粋)
  defp deps do
    [
      # Dependencies for all targets
      {:nerves, "~> 1.5.0", runtime: false},
      {:shoehorn, "~> 0.6"},

      〜(中略)〜

      #############
      ## 最後に、以下の2つを追加
      ############# 
      {:nerves_leds, "~> 0.8", targets: @all_targets},
      {:circuits_gpio, "~> 0.4", targets: @all_targets},
    ]
  end
lib/nerves_gpio_sample/application.ex(抜粋)

  def children(_target) do
    [
      # Children for all targets except host
      # Starts a worker by calling: NervesGpioSample.Worker.start_link(arg)
      # {NervesGpioSample.Worker, arg},
      {NervesGpioSample.Led, []},    # <- ここにワーカーになるモジュール名を追加。
    ]
  end

lib/nerves_gpio_sample/led_raspi.ex

defmodule NervesGpioSample.Led do
  use GenServer

  ## 短縮名で利用できるように宣言
  alias Nerves.Leds
  alias Circuits.GPIO

  ## スーパーバイザー側からcallされる処理。
  ## 関数内で、GenServer.start_link/3を呼び出し。
  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  ## GenServer.start_linkによって呼び出される初期化処理。
  def init(state) do
    run()
    {:ok, state}
  end

  ### これ以降は、Lチカ/GPIO操作用の関数

  def run() do
    led_list = Application.get_env(:nerves_gpio_sample, :led_list)
    spawn(fn -> blink_list_forever(led_list) end)
  end

  defp blink_list_forever(led_list) do
    Enum.each(led_list, &blink(&1))
    blink_list_forever(led_list)
  end

  defp blink(led_key) do
    1..10 |> Enum.each(fn _ -> led_set(led_key,{300, 300}) end)

    {:ok, gpio17} = GPIO.open(17, :output)
    {:ok, gpio27} = GPIO.open(27, :output)

    GPIO.write(gpio17, 1)
    led_set(led_key,{3000, 2000})
    GPIO.write(gpio17, 0)

    GPIO.write(gpio27, 1)
    led_set(led_key,{2000, 3000})
    GPIO.write(gpio27, 0)
  end

  defp led_set(led_key, {on_time, off_time}) do
    Leds.set([{led_key, true}])
    :timer.sleep(on_time)
    Leds.set([{led_key, false}])
    :timer.sleep(off_time)
  end

end

実施結果:

電源を入れると、しばらくしてLチカ+GPIOによる出力で外部回路のLEDを点灯/消灯を確認できました。

以下の動きを繰り返します。

  • rpi0のLEDが点灯/消灯が、0.3秒間ずつ10回くりかえされます
  • GPIO17がonになり外部回路のLEDが点灯、同時にrpi0のLEDが3秒点灯/2秒消灯します。
  • GPIO17がoffになってGPIO27がon(この時、GPIOの17番と27番にそれぞれ繋いでいる外部回路のLEDも消灯/点灯)になります。同時にrpi0のLEDが2秒点灯/3秒消灯します。
  • GPIO27がoffになり外部回路のLEDも消灯、最初の動作に戻ります。

参考情報

実施内容自体は、Elixir Schoolの「Nerves」のページに書いてありました。

また、こちらのQiita記事も参考にさせていただきました。

加えて、「GenServer」7や「Supervisor」8のマニュアルも参照しました。

Nervesの内容というよりも、OTPの並行性(SupervisorやGenServerなども含む)に関する情報になった気がします。
また、init/1spawnさせているあたり、GenServerの利点を活かせていないのでツッコミどころはあるかと思っていますが...
handle_cast/2を用意し、init/1の中でGenServer.castを実行するパターンも試してみたので、内容を整理できしだい追記して行こうと思います。


  1. ここでいう"状態"とは、OTPサーバでいうところの"状態"を指します。 

  2. もしくは、さらにGenServerのコールバック関数(handle_call/3handle_cast/2など)を実装し、init/1からGenServer.callやGenServer.castを呼び出してメインの処理が実行されるようにします。 

  3. https://hexdocs.pm/circuits_gpio/readme.html 

  4. https://hexdocs.pm/nerves_leds/Nerves.Leds.htm 

  5. 厳密には、start/2関数の中で呼び出しているSupervisor.start_link(children, opts)に渡している引数childrenの中に設定されていれば良いです。ですので、start/2関数の中で定義しても可です。 

  6. init/1関数からrun/0関数を起動し、spawnを利用してさらに別プロセスで起動。GPIOについては、GPIO17番と27番を利用して、on/offの実施。 

  7. https://hexdocs.pm/elixir/GenServer.html 

  8. https://hexdocs.pm/elixir/Supervisor.html