LoginSignup
9
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-17

はじめに

この記事は、#NervesJP Advent Calendar 2020 の18日目です。

昨日は @nishiuchikazuma さんのNervesとPhonenix(Gigalixir)とGCP Cloud PubSubを使ってBBG CapeのLEDをチカした話〜Phoenix/GCPでPub編〜(1/2)でした。

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

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

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

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

明日は3日連続kochi.ex @nishiuchikazuma さんのNervesとPhonenix(Gigalixir)とGCP Cloud PubSubを使ってBBG CapeのLEDをチカした話〜Phoenix/GCPでPub編〜(2/2)です。

参考

環境

  • 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
電気通信アプリケーションに適した分散型RDBMS、データはメモリとディスクに保存ができ、複数ノードにレプリケーションを構成することができます。
プログラムはデータの場所を意識すること無く作成する事ができます。
非常に高速なリアルタイムデータ検索を提供し、データへの一連の操作を一つのトランザクショングループとして扱うこともできます。

前提

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

qiita2020
 ├exineris
 └phxif

#親ディレクトリを作成
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をインストール
以降、Nerves+PhoenixがPonchoで構成されているものとして進めます。

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

extra_applicationsに:mnesiaを追加(mix.exs)

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に持ち込めます。
exineris/config/config.exs
  ...
config :mnesia,
   dir: '/data/dbdata' #サブディレクトリは存在しなければ作成されます。

有線ネットワークを構成(target.exs)

BeagleBone Greenは有線だけです。

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で作成します。
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)

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)

phxif/mix.exs
  ...
  defp deps do
    [
      ...
      {:circuits_gpio, "~> 0.4"},
      {:ring_logger, "~> 0.6"}
    ]

GPIO操作のライブラリを追加(gpioinout.ex)

このライブラリは @kikuyuta さんのgpioライブラリです。

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 さんの記事を参考に作成

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
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>
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

exineris/config/target.exs
...
config :vintage_net,
  ...
  config: [
    ...
    {"eth0",
     %{
       type: VintageNetEthernet,
       ipv4: %{
          method: :static,
          address: "192.168.0.30",  #コンフリクトしないようにIPを設定
    ...
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

実行

  • 起動順
    • スレーブノードを起動
    • マスターノードを起動

実行結果

9
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
0