Skip to content
 
 

Repository files navigation

DIY Sim Racing Button Box

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.

Features

  • Arduino Pro Micro / ATmega32U4 HID joystick firmware
  • Default 5x5 matrix configuration with multiple matrix presets in the codebase (3x11, 6x5, 6x4, 7x3, 5x5)
  • 3-layer input 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 4 rotary encoders with per-layer clockwise and counter-clockwise actions
  • #define ROTARY_ONLY_LAYERS compile-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 100 declared 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:

What This Repo Is

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.

Current Firmware At A Glance

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 Up and Layer Down (default unassigned)
  • Layer programming buttons: 8 and 13
  • 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)

Source Of Truth

Use the repo in this order:

  1. button_box/button_box.ino for actual runtime behavior
  2. DEVICES-MANAGEMENT.md for USB identity allocation and per-device tracking
  3. buttonbox_layout.ai and buttonbox_wiring.drawio for physical reference assets

Firmware Architecture

The sketch is organized around four main responsibilities:

  1. Compile-time layout selection
  2. Matrix scanning and button state handling
  3. Layer-aware output mapping
  4. 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 loop
  • UpdateLayer() derives the current layer from the live selector button states
  • CheckAllEncoders() emits encoder pulses using the current layer
  • CheckProgrammingGestures() runs the long-press state machine for layer selector assignment, reset, and version output
  • CheckAllButtons() translates matrix events into joystick button state changes

Compile-Time Configuration Strategy

Most behavior is selected by preprocessor defines near the top of the sketch.

Controller Identity In The HID Report Layer

Each flashed box should get its own CONTROLLER_ID:

#define CONTROLLER_ID 2

This 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 ID 500
  • 2 -> report ID 510
  • 3 -> report ID 520
  • 4 -> report ID 530
  • fallback -> report ID 540

USB VID/PID/product-name management is handled separately in DEVICES-MANAGEMENT.md.

Matrix Presets

The sketch still carries multiple matrix presets:

  • DIMENSION_3x11
  • DIMENSION_6x5
  • DIMENSION_6x4
  • DIMENSION_7x3
  • DIMENSION_5x5

Each preset determines:

  • NUMROWS
  • NUMCOLS
  • NUMROTARIES
  • NUMBUTTONS
  • rowPins[]
  • colPins[]
  • the buttons[][] logical key map

The current default is:

#define DIMENSION_6x4

For 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};

Matrix Mapping Strategy

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},
};

Layering Strategy

The firmware supports a 3-layer system with a compile-time choice between two input models.

Layer Selection Mode

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_DOWN

Hold Mode

Hold 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.

Up/Down Mode

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.

Configurable Programming Buttons

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 13

These are ordinary matrix indices. If your device has unwired positions at 0 and 1, choose indices that are actually connected.

EEPROM Layout

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).

Rotary-Only Layer Mode

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).

Why Modifier Inputs Are Silent

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.

Output Numbering Strategy

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.

Step 1: Exclude Modifier Inputs From Normal Button Output

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.

Step 2: Map Physical Buttons To Outputs Per Layer

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.

Step 3: Reserve A Separate Encoder Range

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.

Why Releases Stay Correct Across Layer Changes

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.

Button Event Handling Strategy

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 if ROTARY_ONLY_LAYERS is enabled)
  • on RELEASED or IDLE, the sketch releases the previously stored output if one is active

This is effectively standard hold-to-press behavior.

Rotary Encoder Strategy

Rotary encoders are handled separately from the matrix.

Pin Allocation

The default build declares 4 encoders:

#define MAX_ROTARIES 4

The 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

Per-Layer Output Allocation

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.

Decoder Implementation

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:

  1. rotary_process() reads the two encoder pins
  2. it advances the state machine using ttable
  3. it returns DIR_CCW, DIR_CW, or no completed step
  4. CheckAllEncoders() selects the output pair for the current layer
  5. the chosen joystick button is pulsed with a 50 ms press

That last point is important: encoders are not held. They emit short button pulses so games see each detent as a discrete action.

Input Electrical Behavior

The sketch enables pull-ups:

#define ENABLE_PULLUPS

Encoder 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.

Device Identity Management

There are two different identity layers to keep in mind.

1. Internal Report Identity

This is controlled by CONTROLLER_ID -> JOYSTICK_REPORT_ID in the sketch.

2. USB Identity Seen By Windows And Games

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.

Build Assets And Their Limits

The repo still includes the original physical build assets.

Layout Template

Layout Template Download vector template

This is a real-size drilling template for a 200 x 120 mm case.

The original documented drill sizes are:

  • 6 mm
  • 7 mm
  • 12 mm
  • 14 mm
  • 16 mm

Wiring Diagram

Wiring Diagram Download drawio diagram

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.

Viewing Current Configuration

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:

  1. Connect the Pro Micro to your PC via USB
  2. In Arduino IDE, select Tools → Serial Monitor (or Ctrl+Shift+M)
  3. Set the baud rate to 9600 in the bottom-right dropdown
  4. 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.

Arduino Setup

Required Libraries

Install these before compiling:

Flashing Steps

  1. Install Arduino IDE.
  2. Install the Joystick library.
  3. Install the Keypad library from the Arduino Library Manager.
  4. Open button_box/button_box.ino in Arduino IDE.
  5. Review the PRE-DEPLOYMENT CONFIGURATION block and set:
    • CONTROLLER_ID for the device you are flashing
    • the correct DIMENSION_* matrix preset
    • LAYER_SELECTION_MODE_HOLD or LAYER_SELECTION_MODE_UP_DOWN
    • LAYER_PROG_BUTTON_2 and LAYER_PROG_BUTTON_3 to wired indices
    • BOX_VID, BOX_PID, and BOX_PRODUCT to match the target device entry in DEVICES-MANAGEMENT.md
    • ROTARY_ONLY_LAYERS if needed
  6. Confirm any custom pin/layout edits.
  7. Connect the Pro Micro over USB.
  8. In Arduino IDE, select Tools > Board > Arduino Leonardo.
  9. Select the correct port under Tools > Port.
  10. Verify the sketch.
  11. Upload the sketch.

If you also need custom USB naming, apply the boards.txt changes from DEVICES-MANAGEMENT.md before compiling.

Supported And Tracked Device Variants

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.

Compatibility

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.

Practical Guidance For Further Changes

If you extend this firmware, the safest order is:

  1. decide the physical matrix layout and pin map
  2. update the matrix preset and buttons[][] mapping
  3. reserve any silent modifier positions before assigning joystick outputs
  4. verify layer math and encoder output ranges do not overlap
  5. assign a unique CONTROLLER_ID
  6. 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.

About

DIY button box for sim racing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages