diff --git a/src/extensions/scratch3_wedo2/index.js b/src/extensions/scratch3_wedo2/index.js index 1b20f245a..9d62603ba 100644 --- a/src/extensions/scratch3_wedo2/index.js +++ b/src/extensions/scratch3_wedo2/index.js @@ -2,6 +2,8 @@ const ArgumentType = require('../../extension-support/argument-type'); const BlockType = require('../../extension-support/block-type'); const color = require('../../util/color'); const log = require('../../util/log'); +const BLESession = require('../../io/bleSession'); +const Base64Util = require('../../util/base64-util'); /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. @@ -10,6 +12,51 @@ const log = require('../../util/log'); // eslint-disable-next-line max-len const iconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+d2VkbzItYmxvY2staWNvbjwvdGl0bGU+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMzUuMzEzIDEwLjQ2N0gzMi4wOVY4Ljg2NWMwLS4yMjMuMTgtLjQwNC40MDUtLjQwNGgyLjQxMmMuMjI0IDAgLjQwNi4xODIuNDA2LjQwNXYxLjYwMnpNMzAuNDc3IDEwLjQ2N2gtMy4yMjRWOC44NjVjMC0uMjIzLjE4My0uNDA0LjQwNy0uNDA0aDIuNDFjLjIyNiAwIC40MDcuMTgyLjQwNy40MDV2MS42MDJ6TTI1LjY0IDEwLjQ2N0gyMi40MlY4Ljg2NWMwLS4yMjMuMTgyLS40MDQuNDA2LS40MDRoMi40MWMuMjI2IDAgLjQwNy4xODIuNDA3LjQwNXYxLjYwMnpNMjAuODA2IDEwLjQ2N2gtMy4yMjRWOC44NjVjMC0uMjIzLjE4Mi0uNDA0LjQwNi0uNDA0SDIwLjRjLjIyNCAwIC40MDYuMTgyLjQwNi40MDV2MS42MDJ6TTE1Ljk3IDEwLjQ2N2gtMy4yMjRWOC44NjVjMC0uMjIzLjE4Mi0uNDA0LjQwNy0uNDA0aDIuNDFjLjIyNiAwIC40MDcuMTgyLjQwNy40MDV2MS42MDJ6TTExLjEzNSAxMC40NjdINy45MVY4Ljg2NWMwLS4yMjMuMTgzLS40MDQuNDA3LS40MDRoMi40MTJjLjIyMyAwIC40MDUuMTgyLjQwNS40MDV2MS42MDJ6IiBzdHJva2U9IiM2Rjc4OTMiIGZpbGw9IiNGRkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0zNy43MyAxMC40NjdINi4zYy0yLjY3IDAtNC44MzYgMi4xNTMtNC44MzYgNC44MDh2My4yMDVoMzcuMDczdi03LjIxYzAtLjQ0NC0uMzYyLS44MDMtLjgwNy0uODAzeiIgc3Ryb2tlPSIjNkY3ODkzIiBmaWxsPSIjRkZGIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMzguMTM0IDMwLjk4SDEuODY3Yy0uMjI0IDAtLjQwMy0uMTgtLjQwMy0uNFYxNi4yMzZoMzIuNzFjLjczIDAgMS40My4yODcgMS45NDUuOC41MTUuNTE0IDEuMjE1LjgwMiAxLjk0NC44MDJoLjQ3M3YxMi43NGMwIC4yMi0uMTguNC0uNDAzLjR6IiBzdHJva2U9IiM2Rjc4OTMiIGZpbGw9IiNFNkU3RTgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIHN0cm9rZT0iIzZGNzg5MyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNMzQuODMgMTYuMjM3bC40ODMtMi41NjVoMy4yMjMiLz48cGF0aCBkPSJNMzguNTM2IDExLjI2OFYzMC41OGMwIC4yMi0uMTguNC0uNDAzLjRIMS44NjZjLS4yMiAwLS40MDMtLjE4LS40MDMtLjR2LTEuMjAzaDM0LjI4MmMuNjUgMCAxLjE4LS41MjQgMS4xOC0xLjE3M1YxMC40NjdoLjgwNWMuNDQ2IDAgLjgwNi4zNi44MDYuOHoiIHN0cm9rZT0iIzZGNzg5MyIgZmlsbD0iIzZGNzg5MyIgb3BhY2l0eT0iLjE1IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTEuNTM4IDE2LjI4aDIwLjE0OGMuMjIyIDAgLjQwMy4xOC40MDMuNHY2LjUyN2MwIC4yMjItLjE4Mi40LS40MDQuNEgxMS41MzhjLS4yMjMgMC0uNDA0LS4xNzgtLjQwNC0uNFYxNi42OGMwLS4yMi4xOC0uNC40MDQtLjQiIGZpbGw9IiNFNkU3RTgiLz48cGF0aCBkPSJNMTEuNTM4IDE2LjI4aDIwLjE0OGMuMjIyIDAgLjQwMy4xOC40MDMuNHY2LjUyN2MwIC4yMjItLjE4Mi40LS40MDQuNEgxMS41MzhjLS4yMjMgMC0uNDA0LS4xNzgtLjQwNC0uNFYxNi42OGMwLS4yMi4xOC0uNC40MDQtLjR6IiBzdHJva2U9IiM2Rjc4OTMiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0zMi4wOSAxNi4yOHY2LjkyN2MwIC4yMjItLjE4LjQtLjQwNC40aC0yMC4xNWMtLjIyIDAtLjQtLjE4LS40LS40di0xLjJoMTguMTZjLjY1MyAwIDEuMTgtLjUyNiAxLjE4LTEuMTc0VjE2LjI4aDEuNjEzeiIgc3Ryb2tlPSIjNkY3ODkzIiBmaWxsPSIjNkU3NzkyIiBvcGFjaXR5PSIuMTUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0zMC40NzcgMTYuMjhoLTMuMjI0di0xLjYwNGMwLS4yMjMuMTgzLS40MDQuNDA3LS40MDRoMi40MWMuMjI2IDAgLjQwNy4xOC40MDcuNDA0djEuNjAzek0xNS45NyAxNi4yOGgtMy4yMjR2LTEuNjA0YzAtLjIyMy4xODItLjQwNC40MDctLjQwNGgyLjQxYy4yMjYgMCAuNDA3LjE4LjQwNy40MDR2MS42MDN6TTI1LjY0IDE2LjI4SDIyLjQydi0xLjYwNGMwLS4yMjMuMTgyLS40MDQuNDA2LS40MDRoMi40MWMuMjI2IDAgLjQwNy4xOC40MDcuNDA0djEuNjAzek0yMC44MDYgMTYuMjhoLTMuMjI0di0xLjYwNGMwLS4yMjMuMTgyLS40MDQuNDA2LS40MDRIMjAuNGMuMjI0IDAgLjQwNi4xOC40MDYuNDA0djEuNjAzeiIgc3Ryb2tlPSIjNkY3ODkzIiBmaWxsPSIjRTZFN0U4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTguNTU3IDE5LjkxYzAgMS4wMjUtLjgzNyAxLjg1Ny0xLjg3IDEuODU3LTEuMDMgMC0xLjg2Ny0uODMyLTEuODY3LTEuODU4IDAtMS4wMjcuODM3LTEuODU4IDEuODY4LTEuODU4IDEuMDMyIDAgMS44Ny44MyAxLjg3IDEuODU3ek0yMy40OCAxOS45MWMwIDEuMDI1LS44MzYgMS44NTctMS44NjggMS44NTdzLTEuODctLjgzMi0xLjg3LTEuODU4YzAtMS4wMjcuODM4LTEuODU4IDEuODctMS44NThzMS44NjguODMgMS44NjggMS44NTd6TTI4LjQwNCAxOS45MWMwIDEuMDI1LS44MzcgMS44NTctMS44NjggMS44NTctMS4wMzIgMC0xLjg3LS44MzItMS44Ny0xLjg1OCAwLTEuMDI3LjgzOC0xLjg1OCAxLjg3LTEuODU4IDEuMDMgMCAxLjg2OC44MyAxLjg2OCAxLjg1N3oiIHN0cm9rZT0iIzZGNzg5MyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTE4LjU1NyAxOS45MjJjMCAxLjAyNi0uODM3IDEuODU4LTEuODcgMS44NTgtMS4wMyAwLTEuODY3LS44MzItMS44NjctMS44NTggMC0xLjAyNS44MzctMS44NTcgMS44NjgtMS44NTcgMS4wMzIgMCAxLjg3LjgzMiAxLjg3IDEuODU3TTIzLjQ4IDE5LjkyMmMwIDEuMDI2LS44MzYgMS44NTgtMS44NjggMS44NThzLTEuODctLjgzMi0xLjg3LTEuODU4YzAtMS4wMjUuODM4LTEuODU3IDEuODctMS44NTdzMS44NjguODMyIDEuODY4IDEuODU3TTI4LjQwNCAxOS45MjJjMCAxLjAyNi0uODM3IDEuODU4LTEuODY4IDEuODU4LTEuMDMyIDAtMS44Ny0uODMyLTEuODctMS44NTggMC0xLjAyNS44MzgtMS44NTcgMS44Ny0xLjg1NyAxLjAzIDAgMS44NjguODMyIDEuODY4IDEuODU3IiBmaWxsPSIjNkY3ODkzIiBvcGFjaXR5PSIuNSIvPjwvZz48L3N2Zz4='; +const UUID = { + DEVICE_SERVICE: '00001523-1212-efde-1523-785feabcd123', + IO_SERVICE: '00004f0e-1212-efde-1523-785feabcd123', + ATTACHED_IO: '00001527-1212-efde-1523-785feabcd123', + INPUT_VALUES: '00001560-1212-efde-1523-785feabcd123', + INPUT_COMMAND: '00001563-1212-efde-1523-785feabcd123', + OUTPUT_COMMAND: '00001565-1212-efde-1523-785feabcd123' +}; + +/** + * Enum for WeDo2 sensor and output types. + * @readonly + * @enum {number} + */ +const WeDo2Types = { + MOTOR: 1, + PIEZO: 22, + LED: 23, + TILT: 34, + DISTANCE: 35 +}; + +/** + * Enum for connection/port ids assigned to internal WeDo2 output devices. + * @readonly + * @enum {number} + */ +const WeDo2ConnectIDs = { + LED: 6, + PIEZO: 5 +}; + +/** + * Enum for ids for various output commands on the WeDo2. + * @readonly + * @enum {number} + */ +const WeDo2Commands = { + MOTOR_POWER: 1, + PLAY_TONE: 2, + STOP_TONE: 3, + WRITE_RGB: 4, + SET_VOLUME: 255 +}; + /** * Manage power, direction, and timers for one WeDo 2.0 motor. */ @@ -118,7 +165,14 @@ class WeDo2Motor { * Turn this motor on indefinitely. */ setMotorOn () { - this._parent._send('motorOn', {motorIndex: this._index, power: this._direction * this._power}); + const cmd = new Uint8Array(4); + cmd[0] = this._index + 1; // connect id + cmd[1] = WeDo2Commands.MOTOR_POWER; // command + cmd[2] = 1; // 1 byte to follow + cmd[3] = this._power * this._direction; // power in range 0-100 + + this._parent._send(UUID.OUTPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd)); + this._isOn = true; this._clearTimeout(); } @@ -137,7 +191,14 @@ class WeDo2Motor { * Start active braking on this motor. After a short time, the motor will turn off. */ startBraking () { - this._parent._send('motorBrake', {motorIndex: this._index}); + const cmd = new Uint8Array(4); + cmd[0] = this._index + 1; // connect id + cmd[1] = WeDo2Commands.MOTOR_POWER; // command + cmd[2] = 1; // 1 byte to follow + cmd[3] = 127; // power in range 0-100 + + this._parent._send(UUID.OUTPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd)); + this._isOn = false; this._setNewTimeout(this.setMotorOff, WeDo2Motor.BRAKE_TIME_MS); } @@ -146,7 +207,14 @@ class WeDo2Motor { * Turn this motor off. */ setMotorOff () { - this._parent._send('motorOff', {motorIndex: this._index}); + const cmd = new Uint8Array(4); + cmd[0] = this._index + 1; // connect id + cmd[1] = WeDo2Commands.MOTOR_POWER; // command + cmd[2] = 1; // 1 byte to follow + cmd[3] = 0; // power in range 0-100 + + this._parent._send(UUID.OUTPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd)); + this._isOn = false; } @@ -180,35 +248,33 @@ class WeDo2Motor { } /** - * Manage communication with a WeDo 2.0 device over a Device Manager client socket. + * Manage communication with a WeDo 2.0 device over a Bluetooth Low Energy client socket. */ class WeDo2 { - /** - * @return {string} - the type of Device Manager device socket that this class will handle. - */ - static get DEVICE_TYPE () { - return 'wedo2'; - } + constructor (runtime, extensionId) { - /** - * Construct a WeDo2 communication object. - * @param {Socket} socket - the socket for a WeDo 2.0 device, as provided by a Device Manager client. - */ - constructor (socket) { /** - * The socket-IO socket used to communicate with the Device Manager about this device. - * @type {Socket} + * The Scratch 3.0 runtime used to trigger the green flag button. + * @type {Runtime} * @private */ - this._socket = socket; + this._runtime = runtime; + this._runtime.on('PROJECT_STOP_ALL', this._stopAll.bind(this)); + + /** + * The device ports that connect to motors and sensors. + * @type {string[]} + * @private + */ + this._ports = ['none', 'none']; // TODO: rename? /** * The motors which this WeDo 2.0 could possibly have. * @type {WeDo2Motor[]} * @private */ - this._motors = [new WeDo2Motor(this, 0), new WeDo2Motor(this, 1)]; + this._motors = [null, null]; /** * The most recently received value for each sensor. @@ -221,17 +287,16 @@ class WeDo2 { distance: 0 }; - this._onSensorChanged = this._onSensorChanged.bind(this); - this._onDisconnect = this._onDisconnect.bind(this); + /** + * The Bluetooth connection session for reading/writing device data. + * @type {BLESession} + * @private + */ + this._ble = null; + this._runtime.registerExtensionDevice(extensionId, this); - this._connectEvents(); - } - - /** - * Manually dispose of this object. - */ - dispose () { - this._disconnectEvents(); + this._onConnect = this._onConnect.bind(this); + this._onMessage = this._onMessage.bind(this); } /** @@ -252,7 +317,7 @@ class WeDo2 { * @return {number} - the latest value received from the distance sensor. */ get distance () { - return this._sensors.distance * 10; + return this._sensors.distance; } /** @@ -264,12 +329,46 @@ class WeDo2 { return this._motors[index]; } + /** + * Stop all the motors that are currently running. + */ + stopAllMotors () { + this._motors.forEach(motor => { + if (motor && motor.isOn) { + motor.setMotorOff(); + } + }); + } + /** * Set the WeDo 2.0 hub's LED to a specific color. * @param {int} rgb - a 24-bit RGB color in 0xRRGGBB format. */ setLED (rgb) { - this._send('setLED', {rgb}); + const cmd = new Uint8Array(6); + cmd[0] = WeDo2ConnectIDs.LED; // connect id + cmd[1] = WeDo2Commands.WRITE_RGB; // command + cmd[2] = 3; // 3 bytes to follow + cmd[3] = (rgb >> 16) & 0x000000FF; + cmd[4] = (rgb >> 8) & 0x000000FF; + cmd[5] = (rgb) & 0x000000FF; + + this._send(UUID.OUTPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd)); + } + + /** + * Switch off the LED on the WeDo2. + */ + stopLED () { + const cmd = new Uint8Array(6); + cmd[0] = WeDo2ConnectIDs.LED; // connect id + cmd[1] = WeDo2Commands.WRITE_RGB; // command + cmd[2] = 3; // 3 bytes to follow + cmd[3] = 0x000000; // off + cmd[4] = 0x000000; + cmd[5] = 0x000000; + + this._send(UUID.OUTPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd)); } /** @@ -278,63 +377,237 @@ class WeDo2 { * @param {int} milliseconds - the duration of the note, in milliseconds. */ playTone (tone, milliseconds) { - this._send('playTone', {tone, ms: milliseconds}); + const cmd = new Uint8Array(7); + cmd[0] = WeDo2ConnectIDs.PIEZO; // connect id + cmd[1] = WeDo2Commands.PLAY_TONE; // command + cmd[2] = 4; // 4 bytes to follow + cmd[3] = tone; + cmd[4] = tone >> 8; + cmd[5] = milliseconds; + cmd[6] = milliseconds >> 8; + + this._send(UUID.OUTPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd)); } /** * Stop the tone playing from the WeDo 2.0 hub, if any. */ stopTone () { - this._send('stopTone'); + const cmd = new Uint8Array(2); + cmd[0] = WeDo2ConnectIDs.PIEZO; // connect id + cmd[1] = WeDo2Commands.STOP_TONE; // command + + this._send(UUID.OUTPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd)); } /** - * Attach event handlers to the device socket. - * @private + * Called by the runtime when user wants to scan for a device. */ - _connectEvents () { - this._socket.on('sensorChanged', this._onSensorChanged); - this._socket.on('deviceWasClosed', this._onDisconnect); - this._socket.on('disconnect', this._onDisconnect); + // TODO: rename scan? + startDeviceScan () { + this._ble = new BLESession(this._runtime, { + filters: [{services: [UUID.DEVICE_SERVICE]}], + optionalServices: [UUID.IO_SERVICE] + }, this._onConnect); } /** - * Detach event handlers from the device socket. - * @private + * Called by the runtime when user wants to connect to a certain device. + * @param {number} id - the id of the device to connect to. */ - _disconnectEvents () { - this._socket.off('sensorChanged', this._onSensorChanged); - this._socket.off('deviceWasClosed', this._onDisconnect); - this._socket.off('disconnect', this._onDisconnect); + // TODO: rename connect? + connectDevice (id) { + this._ble.connectDevice(id); } /** - * Store the sensor value from an incoming 'sensorChanged' event. - * @param {object} event - the 'sensorChanged' event. - * @property {string} sensorName - the name of the sensor which changed. - * @property {number} sensorValue - the new value of the sensor. - * @private + * Disconnects from the current BLE session. */ - _onSensorChanged (event) { - this._sensors[event.sensorName] = event.sensorValue; + // TODO: rename disconnect? + disconnectSession () { + // window.clearInterval(this._timeoutID); + this._ble.disconnectSession(); } /** - * React to device disconnection. May be called more than once. - * @private + * Called by the runtime to detect whether the device is connected. + * @return {boolean} - the connected state. */ - _onDisconnect () { - this._disconnectEvents(); + // TODO: rename isConnected + getPeripheralIsConnected () { + let connected = false; + if (this._ble) { + connected = this._ble.getPeripheralIsConnected(); + } + return connected; } /** - * Send a message to the device socket. - * @param {string} message - the name of the message, such as 'playTone'. - * @param {object} [details] - optional additional details for the message, such as tone duration and pitch. + * Sets LED mode and starts reading data from device after BLE has connected. * @private */ - _send (message, details) { - this._socket.emit(message, details); + _onConnect () { + // set LED input mode to RGB + this._setLEDMode() + .then(() => { + // register for attached io notifications + this._ble.read(UUID.DEVICE_SERVICE, UUID.ATTACHED_IO, true, this._onMessage); + }); + + // this._setVolume(); + } + + /** + * Process the sensor data from the incoming BLE characteristic. + * @param {object} base64 - the incoming BLE data. + * @private + */ + _onMessage (base64) { + const data = Base64Util.base64ToUint8Array(base64); + // log.info(data); + + if (data.length === 2) { // disconnect sensor + const connectID = data[0]; + // zero out tilt + if (this._ports[connectID - 1] === WeDo2Types.TILT) { + this._sensors.tiltX = this._sensors.tiltY = 0; + } + // zero out distance + if (this._ports[connectID - 1] === WeDo2Types.DISTANCE) { + this._sensors.distance = 0; + } + // remove references to ports and motors + if (connectID === 1 || connectID === 2) { + this._ports[connectID - 1] = 'none'; + this._motors[connectID - 1] = null; + // log.info(`this._ports = ${this._ports}`); + // log.info(`this._motors = ${this._mtors}`); + } + } + + if (data.length === 3) { // distance sensor value? + this._sensors.distance = data[2]; + } + + if (data.length === 4) { // tilt sensor value? + this._sensors.tiltX = data[2]; + this._sensors.tiltY = data[3]; + } + + if (data.length === 12) { // attached io? + + const connectID = data[0]; + const type = data[3]; + + // Record which port is connected to what type of device + if (connectID === 1 || connectID === 2) { + this._ports[connectID - 1] = type; + } + + // Motor + if (type === WeDo2Types.MOTOR) { + this._motors[connectID - 1] = new WeDo2Motor(this, connectID - 1); + } + + // Tilt Sensor + if (type === WeDo2Types.TILT) { + const cmd = new Uint8Array(11); + cmd[0] = 1; // sensor format + cmd[1] = 2; // command type: write + cmd[2] = connectID; // connect id + cmd[3] = WeDo2Types.TILT; // type + cmd[4] = 0; // mode: angle + cmd[5] = 1; // delta interval, 4 bytes + cmd[6] = 0; + cmd[7] = 0; + cmd[8] = 0; + cmd[9] = 0; // unit? + cmd[10] = 1; // notifications enabled: true + + this._send(UUID.INPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd)) + .then(() => { + this._ble.read(UUID.IO_SERVICE, UUID.INPUT_VALUES, true, this._onMessage.bind(this)); + }); + } + + // Distance Sensor + if (type === WeDo2Types.DISTANCE) { + const cmd = new Uint8Array(11); + cmd[0] = 1; // sensor format + cmd[1] = 2; // command type: write + cmd[2] = connectID; // connect id + cmd[3] = WeDo2Types.DISTANCE; // type + cmd[4] = 0; // mode: detect + cmd[5] = 1; // delta interval, 4 bytes + cmd[6] = 0; + cmd[7] = 0; + cmd[8] = 0; + cmd[9] = 0; // unit: ? + cmd[10] = 1; // notifications enabled: true + + this._send(UUID.INPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd)) + .then(() => { + this._ble.read(UUID.IO_SERVICE, UUID.INPUT_VALUES, true, this._onMessage.bind(this)); + }); + } + } + } + + /** + * Write a message to the device BLE session. + * @param {number} uuid - the UUID of the characteristic to write to + * @param {Uint8Array} message - the message to write. + * @return {Promise} - a promise result of the write operation + * @private + */ + _send (uuid, message) { + if (!this.getPeripheralIsConnected()) return; + return this._ble.write(UUID.IO_SERVICE, uuid, message, 'base64'); + } + + /** + * Sets the volume for the piezo. + * @private + */ + _setVolume () { + const cmd = new Uint8Array(4); + cmd[0] = WeDo2ConnectIDs.PIEZO; // connect id + cmd[1] = WeDo2Commands.SET_VOLUME; // command + cmd[2] = 1; // 1 byte to follow + cmd[3] = 100; // volume in range 0-100 + + this._send(UUID.OUTPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd)); + } + + /** + * Sets the input mode of the LED to RGB. + * @return {Promise} - a promise returned by the send operation. + * @private + */ + _setLEDMode () { + const cmd = new Uint8Array(11); + cmd[0] = 1; // sensor format + cmd[1] = 2; // command type: 2 = write + cmd[2] = WeDo2ConnectIDs.LED; // port + cmd[3] = WeDo2Types.LED; // type + cmd[4] = 1; // mode + cmd[5] = 0; // delta interval, 4 bytes + cmd[6] = 0; + cmd[7] = 0; + cmd[8] = 0; + cmd[9] = 0; // unit = raw + cmd[10] = 0; // notifications enabled: false + + return this._send(UUID.INPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd)); + } + + /** + * Stop the tone playing, LED output and motors on the WeDo 2.0 hub. + */ + _stopAll () { + this.stopTone(); + this.stopAllMotors(); + this.stopLED(); } } @@ -404,7 +677,8 @@ class Scratch3WeDo2Blocks { */ this.runtime = runtime; - this.connect(); + // Create a new WeDo2 device instance + this._device = new WeDo2(this.runtime, Scratch3WeDo2Blocks.EXTENSION_ID); } /** @@ -415,6 +689,7 @@ class Scratch3WeDo2Blocks { id: Scratch3WeDo2Blocks.EXTENSION_ID, name: 'WeDo 2.0', iconURI: iconURI, + showStatusButton: true, blocks: [ { opcode: 'motorOnFor', @@ -585,35 +860,6 @@ class Scratch3WeDo2Blocks { }; } - /** - * Use the Device Manager client to attempt to connect to a WeDo 2.0 device. - */ - connect () { - if (this._device || this._finder) { - return; - } - const deviceManager = this.runtime.ioDevices.deviceManager; - const finder = this._finder = - deviceManager.searchAndConnect(Scratch3WeDo2Blocks.EXTENSION_ID, WeDo2.DEVICE_TYPE); - this._finder.promise.then( - socket => { - if (this._finder === finder) { - this._finder = null; - this._device = new WeDo2(socket); - } else { - log.warn('Ignoring success from stale WeDo 2.0 connection attempt'); - } - }, - reason => { - if (this._finder === finder) { - this._finder = null; - log.warn(`WeDo 2.0 connection failed: ${reason}`); - } else { - log.warn('Ignoring failure from stale WeDo 2.0 connection attempt'); - } - }); - } - /** * Turn specified motor(s) on for a specified duration. * @param {object} args - the block's arguments. @@ -625,7 +871,10 @@ class Scratch3WeDo2Blocks { const durationMS = args.DURATION * 1000; return new Promise(resolve => { this._forEachMotor(args.MOTOR_ID, motorIndex => { - this._device.motor(motorIndex).setMotorOnFor(durationMS); + const motor = this._device.motor(motorIndex); + if (motor) { + motor.setMotorOnFor(durationMS); + } }); // Ensure this block runs for a fixed amount of time even when no device is connected. @@ -640,7 +889,10 @@ class Scratch3WeDo2Blocks { */ motorOn (args) { this._forEachMotor(args.MOTOR_ID, motorIndex => { - this._device.motor(motorIndex).setMotorOn(); + const motor = this._device.motor(motorIndex); + if (motor) { + motor.setMotorOn(); + } }); } @@ -651,7 +903,10 @@ class Scratch3WeDo2Blocks { */ motorOff (args) { this._forEachMotor(args.MOTOR_ID, motorIndex => { - this._device.motor(motorIndex).setMotorOff(); + const motor = this._device.motor(motorIndex); + if (motor) { + motor.setMotorOff(); + } }); } @@ -664,8 +919,10 @@ class Scratch3WeDo2Blocks { startMotorPower (args) { this._forEachMotor(args.MOTOR_ID, motorIndex => { const motor = this._device.motor(motorIndex); - motor.power = args.POWER; - motor.setMotorOn(); + if (motor) { + motor.power = args.POWER; + motor.setMotorOn(); + } }); } @@ -679,19 +936,21 @@ class Scratch3WeDo2Blocks { setMotorDirection (args) { this._forEachMotor(args.MOTOR_ID, motorIndex => { const motor = this._device.motor(motorIndex); - switch (args.DIRECTION) { - case MotorDirection.FORWARD: - motor.direction = 1; - break; - case MotorDirection.BACKWARD: - motor.direction = -1; - break; - case MotorDirection.REVERSE: - motor.direction = -motor.direction; - break; - default: - log.warn(`Unknown motor direction in setMotorDirection: ${args.DIRECTION}`); - break; + if (motor) { + switch (args.DIRECTION) { + case MotorDirection.FORWARD: + motor.direction = 1; + break; + case MotorDirection.BACKWARD: + motor.direction = -1; + break; + case MotorDirection.REVERSE: + motor.direction = -motor.direction; + break; + default: + log.warn(`Unknown motor direction in setMotorDirection: ${args.DIRECTION}`); + break; + } } }); } @@ -813,13 +1072,13 @@ class Scratch3WeDo2Blocks { _getTiltAngle (direction) { switch (direction) { case TiltDirection.UP: - return -this._device.tiltY; + return this._device.tiltY > 45 ? 256 - this._device.tiltY : -this._device.tiltY; case TiltDirection.DOWN: - return this._device.tiltY; + return this._device.tiltY > 45 ? this._device.tiltY - 256 : this._device.tiltY; case TiltDirection.LEFT: - return -this._device.tiltX; + return this._device.tiltX > 45 ? 256 - this._device.tiltX : -this._device.tiltX; case TiltDirection.RIGHT: - return this._device.tiltX; + return this._device.tiltX > 45 ? this._device.tiltX - 256 : this._device.tiltX; default: log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`); } diff --git a/src/io/bleSession.js b/src/io/bleSession.js index 357b80ef6..f19d81148 100644 --- a/src/io/bleSession.js +++ b/src/io/bleSession.js @@ -1,5 +1,5 @@ const JSONRPCWebSocket = require('../util/jsonrpc-web-socket'); -// const log = require('../util/log'); +const log = require('../util/log'); const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/ble'; class BLESession extends JSONRPCWebSocket { @@ -121,7 +121,9 @@ class BLESession extends JSONRPCWebSocket { this._characteristicDidChangeCallback = onCharacteristicChanged; return this.sendRemoteRequest('read', params) .catch(e => { - this._sendError(e); + if (e.data !== 'Reading is not permitted.') { // TODO: workaround til notify-only supported + this._sendError(e); + } }); } @@ -144,9 +146,9 @@ class BLESession extends JSONRPCWebSocket { }); } - _sendError (/* e */) { + _sendError (e) { this._connected = false; - // log.error(`BLESession error: ${JSON.stringify(e)}`); + log.error(`BLESession error: ${JSON.stringify(e)}`); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); }