Python bridges, protocol notes, and setup guides for the TurboWarp/Scratch extensions that talk to LEGO bricks (NXT, EV3, Boost, Spike Prime, WeDo 2.0, Powered UP) over Bluetooth Classic, BLE, ScratchLink, Web Serial, or a local Python WebSocket bridge — whichever the platform/firmware actually allows.
The extension .js files themselves live in
CrispStrobe/extensions.
They used to be mirrored here but were collapsed on 2026-05-02 to stop
drift; this repo now holds the on-device Python bridges and protocol
docs only. The four transpilers’ Phase 2 + Phase 4 audit fixes live on
the wip-pre-collapse
branch — hardware-validated for ev3dev as of 2026-05-05 (44/44
smoke-test cases pass on a real brick); Spike/NXT/LMS hardware checks
deferred. See LEARNINGS.md for the full picture.
Heads up: Most of these extensions need Sandbox Mode disabled in TurboWarp. Click the menu next to a loaded extension and toggle off “Run in sandbox” before connecting to hardware.
Pick your brick + platform, then jump straight to the matching guide.
| Your brick | Your editor / platform | What you need | Guide |
|---|---|---|---|
| EV3 with ev3dev firmware | Any browser on any OS | Bridge running on the brick | § EV3dev quick start ↓ |
| EV3 with original firmware | macOS/Win Chrome/Edge | LEGO ScratchLink (BTC) | § EV3 (LEGO firmware) ↓ |
| EV3 with original firmware | iOS / Android | Native shell (brickwright-ios, Scrub) | brickwright-ios / Scrub |
| NXT | macOS/Win + browser | Pair via OS, run nxt_bridge.py |
§ NXT setup ↓ |
| NXT | TurboWarp Desktop on Mac/Win | Web Serial via legonxt_transpile_universal.js |
§ NXT setup ↓ |
| Spike Prime FW 2.x | macOS/Win Chrome | LEGO ScratchLink (BTC) | extensions gallery — legospike*.js |
| Spike Prime FW 3.x | macOS/Win Chrome | Web Bluetooth (no bridge) | gallery — legospike*.js |
| Boost / Powered UP / Technic Hub | macOS/Win Chrome | Web Bluetooth (no bridge) | gallery — legoboost_universal.js, lego_poweredup.js |
| WeDo 2.0 | macOS/Win Chrome | Web Bluetooth (no bridge) | gallery — lego_wedo2_universal.js |
| Spike Prime / EV3 / NXT on iOS/iPad | brickwright-ios or Scrub | Native shell + (for HTTPS) cert install | brickwright-ios — see iOS notes ↓ |
The TurboWarp editor is at scratch-gui-three.vercel.app/editor.html
(fork of TurboWarp that loads CrispStrobe/extensions). On macOS/Windows
desktops you can use TurboWarp Desktop
instead.
You have an EV3 brick flashed with ev3dev, it’s
on Wi-Fi, and it has an IP like 192.168.178.57.
# 1. From your laptop: copy the bridge to the brick (default password: maker)
scp ev3dev_ondevice.py robot@ev3dev.local:/home/robot/
# 2. SSH in and run it. UTF-8 in the env avoids a locale crash; -u keeps logs flushed.
ssh robot@ev3dev.local 'PYTHONIOENCODING=utf-8 python3 -u /home/robot/ev3dev_ondevice.py --verbose'
That serves both HTTP on :8080 and HTTPS on :8443. From any device
on the same Wi-Fi:
# Sanity check (HTTP — works in every browser without any cert dance)
curl http://<brick-ip>:8080/status
Open http://<brick-ip>:8080/test.html in any browser to get the
interactive test panel.
For Safari, iOS/iPad, or any HTTPS-only frontend (e.g. the Vercel-hosted
editor) — the brick’s TLS cert is self-signed, so you have to install it
once. The ten-line copy-paste recipe per platform lives in
README_ev3dev_bridge.md — it has been
tested end-to-end on macOS Safari, Firefox, and iPadOS and will get you
from “cert error” to “green padlock” without surprises. It also documents
the three latent bugs in the bridge that need a --verbose start to
even surface, the regenerate-cert-when-the-IP-changes recipe, and the
“why does it work in Firefox but not Safari” answer.
If you’re impatient: the bridge currently identifies as v2.3.1 in
/status. If yours says 2.3.0, you’re running stock and will hit the
locale crash on a fresh start. Update from this repo and restart with
PYTHONIOENCODING=utf-8.
| Repo | What it does |
|---|---|
CrispStrobe/extensions |
Extension gallery — the .js files for the editor. The maintained EV3/NXT/Spike code lives here. |
CrispStrobe/scratch-gui |
TurboWarp editor fork that loads the gallery above |
CrispStrobe/brickwright-desktop |
Electron build of the editor for macOS / Windows / Linux |
CrispStrobe/brickwright-android |
Android wrapper with native Bluetooth bridges |
CrispStrobe/brickwright-ios |
iOS / iPadOS wrapper with native Bluetooth bridges |
CrispStrobe/legacy-lego-compiler |
Hosted REST API that compiles NXC → .rxe and lmsasm → EV3 bytecode (used by the transpile extensions) |
CrispStrobe/scratch-lego-bluetooth-extensions |
Older Xcratch-style .mjs build of the LEGO extensions (feature-frozen) |
| Category | File | Notes |
|---|---|---|
| EV3 (ev3dev firmware) | ev3dev_ondevice.py |
HTTP/HTTPS JSON bridge that runs on the brick. Currently v2.3.1; see README_ev3dev_bridge.md. |
ev3_local_bridge.py |
Local-host HTTP bridge variant — see README_ev3_local_bridge.md |
|
| EV3 (original firmware) | ev3-compiler-service/ |
Flask app + bundled lmsasm-binary used by the gallery’s ev3_lms_transpile.js for server-side EV3-G compilation |
| NXT | nxt_bridge.py |
Local WebSocket bridge for NXT control over RFCOMM (see § NXT setup below) |
nxt-pybluez-bridge.py |
Experimental PyBluez-based alternative bridge | |
nxt-diag.py, test_bt.py, reset_nxt.sh |
Diagnostic / pairing helpers | |
| Generic Python bridges | lego_bridge.py, lego_bridge_unified.py, universal_bridge.py, universal_lego_bridge.py |
Older / experimental WebSocket bridges. See README_bridges.md |
| Audit notes | PLAN.md, LEARNINGS.md |
Phase-by-phase audit log of the four transpilers + bridge bugs. Skim if you want to know why something is the way it is. |
| Hub | BTC (ScratchLink) | BLE (Web BT / native) | Custom WebSocket / HTTP bridge | Notes |
|---|---|---|---|---|
| EV3 (orig FW) | yes | — | yes | also direct HTTP via ev3_universal.js |
| EV3 (ev3dev) | — | — | yes (HTTP/HTTPS JSON) | ev3dev_ondevice.py runs on the brick |
| NXT | yes | — | yes (nxt_bridge.py) |
RFCOMM, drops on macOS — see below |
| Spike Prime / Robot Inventor (FW 2.x) | yes | — | yes | BTC |
| Spike Prime (FW 3.x) | — | yes | — | BLE |
| Boost | — | yes | — | BLE |
| Powered UP / Technic Hub | — | yes | — | BLE |
| WeDo 2.0 | — | yes | — | BLE |
Platform caveats:
brickwright-ios which has native BLE/BTC bridges built in.ev3_lms_transpile.js lets you either stream direct commands (works while
the editor stays connected) or transpile the project to lmsasm and compile
it to EV3 bytecode you can copy onto the brick. Compilation goes through the
hosted REST API at https://lego-compiler.vercel.app/ — source in
CrispStrobe/legacy-lego-compiler.
Internet connectivity is required for compile. Use with caution if your
project has destructive blocks.
ev3_direct.js is streaming-only. ev3_universal.js bundles ScratchLink,
Web Serial, WebSocket, and direct-HTTP backends in one extension and lets
you pick at runtime.
ev3dev replaces the LEGO firmware with a Debian
Linux. With it on the brick, ev3dev_py_transpile.js lets you either:
{"cmd":"beep","freq":1000,"dur":500}; orThe bridge is ev3dev_ondevice.py, running on the
brick under stock ev3dev (Python 3.5.3). Full setup, cert install, and
troubleshooting in README_ev3dev_bridge.md.
The NXT brick speaks SPP-over-Bluetooth (an old profile that browsers can’t reach directly). Two viable paths:
nxt_bridge.py (a local WebSocket bridge) →
editor connects to ws://localhost:8080. Recommended.legonxt_transpile_universal.js →
connect to /dev/cu.NXT (macOS) or the COM port (Windows). Experimental.pip install websockets pyserialThe macOS RFCOMM channel drops frequently. Workflow:
blueutil --disconnect 00-16-53-XX-XX-XX
blueutil --unpair 00-16-53-XX-XX-XX
# turn the NXT off and back on
blueutil --pair 00-16-53-XX-XX-XX # PIN: 1234
python3 nxt-diag.py # should beep + print battery
If commands stop working after inactivity, run ./reset_nxt.sh
to re-pair. The port to use in the direct extension is /dev/cu.NXT — not
/dev/cu.Bluetooth-Incoming-Port.
Settings → Bluetooth & devices → Devices → set discovery to Advanced, then
pair with PIN 1234. Or in a terminal:
btpair -u
Note the COM port and use it in the extension.
The NXT shows up as /dev/rfcomm0 after pairing.
| Symptom | Cause | Fix |
|---|---|---|
No beep, no battery from nxt-diag.py |
RFCOMM channel never came up | unpair + repair (steps above) |
| Commands sent, nothing happens | RFCOMM channel dropped after idle | run ./reset_nxt.sh; wait ~10 s after pairing |
| Display patterns work but text/lines don’t | Forgot the update display block | NXT display is double-buffered — call update after batched draws |
ModuleNotFoundError: No module named 'serial' |
missing dependency | pip install pyserial |
| “Port not found” | wrong port name | macOS /dev/cu.NXT; Windows: check Device Manager; Linux: /dev/rfcomm0 |
The display is slow — each full updateDisplay() is 1–3 s. Batch all draws
then update once:
clearScreen()
drawText('Line 1', 0, 0)
drawText('Line 2', 0, 10)
drawRect(0, 0, 100, 64, false)
updateDisplay() # one 1–3 s call
Telegram structure: [u16 length LE] [cmd type] [opcode] [payload...]
| Byte | Meaning |
|---|---|
0x00 |
direct command (with reply) |
0x80 |
direct command (no reply) |
0x01 |
system command (with reply) |
0x02 |
reply telegram |
Frequently-used opcodes: 0x03 PLAY_TONE · 0x04 SET_OUT_STATE · 0x05
SET_IN_MODE · 0x07 GET_IN_VALS · 0x0B GET_BATT_LVL · 0x0F/0x10
LS_WRITE/LS_READ (I²C) · 0x94/0x95 READ_IO_MAP/WRITE_IO_MAP (display).
Display memory: 100×64 mono, module ID 0xA0001, offset 119, 800 bytes (8
vertical pixels per byte, LSB top).
iOS Safari (mobile + iPad) and any WebKit-based shell (brickwright-ios, Scrub, in-app webviews) all share the same TLS trust store. So one cert install on the device makes all of them work simultaneously.
The full step-by-step is in README_ev3dev_bridge.md § Certificate
installation. Three steps:
https://<brick-ip>:8443/profile → tap through warning →
download the configuration profile.Step 3 is the one almost everyone misses. Without it the cert is installed but iOS does not trust it for SSL.
If you fork brickwright-ios
and intend to submit to the App Store, don’t use
NSAllowsArbitraryLoads = true — Apple will scrutinize that and may reject.
Use the purpose-built local-network exception instead, in Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>Connects to your LEGO brick on this Wi-Fi network.</string>
NSAllowsLocalNetworking was added in iOS 10 specifically for this case.
It permits TLS-or-HTTP connections to RFC 1918 private ranges
(192.168.*.*, 10.*, 172.16-31.*) and .local mDNS hosts without
disabling ATS for the rest of the internet. The
NSLocalNetworkUsageDescription purpose string is required by iOS 14+ for
local-network access — Apple shows it to the user on first connect.
For sideloaded debug builds (Xcode → Run on device) either setting works, since App Review never sees the build.
Per-extension licenses live at the top of each .js (in the gallery repo).
Most files are GPL-3.0; some are MPL-2.0. The Python bridges in this repo
are GPL-3.0.