mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 14:32:59 -05:00
WeDo2 Extension: First Pass (#1441)
* Adding WeDo2 connection/disconnection over Scratch Link BLE. * Testing motor and LED output. * Reading sensor attachment and registering for sensor notifications. * Motor indexing. * Some refactoring notes. Moving some functions around. * Refactoring and enums. * Removing some comments * Fixing linting error. * Fixing tilt reporter values and added stopAll bound to stop button to stop piezo, LED and motors.
This commit is contained in:
parent
30892dfd43
commit
f01733f019
2 changed files with 378 additions and 117 deletions
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue