ESP32 + LEGO Powered Up: Complete Wireless Control Guide with Legoino
The ESP32 has emerged as the premier choice for LEGO integration, primarily due to its dual-core architecture, integrated Wi-Fi and Bluetooth stacks, and exceptionally low cost. This comprehensive guide takes you from first-time Arduino user to advanced hub emulation, with real code examples that work on actual hardware.
Table of Contents
- Why ESP32 for LEGO?
- Beginner: Arduino IDE Setup
- Hardware Options: M5Stack Ecosystem
- Legoino Library Deep Dive
- Motor Control & PWM
- Reading Sensors via BLE
- Advanced: Hub Emulation ⚡ Skip here if experienced
- Headless Operation
- Bonus: Legacy Power Functions IR
- Troubleshooting
Why ESP32 for LEGO Integration?
The ESP32's dominance in LEGO automation is driven by specific hardware capabilities that align perfectly with robotics requirements:
| Feature | ESP32 | Arduino Uno | Raspberry Pi |
|---|---|---|---|
| Bluetooth LE | Built-in | Requires module | Built-in (Pi 3+) |
| Wi-Fi | Built-in | Requires shield | Built-in |
| Cores | Dual-core 240MHz | Single 16MHz | Quad-core 1.5GHz |
| Power Draw | ~80mA active | ~50mA | ~700mA minimum |
| Cost | $3-8 | $5-25 | $35-75 |
| Form Factor | Compact (M5Atom: 24x24mm) | Standard | Credit card sized |
Key Advantage: The ESP32 can act as a BLE Client, connecting directly to LEGO Powered Up hubs without an intermediary app. Standard smartphones connect to LEGO hubs as clients; the ESP32 can emulate this role, enabling headless operation critical for museum exhibits, automated displays, and train layouts.
Beginner: Arduino IDE Setup
If you're new to microcontrollers, follow these steps to set up your development environment:
Step 1: Install Arduino IDE
- Download Arduino IDE from arduino.cc
- Install for your operating system (Windows, Mac, or Linux)
Step 2: Add ESP32 Board Support
- Open Arduino IDE → File → Preferences
- In "Additional Board Manager URLs" add:
https://dl.espressif.com/dl/package_esp32_index.json - Go to Tools → Board → Boards Manager
- Search "ESP32" and install "ESP32 by Espressif Systems"
Step 3: Install Required Libraries
- Go to Sketch → Include Library → Manage Libraries
- Search "NimBLE-Arduino" and install it (required dependency)
- Search "Legoino" and install it (maintained by corneliusmunz)
Step 4: Select Your Board
- Tools → Board → ESP32 Arduino → select your specific board
- For M5Atom: Select "M5Stack-ATOM"
- For generic ESP32: Select "ESP32 Dev Module"
- Tools → Port → select the COM port (or /dev/tty.* on Mac)
Hardware Options: The M5Stack Ecosystem
Unlike bare development boards that require breadboards and jumper wires—awkward to mount on a Technic frame—M5Stack devices come in industrial-grade cases with native LEGO-compatible mounting.
M5Atom Matrix (Recommended for Beginners)
- Size: 24.0 x 24.0mm - fits within a standard 4x4 stud footprint
- Chip: ESP32-PICO-D4 with 4MB SPI flash, Wi-Fi, Bluetooth
- Display: 5x5 RGB LED matrix (WS2812) for status feedback
- Sensors: Built-in 6-axis IMU (MPU6886) for motion detection
- IR: Infrared LED transmitter (can control PowerFunction IR devices!)
- Button: Programmable button hidden under the LED matrix
- Mounting: One M2 screw hole on back
- LEGO Adapter: The Atom Mate accessory kit includes building block adapters with cross-shaped holes compatible with LEGO Technic
- Price: ~$15 (Atom Mate kit ~$5 extra)
// M5Atom Matrix LED status display
#include <M5Atom.h>
void showConnected() {
// Green checkmark pattern
M5.dis.fillpix(0x00FF00);
}
void showError() {
// Red X pattern
M5.dis.fillpix(0xFF0000);
}
M5StickC Plus2 (For Controllers)
- Display: Built-in 1.14" LCD screen
- Sensors: 6-axis IMU (gyro + accelerometer)
- Buttons: Two physical buttons for input
- Battery: Internal battery with PMIC for autonomous operation
- LEGO Mounting: Watch Accessories kit includes LEGO-compatible adapter
- Use Case: Wearable controllers, smart dashboards on vehicles
M5Stack Core2 (For Complex Projects)
- Display: 2" capacitive touch screen
- Base Plate: M5GO Bottom features a grid of LEGO-compatible holes
- Battery: Large internal battery
- Use Case: Central "brain brick" for large robots or GBC modules
- Security: AWS version includes ATECC608 hardware encryption
Legoino Library Deep Dive
The Legoino library abstracts the complexity of the BLE handshake protocol. Instead of manually manipulating byte arrays, you write high-level code:
Basic Connection Example
#include "Lpf2Hub.h"
// Create hub instance
Lpf2Hub myHub;
void setup() {
Serial.begin(115200);
myHub.init(); // Initialize BLE
}
void loop() {
// Check for hubs and connect
if (myHub.isConnecting()) {
myHub.connectHub();
if (myHub.isConnected()) {
Serial.println("Connected to hub!");
}
}
delay(100);
}
Supported Hubs
Legoino supports all major LEGO hub types:
- Powered Up Hub (City trains, simple vehicles)
- Technic Hub (Control+, advanced builds like the Liebherr R 9800 Excavator (42100))
- Boost Move Hub (Educational robotics)
- DUPLO Train Hub (Toddler-friendly motorized trains)
- Remote Control (Handset device)
Supported Devices
Motors, sensors, and accessories:
- Train Motor, Medium/Large/XL Angular Motors
- Color & Distance Sensor, Tilt Sensor
- Speedometer (for speed/distance tracking)
- Hub LED (RGB color control)
Multi-Hub Support
A single ESP32 can connect to up to 9 LEGO hubs simultaneously. To enable more than 3 connections, adjust the NimBLE configuration:
// In your Arduino sketch, before including Legoino:
#define CONFIG_BT_NIMBLE_MAX_CONNECTIONS 9
This enables complex layouts with multiple trains, each controlled independently from one ESP32.
Motor Control & PWM
Basic Speed Control
#include "Lpf2Hub.h"
Lpf2Hub myTrainHub;
byte port = (byte)PoweredUpHubPort::A;
void setup() {
Serial.begin(115200);
myTrainHub.init();
}
void loop() {
if (myTrainHub.isConnecting()) {
myTrainHub.connectHub();
if (myTrainHub.isConnected()) {
Serial.println("Connected!");
// Run motor at 50% speed
myTrainHub.setBasicMotorSpeed(port, 50);
delay(3000);
// Stop motor
myTrainHub.stopBasicMotor(port);
delay(1000);
// Reverse at 30%
myTrainHub.setBasicMotorSpeed(port, -30);
delay(3000);
myTrainHub.stopBasicMotor(port);
}
}
delay(100);
}
Advanced: Tacho Motor Control
For motors with encoders (Technic motors), you can control position precisely:
// Rotate motor to specific angle
void moveToAngle(Lpf2Hub &hub, byte port, int angle, int speed) {
// Set acceleration profile
hub.setAccelerationProfile(port, 500); // 500ms ramp
hub.setDecelerationProfile(port, 500);
// Move to absolute position
hub.setAbsoluteMotorPosition(port, angle, speed);
}
// Example: Move 90 degrees at 50% speed
moveToAngle(myHub, portA, 90, 50);
PWM Frequency and Motor Response
LEGO motors respond to PWM signals at the hub's internal frequency (~1.2kHz). The speed parameter (-100 to +100) controls duty cycle:
- 0: Motor stopped (brake mode)
- 1-100: Forward, increasing speed
- -1 to -100: Reverse, increasing speed
Reading Sensors via BLE
Color and Distance Sensor
void colorSensorCallback(void *hub, byte portNumber,
DeviceType deviceType, uint8_t *pData) {
Lpf2Hub *myHub = (Lpf2Hub *)hub;
if (deviceType == DeviceType::COLOR_DISTANCE_SENSOR) {
int color = cycleBuffer[0];
int distance = cycleBuffer[1];
Serial.print("Color: ");
Serial.print(color); // 0=Black, 3=Blue, 5=Green, 7=Yellow, 9=Red, 10=White
Serial.print(" Distance: ");
Serial.println(distance);
}
}
void setup() {
// ... init code ...
myHub.activatePortDevice(portB, colorSensorCallback);
}
Reading Battery Level
void hubPropertyCallback(void *hub, HubPropertyReference property,
uint8_t *pData) {
Lpf2Hub *myHub = (Lpf2Hub *)hub;
if (property == HubPropertyReference::BATTERY_VOLTAGE) {
Serial.print("Battery: ");
Serial.print(pData[0]);
Serial.println("%");
}
}
// In setup:
myHub.activateHubPropertyUpdate(
HubPropertyReference::BATTERY_VOLTAGE,
hubPropertyCallback
);
Advanced: Hub Emulation
POWERED_UP_HUB type is fully supported, and only TRAIN_MOTOR and HUB_LED devices work reliably. Check the GitHub issues for updates.
The Legoino library can broadcast the specific BLE advertising packets that identify a device as a LEGO Hub. To the Powered Up app, your custom Arduino robot "looks" like a real hub.
How Hub Emulation Works
- Advertising: ESP32 broadcasts LEGO-specific service UUIDs
- Connection: Phone app discovers and connects
- Characteristics: ESP32 exposes BLE characteristics matching LEGO's official BLE protocol spec
- Commands: App sends motor commands, ESP32 routes to your hardware
// Hub emulation example using Legoino
#include "Lpf2HubEmulation.h"
#include "LegoinoCommon.h"
// Create emulated hub - name shown in Powered Up app
Lpf2HubEmulation myEmulatedHub("TrainHub", HubType::POWERED_UP_HUB);
// Motor control pins (connect to L298N or similar driver)
const int MOTOR_PIN_A = 25;
const int MOTOR_PIN_B = 26;
void setup() {
Serial.begin(115200);
// Setup motor pins
pinMode(MOTOR_PIN_A, OUTPUT);
pinMode(MOTOR_PIN_B, OUTPUT);
// Start hub emulation - ESP32 now advertises as LEGO hub
myEmulatedHub.start();
Serial.println("Hub emulation started. Open Powered Up app!");
}
void loop() {
// Check if app is connected
if (myEmulatedHub.isConnected()) {
// Read motor command from app (port A)
int speed = myEmulatedHub.getMotorSpeed(0); // Port A = 0
// Convert -100..100 to PWM
if (speed > 0) {
analogWrite(MOTOR_PIN_A, map(speed, 0, 100, 0, 255));
analogWrite(MOTOR_PIN_B, 0);
} else if (speed < 0) {
analogWrite(MOTOR_PIN_A, 0);
analogWrite(MOTOR_PIN_B, map(-speed, 0, 100, 0, 255));
} else {
analogWrite(MOTOR_PIN_A, 0);
analogWrite(MOTOR_PIN_B, 0);
}
}
delay(10);
}
Use Cases for Hub Emulation
- Custom Motors: Use more powerful non-LEGO motors but control via the polished Powered Up app
- Legacy Power Functions: Bridge old IR-controlled motors to the new app (ESP32 can transmit IR!)
- Custom Vehicles: Build RC cars with standard hobby motors and LEGO-compatible control
Headless Operation for Exhibits
A key advantage of ESP32 control is headless operation—no smartphone required. This is critical for:
- Museum exhibits: Train runs on schedule without staff intervention
- Store displays: Animated LEGO display starts at opening time
- Home automation: Train runs when motion sensor triggers
Example: Scheduled Train Operation
#include "Lpf2Hub.h"
#include <WiFi.h>
#include <time.h>
Lpf2Hub trainHub;
bool isRunning = false;
void setup() {
Serial.begin(115200);
// Connect to WiFi for NTP time
WiFi.begin("YourSSID", "YourPassword");
while (WiFi.status() != WL_CONNECTED) delay(500);
// Configure NTP
configTime(-5 * 3600, 0, "pool.ntp.org"); // EST timezone
trainHub.init();
}
void loop() {
// Get current time
struct tm timeinfo;
getLocalTime(&timeinfo);
// Run train between 9 AM and 5 PM
bool shouldRun = (timeinfo.tm_hour >= 9 && timeinfo.tm_hour < 17);
if (shouldRun && !isRunning) {
connectAndStart();
} else if (!shouldRun && isRunning) {
stopAndDisconnect();
}
delay(1000);
}
void connectAndStart() {
if (trainHub.isConnecting()) {
trainHub.connectHub();
if (trainHub.isConnected()) {
trainHub.setBasicMotorSpeed((byte)PoweredUpHubPort::A, 40);
isRunning = true;
}
}
}
void stopAndDisconnect() {
trainHub.stopBasicMotor((byte)PoweredUpHubPort::A);
trainHub.shutDownHub();
isRunning = false;
}
Bonus: Control Legacy Power Functions via IR
The M5Atom Matrix has a built-in IR LED, and Legoino supports the old Power Functions IR protocol. This means you can control legacy 9V motors from 2007-2018 without any additional hardware!
// Control Power Functions motor via IR
#include "PowerFunctions.h"
// IR LED pin (M5Atom Matrix built-in IR is GPIO12)
PowerFunctions pf(12);
void setup() {
Serial.begin(115200);
}
void loop() {
// Channel 1 (blue), Red output, forward
pf.single_pwm(PowerFunctionsChannel::CHANNEL_1,
PowerFunctionsOutput::RED,
PowerFunctionsPWM::FORWARD4);
delay(2000);
// Stop
pf.single_pwm(PowerFunctionsChannel::CHANNEL_1,
PowerFunctionsOutput::RED,
PowerFunctionsPWM::BRAKE);
delay(1000);
}
Bridge Old and New: Combine hub emulation with IR output to control old Power Functions motors using the modern Powered Up app!
Troubleshooting
Connection Issues
- Hub not found: Ensure hub is in pairing mode (LED blinking white)
- Connection drops: Check battery level; low battery causes BLE instability
- Multiple hubs: Use hub address filtering to connect to specific hub
Motor Not Responding
- Verify correct port letter (A, B, C, D)
- Check that motor is properly connected to hub
- Try different speed values (some motors have minimum threshold)
ESP32 Rebooting
- Power issue: BLE transmission draws significant current; use quality USB cable
-
Watchdog: Add
delay(10)in tight loops to feed watchdog - Memory: BLE stack is memory-intensive; avoid large buffers
Next Steps
Now that you've mastered ESP32 + LEGO basics, explore these advanced topics:
- MQTT Integration: Connect your LEGO train to Home Assistant
- Web Dashboard: Build a browser-based control panel with ESP32 web server
- Sensor Fusion: Add external sensors (RFID, ultrasonic) for automation
- Multi-Hub Control: Coordinate multiple LEGO hubs from single ESP32
Resources
- Legoino GitHub Repository - Source code, examples, and documentation
- Legoino on Arduino Library Reference - Official Arduino library page
- LEGO BLE Protocol Documentation - Official protocol specification (open source!)
- MyOwnBricks Library - Build and simulate custom Powered Up compatible sensors
- M5Stack ATOM Matrix Documentation - Hardware specs and pinout
- M5Stack ATOM Matrix Store - Purchase link
- ESP32 Documentation - Chip specifications from Espressif
Use Our Tools to Go Further
Get more insights about the sets mentioned in this article with our free LEGO tools