Firmware and prototype hardware for a custom ESP32-C3 handheld controller developed alongside the Miniature Quadcopter Flight Controller. The controller combines dual joysticks, four buttons, an EC11 rotary encoder, and a 1.3-inch OLED with bidirectional ESP-NOW communication for flight commands, link monitoring, safety controls, and runtime PID configuration.
Project status: The controller hardware and firmware have been validated with the Rev1 flight controller through propeller-off bench testing. Joystick control, ARM/DISARM/KILL handling, bidirectional telemetry, link recovery, the OLED interface, and runtime PID synchronization and updates are working. End-to-end free-flight validation remains pending the Rev2 flight-controller hardware described in the companion repository.
Hand-wired ESP32-C3 controller prototype showing the dual joysticks, OLED, buttons, and EC11 encoder.
- 50 Hz control-packet transmission over ESP-NOW
- Dual filtered and calibrated joysticks for throttle, yaw, roll, and pitch commands
- Rate-limited throttle accumulation with immediate reset on disarm, failsafe, KILL, or PID configuration
- Dedicated ARM/DISARM and level-triggered KILL controls
- Bidirectional link status derived from send callbacks and flight-controller telemetry
- Thread-safe mailbox handoff from ESP-NOW callbacks to the main loop
- Automatic radio recovery for missing callbacks, repeated send failures, or channel changes
- 1.3-inch OLED status display and EC11-driven menu
- Runtime synchronization, editing, and update of nine rate-PID gains
- Sequence-matched PID acknowledgements, retry timing, timeout reporting, and rejection messages
- Safety interlocks that prevent PID configuration while the aircraft is armed
flowchart TD
Input["Joysticks, buttons, and EC11"] --> Logic["Safety, UI, and PID coordinators"]
Logic --> Tx["Control and configuration transmitter"]
Tx --> Radio["ESP-NOW radio"]
Radio <-->|"Control, telemetry, and ACK packets"| FC["Miniature flight controller"]
Radio --> Mailbox["Thread-safe packet mailbox"]
Mailbox --> Logic
Logic --> OLED["OLED status and menus"]
The ESP-NOW receive callback only decodes supported packets and stores the newest telemetry or PID acknowledgement in ControllerPacketMailbox. The main loop consumes those snapshots and performs safety, PID, and display updates outside callback context. Outgoing control and configuration traffic is serialized so that only one ESP-NOW packet is in flight at a time.
| Input | Function |
|---|---|
| Left joystick Y | Rate-limited throttle increase/decrease |
| Left joystick X | Yaw-rate command, up to ±120 degrees/s |
| Right joystick X | Roll-angle command, up to ±15 degrees |
| Right joystick Y | Pitch-angle command, up to ±15 degrees |
| Button 1 | Request ARM when safe; request DISARM when already armed/requested |
| Button 2 | KILL while held; the KILL flag is repeated in every control packet |
| Buttons 3 and 4 | Auxiliary button bits included in the control packet |
| EC11 rotation/press | Navigate menus, select PID terms, and edit values |
Joystick centres are calibrated during startup. Deadzone, filtering, sample averaging, axis inversion, and expo are applied before commands are placed in the control packet.
| Condition | Controller response |
|---|---|
| Startup, stale control link, or non-zero throttle | Block a new ARM request |
| Valid control link and zero throttle | Allow Button 1 to request ARM |
| Button 1 while armed or already requesting ARM | Clear the ARM request and reset throttle |
| Button 2 held | Clear ARM, reset throttle, and assert KILL in every control packet |
| Flight controller reports disarm or failsafe | Immediately reset accumulated throttle |
| PID configuration requested while armed, disconnected, or unsafe | Block entry into PID configuration |
| PID configuration active | Force local disarm, hold throttle at zero, and suppress motion commands |
| Missing send callback, repeated send failures, or unexpected Wi-Fi channel | Restart ESP-NOW with a cooldown to prevent restart loops |
The flight controller independently validates arming, configuration mode, link timeouts, and KILL. Controller-side checks provide an additional user-interface safety layer rather than replacing flight-controller enforcement.
The controller and flight controller share packed, versioned packet definitions:
ControlPacket— sequence number, timestamp, ARM/KILL/configuration flags, throttle, attitude/rate targets, and button bitsTelemetryPacket— telemetry sequence, acknowledged control sequence, flight state, failsafe state, attitude, and link-loss countConfigPacket— PID GET/SET commands, fixed-point gains, status, sequence number, and acknowledgement
Packet sizes are protected with compile-time static_assert checks. Angles use centidegrees and PID gains use integer micro-units on air. Telemetry and configuration acknowledgements both count as inbound-link activity.
The current prototype uses ESP-NOW channel 1, 13 dBm transmit power, and a fixed peer MAC address configured in src/main.cpp. Peer encryption is not enabled, so this link is intended for controlled prototype and bench use rather than as a secure production RC system.
PID configuration is available only when both controller and flight controller are disarmed, throttle is zero, the control link is recent, and KILL is not active.
- Enter PID Settings with the EC11 encoder.
- The controller sends a
GET_RATE_PIDrequest and waits for a matching acknowledgement. - Rotate and press the encoder to select and edit roll, pitch, or yaw
Kp,Ki, andKd. - Select Send & Exit to transmit
SET_RATE_PID. - The request is retried every 100 ms until a matching ACK is received or the two-second timeout expires.
- Applied or rejected status is shown on the OLED; stale acknowledgements with the wrong sequence are ignored.
Runtime rate-PID values synchronized from the flight controller and displayed for editing.
| Module | Responsibility |
|---|---|
src/main.cpp |
Pin configuration, hardware setup, and high-level main-loop coordination |
input/ |
Joystick sampling, EC11 decoding, button debouncing, and throttle accumulation |
control/ |
Safety coordination, control-packet construction, and packet transmission |
radio/DroneProtocol |
Versioned packet definitions, conversion, encoding, and validation |
radio/EspNowRadio |
ESP-NOW initialization, peer management, send, and callback routing |
radio/ControllerPacketMailbox |
Thread-safe telemetry and PID-ACK handoff |
radio/ControllerRadioState |
Link timing, send state, callback results, and failure counters |
radio/ControllerRadioCoordinator |
Radio startup, channel checks, callbacks, and automatic recovery |
config/ |
PID editor, transfer timing, session state, ACK handling, and OLED results |
ui/ |
Menu state and coordination between the encoder, safety checks, PID flow, and OLED |
display/OLEDDisplay |
Status, menu, PID editor, and result rendering with U8g2 |
| Item | Prototype configuration |
|---|---|
| MCU | ESP32-C3 development board |
| Primary inputs | Two analog joystick modules |
| Safety controls | Dedicated ARM/DISARM and KILL buttons |
| Auxiliary inputs | Two additional tactile buttons |
| Menu input | EC11 rotary encoder with push switch |
| Display | 1.3-inch 128×64 SH1106 OLED over I²C |
| Radio | ESP-NOW, channel 1, 13 dBm |
| Control update | 20 ms interval (50 Hz) |
| Construction | Hand-wired perfboard prototype |
| Internal wiring and power hardware | Live bidirectional-link status |
|---|---|
![]() |
![]() |
| Function | ESP32-C3 GPIO |
|---|---|
| Left joystick X / Y | 0 / 1 |
| Right joystick X / Y | 3 / 4 |
| OLED SDA / SCL | 6 / 5 |
| EC11 CLK / DT / switch | 7 / 10 / 2 |
| Button 1 / 2 / 3 / 4 | 12 / 13 / 18 / 19 |
Pins, peer MAC address, transmit power, channel, command limits, and safety timeouts are hardware-specific values in src/main.cpp and must be reviewed before adapting the firmware to another controller.
The project uses PlatformIO with the Arduino framework for ESP32-C3 and the U8g2 display library.
pio run
pio run --target upload
pio device monitor --baud 115200The default upload speed is 921600 baud. No machine-specific serial port is committed; PlatformIO can auto-detect the connected board or a local port can be supplied when required.
| Test | Status |
|---|---|
| Clean PlatformIO release build | Complete |
| Joystick startup calibration, filtering, deadzone, and axis response | Complete |
| EC11 navigation and button debouncing | Complete |
| OLED startup, status, menu, PID, success, and failure pages | Complete |
| 50 Hz control packet transmission | Complete — propeller-off bench test |
| Telemetry and configuration-ACK reception | Complete |
| ARM, DISARM, and level-triggered KILL behaviour | Complete — propeller-off bench test |
| Throttle reset after disarm, failsafe, KILL, and PID entry | Complete |
| PID entry blocked while armed | Complete |
| PID synchronization, editing, update, rejection, retry, and timeout paths | Complete |
| Link indication and ESP-NOW automatic recovery | Complete — bench fault-injection test |
| End-to-end operation with the Rev1 flight controller | Complete — propeller-off bench test |
| Sustained free-flight operation | Pending Rev2 flight-controller hardware |
- The controller is a hand-wired engineering prototype rather than an enclosed production handset.
- The ESP-NOW link uses a fixed peer MAC and is not encrypted or authenticated.
- Pairing, calibration, rate, and expo menu pages are not exposed; the current interface contains only implemented and validated status and PID functions.
- Final airborne validation depends on the Rev2 flight-controller power and PCB redesign.
These limitations are documented explicitly because this repository represents a tested engineering prototype, not a production-ready RC system.
This controller operates experimental flight hardware. Initial radio, control-direction, ARM/DISARM/KILL, PID, and failsafe testing must be performed with propellers removed. Verify joystick direction, button mapping, peer address, channel, control limits, and flight-controller safety behaviour before any restrained or free-flight test.



