A combat-style workshop robot built at Asmbly — controlled from your browser, no app required.
Author: Angel Hernandez — angel@thehomelab.dev · thehomelab.dev
AI Assistants (GitHub Copilot, Claude, etc.): Read
AI_CONTEXT.mdinstead — it contains the lean technical reference optimized for code generation.
Rotato is a small combat-style workshop robot built at Asmbly. It has two drive wheels and an optional weapon motor, all controlled from your phone or laptop through a browser — no app to install.
The brain is an ESP32-C3 Super Mini, a tiny WiFi + Bluetooth microcontroller. The firmware (the software running on it) is written in C++ and built with PlatformIO.
Plug in the battery. You'll hear a startup beep (if the buzzer is installed).
The robot creates its own WiFi hotspot on boot:
| Setting | Value |
|---|---|
| SSID | Rotato-XXXX (XXXX = unique 4-char code for your robot) |
| Password | 12345678 |
Connect your phone or laptop to this network.
Open a browser and go to: http://192.168.4.1
You'll see the robot controller UI with a joystick, weapon button, and status panel.
Open the Settings panel in the web UI, enter your WiFi network name and password, and save. The robot will reboot and join your network. If it can't connect, it falls back to its own hotspot automatically.
To reset WiFi credentials: Hold the BOOT button (small button on the ESP32 board) for 3 seconds after the robot has fully booted.
| GPIO | Connector | What's connected |
|---|---|---|
| GPIO0 | — | Battery voltage sensor (ADC) |
| GPIO1 | J5 | Left wheel ESC signal |
| GPIO3 | J6 | Right wheel ESC signal |
| GPIO4 | J7 | Weapon motor ESC signal |
| GPIO5 | J8 | Safety switch (short to GND = weapon blocked) |
| GPIO6 | J9 | Expansion — free for your own additions |
| GPIO7 | JP1 | Buzzer (only active if JP1 solder jumper is closed) |
| GPIO9 | — | Built-in BOOT button (hold 3s to reset WiFi) |
- Type: 2S LiPo (two cells in series)
- Safe range: 6.0V (empty) to 8.4V (full)
- The battery voltage is measured through a resistor divider on GPIO0. The web UI shows battery % and voltage.
All three motors use the standard RC servo signal: 50 Hz, pulse width 1000–2000 µs.
| Pulse | Effect |
|---|---|
| 1000 µs | Full reverse (drive) / stopped (weapon) |
| 1500 µs | Neutral / stop |
| 2000 µs | Full forward / full speed |
A physical kill-switch for the weapon. When triggered, the weapon motor stops immediately regardless of what the web UI says. Wire a switch between GPIO5 and GND on connector J8.
A passive buzzer on GPIO7, connected through solder jumper JP1. Produces startup, weapon-on, weapon-off, and error tones. If JP1 is not soldered, GPIO7 is free to use for something else.
The weapon ESC (motor speed controller) needs special handling:
-
On power-on, the weapon is locked for 2 seconds. The firmware holds the signal at minimum throttle (1000 µs) to arm the ESC. The weapon button in the UI is disabled until this completes.
-
ESC full-range calibration teaches the ESC what your signal range is. Trigger it from the Settings panel in the web UI. Always disconnect the weapon motor (unplug the motor wires) before running calibration. The ESC will beep to confirm each step.
-
The safety switch physically overrides the weapon. Even if the UI shows the weapon as "on", if the safety switch is triggered the weapon will not spin.
- VS Code with the PlatformIO extension
- A USB-C cable connected to the ESP32-C3 Super Mini
# 1. Upload the web UI to the robot's filesystem first
pio run --target uploadfs
# 2. Then flash the firmware
pio run --target uploadAfter that, for firmware-only changes you only need:
pio run --target upload# Build without flashing (check for errors)
pio run
# Open the serial monitor to see debug output
pio device monitorThe robot prints detailed information over USB Serial at 115200 baud. Open the serial monitor right after powering on to see the boot sequence, IP address, and any errors.
Almost all hardware customization lives in a single file: src/config.h. It is heavily commented. Here are the most common things to change:
#define MOTOR_LEFT_REVERSED false // change to true
#define MOTOR_RIGHT_REVERSED true // change to false#define AP_SSID_PREFIX "Rotato-" // your robot will be "Rotato-XXXX"
#define AP_PASSWORD "12345678" // must be 8+ characters// Comment out this line:
// #define HAS_BUZZER// Comment out this line:
// #define HAS_SAFETY_SWITCH#define PWM_DRIVE_HALF_RANGE_US 500 // try increasing to 600 or 700| File | What it does | Should you edit it? |
|---|---|---|
src/config.h |
All hardware settings | ✅ Yes — start here |
src/version.h |
Firmware version number | ✅ Yes — bump when releasing |
src/main.cpp |
Wires everything together | ✅ Yes — for new features |
src/hardware/*.h/.cpp |
Motor, buzzer, battery, safety drivers | ✅ Yes |
src/connectivity/*.h/.cpp |
WiFi, web server, BLE | ✅ Yes |
data/index.html |
The web UI running in your browser | ✅ Yes |
src/version_build.h |
Auto-generated — build timestamp | ❌ No |
src/web_ui_html.h |
Auto-generated — embedded web UI | ❌ No |
build_number.txt |
Auto-incremented build counter | ❌ No |
scripts/*.py |
Build pipeline scripts |
Web UI → Settings → Robot Name → type a name → Save. The robot reboots and uses the new name as its WiFi SSID.
If the battery percentage looks wrong, go to the web UI Settings → Battery Calibration. Adjust the ratio until the displayed voltage matches a multimeter reading.
- Open
src/hardware/BuzzerController.cpp - Define a new sequence array:
static const BeepStep mySound[] = {{800,100},{0,50},{1200,200}}; - Add
void beepMyEvent() { playSequence(mySound, 3); }to the class - Declare it in
BuzzerController.h - Call
buzzer.beepMyEvent()frommain.cpp
- Add the HTML/JS to
data/index.html - Send a new JSON message over WebSocket, e.g.
{"type":"myCommand","value":42} - Add a callback type and
onMyCommand()registration method inWebServerManager.h - Parse the new type in
WebServerManager.cpp - Register and handle the callback in
main.cpp
AI Assistants: See
AI_CONTEXT.mdfor the technical reference optimized for code generation.