micro:bit - Radio Packets

Introduction

The main purpose of micro:bit's Radio functionality is to share data between micro:bits. Data has to be broken up into small chunks of data known as data packets in order to be transmitted over the air. The data is then re-built once it reaches the destination micro:bit.

What I discovered was that MakeCode had created a packet specification on on top of the default Radio packet specified in the Device Abstraction Layer (DAL). Protocols are used to control how data is transmitted across networks.

The advantage of this extra specification is that it allows the identification of what type of data is being sent. i.e. integer, string or, variable name and value pair.
The disadvantage of this additional packet specification is that some additional formatting of the data packet is required if we want MakeCode to correctly interpret the data we are sending.

Research Makecode Specification

After asking in the micro:bit community Slack channel I was pointed at two great sources of information about the packet structure:

The GitHub account for Microsoft had comments in the code documenting the packet structure https://github.com/Microsoft/pxt-microbit/blob/master/libs/radio/radio.cpp

// Packet Spec:
// | 0              | 1 ... 4       | 5 ... 8           | 9 ... 28
// ----------------------------------------------------------------
// | packet type    | system time   | serial number     | payload
//
// Serial number defaults to 0 unless enabled by user

// payload: number (9 ... 12)
#define PACKET_TYPE_NUMBER 0

// payload: number (9 ... 12), name length (13), name (14 ... 26)
#define PACKET_TYPE_VALUE 1

// payload: string length (9), string (10 ... 28)
#define PACKET_TYPE_STRING 2

// payload: buffer length (9), buffer (10 ... 28)
#define PACKET_TYPE_BUFFER 3

A micro:support answer containing some more information on the packet structures https://support.microbit.org/support/solutions/articles/19000053168-receiving-radio-data-from-pxt-within-python

    01 00 01 | 02 | 1E 01 01 00 | 00 00 00 00 | 05 44 41 4C 45 4B

    --- DAL HEADER
    01                      raw payload
    00                      group number
    01                      version 1

    -- PXT HEADER
    02                      type=string
    1E 01 01 00             timestamp
    00 00 00 00             serial number (disabled)

    -- PXT DATA
    05                      length of string
    44 41 4C 45 4B          DALEK

Bits, Bytes and Octets

The numbers referenced above (and used elsewhere) in this article, when talking about packets, are shown as the hexadecimal representation of an octet.
Although the term byte is often used to represent 8 bits, there are some systems that use values other than 8 bits for a byte. Hence octet is the clearest term to use when talking about 8 bits.

Octet
Binary Bits11111111
HexadecimalFF
Denary255

The MakeCode Packet Specification Summary

To send compatible packets from MicroPython to the MakeCode over the radio it is required to send the DAL header and MakeCode Packet information. This is summarised below. The empty squares represent octets containing user/program specific data. Squares with octets in them already represent fixed values for that entry type

MakeCode Packet TypeDAL HeaderMakeCode Packet Specification
raw payloadgroup numberversionpacket typesystem timeserial numberpayload
Number Packet01010000000000
Value Packet01010100000000
String Packet01010200000000
Buffer Packet01010300000000

Now to look at the payloads of the different MakeCode Packet types.

Number Packet

This packet allow the sending of a two's-complement signed 32-bit integer.

Packet TypePayload
Number Packetnumber
00

Value Packet

This packet allows you to send a two's complement signed 32-bit integer along with the variable name. This key/value pair enables you to specify what the number being represents. e.g. temperature or humidity
The name is a string can be upto 13 characters long. The length of the string has to be specified in all cases.

Packet TypePayload
Value Packetnumbername lengthname
01

String Packet

This packet type allows the sending of string up to 19 characters. The length of the string has to be specified.

Packet TypePayload
Number Packetstring lengthstring
02

Buffer Packet

Allows up to 19 raw bytes to be sent. The length of the raw buffer needs to be specified.

Packet TypePayload
Buffer Packetbuffer lengthbuffer
03

Implementation

I have made a start on the implementation as I needed a solution for a project I was working on. However longer term this should probably be implemented within the MicroPython radio module.
My code so far only implements the sending of the first three packet types. It does not implement decoding of any received packets from MakeCode .
The implementation always uses the serial number of 00000000 as recommended to protect the privacy of those using the micro:bit.

class MakeRadio:
    def __init__(self, group_id):
        radio.config(group=group_id)
        radio.on()
        self.dal_header = b'\x01' + group_id.to_bytes(1) + b'\x01'

    def send_number(self, msg):
        packet_type = int('0').to_bytes(1)
        time_stamp = running_time().to_bytes(4)
        serial_num = int('0').to_bytes(4)
        msg_bytes = msg.to_bytes(4)
        raw_bytes = (self.dal_header +
                     packet_type +
                     time_stamp +
                     serial_num +
                     msg_bytes)
        radio.send_bytes(raw_bytes)

    def send_value(self, name, value):
        packet_type = int('1').to_bytes(1)
        time_stamp = running_time().to_bytes(4)
        serial_num = int('0').to_bytes(4)
        number = int(value).to_bytes(4)
        name_bytes = bytes(str(name), 'utf8')
        name_length = len(name_bytes).to_bytes(1)
        raw_bytes = (self.dal_header +
                     packet_type +
                     time_stamp +
                     serial_num +
                     number +
                     name_length +
                     name_bytes)
        radio.send_bytes(raw_bytes)

    def send_string(self, msg):
        packet_type = int('2').to_bytes(1)
        time_stamp = running_time().to_bytes(4)
        serial_num = int('0').to_bytes(4)
        msg_bytes = bytes(str(msg), 'utf8')
        msg_length = len(msg_bytes).to_bytes(1)
        raw_bytes = (self.dal_header +
                     packet_type +
                     time_stamp +
                     serial_num +
                     msg_length +
                     msg_bytes)
        radio.send_bytes(raw_bytes)

Conclusion

This implementation has been enough for me to be able to progress my project forward.

I have documented this here for my own reference when I need to come back to the topic later. It is also here should anyone else wish to take this forward and do a more complete implementation

License