前回は GPIO 入出力を使ってボタンを押すと LED が何らかの動作をするようなプログラムを書きました。今回はもうちょっとプロセスを使って Elixir らしい感じのプログラムに挑戦してみます。具体的には共通の I/O は使うものの動作が独立している複数のプロセスでのプログラムを作ります。これは Nerves 1.5 (Elixir 1.9) を用いてます。
準備
ハードウェアは前回の はじめてNerves(3) GPIO入力を追加してボタチカをする 同様、タクトスイッチが GPIO 17 と GPIO 22 に、LED が GPIO 18 と GPIO 23 に繋がっているものとします。
GPIO への出力を真偽値でも良くする
GpioInOut.write/2
関数の第2引数には 0 か 1 を渡していました。使うのにちょっと不便なので、これを :false と :true でも受け付けるようにします。あと、本体が1行しかない関数が多いので def
で始まる行に全部収まるように書き換えてみます。
defmodule GpioInOut do
@behaviour GenServer
require Circuits.GPIO
require Logger
def start_link(pname, gpio_no, in_out) do
Logger.debug("#{__MODULE__} start_link: #{inspect(pname)}, #{gpio_no}, #{in_out} ")
GenServer.start_link(__MODULE__, {gpio_no, in_out}, 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}) do
Logger.debug("#{__MODULE__} init_open: #{gpio_no}, #{in_out} ")
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
Logger.debug("#{__MODULE__} terminate: #{inspect(reason)}")
Circuits.GPIO.close(gpioref)
reason
end
end
論理積(AND)回路
スイッチが2つとも押されたときのみに LED が光るようにします。
これまでは Worker
モジュールから呼び出された関数で GPIO を Circuits.GPIO.open/2
していました。この設計ですと、同一の GPIO を使う複数のプロセスを起動すると重複して open してしまうという欠点があります。ですので、この設計を変更して Circuits.GPIO.open/2
(を行う GpioInOut.start_link/3
)を Worker
モジュールから呼び出すように変更します。この変更は今ここでは特にメリットがありませんが、次に OR を追加するときに効いてきます。
def init(state = [:logicled]) do
Logger.debug("#{__MODULE__}: Logical AND start...")
GpioInOut.start_link(:button_a, 17, :input)
GpioInOut.start_link(:button_b, 22, :input)
GpioInOut.start_link(:led_x, 18, :output)
Task.async(fn -> LogicLed.logical_and(:button_a, :button_b, :led_x) end)
{:ok, state}
end
これまで通り read
と write
は Worker
から呼び出された関数で行います。前回は本体関数とループ用の関数があったのが、このように本体関数でループできるようになるので、ちょっときれいになった感じがします。なお「ボタンを押すと 0」と整数のゼロが返るのが使いにくいので to_nboolean/1
関数で「ボタンを押すと :true」になるようにしてます。
defmodule LogicLed do
require GpioInOut
def logical_and(button_a_name, button_b_name, led_name, interval \\ 50) do
{:ok, a} = GpioInOut.read(button_a_name)
{:ok, b} = GpioInOut.read(button_b_name)
GpioInOut.write(led_name, to_nboolean(a) and to_nboolean(b))
Process.sleep(interval)
logical_and(button_a_name, button_b_name, led_name, interval)
end
defp to_nboolean(0), do: :true # GPIO で負論理のボタンが押された時に :ture
defp to_nboolean(_), do: :false # GPIO で負論理のボタンが押されてない時に :false
end
Application
モジュールからは Worker
の該当する関数を呼び出すようにします。
def children(_target) do
[
{Exineris3b.Worker, [:logicled]},
]
end
これを実行すると、両方のボタンを押したときだけ LED が光ります。
論理積(AND)回路と論理和(OR)回路と
LED がもう1つあるので、今度は OR も作ります。先程の logical_and/4
関数をコピペして and/2
関数を or/2
関数にすればよいだけです。が、それだと芸がないのでちょっと工夫をしてみます。
以下の logical_any/5
関数では、2引数の論理演算をする ope/2
関数を第4引数として受け取るようにしています。これは関数内で ope.(a, b)
の形式で関数を引数に適用させます。これを使う logical_and/4
と logical_or/4
では、第4引数に &and/2
や &or/2
と記述することで logical_any/5
に論理演算関数を渡します。これによってプログラムがスマートになりました。
defmodule LogicLed do
require Logger
require GpioInOut
def logical_any(button_a_name, button_b_name, led_name, ope, interval \\ 50) do
{:ok, a} = GpioInOut.read(button_a_name)
{:ok, b} = GpioInOut.read(button_b_name)
GpioInOut.write(led_name, ope.(to_nboolean(a), to_nboolean(b)))
Process.sleep(interval)
logical_any(button_a_name, button_b_name, led_name, ope, interval)
end
def logical_and(button_a_name, button_b_name, led_name, interval \\ 50) do
logical_any(button_a_name, button_b_name, led_name, &and/2, interval)
end
def logical_or(button_a_name, button_b_name, led_name, interval \\ 50) do
logical_any(button_a_name, button_b_name, led_name, &or/2, interval)
end
defp to_nboolean(0), do: :true
defp to_nboolean(_), do: :false
end
Worker
モジュールでは2つの関数を非同期のプロセスとして立ち上げます。一方のプロセスは AND の演算を、他方のプロセスは OR の演算をします。これらのプロセスは共通の入力を持ちますが、プロセス同士では一切通信をしないで独立なプロセスとして動作します。AND の例で GpioInOut.start_link
を Worker
で呼ぶように変更したのはここで効いてきます。
def init(state = [:logicled]) do
Logger.debug("#{__MODULE__}: Logical AND start...")
GpioInOut.start_link(:button_a, 17, :input)
GpioInOut.start_link(:button_b, 22, :input)
GpioInOut.start_link(:led_x, 18, :output)
GpioInOut.start_link(:led_y, 23, :output)
Task.async(fn -> LogicLed.logical_and(:button_a, :button_b, :led_x) end)
Task.async(fn -> LogicLed.logical_or(:button_a, :button_b, :led_y) end)
{:ok, state}
end
Application
モジュールはそのままです。
def children(_target) do
[
{Exineris3b.Worker, [:logicled]},
]
end
これで 2つのスイッチの and と or で LED が点灯するようになります。
プロセス同士のつながり
ここで作ったプログラムが生成するプロセスには以下の6つがあります1。
- ボタン入力を司るプロセス
- GPIO 17 用(プロセス名: button_a)
- GPIO 22 用(プロセス名: button_b)
- LED 出力を司るプロセス
- GPIO 18 用(プロセス名: led_x)
- GPIO 23 用(プロセス名: led_y)
- ボタン入力から LED 出力を決めるプロセス
- AND 演算用(関数名: logical_and)
- OR 演算用(関数名: logical_or)
個々の GPIO デバイスには各々プロセスが張り付いてます。各I/Oは担当プロセスからのみアクセスされるようにしてあります。以下の図はプロセス間のメッセージングの様子を示しています。茶色の矢印が問い合わせのメッセージ、紺色の矢印がその handle_call
の返すメッセージ、紺色の破線矢印がメッセージを返さない handle_cast
を示しています。
これは GPIO 17 のボタンが押されて、 GPIO 22 のボタンは押されておらず、その結果として GPIO 18 の LED が消灯して、GPIO 23 が点灯している様子を示しています。
まとめ
プロセスを独立して動かして論理演算をさせてみました。両方のプロセスに共通の入力(タクトスイッチ)があって、それぞれのプロセスが独立した出力(LED)を持っています。プロセス同士はメッセージを交換しながら動作を進めていきます。
謝辞
今回は オカザキリンビーム のもくもく時間を使って記事を完成させました。機会を与えてくれた pjiroさん みなさん、ありがとうございました。
参考文献
- Nerves Circuits.GPIO
- はじめてNerves(0) ElixirによるIoTフレームワークNervesがとにかく動くようになるためのリンク集
- はじめてNerves(1) 電源ONでLチカアプリを起動させる
- はじめてNerves(2) GenServer を使ってLチカをする
- はじめてNerves(3) GPIO入力を追加してボタチカをする
-
あと Supervisor と Worker がプロセスになってるはず。 ↩