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..a4d92d442 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._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 a2056209c..b962d3c59 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,14 +74,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); } 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..035cea566 --- /dev/null +++ b/src/util/scratch-link-websocket.js @@ -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; 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;