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.