From eb931fd99bf0a32e694e2ac67d495ddd7799ebfd Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Wed, 24 May 2017 15:30:29 -0700 Subject: [PATCH 01/11] Specify dataFormat when loading asset for import When importing a project we know the file extension for each asset to be loaded. This change provides that information to the storage system so that we can load assets which don't use the default. For example, this allows loading JPG-format backdrops. In support of this change, there's a new function on `StringUtil` called `splitFirst`, which splits a string on the first instance of a separator character. This change includes unit tests for this new function. --- src/import/load-costume.js | 9 +++++---- src/import/load-sound.js | 6 ++++-- src/playground/playground.js | 2 +- src/serialization/sb3.js | 2 +- src/util/string-util.js | 24 ++++++++++++++++++++++++ test/fixtures/attach-test-storage.js | 2 +- test/unit/util_string.js | 8 ++++++++ 7 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/import/load-costume.js b/src/import/load-costume.js index a9323398d..54962ea1f 100644 --- a/src/import/load-costume.js +++ b/src/import/load-costume.js @@ -1,3 +1,4 @@ +const StringUtil = require('../util/string-util'); const log = require('../util/log'); /** @@ -19,17 +20,17 @@ const loadCostume = function (md5ext, costume, runtime) { } const AssetType = runtime.storage.AssetType; - const idParts = md5ext.split('.'); + const idParts = StringUtil.splitFirst(md5ext, '.'); const md5 = idParts[0]; - const ext = idParts[1].toUpperCase(); - const assetType = (ext === 'SVG') ? AssetType.ImageVector : AssetType.ImageBitmap; + const ext = idParts[1].toLowerCase(); + const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap; const rotationCenter = [ costume.rotationCenterX / costume.bitmapResolution, costume.rotationCenterY / costume.bitmapResolution ]; - let promise = runtime.storage.load(assetType, md5).then(costumeAsset => { + let promise = runtime.storage.load(assetType, md5, ext).then(costumeAsset => { costume.assetId = costumeAsset.assetId; costume.assetType = assetType; return costumeAsset; diff --git a/src/import/load-sound.js b/src/import/load-sound.js index a308e1516..80b595987 100644 --- a/src/import/load-sound.js +++ b/src/import/load-sound.js @@ -1,3 +1,4 @@ +const StringUtil = require('../util/string-util'); const log = require('../util/log'); /** @@ -17,9 +18,10 @@ const loadSound = function (sound, runtime) { log.error('No audio engine present; cannot load sound asset: ', sound.md5); return Promise.resolve(sound); } - const idParts = sound.md5.split('.'); + const idParts = StringUtil.splitFirst(sound.md5, '.'); const md5 = idParts[0]; - return runtime.storage.load(runtime.storage.AssetType.Sound, md5) + const ext = idParts[1].toLowerCase(); + return runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext) .then(soundAsset => { sound.assetId = soundAsset.assetId; sound.assetType = runtime.storage.AssetType.Sound; diff --git a/src/playground/playground.js b/src/playground/playground.js index c1b655a58..814bfb628 100644 --- a/src/playground/playground.js +++ b/src/playground/playground.js @@ -34,7 +34,7 @@ const getAssetUrl = function (asset) { 'internalapi/asset/', asset.assetId, '.', - asset.assetType.runtimeFormat, + asset.dataFormat, '/get/' ]; return assetUrlParts.join(''); diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 4477a7b51..9708b9195 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -75,7 +75,7 @@ const parseScratchObject = function (object, runtime) { rotationCenterX: costumeSource.rotationCenterX, rotationCenterY: costumeSource.rotationCenterY }; - const costumeMd5 = `${costumeSource.assetId}.${costumeSource.assetType.runtimeFormat}`; + const costumeMd5 = `${costumeSource.assetId}.${costumeSource.dataFormat}`; return loadCostume(costumeMd5, costume, runtime); }); // Sounds from JSON diff --git a/src/util/string-util.js b/src/util/string-util.js index 0ee712548..48dde0277 100644 --- a/src/util/string-util.js +++ b/src/util/string-util.js @@ -12,6 +12,30 @@ class StringUtil { while (existingNames.indexOf(name + i) >= 0) i++; return name + i; } + + /** + * Split a string on the first occurrence of a split character. + * @param {string} text - the string to split. + * @param {string} separator - split the text on this character. + * @returns {[string, string]} - the two parts of the split string, or [text, null] if no split character found. + * @example + * // returns ['foo', 'tar.gz'] + * splitFirst('foo.tar.gz', '.'); + * @example + * // returns ['foo', null] + * splitFirst('foo', '.'); + * @example + * // returns ['foo', ''] + * splitFirst('foo.', '.'); + */ + static splitFirst (text, separator) { + const index = text.indexOf(separator); + if (index >= 0) { + return [text.substring(0, index), text.substring(index + 1)]; + } else { + return [text, null]; + } + } } module.exports = StringUtil; diff --git a/test/fixtures/attach-test-storage.js b/test/fixtures/attach-test-storage.js index 3e75f5967..bb0cc3a08 100644 --- a/test/fixtures/attach-test-storage.js +++ b/test/fixtures/attach-test-storage.js @@ -26,7 +26,7 @@ const getAssetUrl = function (asset) { 'internalapi/asset/', asset.assetId, '.', - asset.assetType.runtimeFormat, + asset.dataFormat, '/get/' ]; return assetUrlParts.join(''); diff --git a/test/unit/util_string.js b/test/unit/util_string.js index 741336258..aca20f9a4 100644 --- a/test/unit/util_string.js +++ b/test/unit/util_string.js @@ -1,6 +1,14 @@ const test = require('tap').test; const StringUtil = require('../../src/util/string-util'); +test('splitFirst', t => { + t.deepEqual(StringUtil.splitFirst('asdf.1234', '.'), ['asdf', '1234']); + t.deepEqual(StringUtil.splitFirst('asdf.', '.'), ['asdf', '']); + t.deepEqual(StringUtil.splitFirst('.1234', '.'), ['', '1234']); + t.deepEqual(StringUtil.splitFirst('foo', '.'), ['foo', null]); + t.end(); +}); + test('withoutTrailingDigits', t => { t.strictEqual(StringUtil.withoutTrailingDigits('boeing747'), 'boeing'); t.strictEqual(StringUtil.withoutTrailingDigits('boeing747 '), 'boeing747 '); From 2625529ebe2a921eb3f912b69f96948a616a83ea Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Wed, 3 May 2017 16:39:31 -0700 Subject: [PATCH 02/11] Create WeDo 2.0 device communication classes --- src/blocks/scratch3_wedo2.js | 387 +++++++++++++++++++++++++++++++++++ src/engine/runtime.js | 3 +- 2 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 src/blocks/scratch3_wedo2.js diff --git a/src/blocks/scratch3_wedo2.js b/src/blocks/scratch3_wedo2.js new file mode 100644 index 000000000..ac945ef84 --- /dev/null +++ b/src/blocks/scratch3_wedo2.js @@ -0,0 +1,387 @@ +const log = require('../util/log'); + +/** + * 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; + + 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; + } + + /** + * Turn this motor on indefinitely. + */ + setMotorOn () { + this._parent._send('motorOn', {motorIndex: this._index, power: this._direction * this._power}); + 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 () { + this._parent._send('motorBrake', {motorIndex: this._index}); + this._isOn = false; + this._setNewTimeout(this.setMotorOff, WeDo2Motor.BRAKE_TIME_MS); + } + + /** + * Turn this motor off. + */ + setMotorOff () { + this._parent._send('motorOff', {motorIndex: this._index}); + 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; + } + } + + /** + * 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; + } +} + +/** + * Manage communication with a WeDo 2.0 device over a Device Manager client socket. + */ +class WeDo2 { + + /** + * @return {string} - the type of Device Manager device socket that this class will handle. + */ + static get DEVICE_TYPE () { + return 'wedo2'; + } + + /** + * 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} + * @private + */ + this._socket = socket; + + /** + * The motors which this WeDo 2.0 could possibly have. + * @type {[WeDo2Motor]} + * @private + */ + this._motors = [new WeDo2Motor(this, 0), new WeDo2Motor(this, 1)]; + + /** + * The most recently received value for each sensor. + * @type {Object.<string, number>} + * @private + */ + this._sensors = { + tiltX: 0, + tiltY: 0, + distance: 0 + }; + + this._onSensorChanged = this._onSensorChanged.bind(this); + this._onDisconnect = this._onDisconnect.bind(this); + + this._connectEvents(); + } + + /** + * Manually dispose of this object. + */ + dispose () { + this._disconnectEvents(); + } + + /** + * @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]; + } + + /** + * 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}); + } + + /** + * 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) { + this._send('playTone', {tone, ms: milliseconds}); + } + + /** + * Stop the tone playing from the WeDo 2.0 hub, if any. + */ + stopTone () { + this._send('stopTone'); + } + + /** + * Attach event handlers to the device socket. + * @private + */ + _connectEvents () { + this._socket.on('sensorChanged', this._onSensorChanged); + this._socket.on('deviceWasClosed', this._onDisconnect); + this._socket.on('disconnect', this._onDisconnect); + } + + /** + * Detach event handlers from the device socket. + * @private + */ + _disconnectEvents () { + this._socket.off('sensorChanged', this._onSensorChanged); + this._socket.off('deviceWasClosed', this._onDisconnect); + this._socket.off('disconnect', this._onDisconnect); + } + + /** + * 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 + */ + _onSensorChanged (event) { + this._sensors[event.sensorName] = event.sensorValue; + } + + /** + * React to device disconnection. May be called more than once. + * @private + */ + _onDisconnect () { + this._disconnectEvents(); + } + + /** + * 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. + * @private + */ + _send (message, details) { + this._socket.emit(message, details); + } +} + +/** + * Scratch 3.0 blocks to interact with a LEGO WeDo 2.0 device. + */ +class Scratch3WeDo2Blocks { + + /** + * @return {string} - the name of this extension. + */ + static get EXTENSION_NAME () { + return 'wedo2'; + } + + /** + * 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; + + this.runtime.HACK_WeDo2Blocks = this; + } + + /** + * 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_NAME, 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'); + } + }); + } +} + +module.exports = Scratch3WeDo2Blocks; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index c938c37ed..dd037c57d 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -19,7 +19,8 @@ const defaultBlockPackages = { scratch3_sound: require('../blocks/scratch3_sound'), scratch3_sensing: require('../blocks/scratch3_sensing'), scratch3_data: require('../blocks/scratch3_data'), - scratch3_procedures: require('../blocks/scratch3_procedures') + scratch3_procedures: require('../blocks/scratch3_procedures'), + scratch3_wedo2: require('../blocks/scratch3_wedo2') }; /** From 06fe701624d3954bf67aefddbd6d5fd2fa0268ac Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Fri, 5 May 2017 15:05:21 -0700 Subject: [PATCH 03/11] Implement WeDo 2.0 blocks --- src/blocks/scratch3_wedo2.js | 312 +++++++++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) diff --git a/src/blocks/scratch3_wedo2.js b/src/blocks/scratch3_wedo2.js index ac945ef84..bd9e6c119 100644 --- a/src/blocks/scratch3_wedo2.js +++ b/src/blocks/scratch3_wedo2.js @@ -1,3 +1,4 @@ +const color = require('../util/color'); const log = require('../util/log'); /** @@ -328,6 +329,42 @@ class WeDo2 { } } +/** + * 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. */ @@ -340,6 +377,13 @@ class Scratch3WeDo2Blocks { return 'wedo2'; } + /** + * @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. @@ -382,6 +426,274 @@ class Scratch3WeDo2Blocks { } }); } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.<string, Function>} Mapping of opcode to Function. + */ + getPrimitives () { + return { + wedo2_motorOnFor: this.motorOnFor, + wedo2_motorOn: this.motorOn, + wedo2_motorOff: this.motorOff, + wedo2_startMotorPower: this.startMotorPower, + wedo2_setMotorDirection: this.setMotorDirection, + wedo2_setLightHue: this.setLightHue, + wedo2_playNoteFor: this.playNoteFor, + wedo2_whenDistance: this.whenDistance, + wedo2_whenTilted: this.whenTilted, + wedo2_getDistance: this.getDistance, + wedo2_isTilted: this.isTilted, + wedo2_getTiltAngle: this.getTiltAngle + }; + } + + /** + * 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) { + return new Promise(resolve => { + this._forEachMotor(args.MOTOR_ID, motorIndex => { + this._device.motor(motorIndex).setMotorOnFor(args.DURATION); + }); + + // Ensure this block runs for a fixed amount of time even when no device is connected. + setTimeout(resolve, args.DURATION); + }); + } + + /** + * 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 => { + this._device.motor(motorIndex).setMotorOn(); + }); + } + + /** + * 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 => { + this._device.motor(motorIndex).setMotorOff(); + }); + } + + /** + * 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); + motor.power = args.POWER; + motor.setMotorOn(); + }); + } + + /** + * 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} DIRECTION - the new direction for the motor(s). + */ + 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; + } + }); + } + + /** + * 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] + const hue = args.HUE * 360 / 100; + + 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) { + return new Promise(resolve => { + const durationMS = args.DURATION * 1000; + const tone = this._noteToTone(args.NOTE); + 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 '<': + return this._device.distance < args.REFERENCE; + case '>': + return this._device.distance > args.REFERENCE; + 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} 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. + */ + whenTilted (args) { + return this._isTilted(args.DIRECTION); + } + + /** + * @return {number} - the distance sensor's value, scaled to the [0,100] range. + */ + getDistance () { + return this._device.distance * 10; + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {object} args - the block's arguments. + * @property {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. + */ + isTilted (args) { + return this._isTilted(args.DIRECTION); + } + + /** + * @param {object} args - the block's arguments. + * @property {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). + */ + getTiltAngle (args) { + return this._getTiltAngle(args.DIRECTION); + } + + /** + * 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; + case TiltDirection.DOWN: + return this._device.tiltY; + case TiltDirection.LEFT: + return -this._device.tiltX; + case TiltDirection.RIGHT: + return this._device.tiltX; + 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; From cbfbc5d600bfa2e1f57a925bfde831be8a21c1cf Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Thu, 11 May 2017 22:39:24 -0700 Subject: [PATCH 04/11] Convert motor duration: seconds -> milliseconds The block takes seconds, whereas the device takes milliseconds. Turning on a motor for 1 millisecond isn't very dramatic. --- src/blocks/scratch3_wedo2.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/blocks/scratch3_wedo2.js b/src/blocks/scratch3_wedo2.js index bd9e6c119..d79ef9096 100644 --- a/src/blocks/scratch3_wedo2.js +++ b/src/blocks/scratch3_wedo2.js @@ -456,13 +456,14 @@ class Scratch3WeDo2Blocks { * @return {Promise} - a promise which will resolve at the end of the duration. */ motorOnFor (args) { + const durationMS = args.DURATION * 1000; return new Promise(resolve => { this._forEachMotor(args.MOTOR_ID, motorIndex => { - this._device.motor(motorIndex).setMotorOnFor(args.DURATION); + this._device.motor(motorIndex).setMotorOnFor(durationMS); }); // Ensure this block runs for a fixed amount of time even when no device is connected. - setTimeout(resolve, args.DURATION); + setTimeout(resolve, durationMS); }); } From 467b747283afdd610cc1ac8b8bf8e31f074fa2ad Mon Sep 17 00:00:00 2001 From: Ray Schamp <ray@scratch.mit.edu> Date: Fri, 2 Jun 2017 08:57:21 -0400 Subject: [PATCH 05/11] Bump scratch-storage version Now 0.2.0 is the lowest compatible version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0308aa6f7..11fb66efe 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "scratch-audio": "^0.1.0-prerelease.0", "scratch-blocks": "^0.1.0-prerelease.0", "scratch-render": "^0.1.0-prerelease.0", - "scratch-storage": "^0.1.0", + "scratch-storage": "^0.2.0", "script-loader": "0.7.0", "stats.js": "^0.17.0", "tap": "^10.2.0", From ab0cef52eb759bad7e7aad3d637fa31a19b1c18c Mon Sep 17 00:00:00 2001 From: DD Liu <liudi@media.mit.edu> Date: Mon, 5 Jun 2017 16:55:15 -0400 Subject: [PATCH 06/11] Change wait to use a promise instead of being called every frame --- src/blocks/scratch3_control.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index f3b53baa2..f460399e1 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -73,18 +73,13 @@ class Scratch3ControlBlocks { util.startBranch(1, true); } - wait (args, util) { - if (!util.stackFrame.timer) { - util.stackFrame.timer = new Timer(); - util.stackFrame.timer.start(); - util.yield(); - this.runtime.requestRedraw(); - } else { - const duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION)); - if (util.stackFrame.timer.timeElapsed() < duration) { - util.yield(); - } - } + wait (args) { + const duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION)); + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, duration); + }); } if (args, util) { From e237d2484611298d2c8faf494f20b1ec431ea270 Mon Sep 17 00:00:00 2001 From: DD Liu <liudi@media.mit.edu> Date: Tue, 6 Jun 2017 10:21:27 -0400 Subject: [PATCH 07/11] fix lint --- src/blocks/scratch3_control.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index f460399e1..975dfeb81 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -1,5 +1,4 @@ const Cast = require('../util/cast'); -const Timer = require('../util/timer'); class Scratch3ControlBlocks { constructor (runtime) { From 40345384aa47e267115b9a201ab1e93bd6e1535f Mon Sep 17 00:00:00 2001 From: Paul Kaplan <pkaplan@media.mit.edu> Date: Wed, 7 Jun 2017 09:06:46 -0400 Subject: [PATCH 08/11] Select the last target instead of the first --- src/virtual-machine.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 19a4a8dd4..436ff60e8 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -379,7 +379,8 @@ class VirtualMachine extends EventEmitter { this.runtime.disposeTarget(sprite.clones[i]); // Ensure editing target is switched if we are deleting it. if (clone === currentEditingTarget) { - this.setEditingTarget(this.runtime.targets[0].id); + const lastTargetIndex = this.runtime.targets.length - 1; + this.setEditingTarget(this.runtime.targets[lastTargetIndex].id); } } // Sprite object should be deleted by GC. From 454082b569e6cb1969ede222ba5fb771a4288e9c Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Wed, 7 Jun 2017 17:05:24 -0700 Subject: [PATCH 09/11] Improve cleanup on clone disposal The `dispose()` method on `RenderedTarget` now: - informs the runtime that it should end any threads corresponding the target being disposed, and - removes the clone from its sprite. --- src/sprites/rendered-target.js | 2 ++ src/sprites/sprite.js | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 0bc5cff6e..2fb0ddf9e 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -832,6 +832,8 @@ class RenderedTarget extends Target { */ dispose () { this.runtime.changeCloneCounter(-1); + this.runtime.stopForTarget(this); + this.sprite.removeClone(this); if (this.renderer && this.drawableID !== null) { this.renderer.destroyDrawable(this.drawableID); if (this.visible) { diff --git a/src/sprites/sprite.js b/src/sprites/sprite.js index 3a0402a16..293a1f265 100644 --- a/src/sprites/sprite.js +++ b/src/sprites/sprite.js @@ -58,6 +58,18 @@ class Sprite { } return newClone; } + + /** + * Disconnect a clone from this sprite. The clone is unmodified. + * In particular, the clone's dispose() method is not called. + * @param {!RenderedTarget} clone - the clone to be removed. + */ + removeClone (clone) { + const cloneIndex = this.clones.indexOf(clone); + if (cloneIndex >= 0) { + this.clones.splice(cloneIndex, 1); + } + } } module.exports = Sprite; From 6320fd72c311a65547adeb89e37c80c585f1c2b8 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <cwillisf@media.mit.edu> Date: Thu, 8 Jun 2017 11:16:29 -0700 Subject: [PATCH 10/11] Add test for clone cleanup This new test verifies that clones and their associated threads are cleaned up properly by the `delete this clone` block. The clones run two stacks each: one which waits and then deletes the clone, and another which includes a `forever` loop: this is to verify that the thread running the `forever` loop is ended when the clone itself is deleted. The project does this with two batches of clones to ensure there are no problems with reusing array indices previously occupied by now-removed threads or clones. --- test/fixtures/clone-cleanup.sb2 | Bin 0 -> 54947 bytes test/integration/clone-cleanup.js | 96 ++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 test/fixtures/clone-cleanup.sb2 create mode 100644 test/integration/clone-cleanup.js diff --git a/test/fixtures/clone-cleanup.sb2 b/test/fixtures/clone-cleanup.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..61baeb7f2def6182f3372a4d89edcee7bf23db88 GIT binary patch literal 54947 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 M4;o(Z)~9d(55ztL+yDRo literal 0 HcmV?d00001 diff --git a/test/integration/clone-cleanup.js b/test/integration/clone-cleanup.js new file mode 100644 index 000000000..bfecd6d77 --- /dev/null +++ b/test/integration/clone-cleanup.js @@ -0,0 +1,96 @@ +const path = require('path'); +const test = require('tap').test; +const attachTestStorage = require('../fixtures/attach-test-storage'); +const extract = require('../fixtures/extract'); +const VirtualMachine = require('../../src/index'); + +const projectUri = path.resolve(__dirname, '../fixtures/clone-cleanup.sb2'); +const project = extract(projectUri); + +test('clone-cleanup', t => { + const vm = new VirtualMachine(); + attachTestStorage(vm); + + /** + * Track which step of the project is currently under test. + * @type {number} + */ + let testStep = -1; + + /** + * We test using setInterval; track the interval ID here so we can cancel it. + * @type {object} + */ + let testInterval = null; + + const verifyCounts = (expectedClones, extraThreads) => { + // stage plus one sprite, plus clones + t.strictEqual(vm.runtime.targets.length, 2 + expectedClones, + `target count at step ${testStep}`); + + // the stage should never have any clones + t.strictEqual(vm.runtime.targets[0].sprite.clones.length, 1, + `stage clone count at step ${testStep}`); + + // check sprite clone count (+1 for original) + t.strictEqual(vm.runtime.targets[1].sprite.clones.length, 1 + expectedClones, + `sprite clone count at step ${testStep}`); + + // thread count isn't directly tied to clone count since threads can end + t.strictEqual(vm.runtime.threads.length, extraThreads + (2 * expectedClones), + `thread count at step ${testStep}`); + }; + + const testNextStep = () => { + ++testStep; + switch (testStep) { + case 0: + // Project has started, main thread running, no clones yet + verifyCounts(0, 1); + break; + + case 1: + // 10 clones have been created, main thread still running + verifyCounts(10, 1); + break; + + case 2: + // The first batch of clones has deleted themselves; main thread still running + verifyCounts(0, 1); + break; + + case 3: + // The second batch of clones has been created and the main thread has ended + verifyCounts(10, 0); + break; + + case 4: + // The second batch of clones has deleted themselves; everything is finished + verifyCounts(0, 0); + + clearInterval(testInterval); + t.end(); + process.nextTick(process.exit); + break; + } + }; + + // Start VM, load project, and run + t.doesNotThrow(() => { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project).then(() => { + + // Verify initial state: no clones, nothing running ("step -1") + verifyCounts(0, 0); + + vm.greenFlag(); + + // Every second, advance the testing step + testInterval = setInterval(testNextStep, 1000); + }); + }); + +}); From 9c5d43e9d3f6da2c75cccc851756b7b355244cb4 Mon Sep 17 00:00:00 2001 From: Paul Kaplan <pkaplan@media.mit.edu> Date: Mon, 12 Jun 2017 08:35:27 -0400 Subject: [PATCH 11/11] Enhance sprite delete behavior --- src/virtual-machine.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 436ff60e8..12e6e7a77 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -364,6 +364,8 @@ class VirtualMachine extends EventEmitter { */ deleteSprite (targetId) { const target = this.runtime.getTargetById(targetId); + const targetIndexBeforeDelete = this.runtime.targets.map(t => t.id).indexOf(target.id); + if (target) { if (!target.isSprite()) { throw new Error('Cannot delete non-sprite targets.'); @@ -379,8 +381,8 @@ class VirtualMachine extends EventEmitter { this.runtime.disposeTarget(sprite.clones[i]); // Ensure editing target is switched if we are deleting it. if (clone === currentEditingTarget) { - const lastTargetIndex = this.runtime.targets.length - 1; - this.setEditingTarget(this.runtime.targets[lastTargetIndex].id); + const nextTargetIndex = Math.min(this.runtime.targets.length - 1, targetIndexBeforeDelete); + this.setEditingTarget(this.runtime.targets[nextTargetIndex].id); } } // Sprite object should be deleted by GC.