以前購入した Raspberry Pi 用のタッチスクリーンを Elixir から利用する方法をまとめましたので、ご報告がてらの記事です。
- InputEvent - タッチスクリーンの入力を得る
- Scenic - タッチスクリーンに表示する
- calibration
- Scenic 用ドライバを書く
- Scenic で利用する
- いつか読むはずっと読まない:知恵の箱、金の鍵、勇気の棒、願いの杖、…
今回、表示周りには Scenic を、入力周りには InputEvent を利用しています。 それぞれの詳細については、各々の資料をご参照ください。
Raspberry Pi のセットアップについても各所によい資料がありますので、そちらのご参照をお願いします。
また入力のドライバの記述は scenic_driver_nerves_touch を参考にしています。
…と言いますか、元々は scenic_driver_nerves_touch で済ませるつもりだったんですが、購入したタッチスクリーンに対応していなかったので自分で書き直したというのが本当のところ。
今回の作業はすべて Raspberry Pi 上で行っています。
また最終的に記述したコードは GitHub にも push しています。
InputEvent - タッチスクリーンの入力を得る
まず InputEvent を使って入力を取得するところから始めます。
適当なプロジェクトを用意します。
$ mix new touch_screen $ cd touch_screen
mix.exs
を編集して、deps
に input_event
を追加します。
defp deps do [ {:input_event, "~> 0.4"} ] end
取得できるイベントを確認する
依存パッケージを取得して iex を起動します。
$ mix desp.get $ iex -S mix
InputEvent.enumerate/0
で利用できるデバイスの一覧を取得します。
iex(1)> InputEvent.enumerate() [ {"/dev/input/event0", %InputEvent.Info{ bus: 0, input_event_version: "1.0.1", name: "ADS7846 Touchscreen", product: 0, report_info: [ ev_abs: [ abs_x: %{flat: 0, fuzz: 0, max: 4095, min: 0, resolution: 0, value: 0}, abs_y: %{flat: 0, fuzz: 0, max: 4095, min: 0, resolution: 0, value: 0}, abs_pressure: %{ flat: 0, fuzz: 0, max: 65535, min: 0, resolution: 0, value: 0 } ], ev_key: [:btn_touch] ], vendor: 0, version: 0 }} ]
ここから次のようなことがわかります。
- デバイス名が
ADS7846 Touchscreen
- 取得できるイベントは
abs_x
,abs_y
,abs_pressure
,ev_key
の 4 種類 abs_x
,abs_y
の値は 0〜4095abs_pressure
の値は 0〜65535ev_key
で状態を取得できるキーの種類はbtn_touch
(1 種類)
これらの知識を持って、実際に入力を確認します。
イベントを取得する
InputEvent は InputEvent.start_link/1
でプロセスを起動すると、起動した親プロセスにメッセージでイベントを送るので、GenServer で親プロセスを実装し、イベントを受けたらログに出力するようにしてみます。
lib/touch_screen.ex
を編集します。
defmodule TouchScreen do use GenServer require Logger def start_link(device) do GenServer.start_link(__MODULE__, [device: device]) end def init(opts) do {path, _} = InputEvent.enumerate() |> Enum.find(fn {_, %{name: name}} -> name == opts[:device] end) InputEvent.start_link(path) {:ok, %{path: path}} end def handle_info(message, state) do Logger.debug(inspect(message)) {:noreply, state} end end
起動時にデバイス名を指定できるようにしました。
デバイス名を指定してプロセスを起動します。 起動したらタッチスクリーンに触れて動かしてみてください。 次のようなログが出力されると思います。
iex(1)> TouchScreen.start_link("ADS7846 Touchscreen") {:ok, #PID<0.189.0>} iex(2)> 10:30:45.185 [debug] {:input_event, "/dev/input/event0", [{:ev_key, :btn_touch, 1}, {:ev_abs, :abs_x, 3246}, {:ev_abs, :abs_y, 1151}, {:ev_abs, :abs_pressure, 64778}]} 10:30:45.214 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 3252}, {:ev_abs, :abs_y, 1162}, {:ev_abs, :abs_pressure, 64753}]} 10:30:45.214 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 3249}, {:ev_abs, :abs_y, 1196}, {:ev_abs, :abs_pressure, 64745}]} 10:30:45.215 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 3293}, {:ev_abs, :abs_y, 1247}, {:ev_abs, :abs_pressure, 64724}]} 10:30:45.233 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 3156}, {:ev_abs, :abs_y, 1341}, {:ev_abs, :abs_pressure, 64777}]} 10:30:45.250 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 3050}, {:ev_abs, :abs_y, 1453}, {:ev_abs, :abs_pressure, 64695}]} 10:30:45.270 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 2944}, {:ev_abs, :abs_y, 1546}, {:ev_abs, :abs_pressure, 64835}]} 10:30:45.290 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 2894}, {:ev_abs, :abs_y, 1603}, {:ev_abs, :abs_pressure, 64888}]} 10:30:45.310 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 2872}, {:ev_abs, :abs_y, 1626}, {:ev_abs, :abs_pressure, 64895}]} 10:30:45.330 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 2882}, {:ev_abs, :abs_y, 1618}, {:ev_abs, :abs_pressure, 64886}]} 10:30:45.350 [debug] {:input_event, "/dev/input/event0", [{:ev_abs, :abs_x, 2886}, {:ev_abs, :abs_y, 1609}, {:ev_abs, :abs_pressure, 64898}]} 10:30:45.368 [debug] {:input_event, "/dev/input/event0", [{:ev_key, :btn_touch, 0}, {:ev_abs, :abs_pressure, 0}]}
メッセージは次の 3 つの値のタプルで構成されていることが確認できます。
:input_event
- デバイスのパス
- イベントのリスト
- 先に確認した touch, x, y, pressure の 4 種類のイベントのリストになっています
handle_info/2
の引数を次のように書くことで、指定したデバイスの InputEvent のメッセージのみをハンドリングすることができそうです。
def handle_info({:input_event, path, events}, %{path: path} = state) do # ... end
一つのイベントに畳み込む
イベントは touch や x や y とった要素に別れた状態で受け取りますが、利用するばあいには「(x, y) の位置にタッチされた」といった一つのイベントになっていた方が扱いやすくなります。
Enum.reduce/2
を使って、複数のイベントの要素を一つのマップに畳み込みます(このあと pressure の値を使う予定がないので、pressure は畳み込みの対象から外しています)。
加えて、畳み込んだ結果の :touch
の値を調べて 1
であれば down
、 0
であれば up
、それ以外であれば move
と扱うようにしました。
これら down, up, move のイベントは、あとで Scenic の入力になります。
def handle_info({:input_event, path, events}, %{path: path} = state) do events |> Enum.reduce(%{touch: nil, x: 0, y: 0}, fn {:ev_abs, :abs_x, x}, event -> %{event | x: x} {:ev_abs, :abs_y, y}, event -> %{event | y: y} {:ev_key, :btn_touch, touch}, event -> %{event | touch: touch} _, event -> event end) |> case do %{touch: 1, x: x, y: y} -> Logger.debug("down (#{x}, #{y})") %{touch: 0, x: x, y: y} -> Logger.debug("up (#{x}, #{y})") %{x: x, y: y} -> Logger.debug("move (#{x}, #{y})") event -> Logger.debug("unknown event #{inspect(event)}") end {:noreply, new_state} end
Scenic - タッチスクリーンに表示する
次に Scenic を使ってタッチスクリーンに表示してみます。
プロジェクトを用意する
まず Scenic のプロジェクトを作成するために、 scenic_new をインストールします。
$ mix archive.install hex scenic_new
mix scenic.new
というタスクが追加されるので、これを使って新しいプロジェクトを作成します。
$ mix scenic.new touch_screen_scenic $ cd touch_screen_scenic
mix.exs
の deps/0
を編集します。
Raspberry Pi の環境ですので、 scenic_driver_glfw
を scenic_driver_nerves_rpi
を置き換えます。
また入力では input_event
を利用するので、これも追加しておきます。
なお、この記事を書いている 2021-05-23 現在、font_metrics の最新は 0.5.1 なのですが Scenic がこのバージョンに対応していない様子でした。
このため、font_metrics のバージョンを 0.3.x に固定するために font_metrics
も deps/0
に記述しておきます。
defp deps do [ {:scenic, "~> 0.10"}, {:scenic_driver_nerves_rpi, "~> 0.10.1"}, {:input_event, "~> 0.4"}, {:font_metrics, "~> 0.3.0"} ] end
依存するパッケージを取得しコンパイルします。
$ mix deps.get $ mix deps.compile
もし C のコードのコンパイルに失敗するばあいには、include と lib のパスを追加して試してみてください。
$ export C_INCLUDE_PATH=/opt/vc/include:$C_INCLUDE_PATH $ export LIBRARY_PATH=/opt/vc/lib:$LIBRARY_PATH
config を編集する
config/config.exs
を編集し設定をデバイスに合わせます。
:size
を利用している表示のサイズに合わせます。
サイズは /boot/config.txt
の hdmi_cvt
で設定されていると思いますのでその値にします。
わたしは 480x320 で利用しているので、size: {480, 320}
と設定しています。
また、ドライバを scenic_driver_nerves_rpi
に置き換えましたので、ドライバに指定するモジュールも Scenic.Driver.Nerves.Rpi
に変更します。
config :touch_screen_scenic, :viewport, %{ name: :main_viewport, size: {480, 320}, default_scene: {TouchScreenScenic.Scene.Home, nil}, drivers: [ %{ module: Scenic.Driver.Nerves.Rpi } ] }
表示を確認します。
iex -S mix
もしくは mix scenic.run
で起動してみてください。
問題がなければスクリーンに lib/scenes/home.ex
に記述された文字列( This is a very simple starter application.
等)が表示されると思います。
入力用のモジュールを用意する
先ほど入力を確認するために書いたモジュールを再利用します。
このあとドライバとして利用する予定ですので、lib/touch_screen_scenic/driver.ex にコピーしてモジュール名も TouchScreenScenic.Driver
としておきます。
defmodule TouchScreenScenic.Driver do use GenServer # ... end
iex -S mix
で起動し、モジュールが機能することを確認しておきます。
iex(1)>TouchScreenScenic.Driver.start_link("ADS7846 Touchscreen") {:ok, #PID<0.203.0>} iex(2)> 10:46:21.065 [debug] down (2921, 1721) 10:46:21.081 [debug] move (2879, 1718)
calibration
先にタッチスクリーンの入力は x, y それぞれ 0〜4095 ということを確認しました。 一方で表示領域のサイズは 480x320 です。
これをタッチした位置と表示の位置が一致するようにキャリブレーションします。
キャリブレーションの方法
結論から言うと、タッチした位置 (x_touch, y_touch)
から表示の位置 (x_screen, y_screen)
は、次のような式で求めることができます。
x_screen = ax * x_touch + bx * y_touch + dx y_screen = ay * x_touch + by * y_touch + dy
この 6 つの定数 ax, bx, dx, ay, by, dy は 3 点での対応がわかれば算出することができます。
Texas Instruments の ADS7846 のデータシートのページからキャリブレーションの方法についてのドキュメントのリンクがあります。 詳しくはこれらの資料を確認してみてください。
タッチの位置と表示の位置の対応を調べる
スクリーン上に指定した位置に表示した図形をタッチし、その時の入力を確認することで、タッチした位置と表示の位置の対応を調べます。
ここでは、スクリーンの上下左右それぞれ 20 ピクセルの位置に縦横 2 本ずつの線を表示します。
lib/scenes/home.ex の init/2
を次のように編集します。
def init(_, opts) do graph = Graph.build(font: :roboto, font_size: @text_size) |> line({{0, 20}, {480, 20}}, stroke: {1, :white}) |> line({{0, 300}, {480, 300}}, stroke: {1, :white}) |> line({{20, 0}, {20, 320}}, stroke: {1, :white}) |> line({{460, 0}, {460, 320}}, stroke: {1, :white}) {:ok, graph, push: graph} end
iex -S mix
で起動すると 4 本の線が表示され、交点が 4 つできます。
このうちの 3 点をタッチしそのときのログを確認します。
わたしが確認したときは次のような値を得ることができました。
表示上の点 | タッチした点 |
---|---|
左上 (20, 20) | (3653, 3777) |
右上 (460, 20) | (3682, 369) |
右下 (460, 300) | (425, 352) |
ここから行列演算すれば欲しい値が得られるわけですが、計算はそれらが得意な言語にまかせることにします。
おもむろに Julia を起動して計算します。
julia> a = [3653 3777 1; 3682 369 1; 425 352 1] 3×3 Matrix{Int64}: 3653 3777 1 3682 369 1 425 352 1
a の逆行列をスクリーン上の 3 点の x 座標にかけると、ax, bx, dx が求まります。
julia> inv(a) * [20; 460; 460] 3-element Vector{Float64}: 0.000673852686974119 -0.1291022471455627 505.15760360327397
同様に ay, by, dy を求めます。
julia> inv(a) * [20; 20; 300] 3-element Vector{Float64}: -0.0859648647083078 -0.0007315085318488684 336.7925585042416
4 桁目で丸めるてだいたいこれくらい。
ax = 0.0006739 bx = -0.1291 dx = 505.2 ay =-0.08596 by = -0.0007315 dy = 336.8
入力を扱う TouchScreenScenic.Driver
にキャリブレーションのコードを追加します。
calibrate/1
を追加して、受け取ったイベントの x, y の値を変換します。
あらためて iex -S mix
で起動し直します。
TouchScreenScenic.Driver.start_link("ADS7846 Touchscreen")
で入力プロセスを起動しスクリーンにタッチすると、今度はスクリーンの位置とタッチした位置がおおよそ一致することが確認できると思います。
def handle_info({:input_event, path, events}, %{path: path} = state) do events |> Enum.reduce(%{touch: nil, x: 0, y: 0}, fn {:ev_abs, :abs_x, x}, event -> %{event | x: x} {:ev_abs, :abs_y, y}, event -> %{event | y: y} {:ev_key, :btn_touch, touch}, event -> %{event | touch: touch} _, event -> event end) |> case do %{touch: 1, x: x, y: y} -> Logger.debug("down #{inspect(calibrarte({x, y}))}") {%{touch: 0, x: x, y: y}, _} -> Logger.debug("up #{inspect(calibrarte({x, y}))}") %{x: x, y: y} -> Logger.debug("move #{inspect(calibrarte({x, y}))}") event -> Logger.debug("unknown event #{inspect(event)}") end {:noreply, new_state} end defp calibrate({x, y}) do ax = 0.0006739 bx = -0.1291 dx = 505.2 ay =-0.08596 by = -0.0007315 dy = 336.8 { ax * x + bx * y + dx, ay * x + by * y + dy } end
Scenic 用ドライバを書く
ここまででスクリーンへの表示とタッチ入力の取得ができるようになりました。
最後に TouchScreenScenic.Driver
が Scenic のドライバとして利用できるように、手を加えていきます。
TouchScreenScenic.Driver を Scenic のドライバに指定する
まず TouchScreenScenic.Driver
を Scenic のドライバとして登録します。
すでに Scenic.Driver.Nerves.Rpi
の登録で見たように、ドライバとして登録するには config/config.exs
を編集し、:drivers
のリストに設定を追加します。
今回はドライバに渡す設定(デバイス名とキャリブレーションの数値)も一緒に記述します。
ドライバの設定はマップからできていて、キー :module
にモジュールの識別子を指定します。
次にキー :opts
に、ドライバに渡したい値を記述します。
ここでは :device
にデバイス名を、 :calibration
にキャリブレーションの数値を記述します。
この :opts
の値は後述の init/3
に第 3 引数としてそのまま渡ります。
config :touch_screen_scenic, :viewport, %{ name: :main_viewport, size: {480, 320}, default_scene: {TouchScreenScenic.Scene.Home, nil}, drivers: [ %{ module: Scenic.Driver.Nerves.Rpi }, %{ module: TouchScreenScenic.Driver, opts: [ device: "ADS7846 Touchscreen", calibration: { {0.0006739, -0.1291, 505.2}, {-0.08596, -0.0007315, 336.8} } ] } ] }
Scenic.ViewPort.Driver を use する
Scenic のドライバとして利用するには、use で GenServer
の代わりに Scenic.ViewPort.Driver
を指定します。
defmodule TouchScreenScenic.Driver do use Scenic.ViewPort.Driver # ... end
実装を確認すると Scenic.ViewPort.Driver
が GenServer
として起動され、メッセージを受信したらそれを use したモジュールに転送する、というしくみになっています。
このようなしくみため、 GenServer
を起動するための関数 start_link/1
が不要になります。
Scenic.ViewPort.Driver
から削除します。
init/3 - ドライバを初期化する
初期化のときは GenServer
の init/2
に代わり init/3
がコールバックで呼び出されるようになります。
init/3
は 3 つの引数をとります。
- Scenic.ViewPort プロセスの pid 。入力したイベントをこのプロセスに送ります
- スクリーンサイズ。config/config.exs で
:size
に指定した値です。今回は利用しません - config/config.exs で
:opts
に設定したオプション。config/config.exs で設定したデバイス名とキャリブレーションの値をここから取り出します
また ViewPort の pid とキャリブレーションの値はタッチ入力のイベントを受けたときに利用しますので、state に格納しておきます。
def init(viewport, {_, _}, opts) do {path, _} = InputEvent.enumerate() |> Enum.find(fn {_, %{name: name}} -> name == opts[:device] end) InputEvent.start_link(path) {:ok, %{path: path, viewport: viewport, calibration: opts[:calibration]}} end
handle_info/2 & calibrate/2
最後に handle_info/2
でイベントを受け取ったら Scenic.ViewPort.input/2
に送る処理を書きます。
down, up, move と書いたイベントは、 Scenic.ViewPort.input/2
のイベントに対応づけることができます。
入力 | Scenic.ViewPort.input/2 に送るイベント |
---|---|
down | :cursor_button の :press |
up | : cursor_button の :release |
move | :cursor_pos |
:cursor_button
はボタンの種類(左、右、中央)を指定できますが、ここでは左ボタン :left
として扱うことにします。
他にどのようなイベントを指定できるかはScenic.ViewPort.input/2
のドキュメントを参照してみてください。
またキャリブレーションは、state
に格納した :calibration
の値を渡して計算するように修正します。
def handle_info({:input_event, path, events}, %{path: path} = state) do events |> Enum.reduce(%{touch: nil, x: 0, y: 0}, fn {:ev_abs, :abs_x, x}, event -> %{event | x: x} {:ev_abs, :abs_y, y}, event -> %{event | y: y} {:ev_key, :btn_touch, touch}, event -> %{event | touch: touch} _, event -> event end) |> case do %{touch: 1, x: x, y: y} -> pos = calibrate(state.calibration, {x, y}) Logger.debug("down #{inspect(pos)}") Scenic.ViewPort.input(state.viewport, {:cursor_button, {:left, :press, 0, pos}}) %{touch: 0, x: x, y: y} -> pos = calibrate(state.calibration, {x, y}) Logger.debug("up #{inspect(pos)}") Scenic.ViewPort.input(state.viewport, {:cursor_button, {:left, :release, 0, pos}}) %{x: x, y: y} -> pos = calibrate(state.calibration, {x, y}) Logger.debug("move #{inspect(pos)}") Scenic.ViewPort.input(state.viewport, {:cursor_pos, pos}) event -> Logger.debug("unknown event #{inspect(event)}") end {:noreply, state} end defp calibrate({{ax, bx, dx}, {ay, by, dy}}, {x, y}) do { (ax * x) + (bx * y) + dx, (ay * x) + (by * y) + dy } end
TouchScreenScenic.Driver 全体像
最終的に TouchScreenScenic.Driver
は次のようになりました。
defmodule TouchScreenScenic.Driver do use Scenic.ViewPort.Driver require Logger def init(viewport, {_, _}, opts) do {path, _} = InputEvent.enumerate() |> Enum.find(fn {_, %{name: name}} -> name == opts[:device] end) InputEvent.start_link(path) {:ok, %{path: path, x: 0, y: 0, viewport: viewport, calibration: opts[:calibration]}} end def handle_info({:input_event, path, events}, %{path: path} = state) do events |> Enum.reduce(%{touch: nil, x: 0, y: 0}, fn {:ev_abs, :abs_x, x}, event -> %{event | x: x} {:ev_abs, :abs_y, y}, event -> %{event | y: y} {:ev_key, :btn_touch, touch}, event -> %{event | touch: touch} _, event -> event end) |> case do %{touch: 1, x: x, y: y} -> pos = calibrate(state.calibration, {x, y}) Logger.debug("down #{inspect(pos)}") Scenic.ViewPort.input(state.viewport, {:cursor_button, {:left, :press, 0, pos}}) %{touch: 0, x: x, y: y} -> pos = calibrate(state.calibration, {x, y}) Logger.debug("up #{inspect(pos)}") Scenic.ViewPort.input(state.viewport, {:cursor_button, {:left, :release, 0, pos}}) %{x: x, y: y} -> pos = calibrate(state.calibration, {x, y}) Logger.debug("move #{inspect(pos)}") Scenic.ViewPort.input(state.viewport, {:cursor_pos, pos}) event -> Logger.debug("unknown event #{inspect(event)}") end {:noreply, state} end defp calibrate({{ax, bx, dx}, {ay, by, dy}}, {x, y}) do { (ax * x) + (bx * y) + dx, (ay * x) + (by * y) + dy } end end
Scenic で利用する
最後に、本当に Scenic でイベントを受け取れるか、確認します。
lib/scenes/home.ex を次のように編集します。
defmodule TouchScreenScenic.Scene.Home do use Scenic.Scene require Logger alias Scenic.Graph import Scenic.Primitives import Scenic.Components @text_size 24 def init(_, _opts) do graph = Graph.build(font: :roboto, font_size: @text_size) |> button("Greet", id: :btn_greet, translate: {200, 140}, width: 80, height: 40) |> text("", id: :text_greet, translate: {240, 200}, text_align: :center) {:ok, %{graph: graph, text: ""}, push: graph} end def filter_event({:click, :btn_greet}, _, %{graph: graph, text: text} = state) do {caption, text} = case text do "" -> {"Clear", "Hello!"} _ -> {"Greet", ""} end graph = graph |> Graph.modify(:btn_greet, &button(&1, caption)) |> Graph.modify(:text_greet, &text(&1, text)) {:noreply, %{state | graph: graph, text: text}, push: graph} end def handle_input(event, _context, state) do Logger.info("Received event: #{inspect(event)}") {:noreply, state} end end
実行すると、Greet
と書かれたボタンが一つスクリーンの中央に表示されます。
ボタンを押すと、ボタンの下に Hello!
と表示されるはずです。
ボタンの表示は Clear
に替わり、もう一度ボタンを押すと Hello!
の文字が消えます。
ドライバがきちんと設定できていれば、ボタンが表示されている領域を押したときだけボタンが反応してくれます。
いつか読むはずっと読まない:知恵の箱、金の鍵、勇気の棒、願いの杖、…
「知恵の箱」 自分にとって何が正しくて何が正しくないかを見分ける能力。
「金の鍵」 新たに学習し、実践する分野への扉を開き、それが今の自分に合わなければ閉める能力。
「勇気の棒」 新しいことに挑戦し、失敗の危険を犯す勇気。
「願いの杖」 自分の欲しいものを求め、必要とあれば、それが手に入らなくてもどうにかする能力。
…
何年も前に手にした本です。 当時もこの本に勇気付けられた。
あらためてページを開くと、当時よりもより響く感じがします。 当時よりものごとが見えるようになったから、と思いたいところ。
あらためて読み返してみようと思います。

- 作者:ジェラルド・M・ワインバーグ
- 発売日: 2003/07/29
- メディア: 単行本(ソフトカバー)