8
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?

More than 3 years have passed since last update.

#NervesJPAdvent Calendar 2020

Day 19

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

Last updated at Posted at 2020-12-18

この記事は#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で遊んでみたくなってきてます〜。

8
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
8
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?