#1.はじめに
入門編1では、ElixirのGUIライブラリ「Scenic」を使って、簡単なウィンドウアプリを作ってみました。
今回は、GUIのプロジェクトhmi
と、別のプロジェクトplc
を用意して、plc
から定期的に画面更新をしてみます。
###シリーズ
ステップ | 概要 |
---|---|
入門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の設定を移す必要があります。
hmiplc/apps/hmi/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
#3.各プロジェクトの編集
##(1)GUIのプロジェクトhmi
依存関係の設定に、scenic_sensor
を追加します。
(・・・省略・・・)
# 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
ウィンドウのソースを編集します。
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を一部編集します。
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
ファイルを作成して、下記の通りソースを入力します。
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秒おきに黄色で点滅します。
黄色点灯
(1秒後)消灯
これを繰り返します。
(アニメーションGIFでなくてすみません・・・)
#5.まとめ
二回に分けて、ScenicでGUIを作る手順を紹介しました。
次は実践的な使い方を試してみようと思います。(ちょっと待ってね^^
#7.参考資料