Control your LEGO MINDSTORMS, SPIKE Prime, or Powered UP sets directly from a web browser – no apps to install, no native code required. This comprehensive tutorial teaches you to build real web applications using the Web Bluetooth API and LEGO Wireless Protocol, with complete working code examples.

Table of Contents

What is Web Bluetooth?

Web Bluetooth is a JavaScript API that enables websites to communicate with Bluetooth Low Energy (BLE) devices. Combined with LEGO's open wireless protocol, it unlocks powerful capabilities:

  • No Installation: Run LEGO controllers directly in a browser
  • Cross-Platform: Works on Windows, Mac, Linux, Android, ChromeOS
  • Custom UIs: Build intuitive control panels with HTML/CSS/JavaScript
  • Real-Time: Low-latency communication (~50-100ms)
  • Educational: Teach web development + robotics simultaneously
  • Demos & Kiosks: Create interactive product demonstrations

Browser & Hub Compatibility

Supported Browsers

Browser Desktop Mobile Notes
Chrome ✅ Yes ✅ Android Best support, recommended
Edge ✅ Yes ✅ Android Chromium-based, full support
Opera ✅ Yes ✅ Android Chromium-based, full support
Safari ❌ No ❌ iOS No Web Bluetooth support
Firefox ⚠️ Flag ❌ No Experimental, disabled by default
⚠️ iOS Limitation: Apple does not support Web Bluetooth API on iOS/iPadOS. Use Android or desktop browsers for LEGO control.

Compatible LEGO Hubs

  • MINDSTORMS Robot Inventor (51515) - Technic Hub - Full Support
  • SPIKE Prime (45678) - Technic Hub - Full Support
  • SPIKE Essential (45345) - Small Hub - Full Support
  • Powered UP Hub (88016) - Full Support
  • Boost Move Hub (17101) - Full Support
  • Technic Control+ Hub (88012) - Full Support
  • Duplo Train Base (28743) - Basic Support

Initial Setup & Connection

Step 1: Feature Detection

Always check if Web Bluetooth is available before attempting to connect:

// Check if Web Bluetooth is supported if (!navigator.bluetooth) { alert('Web Bluetooth is not supported in this browser. Try Chrome or Edge.'); throw new Error('Web Bluetooth not supported'); }
console.log('Web Bluetooth is supported!'); 

Step 2: Request Bluetooth Device

Request access to LEGO hubs by filtering for the LEGO Wireless Protocol service UUID:

// LEGO Wireless Protocol service UUID const LEGO_SERVICE_UUID = '00001623-1212-efde-1623-785feabcd123';
async function connectToLEGOHub() { try { console.log('Requesting LEGO hub...');
// Request device with LEGO service filter const device = await navigator.bluetooth.requestDevice({ filters: [{ services: [LEGO_SERVICE_UUID] }], optionalServices: [LEGO_SERVICE_UUID] });
console.log('Device selected:', device.name);
// Connect to GATT server const server = await device.gatt.connect(); console.log('Connected to GATT server');
// Get LEGO service const service = await server.getPrimaryService(LEGO_SERVICE_UUID); console.log('LEGO service found');
// Get characteristic for communication const characteristic = await service.getCharacteristic( '00001624-1212-efde-1623-785feabcd123'  // LEGO characteristic UUID );
console.log('Ready to communicate!'); return { device, server, characteristic };
} catch (error) { console.error('Connection failed:', error); throw error; } } 

Step 3: HTML Button to Trigger Connection

<!DOCTYPE html> <html> <head> <title>LEGO Web Bluetooth Controller</title> </head> <body> <h1>LEGO Hub Controller</h1> <button id="connectBtn">Connect to LEGO Hub</button> <div id="status">Not connected</div>
<script> document.getElementById('connectBtn').addEventListener('click', async () => { const connection = await connectToLEGOHub(); document.getElementById('status').textContent = 'Connected!'; }); </script> </body> </html> 
💡 User Gesture Required: Web Bluetooth requires a user gesture (button click) to request device access. You cannot auto-connect on page load for security reasons.

LEGO Wireless Protocol Basics

LEGO hubs use a binary protocol called the LEGO Wireless Protocol (LWP). Commands are sent as byte arrays:

Message Structure

// Basic message format: // [Length, HubID, MessageType, ...Payload]
// Example: Turn on LED to blue const message = new Uint8Array([ 0x08,  // Length (8 bytes) 0x00,  // Hub ID 0x81,  // Port Output Command 0x32,  // Port 50 (RGB LED) 0x11,  // Start Power (Mode 1, Exec Immediately) 0x51,  // Sub-command: Set RGB 0x01,  // Mode: Direct RGB 0x00, 0x00, 0xFF  // RGB: Blue (R=0, G=0, B=255) ]);
// Send command to hub await characteristic.writeValue(message); 

Common Message Types

Message Type Hex Purpose
Hub Properties 0x01 Get hub name, battery, etc.
Hub Actions 0x02 Shutdown, disconnect, etc.
Port Information 0x43 Query port capabilities
Port Value 0x45 Receive sensor data
Port Output Command 0x81 Control motors/LEDs

Motor Control Examples

Example 1: Run Motor at Speed

/** * Run a motor at specified speed * @param {BluetoothRemoteGATTCharacteristic} characteristic * @param {number} port - Motor port (0-5 for ports A-F) * @param {number} speed - Speed (-100 to 100) */ async function runMotor(characteristic, port, speed) { // Clamp speed to valid range speed = Math.max(-100, Math.min(100, speed));
// Convert to signed byte const speedByte = speed < 0 ? 256 + speed : speed;
const message = new Uint8Array([ 0x0A,  // Length 0x00,  // Hub ID 0x81,  // Port Output Command port,  // Port number (0=A, 1=B, etc.) 0x11,  // Start Power 0x01,  // Mode: Speed speedByte  // Speed value ]);
await characteristic.writeValue(message); console.log(`Motor on port ${port} running at ${speed}%`); }
// Usage: await runMotor(characteristic, 0, 50);   // Port A forward at 50% await runMotor(characteristic, 1, -75);  // Port B backward at 75% 

Example 2: Run Motor for Degrees

/** * Run motor for specific number of degrees * @param {BluetoothRemoteGATTCharacteristic} characteristic * @param {number} port - Motor port * @param {number} degrees - Degrees to rotate * @param {number} speed - Speed (0-100) */ async function runMotorForDegrees(characteristic, port, degrees, speed = 50) { // Convert degrees to little-endian 32-bit integer const degreesBytes = new Uint8Array(4); new DataView(degreesBytes.buffer).setInt32(0, degrees, true);
const message = new Uint8Array([ 0x0E,  // Length 0x00,  // Hub ID 0x81,  // Port Output Command port, 0x11,  // Start Power 0x0B,  // Mode: GoToAbsolutePosition ...degreesBytes,  // Degrees (4 bytes, little-endian) speed,  // Speed 0x64,   // Max power (100%) 0x7F    // End state: Hold ]);
await characteristic.writeValue(message); console.log(`Motor ${port} rotating ${degrees} degrees`); }
// Usage: await runMotorForDegrees(characteristic, 0, 360, 75);  // 1 full rotation 

Example 3: Stop Motor

async function stopMotor(characteristic, port) { const message = new Uint8Array([ 0x07,  // Length 0x00,  // Hub ID 0x81,  // Port Output Command port, 0x11,  // Start Power 0x00   // Speed: 0 (stop) ]);
await characteristic.writeValue(message); console.log(`Motor ${port} stopped`); } 

Example 4: Set Hub LED Color

/** * Set the hub's RGB LED color * @param {number} r - Red (0-255) * @param {number} g - Green (0-255) * @param {number} b - Blue (0-255) */ async function setHubLED(characteristic, r, g, b) { const message = new Uint8Array([ 0x08,  // Length 0x00,  // Hub ID 0x81,  // Port Output Command 0x32,  // Port 50 (RGB LED on hub) 0x11,  // Start Power 0x51,  // Sub-command: Set RGB 0x00,  // Mode r, g, b  // RGB values ]);
await characteristic.writeValue(message); }
// Usage: await setHubLED(characteristic, 255, 0, 0);    // Red await setHubLED(characteristic, 0, 255, 0);    // Green await setHubLED(characteristic, 0, 0, 255);    // Blue await setHubLED(characteristic, 255, 255, 0);  // Yellow 

Reading Sensor Data

Example 5: Subscribe to Port Value Updates

/** * Enable notifications from a sensor port * @param {BluetoothRemoteGATTCharacteristic} characteristic * @param {number} port - Sensor port * @param {number} mode - Sensor mode (0 = color, 1 = distance, etc.) */ async function subscribeSensor(characteristic, port, mode = 0) { // Enable notifications await characteristic.startNotifications();
// Listen for port value updates characteristic.addEventListener('characteristicvaluechanged', (event) => { const value = event.target.value; const bytes = new Uint8Array(value.buffer);
// Check if this is a port value message (0x45) if (bytes[2] === 0x45 && bytes[3] === port) { // Parse sensor value (depends on sensor type) const sensorValue = bytes[4]; console.log(`Port ${port} value:`, sensorValue);
// Trigger custom event with sensor data document.dispatchEvent(new CustomEvent('sensorData', { detail: { port, value: sensorValue } })); } });
// Send port input format setup const message = new Uint8Array([ 0x0A,  // Length 0x00,  // Hub ID 0x41,  // Port Input Format Setup (Single) port, mode,  // Mode 0x01, 0x00, 0x00, 0x00,  // Delta interval (1) 0x01   // Notification enabled ]);
await characteristic.writeValue(message); console.log(`Subscribed to port ${port} in mode ${mode}`); }
// Usage: Subscribe to color sensor on port 2 await subscribeSensor(characteristic, 2, 0);
// Listen for sensor updates document.addEventListener('sensorData', (e) => { console.log(`Sensor on port ${e.detail.port}: ${e.detail.value}`); }); 

Example 6: Reading Distance Sensor

// Subscribe to distance sensor (port 3, mode 0 = distance in cm) await subscribeSensor(characteristic, 3, 0);
document.addEventListener('sensorData', (e) => { if (e.detail.port === 3) { const distanceCm = e.detail.value; document.getElementById('distance').textContent = `${distanceCm} cm`;
// React to proximity if (distanceCm < 10) { console.log('Object detected close by!'); } } }); 

Building a Complete Control Interface

<!DOCTYPE html> <html> <head> <title>LEGO Hub Controller</title> <style> body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; } button { padding: 10px 20px; margin: 5px; font-size: 16px; } .motor-control { margin: 20px 0; padding: 20px; border: 1px solid #ccc; } input[type=range] { width: 100%; } #status { padding: 10px; background: #f0f0f0; border-radius: 5px; } </style> </head> <body> <h1>LEGO Web Bluetooth Controller</h1>
<button id="connectBtn">Connect to Hub</button> <div id="status">Not connected</div>
<div class="motor-control" id="controls" style="display:none;"> <h2>Motor A Control</h2> <label>Speed: <span id="speedValue">0</span>%</label> <input type="range" id="speedSlider" min="-100" max="100" value="0"> <button id="stopBtn">Stop Motor</button>
<h2>Hub LED</h2> <button onclick="setColor(255,0,0)">Red</button> <button onclick="setColor(0,255,0)">Green</button> <button onclick="setColor(0,0,255)">Blue</button> <button onclick="setColor(0,0,0)">Off</button>
<h2>Distance Sensor</h2> <div>Distance: <span id="distance">--</span> cm</div> </div>
<script> const LEGO_SERVICE = '00001623-1212-efde-1623-785feabcd123'; const LEGO_CHARACTERISTIC = '00001624-1212-efde-1623-785feabcd123'; let characteristic = null;
// Connect button document.getElementById('connectBtn').addEventListener('click', async () => { try { const device = await navigator.bluetooth.requestDevice({ filters: [{ services: [LEGO_SERVICE] }] });
const server = await device.gatt.connect(); const service = await server.getPrimaryService(LEGO_SERVICE); characteristic = await service.getCharacteristic(LEGO_CHARACTERISTIC);
document.getElementById('status').textContent = 'Connected to ' + device.name; document.getElementById('controls').style.display = 'block';
// Subscribe to distance sensor on port 3 await subscribeSensor(characteristic, 3, 0);
} catch (error) { alert('Connection failed: ' + error); } });
// Speed slider document.getElementById('speedSlider').addEventListener('input', async (e) => { const speed = parseInt(e.target.value); document.getElementById('speedValue').textContent = speed; if (characteristic) { await runMotor(characteristic, 0, speed);  // Port A } });
// Stop button document.getElementById('stopBtn').addEventListener('click', async () => { if (characteristic) { await stopMotor(characteristic, 0); document.getElementById('speedSlider').value = 0; document.getElementById('speedValue').textContent = 0; } });
// LED color function async function setColor(r, g, b) { if (characteristic) { await setHubLED(characteristic, r, g, b); } }
// Listen for sensor data document.addEventListener('sensorData', (e) => { if (e.detail.port === 3) { document.getElementById('distance').textContent = e.detail.value; } });
// [Include motor control functions from previous examples] </script> </body> </html> 

Using node-poweredup Library (Easier Approach)

For production projects, use the node-poweredup library which abstracts the protocol complexity:

<!-- Include library via CDN --> <script src="https://cdn.jsdelivr.net/npm/node-poweredup@latest/dist/poweredup-browser.js"></script>
<script> const PoweredUP = window.poweredup; const hub = new PoweredUP.Hub();
// Connect to hub document.getElementById('connectBtn').addEventListener('click', async () => { await hub.connect(); console.log('Connected!'); });
// Run motor (much simpler!) async function runMotor(port, speed) { const motor = await hub.waitForDeviceAtPort(port); await motor.setPower(speed); }
// Usage: await runMotor('A', 50);  // Port A at 50% speed </script> 

Benefits of node-poweredup:

  • ✅ Handles protocol encoding/decoding automatically
  • ✅ Type-safe TypeScript support
  • ✅ Supports all LEGO hub types
  • ✅ Active maintenance and community
  • ✅ Built-in sensor data parsing

GitHub: nathankellenicki/node-poweredup

Troubleshooting Common Issues

"Bluetooth not supported" Error

  • ✓ Use Chrome, Edge, or Opera (not Safari/Firefox)
  • ✓ Ensure HTTPS connection (required for security)
  • ✓ Check chrome://flags/#enable-web-bluetooth is enabled

Hub Not Appearing in Device List

  • ✓ Turn hub off and on again (press center button 5+ seconds)
  • ✓ Make sure hub is not connected to another device
  • ✓ Check hub battery is charged (LED should be green/blue)
  • ✓ Ensure Bluetooth is enabled on your computer

Commands Not Working

  • ✓ Verify correct port numbers (0=A, 1=B, 2=C, etc.)
  • ✓ Check message length byte is correct
  • ✓ Ensure motor/sensor is actually connected to that port
  • ✓ Use browser DevTools console to see error messages

Connection Drops Frequently

  • ✓ Reduce distance between computer and hub (BLE range ~10m)
  • ✓ Remove obstacles between devices
  • ✓ Avoid interference from other Bluetooth devices
  • ✓ Update hub firmware via official LEGO app

Advanced Topics


Multi-Hub Control

Control multiple hubs simultaneously by maintaining separate connections:


const hub1 = await connectToHub();  // First hub const hub2 = await connectToHub();  // Second hub
// Control both independently await runMotor(hub1.characteristic, 0, 50); await runMotor(hub2.characteristic, 0, -50);

Synchronized Motor Control

Use virtual port commands to synchronize multiple motors:


// Command 0x81 with virtual port combines motors A+B // See LEGO Wireless Protocol docs for virtual port details 

Learning Resources



Project Ideas


  1. Virtual Joystick: Use HTML5 canvas to create on-screen joystick for robot control
  2. Sensor Dashboard: Real-time graphs of distance/color sensor data with Chart.js
  3. Voice Control: Combine Web Speech API with Web Bluetooth for voice-controlled robots
  4. Multiplayer Game: WebSocket server coordinates multiple players controlling different hubs
  5. Educational IDE: Block-based programming interface that generates Web Bluetooth code
  6. AR Control: Use WebXR + Web Bluetooth to control physical robot with AR interface

Get MINDSTORMS Robot Inventor for Web Control Get SPIKE Prime for Educational Web Apps Get Powered UP Hub for Custom Projects