はじめに
この記事は、#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)
...
# 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に持ち込めます。
...
config :mnesia,
dir: '/data/dbdata' #サブディレクトリは存在しなければ作成されます。
###有線ネットワークを構成(target.exs)
BeagleBone Greenは有線だけです。
...
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で作成します。
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)
...
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)
...
defp deps do
[
...
{:circuits_gpio, "~> 0.4"},
{:ring_logger, "~> 0.6"}
]
###GPIO操作のライブラリを追加(gpioinout.ex)
このライブラリは @kikuyuta さんのgpioライブラリです。
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 さんの記事を参考に作成
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
<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>
...
scope "/", PhxifWeb do
...
live "/bbb", BbbLive
end
#MasterとSlaveの切り替え
MasterとSlaveを可能な限り同じプロジェクトにしたかった(別にmix.newは面倒くs..)ので、最小限の設定で切り替えられるようにしてます。
変更するファイルは次の2ファイルです。
exineris
├exineris
└target.exs
└lib
└exineris
└application.ex
...
config :vintage_net,
...
config: [
...
{"eth0",
%{
type: VintageNetEthernet,
ipv4: %{
method: :static,
address: "192.168.0.30", #コンフリクトしないようにIPを設定
...
...
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
#実行
- 起動順
- スレーブノードを起動
- マスターノードを起動
###実行結果
分散型DB Mnesiaを使ったエムネチカでっす。#qiita #アドベントカレンダー #elixir #nerves #mnesia pic.twitter.com/oEx0WAgcfE
— みっちぃ (@rigutter) December 17, 2020