Community project
Steering Wheel Controller
Build a racing sim controller with steering wheel, pedals, paddle shifters, and a button box—all controlled by an ESP32-S3 and displayed on a 3.4-inch touchscreen. This guide walks through wiring the potentiometers for analog inputs, connecting the six-button control panel, and setting up the ignition key switch and mode selector. The controller emulates a USB gamepad, making it compatible with racing simulators on PC.
This project delivers a complete parts list, wiring diagram, and ready-to-flash firmware that handles all analog and digital inputs while rendering a live dashboard overlay on the TFT display. Assembly takes a few hours and requires basic soldering; no prior experience with game controllers or ESP32 development is needed.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| 10kΩ Potentiometer10kΩ | 1 | A 3-terminal passive resistive voltage divider with a total resistance of 10kΩ. One end connects to 5V, the other to GND, and the wiper outputs a variable voltage between 0V and 5V. |
| 10kΩ Potentiometer10kΩ | 1 | A 3-terminal passive resistive voltage divider with a total resistance of 10kΩ. One end connects to 5V, the other to GND, and the wiper outputs a variable voltage between 0V and 5V. |
| 10kΩ Potentiometer10kΩ | 1 | A 3-terminal passive resistive voltage divider with a total resistance of 10kΩ. One end connects to 5V, the other to GND, and the wiper outputs a variable voltage between 0V and 5V. |
| 10kΩ Potentiometer10kΩ | 1 | A 3-terminal passive resistive voltage divider with a total resistance of 10kΩ. One end connects to 5V, the other to GND, and the wiper outputs a variable voltage between 0V and 5V. |
| Push ButtonShift Up (Right Paddle) | 1 | Momentary push button switch |
| Push ButtonShift Down (Left Paddle) | 1 | Momentary push button switch |
| Push ButtonManual/Auto Toggle | 1 | Momentary push button switch |
| ILI9341 TFT Touchscreen | 1 | 240x320 SPI TFT display using the ILI9341 LCD controller with an XPT2046 resistive touch controller sharing the SPI bus |
| 10kΩ Potentiometer10kΩ | 1 | A 3-terminal passive resistive voltage divider with a total resistance of 10kΩ. One end connects to 5V, the other to GND, and the wiper outputs a variable voltage between 0V and 5V. |
| 2-Position Key Switch | 1 | A panel-mount key switch with two positions: OFF and ON/ACC. Outputs a digital HIGH when turned to ON, LOW when off. Used to trigger engine start/stop in the sim. |
| Button Box (6 Buttons) | 1 | Six panel-mount momentary buttons for sim functions: Lights, Wipers, Traction Control, ABS, Pit Limiter, DRS. Each button connects between signal pin and GND, using internal pull-ups. |
Assembly
10 stepsMount the ESP32-S3 DevKitC-1
Mount the ESP32-S3-DevKitC-1 board centrally inside the cabinet on a non-conductive base. Secure it with standoffs or double-sided tape. Leave the USB-C port accessible — this is your PC connection for HID and power.
- Tip: Route a USB-C cable from the board port to the front or rear of the cabinet for easy PC connection.
Wire the steering wheel potentiometer
Mount a 10kΩ linear pot on the steering column shaft. Connect End1 to the 3.3V pin, End2 to GND, and Wiper to GPIO1.
- ⚠ Use 3.3V only — NEVER 5V — for all potentiometers. The ESP32-S3 ADC is 3.3V max.
Wire the pedal potentiometers
Mount three 10kΩ pots on the gas, brake, and clutch pedal mechanisms. For each: End1 → 3.3V, End2 → GND. Gas Wiper → GPIO2, Brake Wiper → GPIO3, Clutch Wiper → GPIO4.
- Tip: A return spring or rubber stop on each pedal makes the sim feel much more realistic.
Wire the handbrake lever potentiometer
Mount a 10kΩ pot at the pivot of your handbrake lever so pulling the lever rotates the pot shaft. Connect End1 → 3.3V, End2 → GND, Wiper → GPIO5.
- Tip: A short lever (10–15 cm) with the pot at the base pivot gives the best feel.
Wire the paddle shifters
Mount two momentary switches behind the steering wheel — right paddle to GPIO14, left paddle to GPIO21. Both share GND. The firmware uses internal pull-ups, so no external resistors needed.
- Tip: Micro-switches (like those in a mouse) give a satisfying click feel for paddle shifters.
Wire the Manual/Auto mode button
Mount a momentary button on the dashboard or steering column. Connect SIGNAL to GPIO38 and GND to GND.
- Tip: A flush-mount panel button works well here — label it M/A.
Wire the ignition key switch
Mount a 2-position panel key switch on the dashboard. Connect SIGNAL to GPIO6 and GND to GND. The firmware uses an internal pull-up — turning the key LOW triggers engine ON/OFF toggle.
- Tip: Key switches are available on AliExpress/Amazon for ~$2 in panel-mount format.
- Tip: A short barrel-key switch (12mm panel mount) looks the most realistic.
- ⚠ The key switch toggles engine state on each turn — make sure it's in OFF position before powering up.
Wire the button box
Mount 6 momentary buttons on a panel (the button box). Wire them as follows — all share a common GND rail: • BTN1 (Lights) → GPIO7 • BTN2 (Wipers) → GPIO15 • BTN3 (Traction Control) → GPIO16 • BTN4 (ABS) → GPIO17 • BTN5 (Pit Limiter) → GPIO18 • BTN6 (DRS) → GPIO47
- Tip: Label each button clearly with a label maker or printed panel insert.
- Tip: 16mm or 19mm illuminated panel buttons look great on a button box — the LED ring just needs 3.3V and a resistor.
Wire the TFT touchscreen display
Connect the ILI9341 2.8" TFT to the SPI bus: • VCC → 3.3V • GND → GND • MOSI → GPIO11 • MISO → GPIO13 • SCK → GPIO12 • TFT_CS → GPIO10 • TFT_DC → GPIO39 • TFT_RST → GPIO40 • TOUCH_CS → GPIO41 • TOUCH_IRQ → GPIO42 Mount the screen on the cabinet dashboard where the driver can see it as an instrument cluster.
- Tip: Hot-glue or a 3D-printed bezel looks great for mounting the TFT in the dashboard.
- Tip: The TFT shows Speed (MPH) and Gear only — touch MENU CAR/BG to change the theme.
Connect to PC and open the overlay
Plug the USB-C cable from the ESP32-S3 into your PC. The board appears as a USB HID gamepad immediately. Open data/pc_overlay.html in Chrome or Edge, click anywhere on the page, and select the ESP32 COM port from the browser's serial picker (115200 baud). The overlay shows mode badge, gas/brake/handbrake bars, steering indicator, engine status, and button box lights.
- Tip: If the serial picker doesn't appear, make sure you're opening the file via localhost or a local web server — Web Serial only works on HTTPS or localhost in Chrome/Edge.
- Tip: Use joy.cpl on Windows to verify all axes and buttons before launching a game.
Firmware
ESP32#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <XPT2046_Touchscreen.h>
#include "USB.h"
#include "USBHIDGamepad.h"
// ── Pin definitions ──────────────────────────────────────────
#define WHEEL_PIN 1
#define GAS_PIN 2
#define BRAKE_PIN 3
#define CLUTCH_PIN 4
#define PADDLE_UP_PIN 14
#define PADDLE_DN_PIN 21
#define MODE_BTN_PIN 38
#define HANDBRAKE_PIN 5
#define IGNITION_PIN 6
#define BTN1_PIN 7
#define BTN2_PIN 15
#define BTN3_PIN 16
#define BTN4_PIN 17
#define BTN5_PIN 18
#define BTN6_PIN 47
#define TFT_CS 10
#define TFT_DC 39
#define TFT_RST 40
#define TOUCH_CS 41
#define TOUCH_IRQ 42
// ── Display & Touch ──────────────────────────────────────────
// Only two states now: dashboard or overlay open
bool menuOpen = false;
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
XPT2046_Touchscreen touch(TOUCH_CS, TOUCH_IRQ);
// ── USB HID Gamepad ──────────────────────────────────────────
USBHIDGamepad Gamepad;
// ── Colours ──────────────────────────────────────────────────
#define C_BG 0x0841 // dark charcoal
#define C_RED 0xF800
#define C_GREEN 0x07E0
#define C_BLUE 0x001F
#define C_YELLOW 0xFFE0
#define C_WHITE 0xFFFF
#define C_GREY 0x8410
#define C_ORANGE 0xFD20
#define C_DARKGREY 0x4208
// ── Car & background presets ────────────────────────────────
const char* CAR_NAMES[] = {"F1 Car", "GT500", "Rally", "Muscle"};
const char* BG_NAMES[] = {"Night", "Sunset", "Desert", "Track" };
const uint16_t CAR_COLOURS[] = {C_RED, C_BLUE, C_YELLOW, C_ORANGE};
const uint16_t BG_COLOURS[] = {0x0841, 0x8C00, 0xC600, 0x0460};
#define NUM_CARS 4
#define NUM_BGS 4
// ── State ────────────────────────────────────────────────────
bool autoMode = false;
int gear = 1;
uint32_t lastShiftMs = 0;
const uint32_t DEBOUNCE_MS = 50;
const uint32_t AUTO_SHIFT_MS = 800;
#define AUTO_UP_THRESH 200 // out of 255 (~78%)
#define AUTO_DN_THRESH 50 // out of 255 (~20%)
int selectedCar = 0;
int selectedBg = 0;
// Previous values for selective TFT redraw
int prevGear = -1;
int prevSpeedDisp = -1;
// ── Touch calibration (adjust if needed) ────────────────────
#define TOUCH_X_MIN 350
#define TOUCH_X_MAX 3700
#define TOUCH_Y_MIN 350
#define TOUCH_Y_MAX 3800
// ── Forward declarations ─────────────────────────────────────
void drawDashboard(bool full);
void drawMenuOverlay();
void closeMenu();
void handleTouch();
int mapTouch(int raw, int rMin, int rMax, int oMin, int oMax);
// ─────────────────────────────────────────────────────────────
void setup() {
// Buttons & switches
pinMode(PADDLE_UP_PIN, INPUT_PULLUP);
pinMode(PADDLE_DN_PIN, INPUT_PULLUP);
pinMode(MODE_BTN_PIN, INPUT_PULLUP);
pinMode(IGNITION_PIN, INPUT_PULLUP);
pinMode(BTN1_PIN, INPUT_PULLUP);
pinMode(BTN2_PIN, INPUT_PULLUP);
pinMode(BTN3_PIN, INPUT_PULLUP);
pinMode(BTN4_PIN, INPUT_PULLUP);
pinMode(BTN5_PIN, INPUT_PULLUP);
pinMode(BTN6_PIN, INPUT_PULLUP);
// Serial (for PC companion overlay)
Serial.begin(115200);
// Display
SPI.begin(12, 13, 11); // SCK, MISO, MOSI
tft.begin();
tft.setRotation(1); // landscape
touch.begin();
touch.setRotation(1);
menuOpen = false;
tft.fillScreen(BG_COLOURS[selectedBg]);
drawDashboard(true);
// USB HID
Gamepad.begin();
USB.begin();
}
// ─────────────────────────────────────────────────────────────
void loop() {
uint32_t now = millis();
// ── Read ADC axes ────────────────────────────────────────
int rawWheel = analogRead(WHEEL_PIN);
int rawGas = analogRead(GAS_PIN);
int rawBrake = analogRead(BRAKE_PIN);
int rawClutch = analogRead(CLUTCH_PIN);
int rawHandbrake = analogRead(HANDBRAKE_PIN);
// Map to 0–255 for HID
int axisWheel = map(rawWheel, 0, 4095, -127, 127);
int axisGas = map(rawGas, 0, 4095, 0, 255);
int axisBrake = map(rawBrake, 0, 4095, 0, 255);
int axisClutch = map(rawClutch, 0, 4095, 0, 255);
int axisHandbrake = map(rawHandbrake, 0, 4095, 0, 255);
// ── Ignition key ─────────────────────────────────────────
static bool lastIgnition = HIGH;
static bool engineOn = false;
bool ignState = digitalRead(IGNITION_PIN);
if (lastIgnition == HIGH && ignState == LOW) {
engineOn = !engineOn; // toggle on key turn
if (!engineOn) { gear = 1; } // reset gear on engine off
}
lastIgnition = ignState;
// ── Button box reads ─────────────────────────────────────
bool bb1 = !digitalRead(BTN1_PIN); // active LOW
bool bb2 = !digitalRead(BTN2_PIN);
bool bb3 = !digitalRead(BTN3_PIN);
bool bb4 = !digitalRead(BTN4_PIN);
bool bb5 = !digitalRead(BTN5_PIN);
bool bb6 = !digitalRead(BTN6_PIN);
// ── Mode toggle ──────────────────────────────────────────
static bool lastModeBtn = HIGH;
bool modeBtn = digitalRead(MODE_BTN_PIN);
if (lastModeBtn == HIGH && modeBtn == LOW && (now - lastShiftMs) > DEBOUNCE_MS) {
autoMode = !autoMode;
gear = 1;
lastShiftMs = now;
if (!menuOpen) drawDashboard(true);
}
lastModeBtn = modeBtn;
// ── Shifting ─────────────────────────────────────────────
if (!autoMode) {
static bool lastUp = HIGH, lastDn = HIGH;
bool upBtn = digitalRead(PADDLE_UP_PIN);
bool dnBtn = digitalRead(PADDLE_DN_PIN);
if (lastUp == HIGH && upBtn == LOW && (now - lastShiftMs) > DEBOUNCE_MS) {
if (gear < 6) { gear++; lastShiftMs = now; }
}
if (lastDn == HIGH && dnBtn == LOW && (now - lastShiftMs) > DEBOUNCE_MS) {
if (gear > 1) { gear--; lastShiftMs = now; }
}
lastUp = upBtn;
lastDn = dnBtn;
} else {
if ((now - lastShiftMs) > AUTO_SHIFT_MS) {
if (axisGas > AUTO_UP_THRESH && gear < 6) { gear++; lastShiftMs = now; }
else if (axisGas < AUTO_DN_THRESH && gear > 1) { gear--; lastShiftMs = now; }
}
}
// ── Simulated speed (MPH) from gas + gear ───────────────
// Pure display value — real speed comes from the game on the PC
int simSpeed = (axisGas * gear * 15) / 255;
if (simSpeed > 199) simSpeed = 199;
// ── HID report ───────────────────────────────────────────
Gamepad.leftStick(axisWheel, 0);
Gamepad.rightStick(axisHandbrake, axisClutch); // handbrake = Ry, clutch = Rx
uint32_t buttons = 0;
buttons |= (1u << (gear - 1)); // buttons 1-6: gear
if (autoMode) buttons |= (1u << 6); // button 7: auto mode
if (engineOn) buttons |= (1u << 7); // button 8: ignition/engine
if (bb1) buttons |= (1u << 8); // button 9: Lights
if (bb2) buttons |= (1u << 9); // button 10: Wipers
if (bb3) buttons |= (1u << 10); // button 11: Traction Control
if (bb4) buttons |= (1u << 11); // button 12: ABS
if (bb5) buttons |= (1u << 12); // button 13: Pit Limiter
if (bb6) buttons |= (1u << 13); // button 14: DRS
Gamepad.buttons(buttons);
Gamepad.send();
// ── Touch / menu ─────────────────────────────────────────
handleTouch();
// ── Serial JSON → PC overlay ─────────────────────────────
// Send at ~20 Hz so the PC companion app can display
// mode, gas, brake, steering without adding display load to TFT
static uint32_t lastSerialMs = 0;
if (now - lastSerialMs >= 50) {
lastSerialMs = now;
int gasDisp = axisGas * 100 / 255;
int brakeDisp = axisBrake * 100 / 255;
int steerDisp = map(rawWheel, 0, 4095, -100, 100);
Serial.print('{');
Serial.print("\"mode\":\""); Serial.print(autoMode ? "AUTO" : "MANUAL"); Serial.print('"');
Serial.print(",\"gas\":"); Serial.print(gasDisp);
Serial.print(",\"brake\":"); Serial.print(brakeDisp);
Serial.print(",\"steer\":"); Serial.print(steerDisp);
Serial.print(",\"gear\":"); Serial.print(gear);
Serial.print(",\"speed\":"); Serial.print((axisGas * gear * 15) / 255);
Serial.print(",\"handbrake\":"); Serial.print(axisHandbrake * 100 / 255);
Serial.print(",\"engine\":"); Serial.print(engineOn ? 1 : 0);
Serial.print(",\"lights\":"); Serial.print(bb1 ? 1 : 0);
Serial.print(",\"wipers\":"); Serial.print(bb2 ? 1 : 0);
Serial.print(",\"tc\":"); Serial.print(bb3 ? 1 : 0);
Serial.print(",\"abs\":"); Serial.print(bb4 ? 1 : 0);
Serial.print(",\"pitlimit\":"); Serial.print(bb5 ? 1 : 0);
Serial.print(",\"drs\":"); Serial.print(bb6 ? 1 : 0);
Serial.println('}');
}
// ── TFT instrument cluster update (speed + gear only) ─────
if (!menuOpen) {
int speedDisp = (axisGas * gear * 15) / 255;
if (speedDisp > 199) speedDisp = 199;
if (gear != prevGear || speedDisp != prevSpeedDisp) {
drawDashboard(false);
prevGear = gear;
prevSpeedDisp = speedDisp;
}
}
delay(10);
}
// ─────────────────────────────────────────────────────────────
int mapTouch(int raw, int rMin, int rMax, int oMin, int oMax) {
return constrain(map(raw, rMin, rMax, oMin, oMax), oMin, oMax);
}
// ── Draw the TFT instrument cluster: Speed + Gear only ───────
void drawDashboard(bool full) {
int rawGas = analogRead(GAS_PIN);
int axisGas = map(rawGas, 0, 4095, 0, 255);
int simSpeed = (axisGas * gear * 15) / 255;
if (simSpeed > 199) simSpeed = 199;
uint16_t bg = BG_COLOURS[selectedBg];
uint16_t acc = CAR_COLOURS[selectedCar];
if (full) {
tft.fillScreen(bg);
// ── Title bar ─────────────────────────────────────────
tft.fillRect(0, 0, 320, 26, C_DARKGREY);
tft.setTextColor(C_WHITE); tft.setTextSize(1);
tft.setCursor(6, 9);
tft.print("INSTRUMENT CLUSTER");
tft.setTextColor(acc);
tft.setCursor(210, 9);
tft.print(CAR_NAMES[selectedCar]);
// ── Divider line ──────────────────────────────────────
tft.drawFastHLine(0, 26, 320, C_GREY);
tft.drawFastVLine(160, 26, 194, C_GREY); // centre divider
// ── Labels ────────────────────────────────────────────
tft.setTextColor(C_GREY); tft.setTextSize(1);
tft.setCursor(10, 34); tft.print("SPEED (MPH)");
tft.setCursor(172, 34); tft.print("GEAR");
// ── MENU button (bottom-right, touchable) ─────────────
tft.fillRoundRect(222, 208, 88, 24, 6, C_DARKGREY);
tft.setTextColor(C_WHITE); tft.setTextSize(1);
tft.setCursor(234, 218); tft.print("MENU CAR/BG");
} else {
// Erase number areas only
tft.fillRect(6, 48, 148, 90, bg);
tft.fillRect(166, 48, 148, 90, bg);
}
// ── Speed — large centred in left half ────────────────────
tft.setTextColor(C_WHITE); tft.setTextSize(7);
// 3-digit field: each char ~42px wide at size 7
int sx = (simSpeed < 10) ? 46 : (simSpeed < 100) ? 25 : 6;
tft.setCursor(sx, 52);
tft.print(simSpeed);
// MPH unit label
tft.setTextColor(C_GREY); tft.setTextSize(1);
tft.setCursor(10, 142); tft.print("MPH");
// ── Gear — large centred in right half ────────────────────
tft.setTextColor(acc); tft.setTextSize(7);
tft.setCursor(204, 52); // single digit, centred in right half
tft.print(gear);
}
// ── Menu overlay (drawn ON TOP of the dashboard) ─────────────
// Layout (centred panel 300x220 at x=10, y=10):
// Title row
// CAR section: 4 car buttons in 2 columns
// BG section: 4 bg buttons in 2 columns
// CLOSE button at bottom
#define OVL_X 10
#define OVL_Y 8
#define OVL_W 300
#define OVL_H 226
#define OVL_BTN_W 130
#define OVL_BTN_H 26
void drawMenuOverlay() {
// Dim background with a dark semi-frame
tft.fillRoundRect(OVL_X, OVL_Y, OVL_W, OVL_H, 10, 0x1082); // very dark grey
tft.drawRoundRect(OVL_X, OVL_Y, OVL_W, OVL_H, 10, C_WHITE);
// Title
tft.setTextColor(C_WHITE); tft.setTextSize(1);
tft.setCursor(OVL_X + 8, OVL_Y + 6);
tft.print("SETTINGS");
// Divider
tft.drawFastHLine(OVL_X + 1, OVL_Y + 18, OVL_W - 2, C_GREY);
// ── CAR section ────────────────────────────────────────────
tft.setTextColor(C_GREY); tft.setTextSize(1);
tft.setCursor(OVL_X + 8, OVL_Y + 22);
tft.print("CAR");
// 4 cars in 2 columns, 2 rows (each button 130x26)
for (int i = 0; i < NUM_CARS; i++) {
int col = i % 2; // 0 or 1
int row = i / 2; // 0 or 1
int bx = OVL_X + 8 + col * (OVL_BTN_W + 6);
int by = OVL_Y + 32 + row * (OVL_BTN_H + 6);
bool sel = (i == selectedCar);
tft.fillRoundRect(bx, by, OVL_BTN_W, OVL_BTN_H, 5,
sel ? CAR_COLOURS[i] : C_DARKGREY);
if (sel) tft.drawRoundRect(bx, by, OVL_BTN_W, OVL_BTN_H, 5, C_WHITE);
tft.setTextColor(C_WHITE); tft.setTextSize(1);
tft.setCursor(bx + 6, by + 9);
tft.print(CAR_NAMES[i]);
}
// ── BG section ─────────────────────────────────────────────
int bgSectionY = OVL_Y + 32 + 2 * (OVL_BTN_H + 6) + 8;
tft.drawFastHLine(OVL_X + 1, bgSectionY - 4, OVL_W - 2, C_DARKGREY);
tft.setTextColor(C_GREY); tft.setTextSize(1);
tft.setCursor(OVL_X + 8, bgSectionY);
tft.print("BACKGROUND");
for (int i = 0; i < NUM_BGS; i++) {
int col = i % 2;
int row = i / 2;
int bx = OVL_X + 8 + col * (OVL_BTN_W + 6);
int by = bgSectionY + 12 + row * (OVL_BTN_H + 6);
bool sel = (i == selectedBg);
tft.fillRoundRect(bx, by, OVL_BTN_W, OVL_BTN_H, 5, BG_COLOURS[i]);
if (sel) tft.drawRoundRect(bx, by, OVL_BTN_W, OVL_BTN_H, 5, C_WHITE);
tft.setTextColor(C_WHITE); tft.setTextSize(1);
tft.setCursor(bx + 6, by + 9);
tft.print(BG_NAMES[i]);
}
// ── CLOSE button ───────────────────────────────────────────
int closeY = OVL_Y + OVL_H - 30;
tft.fillRoundRect(OVL_X + 80, closeY, 140, 24, 6, C_GREEN);
tft.setTextColor(0x0000); tft.setTextSize(1);
tft.setCursor(OVL_X + 112, closeY + 8);
tft.print("CLOSE");
}
void closeMenu() {
menuOpen = false;
drawDashboard(true); // full redraw restores dashboard
}
// ── Touch handler ────────────────────────────────────────────
void handleTouch() {
if (!touch.touched()) return;
TS_Point p = touch.getPoint();
// Map raw touch to screen coords (landscape)
int tx = mapTouch(p.x, TOUCH_X_MIN, TOUCH_X_MAX, 0, 320);
int ty = mapTouch(p.y, TOUCH_Y_MIN, TOUCH_Y_MAX, 0, 240);
delay(30); // debounce touch
while (touch.touched()) {};
if (!menuOpen) {
// ── Dashboard: only the MENU button is tappable ──────────
if (tx >= 222 && tx <= 310 && ty >= 196 && ty <= 222) {
menuOpen = true;
drawMenuOverlay();
}
} else {
// ── Overlay is open ──────────────────────────────────────
// CAR buttons: 2 columns x 2 rows starting at (OVL_X+8, OVL_Y+32)
for (int i = 0; i < NUM_CARS; i++) {
int col = i % 2;
int row = i / 2;
int bx = OVL_X + 8 + col * (OVL_BTN_W + 6);
int by = OVL_Y + 32 + row * (OVL_BTN_H + 6);
if (tx >= bx && tx <= bx + OVL_BTN_W &&
ty >= by && ty <= by + OVL_BTN_H) {
selectedCar = i;
drawMenuOverlay();
return;
}
}
// BG buttons: same grid but offset to bgSectionY
int bgSectionY = OVL_Y + 32 + 2 * (OVL_BTN_H + 6) + 8;
for (int i = 0; i < NUM_BGS; i++) {
int col = i % 2;
int row = i / 2;
int bx = OVL_X + 8 + col * (OVL_BTN_W + 6);
int by = bgSectionY + 12 + row * (OVL_BTN_H + 6);
if (tx >= bx && tx <= bx + OVL_BTN_W &&
ty >= by && ty <= by + OVL_BTN_H) {
selectedBg = i;
drawMenuOverlay();
return;
}
}
// CLOSE button
int closeY = OVL_Y + OVL_H - 30;
if (tx >= OVL_X + 80 && tx <= OVL_X + 220 &&
ty >= closeY && ty <= closeY + 24) {
closeMenu();
}
}
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.