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/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index bf666a28c..b27b0d642 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -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'), diff --git a/src/extensions/scratch3_boost/index.js b/src/extensions/scratch3_boost/index.js index c14543e45..854ef7b71 100644 --- a/src/extensions/scratch3_boost/index.js +++ b/src/extensions/scratch3_boost/index.js @@ -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; } } diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index 208f3391f..971f0662c 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -16,6 +16,12 @@ const log = require('../../util/log'); // eslint-disable-next-line max-len const blockIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNDBweCIgaGVpZ2h0PSI0MHB4IiB2aWV3Qm94PSIwIDAgNDAgNDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUwLjIgKDU1MDQ3KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5ldjMtYmxvY2staWNvbjwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJldjMtYmxvY2staWNvbiIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9ImV2MyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNS41MDAwMDAsIDMuNTAwMDAwKSIgZmlsbC1ydWxlPSJub256ZXJvIj4KICAgICAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZS1wYXRoIiBzdHJva2U9IiM3Qzg3QTUiIGZpbGw9IiNGRkZGRkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgeD0iMC41IiB5PSIzLjU5IiB3aWR0aD0iMjgiIGhlaWdodD0iMjUuODEiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgc3Ryb2tlPSIjN0M4N0E1IiBmaWxsPSIjRTZFN0U4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHg9IjIuNSIgeT0iMC41IiB3aWR0aD0iMjQiIGhlaWdodD0iMzIiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgc3Ryb2tlPSIjN0M4N0E1IiBmaWxsPSIjRkZGRkZGIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHg9IjIuNSIgeT0iMTQuNSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjEzIj48L3JlY3Q+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0xNC41LDEwLjUgTDE0LjUsMTQuNSIgaWQ9IlNoYXBlIiBzdHJva2U9IiM3Qzg3QTUiIGZpbGw9IiNFNkU3RTgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PC9wYXRoPgogICAgICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlLXBhdGgiIGZpbGw9IiM0MTQ3NTciIHg9IjQuNSIgeT0iMi41IiB3aWR0aD0iMjAiIGhlaWdodD0iMTAiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgZmlsbD0iIzdDODdBNSIgb3BhY2l0eT0iMC41IiB4PSIxMy41IiB5PSIyMC4xMyIgd2lkdGg9IjIiIGhlaWdodD0iMiIgcng9IjAuNSI+PC9yZWN0PgogICAgICAgICAgICA8cGF0aCBkPSJNOS4wNiwyMC4xMyBMMTAuNTYsMjAuMTMgQzEwLjgzNjE0MjQsMjAuMTMgMTEuMDYsMjAuMzUzODU3NiAxMS4wNiwyMC42MyBMMTEuMDYsMjEuNjMgQzExLjA2LDIxLjkwNjE0MjQgMTAuODM2MTQyNCwyMi4xMyAxMC41NiwyMi4xMyBMOS4wNiwyMi4xMyBDOC41MDc3MTUyNSwyMi4xMyA4LjA2LDIxLjY4MjI4NDcgOC4wNiwyMS4xMyBDOC4wNiwyMC41Nzc3MTUzIDguNTA3NzE1MjUsMjAuMTMgOS4wNiwyMC4xMyBaIiBpZD0iU2hhcGUiIGZpbGw9IiM3Qzg3QTUiIG9wYWNpdHk9IjAuNSI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMTguOTEsMjAuMTMgTDIwLjQyLDIwLjEzIEMyMC42OTYxNDI0LDIwLjEzIDIwLjkyLDIwLjM1Mzg1NzYgMjAuOTIsMjAuNjMgTDIwLjkyLDIxLjYzIEMyMC45MiwyMS45MDYxNDI0IDIwLjY5NjE0MjQsMjIuMTMgMjAuNDIsMjIuMTMgTDE4LjkyLDIyLjEzIEMxOC4zNjc3MTUzLDIyLjEzIDE3LjkyLDIxLjY4MjI4NDcgMTcuOTIsMjEuMTMgQzE3LjkxOTk3MjYsMjAuNTgxNTk3IDE4LjM2MTYyNDUsMjAuMTM1NDg0IDE4LjkxLDIwLjEzIFoiIGlkPSJTaGFwZSIgZmlsbD0iIzdDODdBNSIgb3BhY2l0eT0iMC41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxOS40MjAwMDAsIDIxLjEzMDAwMCkgcm90YXRlKC0xODAuMDAwMDAwKSB0cmFuc2xhdGUoLTE5LjQyMDAwMCwgLTIxLjEzMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik04LjIzLDE3LjUgTDUsMTcuNSBDNC43MjM4NTc2MywxNy41IDQuNSwxNy4yNzYxNDI0IDQuNSwxNyBMNC41LDE0LjUgTDEwLjUsMTQuNSBMOC42NSwxNy4yOCBDOC41NTQ2Njk2MSwxNy40MTc5MDgyIDguMzk3NjUwMDYsMTcuNTAwMTU2NiA4LjIzLDE3LjUgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjN0M4N0E1IiBvcGFjaXR5PSIwLjUiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggZD0iTTE4LjE1LDE4Ljg1IEwxNy42NSwxOS4zNSBDMTcuNTUyMzQxNiwxOS40NDQwNzU2IDE3LjQ5ODAzMzksMTkuNTc0NDE0MiAxNy41LDE5LjcxIEwxNy41LDIwIEMxNy41LDIwLjI3NjE0MjQgMTcuMjc2MTQyNCwyMC41IDE3LDIwLjUgTDE2LjUsMjAuNSBDMTYuMjIzODU3NiwyMC41IDE2LDIwLjI3NjE0MjQgMTYsMjAgQzE2LDE5LjcyMzg1NzYgMTUuNzc2MTQyNCwxOS41IDE1LjUsMTkuNSBMMTMuNSwxOS41IEMxMy4yMjM4NTc2LDE5LjUgMTMsMTkuNzIzODU3NiAxMywyMCBDMTMsMjAuMjc2MTQyNCAxMi43NzYxNDI0LDIwLjUgMTIuNSwyMC41IEwxMiwyMC41IEMxMS43MjM4NTc2LDIwLjUgMTEuNSwyMC4yNzYxNDI0IDExLjUsMjAgTDExLjUsMTkuNzEgQzExLjUwMTk2NjEsMTkuNTc0NDE0MiAxMS40NDc2NTg0LDE5LjQ0NDA3NTYgMTEuMzUsMTkuMzUgTDEwLjg1LDE4Ljg1IEMxMC42NTgyMTY3LDE4LjY1MjE4NjMgMTAuNjU4MjE2NywxOC4zMzc4MTM3IDEwLjg1LDE4LjE0IEwxMi4zNiwxNi42NSBDMTIuNDUwMjgwMywxNi41NTI4NjE3IDEyLjU3NzM5NjEsMTYuNDk4MzgzNSAxMi43MSwxNi41IEwxNi4yOSwxNi41IEMxNi40MjI2MDM5LDE2LjQ5ODM4MzUgMTYuNTQ5NzE5NywxNi41NTI4NjE3IDE2LjY0LDE2LjY1IEwxOC4xNSwxOC4xNCBDMTguMzQxNzgzMywxOC4zMzc4MTM3IDE4LjM0MTc4MzMsMTguNjUyMTg2MyAxOC4xNSwxOC44NSBaIiBpZD0iU2hhcGUiIGZpbGw9IiM3Qzg3QTUiIG9wYWNpdHk9IjAuNSI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMTAuODUsMjMuNDUgTDExLjM1LDIyLjk1IEMxMS40NDc2NTg0LDIyLjg1NTkyNDQgMTEuNTAxOTY2MSwyMi43MjU1ODU4IDExLjUsMjIuNTkgTDExLjUsMjIuMyBDMTEuNSwyMi4wMjM4NTc2IDExLjcyMzg1NzYsMjEuOCAxMiwyMS44IEwxMi41LDIxLjggQzEyLjc3NjE0MjQsMjEuOCAxMywyMi4wMjM4NTc2IDEzLDIyLjMgQzEzLDIyLjU3NjE0MjQgMTMuMjIzODU3NiwyMi44IDEzLjUsMjIuOCBMMTUuNSwyMi44IEMxNS43NzYxNDI0LDIyLjggMTYsMjIuNTc2MTQyNCAxNiwyMi4zIEMxNiwyMi4wMjM4NTc2IDE2LjIyMzg1NzYsMjEuOCAxNi41LDIxLjggTDE3LDIxLjggQzE3LjI3NjE0MjQsMjEuOCAxNy41LDIyLjAyMzg1NzYgMTcuNSwyMi4zIEwxNy41LDIyLjU5IEMxNy40OTgwMzM5LDIyLjcyNTU4NTggMTcuNTUyMzQxNiwyMi44NTU5MjQ0IDE3LjY1LDIyLjk1IEwxOC4xNSwyMy40NSBDMTguMzQwNTcxNCwyMy42NDQ0MjE4IDE4LjM0MDU3MTQsMjMuOTU1NTc4MiAxOC4xNSwyNC4xNSBMMTYuNjQsMjUuNjUgQzE2LjU0OTcxOTcsMjUuNzQ3MTM4MyAxNi40MjI2MDM5LDI1LjgwMTYxNjUgMTYuMjksMjUuOCBMMTIuNzEsMjUuOCBDMTIuNTc3Mzk2MSwyNS44MDE2MTY1IDEyLjQ1MDI4MDMsMjUuNzQ3MTM4MyAxMi4zNiwyNS42NSBMMTAuODUsMjQuMTUgQzEwLjY1OTQyODYsMjMuOTU1NTc4MiAxMC42NTk0Mjg2LDIzLjY0NDQyMTggMTAuODUsMjMuNDUgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjN0M4N0E1IiBvcGFjaXR5PSIwLjUiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggZD0iTTIxLjUsMjcuNSBMMjYuNSwyNy41IEwyNi41LDMxLjUgQzI2LjUsMzIuMDUyMjg0NyAyNi4wNTIyODQ3LDMyLjUgMjUuNSwzMi41IEwyMS41LDMyLjUgTDIxLjUsMjcuNSBaIiBpZD0iU2hhcGUiIHN0cm9rZT0iI0NDNEMyMyIgZmlsbD0iI0YxNUEyOSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48L3BhdGg+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4='; +/** + * String with Ev3 expected pairing pin. + * @readonly + */ +const Ev3PairingPin = '1234'; + /** * A maximum number of BT message sends per second, to be enforced by the rate limiter. * @type {number} @@ -586,7 +592,7 @@ class EV3 { */ connect (id) { if (this._bt) { - this._bt.connectPeripheral(id); + this._bt.connectPeripheral(id, Ev3PairingPin); } } 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..213930c11 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,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); } 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;