Initial commit of 001code-html Scratch frontend project.
Includes scratch-gui, scratch-vm, scratch-blocks, scratch-render, scratch-l10n, and deployment config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
981
scratch-vm/src/extensions/scratch3_gdx_for/index.js
Normal file
981
scratch-vm/src/extensions/scratch3_gdx_for/index.js
Normal file
@@ -0,0 +1,981 @@
|
||||
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/dist/godirect.min.umd.js');
|
||||
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;
|
||||
Reference in New Issue
Block a user