0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめてNerves(18) 照度センサ・ブザー・LEDマトリックスを1台にまとめる(デバイス3部作・集大成編)

0
Last updated at Posted at 2026-08-20

これまで3回に渡って、プレゼンのデモ用に作った「光ったり鳴ったりする」3つのデバイスをご紹介してきました。

今回はこの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ですが、Y2KB040BuzzerBH1745Luminance は素のI2C操作関数(open/closebeepread_rgbw)しか持っていないライブラリです。そこでこの2つには薄いGenServerラッパー(BuzzerLuminance)を被せて、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/1GenServer.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秒ごとに以下をやります。

  1. Luminance.read_rgbw/0 で現在の照度(RGBW)を読む
  2. カウンタを16進1桁(0〜F)としてLEDマトリックスに表示する
  3. ビープが有効なら、照度の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 testiex -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つのアプリにまとめました。

参考文献

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?