From mix nerves.new to an ExRatatui app on a Raspberry Pi's display, with a USB keyboard and the touch panel. The same steps fit any Nerves target whose display is a Linux framebuffer. The rpi_framebuffer example is this guide, built.
The project
mix nerves.new my_device
Two dependencies in mix.exs. input_event reads the keyboard and the touch panel; it is a C port that only builds on Linux, so it is the project's dependency, not the library's.
{:raster_ex_ratatui, "~> 0.4"},
{:input_event, "~> 1.4", targets: @all_targets}The app is a plain use ExRatatui.App module, built and tried in a terminal on the host first. On the panel it runs through a surface:
defmodule MyDevice.Surface do
use RasterExRatatui.Framebuffer.Surface, app: MyDevice.Dashboard
endThe surface only belongs on the device, so the application starts it when the target config has an entry for it. It goes last among the children: its start waits for the display driver, up to thirty seconds.
def start(_type, _args) do
children =
case Application.fetch_env(:my_device, MyDevice.Surface) do
{:ok, opts} -> [{MyDevice.Surface, Keyword.put_new(opts, :name, MyDevice.Surface)}]
:error -> []
end
Supervisor.start_link(children, strategy: :one_for_one, name: MyDevice.Supervisor)
endAnd in config/target.exs:
config :my_device, MyDevice.Surface, rotate: 90, touch: true
# IEx on the serial port instead of the panel; the surface detaches the kernel's console itself.
config :nerves, :erlinit, ctty: "ttyS0"Nothing there describes the panel: its size and depth are read from sysfs at boot, the pixel format and font scale follow from them. The option table is in RasterExRatatui.Framebuffer.Surface.
Build and burn
export MIX_TARGET=rpi4
mix deps.get
mix firmware
mix burn # the first time; later: mix upload nerves.local
Two things get in the way:
- The Elixir must be built for the system's OTP.
elixir --versionsays "compiled with Erlang/OTP N", and N must be the OTP major of the Nerves system (28 fornerves_system_rpi42.0.x). A version manager's plainelixir 1.xis often an older-OTP build; with mise,mise use erlang@28 elixir@1.19.4-otp-28in the project, thenmix local.hex,mix local.rebar, andmix archive.install hex nerves_bootstrapfor that install. EX_RATATUI_BUILDset in the shell builds the NIF from source for the host; unset it for firmware builds.
A panel on its side, and touch
/dev/fb0 always has the panel's native orientation. rotate: 90 (or 270, whichever puts the text the right way up) turns the app; touch follows the rotation by itself. If taps land mirrored or on the wrong axis, invert_x: true, invert_y: true, or swap_xy: true in the same config fix it.
First checks over SSH
RasterExRatatui.Framebuffer.info("fb0")
#=> {:ok, %{width: 720, height: 1280, bits_per_pixel: 16, stride: 1440}}
# Paint it red (16 bits per pixel; at 32 use <<0, 0, 255, 255>>)
File.write!("/dev/fb0", :binary.copy(<<0x00, 0xF8>>, 720 * 1280))
InputEvent.enumerate()
RingLogger.next()
RasterExRatatui.Telemetry.probe(MyDevice.Surface, 10)The surface logs the geometry, format, and scale it chose, and each input device it reads. The probe says whether the panel keeps up: raster plus push bounds the frame rate, and a mailbox that grows means the surface is falling behind.
When it does not work
- The panel stays dark, SSH works, and the log shows
{:framebuffer, {:enoent, ...}}. No framebuffer appeared withinframebuffer_timeout:. The Raspberry Pi Touch Display 2 does not come up on the 2.1.x Pi systems: their kernel lacksCONFIG_BACKLIGHT_PWM, which its overlay needs since Linux 6.18. Stay on~> 2.0.0for it; HDMI is not affected. - A cursor or boot text over the app. The console unbind failed or the framebuffer console is not
vtcon1:cat /sys/class/vtconsole/*/nametells which one is the "frame buffer device", andconsole:takes its name. - No keys.
InputEvent.enumerate/0should list a device whose:ev_keyreport includes:key_a. Wireless keyboards behind a receiver can show up as several devices; the surface takes the first that matches, andkeyboard: "/dev/input/eventN"picks one. - A 3D object or an image turns slowly. A pixel region costs time per pixel every time it changes, on a rotated panel as on a flat one. A slower animation tick or a smaller region is the fix;
Telemetry.probe/3shows the cost. Under-voltage detectedindmesg. A Pi with a display draws more than a laptop's USB port gives; use a wall supply.
When the defaults do not fit
Each of init/1, push/2, handle_info/2, and terminate/2 can be overridden and call the default (Linux Framebuffers). A panel that is not a framebuffer gets its own surface (Building a Surface), and a device that already has a process in charge of its display runs a RasterExRatatui.Session from it.