hotch-potch, Note to self

いろいろ作業記録

Elixir CircuitsでIOエキスパンダ(MCP23017)を動かす

1.はじめに

仕事&趣味柄、RaspberryPiを活用した支援機器を作っています。 情報量の多いPythonを中心に開発をしていますが、それ以外の言語での実装もチャレンジしています。

今回は、Elixirと、ElixirでのGPIO制御ライブラリElixir Circuitsを使って、IOエキスパンダを動かしてみた例を紹介します。

以前、似たような記事(Elixir Circuits I2CでLチカ - Qiita)を書いてますが、今回はkikuyuta先生の記事(はじめてNerves(7) I2C で液晶表示する - Qiita)をベースに、GenServerを使って、かつ各機能を別ファイルに分けてみました。

もうちょっとクリーンアップしたかったのですが、とりあえず・・・

2.サンプルコード

プロジェクト作成

$ mix new i2cio

mix.exs

defmodule I2cio.MixProject do
  use Mix.Project

  def project do
    [
      app: :i2cio,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger],
      mod: {I2cio.Application, []}
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
      {:circuits_gpio, "~> 0.4", override: true},        #追記
      {:circuits_i2c, "~> 0.3", override: true}         #追記
    ]
  end
end

i2cio/i2cinout.ex

kikuyuta先生の記事(はじめてNerves(2) GenServer を使ってLチカをする - Qiita)の書き方を参考に作成しています。

defmodule I2cio.I2cInOut do
  @behaviour GenServer
  require Circuits.I2C
  require Logger

  def start_link(pname, i2c_bus) do
    Logger.debug("#{__MODULE__} start_link: #{inspect(pname)}, #{i2c_bus} ")
    GenServer.start_link(__MODULE__, i2c_bus, name: pname)
  end

  def write(pname, addr, data, retries \\ []) do
    GenServer.cast(pname, {:write, addr, data, retries})
  end

  def read(pname, addr, bytes, retries \\ []) do
    GenServer.call(pname, {:read, addr, bytes, retries})
  end

  def writeread(pname, addr, bytes, retries \\ []) do
    GenServer.call(pname, {:writeread, addr, bytes, retries})
  end

  def stop(pname), do: GenServer.stop(pname)

  @impl GenServer
  def init(i2c_name) do
    Logger.debug("#{__MODULE__} init_open: #{inspect(i2c_name)} ")
    # expected to return {:ok, i2cref}
    Circuits.I2C.open(i2c_name)
  end

  @impl GenServer
  def handle_cast({:write, addr, data, retries}, i2cref) do
    Circuits.I2C.write(i2cref, addr, data, retries)
    {:noreply, i2cref}
  end

  @impl GenServer
  def handle_call({:read, addr, bytes, retries}, _from, i2cref) do
    {:reply, {:ok, Circuits.I2C.read(i2cref, addr, bytes, retries)}, i2cref}
  end

  @impl GenServer
  def handle_call({:writeread, addr, bytes, retries}, _from, i2cref) do
    {:reply, {:ok, Circuits.I2C.write_read(i2cref, addr, bytes, retries)}, i2cref}
  end

  @impl GenServer
  def terminate(reason, i2cref) do
    Logger.debug("#{__MODULE__} terminate: #{inspect(reason)}")
    Circuits.I2C.close(i2cref)
    reason
  end
end

i2cio/mcp23017.ex

defmodule I2cio.MCP23017 do
  @moduledoc """
  Documentation for `MCP23017`.
  """

  @behaviour GenServer
  require Logger

  @doc """
  初期化
  """
  def start_link(exp_name, i2c_bus, i2c_addr) do
    GenServer.start_link(
      __MODULE__,
      {exp_name, i2c_bus, i2c_addr},
      name: exp_name
    )
  end

  @doc """
  出力制御:直接値で制御する
  """
  def puts(exp_name, value) do
    GenServer.cast(exp_name, {:puts, value})
  end

  @doc """
  出力制御:指定のビットだけ制御する
  """
  def put(exp_name, bit, value) do
    GenServer.cast(exp_name, {:put, bit, value})
  end

  @doc """
  停止
  """
  def stop(exp_name), do: GenServer.stop(exp_name)

  # ------------------------------------------------------------------
  # GenServer実装部
  # ------------------------------------------------------------------

  @doc """
  GenServer初期化
  """
  @impl GenServer
  def init({exp_name, i2c_bus, i2c_addr}) do
    # i2c_nameを生成(文字連結してatomに変換)
    i2c_name = (to_string(exp_name) <> ":i2c") |> String.to_atom()
    # I2cInOutの起動
    I2cio.I2cInOut.start_link(i2c_name, i2c_bus)
    # MCP23017初期化
    init_ioexp(i2c_name, i2c_addr)
    {:ok, {i2c_name, i2c_addr}}
  end

  @doc """
  MCP23017初期化
  """
  defp init_ioexp(i2c_name, i2c_addr) do
    # PORT A 出力方向に設定
    I2cio.I2cInOut.write(i2c_name, i2c_addr, <<0x00, 0x00>>)
    # PORT A 全消灯
    I2cio.I2cInOut.write(i2c_name, i2c_addr, <<0x12, 0x00>>)
    # PORT B 入力方向に設定
    I2cio.I2cInOut.write(i2c_name, i2c_addr, <<0x01, 0xFF>>)
  end

  @doc """
  非同期呼び出し・出力を直接値で制御する
  """
  @impl GenServer
  def handle_cast({:puts, value}, {i2c_name, i2c_addr}) do
    # 新しい値を上書き
    I2cio.I2cInOut.write(i2c_name, i2c_addr, <<0x12, value>>)
    {:noreply, {i2c_name, i2c_addr}}
  end

  @doc """
  非同期呼び出し・出力をビット単位で制御する
  """
  @impl GenServer
  def handle_cast({:put, bit, value}, {i2c_name, i2c_addr}) do
    # 現在の点灯状況を読み出し
    {_, {_, <<now>>}} = I2cio.I2cInOut.writeread(i2c_name, i2c_addr, <<0x12>>, 1)
    Logger.info("now: #{now}, bit: #{bit}")

    # ビット計算を読み込み
    # https://hexdocs.pm/elixir/Bitwise.html
    use Bitwise
    # 現在の値に対し、指定のビットを新しい値で上書き
    val =
      case {value} do
        # 消灯操作
        {0} -> now &&& bnot(1 <<< bit)
        # 点灯操作
        {_} -> now ||| 1 <<< bit
      end

    # 新しい値を上書き
    I2cio.I2cInOut.write(i2c_name, i2c_addr, <<0x12, val>>)
    Logger.info("now: #{now}, bit: #{bit}")

    {:noreply, {i2c_name, i2c_addr}}
  end

  @doc """
  0のとき :falseを返す
  """
  defp to_boolean(0), do: false

  @doc """
  0以外のとき :trueを返す
  """
  defp to_boolean(_), do: true
end

i2cio/worker.ex

defmodule I2cio.Worker do
  @moduledoc """
  Documentation for `Worker`.
  LED点灯の移動アニメーション
  """
  use GenServer
  require Logger

  @doc """
  初期化
  """
  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  @doc """
  初期化
  """
  def init(state = [:i2cpika]) do
    # 初期化
    I2cio.MCP23017.start_link(:exp, "i2c-1", 0x20)

    # 点灯・消灯
    I2cio.MCP23017.puts(:exp, 0xFF)
    Process.sleep(100)
    I2cio.MCP23017.puts(:exp, 0x00)
    Process.sleep(100)

    # アニメーション1
    updown_all(-1, :up)

    # アニメーション2
    # updown_all(0)

    {:ok, state}
  end

  @doc """
  点灯したまま左右移動
  """
  defp updown_all(val, mode) do
    # 上下移動切替の条件をつくる無名関数
    updowncheck = fn
      # 7を超えたら:downに切替
      x when x >= 7 -> :down
      # 0を下回ったら:upに切替
      x when x <= 0 -> :up
      x -> mode
    end

    # 上下移動切替の条件判断
    mode = updowncheck.(val)

    # 点灯(:up)・消灯(:down)する点の位置を求める
    {val_after, onoff} =
      case {mode} do
        # 加算側の時の計算
        {:up} -> {val + 1, 1}
        # 減算側
        {:down} -> {val - 1, 0}
        # 上記以外
        _ -> 0
      end

    # 点灯パターン1:指定箇所まで点灯・左右
    # 表示の更新:点灯させたまま上下
    # MCP23017.put(:exp, val_after, onoff)

    # 点灯パターン2:1個だけ点灯・左右
    # 表示の更新:直前の位置を消灯
    I2cio.MCP23017.put(:exp, val, 0)
    # 表示の更新:次の位置を点灯
    I2cio.MCP23017.put(:exp, val_after, 1)

    # ウェイト
    Process.sleep(100)

    # 無限ループ
    updown_all(val_after, mode)
  end
end

i2cio.ex

defmodule I2cio do
  @moduledoc """
  Documentation for `I2cio`.
  """

  @doc """
  メイン
  """
  def main(args \\ []) do
    {:ok, pid} = GenServer.start_link(I2cio.Worker, [:i2cpika])

    loop()
  end

  @doc """
  無限ループ
  """
  def loop() do
    Process.sleep(1000)
    loop()
  end

  @doc """
  Hello world.

  ## Examples

      iex> I2cio.hello()
      :world

  """
  def hello do
    :world
  end
end

3.実行

$ mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
Unchanged:
  circuits_gpio 0.4.5
  circuits_i2c 0.3.6
  dep_from_hexpm 0.3.0
  elixir_make 0.6.0
All dependencies are up to date
$ mix run -e "I2cio.main" 
14:09:51.450 [debug] Elixir.I2cInOut start_link: :"exp:i2c", i2c-1 
14:09:51.463 [debug] Elixir.I2cInOut init_open: "i2c-1" 
now: 0, bit: -1
0
now: 0, bit: 0
1
now: 1, bit: 0
0
now: 0, bit: 1
2
(・・・省略・・・)

実行中の様子

f:id:hotch-potch:20200506214252g:plain

(高解像度版)