HAN Competitive
Robotics Team
Jun 2025 – Present · Featherweight class · ESP32 · nRF24L01+ · FreeRTOS
The robot
Image placeholder
As founder and technical lead of HAN's competitive robotics team, I built a featherweight combat robot's full control stack from scratch: a Python ground station, a 2.4 GHz radio bridge, and dual-core ESP32 firmware. One principle runs through the whole design: the driver station stays stateless, and the robot enforces every safety rule on its own.
Driver station
Laptop · Python / Pygame
Gamepad or keyboard input, arcade & tank mixing, the arming state machine.
Radio bridge
nRF52840 dongle · Zephyr
A USB-CDC to 2.4 GHz bridge that relays packets to the robot.
Robot
ESP32 DevKit · ESP-IDF
Receives packets and drives everything: motors, weapon, and all failsafes.
driver station → radio bridge → robot · one-way, un-ACKed ESB link by design
Drive
Two bidirectional motors on left/right ESCs (GPIO 13/14).
Weapon
Brushless motor + 8BL150 150 A ESC, with a Hall encoder (2 ppr) read via the ESP32 PCNT peripheral.
PWM
All three ESCs run as 50 Hz servo PWM (1000–2000 µs) off a single MCPWM timer, so they stay phase-synced.
Radio
nRF24L01+ on SPI (8 MHz, manual CS), speaking Nordic ESB on channel 40 at 2 Mbps.
Sensors
Three BMP280s used as temperature sensors for two drive wheels and the weapon, across two I²C buses.
Power
Current-sense driver code is present but inactive; battery spec to be added.
Electronics bay
Image placeholder
Three FreeRTOS tasks split across both ESP32 cores, talking through a depth-1 overwrite queue (always-latest state) and C11 atomics for the thermal flags and encoder RPM.
Radio RX & drive
Blocks on an interrupt semaphore rather than polling, writes the drive ESC outputs, and runs the packet-timeout failsafe.
Weapon loop @ 100 Hz
A fixed-rate control loop paced precisely with vTaskDelayUntil.
Thermal monitor @ 1 Hz
Polls the BMP280s and raises the thermal flags the other tasks act on.
- Drive mixing lives in the ground station, not the robot. The robot maps each byte linearly to ±100% throttle; arcade/tank mixing, and a flip-recovery drive-invert, run in Python.
- The weapon's PI controller runs open-loop. The closed-loop framework (anti-windup, 3000/5000 RPM targets) is in place, but the gains are zero, so it runs on a 50% feed-forward baseline. Encoder RPM is read and logged, but not yet closing the loop.
- The radio link is one-way. The robot prints RPM and temperatures to local USB serial for debugging. Nothing is sent back to the driver.
The driver station is stateless; the robot enforces all of this independently.
Signal-loss failsafe
500 ms with no valid packet stops all motors and zeroes the weapon. It auto-recovers on the next good packet.
Killswitch → deep sleep
A killswitch bit latches a hard failsafe and drops the ESP32 into deep sleep; power-cycle only to recover. The station fires a 5-packet burst so a dropped one still lands.
Thermal deep-sleep @ 90 °C
Checked on every received packet (~20 ms), not in the slow 1 Hz loop, so an overheating pack trips it fast.
Per-motor cutoff @ 100 °C
The only reversible thermal protection, with 10 °C hysteresis. A failed or NaN sensor reading also cuts that motor.
Arming state machine
safe → idle → attack in the station; any disarm or failsafe forces neutral and drops straight back to safe.
The things I know aren't done yet, and would close next:
- No encoder-fault detection: if the Hall sensor disconnects, the weapon keeps spinning on the 50% feed-forward.
- The newline-delimited serial framing can desync if a motor byte happens to be 0x0A.
- The nRF link ACKs unnecessarily because of a feature-register setting.