Session context for future Claude instances. Read this before modifying any source.
Custom virtual touchscreen keyboard for the Anbernic RG DS handheld running ROCKNIX Linux. Renders a phone-style keyboard on the bottom screen (DSI-1), injects keystrokes via uinput to apps on the top screen (DSI-2).
Multi-module package in /storage/rgds-keyboard/:
| Module | Responsibility |
|---|---|
main.py |
Entry point, epoll event loop (selectors), touch/joypad handling, signal dispatch |
config.py |
8 theme palettes, JSON settings persistence at /storage/.config/rgds-keyboard/config.json |
devices.py |
Auto-detect touchscreen/joypad/haptic via /proc/bus/input/devices parsing |
font.py |
Embedded 5×7 bitmap font glyph data |
layouts.py |
Keyboard layout definitions (main/shift/symbols/nav), keycode constants, rect computation |
renderer.py |
Dirty-region rendering with SDL2 render-target texture caching |
sdl2.py |
SDL2 ctypes bindings including texture/render-target support |
uinput_kb.py |
Virtual keyboard creation and keystroke injection via /dev/uinput |
These were learned through debugging on real hardware. Violating them will cause breakage.
-
Window title must contain
[Bottom]— ROCKNIX's Sway config has a built-in rule that routes windows with[Bottom]in the title to DSI-1. -
DSI-1 must be periodically reasserted — EmulationStation has a rule that powers off DSI-1 on focus. Counter with
swaymsg 'output DSI-1 power on'every 1.5 seconds. -
Rendering must stay on main thread — SDL2 rendering from background threads causes silent failures on this platform.
-
Never use SDL2 hide/show for toggling — Hiding/showing the SDL2 window causes it to leave Sway's window tree and lose DSI-1 assignment. Use render-black instead.
-
uinput abs arrays are 64 entries × 4 bytes — Not 256. The struct layout is: name[80] + id(8+4 bytes) + absmax[64×4] + absmin[64×4] + absfuzz[64×4] + absflat[64×4].
-
Touchscreen grab must be cleaned up on crash — Use
atexithandler to release EVIOCGRAB on all grabbed file descriptors. -
Ports shortcuts go in
/storage/roms/ports/— NOT/storage/.config/modules/. The latter gets wiped by ROCKNIX's001-sync-modulesautostart script on every boot. -
Brightness control must use swaymsg —
/sys/class/backlight/*targets the top screen. Useswaymsg 'output DSI-1 brightness <float>'for the bottom screen.
The main loop uses selectors.DefaultSelector (epoll on Linux) to multiplex:
- Touch fd (bottom touchscreen evdev)
- Joypad fd (R3 button on gamepad)
- Signal pipe fd (for SIGUSR1/2/SIGRTMIN wakeup)
Timeouts:
- Hidden: blocks until joypad/signal event (with 1.5s timeout for DSI-1 reassertion)
- Visible, idle: 33ms timeout (30fps cap)
- Visible, key held: 16ms timeout (60fps for smooth repeat)
SIGUSR1→ hide keyboardSIGUSR2→ show keyboardSIGRTMIN→ toggle keyboardSIGTERM/SIGINT→ clean shutdown
Parses /proc/bus/input/devices to find devices by capability bits:
- Touchscreen: EV_ABS + ABS_MT_POSITION_X capability
- Joypad: EV_KEY + BTN_THUMBR capability
- Haptic: EV_FF capability or
/sys/class/leds/vibrator/brightness
Dual touchscreens (RG DS) are differentiated by event number (bottom = higher index).
Config allows override: set touch_device / joypad_device in config.json.
- All keys rendered to an
SDL_TexturewithSDL_TEXTUREACCESS_TARGET - Dirty tracking: each key's visual state (pressed + shift_active) is cached
- On change: only dirty keys are re-rendered to the texture
- Full texture blitted to screen via single
SDL_RenderCopy - Layer changes trigger full redraw
8 palettes defined in config.py with semantic colour roles:
bg, key, key_hi, key_press, key_sp, key_act, text, text_sp, text_num, accent, border, divider
Default: Tokyo Night. All meet WCAG 2.1 AA contrast requirements.
- Device: Anbernic RG DS (RK3568, Mali G52-2EE, dual 640×480 IPS, Goodix touch)
- OS: ROCKNIX nightly (March 2026+), Sway compositor
- Touch: Two Goodix capacitive touchscreens (event1=top, event2=bottom typically)
- Joypad: retrogame device (event6 typically, R3 = BTN_THUMBR code 318)
- Stack: Python 3, SDL2 via ctypes, uinput — zero external dependencies