ElixirでGUI・Scenic [入門編２]

#１．はじめに

[入門編１](https://qiita.com/myasu/items/86a2038d7a8a5bf30cf6)では、ElixirのGUIライブラリ「[Scenic](https://github.com/boydm/scenic)」を使って、簡単なウィンドウアプリを作ってみました。
今回は、GUIのプロジェクト`hmi`と、別のプロジェクト`plc`を用意して、`plc`から定期的に画面更新をしてみます。

（作例）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/fc1b55f2-829e-af1d-a00d-4bab060c91bb.png)


###シリーズ

|ステップ|概要|
|:--|:--|
|[入門１](https://qiita.com/myasu/items/86a2038d7a8a5bf30cf6)|プロジェクトの作り方、デザイン、ボタンイベント|
|入門２|複数プロジェクト間のイベント通信|
（続くかも･･･）

<!--
|応用１|制御HMI|Elixir.Circuitsとの連携|
|応用２||レイアウトを綺麗に|
|実践１|制御HMI|電動シリンダの制御|
-->

###実行環境

| | |
|:--|:--|
|ハードウェア|VirtualBox for Windows|
|OS|Ubuntu Linux 20.04 LTS|
|Elixir|Ver.1.9.1|

```sh:コマンドライン
$ uname -a
Linux ubuntuvb 5.4.0-40-generic #44-Ubuntu SMP Tue Jun 23 00:01:04 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
$ elixir -v
Erlang/OTP 22 [erts-10.6.4] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1]
Elixir 1.9.1 (compiled with Erlang/OTP 22)
```

#２．プロジェクトを作成

##(1)アンブレラ・プロジェクトを作成
複数のプロジェクトをまとめて管理できるアンブレラ・プロジェクトを作成します。

```sh:コマンドライン
$ cd （ご自身のワーキングディレクトリに移動）

#--umbrellaオプションを付けてnewします
$ mix new hmiplc --umbrella
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating apps
* creating config
* creating config/config.exs

Your umbrella project was created successfully.
Inside your project, you will find an apps/ directory
where you can create and host many apps:

    cd hmiplc
    cd apps
    mix new my_app

Commands like "mix compile" and "mix test" when executed
in the umbrella project root will automatically run
for each application in the apps/ directory.
```

###(2)個別のプロジェクトを作成

`apps/`ディレクトリに、２つのプロジェクトを作成します。

- hmi ：ScenicのGUIプロジェクト
- plc ：GUIを外部から更新するプロジェクト

```sh:コマンドライン
# アンブレラ・プロジェクトの中身を確認
$ cd hmiplc
$ ls
README.md  apps  config  mix.exs

# appsディレクトリに移動
$ cd apps

#GUIのプロジェクトを作る
$ mix scenic.new hmi

#GUIを外部から更新するプロジェクトを作る
$ mix new plc --sup

#一つ上のディレクトリ（hmiplc）に戻る
$ cd ..

#いつもの操作
$ mix deps.get
```

この時点で、こんな感じになってると思います。（細々したディレクトリは省略）

```
gitwork\
 |
 + hmiplc\      ← iex を起動するときに居るディレクトリ
    |
    + app\
       + hmi\   ← ウィンドウのプロジェクト
       + plc\   ← 定期実行のプロジェクト
```

###(3)一度実行、ちょっと修正

```sh:コマンドライン
$ iex -S mix
Erlang/OTP 22 [erts-10.6.4] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1]

==> msgpax
Compiling 8 files (.ex)
Generated msgpax app
（･･･省略･･･）
Compiling 2 files (.ex)
Generated hmi app
** (Mix) Could not start application hmi: Hmi.start(:normal, []) returned an error: shutdown: failed to start child: Scenic
    ** (EXIT) shutdown: failed to start child: Scenic.ViewPort.SupervisorTop
        ** (EXIT) an exception was raised:
            ** (FunctionClauseError) no function clause matching in Scenic.ViewPort.Config.valid!/1
                (scenic) lib/scenic/view_port/config.ex:89: Scenic.ViewPort.Config.valid!(nil)
                (scenic) lib/scenic/view_port/supervisor_top.ex:27: anonymous fn/1 in Scenic.ViewPort.SupervisorTop.init/1
                (elixir) lib/enum.ex:1336: Enum."-map/2-lists^map/1-0-"/2
                (scenic) lib/scenic/view_port/supervisor_top.ex:26: Scenic.ViewPort.SupervisorTop.init/1
                (stdlib) supervisor.erl:295: :supervisor.init/1
                (stdlib) gen_server.erl:374: :gen_server.init_it/2
                (stdlib) gen_server.erl:342: :gen_server.init_it/6
                (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
$
```

このような感じで、エラーが出てしまいます。

アンブレラ・プロジェクトを実行するときに参照する`confis.exs`は、`hmiplc/config/config.exs`の方が参照されるため、こちらにScenicの設定を移す必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/0793bd87-dc03-cfd9-75cc-038697a598e9.png)

`hmiplc/apps/hmi/config/config.exs`から`hmiplc/config/config.exs`へ、まるまるコピペします。

```elixir:hmiplc/config.config.exs
（･･･省略･･･）
# Configure the main viewport for the Scenic application

#ここから追記（元は hmiplc/apps/hmi/config/config.exsの内容）
config :hmi, :viewport, %{
  name: :main_viewport,
  size: {700, 600},
  default_scene: {Hmi.Scene.Home, nil},
  drivers: [
    %{
      module: Scenic.Driver.Glfw,
      name: :glfw,
      opts: [resizeable: false, title: "hmi"]
    }
  ]
}
```

追記してから改めて実行すると、ウィンドウが表示されます。

```sh:コマンドライン
$ iex -S mix
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/646bf655-fafe-f2e8-7108-84af85948a04.png)


#３．各プロジェクトの編集

##(1)GUIのプロジェクト`hmi`


依存関係の設定に、`scenic_sensor`を追加します。

```elixir:hmiplc/apps/hmi/mix.exs
（･･･省略･･･）
  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:scenic, "~> 0.10"},
      {:scenic_driver_glfw, "~> 0.10", targets: :host},
      # 追加
      {:scenic_sensor, "~> 0.7"},
    ]
  end
```

ウィンドウのソースを編集します。

```elixir:hmiplc/apps/hmi/lib/scenes/home.ex
defmodule Hmi.Scene.Home do
  use Scenic.Scene
  require Logger

  alias Scenic.Graph
  alias Scenic.ViewPort

  import Scenic.Primitives
  #コメントアウトを取る
  import Scenic.Components
  # 更に追加
  alias Scenic.Sensor

  @text_size 24

  # ============================================================================
  # setup

  # --------------------------------------------------------
  def init(_, opts) do
    # get the width and height of the viewport. This is to demonstrate creating
    # a transparent full-screen rectangle to catch user input
    {:ok, %ViewPort.Status{size: {width, height}}} = ViewPort.info(opts[:viewport])

    # show the version of scenic and the glfw driver
    scenic_ver = Application.spec(:scenic, :vsn) |> to_string()
    glfw_ver = Application.spec(:scenic, :vsn) |> to_string()

    graph =
      Graph.build(font: :roboto, font_size: @text_size)
      |> add_specs_to_graph([

        # ここから追記
        #（今回のサンプルでは、表示だけで、イベントの取得まではしていません）
        text_spec("Hello World", t: {40, 40}, font_size: 30),
        circle_spec(10, fill: :blue, stroke: {2, :white}, t: {20, 30}),
        circle_spec(10, fill: :yellow, stroke: {2, :red}, t: {190, 30}),
        line_spec({{10, 50}, {200, 50}}, stroke: {4, :cyan}, cap: :round),
        text_spec("---", t: {80, 80}, font_size: 30, id: :event_text),
        circle_spec(10, fill: :grey, stroke: {2, :white}, t: {60, 70}, id: :event_circle),
        button_spec("ON", id: :btn_on, t: {40, 90}, theme: :success),
        button_spec("OFF", id: :btn_off, t: {120, 90}, theme: :danger),

        # さらにこれも追加（ランプの点滅用）
        circle_spec(15, fill: :grey, stroke: {3, :white}, t: {60, 170}, id: :event_lamp)
      ])

    # Sensorの受信許可
    Sensor.subscribe(:message_lamp)
    Logger.info("#{__MODULE__} / Init Sensor.subscribe: :message_lamp")

    {:ok, graph, push: graph}
  end

  #ここを追記
  @doc """
  Sensorの受信
  """
  def handle_info({:sensor, :data, {:message_lamp, val, _}}, graph) do
    graph =
      case {val} do
        #ランプを黄色く点灯
        {1} -> Graph.modify(graph, :event_lamp, &update_opts(&1, fill: :yellow))
        #ランプを灰色に消灯
        {_} -> Graph.modify(graph, :event_lamp, &update_opts(&1, fill: :grey))
      end

    Logger.info("#{__MODULE__} / Received event: :event_lamp #{val}")
    {:noreply, graph, push: graph}
  end

  # （ここはプロジェクトをScenic.newしたときに自動的に作られます）
  def handle_input(event, _context, state) do
    Logger.info("#{__MODULE__} / Received event: #{inspect(event)}")
    {:noreply, state}
  end
end
```

|キーワード|説明|
|:--|:--|
|id: :event_lamp|（ランプに見立てた）丸図形を書き換えるときのID|
|:message_lamp|SensorのメッセージをやりとりするときのID|



##(2)GUIを外部から更新するプロジェクト`plc`

application.exを一部編集します。

```elixir:hmiplc/apps/plc/lib/plc/application.ex
defmodule Plc.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      # Starts a worker by calling: Plc.Worker.start_link(arg)
      # {Plc.Worker, arg}

      # 追記
      Scenic.Sensor,
      # 追記
      {Auto, 1}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Plc.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
```

`auto.ex`ファイルを作成して、下記の通りソースを入力します。

```elixir:hmiplc/apps/plc/lib/plc/auto.ex
defmodule Auto do
  use GenServer

  #追記
  require Logger
  #追記：Sensor
  alias Scenic.Sensor

  # センサの情報
  @version "0.1"
  @description "message"

  def start_link(state) do
    IO.puts("start_link/1 call")
    Logger.info("#{__MODULE__} / start_link")
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  @doc """
  初期化
  """
  def init(state) do
    # Sensorを登録
    Sensor.register(:message_lamp, @version, "PL Driver")
    Logger.info("#{__MODULE__} / Init Sensor.register: :message_lamp")

    # 定期実行を開始
    schedule_work1()
    {:ok, state}
  end

  @doc """
  定期実行の初期化
  """
  def schedule_work1 do
    # １秒後にイベント発生
    Process.send_after(self(), :work1, 1000)
  end

  @doc """
  定期実行の処理
  """
  def handle_info(:work1, current_num) do
    # ランプの点滅処理（ここを通る度に、値を反転）
    current_num =
      case {current_num} do
        {1} -> 0
        {_} -> 1
      end

    # メッセージを送信
    Sensor.publish(:message_lamp, current_num)
    Logger.info("#{__MODULE__} / Send event: :message_lamp #{inspect(current_num)}")

    # 定期実行の初期化
    schedule_work1()
    {:noreply, current_num}
  end
end
```

###補足

冒頭にこういう記述があります。

```elixir
  # センサの情報
  @version "0.1"
  @description "message"
```

Scenicは、IoTとかの用途も想定し作られているようで、装置外部に接続するセンサー類のプロファイルを、このような形で記述出来るようになっています。

#### [list/0](https://hexdocs.pm/scenic_sensor/Scenic.Sensor.html#list/0)

"接続しているセンサーの一覧"を表示します。

```elixir
iex(1)> Scenic.Sensor.list
[{:message_lamp, "0.1", "PL Driver", #PID<0.199.0>}]
```

↑　先ほどのマクロは、この表示に使われます。

#### [get/1](https://hexdocs.pm/scenic_sensor/Scenic.Sensor.html#get/1)

指定のIDのセンサーの値を、直接取得します。

```elixir
iex(2)> Scenic.Sensor.get:message_lamp) 
{:ok, {:message_lamp, 1, 1595105786907899}}
iex(3)> Scenic.Sensor.get(:message_lamp)
{:ok, {:message_lamp, 0, 1595105873189069}}
```

|||
|:--|:--|
|第１引数|センサーのID|
|第２引数|センサーの値|
|第３引数|最後にpublishしたときの時刻|

##(3)実行

```sh:コマンドライン
#依存関係を編集したので、再度実行
$ mix deps.get

#実行
$ iex -S mix
Erlang/OTP 22 [erts-10.6.4] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1]

start_link/1 call
20:15:04.855 [info]  Elixir.Auto / start_link
20:15:04.858 [info]  Elixir.Auto / Init Sensor.register: :message_lamp
Interactive Elixir (1.9.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 
20:15:05.025 [info]  Elixir.Hmi.Scene.Home / Init Sensor.subscribe: :message_lamp
20:15:05.859 [info]  Elixir.Hmi.Scene.Home / Received event: :event_lamp 0
20:15:05.861 [info]  Elixir.Auto / Send event: :message_lamp 0
20:15:06.862 [info]  Elixir.Auto / Send event: :message_lamp 1
20:15:06.863 [info]  Elixir.Hmi.Scene.Home / Received event: :event_lamp 1
20:15:07.863 [info]  Elixir.Auto / Send event: :message_lamp 0
（･･･省略･･･）
```

##ウィンドウの様子

ランプに見立てた丸が、１秒おきに黄色で点滅します。

**黄色点灯**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/fc1b55f2-829e-af1d-a00d-4bab060c91bb.png)

**（１秒後）消灯**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145581/c8ba51d5-fb4e-eab3-8a5d-76f7e614dc75.png)

これを繰り返します。
（アニメーションGIFでなくてすみません・・・）

#５．まとめ

二回に分けて、ScenicでGUIを作る手順を紹介しました。

次は実践的な使い方を試してみようと思います。（ちょっと待ってね^^

#７．参考資料

- https://hexdocs.pm/scenic/
- https://github.com/boydm/scenic/
- https://hexdocs.pm/scenic_sensor/


<!--
次回は並行処理、アンブレラプロジェクト。

[ScenicLiveReload](https://github.com/axelson/scenic_live_reload)

-->