pip install https://github.com/ApfelPresse/scratchlink-fakehub.git
python3 main.pyNote: Scratch must be open and Scratch Link must be accessible on port 20111.
-
Start Scratch (Desktop or Browser).
-
Add Extension → micro:bit or LEGO® Education WeDo 2.0.
-
The device list should show an entry from this hub (e.g. “Fake-Device”). Connect.
-
Try blocks (buttons/gestures/tilt/pins for micro:bit; sensors for WeDo).
|
Tip
|
Every JSON-RPC message sent/received is logged (depending on your hub setup), which is very handy for debugging and reverse engineering. |
The fake micro:bit exposes a session stream on Service 0xF005 (61445)
with these characteristics:
-
RX (notify):
5261da01-fa7e-42ab-850b-7c80220097cc— periodic snapshot -
TX (write):
5261da02-fa7e-42ab-850b-7c80220097cc— display commands
Minimal example — starts the hub and briefly triggers button A:
import asyncio, logging
from scratchlink_fakehub.device import FakeDevice
from scratchlink_fakehub.hub import ScratchLinkHub
from scratchlink_fakehub.microbit import MicrobitDevice
logging.basicConfig(level=logging.DEBUG, format="[%(asctime)s] [%(levelname)s] %(message)s")
async def sensor_loop(mb: MicrobitDevice):
await asyncio.sleep(2) # give Scratch time to connect
await mb.press_a(); await asyncio.sleep(1); await mb.release_a()
async def main():
link = FakeDevice()
mb = MicrobitDevice()
hub = ScratchLinkHub(link, devices=[mb])
await asyncio.gather(hub.start(), sensor_loop(mb))
asyncio.run(main())10-byte frame (tilt is little-endian int16):
| Byte | Meaning |
|---|---|
0–1 |
|
2–3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
|
Note
|
For When pin X connected, set the corresponding pin byte (6–8) from 0→1 and keep it for ~0.3–0.5 s, then set it back to 0. |
This implementation uses the following values (tested with Scratch):
| Gesture | Code |
|---|---|
moved |
4 |
shaken |
2 |
jumped |
1 |
none |
0 |
The TX characteristic accepts simple display commands:
| Opcode | Meaning |
|---|---|
|
display text — UTF‑8 text |
|
display 5×5 — 5 bytes bitmap; all zeros ⇒ clear display |
|
set pixel — 3 bytes: |
Buttons
await mb.press_a(); await mb.release_a()
await mb.press_b(); await mb.release_b()Gestures
await mb.moved()
await mb.shaken()
await mb.jumped()
await mb.clear_gesture()Tilt
await mb.tilt_front(); await mb.tilt_back()
await mb.tilt_left(); await mb.tilt_right()
await mb.tilt_any(); await mb.reset_tilt()Pins
await mb.pin_connected(0, True) # connect P0
await mb.pin_connected(0, False) # resetDisplay callbacks (default: logging; override if you want a UI)
async def on_display_text(self, text: str): ...
async def on_display_matrix(self, rows: Sequence[int]): ...
async def on_clear_display(self): ...
async def on_set_pixel(self, x: int, y: int, on: bool): ...Currently emulates:
-
Port 1: Distance Sensor (Input)
-
Port 2: Tilt Sensor (Input)
Ports and device types are configurable.
Based on reverse-engineering documentation at https://ofalcao.pt/blog/series/wedo-2-0-reverse-engineering
|
00001523-1212-efde-1523-785feabcd123 |
|
00001527-1212-efde-1523-785feabcd123 |
|
00001528-1212-efde-1523-785feabcd123 |
|
00004f0e-1212-efde-1523-785feabcd123 |
|
00001560-1212-efde-1523-785feabcd123 |
|
00001565-1212-efde-1523-785feabcd123 |
These UUIDs define how the simulated device exposes characteristics for motor, LED, sensor values, and notification control.
Scratch only detects new values if an intermediate dummy value (e.g., 0) is sent before the target value.
await send(0) # Dummy impulse
await send(30) # Actual valueThis is required due to how Scratch deduplicates incoming BLE notifications.
Example JSON-RPC message:
{
"jsonrpc": "2.0",
"method": "characteristicDidChange",
"params": {
"serviceId": "00001523-1212-efde-1523-785feabcd123",
"characteristicId": "00001527-1212-efde-1523-785feabcd123",
"encoding": "base64",
"message": "BQEU"
}
}Decoded message (base64.b64decode("BQEU")) → [0x05, 0x01, 0x14]
-
0x05: Notification prefix (fixed) -
0x01: Port 1 -
0x14: Decimal 20 (value)
For tilt: [0x05, port, x, y] → each axis ranges from 0 to 255
This announces a device (e.g., distance sensor) connected to a port.
bytes([
port, 0x01, 0x00, DEVICE_TYPES[device],
0x00, 0x01, 0x01, 0x10,
0x00, 0x00, 0x00, 0x10
])Explanation:
-
port: Port index (1–2) -
0x01, 0x00: Hub capabilities (fixed) -
DEVICE_TYPES[device]: 0x23 (distance), 0x22 (tilt), 0x01 (motor) -
Remaining: likely mode & unit setup. Reverse-engineered but partially undocumented.
device.set_distance(50)
await asyncio.sleep(0.05)
#device.set_distance(0) # Reset, this happens automatically in the python class
await asyncio.sleep(0.05)
device.set_distance(50) # Trigger againThis ensures Scratch receives the updated distance.
The following methods define simulated tilt directions:
def tilt_up(self): self.set_tilt(0, 60)
def tilt_down(self): self.set_tilt(0, 30)
def tilt_left(self): self.set_tilt(60, 0)
def tilt_right(self): self.set_tilt(30, 0)Thresholds for x/y axis were determined empirically. Scratch blocks trigger based on crossing value thresholds. Scratch ignores gradual or unchanged values.
@startuml
title WeDo 2.0 ScratchLink Protocol
Scratch -> ScratchLink: discover
ScratchLink -> FakeHub: discover
FakeHub -> ScratchLink: didDiscoverPeripheral
Scratch -> ScratchLink: connect
ScratchLink -> FakeHub: connect
Scratch -> ScratchLink: startNotifications
ScratchLink -> FakeHub: startNotifications
FakeHub -> ScratchLink: characteristicDidChange (attach)
loop Every interval
FakeHub -> ScratchLink: characteristicDidChange (sensor value)
end
Scratch -> ScratchLink: stopNotifications
ScratchLink -> FakeHub: stopNotifications
@enduml