mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-08-28 22:30:40 -04:00
Merge branch 'develop' into non-droppable-extension-menus
This commit is contained in:
commit
eba89d57d8
22 changed files with 574 additions and 320 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
78
src/engine/blocks-runtime-cache.js
Normal file
78
src/engine/blocks-runtime-cache.js
Normal 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');
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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())) {
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
58
src/io/bt.js
58
src/io/bt.js
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
84
src/util/scratch-link-websocket.js
Normal file
84
src/util/scratch-link-websocket.js
Normal 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;
|
|
@ -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;
|
||||
|
|
BIN
test/fixtures/execute/control-stop-all-leaks.sb2
vendored
Normal file
BIN
test/fixtures/execute/control-stop-all-leaks.sb2
vendored
Normal file
Binary file not shown.
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue