Documentation
Car UI Input Routing
Source:
apps/carUi/input/README.md
Car UI input is split across hardware, controller, frontend, and application runtime boundaries.
Architecture
runtime.toml
|
v
CarUiInputRuntime
|
v
KeyboardInputAdapter / PushButtonInputAdapter / RotaryEncoderInputAdapter
|
v
UiInputEventDispatcher
|
v
InputManager -> InputMapper -> UiAction -> CarUiFrontendIf
RotaryEncoderInputAdapter translates one RotaryEncoderIf into generic
InputEvent values. frontends/common/input/UiInputEventDispatcher queues
those values until the frontend event-loop thread drains them.
The neutral InputEvent, InputDeviceId, and InputHandlerIf contracts live
under input_events. This lets controllers and frontend queues share physical
input values without either layer depending on the other.
KeyboardInputAdapter accepts KeyboardReaderIf; the current runtime supplies
the optional Linux KeyboardReader implementation. It and
PushButtonInputAdapter publish through that same
queue. Keyboard key names are mapped by InputMapper; standalone pushbuttons
use an explicit InputDeviceId to UiAction mapping so physical button
numbers do not acquire hidden application meaning.
apps/carUi/runtime/CarUiInputRuntime owns the configured adapters, polling,
partial-device failure handling, and shutdown. InputMapper alone assigns
semantic meaning such as navigation, selection, volume, and mute.
Encoder identity
The configured volume_index selects one device for global system-volume
control. Its rotation is never forwarded to a panel. Pressing its button
toggles system mute; releasing it has no additional action.
Remaining devices map to general navigation actions in configured device
order. Panels receive UiAction values through the active screen; they do not
receive physical encoder callbacks.
Threading
Hardware callbacks enqueue events. CarUiInputRuntime polls adapters and
drains the common frontend queue through UiDispatcherIf, ensuring UI actions
execute on the frontend thread. This works with Tk today and permits a future
Qt dispatcher.
The runtime accepts optional keyboard readers and standalone pushbuttons.
They are not enabled by the current default Car UI TOML; rotary-encoder buttons
are already included through each RotaryEncoderInputAdapter.
Example configuration:
[input.keyboard]
enabled = true
device_path = "/dev/input/event3" # optional; auto-detected when omitted
[[input.push_buttons]]
pin = 11 # physical Raspberry Pi header pin
action = "home"
active_low = true
debounce_seconds = 0.05
[[input.push_buttons]]
pin = 13
action = "back"
Supported standalone-button actions are back, home, select,
navigate_up, navigate_down, volume_up, volume_down, and
volume_mute. Keyboard and GPIO dependencies remain optional and are imported
only when their corresponding devices are enabled.
Car UI dependency ownership also uses KeyboardReaderIf, so alternate
keyboard sources can be injected without changing composition, input mapping,
or cleanup code.
pin is a physical Raspberry Pi 40-pin header number, not a BCM number.
Configured standalone pins must be unique and cannot overlap GPIO rotary
encoder pins. active_low = true enables the normal pull-up wiring where the
button connects the input to ground. debounce_seconds must be non-negative.
Platform requirements
Keyboard input requires the optional evdev package and permission to read
the selected /dev/input/event* device. When device_path is omitted,
KeyboardReader attempts to locate a keyboard-like device. Linux permissions
are commonly provided through the appropriate input-device group or a udev
rule; avoid running the complete UI as root solely to access input devices.
Standalone pushbuttons require Raspberry Pi GPIO support and gpiozero.
They are instantiated only when the application is running on a Raspberry Pi.
The default configuration leaves keyboard input disabled and contains no
active standalone pushbutton entries.
Lifecycle
Startup parses the input schema, constructs enabled devices, and transfers
them into CarUiDependencies. CarUiInputRuntime connects their controller
adapters and drains events on the frontend thread. Shutdown disconnects the
adapters and then closes or stops the owned hardware resources. A connection
failure for one constructed device does not prevent the remaining devices from
running; a configured device whose optional Python dependency is missing is a
startup configuration error.
System volume component test
Run:
python3 -m apps.carUi.input.component_test.volume_encoder_cli
This loads the production TOML but constructs and starts only the device
selected by volume_index. Contextual devices are intentionally not started,
so a disconnected panel encoder cannot prevent testing the volume knob.
The test changes the actual default PipeWire sink volume and prints the
resulting level after each rotation step. It requires wpctl and the configured
volume encoder hardware. It reports 20 levels by default, matching the default
5% PipeWire increment; the Car UI’s eight bars are only a visual indicator.
The PipeWire controller limits positive adjustments to 100%.
The production VolumeManager maps the 20-level audio range proportionally to
the top bar’s eight segments. For example, levels 5, 10, 15, and 20
display two, four, six, and eight bars respectively. While muted, all bars are
rendered in red so mute is distinguishable from volume zero.
Automated tests
python3 -m unittest \
apps.carUi.runtime.unit_test.test_car_ui_input_runtime \
apps.carUi.input.unit_test.test_volume_encoder_cli \
frontends.common.input.unit_test.test_ui_input_event_dispatcher \
controllers.input.unit_test.test_input_adapters \
config.integration_test.test_input_device_config