Keyboard workflow · Tutorial
How to define your own TCA8418 keyboard
Build a keyboard around a TCA8418 matrix controller, choose what each switch does, and use the same keymap JSON for a runtime experiment or a firmware default. This tutorial uses a four-row, ten-column matrix and SolarOS 4.16.2 or later, with the desktop keymap tool available in your source checkout.
How SolarOS handles the keyboard
The TCA8418 scans a switch matrix and queues physical press and release events. SolarOS reads that FIFO over a named I2C bus. The controller backend is shared by three expansion drivers:
| Driver | Matrix | Default mapping |
|---|---|---|
tca8418 | Configurable, up to 8 rows and 10 columns | Generic reference map |
inputronic-keyboard | Fixed 8 × 10 | Soldered Inputronic key positions and symbols |
lilygo-pager-keyboard | Fixed 4 × 10 | LilyGO T-LoRa-Pager key positions and symbols |
Use tca8418 for your own wiring. Each attachment creates an input source, such as keyboard0. Its keymap turns physical positions into HID keyboard usages, modifiers, or SolarOS logical key bytes. The source maintains its own Shift, Control, Alt, Caps Lock, and symbol-layer state. The common input service then handles held-key repeat and routes input to shells and applications.
A physical mapping and a language layout serve different purposes. input keymap keyboard0 load ... changes what a switch means; setterm keyboard us or setterm keyboard de selects how HID usages become characters. Direct logical bytes, such as char:!, bypass character translation.
Rows and columns are zero-based. The physical ID is always 1 + row * 10 + col, even if your matrix has fewer than ten columns. For example, row 0 / column 0 is ID 1, and row 3 / column 0 is ID 31. The attachment defines the matrix dimensions; a keymap cannot change them.
Meet the desktop keymap tool
For a visual editor, open the Keyboard Configurator. Choose the matrix dimensions, click a key to assign its action, then download the same JSON used by the runtime and firmware build workflows below.
scripts/keymap.py is a Python CLI for creating, editing, previewing, and validating keymaps. It uses only the Python standard library and shares the standard keyboard profiles with the firmware generator, so you can select keys by name rather than look up HID numbers by hand.
The tool writes ordinary JSON containing your selected overrides. You can load that file into a running keyboard source while experimenting, then use the same file as a compiled default once the mapping is settled. The desktop preview starts from the standard profile; use the device's input keymap command to inspect defaults customized in a particular firmware image.
Prepare a reproducible prototype
For this example you need an ESP32-S3-DevKitC-1-N16R8, a TCA8418 breakout compatible with a 3.3 V supply and I2C signals, and switches connected between rows 0–3 and columns 0–9. You also need Python 3, PlatformIO Core, and a SolarOS source checkout containing scripts/keymap.py. The quick start explains source installation and finding a serial port.
Power off before wiring. Reuse the DevKitC board profile's shared i2c0 bus:
| Breakout signal | DevKitC connection |
|---|---|
| Supply | 3V3, according to the breakout's supply requirements |
| Ground | GND |
| SDA | GPIO8, i2c0 SDA |
| SCL | GPIO9, i2c0 SCL |
Ensure RESET is held high and the I2C pull-ups use 3.3 V. This example leaves INT unconnected; the driver polls the FIFO. If you wire RESET or INT to host GPIOs instead, add reset=gpioN or irq=gpioN to the attachment using your actual free GPIO numbers.
All desktop commands below run from the SolarOS project directory. Start with the full flavor, which includes the driver, keymap service, Wi-Fi, and curl used in this tutorial:
cd solar_os
SOLAR_OS_FLAVOR=full pio run -e esp32_s3_devkitc1_n16r8
SOLAR_OS_FLAVOR=full pio run -e esp32_s3_devkitc1_n16r8 -t upload --upload-port <PORT>
Replace <PORT> with the board's serial port from pio device list. On this headless target the primary SolarOS shell uses uart0 at 115200 baud; connect through the board's USB-to-UART connector. The native USB connector provides cdc0 and does not host the primary shell by default. Use your desktop keyboard in that serial shell to issue the commands while you test the matrix keyboard.
Run these commands in the SolarOS shell:
i2c status
i2c scan i2c0
expansion attach tca8418 keyboard0 i2c=i2c0 addr=0x34 rows=4 cols=10
input keyboard
input keymap keyboard0 show
setterm keyboard us
The scan should find 0x34, the controller's fixed address, and input keyboard should list keyboard0 as ready. If the scan finds nothing, check power, RESET, SDA, and SCL before changing mappings. A second TCA8418 needs a separate named I2C bus because its address is also 0x34.
The first four rows of the generic reference map are:
| Row | Columns 0 through 9 |
|---|---|
| 0 | Q W E R T Y U I O P |
| 1 | A S D F G H J K L Enter |
| 2 | Left Shift Z X C V B N M Backspace Left Control |
| 3 | Space Tab Escape Caps Lock Left Alt Left Down Up Right Delete |
These names describe reference positions, so your switches need not carry matching keycaps. Press a switch and run input test keyboard0 from the serial shell to inspect its last event and physical ID. This command prints a diagnostic snapshot; run it again after another key action.
Create the map with the desktop tool
Start by listing the profiles and inspecting the positions in your chosen geometry:
python3 scripts/keymap.py profiles
python3 scripts/keymap.py show --profile tca8418 --rows 4 --cols 10
Assignments use KEY=ACTION. A key name selects a physical position from the reference profile; an action selects its output. For unfamiliar wiring, use physical:ID instead of a name. Actions include A, Enter, Escape, F1, Ctrl, and Shift; char:! emits a logical byte. raw exposes physical events without logical output, and none clears an entry.
Create a map that changes the Q position to A and makes Space a symbol-layer selector. An unused Space tap still produces a space; holding Space while pressing the Q position produces an exclamation mark:
mkdir -p keymap-demo
python3 scripts/keymap.py create --profile tca8418 --rows 4 --cols 10 \
--set Q=A --set Space=tap:Space --symbol 'Q=char:!' \
--output keymap-demo/my-tca8418.json
python3 scripts/keymap.py validate --profile tca8418 --rows 4 --cols 10 \
--input keymap-demo/my-tca8418.json
python3 scripts/keymap.py show --profile tca8418 --rows 4 --cols 10 \
--input keymap-demo/my-tca8418.json
The file contains only the overrides:
{
"schema": 1,
"keys": [
{"physical": 1, "usage": 4},
{"physical": 31, "usage": 44, "layer_tap": true}
],
"symbols": [
{"physical": 1, "key": 33}
]
}
HID usage 4 means A, usage 44 means Space, and logical byte 33 means !. Unlisted positions retain their default mappings. Unlisted symbol positions fall back to the base layer. Q still selects physical ID 1 after remapping; it does not follow the newly assigned A output.
To adjust an existing file, use edit with the same profile and dimensions, for example:
python3 scripts/keymap.py edit --profile tca8418 --rows 4 --cols 10 \
--input keymap-demo/my-tca8418.json --set physical:1=A
You can also create a map with --interactive to enter assignments at prompts. create requires --force to replace an existing file; edit updates its input file. Keep --rows and --cols consistent with the device attachment.
Try the JSON at runtime
Transfer the generated file to a device filesystem, then load it into the registered source. Here we use a small RAM filesystem, so no SD card is needed. Connect the board and desktop to the same reachable network. In SolarOS:
wifi connect "YOUR_SSID" "YOUR_PASSWORD"
wifi status
ramfs mount /keymaps 64k
On the desktop, serve just the example directory. Leave this command running in a second terminal:
python3 -m http.server 8000 --bind 0.0.0.0 --directory keymap-demo
Replace <DESKTOP_IP> below with the desktop's LAN address, rather than the board's address or localhost. Allow the board to reach TCP port 8000 on the desktop. Run these commands in SolarOS:
curl -o /keymaps/my-tca8418.json http://<DESKTOP_IP>:8000/my-tca8418.json
input keymap keyboard0 load /keymaps/my-tca8418.json
input keymap keyboard0 show
After the transfer completes, load validates the JSON before applying it. A rejected file leaves the active map intact. A successful load releases held keys and clears modifier and layer state, so release any keys you were holding before testing again.
Check the following behavior with Caps Lock off. On the headless prototype, hold the indicated switch or chord and run input test keyboard0 from the serial shell; for the Space tap, inspect the snapshot after release. A display shell can also show the resulting characters directly.
| Matrix action | Expected logical output | Event key value |
|---|---|---|
| Row 0 / column 0 | a | 97 |
| Left Shift + row 0 / column 0 | A | 65 |
| Tap and release row 3 / column 0 | Space | 32 |
| Hold row 3 / column 0, then press row 0 / column 0 | ! | 33 |
| Row 0 / column 1 | w, unchanged | 119 |
Each load starts from the source's firmware default and replaces the listed entries. It does not add to a previous runtime load. A file containing only your special keys therefore preserves all other defaults. Each listed entry replaces that position's output fields in its selected layer.
Runtime mappings last until detach or reboot. With the standard firmware used above, this restores the reference map:
input keymap keyboard0 reset
The RAM file also disappears on reboot. For a persistent runtime setup, store the JSON on persistent storage and load it from a startup script after the keyboard has attached. The source keymap reference describes the complete JSON format and scripting APIs. Stop the desktop HTTP server with Ctrl+C when the transfer is finished.
Bake the same map into firmware
Use the same JSON file as the default for the generic tca8418 profile. From the SolarOS project directory on the desktop:
SOLAR_OS_FLAVOR=full \
SOLAR_OS_KEYMAP_PROFILE=tca8418 \
SOLAR_OS_KEYMAP_FILE=keymap-demo/my-tca8418.json \
pio run -e esp32_s3_devkitc1_n16r8
The build validates the file and compiles the merged map into the firmware. Relative paths are resolved from the SolarOS project directory. Supply the same selections when uploading, since PlatformIO can rebuild during upload:
SOLAR_OS_FLAVOR=full \
SOLAR_OS_KEYMAP_PROFILE=tca8418 \
SOLAR_OS_KEYMAP_FILE=keymap-demo/my-tca8418.json \
pio run -e esp32_s3_devkitc1_n16r8 -t upload --upload-port <PORT>
After reboot, reconnect to the SolarOS serial shell and attach the keyboard:
expansion attach tca8418 keyboard0 i2c=i2c0 addr=0x34 rows=4 cols=10
setterm keyboard us
input keymap keyboard0 show
Repeat the same key checks. The custom map is now available immediately after attachment, without downloading or loading a JSON file on the device. The selection applies to every attachment of the tca8418 profile; it does not change Inputronic or Pager defaults.
The build selection supplies a map, while the attachment supplies the bus, pins, and matrix geometry. For automatic attachment at boot, save the expansion configuration or define a board-owned keyboard in a custom board manifest; the custom board tutorial covers that workflow.
With this firmware, input keymap keyboard0 reset restores the custom compiled default. A later sparse runtime load also starts from that default. To restore the standard reference map, rebuild and upload with the custom keymap selections removed. Keep the JSON and build selections alongside your board configuration so future builds reproduce your keyboard.
For fixed build settings, PlatformIO environments also accept custom_solaros_keymap_profile and custom_solaros_keymap_file. For multiple custom profiles in one image, use the index-file workflow in the source keymap reference.