search
LoginSignup
0
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

#NervesJP Advent Calendar 2020 Day 19

posted at

updated at

NervesとPhonenix(Gigalixir)とGCP Cloud PubSubを使ってBBG CapeのLEDをチカした話〜NervesでSub編〜(2/2)

この記事は#NervesJP Advent Calendar 2020の19日目です。

18日目は @32hero さんの「Nerves+Phoenix 003 エムネチカ:分散型DB Mnesiaを使ってオリジナルCapeのLEDをエムネチカ」でした。


@kentaro さんからの パス(ウェブチカでElixir/Nervesに入門する(2020年12月版)) を受けて、kochi.exに所属する私がkochi.ex #4でお披露目させてもらった「NervesとPhonenix(Gigalixir)とGCP PubSubを使ってLチカ」した話を当日よりElixir成分多めで一昨日と今日の2回に分けて紹介します。

NervesだけでなくPhoenixやGCPの話が入ってくると1回でまとまらないことが多いんですよね。
ということで、2回に分けて投稿することにしました。

ちなみに明日は @piacerex さんの「WindowsからElixir IoT端末を作ってみた:「Raspberry Pi OS」を入れた後、Elixir IoTフレームワーク「Nerves」へ)」です。

概要(全体構成)

  • Elixir/PhoenixのHeroku風PaaSサービスのGigalixirにウェブページを作成し、GCP Cloud Pub/SubにメッセージをPublish
  • NervesをインストールしたBeagleBone Green(以下、BBG)でGCP Cloud Pub/Sub経由でSubscribeし、BBGに取り付けているCapeのLEDをON/OFF操作

kochi.exはRaspberry PiでなくBeagleBone Green推しなんです :dog2: :green_heart:

BBGに取り付けるCapeは半分(以上?!)kochi.exに足を突っ込んでもらっている myasu さんの多大なるご協力のもと、koxhi.exで作成したものになります。他にもBBGとCapeがピッタリはまるケースも3Dプリンターで作っていたりするのですが、それは別の機会に…。

今回は下の絵の右半分のことを書きます。

スクリーンショット 2020-11-20 15.54.45.png

Nervesの設定・実装

ここでようやくNervesが登場です。
NervesからPhoenixが起動するように設定して、PhoenixのLiveView画面でLEDの状態とSubされたテキストメッセージを確認できるようにします。

NervesからPhoenixを起動する

NervesからPhoenixを起動するための設定は以下のQiitaにあるので参考にしながらPoncho構成で設定します。

GCPからSubする

以降は、Ponchoで構成したPhoenixのプロジェクトディレクトリで操作します。
Phoenix側のプロジェクトディレクトリは ledlive としています。

❯ tree -L 1 led_live/
led_live/
├── exineris    ←Nervesのプロジェクトディレクトリ
└── ledlive     ←Phoenixのプロジェクトディレクトリ

依存関係の解消(mix deps.get)

mix.exsに利用するパッケージを追記し、mix deps.getコマンドを実行します。

mix.exs
defp deps() do
  ...
  {:circuits_gpio, "~> 0.4"},           # GPIO操作ライブラリ
  {:goth, "~> 1.1.0"},                  # Google認証ライブラリ
  {:google_api_pub_sub, "~> 0.28.1"},   # Google PubSubライブラリ
end
fish
❯ mix deps.get

GCP鍵ファイル読み込み設定

config/config.exsにGCPの鍵ファイルを読み込む設定を追記します。

見返してみると、どうして当時はlibディレクトリに置いてしま(略

config/config.exs
config :goth,
  json: "./lib/gcp/key/project-name-key.json" |> File.read!

LiveView用のスコープを設定(lib/ledlive_web/router.ex)

lib/ledlive_web/router.exにLiveView用のスコープを追記します。

ちなみに bbb となっているのは、RaspberryPiではなくBeagleBone Greenを使っていて、 MIX_TARGET=bbb としているからです(いつも使っている fishだと環境変数の書き方違いますがー)。

lib/ledlive_web/router.ex
  scope "/", LedliveWeb do
    pipe_through :browser

    live "/", PageLive, :index
    live "/bbb", BbbLive    # ←この行を追記
  end

Subするライブラリを作成(lib/ledlive/exineris_gcp_pubsub_subscripton.ex)

GCPのPubSubからSubscriptionするモジュールです。
利用したいモジュールでGoogleの プロジェクトIDサブスクリプション名 を指定してスーパーバイザーを起動すると、handle_info/2でSubscriptionされた値を取得できます。

  {:ok, _task} = Exineris.GCP.PubSub.Subscription.start_supervisor(self(), :process_name, "プロジェクトID", "サブスクリプション名")

  def handle_info({:process_name, subscription_message}, state) do
    ...
  end
lib/ledlive/exineris_gcp_pubsub_subscripton.ex
defmodule Exineris.GCP.PubSub.Subscription do
  use Task
  require Logger

  @spec start_supervisor(pid, atom, String.t, String.t) :: {:ok, pid}
  def start_supervisor(pid, atom_name, project_id, subscription_name) do
    import Supervisor.Spec

    children = [
      supervisor(Exineris.GCP.PubSub.Subscription, [pid, atom_name, project_id, subscription_name])
    ]
    opts = [strategy: :one_for_one, name: Exineris.GCP.PubSub.Subscription.Supervisor]

    Supervisor.start_link(children, opts)
  end

  def start_link(pid, atom_name, project_id, subscription_name) do
    task = Task.async(Exineris.GCP.PubSub.Subscription, :listen, [pid, atom_name, project_id, subscription_name])
    {:ok, task.pid}
  end

  def listen(pid, atom_name, project_id, subscription_name) do
    Logger.debug("#{__MODULE__} listen: #{inspect(atom_name)}, #{inspect(project_id)}, #{inspect(subscription_name)}")

    # Authenticate
    {:ok, token} = Goth.Token.for_scope("https://www.googleapis.com/auth/cloud-platform")
    conn = GoogleApi.PubSub.V1.Connection.new(token.token)

    # Make a subscription pull
    {:ok, response} = GoogleApi.PubSub.V1.Api.Projects.pubsub_projects_subscriptions_pull(
      conn,
      project_id,
      subscription_name,
      [body: %GoogleApi.PubSub.V1.Model.PullRequest{
        maxMessages: 10
      }]
    )

    # Acknowledge the message was received & send message
    if response.receivedMessages != nil do
      Enum.each(response.receivedMessages, fn message ->
        GoogleApi.PubSub.V1.Api.Projects.pubsub_projects_subscriptions_acknowledge(
          conn,
          project_id,
          subscription_name,
          [body: %GoogleApi.PubSub.V1.Model.AcknowledgeRequest{
            ackIds: [message.ackId]
          }]
        )
        send(pid, {atom_name, Base.decode64!(message.message.data)})
      end)
    end

    listen(pid, atom_name, project_id, subscription_name)
  end

end

GPIOを操作するライブラリを作成(lib/ledlive/gpioinout.ex)

GPIOの操作はkochi.exな @kikuyuta さんのはじめてNerves(8) 単一ホストで動くシステムを複数ホストに分散する / GPIO 操作の基本モジュールにあるgpioinout.exを使っています。

lib/ledlive/gpioinout.ex
defmodule GpioInOut do
  @behaviour GenServer
#  use GenServer
  require Circuits.GPIO
  require Logger

  def start_link(pname, gpio_no, in_out, ppid \\ []) do
    Logger.debug("#{__MODULE__} start_link: #{inspect(pname)}, #{gpio_no}, #{in_out} #{inspect(ppid)}")
    GenServer.start_link(__MODULE__, {gpio_no, in_out, ppid}, 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 = :input, ppid}) do
    Logger.debug("#{__MODULE__} init_open: #{gpio_no}, #{in_out} ")
    {:ok, gpioref} = Circuits.GPIO.open(gpio_no, in_out)
    Circuits.GPIO.set_interrupts(gpioref, :both, receiver: ppid)
    {:ok, gpioref}
  end

  @impl GenServer
  def init({gpio_no, in_out = :output, _ppid}) do
    Logger.debug("#{__MODULE__} init_open: #{gpio_no}, #{in_out} ")
    Circuits.GPIO.open(gpio_no, in_out)
  end

  @impl GenServer
  def handle_cast({:write, val}, gpioref) do
#    Logger.debug("#{__MODULE__} :write #{val} ")
    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 handle_info(msg, gpioref) do
    Logger.warn("#{__MODULE__} get_message: #{inspect(msg)}")
    Circuits.GPIO.set_interrupts(gpioref, :both)
    {:noreply, gpioref}
  end

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

Sub確認用HTMLを作成(lib/ledlive_web/live/bbb_live.html.leex)

Subしたメッセージを確認するHTMLを作成します。

  • @sub_message:Subscriptionされたメッセージを表示します
  • @ai0@ai3:Capeから取得できるアナログインタフェースの情報を表示します(今回のPubSubに関係ありません)
  • @x0@x3:Capeから取得できるデジタルインタフェース(入力)の情報を表示します(今回のPubSubに関係ありません)
  • @y0_click@y3_click:Capeから取得できるLEDインタフェースの情報を表示します
lib/ledlive_web/live/bbb_live.html.leex
<h2>LEDの操作と状態(bbb)</h2>

<h3 style="margin-top: 2em;">GCP Subscription Message</h3>
<div>
    Subscription Message: <%= @sub_message %>
</div>


<h3 style="margin-top: 2em;">Analog Input</h3>
<div>
    AI0: <%= @ai0 %><br />
    AI1: <%= @ai1 %><br />
    AI2: <%= @ai2 %><br />
    AI3: <%= @ai3 %><br />
</div>

<h3 style="margin-top: 2em;">Input</h3>
<div>
    X0: <%= @x0 %><br />
    X1: <%= @x1 %><br />
    X2: <%= @x2 %><br />
    X3: <%= @x3 %><br />
</div>

<h3 style="margin-top: 2em;">Output</h3>
<div>
    Y0: &nbsp;&nbsp;
    <button phx-click="y0_click_on">on</button>
    <button phx-click="y0_click_off">off</button>
    <%= @y0_click %>
</div>
<div>
    Y1: &nbsp;&nbsp;
    <button phx-click="y1_click_on">on</button>
    <button phx-click="y1_click_off">off</button>
    <%= @y1_click %>
</div>
<div>
    Y2: &nbsp;&nbsp;
    <button phx-click="y2_click_on">on</button>
    <button phx-click="y2_click_off">off</button>
    <%= @y2_click %>
</div>
<div>
    Y3: &nbsp;&nbsp;
    <button phx-click="y3_click_on">on</button>
    <button phx-click="y3_click_off">off</button>
    <%= @y3_click %>
</div>

Subするプログラムを作成(lib/ledlive_web/live/bbb_live.ex)

GigalixirのPhoenixからPubされたメッセージを受け取ってLEDを操作する処理を書きます。

  • @project_id GCPのプロジェクトIDを記載
  • @subscription_name GCPのサブスクリプション名を記載
lib/ledlive_web/live/bbb_live.ex
defmodule LedliveWeb.BbbLive do
  use Phoenix.LiveView
  require Logger

  @analog_polling_msec 100
  @ai_in_0 "/sys/bus/iio/devices/iio:device0/in_voltage0_raw"
  @ai_in_1 "/sys/bus/iio/devices/iio:device0/in_voltage1_raw"
  @ai_in_2 "/sys/bus/iio/devices/iio:device0/in_voltage2_raw"
  @ai_in_3 "/sys/bus/iio/devices/iio:device0/in_voltage3_raw"

  @pname :gcp_sub
  @project_id "PROJECT_ID"
  @subscription_name "bushukan"

  # CapeのGPIO
  @gpio_in_x0 5
  @gpio_in_x1 48
  @gpio_in_x2 31
  @gpio_in_x3 30
  @gpio_out_y0 60
  @gpio_out_y1 50
  @gpio_out_y2 51
  @gpio_out_y3 4

  def mount(_param, _session, socket) do
    if connected?(socket) do
      GpioInOut.start_link(:gpio_in_x0, @gpio_in_x0, :input, self())
      GpioInOut.start_link(:gpio_in_x1, @gpio_in_x1, :input, self())
      GpioInOut.start_link(:gpio_in_x2, @gpio_in_x2, :input, self())
      GpioInOut.start_link(:gpio_in_x3, @gpio_in_x3, :input, self())
      GpioInOut.start_link(:gpio_out_y0, @gpio_out_y0, :output)
      GpioInOut.start_link(:gpio_out_y1, @gpio_out_y1, :output)
      GpioInOut.start_link(:gpio_out_y2, @gpio_out_y2, :output)
      GpioInOut.start_link(:gpio_out_y3, @gpio_out_y3, :output)
      Process.send_after(self(), :analog_polling, @analog_polling_msec)

      {:ok, _task} = Exineris.GCP.PubSub.Subscription.start_supervisor(self(), @pname, @project_id, @subscription_name)
    end

    socket = assign(
      socket,
      ai0: "",
      ai1: "",
      ai2: "",
      ai3: "",
      x0: "...",
      x1: "...",
      x2: "...",
      x3: "...",
      y0_click: "",
      y1_click: "",
      y2_click: "",
      y3_click: "",
      sub_message: ""
    )

    {:ok, socket}
  end

  ### GCP Subした場合の処理
  def handle_info({@pname, sub_message}, socket) when sub_message == "Y0:on" do
    Logger.debug("#{__MODULE__} sub_message: #{inspect(sub_message)}")
    GpioInOut.write(:gpio_out_y0, 1)
    socket = assign(socket, y0_click: "on", sub_message: sub_message)
    {:noreply, socket}
  end

  def handle_info({@pname, sub_message}, socket) when sub_message == "Y0:off" do
    Logger.debug("#{__MODULE__} sub_message: #{inspect(sub_message)}")
    GpioInOut.write(:gpio_out_y0, 0)
    socket = assign(socket, y0_click: "off", sub_message: sub_message)
    {:noreply, socket}
  end

  def handle_info({@pname, sub_message}, socket) when sub_message == "Y1:on" do
    Logger.debug("#{__MODULE__} sub_message: #{inspect(sub_message)}")
    GpioInOut.write(:gpio_out_y1, 1)
    socket = assign(socket, y1_click: "on", sub_message: sub_message)
    {:noreply, socket}
  end

  def handle_info({@pname, sub_message}, socket) when sub_message == "Y1:off" do
    Logger.debug("#{__MODULE__} sub_message: #{inspect(sub_message)}")
    GpioInOut.write(:gpio_out_y1, 0)
    socket = assign(socket, y1_click: "off", sub_message: sub_message)
    {:noreply, socket}
  end

  def handle_info({@pname, sub_message}, socket) when sub_message == "Y2:on" do
    Logger.debug("#{__MODULE__} sub_message: #{inspect(sub_message)}")
    GpioInOut.write(:gpio_out_y2, 1)
    socket = assign(socket, y2_click: "on", sub_message: sub_message)
    {:noreply, socket}
  end

  def handle_info({@pname, sub_message}, socket) when sub_message == "Y2:off" do
    Logger.debug("#{__MODULE__} sub_message: #{inspect(sub_message)}")
    GpioInOut.write(:gpio_out_y2, 0)
    socket = assign(socket, y2_click: "off", sub_message: sub_message)
    {:noreply, socket}
  end

  def handle_info({@pname, sub_message}, socket) when sub_message == "Y3:on" do
    Logger.debug("#{__MODULE__} sub_message: #{inspect(sub_message)}")
    GpioInOut.write(:gpio_out_y3, 1)
    socket = assign(socket, y3_click: "on", sub_message: sub_message)
    {:noreply, socket}
  end

  def handle_info({@pname, sub_message}, socket) when sub_message == "Y3:off" do
    Logger.debug("#{__MODULE__} sub_message: #{inspect(sub_message)}")
    GpioInOut.write(:gpio_out_y3, 0)
    socket = assign(socket, y3_click: "off", sub_message: sub_message)
    {:noreply, socket}
  end

  def handle_info({@pname, sub_message}, socket) do
    Logger.debug("#{__MODULE__} sub_message: #{inspect(sub_message)}")
    socket = assign(socket, sub_message: sub_message)
    {:noreply, socket}
  end

  ### Capeのアナログ値を取得・表示する処理
  def handle_info(:analog_polling, socket) do
    Process.send_after(self(), :analog_polling, @analog_polling_msec)

    {:ok, ai0} = File.read(@ai_in_0)
    {:ok, ai1} = File.read(@ai_in_1)
    {:ok, ai2} = File.read(@ai_in_2)
    {:ok, ai3} = File.read(@ai_in_3)

    socket = assign(socket, ai0: ai0, ai1: ai1, ai2: ai2, ai3: ai3)
    {:noreply, socket}
  end

  ### Capeのデジタル値を取得・表示する処理
  def handle_info({:circuits_gpio, @gpio_in_x0, _time, on_off}, socket) do
    socket = assign(socket, x0: on_off)
    {:noreply, socket}
  end

  def handle_info({:circuits_gpio, @gpio_in_x1, _time, on_off}, socket) do
    socket = assign(socket, x1: on_off)
    {:noreply, socket}
  end

  def handle_info({:circuits_gpio, @gpio_in_x2, _time, on_off}, socket) do
    socket = assign(socket, x2: on_off)
    {:noreply, socket}
  end

  def handle_info({:circuits_gpio, @gpio_in_x3, _time, on_off}, socket) do
    socket = assign(socket, x3: on_off)
    {:noreply, socket}
  end

  ### Phoenixの画面からLEDを操作した場合の処理
  def handle_event("y0_click_on", _, socket) do
    GpioInOut.write(:gpio_out_y0, 1)
    socket = assign(socket, y0_click: "on")
    {:noreply, socket}
  end

  def handle_event("y0_click_off", _, socket) do
    GpioInOut.write(:gpio_out_y0, 0)
    socket = assign(socket, y0_click: "off")
    {:noreply, socket}
  end

  def handle_event("y1_click_on", _, socket) do
    GpioInOut.write(:gpio_out_y1, 1)
    socket = assign(socket, y1_click: "on")
    {:noreply, socket}
  end

  def handle_event("y1_click_off", _, socket) do
    GpioInOut.write(:gpio_out_y1, 0)
    socket = assign(socket, y1_click: "off")
    {:noreply, socket}
  end

  def handle_event("y2_click_on", _, socket) do
    GpioInOut.write(:gpio_out_y2, 1)
    socket = assign(socket, y2_click: "on")
    {:noreply, socket}
  end

  def handle_event("y2_click_off", _, socket) do
    GpioInOut.write(:gpio_out_y2, 0)
    socket = assign(socket, y2_click: "off")
    {:noreply, socket}
  end

  def handle_event("y3_click_on", _, socket) do
    GpioInOut.write(:gpio_out_y3, 1)
    socket = assign(socket, y3_click: "on")
    {:noreply, socket}
  end

  def handle_event("y3_click_off", _, socket) do
    GpioInOut.write(:gpio_out_y3, 0)
    socket = assign(socket, y3_click: "off")
    {:noreply, socket}
  end
end

確認

前回作成したGigalixirの画面から手元のNervesのLEDを操作することができました!

@piacerex さんのTwitterを拝借 :wink:

明日は、 @piacerex さんの「WindowsからElixir IoT端末を作ってみた:「Raspberry Pi OS」を入れた後、Elixir IoTフレームワーク「Nerves」へ)」です。

そろそろNeosVRで遊んでみたくなってきてます〜。

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
What you can do with signing up
0
Help us understand the problem. What are the problem?