LoginSignup
0

More than 1 year has passed since last update.

posted at

updated at

Organization

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

はじめに

この記事は、#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

実行

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

実行結果

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
What you can do with signing up
0