From 730b79868604ff7e7315e4e6a2031464b011dd3f Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Wed, 20 Jun 2018 13:59:23 -0400 Subject: [PATCH] Extension/runtime protocol refactoring. --- src/engine/runtime.js | 4 +- src/extensions/scratch3_microbit/index.js | 42 ++----- src/io/bleSession.js | 143 ++++++++++++++++++++++ src/io/{scratchBT.js => btSession.js} | 5 +- src/io/peripheralChooser.js | 48 -------- src/io/scratchBLE.js | 123 ------------------- 6 files changed, 158 insertions(+), 207 deletions(-) create mode 100644 src/io/bleSession.js rename src/io/{scratchBT.js => btSession.js} (92%) delete mode 100644 src/io/peripheralChooser.js delete mode 100644 src/io/scratchBLE.js diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 0fce732c8..2fb45660e 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -891,13 +891,13 @@ class Runtime extends EventEmitter { startDeviceScan (extensionId) { if (this.extensionDevices[extensionId]) { - this.extensionDevices[extensionId].startScan(); + this.extensionDevices[extensionId].requestDevice(); } } connectToPeripheral (extensionId, peripheralId) { if (this.extensionDevices[extensionId]) { - this.extensionDevices[extensionId].connectToPeripheral(peripheralId); + this.extensionDevices[extensionId].connectDevice(peripheralId); } } diff --git a/src/extensions/scratch3_microbit/index.js b/src/extensions/scratch3_microbit/index.js index 825e57b18..b5775350c 100644 --- a/src/extensions/scratch3_microbit/index.js +++ b/src/extensions/scratch3_microbit/index.js @@ -1,7 +1,7 @@ const ArgumentType = require('../../extension-support/argument-type'); const BlockType = require('../../extension-support/block-type'); const log = require('../../util/log'); -const ScratchBLE = require('../../io/scratchBLE'); +const BLESession = require('../../io/bleSession'); const Base64Util = require('../../util/base64-util'); /** @@ -62,17 +62,15 @@ class MicroBit { this._runtime = runtime; /** - * The ScratchBLE connection session for reading/writing device data. - * @type {ScratchBLE} + * The BluetoothLowEnergy connection session for reading/writing device data. + * @type {BLESession} * @private */ - this._ble = new ScratchBLE(this._runtime, { + this._ble = new BLESession(this._runtime, extensionId, { filters: [ {services: [BLEUUID.service]} ] - }); - - this._runtime.registerExtensionDevice(extensionId, this._ble); + }, this._onSessionConnect.bind(this)); /** * The most recently received value for each sensor. @@ -119,14 +117,14 @@ class MicroBit { for (let i = 0; i < text.length; i++) { output[i] = text.charCodeAt(i); } - this._writeBLE(BLECommand.CMD_DISPLAY_TEXT, output); + this._writeSessionData(BLECommand.CMD_DISPLAY_TEXT, output); } /** * @param {Uint8Array} matrix - the matrix to display. */ displayMatrix (matrix) { - this._writeBLE(BLECommand.CMD_DISPLAY_LED, matrix); + this._writeSessionData(BLECommand.CMD_DISPLAY_LED, matrix); } /** @@ -179,38 +177,20 @@ class MicroBit { return this._sensors.touchPins[pin]; } - /** - * Requests connection to a device when BLE session is ready. - */ - // _onBLEReady () { - // this._ble.requestDevice({ - // filters: [ - // {services: [BLEUUID.service]} - // ] - // }, this._onBLEConnect.bind(this), this._onBLEError); - // } - /** * Starts reading data from device after BLE has connected to it. */ - _onBLEConnect () { - const callback = this._processBLEData.bind(this); + _onSessionConnect () { + const callback = this._processSessionData.bind(this); this._ble.read(BLEUUID.service, BLEUUID.rxChar, true, callback); } - /** - * @param {string} e - Error from BLE session. - */ - _onBLEError (e) { - log.error(`BLE error: ${e}`); - } - /** * Process the sensor data from the incoming BLE characteristic. * @param {object} base64 - the incoming BLE data. * @private */ - _processBLEData (base64) { + _processSessionData (base64) { const data = Base64Util.base64ToUint8Array(base64); this._sensors.tiltX = data[1] | (data[0] << 8); @@ -234,7 +214,7 @@ class MicroBit { * @param {Uint8Array} message - the message to write. * @private */ - _writeBLE (command, message) { + _writeSessionData (command, message) { const output = new Uint8Array(message.length + 1); output[0] = command; // attach command to beginning of message for (let i = 0; i < message.length; i++) { diff --git a/src/io/bleSession.js b/src/io/bleSession.js new file mode 100644 index 000000000..2dc74a7c6 --- /dev/null +++ b/src/io/bleSession.js @@ -0,0 +1,143 @@ +const JSONRPCWebSocket = require('../util/jsonrpc-web-socket'); +const ScratchLinkWebSocket = 'ws://localhost:20110/scratch/ble'; + +class BLESession extends JSONRPCWebSocket { + + /** + * A BLE device session object. It handles connecting, over web sockets, to + * BLE devices, and reading and writing data to them. + * @param {Runtime} runtime - the Runtime for sending/receiving GUI update events. + * @param {string} extensionId - the id of the extension. + * @param {object} deviceOptions - the list of options for device discovery. + * @param {object} connectCallback - a callback for connection. + */ + constructor (runtime, extensionId, deviceOptions, connectCallback) { + const ws = new WebSocket(ScratchLinkWebSocket); + super(ws); + + this._socketPromise = new Promise((resolve, reject) => { + this._ws.onopen = resolve; + this._ws.onerror = this._sendError(); // TODO: socket error? + }); + + this._availablePeripherals = {}; + this._connectCallback = connectCallback; + this._characteristicDidChangeCallback = null; + this._deviceOptions = deviceOptions; + this._runtime = runtime; + this._ws = ws; + + this._runtime.registerExtensionDevice(extensionId, this); + } + + /** + * Request connection to the device. + * If the web socket is not yet open, request when the socket promise resolves. + */ + requestDevice () { + // TODO: add timeout for 'no devices yet found' ? + if (this._ws.readyState === 1) { + this.sendRemoteRequest('pingMe') // TODO: remove pingMe when no longer needed + .then(() => this.sendRemoteRequest('discover', this._deviceOptions)) + .catch(e => { + // TODO: what if discover doesn't initiate? + this._sendError(e); + }); + } else { + // Try again to connect to the websocket + this._socketPromise(this.sendRemoteRequest('pingMe') // TODO: remove pingMe when no longer needed + .then(() => this.sendRemoteRequest('discover', this._deviceOptions))) + .catch(e => { + // TODO: what if discover doesn't initiate? + this._sendError(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 + */ + connectDevice (id) { + this.sendRemoteRequest('connect', {peripheralId: id}) + .then(() => { + this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED); + this._connectCallback(); + }) + .catch(e => { + // TODO: what if the peripheral loses power? + // TODO: what if tries to connect to an unknown peripheral id? + this._sendError(e); + }); + } + + /** + * Handle a received call from the socket. + * @param {string} method - a received method label. + * @param {object} params - a received list of parameters. + * @return {object} - optional return value. + */ + didReceiveCall (method, params) { + // TODO: does didReceiveCall receive any errors? + // TODO: Add peripheral 'undiscover' handling + switch (method) { + case 'didDiscoverPeripheral': + this._availablePeripherals[params.peripheralId] = params; + this._runtime.emit( + this._runtime.constructor.PERIPHERAL_LIST_UPDATE, + this._availablePeripherals + ); + break; + case 'characteristicDidChange': + this._characteristicDidChangeCallback(params.message); + break; + case 'ping': + return 42; + } + } + + /** + * Start reading from the specified ble service. + * @param {number} serviceId - the ble service to read. + * @param {number} characteristicId - the ble characteristic to read. + * @param {boolean} optStartNotifications - whether to start receiving characteristic change notifications. + * @param {object} onCharacteristicChanged - callback for characteristic change notifications. + * @return {Promise} - a promise from the remote read request. + */ + read (serviceId, characteristicId, optStartNotifications = false, onCharacteristicChanged) { + const params = { + serviceId, + characteristicId + }; + if (optStartNotifications) { + params.startNotifications = true; + } + this._characteristicDidChangeCallback = onCharacteristicChanged; + return this.sendRemoteRequest('read', params); + } + + /** + * Write data to the specified ble service. + * @param {number} serviceId - the ble service to write. + * @param {number} characteristicId - the ble characteristic to write. + * @param {string} message - the message to send. + * @param {string} encoding - the message encoding type. + * @return {Promise} - a promise from the remote send request. + */ + write (serviceId, characteristicId, message, encoding = null) { + const params = {serviceId, characteristicId, message}; + if (encoding) { + params.encoding = encoding; + } + return this.sendRemoteRequest('write', params); + } + + _sendError (e) { + console.log(`BLESession error ${e}`); + // are there different error types? + // this._runtime.emit(???????????????) + } +} + +module.exports = BLESession; diff --git a/src/io/scratchBT.js b/src/io/btSession.js similarity index 92% rename from src/io/scratchBT.js rename to src/io/btSession.js index 2890b525d..2a24f93af 100644 --- a/src/io/scratchBT.js +++ b/src/io/btSession.js @@ -1,8 +1,7 @@ const JSONRPCWebSocket = require('../util/jsonrpc'); - const ScratchLinkWebSocket = 'ws://localhost:20110/scratch/bt'; -class ScratchBT extends JSONRPCWebSocket { +class BTSession extends JSONRPCWebSocket { constructor () { super(new WebSocket(ScratchLinkWebSocket)); } @@ -34,4 +33,4 @@ class ScratchBT extends JSONRPCWebSocket { } } -module.exports = ScratchBT; +module.exports = BTSession; diff --git a/src/io/peripheralChooser.js b/src/io/peripheralChooser.js deleted file mode 100644 index aa1d4d70f..000000000 --- a/src/io/peripheralChooser.js +++ /dev/null @@ -1,48 +0,0 @@ -class PeripheralChooser { - - get chosenPeripheralId () { - return this._chosenPeripheralId; - } - - constructor (runtime) { - this._runtime = runtime; - this._availablePeripherals = {}; - this._chosenPeripheralId = null; - } - - /** - * Launches a GUI menu to choose a peripheral. - * @return {Promise} - chosen peripheral promise. - */ - choosePeripheral () { - return new Promise((resolve, reject) => { - // TODO: Temporary: should launch gui instead. - this._tempPeripheralChosenCallback = resolve; - this._tempPeripheralChosenReject = reject; - }); - } - - /** - * Adds the peripheral ID to list of available peripherals. - * @param {object} info - the peripheral info object. - */ - addPeripheral (info) { - // Add a new peripheral, or if the id is already present, update it - this._availablePeripherals[info.peripheralId] = info; - - const peripheralArray = Object.keys(this._availablePeripherals).map(id => - this._availablePeripherals[id] - ); - - // @todo: sort peripherals by signal strength? or maybe not, so they don't jump around? - - this._runtime.emit(this._runtime.constructor.PERIPHERAL_LIST_UPDATE, peripheralArray); - - // TODO: Temporary: calls chosen callback on whatever peripherals are added. - // this._chosenPeripheralId = this._availablePeripherals[0]; - // this._tempPeripheralChosenCallback(this._chosenPeripheralId); - } - -} - -module.exports = PeripheralChooser; diff --git a/src/io/scratchBLE.js b/src/io/scratchBLE.js deleted file mode 100644 index 72b983110..000000000 --- a/src/io/scratchBLE.js +++ /dev/null @@ -1,123 +0,0 @@ -const JSONRPCWebSocket = require('../util/jsonrpc-web-socket'); -const PeripheralChooser = require('./peripheralChooser'); - -const ScratchLinkWebSocket = 'ws://localhost:20110/scratch/ble'; - -class ScratchBLE extends JSONRPCWebSocket { - constructor (runtime, deviceOptions) { - const ws = new WebSocket(ScratchLinkWebSocket); - super(ws); - - this._socketPromise = new Promise((resolve, reject) => { - this._ws.onopen = resolve; - this._ws.onerror = reject; - }); - - this._runtime = runtime; - - this._ws = ws; - this.peripheralChooser = new PeripheralChooser(this._runtime); // TODO: finalize gui connection - this._characteristicDidChange = null; - - this._deviceOptions = deviceOptions; - } - - // @todo handle websocket failed - startScan () { - console.log('BLE startScan', this._ws.readyState); - if (this._ws.readyState === 1) { - this.sendRemoteRequest('pingMe') - .then(() => this.requestDevice(this._deviceOptions)); - } else { - // Try again to connect to the websocket - this._socketPromise(this.sendRemoteRequest('pingMe') - .then(() => this.requestDevice(this._deviceOptions))); - } - } - - connectToPeripheral (id) { - this.sendRemoteRequest( - 'connect', - {peripheralId: id} - ).then(() => - this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED) - ); - } - - /** - * Request a device with the device options and optional gui options. - * @param {object} deviceOptions - list of device guiOptions. - * @param {object} onConnect - on connect callback. - * @param {object} onError - on error callbackk. - */ - requestDevice (deviceOptions, onConnect, onError) { - this.sendRemoteRequest('discover', deviceOptions) - .then(() => this.peripheralChooser.choosePeripheral()) // TODO: use gui options? - .then(id => this.sendRemoteRequest( - 'connect', - {peripheralId: id} - )) - .then( - onConnect, - onError - ); - } - - /** - * Handle a received call from the socket. - * @param {string} method - a received method label. - * @param {object} params - a received list of parameters. - * @return {object} - optional return value. - */ - didReceiveCall (method, params) { - // TODO: Add peripheral 'undiscover' handling - switch (method) { - case 'didDiscoverPeripheral': - this.peripheralChooser.addPeripheral(params); - break; - case 'characteristicDidChange': - this._characteristicDidChange(params.message); - break; - case 'ping': - return 42; - } - } - - /** - * Start reading from the specified ble service. - * @param {number} serviceId - the ble service to read. - * @param {number} characteristicId - the ble characteristic to read. - * @param {boolean} optStartNotifications - whether to start receiving characteristic change notifications. - * @param {object} onCharacteristicChanged - callback for characteristic change notifications. - * @return {Promise} - a promise from the remote read request. - */ - read (serviceId, characteristicId, optStartNotifications = false, onCharacteristicChanged) { - const params = { - serviceId, - characteristicId - }; - if (optStartNotifications) { - params.startNotifications = true; - } - this._characteristicDidChange = onCharacteristicChanged; - return this.sendRemoteRequest('read', params); - } - - /** - * Write data to the specified ble service. - * @param {number} serviceId - the ble service to write. - * @param {number} characteristicId - the ble characteristic to write. - * @param {string} message - the message to send. - * @param {string} encoding - the message encoding type. - * @return {Promise} - a promise from the remote send request. - */ - write (serviceId, characteristicId, message, encoding = null) { - const params = {serviceId, characteristicId, message}; - if (encoding) { - params.encoding = encoding; - } - return this.sendRemoteRequest('write', params); - } -} - -module.exports = ScratchBLE;