LoginSignup
8
2

More than 3 years have passed since last update.

はじめてNerves(4) 独立したプロセスでボタチカをする

Last updated at Posted at 2020-02-13

前回は 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 で始まる行に全部収まるように書き換えてみます。

lib/exineris3b/gpioinout.ex
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 を追加するときに効いてきます。

lib/exineris3b/worker.ex
  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

これまで通り readwriteWorker から呼び出された関数で行います。前回は本体関数とループ用の関数があったのが、このように本体関数でループできるようになるので、ちょっときれいになった感じがします。なお「ボタンを押すと 0」と整数のゼロが返るのが使いにくいので to_nboolean/1 関数で「ボタンを押すと :true」になるようにしてます。

lib/exineris3b/logicled.ex
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 の該当する関数を呼び出すようにします。

lib/exineris3b/application.ex
  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/4logical_or/4 では、第4引数に &and/2&or/2 と記述することで logical_any/5 に論理演算関数を渡します。これによってプログラムがスマートになりました。

lib/exineris3b/logicled.ex
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_linkWorker で呼ぶように変更したのはここで効いてきます。

lib/exineris3b/worker.ex
  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 モジュールはそのままです。

lib/exineris3b/application.ex
  def children(_target) do
    [
      {Exineris3b.Worker, [:logicled]},
    ]
  end

これで 2つのスイッチの and と or で LED が点灯するようになります。

and_or.gif

プロセス同士のつながり

ここで作ったプログラムが生成するプロセスには以下の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 を示しています。

Nerves4-fig.png

これは GPIO 17 のボタンが押されて、 GPIO 22 のボタンは押されておらず、その結果として GPIO 18 の LED が消灯して、GPIO 23 が点灯している様子を示しています。

まとめ

プロセスを独立して動かして論理演算をさせてみました。両方のプロセスに共通の入力(タクトスイッチ)があって、それぞれのプロセスが独立した出力(LED)を持っています。プロセス同士はメッセージを交換しながら動作を進めていきます。

謝辞

今回は オカザキリンビーム のもくもく時間を使って記事を完成させました。機会を与えてくれた pjiroさん みなさん、ありがとうございました。

参考文献


  1. あと Supervisor と Worker がプロセスになってるはず。 

8
2
0

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