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

この記事は[#NervesJP Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nervesjp)の19日目です。

18日目は @32hero さんの「[Nerves＋Phoenix 003　エムネチカ：分散型DB Mnesiaを使ってオリジナルCapeのLEDをエムネチカ](https://qiita.com/32hero/items/b7fb8409f3f6d0a60f66)」でした。

----

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

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

- [NervesとPhonenix(Gigalixir)とGCP Cloud PubSubを使ってBBG CapeのLEDをチカした話〜Phoenix/GCPでPub編〜（1/2）](https://qiita.com/nishiuchikazuma/items/6c537342f8815728f69d)
- [NervesとPhonenix(Gigalixir)とGCP Cloud PubSubを使ってBBG CapeのLEDをチカした話〜NervesでSub編〜（2/2）](https://qiita.com/nishiuchikazuma/items/d0d1891349ea5b97a3cc)　←いまここ



ちなみに明日は @piacerex さんの「[WindowsからElixir IoT端末を作ってみた：「Raspberry Pi OS」を入れた後、Elixir IoTフレームワーク「Nerves」へ）](https://qiita.com/piacerex/items/98e1bde676263f5f9f81)」です。


# 概要（全体構成）

- 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がピッタリはまるケースも３Dプリンターで作っていたりするのですが、それは別の機会に…。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">夏休みの <a href="https://twitter.com/hashtag/NervesJP?src=hash&amp;ref_src=twsrc%5Etfw">#NervesJP</a> 工作。まだ途中なので月末までに終わらせる。<br>My <a href="https://twitter.com/hashtag/Nerves?src=hash&amp;ref_src=twsrc%5Etfw">#Nerves</a> craft work of summer vacation. Still manufacturing so I will finish it by the end of this month. <a href="https://t.co/vnzWFheTCF">pic.twitter.com/vnzWFheTCF</a></p>&mdash; KIKUCHI Yutaka 🌗 菊池 豊 (@kikuyuta) <a href="https://twitter.com/kikuyuta/status/1295371740097347585?ref_src=twsrc%5Etfw">August 17, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


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

![スクリーンショット 2020-11-20 15.54.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51020/b4ebf7d1-b500-4ba2-70ef-c0632696780d.png)


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


## NervesからPhoenixを起動する

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

- [NervesにPhoenixを入れてHTTPのGETでLEDをウェブチカ〜準備編1/3〜](https://qiita.com/nishiuchikazuma/items/bfd04b36fb4524d49a59)
- [NervesにPhoenixを入れてHTTPのGETでLEDをウェブチカ〜Nerves設定編2/3〜](https://qiita.com/nishiuchikazuma/items/175fdc4a646cbbfcc04d)
- [NervesにPhoenixを入れてHTTPのGETでLEDをウェブチカ〜Phoenix設定・確認編3/3〜](https://qiita.com/nishiuchikazuma/items/609c3faa4cfa36b77e90)


## GCPからSubする

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

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


### 依存関係の解消（mix deps.get）

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

```elixir: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
```

```shell:fish
❯ mix deps.get
```

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

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

```elixir: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`だと環境変数の書き方違いますがー）。

```elixir: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された値を取得できます。


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

  def handle_info({:process_name, subscription_message}, state) do
    ...
  end
```

```elixir: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 操作の基本モジュール](https://qiita.com/kikuyuta/items/4b8326d0584fe26083a6#gpio-%E6%93%8D%E4%BD%9C%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB)にある`gpioinout.ex`を使っています。


```elixir: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インタフェースの情報を表示します


```leex: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のサブスクリプション名を記載

```elixir: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: 

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">kochi.ex最初のセッションは、GCP PubSub経由でWeb（LiveView製）←→LED付きデバイスのLチカ😜 <a href="https://twitter.com/hashtag/kochiex?src=hash&amp;ref_src=twsrc%5Etfw">#kochiex</a><br><br>左のWeb画面のボタンをクリックすると、LED付きデバイスのLEDを付けたり、消したりできる😌<br><br>こりゃおもろい😙 <a href="https://t.co/mG4QjORcJJ">pic.twitter.com/mG4QjORcJJ</a></p>&mdash; piacere@DigiDock (love Elixir&amp;Gravity＋仮想世界創造機構) (@piacere_ex) <a href="https://twitter.com/piacere_ex/status/1322118825127469058?ref_src=twsrc%5Etfw">October 30, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


明日は、 @piacerex さんの「[WindowsからElixir IoT端末を作ってみた：「Raspberry Pi OS」を入れた後、Elixir IoTフレームワーク「Nerves」へ）](https://qiita.com/piacerex/items/98e1bde676263f5f9f81)」です。

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