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 (・・・省略・・・)
実行中の様子
(高解像度版)
勉強会の中では時間が無かったので・・・IOエキスパンダー。
— myasu🍊 (@etcinitd) 2020年5月6日
いちおうElixir Circuits I2C + GenServerで実装してます。#NervesJP
実験記事↓https://t.co/xqkQbovVXL
流行り(?)のCLIがうまくうごかないメモ↓https://t.co/yLPoyJ3nbX pic.twitter.com/hodoIWUQG6