From d778ddf00ec9ae832d211cb126e977d2875ab07b Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 19 Jun 2018 17:59:03 -0400 Subject: [PATCH 01/25] wip --- src/engine/runtime.js | 34 ++++++++++++++++ src/extensions/scratch3_microbit/index.js | 36 ++++++++--------- src/io/peripheralChooser.js | 24 ++++++++---- src/io/scratchBLE.js | 47 ++++++++++++++++------- src/virtual-machine.js | 15 ++++++++ 5 files changed, 116 insertions(+), 40 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 1ba19570b..0fce732c8 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -254,6 +254,8 @@ class Runtime extends EventEmitter { video: new Video(this) }; + this.extensionDevices = {}; + /** * A runtime profiler that records timed events for later playback to * diagnose Scratch performance. @@ -394,6 +396,22 @@ class Runtime extends EventEmitter { return 'EXTENSION_ADDED'; } + /** + * Event name for updating the available set of peripheral devices. + * @const {string} + */ + static get PERIPHERAL_LIST_UPDATE () { + return 'PERIPHERAL_LIST_UPDATE'; + } + + /** + * Event name for reporting that a peripheral has connected. + * @const {string} + */ + static get PERIPHERAL_CONNECTED () { + return 'PERIPHERAL_CONNECTED'; + } + /** * Event name for reporting that blocksInfo was updated. * @const {string} @@ -867,6 +885,22 @@ class Runtime extends EventEmitter { (result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []); } + registerExtensionDevice (extensionId, device) { + this.extensionDevices[extensionId] = device; + } + + startDeviceScan (extensionId) { + if (this.extensionDevices[extensionId]) { + this.extensionDevices[extensionId].startScan(); + } + } + + connectToPeripheral (extensionId, peripheralId) { + if (this.extensionDevices[extensionId]) { + this.extensionDevices[extensionId].connectToPeripheral(peripheralId); + } + } + /** * Retrieve the function associated with the given opcode. * @param {!string} opcode The opcode to look up. diff --git a/src/extensions/scratch3_microbit/index.js b/src/extensions/scratch3_microbit/index.js index a121b43dc..825e57b18 100644 --- a/src/extensions/scratch3_microbit/index.js +++ b/src/extensions/scratch3_microbit/index.js @@ -50,8 +50,9 @@ class MicroBit { /** * Construct a MicroBit communication object. * @param {Runtime} runtime - the Scratch 3.0 runtime + * @param {string} extensionId - the id of the extension */ - constructor (runtime) { + constructor (runtime, extensionId) { /** * The Scratch 3.0 runtime used to trigger the green flag button. @@ -65,7 +66,13 @@ class MicroBit { * @type {ScratchBLE} * @private */ - this._ble = new ScratchBLE(); + this._ble = new ScratchBLE(this._runtime, { + filters: [ + {services: [BLEUUID.service]} + ] + }); + + this._runtime.registerExtensionDevice(extensionId, this._ble); /** * The most recently received value for each sensor. @@ -102,15 +109,6 @@ class MicroBit { timeout: false } }; - - // TODO: Temporary until the gui requests a device connection - this._ble.waitForSocket() - // TODO: remove pinging once no longer needed - .then(() => this._ble.sendRemoteRequest('pingMe')) - .then(() => this._onBLEReady()); - - // TODO: Add ScratchBLE 'disconnect' handling - } /** @@ -184,13 +182,13 @@ class MicroBit { /** * Requests connection to a device when BLE session is ready. */ - _onBLEReady () { - this._ble.requestDevice({ - filters: [ - {services: [BLEUUID.service]} - ] - }, this._onBLEConnect.bind(this), this._onBLEError); - } + // _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. @@ -318,7 +316,7 @@ class Scratch3MicroBitBlocks { this.runtime = runtime; // Create a new MicroBit device instance - this._device = new MicroBit(this.runtime); + this._device = new MicroBit(this.runtime, Scratch3MicroBitBlocks.EXTENSION_ID); } /** diff --git a/src/io/peripheralChooser.js b/src/io/peripheralChooser.js index 53d0a50bc..aa1d4d70f 100644 --- a/src/io/peripheralChooser.js +++ b/src/io/peripheralChooser.js @@ -4,8 +4,9 @@ class PeripheralChooser { return this._chosenPeripheralId; } - constructor () { - this._availablePeripherals = []; // TODO for use in gui? + constructor (runtime) { + this._runtime = runtime; + this._availablePeripherals = {}; this._chosenPeripheralId = null; } @@ -23,14 +24,23 @@ class PeripheralChooser { /** * Adds the peripheral ID to list of available peripherals. - * @param {number} peripheralId - the id to add. + * @param {object} info - the peripheral info object. */ - addPeripheral (peripheralId) { - this._availablePeripherals.push(peripheralId); + 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); + // this._chosenPeripheralId = this._availablePeripherals[0]; + // this._tempPeripheralChosenCallback(this._chosenPeripheralId); } } diff --git a/src/io/scratchBLE.js b/src/io/scratchBLE.js index 36bd9a7da..72b983110 100644 --- a/src/io/scratchBLE.js +++ b/src/io/scratchBLE.js @@ -4,25 +4,44 @@ const PeripheralChooser = require('./peripheralChooser'); const ScratchLinkWebSocket = 'ws://localhost:20110/scratch/ble'; class ScratchBLE extends JSONRPCWebSocket { - constructor () { + constructor (runtime, deviceOptions) { const ws = new WebSocket(ScratchLinkWebSocket); - super(ws); - this._ws = ws; - this.peripheralChooser = new PeripheralChooser(); // TODO: finalize gui connection - this._characteristicDidChange = null; - } - - /** - * Returns a promise for when the web socket opens. - * @return {Promise} - a promise when BLE socket is open. - */ - waitForSocket () { - return new Promise((resolve, reject) => { + 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) + ); } /** @@ -54,7 +73,7 @@ class ScratchBLE extends JSONRPCWebSocket { // TODO: Add peripheral 'undiscover' handling switch (method) { case 'didDiscoverPeripheral': - this.peripheralChooser.addPeripheral(params.peripheralId); + this.peripheralChooser.addPeripheral(params); break; case 'characteristicDidChange': this._characteristicDidChange(params.message); diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 922c2fc20..a8fca37fa 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -106,6 +106,13 @@ class VirtualMachine extends EventEmitter { this.emit(Runtime.BLOCKSINFO_UPDATE, blocksInfo); }); + this.runtime.on(Runtime.PERIPHERAL_LIST_UPDATE, info => { + this.emit(Runtime.PERIPHERAL_LIST_UPDATE, info); + }); + this.runtime.on(Runtime.PERIPHERAL_CONNECTED, () => + this.emit(Runtime.PERIPHERAL_CONNECTED) + ); + this.extensionManager = new ExtensionManager(this.runtime); this.blockListener = this.blockListener.bind(this); @@ -195,6 +202,14 @@ class VirtualMachine extends EventEmitter { this.runtime.ioDevices.video.setProvider(videoProvider); } + startDeviceScan (extensionId) { + this.runtime.startDeviceScan(extensionId); + } + + connectToPeripheral (extensionId, peripheralId) { + this.runtime.connectToPeripheral(extensionId, peripheralId); + } + /** * Load a Scratch project from a .sb, .sb2, .sb3 or json string. * @param {string | object} input A json string, object, or ArrayBuffer representing the project to load. From 730b79868604ff7e7315e4e6a2031464b011dd3f Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Wed, 20 Jun 2018 13:59:23 -0400 Subject: [PATCH 02/25] 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; From 701f430e565665a17930b40ef4acb616272b541c Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Wed, 20 Jun 2018 21:20:18 -0400 Subject: [PATCH 03/25] Remove pinging in BLE connection. --- src/io/bleSession.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/io/bleSession.js b/src/io/bleSession.js index 2dc74a7c6..af4f67b92 100644 --- a/src/io/bleSession.js +++ b/src/io/bleSession.js @@ -37,16 +37,14 @@ class BLESession extends JSONRPCWebSocket { 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)) + 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))) + this._socketPromise(this.sendRemoteRequest('discover', this._deviceOptions)) .catch(e => { // TODO: what if discover doesn't initiate? this._sendError(e); @@ -115,6 +113,7 @@ class BLESession extends JSONRPCWebSocket { } this._characteristicDidChangeCallback = onCharacteristicChanged; return this.sendRemoteRequest('read', params); + // TODO: handle error here } /** @@ -131,6 +130,8 @@ class BLESession extends JSONRPCWebSocket { params.encoding = encoding; } return this.sendRemoteRequest('write', params); + // TODO: .then() to clear a busy flag cuz it returned + // TODO: send error to runtime to stop yielding } _sendError (e) { From 73cc149783d27d8dfb497116f02c5cdafbaa6bf7 Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Thu, 21 Jun 2018 14:36:10 -0400 Subject: [PATCH 04/25] Adding promises to return on BLE write. Some refactoring. --- src/extensions/scratch3_microbit/index.js | 19 +++++++++++-------- src/io/bleSession.js | 18 +++++++----------- src/util/jsonrpc-web-socket.js | 4 ++++ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/extensions/scratch3_microbit/index.js b/src/extensions/scratch3_microbit/index.js index b5775350c..ae131a431 100644 --- a/src/extensions/scratch3_microbit/index.js +++ b/src/extensions/scratch3_microbit/index.js @@ -111,20 +111,22 @@ class MicroBit { /** * @param {string} text - the text to display. + * @return {Promise} - a Promise that resolves when writing to device. */ displayText (text) { const output = new Uint8Array(text.length); for (let i = 0; i < text.length; i++) { output[i] = text.charCodeAt(i); } - this._writeSessionData(BLECommand.CMD_DISPLAY_TEXT, output); + return this._writeSessionData(BLECommand.CMD_DISPLAY_TEXT, output); } /** * @param {Uint8Array} matrix - the matrix to display. + * @return {Promise} - a Promise that resolves when writing to device. */ displayMatrix (matrix) { - this._writeSessionData(BLECommand.CMD_DISPLAY_LED, matrix); + return this._writeSessionData(BLECommand.CMD_DISPLAY_LED, matrix); } /** @@ -212,6 +214,7 @@ class MicroBit { * Write a message to the device BLE session. * @param {number} command - the BLE command hex. * @param {Uint8Array} message - the message to write. + * @return {Promise} - a Promise that resolves when writing to device. * @private */ _writeSessionData (command, message) { @@ -220,8 +223,8 @@ class MicroBit { for (let i = 0; i < message.length; i++) { output[i + 1] = message[i]; } - const b64enc = Base64Util.uint8ArrayToBase64(output); - this._ble.write(BLEUUID.service, BLEUUID.txChar, b64enc, 'base64'); + const data = Base64Util.uint8ArrayToBase64(output); + return this._ble.write(BLEUUID.service, BLEUUID.txChar, data, 'base64'); } } @@ -494,17 +497,18 @@ class Scratch3MicroBitBlocks { /** * Display text on the 5x5 LED matrix. * @param {object} args - the block's arguments. + * @return {Promise} - a Promise that resolves when writing to device. * Note the limit is 19 characters */ displayText (args) { const text = String(args.TEXT).substring(0, 19); - this._device.displayText(text); - return; + return this._device.displayText(text); } /** * Display a predefined symbol on the 5x5 LED matrix. * @param {object} args - the block's arguments. + * @return {Promise} - a Promise that resolves when writing to device. */ displaySymbol (args) { const hex = symbols2hex[args.SYMBOL]; @@ -514,8 +518,7 @@ class Scratch3MicroBitBlocks { this._device.ledMatrixState[2] = (hex >> 10) & 0x1F; this._device.ledMatrixState[3] = (hex >> 5) & 0x1F; this._device.ledMatrixState[4] = hex & 0x1F; - this._device.displayMatrix(this._device.ledMatrixState); - return; + return this._device.displayMatrix(this._device.ledMatrixState); } /** diff --git a/src/io/bleSession.js b/src/io/bleSession.js index af4f67b92..75eafe7e1 100644 --- a/src/io/bleSession.js +++ b/src/io/bleSession.js @@ -18,6 +18,7 @@ class BLESession extends JSONRPCWebSocket { this._socketPromise = new Promise((resolve, reject) => { this._ws.onopen = resolve; this._ws.onerror = this._sendError(); // TODO: socket error? + // TODO: generally handle socket disconnects as errors }); this._availablePeripherals = {}; @@ -37,18 +38,14 @@ class BLESession extends JSONRPCWebSocket { requestDevice () { // TODO: add timeout for 'no devices yet found' ? if (this._ws.readyState === 1) { + // TODO: what if discover doesn't initiate? this.sendRemoteRequest('discover', this._deviceOptions) - .catch(e => { - // TODO: what if discover doesn't initiate? - this._sendError(e); - }); + .catch(e => this._sendError(e)); } else { // Try again to connect to the websocket + // TODO: what if discover doesn't initiate? this._socketPromise(this.sendRemoteRequest('discover', this._deviceOptions)) - .catch(e => { - // TODO: what if discover doesn't initiate? - this._sendError(e); - }); + .catch(e => this._sendError(e)); } } @@ -64,7 +61,7 @@ class BLESession extends JSONRPCWebSocket { this._connectCallback(); }) .catch(e => { - // TODO: what if the peripheral loses power? + // TODO: what if the peripheral loses power? (web socket closes?) // TODO: what if tries to connect to an unknown peripheral id? this._sendError(e); }); @@ -77,8 +74,7 @@ class BLESession extends JSONRPCWebSocket { * @return {object} - optional return value. */ didReceiveCall (method, params) { - // TODO: does didReceiveCall receive any errors? - // TODO: Add peripheral 'undiscover' handling + // TODO: Add peripheral 'undiscover' handling with timeout? switch (method) { case 'didDiscoverPeripheral': this._availablePeripherals[params.peripheralId] = params; diff --git a/src/util/jsonrpc-web-socket.js b/src/util/jsonrpc-web-socket.js index 22310d337..edfdf9373 100644 --- a/src/util/jsonrpc-web-socket.js +++ b/src/util/jsonrpc-web-socket.js @@ -27,6 +27,10 @@ class JSONRPCWebSocket extends JSONRPC { _onSocketMessage (e) { const json = JSON.parse(e.data); + if (json.method !== 'characteristicDidChange') { + console.log('received message: '); + console.log(json); + } this._handleMessage(json); } From 9d66c54f8a7ac2ea3524fe941524700cc8b50ec3 Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Thu, 21 Jun 2018 15:18:56 -0400 Subject: [PATCH 05/25] Adding error reporting to runtime from BLESession. --- src/engine/runtime.js | 8 ++++++++ src/io/bleSession.js | 8 +++----- src/virtual-machine.js | 3 +++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 2fb45660e..6dd1dc219 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -412,6 +412,14 @@ class Runtime extends EventEmitter { return 'PERIPHERAL_CONNECTED'; } + /** + * Event name for reporting that a peripheral has encountered an error. + * @const {string} + */ + static get PERIPHERAL_ERROR () { + return 'PERIPHERAL_ERROR'; + } + /** * Event name for reporting that blocksInfo was updated. * @const {string} diff --git a/src/io/bleSession.js b/src/io/bleSession.js index 75eafe7e1..ebe150b1a 100644 --- a/src/io/bleSession.js +++ b/src/io/bleSession.js @@ -17,7 +17,7 @@ class BLESession extends JSONRPCWebSocket { this._socketPromise = new Promise((resolve, reject) => { this._ws.onopen = resolve; - this._ws.onerror = this._sendError(); // TODO: socket error? + this._ws.onerror = this._sendError; // TODO: socket error? // TODO: generally handle socket disconnects as errors }); @@ -27,6 +27,7 @@ class BLESession extends JSONRPCWebSocket { this._deviceOptions = deviceOptions; this._runtime = runtime; this._ws = ws; + this._ws.onclose = this._sendError(); this._runtime.registerExtensionDevice(extensionId, this); } @@ -126,14 +127,11 @@ class BLESession extends JSONRPCWebSocket { params.encoding = encoding; } return this.sendRemoteRequest('write', params); - // TODO: .then() to clear a busy flag cuz it returned - // TODO: send error to runtime to stop yielding } _sendError (e) { console.log(`BLESession error ${e}`); - // are there different error types? - // this._runtime.emit(???????????????) + this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); } } diff --git a/src/virtual-machine.js b/src/virtual-machine.js index a8fca37fa..dc53ce800 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -112,6 +112,9 @@ class VirtualMachine extends EventEmitter { this.runtime.on(Runtime.PERIPHERAL_CONNECTED, () => this.emit(Runtime.PERIPHERAL_CONNECTED) ); + this.runtime.on(Runtime.PERIPHERAL_ERROR, () => + this.emit(Runtime.PERIPHERAL_ERROR) + ); this.extensionManager = new ExtensionManager(this.runtime); From 4332725d3392f5a8dfab4fd3c7c5f7e838479592 Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Sun, 24 Jun 2018 22:00:23 -0400 Subject: [PATCH 06/25] Adding more errors based on web socket onerror and onclose. Moved extension runtime mapping of devices back to MicroBit class. --- src/engine/runtime.js | 2 +- src/extensions/scratch3_microbit/index.js | 29 +++++++++++++++--- src/io/bleSession.js | 37 ++++++++--------------- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 6dd1dc219..64e469e05 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -899,7 +899,7 @@ class Runtime extends EventEmitter { startDeviceScan (extensionId) { if (this.extensionDevices[extensionId]) { - this.extensionDevices[extensionId].requestDevice(); + this.extensionDevices[extensionId].startDeviceScan(); } } diff --git a/src/extensions/scratch3_microbit/index.js b/src/extensions/scratch3_microbit/index.js index ae131a431..0cb09775b 100644 --- a/src/extensions/scratch3_microbit/index.js +++ b/src/extensions/scratch3_microbit/index.js @@ -66,11 +66,8 @@ class MicroBit { * @type {BLESession} * @private */ - this._ble = new BLESession(this._runtime, extensionId, { - filters: [ - {services: [BLEUUID.service]} - ] - }, this._onSessionConnect.bind(this)); + this._ble = null; + this._runtime.registerExtensionDevice(extensionId, this); /** * The most recently received value for each sensor. @@ -109,6 +106,28 @@ class MicroBit { }; } + // TODO: keep here? + /** + * Called by the runtime when user wants to scan for a device. + */ + startDeviceScan () { + console.log('making a new BLE session'); + this._ble = new BLESession(this._runtime, { + filters: [ + {services: [BLEUUID.service]} + ] + }, this._onSessionConnect.bind(this)); + } + + // TODO: keep here? + /** + * Called by the runtime when user wants to connect to a certain device. + * @param {number} id - the id of the device to connect to. + */ + connectDevice (id) { + this._ble.connectDevice(id); + } + /** * @param {string} text - the text to display. * @return {Promise} - a Promise that resolves when writing to device. diff --git a/src/io/bleSession.js b/src/io/bleSession.js index ebe150b1a..8cb95f78f 100644 --- a/src/io/bleSession.js +++ b/src/io/bleSession.js @@ -7,29 +7,23 @@ 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) { + constructor (runtime, 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? - // TODO: generally handle socket disconnects as errors - }); + this._ws = ws; + this._ws.onopen = this.requestDevice.bind(this); // only call request device after socket opens + this._ws.onerror = this._sendError.bind(this, 'ws onerror'); + this._ws.onclose = this._sendError.bind(this, 'ws onclose'); this._availablePeripherals = {}; this._connectCallback = connectCallback; this._characteristicDidChangeCallback = null; this._deviceOptions = deviceOptions; this._runtime = runtime; - this._ws = ws; - this._ws.onclose = this._sendError(); - - this._runtime.registerExtensionDevice(extensionId, this); } /** @@ -37,17 +31,12 @@ class BLESession extends JSONRPCWebSocket { * 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) { - // TODO: what if discover doesn't initiate? + if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen? + // TODO: start a 'discover' timeout this.sendRemoteRequest('discover', this._deviceOptions) - .catch(e => this._sendError(e)); - } else { - // Try again to connect to the websocket - // TODO: what if discover doesn't initiate? - this._socketPromise(this.sendRemoteRequest('discover', this._deviceOptions)) - .catch(e => this._sendError(e)); + .catch(e => this._sendError('error on discover')); // never reached? } + // TODO: else? } /** @@ -58,12 +47,11 @@ class BLESession extends JSONRPCWebSocket { connectDevice (id) { this.sendRemoteRequest('connect', {peripheralId: id}) .then(() => { + console.log('should have connected'); this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED); this._connectCallback(); }) .catch(e => { - // TODO: what if the peripheral loses power? (web socket closes?) - // TODO: what if tries to connect to an unknown peripheral id? this._sendError(e); }); } @@ -75,7 +63,6 @@ class BLESession extends JSONRPCWebSocket { * @return {object} - optional return value. */ didReceiveCall (method, params) { - // TODO: Add peripheral 'undiscover' handling with timeout? switch (method) { case 'didDiscoverPeripheral': this._availablePeripherals[params.peripheralId] = params; @@ -83,6 +70,7 @@ class BLESession extends JSONRPCWebSocket { this._runtime.constructor.PERIPHERAL_LIST_UPDATE, this._availablePeripherals ); + // TODO: cancel a discover timeout if one is active break; case 'characteristicDidChange': this._characteristicDidChangeCallback(params.message); @@ -130,7 +118,8 @@ class BLESession extends JSONRPCWebSocket { } _sendError (e) { - console.log(`BLESession error ${e}`); + console.log(`BLESession error:`); + console.log(e); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); } } From 2d8ad05a781db3a941b6f9f40c42333bcf3c2760 Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Mon, 25 Jun 2018 13:42:48 -0400 Subject: [PATCH 07/25] Adding Ev3 extension with three test blocks to latest device-connection work, auto-connecting to device for now. --- src/extension-support/extension-manager.js | 4 +- src/extensions/scratch3_ev3/index.js | 381 +++++++++++++++++++++ src/extensions/scratch3_microbit/index.js | 2 +- src/io/bleSession.js | 9 +- src/io/btSession.js | 80 ++++- src/util/base64-util.js | 15 + 6 files changed, 476 insertions(+), 15 deletions(-) create mode 100644 src/extensions/scratch3_ev3/index.js diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index cff9f4f0e..7a004104f 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -15,6 +15,7 @@ const Scratch3SpeakBlocks = require('../extensions/scratch3_speak'); const Scratch3TranslateBlocks = require('../extensions/scratch3_translate'); const Scratch3VideoSensingBlocks = require('../extensions/scratch3_video_sensing'); const Scratch3SpeechBlocks = require('../extensions/scratch3_speech'); +const Scratch3Ev3Blocks = require('../extensions/scratch3_ev3'); const builtinExtensions = { pen: Scratch3PenBlocks, @@ -24,7 +25,8 @@ const builtinExtensions = { speak: Scratch3SpeakBlocks, translate: Scratch3TranslateBlocks, videoSensing: Scratch3VideoSensingBlocks, - speech: Scratch3SpeechBlocks + speech: Scratch3SpeechBlocks, + ev3: Scratch3Ev3Blocks }; /** diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js new file mode 100644 index 000000000..aec3f4b20 --- /dev/null +++ b/src/extensions/scratch3_ev3/index.js @@ -0,0 +1,381 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Cast = require('../../util/cast'); +const log = require('../../util/log'); +const Base64Util = require('../../util/base64-util'); +const BTSession = require('../../io/BTSession'); + +/** + * High-level primitives / constants used by the extension. + * @type {object} + */ +const BTCommand = { + LAYER: 0x00, + NUM8: 0x81, + NUM16: 0x82, + NUM32: 0x83, + COAST: 0x0, + BRAKE: 0x1, + LONGRAMP: 50, + STEPSPEED: 0xAE, + TIMESPEED: 0xAF, + OUTPUTSTOP: 0xA3, + OUTPUTRESET: 0xA2, + STEPSPEEDSYNC: 0xB0, + TIMESPEEDSYNC: 0xB1 +}; + +/** + * Array of accepted motor ports. + * @note These should not be translated as they correspond to labels on + * the EV3 hub. + * @type {array} + */ +const MOTOR_PORTS = [ + { + name: 'A', + value: 1 + }, + { + name: 'B', + value: 2 + }, + { + name: 'C', + value: 4 + }, + { + name: 'D', + value: 8 + } +]; + +/** + * Array of accepted sensor ports. + * @note These should not be translated as they correspond to labels on + * the EV3 hub. + * @type {array} + */ +// const SENSOR_PORTS = ['1', '2', '3', '4']; + +class EV3 { + + constructor (runtime, extensionId) { + + /** + * The Scratch 3.0 runtime used to trigger the green flag button. + * @type {Runtime} + * @private + */ + this._runtime = runtime; + + this.connected = false; + this.speed = 50; + + /** + * The Bluetooth connection session for reading/writing device data. + * @type {BTSession} + * @private + */ + this._bt = null; + this._runtime.registerExtensionDevice(extensionId, this); + + // TODO: auto-connect temporary - until button is added + this.startDeviceScan(); + } + + // TODO: keep here? + /** + * Called by the runtime when user wants to scan for a device. + */ + startDeviceScan () { + log.info('making a new BT session'); + this._bt = new BTSession(this._runtime, { + majorDeviceClass: 8, + minorDeviceClass: 1 + }, this._onSessionConnect.bind(this)); + } + + // TODO: keep here? + /** + * Called by the runtime when user wants to connect to a certain device. + * @param {number} id - the id of the device to connect to. + */ + connectDevice (id) { + this._bt.connectDevice(id); + } + + beep () { + if (!this.connected) return; + this._bt.sendMessage({ + message: 'DwAAAIAAAJQBgQKC6AOC6AM=', + encoding: 'base64' + }); + } + + motorTurnClockwise (port, time) { + if (!this.connected) return; + + // Build up motor command + const cmd = this._applyPrefix(0, this._motorCommand( + BTCommand.TIMESPEED, + port, + time, + this.speed, + BTCommand.LONGRAMP + )); + + // Send message + this._bt.sendMessage({ + message: Base64Util.arrayBufferToBase64(cmd), + encoding: 'base64' + }); + + // Yield for time + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, time); + }); + } + + motorTurnCounterClockwise (port, time) { + if (!this.connected) return; + + // Build up motor command + const cmd = this._applyPrefix(0, this._motorCommand( + BTCommand.TIMESPEED, + port, + time, + this.speed * -1, + BTCommand.LONGRAMP + )); + + // Send message + this._bt.sendMessage({ + message: Base64Util.arrayBufferToBase64(cmd), + encoding: 'base64' + }); + + // Yield for time + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, time); + }); + } + + _applyPrefix (n, cmd) { + const len = cmd.length + 5; + return [].concat( + len & 0xFF, + (len >> 8) & 0xFF, + 0x1, + 0x0, + 0x0, + n, + 0x0, + cmd + ); + } + + /** + * Generate a motor command in EV3 byte array format (CMD, LAYER, PORT, + * SPEED, RAMP UP, RUN, RAMP DOWN, BREAKING TYPE) + * @param {string} command Motor command primitive (i.e. "prefix") + * @param {string} port Port to address + * @param {number} n Value to be passed to motor command + * @param {number} speed Speed value + * @param {number} ramp Ramp value + * @return {array} Byte array + */ + _motorCommand (command, port, n, speed, ramp) { + /** + * Generate run values for a given input. + * @param {number} run Run input + * @return {array} Run values (byte array) + */ + const getRunValues = function (run) { + // If run duration is less than max 16-bit integer + if (run < 0x7fff) { + return [ + BTCommand.NUM16, + run & 0xff, + (run >> 8) & 0xff + ]; + } + + // Run forever + return [ + BTCommand.NUM32, + run & 0xff, + (run >> 8) & 0xff, + (run >> 16) & 0xff, + (run >> 24) & 0xff + ]; + }; + + // If speed is less than zero, make it positive and multiply the input + // value by -1 + if (speed < 0) { + speed = -1 * speed; + n = -1 * n; + } + + // If the input value is less than 0 + const dir = (n < 0) ? 0x100 - speed : speed; // step negative or possitive + n = Math.abs(n); + + // Setup motor run duration and ramping behavior + let rampup = ramp; + let rampdown = ramp; + let run = n - ramp * 2; + if (run < 0) { + rampup = Math.floor(n / 2); + run = 0; + rampdown = n - rampup; + } + + // Generate motor command + const runcmd = getRunValues(run); + return [ + command, + BTCommand.LAYER, + port, + BTCommand.NUM8, + dir & 0xff, + BTCommand.NUM8, + rampup + ].concat(runcmd.concat([ + BTCommand.NUM8, + rampdown, + BTCommand.BRAKE + ])); + } + + _onSessionConnect () { + log.info('bt device connected!'); + this.connected = true; + // start reading data? + } + +} + +class Scratch3Ev3Blocks { + + /** + * The ID of the extension. + * @return {string} the id + */ + static get EXTENSION_ID () { + return 'ev3'; + } + + /** + * Creates a new instance of the EV3 extension. + * @param {object} runtime VM runtime + * @constructor + */ + constructor (runtime) { + /** + * The Scratch 3.0 runtime. + * @type {Runtime} + */ + this.runtime = runtime; + + // Create a new MicroBit device instance + this._device = new EV3(this.runtime, Scratch3Ev3Blocks.EXTENSION_ID); + } + + /** + * Define the EV3 extension. + * @return {object} Extension description. + */ + getInfo () { + return { + id: Scratch3Ev3Blocks.EXTENSION_ID, + name: 'LEGO MINDSTORMS EV3', + iconURI: null, + blocks: [ + { + opcode: 'motorTurnClockwise', + text: '[PORT] turn clockwise [TIME] seconds', + blockType: BlockType.COMMAND, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'motorPorts', + defaultValue: MOTOR_PORTS[0].value + }, + TIME: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'motorTurnCounterClockwise', + text: '[PORT] turn counter [TIME] seconds', + blockType: BlockType.COMMAND, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'motorPorts', + defaultValue: MOTOR_PORTS[0].value + }, + TIME: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'beep', + text: 'beep', + blockType: BlockType.COMMAND + } + ], + menus: { + motorPorts: this._buildMenu(MOTOR_PORTS) + } + }; + } + + /** + * Create data for a menu in scratch-blocks format, consisting of an array of objects with text and + * value properties. The text is a translated string, and the value is one-indexed. + * @param {object[]} info - An array of info objects each having a name property. + * @return {array} - An array of objects with text and value properties. + * @private + */ + _buildMenu (info) { + return info.map((entry, index) => { + const obj = {}; + obj.text = entry.name; + obj.value = String(index + 1); + return obj; + }); + } + + motorTurnClockwise (args) { + // Validate arguments + const port = Cast.toNumber(args.PORT); + const time = Cast.toNumber(args.TIME) * 1000; + + this._device.motorTurnClockwise(port, time); + } + + motorTurnCounterClockwise (args) { + // Validate arguments + const port = Cast.toNumber(args.PORT); + const time = Cast.toNumber(args.TIME) * 1000; + + this._device.motorTurnCounterClockwise(port, time); + } + + beep () { + return this._device.beep(); + } +} + +module.exports = Scratch3Ev3Blocks; diff --git a/src/extensions/scratch3_microbit/index.js b/src/extensions/scratch3_microbit/index.js index 0cb09775b..22203ccb4 100644 --- a/src/extensions/scratch3_microbit/index.js +++ b/src/extensions/scratch3_microbit/index.js @@ -111,7 +111,7 @@ class MicroBit { * Called by the runtime when user wants to scan for a device. */ startDeviceScan () { - console.log('making a new BLE session'); + log.info('making a new BLE session'); this._ble = new BLESession(this._runtime, { filters: [ {services: [BLEUUID.service]} diff --git a/src/io/bleSession.js b/src/io/bleSession.js index 8cb95f78f..9a4cd1aab 100644 --- a/src/io/bleSession.js +++ b/src/io/bleSession.js @@ -1,4 +1,5 @@ const JSONRPCWebSocket = require('../util/jsonrpc-web-socket'); +const log = require('../util/log'); const ScratchLinkWebSocket = 'ws://localhost:20110/scratch/ble'; class BLESession extends JSONRPCWebSocket { @@ -34,7 +35,7 @@ class BLESession extends JSONRPCWebSocket { if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen? // TODO: start a 'discover' timeout this.sendRemoteRequest('discover', this._deviceOptions) - .catch(e => this._sendError('error on discover')); // never reached? + .catch(e => this._sendError(e)); // never reached? } // TODO: else? } @@ -47,7 +48,7 @@ class BLESession extends JSONRPCWebSocket { connectDevice (id) { this.sendRemoteRequest('connect', {peripheralId: id}) .then(() => { - console.log('should have connected'); + log.info('should have connected'); this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED); this._connectCallback(); }) @@ -118,8 +119,8 @@ class BLESession extends JSONRPCWebSocket { } _sendError (e) { - console.log(`BLESession error:`); - console.log(e); + log.error(`BLESession error:`); + log.error(e); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); } } diff --git a/src/io/btSession.js b/src/io/btSession.js index 2a24f93af..3d52189cb 100644 --- a/src/io/btSession.js +++ b/src/io/btSession.js @@ -1,28 +1,84 @@ -const JSONRPCWebSocket = require('../util/jsonrpc'); +const JSONRPCWebSocket = require('../util/jsonrpc-web-socket'); +const log = require('../util/log'); const ScratchLinkWebSocket = 'ws://localhost:20110/scratch/bt'; class BTSession extends JSONRPCWebSocket { - constructor () { - super(new WebSocket(ScratchLinkWebSocket)); + + /** + * A BT device session object. It handles connecting, over web sockets, to + * BT devices, and reading and writing data to them. + * @param {Runtime} runtime - the Runtime for sending/receiving GUI update events. + * @param {object} deviceOptions - the list of options for device discovery. + * @param {object} connectCallback - a callback for connection. + */ + constructor (runtime, deviceOptions, connectCallback) { + const ws = new WebSocket(ScratchLinkWebSocket); + super(ws); + + this._ws = ws; + this._ws.onopen = this.requestDevice.bind(this); // only call request device after socket opens + this._ws.onerror = this._sendError.bind(this, 'ws onerror'); + this._ws.onclose = this._sendError.bind(this, 'ws onclose'); + + this._availablePeripherals = {}; + this._connectCallback = connectCallback; + this._characteristicDidChangeCallback = null; + this._deviceOptions = deviceOptions; + this._runtime = runtime; } - requestDevice (options) { - return this.sendRemoteRequest('discover', options); + /** + * Request connection to the device. + * If the web socket is not yet open, request when the socket promise resolves. + */ + requestDevice () { + if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen? + // TODO: start a 'discover' timeout + this.sendRemoteRequest('discover', this._deviceOptions) + .catch(e => this._sendError(e)); // never reached? + } + // TODO: else? } - connectDevice (options) { - return this.sendRemoteRequest('connect', options); + /** + * 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(() => { + log.info('should have connected'); + this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED); + this._connectCallback(); + }) + .catch(e => { + this._sendError(e); + }); } sendMessage (options) { return this.sendRemoteRequest('send', options); } - didReceiveCall (method /* , params */) { + /** + * 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': - // TODO: do something on peripheral discovered + /* this._availablePeripherals[params.peripheralId] = params; + this._runtime.emit( + this._runtime.constructor.PERIPHERAL_LIST_UPDATE, + this._availablePeripherals + ); */ + // TODO: auto-connect temporary until button is added + this.connectDevice(params.peripheralId); + // TODO: cancel a discover timeout if one is active break; case 'didReceiveMessage': // TODO: do something on received message @@ -31,6 +87,12 @@ class BTSession extends JSONRPCWebSocket { return 'nah'; } } + + _sendError (e) { + log.error(`BLESession error:`); + log.error(e); + this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); + } } module.exports = BTSession; diff --git a/src/util/base64-util.js b/src/util/base64-util.js index 60680e851..c2bd7f743 100644 --- a/src/util/base64-util.js +++ b/src/util/base64-util.js @@ -28,6 +28,21 @@ class Base64Util { return base64; } + /** + * Convert an array buffer to a base64 encoded string. + * @param {array} buffer - an array buffer to convert. + * @return {string} - the base64 encoded string. + */ + static arrayBufferToBase64 (buffer) { + let binary = ''; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[ i ]); + } + return btoa(binary); + } + } module.exports = Base64Util; From 42e0576549ba492ac93b77c754acd51eced41590 Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Mon, 25 Jun 2018 14:11:26 -0400 Subject: [PATCH 08/25] All EV3 blocks stubbed out with console logs for now. --- src/extensions/scratch3_ev3/index.js | 183 ++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 4 deletions(-) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index aec3f4b20..fae830e26 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -56,7 +56,25 @@ const MOTOR_PORTS = [ * the EV3 hub. * @type {array} */ -// const SENSOR_PORTS = ['1', '2', '3', '4']; + // TODO: are these names/values correct? +const SENSOR_PORTS = [ + { + name: '1', + value: 1 + }, + { + name: '2', + value: 2 + }, + { + name: '3', + value: 3 + }, + { + name: '4', + value: 4 + } +]; class EV3 { @@ -299,7 +317,7 @@ class Scratch3Ev3Blocks { blocks: [ { opcode: 'motorTurnClockwise', - text: '[PORT] turn clockwise [TIME] seconds', + text: 'motor [PORT] turn clockwise for [TIME] seconds', blockType: BlockType.COMMAND, arguments: { PORT: { @@ -315,7 +333,7 @@ class Scratch3Ev3Blocks { }, { opcode: 'motorTurnCounterClockwise', - text: '[PORT] turn counter [TIME] seconds', + text: 'motor [PORT] turn counter for [TIME] seconds', blockType: BlockType.COMMAND, arguments: { PORT: { @@ -329,6 +347,122 @@ class Scratch3Ev3Blocks { } } }, + { + opcode: 'motorRotate', + text: 'motor [PORT] rotate [DEGREES] degrees', + blockType: BlockType.COMMAND, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'motorPorts', + defaultValue: MOTOR_PORTS[0].value + }, + DEGREES: { + type: ArgumentType.NUMBER, + defaultValue: 90 + } + } + }, + { + opcode: 'motorSetPosition', + text: 'motor [PORT] set position [DEGREES] degrees', + blockType: BlockType.COMMAND, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'motorPorts', + defaultValue: MOTOR_PORTS[0].value + }, + DEGREES: { + type: ArgumentType.NUMBER, + defaultValue: 90 + } + } + }, + { + opcode: 'motorSetPower', + text: 'motor [PORT] set power [POWER] %', + blockType: BlockType.COMMAND, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'motorPorts', + defaultValue: MOTOR_PORTS[0].value + }, + POWER: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + }, + { + opcode: 'getMotorPosition', + text: 'motor [PORT] position', + blockType: BlockType.REPORTER, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'motorPorts', + defaultValue: MOTOR_PORTS[0].value + } + } + }, + { + opcode: 'whenButtonPressed', + text: 'when button [PORT] pressed', + blockType: BlockType.HAT, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'sensorPorts', + defaultValue: SENSOR_PORTS[0].value + } + } + }, + { + opcode: 'whenDistanceLessThan', + text: 'when distance < [DISTANCE]', + blockType: BlockType.HAT, + arguments: { + DISTANCE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + }, + { + opcode: 'whenBrightnessLessThan', + text: 'when brightness < [DISTANCE]', + blockType: BlockType.HAT, + arguments: { + DISTANCE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + }, + { + opcode: 'buttonPressed', + text: 'button [PORT] pressed?', + blockType: BlockType.BOOLEAN, + arguments: { + PORT: { + type: ArgumentType.STRING, + menu: 'sensorPorts', + defaultValue: SENSOR_PORTS[0].value + } + } + }, + { + opcode: 'getDistance', + text: 'distance', + blockType: BlockType.REPORTER + }, + { + opcode: 'getBrightness', + text: 'brightness', + blockType: BlockType.REPORTER + }, { opcode: 'beep', text: 'beep', @@ -336,7 +470,8 @@ class Scratch3Ev3Blocks { } ], menus: { - motorPorts: this._buildMenu(MOTOR_PORTS) + motorPorts: this._buildMenu(MOTOR_PORTS), + sensorPorts: this._buildMenu(SENSOR_PORTS) } }; } @@ -373,6 +508,46 @@ class Scratch3Ev3Blocks { this._device.motorTurnCounterClockwise(port, time); } + motorRotate (args) { + log.info(`motor rotate: ${args}`); + } + + motorSetPosition (args) { + log.info(`motor set position: ${args}`); + } + + motorSetPower (args) { + log.info(`motor set power: ${args}`); + } + + getMotorPosition (args) { + log.info(`get motor position: ${args}`); + } + + whenButtonPressed (args) { + log.info(`when button pressed: ${args}`); + } + + whenDistanceLessThan (args) { + log.info(`when distance less than: ${args}`); + } + + whenBrightnessLessThan (args) { + log.info(`when brightness less than: ${args}`); + } + + buttonPressed (args) { + log.info(`button pressed: ${args}`); + } + + getDistance () { + log.info(`distance`); + } + + getBrightness () { + log.info(`brightness`); + } + beep () { return this._device.beep(); } From d1d9b38bf9f30605da46ae83fdc9f6f331b67d96 Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Mon, 25 Jun 2018 14:39:38 -0400 Subject: [PATCH 09/25] Added all EV3 block commands/reporters/booleans to Ev3 device class as stubbed functions. --- src/extensions/scratch3_ev3/index.js | 109 ++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 12 deletions(-) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index fae830e26..b6bd1de56 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -123,6 +123,48 @@ class EV3 { this._bt.connectDevice(id); } + get distance () { + if (!this.connected) return; + + // TODO: read distance sensor data + log.info(`return distance`); + } + + get brightness () { + if (!this.connected) return; + + // TODO: read brightness sensor data + log.info(`return brightness`); + } + + getMotorPosition (args) { + if (!this.connected) return; + + // TODO: read motor position data + log.info(`return motor ${args} position`); + } + + isButtonPressed (args) { + if (!this.connected) return; + + // TODO: read button pressed data + log.info(`return button ${args} pressed`); + } + + isDistanceLessThan (args) { + if (!this.connected) return; + + // TODO: read distance sensor data + log.info(`return distance less than ${args}`); + } + + isBrightnessLessThan (args) { + if (!this.connected) return; + + // TODO: read brightness sensor data + log.info(`return brightness less than ${args}`); + } + beep () { if (!this.connected) return; this._bt.sendMessage({ @@ -183,6 +225,27 @@ class EV3 { }); } + motorRotate (port, degrees) { + if (!this.connected) return; + + // TODO: Build up motor command + log.info(`motor rotate port: ${port} and degrees: ${degrees}`); + } + + motorSetPosition (port, degrees) { + if (!this.connected) return; + + // TODO: Build up motor command + log.info(`motor set position port: ${port} and degrees: ${degrees}`); + } + + motorSetPower (port, power) { + if (!this.connected) return; + + // TODO: Build up motor command + log.info(`motor set power port: ${port} and degrees: ${power}`); + } + _applyPrefix (n, cmd) { const len = cmd.length + 5; return [].concat( @@ -493,59 +556,81 @@ class Scratch3Ev3Blocks { } motorTurnClockwise (args) { - // Validate arguments const port = Cast.toNumber(args.PORT); const time = Cast.toNumber(args.TIME) * 1000; this._device.motorTurnClockwise(port, time); + return; } motorTurnCounterClockwise (args) { - // Validate arguments const port = Cast.toNumber(args.PORT); const time = Cast.toNumber(args.TIME) * 1000; this._device.motorTurnCounterClockwise(port, time); + return; } motorRotate (args) { - log.info(`motor rotate: ${args}`); + const port = Cast.toNumber(args.PORT); + const degrees = Cast.toNumber(args.DEGREES); + + this._device.motorRotate(port, degrees); + return; } motorSetPosition (args) { - log.info(`motor set position: ${args}`); + const port = Cast.toNumber(args.PORT); + const degrees = Cast.toNumber(args.DEGREES); + + this._device.motorSetPosition(port, degrees); + return; } motorSetPower (args) { - log.info(`motor set power: ${args}`); + const port = Cast.toNumber(args.PORT); + const power = Cast.toNumber(args.POWER); + + this._device.motorSetPower(port, power); + return; } getMotorPosition (args) { - log.info(`get motor position: ${args}`); + const port = Cast.toNumber(args.PORT); + + return this._device.getMotorPosition(port); } whenButtonPressed (args) { - log.info(`when button pressed: ${args}`); + const port = Cast.toNumber(args.PORT); + + return this._device.isButtonPressed(port); } whenDistanceLessThan (args) { - log.info(`when distance less than: ${args}`); + const distance = Cast.toNumber(args.DISTANCE); + + return this._device.isDistanceLessThan(distance); } whenBrightnessLessThan (args) { - log.info(`when brightness less than: ${args}`); + const brightness = Cast.toNumber(args.DISTANCE); + + return this._device.isBrightnessLessThan(brightness); } buttonPressed (args) { - log.info(`button pressed: ${args}`); + const port = Cast.toNumber(args.PORT); + + return this._device.isButtonPressed(port); } getDistance () { - log.info(`distance`); + return this._device.distance; } getBrightness () { - log.info(`brightness`); + return this._device.brightness; } beep () { From 0280966a619300e99c38e20d9d361c6877cfc4a8 Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Mon, 25 Jun 2018 18:24:23 -0400 Subject: [PATCH 10/25] Connected Ev3 extension to device connection modals. --- src/extensions/scratch3_ev3/index.js | 7 ++----- src/io/btSession.js | 6 ++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index b6bd1de56..868572822 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -56,7 +56,6 @@ const MOTOR_PORTS = [ * the EV3 hub. * @type {array} */ - // TODO: are these names/values correct? const SENSOR_PORTS = [ { name: '1', @@ -97,9 +96,6 @@ class EV3 { */ this._bt = null; this._runtime.registerExtensionDevice(extensionId, this); - - // TODO: auto-connect temporary - until button is added - this.startDeviceScan(); } // TODO: keep here? @@ -364,7 +360,7 @@ class Scratch3Ev3Blocks { */ this.runtime = runtime; - // Create a new MicroBit device instance + // Create a new EV3 device instance this._device = new EV3(this.runtime, Scratch3Ev3Blocks.EXTENSION_ID); } @@ -377,6 +373,7 @@ class Scratch3Ev3Blocks { id: Scratch3Ev3Blocks.EXTENSION_ID, name: 'LEGO MINDSTORMS EV3', iconURI: null, + showStatusButton: true, blocks: [ { opcode: 'motorTurnClockwise', diff --git a/src/io/btSession.js b/src/io/btSession.js index 3d52189cb..6724df9c9 100644 --- a/src/io/btSession.js +++ b/src/io/btSession.js @@ -71,13 +71,11 @@ class BTSession extends JSONRPCWebSocket { // TODO: Add peripheral 'undiscover' handling switch (method) { case 'didDiscoverPeripheral': - /* this._availablePeripherals[params.peripheralId] = params; + this._availablePeripherals[params.peripheralId] = params; this._runtime.emit( this._runtime.constructor.PERIPHERAL_LIST_UPDATE, this._availablePeripherals - ); */ - // TODO: auto-connect temporary until button is added - this.connectDevice(params.peripheralId); + ); // TODO: cancel a discover timeout if one is active break; case 'didReceiveMessage': From 613a9f96c36051580d60a6fb363df053cbf36b24 Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Tue, 26 Jun 2018 15:07:08 -0400 Subject: [PATCH 11/25] Adding initial test polling for EV3 distance sensor data. --- src/extensions/scratch3_ev3/index.js | 79 +++++++++++++++++++++++----- src/io/btSession.js | 5 +- src/util/jsonrpc-web-socket.js | 3 +- 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index 868572822..d62979a70 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -86,8 +86,14 @@ class EV3 { */ this._runtime = runtime; + /** + * State + */ this.connected = false; this.speed = 50; + this._sensors = { + distance: 0 + }; /** * The Bluetooth connection session for reading/writing device data. @@ -107,7 +113,7 @@ class EV3 { this._bt = new BTSession(this._runtime, { majorDeviceClass: 8, minorDeviceClass: 1 - }, this._onSessionConnect.bind(this)); + }, this._onSessionConnect.bind(this), this._onSessionMessage.bind(this)); } // TODO: keep here? @@ -122,8 +128,7 @@ class EV3 { get distance () { if (!this.connected) return; - // TODO: read distance sensor data - log.info(`return distance`); + return this._sensors.distance; } get brightness () { @@ -147,13 +152,6 @@ class EV3 { log.info(`return button ${args} pressed`); } - isDistanceLessThan (args) { - if (!this.connected) return; - - // TODO: read distance sensor data - log.info(`return distance less than ${args}`); - } - isBrightnessLessThan (args) { if (!this.connected) return; @@ -333,7 +331,62 @@ class EV3 { _onSessionConnect () { log.info('bt device connected!'); this.connected = true; - // start reading data? + + // Start reading data + const intervalID = window.setInterval(this._getSessionData.bind(this), 300); + // TODO: clear intervalID later on disconnect, etc. + } + + _getSessionData () { + // GET EV3 DISTANCE PORT 0 + /* + 99 [153] input device + 1D [ 29] ready si + 00 [ 0] layer (this brick) + 00 [ 0] sensor port 0 + 00 [ 0] do not change type + 01 [ 1] mode 1 = EV3-Ultrasonic-Inch + 01 [ 1] one data set + 60 [ 96] global var index + */ + this._bt.sendMessage({ + message: 'DQAAAAAEAJkdAAAAAQFg', // [13, 0, 0, 0, 0, 4, 0, 153, 29, 0, 0, 0, 1, 1, 96] + encoding: 'base64' + }); + + // GET EV3 BRIGHTNESS PORT 1 + /* + 99 [153] input device + 1D [ 29] ready si + 00 [ 0] layer (this brick) + 01 [ 1] sensor port 1 + 00 [ 0] do not change type + 01 [ 1] mode 1 = EV3-Color-Ambient + 01 [ 1] one data set + 60 [ 96] global var index + */ + /* + this._bt.sendMessage({ + message: 'DQAAAAAEAJkdAAEAAQFg', // [13, 0, 0, 0, 0, 4, 0, 153, 29, 0, 1, 0, 1, 1, 96] + encoding: 'base64' + }); + */ + } + + _onSessionMessage (params) { + const message = params.message; + const array = Base64Util.base64ToUint8Array(message); + + // TODO: this only gets distance so far + const distance = this._array2float([array[5], array[6], array[7], array[8]]); + this._sensors.distance = distance; + } + + // TODO: put elsewhere, in a util + _array2float (list) { + const buffer = new Uint8Array(list).buffer; + const view = new DataView(buffer); + return view.getFloat32(0, true); } } @@ -486,7 +539,7 @@ class Scratch3Ev3Blocks { arguments: { DISTANCE: { type: ArgumentType.NUMBER, - defaultValue: 50 + defaultValue: 5 } } }, @@ -607,7 +660,7 @@ class Scratch3Ev3Blocks { whenDistanceLessThan (args) { const distance = Cast.toNumber(args.DISTANCE); - return this._device.isDistanceLessThan(distance); + return this._device.distance < distance; } whenBrightnessLessThan (args) { diff --git a/src/io/btSession.js b/src/io/btSession.js index 6724df9c9..14228cc60 100644 --- a/src/io/btSession.js +++ b/src/io/btSession.js @@ -11,7 +11,7 @@ class BTSession extends JSONRPCWebSocket { * @param {object} deviceOptions - the list of options for device discovery. * @param {object} connectCallback - a callback for connection. */ - constructor (runtime, deviceOptions, connectCallback) { + constructor (runtime, deviceOptions, connectCallback, messageCallback) { const ws = new WebSocket(ScratchLinkWebSocket); super(ws); @@ -24,6 +24,7 @@ class BTSession extends JSONRPCWebSocket { this._connectCallback = connectCallback; this._characteristicDidChangeCallback = null; this._deviceOptions = deviceOptions; + this._messageCallback = messageCallback; this._runtime = runtime; } @@ -79,7 +80,7 @@ class BTSession extends JSONRPCWebSocket { // TODO: cancel a discover timeout if one is active break; case 'didReceiveMessage': - // TODO: do something on received message + this._messageCallback(params); // TODO: refine? break; default: return 'nah'; diff --git a/src/util/jsonrpc-web-socket.js b/src/util/jsonrpc-web-socket.js index edfdf9373..39b60b92b 100644 --- a/src/util/jsonrpc-web-socket.js +++ b/src/util/jsonrpc-web-socket.js @@ -28,8 +28,7 @@ class JSONRPCWebSocket extends JSONRPC { _onSocketMessage (e) { const json = JSON.parse(e.data); if (json.method !== 'characteristicDidChange') { - console.log('received message: '); - console.log(json); + console.log('received message: ' + JSON.stringify(json)); } this._handleMessage(json); } From 6f5ff31eb3248605ae2c2ed5809a16ff3aa96fa7 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 27 Jun 2018 14:21:11 -0400 Subject: [PATCH 12/25] Add disconnectExtensionSession and getPeripheralIsConnected --- src/engine/runtime.js | 14 ++++++++++++++ src/extensions/scratch3_microbit/index.js | 12 ++++++++++++ src/io/bleSession.js | 19 +++++++++++++++++++ src/io/btSession.js | 19 +++++++++++++++++++ src/virtual-machine.js | 8 ++++++++ 5 files changed, 72 insertions(+) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 64e469e05..530db3b40 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -909,6 +909,20 @@ class Runtime extends EventEmitter { } } + disconnectExtensionSession (extensionId) { + if (this.extensionDevices[extensionId]) { + this.extensionDevices[extensionId].disconnectSession(); + } + } + + getPeripheralIsConnected (extensionId) { + let isConnected = false; + if (this.extensionDevices[extensionId]) { + isConnected = this.extensionDevices[extensionId].getPeripheralIsConnected(); + } + return isConnected; + } + /** * Retrieve the function associated with the given opcode. * @param {!string} opcode The opcode to look up. diff --git a/src/extensions/scratch3_microbit/index.js b/src/extensions/scratch3_microbit/index.js index 22203ccb4..e9ad6bab7 100644 --- a/src/extensions/scratch3_microbit/index.js +++ b/src/extensions/scratch3_microbit/index.js @@ -128,6 +128,18 @@ class MicroBit { this._ble.connectDevice(id); } + disconnectSession () { + this._ble.disconnectSession(); + } + + getPeripheralIsConnected () { + let connected = false; + if (this._ble) { + connected = this._ble.getPeripheralIsConnected(); + } + return connected; + } + /** * @param {string} text - the text to display. * @return {Promise} - a Promise that resolves when writing to device. diff --git a/src/io/bleSession.js b/src/io/bleSession.js index 9a4cd1aab..983399c90 100644 --- a/src/io/bleSession.js +++ b/src/io/bleSession.js @@ -25,6 +25,8 @@ class BLESession extends JSONRPCWebSocket { this._characteristicDidChangeCallback = null; this._deviceOptions = deviceOptions; this._runtime = runtime; + + this._connected = false; } /** @@ -50,6 +52,7 @@ class BLESession extends JSONRPCWebSocket { .then(() => { log.info('should have connected'); this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED); + this._connected = true; this._connectCallback(); }) .catch(e => { @@ -57,6 +60,21 @@ class BLESession extends JSONRPCWebSocket { }); } + /** + * Close the websocket. + */ + disconnectSession () { + this._ws.close(); + this._connected = false; + } + + /** + * @return {bool} whether the peripheral is connected. + */ + getPeripheralIsConnected () { + return this._connected; + } + /** * Handle a received call from the socket. * @param {string} method - a received method label. @@ -119,6 +137,7 @@ class BLESession extends JSONRPCWebSocket { } _sendError (e) { + this._connected = false; log.error(`BLESession error:`); log.error(e); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); diff --git a/src/io/btSession.js b/src/io/btSession.js index 14228cc60..7d1f6a4c8 100644 --- a/src/io/btSession.js +++ b/src/io/btSession.js @@ -26,6 +26,8 @@ class BTSession extends JSONRPCWebSocket { this._deviceOptions = deviceOptions; this._messageCallback = messageCallback; this._runtime = runtime; + + this._connected = false; } /** @@ -51,6 +53,7 @@ class BTSession extends JSONRPCWebSocket { .then(() => { log.info('should have connected'); this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED); + this._connected = true; this._connectCallback(); }) .catch(e => { @@ -58,6 +61,22 @@ class BTSession extends JSONRPCWebSocket { }); } + /** + * Close the websocket. + */ + disconnectSession () { + this._ws.close(); + this._connected = false; + } + + /** + * @return {bool} whether the peripheral is connected. + */ + getPeripheralIsConnected () { + return this._connected; + } + + sendMessage (options) { return this.sendRemoteRequest('send', options); } diff --git a/src/virtual-machine.js b/src/virtual-machine.js index dc53ce800..e95258fb4 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -213,6 +213,14 @@ class VirtualMachine extends EventEmitter { this.runtime.connectToPeripheral(extensionId, peripheralId); } + disconnectExtensionSession (extensionId) { + this.runtime.disconnectExtensionSession(extensionId); + } + + getPeripheralIsConnected (extensionId) { + return this.runtime.getPeripheralIsConnected(extensionId); + } + /** * Load a Scratch project from a .sb, .sb2, .sb3 or json string. * @param {string | object} input A json string, object, or ArrayBuffer representing the project to load. From 387f03b08c0b1647c42ace912173d0599dd17777 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 27 Jun 2018 15:26:57 -0400 Subject: [PATCH 13/25] Add disconnect and getPeripheralIsConnected to EV3 extension --- src/extensions/scratch3_ev3/index.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index d62979a70..1b56759cb 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -125,6 +125,18 @@ class EV3 { this._bt.connectDevice(id); } + disconnectSession () { + this._bt.disconnectSession(); + } + + getPeripheralIsConnected () { + let connected = false; + if (this._bt) { + connected = this._bt.getPeripheralIsConnected(); + } + return connected; + } + get distance () { if (!this.connected) return; From d79efbece83038fd6d519861ca19a6a251b47a1e Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Wed, 27 Jun 2018 22:28:33 -0400 Subject: [PATCH 14/25] Some logging cleanup. --- src/io/bleSession.js | 3 +-- src/io/btSession.js | 4 ++-- src/util/jsonrpc-web-socket.js | 3 ++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/io/bleSession.js b/src/io/bleSession.js index 983399c90..600373ea3 100644 --- a/src/io/bleSession.js +++ b/src/io/bleSession.js @@ -138,8 +138,7 @@ class BLESession extends JSONRPCWebSocket { _sendError (e) { this._connected = false; - log.error(`BLESession error:`); - log.error(e); + log.error(`BLESession error: ${e}`); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); } } diff --git a/src/io/btSession.js b/src/io/btSession.js index 7d1f6a4c8..d0dfea3c7 100644 --- a/src/io/btSession.js +++ b/src/io/btSession.js @@ -10,6 +10,7 @@ class BTSession extends JSONRPCWebSocket { * @param {Runtime} runtime - the Runtime for sending/receiving GUI update events. * @param {object} deviceOptions - the list of options for device discovery. * @param {object} connectCallback - a callback for connection. + * @param {object} messageCallback - a callback for message sending. */ constructor (runtime, deviceOptions, connectCallback, messageCallback) { const ws = new WebSocket(ScratchLinkWebSocket); @@ -107,8 +108,7 @@ class BTSession extends JSONRPCWebSocket { } _sendError (e) { - log.error(`BLESession error:`); - log.error(e); + log.error(`BLESession error: ${e}`); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); } } diff --git a/src/util/jsonrpc-web-socket.js b/src/util/jsonrpc-web-socket.js index 39b60b92b..c06a269b5 100644 --- a/src/util/jsonrpc-web-socket.js +++ b/src/util/jsonrpc-web-socket.js @@ -1,4 +1,5 @@ const JSONRPC = require('./jsonrpc'); +const log = require('../util/log'); class JSONRPCWebSocket extends JSONRPC { constructor (webSocket) { @@ -28,7 +29,7 @@ class JSONRPCWebSocket extends JSONRPC { _onSocketMessage (e) { const json = JSON.parse(e.data); if (json.method !== 'characteristicDidChange') { - console.log('received message: ' + JSON.stringify(json)); + // log.info(`received message: ${json}`); } this._handleMessage(json); } From cf98e355643c22e3c692eed7e87ffe4068480459 Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Thu, 28 Jun 2018 13:40:16 -0400 Subject: [PATCH 15/25] EV3 blocks progress: distance, brightness, button, motor position. Only single button for now, distance needs threshold at 0. --- src/extensions/scratch3_ev3/index.js | 271 +++++++++++++++++++++++---- 1 file changed, 238 insertions(+), 33 deletions(-) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index 1b56759cb..ef2d6fbfd 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -5,6 +5,8 @@ const log = require('../../util/log'); const Base64Util = require('../../util/base64-util'); const BTSession = require('../../io/BTSession'); +// TODO: Refactor/rename all these high level primitives to be clearer/match + /** * High-level primitives / constants used by the extension. * @type {object} @@ -75,6 +77,31 @@ const SENSOR_PORTS = [ } ]; +// firmware pdf page 100 +const EV_DEVICE_TYPES = { + 29: 'color', + 30: 'ultrasonic', + 32: 'gyro', + 16: 'touch', + 8: 'mediumMotor', + 7: 'largeMotor', + 126: 'none' +}; + +// firmware pdf page 100? +const EV_DEVICE_MODES = { + touch: 0, + color: 1, + ultrasonic: 1 +}; + +const EV_DEVICE_LABELS = { + touch: 'button', + color: 'brightness', + ultrasonic: 'distance' +}; + + class EV3 { constructor (runtime, extensionId) { @@ -87,13 +114,19 @@ class EV3 { this._runtime = runtime; /** - * State + * EV3 State */ this.connected = false; this.speed = 50; this._sensors = { - distance: 0 + distance: 0, + brightness: 0 }; + this._sensorPorts = []; + this._motorPorts = []; + this._sensorPortsWaiting = [false, false, false, false]; + this._motorPortsWaiting = [false, false, false, false]; + this._pollingIntervalID = null; /** * The Bluetooth connection session for reading/writing device data. @@ -109,7 +142,6 @@ class EV3 { * Called by the runtime when user wants to scan for a device. */ startDeviceScan () { - log.info('making a new BT session'); this._bt = new BTSession(this._runtime, { majorDeviceClass: 8, minorDeviceClass: 1 @@ -125,10 +157,19 @@ class EV3 { this._bt.connectDevice(id); } + // TODO: keep here? + /** + * Called by the runtime when user wants to disconnect from the device. + */ disconnectSession () { this._bt.disconnectSession(); + window.clearInterval(this._pollingIntervalID); // TODO: window? } + /** + * Called by the runtime to detect whether the device is connected. + * @return {boolean} - the connected state. + */ getPeripheralIsConnected () { let connected = false; if (this._bt) { @@ -140,14 +181,19 @@ class EV3 { get distance () { if (!this.connected) return; - return this._sensors.distance; + // https://shop.lego.com/en-US/EV3-Ultrasonic-Sensor-45504 + // Measures distances between one and 250 cm (one to 100 in.) + // Accurate to +/- 1 cm (+/- .394 in.) + let value = this._sensors.distance > 100 ? 100 : this._sensors.distance; + value = value < 0 ? 0 : value; + + return Math.round(value); } get brightness () { if (!this.connected) return; - // TODO: read brightness sensor data - log.info(`return brightness`); + return this._sensors.brightness; } getMotorPosition (args) { @@ -160,19 +206,12 @@ class EV3 { isButtonPressed (args) { if (!this.connected) return; - // TODO: read button pressed data - log.info(`return button ${args} pressed`); - } - - isBrightnessLessThan (args) { - if (!this.connected) return; - - // TODO: read brightness sensor data - log.info(`return brightness less than ${args}`); + return this._sensors.button; } beep () { if (!this.connected) return; + this._bt.sendMessage({ message: 'DwAAAIAAAJQBgQKC6AOC6AM=', encoding: 'base64' @@ -316,7 +355,7 @@ class EV3 { // Setup motor run duration and ramping behavior let rampup = ramp; let rampdown = ramp; - let run = n - ramp * 2; + let run = n - (ramp * 2); if (run < 0) { rampup = Math.floor(n / 2); run = 0; @@ -341,15 +380,43 @@ class EV3 { } _onSessionConnect () { - log.info('bt device connected!'); this.connected = true; - // Start reading data - const intervalID = window.setInterval(this._getSessionData.bind(this), 300); - // TODO: clear intervalID later on disconnect, etc. + // GET EV3 SENSOR LIST + /* + 0B [ 11] + 00 [ 0] + 01 [ 1] + 00 [ 0] + 00 [ 0] + 21 [ 33] + 00 [ 0] + 98 [152] opInput_Device_List + 81 [129] LENGTH + 21 [ 33] ARRAY + 60 [ 96] CHANGED + E1 [225] size of global var? + 20 [ 32] global var index + */ + this._bt.sendMessage({ + message: 'CwABAAAhAJiBIWDhIA==', // [11, 0, 1, 0, 0, 33, 0, 152, 129, 33, 96, 225, 32] + encoding: 'base64' + }).then( + x => { + log.info(`get device list resolved: ${x}`); + }, + e => { + log.info(`get device list rejected: ${e}`); + } + ); } _getSessionData () { + if (!this.connected) { + window.clearInterval(this._pollingIntervalID); + return; + } + // GET EV3 DISTANCE PORT 0 /* 99 [153] input device @@ -361,21 +428,23 @@ class EV3 { 01 [ 1] one data set 60 [ 96] global var index */ + /* this._bt.sendMessage({ message: 'DQAAAAAEAJkdAAAAAQFg', // [13, 0, 0, 0, 0, 4, 0, 153, 29, 0, 0, 0, 1, 1, 96] encoding: 'base64' }); + */ // GET EV3 BRIGHTNESS PORT 1 /* - 99 [153] input device - 1D [ 29] ready si - 00 [ 0] layer (this brick) - 01 [ 1] sensor port 1 - 00 [ 0] do not change type - 01 [ 1] mode 1 = EV3-Color-Ambient - 01 [ 1] one data set - 60 [ 96] global var index + 0x99 [153] input device + 0x1D [ 29] ready si + 0x00 [ 0] layer (this brick) + 0x01 [ 1] sensor port 1 + 0x00 [ 0] do not change type + 0x01 [ 1] mode 1 = EV3-Color-Ambient + 0x01 [ 1] one data set + 0x60 [ 96] global var index */ /* this._bt.sendMessage({ @@ -383,19 +452,155 @@ class EV3 { encoding: 'base64' }); */ + + + // COMPOUND COMMAND FOR READING sensors0x27 command size + // 0x?? [ ] command size + // 0x00 [ 0] command size + // 0x01 [ 1] message counter + // 0x00 [ 0] message counter + // 0x00 [ 0] command type + // 0x?? [ ] result payload size of global/local vars + // 0x00 [ 0] result payload size of global/local vars + const compoundCommand = []; + compoundCommand[0] = 0; // calculate length later + compoundCommand[1] = 0; // command size + compoundCommand[2] = 1; // message counter // TODO: ????? + compoundCommand[3] = 0; // message counter + compoundCommand[4] = 0; // command type: direct command + compoundCommand[5] = 0; // global/local vars + compoundCommand[6] = 0; // global/local vars + let compoundCommandIndex = 7; + let sensorCount = -1; + + // Read from available sensors + for (let i = 0; i < this._sensorPorts.length; i++) { + if (this._sensorPorts[i] !== 'none') { + sensorCount++; + // make up sensor command array + // 0x9D [ 157] op: get sensor value + // 0x00 [ 0] layer + // 0x02 [ ] port + // 0x00 [ 0] do not change type + // 0x00 [ ] mode + // 0xE1 [ 225] + // 0x0C [ ] global index + compoundCommand[compoundCommandIndex + 0] = 157; + compoundCommand[compoundCommandIndex + 1] = 0; + compoundCommand[compoundCommandIndex + 2] = i; + compoundCommand[compoundCommandIndex + 3] = 0; + compoundCommand[compoundCommandIndex + 4] = EV_DEVICE_MODES[this._sensorPorts[i]]; + compoundCommand[compoundCommandIndex + 5] = 225; + compoundCommand[compoundCommandIndex + 6] = sensorCount * 4; + compoundCommandIndex += 7; + } + } + // Read from available motors + // let motorCount = 0; + for (let i = 0; i < this._motorPorts.length; i++) { + if (this._motorPorts[i] !== 'none') { + sensorCount++; + // make up sensor command array + // 0xB3 [ 179] op: get motor position value + // 0x00 [ 0] layer + // 0x02 [ ] output bit fields ?? + // 0xE1 [ 225] + // 0x?? [ 0] global index + compoundCommand[compoundCommandIndex + 0] = 179; + compoundCommand[compoundCommandIndex + 1] = 0; + compoundCommand[compoundCommandIndex + 2] = i; + compoundCommand[compoundCommandIndex + 3] = 225; + compoundCommand[compoundCommandIndex + 4] = sensorCount * 4; + compoundCommandIndex += 5; + // motorCount++; + } + } + + + // Calculate compound command length + compoundCommand[0] = compoundCommand.length - 2; + // Calculate global var payload length needed + compoundCommand[5] = (sensorCount + 1) * 4; + console.log('compound command to send: ' + compoundCommand); + this._bt.sendMessage({ + message: Base64Util.uint8ArrayToBase64(compoundCommand), + encoding: 'base64' + }); + + // TODO: Read from available motor ports } _onSessionMessage (params) { const message = params.message; const array = Base64Util.base64ToUint8Array(message); - // TODO: this only gets distance so far - const distance = this._array2float([array[5], array[6], array[7], array[8]]); - this._sensors.distance = distance; + if (this._sensorPorts.length === 0) { + // SENSOR LIST + // JAABAAIefn5+fn5+fn5+fn5+fn5+Bwd+fn5+fn5+fn5+fn5+fgA= + // [36, 0, 1, 0, 2, 30, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 7, 7, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 0] + this._sensorPorts[0] = EV_DEVICE_TYPES[array[5]]; + this._sensorPorts[1] = EV_DEVICE_TYPES[array[6]]; + this._sensorPorts[2] = EV_DEVICE_TYPES[array[7]]; + this._sensorPorts[3] = EV_DEVICE_TYPES[array[8]]; + this._motorPorts[0] = EV_DEVICE_TYPES[array[21]]; + this._motorPorts[1] = EV_DEVICE_TYPES[array[22]]; + this._motorPorts[2] = EV_DEVICE_TYPES[array[23]]; + this._motorPorts[3] = EV_DEVICE_TYPES[array[24]]; + log.info(`sensor ports: ${this._sensorPorts}`); + log.info(`motor ports: ${this._motorPorts}`); + + // Now ready to read from assigned _sensors + // Start reading sensor data + // TODO: window? + this._pollingIntervalID = window.setInterval(this._getSessionData.bind(this), 100); + } else { + log.info(`received compound command result: ${array}`); + let offset = 5; + for (let i = 0; i < this._sensorPorts.length; i++) { + if (this._sensorPorts[i] !== 'none') { + const value = this._array2float([ + array[offset], + array[offset + 1], + array[offset + 2], + array[offset + 3] + ]); + log.info(`sensor at port ${i} ${this._sensorPorts[i]} value: ${value}`); + this._sensors[EV_DEVICE_LABELS[this._sensorPorts[i]]] = value; + offset += 4; + } + } + for (let i = 0; i < this._motorPorts.length; i++) { + if (this._motorPorts[i] !== 'none') { + let value = this._tachoValue([ + array[offset], + array[offset + 1], + array[offset + 2], + array[offset + 3] + ]); + if (value > 0x7fffffff) { + value = value - 0x100000000; + } + log.info(`motor at port ${i} ${this._motorPorts[i]} value: ${value}`); + // this._motors[EV_DEVICE_LABELS[this._motorPorts[i]]] = value; + offset += 4; + } + } + // const sensorValue = this._array2float([array[5], array[6], array[7], array[8]]); + // log.info('receiving port array?: ' + array); + // log.info('receiving port sensorValue?: ' + sensorValue); + // this._sensors.distance = distance; + } + } - // TODO: put elsewhere, in a util + _tachoValue (list) { + const value = list[0] + (list[1] * 256) + (list[2] * 256 * 256) + (list[3] * 256 * 256 * 256); + return value; + } + + // TODO: put elsewhere _array2float (list) { + log.info(`list ${list}`); const buffer = new Uint8Array(list).buffer; const view = new DataView(buffer); return view.getFloat32(0, true); @@ -678,7 +883,7 @@ class Scratch3Ev3Blocks { whenBrightnessLessThan (args) { const brightness = Cast.toNumber(args.DISTANCE); - return this._device.isBrightnessLessThan(brightness); + return this._device.brightness < brightness; } buttonPressed (args) { From b9ea6005f811e54d34bc4063e179132a6bcfeb64 Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Thu, 28 Jun 2018 13:57:17 -0400 Subject: [PATCH 16/25] Clearing ports on disconnect. --- src/extensions/scratch3_ev3/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index ef2d6fbfd..053711bc1 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -164,6 +164,8 @@ class EV3 { disconnectSession () { this._bt.disconnectSession(); window.clearInterval(this._pollingIntervalID); // TODO: window? + this._sensorPorts = []; + this._motorPorts = []; } /** @@ -521,7 +523,7 @@ class EV3 { compoundCommand[0] = compoundCommand.length - 2; // Calculate global var payload length needed compoundCommand[5] = (sensorCount + 1) * 4; - console.log('compound command to send: ' + compoundCommand); + // console.log('compound command to send: ' + compoundCommand); this._bt.sendMessage({ message: Base64Util.uint8ArrayToBase64(compoundCommand), encoding: 'base64' @@ -538,6 +540,7 @@ class EV3 { // SENSOR LIST // JAABAAIefn5+fn5+fn5+fn5+fn5+Bwd+fn5+fn5+fn5+fn5+fgA= // [36, 0, 1, 0, 2, 30, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 7, 7, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 0] + log.info(`device array: ${array}`); this._sensorPorts[0] = EV_DEVICE_TYPES[array[5]]; this._sensorPorts[1] = EV_DEVICE_TYPES[array[6]]; this._sensorPorts[2] = EV_DEVICE_TYPES[array[7]]; @@ -554,7 +557,7 @@ class EV3 { // TODO: window? this._pollingIntervalID = window.setInterval(this._getSessionData.bind(this), 100); } else { - log.info(`received compound command result: ${array}`); + //log.info(`received compound command result: ${array}`); let offset = 5; for (let i = 0; i < this._sensorPorts.length; i++) { if (this._sensorPorts[i] !== 'none') { @@ -600,7 +603,6 @@ class EV3 { // TODO: put elsewhere _array2float (list) { - log.info(`list ${list}`); const buffer = new Uint8Array(list).buffer; const view = new DataView(buffer); return view.getFloat32(0, true); From 1cc013ea1b5c9d3ec312b4352ed0054f8a1e2d0d Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Jul 2018 10:35:50 -0400 Subject: [PATCH 17/25] Stringify errors --- src/io/bleSession.js | 2 +- src/io/btSession.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/io/bleSession.js b/src/io/bleSession.js index 600373ea3..1596c721a 100644 --- a/src/io/bleSession.js +++ b/src/io/bleSession.js @@ -138,7 +138,7 @@ class BLESession extends JSONRPCWebSocket { _sendError (e) { this._connected = false; - log.error(`BLESession error: ${e}`); + log.error(`BLESession error: ${JSON.stringify(e)}`); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); } } diff --git a/src/io/btSession.js b/src/io/btSession.js index d0dfea3c7..bf847296e 100644 --- a/src/io/btSession.js +++ b/src/io/btSession.js @@ -108,7 +108,7 @@ class BTSession extends JSONRPCWebSocket { } _sendError (e) { - log.error(`BLESession error: ${e}`); + log.error(`BTSession error: ${JSON.stringify(e)}`); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); } } From 2ec894b5a7e43b6d5f20495e7e9cdcc0b822760c Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Jul 2018 10:36:13 -0400 Subject: [PATCH 18/25] Get motor positions for reporter block --- src/extensions/scratch3_ev3/index.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index 053711bc1..801ea25d0 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -122,6 +122,12 @@ class EV3 { distance: 0, brightness: 0 }; + this._motorPositions = { + 1: 0, + 2: 0, + 4: 0, + 8: 0 + }; this._sensorPorts = []; this._motorPorts = []; this._sensorPortsWaiting = [false, false, false, false]; @@ -198,9 +204,9 @@ class EV3 { return this._sensors.brightness; } - getMotorPosition (args) { + getMotorPosition (port) { if (!this.connected) return; - + return this._motorPositions[port]; // TODO: read motor position data log.info(`return motor ${args} position`); } @@ -584,7 +590,7 @@ class EV3 { value = value - 0x100000000; } log.info(`motor at port ${i} ${this._motorPorts[i]} value: ${value}`); - // this._motors[EV_DEVICE_LABELS[this._motorPorts[i]]] = value; + this._motorPositions[MOTOR_PORTS[i].value] = value; offset += 4; } } From b1976d6fec67567e2fe8338cfe3bf9a14692b70f Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Jul 2018 10:53:29 -0400 Subject: [PATCH 19/25] update package-lock --- package-lock.json | 125 +++++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index c887d5869..f568978fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4009,27 +4009,6 @@ "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==", "dev": true }, - "inquirer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", - "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.1.0", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^5.5.2", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - } - }, "is-resolvable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", @@ -6834,25 +6813,24 @@ "dev": true }, "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", "dev": true, "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.0.4", - "figures": "^2.0.0", - "lodash": "^4.3.0", + "ansi-escapes": "3.1.0", + "chalk": "2.4.1", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "2.2.0", + "figures": "2.0.0", + "lodash": "4.17.4", "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rx-lite": "^4.0.8", - "rx-lite-aggregates": "^4.0.8", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" + "run-async": "2.3.0", + "rxjs": "5.5.10", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" }, "dependencies": { "ansi-regex": { @@ -6867,42 +6845,36 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "1.9.1" } }, "chalk": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", - "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "3.0.0" } }, "supports-color": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", - "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" } } } @@ -13129,9 +13101,9 @@ } }, "scratch-blocks": { - "version": "0.1.0-prerelease.1530135682", - "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1530135682.tgz", - "integrity": "sha512-JadXtaqDLmebmitbk5s5RVtuApvWWaj+ECSIfXY01dY9X3JDZAopMDWZuP/8uNWg7S1xMBrIaD8Rv0r83lGdmA==", + "version": "0.1.0-prerelease.1531144787", + "resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1531144787.tgz", + "integrity": "sha512-fSS/C6pBh5kNkimFfHDO+XG8Ny6lzHePb5eeqxDkel2u5agJgkAbIgBgJnsqwJ+iJmKhmJgW29K/iM442MDYXA==", "dev": true, "requires": { "exports-loader": "0.6.3", @@ -16054,6 +16026,12 @@ "untildify": "^3.0.2" }, "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -16102,12 +16080,43 @@ "pinkie-promise": "^2.0.0" } }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.4", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx-lite": "^4.0.8", + "rx-lite-aggregates": "^4.0.8", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, "supports-color": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", From 190f4b57968ea5bad0b3f3f859d5b0b1f29f14ce Mon Sep 17 00:00:00 2001 From: Evelyn Eastmond Date: Mon, 2 Jul 2018 11:03:19 -0400 Subject: [PATCH 20/25] Fixing linting errors. --- src/extensions/scratch3_ev3/index.js | 12 +++++------- src/util/jsonrpc-web-socket.js | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index 801ea25d0..016919f8c 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -187,7 +187,7 @@ class EV3 { } get distance () { - if (!this.connected) return; + if (!this.connected) return 0; // https://shop.lego.com/en-US/EV3-Ultrasonic-Sensor-45504 // Measures distances between one and 250 cm (one to 100 in.) @@ -199,19 +199,18 @@ class EV3 { } get brightness () { - if (!this.connected) return; + if (!this.connected) return 0; return this._sensors.brightness; } getMotorPosition (port) { if (!this.connected) return; + return this._motorPositions[port]; - // TODO: read motor position data - log.info(`return motor ${args} position`); } - isButtonPressed (args) { + isButtonPressed (/* args */) { if (!this.connected) return; return this._sensors.button; @@ -545,7 +544,6 @@ class EV3 { if (this._sensorPorts.length === 0) { // SENSOR LIST // JAABAAIefn5+fn5+fn5+fn5+fn5+Bwd+fn5+fn5+fn5+fn5+fgA= - // [36, 0, 1, 0, 2, 30, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 7, 7, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 126, 0] log.info(`device array: ${array}`); this._sensorPorts[0] = EV_DEVICE_TYPES[array[5]]; this._sensorPorts[1] = EV_DEVICE_TYPES[array[6]]; @@ -563,7 +561,7 @@ class EV3 { // TODO: window? this._pollingIntervalID = window.setInterval(this._getSessionData.bind(this), 100); } else { - //log.info(`received compound command result: ${array}`); + // log.info(`received compound command result: ${array}`); let offset = 5; for (let i = 0; i < this._sensorPorts.length; i++) { if (this._sensorPorts[i] !== 'none') { diff --git a/src/util/jsonrpc-web-socket.js b/src/util/jsonrpc-web-socket.js index c06a269b5..ac118cdff 100644 --- a/src/util/jsonrpc-web-socket.js +++ b/src/util/jsonrpc-web-socket.js @@ -1,5 +1,5 @@ const JSONRPC = require('./jsonrpc'); -const log = require('../util/log'); +// const log = require('../util/log'); class JSONRPCWebSocket extends JSONRPC { constructor (webSocket) { From 2cfc6d601a2509467ed5ab5d4488449787ea784e Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Jul 2018 11:06:23 -0400 Subject: [PATCH 21/25] Fix required module name to lowercase --- src/extensions/scratch3_ev3/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index 016919f8c..b7b1d06dc 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -3,7 +3,7 @@ const BlockType = require('../../extension-support/block-type'); const Cast = require('../../util/cast'); const log = require('../../util/log'); const Base64Util = require('../../util/base64-util'); -const BTSession = require('../../io/BTSession'); +const BTSession = require('../../io/btSession'); // TODO: Refactor/rename all these high level primitives to be clearer/match From 15dcee423b0fe5a47c8f79502b404ebffaadee6a Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Jul 2018 14:49:42 -0400 Subject: [PATCH 22/25] Revert changes to package-lock --- package-lock.json | 123 +++++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index f568978fa..824406d2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3799,9 +3799,9 @@ } }, "eslint": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.0.1.tgz", - "integrity": "sha512-D5nG2rErquLUstgUaxJlWB5+gu+U/3VDY0fk/Iuq8y9CUFy/7Y6oF4N2cR1tV8knzQvciIbfqfohd359xTLIKQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.1.0.tgz", + "integrity": "sha512-DyH6JsoA1KzA5+OSWFjg56DFJT+sDLO0yokaPZ9qY0UEmYrPA1gEX/G1MnVkmRDsksG4H1foIVz2ZXXM3hHYvw==", "dev": true, "requires": { "ajv": "^6.5.0", @@ -3811,6 +3811,7 @@ "debug": "^3.1.0", "doctrine": "^2.1.0", "eslint-scope": "^4.0.0", + "eslint-utils": "^1.3.1", "eslint-visitor-keys": "^1.0.0", "espree": "^4.0.0", "esquery": "^1.0.1", @@ -3818,7 +3819,7 @@ "file-entry-cache": "^2.0.0", "functional-red-black-tree": "^1.0.1", "glob": "^7.1.2", - "globals": "^11.5.0", + "globals": "^11.7.0", "ignore": "^3.3.3", "imurmurhash": "^0.1.4", "inquirer": "^5.2.0", @@ -4009,6 +4010,27 @@ "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==", "dev": true }, + "inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, "is-resolvable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", @@ -4117,6 +4139,12 @@ "estraverse": "^4.1.1" } }, + "eslint-utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", + "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", + "dev": true + }, "eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -6813,24 +6841,25 @@ "dev": true }, "inquirer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", - "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", "dev": true, "requires": { - "ansi-escapes": "3.1.0", - "chalk": "2.4.1", - "cli-cursor": "2.1.0", - "cli-width": "2.2.0", - "external-editor": "2.2.0", - "figures": "2.0.0", - "lodash": "4.17.4", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.4", + "figures": "^2.0.0", + "lodash": "^4.3.0", "mute-stream": "0.0.7", - "run-async": "2.3.0", - "rxjs": "5.5.10", - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "through": "2.3.8" + "run-async": "^2.2.0", + "rx-lite": "^4.0.8", + "rx-lite-aggregates": "^4.0.8", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" }, "dependencies": { "ansi-regex": { @@ -6845,36 +6874,42 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "1.9.1" + "color-convert": "^1.9.0" } }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", "dev": true, "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "3.0.0" + "ansi-regex": "^3.0.0" } }, "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", "dev": true, "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } } } @@ -16029,8 +16064,7 @@ "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "ansi-styles": { "version": "3.2.1", @@ -16080,28 +16114,6 @@ "pinkie-promise": "^2.0.0" } }, - "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.0.4", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rx-lite": "^4.0.8", - "rx-lite-aggregates": "^4.0.8", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - } - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -16112,7 +16124,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, "requires": { "ansi-regex": "^3.0.0" } From a78866f99a8854c30049f4a9632e4a85373b941c Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Jul 2018 18:07:34 -0400 Subject: [PATCH 23/25] Add EV3 block icon --- src/extensions/scratch3_ev3/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index b7b1d06dc..c387a169b 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -7,6 +7,13 @@ const BTSession = require('../../io/btSession'); // TODO: Refactor/rename all these high level primitives to be clearer/match +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// 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='; + /** * High-level primitives / constants used by the extension. * @type {object} @@ -648,7 +655,7 @@ class Scratch3Ev3Blocks { return { id: Scratch3Ev3Blocks.EXTENSION_ID, name: 'LEGO MINDSTORMS EV3', - iconURI: null, + blockIconURI: blockIconURI, showStatusButton: true, blocks: [ { From b30092e890489c42933b873285f27e903a4d71d6 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 2 Jul 2018 18:09:51 -0400 Subject: [PATCH 24/25] Motor for time opcodes return promise to wait --- src/extensions/scratch3_ev3/index.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index c387a169b..e5ea01660 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -839,16 +839,14 @@ class Scratch3Ev3Blocks { const port = Cast.toNumber(args.PORT); const time = Cast.toNumber(args.TIME) * 1000; - this._device.motorTurnClockwise(port, time); - return; + return this._device.motorTurnClockwise(port, time); } motorTurnCounterClockwise (args) { const port = Cast.toNumber(args.PORT); const time = Cast.toNumber(args.TIME) * 1000; - this._device.motorTurnCounterClockwise(port, time); - return; + return this._device.motorTurnCounterClockwise(port, time); } motorRotate (args) { From e02dd35dbfab814afc65156b7349e39266fc83f7 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 9 Jul 2018 11:35:59 -0400 Subject: [PATCH 25/25] remove logging --- src/util/jsonrpc-web-socket.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/util/jsonrpc-web-socket.js b/src/util/jsonrpc-web-socket.js index ac118cdff..af5af27e1 100644 --- a/src/util/jsonrpc-web-socket.js +++ b/src/util/jsonrpc-web-socket.js @@ -28,9 +28,6 @@ class JSONRPCWebSocket extends JSONRPC { _onSocketMessage (e) { const json = JSON.parse(e.data); - if (json.method !== 'characteristicDidChange') { - // log.info(`received message: ${json}`); - } this._handleMessage(json); }