Personal hardware project
ESP32 BLE servo-control stack (firmware + Python client)
A full firmware-to-client control link: a custom BLE GATT service on an ESP32 driving two servos, and an asyncio Python client that reconnects on its own.
- Embedded
- Hardware
- Python
01 Problem
I wanted a reliable wireless control link for two servos that a program, not a human, could drive, after Bluetooth Classic proved unstable on Windows.
02 Approach
Wrote Arduino C++ firmware for an ESP32 driving two SG90 servos through a PCA9685 PWM driver, with a TTP223 capacitive touch input.
I replaced the generic Nordic UART profile with a custom BLE GATT service exposing three characteristics — command, response, touch — so command replies never mix with asynchronous gesture events, and classified gestures (tap, double, long-press with duration) in a non-blocking state machine.
Because serial debug output was not available, I built an LED blink-code diagnostic system, which is how I traced idle disconnects to BLE reason code 0x13.
On the host side I wrote a Python bleak client using asyncio, subscribing to the response and touch streams separately, with a link-supervisor task that reconnects using exponential backoff and jitter and re-arms subscriptions each time. A WiFi access-point mode serves as a fallback control path.
03 Stack
- ESP32
- PCA9685
- SG90 servos
- TTP223
- Arduino C++
- BLE GATT
- Python
- bleak
- asyncio
04 Result
A full firmware-to-client control stack, verified against real hardware at each layer, with a documented BLE contract handed off for reuse.
05 What broke, and what fixed it
-
BLE reason code
0x13Idle disconnects traced with an LED blink-code diagnostic system, built because serial debug was unavailable.
-
Custom GATT service
Three characteristics so command replies never interleave with asynchronous gesture events.
-
Self-healing link
Supervisor task reconnects with exponential backoff and jitter, re-arming subscriptions each time.