scratch-vm/src/extensions/scratch3_wedo2/index.js

1243 lines
42 KiB
JavaScript
Raw Normal View History

const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Cast = require('../../util/cast');
const formatMessage = require('format-message');
const color = require('../../util/color');
const log = require('../../util/log');
const BLESession = require('../../io/bleSession');
const Base64Util = require('../../util/base64-util');
const MathUtil = require('../../util/math-util');
2017-11-06 10:00:13 -05:00
/**
* Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
* @type {string}
*/
2017-11-06 10:10:41 -05:00
// eslint-disable-next-line max-len
2018-08-14 13:51:17 -04:00
const iconURI = 'data:image/png;base64,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';
2017-11-06 10:00:13 -05:00
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
};
/**
* Enum for modes for input sensors on the WeDo2.
* @enum {number}
*/
const WeDo2Modes = {
TILT: 0, // angle
DISTANCE: 0 // detect
};
/**
* Enum for units for input sensors on the WeDo2.
* @enum {number}
*/
const WeDo2Units = {
TILT: 0, // raw
DISTANCE: 1 // percent
};
/**
* Manage power, direction, and timers for one WeDo 2.0 motor.
*/
class WeDo2Motor {
/**
* Construct a WeDo2Motor instance.
* @param {WeDo2} parent - the WeDo 2.0 device which owns this motor.
* @param {int} index - the zero-based index of this motor on its parent device.
*/
constructor (parent, index) {
/**
* The WeDo 2.0 device which owns this motor.
* @type {WeDo2}
* @private
*/
this._parent = parent;
/**
* The zero-based index of this motor on its parent device.
* @type {int}
* @private
*/
this._index = index;
/**
* This motor's current direction: 1 for "this way" or -1 for "that way"
* @type {number}
* @private
*/
this._direction = 1;
/**
* This motor's current power level, in the range [0,100].
* @type {number}
* @private
*/
this._power = 100;
/**
* Is this motor currently moving?
* @type {boolean}
* @private
*/
this._isOn = false;
/**
* If the motor has been turned on or is actively braking for a specific duration, this is the timeout ID for
* the end-of-action handler. Cancel this when changing plans.
* @type {Object}
* @private
*/
this._pendingTimeoutId = null;
/**
* The starting time for the pending timeout.
* @type {Object}
* @private
*/
this._pendingTimeoutStartTime = null;
/**
* The delay/duration of the pending timeout.
* @type {Object}
* @private
*/
this._pendingTimeoutDelay = null;
this.startBraking = this.startBraking.bind(this);
this.setMotorOff = this.setMotorOff.bind(this);
}
/**
* @return {number} - the duration of active braking after a call to startBraking(). Afterward, turn the motor off.
* @constructor
*/
static get BRAKE_TIME_MS () {
return 1000;
}
/**
* @return {int} - this motor's current direction: 1 for "this way" or -1 for "that way"
*/
get direction () {
return this._direction;
}
/**
* @param {int} value - this motor's new direction: 1 for "this way" or -1 for "that way"
*/
set direction (value) {
if (value < 0) {
this._direction = -1;
} else {
this._direction = 1;
}
}
/**
* @return {int} - this motor's current power level, in the range [0,100].
*/
get power () {
return this._power;
}
/**
* @param {int} value - this motor's new power level, in the range [0,100].
*/
set power (value) {
this._power = Math.max(0, Math.min(value, 100));
}
/**
* @return {boolean} - true if this motor is currently moving, false if this motor is off or braking.
*/
get isOn () {
return this._isOn;
}
/**
* @return {boolean} - time, in milliseconds, of when the pending timeout began.
*/
get pendingTimeoutStartTime () {
return this._pendingTimeoutStartTime;
}
/**
* @return {boolean} - delay, in milliseconds, of the pending timeout.
*/
get pendingTimeoutDelay () {
return this._pendingTimeoutDelay;
}
/**
* Turn this motor on indefinitely.
*/
setMotorOn () {
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();
}
/**
* Turn this motor on for a specific duration.
* @param {number} milliseconds - run the motor for this long.
*/
setMotorOnFor (milliseconds) {
milliseconds = Math.max(0, milliseconds);
this.setMotorOn();
this._setNewTimeout(this.startBraking, milliseconds);
}
/**
* Start active braking on this motor. After a short time, the motor will turn off.
*/
startBraking () {
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);
}
/**
* Turn this motor off.
*/
setMotorOff () {
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;
}
/**
* Clear the motor action timeout, if any. Safe to call even when there is no pending timeout.
* @private
*/
_clearTimeout () {
if (this._pendingTimeoutId !== null) {
clearTimeout(this._pendingTimeoutId);
this._pendingTimeoutId = null;
this._pendingTimeoutStartTime = null;
this._pendingTimeoutDelay = null;
}
}
/**
* Set a new motor action timeout, after clearing an existing one if necessary.
* @param {Function} callback - to be called at the end of the timeout.
* @param {int} delay - wait this many milliseconds before calling the callback.
* @private
*/
_setNewTimeout (callback, delay) {
this._clearTimeout();
const timeoutID = setTimeout(() => {
if (this._pendingTimeoutId === timeoutID) {
this._pendingTimeoutId = null;
}
callback();
}, delay);
this._pendingTimeoutId = timeoutID;
this._pendingTimeoutStartTime = Date.now();
this._pendingTimeoutDelay = delay;
}
}
/**
* Manage communication with a WeDo 2.0 device over a Bluetooth Low Energy client socket.
*/
class WeDo2 {
constructor (runtime, extensionId) {
/**
* The Scratch 3.0 runtime used to trigger the green flag button.
* @type {Runtime}
* @private
*/
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 = [null, null];
/**
* The most recently received value for each sensor.
* @type {Object.<string, number>}
* @private
*/
this._sensors = {
tiltX: 0,
tiltY: 0,
distance: 0
};
/**
* The Bluetooth connection session for reading/writing device data.
* @type {BLESession}
* @private
*/
this._ble = null;
this._runtime.registerExtensionDevice(extensionId, this);
this._onConnect = this._onConnect.bind(this);
this._onMessage = this._onMessage.bind(this);
}
/**
* @return {number} - the latest value received for the tilt sensor's tilt about the X axis.
*/
get tiltX () {
return this._sensors.tiltX;
}
/**
* @return {number} - the latest value received for the tilt sensor's tilt about the Y axis.
*/
get tiltY () {
return this._sensors.tiltY;
}
/**
* @return {number} - the latest value received from the distance sensor.
*/
get distance () {
return this._sensors.distance;
}
/**
* Access a particular motor on this device.
* @param {int} index - the zero-based index of the desired motor.
* @return {WeDo2Motor} - the WeDo2Motor instance, if any, at that index.
*/
motor (index) {
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.
* @return {Promise} - a promise of the set led send operation.
*/
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;
return 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));
}
/**
* Play a tone from the WeDo 2.0 hub for a specific amount of time.
* @param {int} tone - the pitch of the tone, in Hz.
* @param {int} milliseconds - the duration of the note, in milliseconds.
*/
playTone (tone, 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 () {
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));
}
/**
* Called by the runtime when user wants to scan for a device.
*/
// TODO: rename scan?
startDeviceScan () {
this._ble = new BLESession(this._runtime, {
filters: [{services: [UUID.DEVICE_SERVICE]}],
optionalServices: [UUID.IO_SERVICE]
}, this._onConnect);
}
/**
* Called by the runtime when user wants to connect to a certain device.
* @param {number} id - the id of the device to connect to.
*/
// TODO: rename connect?
connectDevice (id) {
this._ble.connectDevice(id);
}
/**
* Disconnects from the current BLE session.
*/
// TODO: rename disconnect?
disconnectSession () {
// window.clearInterval(this._timeoutID);
this._ble.disconnectSession();
}
/**
* Called by the runtime to detect whether the device is connected.
* @return {boolean} - the connected state.
*/
// TODO: rename isConnected
getPeripheralIsConnected () {
let connected = false;
if (this._ble) {
connected = this._ble.getPeripheralIsConnected();
}
return connected;
}
/**
* Sets LED mode and initial color and starts reading data from device after BLE has connected.
* @private
*/
_onConnect () {
// set LED input mode to RGB
this._setLEDMode()
.then(() => {
// set LED to blue
this.setLED(0x0000FF);
})
.then(() => {
// register for attached io notifications
// TODO: make backwards compatible with 'read':
// - try 'startNotifications'
// - then try 'read' with 'startNotifications' flag
// - then catch OSX and Windows errors
this._ble.startNotifications(UUID.DEVICE_SERVICE, UUID.ATTACHED_IO, this._onMessage);
});
}
2018-08-14 13:45:55 -04:00
/**
* 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');
}
/**
* 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 first byte of data is '1' or '2', then either clear the
* sensor present in ports 1 or 2 or set their format.
*
* If first byte of data is anything else, read incoming sensor value.
*/
switch (data[0]) {
case 1:
case 2: {
const connectID = data[0];
if (data[1] === 0) {
// clear sensor or motor
this._clearPort(connectID);
} else {
// register sensor or motor
this._registerSensorOrMotor(connectID, data[3]);
}
break;
}
default: {
// read incoming sensor value
const connectID = data[1];
const type = this._ports[connectID - 1];
if (type === WeDo2Types.DISTANCE) {
this._sensors.distance = data[2];
}
if (type === WeDo2Types.TILT) {
this._sensors.tiltX = data[2];
this._sensors.tiltY = data[3];
}
break;
}
}
}
/**
* Clear the sensor or motor present at port 1 or 2.
* @param {number} connectID - the port to clear.
* @private
*/
_clearPort (connectID) {
const type = this._ports[connectID - 1];
if (type === WeDo2Types.TILT) {
this._sensors.tiltX = this._sensors.tiltY = 0;
}
if (type === WeDo2Types.DISTANCE) {
this._sensors.distance = 0;
}
this._ports[connectID - 1] = 'none';
this._motors[connectID - 1] = null;
}
/**
* Register a new sensor or motor connected at a port. Store the type of
* sensor or motor internally, and then register for notifications on input
* values if it is a sensor.
* @param {number} connectID - the port to register a sensor or motor on.
* @param {number} type - the type ID of the sensor or motor
*/
_registerSensorOrMotor (connectID, type) {
// Record which port is connected to what type of device
this._ports[connectID - 1] = type;
// Register motor
if (type === WeDo2Types.MOTOR) {
this._motors[connectID - 1] = new WeDo2Motor(this, connectID - 1);
} else {
// Register tilt or distance sensor
const typeString = type === WeDo2Types.DISTANCE ? 'DISTANCE' : 'TILT';
const cmd = new Uint8Array(11);
cmd[0] = 1; // sensor format
cmd[1] = 2; // command type: write
cmd[2] = connectID; // connect id
cmd[3] = type; // type
cmd[4] = WeDo2Modes[typeString]; // mode
cmd[5] = 1; // delta interval, 4 bytes, 1 = continuous updates
cmd[6] = 0;
cmd[7] = 0;
cmd[8] = 0;
cmd[9] = WeDo2Units[typeString]; // unit
cmd[10] = 1; // notifications enabled: true
this._send(UUID.INPUT_COMMAND, Base64Util.uint8ArrayToBase64(cmd))
.then(() => {
// TODO: make backwards compatible with 'read':
// - try 'startNotifications'
// - then try 'read' with 'startNotifications' flag
// - then catch OSX and Windows errors
this._ble.startNotifications(UUID.IO_SERVICE, UUID.INPUT_VALUES, this._onMessage);
});
}
}
/**
* 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 and motors on the WeDo 2.0 hub.
*/
_stopAll () {
this.stopTone();
this.stopAllMotors();
// this.stopLED();
}
}
2017-05-05 18:05:21 -04:00
/**
* Enum for motor specification.
* @readonly
* @enum {string}
*/
const MotorID = {
DEFAULT: 'motor',
A: 'motor A',
B: 'motor B',
ALL: 'all motors'
};
/**
* Enum for motor direction specification.
* @readonly
* @enum {string}
*/
const MotorDirection = {
FORWARD: 'this way',
BACKWARD: 'that way',
REVERSE: 'reverse'
};
/**
* Enum for tilt sensor direction.
* @readonly
* @enum {string}
*/
const TiltDirection = {
UP: 'up',
DOWN: 'down',
LEFT: 'left',
RIGHT: 'right',
ANY: 'any'
};
/**
* Scratch 3.0 blocks to interact with a LEGO WeDo 2.0 device.
*/
class Scratch3WeDo2Blocks {
/**
* @return {string} - the ID of this extension.
*/
static get EXTENSION_ID () {
return 'wedo2';
}
2017-05-05 18:05:21 -04:00
/**
* @return {number} - the tilt sensor counts as "tilted" if its tilt angle meets or exceeds this threshold.
*/
static get TILT_THRESHOLD () {
return 15;
}
/**
* Construct a set of WeDo 2.0 blocks.
* @param {Runtime} runtime - the Scratch 3.0 runtime.
*/
constructor (runtime) {
/**
* The Scratch 3.0 runtime.
* @type {Runtime}
*/
this.runtime = runtime;
// Create a new WeDo2 device instance
this._device = new WeDo2(this.runtime, Scratch3WeDo2Blocks.EXTENSION_ID);
}
/**
* @returns {object} metadata for this extension and its blocks.
*/
getInfo () {
return {
id: Scratch3WeDo2Blocks.EXTENSION_ID,
name: 'WeDo 2.0',
2018-08-14 13:51:17 -04:00
blockIconURI: iconURI,
showStatusButton: true,
blocks: [
{
opcode: 'motorOnFor',
text: formatMessage({
id: 'wedo2.motorOnFor',
default: 'turn [MOTOR_ID] on for [DURATION] seconds',
description: 'turn a motor on for some time'
}),
blockType: BlockType.COMMAND,
arguments: {
MOTOR_ID: {
type: ArgumentType.STRING,
menu: 'MOTOR_ID',
defaultValue: MotorID.DEFAULT
},
DURATION: {
type: ArgumentType.NUMBER,
defaultValue: 1
}
}
},
{
opcode: 'motorOn',
text: formatMessage({
id: 'wedo2.motorOn',
default: 'turn [MOTOR_ID] on',
description: 'turn a motor on indefinitely'
}),
blockType: BlockType.COMMAND,
arguments: {
MOTOR_ID: {
type: ArgumentType.STRING,
menu: 'MOTOR_ID',
defaultValue: MotorID.DEFAULT
}
}
},
{
opcode: 'motorOff',
text: formatMessage({
id: 'wedo2.motorOff',
default: 'turn [MOTOR_ID] off',
description: 'turn a motor off'
}),
blockType: BlockType.COMMAND,
arguments: {
MOTOR_ID: {
type: ArgumentType.STRING,
menu: 'MOTOR_ID',
defaultValue: MotorID.DEFAULT
}
}
},
{
opcode: 'startMotorPower',
text: formatMessage({
id: 'wedo2.startMotorPower',
default: 'set [MOTOR_ID] power to [POWER]',
description: 'set the motor\'s power and turn it on'
}),
blockType: BlockType.COMMAND,
arguments: {
MOTOR_ID: {
type: ArgumentType.STRING,
menu: 'MOTOR_ID',
defaultValue: MotorID.DEFAULT
},
POWER: {
type: ArgumentType.NUMBER,
defaultValue: 100
}
}
},
{
2017-10-06 13:56:38 -04:00
opcode: 'setMotorDirection',
text: formatMessage({
id: 'wedo2.setMotorDirection',
default: 'set [MOTOR_ID] direction to [MOTOR_DIRECTION]',
description: 'set the motor\'s turn direction'
}),
blockType: BlockType.COMMAND,
arguments: {
MOTOR_ID: {
type: ArgumentType.STRING,
menu: 'MOTOR_ID',
defaultValue: MotorID.DEFAULT
},
MOTOR_DIRECTION: {
type: ArgumentType.STRING,
menu: 'MOTOR_DIRECTION',
defaultValue: MotorDirection.FORWARD
}
}
},
{
opcode: 'setLightHue',
text: formatMessage({
id: 'wedo2.setLightHue',
default: 'set light color to [HUE]',
description: 'set the LED color'
}),
blockType: BlockType.COMMAND,
arguments: {
HUE: {
type: ArgumentType.NUMBER,
defaultValue: 50
}
}
},
{
opcode: 'playNoteFor',
text: formatMessage({
id: 'wedo2.playNoteFor',
default: 'play note [NOTE] for [DURATION] seconds',
description: 'play a certain note for some time'
}),
blockType: BlockType.COMMAND,
arguments: {
NOTE: {
type: ArgumentType.NUMBER, // TODO: ArgumentType.MIDI_NOTE?
defaultValue: 60
},
DURATION: {
type: ArgumentType.NUMBER,
defaultValue: 0.5
}
},
hideFromPalette: true
},
{
opcode: 'whenDistance',
text: formatMessage({
id: 'wedo2.whenDistance',
default: 'when distance [OP] [REFERENCE]',
description: 'check for when distance is < or > than reference'
}),
blockType: BlockType.HAT,
arguments: {
OP: {
type: ArgumentType.STRING,
menu: 'OP',
defaultValue: '<'
},
REFERENCE: {
type: ArgumentType.NUMBER,
defaultValue: 50
}
}
},
{
opcode: 'whenTilted',
text: formatMessage({
id: 'wedo2.whenTilted',
default: 'when tilted [TILT_DIRECTION_ANY]',
description: 'check when tilted in a certain direction'
}),
func: 'isTilted',
blockType: BlockType.HAT,
arguments: {
TILT_DIRECTION_ANY: {
type: ArgumentType.STRING,
menu: 'TILT_DIRECTION_ANY',
defaultValue: TiltDirection.ANY
}
}
},
{
opcode: 'getDistance',
text: formatMessage({
id: 'wedo2.getDistance',
default: 'distance',
description: 'the value returned by the distance sensor'
}),
blockType: BlockType.REPORTER
},
{
opcode: 'isTilted',
text: formatMessage({
id: 'wedo2.isTilted',
default: 'tilted [TILT_DIRECTION_ANY]?',
description: 'whether the tilt sensor is tilted'
}),
2017-11-06 10:17:53 -05:00
blockType: BlockType.BOOLEAN,
arguments: {
TILT_DIRECTION_ANY: {
type: ArgumentType.STRING,
menu: 'TILT_DIRECTION_ANY',
defaultValue: TiltDirection.ANY
}
}
},
{
opcode: 'getTiltAngle',
text: formatMessage({
id: 'wedo2.getTiltAngle',
default: 'tilt angle [TILT_DIRECTION]',
description: 'the angle returned by the tilt sensor'
}),
blockType: BlockType.REPORTER,
arguments: {
TILT_DIRECTION: {
type: ArgumentType.STRING,
menu: 'TILT_DIRECTION',
defaultValue: TiltDirection.UP
}
}
}
],
menus: {
MOTOR_ID: [MotorID.DEFAULT, MotorID.A, MotorID.B, MotorID.ALL],
MOTOR_DIRECTION: [MotorDirection.FORWARD, MotorDirection.BACKWARD, MotorDirection.REVERSE],
TILT_DIRECTION: [TiltDirection.UP, TiltDirection.DOWN, TiltDirection.LEFT, TiltDirection.RIGHT],
TILT_DIRECTION_ANY:
[TiltDirection.UP, TiltDirection.DOWN, TiltDirection.LEFT, TiltDirection.RIGHT, TiltDirection.ANY],
OP: ['<', '>']
}
};
}
2017-05-05 18:05:21 -04:00
/**
* Turn specified motor(s) on for a specified duration.
* @param {object} args - the block's arguments.
* @property {MotorID} MOTOR_ID - the motor(s) to activate.
* @property {int} DURATION - the amount of time to run the motors.
* @return {Promise} - a promise which will resolve at the end of the duration.
*/
motorOnFor (args) {
let durationMS = Cast.toNumber(args.DURATION) * 1000;
durationMS = MathUtil.clamp(durationMS, 0, 15000);
2017-05-05 18:05:21 -04:00
return new Promise(resolve => {
this._forEachMotor(args.MOTOR_ID, motorIndex => {
const motor = this._device.motor(motorIndex);
if (motor) {
motor.setMotorOnFor(durationMS);
}
2017-05-05 18:05:21 -04:00
});
// Ensure this block runs for a fixed amount of time even when no device is connected.
setTimeout(resolve, durationMS);
2017-05-05 18:05:21 -04:00
});
}
/**
* Turn specified motor(s) on indefinitely.
* @param {object} args - the block's arguments.
* @property {MotorID} MOTOR_ID - the motor(s) to activate.
*/
motorOn (args) {
this._forEachMotor(args.MOTOR_ID, motorIndex => {
const motor = this._device.motor(motorIndex);
if (motor) {
motor.setMotorOn();
}
2017-05-05 18:05:21 -04:00
});
}
/**
* Turn specified motor(s) off.
* @param {object} args - the block's arguments.
* @property {MotorID} MOTOR_ID - the motor(s) to deactivate.
*/
motorOff (args) {
this._forEachMotor(args.MOTOR_ID, motorIndex => {
const motor = this._device.motor(motorIndex);
if (motor) {
motor.setMotorOff();
}
2017-05-05 18:05:21 -04:00
});
}
/**
* Turn specified motor(s) off.
* @param {object} args - the block's arguments.
* @property {MotorID} MOTOR_ID - the motor(s) to be affected.
* @property {int} POWER - the new power level for the motor(s).
*/
startMotorPower (args) {
this._forEachMotor(args.MOTOR_ID, motorIndex => {
const motor = this._device.motor(motorIndex);
if (motor) {
motor.power = MathUtil.clamp(Cast.toNumber(args.POWER), 0, 100);
motor.setMotorOn();
}
2017-05-05 18:05:21 -04:00
});
}
/**
* Set the direction of rotation for specified motor(s).
* If the direction is 'reverse' the motor(s) will be reversed individually.
* @param {object} args - the block's arguments.
* @property {MotorID} MOTOR_ID - the motor(s) to be affected.
* @property {MotorDirection} MOTOR_DIRECTION - the new direction for the motor(s).
2017-05-05 18:05:21 -04:00
*/
setMotorDirection (args) {
this._forEachMotor(args.MOTOR_ID, motorIndex => {
const motor = this._device.motor(motorIndex);
if (motor) {
switch (args.MOTOR_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;
}
// keep the motor on if it's running, and update the pending timeout if needed
if (motor.isOn) {
if (motor.pendingTimeoutDelay) {
motor.setMotorOnFor(motor.pendingTimeoutStartTime + motor.pendingTimeoutDelay - Date.now());
} else {
motor.setMotorOn();
}
}
2017-05-05 18:05:21 -04:00
}
});
}
/**
* Set the LED's hue.
* @param {object} args - the block's arguments.
* @property {number} HUE - the hue to set, in the range [0,100].
*/
setLightHue (args) {
// Convert from [0,100] to [0,360]
let inputHue = Cast.toNumber(args.HUE);
inputHue = MathUtil.wrapClamp(inputHue, 0, 100);
const hue = inputHue * 360 / 100;
2017-05-05 18:05:21 -04:00
const rgbObject = color.hsvToRgb({h: hue, s: 1, v: 1});
const rgbDecimal = color.rgbToDecimal(rgbObject);
this._device.setLED(rgbDecimal);
}
/**
* Make the WeDo 2.0 hub play a MIDI note for the specified duration.
* @param {object} args - the block's arguments.
* @property {number} NOTE - the MIDI note to play.
* @property {number} DURATION - the duration of the note, in seconds.
* @return {Promise} - a promise which will resolve at the end of the duration.
*/
playNoteFor (args) {
let durationMS = Cast.toNumber(args.DURATION) * 1000;
durationMS = MathUtil.clamp(durationMS, 0, 3000);
const note = MathUtil.clamp(Cast.toNumber(args.NOTE), 25, 125); // valid WeDo2 sounds
if (durationMS === 0) return; // WeDo2 plays duration '0' forever
2017-05-05 18:05:21 -04:00
return new Promise(resolve => {
const tone = this._noteToTone(note);
2017-05-05 18:05:21 -04:00
this._device.playTone(tone, durationMS);
// Ensure this block runs for a fixed amount of time even when no device is connected.
setTimeout(resolve, durationMS);
});
}
/**
* Compare the distance sensor's value to a reference.
* @param {object} args - the block's arguments.
* @property {string} OP - the comparison operation: '<' or '>'.
* @property {number} REFERENCE - the value to compare against.
* @return {boolean} - the result of the comparison, or false on error.
*/
whenDistance (args) {
switch (args.OP) {
case '<':
2017-10-13 04:48:25 -04:00
case '&lt;':
return this._device.distance < Cast.toNumber(args.REFERENCE);
2017-05-05 18:05:21 -04:00
case '>':
2017-10-13 04:48:25 -04:00
case '&gt;':
return this._device.distance > Cast.toNumber(args.REFERENCE);
2017-05-05 18:05:21 -04:00
default:
log.warn(`Unknown comparison operator in whenDistance: ${args.OP}`);
return false;
}
}
/**
* Test whether the tilt sensor is currently tilted.
* @param {object} args - the block's arguments.
* @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any).
2017-05-05 18:05:21 -04:00
* @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
*/
whenTilted (args) {
return this._isTilted(args.TILT_DIRECTION_ANY);
2017-05-05 18:05:21 -04:00
}
/**
* @return {number} - the distance sensor's value, scaled to the [0,100] range.
*/
getDistance () {
2017-10-13 04:48:25 -04:00
return this._device.distance;
2017-05-05 18:05:21 -04:00
}
/**
* Test whether the tilt sensor is currently tilted.
* @param {object} args - the block's arguments.
* @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any).
2017-05-05 18:05:21 -04:00
* @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
*/
isTilted (args) {
return this._isTilted(args.TILT_DIRECTION_ANY);
2017-05-05 18:05:21 -04:00
}
/**
* @param {object} args - the block's arguments.
* @property {TiltDirection} TILT_DIRECTION - the direction (up, down, left, right) to check.
2017-05-05 18:05:21 -04:00
* @return {number} - the tilt sensor's angle in the specified direction.
* Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right).
*/
getTiltAngle (args) {
return this._getTiltAngle(args.TILT_DIRECTION);
2017-05-05 18:05:21 -04:00
}
/**
* Test whether the tilt sensor is currently tilted.
* @param {TiltDirection} direction - the tilt direction to test (up, down, left, right, or any).
* @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
* @private
*/
_isTilted (direction) {
switch (direction) {
case TiltDirection.ANY:
return (Math.abs(this._device.tiltX) >= Scratch3WeDo2Blocks.TILT_THRESHOLD) ||
(Math.abs(this._device.tiltY) >= Scratch3WeDo2Blocks.TILT_THRESHOLD);
default:
return this._getTiltAngle(direction) >= Scratch3WeDo2Blocks.TILT_THRESHOLD;
}
}
/**
* @param {TiltDirection} direction - the direction (up, down, left, right) to check.
* @return {number} - the tilt sensor's angle in the specified direction.
* Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right).
* @private
*/
_getTiltAngle (direction) {
switch (direction) {
case TiltDirection.UP:
return this._device.tiltY > 45 ? 256 - this._device.tiltY : -this._device.tiltY;
2017-05-05 18:05:21 -04:00
case TiltDirection.DOWN:
return this._device.tiltY > 45 ? this._device.tiltY - 256 : this._device.tiltY;
2017-05-05 18:05:21 -04:00
case TiltDirection.LEFT:
return this._device.tiltX > 45 ? 256 - this._device.tiltX : -this._device.tiltX;
2017-05-05 18:05:21 -04:00
case TiltDirection.RIGHT:
return this._device.tiltX > 45 ? this._device.tiltX - 256 : this._device.tiltX;
2017-05-05 18:05:21 -04:00
default:
log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`);
}
}
/**
* Call a callback for each motor indexed by the provided motor ID.
* @param {MotorID} motorID - the ID specifier.
* @param {Function} callback - the function to call with the numeric motor index for each motor.
* @private
*/
_forEachMotor (motorID, callback) {
let motors;
switch (motorID) {
case MotorID.A:
motors = [0];
break;
case MotorID.B:
motors = [1];
break;
case MotorID.ALL:
case MotorID.DEFAULT:
motors = [0, 1];
break;
default:
log.warn(`Invalid motor ID: ${motorID}`);
motors = [];
break;
}
for (const index of motors) {
callback(index);
}
}
/**
* @param {number} midiNote - the MIDI note value to convert.
* @return {number} - the frequency, in Hz, corresponding to that MIDI note value.
* @private
*/
_noteToTone (midiNote) {
// Note that MIDI note 69 is A4, 440 Hz
return 440 * Math.pow(2, (midiNote - 69) / 12);
}
}
module.exports = Scratch3WeDo2Blocks;