Smooth controls for LEGO Mario Kart using LEGO Mario figure + BBC micro:bit
This project lets you control a LEGO Mario Kart using LEGO Mario's built‑in IMU and send smooth throttle/steering commands to a BBC micro:bit over Bluetooth. It started as a toy experiment but evolved into smooth, proportional control that makes driving faster, more precise, and way more fun—especially for kids!
See the step-by-step build on Hackster
Inspired by RickP/lego_mario_controller
- Bluetooth LE bridge from macOS (Python + Bleak) to micro:bit (UART/NUS). Mario sends raw IMU data to Mac OS -> Mac OS Python script is filtering IMU data -> Mac OS Python script sends control commands to the micro:bit.
- Smooth analog control with continuous throttle (−1.0 … +1.0) and steering (−1.0 … +1.0).
- micro:bit v2 MakeCode with WuKong motor controller.
- Tested with LEGO set 72043.
+--------------------+ +-------------------------------+ +------------------------+
| | | | | |
| LEGO Mario (BLE) |----->| macOS Bridge (Python/Bleak) |----->| micro:bit (BLE UART) |
| IMU (roll/pitch) | | EMA + bias; map to [-1..1] | | parse "<thr,steer>:" |
| notifications | | stream @ 10 Hz | | mix -> L/R motors |
+--------------------+ +-------------------------------+ +-----------+------------+
|
v
+----------------------+
| Motor Driver Board |
| (Geekservos L/R) |
+----------------------+
- Mario -> Bridge: BLE GATT notifications (IMU tilt).
- Bridge -> micro:bit: BLE UART (Nordic UART Service) for low-latency command lines.
- Device discovery and reconnect handled by CLI (scan -> connect). Optional local CSV logging for offline analysis.
- Filtering: EMA smoothing + brief startup bias calibration.
- Shaping: deadzone and exponential (“expo”) response for fine control around center.
- Mapping: roll/pitch -> continuous throttle/steer in [-1.0 … +1.0].
- Mixing (on device): throttle/steer -> left/right motor outputs for differential drive.
python mario_microbit_scan.py— scan to verify Mario and micro:bit are advertising and reachable.python mario_bridge.py[--x-scale <f>] [--z-scale <f>] [--deadzone <f>] [--expo <f>] [--invert-x] [--invert-z]— run the BLE bridge, tune sensitivity/feel, and stream commands at 10 Hz.- ART line format (Bridge -> micro:bit):
- Text line:
"<throttle>,<steer>:\n" - Two floats in
[-1.0 … +1.0], colon as terminator for safe readLine() parsing. - micro:bit app (MakeCode): Enable Bluetooth UART, “No pairing required,” read a line each tick, split, parse, and drive left/right motors.
- Connectivity checks: scan before connect; auto-retry on drop.
- Safety: deadzone prevents creep at neutral; bounds-clamp commands to
[-1.0 … +1.0].
- AI Adaptive curves per driver (learned sensitivity/expo on the bridge).
- Recording & playback to simulate sessions without hardware attached.
- LEGO Mario figure with Bluetooth enabled
- LEGO Mario Kart 72043 (or your own wheeled LEGO build), it doesn't matter.
- BBC micro:bit v2 with WuKong motor driver board
- Geekservo (aka Geekmotor) — e.g. KittenBot Red Geekservo
- macOS with Bluetooth LE (developed on Apple M1 15.6 Sequoia)
- Python 3.9+
- Bleak 0.21.x
- I published a modified Kart 3D model on BrickLink. See the full parts list in the: BrickLink Studio 3D Model
Technic gears (blue, used in this build)
- 2×
6396479 - 2×
69779
Alternative gears (tested, faster gearing, easier to source)
- 2×
10928 (3647) - 2×
3648
Structural System parts (colors don’t matter)
- 1×
4210998 - 2×
4654580 - 2×
6083620 - 6×
4210719 - 1×
303426 - 2×
6225230 - 4×
6092585
Technic beams (to mount Geekservos to the WuKong board)
- 2×
4210686 - 8×
6279875
Tyres and rims
- 2× Tyre
30391(or equivalent) - 2× Rim
55981(or equivalent)
Step 1 — Modify the rear axle of the kart
- Rear axle (rear view):

- Rear axle (alternate rear view):

- Additional view with the blue gear installed:

Step 2 — Build the micro:bit + Geekservo module
- Create the micro:bit + Geekservo motors block as shown:
Step 3 — Join the kart and the module
/bridge
mario_bridge.py # macOS BLE bridge (Mario -> micro:bit)
mario_microbit_scan.py # helper: list BLE devices for sanity check
/microbit
microbit_mario_kart_driver.hex # MakeCode micro:bit program
git clone https://github.com/maxxlife/interactive_microbit_mario_kart_toy.git
cd interactive_microbit_mario_kart_toyconda create -n lego_mario python=3.9
conda activate lego_mario
pip install -r requirements.txtconda env create -f environment.yml
conda activate lego_marioNote (macOS): The Conda environment includes the PyObjC bits Bleak needs for CoreBluetooth.
- Open MakeCode, import
microbit/microbit_mario_kart_driver.hex. - Ensure Bluetooth UART service is enable, and No Pairing required is set

- Flash to the micro:bit.
cd bridge
python mario_microbit_scan.pyExample terminal output:
% python mario_microbit_scan.py
Scanning for Bluetooth LE devices (5s)…
- BBC micro:bit [tapag] (E9F4DD5B-E022-4C3A-E889-784AB44424A5) UUIDs: []
- Mario asr5 (DC2BBB46-1DBE-BFBF-5C54-4971BDEE1F78) UUIDs: ['00001623-1212-efde-1623-785feabcd123']
Summary:
LEGO Mario found
micro:bit found
If both devices are in Summary, you’re ready to bridge.
# If you used Conda:
conda activate lego_mario
cd bridge
python mario_bridge.py --x-scale 28 --z-scale 32 --deadzone 0.08 --expo 1.3Hold Mario still for ~0.5 s while the bridge calibrates center (bias).
Then drive your Mario Kart with smooth control 🚗💨
The bridge reads Mario IMU (roll -> x, pitch -> z), smooths via EMA, subtracts a bias (captured at startup), then maps to analog outputs.
Analog output format (10 Hz):
<throttle>,<steer>:
- Floats in [−1.00 … +1.00]
throttle > 0= forward,< 0= reversesteer > 0= right,< 0= left- Each line ends with a colon
:and newline\nso the micro:bit canreadLine()safely.
CLI flags (feel dials):
--x-scale <float>– Steering sensitivity (larger = less sensitive). Start 28–36.--z-scale <float>– Throttle sensitivity (larger = less sensitive). Start 28–36.--deadzone <float>– Neutral band around center; start at 0.08–0.15 if the kart creeps.--expo <float>– Exponential response; 1.2–1.5 gives softer center and punchy ends.- Optional:
--invert-x,--invert-zto flip axes if your build feels backward.
Typical workflow
- Get neutral correct -> raise deadzone until idle is stable.
- Set overall feel -> tweak x/z-scale for sensitivity.
- Refine center -> adjust expo for smoother micro-movements.
- Invert if needed.
micro:bit motor mix (WuKong):
# After parsing throttle, steer in [-1.0, 1.0]
left = clamp((throttle + steer) * 100, -100, 100)
right = clamp((throttle - steer) * 100, -100, 100)
# Map to WuKong motor speed %MakeCode tip: readLine(), check it ends with “:”, then split(",") and convert to numbers.
Timing
- Streams at 10 Hz (
TICK_SEC = 0.10) - Sends latest filtered values on each tick
- ✅ Final result — smooth analog control (if everything is done correctly): Watch on YouTube
- 🧪 Early prototype — discrete controls (kid test, pre-smooth controller): Watch on YouTube
- Mario not found -> press Mario’s Bluetooth button to (re)advertise.
- micro:bit not writable -> ensure UART (NUS) characteristic is used and not busy.
- Kart creeps at rest -> increase
--deadzoneor hold Mario steadier during startup. - Laggy feel -> lower tick interval or reduce extra filtering in MakeCode side.
MIT License – free to use, share, and modify.





