はじめてNerves(2) GenServer を使ってLチカをする

今回はラズパイのGPIOを用いて外部に用意したLEDをチカります。できるだけ汎用性高くして再利用性とか可読性とかを上げたいと思ってます。さあどうなるか。

これは Nerves 1.5 (Elixir 1.9) を用いてます。

#準備
Nerves の準備がまだの方は [はじめてNerves(1) 電源ONでLチカアプリを起動させる](https://qiita.com/kikuyuta/items/6c8debcfe7741ef48dd3) をご覧ください。この記事の [まずはターゲットのファームウェアを焼けるようにする](https://qiita.com/kikuyuta/items/6c8debcfe7741ef48dd3#まずはターゲットのファームウェアを焼けるようにする) までは同じです。このあとの [ターゲット用のプログラムを書く](https://qiita.com/kikuyuta/items/6c8debcfe7741ef48dd3#ターゲット用のプログラムを書く) の章から道が分かれますので、この直前までやってください。ただし今回のプロジェクト名は **exineris3b** でやります。

## ハードウェアを接続する
今回はラズパイについているLEDではなく、外部に回路を作ってそれを制御します。ココからハードウェアをどんどん追加していきます。今回やる内容は LED と抵抗とタクトスイッチをブレッドボード上に接続して使います。

ラズベリーパイの外部入出力用の信号は基板の脇に立ってる40ピンのコネクタでやりとりします。どのピンにどんな信号が出ているかは [Raspberri Pi Pinout] (https://pinout.xyz) などをご覧ください。以下の図はこの抜粋で数字だけ書いてあるのが GPIO の番号を示しています。
![alt](https://pinout.xyz/resources/raspberry-pi-pinout.png)


これらのハードウェアをどう組んでいくかあまり詳しくない方は、例えば [カラー図解 最新 Raspberry Piで学ぶ電子工作 作って動かしてしくみがわかる (ブルーバックス)](https://www.amazon.co.jp/dp/4062579774/) をごらんください。これに出てくるような回路を Nerves で制御していこうかなと考えています（どこまで忠実にやるかは別として）。なお、これ用に [Raspberry Piで学ぶ電子工作 パーツセット] (http://akizukidenshi.com/catalog/g/gK-10852/) もありますのでご参考まで。

今回はまず、ラズベリーパイの GPIO18 の出力を LED の点灯用に使います。これは [Circuits.GPIO](https://github.com/elixir-circuits/circuits_gpio) の例に従っています。
![alt](https://github.com/elixir-circuits/circuits_gpio/blob/master/assets/images/schematic-gpio-led.png?raw=true)

なお、この図では電流制限抵抗が 100Ω になっていますが、私が使った回路では 330Ω を使いました。

## GPIO 用ののライブラリを読み込む設定
今回は GPIO というインタフェースを用います。これには [Circuits.GPIO](https://github.com/elixir-circuits/circuits_gpio) ライブラリを使います。前回同様に、このライブラリを使うのに依存関係を `mix.exs` ファイルに指定します。

```exs:mix.exs
  ...
  defp deps do
    [
      ... # 途中省略
      {:circuits_gpio, "~> 0.4"},
    ]
  end
  ...
```

これを書いたのちに `mix deps.get` コマンドを投入してください。
なお、前回の `nerves.leds` の記述が残っていても構いません。

```
New:
  circuits_gpio 0.4.4
* Getting circuits_gpio (Hex package)
```
とか出てきて読み込まれたのがわかります。沢山出てくるメッセージの一部ですので注意して見てください。

# LEDが1個のLチカ
では早速Lチカです。ただし GenServer をきちんとつかうことにしますので、そうやすやすとは点きません。モジュールの構造は以下のようにします。

- Application: Nerves システムが立ち上がった時にここが呼ばれるので、ここから必要な関数を呼び出すようにしておくと、ブート時に自動的にプログラムが走るようになります
- Worker: Application から呼び出される関数を定義します。これは Application から Supervisor 監視下で実行されるプロセスとして動きますので GenServer を用いて記述します
- LedBlink: 特定の GPIO に接続された LED を点滅させるためのモジュールです
- GpioInOut: GPIO 経由での入出力を司ります。GPIO のピンごとに GenServer でプロセスを立てて、GPIO に対するアクセスは必ずこのプロセスを経由するようにします。

## 自動起動の設定をする
電源オンでプログラムが立ち上がるように `lib/プロジェクト名/application.ex` に記述をします。ここでは `lib/exineris3b/application.ex` に1行を加えます。

```exs:lib/exineris3b/application.ex
  def children(_target) do
    [
      {Exineris3b.Worker, [:led1]},
    ]
  end
```

こうすると `Exineris3b.Worker` モジュールの `start_link/1` 関数を `:led1` という引数で起動します。その際にプロセスを Supervisor の監視下においてくれます。


## GPIO制御用モジュール GpioInOut
では GenServer で GPIO を制御するモジュールを書いてみます。あとで IO 数が増えたときも改変しなくて済むように出来るだけ汎用的に書いてみます。なれないうちはなんでこんな面倒なことをしないとならないのだろうと思われるかもしれません。今回書いてみたら GenServer の基本的な書き方が一通り入っているのでちょうど良いサンプルになってるかと思います。

- `start_link/3` は指定された番号の GPIO を指定された入出力方向でオープンします。
 - gpio_no は GPIO の番号
 - in_out は入出力を :in か :out で指定
 - pname は他のプロセスと区別しやすいように適当なプロセス名を
- `write/2` GPIOピンへの出力をします
 - pname は start_link/3 で指定したプロセス名
 - val は何を出力するかを 0 か 1 で指定（0 で Low レベルに、1 で High レベル）
- `read/1` で GPIO ピンからの入力をします。返り値にプロセス名と入力の値のペアが入ります。
 - pname は start_link/3 で指定したプロセス名を指定
- `stop/1` 使っていた GPIO をクローズして、プロセスを終了します。
 - pname は start_link/3 で指定したプロセス名を指定します。

```lib/exineris3b/gpioinout.ex
defmodule GpioInOut do
  @behaviour GenServer
  require Circuits.GPIO
  require Logger

  def start_link(pname, gpio_no, in_out) do
    GenServer.start_link(__MODULE__, {gpio_no, in_out}, name: pname)
  end

  def write(pname, val) do
    GenServer.cast(pname, {:write, val})
  end

  def read(pname) do
    GenServer.call(pname, :read)
  end

  def stop(pname) do
    GenServer.stop(pname)
  end

  @impl GenServer
  def init({gpio_no, in_out}) do
    Logger.debug("#{__MODULE__} init: #{gpio_no}")
    Circuits.GPIO.open(gpio_no, in_out) # {:ok, ref} が返るのを期待
  end

  @impl GenServer
  def handle_cast({:write, val}, gpioref) do
    Circuits.GPIO.write(gpioref, val)
    {:noreply, gpioref}
  end

  @impl GenServer
  def handle_call(:read, _from, gpioref) do
    {:reply, {:ok, Circuits.GPIO.read(gpioref)}, gpioref}
  end

  @impl GenServer
  def terminate(reason, gpioref) do
    Circuits.GPIO.close(gpioref)
    reason
  end
```

## LEDをチカらせる LedBlink
では準備が整ったところでGPIO18につないでいるLEDが点滅するモジュールを書きます。ポイントは `led_blink/2` 関数でプロセス名とGPIO番号を指定するところです。これで、以降はプロセス名であれこれ出来るようになります。

- `LedBlink.init/4` は指定した LED を点滅させます
 - gpio_name は GPIO に名前をつけます。ピン番号で物理的に直に指定しないで済むようにこれで論理名をつけます
 - gpio_no は LED を点滅させる GPIO の物理ピンを指定します
 - on_delay は LED が点灯している時間をミリ秒で指定します
 - off_delay は LED が消灯している時間をミリ秒で指定します

関数名に `init` とつけていますが、これは GenServer のコールバック関数ではありません。

```exs:ledblink.ex
defmodule LedBlink do
  require Logger
  require GpioInOut

  def init(gpio_name, gpio_no, on_delay, off_delay) do
    Logger.debug("#{__MODULE__}: led_blink #{gpio_name} #{gpio_no} start...")
    GpioInOut.start_link(gpio_name, gpio_no, :output)
    loop(gpio_name, on_delay, off_delay)
  end

  defp loop(gpio_name, on_delay, off_delay) do
    Logger.debug("#{__MODULE__}: led_blink #{gpio_name} on...")
    GpioInOut.write(gpio_name, 1)
    Process.sleep(on_delay)
    Logger.debug("#{__MODULE__}: led_blink #{gpio_name} off...")
    GpioInOut.write(gpio_name, 0)
    Process.sleep(off_delay)
    loop(gpio_name, on_delay, off_delay)
  end
end
```

## メイン関数 Worker
Worker 関数は Application から呼び出されます。これ、明示していませんが Supervisor の下で管理されるプロセスとして動きます。

- `start_link/1` 起動用です。`Exineris3b.Application` より呼ばれます。引数は `Worker` モジュール内のどの動作をさせるかを選択させるために用います。
- `init/1` は `start_link/1` のコールバックで、ここではLED点滅用の関数を呼びます

なお、本来は `@behaviour GenServer` と書くべきと思いますが Nerves でうまく動かないので `use GenServer` で代用しています。これ、もう少し調べて後日ご報告します。


```exs:lib/exineris3b/worker.ex
defmodule Exineris3b.Worker do
  require Logger
  require LedBlink
  use GenServer

  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  def init(state = [:led1]) do
    Logger.debug("#{__MODULE__}: single led blink start...")
    LedBlink.init(:led_no0, 18, 1000, 1000)
    {:ok, state}
  end

  def init(state) do # Application.children/1 の引数を間違うとここに来る
    Logger.debug("#{__MODULE__}: No such operation defined!! state: #{inspect(state)}")
    {:error, state}
  end
```

## ブート時に自動で立ち上げる Application.ex
ブート後にターゲットにログインして iex から起動もできますが、ここでは自動で立ち上げてみます。`Exineris3b.Application.children/1` に以下の様に記述します。コメントが長いですが、プログラムとして意味があるのは最後の一行です。

```exs:lib/exineris3b/application.ex
  def children(_target) do
    [
      # Children for all targets except host
      # Starts a worker by calling: Exineris3b.Worker.start_link(arg)
      # {Exineris3b.Worker, arg},
      # {Exineris3b.Worker, []},
      # {Worker, []}
      {Exineris3b.Worker, [:led1]},
    ]
  end
```

## 実行してみる
ここまで準備したら `mix firmware` してコンパイルしてください。ファームウェアができたら `mix firmware.burn` で SD に焼いてラズパイをブートします[^uploadsh]。
[^uploadsh]: ネットワーク周りを上手にセットアップしているなら、もちろん `mix firmware.gen.script` で `upload.sh` ファイルを生成して `./upload.sh IPアドレス` で焼いていただいて構いません。

リブートしてくると LED が点滅します。ターゲットのコンソールに入れるなら `RingLogger.attach` コマンドを実行すると LED の on/off に従ってログが出力されているのが見えます。

![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/462608e9-96cf-ef0b-2a6e-c9878023101b.gif)

なお、自動で立ち上がらなかったらログインして iex で RingLogger.attach したあとに `Exineris3b.Worker.start_link([:led1])` と入力して手動で実行できるか確認してください。


# LEDが2個のLチカ
1つのLEDのLチカができたので、2つにしましょう。2つめのLEDはGPIO23（ラズパイのピン16番）を使います[^pin16]。
[^pin16]: たまたま1つ目のLEDが12番ピンで、GND挟んで隣だったのでこれにしてます。

## 2つの点滅を独立したプロセスにする
LEDが1つのときの Worker.ex を以下と書いたので、

```exs:lib/exineris/worker.ex
    LedBlink.init(:led_no0, 18, 1000, 1000)
```

LEDが2つになるならこんな風に書きたくなります。

```exs:lib/exineris/worker.ex
    LedBlink.init(:led_no0, 18, 200, 200)
    LedBlink.init(:led_no1, 23, 400, 600)
```

が、これでは動きません。最初のLEDの制御に行ったっきりになるので2番目のLEDが制御されないのです。ここでは両方を独立した並行プロセスとして別々に起動する必要があります。いろんな書き方が思いつきますが、ここではシンプルになるように Task を使ってみます。

```exs:lib/exineris/worker.ex
    Task.async(fn -> LedBlink.init(:led_no0, 18, 200, 200) end)
    Task.async(fn -> LedBlink.init(:led_no1, 23, 400, 600) end)
```
こうのように記述すると双方のLEDが独立したプロセスで制御されます。改めて Worker.ex の全体を書き直すとこうなります。

```exs:lib/exineris3b/worker.ex
defmodule Exineris3b.Worker do
  require Logger
  require LedBlink
  use GenServer

  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  def init(state = [:led1]) do
    Logger.debug("#{__MODULE__}: single led blink start...")
    LedBlink.init(:led_no0, 18, 1000, 1000)
    {:ok, state}
  end

  def init([:led2] = state) do
    Logger.debug("#{__MODULE__}: double led blink start...")
    Task.async(fn -> LedBlink.init(:led_no0, 18, 200, 200) end)
    Task.async(fn -> LedBlink.init(:led_no1, 23, 400, 600) end)
    {:ok, state}
  end

  def init(state) do
    Logger.debug("#{__MODULE__}: No such operation defined!! state: #{inspect(state)}")
    {:error, state}
  end
```

呼び出すプロセスがLED 2個用になるように `Application.ex` の方も書き換えます。

```exs:lib/exineris3b/application.ex
  def children(_target) do
    [
      {Exineris3b.Worker, [:led2]},
    ]
  end
```

### 関数引数のパターンマッチ
ここで LED が 1個のときと 2個のときとを `Exineris3b.Worker.start_link/1` への引数で区別するため `init/1` 関数の仮引数に以下のように記述してみました。これどちらもパターンマッチで動きます。マッチしてから state 変数に束縛するか、一旦 state に束縛してからマッチさせるかが違いますが、望む引数があったら実行してそうでなかったらスルーして次のマッチに行くという動作は同様に起こります。

```exs
  def init(state = [:led1]) do
    ...
  end

  def init([:led2] = state) do
    ...
  end
```

## 実行
あとのファイルは変える必要がありません。mix firmware して SD カードに焼いて、ラズパイをリブートしてみてください。

![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/aeb22291-f2d7-6874-866c-bdd47881b45f.gif)

# まとめ
ラズパイのGPIOを使ってNervesでLチカをやりました。プロセスが Supervisor 下で管理され、かつ各LEDが独立したプロセスで制御されるようにしてみました。これがモジュラリティ高く記述されていて、今後どれぐらいスマートにスムーズに拡張していけるのか検証していきたいです。

# 参考文献
- [Nerves Circuits.GPIO](https://github.com/elixir-circuits/circuits_gpio)
- [Elixir GenServer](https://hexdocs.pm/elixir/GenServer.html)
- [Elixir Task](https://hexdocs.pm/elixir/Task.html)
- [はじめてなElixir(18) Taskにも触ってみる](https://qiita.com/kikuyuta/items/62c95362677a1f9174d2)
- [はじめてな Elixir(19) GenServer で定期的なお仕事をする（まだ終わってない編）](https://qiita.com/kikuyuta/items/245513914eb68bd338b5)
- [はじめてNerves(0) ElixirによるIoTフレームワークNervesがとにかく動くようになるためのリンク集](https://qiita.com/kikuyuta/items/9dee85c32004c8e827e7)
- [はじめてNerves(1) 電源ONでLチカアプリを起動させる](https://qiita.com/kikuyuta/items/6c8debcfe7741ef48dd3)