RasterExRatatui.Framebuffer.Surface (RasterExRatatui v0.4.0)

Copy Markdown View Source

A complete surface for a Linux framebuffer and its evdev keyboard, in one use.

defmodule MyDevice.Surface do
  use RasterExRatatui.Framebuffer.Surface, app: MyDevice.Dashboard, rotate: 90
end

# in the application's supervision tree
children = [MyDevice.Surface]

That is the whole consumer for a Raspberry Pi with a display: at start the surface waits for /dev/fb0 to appear, reads its size and depth from sysfs, picks the pixel format (RGB565 at 16 bits per pixel, XRGB8888 at 32) and a font scale that keeps about a hundred columns on the long side, detaches the kernel console from the framebuffer, starts the app, and reads the first USB keyboard it finds (and the touch panel, with touch: true), looking again whenever there is none. Pixels go out with RasterExRatatui.Framebuffer.write/2; keys and taps come in through RasterExRatatui.Input.Devices. When the app exits, a new one starts on the same surface (on_app_exit: :restart), because a panel has nothing else to show.

Options

Given to use or to the child spec ({MyDevice.Surface, rotate: 90}), and read from the application's config the usual way. All of RasterExRatatui.Surface's options apply too; these are the ones this module adds or defaults.

OptionDefaultWhat it does
:apprequiredthe ExRatatui.App to run
:framebuffer"fb0"the framebuffer to draw on
:framebuffer_timeout30_000how long to wait for it to appear, in milliseconds (display drivers load during boot)
:scale:autointeger font scale, or :auto for RasterExRatatui.Framebuffer.auto_scale/2
:columns100the columns :auto keeps on the long side
:rotate090, 180, or 270 for a panel mounted on its side (see RasterExRatatui.Raster)
:console"vtcon1"the framebuffer console to detach, or false to leave it
:keyboardtrueread the first keyboard found, a "/dev/input/eventN" path, or false
:touchfalsethe touch panel, the same way: taps and drags reach the app as ExRatatui.Event.Mouse events on the cell under the finger, rotation included
:swap_xy, :invert_x, :invert_yfalsefor a touch controller that does not follow the panel's orientation (RasterExRatatui.Input.Touch)
:on_app_exit:restart:stop to let the supervisor decide instead; with :restart, :max_restarts crashes (default 3) within :max_seconds (default 5) still stop the surface
:font, :format_opts, :app_opts, :min_interval, :push_mode, :shutdown_timeout, :nameas in RasterExRatatui.Surface
:retry_ms, :layout, :emit_release, :inputas in RasterExRatatui.Input.Devices
:root"/"where /sys and /dev are (tests point it at a fake tree)

Starting blocks until the framebuffer is there, up to :framebuffer_timeout, so the surface belongs last in its supervisor: children after it would wait that long on a slow or missing display.

The keyboard needs input_event in the consumer's deps, a C port for Linux. Without it the surface logs a warning once and runs without input, so the same project still compiles and runs its tests on a host.

Under the hood, and overriding

use gives the module init/1, push/2, handle_info/2, and terminate/2, each delegating to the function of the same name here, and all overridable. Override one and call the default from it to add to what it does; the state is a map with :fb (the open RasterExRatatui.Framebuffer) and :devices (the RasterExRatatui.Input.Devices, or nil), and anything else added to it travels along.

defmodule MyDevice.Surface do
  use RasterExRatatui.Framebuffer.Surface, app: MyDevice.Dashboard

  @impl true
  def init(opts) do
    {:ok, config, state} = RasterExRatatui.Framebuffer.Surface.init(opts)
    {:ok, config, Map.put(state, :backlight, MyDevice.Backlight.on!())}
  end

  @impl true
  def handle_info({:button, :back}, state),
    do: {:events, [%ExRatatui.Event.Key{code: "esc", kind: "press"}], state}

  def handle_info(msg, state), do: RasterExRatatui.Framebuffer.Surface.handle_info(msg, state)
end

A panel that is not a framebuffer, or a keyboard that is not evdev, starts from RasterExRatatui.Surface instead; Building a Surface is the guide.

Summary

Types

The surface's state: the open framebuffer and the input devices, plus whatever an override adds.

Functions

The default RasterExRatatui.Surface.init/1: waits for the framebuffer, picks the format and scale, detaches the console, starts the input devices.

The default RasterExRatatui.Surface.terminate/2: stops the input readers and closes the framebuffer.

Types

state()

@type state() :: %{
  :fb => RasterExRatatui.Framebuffer.t(),
  :devices => RasterExRatatui.Input.Devices.t() | nil,
  optional(atom()) => term()
}

The surface's state: the open framebuffer and the input devices, plus whatever an override adds.

Functions

handle_info(msg, state)

@spec handle_info(term(), state()) ::
  {:noreply, state()} | {:events, [ExRatatui.Event.t()], state()}

The default RasterExRatatui.Surface.handle_info/2: RasterExRatatui.Input.Devices.handle_info/2, with anything it does not know ignored.

init(opts)

@spec init(keyword()) :: {:ok, keyword(), state()} | {:stop, {:framebuffer, term()}}

The default RasterExRatatui.Surface.init/1: waits for the framebuffer, picks the format and scale, detaches the console, starts the input devices.

Returns {:ok, [size:, format:, scale:, rotate:, on_app_exit:], %{fb:, devices:}}, or {:stop, {:framebuffer, reason}} when the framebuffer never appears or has a depth the library cannot pack (that one fails at once).

push(pixels, state)

@spec push([RasterExRatatui.Patch.t()] | {:frame, binary()}, state()) :: state()

The default RasterExRatatui.Surface.push/2: RasterExRatatui.Framebuffer.write/2.

terminate(reason, state)

@spec terminate(term(), state()) :: :ok | {:error, term()}

The default RasterExRatatui.Surface.terminate/2: stops the input readers and closes the framebuffer.