Nerves＋Phoenix 003　エムネチカ：分散型DB Mnesiaを使ってオリジナルCapeのLEDをエムネチカ


# はじめに
この記事は、[#NervesJP Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nervesjp) の18日目です。

昨日は @nishiuchikazuma さんの[NervesとPhonenix(Gigalixir)とGCP Cloud PubSubを使ってBBG CapeのLEDをチカした話〜Phoenix/GCPでPub編〜（1/2）](https://qiita.com/nishiuchikazuma/items/6c537342f8815728f69d)でした。

*チカシリーズとして、分散型DB Mnesiaを使い複数ノードで接点の状態を共有しLEDをチカらせる「エムネチカ」をやります。

###Mnesiaを選択した理由は以下の3つです。

- 接点の最新の状態(単純なデータ)を共有したかった。時系列のデータ検索をする必要がなかったので手軽さを重視
- ノード間でデータを共有でき、インデックス、トランザクションやバックアップの機能も持っている。
- 信頼性が高く(99.9999999とかなんとか)、非常に高速、これはPLCに代わる使用を想定したときに重要だと思いました。

PUB/SUB、Genserver等、データの送受信には選択肢が多くありますが、１０台程度までのノード数で手軽に使うにはMnesiaも選択肢に入れてみてはどうでしょうか。

明日は３日連続kochi.ex　@nishiuchikazuma さんの[NervesとPhonenix(Gigalixir)とGCP Cloud PubSubを使ってBBG CapeのLEDをチカした話〜Phoenix/GCPでPub編〜（2/2）](https://qiita.com/nishiuchikazuma/items/d0d1891349ea5b97a3cc)です。

#参考
- [Mnesia Reference Manual Version 4.18.1](https://erlang.org/doc/man/mnesia.html)
- [NervesでPhoenix LiveViewを動かし2つのLEDを操作する](https://qiita.com/nishiuchikazuma/private/4790f505e3f477629d0d)
- @kikuyuta [gpioライブラリ](https://qiita.com/kikuyuta)

# 環境
- macOS
- elixir 1.10.3-otp-23
- erlang 23.0.2
- Nerves 1.7.1ie
- Nerves Bootstrap 1.10.0
- Phoenix v1.5.7

備忘録：asdf current、mix nerves.info、mix phx.new -v 

- Masterノード
    - BeagleBone Green + Grove Button
- Slaveノード
    - BeagleBone Green + kochi.ex謹製 オリジナルCape

# 目次
- Mnesiaとは
- 前提
- NervesでMnesiaを使えるようにする。
- Phoenix LiveViewで接点・DBの状態を表示しハットのLEDを点灯できる状態にする
- MasterとSlaveの切り替え
- 実行


# Mnesiaとは
[Mnesia Reference Manual Version 4.18.1](https://erlang.org/doc/man/mnesia.html)
電気通信アプリケーションに適した分散型RDBMS、データはメモリとディスクに保存ができ、複数ノードにレプリケーションを構成することができます。
プログラムはデータの場所を意識すること無く作成する事ができます。
非常に高速なリアルタイムデータ検索を提供し、データへの一連の操作を一つのトランザクショングループとして扱うこともできます。

# 前提
ディレクトリ構成は以下としPonchoで構成します。

qiita2020
　├exineris
　└phxif

```Elixir:
#親ディレクトリを作成
mkdir qiita2020
cd qiita2020

#BeagleBone Greenをターゲット
set -x MIX_TARGET bbb

#Nervesプロジェクトを作成
mix nerves.new exineris

#phoenixプロジェクトを作成
mix phx.new phxif --no-ecto --live
```
その他Ponchoの構成はこちらを参考にしてください。
[Nerves＋Phoenix 002＋asdfでnodeをインストール](https://qiita.com/32hero/items/d055d2e7c95916e9292a)
以降、Nerves＋PhoenixがPonchoで構成されているものとして進めます。



# NervesでMnesiaを使えるようにする。

###extra_applicationsに:mnesiaを追加(mix.exs)
```Elixir:exineris/mix.exs
  ...
  # extra_applicationsに:mnesiaを追加
  def application do
    [
      mod: {Exineris.Application, []},
      extra_applications: [:logger, :runtime_tools, :mnesia]
    ]
  end
  ...
  defp deps do
    [
      ...
      {:phxif, path: "../phxif"},
      {:nerves_runtime_shell, "~> 0.1.0", targets: @all_targets}, #Nervesでshellに落ちるため。デバッグ用
    ]
```

###Mnesiaのデータ保存ディレクトリを指定(config.exs)
Nervesでは/data(/root)以外は読み取り専用のため、Mnesiaのディレクトリを/data配下に指定する必要があります。

- /data は ./upload.sh でデプロイした場合は上書きされません。
- dumpファイルを持ち込む場合は、exineris/rootfs_overlay　配下にファイルを配置することでNervesに持ち込めます。

```Elixir:exineris/config/config.exs
  ...
config :mnesia,
   dir: '/data/dbdata' #サブディレクトリは存在しなければ作成されます。
```

###有線ネットワークを構成(target.exs)
BeagleBone Greenは有線だけです。

```Elixir:exineris/config/target.exs
...
config :vintage_net,
  regulatory_domain: "JP",
  config: [
    {"usb0", %{type: VintageNetDirect}},
    {"eth0",
     %{
       type: VintageNetEthernet,
       ipv4: %{
          method: :static,
          address: "192.168.0.30",
          prefix_length: 24,
        }
     }},
    {"wlan0", %{type: VintageNetWiFi}}
  ]
```

###起動時にMnesiaを設定するモジュールを追加(bootmnesia.ex)
- 再接続等は今後追加します。
- Nerves起動時には別のMnesiaノードが起動しているため、stopしてから構成してます。
- phxif配下に作成しても動作しますが整理のためにexinerisで作成します。

```Elixir:exineris/lib/exineris/bootmnesia.exs
defmodule Bootmnesia do
  use GenServer
  require Logger

  def start_link(node_option \\ []) do
    # to init/1
    GenServer.start_link(__MODULE__, node_option, name: __MODULE__)
  end

  def init(node_option) do
    init_mnesia(eth0_ready?(), node_option)
    {:ok, node_option}
  end

  defp init_mnesia(true, [node_name, cookie, node, mode]) do
    node_host = get_ipaddr_eth0()
    System.cmd("epmd", ["-daemon"])
    Node.start(:"#{node_name}@#{node_host}")
    Node.set_cookie(:"#{cookie}")

    Logger.info("=== Node.start -> #{node_name}@#{node_host} ===")
    Logger.info("=== Node.set_cookie -> #{cookie} ===")

    case mode do
      "master" ->
         # 停止
        :mnesia.stop()
        # スキーマ削除
        :mnesia.delete_schema([node()])
        # スキーマ作成
        :mnesia.create_schema([node()])
        # 開始
        :mnesia.start()
        # テーブル作成
        :mnesia.create_table(State, [attributes: [:id, :data]])
        # ノードの追加
        :mnesia.change_config(:extra_db_nodes, [:"#{node}"])
      _ ->
        # 停止
        :mnesia.stop()
        # 開始
        :mnesia.start()
    end
  end

  defp init_mnesia(false, [_, _, _]) do
    Logger.info("=== init_mnesia -> false, eth0_ready(#{inspect(eth0_ready?())}) ===")
  end

  def eth0_ready?() do
    case get_ipaddr_eth0() do
      nil -> false
      _ -> true
    end
  end

  def get_ipaddr_eth0() do
    case VintageNet.get_by_prefix(["interface", "eth0", "addresses"]) do
      [] ->
        nil

      [list_int_eth0_addr] ->
        list_int_eth0_addr
        |> (fn {_, list_settings} -> list_settings end).()
        |> hd()
        |> Map.get(:address)
        |> VintageNet.IP.ip_to_string()
    end
  end

  def node_start?() do
    case Node.self() do
      :nonode@nohost -> false
      _ -> true
    end
  end

  def node_set_cookie?() do
    case Node.get_cookie() do
      :nocookie -> false
      _ -> true
    end
  end
end
```

###bootmnesiaを起動時に呼び出し(application.ex)
```Elixir:exineris/lib/exineris/application.ex
  ...
  def children(_target) do
    [
      # "ノード名","cookie","接続先名(スレーブ)","master" or "_"
      {Bootmnesia, ["1go", "comecomeeverybody", "2go@192.168.0.40","master"]} #マスターノードの場合
      # {Bootmnesia, ["2go", "comecomeeverybody", "","slave"]} #スレーブノードの場合
    ]
  end
```

# Phoenix LiveViewで接点・DBの状態を表示しハットのLEDを点灯できる状態にする
ここからPhoenix側を作ります。

###circuit_gpioを追加(mix.exs)
```Elixir:phxif/mix.exs
  ...
  defp deps do
    [
      ...
      {:circuits_gpio, "~> 0.4"},
      {:ring_logger, "~> 0.6"}
    ]
```

###GPIO操作のライブラリを追加(gpioinout.ex)
このライブラリは @kikuyuta さんの[gpioライブラリ](https://qiita.com/kikuyuta)です。

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

  def start_link(pname, gpio_no, in_out, ppid \\ []) do
    Logger.debug("#{__MODULE__} start_link: #{inspect(pname)}, #{gpio_no}, #{in_out} #{inspect(ppid)}")
    GenServer.start_link(__MODULE__, {gpio_no, in_out, ppid}, name: pname)
  end

  def write(pname, :true), do: GenServer.cast(pname, {:write, 1})
  def write(pname, :false), do: GenServer.cast(pname, {:write, 0})
  def write(pname, val), do: GenServer.cast(pname, {:write, val})

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

  @impl GenServer
  def init({gpio_no, in_out = :input, ppid}) do
    Logger.debug("#{__MODULE__} init_open: #{gpio_no}, #{in_out} ")
    {:ok, gpioref} = Circuits.GPIO.open(gpio_no, in_out)
    Circuits.GPIO.set_interrupts(gpioref, :both, receiver: ppid)
    {:ok, gpioref}
  end

  @impl GenServer
  def init({gpio_no, in_out = :output, _ppid}) do
    Logger.debug("#{__MODULE__} init_open: #{gpio_no}, #{in_out} ")
    Circuits.GPIO.open(gpio_no, in_out)
  end

  @impl GenServer
  def handle_cast({:write, val}, gpioref) do
    Logger.debug("#{__MODULE__} :write #{val} ")
    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 handle_info(msg, gpioref) do
    Logger.warn("#{__MODULE__} get_message: #{inspect(msg)}")
    Circuits.GPIO.set_interrupts(gpioref, :both)
    {:noreply, gpioref}
  end

  @impl GenServer
  def terminate(reason, gpioref) do
    Logger.debug("#{__MODULE__} terminate: #{inspect(reason)}")
    Circuits.GPIO.close(gpioref)
    reason
  end
end
```

###Phoenix LiveViewで接点・DBの状態を表示しハットのLEDを点灯する
@nishiuchikazuma さんの記事を参考に作成

```Elixir:phxif/lib/phxif_web/live/bbb_live.ex
defmodule PhxifWeb.BbbLive do
  use Phoenix.LiveView

  require Logger

  @db_polling_msec 100

  #### Cape基盤の色：イエロー
  #Grove Button BBBのGroveConnector(J5)はNervesの場合GPIO:2になります。
  @gpio_in_xs 2
  #CapeのLED
  @gpio_out_y0 50

  def mount(_param, _session, socket) do
    if connected?(socket) do
      GpioInOut.start_link(:gpio_in_xs, @gpio_in_xs, :input, self())
      GpioInOut.start_link(:gpio_out_y0, @gpio_out_y0, :output)
      Process.send_after(self(), :db_polling, @db_polling_msec)
    end

    socket = assign(
      socket,
      xs: "...",
      xsdb: "..."
    )
    {:ok, socket}
  end

  #DB ReadしLEDの操作を行う。指定msでループ
  def handle_info(:db_polling, socket) do
    Process.send_after(self(), :db_polling, @db_polling_msec)
    #DB Read、dirty_readはトランザクションを使用したreadと比較して10倍以上早い
    [{table,id,data}] = :mnesia.dirty_read({State, "xs"})
    
    #接点の状態をLEDに出力
    GpioInOut.write(:gpio_out_y0, data)
    socket = assign(socket, xsdb: data)
    {:noreply, socket}
  end

  #Grove Buttonが押されたら呼び出され、状態をDBに書き込み、画面更新
  def handle_info({:circuits_gpio, @gpio_in_xs, _time, on_off}, socket) do
    :mnesia.dirty_write({State, "xs", on_off})
    socket = assign(socket, xs: on_off)
    {:noreply, socket}
  end
end
```

```Elixir:phxif/lib/phxif_web/live/bbb_live.html.eex
<h2>LEDの操作と状態（bbb）</h2>

<h3 style="margin-top: 2em;">Input(GPIO:2 GROVE SWITCH)</h3>
<br />Switch on:1　off:2
<div>
    Xs: <%= @xs %><br />
</div>

<h3 style="margin-top: 2em;">DB</h3>
<br />DB Read Result
<div>
    XsDB: <%= @xsdb %><br />
</div>
```

```Elixir:phxif/lib/phxif_web/router.ex
  ...
  scope "/", PhxifWeb do
    ...
    live "/bbb", BbbLive
  end
```

#MasterとSlaveの切り替え
MasterとSlaveを可能な限り同じプロジェクトにしたかった(別にmix.newは面倒くs..)ので、最小限の設定で切り替えられるようにしてます。
変更するファイルは次の2ファイルです。
exineris
　├exineris
　　└target.exs
　└lib
　　└exineris
　　　└application.ex

```Elixir:exineris/config/target.exs
...
config :vintage_net,
  ...
  config: [
    ...
    {"eth0",
     %{
       type: VintageNetEthernet,
       ipv4: %{
          method: :static,
          address: "192.168.0.30",  #コンフリクトしないようにIPを設定
    ...
```

```Elixir:exineris/lib/exineris/application.ex
...
  def children(_target) do
    [
      # {Boomnesia, [node_name, cookie, conn_node, mode]}
      # "ノード名","cookie","接続先名(スレーブ)","master" or "_"
      {Bootmnesia, ["1go", "comecomeeverybody", "2go@192.168.0.40","master"]}　#マスターノードの場合
      #↑↓どちらか
      {Bootmnesia, ["2go", "comecomeeverybody", "","slave"]} #スレーブノードの場合
    ]
  end
```

#実行
- 起動順
    - スレーブノードを起動
    - マスターノードを起動

###実行結果
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">分散型DB Mnesiaを使ったエムネチカでっす。<a href="https://twitter.com/hashtag/qiita?src=hash&amp;ref_src=twsrc%5Etfw">#qiita</a> <a href="https://twitter.com/hashtag/%E3%82%A2%E3%83%89%E3%83%99%E3%83%B3%E3%83%88%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC?src=hash&amp;ref_src=twsrc%5Etfw">#アドベントカレンダー</a> <a href="https://twitter.com/hashtag/elixir?src=hash&amp;ref_src=twsrc%5Etfw">#elixir</a> <a href="https://twitter.com/hashtag/nerves?src=hash&amp;ref_src=twsrc%5Etfw">#nerves</a> <a href="https://twitter.com/hashtag/mnesia?src=hash&amp;ref_src=twsrc%5Etfw">#mnesia</a> <a href="https://t.co/oEx0WAgcfE">pic.twitter.com/oEx0WAgcfE</a></p>&mdash; みっちぃ (@rigutter) <a href="https://twitter.com/rigutter/status/1339520785396817921?ref_src=twsrc%5Etfw">December 17, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>