CodeRover is an ESP32-based differential drive robot built for autonomous point-to-point navigation and manual remote control.
The project combines Android computer vision, BLE communication, embedded motor control, Unity simulation, and a web joystick interface into a single robotics platform.
The robot supports two control modes.
The Android app detects the robot using an ArUco marker and estimates its pose from the phone camera. The user taps a target point on the live video feed, and a proportional controller computes the required wheel velocities to reach that point.
A browser-based joystick allows direct teleoperation of the robot through Web Bluetooth.
Both modes send commands to the ESP32, which drives the motors using PWM through an H‑bridge driver.
The robot uses a proportional controller that was first tuned in Unity simulation before deployment to hardware. The controller computes wheel velocities based on the robot’s pose error relative to a target point.
The robot’s current state is extracted from ArUco marker detection:
- Position (x, y): Center of the detected marker
- Heading (θ): Orientation vector from marker's bottom to top corner
val theta = Math.atan2(yTop - yBtm, xTop - xBtm) // Robot heading
val xCntr = mapX(result.centerX, result.centerY) // Robot position
val yCntr = mapY(result.centerX, result.centerY)Given a target point (x_t, y_t) selected by the user:
angleError = atan2(y_t - y, x_t - x) - θ
The angle error is normalized to [-π, π] to avoid wrap-around issues:
var angleError = angleToTarget - theta
while (angleError > Math.PI) angleError -= 2 * Math.PI
while (angleError < -Math.PI) angleError += 2 * Math.PIThe distance error is:
distanceError = sqrt((x_t - x)^2 + (y_t - y)^2)
The controller generates linear and angular velocity commands:
ω = kω * angleError
v = kv * distanceError
Where:
- kω = 0.6 (angular gain)
- kv = 0.3 (linear gain)
To ensure stable navigation, the robot first aligns itself with the target before moving forward:
v = 0 if |angleError| > π/4
v = kv * distanceError otherwise
This prevents the robot from drifting while turning sharply.
The linear and angular velocities are converted to individual wheel speeds using the robot's kinematics model:
v_right = v + (ω * L / 2)
v_left = v - (ω * L / 2)
Where:
- L = 100 mm is the wheelbase (distance between wheels).
vRight = v - (w * wheelbase / 2.0)
vLeft = v + (w * wheelbase / 2.0)- The control loop runs at 20 Hz (50 ms throttle) to balance responsiveness with BLE bandwidth.
- Wheel velocities are sent as CSV strings over BLE:
"vLeft,vRight,honk". - The ESP32 maps these velocities to 8-bit PWM duty cycles (0–255) for motor control.
This control strategy enables smooth point-to-point navigation while maintaining stability through the turn-in-place behavior and carefully tuned proportional gains.
The CodeRover uses Bluetooth Low Energy (BLE) for wireless communication between controllers (Android/Web) and the ESP32 rover. The protocol is designed for low-latency, real-time motor control with built-in safety features.
| Attribute | Value | Purpose |
|---|---|---|
| Device Name | CarRover | Identifiable device name during scanning |
| Service UUID | 12345678-1234-1234-1234-1234567890ab | Custom service for motor control |
| Characteristic UUID | abcd1234-1234-1234-1234-abcdef123456 | Write-only characteristic for commands |
| Property | WRITE_NR (Write Without Response) | Minimizes latency for real-time control |
Commands are sent as CSV strings with three values:
vLeft, vRight, honk
Where value ranges are
- vLeft, vRight: -255 to 255 (negative = reverse, positive = forward, 0 to brake)
- honk: 0 (off) or = 1 (on)
// BLEManager sends formatted payload
val payload = String.format(Locale.US, "%.3f, %.3f, %d", vLeft, vRight, honk)
chr.value = payload.toByteArray(Charsets.UTF_8)
bluetoothGatt?.writeCharacteristic(chr)// Parse incoming CSV payload
float vLeft = 0.0f, vRight = 0.0f, honk = 0.0f;
if (sscanf(val.c_str(), "%f, %f, %f", &vLeft, &vRight, &honk) >= 2) {
updateMotors(vLeft, vRight);
if (honk >= 1) Serial2.println(honk);
}// Web Bluetooth sends same CSV format
const dataString = `${vLeft},${vRight},${honk}`;
bleCharacteristic.writeValue(encoder.encode(dataString));The ESP32 implements a 300 ms safety watchdog that automatically stops the motors if no commands are received:
if (now - lastReceiveMillis >= TIMEOUT_MS) {
if (!stoppedByTimeout) {
stop();
stoppedByTimeout = true;
}
}The Android app automatically scans and reconnects if the connection is lost.
CodeRover is built around an ESP32 that drives two DC motors through an H-bridge motor driver. A separate Arduino Uno handles the horn module, which keeps the motor-control path simple and lets the honk actuator stay isolated from the main drive logic.
The H-bridge is used to control both speed and direction of each DC motor using PWM outputs from the ESP32. By toggling the input pins in opposite states, each motor can rotate in either direction.
The ESP32 uses four GPIO pins configured with LEDC PWM at 5 kHz and 8-bit resolution:
| ESP32 Pin | PWM Channel | Motor Side | Direction |
|---|---|---|---|
| GPIO 27 | 0 | Right | Backward |
| GPIO 26 | 1 | Right | Forward |
| GPIO 19 | 2 | Left | Backward |
| GPIO 13 | 3 | Left | Forward |
Each motor uses two pins for direction control. Setting one pin high and the other low drives the motor in one direction; swapping them reverses rotation.
The ESP32 communicates with the Arduino Uno over Serial2 to forward horn commands.
| ESP32 Pin | Purpose | Destination |
|---|---|---|
| GPIO 23 | Serial2 TX (115200 baud) | Arduino Uno RX |
The ESP32 sends honk commands through this serial link, and the Arduino Uno handles the actual horn actuation.
- Kotlin-based Android application
- OpenCV 4.12.0 for ArUco detection
- CameraX camera pipeline
- Control loop running at ~20 Hz
- BLE command transmission to ESP32
Responsibilities:
- Detect robot pose
- Compute control commands
- Send motor commands over BLE
- BLE server using NimBLE-Arduino
- Parses incoming motor commands
- Generates PWM via LEDC hardware timers
PWM configuration:
- Frequency: 5 kHz
- Resolution: 8-bit
Safety feature:
Motor watchdog timeout: 300 ms
If commands stop arriving, the robot automatically stops.
Browser-based interface for manual driving.
Features:
- Virtual joystick
- Speed and drift sliders
- Honk button
- Input logging with timestamps
Stack:
- Vanilla JavaScript
- Web Bluetooth
- FastAPI backend
- SQLite logging
Unity environment used to validate controller behavior before running on hardware.
Key scripts:
| Script | Purpose |
|---|---|
| Brain.cs | Proportional controller |
| DifferentialDriveController.cs | Wheel physics |
- Open
android_app/in Android Studio - Download OpenCV Android SDK 4.12.0
- Import it as module
opencv - Sync Gradle
- Run on device
Required permissions:
CAMERA
BLUETOOTH_SCAN
BLUETOOTH_CONNECT
ACCESS_FINE_LOCATION
Minimum Android version:
Android 7.0 (API 24)
- Install PlatformIO
- Open
esp32/project - Connect board
- Build and upload
Dependencies are installed automatically.
Flash the firmware in arduino/ and connect:
ESP32 GPIO 23 → Arduino RX (Pin 12)
cd web_gui
pip install fastapi uvicorn sqlalchemy
uvicorn server:app --reloadThen open:
static/index.html
in a Web Bluetooth compatible browser.
Install dependencies:
pip install jupyter numpy opencv-pythonOpen notebooks in:
ai/notebooks/
- Flowcharts were generated thanks to deepwiki







