DroidFX documentation
DroidFX is firmware for a Freenove ESP32-S3 board that turns it into an SBUS-driven prop controller. Flip a switch on your RC transmitter and lights, sound effects and servo sequences fire together. You configure all of it from a web UI the board hosts itself.
This is the reference guide for DroidFX. It isn't part of the firmware and isn't stored on the board — the board serves its own configuration UI, and this site explains what that UI does and how to use it. Keep it open on a phone or second screen while you set your prop up.
What DroidFX is
You wire an ESP32-S3 between your SBUS receiver and your prop's lighting, audio and servos. DroidFX decodes all 16 radio channels and maps three of them — which ones is your choice — onto a master LED switch, a button selector, and volume.
The button selector is a multi-position switch on your transmitter. Each of its positions picks one of 15 buttons, and a button is a bundle of effects you define: a sound, up to 4 LED channels, up to 4 servo sequences. Flip to a position and that bundle fires.
Everything else happens in a browser. Settings persist to flash and survive reboots and firmware updates.
16-channel SBUS
Standard SBUS at 100000 baud with failsafe detection. Lose the transmitter link and everything stops.
10 LED channels
Any mix of plain LEDs/MOSFETs and addressable NeoPixel chains, each with its own presets.
24 effects
10 blink patterns for plain LEDs, 14 animated effects for NeoPixels.
Sound
A DY-SV5W audio board over UART — per-button track ranges, startup track, EQ and radio-trimmed volume.
16 servo channels
A PCA9685 driver with 16 recordable sequences, posed and captured frame by frame.
Nothing hardcoded
Every GPIO role is assigned from the Pin Assignment page and stored in flash.
Hardware & limits
| Part | Details |
|---|---|
| Board | Freenove ESP32-S3 DEV (FNK0099A) — ESP32-S3-WROOM, 8 MB flash, WiFi + BLE |
| RC receiver | Any SBUS receiver. 100000 baud, 8E2, 25-byte frames. One signal wire to an assigned GPIO. |
| Audio | DY-SV5W, or a compatible DFPlayer-style module, over UART (TX + RX) |
| Servo driver | PCA9685 over I2C at address 0x40 — one bus, 16 channels |
| LEDs | Up to 10 channels: plain LED/MOSFET, or WS2812-class NeoPixel |
| WiFi switch | A switch from GPIO1 to GND |
Limits to plan around
These are hardware ceilings, not arbitrary choices. The UI shows a live budget on the LED Configuration page and won't let you exceed them.
| Limit | Value | Why |
|---|---|---|
| LED channels | 10 | Channel slots |
| PWM-dimmable at once | 8 | The ESP32-S3's LEDC peripheral has 8 channels, not 16 like the classic ESP32. Channels past that must be plain on/off. |
| NeoPixel chains | 4 | The RMT peripheral has 4 usable transmit channels |
| Pixels per chain | 150 | Frame budget — see below |
| Presets per channel | 8 | |
| Servo channels | 16 | PCA9685 |
| Servo sequences | 16 × 32 frames | |
| Buttons | 15 | Button 15 becomes STOP ALL when Emergency Stop is enabled |
| Actions per button | 4 LED + 4 servo |
A WS2812-class pixel is 24 bits at 800 kHz — 30 µs on the wire — and transmission blocks until it completes. Four chains are driven in sequence, so four full 150-pixel chains already consume most of a 20 ms frame budget. Go much higher and your frame rate drops visibly. No amount of faster code fixes it; it's the wire protocol.
GPIO notes
GPIO46is input-only and can't drive anything.GPIO1is reserved for the WiFi enable switch and rejected for every other role.GPIO0is the BOOT button and doubles as the WiFi override.- Assignable but flagged as risky in the UI:
GPIO0,GPIO3,GPIO45(strapping pins),GPIO43/GPIO44(UART0, shared with the USB console),GPIO19/GPIO20(native USB).
Quick start
From a blank board to a prop that responds to your transmitter.
-
Flash the board
DroidFX builds with PlatformIO. Connect over USB and run both uploads — the firmware, and the filesystem image that carries the web UI:
pio run -t upload # firmware pio run -t uploadfs # web UI + filesystem -
Turn WiFi on
WiFi is off by default. Either close your
GPIO1–GNDswitch, or power the board up and then hold the BOOT button for 3 seconds. See WiFi for the details. -
Connect
The board starts its own access point: SSID
DROIDFX_xxxx, password12345678. Join it and browse to the IP shown in the serial log at 115200 baud. -
Assign your pins
Open Pin Assignment first. Nothing works until the board knows what's wired where. Save, then reboot — pin changes need one.
-
Find your radio channels
Open R/C Monitor, move each switch on your transmitter, and watch which of the 16 bars responds. Note the numbers, then set them on Droid Config.
-
Build a preset
On LED Configuration, pick a channel, create a preset, choose its effect and colour, and save. Presets save live, the moment you save each one.
-
Wire it to a button
On Button Pad, name a button, give it a sound track range, and add the LED channels it should light. Save.
-
Test it, then back it up
Flip your master switch on, select that button position, and watch it fire. Then go to Firmware and download a config backup — it's what saves you if a filesystem upload goes wrong later.
The web UI
Nine pages, served by the board itself.
| Page | What you do there |
|---|---|
| Home | Status at a glance and the running firmware version |
| Droid Config | Radio channel map, sound settings, and a Safety card — LEDs Enabled and Emergency Stop |
| R/C Monitor | Live telemetry for all 16 channels, with connection state |
| Button Pad | Name the 15 buttons and attach sound, LED actions and servo actions to each |
| Pin Assignment | Click-to-assign GPIO roles, with reserved and risky pins flagged |
| LED Configuration | Channel hardware setup, plus the preset editor |
| Servo Config | Channel setup and the sequence editor |
| System | WiFi — access point or station mode, credentials |
| Firmware | System info, backup/restore, OTA updates, reboot, factory reset, licenses |
The NeoPixel Designer isn't a tab. It opens from a NeoPixel channel's card on LED Configuration, and has a back link to return.
Anything describing hardware — pin assignment, LED count, pixel count, the RGBW setting, NeoPixel layouts — needs a reboot. Anything describing behaviour — presets, button bundles, sound, channel names, Always On, pairing — applies on Save.
Pin assignment
This is the first page to visit on a new board and the one everything else depends on. No pin has a fixed role in DroidFX — you tell it what's wired where.
The page draws the board's two pin headers in physical order. Click a pin and choose its role:
- SBUS RX — the signal wire from your receiver
- Audio TX / Audio RX — the UART pair to the sound module
- Servo SDA / SCL — the I2C pair to the PCA9685
- LED 1–10 — one per LED channel, plain or NeoPixel
Reserved pins are rejected outright. Risky-but-usable pins are flagged with a warning so you can make an informed choice — assigning GPIO43/GPIO44, for instance, costs you the USB serial console.
Pin assignments take effect at boot. Save the page, then reboot from the Firmware page before testing.
Radio channels
DroidFX decodes all 16 SBUS channels, and you choose which three it acts on. Set them on Droid Config.
| Role | What it does |
|---|---|
| LED master switch | Below 1500 µs every LED channel is off, unconditionally. Above 1500 µs the per-channel engine runs. This always wins. |
| Button select | A multi-position switch. Its 15 discrete positions map to the 15 buttons. |
| Volume | Optional. Ties audio volume to a radio channel so you can trim it from the transmitter without opening the UI. |
Finding your channels
Open R/C Monitor. It streams all 16 channels live over a WebSocket, so you can move one switch at a time and watch which bar reacts. That's far faster than counting through your transmitter's mixer screens, and it confirms the receiver is actually talking to the board.
Most SBUS receivers keep transmitting on schedule after losing the transmitter link — they just set a failsafe flag rather than going quiet. DroidFX reads that flag, so R/C Monitor shows a genuine disconnect rather than a frozen "connected". On failsafe, every LED channel and sequence stops.
WiFi
DroidFX keeps WiFi off by default. A prop running at an event shouldn't broadcast an access point unless you ask it to, and the controller does its whole job without WiFi — you only need it to reconfigure.
The enable switch
Wire a switch from GPIO1 to GND. Open (the default) means WiFi off; closed means WiFi starts. The pin is read once, at boot — flipping it mid-session won't tear down a connection you're configuring over.
The BOOT override
If the switch isn't wired yet, or its wire breaks, WiFi is the only configuration interface you have — so there's a second way in. Power the board on, and then press and hold BOOT for 3 seconds. The serial log confirms WiFi is starting.
Don't hold BOOT during reset. GPIO0 is the ESP32-S3's boot-mode strapping pin — held low at reset, the chip enters serial download mode and the firmware never runs at all. Power up first, then press.
Access point vs station
| Mode | Behaviour |
|---|---|
| Access point | The default. The board makes its own network: SSID DROIDFX_xxxx (last four hex digits of its MAC), password 12345678. Works anywhere, no infrastructure. |
| Station | The board joins an existing network. Set credentials on the System page. Convenient at a workbench where you want internet at the same time. |
LED channels
A channel is one physical thing you wired to one pin — a MOSFET driving a strip of plain LEDs, or a NeoPixel chain. On LED Configuration each channel gets a card describing that hardware.
| Setting | Meaning | Takes effect |
|---|---|---|
| Name | Yours, for your own sanity — "Dome Holo", not "LED 4" | on save |
| Type | Plain LED/MOSFET, or NeoPixel | reboot |
| PWM dimmable | Whether this channel gets one of the 8 hardware dimming slots, or is plain on/off | on save |
| Pixel count / RGBW | NeoPixel channels only | reboot |
| Always On | Runs continuously, independent of any button | on save |
| Pair with | Links two plain channels for two-tone patterns | on save |
| Active preset | Which of the channel's 8 presets plays | on save |
What turns a channel on
- Always On — the channel runs whenever the master switch is up. A channel marked Always On is automatically hidden from the Button Pad's picker, since it isn't button-controlled.
- A button's LED Actions — the button names up to 4 channels to activate. It decides whether a channel lights, never how it looks; that comes from the channel's active preset.
Above both sits the master radio switch. Below 1500 µs, everything is off — no exceptions, no mode that bypasses it.
Pairing
Two plain LED channels can be paired so a two-tone pattern actually alternates between them. Take Police Alternate: without pairing, both channels show the identical value and you get a synchronised flash. Paired, one channel plays the pattern's normal half and its partner plays the opposite half — a real alternating blue/white.
The partner is driven entirely by its primary. Its own type, Always On, effect and intensity are ignored while paired.
Presets & effects
A preset is a saved look for one channel: an effect, its colours, and its parameters. Each channel holds up to 8, and the channel card picks which one is active.
The point is to set up several looks once and switch between them later without rebuilding anything — a dome channel might carry "Idle Breathe Blue", "Alarm Fast Strobe Red" and "Shutdown Fade", with one of them live at a time.
Each preset saves the moment you save it, independently of the page's main Save LED Configuration button. You don't need to save the whole page to keep a preset you just built.
Blink patterns — plain LED channels
Ten patterns, all brightness-driven, for channels wired to plain LEDs or MOSFETs.
| Pattern | What it looks like |
|---|---|
| Single Blink | Classic steady on/off blink |
| Double Blink | Two quick flashes, then a pause |
| Triple Blink | Three quick flashes, then a pause |
| Fade In/Out | A smooth continuous breathing ramp |
| Fast Strobe | Rapid strobe |
| Slow Strobe | A brief flash, then a long dark pause — beacon-style |
| Police Alternate | Fast alternating emergency-vehicle look. Pair two channels to get the real effect. |
| Wig-Wag | One side flashes twice, then the other flashes twice |
| SOS | Morse SOS — three short, three long, three short |
| Heartbeat | A quick double pulse, then a pause |
NeoPixel effects
Fourteen animated effects for addressable chains. Each one only shows the controls it actually uses — a Speed slider on Solid Color would just be a lie, so it isn't there.
| Effect | Controls | What it does |
|---|---|---|
| Solid Color | colour | One steady colour |
| Color Cycle | speed | The whole chain drifts through the hue wheel together |
| Rainbow | speed, size, direction, axis | A hue gradient travelling across the chain |
| Chase | 2 colours, speed, size, direction, axis | A comet running along the chain with a trailing tail |
| Breathe | colour, speed | A smooth swell in and out |
| Strobe | colour, speed | Hard on/off flash |
| Gradient Drift | 2 colours, speed, direction, axis | A two-colour blend sliding across the shape |
| Scanner | 2 colours, speed, size, axis | A bar sweeping back and forth, Cylon-style |
| Theater Chase | 2 colours, speed, direction | Marquee dots stepping along the chain |
| Radar Sweep | 2 colours, speed, size, direction | A rotating arm with a fading wake — built for rings |
| Ripple | 2 colours, speed, size, direction | Rings expanding outward from the centre |
| Twinkle | 2 colours, speed, size | Random pixels sparkling and fading |
| Fire | 2 colours, speed | Flickering flame simulation |
| Logic Display | 3 colours, speed | Random blocks cycling — the classic droid logic-display look |
Axis
Effects that travel need to know which direction counts as "forward" on your shape. The Axis control picks that:
| Along path | Follows the data order, pixel 0 to last — the natural choice for a strip |
| Up / down | Vertical across the shape — for matrices and stacked rows |
| Around center | Rotational — for rings and jewels |
| Outward | Centre outward — for concentric rings and ripples |
Axis only means something once the channel has a layout describing its physical shape — see below.
NeoPixel layouts
A NeoPixel channel isn't "N pixels in a line". It's whatever you physically wired to that data pin, in the order data flows through it — maybe a ring, then a jumper, then a short strip, then a jewel. The NeoPixel Designer is where you describe that shape once, so effects can be aware of it.
Open it from a NeoPixel channel's card on LED Configuration.
Segments
A layout is up to 8 segments per channel, in data order:
| strip | A straight run of pixels |
| ring | A single circular ring |
| jewel | A small cluster with a centre pixel |
| rings | Concentric rings on one board |
| matrix | A regular grid panel |
| rows | Ragged row boards — rows of differing lengths |
| single | One lone pixel |
If you don't draw one
A channel with no layout behaves as a plain strip of its pixel count. Nothing you set up before layouts existed changes, and nothing is migrated — a channel only gets a layout when you design one.
Identify
Wiring order is the thing people get wrong, and it's invisible until it animates badly. Identify walks the chain on real hardware so you can check your drawing against what's in front of you: pixel 0 goes red, the last pixel goes blue, and a white dot steps between them in data order, dimming the pixels it has already visited.
It deliberately outranks presets and the master switch — a diagnostic you can't see because a switch is down would be useless. It stops on its own after 60 seconds, when you save a layout, or on failsafe.
Each strip's buffer is allocated once at boot, so layout and pixel-count changes need a reboot before the strip is re-created at the new length.
Sound
Sound runs on a DY-SV5W or DFPlayer-compatible module over UART. Tracks live on the module's own storage, numbered; DroidFX tells it which number to play.
Global settings — Droid Config
| Volume | A slider, optionally tied to a radio channel so you can trim it from the transmitter |
| Equalizer | Normal, Pop, Rock, Jazz, Classic |
| Startup track | Plays once at boot. Set it to 0 to disable startup playback. |
| Startup delay | How long after boot to play it — useful when the audio board powers up more slowly than the ESP32 |
Per button — Button Pad
Each button names a track range with Min and Max, which gives you three behaviours:
- Fixed — set Min and Max to the same number
- Random — a range with Random ticked, so a repeated press varies
- Sequential — a range with Random unticked, walked in order press after press
The audio board is powered through the RC receiver, so it has no power until the receiver is on. If the Button Pad shows no tracks detected, that's the usual cause rather than a wiring fault — and saving still works regardless.
Servo sequences
One PCA9685 over I2C fans out to 16 servo channels. On Servo Config you set each channel up and record sequences — up to 16 sequences of 32 frames each.
Posing and capturing
You don't type angles. The editor gives you a Pose panel of sliders, one per channel, driving the real servos live — move them until the prop looks right, then capture that pose as a frame. Repeat for each position in the move, give each frame a duration, and you have a sequence.
A frame records only the channels you actually moved. Everything else is inherited — a channel a frame doesn't mention isn't "off", it's "whatever the most recent frame before it left it at". So a sequence that spins a dome while an arm holds still only needs the dome channel in its frames, and editing the arm later doesn't mean rebuilding the spin.
Testing and using
Sequences test-play from the editor, so you can iterate without touching your transmitter. Once saved, a sequence appears in the Button Pad's Servo Actions picker and can be attached to any button alongside its lights and sound — up to 4 per button.
Backup & updates
Where your settings live
Settings are stored in the ESP32's NVS flash. After every successful save, the complete settings tree is also mirrored to a config.json file on the board's filesystem.
That mirror exists for one reason: if a firmware update changes a setting's internal layout, NVS can no longer read that section — and without the mirror you'd silently lose it. Instead, the board notices the failed read at boot and restores that section from the mirror, once, in the new layout. LED presets, servo sequences and NeoPixel layouts are separate files on the same filesystem.
Updating over WiFi
The Firmware page handles all of it, no USB cable needed:
| Option | What it does |
|---|---|
| Firmware OTA | Upload a new firmware.bin |
| Web UI update | Writes only index.html, app.js and style.css. Settings, presets and sequences are untouched, and no reboot is needed. The safe option for UI changes. |
| Filesystem image | The advanced path. Leave Preserve presets & sequences ticked unless you genuinely want a factory image. |
| Backup | Downloads every setting, LED preset and servo sequence as one JSON file |
| Restore | Replaces all of them. Every section is validated before anything is committed. |
| Factory reset | Clears every saved setting, preset and sequence. The web UI files stay, so the board still serves the page afterwards. |
pio run -t uploadfs over USB erases the entire data partition — every LED preset, servo sequence and NeoPixel layout. Download a backup first, or use the Firmware page's Web UI Update instead, which only touches the three UI files.
Troubleshooting
I can't reach the web UI
WiFi is off by default. Check your GPIO1–GND switch is closed, or power the board up and then hold BOOT for 3 seconds.
If the serial log at 115200 baud says WiFi is starting but you still can't connect, you may be in station mode with wrong credentials. The BOOT override still gets WiFi up; if the saved network is unreachable, a factory reset returns the board to access-point mode.
R/C Monitor shows no channels
Check the SBUS pin is assigned on Pin Assignment and that you rebooted after saving it — pin changes only take effect at boot. Then confirm the receiver is powered and bound, and that it's outputting SBUS rather than PPM or PWM.
R/C Monitor looks fine, but nothing lights up
Three things gate every LED, in order. Work down the list:
1. LEDs Enabled on Droid Config's Safety card. 2. The master switch channel — it must be above 1500 µs. Watch its value on R/C Monitor while you flip it. 3. The channel needs a reason to be on: either Always On, or a button that names it in its LED Actions.
My NeoPixel effect animates in the wrong direction or wrong place
The layout doesn't match the wiring. Run Identify on that channel — pixel 0 red, last pixel blue, a white dot stepping between — and compare what the hardware does against what you drew. Segment order in the designer must follow the order data actually flows.
If the shape is right but the motion isn't, check the effect's Axis control.
I changed the pixel count or layout and nothing happened
Both need a reboot. Strip buffers are allocated once at boot and can't be re-sized live. Same for pin assignment, LED count and the RGBW setting.
The Button Pad says no tracks are detected
On this build the audio board is powered through the RC receiver, so it reports nothing until the receiver is on. Power the receiver and reload. Saving the page works either way — it only validates values you actually changed.
A setting reset itself after a firmware update
That section's internal layout changed, so the old stored bytes couldn't be read back safely. The board recovers it automatically from the config.json mirror on the next boot — reboot once and check again.
If it's still wrong, restore your backup from the Firmware page.
Two paired channels both flash together instead of alternating
Pairing is set on the primary channel's card via Pair with, and only applies to plain LED channels — not NeoPixel. Confirm the pattern is a genuinely two-tone one, such as Police Alternate or Wig-Wag; a single-tone pattern has no opposite half to give the partner.
Building from source
DroidFX uses PlatformIO. Dependencies are fetched automatically: AsyncTCP-esphome, ESPAsyncWebServer-esphome, ArduinoJson, Adafruit NeoPixel and Adafruit PWMServoDriver.
pio run # build firmware
pio run -t buildfs # build the filesystem image
pio run -t upload # flash firmware over USB
pio run -t uploadfs # flash filesystem over USB (erases presets!)
If you're changing anything in data/, run the pre-upload checks first:
node tools/check-ui.mjs
The version string is derived from git describe at build time and shown on the Home page, so there's no version constant to keep in sync.
How it's put together
The firmware is a single src/main.cpp. The web UI is three files in data/, served from LittleFS. Time-critical SBUS decoding and LED output run in their own FreeRTOS task pinned to core 1, while the event-driven async web server handles requests — so a browser session can't stall your lighting, and lighting can't stall the browser.
License
DroidFX is free software under the GNU General Public License v3.0. It's offered in the hope that it's useful, with no warranty of any kind — it drives servos, lighting and audio on physical hardware, and operating it safely is your responsibility.
GPL-3.0 rather than something more permissive because three of the libraries it links are LGPL-3.0 — ESPAsyncWebServer-esphome, AsyncTCP-esphome and Adafruit_NeoPixel — and they link statically into the firmware image. Anyone handed a built firmware.bin therefore has to be able to rebuild it from source.
The remaining dependencies are MIT (ArduinoJson, Adafruit_BusIO), BSD 3-Clause (Adafruit_PWMServoDriver) and LGPL-2.1 (Arduino-ESP32). The board's own UI lists the same information under Firmware → Licenses & Credits.
