DroidFX Documentation

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.

About this site

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

PartDetails
BoardFreenove ESP32-S3 DEV (FNK0099A) — ESP32-S3-WROOM, 8 MB flash, WiFi + BLE
RC receiverAny SBUS receiver. 100000 baud, 8E2, 25-byte frames. One signal wire to an assigned GPIO.
AudioDY-SV5W, or a compatible DFPlayer-style module, over UART (TX + RX)
Servo driverPCA9685 over I2C at address 0x40 — one bus, 16 channels
LEDsUp to 10 channels: plain LED/MOSFET, or WS2812-class NeoPixel
WiFi switchA 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.

LimitValueWhy
LED channels10Channel slots
PWM-dimmable at once8The ESP32-S3's LEDC peripheral has 8 channels, not 16 like the classic ESP32. Channels past that must be plain on/off.
NeoPixel chains4The RMT peripheral has 4 usable transmit channels
Pixels per chain150Frame budget — see below
Presets per channel8
Servo channels16PCA9685
Servo sequences16 × 32 frames
Buttons15Button 15 becomes STOP ALL when Emergency Stop is enabled
Actions per button4 LED + 4 servo
Why 150 pixels

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

  • GPIO46 is input-only and can't drive anything.
  • GPIO1 is reserved for the WiFi enable switch and rejected for every other role.
  • GPIO0 is 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.

  1. 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
  2. Turn WiFi on

    WiFi is off by default. Either close your GPIO1–GND switch, or power the board up and then hold the BOOT button for 3 seconds. See WiFi for the details.

  3. Connect

    The board starts its own access point: SSID DROIDFX_xxxx, password 12345678. Join it and browse to the IP shown in the serial log at 115200 baud.

  4. Assign your pins

    Open Pin Assignment first. Nothing works until the board knows what's wired where. Save, then reboot — pin changes need one.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

PageWhat you do there
HomeStatus at a glance and the running firmware version
Droid ConfigRadio channel map, sound settings, and a Safety card — LEDs Enabled and Emergency Stop
R/C MonitorLive telemetry for all 16 channels, with connection state
Button PadName the 15 buttons and attach sound, LED actions and servo actions to each
Pin AssignmentClick-to-assign GPIO roles, with reserved and risky pins flagged
LED ConfigurationChannel hardware setup, plus the preset editor
Servo ConfigChannel setup and the sequence editor
SystemWiFi — access point or station mode, credentials
FirmwareSystem 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.

Rule of thumb

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.

Reboot required

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.

RoleWhat it does
LED master switchBelow 1500 µs every LED channel is off, unconditionally. Above 1500 µs the per-channel engine runs. This always wins.
Button selectA multi-position switch. Its 15 discrete positions map to the 15 buttons.
VolumeOptional. 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.

Failsafe

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.

Order matters

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

ModeBehaviour
Access pointThe default. The board makes its own network: SSID DROIDFX_xxxx (last four hex digits of its MAC), password 12345678. Works anywhere, no infrastructure.
StationThe 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.

SettingMeaningTakes effect
NameYours, for your own sanity — "Dome Holo", not "LED 4"on save
TypePlain LED/MOSFET, or NeoPixelreboot
PWM dimmableWhether this channel gets one of the 8 hardware dimming slots, or is plain on/offon save
Pixel count / RGBWNeoPixel channels onlyreboot
Always OnRuns continuously, independent of any buttonon save
Pair withLinks two plain channels for two-tone patternson save
Active presetWhich of the channel's 8 presets playson 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.

Presets save live

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.

PatternWhat it looks like
Single BlinkClassic steady on/off blink
Double BlinkTwo quick flashes, then a pause
Triple BlinkThree quick flashes, then a pause
Fade In/OutA smooth continuous breathing ramp
Fast StrobeRapid strobe
Slow StrobeA brief flash, then a long dark pause — beacon-style
Police AlternateFast alternating emergency-vehicle look. Pair two channels to get the real effect.
Wig-WagOne side flashes twice, then the other flashes twice
SOSMorse SOS — three short, three long, three short
HeartbeatA 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.

EffectControlsWhat it does
Solid ColorcolourOne steady colour
Color CyclespeedThe whole chain drifts through the hue wheel together
Rainbowspeed, size, direction, axisA hue gradient travelling across the chain
Chase2 colours, speed, size, direction, axisA comet running along the chain with a trailing tail
Breathecolour, speedA smooth swell in and out
Strobecolour, speedHard on/off flash
Gradient Drift2 colours, speed, direction, axisA two-colour blend sliding across the shape
Scanner2 colours, speed, size, axisA bar sweeping back and forth, Cylon-style
Theater Chase2 colours, speed, directionMarquee dots stepping along the chain
Radar Sweep2 colours, speed, size, directionA rotating arm with a fading wake — built for rings
Ripple2 colours, speed, size, directionRings expanding outward from the centre
Twinkle2 colours, speed, sizeRandom pixels sparkling and fading
Fire2 colours, speedFlickering flame simulation
Logic Display3 colours, speedRandom 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 pathFollows the data order, pixel 0 to last — the natural choice for a strip
Up / downVertical across the shape — for matrices and stacked rows
Around centerRotational — for rings and jewels
OutwardCentre 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:

stripA straight run of pixels
ringA single circular ring
jewelA small cluster with a centre pixel
ringsConcentric rings on one board
matrixA regular grid panel
rowsRagged row boards — rows of differing lengths
singleOne 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.

Reboot required

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.

Buttons

A button is a bundle of effects bound to one position of your transmitter's selector switch. There are 15, configured on the Button Pad page — one row each.

ColumnWhat it sets
NameYours — "Scream", "Startup", "Panels Open"
Min / MaxThe track range on the audio card. Set both the same for a fixed sound.
DelayHow long to wait before the sound plays
RandomOn, a random track from the range. Off, the range is walked in order, press after press.
RepeatWhether the sound loops
LED ActionsUp to 4 LED channels this button turns on
Servo ActionsUp to 4 servo sequences this button plays

Buttons layer, they don't replace

Pressing a button is an on/off toggle, not "select this one instead". Press one button, then another a few seconds later, and the second one's channels layer on top of what's already lit. Press the same button again to turn just its own channels back off — everything else stays.

STOP ALL, and a lost RC signal, clears everything at once.

A button's sound plays on the toggle-on press only; toggling its LEDs back off stays silent. A button with no LED actions configured just plays its sound on every press, every time.

STOP ALL

Enable Emergency Stop on Droid Config and button 15 becomes STOP ALL — it kills every active LED channel and servo sequence at once. On a prop with moving parts, it's worth the slot.

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

VolumeA slider, optionally tied to a radio channel so you can trim it from the transmitter
EqualizerNormal, Pop, Rock, Jazz, Classic
Startup trackPlays once at boot. Set it to 0 to disable startup playback.
Startup delayHow 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
Known quirk on this build

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.

Frames only store what you touched

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:

OptionWhat it does
Firmware OTAUpload a new firmware.bin
Web UI updateWrites 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 imageThe advanced path. Leave Preserve presets & sequences ticked unless you genuinely want a factory image.
BackupDownloads every setting, LED preset and servo sequence as one JSON file
RestoreReplaces all of them. Every section is validated before anything is committed.
Factory resetClears every saved setting, preset and sequence. The web UI files stay, so the board still serves the page afterwards.
Back up before you flash

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.