ESP32-based control system for autonomous go-kart using Bluepad32 for PS4/PS5 controller input, with PID-controlled steering and analog throttle/brake output.
- Microcontroller: ESP32 WROOM 32
- Controller Input: PS4/PS5 DualShock/DualSense via Bluetooth (Bluepad32)
- Steering Sensor: AS5600 Magnetic Angle Sensor (currently disabled - hardware not connected)
- Motor Control:
- DAC analog outputs for throttle/brake
- PWM/DIR outputs for steering motor
| Function | ESP32 Pin | Type | Range | Description |
|---|---|---|---|---|
| Throttle | GPIO 26 | DAC2 | 0-255 (8-bit) | Analog throttle output |
| Brake | GPIO 25 | DAC1 | 0-255 (8-bit) | Analog brake output |
| Steering PWM | GPIO 27 | PWM (LEDC) | 0-255 | Steering motor speed |
| Steering DIR | GPIO 14 | Digital | 0/1 | Steering motor direction |
| AS5600 Pin | ESP32 Pin | Signal |
|---|---|---|
| SCL | GPIO 22 | I2C Clock |
| SDA | GPIO 21 | I2C Data |
| VCC | 3.3V | Power |
| GND | GND | Ground |
Note: AS5600 sensor is disabled in firmware (main/sketch.cpp:4) until hardware is physically connected.
- LED: GPIO 2 (Onboard LED)
- UART to Orin: GPIO 18 (TX), GPIO 19 (RX) - for future integration
kart_medulla/
├── main/
│ ├── sketch.cpp # Main application (Bluepad32 + PID control)
│ └── main.c # ESP-IDF entry point
├── components/
│ ├── motor_control/ # DAC/PWM motor controllers
│ ├── pid_controller/ # PID control logic
│ └── as5600_sensor/ # I2C magnetic encoder (disabled)
├── managed_components/ # ESP-IDF dependencies (Bluepad32, Arduino)
├── CMakeLists.txt # ESP-IDF project config
└── platformio.ini # Build configuration
Framework: ESP-IDF 5.x with Arduino as component Build System: PlatformIO Controller Library: Bluepad32 (Bluetooth Classic HID)
- ✅ PS4/PS5 controller pairing via Bluepad32
- ✅ Real-time joystick input (left/right sticks)
- ✅ Trigger input (L2/R2) → Brake/Throttle DAC output
- ✅ PID steering control (Kp=10, verified optimal)
- ✅ PWM steering motor control
- ✅ CSV serial data output (50Hz)
- ✅ Real-time visualization GUI (
controller_gui.py) - ✅ Battery level monitoring
- ❌ AS5600 sensor (disabled - hardware not connected)
-
PlatformIO - Install via VS Code extension or CLI:
pip install platformio
-
Python 3 (for GUI) - with
pyserialandtkinter:pip install pyserial # tkinter usually included with Python
# Build firmware
pio run --environment esp32dev
# Upload to ESP32
pio run --target upload --environment esp32dev
# Build + Upload + Monitor (one command)
pio run --target upload --environment esp32dev && pio device monitorCommon Issue: If ESP32 stays in bootloader mode after upload, press the EN (reset) button once.
Simple Monitor (raw CSV data):
python3 monitor_serial.py # Auto-detect port
python3 monitor_serial.py /dev/cu.usbserial-0001 # Specify portController GUI (real-time visualization):
python3 controller_gui.py # Auto-detect port
python3 controller_gui.py /dev/cu.usbserial-0001 # Specify portPlatformIO Monitor:
pio device monitor # 115200 baud, auto-detects port| Platform | Port Pattern | Example |
|---|---|---|
| macOS | /dev/cu.* |
/dev/cu.usbserial-0001 |
| Linux | /dev/ttyUSB* or /dev/ttyACM* |
/dev/ttyUSB0 |
| Windows | COM* |
COM3 |
-
Ensure ESP32 is running - Serial should show:
=== Kart Medulla System Initialization === System ready! Waiting for controller... -
Put controller in pairing mode:
- Hold PS button + SHARE button for 3 seconds
- LED will flash rapidly (blue for PS5, white for PS4)
-
Wait for pairing:
CALLBACK: Controller is connected, index=0 Controller model: PS5 DualSense, VID=0x054c, PID=0x0ce6 -
Controller LED turns solid - Paired successfully!
- PS5 DualSense
- PS4 DualShock 4
- Xbox One/Series controllers
- Nintendo Switch Pro Controller
- Generic Bluetooth gamepads
All use the same API - no code changes needed to switch controllers.
ESP32 outputs CSV data every 20ms (50Hz):
DATA,LX,LY,RX,RY,L2,R2,BUTTONS,BATTERY,CHARGING,TARGET,ACTUAL,ERROR,PID,SENSOR_STATUSExample:
DATA,-8,4,4,12,0,512,0x0100,176,0,15.2,0.0,15.20,0.523,NO_SENSOR
Fields:
LX,LY- Left stick X/Y (-511 to +512)RX,RY- Right stick X/Y (-511 to +512)L2,R2- Trigger values (0-1023)BUTTONS- Button bitmask (hex)BATTERY- Battery level (0-255)TARGET- Target steering angle (degrees)ACTUAL- Current steering angle (degrees, requires sensor)ERROR- Steering error (TARGET - ACTUAL)PID- PID controller output (-1.0 to +1.0)SENSOR_STATUS-NO_SENSORorSENSOR_OK
Real-time visualization of controller inputs:
python3 controller_gui.pyFeatures:
- Left/Right joystick position visualizers
- L2/R2 trigger bars with percentages
- Button state display (all buttons)
- Steering angle gauge (target vs actual)
- PID output visualization
- Battery level indicator
- Sensor status
Performance: 20 FPS update rate with smart canvas redrawing for smooth visualization.
- Body Frame: X forward, Y left, Z up (right-hand rule)
- Steering Convention:
- Left joystick LEFT (negative axisX) → Positive angle (CCW rotation around Z)
- Left joystick RIGHT (positive axisX) → Negative angle (CW rotation around Z)
- Range: ±45° (configurable in
sketch.cpp:39)
Current Values (main/sketch.cpp:34-36):
const float KP = 10.0f; // Proportional gain (verified optimal)
const float KI = 0.0f; // Integral gain (disabled)
const float KD = 0.0f; // Derivative gain (not needed)Tuning Results:
- Kp=10 provides good accuracy (avg error 2.6°, max error 4.4°)
- Ki should remain 0 to avoid integral windup
- Kd not needed unless oscillation occurs
Note: PID currently runs in open-loop mode (no sensor feedback). When AS5600 is connected, system will switch to closed-loop control.
When AS5600 hardware is physically connected:
-
Uncomment sensor code in
main/sketch.cpp:#include "as5600_sensor.h" // Line 4 AS5600Sensor steeringSensor; // Line 21
-
Uncomment sensor initialization in
setup()(lines 165-171) -
Uncomment sensor reading in
processGamepad()(replace lines 77-78) -
Clean build and upload:
rm -rf .pio/build/esp32dev pio run --target upload --environment esp32dev
Important: DO NOT enable sensor code without hardware connected - I2C errors will flood serial output and prevent controller pairing.
- Check serial output - Should show "System ready! Waiting for controller..."
- Verify no I2C errors - If sensor code is enabled without hardware
- Reset ESP32 - Press EN button
- Try different controller - PS4/PS5/Xbox all supported
Symptom: Controller LEDs turn off after ~10 seconds of no input.
Solution: Already fixed in firmware - removed hasData() checks that blocked keepalive signals.
Check these:
- No
src/directory exists (should only havemain/) platformio.inihasframework = espidf(NOTarduino)platformio.inihassrc_dir = main- Clean build cache:
rm -rf .pio/build/esp32dev
macOS:
ls /dev/cu.* # Should show /dev/cu.usbserial-XXXXLinux:
ls /dev/ttyUSB* /dev/ttyACM*
sudo usermod -a -G dialout $USER # Add user to dialout group
# Log out and back in after thisWindows:
mode # Lists COM portsSee AGENTS.md for comprehensive documentation:
- Repository structure and critical conventions
- Build system configuration
- Common issues and solutions
- PID tuning history
- Hardware configuration details
| Button | Function | Usage |
|---|---|---|
| EN | Reset | Press once to restart ESP32 |
| BOOT | Bootloader | Hold during power-on/reset to enter flash mode |
This project is part of the UM Driverless autonomous kart system.
When making changes:
- Never create a
src/directory (usemain/only) - Keep
framework = espidfinplatformio.ini - Test with clean build:
rm -rf .pio/build && pio run - Verify controller pairing works after firmware updates
- Document any pin changes in this README
For issues or questions, see the UM Driverless team.