From 61aae94d8be9f8f923ae28a6c15db73c6a0a9d71 Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Fri, 12 Apr 2019 13:10:47 -0700 Subject: [PATCH] Rewriting the radio in TypeScript (#2000) * Refactoring radio into the ts * Moving the rest of the radio functionality to the TypeScript * Removing sim implementation of old radio cpp * Adding test script * Removing handler key * Rename internal functions * PR feedback * Refactoring to use event bus --- libs/radio/_locales/radio-jsdoc-strings.json | 38 +- libs/radio/deprecated.ts | 188 ++++++++ libs/radio/pxt.json | 3 +- libs/radio/radio.cpp | 434 +---------------- libs/radio/radio.ts | 480 +++++++++++++------ libs/radio/shims.d.ts | 130 +---- libs/radio/test.ts | 349 ++++++++++++++ sim/state/radio.ts | 116 +---- 8 files changed, 926 insertions(+), 812 deletions(-) create mode 100644 libs/radio/deprecated.ts create mode 100644 libs/radio/test.ts diff --git a/libs/radio/_locales/radio-jsdoc-strings.json b/libs/radio/_locales/radio-jsdoc-strings.json index 0543912f..d735a9de 100644 --- a/libs/radio/_locales/radio-jsdoc-strings.json +++ b/libs/radio/_locales/radio-jsdoc-strings.json @@ -1,39 +1,41 @@ { "radio": "Communicate data using radio packets", - "radio.Packet.receivedBuffer": "The buffer payload if a buffer was sent in this packet\nor the empty buffer", - "radio.Packet.receivedNumber": "The number payload if a number was sent in this packet (via ``sendNumber()`` or ``sendValue()``)\nor 0 if this packet did not contain a number.", - "radio.Packet.receivedString": "The string payload if a string was sent in this packet (via ``sendString()`` or ``sendValue()``)\nor the empty string if this packet did not contain a string.", + "radio.Packet.receivedBuffer": "The buffer payload if a buffer was sent in this packet\r\nor the empty buffer", + "radio.Packet.receivedNumber": "The number payload if a number was sent in this packet (via ``sendNumber()`` or ``sendValue()``)\r\nor 0 if this packet did not contain a number.", + "radio.Packet.receivedString": "The string payload if a string was sent in this packet (via ``sendString()`` or ``sendValue()``)\r\nor the empty string if this packet did not contain a string.", "radio.Packet.serial": "The serial number of the sender of the packet or 0 if the sender did not sent their serial number.", "radio.Packet.signal": "The received signal strength indicator (RSSI) of the packet.", "radio.Packet.time": "The system time of the sender of the packet at the time the packet was sent.", "radio._packetProperty": "Gets a packet property.", "radio._packetProperty|param|type": "the packet property type, eg: PacketProperty.time", - "radio.onDataPacketReceived": "Registers code to run when the radio receives a packet. Also takes the\nreceived packet from the radio queue.", + "radio.onDataPacketReceived": "Deprecated. Use onDataReceived() instead\r\nRegisters code to run when the radio receives a packet. Also takes the\r\nreceived packet from the radio queue.", "radio.onDataReceived": "Registers code to run when a packet is received over radio.", "radio.onReceivedBuffer": "Registers code to run when the radio receives a buffer.", - "radio.onReceivedBufferDeprecated": "Registers code to run when the radio receives a buffer. Deprecated, use\nonReceivedBuffer instead.", + "radio.onReceivedBufferDeprecated": "Registers code to run when the radio receives a buffer. Deprecated, use\r\nonReceivedBuffer instead.", "radio.onReceivedNumber": "Registers code to run when the radio receives a number.", - "radio.onReceivedNumberDeprecated": "Registers code to run when the radio receives a number. Deprecated, use\nonReceivedNumber instead.", + "radio.onReceivedNumberDeprecated": "Registers code to run when the radio receives a number. Deprecated, use\r\nonReceivedNumber instead.", "radio.onReceivedString": "Registers code to run when the radio receives a string.", - "radio.onReceivedStringDeprecated": "Registers code to run when the radio receives a string. Deprecated, use\nonReceivedString instead.", + "radio.onReceivedStringDeprecated": "Registers code to run when the radio receives a string. Deprecated, use\r\nonReceivedString instead.", "radio.onReceivedValue": "Registers code to run when the radio receives a key value pair.", - "radio.onReceivedValueDeprecated": "Registers code to run when the radio receives a key value pair. Deprecated, use\nonReceivedValue instead.", + "radio.onReceivedValueDeprecated": "Registers code to run when the radio receives a key value pair. Deprecated, use\r\nonReceivedValue instead.", "radio.raiseEvent": "Sends an event over radio to neigboring devices", - "radio.receiveNumber": "Reads the next packet from the radio queue and returns the packet's number\npayload or 0 if the packet did not contain a number.", - "radio.receiveString": "Reads the next packet from the radio queue and returns the packet's string\npayload or the empty string if the packet did not contain a string.", - "radio.receivedBuffer": "Returns the buffer payload from the last packet taken from the radio queue\n(via ``receiveNumber``, ``receiveString``, etc) or the empty string if that\npacket did not contain a string.", - "radio.receivedNumber": "Returns the number payload from the last packet taken from the radio queue\n(via ``receiveNumber``, ``receiveString``, etc) or 0 if that packet did not\ncontain a number.", + "radio.readRawPacket": "Takes the next packet from the radio queue and returns its contents in a Buffer", + "radio.receiveNumber": "Reads the next packet from the radio queue and returns the packet's number\r\npayload or 0 if the packet did not contain a number.", + "radio.receiveString": "Reads the next packet from the radio queue and returns the packet's string\r\npayload or the empty string if the packet did not contain a string.", + "radio.receivedBuffer": "Returns the buffer payload from the last packet taken from the radio queue\r\n(via ``receiveNumber``, ``receiveString``, etc) or the empty string if that\r\npacket did not contain a string.", + "radio.receivedNumber": "Returns the number payload from the last packet taken from the radio queue\r\n(via ``receiveNumber``, ``receiveString``, etc) or 0 if that packet did not\r\ncontain a number.", "radio.receivedPacket": "Returns properties of the last radio packet received.", "radio.receivedPacket|param|type": "the type of property to retrieve from the last packet", - "radio.receivedSerial": "Returns the serial number of the sender micro:bit from the last packet taken\nfrom the radio queue (via ``receiveNumber``, ``receiveString``, etc) or 0 if\nthat packet did not send a serial number.", + "radio.receivedSerial": "Returns the serial number of the sender micro:bit from the last packet taken\r\nfrom the radio queue (via ``receiveNumber``, ``receiveString``, etc) or 0 if\r\nthat packet did not send a serial number.", "radio.receivedSignalStrength": "Gets the received signal strength indicator (RSSI) from the last packet taken\nfrom the radio queue (via ``receiveNumber``, ``receiveString``, etc). Not supported in simulator.\nnamespace=radio", - "radio.receivedString": "Returns the string payload from the last packet taken from the radio queue\n(via ``receiveNumber``, ``receiveString``, etc) or the empty string if that\npacket did not contain a string.", - "radio.receivedTime": "Returns the system time of the sender micro:bit at the moment when it sent the\nlast packet taken from the radio queue (via ``receiveNumber``,\n``receiveString``, etc).", + "radio.receivedString": "Returns the string payload from the last packet taken from the radio queue\r\n(via ``receiveNumber``, ``receiveString``, etc) or the empty string if that\r\npacket did not contain a string.", + "radio.receivedTime": "Returns the system time of the sender micro:bit at the moment when it sent the\r\nlast packet taken from the radio queue (via ``receiveNumber``,\r\n``receiveString``, etc).", "radio.sendBuffer": "Broadcasts a buffer (up to 19 bytes long) along with the device serial number\nand running time to any connected micro:bit in the group.", "radio.sendNumber": "Broadcasts a number over radio to any connected micro:bit in the group.", + "radio.sendRawPacket": "Sends a raw packet through the radio", "radio.sendString": "Broadcasts a string along with the device serial number\nand running time to any connected micro:bit in the group.", - "radio.sendValue": "Broadcasts a name / value pair along with the device serial number\nand running time to any connected micro:bit in the group.", - "radio.sendValue|param|name": "the field name (max 12 characters), eg: \"name\"", + "radio.sendValue": "Broadcasts a name / value pair along with the device serial number\nand running time to any connected micro:bit in the group. The name can\ninclude no more than 8 characters.", + "radio.sendValue|param|name": "the field name (max 8 characters), eg: \"name\"", "radio.sendValue|param|value": "the numeric value", "radio.setGroup": "Sets the group id for radio communications. A micro:bit can only listen to one group ID at any time.", "radio.setGroup|param|id": "the group id between ``0`` and ``255``, eg: 1", @@ -42,5 +44,5 @@ "radio.setTransmitSerialNumber": "Set the radio to transmit the serial number in each message.", "radio.setTransmitSerialNumber|param|transmit": "value indicating if the serial number is transmitted, eg: true", "radio.writeReceivedPacketToSerial": "Writes the last received packet to serial as JSON. This should be called\nwithin an ``onDataPacketReceived`` callback.", - "radio.writeValueToSerial": "Reads the next packet from the radio queue and and writes it to serial\nas JSON." + "radio.writeValueToSerial": "Reads the next packet from the radio queue and and writes it to serial\r\nas JSON." } \ No newline at end of file diff --git a/libs/radio/deprecated.ts b/libs/radio/deprecated.ts new file mode 100644 index 00000000..3677a650 --- /dev/null +++ b/libs/radio/deprecated.ts @@ -0,0 +1,188 @@ +namespace radio { + export class Packet { + /** + * The number payload if a number was sent in this packet (via ``sendNumber()`` or ``sendValue()``) + * or 0 if this packet did not contain a number. + */ + public receivedNumber: number; + /** + * The string payload if a string was sent in this packet (via ``sendString()`` or ``sendValue()``) + * or the empty string if this packet did not contain a string. + */ + public receivedString: string; + /** + * The buffer payload if a buffer was sent in this packet + * or the empty buffer + */ + public receivedBuffer: Buffer; + /** + * The system time of the sender of the packet at the time the packet was sent. + */ + public time: number; + /** + * The serial number of the sender of the packet or 0 if the sender did not sent their serial number. + */ + public serial: number; + /** + * The received signal strength indicator (RSSI) of the packet. + */ + public signal: number; + } + + /** + * Deprecated. Use onDataReceived() instead + * Registers code to run when the radio receives a packet. Also takes the + * received packet from the radio queue. + */ + //% help=radio/on-data-packet-received blockHandlerKey="radioreceived" deprecated=true + //% mutate=objectdestructuring + //% mutateText=Packet + //% mutateDefaults="receivedNumber;receivedString:name,receivedNumber:value;receivedString" + //% blockId=radio_on_packet block="on radio received" blockGap=8 + export function onDataPacketReceived(cb: (packet: Packet) => void) { + onDataReceived(() => { + receiveNumber(); + const packet = new Packet(); + packet.receivedNumber = receivedNumber(); + packet.time = receivedTime(); + packet.serial = receivedSerial(); + packet.receivedString = receivedString(); + packet.receivedBuffer = receivedBuffer(); + packet.signal = receivedSignalStrength(); + cb(packet) + }); + } + + /** + * Registers code to run when the radio receives a number. Deprecated, use + * onReceivedNumber instead. + */ + //% help=radio/on-received-number blockHandlerKey="radioreceived" + //% blockId=radio_on_number block="on radio received" blockGap=16 + //% useLoc="radio.onDataPacketReceived" deprecated=1 + export function onReceivedNumberDeprecated(cb: (receivedNumber: number) => void) { + onReceivedNumber(cb); + } + + /** + * Registers code to run when the radio receives a key value pair. Deprecated, use + * onReceivedValue instead. + */ + //% help=radio/on-received-value blockHandlerKey="radioreceived" + //% blockId=radio_on_value block="on radio received" blockGap=16 + //% useLoc="radio.onDataPacketReceived" deprecated=1 + export function onReceivedValueDeprecated(cb: (name: string, value: number) => void) { + onReceivedValue(cb); + } + + /** + * Registers code to run when the radio receives a string. Deprecated, use + * onReceivedString instead. + */ + //% help=radio/on-received-string blockHandlerKey="radioreceived" + //% blockId=radio_on_string block="on radio received" blockGap=16 + //% useLoc="radio.onDataPacketReceived" deprecated=1 + export function onReceivedStringDeprecated(cb: (receivedString: string) => void) { + onReceivedString(cb); + } + + /** + * Registers code to run when the radio receives a buffer. Deprecated, use + * onReceivedBuffer instead. + */ + //% help=radio/on-received-buffer blockHandlerKey="radioreceived" blockHidden=1 + //% blockId=radio_on_buffer block="on radio received" blockGap=16 + //% useLoc="radio.onDataPacketReceived" deprecated=1 + export function onReceivedBufferDeprecated(cb: (receivedBuffer: Buffer) => void) { + onReceivedBuffer(cb); + } + + /** + * Reads the next packet from the radio queue and and writes it to serial + * as JSON. + */ + //% help=radio/write-value-to-serial + //% weight=3 + //% blockId=radio_write_value_serial block="radio write value to serial" + //% deprecated=true + export function writeValueToSerial() { + const p = RadioPacket.getPacket(radio.readRawPacket()); + writeToSerial(p); + } + + /** + * Returns the number payload from the last packet taken from the radio queue + * (via ``receiveNumber``, ``receiveString``, etc) or 0 if that packet did not + * contain a number. + */ + //% help=radio/received-number deprecated=1 + export function receivedNumber(): number { + return (lastPacket ? lastPacket.numberPayload : 0) || 0; + } + + /** + * Returns the serial number of the sender micro:bit from the last packet taken + * from the radio queue (via ``receiveNumber``, ``receiveString``, etc) or 0 if + * that packet did not send a serial number. + */ + //% help=radio/received-serial deprecated=1 + export function receivedSerial(): number { + return lastPacket ? lastPacket.serial : 0; + } + + /** + * Returns the string payload from the last packet taken from the radio queue + * (via ``receiveNumber``, ``receiveString``, etc) or the empty string if that + * packet did not contain a string. + */ + //% help=radio/received-string deprecated=1 + export function receivedString(): string { + return (lastPacket ? lastPacket.stringPayload : "") || ""; + } + + /** + * Returns the buffer payload from the last packet taken from the radio queue + * (via ``receiveNumber``, ``receiveString``, etc) or the empty string if that + * packet did not contain a string. + */ + //% help=radio/received-buffer deprecated=1 + export function receivedBuffer(): Buffer { + return (lastPacket ? lastPacket.bufferPayload : null) || control.createBuffer(0); + } + + /** + * Returns the system time of the sender micro:bit at the moment when it sent the + * last packet taken from the radio queue (via ``receiveNumber``, + * ``receiveString``, etc). + */ + //% help=radio/received-time deprecated=1 + export function receivedTime(): number { + return lastPacket ? lastPacket.time : 0; + } + + /** + * Reads the next packet from the radio queue and returns the packet's number + * payload or 0 if the packet did not contain a number. + */ + //% help=radio/receive-number + //% weight=46 + //% blockId=radio_datagram_receive block="radio receive number" blockGap=8 + //% deprecated=true + export function receiveNumber(): number { + lastPacket = RadioPacket.getPacket(readRawPacket()); + return receivedNumber(); + } + + /** + * Reads the next packet from the radio queue and returns the packet's string + * payload or the empty string if the packet did not contain a string. + */ + //% blockId=radio_datagram_receive_string block="radio receive string" blockGap=8 + //% weight=44 + //% help=radio/receive-string + //% deprecated=true + export function receiveString(): string { + lastPacket = RadioPacket.getPacket(readRawPacket()); + return receivedString(); + } +} \ No newline at end of file diff --git a/libs/radio/pxt.json b/libs/radio/pxt.json index 4271e876..9a1374cc 100644 --- a/libs/radio/pxt.json +++ b/libs/radio/pxt.json @@ -6,7 +6,8 @@ "shims.d.ts", "enums.d.ts", "radio.cpp", - "radio.ts" + "radio.ts", + "deprecated.ts" ], "icon": "./static/packages/radio/icon.png", "public": true, diff --git a/libs/radio/radio.cpp b/libs/radio/radio.cpp index 739dcede..b5025527 100644 --- a/libs/radio/radio.cpp +++ b/libs/radio/radio.cpp @@ -2,58 +2,12 @@ using namespace pxt; -#define MAX_FIELD_NAME_LENGTH 12 -#define MAX_FIELD_DOUBLE_NAME_LENGTH 8 -#define MAX_PAYLOAD_LENGTH 20 -#define PACKET_PREFIX_LENGTH 9 -#define VALUE_PACKET_NAME_LEN_OFFSET 13 -#define DOUBLE_VALUE_PACKET_NAME_LEN_OFFSET 17 - - -// 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 - -// payload: number (9 ... 16) -#define PACKET_TYPE_DOUBLE 4 - -// payload: number (9 ... 16), name length (17), name (18 ... 26) -#define PACKET_TYPE_DOUBLE_VALUE 5 - //% color=#E3008C weight=96 icon="\uf012" namespace radio { - // ------------------------------------------------------------------------- - // Radio - // ------------------------------------------------------------------------- bool radioEnabled = false; - bool transmitSerialNumber = false; - PacketBuffer packet; - uint8_t type; - uint32_t time; - uint32_t serial; - int ivalue; - double dvalue; - String msg; // may be NULL before first packet - Buffer bufMsg; // may be NULL before first packet - int radioEnable() { int r = uBit.radio.enable(); if (r != MICROBIT_OK) { @@ -81,303 +35,23 @@ namespace radio { uBit.radio.event.eventReceived(MicroBitEvent(src, value, CREATE_ONLY)); } - void setPacketPrefix(uint8_t* buf, int type) { - // prefix: type (0), time (1..4), serial (5..8) - uint32_t t = system_timer_current_time(); - uint32_t sn = transmitSerialNumber ? microbit_serial_number() : 0; - - buf[0] = (uint8_t) type; - memcpy(buf + 1, &t, 4); - memcpy(buf + 5, &sn, 4); - } - - uint8_t copyStringValue(uint8_t* buf, String data, uint8_t maxLength) { - uint8_t len = min_(maxLength, data->getUTF8Size()); - - // One byte for length of the string - buf[0] = len; - - if (len > 0) { - memcpy(buf + 1, data->getUTF8Data(), len); - } - return len + 1; - } - - String getStringValue(uint8_t* buf, uint8_t maxLength) { - // First byte is the string length - uint8_t len = min_(maxLength, buf[0]); - return mkString((char*)buf + 1, len); - } - - uint8_t copyBufferValue(uint8_t* buf, Buffer data, uint8_t maxLength) { - uint8_t len = min_(maxLength, data->length); - - // One byte for length of the buffer - buf[0] = len; - if (len > 0) { - memcpy(buf + 1, data->data, len); - } - return len + 1; - } - - Buffer getBufferValue(uint8_t* buf, uint8_t maxLength) { - // First byte is the buffer length - uint8_t len = min_(maxLength, buf[0]); - // skip first byte - return mkBuffer(buf + 1, len); - } - - void writePacketAsJSON(uint8_t tp, int iv, double dv, int s, int t, String m, Buffer b) { - // Convert the packet to JSON and send over serial - uBit.serial.send("{"); - uBit.serial.send("\"t\":"); - uBit.serial.send(t); - uBit.serial.send(",\"s\":"); - uBit.serial.send(s); - if ((tp == PACKET_TYPE_STRING || tp == PACKET_TYPE_VALUE) && NULL != m) { - uBit.serial.send(",\"n\":\""); - uBit.serial.send((uint8_t*)m->getUTF8Data(), m->getUTF8Size()); - uBit.serial.send("\""); - } - if (tp == PACKET_TYPE_BUFFER && NULL != b) { - uBit.serial.send(",\"b\":\""); - // TODO: proper base64 encoding - uBit.serial.send(b->data, b->length); - uBit.serial.send("\""); - } - if (tp == PACKET_TYPE_NUMBER || tp == PACKET_TYPE_VALUE) { - uBit.serial.send(",\"v\":"); - uBit.serial.send(iv); - } else if (tp == PACKET_TYPE_DOUBLE || tp == PACKET_TYPE_DOUBLE_VALUE) { - uBit.serial.send(",\"v\":"); - TNumber td = fromDouble(dv); - String sd = numops::toString(td); - uBit.serial.send((uint8_t*)sd->getUTF8Data(), sd->getUTF8Size()); - decrRC(sd); - } - uBit.serial.send("}\r\n"); - } - /** - * Takes a packet from the micro:bit radio queue. - * @param writeToSerial if true, write the received packet to serial without updating the global packet; - if false, update the global packet instead + * Takes the next packet from the radio queue and returns its contents in a Buffer */ - void receivePacket(bool writeToSerial) { - PacketBuffer p = uBit.radio.datagram.recv(); - - uint8_t* buf = p.getBytes(); - uint8_t tp; - int t; - int s; - int iv = 0; - double dv = 0; - String m = NULL; - Buffer b = NULL; - - memcpy(&tp, buf, 1); - memcpy(&t, buf + 1, 4); - memcpy(&s, buf + 5, 4); - - switch(tp) { - case PACKET_TYPE_STRING: - m = getStringValue(buf + PACKET_PREFIX_LENGTH, MAX_PAYLOAD_LENGTH - 1); - break; - case PACKET_TYPE_BUFFER: - b = getBufferValue(buf + PACKET_PREFIX_LENGTH, MAX_PAYLOAD_LENGTH - 1); - break; - case PACKET_TYPE_DOUBLE: - case PACKET_TYPE_DOUBLE_VALUE: - memcpy(&dv, buf + PACKET_PREFIX_LENGTH, sizeof(double)); - if (tp == PACKET_TYPE_DOUBLE_VALUE) { - m = getStringValue(buf + DOUBLE_VALUE_PACKET_NAME_LEN_OFFSET, MAX_FIELD_DOUBLE_NAME_LENGTH); - } - break; - case PACKET_TYPE_NUMBER: - case PACKET_TYPE_VALUE: - memcpy(&iv, buf + PACKET_PREFIX_LENGTH, sizeof(int)); - if (tp == PACKET_TYPE_VALUE) { - m = getStringValue(buf + VALUE_PACKET_NAME_LEN_OFFSET, MAX_FIELD_NAME_LENGTH); - } - break; - default: // unknown packet - return; - } - - if (NULL == m) - m = mkString("", 0); - if (NULL == b) - b = mkBuffer(NULL, 0); - - if (!writeToSerial) { - // Refresh global packet - packet = p; - type = tp; - time = t; - serial = s; - ivalue = iv; - dvalue = dv; - decrRC(msg); - decrRC(bufMsg); - msg = m; - bufMsg = b; - } - else { - writePacketAsJSON(tp, iv, dv, s, t, m, b); - decrRC(m); - decrRC(b); - } + //% help=radio/received-packet + Buffer readRawPacket() { + if (radioEnable() != MICROBIT_OK) return mkBuffer(NULL, 0); + packet = uBit.radio.datagram.recv(); + return mkBuffer(packet.getBytes(), packet.length()); } /** - * Broadcasts a number over radio to any connected micro:bit in the group. + * Sends a raw packet through the radio */ - //% help=radio/send-number - //% weight=60 - //% blockId=radio_datagram_send block="radio send number %value" blockGap=8 - void sendNumber(TNumber value) { - if (radioEnable() != MICROBIT_OK) return; - - int iv = toInt(value); - double dv = toDouble(value); - if (iv == dv) { - uint8_t length = PACKET_PREFIX_LENGTH + sizeof(int); - uint8_t buf[length]; - memset(buf, 0, length); - setPacketPrefix(buf, PACKET_TYPE_NUMBER); - memcpy(buf + PACKET_PREFIX_LENGTH, &iv, sizeof(int)); - uBit.radio.datagram.send(buf, length); - } else { - uint8_t length = PACKET_PREFIX_LENGTH + sizeof(double); - uint8_t buf[length]; - memset(buf, 0, length); - setPacketPrefix(buf, PACKET_TYPE_DOUBLE); - memcpy(buf + PACKET_PREFIX_LENGTH, &dv, sizeof(double)); - uBit.radio.datagram.send(buf, length); - } - } - - /** - * Broadcasts a name / value pair along with the device serial number - * and running time to any connected micro:bit in the group. The name can - * include no more than 8 characters. - * @param name the field name (max 8 characters), eg: "name" - * @param value the numeric value - */ - //% help=radio/send-value - //% weight=59 - //% blockId=radio_datagram_send_value block="radio send|value %name|= %value" blockGap=8 - void sendValue(String name, TNumber value) { - if (radioEnable() != MICROBIT_OK) return; - - uint8_t buf[32]; - memset(buf, 0, 32); - - int iv = toInt(value); - double dv = toDouble(value); - if (iv == dv) { - setPacketPrefix(buf, PACKET_TYPE_VALUE); - memcpy(buf + PACKET_PREFIX_LENGTH, &iv, sizeof(int)); - - int stringLen = copyStringValue(buf + VALUE_PACKET_NAME_LEN_OFFSET, name, MAX_FIELD_DOUBLE_NAME_LENGTH); - uBit.radio.datagram.send(buf, VALUE_PACKET_NAME_LEN_OFFSET + stringLen); - } else { - setPacketPrefix(buf, PACKET_TYPE_DOUBLE_VALUE); - memcpy(buf + PACKET_PREFIX_LENGTH, &dv, sizeof(double)); - - int stringLen = copyStringValue(buf + DOUBLE_VALUE_PACKET_NAME_LEN_OFFSET, name, MAX_FIELD_NAME_LENGTH); - uBit.radio.datagram.send(buf, DOUBLE_VALUE_PACKET_NAME_LEN_OFFSET + stringLen); - } - } - - /** - * Broadcasts a string along with the device serial number - * and running time to any connected micro:bit in the group. - */ - //% help=radio/send-string - //% weight=58 - //% blockId=radio_datagram_send_string block="radio send string %msg" - //% msg.shadowOptions.toString=true - void sendString(String msg) { - if (radioEnable() != MICROBIT_OK || NULL == msg) return; - - uint8_t buf[32]; - memset(buf, 0, 32); - - setPacketPrefix(buf, PACKET_TYPE_STRING); - int stringLen = copyStringValue(buf + PACKET_PREFIX_LENGTH, msg, MAX_PAYLOAD_LENGTH - 1); - - uBit.radio.datagram.send(buf, PACKET_PREFIX_LENGTH + stringLen); - } - - /** - * Broadcasts a buffer (up to 19 bytes long) along with the device serial number - * and running time to any connected micro:bit in the group. - */ - //% help=radio/send-buffer - //% weight=57 //% advanced=true - void sendBuffer(Buffer msg) { + void sendRawPacket(Buffer msg) { if (radioEnable() != MICROBIT_OK || NULL == msg) return; - - uint8_t buf[32]; - memset(buf, 0, 32); - - setPacketPrefix(buf, PACKET_TYPE_BUFFER); - int bufLen = copyBufferValue(buf + PACKET_PREFIX_LENGTH, msg, MAX_PAYLOAD_LENGTH - 1); - - uBit.radio.datagram.send(buf, PACKET_PREFIX_LENGTH + bufLen); - } - - - /** - * Reads the next packet from the radio queue and and writes it to serial - * as JSON. - */ - //% help=radio/write-value-to-serial - //% weight=3 - //% blockId=radio_write_value_serial block="radio write value to serial" - //% deprecated=true - void writeValueToSerial() { - if (radioEnable() != MICROBIT_OK) return; - receivePacket(true); - } - - /** - * Writes the last received packet to serial as JSON. This should be called - * within an ``onDataPacketReceived`` callback. - */ - //% help=radio/write-received-packet-to-serial - //% weight=3 - //% blockId=radio_write_packet_serial block="radio write received packet to serial" - //% advanced=true - void writeReceivedPacketToSerial() { - if (radioEnable() != MICROBIT_OK) return; - writePacketAsJSON(type, ivalue, dvalue, (int) serial, (int) time, msg, bufMsg); - } - - TNumber readNumber() { - if (type == PACKET_TYPE_NUMBER || type == PACKET_TYPE_VALUE) - return fromInt(ivalue); - else if (type == PACKET_TYPE_DOUBLE || type == PACKET_TYPE_DOUBLE_VALUE) - return fromDouble(dvalue); - else - return fromInt(0); - } - - /** - * Reads the next packet from the radio queue and returns the packet's number - * payload or 0 if the packet did not contain a number. - */ - //% help=radio/receive-number - //% weight=46 - //% blockId=radio_datagram_receive block="radio receive number" blockGap=8 - //% deprecated=true - TNumber receiveNumber() - { - if (radioEnable() != MICROBIT_OK) return 0; - receivePacket(false); - return readNumber(); + uBit.radio.datagram.send(msg->data, msg->length); } /** @@ -390,23 +64,7 @@ namespace radio { void onDataReceived(Action body) { if (radioEnable() != MICROBIT_OK) return; registerWithDal(MICROBIT_ID_RADIO, MICROBIT_RADIO_EVT_DATAGRAM, body); - // make sure the receive buffer has a free spot - receivePacket(false); - } - - - /** - * Reads the next packet from the radio queue and returns the packet's string - * payload or the empty string if the packet did not contain a string. - */ - //% blockId=radio_datagram_receive_string block="radio receive string" blockGap=8 - //% weight=44 - //% help=radio/receive-string - //% deprecated=true - String receiveString() { - if (radioEnable() != MICROBIT_OK) return mkString("", 0); - receivePacket(false); - return msg; + readRawPacket(); } /** @@ -419,7 +77,7 @@ namespace radio { //% blockId=radio_datagram_rssi block="radio received signal strength" //% deprecated=true int receivedSignalStrength() { - if (radioEnable() != MICROBIT_OK) return 0; + if (radioEnable() != MICROBIT_OK || packet == NULL) return 0; return packet.getRSSI(); } @@ -449,74 +107,4 @@ namespace radio { if (radioEnable() != MICROBIT_OK) return; uBit.radio.setTransmitPower(power); } - - /** - * Set the radio to transmit the serial number in each message. - * @param transmit value indicating if the serial number is transmitted, eg: true - */ - //% help=radio/set-transmit-serial-number - //% weight=8 blockGap=8 - //% blockId=radio_set_transmit_serial_number block="radio set transmit serial number %transmit" - //% advanced=true - void setTransmitSerialNumber(bool transmit) { - if (radioEnable() != MICROBIT_OK) return; - transmitSerialNumber = transmit; - } - - /** - * Returns the number payload from the last packet taken from the radio queue - * (via ``receiveNumber``, ``receiveString``, etc) or 0 if that packet did not - * contain a number. - */ - //% help=radio/received-number - TNumber receivedNumber() { - if (radioEnable() != MICROBIT_OK) return 0; - return readNumber(); - } - - /** - * Returns the serial number of the sender micro:bit from the last packet taken - * from the radio queue (via ``receiveNumber``, ``receiveString``, etc) or 0 if - * that packet did not send a serial number. - */ - //% help=radio/received-serial - uint32_t receivedSerial() { - if (radioEnable() != MICROBIT_OK) return 0; - return serial; - } - - /** - * Returns the string payload from the last packet taken from the radio queue - * (via ``receiveNumber``, ``receiveString``, etc) or the empty string if that - * packet did not contain a string. - */ - //% help=radio/received-string - String receivedString() { - if (radioEnable() != MICROBIT_OK || NULL == msg) return mkString("", 0); - incrRC(msg); - return msg; - } - - /** - * Returns the buffer payload from the last packet taken from the radio queue - * (via ``receiveNumber``, ``receiveString``, etc) or the empty string if that - * packet did not contain a string. - */ - //% help=radio/received-buffer - Buffer receivedBuffer() { - if (radioEnable() != MICROBIT_OK || NULL == bufMsg) return mkBuffer(NULL, 0); - incrRC(bufMsg); - return bufMsg; - } - - /** - * Returns the system time of the sender micro:bit at the moment when it sent the - * last packet taken from the radio queue (via ``receiveNumber``, - * ``receiveString``, etc). - */ - //% help=radio/received-time - uint32_t receivedTime() { - if (radioEnable() != MICROBIT_OK) return 0; - return time; - } } diff --git a/libs/radio/radio.ts b/libs/radio/radio.ts index 61bf8bcc..299b623c 100644 --- a/libs/radio/radio.ts +++ b/libs/radio/radio.ts @@ -16,182 +16,121 @@ enum RadioPacketProperty { */ //% color=#E3008C weight=96 icon="\uf012" namespace radio { - export class Packet { - /** - * The number payload if a number was sent in this packet (via ``sendNumber()`` or ``sendValue()``) - * or 0 if this packet did not contain a number. - */ - public receivedNumber: number; - /** - * The string payload if a string was sent in this packet (via ``sendString()`` or ``sendValue()``) - * or the empty string if this packet did not contain a string. - */ - public receivedString: string; - /** - * The buffer payload if a buffer was sent in this packet - * or the empty buffer - */ - public receivedBuffer: Buffer; - /** - * The system time of the sender of the packet at the time the packet was sent. - */ - public time: number; - /** - * The serial number of the sender of the packet or 0 if the sender did not sent their serial number. - */ - public serial: number; - /** - * The received signal strength indicator (RSSI) of the packet. - */ - public signal: number; - } + export const MAKECODE_RADIO_EVT_NUMBER = 10; + export const MAKECODE_RADIO_EVT_STRING = 11; + export const MAKECODE_RADIO_EVT_BUFFER = 12; + export const MAKECODE_RADIO_EVT_VALUE = 13; + + const MAX_FIELD_DOUBLE_NAME_LENGTH = 8; + const MAX_PAYLOAD_LENGTH = 20; + const PACKET_PREFIX_LENGTH = 9; + const VALUE_PACKET_NAME_LEN_OFFSET = 13; + const DOUBLE_VALUE_PACKET_NAME_LEN_OFFSET = 17; + + // 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) + const PACKET_TYPE_NUMBER = 0; + // payload: number (9 ... 12), name length (13), name (14 ... 26) + const PACKET_TYPE_VALUE = 1; + // payload: string length (9), string (10 ... 28) + const PACKET_TYPE_STRING = 2; + // payload: buffer length (9), buffer (10 ... 28) + const PACKET_TYPE_BUFFER = 3; + // payload: number (9 ... 16) + const PACKET_TYPE_DOUBLE = 4; + // payload: number (9 ... 16), name length (17), name (18 ... 26) + const PACKET_TYPE_DOUBLE_VALUE = 5; + + let transmittingSerial: boolean; + let initialized = false; + + export let lastPacket: RadioPacket; + + function init() { + if (initialized) return; + initialized = true; - /** - * Registers code to run when the radio receives a packet. Also takes the - * received packet from the radio queue. - */ - //% help=radio/on-data-packet-received blockHandlerKey="radioreceived" deprecated=true - //% mutate=objectdestructuring - //% mutateText=Packet - //% mutateDefaults="receivedNumber;receivedString:name,receivedNumber:value;receivedString" - //% blockId=radio_on_packet block="on radio received" blockGap=8 - export function onDataPacketReceived(cb: (packet: Packet) => void) { onDataReceived(() => { - receiveNumber(); - const packet = new Packet(); - packet.receivedNumber = receivedNumber(); - packet.time = receivedTime(); - packet.serial = receivedSerial(); - packet.receivedString = receivedString(); - packet.receivedBuffer = receivedBuffer(); - packet.signal = receivedSignalStrength(); - lastPacket = packet; - cb(packet) - }); + lastPacket = RadioPacket.getPacket(readRawPacket()); + lastPacket.signal = receivedSignalStrength(); + + switch (lastPacket.packetType) { + case PACKET_TYPE_NUMBER: + case PACKET_TYPE_DOUBLE: + control.raiseEvent(DAL.MICROBIT_ID_RADIO, MAKECODE_RADIO_EVT_NUMBER); + break; + case PACKET_TYPE_VALUE: + case PACKET_TYPE_DOUBLE_VALUE: + control.raiseEvent(DAL.MICROBIT_ID_RADIO, MAKECODE_RADIO_EVT_VALUE); + break; + case PACKET_TYPE_BUFFER: + control.raiseEvent(DAL.MICROBIT_ID_RADIO, MAKECODE_RADIO_EVT_BUFFER); + break; + case PACKET_TYPE_STRING: + control.raiseEvent(DAL.MICROBIT_ID_RADIO, MAKECODE_RADIO_EVT_STRING); + break; + } + }) } /** * Registers code to run when the radio receives a number. */ - //% help=radio/on-received-number blockHandlerKey="radioreceived" + //% help=radio/on-received-number //% blockId=radio_on_number_drag block="on radio received" blockGap=16 //% useLoc="radio.onDataPacketReceived" draggableParameters=reporter export function onReceivedNumber(cb: (receivedNumber: number) => void) { - onDataReceived(() => { - receiveNumber(); - const packet = new Packet(); - packet.time = receivedTime(); - packet.serial = receivedSerial(); - packet.signal = receivedSignalStrength(); - packet.receivedNumber = receivedNumber(); - lastPacket = packet; - cb(packet.receivedNumber); + init(); + control.onEvent(DAL.MICROBIT_ID_RADIO, MAKECODE_RADIO_EVT_NUMBER, () => { + cb(lastPacket.numberPayload); }); } /** * Registers code to run when the radio receives a key value pair. */ - //% help=radio/on-received-value blockHandlerKey="radioreceived" + //% help=radio/on-received-value //% blockId=radio_on_value_drag block="on radio received" blockGap=16 //% useLoc="radio.onDataPacketReceived" draggableParameters=reporter export function onReceivedValue(cb: (name: string, value: number) => void) { - onDataReceived(() => { - receiveNumber(); - const packet = new Packet(); - packet.time = receivedTime(); - packet.serial = receivedSerial(); - packet.signal = receivedSignalStrength(); - packet.receivedNumber = receivedNumber(); - packet.receivedString = receivedString(); - lastPacket = packet; - cb(packet.receivedString, packet.receivedNumber) + init(); + control.onEvent(DAL.MICROBIT_ID_RADIO, MAKECODE_RADIO_EVT_VALUE, () => { + cb(lastPacket.stringPayload, lastPacket.numberPayload); }); } /** * Registers code to run when the radio receives a string. */ - //% help=radio/on-received-string blockHandlerKey="radioreceived" + //% help=radio/on-received-string //% blockId=radio_on_string_drag block="on radio received" blockGap=16 //% useLoc="radio.onDataPacketReceived" draggableParameters=reporter export function onReceivedString(cb: (receivedString: string) => void) { - onDataReceived(() => { - receiveNumber(); - const packet = new Packet(); - packet.time = receivedTime(); - packet.serial = receivedSerial(); - packet.signal = receivedSignalStrength(); - packet.receivedString = receivedString(); - lastPacket = packet; - cb(packet.receivedString); + init(); + control.onEvent(DAL.MICROBIT_ID_RADIO, MAKECODE_RADIO_EVT_STRING, () => { + cb(lastPacket.stringPayload); }); } /** * Registers code to run when the radio receives a buffer. */ - //% help=radio/on-received-buffer blockHandlerKey="radioreceived" blockHidden=1 + //% help=radio/on-received-buffer blockHidden=1 //% blockId=radio_on_buffer_drag block="on radio received" blockGap=16 //% useLoc="radio.onDataPacketReceived" draggableParameters=reporter export function onReceivedBuffer(cb: (receivedBuffer: Buffer) => void) { - onDataReceived(() => { - receiveNumber(); - const packet = new Packet(); - packet.time = receivedTime(); - packet.serial = receivedSerial(); - packet.signal = receivedSignalStrength(); - packet.receivedBuffer = receivedBuffer(); - lastPacket = packet; - cb(packet.receivedBuffer) + init(); + control.onEvent(DAL.MICROBIT_ID_RADIO, MAKECODE_RADIO_EVT_BUFFER, () => { + cb(lastPacket.bufferPayload); }); } - /** - * Registers code to run when the radio receives a number. Deprecated, use - * onReceivedNumber instead. - */ - //% help=radio/on-received-number blockHandlerKey="radioreceived" - //% blockId=radio_on_number block="on radio received" blockGap=16 - //% useLoc="radio.onDataPacketReceived" deprecated=1 - export function onReceivedNumberDeprecated(cb: (receivedNumber: number) => void) { - onReceivedNumber(cb); - } - - /** - * Registers code to run when the radio receives a key value pair. Deprecated, use - * onReceivedValue instead. - */ - //% help=radio/on-received-value blockHandlerKey="radioreceived" - //% blockId=radio_on_value block="on radio received" blockGap=16 - //% useLoc="radio.onDataPacketReceived" deprecated=1 - export function onReceivedValueDeprecated(cb: (name: string, value: number) => void) { - onReceivedValue(cb); - } - - /** - * Registers code to run when the radio receives a string. Deprecated, use - * onReceivedString instead. - */ - //% help=radio/on-received-string blockHandlerKey="radioreceived" - //% blockId=radio_on_string block="on radio received" blockGap=16 - //% useLoc="radio.onDataPacketReceived" deprecated=1 - export function onReceivedStringDeprecated(cb: (receivedString: string) => void) { - onReceivedString(cb); - } - - /** - * Registers code to run when the radio receives a buffer. Deprecated, use - * onReceivedBuffer instead. - */ - //% help=radio/on-received-buffer blockHandlerKey="radioreceived" blockHidden=1 - //% blockId=radio_on_buffer block="on radio received" blockGap=16 - //% useLoc="radio.onDataPacketReceived" deprecated=1 - export function onReceivedBufferDeprecated(cb: (receivedBuffer: Buffer) => void) { - onReceivedBuffer(cb); - } - - let lastPacket: Packet; /** * Returns properties of the last radio packet received. * @param type the type of property to retrieve from the last packet @@ -219,4 +158,271 @@ namespace radio { export function _packetProperty(type: RadioPacketProperty): number { return type; } -} + + export class RadioPacket { + public static getPacket(data: Buffer) { + return new RadioPacket(data); + } + + public static mkPacket(packetType: number) { + const res = new RadioPacket(); + res.data[0] = packetType; + return res; + } + + private constructor(public readonly data?: Buffer) { + if (!data) this.data = control.createBuffer(32); + } + + public signal: number; + + get packetType() { + return this.data[0]; + } + + get time() { + return this.data.getNumber(NumberFormat.Int32LE, 1); + } + + set time(val: number) { + this.data.setNumber(NumberFormat.Int32LE, 1, val); + } + + get serial() { + return this.data.getNumber(NumberFormat.Int32LE, 5); + } + + set serial(val: number) { + this.data.setNumber(NumberFormat.Int32LE, 5, val); + } + + get stringPayload() { + const offset = getStringOffset(this.packetType) as number; + return offset ? this.data.slice(offset + 1, this.data[offset]).toString() : undefined; + } + + set stringPayload(val: string) { + const offset = getStringOffset(this.packetType) as number; + if (offset) { + const buf = control.createBufferFromUTF8(truncateString(val, getMaxStringLength(this.packetType))); + this.data[offset] = buf.length; + this.data.write(offset + 1, buf); + } + } + + get numberPayload() { + switch (this.packetType) { + case PACKET_TYPE_NUMBER: + case PACKET_TYPE_VALUE: + return this.data.getNumber(NumberFormat.Int32LE, PACKET_PREFIX_LENGTH); + case PACKET_TYPE_DOUBLE: + case PACKET_TYPE_DOUBLE_VALUE: + return this.data.getNumber(NumberFormat.Float64LE, PACKET_PREFIX_LENGTH); + } + return undefined; + } + + set numberPayload(val: number) { + switch (this.packetType) { + case PACKET_TYPE_NUMBER: + case PACKET_TYPE_VALUE: + this.data.setNumber(NumberFormat.Int32LE, PACKET_PREFIX_LENGTH, val); + break; + case PACKET_TYPE_DOUBLE: + case PACKET_TYPE_DOUBLE_VALUE: + this.data.setNumber(NumberFormat.Float64LE, PACKET_PREFIX_LENGTH, val); + break; + } + } + + get bufferPayload() { + const len = this.data[PACKET_PREFIX_LENGTH]; + return this.data.slice(PACKET_PREFIX_LENGTH + 1, len); + } + + set bufferPayload(b: Buffer) { + const len = Math.min(b.length, MAX_PAYLOAD_LENGTH - 1); + this.data[PACKET_PREFIX_LENGTH] = len; + this.data.write(PACKET_PREFIX_LENGTH + 1, b.slice(0, len)); + } + + hasString() { + return this.packetType === PACKET_TYPE_STRING || + this.packetType === PACKET_TYPE_VALUE || + this.packetType === PACKET_TYPE_DOUBLE_VALUE; + } + + hasNumber() { + return this.packetType === PACKET_TYPE_NUMBER || + this.packetType === PACKET_TYPE_DOUBLE || + this.packetType === PACKET_TYPE_VALUE || + this.packetType === PACKET_TYPE_DOUBLE_VALUE; + } + } + + + /** + * Broadcasts a number over radio to any connected micro:bit in the group. + */ + //% help=radio/send-number + //% weight=60 + //% blockId=radio_datagram_send block="radio send number %value" blockGap=8 + export function sendNumber(value: number) { + let packet: RadioPacket; + + if (value === (value | 0)) { + packet = RadioPacket.mkPacket(PACKET_TYPE_NUMBER); + } + else { + packet = RadioPacket.mkPacket(PACKET_TYPE_DOUBLE); + } + + packet.numberPayload = value; + sendPacket(packet); + } + + /** + * Broadcasts a name / value pair along with the device serial number + * and running time to any connected micro:bit in the group. The name can + * include no more than 8 characters. + * @param name the field name (max 8 characters), eg: "name" + * @param value the numeric value + */ + //% help=radio/send-value + //% weight=59 + //% blockId=radio_datagram_send_value block="radio send|value %name|= %value" blockGap=8 + export function sendValue(name: string, value: number) { + let packet: RadioPacket; + + if (value === (value | 0)) { + packet = RadioPacket.mkPacket(PACKET_TYPE_VALUE); + } + else { + packet = RadioPacket.mkPacket(PACKET_TYPE_DOUBLE_VALUE); + } + + packet.numberPayload = value; + packet.stringPayload = name; + sendPacket(packet); + } + + /** + * Broadcasts a string along with the device serial number + * and running time to any connected micro:bit in the group. + */ + //% help=radio/send-string + //% weight=58 + //% blockId=radio_datagram_send_string block="radio send string %msg" + //% msg.shadowOptions.toString=true + export function sendString(value: string) { + const packet = RadioPacket.mkPacket(PACKET_TYPE_STRING); + packet.stringPayload = value; + sendPacket(packet); + } + + /** + * Broadcasts a buffer (up to 19 bytes long) along with the device serial number + * and running time to any connected micro:bit in the group. + */ + //% help=radio/send-buffer + //% weight=57 + //% advanced=true + export function sendBuffer(msg: Buffer) { + const packet = RadioPacket.mkPacket(PACKET_TYPE_BUFFER); + packet.bufferPayload = msg; + sendPacket(packet); + } + + /** + * Writes the last received packet to serial as JSON. This should be called + * within an ``onDataPacketReceived`` callback. + */ + //% help=radio/write-received-packet-to-serial + //% weight=3 + //% blockId=radio_write_packet_serial block="radio write received packet to serial" + //% advanced=true + export function writeReceivedPacketToSerial() { + if (lastPacket) writeToSerial(lastPacket) + } + + /** + * Set the radio to transmit the serial number in each message. + * @param transmit value indicating if the serial number is transmitted, eg: true + */ + //% help=radio/set-transmit-serial-number + //% weight=8 blockGap=8 + //% blockId=radio_set_transmit_serial_number block="radio set transmit serial number %transmit" + //% advanced=true + export function setTransmitSerialNumber(transmit: boolean) { + transmittingSerial = transmit; + } + + export function writeToSerial(packet: RadioPacket) { + serial.writeString("{"); + serial.writeString("\"t\":"); + serial.writeString("" + packet.time); + serial.writeString(",\"s\":"); + serial.writeString("" + packet.serial); + + if (packet.hasString()) { + serial.writeString(",\"n\":\""); + serial.writeString(packet.stringPayload); + serial.writeString("\""); + } + if (packet.packetType == PACKET_TYPE_BUFFER) { + serial.writeString(",\"b\":\""); + // TODO: proper base64 encoding + serial.writeString(packet.bufferPayload.toString()); + serial.writeString("\""); + } + if (packet.hasNumber()) { + serial.writeString(",\"v\":"); + serial.writeString("" + packet.numberPayload); + } + + serial.writeString("}\r\n"); + } + + function sendPacket(packet: RadioPacket) { + packet.time = input.runningTime(); + packet.serial = transmittingSerial ? control.deviceSerialNumber() : 0; + radio.sendRawPacket(packet.data); + } + + function truncateString(str: string, bytes: number) { + str = str.substr(0, bytes); + let buff = control.createBufferFromUTF8(str); + + while (buff.length > bytes) { + str = str.substr(0, str.length - 1); + buff = control.createBufferFromUTF8(str); + } + + return str; + } + + function getStringOffset(packetType: number) { + switch (packetType) { + case PACKET_TYPE_STRING: + return PACKET_PREFIX_LENGTH; + case PACKET_TYPE_VALUE: + return VALUE_PACKET_NAME_LEN_OFFSET; + case PACKET_TYPE_DOUBLE_VALUE: + return DOUBLE_VALUE_PACKET_NAME_LEN_OFFSET; + default: + return undefined; + } + } + + function getMaxStringLength(packetType: number) { + switch (packetType) { + case PACKET_TYPE_STRING: + return MAX_PAYLOAD_LENGTH - 2; + case PACKET_TYPE_VALUE: + case PACKET_TYPE_DOUBLE_VALUE: + return MAX_FIELD_DOUBLE_NAME_LENGTH; + default: + return undefined; + } + } +} \ No newline at end of file diff --git a/libs/radio/shims.d.ts b/libs/radio/shims.d.ts index 076de968..b8da55fe 100644 --- a/libs/radio/shims.d.ts +++ b/libs/radio/shims.d.ts @@ -8,7 +8,7 @@ declare namespace radio { /** * Sends an event over radio to neigboring devices */ - //% blockId=radioRaiseEvent block="radio raise event|from source %src=control_event_source_id|with value %value=control_event_value_id" + //% blockId=radioRaiseEvent block="radio raise event|from source %src=control_event_source_id|with value %value=control_event_value_id" //% blockExternalInputs=1 //% advanced=true //% weight=1 @@ -16,72 +16,16 @@ declare namespace radio { function raiseEvent(src: int32, value: int32): void; /** - * Broadcasts a number over radio to any connected micro:bit in the group. + * Takes the next packet from the radio queue and returns its contents in a Buffer */ - //% help=radio/send-number - //% weight=60 - //% blockId=radio_datagram_send block="radio send number %value" blockGap=8 shim=radio::sendNumber - function sendNumber(value: number): void; + //% help=radio/received-packet shim=radio::readRawPacket + function readRawPacket(): Buffer; /** - * Broadcasts a name / value pair along with the device serial number - * and running time to any connected micro:bit in the group. - * @param name the field name (max 12 characters), eg: "name" - * @param value the numeric value + * Sends a raw packet through the radio */ - //% help=radio/send-value - //% weight=59 - //% blockId=radio_datagram_send_value block="radio send|value %name|= %value" blockGap=8 shim=radio::sendValue - function sendValue(name: string, value: number): void; - - /** - * Broadcasts a string along with the device serial number - * and running time to any connected micro:bit in the group. - */ - //% help=radio/send-string - //% weight=58 - //% blockId=radio_datagram_send_string block="radio send string %msg" - //% msg.shadowOptions.toString=true shim=radio::sendString - function sendString(msg: string): void; - - /** - * Broadcasts a buffer (up to 19 bytes long) along with the device serial number - * and running time to any connected micro:bit in the group. - */ - //% help=radio/send-buffer - //% weight=57 - //% advanced=true shim=radio::sendBuffer - function sendBuffer(msg: Buffer): void; - - /** - * Reads the next packet from the radio queue and and writes it to serial - * as JSON. - */ - //% help=radio/write-value-to-serial - //% weight=3 - //% blockId=radio_write_value_serial block="radio write value to serial" - //% deprecated=true shim=radio::writeValueToSerial - function writeValueToSerial(): void; - - /** - * Writes the last received packet to serial as JSON. This should be called - * within an ``onDataPacketReceived`` callback. - */ - //% help=radio/write-received-packet-to-serial - //% weight=3 - //% blockId=radio_write_packet_serial block="radio write received packet to serial" - //% advanced=true shim=radio::writeReceivedPacketToSerial - function writeReceivedPacketToSerial(): void; - - /** - * Reads the next packet from the radio queue and returns the packet's number - * payload or 0 if the packet did not contain a number. - */ - //% help=radio/receive-number - //% weight=46 - //% blockId=radio_datagram_receive block="radio receive number" blockGap=8 - //% deprecated=true shim=radio::receiveNumber - function receiveNumber(): number; + //% advanced=true shim=radio::sendRawPacket + function sendRawPacket(msg: Buffer): void; /** * Registers code to run when a packet is received over radio. @@ -92,16 +36,6 @@ declare namespace radio { //% deprecated=true shim=radio::onDataReceived function onDataReceived(body: () => void): void; - /** - * Reads the next packet from the radio queue and returns the packet's string - * payload or the empty string if the packet did not contain a string. - */ - //% blockId=radio_datagram_receive_string block="radio receive string" blockGap=8 - //% weight=44 - //% help=radio/receive-string - //% deprecated=true shim=radio::receiveString - function receiveString(): string; - /** * Gets the received signal strength indicator (RSSI) from the last packet taken * from the radio queue (via ``receiveNumber``, ``receiveString``, etc). Not supported in simulator. @@ -133,56 +67,6 @@ declare namespace radio { //% power.min=0 power.max=7 //% advanced=true shim=radio::setTransmitPower function setTransmitPower(power: int32): void; - - /** - * Set the radio to transmit the serial number in each message. - * @param transmit value indicating if the serial number is transmitted, eg: true - */ - //% help=radio/set-transmit-serial-number - //% weight=8 blockGap=8 - //% blockId=radio_set_transmit_serial_number block="radio set transmit serial number %transmit" - //% advanced=true shim=radio::setTransmitSerialNumber - function setTransmitSerialNumber(transmit: boolean): void; - - /** - * Returns the number payload from the last packet taken from the radio queue - * (via ``receiveNumber``, ``receiveString``, etc) or 0 if that packet did not - * contain a number. - */ - //% help=radio/received-number shim=radio::receivedNumber - function receivedNumber(): number; - - /** - * Returns the serial number of the sender micro:bit from the last packet taken - * from the radio queue (via ``receiveNumber``, ``receiveString``, etc) or 0 if - * that packet did not send a serial number. - */ - //% help=radio/received-serial shim=radio::receivedSerial - function receivedSerial(): uint32; - - /** - * Returns the string payload from the last packet taken from the radio queue - * (via ``receiveNumber``, ``receiveString``, etc) or the empty string if that - * packet did not contain a string. - */ - //% help=radio/received-string shim=radio::receivedString - function receivedString(): string; - - /** - * Returns the buffer payload from the last packet taken from the radio queue - * (via ``receiveNumber``, ``receiveString``, etc) or the empty string if that - * packet did not contain a string. - */ - //% help=radio/received-buffer shim=radio::receivedBuffer - function receivedBuffer(): Buffer; - - /** - * Returns the system time of the sender micro:bit at the moment when it sent the - * last packet taken from the radio queue (via ``receiveNumber``, - * ``receiveString``, etc). - */ - //% help=radio/received-time shim=radio::receivedTime - function receivedTime(): uint32; } // Auto-generated. Do not edit. Really. diff --git a/libs/radio/test.ts b/libs/radio/test.ts new file mode 100644 index 00000000..78182f64 --- /dev/null +++ b/libs/radio/test.ts @@ -0,0 +1,349 @@ +/** + * Tests for the radio. Press A on mbit 1 and B on mbit 2 to run the tests. + * Sends random ints, doubles, strings, and buffers and checks them on + * the other side + */ + +class FastRandom { + private lfsr: number; + public seed: number; + + constructor(seed?: number) { + if (seed === undefined) seed = Math.randomRange(0x0001, 0xFFFF); + this.seed = seed; + this.lfsr = seed; + } + + next(): number { + return this.lfsr = (this.lfsr >> 1) ^ ((-(this.lfsr & 1)) & 0xb400); + } + + randomRange(min: number, max: number): number { + return min + (max > min ? this.next() % (max - min + 1) : 0); + } + + reset() { + this.lfsr = this.seed; + } +} + +enum TestStage { + Integer, + String, + Double, + IntValue, + DblValue, + Buffer +} + +const TEST_COUNT = 30; + +radio.setGroup(78) +const rand = new FastRandom(1234); + +let stage = TestStage.Integer; + +function initSender() { + let lastReceived: number; + let lastString: string; + let testIndex = 0; + let lastBuf: Buffer; + + let lastAck = -1; + + rand.reset(); + basic.clearScreen(); + + // Send loop + control.inBackground(function () { + while (true) { + for (let i = 0; i < TEST_COUNT; i++) { + toggle(testIndex); + + if (stage === TestStage.Integer) { + lastReceived = getNextInt(); + } + else if (stage === TestStage.Double) { + lastReceived = getNextDouble(); + } + else if (stage === TestStage.IntValue) { + lastString = getNextName(); + console.log(truncateString(lastString, 8)) + lastReceived = getNextInt(); + } + else if (stage === TestStage.DblValue) { + lastString = getNextName(); + lastReceived = getNextDouble(); + } + else if (stage === TestStage.String) { + lastString = getNextString(); + console.log(truncateString(lastString, 19)) + } + else if (stage === TestStage.Buffer) { + lastBuf = getNextBuffer(); + } + + while (lastAck !== testIndex) { + if (stage === TestStage.Integer || stage === TestStage.Double) { + radio.sendNumber(lastReceived) + } + else if (stage === TestStage.IntValue || stage === TestStage.DblValue) { + radio.sendValue(lastString, lastReceived) + } + else if (stage === TestStage.String) { + radio.sendString(lastString); + } + else if (stage === TestStage.Buffer) { + radio.sendBuffer(lastBuf); + } + basic.pause(10); + } + testIndex++; + } + + stage++; + if (stage > TestStage.Buffer) { + basic.showIcon(IconNames.Yes); + return; + } + } + }) + + radio.onReceivedNumber(function (receivedNumber: number) { + if (receivedNumber > lastAck) { + lastAck = receivedNumber; + } + }); +} + +let lastReceived: number; +let lastString: string; +let testIndex = -1; +let running = true; +let lastBuf: Buffer; + +let lastPacket = new radio.Packet(); +let currentPacket = new radio.Packet(); + +function truncateString(str: string, bytes: number) { + str = str.substr(0, bytes); + let buff = control.createBufferFromUTF8(str); + + while (buff.length > bytes) { + str = str.substr(0, str.length - 1); + buff = control.createBufferFromUTF8(str); + } + + return str; +} + +function initReceiver() { + + rand.reset(); + + basic.clearScreen(); + + radio.onDataReceived(function () { + radio.receiveNumber(); + + currentPacket.receivedNumber = radio.receivedNumber(); + currentPacket.receivedString = radio.receivedString(); + currentPacket.receivedBuffer = radio.receivedBuffer(); + + if (currentPacket.receivedNumber === lastPacket.receivedNumber && + currentPacket.receivedString === lastPacket.receivedString && + checkBufferEqual(currentPacket.receivedBuffer, lastPacket.receivedBuffer)) { + return; + } + + lastPacket.receivedNumber = currentPacket.receivedNumber + lastPacket.receivedString = currentPacket.receivedString + lastPacket.receivedBuffer = currentPacket.receivedBuffer + + switch (stage) { + case TestStage.Integer: + verifyInt(radio.receivedNumber()); + break; + case TestStage.Double: + verifyDouble(radio.receivedNumber()); + break; + case TestStage.IntValue: + verifyIntValue(radio.receivedString(), radio.receivedNumber()); + break; + case TestStage.DblValue: + verifyDblValue(radio.receivedString(), radio.receivedNumber()); + break; + case TestStage.String: + verifyString(radio.receivedString()); + break; + case TestStage.Buffer: + verifyBuffer(radio.receivedBuffer()); + break; + } + }) + + control.inBackground(function () { + while (running) { + radio.sendNumber(testIndex); + basic.pause(10) + } + }) +} + +function nextTest() { + testIndex++; + toggle(testIndex); + console.log(`test ${testIndex}`) + if (((testIndex + 1) % TEST_COUNT) === 0) { + stage++; + + if (stage > TestStage.Buffer) { + basic.showIcon(IconNames.Yes) + running = false; + } + } +} + +function verifyInt(int: number) { + if (int === lastReceived) return; + lastReceived = int; + if (lastReceived != getNextInt()) fail(); + nextTest() +} + +function verifyDouble(dbl: number) { + if (dbl === lastReceived) return; + lastReceived = dbl; + if (lastReceived != getNextDouble()) fail(); + nextTest() +} + +function verifyIntValue(name: string, val: number) { + if (val === lastReceived) return; + lastReceived = val; + + if (name != truncateString(getNextName(), 8) || lastReceived != getNextInt()) fail(); + nextTest() +} + +function verifyDblValue(name: string, val: number) { + if (val === lastReceived) return; + lastReceived = val; + + if (name != truncateString(getNextName(), 8) || lastReceived != getNextDouble()) fail(); + nextTest() +} + +function verifyString(str: string) { + if (!str || str === lastString) return; + + lastString = str; + let next = truncateString(getNextString(), 19); + + if (lastString !== next) { + console.log(`got ${control.createBufferFromUTF8(lastString).toHex()} expected ${control.createBufferFromUTF8(next).toHex()}`) + } + nextTest() +} + +function verifyBuffer(buf: Buffer) { + if (checkBufferEqual(lastBuf, buf)) return; + + lastBuf = buf; + + if (!checkBufferEqual(lastBuf, getNextBuffer())) { + fail(); + } + nextTest() +} + +function fail() { + control.panic(testIndex); +} + + +let _lastInt: number; +let _lastDbl: number; +let _lastStr: string; +let _lastBuf: Buffer; +let _lastNam: string; + +function getNextInt(): number { + let res = rand.next(); + if (!res || res === _lastInt) return getNextInt(); + _lastInt = res; + return res; +} + +function getNextDouble(): number { + let res = rand.next() / rand.next(); + if (res === _lastDbl) return getNextDouble(); + _lastDbl = res; + return res; +} + +function getNextString(): string { + let len = rand.randomRange(1, 19); + let res = ""; + for (let i = 0; i < len; i++) { + res += String.fromCharCode(rand.next() & 0xbfff); + } + + if (res === _lastStr) return getNextString(); + + _lastStr = res; + return res; +} + +function getNextName(): string { + let len = rand.randomRange(1, 8); + let res = ""; + for (let i = 0; i < len; i++) { + res += String.fromCharCode(rand.next() & 0xbfff); + } + + if (res === _lastNam) return getNextName(); + + _lastNam = res; + return res; +} + +function getNextBuffer(): Buffer { + let len = rand.randomRange(0, 8); + let res = control.createBuffer(len); + + for (let i = 0; i < len; i++) { + res[i] = rand.next() & 0xff; + } + + if (checkBufferEqual(_lastBuf, res)) return getNextBuffer(); + + _lastBuf = res; + return res; +} + +function checkBufferEqual(a: Buffer, b: Buffer) { + if (a === b) return true; + if ((!a && b) || (a && !b)) return false; + if (a.length != b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +} + +input.onButtonPressed(Button.A, function () { + basic.showString("S"); + initSender(); +}) + +input.onButtonPressed(Button.B, function () { + basic.showString("R"); + initReceiver(); +}) + +function toggle(index: number) { + const x = index % 5; + const y = Math.idiv(index, 5) % 5; + led.toggle(x, y); +} diff --git a/sim/state/radio.ts b/sim/state/radio.ts index de13172c..50fac326 100644 --- a/sim/state/radio.ts +++ b/sim/state/radio.ts @@ -96,13 +96,6 @@ namespace pxsim { } namespace pxsim.radio { - enum PacketPayloadType { - NUMBER = 0, - VALUE = 1, - STRING = 2, - BUFFER = 3 - } - export function raiseEvent(id: number, eventid: number): void { board().radioState.raiseEvent(id, eventid); } @@ -115,70 +108,17 @@ namespace pxsim.radio { board().radioState.setTransmitPower(power); } - export function setTransmitSerialNumber(transmit: boolean): void { - board().radioState.setTransmitSerialNumber(transmit); - } - - export function sendNumber(value: number): void { + export function sendRawPacket(buf: RefBuffer) { board().radioState.datagram.send({ - type: PacketPayloadType.NUMBER, + type: 0, groupId: board().radioState.groupId, - numberData: value, + bufferData: buf.data }); } - export function sendString(msg: string): void { - if (msg === undefined) return; - - msg = msg.substr(0, 19); - board().radioState.datagram.send({ - type: PacketPayloadType.STRING, - groupId: board().radioState.groupId, - stringData: msg, - }); - } - - export function sendBuffer(buf: RefBuffer): void { - if (!buf) return; - - const data = buf.data.slice(0, 18); - board().radioState.datagram.send({ - type: PacketPayloadType.STRING, - groupId: board().radioState.groupId, - bufferData: data - }); - } - - export function writeValueToSerial(): void { - const b = board(); - writePacketToSerial(b, b.radioState.datagram.recv()) - } - - export function writeReceivedPacketToSerial(): void { - const b = board(); - writePacketToSerial(b, b.radioState.datagram.lastReceived); - } - - export function sendValue(name: string, value: number) { - name = name.substr(0, 8); - const msg: number[] = []; - msg.push() - board().radioState.datagram.send({ - type: PacketPayloadType.VALUE, - groupId: board().radioState.groupId, - stringData: name, - numberData: value - }); - } - - export function receiveNumber(): number { + export function readRawPacket() { const packet = board().radioState.datagram.recv(); - return receivedNumber(); - } - - export function receiveString(): string { - const packet = board().radioState.datagram.recv(); - return receivedString(); + return new RefBuffer(packet.payload.bufferData) } export function receivedSignalStrength(): number { @@ -187,50 +127,6 @@ namespace pxsim.radio { export function onDataReceived(handler: RefAction): void { pxtcore.registerWithDal(DAL.MICROBIT_ID_RADIO, DAL.MICROBIT_RADIO_EVT_DATAGRAM, handler); - radio.receiveNumber(); - } - - export function receivedNumber(): number { - return board().radioState.datagram.lastReceived.payload.numberData || 0; - } - - export function receivedSerial(): number { - return board().radioState.datagram.lastReceived.serial; - } - - export function receivedString(): string { - return initString(board().radioState.datagram.lastReceived.payload.stringData || ""); - } - - export function receivedBuffer(): RefBuffer { - return new RefBuffer(board().radioState.datagram.lastReceived.payload.bufferData || new Uint8Array(0)) - } - - export function receivedTime(): number { - return board().radioState.datagram.lastReceived.time; - } - - function writePacketToSerial(b: DalBoard, p: PacketBuffer) { - switch (p.payload.type) { - case PacketPayloadType.NUMBER: - b.writeSerial(`{"t":${p.time},"s":${p.serial},"v":${p.payload.numberData}}\r\n`) - break; - case PacketPayloadType.VALUE: - b.writeSerial(`{"t":${p.time},"s":${p.serial},"n":"${p.payload.stringData}","v":${p.payload.numberData}}\r\n`) - break; - case PacketPayloadType.STRING: - b.writeSerial(`{"t":${p.time},"s":${p.serial},"n":"${p.payload.stringData}"}\r\n`) - break; - // TODO: (microbit master) - // case PacketPayloadType.BUFFER: - // const buf = new Uint8Array(p.payload.bufferData.buffer); - // let res = ""; - // for (let i = 0; i < buf.length; ++i) - // res += String.fromCharCode(buf[i]); - // b.writeSerial(`{"t":${p.time},"s":${p.serial},"b":"${res}"}\r\n`) - default: - // unknown type - break; - } + readRawPacket(); } } \ No newline at end of file