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.
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
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 Bits | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Hexadecimal | F | F | ||||||
Denary | 255 |
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 Type | DAL Header | MakeCode Packet Specification | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
raw payload | group number | version | packet type | system time | serial number | payload | ||||||||||||||||||||||||||
Number Packet | 01 | 01 | 00 | 00 | 00 | 00 | 00 | |||||||||||||||||||||||||
Value Packet | 01 | 01 | 01 | 00 | 00 | 00 | 00 | |||||||||||||||||||||||||
String Packet | 01 | 01 | 02 | 00 | 00 | 00 | 00 | |||||||||||||||||||||||||
Buffer Packet | 01 | 01 | 03 | 00 | 00 | 00 | 00 |
Now to look at the payloads of the different MakeCode Packet types.
This packet allow the sending of a two's-complement signed 32-bit integer.
Packet Type | Payload | |||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Number Packet | number | |||||||||||||||||||
00 |
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 Type | Payload | |||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Value Packet | number | name length | name | |||||||||||||||||
01 |
This packet type allows the sending of string up to 19 characters. The length of the string has to be specified.
Packet Type | Payload | |||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Number Packet | string length | string | ||||||||||||||||||
02 |
Allows up to 19 raw bytes to be sent. The length of the raw buffer needs to be specified.
Packet Type | Payload | |||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Buffer Packet | buffer length | buffer | ||||||||||||||||||
03 |
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)
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
Thanks to @rhubarbdog who has taken this further and published a more complete solution at: https://github.com/rhubarbdog/microbit-radio