Skip to content

Commit

Permalink
Merge pull request #2024 from evhan55/extensions/microbit-rate-limiting
Browse files Browse the repository at this point in the history
Rate limiting for micro:bit using TaskQueue
  • Loading branch information
evhan55 authored Mar 14, 2019
2 parents 7d7193c + c603e0d commit 94ca11f
Showing 1 changed file with 50 additions and 58 deletions.
108 changes: 50 additions & 58 deletions src/extensions/scratch3_microbit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const cast = require('../../util/cast');
const formatMessage = require('format-message');
const BLE = require('../../io/ble');
const Base64Util = require('../../util/base64-util');
const TaskQueue = require('../../util/task-queue');

/**
* Icon png to be displayed at the left edge of each extension block, encoded as a data URI.
Expand Down Expand Up @@ -32,12 +33,6 @@ const BLECommand = {
*/
const BLETimeout = 4500;

/**
* A time interval to wait (in milliseconds) while a block that sends a BLE message is running.
* @type {number}
*/
const BLESendInterval = 100;

/**
* A string to report to the BLE socket when the micro:bit has stopped receiving data.
* @type {string}
Expand Down Expand Up @@ -74,6 +69,7 @@ class MicroBit {
* @private
*/
this._runtime = runtime;
this._runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this));

/**
* The BluetoothLowEnergy connection socket for reading/writing peripheral data.
Expand Down Expand Up @@ -132,23 +128,32 @@ class MicroBit {
this._timeoutID = null;

/**
* A flag that is true while we are busy sending data to the BLE socket.
* @type {boolean}
* @private
*/
this._busy = false;

/**
* ID for a timeout which is used to clear the busy flag if it has been
* true for a long time.
* A task queue to limit the rate of Bluetooth message sends by limiting
* the rate of execution of blocks which send Bluetooth messages.
*
* The bucket in this task queue holds 1 task at a time, and refills
* at a rate of 10 tasks per second, from a queue that holds tasks with
* a maximum total cost of 30. Since most tasks have a cost of 1 this
* means the queue will generally have at most 30 tasks. If more than 30
* tasks are added to the task queue in a short period, some tasks may
* be rejected (ignored) by the task queue.
*
* @type {TaskQueue}
*/
this._busyTimeoutID = null;
this._queue = new TaskQueue(1, 10, {maxTotalCost: 30});

this.disconnect = this.disconnect.bind(this);
this._onConnect = this._onConnect.bind(this);
this._onMessage = this._onMessage.bind(this);
}

/**
* Stop all the communication tasks pending on the task queue.
*/
stopAll () {
this._queue.cancelAll();
}

/**
* @param {string} text - the text to display.
* @return {Promise} - a Promise that resolves when writing to peripheral.
Expand Down Expand Up @@ -264,19 +269,6 @@ class MicroBit {
*/
send (command, message) {
if (!this.isConnected()) return;
if (this._busy) return;

// Set a busy flag so that while we are sending a message and waiting for
// the response, additional messages are ignored.
this._busy = true;

// Set a timeout after which to reset the busy flag. This is used in case
// a BLE message was sent for which we never received a response, because
// e.g. the peripheral was turned off after the message was sent. We reset
// the busy flag after a while so that it is possible to try again later.
this._busyTimeoutID = window.setTimeout(() => {
this._busy = false;
}, 5000);

const output = new Uint8Array(message.length + 1);
output[0] = command; // attach command to beginning of message
Expand All @@ -285,12 +277,7 @@ class MicroBit {
}
const data = Base64Util.uint8ArrayToBase64(output);

this._ble.write(BLEUUID.service, BLEUUID.txChar, data, 'base64', true).then(
() => {
this._busy = false;
window.clearTimeout(this._busyTimeoutID);
}
);
this._ble.write(BLEUUID.service, BLEUUID.txChar, data, 'base64', true);
}

/**
Expand Down Expand Up @@ -817,19 +804,19 @@ class Scratch3MicroBitBlocks {
return value;
};
const hex = symbol.split('').reduce(reducer, 0);
if (hex !== null) {

if (hex === null) return;

return this._peripheral._queue.do(() => {
this._peripheral.ledMatrixState[0] = hex & 0x1F;
this._peripheral.ledMatrixState[1] = (hex >> 5) & 0x1F;
this._peripheral.ledMatrixState[2] = (hex >> 10) & 0x1F;
this._peripheral.ledMatrixState[3] = (hex >> 15) & 0x1F;
this._peripheral.ledMatrixState[4] = (hex >> 20) & 0x1F;
this._peripheral.displayMatrix(this._peripheral.ledMatrixState);
}

return new Promise(resolve => {
setTimeout(() => {
resolve();
}, BLESendInterval);
}).catch(() => {
// console.log('*** CATCH DISPLAY_SYMBOL REJECTION');
// console.log(e);
});
}

Expand All @@ -845,13 +832,19 @@ class Scratch3MicroBitBlocks {
*/
displayText (args) {
const text = String(args.TEXT).substring(0, 19);
if (text.length > 0) this._peripheral.displayText(text);
const yieldDelay = 120 * ((6 * text.length) + 6);

return new Promise(resolve => {
setTimeout(() => {
resolve();
}, yieldDelay);

return this._peripheral._queue.do(() => {
if (text.length > 0) this._peripheral.displayText(text);
const yieldDelay = 120 * ((6 * text.length) + 6);

return new Promise(resolve => {
setTimeout(() => {
resolve();
}, yieldDelay);
});
}).catch(() => {
// console.log('*** CATCH DISPLAY_TEXT REJECTION');
// console.log(e);
});
}

Expand All @@ -860,15 +853,14 @@ class Scratch3MicroBitBlocks {
* @return {Promise} - a Promise that resolves after a tick.
*/
displayClear () {
for (let i = 0; i < 5; i++) {
this._peripheral.ledMatrixState[i] = 0;
}
this._peripheral.displayMatrix(this._peripheral.ledMatrixState);

return new Promise(resolve => {
setTimeout(() => {
resolve();
}, BLESendInterval);
return this._peripheral._queue.do(() => {
for (let i = 0; i < 5; i++) {
this._peripheral.ledMatrixState[i] = 0;
}
this._peripheral.displayMatrix(this._peripheral.ledMatrixState);
}).catch(() => {
// console.log('*** CATCH DISPLAY_CLEAR REJECTION');
// console.log(e);
});
}

Expand Down

0 comments on commit 94ca11f

Please sign in to comment.