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 StageLayering = require('./stage-layering');
const Variable = require('./variable'); const Variable = require('./variable');
const xmlEscape = require('../util/xml-escape'); const xmlEscape = require('../util/xml-escape');
const ScratchLinkWebSocket = require('../util/scratch-link-websocket');
// Virtual I/O devices. // Virtual I/O devices.
const Clock = require('../io/clock'); const Clock = require('../io/clock');
@ -1289,6 +1290,34 @@ class Runtime extends EventEmitter {
(result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []); (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, * Register an extension that communications with a hardware peripheral by id,
* to have access to it and its peripheral functions in the future. * 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'), text2speech: () => require('../extensions/scratch3_text2speech'),
translate: () => require('../extensions/scratch3_translate'), translate: () => require('../extensions/scratch3_translate'),
videoSensing: () => require('../extensions/scratch3_video_sensing'), videoSensing: () => require('../extensions/scratch3_video_sensing'),
speech2text: () => require('../extensions/scratch3_speech2text'),
ev3: () => require('../extensions/scratch3_ev3'), ev3: () => require('../extensions/scratch3_ev3'),
makeymakey: () => require('../extensions/scratch3_makeymakey'), makeymakey: () => require('../extensions/scratch3_makeymakey'),
boost: () => require('../extensions/scratch3_boost'), boost: () => require('../extensions/scratch3_boost'),

View file

@ -332,36 +332,36 @@ class BoostMotor {
* @type {Object} * @type {Object}
* @private * @private
*/ */
this._pendingTimeoutId = null; this._pendingDurationTimeoutId = null;
/** /**
* The starting time for the pending timeout. * The starting time for the pending duration timeout.
* @type {number} * @type {number}
* @private * @private
*/ */
this._pendingTimeoutStartTime = null; this._pendingDurationTimeoutStartTime = null;
/** /**
* The delay/duration of the pending timeout. * The delay/duration of the pending duration timeout.
* @type {number} * @type {number}
* @private * @private
*/ */
this._pendingTimeoutDelay = null; this._pendingDurationTimeoutDelay = null;
/** /**
* The target position of a turn-based command. * The target position of a turn-based command.
* @type {number} * @type {number}
* @private * @private
*/ */
this._pendingPositionDestination = null; this._pendingRotationDestination = null;
/** /**
* If the motor has been turned on run for a specific duration, * If the motor has been turned on run for a specific rotation, this is the function
* this is the function that will be called once Scratch VM gets a notification from the Move Hub. * that will be called once Scratch VM gets a notification from the Move Hub.
* @type {Object} * @type {Object}
* @private * @private
*/ */
this._pendingPromiseFunction = null; this._pendingRotationPromise = null;
this.turnOff = this.turnOff.bind(this); this.turnOff = this.turnOff.bind(this);
} }
@ -432,43 +432,43 @@ class BoostMotor {
*/ */
set status (value) { set status (value) {
this._clearRotationState(); this._clearRotationState();
this._clearTimeout(); this._clearDurationTimeout();
this._status = value; 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 () { get pendingDurationTimeoutStartTime () {
return this._pendingTimeoutStartTime; return this._pendingDurationTimeoutStartTime;
} }
/** /**
* @return {number} - delay, in milliseconds, of the pending timeout. * @return {number} - delay, in milliseconds, of the pending duration timeout.
*/ */
get pendingTimeoutDelay () { get pendingDurationTimeoutDelay () {
return this._pendingTimeoutDelay; return this._pendingDurationTimeoutDelay;
} }
/** /**
* @return {number} - delay, in milliseconds, of the pending timeout. * @return {number} - target position, in degrees, of the pending rotation.
*/ */
get pendingPositionDestination () { get pendingRotationDestination () {
return this._pendingPositionDestination; 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 () { get pendingRotationPromise () {
return this._pendingPromiseFunction; return this._pendingRotationPromise;
} }
/** /**
* @param {function} func - function to resolve promise * @param {function} func - function to resolve pending rotation Promise
*/ */
set pendingPromiseFunction (func) { set pendingRotationPromise (func) {
this._pendingPromiseFunction = func; this._pendingRotationPromise = func;
} }
/** /**
@ -505,7 +505,7 @@ class BoostMotor {
milliseconds = Math.max(0, milliseconds); milliseconds = Math.max(0, milliseconds);
this.status = BoostMotorState.ON_FOR_TIME; this.status = BoostMotorState.ON_FOR_TIME;
this._turnOn(); 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.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); 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. * Clear the motor action timeout, if any. Safe to call even when there is no pending timeout.
* @private * @private
*/ */
_clearTimeout () { _clearDurationTimeout () {
if (this._pendingTimeoutId !== null) { if (this._pendingDurationTimeoutId !== null) {
clearTimeout(this._pendingTimeoutId); clearTimeout(this._pendingDurationTimeoutId);
this._pendingTimeoutId = null; this._pendingDurationTimeoutId = null;
this._pendingTimeoutStartTime = null; this._pendingDurationTimeoutStartTime = null;
this._pendingTimeoutDelay = null; this._pendingDurationTimeoutDelay = null;
} }
} }
@ -573,19 +573,19 @@ class BoostMotor {
* @param {int} delay - wait this many milliseconds before calling the callback. * @param {int} delay - wait this many milliseconds before calling the callback.
* @private * @private
*/ */
_setNewTimeout (callback, delay) { _setNewDurationTimeout (callback, delay) {
this._clearTimeout(); this._clearDurationTimeout();
const timeoutID = setTimeout(() => { const timeoutID = setTimeout(() => {
if (this._pendingTimeoutId === timeoutID) { if (this._pendingDurationTimeoutId === timeoutID) {
this._pendingTimeoutId = null; this._pendingDurationTimeoutId = null;
this._pendingTimeoutStartTime = null; this._pendingDurationTimeoutStartTime = null;
this._pendingTimeoutDelay = null; this._pendingDurationTimeoutDelay = null;
} }
callback(); callback();
}, delay); }, delay);
this._pendingTimeoutId = timeoutID; this._pendingDurationTimeoutId = timeoutID;
this._pendingTimeoutStartTime = Date.now(); this._pendingDurationTimeoutStartTime = Date.now();
this._pendingTimeoutDelay = delay; this._pendingDurationTimeoutDelay = delay;
} }
/** /**
@ -594,11 +594,11 @@ class BoostMotor {
* @private * @private
*/ */
_clearRotationState () { _clearRotationState () {
if (this._pendingPromiseFunction !== null) { if (this._pendingRotationPromise !== null) {
this._pendingPromiseFunction(); this._pendingRotationPromise();
this._pendingPromiseFunction = null; this._pendingRotationPromise = null;
} }
this._pendingPositionDestination = null; this._pendingRotationDestination = null;
} }
} }
@ -1669,7 +1669,7 @@ class Scratch3BoostBlocks {
if (motor.power === 0) return Promise.resolve(); if (motor.power === 0) return Promise.resolve();
return new Promise(resolve => { return new Promise(resolve => {
motor.turnOnForDegrees(degrees, sign); motor.turnOnForDegrees(degrees, sign);
motor.pendingPromiseFunction = resolve; motor.pendingRotationPromise = resolve;
}); });
} }
return null; return null;
@ -1739,7 +1739,8 @@ class Scratch3BoostBlocks {
motor.turnOnForever(); motor.turnOnForever();
break; break;
case BoostMotorState.ON_FOR_TIME: case BoostMotorState.ON_FOR_TIME:
motor.turnOnFor(motor.pendingTimeoutStartTime + motor.pendingTimeoutDelay - Date.now()); motor.turnOnFor(motor.pendingDurationTimeoutStartTime +
motor.pendingDurationTimeoutDelay - Date.now());
break; break;
} }
} }
@ -1785,7 +1786,8 @@ class Scratch3BoostBlocks {
motor.turnOnForever(); motor.turnOnForever();
break; break;
case BoostMotorState.ON_FOR_TIME: case BoostMotorState.ON_FOR_TIME:
motor.turnOnFor(motor.pendingTimeoutStartTime + motor.pendingTimeoutDelay - Date.now()); motor.turnOnFor(motor.pendingDurationTimeoutStartTime +
motor.pendingDurationTimeoutDelay - Date.now());
break; 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 JSONRPC = require('../util/jsonrpc');
const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/ble';
// const log = require('../util/log');
class BLE extends JSONRPCWebSocket { class BLE extends JSONRPC {
/** /**
* A BLE peripheral socket object. It handles connecting, over web sockets, to * 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. * @param {object} disconnectCallback - a callback for disconnection.
*/ */
constructor (runtime, extensionId, peripheralOptions, connectCallback, disconnectCallback = null) { constructor (runtime, extensionId, peripheralOptions, connectCallback, disconnectCallback = null) {
const ws = new WebSocket(ScratchLinkWebSocket); super();
super(ws);
this._ws = ws; this._socket = runtime.getScratchLinkSocket('BLE');
this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens this._socket.setOnOpen(this.requestPeripheral.bind(this));
this._ws.onerror = this._handleRequestError.bind(this, 'ws onerror'); this._socket.setOnClose(this.handleDisconnectError.bind(this));
this._ws.onclose = this.handleDisconnectError.bind(this, 'ws onclose'); 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._availablePeripherals = {};
this._connectCallback = connectCallback; this._connectCallback = connectCallback;
@ -31,6 +31,8 @@ class BLE extends JSONRPCWebSocket {
this._extensionId = extensionId; this._extensionId = extensionId;
this._peripheralOptions = peripheralOptions; this._peripheralOptions = peripheralOptions;
this._runtime = runtime; 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. * If the web socket is not yet open, request when the socket promise resolves.
*/ */
requestPeripheral () { requestPeripheral () {
if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen? this._availablePeripherals = {};
this._availablePeripherals = {}; if (this._discoverTimeoutID) {
if (this._discoverTimeoutID) { window.clearTimeout(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);
});
} }
// 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. * Close the websocket.
*/ */
disconnect () { disconnect () {
if (this._ws.readyState === this._ws.OPEN) {
this._ws.close();
}
if (this._connected) { if (this._connected) {
this._connected = false; this._connected = false;
} }
if (this._socket.isOpen()) {
this._socket.close();
}
if (this._discoverTimeoutID) { if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID); window.clearTimeout(this._discoverTimeoutID);
} }

View file

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