An ESP32-based BLE remote controller for the LEGO Duplo Train. Uses a potentiometer for variable speed (forward / stop / reverse) and four buttons for horn, LED colour, water refill sound, and emergency stop.
This project is an adaptation of the work by James Clarke, as showcased on Instructables: Wooden Bluetooth Remote for Lego Duplo Train.
The 3D-printed remote enclosure is available on:
| Component | Details |
|---|---|
| Board | BT 2.4G WLAN-Modul ESP32-32 CP2102 USB Micro (AliExpress) |
| Potentiometer | (10K Linear pot) connected to pin 15 |
| BTN_MUSIC | Momentary push-button on pin 18 -- horn sound |
| BTN_LICHT | Momentary push-button on pin 19 -- cycle LED colour |
| BTN_WASSER | Momentary push-button on pin 22 -- water refill sound |
| BTN_STOP | Momentary push-button on pin 23 -- emergency stop |
All buttons use the ESP32 internal pull-up resistors (INPUT_PULLUP), so
each button should connect its pin to GND when pressed.
- Continuous speed control — potentiometer mapped to forward / stop / reverse with smooth ramping (speed range
SPEED_MINto 64) - Adaptive analog smoothing — ResponsiveAnalogRead library filters ADC noise for jitter-free input
- Center dead zone — a configurable band around the midpoint where speed is zero, preventing accidental movement
- Emergency stop with pot-lock — the stop button immediately halts the motor; the potentiometer stays locked until moved past a configurable threshold (
STOP_UNBLOCK_THRESHOLD) - Inactivity auto-sleep — after 10 minutes of no input the controller stops the train, shuts down BLE, and puts the ESP32 into deep sleep to save battery
- Steam sound at max speed — plays a horn sound when the motor reaches full throttle (once per acceleration cycle)
- BLE command resending — periodically resends motor commands to guard against dropped BLE packets
- Modular codebase — split from the original monolithic
main.cppinto focused modules:train_control,buttons,power, and sharedconfig/pinsheaders - Centralized configuration — all tunable parameters (pot calibration, timeouts, thresholds, pin assignments) live in
include/config.h
- Install Visual Studio Code.
- Install the PlatformIO IDE extension from the VSCode marketplace.
- Open this project folder in VSCode (File > Open Folder).
- PlatformIO will automatically detect
platformio.iniand install the required libraries (Legoino,NimBLE-Arduino,Bounce2,ResponsiveAnalogRead). - Connect the ESP32 board via USB.
- Click the PlatformIO: Upload button (arrow icon in the bottom toolbar)
or run
pio run -t uploadin the terminal to compile and flash the firmware. - Open the Serial Monitor (plug icon in the PlatformIO toolbar, or run
pio device monitor) at 115200 baud to view debug output.
Note: The upload and monitor port is configured as
COM6inplatformio.ini. Change it to match your system if needed.
All configuration is done via #define constants in
include/config.h. After changing any value, re-upload the firmware.
The ESP32 ADC is 12-bit (0--4095). The potentiometer rarely uses the full
range, so POT_MIN and POT_MAX define the actual endpoints.
How to calibrate:
- Upload the firmware and open the Serial Monitor at 115200 baud.
- Move the potentiometer handle fully forward and fully backward several times.
- Watch the
Min:andMax:values in the debug output -- these are the observed raw ADC extremes. - Replace
POT_MINwith the observed minimum andPOT_MAXwith the observed maximum ininclude/config.h, then re-upload.
Default values: POT_MIN = 400, POT_MAX = 2300.
If the potentiometer is wired with GND and VCC swapped (i.e. forward motion produces high ADC instead of low), set:
#define POT_REVERSED trueThis flips the internal mapping so the control direction is correct without
re-wiring. Default: false.
The lowest speed value sent to the train motor (in both directions). Values below this threshold cause the motor to stall rather than turn. Increase if the train struggles to start; decrease for finer low-speed control.
Default: 20 (valid range sent to motor: SPEED_MIN to 64).
The normalised potentiometer range (0--1000) is split into three regions:
| Normalised range | Behaviour |
|---|---|
| 600 -- 1000 | Forward -- speed ramps from SPEED_MIN to 64 |
| 400 -- 599 | Dead zone -- motor stopped (speed = 0) |
| 0 -- 399 | Backward -- speed ramps from -SPEED_MIN to -64 |
The dead zone in the centre prevents accidental movement when the handle is near the midpoint.
After the emergency stop button is pressed, the potentiometer is locked until
the calculated speed changes by at least STOP_UNBLOCK_THRESHOLD units from
the speed at the moment of the stop. This prevents accidental resumption from
minor potentiometer drift.
Increase for a larger "dead zone" after stop (safer but requires more handle movement to resume); decrease for quicker re-engagement.
Default: 20 (speed range is -64 to 64).
| Define | Pin | Function |
|---|---|---|
PTI_SPEED |
15 | Potentiometer analog input |
BTN_MUSIC |
18 | Horn / music sound |
BTN_LICHT |
19 | Cycle LED colour |
BTN_WASSER |
22 | Water refill sound |
BTN_STOP |
23 | Emergency stop (blocks pot until handle is moved) |
Open the Serial Monitor at 115200 baud to get real-time debug output. The firmware prints a status line every 500 ms:
[POT] Raw:1842 | Filt:1840 | V:1.48 | Norm: 241 | Min: 398 Max:2301 | Speed:-28 | BLE:SENT(changed)
| Field | Meaning |
|---|---|
Raw |
Unfiltered ADC reading |
Filt |
Smoothed ADC (via ResponsiveAnalogRead) |
V |
Voltage of filtered reading (0--3.3 V) |
Norm |
Normalised position (0--1000; 1000 = full forward) |
Min/Max |
Observed raw ADC extremes since boot (use these for calibrating POT_MIN/POT_MAX) |
Speed |
Motor speed sent to train (-64 to 64, 0 = stopped) |
BLE |
BLE status: SENT(changed), resent, skip, or BLOCKED |
Use the Min and Max values to calibrate POT_MIN and POT_MAX as
described above.
See CHANGELOG.md for all notable changes from the upstream mav00/LDTrainRemote.
This project is licensed under the GNU General Public License v3.0. See LICENSE for details.