pxt-calliope/libs/radio/radio.ts
Richard Knoll 61aae94d8b
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
2019-04-12 13:10:47 -07:00

428 lines
14 KiB
TypeScript

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 {
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;
onDataReceived(() => {
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
//% blockId=radio_on_number_drag block="on radio received" blockGap=16
//% useLoc="radio.onDataPacketReceived" draggableParameters=reporter
export function onReceivedNumber(cb: (receivedNumber: number) => void) {
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
//% 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();
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
//% blockId=radio_on_string_drag block="on radio received" blockGap=16
//% useLoc="radio.onDataPacketReceived" draggableParameters=reporter
export function onReceivedString(cb: (receivedString: string) => void) {
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 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();
control.onEvent(DAL.MICROBIT_ID_RADIO, MAKECODE_RADIO_EVT_BUFFER, () => {
cb(lastPacket.bufferPayload);
});
}
/**
* 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;
}
}
}