scratch-vm
Version:
Virtual Machine for Scratch 3.0
982 lines (885 loc) • 34.3 kB
JavaScript
const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const log = require('../../util/log');
const formatMessage = require('format-message');
const MathUtil = require('../../util/math-util');
const BLE = require('../../io/ble');
const godirect = require('@vernier/godirect');
const ScratchLinkDeviceAdapter = require('./scratch-link-device-adapter');
/**
* Icon png 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/png;base64,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';
/**
* Icon png to be displayed in the blocks category menu, encoded as a data URI.
* @type {string}
*/
// eslint-disable-next-line max-len
const menuIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAA9dJREFUWAnNmE2IFEcUgF/9dE/v7LoaM9kkK4JBRA0EFBIPRm85hBAvEXHXwyo5eFE87GFcReMkObgJiQnkkJzEg9n8HIJixKNe1IMKihgiCbviwV11V3d0d3pmuqsqr5ppcEnb3TNVggVFVVe9eu+r97qqq4tASqp8/fsboQgmU0TMugi571K29bPy9ovPU8Sf16HbpQj3EkYFBcJcr5Am2nZfs94AIWVfqMQeHNwhICUBZ4ypUIA/X2sbIm2AW8AJK0lkEP6TJpfqwXgg4QxmF/fB7Gtvxk1G5ZKHU1CqTgPJoSUXYJYeohSUJu+qrqdVUGh2/pVX4VFffx77WaqBZkrkEFj271+qWH0sXcU3FBzyQe/Mg7B//LbKMTRTxNiDbsMHHjTJlyM7HEJIBHXs2KXFj+oTNSdoQOCYLS5jD9IwBMm5H8NplwwPb/QV4yEIcycaAza9IuA76B38fuz1OF5RXUkmHCdu6rg0BpSMgV/sAe7DdzGFrvvdi0D3mSZjQA0wt7REQsY+iWF0XbfFzyal8SLRxuteD+Du4h4Z/flbqaBHibAQtZmQtcZaAZSMwtTylaR/4vaw1ju5YhWG10pwwAqghmp2FeHO2+t11WqyM80W0m7vAOhsM1kD7CGz8L57Jsq6bitZC/GcWgLf1H6KuHT92cTDAFy/BgXMXm0OCpgV50Bo9kK3BqiBboabQMMU/WoL5im4jToeq/AIgXsiRx5KKCjcwPEsiAv/BQMu9EwyDHXd/3kqCOSzDk6t5/YglQKKeJwq+PNRmJI8kwSTaj1HZy5AhSHqnXkIvU9mMUwEw4Q5wTM57LUtkg8QPw/cdcBJ+PhvKJ0Gj80nGq6JXrg6/XFiX97GXIBpyqTieKpKViOl+WEhWXMaUavvvdIZ8Giy5+Lh3bwKm/t+Be3JazMfxc1tldY26rastiHcsQevTG9pw0znovkAcRWHzSDKnZtaOJLSfMFLB5RqtRBS4LbCurqLCy0YPkU3C0IIPEimMqR2ei7ZX2+KQdRi/WahNT/GmfOD4Vyzhx/66pcjp85dUvcmp6J8+txldXh07PPskdkS+V6EbD0vTOKlB0x9B/O6BS8ULly9PgE6x4kDPR/XX5pyYKj8xcCucsUmkNUQE0JvKKm2VioVK5HRE7UKOHbi6B94RzP+93jtpC0vWgXUF0hr3ipuw8uadwd3jXxoA9IK4Pah8t6BneV9GgjD28Svw1mlxFobgFbeFTz13cKbth93fDryp2CEq0a4hTA+aAPQ/ESJFDdvXLzzzrqNjlTqOP6uDeFf0uhvJ0ZP2QD8D6ZzU6u8YIbBAAAAAElFTkSuQmCC';
/**
* Enum for Vernier godirect protocol.
* @readonly
* @enum {string}
*/
const BLEUUID = {
service: 'd91714ef-28b9-4f91-ba16-f0d9a604f112',
commandChar: 'f4bf14a6-c7d5-4b6d-8aa8-df1a7c83adcb',
responseChar: 'b41e6675-a329-40e0-aa01-44d2f444babe'
};
/**
* A time interval to wait (in milliseconds) before reporting to the BLE socket
* that data has stopped coming from the peripheral.
*/
const BLETimeout = 4500;
/**
* A string to report to the BLE socket when the GdxFor has stopped receiving data.
* @type {string}
*/
const BLEDataStoppedError = 'Force and Acceleration extension stopped receiving data';
/**
* Sensor ID numbers for the GDX-FOR.
*/
const GDXFOR_SENSOR = {
FORCE: 1,
ACCELERATION_X: 2,
ACCELERATION_Y: 3,
ACCELERATION_Z: 4,
SPIN_SPEED_X: 5,
SPIN_SPEED_Y: 6,
SPIN_SPEED_Z: 7
};
/**
* The update rate, in milliseconds, for sensor data input from the peripheral.
*/
const GDXFOR_UPDATE_RATE = 80;
/**
* Threshold for pushing and pulling force, for the whenForcePushedOrPulled hat block.
* @type {number}
*/
const FORCE_THRESHOLD = 5;
/**
* Threshold for acceleration magnitude, for the "shaken" gesture.
* @type {number}
*/
const SHAKEN_THRESHOLD = 30;
/**
* Threshold for acceleration magnitude, to check if we are facing up.
* @type {number}
*/
const FACING_THRESHOLD = 9;
/**
* An offset for the facing threshold, used to check that we are no longer facing up.
* @type {number}
*/
const FACING_THRESHOLD_OFFSET = 5;
/**
* Threshold for acceleration magnitude, below which we are in freefall.
* @type {number}
*/
const FREEFALL_THRESHOLD = 0.5;
/**
* Factor used to account for influence of rotation during freefall.
* @type {number}
*/
const FREEFALL_ROTATION_FACTOR = 0.3;
/**
* Threshold in degrees for reporting that the sensor is tilted.
* @type {number}
*/
const TILT_THRESHOLD = 15;
/**
* Acceleration due to gravity, in m/s^2.
* @type {number}
*/
const GRAVITY = 9.8;
/**
* Manage communication with a GDX-FOR peripheral over a Scratch Link client socket.
*/
class GdxFor {
/**
* Construct a GDX-FOR communication object.
* @param {Runtime} runtime - the Scratch 3.0 runtime
* @param {string} extensionId - the id of the extension
*/
constructor (runtime, extensionId) {
/**
* The Scratch 3.0 runtime used to trigger the green flag button.
* @type {Runtime}
* @private
*/
this._runtime = runtime;
/**
* The BluetoothLowEnergy connection socket for reading/writing peripheral data.
* @type {BLE}
* @private
*/
this._ble = null;
/**
* An @vernier/godirect Device
* @type {Device}
* @private
*/
this._device = null;
this._runtime.registerPeripheralExtension(extensionId, this);
/**
* The id of the extension this peripheral belongs to.
*/
this._extensionId = extensionId;
/**
* The most recently received value for each sensor.
* @type {Object.<string, number>}
* @private
*/
this._sensors = {
force: 0,
accelerationX: 0,
accelerationY: 0,
accelerationZ: 0,
spinSpeedX: 0,
spinSpeedY: 0,
spinSpeedZ: 0
};
/**
* Interval ID for data reading timeout.
* @type {number}
* @private
*/
this._timeoutID = null;
this.reset = this.reset.bind(this);
this._onConnect = this._onConnect.bind(this);
}
/**
* Called by the runtime when user wants to scan for a peripheral.
*/
scan () {
if (this._ble) {
this._ble.disconnect();
}
this._ble = new BLE(this._runtime, this._extensionId, {
filters: [
{namePrefix: 'GDX-FOR'}
],
optionalServices: [
BLEUUID.service
]
}, this._onConnect, this.reset);
}
/**
* Called by the runtime when user wants to connect to a certain peripheral.
* @param {number} id - the id of the peripheral to connect to.
*/
connect (id) {
if (this._ble) {
this._ble.connectPeripheral(id);
}
}
/**
* Called by the runtime when a user exits the connection popup.
* Disconnect from the GDX FOR.
*/
disconnect () {
if (this._ble) {
this._ble.disconnect();
}
this.reset();
}
/**
* Reset all the state and timeout/interval ids.
*/
reset () {
this._sensors = {
force: 0,
accelerationX: 0,
accelerationY: 0,
accelerationZ: 0,
spinSpeedX: 0,
spinSpeedY: 0,
spinSpeedZ: 0
};
if (this._timeoutID) {
window.clearInterval(this._timeoutID);
this._timeoutID = null;
}
}
/**
* Return true if connected to the goforce device.
* @return {boolean} - whether the goforce is connected.
*/
isConnected () {
let connected = false;
if (this._ble) {
connected = this._ble.isConnected();
}
return connected;
}
/**
* Starts reading data from peripheral after BLE has connected to it.
* @private
*/
_onConnect () {
const adapter = new ScratchLinkDeviceAdapter(this._ble, BLEUUID);
godirect.createDevice(adapter, {open: true, startMeasurements: false}).then(device => {
// Setup device
this._device = device;
this._device.keepValues = false; // todo: possibly remove after updating Vernier godirect module
// Enable sensors
this._device.sensors.forEach(sensor => {
sensor.setEnabled(true);
});
// Set sensor value-update behavior
this._device.on('measurements-started', () => {
const enabledSensors = this._device.sensors.filter(s => s.enabled);
enabledSensors.forEach(sensor => {
sensor.on('value-changed', s => {
this._onSensorValueChanged(s);
});
});
this._timeoutID = window.setInterval(
() => this._ble.handleDisconnectError(BLEDataStoppedError),
BLETimeout
);
});
// Start device
this._device.start(GDXFOR_UPDATE_RATE);
});
}
/**
* Handler for sensor value changes from the goforce device.
* @param {object} sensor - goforce device sensor whose value has changed
* @private
*/
_onSensorValueChanged (sensor) {
switch (sensor.number) {
case GDXFOR_SENSOR.FORCE:
// Normalize the force, which can be measured between -50 and 50 N,
// to be a value between -100 and 100.
this._sensors.force = MathUtil.clamp(sensor.value * 2, -100, 100);
break;
case GDXFOR_SENSOR.ACCELERATION_X:
this._sensors.accelerationX = sensor.value;
break;
case GDXFOR_SENSOR.ACCELERATION_Y:
this._sensors.accelerationY = sensor.value;
break;
case GDXFOR_SENSOR.ACCELERATION_Z:
this._sensors.accelerationZ = sensor.value;
break;
case GDXFOR_SENSOR.SPIN_SPEED_X:
this._sensors.spinSpeedX = this._spinSpeedFromGyro(sensor.value);
break;
case GDXFOR_SENSOR.SPIN_SPEED_Y:
this._sensors.spinSpeedY = this._spinSpeedFromGyro(sensor.value);
break;
case GDXFOR_SENSOR.SPIN_SPEED_Z:
this._sensors.spinSpeedZ = this._spinSpeedFromGyro(sensor.value);
break;
}
// cancel disconnect timeout and start a new one
window.clearInterval(this._timeoutID);
this._timeoutID = window.setInterval(
() => this._ble.handleDisconnectError(BLEDataStoppedError),
BLETimeout
);
}
_spinSpeedFromGyro (val) {
const framesPerSec = 1000 / this._runtime.currentStepTime;
val = MathUtil.radToDeg(val);
val = val / framesPerSec; // convert to from degrees per sec to degrees per frame
val = val * -1;
return val;
}
getForce () {
return this._sensors.force;
}
getTiltFrontBack (back = false) {
const x = this.getAccelerationX();
const y = this.getAccelerationY();
const z = this.getAccelerationZ();
// Compute the yz unit vector
const y2 = y * y;
const z2 = z * z;
let value = y2 + z2;
value = Math.sqrt(value);
// For sufficiently small zy vector values we are essentially at 90 degrees.
// The following snaps to 90 and avoids divide-by-zero errors.
// The snap factor was derived through observation -- just enough to
// still allow single degree steps up to 90 (..., 87, 88, 89, 90).
if (value < 0.35) {
value = (x < 0) ? 90 : -90;
} else {
value = x / value;
value = Math.atan(value);
value = MathUtil.radToDeg(value) * -1;
}
// Back is the inverse of front
if (back) value *= -1;
return value;
}
getTiltLeftRight (right = false) {
const x = this.getAccelerationX();
const y = this.getAccelerationY();
const z = this.getAccelerationZ();
// Compute the yz unit vector
const x2 = x * x;
const z2 = z * z;
let value = x2 + z2;
value = Math.sqrt(value);
// For sufficiently small zy vector values we are essentially at 90 degrees.
// The following snaps to 90 and avoids divide-by-zero errors.
// The snap factor was derived through observation -- just enough to
// still allow single degree steps up to 90 (..., 87, 88, 89, 90).
if (value < 0.35) {
value = (y < 0) ? 90 : -90;
} else {
value = y / value;
value = Math.atan(value);
value = MathUtil.radToDeg(value) * -1;
}
// Right is the inverse of left
if (right) value *= -1;
return value;
}
getAccelerationX () {
return this._sensors.accelerationX;
}
getAccelerationY () {
return this._sensors.accelerationY;
}
getAccelerationZ () {
return this._sensors.accelerationZ;
}
getSpinSpeedX () {
return this._sensors.spinSpeedX;
}
getSpinSpeedY () {
return this._sensors.spinSpeedY;
}
getSpinSpeedZ () {
return this._sensors.spinSpeedZ;
}
}
/**
* Enum for pushed and pulled menu options.
* @readonly
* @enum {string}
*/
const PushPullValues = {
PUSHED: 'pushed',
PULLED: 'pulled'
};
/**
* Enum for motion gesture menu options.
* @readonly
* @enum {string}
*/
const GestureValues = {
SHAKEN: 'shaken',
STARTED_FALLING: 'started falling',
TURNED_FACE_UP: 'turned face up',
TURNED_FACE_DOWN: 'turned face down'
};
/**
* Enum for tilt axis menu options.
* @readonly
* @enum {string}
*/
const TiltAxisValues = {
FRONT: 'front',
BACK: 'back',
LEFT: 'left',
RIGHT: 'right',
ANY: 'any'
};
/**
* Enum for axis menu options.
* @readonly
* @enum {string}
*/
const AxisValues = {
X: 'x',
Y: 'y',
Z: 'z'
};
/**
* Scratch 3.0 blocks to interact with a GDX-FOR peripheral.
*/
class Scratch3GdxForBlocks {
/**
* @return {string} - the name of this extension.
*/
static get EXTENSION_NAME () {
return 'Force and Acceleration';
}
/**
* @return {string} - the ID of this extension.
*/
static get EXTENSION_ID () {
return 'gdxfor';
}
get AXIS_MENU () {
return [
{
text: 'x',
value: AxisValues.X
},
{
text: 'y',
value: AxisValues.Y
},
{
text: 'z',
value: AxisValues.Z
}
];
}
get TILT_MENU () {
return [
{
text: formatMessage({
id: 'gdxfor.tiltDirectionMenu.front',
default: 'front',
description: 'label for front element in tilt direction picker for gdxfor extension'
}),
value: TiltAxisValues.FRONT
},
{
text: formatMessage({
id: 'gdxfor.tiltDirectionMenu.back',
default: 'back',
description: 'label for back element in tilt direction picker for gdxfor extension'
}),
value: TiltAxisValues.BACK
},
{
text: formatMessage({
id: 'gdxfor.tiltDirectionMenu.left',
default: 'left',
description: 'label for left element in tilt direction picker for gdxfor extension'
}),
value: TiltAxisValues.LEFT
},
{
text: formatMessage({
id: 'gdxfor.tiltDirectionMenu.right',
default: 'right',
description: 'label for right element in tilt direction picker for gdxfor extension'
}),
value: TiltAxisValues.RIGHT
}
];
}
get TILT_MENU_ANY () {
return [
...this.TILT_MENU,
{
text: formatMessage({
id: 'gdxfor.tiltDirectionMenu.any',
default: 'any',
description: 'label for any direction element in tilt direction picker for gdxfor extension'
}),
value: TiltAxisValues.ANY
}
];
}
get PUSH_PULL_MENU () {
return [
{
text: formatMessage({
id: 'gdxfor.pushed',
default: 'pushed',
description: 'the force sensor was pushed inward'
}),
value: PushPullValues.PUSHED
},
{
text: formatMessage({
id: 'gdxfor.pulled',
default: 'pulled',
description: 'the force sensor was pulled outward'
}),
value: PushPullValues.PULLED
}
];
}
get GESTURE_MENU () {
return [
{
text: formatMessage({
id: 'gdxfor.shaken',
default: 'shaken',
description: 'the sensor was shaken'
}),
value: GestureValues.SHAKEN
},
{
text: formatMessage({
id: 'gdxfor.startedFalling',
default: 'started falling',
description: 'the sensor started free falling'
}),
value: GestureValues.STARTED_FALLING
},
{
text: formatMessage({
id: 'gdxfor.turnedFaceUp',
default: 'turned face up',
description: 'the sensor was turned to face up'
}),
value: GestureValues.TURNED_FACE_UP
},
{
text: formatMessage({
id: 'gdxfor.turnedFaceDown',
default: 'turned face down',
description: 'the sensor was turned to face down'
}),
value: GestureValues.TURNED_FACE_DOWN
}
];
}
/**
* Construct a set of GDX-FOR blocks.
* @param {Runtime} runtime - the Scratch 3.0 runtime.
*/
constructor (runtime) {
/**
* The Scratch 3.0 runtime.
* @type {Runtime}
*/
this.runtime = runtime;
// Create a new GdxFor peripheral instance
this._peripheral = new GdxFor(this.runtime, Scratch3GdxForBlocks.EXTENSION_ID);
}
/**
* @returns {object} metadata for this extension and its blocks.
*/
getInfo () {
return {
id: Scratch3GdxForBlocks.EXTENSION_ID,
name: Scratch3GdxForBlocks.EXTENSION_NAME,
blockIconURI: blockIconURI,
menuIconURI: menuIconURI,
showStatusButton: true,
blocks: [
{
opcode: 'whenGesture',
text: formatMessage({
id: 'gdxfor.whenGesture',
default: 'when [GESTURE]',
description: 'when the sensor detects a gesture'
}),
blockType: BlockType.HAT,
arguments: {
GESTURE: {
type: ArgumentType.STRING,
menu: 'gestureOptions',
defaultValue: GestureValues.SHAKEN
}
}
},
{
opcode: 'whenForcePushedOrPulled',
text: formatMessage({
id: 'gdxfor.whenForcePushedOrPulled',
default: 'when force sensor [PUSH_PULL]',
description: 'when the force sensor is pushed or pulled'
}),
blockType: BlockType.HAT,
arguments: {
PUSH_PULL: {
type: ArgumentType.STRING,
menu: 'pushPullOptions',
defaultValue: PushPullValues.PUSHED
}
}
},
{
opcode: 'getForce',
text: formatMessage({
id: 'gdxfor.getForce',
default: 'force',
description: 'gets force'
}),
blockType: BlockType.REPORTER
},
'---',
{
opcode: 'whenTilted',
text: formatMessage({
id: 'gdxfor.whenTilted',
default: 'when tilted [TILT]',
description: 'when the sensor detects tilt'
}),
blockType: BlockType.HAT,
arguments: {
TILT: {
type: ArgumentType.STRING,
menu: 'tiltAnyOptions',
defaultValue: TiltAxisValues.ANY
}
}
},
{
opcode: 'isTilted',
text: formatMessage({
id: 'gdxfor.isTilted',
default: 'tilted [TILT]?',
description: 'is the device tilted?'
}),
blockType: BlockType.BOOLEAN,
arguments: {
TILT: {
type: ArgumentType.STRING,
menu: 'tiltAnyOptions',
defaultValue: TiltAxisValues.ANY
}
}
},
{
opcode: 'getTilt',
text: formatMessage({
id: 'gdxfor.getTilt',
default: 'tilt angle [TILT]',
description: 'gets tilt'
}),
blockType: BlockType.REPORTER,
arguments: {
TILT: {
type: ArgumentType.STRING,
menu: 'tiltOptions',
defaultValue: TiltAxisValues.FRONT
}
}
},
'---',
{
opcode: 'isFreeFalling',
text: formatMessage({
id: 'gdxfor.isFreeFalling',
default: 'falling?',
description: 'is the device in free fall?'
}),
blockType: BlockType.BOOLEAN
},
{
opcode: 'getSpinSpeed',
text: formatMessage({
id: 'gdxfor.getSpin',
default: 'spin speed [DIRECTION]',
description: 'gets spin speed'
}),
blockType: BlockType.REPORTER,
arguments: {
DIRECTION: {
type: ArgumentType.STRING,
menu: 'axisOptions',
defaultValue: AxisValues.Z
}
}
},
{
opcode: 'getAcceleration',
text: formatMessage({
id: 'gdxfor.getAcceleration',
default: 'acceleration [DIRECTION]',
description: 'gets acceleration'
}),
blockType: BlockType.REPORTER,
arguments: {
DIRECTION: {
type: ArgumentType.STRING,
menu: 'axisOptions',
defaultValue: AxisValues.X
}
}
}
],
menus: {
pushPullOptions: {
acceptReporters: true,
items: this.PUSH_PULL_MENU
},
gestureOptions: {
acceptReporters: true,
items: this.GESTURE_MENU
},
axisOptions: {
acceptReporters: true,
items: this.AXIS_MENU
},
tiltOptions: {
acceptReporters: true,
items: this.TILT_MENU
},
tiltAnyOptions: {
acceptReporters: true,
items: this.TILT_MENU_ANY
}
}
};
}
whenForcePushedOrPulled (args) {
switch (args.PUSH_PULL) {
case PushPullValues.PUSHED:
return this._peripheral.getForce() < FORCE_THRESHOLD * -1;
case PushPullValues.PULLED:
return this._peripheral.getForce() > FORCE_THRESHOLD;
default:
log.warn(`unknown push/pull value in whenForcePushedOrPulled: ${args.PUSH_PULL}`);
return false;
}
}
getForce () {
return Math.round(this._peripheral.getForce());
}
whenGesture (args) {
switch (args.GESTURE) {
case GestureValues.SHAKEN:
return this.gestureMagnitude() > SHAKEN_THRESHOLD;
case GestureValues.STARTED_FALLING:
return this.isFreeFalling();
case GestureValues.TURNED_FACE_UP:
return this._isFacing(GestureValues.TURNED_FACE_UP);
case GestureValues.TURNED_FACE_DOWN:
return this._isFacing(GestureValues.TURNED_FACE_DOWN);
default:
log.warn(`unknown gesture value in whenGesture: ${args.GESTURE}`);
return false;
}
}
_isFacing (direction) {
if (typeof this._facingUp === 'undefined') {
this._facingUp = false;
}
if (typeof this._facingDown === 'undefined') {
this._facingDown = false;
}
// If the sensor is already facing up or down, reduce the threshold.
// This prevents small fluctations in acceleration while it is being
// turned from causing the hat block to trigger multiple times.
let threshold = FACING_THRESHOLD;
if (this._facingUp || this._facingDown) {
threshold -= FACING_THRESHOLD_OFFSET;
}
this._facingUp = this._peripheral.getAccelerationZ() > threshold;
this._facingDown = this._peripheral.getAccelerationZ() < threshold * -1;
switch (direction) {
case GestureValues.TURNED_FACE_UP:
return this._facingUp;
case GestureValues.TURNED_FACE_DOWN:
return this._facingDown;
default:
return false;
}
}
whenTilted (args) {
return this._isTilted(args.TILT);
}
isTilted (args) {
return this._isTilted(args.TILT);
}
getTilt (args) {
return this._getTiltAngle(args.TILT);
}
_isTilted (direction) {
switch (direction) {
case TiltAxisValues.ANY:
return this._getTiltAngle(TiltAxisValues.FRONT) > TILT_THRESHOLD ||
this._getTiltAngle(TiltAxisValues.BACK) > TILT_THRESHOLD ||
this._getTiltAngle(TiltAxisValues.LEFT) > TILT_THRESHOLD ||
this._getTiltAngle(TiltAxisValues.RIGHT) > TILT_THRESHOLD;
default:
return this._getTiltAngle(direction) > TILT_THRESHOLD;
}
}
_getTiltAngle (direction) {
// Tilt values are calculated using acceleration due to gravity,
// so we need to return 0 when the peripheral is not connected.
if (!this._peripheral.isConnected()) {
return 0;
}
switch (direction) {
case TiltAxisValues.FRONT:
return Math.round(this._peripheral.getTiltFrontBack(true));
case TiltAxisValues.BACK:
return Math.round(this._peripheral.getTiltFrontBack(false));
case TiltAxisValues.LEFT:
return Math.round(this._peripheral.getTiltLeftRight(true));
case TiltAxisValues.RIGHT:
return Math.round(this._peripheral.getTiltLeftRight(false));
default:
log.warn(`Unknown direction in getTilt: ${direction}`);
}
}
getSpinSpeed (args) {
switch (args.DIRECTION) {
case AxisValues.X:
return Math.round(this._peripheral.getSpinSpeedX());
case AxisValues.Y:
return Math.round(this._peripheral.getSpinSpeedY());
case AxisValues.Z:
return Math.round(this._peripheral.getSpinSpeedZ());
default:
log.warn(`Unknown direction in getSpinSpeed: ${args.DIRECTION}`);
}
}
getAcceleration (args) {
switch (args.DIRECTION) {
case AxisValues.X:
return Math.round(this._peripheral.getAccelerationX());
case AxisValues.Y:
return Math.round(this._peripheral.getAccelerationY());
case AxisValues.Z:
return Math.round(this._peripheral.getAccelerationZ());
default:
log.warn(`Unknown direction in getAcceleration: ${args.DIRECTION}`);
}
}
/**
* @param {number} x - x axis vector
* @param {number} y - y axis vector
* @param {number} z - z axis vector
* @return {number} - the magnitude of a three dimension vector.
*/
magnitude (x, y, z) {
return Math.sqrt((x * x) + (y * y) + (z * z));
}
accelMagnitude () {
return this.magnitude(
this._peripheral.getAccelerationX(),
this._peripheral.getAccelerationY(),
this._peripheral.getAccelerationZ()
);
}
gestureMagnitude () {
return this.accelMagnitude() - GRAVITY;
}
spinMagnitude () {
return this.magnitude(
this._peripheral.getSpinSpeedX(),
this._peripheral.getSpinSpeedY(),
this._peripheral.getSpinSpeedZ()
);
}
isFreeFalling () {
// When the peripheral is not connected, the acceleration magnitude
// is 0 instead of ~9.8, which ends up calculating as a positive
// free fall; so we need to return 'false' here to prevent returning 'true'.
if (!this._peripheral.isConnected()) {
return false;
}
const accelMag = this.accelMagnitude();
const spinMag = this.spinMagnitude();
// We want to account for rotation during freefall,
// so we tack on a an estimated "rotational effect"
// The FREEFALL_ROTATION_FACTOR const is used to both scale the
// gyro measurements and convert them to radians/second.
// So, we compare our accel magnitude against:
// FREEFALL_THRESHOLD + (some_scaled_magnitude_of_rotation).
const ffThresh = FREEFALL_THRESHOLD + (FREEFALL_ROTATION_FACTOR * spinMag);
return accelMag < ffThresh;
}
}
module.exports = Scratch3GdxForBlocks;