From 5e626eb7e946f208a270222805474216272214e9 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 24 May 2019 12:51:58 -0400 Subject: [PATCH 1/5] Initial prototype of configurable scratch link socket --- src/engine/runtime.js | 29 +++++++++++ src/io/ble.js | 47 +++++++++-------- src/io/bt.js | 45 ++++++++-------- src/util/jsonrpc-web-socket.js | 40 -------------- src/util/scratch-link-websocket.js | 83 ++++++++++++++++++++++++++++++ src/virtual-machine.js | 8 +++ 6 files changed, 165 insertions(+), 87 deletions(-) delete mode 100644 src/util/jsonrpc-web-socket.js create mode 100644 src/util/scratch-link-websocket.js diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 3979b415e..15f59556a 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -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. diff --git a/src/io/ble.js b/src/io/ble.js index 0b97f9ae6..8bc2eb7b4 100644 --- a/src/io/ble.js +++ b/src/io/ble.js @@ -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._socket.isOpen()) { + this._socket.close(); } if (this._connected) { this._connected = false; } - + if (this._discoverTimeoutID) { window.clearTimeout(this._discoverTimeoutID); } diff --git a/src/io/bt.js b/src/io/bt.js index a2056209c..a0d7d03a9 100644 --- a/src/io/bt.js +++ b/src/io/bt.js @@ -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,18 +42,15 @@ 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) + ); } /** @@ -75,8 +74,8 @@ class BT extends JSONRPCWebSocket { * Close the websocket. */ disconnect () { - if (this._ws.readyState === this._ws.OPEN) { - this._ws.close(); + if (this._socket.isOpen()) { + this._socket.close(); } if (this._connected) { diff --git a/src/util/jsonrpc-web-socket.js b/src/util/jsonrpc-web-socket.js deleted file mode 100644 index af5af27e1..000000000 --- a/src/util/jsonrpc-web-socket.js +++ /dev/null @@ -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; diff --git a/src/util/scratch-link-websocket.js b/src/util/scratch-link-websocket.js new file mode 100644 index 000000000..8eb4c13dc --- /dev/null +++ b/src/util/scratch-link-websocket.js @@ -0,0 +1,83 @@ +/** + * 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() + * - set[Open|Close|Error] + * - setHandleMessage + * - 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._ws.onopen = this._onOpen; + this._ws.onclose = this._onClose; + this._ws.onerror = this._onError; + } else { + throw new Error('Must set open, close 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; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 3320238f4..deb1471e1 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -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; From 94afd58b2f97960cac1004f3a8be2681e8e1adac Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 31 May 2019 08:35:27 -0400 Subject: [PATCH 2/5] Require handleMessage to be set before opening scratch-link-socket --- src/util/scratch-link-websocket.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/scratch-link-websocket.js b/src/util/scratch-link-websocket.js index 8eb4c13dc..8c8a2becf 100644 --- a/src/util/scratch-link-websocket.js +++ b/src/util/scratch-link-websocket.js @@ -33,12 +33,12 @@ class ScratchLinkWebSocket { throw new Error(`Unknown ScratchLink socket Type: ${this._type}`); } - if (this._onOpen && this._onClose && this._onError) { + 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 and error handlers before calling open on the socket'); + throw new Error('Must set open, close, message and error handlers before calling open on the socket'); } this._ws.onmessage = this._onMessage.bind(this); From 64cff3d16170633eded5ac5e77072662a148984d Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 31 May 2019 08:41:20 -0400 Subject: [PATCH 3/5] Fix documentation on what methods to implement for ScratchLinkSocket Co-Authored-By: chrisgarrity --- src/util/scratch-link-websocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/scratch-link-websocket.js b/src/util/scratch-link-websocket.js index 8c8a2becf..1f1ce7eb2 100644 --- a/src/util/scratch-link-websocket.js +++ b/src/util/scratch-link-websocket.js @@ -6,7 +6,7 @@ * public methods in this class. * - open() * - close() - * - set[Open|Close|Error] + * - setOn[Open|Close|Error] * - setHandleMessage * - isOpen() */ From 706354d2f0b99d14efb444b976e518d028d0629b Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 31 May 2019 08:55:20 -0400 Subject: [PATCH 4/5] Update documentation about required methods on ScratchLinkSocket Co-Authored-By: chrisgarrity --- src/util/scratch-link-websocket.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/scratch-link-websocket.js b/src/util/scratch-link-websocket.js index 1f1ce7eb2..035cea566 100644 --- a/src/util/scratch-link-websocket.js +++ b/src/util/scratch-link-websocket.js @@ -8,6 +8,7 @@ * - close() * - setOn[Open|Close|Error] * - setHandleMessage + * - sendMessage(msgObj) * - isOpen() */ class ScratchLinkWebSocket { From 615f60252d0d2088afcea9ab40c93fa892d9677b Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Fri, 31 May 2019 09:42:00 -0400 Subject: [PATCH 5/5] Set connected to false before closing to prevent infinite loop There is an implicit assumption that socket.close is async, but if it is sync, it may go into an infinite loop of close callbacks if the connected flag is not set to false before trying to close Co-Authored-By: chrisgarrity --- src/io/ble.js | 8 ++++---- src/io/bt.js | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/io/ble.js b/src/io/ble.js index 8bc2eb7b4..a4d92d442 100644 --- a/src/io/ble.js +++ b/src/io/ble.js @@ -72,14 +72,14 @@ class BLE extends JSONRPC { * Close the websocket. */ disconnect () { - if (this._socket.isOpen()) { - this._socket.close(); - } - if (this._connected) { this._connected = false; } + if (this._socket.isOpen()) { + this._socket.close(); + } + if (this._discoverTimeoutID) { window.clearTimeout(this._discoverTimeoutID); } diff --git a/src/io/bt.js b/src/io/bt.js index a0d7d03a9..b962d3c59 100644 --- a/src/io/bt.js +++ b/src/io/bt.js @@ -74,14 +74,14 @@ class BT extends JSONRPC { * Close the websocket. */ disconnect () { - if (this._socket.isOpen()) { - this._socket.close(); - } - if (this._connected) { this._connected = false; } + if (this._socket.isOpen()) { + this._socket.close(); + } + if (this._discoverTimeoutID) { window.clearTimeout(this._discoverTimeoutID); }