Arduino Pro Micro button box firmware and build assets for a custom sim racing control box.
This repository started as a fork of AMSTUDIO's 32-function button box project, but the firmware in this repo is now its own implementation track. The current sketch is a layered HID joystick firmware built around a matrix-scanned button panel, rotary encoders, and per-device USB identity management.
- Arduino Pro Micro / ATmega32U4 HID joystick firmware
- Default
5x5matrix configuration with multiple matrix presets in the codebase (3x11,6x5,6x4,7x3,5x5) 3-layerinput model with two selection modes, chosen at compile time:- Hold mode: hold assigned Layer 2 / Layer 3 selector buttons
- Up/Down mode: press assigned Layer Up / Layer Down buttons to cycle layers
- Configurable layer-programming button indices
- Silent layer-selector / up-down inputs that do not consume joystick button outputs
- Up to
4rotary encoders with per-layer clockwise and counter-clockwise actions #define ROTARY_ONLY_LAYERScompile-time toggle to restrict layering to encoders only- Long-press Button 22 (default) for 5s to print full firmware configuration to Serial console
- Up to
100declared joystick buttons - Static button-to-joystick output mapping that keeps bindings stable when layer assignments change
- Safe button release handling across mid-press layer changes
- Custom controller report IDs for running multiple boxes on one system
- Documented USB VID/PID/product-name management workflow via
DEVICES-MANAGEMENT.md
Credits to the original projects this build was based on:
This repo contains three things:
- firmware in
button_box/button_box.ino - physical build reference assets such as the drill template and wiring diagram
- device identity documentation in
DEVICES-MANAGEMENT.md
The important distinction is that the firmware is now the primary source of truth. The original fork description focused on a simpler upstream button box. That is no longer accurate for this codebase.
The default configuration in button_box/button_box.ino is ready to deploy to the RS - Akamai Steering Wheel:
- MCU: Arduino Pro Micro / ATmega32U4
- USB mode: HID joystick via
ArduinoJoystickLibrary - Matrix preset:
DIMENSION_5x5 - Physical matrix positions:
25 - Layer selection mode:
LAYER_SELECTION_MODE_UP_DOWN - Programmable layer buttons:
Layer UpandLayer Down(default unassigned) - Layer programming buttons:
8and13 - Rotary encoders:
4 - Layers:
3 - Declared joystick buttons:
100 - Actively used joystick buttons:
99(25 matrix positions × 3 layers + 24 encoder outputs) - Rotary-only layers: disabled
- Firmware version: queryable via Button 22 long-press (Serial, 9600 baud)
Use the repo in this order:
button_box/button_box.inofor actual runtime behaviorDEVICES-MANAGEMENT.mdfor USB identity allocation and per-device trackingbuttonbox_layout.aiandbuttonbox_wiring.drawiofor physical reference assets
The sketch is organized around four main responsibilities:
- Compile-time layout selection
- Matrix scanning and button state handling
- Layer-aware output mapping
- Rotary encoder decoding and pulse generation
At runtime, the main loop is intentionally simple:
void loop() {
buttbx.getKeys();
UpdateLayer();
CheckAllEncoders();
CheckProgrammingGestures();
CheckAllButtons();
}That sequence matters:
getKeys()refreshes the matrix scan once per loopUpdateLayer()derives the current layer from the live selector button statesCheckAllEncoders()emits encoder pulses using the current layerCheckProgrammingGestures()runs the long-press state machine for layer selector assignment, reset, and version outputCheckAllButtons()translates matrix events into joystick button state changes
Most behavior is selected by preprocessor defines near the top of the sketch.
Each flashed box should get its own CONTROLLER_ID:
#define CONTROLLER_ID 2This does not change the USB device name by itself. It changes the joystick report ID used by the Joystick library so multiple controllers do not collide internally.
Current mapping in the sketch:
1-> report ID5002-> report ID5103-> report ID5204-> report ID530- fallback -> report ID
540
USB VID/PID/product-name management is handled separately in DEVICES-MANAGEMENT.md.
The sketch still carries multiple matrix presets:
DIMENSION_3x11DIMENSION_6x5DIMENSION_6x4DIMENSION_7x3DIMENSION_5x5
Each preset determines:
NUMROWSNUMCOLSNUMROTARIESNUMBUTTONSrowPins[]colPins[]- the
buttons[][]logical key map
The current default is:
#define DIMENSION_6x4For the 6x4 preset, the row/column wiring in the sketch is:
byte rowPins[NUMROWS] = {21,20,19,18,15,14};
byte colPins[NUMCOLS] = {16,10,9,8};The Keypad library works with logical key codes stored in the buttons[][] array. In the default 5x5 configuration, the matrix is mapped to logical button indices 0..24.
byte buttons[NUMROWS][NUMCOLS] = {
{0,1,2,3,4},
{5,6,7,8,9},
{10,11,12,13,14},
{15,16,17,18,19},
{20,21,22,23,24},
};The firmware supports a 3-layer system with a compile-time choice between two input models.
Choose one mode near the top of button_box/button_box.ino:
// Choose exactly one:
// LAYER_SELECTION_MODE_HOLD : hold assigned buttons for Layer 2/3
// LAYER_SELECTION_MODE_UP_DOWN : press assigned Up/Down buttons to cycle layers
#define LAYER_SELECTION_MODE_UP_DOWNHold assigned selector buttons to activate Layer 2 or Layer 3:
| Gesture | Duration | Result |
|---|---|---|
LAYER_PROG_BUTTON_2 + Button X |
5s | Assign Button X as Layer 2 selector |
LAYER_PROG_BUTTON_3 + Button Y |
5s | Assign Button Y as Layer 3 selector |
LAYER_PROG_BUTTON_2 + LAYER_PROG_BUTTON_3 |
5s | Reset both selectors to unassigned |
Layer 3 has priority if both selectors are held at the same time.
Press assigned buttons to cycle the active layer up or down. The layer clamps at 1 and 3; it does not wrap around.
| Gesture | Duration | Result |
|---|---|---|
LAYER_PROG_BUTTON_2 + Button X |
5s | Assign Button X as Layer Up button |
LAYER_PROG_BUTTON_3 + Button Y |
5s | Assign Button Y as Layer Down button |
LAYER_PROG_BUTTON_2 + LAYER_PROG_BUTTON_3 |
5s | Reset both buttons to unassigned |
The current layer resets to Layer 1 on every power-up; it is not persisted in EEPROM.
The two buttons used to initiate programming gestures are compile-time constants. Default values depend on the device; for the RS - Akamai Steering Wheel they are:
#define LAYER_PROG_BUTTON_2 8
#define LAYER_PROG_BUTTON_3 13These are ordinary matrix indices. If your device has unwired positions at 0 and 1, choose indices that are actually connected.
Programming assignments persist across power cycles. The addresses differ by mode so switching modes does not corrupt assignments:
| Address | Hold mode | Up/Down mode |
|---|---|---|
| 1 | Layer 2 selector | — |
| 2 | Layer 3 selector | — |
| 3 | — | Layer Up button |
| 4 | — | Layer Down button |
Unprogrammed EEPROM reads as 255 (unassigned).
When #define ROTARY_ONLY_LAYERS is uncommented, buttons always output on Layer 1 regardless of the current layer. Only rotary encoders respond to layer changes. This is useful for devices where physical buttons should remain consistent across modes (e.g., a steering wheel where buttons are fixed but encoders are mode-dependent).
Layer selector / up-down buttons never fire their own joystick output. They are silent modifiers: activating a layer should not also hold a joystick button down the entire time.
The firmware uses a static mapping from physical matrix positions to joystick output numbers. Each physical button always outputs at the same index, regardless of which buttons are assigned as layer modifiers.
Matrix positions assigned as layer selectors (Hold mode) or Layer Up / Layer Down buttons (Up/Down mode) do not generate joystick button outputs. They are consumed by UpdateLayer() before CheckAllButtons() translates matrix events into joystick state changes.
For a matrix with NUMBUTTONS logical positions, each layer occupies a fixed NUMBUTTONS-wide block:
outputButton = kchar + (currentLayer - 1) * NUMBUTTONS;With the default DIMENSION_5x5 preset (NUMBUTTONS = 25):
- Layer 1 matrix outputs:
0..24 - Layer 2 matrix outputs:
25..49 - Layer 3 matrix outputs:
50..74
Because the mapping is static, changing a layer assignment does not shift any other button's output number. Game bindings stay stable.
Encoders are allocated after all matrix-derived outputs:
#define ENCODER_BASE (NUMBUTTONS * 3)For the default 5x5 build that means:
- matrix outputs occupy
0..74 - encoder outputs occupy
75..98
TOTAL_JOYSTICK_BUTTONS is declared as 100, leaving headroom for future expansion.
One subtle problem with layered input systems is this:
- press a button on Layer 1
- change to Layer 2 while still holding it
- release the physical button
If the firmware recomputed the output number on release, it might accidentally release the Layer 2 output instead of the Layer 1 output that was actually pressed.
This sketch avoids that bug by storing the real joystick output index per physical key in activeOutputButton[].
On press:
- the current layer is sampled
- the final joystick output index is computed
- that output index is stored in
activeOutputButton[kchar] - the joystick button is set to
1
On release or idle transition:
- the sketch clears the exact stored output index
- it does not recompute from the current layer
This is one of the more important implementation details in the fork because it keeps layered behavior stable even when the operator changes modes mid-press.
CheckAllButtons() handles matrix-derived button outputs in a single pass.
The pass looks at stateChanged entries from the Keypad list.
Behavior:
- layer modifier keys (selectors in Hold mode, Up/Down buttons in Up/Down mode) are skipped completely
- buttons involved in an active programming gesture are suppressed
- on
PRESSED, the sketch asserts the joystick output for the current layer (or Layer 1 ifROTARY_ONLY_LAYERSis enabled) - on
RELEASEDorIDLE, the sketch releases the previously stored output if one is active
This is effectively standard hold-to-press behavior.
Rotary encoders are handled separately from the matrix.
The default build declares 4 encoders:
#define MAX_ROTARIES 4The current encoder pin pairs are:
- encoder 1: pins
0,1 - encoder 2: pins
2,3 - encoder 3: pins
4,5 - encoder 4: pins
6,7
Each encoder has six assigned output numbers:
- CCW on Layer 1
- CW on Layer 1
- CCW on Layer 2
- CW on Layer 2
- CCW on Layer 3
- CW on Layer 3
That is why 4 encoders consume 24 joystick outputs total.
The sketch uses a quadrature state table ttable and a small per-encoder state byte. This is a standard finite-state approach for rejecting invalid transitions and determining whether a full step was clockwise or counter-clockwise.
At runtime:
rotary_process()reads the two encoder pins- it advances the state machine using
ttable - it returns
DIR_CCW,DIR_CW, or no completed step CheckAllEncoders()selects the output pair for the current layer- the chosen joystick button is pulsed with a
50 mspress
That last point is important: encoders are not held. They emit short button pulses so games see each detent as a discrete action.
The sketch enables pull-ups:
#define ENABLE_PULLUPSEncoder inputs are initialized as INPUT, then driven high to enable the MCU's internal pull-up resistors.
The matrix scanning itself is handled by the Keypad library using the configured row/column pins.
There are two different identity layers to keep in mind.
This is controlled by CONTROLLER_ID -> JOYSTICK_REPORT_ID in the sketch.
This is controlled outside the sketch by editing the Arduino core's boards.txt, as documented in DEVICES-MANAGEMENT.md.
That file tracks:
- VID
- PID
- device name
- layout
- button count
- shift/layer mapping
- device description
If you run multiple button boxes on one PC, this file is the operational registry that prevents collisions and confusion in Device Manager and in-game controller lists.
The repo still includes the original physical build assets.
This is a real-size drilling template for a 200 x 120 mm case.
The original documented drill sizes are:
6 mm7 mm12 mm14 mm16 mm
You can edit the source in diagrams.net.
These files remain useful, but they should be treated as physical reference material, not as a full specification of the current firmware behavior.
Hold Button 22 (matrix index 21) for 5 seconds to print the full firmware configuration to the Serial console at 9600 baud:
--- Button Box Config ---
Firmware: v2.7.0
Dimension: 5x5
Buttons: 25
Rotaries: 4
Controller ID: 2
Joystick report ID: 510
USB VID: 0x255a
USB PID: 0xc611
USB Product: RS - Akamai Steering Wheel
Layer 2 programming button: 8
Layer 3 programming button: 13
Layer selection mode: up/down
Layer Up button: unassigned
Layer Down button: unassigned
Rotary-only layers: false
--------------------------
To view this output:
- Connect the Pro Micro to your PC via USB
- In Arduino IDE, select Tools → Serial Monitor (or
Ctrl+Shift+M) - Set the baud rate to 9600 in the bottom-right dropdown
- Hold Button 22 for 5 seconds
This is useful for verifying which firmware version and configuration is flashed on a device without opening the enclosure. The version string is defined by FIRMWARE_VERSION near the top of the sketch.
Install these before compiling:
- ArduinoJoystickLibrary
Keypadby Mark Stanley and Alexander Brevig
- Install Arduino IDE.
- Install the Joystick library.
- Install the
Keypadlibrary from the Arduino Library Manager. - Open
button_box/button_box.inoin Arduino IDE. - Review the PRE-DEPLOYMENT CONFIGURATION block and set:
CONTROLLER_IDfor the device you are flashing- the correct
DIMENSION_*matrix preset LAYER_SELECTION_MODE_HOLDorLAYER_SELECTION_MODE_UP_DOWNLAYER_PROG_BUTTON_2andLAYER_PROG_BUTTON_3to wired indicesBOX_VID,BOX_PID, andBOX_PRODUCTto match the target device entry inDEVICES-MANAGEMENT.mdROTARY_ONLY_LAYERSif needed
- Confirm any custom pin/layout edits.
- Connect the Pro Micro over USB.
- In Arduino IDE, select
Tools > Board > Arduino Leonardo. - Select the correct port under
Tools > Port. - Verify the sketch.
- Upload the sketch.
If you also need custom USB naming, apply the boards.txt changes from DEVICES-MANAGEMENT.md before compiling.
DEVICES-MANAGEMENT.md currently tracks these device identities:
Akamai Box 5x5 - 96 Buttons(0x256c:0xc616, Hold mode, v2.5.1)Akamai Box 5x5 v2.1 - 96 Buttons(0x256a:0xc615, Hold mode, v2.1)Akamai Steering Wheel(0x255a:0xc610, 6x4, Hold mode, v2.5.1)RS - Akamai Steering Wheel(0x255a:0xc611, 5x5, Up/Down mode, v2.7.0)
Those entries document deployed variants. They are not all direct descriptions of the exact default sketch in this repo at this moment.
The sketch comments mention testing with:
- Windows 10
- Assetto Corsa
Since the board presents itself as a standard HID joystick, the same strategy should work with other sims and games that support generic game controllers.
If you extend this firmware, the safest order is:
- decide the physical matrix layout and pin map
- update the matrix preset and
buttons[][]mapping - reserve any silent modifier positions before assigning joystick outputs
- verify layer math and encoder output ranges do not overlap
- assign a unique
CONTROLLER_ID - register a unique VID/PID/device name in
DEVICES-MANAGEMENT.md
The layered output model is compact and flexible, but most regressions in this kind of firmware come from mismatched assumptions between physical matrix positions, reserved modifier keys, and joystick output numbering. The current implementation is structured specifically to keep those concerns separated.

