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

⚡ Experienced with Arduino? Jump directly to Hub Emulation for advanced protocol details, or Legoino Library for code examples.

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

  1. Download Arduino IDE from arduino.cc
  2. Install for your operating system (Windows, Mac, or Linux)

Step 2: Add ESP32 Board Support

  1. Open Arduino IDE → File → Preferences
  2. In "Additional Board Manager URLs" add: https://dl.espressif.com/dl/package_esp32_index.json
  3. Go to Tools → Board → Boards Manager
  4. Search "ESP32" and install "ESP32 by Espressif Systems"

Step 3: Install Required Libraries

  1. Go to Sketch → Include Library → Manage Libraries
  2. Search "NimBLE-Arduino" and install it (required dependency)
  3. Search "Legoino" and install it (maintained by corneliusmunz)
⚠️ Important: Legoino requires the NimBLE-Arduino library as a dependency. Install it first or you'll get compilation errors.

Step 4: Select Your Board

  1. Tools → Board → ESP32 Arduino → select your specific board
  2. For M5Atom: Select "M5Stack-ATOM"
  3. For generic ESP32: Select "ESP32 Dev Module"
  4. 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:

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

⚡ Advanced Topic: Hub emulation allows your ESP32 to appear as a LEGO hub to the official Powered Up app. This is useful for building custom robots that leverage LEGO's polished UI.
⚠️ Beta Feature: Hub emulation is currently in BETA. Only 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

  1. Advertising: ESP32 broadcasts LEGO-specific service UUIDs
  2. Connection: Phone app discovers and connects
  3. Characteristics: ESP32 exposes BLE characteristics matching LEGO's official BLE protocol spec
  4. 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