Using the Relay

Flash it, open the port, configure it, and push bytes over the radio — from bash, Python, or a serial terminal.

Using the Relay

This page is the practical guide: how to get firmware onto a board and how to drive it from a host. For the exhaustive grammar see the Protocol Reference.

1. Flash the firmware

Build produces a MICROBIT.hex. Flash it by copying it to the mounted MICROBIT USB drive. Use cat, not cp — the drag-and-drop copy that cp performs can race the board’s mass-storage flasher and fail silently:

cat MICROBIT.hex > /Volumes/MICROBIT/      # macOS
# cat MICROBIT.hex > /media/$USER/MICROBIT/   # Linux

When two boards form a RAW250 link, both must run firmware built with the same MICROBIT_RADIO_MAX_PACKET_SIZE (250 here). A mismatch silently drops the larger packets on receive.

2. Open the port (and the reset gotcha)

The relay runs at 115200 baud. Opening the host serial port toggles DTR, which resets the board and drops it back into the command plane (config is preserved). An in-place DTR toggle on an already-open port does not reset — the host must fully close and reopen.

Because the boot banner is emitted during the window the port is still being opened, it is easily missed. The reliable pattern is: open the port, send HELLO, then read the banner from the reply:

DEVICE:RADIOBRIDGE:relay:<deviceName>:<serialNumber>

3. Command quick-start

Everything below is the command plane — line-oriented, \n-terminated, valid only before !GO:

HELLO            # re-request the banner if you missed it
?                # read back: # channel: <ch> group: <g> mode: <m> power: <p>
!C 5             # channel 5 (forces group 10)
!MODE RAW250     # (default) headerless ≤250-byte framing
!ECHO ON         # optional: make this board a transponder
!GO              # enter the transparent data plane
<...bytes...>    # everything after !GO is radio payload, both directions
# close + reopen the port to return to the command plane

Lines you receive from the relay are prefixed so a parser can classify them:

Prefix Meaning
< a message received over the radio
# comment / status / query reply from relay

Common commands (full table in the Protocol Reference):

Command What it does
!C <ch> channel 0–35, forces group 10
!CG <ch> <group> channel 0–83 and group 0–255 (!RC is an alias)
!P <0-7> transmit power
!MODE RAW250 headerless ≤250-byte framing (default)
!MODE MAKECODE 32-byte CODAL packets for a stock MakeCode robot
!ECHO [ON\|OFF] transponder: bounce every received message back (bare = toggle)
!DEFAULTS clear saved config; compiled-in defaults on next reset
!GO leave command plane, enter data plane (exit only by reset)
? / !MODE? query config / query mode

Config changes apply immediately, persist to flash, and are echoed back as a # comment.

4. Driving it without a host

The buttons work in either plane, no host needed:

  • A = channel down, B = channel up (wrapping 0–35, only when on group 10).
  • A+B opens a menu on the 5×5 display. Each press advances; resting on an item for 3 seconds accepts it. Items toggle packet mode (32/25), toggle echo (ghost / west-arrow icon), or cancel (X).

The resting display shows the channel glyph (09, then AZ for 10–35, ? for a custom group) — or a ghost icon when echo mode is on.

5. Worked examples

bash — send one line, command plane

# stty configures the port; then write a command-plane send
stty -f /dev/tty.usbmodem* 115200 raw      # macOS (use -F on Linux)
printf '> hello over the radio\n' > /dev/tty.usbmodem*

bash — transparent data plane

PORT=/dev/tty.usbmodem*
stty -f $PORT 115200 raw
# read incoming lines in the background
cat $PORT &
# configure, then go transparent
printf '!MODE RAW250\n!GO\n' > $PORT
# from here, every byte is radio payload
printf 'streaming bytes with no prefix\n' > $PORT

Python (pyserial)

import serial, time

port = serial.Serial("/dev/tty.usbmodem0001", 115200, timeout=1)
time.sleep(0.3)                 # let the DTR reset settle

port.write(b"HELLO\n")          # ask for the banner
print(port.readline())          # b'DEVICE:RADIOBRIDGE:relay:...\r\n'

port.write(b"!C 5\n")           # channel 5
port.write(b"?\n")              # read back config
print(port.readline())          # b'# channel: 5 group: 10 ...'

port.write(b"!GO\n")            # enter the data plane
port.write(b"payload bytes\n")  # now transparent radio payload

A two-board end-to-end harness (discovery, reset, messaging, channel isolation, throughput) lives at scripts/relay_test.py; the standalone-peer echo/MAKECODE checks are the other scripts/*_test.py files.

6. Talking to a stock MakeCode micro:bit

Switch to MAKECODE mode and send text lines; a stock robot fires on received string:

!MODE MAKECODE   # 32-byte CODAL string packets
!CG 0 10         # match the robot's channel/group (MakeCode default group 1 → set as needed)
!GO
hello robot      # a \n cuts each packet; lines >19 bytes truncate (or !FRAG ON to fragment)

Note MAKECODE caps a single string at 19 bytes. Longer lines truncate by default; !FRAG ON fragments them, but only another board running this firmware can reassemble — a stock robot cannot. See the Protocol Reference §4–5 for the framing detail.