Merge branch 'develop' into non-droppable-extension-menus

This commit is contained in:
Chris Willis-Ford 2019-06-18 16:33:19 -07:00 committed by Christopher Willis-Ford
commit eba89d57d8
22 changed files with 574 additions and 320 deletions

6
package-lock.json generated
View file

@ -12258,9 +12258,9 @@
}
},
"scratch-translate-extension-languages": {
"version": "0.0.20181205140428",
"resolved": "https://registry.npmjs.org/scratch-translate-extension-languages/-/scratch-translate-extension-languages-0.0.20181205140428.tgz",
"integrity": "sha512-vNE8LSIrJl5cCCS1yrR6wivtFhuJmHFhbZnTrkt/E3BfyqMBJgpqJTtYfi+up1sorc7AixhpHTARE6/t3D//MA=="
"version": "0.0.20190416132834",
"resolved": "https://registry.npmjs.org/scratch-translate-extension-languages/-/scratch-translate-extension-languages-0.0.20190416132834.tgz",
"integrity": "sha512-mRDRBqbwf52DSbR9tJLRw2/MIUoFncNaqTKkVjdDJcIRm5niTtKtFDn2BlyQunAAkUVdswncS0t1wpf155lQGw=="
},
"script-loader": {
"version": "0.7.2",

View file

@ -44,7 +44,7 @@
"nets": "3.2.0",
"scratch-parser": "5.0.0",
"scratch-sb1-converter": "0.2.7",
"scratch-translate-extension-languages": "0.0.20181205140428",
"scratch-translate-extension-languages": "0.0.20190416132834",
"socket.io-client": "2.0.4",
"text-encoding": "0.7.0",
"worker-loader": "^1.1.1"

View file

@ -0,0 +1,78 @@
/**
* @fileoverview
* The BlocksRuntimeCache caches data about the top block of scripts so that
* Runtime can iterate a targeted opcode and iterate the returned set faster.
* Many top blocks need to match fields as well as opcode, since that matching
* compares strings in uppercase we can go ahead and uppercase the cached value
* so we don't need to in the future.
*/
/**
* A set of cached data about the top block of a script.
* @param {Blocks} container - Container holding the block and related data
* @param {string} blockId - Id for whose block data is cached in this instance
*/
class RuntimeScriptCache {
constructor (container, blockId) {
/**
* Container with block data for blockId.
* @type {Blocks}
*/
this.container = container;
/**
* ID for block this instance caches.
* @type {string}
*/
this.blockId = blockId;
const block = container.getBlock(blockId);
const fields = container.getFields(block);
/**
* Formatted fields or fields of input blocks ready for comparison in
* runtime.
*
* This is a clone of parts of the targeted blocks. Changes to these
* clones are limited to copies under RuntimeScriptCache and will not
* appear in the original blocks in their container. This copy is
* modified changing the case of strings to uppercase. These uppercase
* values will be compared later by the VM.
* @type {object}
*/
this.fieldsOfInputs = Object.assign({}, fields);
if (Object.keys(fields).length === 0) {
const inputs = container.getInputs(block);
for (const input in inputs) {
if (!inputs.hasOwnProperty(input)) continue;
const id = inputs[input].block;
const inputBlock = container.getBlock(id);
const inputFields = container.getFields(inputBlock);
Object.assign(this.fieldsOfInputs, inputFields);
}
}
for (const key in this.fieldsOfInputs) {
const field = this.fieldsOfInputs[key] = Object.assign({}, this.fieldsOfInputs[key]);
if (field.value.toUpperCase) {
field.value = field.value.toUpperCase();
}
}
}
}
/**
* Get an array of scripts from a block container prefiltered to match opcode.
* @param {Blocks} container - Container of blocks
* @param {string} opcode - Opcode to filter top blocks by
*/
exports.getScripts = function () {
throw new Error('blocks.js has not initialized BlocksRuntimeCache');
};
/**
* Exposed RuntimeScriptCache class used by integration in blocks.js.
* @private
*/
exports._RuntimeScriptCache = RuntimeScriptCache;
require('./blocks');

View file

@ -5,6 +5,7 @@ const MonitorRecord = require('./monitor-record');
const Clone = require('../util/clone');
const {Map} = require('immutable');
const BlocksExecuteCache = require('./blocks-execute-cache');
const BlocksRuntimeCache = require('./blocks-runtime-cache');
const log = require('../util/log');
const Variable = require('./variable');
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');
@ -74,7 +75,13 @@ class Blocks {
* actively monitored.
* @type {Array<{blockId: string, target: Target}>}
*/
_monitored: null
_monitored: null,
/**
* A cache of hat opcodes to collection of theads to execute.
* @type {object.<string, object>}
*/
scripts: {}
};
/**
@ -509,6 +516,7 @@ class Blocks {
this._cache.procedureDefinitions = {};
this._cache._executeCached = {};
this._cache._monitored = null;
this._cache.scripts = {};
}
/**
@ -1221,4 +1229,35 @@ BlocksExecuteCache.getCached = function (blocks, blockId, CacheType) {
return cached;
};
/**
* Cache class constructor for runtime. Used to consider what threads should
* start based on hat data.
* @type {function}
*/
const RuntimeScriptCache = BlocksRuntimeCache._RuntimeScriptCache;
/**
* Get an array of scripts from a block container prefiltered to match opcode.
* @param {Blocks} blocks - Container of blocks
* @param {string} opcode - Opcode to filter top blocks by
* @returns {Array.<RuntimeScriptCache>} - Array of RuntimeScriptCache cache
* objects
*/
BlocksRuntimeCache.getScripts = function (blocks, opcode) {
let scripts = blocks._cache.scripts[opcode];
if (!scripts) {
scripts = blocks._cache.scripts[opcode] = [];
const allScripts = blocks._scripts;
for (let i = 0; i < allScripts.length; i++) {
const topBlockId = allScripts[i];
const block = blocks.getBlock(topBlockId);
if (block.opcode === opcode) {
scripts.push(new RuntimeScriptCache(blocks, topBlockId));
}
}
}
return scripts;
};
module.exports = Blocks;

View file

@ -13,6 +13,8 @@ const mutatorTagToObject = function (dom) {
for (const prop in dom.attribs) {
if (prop === 'xmlns') continue;
obj[prop] = decodeHtml(dom.attribs[prop]);
// Note: the capitalization of block info in the following lines is important.
// The lowercase is read in from xml which normalizes case. The VM uses camel case everywhere else.
if (prop === 'blockinfo') {
obj.blockInfo = JSON.parse(obj.blockinfo);
delete obj.blockinfo;

View file

@ -3,6 +3,7 @@ const {OrderedMap} = require('immutable');
const ArgumentType = require('../extension-support/argument-type');
const Blocks = require('./blocks');
const BlocksRuntimeCache = require('./blocks-runtime-cache');
const BlockType = require('../extension-support/block-type');
const Profiler = require('./profiler');
const Sequencer = require('./sequencer');
@ -15,6 +16,7 @@ const maybeFormatMessage = require('../util/maybe-format-message');
const StageLayering = require('./stage-layering');
const Variable = require('./variable');
const xmlEscape = require('../util/xml-escape');
const ScratchLinkWebSocket = require('../util/scratch-link-websocket');
// Virtual I/O devices.
const Clock = require('../io/clock');
@ -822,13 +824,12 @@ class Runtime extends EventEmitter {
* @private
*/
_refreshExtensionPrimitives (extensionInfo) {
for (const categoryInfo of this._blockInfo) {
if (extensionInfo.id === categoryInfo.id) {
categoryInfo.name = maybeFormatMessage(extensionInfo.name);
this._fillExtensionCategory(categoryInfo, extensionInfo);
const categoryInfo = this._blockInfo.find(info => info.id === extensionInfo.id);
if (categoryInfo) {
categoryInfo.name = maybeFormatMessage(extensionInfo.name);
this._fillExtensionCategory(categoryInfo, extensionInfo);
this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo);
}
this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo);
}
}
@ -1328,6 +1329,34 @@ class Runtime extends EventEmitter {
(result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []);
}
/**
* Get a scratch link socket.
* @param {string} type Either BLE or BT
* @returns {ScratchLinkSocket} The scratch link socket.
*/
getScratchLinkSocket (type) {
const factory = this._linkSocketFactory || this._defaultScratchLinkSocketFactory;
return factory(type);
}
/**
* Configure how ScratchLink sockets are created. Factory must consume a "type" parameter
* either BT or BLE.
* @param {Function} factory The new factory for creating ScratchLink sockets.
*/
configureScratchLinkSocketFactory (factory) {
this._linkSocketFactory = factory;
}
/**
* The default scratch link socket creator, using websockets to the installed device manager.
* @param {string} type Either BLE or BT
* @returns {ScratchLinkSocket} The new scratch link socket (a WebSocket object)
*/
_defaultScratchLinkSocketFactory (type) {
return new ScratchLinkWebSocket(type);
}
/**
* Register an extension that communications with a hardware peripheral by id,
* to have access to it and its peripheral functions in the future.
@ -1474,16 +1503,11 @@ class Runtime extends EventEmitter {
* @return {!Thread} The newly created thread.
*/
_pushThread (id, target, opts) {
opts = Object.assign({
stackClick: false,
updateMonitor: false
}, opts);
const thread = new Thread(id);
thread.target = target;
thread.stackClick = opts.stackClick;
thread.updateMonitor = opts.updateMonitor;
thread.blockContainer = opts.updateMonitor ?
thread.stackClick = Boolean(opts && opts.stackClick);
thread.updateMonitor = Boolean(opts && opts.updateMonitor);
thread.blockContainer = thread.updateMonitor ?
this.monitorBlocks :
target.blocks;
@ -1626,6 +1650,20 @@ class Runtime extends EventEmitter {
}
}
allScriptsByOpcodeDo (opcode, f, optTarget) {
let targets = this.executableTargets;
if (optTarget) {
targets = [optTarget];
}
for (let t = targets.length - 1; t >= 0; t--) {
const target = targets[t];
const scripts = BlocksRuntimeCache.getScripts(target.blocks, opcode);
for (let j = 0; j < scripts.length; j++) {
f(scripts[j], target);
}
}
}
/**
* Start all relevant hats.
* @param {!string} requestedHatOpcode Opcode of hats to start.
@ -1650,71 +1688,52 @@ class Runtime extends EventEmitter {
}
// Consider all scripts, looking for hats with opcode `requestedHatOpcode`.
this.allScriptsDo((topBlockId, target) => {
const blocks = target.blocks;
const block = blocks.getBlock(topBlockId);
const potentialHatOpcode = block.opcode;
if (potentialHatOpcode !== requestedHatOpcode) {
// Not the right hat.
return;
}
this.allScriptsByOpcodeDo(requestedHatOpcode, (script, target) => {
const {
blockId: topBlockId,
fieldsOfInputs: hatFields
} = script;
// Match any requested fields.
// For example: ensures that broadcasts match.
// This needs to happen before the block is evaluated
// (i.e., before the predicate can be run) because "broadcast and wait"
// needs to have a precise collection of started threads.
let hatFields = blocks.getFields(block);
// If no fields are present, check inputs (horizontal blocks)
if (Object.keys(hatFields).length === 0) {
hatFields = {}; // don't overwrite the block's actual fields list
const hatInputs = blocks.getInputs(block);
for (const input in hatInputs) {
if (!hatInputs.hasOwnProperty(input)) continue;
const id = hatInputs[input].block;
const inpBlock = blocks.getBlock(id);
const fields = blocks.getFields(inpBlock);
Object.assign(hatFields, fields);
}
}
if (optMatchFields) {
for (const matchField in optMatchFields) {
if (hatFields[matchField].value.toUpperCase() !==
optMatchFields[matchField]) {
// Field mismatch.
return;
}
for (const matchField in optMatchFields) {
if (hatFields[matchField].value !== optMatchFields[matchField]) {
// Field mismatch.
return;
}
}
if (hatMeta.restartExistingThreads) {
// If `restartExistingThreads` is true, we should stop
// any existing threads starting with the top block.
for (let i = 0; i < instance.threads.length; i++) {
if (instance.threads[i].topBlock === topBlockId &&
!instance.threads[i].stackClick && // stack click threads and hat threads can coexist
instance.threads[i].target === target) {
newThreads.push(instance._restartThread(instance.threads[i]));
for (let i = 0; i < this.threads.length; i++) {
if (this.threads[i].target === target &&
this.threads[i].topBlock === topBlockId &&
// stack click threads and hat threads can coexist
!this.threads[i].stackClick) {
newThreads.push(this._restartThread(this.threads[i]));
return;
}
}
} else {
// If `restartExistingThreads` is false, we should
// give up if any threads with the top block are running.
for (let j = 0; j < instance.threads.length; j++) {
if (instance.threads[j].topBlock === topBlockId &&
instance.threads[j].target === target &&
!instance.threads[j].stackClick && // stack click threads and hat threads can coexist
instance.threads[j].status !== Thread.STATUS_DONE) {
for (let j = 0; j < this.threads.length; j++) {
if (this.threads[j].target === target &&
this.threads[j].topBlock === topBlockId &&
// stack click threads and hat threads can coexist
!this.threads[j].stackClick &&
this.threads[j].status !== Thread.STATUS_DONE) {
// Some thread is already running.
return;
}
}
}
// Start the thread with this top block.
newThreads.push(instance._pushThread(topBlockId, target));
newThreads.push(this._pushThread(topBlockId, target));
}, optTarget);
// For compatibility with Scratch 2, edge triggered hats need to be processed before
// threads are stepped. See ScratchRuntime.as for original implementation
@ -1893,8 +1912,12 @@ class Runtime extends EventEmitter {
}
}
this.targets = newTargets;
// Dispose all threads.
this.threads.forEach(thread => this._stopThread(thread));
// Dispose of the active thread.
if (this.sequencer.activeThread !== null) {
this._stopThread(this.sequencer.activeThread);
}
// Remove all remaining threads from executing in the next tick.
this.threads = [];
}
/**

View file

@ -51,6 +51,8 @@ class Sequencer {
* @type {!Runtime}
*/
this.runtime = runtime;
this.activeThread = null;
}
/**
@ -97,8 +99,9 @@ class Sequencer {
numActiveThreads = 0;
let stoppedThread = false;
// Attempt to run each thread one time.
for (let i = 0; i < this.runtime.threads.length; i++) {
const activeThread = this.runtime.threads[i];
const threads = this.runtime.threads;
for (let i = 0; i < threads.length; i++) {
const activeThread = this.activeThread = threads[i];
// Check if the thread is done so it is not executed.
if (activeThread.stack.length === 0 ||
activeThread.status === Thread.STATUS_DONE) {
@ -165,6 +168,8 @@ class Sequencer {
}
}
this.activeThread = null;
return doneThreads;
}
@ -177,6 +182,12 @@ class Sequencer {
if (!currentBlockId) {
// A "null block" - empty branch.
thread.popStack();
// Did the null follow a hat block?
if (thread.stack.length === 0) {
thread.status = Thread.STATUS_DONE;
return;
}
}
// Save the current block ID to notice if we did control flow.
while ((currentBlockId = thread.peekStack())) {

View file

@ -20,7 +20,6 @@ const builtinExtensions = {
text2speech: () => require('../extensions/scratch3_text2speech'),
translate: () => require('../extensions/scratch3_translate'),
videoSensing: () => require('../extensions/scratch3_video_sensing'),
speech2text: () => require('../extensions/scratch3_speech2text'),
ev3: () => require('../extensions/scratch3_ev3'),
makeymakey: () => require('../extensions/scratch3_makeymakey'),
boost: () => require('../extensions/scratch3_boost'),

View file

@ -332,36 +332,36 @@ class BoostMotor {
* @type {Object}
* @private
*/
this._pendingTimeoutId = null;
this._pendingDurationTimeoutId = null;
/**
* The starting time for the pending timeout.
* The starting time for the pending duration timeout.
* @type {number}
* @private
*/
this._pendingTimeoutStartTime = null;
this._pendingDurationTimeoutStartTime = null;
/**
* The delay/duration of the pending timeout.
* The delay/duration of the pending duration timeout.
* @type {number}
* @private
*/
this._pendingTimeoutDelay = null;
this._pendingDurationTimeoutDelay = null;
/**
* The target position of a turn-based command.
* @type {number}
* @private
*/
this._pendingPositionDestination = null;
this._pendingRotationDestination = null;
/**
* If the motor has been turned on run for a specific duration,
* this is the function that will be called once Scratch VM gets a notification from the Move Hub.
* If the motor has been turned on run for a specific rotation, this is the function
* that will be called once Scratch VM gets a notification from the Move Hub.
* @type {Object}
* @private
*/
this._pendingPromiseFunction = null;
this._pendingRotationPromise = null;
this.turnOff = this.turnOff.bind(this);
}
@ -431,45 +431,44 @@ class BoostMotor {
* @param {BoostMotorState} value - set this motor's state.
*/
set status (value) {
// Clear any time- or rotation-related state from motor and set new status.
this._clearRotationState();
this._clearTimeout();
this._clearDurationTimeout();
this._status = value;
}
/**
* @return {number} - time, in milliseconds, of when the pending timeout began.
* @return {number} - time, in milliseconds, of when the pending duration timeout began.
*/
get pendingTimeoutStartTime () {
return this._pendingTimeoutStartTime;
get pendingDurationTimeoutStartTime () {
return this._pendingDurationTimeoutStartTime;
}
/**
* @return {number} - delay, in milliseconds, of the pending timeout.
* @return {number} - delay, in milliseconds, of the pending duration timeout.
*/
get pendingTimeoutDelay () {
return this._pendingTimeoutDelay;
get pendingDurationTimeoutDelay () {
return this._pendingDurationTimeoutDelay;
}
/**
* @return {number} - delay, in milliseconds, of the pending timeout.
* @return {number} - target position, in degrees, of the pending rotation.
*/
get pendingPositionDestination () {
return this._pendingPositionDestination;
get pendingRotationDestination () {
return this._pendingRotationDestination;
}
/**
* @return {boolean} - true if this motor is currently moving, false if this motor is off or braking.
* @return {Promise} - the Promise function for the pending rotation.
*/
get pendingPromiseFunction () {
return this._pendingPromiseFunction;
get pendingRotationPromise () {
return this._pendingRotationPromise;
}
/**
* @param {function} func - function to resolve promise
* @param {function} func - function to resolve pending rotation Promise
*/
set pendingPromiseFunction (func) {
this._pendingPromiseFunction = func;
set pendingRotationPromise (func) {
this._pendingRotationPromise = func;
}
/**
@ -477,7 +476,6 @@ class BoostMotor {
* @private
*/
_turnOn () {
if (this.power === 0) return;
const cmd = this._parent.generateOutputCommand(
this._index,
BoostOutputExecution.EXECUTE_IMMEDIATELY,
@ -493,40 +491,29 @@ class BoostMotor {
/**
* Turn this motor on indefinitely
* @param {boolean} [resetState=true] - whether to reset the state of the motor when running this command.
*/
turnOnForever (resetState = true){
if (this.power === 0) return;
if (resetState) this.status = BoostMotorState.ON_FOREVER;
turnOnForever () {
this.status = BoostMotorState.ON_FOREVER;
this._turnOn();
}
/**
* Turn this motor on for a specific duration.
* @param {number} milliseconds - run the motor for this long.
* @param {boolean} [resetState=true] - whether to reset the state of the motor when running this command.
*/
turnOnFor (milliseconds, resetState = true) {
if (this.power === 0) return;
turnOnFor (milliseconds) {
milliseconds = Math.max(0, milliseconds);
if (resetState) this.status = BoostMotorState.ON_FOR_TIME;
this.status = BoostMotorState.ON_FOR_TIME;
this._turnOn();
this._setNewTimeout(this.turnOff, milliseconds);
this._setNewDurationTimeout(this.turnOff, milliseconds);
}
/**
* Turn this motor on for a specific rotation in degrees.
* @param {number} degrees - run the motor for this amount of degrees.
* @param {number} direction - rotate in this direction
* @param {boolean} [resetState=true] - whether to reset the state of the motor when running this command.
*/
turnOnForDegrees (degrees, direction, resetState = true) {
if (this.power === 0) {
this._clearRotationState();
return;
}
turnOnForDegrees (degrees, direction) {
degrees = Math.max(0, degrees);
const cmd = this._parent.generateOutputCommand(
@ -542,8 +529,8 @@ class BoostMotor {
]
);
if (resetState) this.status = BoostMotorState.ON_FOR_ROTATION;
this._pendingPositionDestination = this.position + (degrees * this.direction * direction);
this.status = BoostMotorState.ON_FOR_ROTATION;
this._pendingRotationDestination = this.position + (degrees * this.direction * direction);
this._parent.send(BoostBLE.characteristic, cmd);
}
@ -552,8 +539,6 @@ class BoostMotor {
* @param {boolean} [useLimiter=true] - if true, use the rate limiter
*/
turnOff (useLimiter = true) {
if (this.power === 0) return;
const cmd = this._parent.generateOutputCommand(
this._index,
BoostOutputExecution.EXECUTE_IMMEDIATELY ^ BoostOutputExecution.COMMAND_FEEDBACK,
@ -573,12 +558,12 @@ class BoostMotor {
* 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;
_clearDurationTimeout () {
if (this._pendingDurationTimeoutId !== null) {
clearTimeout(this._pendingDurationTimeoutId);
this._pendingDurationTimeoutId = null;
this._pendingDurationTimeoutStartTime = null;
this._pendingDurationTimeoutDelay = null;
}
}
@ -588,19 +573,19 @@ class BoostMotor {
* @param {int} delay - wait this many milliseconds before calling the callback.
* @private
*/
_setNewTimeout (callback, delay) {
this._clearTimeout();
_setNewDurationTimeout (callback, delay) {
this._clearDurationTimeout();
const timeoutID = setTimeout(() => {
if (this._pendingTimeoutId === timeoutID) {
this._pendingTimeoutId = null;
this._pendingTimeoutStartTime = null;
this._pendingTimeoutDelay = null;
if (this._pendingDurationTimeoutId === timeoutID) {
this._pendingDurationTimeoutId = null;
this._pendingDurationTimeoutStartTime = null;
this._pendingDurationTimeoutDelay = null;
}
callback();
}, delay);
this._pendingTimeoutId = timeoutID;
this._pendingTimeoutStartTime = Date.now();
this._pendingTimeoutDelay = delay;
this._pendingDurationTimeoutId = timeoutID;
this._pendingDurationTimeoutStartTime = Date.now();
this._pendingDurationTimeoutDelay = delay;
}
/**
@ -609,11 +594,11 @@ class BoostMotor {
* @private
*/
_clearRotationState () {
if (this._pendingPromiseFunction !== null) {
this._pendingPromiseFunction();
this._pendingPromiseFunction = null;
if (this._pendingRotationPromise !== null) {
this._pendingRotationPromise();
this._pendingRotationPromise = null;
}
this._pendingPositionDestination = null;
this._pendingRotationDestination = null;
}
}
@ -1213,7 +1198,7 @@ class Scratch3BoostBlocks {
getInfo () {
return {
id: Scratch3BoostBlocks.EXTENSION_ID,
name: 'Boost',
name: 'BOOST',
blockIconURI: iconURI,
showStatusButton: true,
blocks: [
@ -1432,51 +1417,27 @@ class Scratch3BoostBlocks {
acceptReporters: true,
items: [
{
text: formatMessage({
id: 'boost.motorId.a',
default: BoostMotorLabel.A,
description: `label for motor A element in motor menu for LEGO Boost extension`
}),
text: 'A',
value: BoostMotorLabel.A
},
{
text: formatMessage({
id: 'boost.motorId.b',
default: BoostMotorLabel.B,
description: `label for motor B element in motor menu for LEGO Boost extension`
}),
text: 'B',
value: BoostMotorLabel.B
},
{
text: formatMessage({
id: 'boost.motorId.c',
default: BoostMotorLabel.C,
description: `label for motor C element in motor menu for LEGO Boost extension`
}),
text: 'C',
value: BoostMotorLabel.C
},
{
text: formatMessage({
id: 'boost.motorId.d',
default: BoostMotorLabel.D,
description: `label for motor D element in motor menu for LEGO Boost extension`
}),
text: 'D',
value: BoostMotorLabel.D
},
{
text: formatMessage({
id: 'boost.motorId.ab',
default: BoostMotorLabel.AB,
description: `label for motor A and B element in motor menu for LEGO Boost extension`
}),
text: 'AB',
value: BoostMotorLabel.AB
},
{
text: formatMessage({
id: 'boost.motorId.all',
default: BoostMotorLabel.ALL,
description: 'label for all motors element in motor menu for LEGO Boost extension'
}),
text: 'ABCD',
value: BoostMotorLabel.ALL
}
]
@ -1485,35 +1446,19 @@ class Scratch3BoostBlocks {
acceptReporters: true,
items: [
{
text: formatMessage({
id: 'boost.motorReporterId.a',
default: BoostMotorLabel.A,
description: 'label for motor A element in motor menu for LEGO Boost extension'
}),
text: 'A',
value: BoostMotorLabel.A
},
{
text: formatMessage({
id: 'boost.motorReporterId.b',
default: BoostMotorLabel.B,
description: 'label for motor B element in motor menu for LEGO Boost extension'
}),
text: 'B',
value: BoostMotorLabel.B
},
{
text: formatMessage({
id: 'boost.motorReporterId.c',
default: BoostMotorLabel.C,
description: 'label for motor C element in motor menu for LEGO Boost extension'
}),
text: 'C',
value: BoostMotorLabel.C
},
{
text: formatMessage({
id: 'boost.motorReporterId.d',
default: BoostMotorLabel.D,
description: 'label for motor D element in motor menu for LEGO Boost extension'
}),
text: 'D',
value: BoostMotorLabel.D
}
]
@ -1745,7 +1690,7 @@ class Scratch3BoostBlocks {
if (motor.power === 0) return Promise.resolve();
return new Promise(resolve => {
motor.turnOnForDegrees(degrees, sign);
motor.pendingPromiseFunction = resolve;
motor.pendingRotationPromise = resolve;
});
}
return null;
@ -1812,20 +1757,20 @@ class Scratch3BoostBlocks {
motor.power = MathUtil.clamp(Cast.toNumber(args.POWER), 0, 100);
switch (motor.status) {
case BoostMotorState.ON_FOREVER:
motor.turnOnForever(false);
motor.turnOnForever();
break;
case BoostMotorState.ON_FOR_TIME:
motor.turnOnFor(motor.pendingTimeoutStartTime + motor.pendingTimeoutDelay - Date.now(), false);
motor.turnOnFor(motor.pendingDurationTimeoutStartTime +
motor.pendingDurationTimeoutDelay - Date.now());
break;
case BoostMotorState.ON_FOR_ROTATION: {
const p = Math.abs(motor.pendingPositionDestination - motor.position, false);
motor.turnOnForDegrees(p, Math.sign(p));
break;
}
}
}
});
return Promise.resolve();
return new Promise(resolve => {
window.setTimeout(() => {
resolve();
}, BoostBLE.sendInterval);
});
}
/**
@ -1859,21 +1804,21 @@ class Scratch3BoostBlocks {
if (motor) {
switch (motor.status) {
case BoostMotorState.ON_FOREVER:
motor.turnOnForever(false);
motor.turnOnForever();
break;
case BoostMotorState.ON_FOR_TIME:
motor.turnOnFor(motor.pendingTimeoutStartTime + motor.pendingTimeoutDelay - Date.now(), false);
motor.turnOnFor(motor.pendingDurationTimeoutStartTime +
motor.pendingDurationTimeoutDelay - Date.now());
break;
case BoostMotorState.ON_FOR_ROTATION: {
const p = Math.abs(motor.pendingPositionDestination - motor.position);
motor.turnOnForDegrees(p, Math.sign(p), false);
break;
}
}
}
}
});
return Promise.resolve();
return new Promise(resolve => {
window.setTimeout(() => {
resolve();
}, BoostBLE.sendInterval);
});
}
/**
@ -1901,7 +1846,13 @@ class Scratch3BoostBlocks {
return false;
}
if (portID && this._peripheral.motor(portID)) {
return MathUtil.wrapClamp(this._peripheral.motor(portID).position, 0, 360);
let val = this._peripheral.motor(portID).position;
// Boost motor A position direction is reversed by design
// so we have to reverse the position here
if (portID === BoostPort.A) {
val *= -1;
}
return MathUtil.wrapClamp(val, 0, 360);
}
return 0;
}

File diff suppressed because one or more lines are too long

View file

@ -134,7 +134,7 @@ class GdxFor {
* @type {BLE}
* @private
*/
this._scratchLinkSocket = null;
this._ble = null;
/**
* An @vernier/godirect Device
@ -181,11 +181,11 @@ class GdxFor {
* Called by the runtime when user wants to scan for a peripheral.
*/
scan () {
if (this._scratchLinkSocket) {
this._scratchLinkSocket.disconnect();
if (this._ble) {
this._ble.disconnect();
}
this._scratchLinkSocket = new BLE(this._runtime, this._extensionId, {
this._ble = new BLE(this._runtime, this._extensionId, {
filters: [
{namePrefix: 'GDX-FOR'}
],
@ -200,8 +200,8 @@ class GdxFor {
* @param {number} id - the id of the peripheral to connect to.
*/
connect (id) {
if (this._scratchLinkSocket) {
this._scratchLinkSocket.connectPeripheral(id);
if (this._ble) {
this._ble.connectPeripheral(id);
}
}
@ -220,8 +220,8 @@ class GdxFor {
spinSpeedY: 0,
spinSpeedZ: 0
};
if (this._scratchLinkSocket) {
this._scratchLinkSocket.disconnect();
if (this._ble) {
this._ble.disconnect();
}
}
@ -231,8 +231,8 @@ class GdxFor {
*/
isConnected () {
let connected = false;
if (this._scratchLinkSocket) {
connected = this._scratchLinkSocket.isConnected();
if (this._ble) {
connected = this._ble.isConnected();
}
return connected;
}
@ -242,7 +242,7 @@ class GdxFor {
* @private
*/
_onConnect () {
const adapter = new ScratchLinkDeviceAdapter(this._scratchLinkSocket, BLEUUID);
const adapter = new ScratchLinkDeviceAdapter(this._ble, BLEUUID);
godirect.createDevice(adapter, {open: true, startMeasurements: false}).then(device => {
// Setup device
this._device = device;
@ -262,7 +262,7 @@ class GdxFor {
});
});
this._timeoutID = window.setInterval(
() => this._scratchLinkSocket.handleDisconnectError(BLEDataStoppedError),
() => this._ble.handleDisconnectError(BLEDataStoppedError),
BLETimeout
);
});
@ -306,7 +306,7 @@ class GdxFor {
// cancel disconnect timeout and start a new one
window.clearInterval(this._timeoutID);
this._timeoutID = window.setInterval(
() => this._scratchLinkSocket.handleDisconnectError(BLEDataStoppedError),
() => this._ble.handleDisconnectError(BLEDataStoppedError),
BLETimeout
);
}

View file

@ -4,8 +4,8 @@ const Base64Util = require('../../util/base64-util');
* Adapter class
*/
class ScratchLinkDeviceAdapter {
constructor (scratchLinkSocket, {service, commandChar, responseChar}) {
this.scratchLinkSocket = scratchLinkSocket;
constructor (socket, {service, commandChar, responseChar}) {
this.socket = socket;
this._service = service;
this._commandChar = commandChar;
@ -21,13 +21,13 @@ class ScratchLinkDeviceAdapter {
writeCommand (commandBuffer) {
const data = Base64Util.uint8ArrayToBase64(commandBuffer);
return this.scratchLinkSocket
return this.socket
.write(this._service, this._commandChar, data, 'base64');
}
setup ({onResponse}) {
this._deviceOnResponse = onResponse;
return this.scratchLinkSocket
return this.socket
.startNotifications(this._service, this._responseChar, this._onResponse);
// TODO:

View file

@ -78,6 +78,7 @@ const FEMALE_GIANT_RATE = 0.79; // -4 semitones
/**
* Language ids. The value for each language id is a valid Scratch locale.
*/
const ARABIC_ID = 'ar';
const CHINESE_ID = 'zh-cn';
const DANISH_ID = 'da';
const DUTCH_ID = 'nl';
@ -211,6 +212,12 @@ class Scratch3Text2SpeechBlocks {
*/
get LANGUAGE_INFO () {
return {
[ARABIC_ID]: {
name: 'Arabic',
locales: ['ar'],
speechSynthLocale: 'arb',
singleGender: true
},
[CHINESE_ID]: {
name: 'Chinese (Mandarin)',
locales: ['zh-cn', 'zh-tw'],

View file

@ -174,7 +174,15 @@ class Scratch3TranslateBlocks {
getViewerLanguage () {
this._viewerLanguageCode = this.getViewerLanguageCode();
const names = languageNames.menuMap[this._viewerLanguageCode];
const langNameObj = names.find(obj => obj.code === this._viewerLanguageCode);
let langNameObj = names.find(obj => obj.code === this._viewerLanguageCode);
// If we don't have a name entry yet, try looking it up via the Google langauge
// code instead of Scratch's (e.g. for es-419 we look up es to get espanol)
if (!langNameObj && languageNames.scratchToGoogleMap[this._viewerLanguageCode]) {
const lookupCode = languageNames.scratchToGoogleMap[this._viewerLanguageCode];
langNameObj = names.find(obj => obj.code === lookupCode);
}
let langName = this._viewerLanguageCode;
if (langNameObj) {
langName = langNameObj.name;
@ -196,12 +204,13 @@ class Scratch3TranslateBlocks {
if (acc) {
return acc;
}
if (languageKeys.indexOf(lang) > -1) {
if (languageKeys.indexOf(lang.toLowerCase()) > -1) {
return lang;
}
return acc;
}, '') || 'en';
return languageCode;
return languageCode.toLowerCase();
}
/**
@ -220,6 +229,14 @@ class Scratch3TranslateBlocks {
if (languageNames.nameMap.hasOwnProperty(languageArg)) {
return languageNames.nameMap[languageArg];
}
// There are some languages we launched in the language menu that Scratch did not
// end up launching in. In order to keep projects that may have had that menu item
// working, check for those language codes and let them through.
// Examples: 'ab', 'hi'.
if (languageNames.previouslySupported.indexOf(languageArg) !== -1) {
return languageArg;
}
// Default to English.
return 'en';
}

View file

@ -297,7 +297,6 @@ class WeDo2Motor {
/**
* Start active braking on this motor. After a short time, the motor will turn off.
* // TODO: rename this to coastAfter?
*/
startBraking () {
if (this._power === 0) return;

View file

@ -1,8 +1,6 @@
const JSONRPCWebSocket = require('../util/jsonrpc-web-socket');
const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/ble';
// const log = require('../util/log');
const JSONRPC = require('../util/jsonrpc');
class BLE extends JSONRPCWebSocket {
class BLE extends JSONRPC {
/**
* A BLE peripheral socket object. It handles connecting, over web sockets, to
@ -14,13 +12,15 @@ class BLE extends JSONRPCWebSocket {
* @param {object} disconnectCallback - a callback for disconnection.
*/
constructor (runtime, extensionId, peripheralOptions, connectCallback, disconnectCallback = null) {
const ws = new WebSocket(ScratchLinkWebSocket);
super(ws);
super();
this._ws = ws;
this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens
this._ws.onerror = this._handleRequestError.bind(this, 'ws onerror');
this._ws.onclose = this.handleDisconnectError.bind(this, 'ws onclose');
this._socket = runtime.getScratchLinkSocket('BLE');
this._socket.setOnOpen(this.requestPeripheral.bind(this));
this._socket.setOnClose(this.handleDisconnectError.bind(this));
this._socket.setOnError(this._handleRequestError.bind(this));
this._socket.setHandleMessage(this._handleMessage.bind(this));
this._sendMessage = this._socket.sendMessage.bind(this._socket);
this._availablePeripherals = {};
this._connectCallback = connectCallback;
@ -31,6 +31,8 @@ class BLE extends JSONRPCWebSocket {
this._extensionId = extensionId;
this._peripheralOptions = peripheralOptions;
this._runtime = runtime;
this._socket.open();
}
/**
@ -38,18 +40,15 @@ class BLE extends JSONRPCWebSocket {
* If the web socket is not yet open, request when the socket promise resolves.
*/
requestPeripheral () {
if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen?
this._availablePeripherals = {};
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(e => {
this._handleRequestError(e);
});
this._availablePeripherals = {};
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
// TODO: else?
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(e => {
this._handleRequestError(e);
});
}
/**
@ -73,14 +72,14 @@ class BLE extends JSONRPCWebSocket {
* Close the websocket.
*/
disconnect () {
if (this._ws.readyState === this._ws.OPEN) {
this._ws.close();
}
if (this._connected) {
this._connected = false;
}
if (this._socket.isOpen()) {
this._socket.close();
}
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}

View file

@ -1,8 +1,6 @@
const JSONRPCWebSocket = require('../util/jsonrpc-web-socket');
const ScratchLinkWebSocket = 'wss://device-manager.scratch.mit.edu:20110/scratch/bt';
// const log = require('../util/log');
const JSONRPC = require('../util/jsonrpc');
class BT extends JSONRPCWebSocket {
class BT extends JSONRPC {
/**
* A BT peripheral socket object. It handles connecting, over web sockets, to
@ -15,13 +13,15 @@ class BT extends JSONRPCWebSocket {
* @param {object} messageCallback - a callback for message sending.
*/
constructor (runtime, extensionId, peripheralOptions, connectCallback, disconnectCallback = null, messageCallback) {
const ws = new WebSocket(ScratchLinkWebSocket);
super(ws);
super();
this._ws = ws;
this._ws.onopen = this.requestPeripheral.bind(this); // only call request peripheral after socket opens
this._ws.onerror = this._handleRequestError.bind(this, 'ws onerror');
this._ws.onclose = this.handleDisconnectError.bind(this, 'ws onclose');
this._socket = runtime.getScratchLinkSocket('BT');
this._socket.setOnOpen(this.requestPeripheral.bind(this));
this._socket.setOnError(this._handleRequestError.bind(this));
this._socket.setOnClose(this.handleDisconnectError.bind(this));
this._socket.setHandleMessage(this._handleMessage.bind(this));
this._sendMessage = this._socket.sendMessage.bind(this._socket);
this._availablePeripherals = {};
this._connectCallback = connectCallback;
@ -33,6 +33,8 @@ class BT extends JSONRPCWebSocket {
this._peripheralOptions = peripheralOptions;
this._messageCallback = messageCallback;
this._runtime = runtime;
this._socket.open();
}
/**
@ -40,27 +42,29 @@ class BT extends JSONRPCWebSocket {
* If the web socket is not yet open, request when the socket promise resolves.
*/
requestPeripheral () {
if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen?
this._availablePeripherals = {};
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(
e => this._handleRequestError(e)
);
this._availablePeripherals = {};
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}
// TODO: else?
this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._peripheralOptions)
.catch(
e => this._handleRequestError(e)
);
}
/**
* Try connecting to the input peripheral id, and then call the connect
* callback if connection is successful.
* @param {number} id - the id of the peripheral to connect to
* @param {string} pin - an optional pin for pairing
*/
connectPeripheral (id) {
this.sendRemoteRequest('connect', {peripheralId: id})
connectPeripheral (id, pin = null) {
const params = {peripheralId: id};
if (pin) {
params.pin = pin;
}
this.sendRemoteRequest('connect', params)
.then(() => {
this._connected = true;
this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED);
@ -75,14 +79,14 @@ class BT extends JSONRPCWebSocket {
* Close the websocket.
*/
disconnect () {
if (this._ws.readyState === this._ws.OPEN) {
this._ws.close();
}
if (this._connected) {
this._connected = false;
}
if (this._socket.isOpen()) {
this._socket.close();
}
if (this._discoverTimeoutID) {
window.clearTimeout(this._discoverTimeoutID);
}

View file

@ -1,40 +0,0 @@
const JSONRPC = require('./jsonrpc');
// const log = require('../util/log');
class JSONRPCWebSocket extends JSONRPC {
constructor (webSocket) {
super();
this._ws = webSocket;
this._ws.onmessage = e => this._onSocketMessage(e);
this._ws.onopen = e => this._onSocketOpen(e);
this._ws.onclose = e => this._onSocketClose(e);
this._ws.onerror = e => this._onSocketError(e);
}
dispose () {
this._ws.close();
this._ws = null;
}
_onSocketOpen () {
}
_onSocketClose () {
}
_onSocketError () {
}
_onSocketMessage (e) {
const json = JSON.parse(e.data);
this._handleMessage(json);
}
_sendMessage (message) {
const messageText = JSON.stringify(message);
this._ws.send(messageText);
}
}
module.exports = JSONRPCWebSocket;

View file

@ -0,0 +1,84 @@
/**
* This class provides a ScratchLinkSocket implementation using WebSockets,
* attempting to connect with the locally installed Scratch-Link.
*
* To connect with ScratchLink without WebSockets, you must implement all of the
* public methods in this class.
* - open()
* - close()
* - setOn[Open|Close|Error]
* - setHandleMessage
* - sendMessage(msgObj)
* - isOpen()
*/
class ScratchLinkWebSocket {
constructor (type) {
this._type = type;
this._onOpen = null;
this._onClose = null;
this._onError = null;
this._handleMessage = null;
this._ws = null;
}
open () {
switch (this._type) {
case 'BLE':
this._ws = new WebSocket('wss://device-manager.scratch.mit.edu:20110/scratch/ble');
break;
case 'BT':
this._ws = new WebSocket('wss://device-manager.scratch.mit.edu:20110/scratch/bt');
break;
default:
throw new Error(`Unknown ScratchLink socket Type: ${this._type}`);
}
if (this._onOpen && this._onClose && this._onError && this._handleMessage) {
this._ws.onopen = this._onOpen;
this._ws.onclose = this._onClose;
this._ws.onerror = this._onError;
} else {
throw new Error('Must set open, close, message and error handlers before calling open on the socket');
}
this._ws.onmessage = this._onMessage.bind(this);
}
close () {
this._ws.close();
this._ws = null;
}
sendMessage (message) {
const messageText = JSON.stringify(message);
this._ws.send(messageText);
}
setOnOpen (fn) {
this._onOpen = fn;
}
setOnClose (fn) {
this._onClose = fn;
}
setOnError (fn) {
this._onError = fn;
}
setHandleMessage (fn) {
this._handleMessage = fn;
}
isOpen () {
return this._ws && this._ws.readyState === this._ws.OPEN;
}
_onMessage (e) {
const json = JSON.parse(e.data);
this._handleMessage(json);
}
}
module.exports = ScratchLinkWebSocket;

View file

@ -798,6 +798,8 @@ class VirtualMachine extends EventEmitter {
sound.assetId = sound.asset.assetId;
sound.dataFormat = storage.DataFormat.WAV;
sound.md5 = `${sound.assetId}.${sound.dataFormat}`;
sound.sampleCount = newBuffer.length;
sound.rate = newBuffer.sampleRate;
}
// If soundEncoding is null, it's because gui had a problem
// encoding the updated sound. We don't want to store anything in this
@ -1528,6 +1530,14 @@ class VirtualMachine extends EventEmitter {
}
return null;
}
/**
* Allow VM consumer to configure the ScratchLink socket creator.
* @param {Function} factory The custom ScratchLink socket factory.
*/
configureScratchLinkSocketFactory (factory) {
this.runtime.configureScratchLinkSocketFactory(factory);
}
}
module.exports = VirtualMachine;

Binary file not shown.

View file

@ -64,6 +64,52 @@ test('edge activated hat thread runs once every frame', t => {
});
});
/**
* When a hat is added it should run in the next frame. Any block related
* caching should be reset.
*/
test('edge activated hat thread runs after being added to previously executed target', t => {
const vm = new VirtualMachine();
vm.attachStorage(makeTestStorage());
// Start VM, load project, and run
t.doesNotThrow(() => {
// Note: don't run vm.start(), we handle calling _step() manually in this test
vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
vm.clear();
vm.setCompatibilityMode(false);
vm.setTurboMode(false);
vm.loadProject(project).then(() => {
t.equal(vm.runtime.threads.length, 0);
vm.runtime._step();
let threads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 0);
t.equal(threads.length, 1);
checkIsHatThread(t, vm, threads[0]);
t.assert(threads[0].status === Thread.STATUS_DONE);
// Add a second hat that should create a second thread
const hatBlock = threads[0].target.blocks.getBlock(threads[0].topBlock);
threads[0].target.blocks.createBlock(Object.assign(
{}, hatBlock, {id: 'hatblock2', next: null}
));
// Check that the hat thread is added again when another step is taken
vm.runtime._step();
threads = vm.runtime._lastStepDoneThreads;
t.equal(vm.runtime.threads.length, 0);
t.equal(threads.length, 2);
checkIsHatThread(t, vm, threads[0]);
checkIsHatThread(t, vm, threads[1]);
t.assert(threads[0].status === Thread.STATUS_DONE);
t.assert(threads[1].status === Thread.STATUS_DONE);
t.end();
});
});
});
/**
* If the hat doesn't finish evaluating within one frame, it shouldn't be added again
* on the next frame. (We skip execution by setting the step time to 0)