enum RadioPacketProperty { //% blockIdentity=radio._packetProperty //% block="signal strength" SignalStrength = 2, //% blockIdentity=radio._packetProperty //% block="time" Time = 0, //% block="serial number" //% blockIdentity=radio._packetProperty SerialNumber = 1 } /** * Communicate data using radio packets */ //% color=#E3008C weight=96 icon="\uf012" namespace radio { 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; let onReceivedNumberHandler: (receivedNumber: number) => void; let onReceivedValueHandler: (name: string, value: number) => void; let onReceivedStringHandler: (receivedString: string) => void; let onReceivedBufferHandler: (receivedBuffer: Buffer) => void; function init() { if (initialized) return; initialized = true; onDataReceived(() => { let buffer: Buffer = readRawPacket(); while (buffer && buffer.length) { lastPacket = RadioPacket.getPacket(buffer); lastPacket.signal = receivedSignalStrength(); switch (lastPacket.packetType) { case PACKET_TYPE_NUMBER: case PACKET_TYPE_DOUBLE: if (onReceivedNumberHandler) onReceivedNumberHandler(lastPacket.numberPayload); break; case PACKET_TYPE_VALUE: case PACKET_TYPE_DOUBLE_VALUE: if (onReceivedValueHandler) onReceivedValueHandler(lastPacket.stringPayload, lastPacket.numberPayload); break; case PACKET_TYPE_BUFFER: if(onReceivedBufferHandler) onReceivedBufferHandler(lastPacket.bufferPayload); break; case PACKET_TYPE_STRING: if (onReceivedStringHandler) onReceivedStringHandler(lastPacket.stringPayload); break; } // read next packet if any buffer = readRawPacket(); } }) } /** * Registers code to run when the radio receives a number. */ //% 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) { init(); onReceivedNumberHandler = cb; } /** * Registers code to run when the radio receives a key value pair. */ //% 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) { init(); onReceivedValueHandler = cb; } /** * Registers code to run when the radio receives a string. */ //% 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) { init(); onReceivedStringHandler = cb; } /** * Registers code to run when the radio receives a buffer. */ //% 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) { init(); onReceivedBufferHandler = cb; } /** * Returns properties of the last radio packet received. * @param type the type of property to retrieve from the last packet */ //% help=radio/received-packet //% weight=11 blockGap=8 //% blockId=radio_received_packet block="received packet %type=radio_packet_property" blockGap=16 export function receivedPacket(type: number) { if (lastPacket) { switch (type) { case RadioPacketProperty.Time: return lastPacket.time; case RadioPacketProperty.SerialNumber: return lastPacket.serial; case RadioPacketProperty.SignalStrength: return lastPacket.signal; } } return 0; } /** * Gets a packet property. * @param type the packet property type, eg: PacketProperty.time */ //% blockId=radio_packet_property block="%note" //% shim=TD_ID blockHidden=1 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; } } }