RasterExRatatui.Input.Devices (RasterExRatatui v0.4.0)

Copy Markdown View Source

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)
end

input_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, or false
  • :touch — true to read the first touch panel found (touch_path/1), a path, or false (default). Its taps and drags come back as ExRatatui.Event.Mouse events 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), normally RasterExRatatui.Raster.cell_at/2 of 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, see RasterExRatatui.Input.Touch
  • :retry_ms — how long to wait before looking for a missing device again (default 2_000)
  • :input — the module standing in for InputEvent (default InputEvent)
  • :layout, :emit_release — passed to RasterExRatatui.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

device()

@type device() :: {path :: String.t(), %{report_info: [{atom(), [term()]}]}}

t()

@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

handle_info(arg1, devices)

@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.

keyboard(devices)

@spec keyboard(t()) :: String.t() | nil

The path of the keyboard being read, or nil while there is none.

keyboard_path(devices)

@spec keyboard_path([device()]) :: String.t() | nil

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

new(opts \\ [])

@spec new(keyword()) :: t()

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}

start(devices)

@spec start(t()) :: {:ok, t()} | {:error, :input_event_missing}

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.

stop(devices)

@spec stop(t()) :: t()

Stops the readers that are running and any pending scan. Returns the devices, idle; start/1 starts them again.

touch(devices)

@spec touch(t()) :: String.t() | nil

The path of the touch panel being read, or nil while there is none.

touch_axes(map)

@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

touch_path(devices)

@spec touch_path([device()]) :: String.t() | 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