@scratch/scratch-vm 11.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsdoc.json +20 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +7004 -0
- package/LICENSE +12 -0
- package/README.md +120 -0
- package/TRADEMARK +1 -0
- package/commitlint.config.js +4 -0
- package/docs/extensions.md +527 -0
- package/package.json +103 -0
- package/release.config.js +13 -0
- package/src/blocks/scratch3_control.js +206 -0
- package/src/blocks/scratch3_core_example.js +69 -0
- package/src/blocks/scratch3_data.js +257 -0
- package/src/blocks/scratch3_event.js +137 -0
- package/src/blocks/scratch3_looks.js +612 -0
- package/src/blocks/scratch3_motion.js +288 -0
- package/src/blocks/scratch3_operators.js +154 -0
- package/src/blocks/scratch3_procedures.js +79 -0
- package/src/blocks/scratch3_sensing.js +336 -0
- package/src/blocks/scratch3_sound.js +350 -0
- package/src/dispatch/central-dispatch.js +143 -0
- package/src/dispatch/shared-dispatch.js +235 -0
- package/src/dispatch/worker-dispatch.js +110 -0
- package/src/engine/adapter.js +176 -0
- package/src/engine/block-utility.js +242 -0
- package/src/engine/blocks-execute-cache.js +19 -0
- package/src/engine/blocks-runtime-cache.js +78 -0
- package/src/engine/blocks.js +1299 -0
- package/src/engine/comment.js +56 -0
- package/src/engine/execute.js +580 -0
- package/src/engine/monitor-record.js +23 -0
- package/src/engine/mutation-adapter.js +48 -0
- package/src/engine/profiler.js +390 -0
- package/src/engine/runtime.js +2686 -0
- package/src/engine/scratch-blocks-constants.js +27 -0
- package/src/engine/sequencer.js +361 -0
- package/src/engine/stage-layering.js +29 -0
- package/src/engine/target.js +799 -0
- package/src/engine/thread.js +404 -0
- package/src/engine/variable.js +70 -0
- package/src/extension-support/argument-type.js +47 -0
- package/src/extension-support/block-type.js +50 -0
- package/src/extension-support/define-messages.js +18 -0
- package/src/extension-support/extension-manager.js +440 -0
- package/src/extension-support/extension-metadata.js +64 -0
- package/src/extension-support/extension-worker.js +59 -0
- package/src/extension-support/reporter-scope.js +18 -0
- package/src/extension-support/target-type.js +17 -0
- package/src/extensions/scratch3_boost/index.js +2113 -0
- package/src/extensions/scratch3_ev3/index.js +1355 -0
- package/src/extensions/scratch3_gdx_for/index.js +981 -0
- package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
- package/src/extensions/scratch3_makeymakey/index.js +396 -0
- package/src/extensions/scratch3_microbit/index.js +984 -0
- package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/index.js +1333 -0
- package/src/extensions/scratch3_music/manifest.js +63 -0
- package/src/extensions/scratch3_pen/index.js +770 -0
- package/src/extensions/scratch3_speech2text/index.js +700 -0
- package/src/extensions/scratch3_text2speech/index.js +766 -0
- package/src/extensions/scratch3_translate/index.js +286 -0
- package/src/extensions/scratch3_video_sensing/debug.js +13 -0
- package/src/extensions/scratch3_video_sensing/index.js +594 -0
- package/src/extensions/scratch3_video_sensing/library.js +384 -0
- package/src/extensions/scratch3_video_sensing/math.js +76 -0
- package/src/extensions/scratch3_video_sensing/view.js +509 -0
- package/src/extensions/scratch3_wedo2/index.js +1616 -0
- package/src/import/load-costume.js +416 -0
- package/src/import/load-sound.js +116 -0
- package/src/index.js +10 -0
- package/src/io/ble.js +256 -0
- package/src/io/bt.js +202 -0
- package/src/io/clock.js +39 -0
- package/src/io/cloud.js +169 -0
- package/src/io/keyboard.js +146 -0
- package/src/io/mouse.js +145 -0
- package/src/io/mouseWheel.js +28 -0
- package/src/io/userData.js +24 -0
- package/src/io/video.js +210 -0
- package/src/playground/benchmark.css +83 -0
- package/src/playground/benchmark.js +753 -0
- package/src/playground/index.html +111 -0
- package/src/playground/suite.css +78 -0
- package/src/playground/suite.html +25 -0
- package/src/playground/suite.js +544 -0
- package/src/playground/video-sensing.html +18 -0
- package/src/playground/video-sensing.js +133 -0
- package/src/serialization/deserialize-assets.js +178 -0
- package/src/serialization/sb2.js +1295 -0
- package/src/serialization/sb2_specmap.js +1818 -0
- package/src/serialization/sb3.js +1320 -0
- package/src/serialization/serialize-assets.js +60 -0
- package/src/sprites/rendered-target.js +1115 -0
- package/src/sprites/sprite.js +177 -0
- package/src/util/base64-util.js +48 -0
- package/src/util/cast.js +218 -0
- package/src/util/clone.js +17 -0
- package/src/util/color.js +204 -0
- package/src/util/fetch-with-timeout.js +53 -0
- package/src/util/get-monitor-id.js +33 -0
- package/src/util/jsonrpc.js +114 -0
- package/src/util/log.js +4 -0
- package/src/util/math-util.js +122 -0
- package/src/util/maybe-format-message.js +18 -0
- package/src/util/new-block-ids.js +33 -0
- package/src/util/rateLimiter.js +73 -0
- package/src/util/scratch-link-websocket.js +133 -0
- package/src/util/string-util.js +94 -0
- package/src/util/task-queue.js +203 -0
- package/src/util/timer.js +103 -0
- package/src/util/uid.js +29 -0
- package/src/util/variable-util.js +48 -0
- package/src/util/xml-escape.js +33 -0
- package/src/virtual-machine.js +1586 -0
- package/webpack.config.js +134 -0
|
@@ -0,0 +1,1616 @@
|
|
|
1
|
+
const ArgumentType = require('../../extension-support/argument-type');
|
|
2
|
+
const BlockType = require('../../extension-support/block-type');
|
|
3
|
+
const Cast = require('../../util/cast');
|
|
4
|
+
const formatMessage = require('format-message');
|
|
5
|
+
const color = require('../../util/color');
|
|
6
|
+
const BLE = require('../../io/ble');
|
|
7
|
+
const Base64Util = require('../../util/base64-util');
|
|
8
|
+
const MathUtil = require('../../util/math-util');
|
|
9
|
+
const RateLimiter = require('../../util/rateLimiter.js');
|
|
10
|
+
const log = require('../../util/log');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
|
|
14
|
+
* @type {string}
|
|
15
|
+
*/
|
|
16
|
+
// eslint-disable-next-line max-len
|
|
17
|
+
const iconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAACXBIWXMAABYlAAAWJQFJUiTwAAAF8klEQVR4Ae2cbWxTVRjH/7ctbVc2tyEMNpWBk0VIkLcEjSAQgglTE5HEaKqJi1E/mbCP/dJA0kQbvzgTQ0Ki2T7V6AeYGoEPLJmGKPiyzZDwEpYJCHSbQIcbdLvres1zOa13Xbvdu2eTDp9fst329Lnn5XfPPfece7tphmFAmDkuccdDBDIRgUxEIBMRyEQEMhGBTEQgExHIRAQyEYFMRCATEchEBDIRgUxEIBMRyEQEMhGBTEQgExHIxMPNIByNVQBoBUDb7kgo2KTS9wBoUmFNkVCwW6U3A1gP4JJKHwxHY/S+WcW2RkLBVhV7AMAOAIMAGlWstbyOSCh4QMU2Uoy1PBVL+a7IqZu1vOZIKNg20/azBarGvKxebw9HY22RULADwBFLTBcATQnZl4lVEimN4ssteXQrQfstebQpmW1q30xshyqvxRLbofYnYW9ZYgeV8C5LLOWlzbTxM3ouHI7GPgSwWx3Z0syBSBku6IYnlTbM+uQenJQaMnKHDaqAFnDrcCFbl3G1defEjas0a4N/Vz10OybyvapfrSX1sjpo+WIz0ME7QL3djgtHPTAcjb2mepw/b2ZaGh5NL5RnofR8R99dIC5fHusK5JsrCUpm7TSx21XvbcwTNwnbAsPR2GcA3qaG+H0LsHlDPZ7fca/ujZ+cRW9/Em5vCXzlNVhQUjFpf/3OTSRvXkKJz43Xt1bh1S1LUeq/5+njQ9/iVmLIfL1ieRU2b1iFtavztXNu6TrTi8PfnYI67WdPoOp5przV9Y8iuHdb9rOW9uumPI+vDIElddBckztPOqVn5X36Xj1WVQeynx1sOWbK83jc2PviM/dFXIYNax9H55leXLoyYHsfWwI14JCRRx7x5ckBU1oheYQ+1G9u39lVM0Hej7+cR7w/Yb7e9+5LqChfaLvixcK088BwNNZkAOV02ubK6+odwt3RcfOULSSPGEveG48bNj08If3kqXPmdtO6unkpDzYn0u/TLxrzcumJJ80Ut79sygzoFF6/siw75mUYupOEpmnY0/A0pw33FTsCa+hX5oJhZXgkZb5zub2O20CnL7EwkPeCPm+wI7CEBvi5wuOZ36tJW7X3uGXJXAgxk8P4eNpRPEvgskqfuR0Z/BNGejxvDM3/5gs0pboWv+motqybCc+tqUCzz43kaBJ/X+2eMjZ3ClNsjIzo5ioknXZ2b4AlkKYltLJoaY9jOJm/B0KJbtg4c4F/XOmH3+dF9dLKbBo1OD6QQGV56YQ55ODtO0jcHkZ1VSX8/n9nB9S7RkZ1rFy+NG8ZR9s70TeQQKDEh7vJUdt1Y9/OopXFB2/WcbMpyOexE9mlFS21aLlHMmKHfzBl0QT/hV2bzM9oLXv0xG8YGR0zpdLEn6RT2k+/XjDzoLX2G3u3TZBLUyral/Z5qCyAK1f/sl2/or+IWNel1Eji3MWrpjyCZHWqdNrSe6ieSHFERl4mP+q5GehgHGvvRGal5XI5uzU47f3A/R99YTgdF2wXrmkolr9ToZ5NvTjT4yOhoC2T057CJM/r9WDxoqmXa07R9THcuDVcMO8bt4ag6ynULKvkFjWBTLl0ugZKvNlyqLeSQKfYGgOpgXt2b5zVhlzrS+Dr451YvKg0b95txztxvS8xZ+VuXFuLJ5+oNgV+9c3PuHDxGs6cu+w4v//9RJo6x5bN9UgbBo4cPY1U6j+cSD8orFvzGFYuX4KxsRQGbth6FCICc9m5dY05HtN46AQRqPB5PWjY+ZT5RnMwkxGBFh5ZVmle9Z3MrGbjwfqccrC1vajrV7QCaVCfS6qrJj96nQlFK5CujPRT7MgYyEQEMhGBTGwJpAW4kJ9pBbo0zbx70X7y7AOv8HxP3LyB4YTpb2cZBt2iqL3QEwf9zDbX+waLca439QMeC7a+YBmOxugLiM/OTt2yaOoMoO+H6LOcNwf6xusrthsh/7mIh1yFmYhAJiKQiQhkIgKZiEAmIpCJCGQiApmIQCYikIkIZCICmYhAJiKQiQhkIgKZiEAmIpCJCGQiAjkA+AeOwQKMcWZqHgAAAABJRU5ErkJggg==';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A list of WeDo 2.0 BLE service UUIDs.
|
|
21
|
+
* @enum
|
|
22
|
+
*/
|
|
23
|
+
const BLEService = {
|
|
24
|
+
DEVICE_SERVICE: '00001523-1212-efde-1523-785feabcd123',
|
|
25
|
+
IO_SERVICE: '00004f0e-1212-efde-1523-785feabcd123'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A list of WeDo 2.0 BLE characteristic UUIDs.
|
|
30
|
+
*
|
|
31
|
+
* Characteristics on DEVICE_SERVICE:
|
|
32
|
+
* - ATTACHED_IO
|
|
33
|
+
*
|
|
34
|
+
* Characteristics on IO_SERVICE:
|
|
35
|
+
* - INPUT_VALUES
|
|
36
|
+
* - INPUT_COMMAND
|
|
37
|
+
* - OUTPUT_COMMAND
|
|
38
|
+
*
|
|
39
|
+
* @enum
|
|
40
|
+
*/
|
|
41
|
+
const BLECharacteristic = {
|
|
42
|
+
ATTACHED_IO: '00001527-1212-efde-1523-785feabcd123',
|
|
43
|
+
LOW_VOLTAGE_ALERT: '00001528-1212-efde-1523-785feabcd123',
|
|
44
|
+
INPUT_VALUES: '00001560-1212-efde-1523-785feabcd123',
|
|
45
|
+
INPUT_COMMAND: '00001563-1212-efde-1523-785feabcd123',
|
|
46
|
+
OUTPUT_COMMAND: '00001565-1212-efde-1523-785feabcd123'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A time interval to wait (in milliseconds) in between battery check calls.
|
|
51
|
+
* @type {number}
|
|
52
|
+
*/
|
|
53
|
+
const BLEBatteryCheckInterval = 5000;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A time interval to wait (in milliseconds) while a block that sends a BLE message is running.
|
|
57
|
+
* @type {number}
|
|
58
|
+
*/
|
|
59
|
+
const BLESendInterval = 100;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A maximum number of BLE message sends per second, to be enforced by the rate limiter.
|
|
63
|
+
* @type {number}
|
|
64
|
+
*/
|
|
65
|
+
const BLESendRateMax = 20;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Enum for WeDo 2.0 sensor and output types.
|
|
69
|
+
* @readonly
|
|
70
|
+
* @enum {number}
|
|
71
|
+
*/
|
|
72
|
+
const WeDo2Device = {
|
|
73
|
+
MOTOR: 1,
|
|
74
|
+
PIEZO: 22,
|
|
75
|
+
LED: 23,
|
|
76
|
+
TILT: 34,
|
|
77
|
+
DISTANCE: 35
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Enum for connection/port ids assigned to internal WeDo 2.0 output devices.
|
|
82
|
+
* @readonly
|
|
83
|
+
* @enum {number}
|
|
84
|
+
*/
|
|
85
|
+
// TODO: Check for these more accurately at startup?
|
|
86
|
+
const WeDo2ConnectID = {
|
|
87
|
+
LED: 6,
|
|
88
|
+
PIEZO: 5
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Enum for ids for various output commands on the WeDo 2.0.
|
|
93
|
+
* @readonly
|
|
94
|
+
* @enum {number}
|
|
95
|
+
*/
|
|
96
|
+
const WeDo2Command = {
|
|
97
|
+
MOTOR_POWER: 1,
|
|
98
|
+
PLAY_TONE: 2,
|
|
99
|
+
STOP_TONE: 3,
|
|
100
|
+
WRITE_RGB: 4,
|
|
101
|
+
SET_VOLUME: 255
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Enum for modes for input sensors on the WeDo 2.0.
|
|
106
|
+
* @enum {number}
|
|
107
|
+
*/
|
|
108
|
+
const WeDo2Mode = {
|
|
109
|
+
TILT: 0, // angle
|
|
110
|
+
DISTANCE: 0, // detect
|
|
111
|
+
LED: 1 // RGB
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Enum for units for input sensors on the WeDo 2.0.
|
|
116
|
+
*
|
|
117
|
+
* 0 = raw
|
|
118
|
+
* 1 = percent
|
|
119
|
+
*
|
|
120
|
+
* @enum {number}
|
|
121
|
+
*/
|
|
122
|
+
const WeDo2Unit = {
|
|
123
|
+
TILT: 0,
|
|
124
|
+
DISTANCE: 1,
|
|
125
|
+
LED: 0
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Manage power, direction, and timers for one WeDo 2.0 motor.
|
|
130
|
+
*/
|
|
131
|
+
class WeDo2Motor {
|
|
132
|
+
/**
|
|
133
|
+
* Construct a WeDo 2.0 Motor instance.
|
|
134
|
+
* @param {WeDo2} parent - the WeDo 2.0 peripheral which owns this motor.
|
|
135
|
+
* @param {int} index - the zero-based index of this motor on its parent peripheral.
|
|
136
|
+
*/
|
|
137
|
+
constructor (parent, index) {
|
|
138
|
+
/**
|
|
139
|
+
* The WeDo 2.0 peripheral which owns this motor.
|
|
140
|
+
* @type {WeDo2}
|
|
141
|
+
* @private
|
|
142
|
+
*/
|
|
143
|
+
this._parent = parent;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The zero-based index of this motor on its parent peripheral.
|
|
147
|
+
* @type {int}
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
this._index = index;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* This motor's current direction: 1 for "this way" or -1 for "that way"
|
|
154
|
+
* @type {number}
|
|
155
|
+
* @private
|
|
156
|
+
*/
|
|
157
|
+
this._direction = 1;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* This motor's current power level, in the range [0,100].
|
|
161
|
+
* @type {number}
|
|
162
|
+
* @private
|
|
163
|
+
*/
|
|
164
|
+
this._power = 100;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Is this motor currently moving?
|
|
168
|
+
* @type {boolean}
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
this._isOn = false;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* If the motor has been turned on or is actively braking for a specific duration, this is the timeout ID for
|
|
175
|
+
* the end-of-action handler. Cancel this when changing plans.
|
|
176
|
+
* @type {Object}
|
|
177
|
+
* @private
|
|
178
|
+
*/
|
|
179
|
+
this._pendingTimeoutId = null;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The starting time for the pending timeout.
|
|
183
|
+
* @type {Object}
|
|
184
|
+
* @private
|
|
185
|
+
*/
|
|
186
|
+
this._pendingTimeoutStartTime = null;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The delay/duration of the pending timeout.
|
|
190
|
+
* @type {Object}
|
|
191
|
+
* @private
|
|
192
|
+
*/
|
|
193
|
+
this._pendingTimeoutDelay = null;
|
|
194
|
+
|
|
195
|
+
this.startBraking = this.startBraking.bind(this);
|
|
196
|
+
this.turnOff = this.turnOff.bind(this);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @return {number} - the duration of active braking after a call to startBraking(). Afterward, turn the motor off.
|
|
201
|
+
* @constructor
|
|
202
|
+
*/
|
|
203
|
+
static get BRAKE_TIME_MS () {
|
|
204
|
+
return 1000;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @return {int} - this motor's current direction: 1 for "this way" or -1 for "that way"
|
|
209
|
+
*/
|
|
210
|
+
get direction () {
|
|
211
|
+
return this._direction;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {int} value - this motor's new direction: 1 for "this way" or -1 for "that way"
|
|
216
|
+
*/
|
|
217
|
+
set direction (value) {
|
|
218
|
+
if (value < 0) {
|
|
219
|
+
this._direction = -1;
|
|
220
|
+
} else {
|
|
221
|
+
this._direction = 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @return {int} - this motor's current power level, in the range [0,100].
|
|
227
|
+
*/
|
|
228
|
+
get power () {
|
|
229
|
+
return this._power;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param {int} value - this motor's new power level, in the range [0,100].
|
|
234
|
+
*/
|
|
235
|
+
set power (value) {
|
|
236
|
+
const p = Math.max(0, Math.min(value, 100));
|
|
237
|
+
// Lego Wedo 2.0 hub only turns motors at power range [30 - 100], so
|
|
238
|
+
// map value from [0 - 100] to [30 - 100].
|
|
239
|
+
if (p === 0) {
|
|
240
|
+
this._power = 0;
|
|
241
|
+
} else {
|
|
242
|
+
const delta = 100 / p;
|
|
243
|
+
this._power = 30 + (70 / delta);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @return {boolean} - true if this motor is currently moving, false if this motor is off or braking.
|
|
249
|
+
*/
|
|
250
|
+
get isOn () {
|
|
251
|
+
return this._isOn;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @return {boolean} - time, in milliseconds, of when the pending timeout began.
|
|
256
|
+
*/
|
|
257
|
+
get pendingTimeoutStartTime () {
|
|
258
|
+
return this._pendingTimeoutStartTime;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @return {boolean} - delay, in milliseconds, of the pending timeout.
|
|
263
|
+
*/
|
|
264
|
+
get pendingTimeoutDelay () {
|
|
265
|
+
return this._pendingTimeoutDelay;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Turn this motor on indefinitely.
|
|
270
|
+
*/
|
|
271
|
+
turnOn () {
|
|
272
|
+
if (this._power === 0) return;
|
|
273
|
+
|
|
274
|
+
const cmd = this._parent.generateOutputCommand(
|
|
275
|
+
this._index + 1,
|
|
276
|
+
WeDo2Command.MOTOR_POWER,
|
|
277
|
+
[this._power * this._direction] // power in range 0-100
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
this._parent.send(BLECharacteristic.OUTPUT_COMMAND, cmd);
|
|
281
|
+
|
|
282
|
+
this._isOn = true;
|
|
283
|
+
this._clearTimeout();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Turn this motor on for a specific duration.
|
|
288
|
+
* @param {number} milliseconds - run the motor for this long.
|
|
289
|
+
*/
|
|
290
|
+
turnOnFor (milliseconds) {
|
|
291
|
+
if (this._power === 0) return;
|
|
292
|
+
|
|
293
|
+
milliseconds = Math.max(0, milliseconds);
|
|
294
|
+
this.turnOn();
|
|
295
|
+
this._setNewTimeout(this.startBraking, milliseconds);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Start active braking on this motor. After a short time, the motor will turn off.
|
|
300
|
+
*/
|
|
301
|
+
startBraking () {
|
|
302
|
+
if (this._power === 0) return;
|
|
303
|
+
|
|
304
|
+
const cmd = this._parent.generateOutputCommand(
|
|
305
|
+
this._index + 1,
|
|
306
|
+
WeDo2Command.MOTOR_POWER,
|
|
307
|
+
[127] // 127 = break
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
this._parent.send(BLECharacteristic.OUTPUT_COMMAND, cmd);
|
|
311
|
+
|
|
312
|
+
this._isOn = false;
|
|
313
|
+
this._setNewTimeout(this.turnOff, WeDo2Motor.BRAKE_TIME_MS);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Turn this motor off.
|
|
318
|
+
* @param {boolean} [useLimiter=true] - if true, use the rate limiter
|
|
319
|
+
*/
|
|
320
|
+
turnOff (useLimiter = true) {
|
|
321
|
+
if (this._power === 0) return;
|
|
322
|
+
|
|
323
|
+
const cmd = this._parent.generateOutputCommand(
|
|
324
|
+
this._index + 1,
|
|
325
|
+
WeDo2Command.MOTOR_POWER,
|
|
326
|
+
[0] // 0 = stop
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
this._parent.send(BLECharacteristic.OUTPUT_COMMAND, cmd, useLimiter);
|
|
330
|
+
|
|
331
|
+
this._isOn = false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Clear the motor action timeout, if any. Safe to call even when there is no pending timeout.
|
|
336
|
+
* @private
|
|
337
|
+
*/
|
|
338
|
+
_clearTimeout () {
|
|
339
|
+
if (this._pendingTimeoutId !== null) {
|
|
340
|
+
clearTimeout(this._pendingTimeoutId);
|
|
341
|
+
this._pendingTimeoutId = null;
|
|
342
|
+
this._pendingTimeoutStartTime = null;
|
|
343
|
+
this._pendingTimeoutDelay = null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Set a new motor action timeout, after clearing an existing one if necessary.
|
|
349
|
+
* @param {Function} callback - to be called at the end of the timeout.
|
|
350
|
+
* @param {int} delay - wait this many milliseconds before calling the callback.
|
|
351
|
+
* @private
|
|
352
|
+
*/
|
|
353
|
+
_setNewTimeout (callback, delay) {
|
|
354
|
+
this._clearTimeout();
|
|
355
|
+
const timeoutID = setTimeout(() => {
|
|
356
|
+
if (this._pendingTimeoutId === timeoutID) {
|
|
357
|
+
this._pendingTimeoutId = null;
|
|
358
|
+
this._pendingTimeoutStartTime = null;
|
|
359
|
+
this._pendingTimeoutDelay = null;
|
|
360
|
+
}
|
|
361
|
+
callback();
|
|
362
|
+
}, delay);
|
|
363
|
+
this._pendingTimeoutId = timeoutID;
|
|
364
|
+
this._pendingTimeoutStartTime = Date.now();
|
|
365
|
+
this._pendingTimeoutDelay = delay;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Manage communication with a WeDo 2.0 peripheral over a Bluetooth Low Energy client socket.
|
|
371
|
+
*/
|
|
372
|
+
class WeDo2 {
|
|
373
|
+
|
|
374
|
+
constructor (runtime, extensionId) {
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The Scratch 3.0 runtime used to trigger the green flag button.
|
|
378
|
+
* @type {Runtime}
|
|
379
|
+
* @private
|
|
380
|
+
*/
|
|
381
|
+
this._runtime = runtime;
|
|
382
|
+
this._runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this));
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The id of the extension this peripheral belongs to.
|
|
386
|
+
*/
|
|
387
|
+
this._extensionId = extensionId;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* A list of the ids of the motors or sensors in ports 1 and 2.
|
|
391
|
+
* @type {string[]}
|
|
392
|
+
* @private
|
|
393
|
+
*/
|
|
394
|
+
this._ports = ['none', 'none'];
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The motors which this WeDo 2.0 could possibly have.
|
|
398
|
+
* @type {WeDo2Motor[]}
|
|
399
|
+
* @private
|
|
400
|
+
*/
|
|
401
|
+
this._motors = [null, null];
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The most recently received value for each sensor.
|
|
405
|
+
* @type {Object.<string, number>}
|
|
406
|
+
* @private
|
|
407
|
+
*/
|
|
408
|
+
this._sensors = {
|
|
409
|
+
tiltX: 0,
|
|
410
|
+
tiltY: 0,
|
|
411
|
+
distance: 0
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The Bluetooth connection socket for reading/writing peripheral data.
|
|
416
|
+
* @type {BLE}
|
|
417
|
+
* @private
|
|
418
|
+
*/
|
|
419
|
+
this._ble = null;
|
|
420
|
+
this._runtime.registerPeripheralExtension(extensionId, this);
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* A rate limiter utility, to help limit the rate at which we send BLE messages
|
|
424
|
+
* over the socket to Scratch Link to a maximum number of sends per second.
|
|
425
|
+
* @type {RateLimiter}
|
|
426
|
+
* @private
|
|
427
|
+
*/
|
|
428
|
+
this._rateLimiter = new RateLimiter(BLESendRateMax);
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* An interval id for the battery check interval.
|
|
432
|
+
* @type {number}
|
|
433
|
+
* @private
|
|
434
|
+
*/
|
|
435
|
+
this._batteryLevelIntervalId = null;
|
|
436
|
+
|
|
437
|
+
this.reset = this.reset.bind(this);
|
|
438
|
+
this._onConnect = this._onConnect.bind(this);
|
|
439
|
+
this._onMessage = this._onMessage.bind(this);
|
|
440
|
+
this._checkBatteryLevel = this._checkBatteryLevel.bind(this);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @return {number} - the latest value received for the tilt sensor's tilt about the X axis.
|
|
445
|
+
*/
|
|
446
|
+
get tiltX () {
|
|
447
|
+
return this._sensors.tiltX;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* @return {number} - the latest value received for the tilt sensor's tilt about the Y axis.
|
|
452
|
+
*/
|
|
453
|
+
get tiltY () {
|
|
454
|
+
return this._sensors.tiltY;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* @return {number} - the latest value received from the distance sensor.
|
|
459
|
+
*/
|
|
460
|
+
get distance () {
|
|
461
|
+
return this._sensors.distance;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Access a particular motor on this peripheral.
|
|
466
|
+
* @param {int} index - the zero-based index of the desired motor.
|
|
467
|
+
* @return {WeDo2Motor} - the WeDo2Motor instance, if any, at that index.
|
|
468
|
+
*/
|
|
469
|
+
motor (index) {
|
|
470
|
+
return this._motors[index];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Stop all the motors that are currently running.
|
|
475
|
+
*/
|
|
476
|
+
stopAllMotors () {
|
|
477
|
+
this._motors.forEach(motor => {
|
|
478
|
+
if (motor) {
|
|
479
|
+
// Send the motor off command without using the rate limiter.
|
|
480
|
+
// This allows the stop button to stop motors even if we are
|
|
481
|
+
// otherwise flooded with commands.
|
|
482
|
+
motor.turnOff(false);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Set the WeDo 2.0 peripheral's LED to a specific color.
|
|
489
|
+
* @param {int} inputRGB - a 24-bit RGB color in 0xRRGGBB format.
|
|
490
|
+
* @return {Promise} - a promise of the completion of the set led send operation.
|
|
491
|
+
*/
|
|
492
|
+
setLED (inputRGB) {
|
|
493
|
+
const rgb = [
|
|
494
|
+
(inputRGB >> 16) & 0x000000FF,
|
|
495
|
+
(inputRGB >> 8) & 0x000000FF,
|
|
496
|
+
(inputRGB) & 0x000000FF
|
|
497
|
+
];
|
|
498
|
+
|
|
499
|
+
const cmd = this.generateOutputCommand(
|
|
500
|
+
WeDo2ConnectID.LED,
|
|
501
|
+
WeDo2Command.WRITE_RGB,
|
|
502
|
+
rgb
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
return this.send(BLECharacteristic.OUTPUT_COMMAND, cmd);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Sets the input mode of the LED to RGB.
|
|
510
|
+
* @return {Promise} - a promise returned by the send operation.
|
|
511
|
+
*/
|
|
512
|
+
setLEDMode () {
|
|
513
|
+
const cmd = this.generateInputCommand(
|
|
514
|
+
WeDo2ConnectID.LED,
|
|
515
|
+
WeDo2Device.LED,
|
|
516
|
+
WeDo2Mode.LED,
|
|
517
|
+
0,
|
|
518
|
+
WeDo2Unit.LED,
|
|
519
|
+
false
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
return this.send(BLECharacteristic.INPUT_COMMAND, cmd);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Switch off the LED on the WeDo 2.0.
|
|
527
|
+
* @return {Promise} - a promise of the completion of the stop led send operation.
|
|
528
|
+
*/
|
|
529
|
+
stopLED () {
|
|
530
|
+
const cmd = this.generateOutputCommand(
|
|
531
|
+
WeDo2ConnectID.LED,
|
|
532
|
+
WeDo2Command.WRITE_RGB,
|
|
533
|
+
[0, 0, 0]
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
return this.send(BLECharacteristic.OUTPUT_COMMAND, cmd);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Play a tone from the WeDo 2.0 peripheral for a specific amount of time.
|
|
541
|
+
* @param {int} tone - the pitch of the tone, in Hz.
|
|
542
|
+
* @param {int} milliseconds - the duration of the note, in milliseconds.
|
|
543
|
+
* @return {Promise} - a promise of the completion of the play tone send operation.
|
|
544
|
+
*/
|
|
545
|
+
playTone (tone, milliseconds) {
|
|
546
|
+
const cmd = this.generateOutputCommand(
|
|
547
|
+
WeDo2ConnectID.PIEZO,
|
|
548
|
+
WeDo2Command.PLAY_TONE,
|
|
549
|
+
[
|
|
550
|
+
tone,
|
|
551
|
+
tone >> 8,
|
|
552
|
+
milliseconds,
|
|
553
|
+
milliseconds >> 8
|
|
554
|
+
]
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
return this.send(BLECharacteristic.OUTPUT_COMMAND, cmd);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Stop the tone playing from the WeDo 2.0 peripheral, if any.
|
|
562
|
+
* @return {Promise} - a promise that the command sent.
|
|
563
|
+
*/
|
|
564
|
+
stopTone () {
|
|
565
|
+
const cmd = this.generateOutputCommand(
|
|
566
|
+
WeDo2ConnectID.PIEZO,
|
|
567
|
+
WeDo2Command.STOP_TONE
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
// Send this command without using the rate limiter, because it is
|
|
571
|
+
// only triggered by the stop button.
|
|
572
|
+
return this.send(BLECharacteristic.OUTPUT_COMMAND, cmd, false);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Stop the tone playing and motors on the WeDo 2.0 peripheral.
|
|
577
|
+
*/
|
|
578
|
+
stopAll () {
|
|
579
|
+
if (!this.isConnected()) return;
|
|
580
|
+
this.stopTone();
|
|
581
|
+
this.stopAllMotors();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Called by the runtime when user wants to scan for a WeDo 2.0 peripheral.
|
|
586
|
+
*/
|
|
587
|
+
scan () {
|
|
588
|
+
if (this._ble) {
|
|
589
|
+
this._ble.disconnect();
|
|
590
|
+
}
|
|
591
|
+
this._ble = new BLE(this._runtime, this._extensionId, {
|
|
592
|
+
filters: [{
|
|
593
|
+
services: [BLEService.DEVICE_SERVICE]
|
|
594
|
+
}],
|
|
595
|
+
optionalServices: [BLEService.IO_SERVICE]
|
|
596
|
+
}, this._onConnect, this.reset);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Called by the runtime when user wants to connect to a certain WeDo 2.0 peripheral.
|
|
601
|
+
* @param {number} id - the id of the peripheral to connect to.
|
|
602
|
+
*/
|
|
603
|
+
connect (id) {
|
|
604
|
+
if (this._ble) {
|
|
605
|
+
this._ble.connectPeripheral(id);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Disconnects from the current BLE socket.
|
|
611
|
+
*/
|
|
612
|
+
disconnect () {
|
|
613
|
+
if (this._ble) {
|
|
614
|
+
this._ble.disconnect();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
this.reset();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Reset all the state and timeout/interval ids.
|
|
622
|
+
*/
|
|
623
|
+
reset () {
|
|
624
|
+
this._ports = ['none', 'none'];
|
|
625
|
+
this._motors = [null, null];
|
|
626
|
+
this._sensors = {
|
|
627
|
+
tiltX: 0,
|
|
628
|
+
tiltY: 0,
|
|
629
|
+
distance: 0
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
if (this._batteryLevelIntervalId) {
|
|
633
|
+
window.clearInterval(this._batteryLevelIntervalId);
|
|
634
|
+
this._batteryLevelIntervalId = null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Called by the runtime to detect whether the WeDo 2.0 peripheral is connected.
|
|
640
|
+
* @return {boolean} - the connected state.
|
|
641
|
+
*/
|
|
642
|
+
isConnected () {
|
|
643
|
+
let connected = false;
|
|
644
|
+
if (this._ble) {
|
|
645
|
+
connected = this._ble.isConnected();
|
|
646
|
+
}
|
|
647
|
+
return connected;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Write a message to the WeDo 2.0 peripheral BLE socket.
|
|
652
|
+
* @param {number} uuid - the UUID of the characteristic to write to
|
|
653
|
+
* @param {Array} message - the message to write.
|
|
654
|
+
* @param {boolean} [useLimiter=true] - if true, use the rate limiter
|
|
655
|
+
* @return {Promise} - a promise result of the write operation
|
|
656
|
+
*/
|
|
657
|
+
send (uuid, message, useLimiter = true) {
|
|
658
|
+
if (!this.isConnected()) return Promise.resolve();
|
|
659
|
+
|
|
660
|
+
if (useLimiter) {
|
|
661
|
+
if (!this._rateLimiter.okayToSend()) return Promise.resolve();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return this._ble.write(
|
|
665
|
+
BLEService.IO_SERVICE,
|
|
666
|
+
uuid,
|
|
667
|
+
Base64Util.uint8ArrayToBase64(message),
|
|
668
|
+
'base64'
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Generate a WeDo 2.0 'Output Command' in the byte array format
|
|
674
|
+
* (CONNECT ID, COMMAND ID, NUMBER OF BYTES, VALUES ...).
|
|
675
|
+
*
|
|
676
|
+
* This sends a command to the WeDo 2.0 to actuate the specified outputs.
|
|
677
|
+
*
|
|
678
|
+
* @param {number} connectID - the port (Connect ID) to send a command to.
|
|
679
|
+
* @param {number} commandID - the id of the byte command.
|
|
680
|
+
* @param {array} values - the list of values to write to the command.
|
|
681
|
+
* @return {array} - a generated output command.
|
|
682
|
+
*/
|
|
683
|
+
generateOutputCommand (connectID, commandID, values = null) {
|
|
684
|
+
let command = [connectID, commandID];
|
|
685
|
+
if (values) {
|
|
686
|
+
command = command.concat(
|
|
687
|
+
values.length
|
|
688
|
+
).concat(
|
|
689
|
+
values
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
return command;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Generate a WeDo 2.0 'Input Command' in the byte array format
|
|
697
|
+
* (COMMAND ID, COMMAND TYPE, CONNECT ID, TYPE ID, MODE, DELTA INTERVAL (4 BYTES),
|
|
698
|
+
* UNIT, NOTIFICATIONS ENABLED).
|
|
699
|
+
*
|
|
700
|
+
* This sends a command to the WeDo 2.0 that sets that input format
|
|
701
|
+
* of the specified inputs and sets value change notifications.
|
|
702
|
+
*
|
|
703
|
+
* @param {number} connectID - the port (Connect ID) to send a command to.
|
|
704
|
+
* @param {number} type - the type of input sensor.
|
|
705
|
+
* @param {number} mode - the mode of the input sensor.
|
|
706
|
+
* @param {number} delta - the delta change needed to trigger notification.
|
|
707
|
+
* @param {array} units - the unit of the input sensor value.
|
|
708
|
+
* @param {boolean} enableNotifications - whether to enable notifications.
|
|
709
|
+
* @return {array} - a generated input command.
|
|
710
|
+
*/
|
|
711
|
+
generateInputCommand (connectID, type, mode, delta, units, enableNotifications) {
|
|
712
|
+
const command = [
|
|
713
|
+
1, // Command ID = 1 = "Sensor Format"
|
|
714
|
+
2, // Command Type = 2 = "Write"
|
|
715
|
+
connectID,
|
|
716
|
+
type,
|
|
717
|
+
mode,
|
|
718
|
+
delta,
|
|
719
|
+
0, // Delta Interval Byte 2
|
|
720
|
+
0, // Delta Interval Byte 3
|
|
721
|
+
0, // Delta Interval Byte 4
|
|
722
|
+
units,
|
|
723
|
+
enableNotifications ? 1 : 0
|
|
724
|
+
];
|
|
725
|
+
|
|
726
|
+
return command;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Sets LED mode and initial color and starts reading data from peripheral after BLE has connected.
|
|
731
|
+
* @private
|
|
732
|
+
*/
|
|
733
|
+
_onConnect () {
|
|
734
|
+
this.setLEDMode();
|
|
735
|
+
this.setLED(0x0000FF);
|
|
736
|
+
this._ble.startNotifications(
|
|
737
|
+
BLEService.DEVICE_SERVICE,
|
|
738
|
+
BLECharacteristic.ATTACHED_IO,
|
|
739
|
+
this._onMessage
|
|
740
|
+
);
|
|
741
|
+
this._batteryLevelIntervalId = window.setInterval(this._checkBatteryLevel, BLEBatteryCheckInterval);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Process the sensor data from the incoming BLE characteristic.
|
|
746
|
+
* @param {object} base64 - the incoming BLE data.
|
|
747
|
+
* @private
|
|
748
|
+
*/
|
|
749
|
+
_onMessage (base64) {
|
|
750
|
+
const data = Base64Util.base64ToUint8Array(base64);
|
|
751
|
+
// log.info(data);
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* If first byte of data is '1' or '2', then either clear the
|
|
755
|
+
* sensor present in ports 1 or 2 or set their format.
|
|
756
|
+
*
|
|
757
|
+
* If first byte of data is anything else, read incoming sensor value.
|
|
758
|
+
*/
|
|
759
|
+
switch (data[0]) {
|
|
760
|
+
case 1:
|
|
761
|
+
case 2: {
|
|
762
|
+
const connectID = data[0];
|
|
763
|
+
if (data[1] === 0) {
|
|
764
|
+
// clear sensor or motor
|
|
765
|
+
this._clearPort(connectID);
|
|
766
|
+
} else {
|
|
767
|
+
// register sensor or motor
|
|
768
|
+
this._registerSensorOrMotor(connectID, data[3]);
|
|
769
|
+
}
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
default: {
|
|
773
|
+
// read incoming sensor value
|
|
774
|
+
const connectID = data[1];
|
|
775
|
+
const type = this._ports[connectID - 1];
|
|
776
|
+
if (type === WeDo2Device.DISTANCE) {
|
|
777
|
+
this._sensors.distance = data[2];
|
|
778
|
+
}
|
|
779
|
+
if (type === WeDo2Device.TILT) {
|
|
780
|
+
this._sensors.tiltX = data[2];
|
|
781
|
+
this._sensors.tiltY = data[3];
|
|
782
|
+
}
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Check the battery level on the WeDo 2.0. If the WeDo 2.0 has disconnected
|
|
790
|
+
* for some reason, the BLE socket will get an error back and automatically
|
|
791
|
+
* close the socket.
|
|
792
|
+
*/
|
|
793
|
+
_checkBatteryLevel () {
|
|
794
|
+
this._ble.read(
|
|
795
|
+
BLEService.DEVICE_SERVICE,
|
|
796
|
+
BLECharacteristic.LOW_VOLTAGE_ALERT,
|
|
797
|
+
false
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Register a new sensor or motor connected at a port. Store the type of
|
|
803
|
+
* sensor or motor internally, and then register for notifications on input
|
|
804
|
+
* values if it is a sensor.
|
|
805
|
+
* @param {number} connectID - the port to register a sensor or motor on.
|
|
806
|
+
* @param {number} type - the type ID of the sensor or motor
|
|
807
|
+
* @private
|
|
808
|
+
*/
|
|
809
|
+
_registerSensorOrMotor (connectID, type) {
|
|
810
|
+
// Record which port is connected to what type of device
|
|
811
|
+
this._ports[connectID - 1] = type;
|
|
812
|
+
|
|
813
|
+
// Record motor port
|
|
814
|
+
if (type === WeDo2Device.MOTOR) {
|
|
815
|
+
this._motors[connectID - 1] = new WeDo2Motor(this, connectID - 1);
|
|
816
|
+
} else {
|
|
817
|
+
// Set input format for tilt or distance sensor
|
|
818
|
+
const typeString = type === WeDo2Device.DISTANCE ? 'DISTANCE' : 'TILT';
|
|
819
|
+
const cmd = this.generateInputCommand(
|
|
820
|
+
connectID,
|
|
821
|
+
type,
|
|
822
|
+
WeDo2Mode[typeString],
|
|
823
|
+
1,
|
|
824
|
+
WeDo2Unit[typeString],
|
|
825
|
+
true
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
this.send(BLECharacteristic.INPUT_COMMAND, cmd);
|
|
829
|
+
this._ble.startNotifications(
|
|
830
|
+
BLEService.IO_SERVICE,
|
|
831
|
+
BLECharacteristic.INPUT_VALUES,
|
|
832
|
+
this._onMessage
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Clear the sensor or motor present at port 1 or 2.
|
|
839
|
+
* @param {number} connectID - the port to clear.
|
|
840
|
+
* @private
|
|
841
|
+
*/
|
|
842
|
+
_clearPort (connectID) {
|
|
843
|
+
const type = this._ports[connectID - 1];
|
|
844
|
+
if (type === WeDo2Device.TILT) {
|
|
845
|
+
this._sensors.tiltX = this._sensors.tiltY = 0;
|
|
846
|
+
}
|
|
847
|
+
if (type === WeDo2Device.DISTANCE) {
|
|
848
|
+
this._sensors.distance = 0;
|
|
849
|
+
}
|
|
850
|
+
this._ports[connectID - 1] = 'none';
|
|
851
|
+
this._motors[connectID - 1] = null;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Enum for motor specification.
|
|
857
|
+
* @readonly
|
|
858
|
+
* @enum {string}
|
|
859
|
+
*/
|
|
860
|
+
const WeDo2MotorLabel = {
|
|
861
|
+
DEFAULT: 'motor',
|
|
862
|
+
A: 'motor A',
|
|
863
|
+
B: 'motor B',
|
|
864
|
+
ALL: 'all motors'
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Enum for motor direction specification.
|
|
869
|
+
* @readonly
|
|
870
|
+
* @enum {string}
|
|
871
|
+
*/
|
|
872
|
+
const WeDo2MotorDirection = {
|
|
873
|
+
FORWARD: 'this way',
|
|
874
|
+
BACKWARD: 'that way',
|
|
875
|
+
REVERSE: 'reverse'
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Enum for tilt sensor direction.
|
|
880
|
+
* @readonly
|
|
881
|
+
* @enum {string}
|
|
882
|
+
*/
|
|
883
|
+
const WeDo2TiltDirection = {
|
|
884
|
+
UP: 'up',
|
|
885
|
+
DOWN: 'down',
|
|
886
|
+
LEFT: 'left',
|
|
887
|
+
RIGHT: 'right',
|
|
888
|
+
ANY: 'any'
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Scratch 3.0 blocks to interact with a LEGO WeDo 2.0 peripheral.
|
|
893
|
+
*/
|
|
894
|
+
class Scratch3WeDo2Blocks {
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* @return {string} - the ID of this extension.
|
|
898
|
+
*/
|
|
899
|
+
static get EXTENSION_ID () {
|
|
900
|
+
return 'wedo2';
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* @return {number} - the tilt sensor counts as "tilted" if its tilt angle meets or exceeds this threshold.
|
|
905
|
+
*/
|
|
906
|
+
static get TILT_THRESHOLD () {
|
|
907
|
+
return 15;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Construct a set of WeDo 2.0 blocks.
|
|
912
|
+
* @param {Runtime} runtime - the Scratch 3.0 runtime.
|
|
913
|
+
*/
|
|
914
|
+
constructor (runtime) {
|
|
915
|
+
/**
|
|
916
|
+
* The Scratch 3.0 runtime.
|
|
917
|
+
* @type {Runtime}
|
|
918
|
+
*/
|
|
919
|
+
this.runtime = runtime;
|
|
920
|
+
|
|
921
|
+
// Create a new WeDo 2.0 peripheral instance
|
|
922
|
+
this._peripheral = new WeDo2(this.runtime, Scratch3WeDo2Blocks.EXTENSION_ID);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* @returns {object} metadata for this extension and its blocks.
|
|
927
|
+
*/
|
|
928
|
+
getInfo () {
|
|
929
|
+
return {
|
|
930
|
+
id: Scratch3WeDo2Blocks.EXTENSION_ID,
|
|
931
|
+
name: 'WeDo 2.0',
|
|
932
|
+
blockIconURI: iconURI,
|
|
933
|
+
showStatusButton: true,
|
|
934
|
+
blocks: [
|
|
935
|
+
{
|
|
936
|
+
opcode: 'motorOnFor',
|
|
937
|
+
text: formatMessage({
|
|
938
|
+
id: 'wedo2.motorOnFor',
|
|
939
|
+
default: 'turn [MOTOR_ID] on for [DURATION] seconds',
|
|
940
|
+
description: 'turn a motor on for some time'
|
|
941
|
+
}),
|
|
942
|
+
blockType: BlockType.COMMAND,
|
|
943
|
+
arguments: {
|
|
944
|
+
MOTOR_ID: {
|
|
945
|
+
type: ArgumentType.STRING,
|
|
946
|
+
menu: 'MOTOR_ID',
|
|
947
|
+
defaultValue: WeDo2MotorLabel.DEFAULT
|
|
948
|
+
},
|
|
949
|
+
DURATION: {
|
|
950
|
+
type: ArgumentType.NUMBER,
|
|
951
|
+
defaultValue: 1
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
opcode: 'motorOn',
|
|
957
|
+
text: formatMessage({
|
|
958
|
+
id: 'wedo2.motorOn',
|
|
959
|
+
default: 'turn [MOTOR_ID] on',
|
|
960
|
+
description: 'turn a motor on indefinitely'
|
|
961
|
+
}),
|
|
962
|
+
blockType: BlockType.COMMAND,
|
|
963
|
+
arguments: {
|
|
964
|
+
MOTOR_ID: {
|
|
965
|
+
type: ArgumentType.STRING,
|
|
966
|
+
menu: 'MOTOR_ID',
|
|
967
|
+
defaultValue: WeDo2MotorLabel.DEFAULT
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
opcode: 'motorOff',
|
|
973
|
+
text: formatMessage({
|
|
974
|
+
id: 'wedo2.motorOff',
|
|
975
|
+
default: 'turn [MOTOR_ID] off',
|
|
976
|
+
description: 'turn a motor off'
|
|
977
|
+
}),
|
|
978
|
+
blockType: BlockType.COMMAND,
|
|
979
|
+
arguments: {
|
|
980
|
+
MOTOR_ID: {
|
|
981
|
+
type: ArgumentType.STRING,
|
|
982
|
+
menu: 'MOTOR_ID',
|
|
983
|
+
defaultValue: WeDo2MotorLabel.DEFAULT
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
opcode: 'startMotorPower',
|
|
989
|
+
text: formatMessage({
|
|
990
|
+
id: 'wedo2.startMotorPower',
|
|
991
|
+
default: 'set [MOTOR_ID] power to [POWER]',
|
|
992
|
+
description: 'set the motor\'s power and turn it on'
|
|
993
|
+
}),
|
|
994
|
+
blockType: BlockType.COMMAND,
|
|
995
|
+
arguments: {
|
|
996
|
+
MOTOR_ID: {
|
|
997
|
+
type: ArgumentType.STRING,
|
|
998
|
+
menu: 'MOTOR_ID',
|
|
999
|
+
defaultValue: WeDo2MotorLabel.DEFAULT
|
|
1000
|
+
},
|
|
1001
|
+
POWER: {
|
|
1002
|
+
type: ArgumentType.NUMBER,
|
|
1003
|
+
defaultValue: 100
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
opcode: 'setMotorDirection',
|
|
1009
|
+
text: formatMessage({
|
|
1010
|
+
id: 'wedo2.setMotorDirection',
|
|
1011
|
+
default: 'set [MOTOR_ID] direction to [MOTOR_DIRECTION]',
|
|
1012
|
+
description: 'set the motor\'s turn direction'
|
|
1013
|
+
}),
|
|
1014
|
+
blockType: BlockType.COMMAND,
|
|
1015
|
+
arguments: {
|
|
1016
|
+
MOTOR_ID: {
|
|
1017
|
+
type: ArgumentType.STRING,
|
|
1018
|
+
menu: 'MOTOR_ID',
|
|
1019
|
+
defaultValue: WeDo2MotorLabel.DEFAULT
|
|
1020
|
+
},
|
|
1021
|
+
MOTOR_DIRECTION: {
|
|
1022
|
+
type: ArgumentType.STRING,
|
|
1023
|
+
menu: 'MOTOR_DIRECTION',
|
|
1024
|
+
defaultValue: WeDo2MotorDirection.FORWARD
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
opcode: 'setLightHue',
|
|
1030
|
+
text: formatMessage({
|
|
1031
|
+
id: 'wedo2.setLightHue',
|
|
1032
|
+
default: 'set light color to [HUE]',
|
|
1033
|
+
description: 'set the LED color'
|
|
1034
|
+
}),
|
|
1035
|
+
blockType: BlockType.COMMAND,
|
|
1036
|
+
arguments: {
|
|
1037
|
+
HUE: {
|
|
1038
|
+
type: ArgumentType.NUMBER,
|
|
1039
|
+
defaultValue: 50
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
opcode: 'playNoteFor',
|
|
1045
|
+
text: formatMessage({
|
|
1046
|
+
id: 'wedo2.playNoteFor',
|
|
1047
|
+
default: 'play note [NOTE] for [DURATION] seconds',
|
|
1048
|
+
description: 'play a certain note for some time'
|
|
1049
|
+
}),
|
|
1050
|
+
blockType: BlockType.COMMAND,
|
|
1051
|
+
arguments: {
|
|
1052
|
+
NOTE: {
|
|
1053
|
+
type: ArgumentType.NUMBER, // TODO: ArgumentType.MIDI_NOTE?
|
|
1054
|
+
defaultValue: 60
|
|
1055
|
+
},
|
|
1056
|
+
DURATION: {
|
|
1057
|
+
type: ArgumentType.NUMBER,
|
|
1058
|
+
defaultValue: 0.5
|
|
1059
|
+
}
|
|
1060
|
+
},
|
|
1061
|
+
hideFromPalette: true
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
opcode: 'whenDistance',
|
|
1065
|
+
text: formatMessage({
|
|
1066
|
+
id: 'wedo2.whenDistance',
|
|
1067
|
+
default: 'when distance [OP] [REFERENCE]',
|
|
1068
|
+
description: 'check for when distance is < or > than reference'
|
|
1069
|
+
}),
|
|
1070
|
+
blockType: BlockType.HAT,
|
|
1071
|
+
arguments: {
|
|
1072
|
+
OP: {
|
|
1073
|
+
type: ArgumentType.STRING,
|
|
1074
|
+
menu: 'OP',
|
|
1075
|
+
defaultValue: '<'
|
|
1076
|
+
},
|
|
1077
|
+
REFERENCE: {
|
|
1078
|
+
type: ArgumentType.NUMBER,
|
|
1079
|
+
defaultValue: 50
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
opcode: 'whenTilted',
|
|
1085
|
+
text: formatMessage({
|
|
1086
|
+
id: 'wedo2.whenTilted',
|
|
1087
|
+
default: 'when tilted [TILT_DIRECTION_ANY]',
|
|
1088
|
+
description: 'check when tilted in a certain direction'
|
|
1089
|
+
}),
|
|
1090
|
+
func: 'isTilted',
|
|
1091
|
+
blockType: BlockType.HAT,
|
|
1092
|
+
arguments: {
|
|
1093
|
+
TILT_DIRECTION_ANY: {
|
|
1094
|
+
type: ArgumentType.STRING,
|
|
1095
|
+
menu: 'TILT_DIRECTION_ANY',
|
|
1096
|
+
defaultValue: WeDo2TiltDirection.ANY
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
opcode: 'getDistance',
|
|
1102
|
+
text: formatMessage({
|
|
1103
|
+
id: 'wedo2.getDistance',
|
|
1104
|
+
default: 'distance',
|
|
1105
|
+
description: 'the value returned by the distance sensor'
|
|
1106
|
+
}),
|
|
1107
|
+
blockType: BlockType.REPORTER
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
opcode: 'isTilted',
|
|
1111
|
+
text: formatMessage({
|
|
1112
|
+
id: 'wedo2.isTilted',
|
|
1113
|
+
default: 'tilted [TILT_DIRECTION_ANY]?',
|
|
1114
|
+
description: 'whether the tilt sensor is tilted'
|
|
1115
|
+
}),
|
|
1116
|
+
blockType: BlockType.BOOLEAN,
|
|
1117
|
+
arguments: {
|
|
1118
|
+
TILT_DIRECTION_ANY: {
|
|
1119
|
+
type: ArgumentType.STRING,
|
|
1120
|
+
menu: 'TILT_DIRECTION_ANY',
|
|
1121
|
+
defaultValue: WeDo2TiltDirection.ANY
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
opcode: 'getTiltAngle',
|
|
1127
|
+
text: formatMessage({
|
|
1128
|
+
id: 'wedo2.getTiltAngle',
|
|
1129
|
+
default: 'tilt angle [TILT_DIRECTION]',
|
|
1130
|
+
description: 'the angle returned by the tilt sensor'
|
|
1131
|
+
}),
|
|
1132
|
+
blockType: BlockType.REPORTER,
|
|
1133
|
+
arguments: {
|
|
1134
|
+
TILT_DIRECTION: {
|
|
1135
|
+
type: ArgumentType.STRING,
|
|
1136
|
+
menu: 'TILT_DIRECTION',
|
|
1137
|
+
defaultValue: WeDo2TiltDirection.UP
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
],
|
|
1142
|
+
menus: {
|
|
1143
|
+
MOTOR_ID: {
|
|
1144
|
+
acceptReporters: true,
|
|
1145
|
+
items: [
|
|
1146
|
+
{
|
|
1147
|
+
text: formatMessage({
|
|
1148
|
+
id: 'wedo2.motorId.default',
|
|
1149
|
+
default: 'motor',
|
|
1150
|
+
description: 'label for motor element in motor menu for LEGO WeDo 2 extension'
|
|
1151
|
+
}),
|
|
1152
|
+
value: WeDo2MotorLabel.DEFAULT
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
text: formatMessage({
|
|
1156
|
+
id: 'wedo2.motorId.a',
|
|
1157
|
+
default: 'motor A',
|
|
1158
|
+
description: 'label for motor A element in motor menu for LEGO WeDo 2 extension'
|
|
1159
|
+
}),
|
|
1160
|
+
value: WeDo2MotorLabel.A
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
text: formatMessage({
|
|
1164
|
+
id: 'wedo2.motorId.b',
|
|
1165
|
+
default: 'motor B',
|
|
1166
|
+
description: 'label for motor B element in motor menu for LEGO WeDo 2 extension'
|
|
1167
|
+
}),
|
|
1168
|
+
value: WeDo2MotorLabel.B
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
text: formatMessage({
|
|
1172
|
+
id: 'wedo2.motorId.all',
|
|
1173
|
+
default: 'all motors',
|
|
1174
|
+
description: 'label for all motors element in motor menu for LEGO WeDo 2 extension'
|
|
1175
|
+
}),
|
|
1176
|
+
value: WeDo2MotorLabel.ALL
|
|
1177
|
+
}
|
|
1178
|
+
]
|
|
1179
|
+
},
|
|
1180
|
+
MOTOR_DIRECTION: {
|
|
1181
|
+
acceptReporters: true,
|
|
1182
|
+
items: [
|
|
1183
|
+
{
|
|
1184
|
+
text: formatMessage({
|
|
1185
|
+
id: 'wedo2.motorDirection.forward',
|
|
1186
|
+
default: 'this way',
|
|
1187
|
+
description:
|
|
1188
|
+
'label for forward element in motor direction menu for LEGO WeDo 2 extension'
|
|
1189
|
+
}),
|
|
1190
|
+
value: WeDo2MotorDirection.FORWARD
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
text: formatMessage({
|
|
1194
|
+
id: 'wedo2.motorDirection.backward',
|
|
1195
|
+
default: 'that way',
|
|
1196
|
+
description:
|
|
1197
|
+
'label for backward element in motor direction menu for LEGO WeDo 2 extension'
|
|
1198
|
+
}),
|
|
1199
|
+
value: WeDo2MotorDirection.BACKWARD
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
text: formatMessage({
|
|
1203
|
+
id: 'wedo2.motorDirection.reverse',
|
|
1204
|
+
default: 'reverse',
|
|
1205
|
+
description:
|
|
1206
|
+
'label for reverse element in motor direction menu for LEGO WeDo 2 extension'
|
|
1207
|
+
}),
|
|
1208
|
+
value: WeDo2MotorDirection.REVERSE
|
|
1209
|
+
}
|
|
1210
|
+
]
|
|
1211
|
+
},
|
|
1212
|
+
TILT_DIRECTION: {
|
|
1213
|
+
acceptReporters: true,
|
|
1214
|
+
items: [
|
|
1215
|
+
{
|
|
1216
|
+
text: formatMessage({
|
|
1217
|
+
id: 'wedo2.tiltDirection.up',
|
|
1218
|
+
default: 'up',
|
|
1219
|
+
description: 'label for up element in tilt direction menu for LEGO WeDo 2 extension'
|
|
1220
|
+
}),
|
|
1221
|
+
value: WeDo2TiltDirection.UP
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
text: formatMessage({
|
|
1225
|
+
id: 'wedo2.tiltDirection.down',
|
|
1226
|
+
default: 'down',
|
|
1227
|
+
description: 'label for down element in tilt direction menu for LEGO WeDo 2 extension'
|
|
1228
|
+
}),
|
|
1229
|
+
value: WeDo2TiltDirection.DOWN
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
text: formatMessage({
|
|
1233
|
+
id: 'wedo2.tiltDirection.left',
|
|
1234
|
+
default: 'left',
|
|
1235
|
+
description: 'label for left element in tilt direction menu for LEGO WeDo 2 extension'
|
|
1236
|
+
}),
|
|
1237
|
+
value: WeDo2TiltDirection.LEFT
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
text: formatMessage({
|
|
1241
|
+
id: 'wedo2.tiltDirection.right',
|
|
1242
|
+
default: 'right',
|
|
1243
|
+
description: 'label for right element in tilt direction menu for LEGO WeDo 2 extension'
|
|
1244
|
+
}),
|
|
1245
|
+
value: WeDo2TiltDirection.RIGHT
|
|
1246
|
+
}
|
|
1247
|
+
]
|
|
1248
|
+
},
|
|
1249
|
+
TILT_DIRECTION_ANY: {
|
|
1250
|
+
acceptReporters: true,
|
|
1251
|
+
items: [
|
|
1252
|
+
{
|
|
1253
|
+
text: formatMessage({
|
|
1254
|
+
id: 'wedo2.tiltDirection.up',
|
|
1255
|
+
default: 'up'
|
|
1256
|
+
}),
|
|
1257
|
+
value: WeDo2TiltDirection.UP
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
text: formatMessage({
|
|
1261
|
+
id: 'wedo2.tiltDirection.down',
|
|
1262
|
+
default: 'down'
|
|
1263
|
+
}),
|
|
1264
|
+
value: WeDo2TiltDirection.DOWN
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
text: formatMessage({
|
|
1268
|
+
id: 'wedo2.tiltDirection.left',
|
|
1269
|
+
default: 'left'
|
|
1270
|
+
}),
|
|
1271
|
+
value: WeDo2TiltDirection.LEFT
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
text: formatMessage({
|
|
1275
|
+
id: 'wedo2.tiltDirection.right',
|
|
1276
|
+
default: 'right'
|
|
1277
|
+
}),
|
|
1278
|
+
value: WeDo2TiltDirection.RIGHT
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
text: formatMessage({
|
|
1282
|
+
id: 'wedo2.tiltDirection.any',
|
|
1283
|
+
default: 'any',
|
|
1284
|
+
description: 'label for any element in tilt direction menu for LEGO WeDo 2 extension'
|
|
1285
|
+
}),
|
|
1286
|
+
value: WeDo2TiltDirection.ANY
|
|
1287
|
+
}
|
|
1288
|
+
]
|
|
1289
|
+
},
|
|
1290
|
+
OP: {
|
|
1291
|
+
acceptReporters: true,
|
|
1292
|
+
items: ['<', '>']
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Turn specified motor(s) on for a specified duration.
|
|
1300
|
+
* @param {object} args - the block's arguments.
|
|
1301
|
+
* @property {MotorID} MOTOR_ID - the motor(s) to activate.
|
|
1302
|
+
* @property {int} DURATION - the amount of time to run the motors.
|
|
1303
|
+
* @return {Promise} - a promise which will resolve at the end of the duration.
|
|
1304
|
+
*/
|
|
1305
|
+
motorOnFor (args) {
|
|
1306
|
+
// TODO: cast args.MOTOR_ID?
|
|
1307
|
+
let durationMS = Cast.toNumber(args.DURATION) * 1000;
|
|
1308
|
+
durationMS = MathUtil.clamp(durationMS, 0, 15000);
|
|
1309
|
+
return new Promise(resolve => {
|
|
1310
|
+
this._forEachMotor(args.MOTOR_ID, motorIndex => {
|
|
1311
|
+
const motor = this._peripheral.motor(motorIndex);
|
|
1312
|
+
if (motor) {
|
|
1313
|
+
motor.turnOnFor(durationMS);
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
// Run for some time even when no motor is connected
|
|
1318
|
+
setTimeout(resolve, durationMS);
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Turn specified motor(s) on indefinitely.
|
|
1324
|
+
* @param {object} args - the block's arguments.
|
|
1325
|
+
* @property {MotorID} MOTOR_ID - the motor(s) to activate.
|
|
1326
|
+
* @return {Promise} - a Promise that resolves after some delay.
|
|
1327
|
+
*/
|
|
1328
|
+
motorOn (args) {
|
|
1329
|
+
// TODO: cast args.MOTOR_ID?
|
|
1330
|
+
this._forEachMotor(args.MOTOR_ID, motorIndex => {
|
|
1331
|
+
const motor = this._peripheral.motor(motorIndex);
|
|
1332
|
+
if (motor) {
|
|
1333
|
+
motor.turnOn();
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
return new Promise(resolve => {
|
|
1338
|
+
window.setTimeout(() => {
|
|
1339
|
+
resolve();
|
|
1340
|
+
}, BLESendInterval);
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Turn specified motor(s) off.
|
|
1346
|
+
* @param {object} args - the block's arguments.
|
|
1347
|
+
* @property {MotorID} MOTOR_ID - the motor(s) to deactivate.
|
|
1348
|
+
* @return {Promise} - a Promise that resolves after some delay.
|
|
1349
|
+
*/
|
|
1350
|
+
motorOff (args) {
|
|
1351
|
+
// TODO: cast args.MOTOR_ID?
|
|
1352
|
+
this._forEachMotor(args.MOTOR_ID, motorIndex => {
|
|
1353
|
+
const motor = this._peripheral.motor(motorIndex);
|
|
1354
|
+
if (motor) {
|
|
1355
|
+
motor.turnOff();
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
return new Promise(resolve => {
|
|
1360
|
+
window.setTimeout(() => {
|
|
1361
|
+
resolve();
|
|
1362
|
+
}, BLESendInterval);
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Turn specified motor(s) off.
|
|
1368
|
+
* @param {object} args - the block's arguments.
|
|
1369
|
+
* @property {MotorID} MOTOR_ID - the motor(s) to be affected.
|
|
1370
|
+
* @property {int} POWER - the new power level for the motor(s).
|
|
1371
|
+
* @return {Promise} - a Promise that resolves after some delay.
|
|
1372
|
+
*/
|
|
1373
|
+
startMotorPower (args) {
|
|
1374
|
+
// TODO: cast args.MOTOR_ID?
|
|
1375
|
+
this._forEachMotor(args.MOTOR_ID, motorIndex => {
|
|
1376
|
+
const motor = this._peripheral.motor(motorIndex);
|
|
1377
|
+
if (motor) {
|
|
1378
|
+
motor.power = MathUtil.clamp(Cast.toNumber(args.POWER), 0, 100);
|
|
1379
|
+
motor.turnOn();
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
return new Promise(resolve => {
|
|
1384
|
+
window.setTimeout(() => {
|
|
1385
|
+
resolve();
|
|
1386
|
+
}, BLESendInterval);
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* Set the direction of rotation for specified motor(s).
|
|
1392
|
+
* If the direction is 'reverse' the motor(s) will be reversed individually.
|
|
1393
|
+
* @param {object} args - the block's arguments.
|
|
1394
|
+
* @property {MotorID} MOTOR_ID - the motor(s) to be affected.
|
|
1395
|
+
* @property {MotorDirection} MOTOR_DIRECTION - the new direction for the motor(s).
|
|
1396
|
+
* @return {Promise} - a Promise that resolves after some delay.
|
|
1397
|
+
*/
|
|
1398
|
+
setMotorDirection (args) {
|
|
1399
|
+
// TODO: cast args.MOTOR_ID?
|
|
1400
|
+
this._forEachMotor(args.MOTOR_ID, motorIndex => {
|
|
1401
|
+
const motor = this._peripheral.motor(motorIndex);
|
|
1402
|
+
if (motor) {
|
|
1403
|
+
switch (args.MOTOR_DIRECTION) {
|
|
1404
|
+
case WeDo2MotorDirection.FORWARD:
|
|
1405
|
+
motor.direction = 1;
|
|
1406
|
+
break;
|
|
1407
|
+
case WeDo2MotorDirection.BACKWARD:
|
|
1408
|
+
motor.direction = -1;
|
|
1409
|
+
break;
|
|
1410
|
+
case WeDo2MotorDirection.REVERSE:
|
|
1411
|
+
motor.direction = -motor.direction;
|
|
1412
|
+
break;
|
|
1413
|
+
default:
|
|
1414
|
+
log.warn(`Unknown motor direction in setMotorDirection: ${args.DIRECTION}`);
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
// keep the motor on if it's running, and update the pending timeout if needed
|
|
1418
|
+
if (motor.isOn) {
|
|
1419
|
+
if (motor.pendingTimeoutDelay) {
|
|
1420
|
+
motor.turnOnFor(motor.pendingTimeoutStartTime + motor.pendingTimeoutDelay - Date.now());
|
|
1421
|
+
} else {
|
|
1422
|
+
motor.turnOn();
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
return new Promise(resolve => {
|
|
1429
|
+
window.setTimeout(() => {
|
|
1430
|
+
resolve();
|
|
1431
|
+
}, BLESendInterval);
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Set the LED's hue.
|
|
1437
|
+
* @param {object} args - the block's arguments.
|
|
1438
|
+
* @property {number} HUE - the hue to set, in the range [0,100].
|
|
1439
|
+
* @return {Promise} - a Promise that resolves after some delay.
|
|
1440
|
+
*/
|
|
1441
|
+
setLightHue (args) {
|
|
1442
|
+
// Convert from [0,100] to [0,360]
|
|
1443
|
+
let inputHue = Cast.toNumber(args.HUE);
|
|
1444
|
+
inputHue = MathUtil.wrapClamp(inputHue, 0, 100);
|
|
1445
|
+
const hue = inputHue * 360 / 100;
|
|
1446
|
+
|
|
1447
|
+
const rgbObject = color.hsvToRgb({h: hue, s: 1, v: 1});
|
|
1448
|
+
|
|
1449
|
+
const rgbDecimal = color.rgbToDecimal(rgbObject);
|
|
1450
|
+
|
|
1451
|
+
this._peripheral.setLED(rgbDecimal);
|
|
1452
|
+
|
|
1453
|
+
return new Promise(resolve => {
|
|
1454
|
+
window.setTimeout(() => {
|
|
1455
|
+
resolve();
|
|
1456
|
+
}, BLESendInterval);
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Make the WeDo 2.0 peripheral play a MIDI note for the specified duration.
|
|
1462
|
+
* @param {object} args - the block's arguments.
|
|
1463
|
+
* @property {number} NOTE - the MIDI note to play.
|
|
1464
|
+
* @property {number} DURATION - the duration of the note, in seconds.
|
|
1465
|
+
* @return {Promise} - a promise which will resolve at the end of the duration.
|
|
1466
|
+
*/
|
|
1467
|
+
playNoteFor (args) {
|
|
1468
|
+
let durationMS = Cast.toNumber(args.DURATION) * 1000;
|
|
1469
|
+
durationMS = MathUtil.clamp(durationMS, 0, 3000);
|
|
1470
|
+
const note = MathUtil.clamp(Cast.toNumber(args.NOTE), 25, 125); // valid WeDo 2.0 sounds
|
|
1471
|
+
if (durationMS === 0) return; // WeDo 2.0 plays duration '0' forever
|
|
1472
|
+
return new Promise(resolve => {
|
|
1473
|
+
const tone = this._noteToTone(note);
|
|
1474
|
+
this._peripheral.playTone(tone, durationMS);
|
|
1475
|
+
|
|
1476
|
+
// Run for some time even when no piezo is connected
|
|
1477
|
+
setTimeout(resolve, durationMS);
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Compare the distance sensor's value to a reference.
|
|
1483
|
+
* @param {object} args - the block's arguments.
|
|
1484
|
+
* @property {string} OP - the comparison operation: '<' or '>'.
|
|
1485
|
+
* @property {number} REFERENCE - the value to compare against.
|
|
1486
|
+
* @return {boolean} - the result of the comparison, or false on error.
|
|
1487
|
+
*/
|
|
1488
|
+
whenDistance (args) {
|
|
1489
|
+
switch (args.OP) {
|
|
1490
|
+
case '<':
|
|
1491
|
+
return this._peripheral.distance < Cast.toNumber(args.REFERENCE);
|
|
1492
|
+
case '>':
|
|
1493
|
+
return this._peripheral.distance > Cast.toNumber(args.REFERENCE);
|
|
1494
|
+
default:
|
|
1495
|
+
log.warn(`Unknown comparison operator in whenDistance: ${args.OP}`);
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* Test whether the tilt sensor is currently tilted.
|
|
1502
|
+
* @param {object} args - the block's arguments.
|
|
1503
|
+
* @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any).
|
|
1504
|
+
* @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
|
|
1505
|
+
*/
|
|
1506
|
+
whenTilted (args) {
|
|
1507
|
+
return this._isTilted(args.TILT_DIRECTION_ANY);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* @return {number} - the distance sensor's value, scaled to the [0,100] range.
|
|
1512
|
+
*/
|
|
1513
|
+
getDistance () {
|
|
1514
|
+
return this._peripheral.distance;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* Test whether the tilt sensor is currently tilted.
|
|
1519
|
+
* @param {object} args - the block's arguments.
|
|
1520
|
+
* @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any).
|
|
1521
|
+
* @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
|
|
1522
|
+
*/
|
|
1523
|
+
isTilted (args) {
|
|
1524
|
+
return this._isTilted(args.TILT_DIRECTION_ANY);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* @param {object} args - the block's arguments.
|
|
1529
|
+
* @property {TiltDirection} TILT_DIRECTION - the direction (up, down, left, right) to check.
|
|
1530
|
+
* @return {number} - the tilt sensor's angle in the specified direction.
|
|
1531
|
+
* Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right).
|
|
1532
|
+
*/
|
|
1533
|
+
getTiltAngle (args) {
|
|
1534
|
+
return this._getTiltAngle(args.TILT_DIRECTION);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* Test whether the tilt sensor is currently tilted.
|
|
1539
|
+
* @param {TiltDirection} direction - the tilt direction to test (up, down, left, right, or any).
|
|
1540
|
+
* @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
|
|
1541
|
+
* @private
|
|
1542
|
+
*/
|
|
1543
|
+
_isTilted (direction) {
|
|
1544
|
+
switch (direction) {
|
|
1545
|
+
case WeDo2TiltDirection.ANY:
|
|
1546
|
+
return this._getTiltAngle(WeDo2TiltDirection.UP) >= Scratch3WeDo2Blocks.TILT_THRESHOLD ||
|
|
1547
|
+
this._getTiltAngle(WeDo2TiltDirection.DOWN) >= Scratch3WeDo2Blocks.TILT_THRESHOLD ||
|
|
1548
|
+
this._getTiltAngle(WeDo2TiltDirection.LEFT) >= Scratch3WeDo2Blocks.TILT_THRESHOLD ||
|
|
1549
|
+
this._getTiltAngle(WeDo2TiltDirection.RIGHT) >= Scratch3WeDo2Blocks.TILT_THRESHOLD;
|
|
1550
|
+
default:
|
|
1551
|
+
return this._getTiltAngle(direction) >= Scratch3WeDo2Blocks.TILT_THRESHOLD;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* @param {TiltDirection} direction - the direction (up, down, left, right) to check.
|
|
1557
|
+
* @return {number} - the tilt sensor's angle in the specified direction.
|
|
1558
|
+
* Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right).
|
|
1559
|
+
* @private
|
|
1560
|
+
*/
|
|
1561
|
+
_getTiltAngle (direction) {
|
|
1562
|
+
switch (direction) {
|
|
1563
|
+
case WeDo2TiltDirection.UP:
|
|
1564
|
+
return this._peripheral.tiltY > 45 ? 256 - this._peripheral.tiltY : -this._peripheral.tiltY;
|
|
1565
|
+
case WeDo2TiltDirection.DOWN:
|
|
1566
|
+
return this._peripheral.tiltY > 45 ? this._peripheral.tiltY - 256 : this._peripheral.tiltY;
|
|
1567
|
+
case WeDo2TiltDirection.LEFT:
|
|
1568
|
+
return this._peripheral.tiltX > 45 ? 256 - this._peripheral.tiltX : -this._peripheral.tiltX;
|
|
1569
|
+
case WeDo2TiltDirection.RIGHT:
|
|
1570
|
+
return this._peripheral.tiltX > 45 ? this._peripheral.tiltX - 256 : this._peripheral.tiltX;
|
|
1571
|
+
default:
|
|
1572
|
+
log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Call a callback for each motor indexed by the provided motor ID.
|
|
1578
|
+
* @param {MotorID} motorID - the ID specifier.
|
|
1579
|
+
* @param {Function} callback - the function to call with the numeric motor index for each motor.
|
|
1580
|
+
* @private
|
|
1581
|
+
*/
|
|
1582
|
+
_forEachMotor (motorID, callback) {
|
|
1583
|
+
let motors;
|
|
1584
|
+
switch (motorID) {
|
|
1585
|
+
case WeDo2MotorLabel.A:
|
|
1586
|
+
motors = [0];
|
|
1587
|
+
break;
|
|
1588
|
+
case WeDo2MotorLabel.B:
|
|
1589
|
+
motors = [1];
|
|
1590
|
+
break;
|
|
1591
|
+
case WeDo2MotorLabel.ALL:
|
|
1592
|
+
case WeDo2MotorLabel.DEFAULT:
|
|
1593
|
+
motors = [0, 1];
|
|
1594
|
+
break;
|
|
1595
|
+
default:
|
|
1596
|
+
log.warn(`Invalid motor ID: ${motorID}`);
|
|
1597
|
+
motors = [];
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
for (const index of motors) {
|
|
1601
|
+
callback(index);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/**
|
|
1606
|
+
* @param {number} midiNote - the MIDI note value to convert.
|
|
1607
|
+
* @return {number} - the frequency, in Hz, corresponding to that MIDI note value.
|
|
1608
|
+
* @private
|
|
1609
|
+
*/
|
|
1610
|
+
_noteToTone (midiNote) {
|
|
1611
|
+
// Note that MIDI note 69 is A4, 440 Hz
|
|
1612
|
+
return 440 * Math.pow(2, (midiNote - 69) / 12);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
module.exports = Scratch3WeDo2Blocks;
|