これまで3回に渡って、プレゼンのデモ用に作った「光ったり鳴ったりする」3つのデバイスをご紹介してきました。
- はじめてNerves(15) I2C デバイス BH1745NUC で照度を測ってみる
- はじめてNerves(16) I2C デバイス Y2KB-040 でブザーを鳴らしてみる
- はじめてNerves(17) I2C デバイス PCA9622 で 8x8 LEDマトリックスを光らせる
今回はこの3部作の集大成として、3つを1台のBeagleBone Greenにまとめて同時に動かします。LEDマトリックスに0〜Fをカウントアップ表示しつつ、照度センサが拾った明るさに応じてブザーの音程を変える、というデモです。
アプリケーション構成
phototaxis というNervesアプリを作りました。3つのライブラリはすでに hex.pm に公開済みなので、それぞれ deps に足すだけで使えます。
defp deps do
[
{:pca9622matrix8x8, "~> 0.1.0"},
{:y2kb040buzzer, "~> 0.1.0"},
{:bh1745luminance, "~> 0.1.0"},
...
]
end
Application
Nerves がブートしたらそのまま実行したいので Application を使い魔s樹。Phototaxis.Application では、3つのライブラリに対応するGenServerと、それらを使う Worker を1つの one_for_one Supervisorの下にぶら下げます。
defmodule Phototaxis.Application do
use Application
@impl true
def start(_type, _args) do
children = [
{PCA9622Matrix8x8, reflesh_interval: 0, priority: :high},
Buzzer,
Luminance,
Worker
]
opts = [strategy: :one_for_one, name: Phototaxis.Supervisor]
Supervisor.start_link(children, opts)
end
end
Application の子どもたち
Application から起動するのは Supervisor か GenServer です。GenServer でない場合はなにかしら GenServer でラップしてやる必要があります。
PCA9622Matrix8x8 は前回の記事で書いたとおりそれ自体がGenServerですが、Y2KB040Buzzer と BH1745Luminance は素のI2C操作関数(open/closeとbeepやread_rgbw)しか持っていないライブラリです。そこでこの2つには薄いGenServerラッパー(Buzzer と Luminance)を被せて、I2Cの接続(ref)をプロセス内に閉じ込め、他のプロセスからはメッセージでアクセスする形に揃えています。
defmodule Buzzer do
use GenServer
def start_link(opts \\ []), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
def beep(server \\ __MODULE__, pitch, length, times),
do: GenServer.cast(server, {:beep, pitch, length, times})
@impl true
def init(opts) do
bus_name = Keyword.get(opts, :bus_name, "i2c-2")
{:ok, ref} = Y2KB040Buzzer.open(bus_name)
{:ok, ref}
end
@impl true
def handle_cast({:beep, pitch, length, times}, ref) do
Y2KB040Buzzer.beep(ref, pitch, length, times)
{:noreply, ref}
end
@impl true
def terminate(_reason, ref) do
Y2KB040Buzzer.close(ref)
:ok
end
end
Luminance も同じパターンで BH1745Luminance.read_rgbw/1 を GenServer.call 越しに公開しているだけです。3本の記事それぞれで単体のライブラリとして作ったものを、最後はこの薄いラッパーで足並みを揃えて組み合わせる、という形に落ち着きました。
Worker: 3つを繋ぐ本体
Worker が実質的なデモのロジックです。以下にまず全体を掲載します。
defmodule Worker do
@moduledoc """
Counts 0~f once a second, rendering each tick on the LED matrix, and
optionally beeps at a pitch derived from the current ambient light
reading. Single, standalone process — no migration to another host.
"""
use GenServer
require Logger
def start_link(_opts \\ []), do: GenServer.start_link(__MODULE__, {0, false}, name: __MODULE__)
def set_beep(on_off) when is_boolean(on_off), do: GenServer.cast(__MODULE__, {:set_beep, on_off})
@impl true
def init(state) do
set_interval(0)
{:ok, state}
end
@impl true
def handle_cast({:set_beep, on_off}, {n, _beep}), do: {:noreply, {n, on_off}}
@impl true
def handle_info(:wakeup, {n, beep}) do
{_r, _g, _b, w} = Luminance.read_rgbw()
Logger.info("n: #{n}, w: #{w}, beep: #{beep}")
mat =
PCA9622Matrix8x8.CharGen.digit_strlist(rem(n, 16), 16)
|> PCA9622Matrix8x8.CharGen.strlist_tuples()
PCA9622Matrix8x8.set_matrix(mat)
if beep do
pitch = w |> div(37) |> Kernel.+(1) |> min(7)
Buzzer.beep(pitch, 3, 2)
end
set_interval(1000)
{:noreply, {n + 1, beep}}
end
defp set_interval(ms), do: Process.send_after(self(), :wakeup, ms)
end
これは1秒ごとに以下をやります。
-
Luminance.read_rgbw/0で現在の照度(RGBW)を読む - カウンタを16進1桁(0〜F)としてLEDマトリックスに表示する
- ビープが有効なら、照度のW(Clear)成分から音程を計算して鳴らす
ロジックのキモになるところは以下です。
@impl true
def handle_info(:wakeup, {n, beep}) do
{_r, _g, _b, w} = Luminance.read_rgbw()
mat =
PCA9622Matrix8x8.CharGen.digit_strlist(rem(n, 16), 16)
|> PCA9622Matrix8x8.CharGen.strlist_tuples()
PCA9622Matrix8x8.set_matrix(mat)
if beep do
# Y2KB040Buzzer.beep/5 は pitch を 1..7 でガードしているので、
# 範囲外の値を渡さないようにここで丸める
pitch = w |> div(37) |> Kernel.+(1) |> min(7)
Buzzer.beep(pitch, 3, 2)
end
set_interval(1000)
{:noreply, {n + 1, beep}}
end
ビープの有効・無効は Worker.set_beep/1 に true/false を与えることで切り替えられるようにしてあります。明るい部屋だと高い音、手で照度センサを覆うと低い音になる、という単純なマッピングです。
デフォルトは無効にしてあります。音を出すときは ssh nerves.local で iex をいごかしてビープを有効にしてください。
iex(1)> Worker.set_beep(true)
:ok
開発を楽にする小ネタ: ホスト上でのI2Cモック
実機のBeagleBone Greenがなくても mix test や iex -S mix がホスト上で動くように、Phototaxis.MockI2C という Circuits.I2C.Backend の実装を用意しています。書き込みは全部受け流し、読み出しには「もっともらしいバイト列」を返すだけの簡単なモックです。
def fake_read(8) do
illuminance = Application.get_env(:phototaxis, :mock_illuminance, 100)
<<10::little-16, 10::little-16, 10::little-16, illuminance::little-16>>
end
Application.put_env(:phototaxis, :mock_illuminance, 0) のように照度の値を差し替えられるので、実機なしでもロジックのテスト(Workerがちゃんとカウントアップし続けるか、set_beep/1が状態を切り替えるか)が書けます。
実行
以下、X-twitter への投稿です。出来上がった動画だけ出したいのですが、スレッドが表示されてしまいます。
まとめ
照度センサ・ブザー・LEDマトリックスと3回に渡って個別に作ってきたElixir/Nervesドライバを、最後は薄いGenServerラッパーで足並みを揃えて1つのアプリにまとめました。