Merge branch 'develop' into extensions/ev3-rate-limiting

This commit is contained in:
Eric Rosenbaum 2019-06-04 11:37:43 -04:00 committed by GitHub
commit b6a33cc3ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 235 additions and 144 deletions

View file

@ -16,6 +16,7 @@ const maybeFormatMessage = require('../util/maybe-format-message');
const StageLayering = require('./stage-layering');
const Variable = require('./variable');
const xmlEscape = require('../util/xml-escape');
const ScratchLinkWebSocket = require('../util/scratch-link-websocket');
// Virtual I/O devices.
const Clock = require('../io/clock');
@ -1289,6 +1290,34 @@ class Runtime extends EventEmitter {
(result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []);
}
/**
* Get a scratch link socket.
* @param {string} type Either BLE or BT
* @returns {ScratchLinkSocket} The scratch link socket.
*/
getScratchLinkSocket (type) {
const factory = this._linkSocketFactory || this._defaultScratchLinkSocketFactory;
return factory(type);
}
/**
* Configure how ScratchLink sockets are created. Factory must consume a "type" parameter
* either BT or BLE.
* @param {Function} factory The new factory for creating ScratchLink sockets.
*/
configureScratchLinkSocketFactory (factory) {
this._linkSocketFactory = factory;
}
/**
* The default scratch link socket creator, using websockets to the installed device manager.
* @param {string} type Either BLE or BT
* @returns {ScratchLinkSocket} The new scratch link socket (a WebSocket object)
*/
_defaultScratchLinkSocketFactory (type) {
return new ScratchLinkWebSocket(type);
}
/**
* Register an extension that communications with a hardware peripheral by id,
* to have access to it and its peripheral functions in the future.

View file

@ -20,7 +20,6 @@ const builtinExtensions = {
text2speech: () => require('../extensions/scratch3_text2speech'),
translate: () => require('../extensions/scratch3_translate'),
videoSensing: () => require('../extensions/scratch3_video_sensing'),
speech2text: () => require('../extensions/scratch3_speech2text'),
ev3: () => require('../extensions/scratch3_ev3'),
makeymakey: () => require('../extensions/scratch3_makeymakey'),
boost: () => require('../extensions/scratch3_boost'),

View file

@ -332,36 +332,36 @@ class BoostMotor {
* @type {Object}
* @private
*/
this._pendingTimeoutId = null;
this._pendingDurationTimeoutId = null;
/**
* The starting time for the pending timeout.
* The starting time for the pending duration timeout.
* @type {number}
* @private
*/
this._pendingTimeoutStartTime = null;
this._pendingDurationTimeoutStartTime = null;
/**
* The delay/duration of the pending timeout.
* The delay/duration of the pending duration timeout.
* @type {number}
* @private
*/
this._pendingTimeoutDelay = null;
this._pendingDurationTimeoutDelay = null;
/**
* The target position of a turn-based command.
* @type {number}
* @private
*/
this._pendingPositionDestination = null;
this._pendingRotationDestination = null;
/**
* If the motor has been turned on run for a specific duration,
* this is the function that will be called once Scratch VM gets a notification from the Move Hub.
* If the motor has been turned on run for a specific rotation, this is the function
* that will be called once Scratch VM gets a notification from the Move Hub.
* @type {Object}
* @private
*/
this._pendingPromiseFunction = null;
this._pendingRotationPromise = null;
this.turnOff = this.turnOff.bind(this);
}
@ -432,43 +432,43 @@ class BoostMotor {
*/
set status (value) {
this._clearRotationState();
this._clearTimeout();
this._clearDurationTimeout();
this._status = value;
}
/**
* @return {number} - time, in milliseconds, of when the pending timeout began.
* @return {number} - time, in milliseconds, of when the pending duration timeout began.
*/
get pendingTimeoutStartTime () {
return this._pendingTimeoutStartTime;
get pendingDurationTimeoutStartTime () {
return this._pendingDurationTimeoutStartTime;
}
/**
* @return {number} - delay, in milliseconds, of the pending timeout.
* @return {number} - delay, in milliseconds, of the pending duration timeout.
*/
get pendingTimeoutDelay () {
return this._pendingTimeoutDelay;
get pendingDurationTimeoutDelay () {
return this._pendingDurationTimeoutDelay;
}
/**
* @return {number} - delay, in milliseconds, of the pending timeout.
* @return {number} - target position, in degrees, of the pending rotation.
*/
get pendingPositionDestination () {
return this._pendingPositionDestination;
get pendingRotationDestination () {
return this._pendingRotationDestination;
}
/**
* @return {boolean} - true if this motor is currently moving, false if this motor is off or braking.
* @return {Promise} - the Promise function for the pending rotation.
*/
get pendingPromiseFunction () {
return this._pendingPromiseFunction;
get pendingRotationPromise () {
return this._pendingRotationPromise;
}
/**
* @param {function} func - function to resolve promise
* @param {function} func - function to resolve pending rotation Promise
*/
set pendingPromiseFunction (func) {
this._pendingPromiseFunction = func;
set pendingRotationPromise (func) {
this._pendingRotationPromise = func;
}
/**
@ -505,7 +505,7 @@ class BoostMotor {
milliseconds = Math.max(0, milliseconds);
this.status = BoostMotorState.ON_FOR_TIME;
this._turnOn();
this._setNewTimeout(this.turnOff, milliseconds);
this._setNewDurationTimeout(this.turnOff, milliseconds);
}
/**
@ -530,7 +530,7 @@ class BoostMotor {
);
this.status = BoostMotorState.ON_FOR_ROTATION;
this._pendingPositionDestination = this.position + (degrees * this.direction * direction);
this._pendingRotationDestination = this.position + (degrees * this.direction * direction);
this._parent.send(BoostBLE.characteristic, cmd);
}
@ -558,12 +558,12 @@ class BoostMotor {
* Clear the motor action timeout, if any. Safe to call even when there is no pending timeout.
* @private
*/
_clearTimeout () {
if (this._pendingTimeoutId !== null) {
clearTimeout(this._pendingTimeoutId);
this._pendingTimeoutId = null;
this._pendingTimeoutStartTime = null;
this._pendingTimeoutDelay = null;
_clearDurationTimeout () {
if (this._pendingDurationTimeoutId !== null) {
clearTimeout(this._pendingDurationTimeoutId);
this._pendingDurationTimeoutId = null;
this._pendingDurationTimeoutStartTime = null;
this._pendingDurationTimeoutDelay = null;
}
}
@ -573,19 +573,19 @@ class BoostMotor {
* @param {int} delay - wait this many milliseconds before calling the callback.
* @private
*/
_setNewTimeout (callback, delay) {
this._clearTimeout();
_setNewDurationTimeout (callback, delay) {
this._clearDurationTimeout();
const timeoutID = setTimeout(() => {
if (this._pendingTimeoutId === timeoutID) {
this._pendingTimeoutId = null;
this._pendingTimeoutStartTime = null;
this._pendingTimeoutDelay = null;
if (this._pendingDurationTimeoutId === timeoutID) {
this._pendingDurationTimeoutId = null;
this._pendingDurationTimeoutStartTime = null;
this._pendingDurationTimeoutDelay = null;
}
callback();
}, delay);
this._pendingTimeoutId = timeoutID;
this._pendingTimeoutStartTime = Date.now();
this._pendingTimeoutDelay = delay;
this._pendingDurationTimeoutId = timeoutID;
this._pendingDurationTimeoutStartTime = Date.now();
this._pendingDurationTimeoutDelay = delay;
}
/**
@ -594,11 +594,11 @@ class BoostMotor {
* @private
*/
_clearRotationState () {
if (this._pendingPromiseFunction !== null) {
this._pendingPromiseFunction();
this._pendingPromiseFunction = null;
if (this._pendingRotationPromise !== null) {
this._pendingRotationPromise();
this._pendingRotationPromise = null;
}
this._pendingPositionDestination = null;
this._pendingRotationDestination = null;
}
}
@ -1669,7 +1669,7 @@ class Scratch3BoostBlocks {
if (motor.power === 0) return Promise.resolve();
return new Promise(resolve => {
motor.turnOnForDegrees(degrees, sign);
motor.pendingPromiseFunction = resolve;
motor.pendingRotationPromise = resolve;
});
}
return null;
@ -1739,7 +1739,8 @@ class Scratch3BoostBlocks {
motor.turnOnForever();
break;
case BoostMotorState.ON_FOR_TIME:
motor.turnOnFor(motor.pendingTimeoutStartTime + motor.pendingTimeoutDelay - Date.now());
motor.turnOnFor(motor.pendingDurationTimeoutStartTime +
motor.pendingDurationTimeoutDelay - Date.now());
break;
}
}
@ -1785,7 +1786,8 @@ class Scratch3BoostBlocks {
motor.turnOnForever();
break;
case BoostMotorState.ON_FOR_TIME:
motor.turnOnFor(motor.pendingTimeoutStartTime + motor.pendingTimeoutDelay - Date.now());
motor.turnOnFor(motor.pendingDurationTimeoutStartTime +
motor.pendingDurationTimeoutDelay - Date.now());
break;
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,6 @@
const JSONRPCWebSocket = require('../util/jsonrpc-web-socket');
const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/ble';
// const log = require('../util/log');
const JSONRPC = require('../util/jsonrpc');
class BLE extends JSONRPCWebSocket {
class BLE extends JSONRPC {
/**
* A BLE peripheral socket object. It handles connecting, over web sockets, to
@ -14,13 +12,15 @@ class BLE extends JSONRPCWebSocket {
* @param {object} disconnectCallback - a callback for disconnection.
*/
constructor (runtime, extensionId, peripheralOptions, connectCallback, disconnectCallback = null) {
const ws = new WebSocket(ScratchLinkWebSocket);
super(ws);
super();
this._ws = ws;
this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens
this._ws.onerror = this._handleRequestError.bind(this, 'ws onerror');
this._ws.onclose = this.handleDisconnectError.bind(this, 'ws onclose');
this._socket = runtime.getScratchLinkSocket('BLE');
this._socket.setOnOpen(this.requestPeripheral.bind(this));
this._socket.setOnClose(this.handleDisconnectError.bind(this));
this._socket.setOnError(this._handleRequestError.bind(this));
this._socket.setHandleMessage(this._handleMessage.bind(this));
this._sendMessage = this._socket.sendMessage.bind(this._socket);
this._availablePeripherals = {};
this._connectCallback = connectCallback;
@ -31,6 +31,8 @@ class BLE extends JSONRPCWebSocket {
this._extensionId = extensionId;
this._peripheralOptions = peripheralOptions;
this._runtime = runtime;
this._socket.open();
}
/**
@ -38,18 +40,15 @@ class BLE extends JSONRPCWebSocket {
* If the web socket is not yet open, request when the socket promise resolves.
*/
requestPeripheral () {
if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen?
this._availablePeripherals = {};
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(e => {
this._handleRequestError(e);
});
this._availablePeripherals = {};
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
// TODO: else?
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(e => {
this._handleRequestError(e);
});
}
/**
@ -73,14 +72,14 @@ class BLE extends JSONRPCWebSocket {
* Close the websocket.
*/
disconnect () {
if (this._ws.readyState === this._ws.OPEN) {
this._ws.close();
}
if (this._connected) {
this._connected = false;
}
if (this._socket.isOpen()) {
this._socket.close();
}
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}

View file

@ -1,8 +1,6 @@
const JSONRPCWebSocket = require('../util/jsonrpc-web-socket');
const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/bt';
// const log = require('../util/log');
const JSONRPC = require('../util/jsonrpc');
class BT extends JSONRPCWebSocket {
class BT extends JSONRPC {
/**
* A BT peripheral socket object. It handles connecting, over web sockets, to
@ -15,13 +13,15 @@ class BT extends JSONRPCWebSocket {
* @param {object} messageCallback - a callback for message sending.
*/
constructor (runtime, extensionId, peripheralOptions, connectCallback, disconnectCallback = null, messageCallback) {
const ws = new WebSocket(ScratchLinkWebSocket);
super(ws);
super();
this._ws = ws;
this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens
this._ws.onerror = this._handleRequestError.bind(this, 'ws onerror');
this._ws.onclose = this.handleDisconnectError.bind(this, 'ws onclose');
this._socket = runtime.getScratchLinkSocket('BT');
this._socket.setOnOpen(this.requestPeripheral.bind(this));
this._socket.setOnError(this._handleRequestError.bind(this));
this._socket.setOnClose(this.handleDisconnectError.bind(this));
this._socket.setHandleMessage(this._handleMessage.bind(this));
this._sendMessage = this._socket.sendMessage.bind(this._socket);
this._availablePeripherals = {};
this._connectCallback = connectCallback;
@ -33,6 +33,8 @@ class BT extends JSONRPCWebSocket {
this._peripheralOptions = peripheralOptions;
this._messageCallback = messageCallback;
this._runtime = runtime;
this._socket.open();
}
/**
@ -40,27 +42,29 @@ class BT extends JSONRPCWebSocket {
* If the web socket is not yet open, request when the socket promise resolves.
*/
requestPeripheral () {
if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen?
this._availablePeripherals = {};
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(
e => this._handleRequestError(e)
);
this._availablePeripherals = {};
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
// TODO: else?
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(
e => this._handleRequestError(e)
);
}
/**
* Try connecting to the input peripheral id, and then call the connect
* callback if connection is successful.
* @param {number} id - the id of the peripheral to connect to
* @param {string} pin - an optional pin for pairing
*/
connectPeripheral (id) {
this.sendRemoteRequest('connect', {peripheralId: id})
connectPeripheral (id, pin = null) {
const params = {peripheralId: id};
if (pin) {
params.pin = pin;
}
this.sendRemoteRequest('connect', params)
.then(() => {
this._connected = true;
this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED);
@ -75,14 +79,14 @@ class BT extends JSONRPCWebSocket {
* Close the websocket.
*/
disconnect () {
if (this._ws.readyState === this._ws.OPEN) {
this._ws.close();
}
if (this._connected) {
this._connected = false;
}
if (this._socket.isOpen()) {
this._socket.close();
}
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}

View file

@ -1,40 +0,0 @@
const JSONRPC = require('./jsonrpc');
// const log = require('../util/log');
class JSONRPCWebSocket extends JSONRPC {
constructor (webSocket) {
super();
this._ws = webSocket;
this._ws.onmessage = e => this._onSocketMessage(e);
this._ws.onopen = e => this._onSocketOpen(e);
this._ws.onclose = e => this._onSocketClose(e);
this._ws.onerror = e => this._onSocketError(e);
}
dispose () {
this._ws.close();
this._ws = null;
}
_onSocketOpen () {
}
_onSocketClose () {
}
_onSocketError () {
}
_onSocketMessage (e) {
const json = JSON.parse(e.data);
this._handleMessage(json);
}
_sendMessage (message) {
const messageText = JSON.stringify(message);
this._ws.send(messageText);
}
}
module.exports = JSONRPCWebSocket;

View file

@ -0,0 +1,84 @@
/**
* This class provides a ScratchLinkSocket implementation using WebSockets,
* attempting to connect with the locally installed Scratch-Link.
*
* To connect with ScratchLink without WebSockets, you must implement all of the
* public methods in this class.
* - open()
* - close()
* - setOn[Open|Close|Error]
* - setHandleMessage
* - sendMessage(msgObj)
* - isOpen()
*/
class ScratchLinkWebSocket {
constructor (type) {
this._type = type;
this._onOpen = null;
this._onClose = null;
this._onError = null;
this._handleMessage = null;
this._ws = null;
}
open () {
switch (this._type) {
case 'BLE':
this._ws = new WebSocket('wss://device-manager.scratch.mit.edu:20110/scratch/ble');
break;
case 'BT':
this._ws = new WebSocket('wss://device-manager.scratch.mit.edu:20110/scratch/bt');
break;
default:
throw new Error(`Unknown ScratchLink socket Type: ${this._type}`);
}
if (this._onOpen && this._onClose && this._onError && this._handleMessage) {
this._ws.onopen = this._onOpen;
this._ws.onclose = this._onClose;
this._ws.onerror = this._onError;
} else {
throw new Error('Must set open, close, message and error handlers before calling open on the socket');
}
this._ws.onmessage = this._onMessage.bind(this);
}
close () {
this._ws.close();
this._ws = null;
}
sendMessage (message) {
const messageText = JSON.stringify(message);
this._ws.send(messageText);
}
setOnOpen (fn) {
this._onOpen = fn;
}
setOnClose (fn) {
this._onClose = fn;
}
setOnError (fn) {
this._onError = fn;
}
setHandleMessage (fn) {
this._handleMessage = fn;
}
isOpen () {
return this._ws && this._ws.readyState === this._ws.OPEN;
}
_onMessage (e) {
const json = JSON.parse(e.data);
this._handleMessage(json);
}
}
module.exports = ScratchLinkWebSocket;

View file

@ -1525,6 +1525,14 @@ class VirtualMachine extends EventEmitter {
}
return null;
}
/**
* Allow VM consumer to configure the ScratchLink socket creator.
* @param {Function} factory The custom ScratchLink socket factory.
*/
configureScratchLinkSocketFactory (factory) {
this.runtime.configureScratchLinkSocketFactory(factory);
}
}
module.exports = VirtualMachine;