11
6

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.

ElixirでGUI・Scenic [入門編2]

Last updated at Posted at 2020-07-16

#1.はじめに

入門編1では、ElixirのGUIライブラリ「Scenic」を使って、簡単なウィンドウアプリを作ってみました。
今回は、GUIのプロジェクトhmiと、別のプロジェクトplcを用意して、plcから定期的に画面更新をしてみます。

(作例)
image.png

###シリーズ

ステップ 概要
入門1 プロジェクトの作り方、デザイン、ボタンイベント
入門2 複数プロジェクト間のイベント通信
(続くかも・・・)

###実行環境

ハードウェア VirtualBox for Windows
OS Ubuntu Linux 20.04 LTS
Elixir Ver.1.9.1
コマンドライン
$ 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)

#2.プロジェクトを作成

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

コマンドライン
$ 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/ディレクトリに、2つのプロジェクトを作成します。

  • hmi :ScenicのGUIプロジェクト
  • plc :GUIを外部から更新するプロジェクト
コマンドライン
# アンブレラ・プロジェクトの中身を確認
$ 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)一度実行、ちょっと修正

コマンドライン
$ 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

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

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"]
    }
  ]
}

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

コマンドライン
$ iex -S mix

image.png

#3.各プロジェクトの編集

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

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

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

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

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を一部編集します。

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ファイルを作成して、下記の通りソースを入力します。

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
    # 1秒後にイベント発生
    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

###補足

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

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

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

list/0

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

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

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

get/1

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

iex(2)> Scenic.Sensor.get:message_lamp) 
{:ok, {:message_lamp, 1, 1595105786907899}}
iex(3)> Scenic.Sensor.get(:message_lamp)
{:ok, {:message_lamp, 0, 1595105873189069}}
第1引数 センサーのID
第2引数 センサーの値
第3引数 最後にpublishしたときの時刻

##(3)実行

コマンドライン
#依存関係を編集したので、再度実行
$ 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
(・・・省略・・・)

##ウィンドウの様子

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

黄色点灯

image.png

(1秒後)消灯

image.png

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

#5.まとめ

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

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

#7.参考資料

11
6
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
11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?