Finds and reads the evdev input devices a panel comes with, and finds them again when they go away.
A keyboard on a Nerves device is /dev/input/eventN for some N that changes with what else is plugged in, may not exist at boot, and goes away when unplugged; a touch panel is another such device. This module owns both: which device to read, starting an input_event reader on it with the device grabbed (so keystrokes do not also reach the kernel console), translating what it delivers into ExRatatui.Event structs (RasterExRatatui.Input.Evdev for keys, RasterExRatatui.Input.Touch for fingers), and looking again, every retry_ms:, whenever there is nothing to read. It has no process of its own: the caller keeps the struct, hands it every message it receives, and gets events back, from a surface's RasterExRatatui.Surface.handle_info/2 or from any process that traps exits.
defmodule Kiosk.Surface do
use RasterExRatatui.Surface, app: Kiosk.Dashboard, format: RasterExRatatui.PixelFormat.RGB565
alias RasterExRatatui.Input.Devices
@impl true
def init(_opts) do
{:ok, devices} = Devices.start(Devices.new(keyboard: true))
{:ok, [size: {480, 320}], devices}
end
@impl true
def push(patches, devices), do: write(patches) && devices
@impl true
def handle_info(msg, devices) do
case Devices.handle_info(msg, devices) do
{:events, keys, devices} -> {:events, keys, devices}
{:noreply, devices} -> {:noreply, devices}
:unknown -> {:noreply, devices}
end
end
@impl true
def terminate(_reason, devices), do: Devices.stop(devices)
endinput_event is the consumer's dependency
The reader is input_event, a C port that only builds on Linux, so this library does not depend on it: the consumer adds {:input_event, "~> 1.4"} to its own deps. start/1 checks that the module is there and returns {:error, :input_event_missing} otherwise, so a host build without it still runs, without input. input: swaps the module for a stub in tests.
Options
new/1 takes:
:keyboard—true(default) to read the first device that reports letter keys, a"/dev/input/eventN"path to read that one, orfalse:touch—trueto read the first touch panel found (touch_path/1), a path, orfalse(default). Its taps and drags come back asExRatatui.Event.Mouseevents on the cell under the finger, which takes the two options below:size— the physical panel in pixels, for the touch axes (required with:touch):cell_at—({px, py} -> {col, row} | :outside), normallyRasterExRatatui.Raster.cell_at/2of the raster the panel shows (required with:touch):swap_xy,:invert_x,:invert_y— for a touch controller that does not follow the panel's orientation, seeRasterExRatatui.Input.Touch:retry_ms— how long to wait before looking for a missing device again (default2_000):input— the module standing in forInputEvent(defaultInputEvent):layout,:emit_release— passed toRasterExRatatui.Input.Evdev.new/1
Messages
start/1 sends the calling process a first {RasterExRatatui.Input.Devices, :scan}; handle_info/2 handles it and every later one, the {:input_event, path, events} and {:input_event, path, :disconnect} messages of the readers it started, and their {:EXIT, pid, reason} (a reader stops when its device goes away, and the search starts over). Everything else is :unknown, including the :normal exits of the short-lived readers that InputEvent.enumerate/0 starts from the caller while scanning.
Summary
Functions
Handles a message from the calling process's mailbox.
The path of the keyboard being read, or nil while there is none.
The path of the first device in an InputEvent.enumerate/0 list that reports letter keys, or nil. Mice, touch panels, and power buttons report :ev_key too, but not :key_a.
Builds the state from the options in the moduledoc. Raises ArgumentError on a bad option, or on :touch without :size and :cell_at.
Starts looking for the devices: the first scan arrives as a message to the calling process, which must trap exits (the readers are linked).
Stops the readers that are running and any pending scan. Returns the devices, idle; start/1 starts them again.
The path of the touch panel being read, or nil while there is none.
The ranges of a touch panel's axes, %{x: {min, max}, y: {min, max}}, from a device's report_info (the multitouch axes when present, else the single-touch ones), or nil when the device has no such axes.
The path of the first device in an InputEvent.enumerate/0 list that is a touch panel: one reporting the multitouch :abs_mt_position_x axis, or else :abs_x together with :btn_touch. nil when there is none.
Types
@type t() :: %RasterExRatatui.Input.Devices{ evdev: RasterExRatatui.Input.Evdev.t(), evdev_opts: keyword(), input: module(), keyboard: boolean() | String.t(), keyboard_path: String.t() | nil, keyboard_reader: pid() | nil, retry_ms: non_neg_integer(), timer: reference() | nil, touch: boolean() | String.t(), touch_opts: keyword(), touch_path: String.t() | nil, touch_reader: pid() | nil, touch_state: RasterExRatatui.Input.Touch.t() | nil }
Functions
@spec handle_info(term(), t()) :: {:events, [ExRatatui.Event.t()], t()} | {:noreply, t()} | :unknown
Handles a message from the calling process's mailbox.
Returns {:events, events, devices} with the ExRatatui.Event structs a reader's message translated to, {:noreply, devices} for the other messages of this module, or :unknown.
The path of the keyboard being read, or nil while there is none.
The path of the first device in an InputEvent.enumerate/0 list that reports letter keys, or nil. Mice, touch panels, and power buttons report :ev_key too, but not :key_a.
Examples
iex> RasterExRatatui.Input.Devices.keyboard_path([
...> {"/dev/input/event0", %{report_info: [ev_key: [:btn_touch], ev_abs: [abs_x: %{}]]}},
...> {"/dev/input/event1", %{report_info: [ev_key: [:key_esc, :key_a, :key_b], ev_led: [:led_capsl]]}}
...> ])
"/dev/input/event1"
iex> RasterExRatatui.Input.Devices.keyboard_path([])
nil
Builds the state from the options in the moduledoc. Raises ArgumentError on a bad option, or on :touch without :size and :cell_at.
Examples
iex> devices = RasterExRatatui.Input.Devices.new(keyboard: "/dev/input/event3", retry_ms: 500)
iex> {devices.keyboard, devices.retry_ms, RasterExRatatui.Input.Devices.keyboard(devices)}
{"/dev/input/event3", 500, nil}
Starts looking for the devices: the first scan arrives as a message to the calling process, which must trap exits (the readers are linked).
Returns {:ok, devices}, or {:error, :input_event_missing} when a device is wanted and the input: module is not available.
Stops the readers that are running and any pending scan. Returns the devices, idle; start/1 starts them again.
The path of the touch panel being read, or nil while there is none.
@spec touch_axes(%{report_info: [{atom(), [term()]}]}) :: RasterExRatatui.Input.Touch.axes() | nil
The ranges of a touch panel's axes, %{x: {min, max}, y: {min, max}}, from a device's report_info (the multitouch axes when present, else the single-touch ones), or nil when the device has no such axes.
Examples
iex> RasterExRatatui.Input.Devices.touch_axes(%{
...> report_info: [
...> ev_key: [:btn_touch],
...> ev_abs: [
...> abs_x: %{min: 0, max: 719},
...> abs_y: %{min: 0, max: 1279},
...> abs_mt_position_x: %{min: 0, max: 719},
...> abs_mt_position_y: %{min: 0, max: 1279}
...> ]
...> ]
...> })
%{x: {0, 719}, y: {0, 1279}}
iex> RasterExRatatui.Input.Devices.touch_axes(%{report_info: [ev_key: [:key_a]]})
nil
The path of the first device in an InputEvent.enumerate/0 list that is a touch panel: one reporting the multitouch :abs_mt_position_x axis, or else :abs_x together with :btn_touch. nil when there is none.
Examples
iex> RasterExRatatui.Input.Devices.touch_path([
...> {"/dev/input/event1", %{report_info: [ev_key: [:key_a]]}},
...> {"/dev/input/event2", %{report_info: [ev_key: [:btn_left], ev_abs: [abs_x: %{}, abs_y: %{}]]}},
...> {"/dev/input/event3", %{report_info: [ev_key: [:btn_touch], ev_abs: [abs_x: %{}, abs_y: %{}, abs_mt_position_x: %{}]]}}
...> ])
"/dev/input/event3"
iex> RasterExRatatui.Input.Devices.touch_path([
...> {"/dev/input/event4", %{report_info: [ev_key: [:btn_touch], ev_abs: [abs_x: %{}, abs_y: %{}]]}}
...> ])
"/dev/input/event4"
iex> RasterExRatatui.Input.Devices.touch_path([])
nil