@scratch/scratch-vm 11.0.0-UEPR-176

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.
Files changed (178) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +120 -0
  3. package/dist/node/extension-worker.js +2 -0
  4. package/dist/node/extension-worker.js.map +1 -0
  5. package/dist/node/scratch-vm.js +3 -0
  6. package/dist/node/scratch-vm.js.LICENSE.txt +205 -0
  7. package/dist/node/scratch-vm.js.map +1 -0
  8. package/dist/web/extension-worker.js +2 -0
  9. package/dist/web/extension-worker.js.map +1 -0
  10. package/dist/web/scratch-vm.js +3 -0
  11. package/dist/web/scratch-vm.js.LICENSE.txt +42 -0
  12. package/dist/web/scratch-vm.js.map +1 -0
  13. package/package.json +108 -0
  14. package/src/.eslintrc.js +7 -0
  15. package/src/blocks/scratch3_control.js +206 -0
  16. package/src/blocks/scratch3_core_example.js +69 -0
  17. package/src/blocks/scratch3_data.js +257 -0
  18. package/src/blocks/scratch3_event.js +137 -0
  19. package/src/blocks/scratch3_looks.js +612 -0
  20. package/src/blocks/scratch3_motion.js +288 -0
  21. package/src/blocks/scratch3_operators.js +154 -0
  22. package/src/blocks/scratch3_procedures.js +79 -0
  23. package/src/blocks/scratch3_sensing.js +336 -0
  24. package/src/blocks/scratch3_sound.js +350 -0
  25. package/src/dispatch/central-dispatch.js +143 -0
  26. package/src/dispatch/shared-dispatch.js +235 -0
  27. package/src/dispatch/worker-dispatch.js +110 -0
  28. package/src/engine/adapter.js +176 -0
  29. package/src/engine/block-utility.js +242 -0
  30. package/src/engine/blocks-execute-cache.js +19 -0
  31. package/src/engine/blocks-runtime-cache.js +78 -0
  32. package/src/engine/blocks.js +1299 -0
  33. package/src/engine/comment.js +56 -0
  34. package/src/engine/execute.js +580 -0
  35. package/src/engine/monitor-record.js +23 -0
  36. package/src/engine/mutation-adapter.js +48 -0
  37. package/src/engine/profiler.js +390 -0
  38. package/src/engine/runtime.js +2686 -0
  39. package/src/engine/scratch-blocks-constants.js +27 -0
  40. package/src/engine/sequencer.js +361 -0
  41. package/src/engine/stage-layering.js +29 -0
  42. package/src/engine/target.js +799 -0
  43. package/src/engine/thread.js +404 -0
  44. package/src/engine/variable.js +70 -0
  45. package/src/extension-support/argument-type.js +47 -0
  46. package/src/extension-support/block-type.js +50 -0
  47. package/src/extension-support/define-messages.js +18 -0
  48. package/src/extension-support/extension-manager.js +440 -0
  49. package/src/extension-support/extension-metadata.js +64 -0
  50. package/src/extension-support/extension-worker.js +59 -0
  51. package/src/extension-support/reporter-scope.js +18 -0
  52. package/src/extension-support/target-type.js +17 -0
  53. package/src/extensions/scratch3_boost/index.js +2113 -0
  54. package/src/extensions/scratch3_ev3/index.js +1355 -0
  55. package/src/extensions/scratch3_gdx_for/index.js +981 -0
  56. package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
  57. package/src/extensions/scratch3_makeymakey/index.js +396 -0
  58. package/src/extensions/scratch3_microbit/index.js +984 -0
  59. package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
  60. package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
  61. package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
  62. package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
  63. package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
  64. package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
  65. package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
  66. package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
  67. package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
  68. package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
  69. package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
  70. package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
  71. package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
  72. package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
  73. package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
  74. package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
  75. package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
  76. package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
  77. package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
  78. package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
  79. package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
  80. package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
  81. package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
  82. package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
  83. package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
  84. package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
  85. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
  86. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
  87. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
  88. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
  89. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
  90. package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
  91. package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
  92. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
  93. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
  94. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
  95. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
  96. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
  97. package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
  98. package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
  99. package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
  100. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
  101. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
  102. package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
  103. package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
  104. package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
  105. package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
  106. package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
  107. package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
  108. package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
  109. package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
  110. package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
  111. package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
  112. package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
  113. package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
  114. package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
  115. package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
  116. package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
  117. package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
  118. package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
  119. package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
  120. package/src/extensions/scratch3_music/index.js +1333 -0
  121. package/src/extensions/scratch3_music/manifest.js +63 -0
  122. package/src/extensions/scratch3_pen/index.js +770 -0
  123. package/src/extensions/scratch3_speech2text/index.js +700 -0
  124. package/src/extensions/scratch3_text2speech/index.js +766 -0
  125. package/src/extensions/scratch3_translate/index.js +286 -0
  126. package/src/extensions/scratch3_video_sensing/debug.js +13 -0
  127. package/src/extensions/scratch3_video_sensing/index.js +594 -0
  128. package/src/extensions/scratch3_video_sensing/library.js +384 -0
  129. package/src/extensions/scratch3_video_sensing/math.js +76 -0
  130. package/src/extensions/scratch3_video_sensing/view.js +509 -0
  131. package/src/extensions/scratch3_wedo2/index.js +1616 -0
  132. package/src/import/load-costume.js +416 -0
  133. package/src/import/load-sound.js +116 -0
  134. package/src/index.js +10 -0
  135. package/src/io/ble.js +256 -0
  136. package/src/io/bt.js +202 -0
  137. package/src/io/clock.js +39 -0
  138. package/src/io/cloud.js +169 -0
  139. package/src/io/keyboard.js +146 -0
  140. package/src/io/mouse.js +145 -0
  141. package/src/io/mouseWheel.js +28 -0
  142. package/src/io/userData.js +24 -0
  143. package/src/io/video.js +210 -0
  144. package/src/playground/benchmark.css +83 -0
  145. package/src/playground/benchmark.js +753 -0
  146. package/src/playground/index.html +111 -0
  147. package/src/playground/suite.css +78 -0
  148. package/src/playground/suite.html +25 -0
  149. package/src/playground/suite.js +544 -0
  150. package/src/playground/video-sensing.html +18 -0
  151. package/src/playground/video-sensing.js +133 -0
  152. package/src/serialization/deserialize-assets.js +178 -0
  153. package/src/serialization/sb2.js +1295 -0
  154. package/src/serialization/sb2_specmap.js +1818 -0
  155. package/src/serialization/sb3.js +1320 -0
  156. package/src/serialization/serialize-assets.js +60 -0
  157. package/src/sprites/rendered-target.js +1115 -0
  158. package/src/sprites/sprite.js +177 -0
  159. package/src/util/base64-util.js +48 -0
  160. package/src/util/cast.js +218 -0
  161. package/src/util/clone.js +17 -0
  162. package/src/util/color.js +204 -0
  163. package/src/util/fetch-with-timeout.js +53 -0
  164. package/src/util/get-monitor-id.js +33 -0
  165. package/src/util/jsonrpc.js +114 -0
  166. package/src/util/log.js +4 -0
  167. package/src/util/math-util.js +122 -0
  168. package/src/util/maybe-format-message.js +18 -0
  169. package/src/util/new-block-ids.js +33 -0
  170. package/src/util/rateLimiter.js +73 -0
  171. package/src/util/scratch-link-websocket.js +133 -0
  172. package/src/util/string-util.js +94 -0
  173. package/src/util/task-queue.js +203 -0
  174. package/src/util/timer.js +103 -0
  175. package/src/util/uid.js +29 -0
  176. package/src/util/variable-util.js +48 -0
  177. package/src/util/xml-escape.js +33 -0
  178. package/src/virtual-machine.js +1586 -0
@@ -0,0 +1,2113 @@
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
+ * The LEGO Wireless Protocol documentation used to create this extension can be found at:
14
+ * https://lego.github.io/lego-ble-wireless-protocol-docs/index.html
15
+ */
16
+
17
+ /**
18
+ * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
19
+ * @type {string}
20
+ */
21
+ // eslint-disable-next-line max-len
22
+ const iconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAMAAAC5zwKfAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACpQTFRF////fIel5ufolZ62/2YavsPS+YZOkJmy9/j53+Hk6+zs6N/b6dfO////tDhMHAAAAA50Uk5T/////////////////wBFwNzIAAAA6ElEQVR42uzX2w6DIBAEUGDVtlr//3dLaLwgiwUd2z7MJPJg5EQWiGhGcAxBggQJEiT436CIfqXJPTn3MKNYYMSDFpoAmp24OaYgvwKnFgL2zvVTCwHrMoMi+nUQLFthaNCCa0iwclLkDgYVsQp0mzxuqXgK1MRzoCLWgkPXNN2wI/q6Kvt7u/cX0HtejN8x2sXpnpb8J8D3b0Keuhh3X975M+i0xNVbg3s1TIasgK21bQyGO+s2PykaGMYbge8KrNrssvkOWDXkErB8UuBHETjoYLkKBA8ZfuDkbwVBggQJEiR4MC8BBgDTtMZLx2nFCQAAAABJRU5ErkJggg==';
23
+
24
+ /**
25
+ * Boost BLE UUIDs.
26
+ * @enum {string}
27
+ */
28
+ const BoostBLE = {
29
+ service: '00001623-1212-efde-1623-785feabcd123',
30
+ characteristic: '00001624-1212-efde-1623-785feabcd123',
31
+ sendInterval: 100,
32
+ sendRateMax: 20
33
+ };
34
+
35
+ /**
36
+ * Boost Motor Max Power Add. Defines how much more power than the target speed
37
+ * the motors may supply to reach the target speed faster.
38
+ * Lower number == softer, slower reached target speed.
39
+ * Higher number == harder, faster reached target speed.
40
+ * @constant {number}
41
+ */
42
+ const BoostMotorMaxPowerAdd = 10;
43
+
44
+ /**
45
+ * A time interval to wait (in milliseconds) in between battery check calls.
46
+ * @type {number}
47
+ */
48
+ const BoostPingInterval = 5000;
49
+
50
+ /**
51
+ * The number of continuous samples the color-sensor will evaluate color from.
52
+ * @type {number}
53
+ */
54
+ const BoostColorSampleSize = 5;
55
+
56
+ /**
57
+ * Enum for Boost sensor and actuator types.
58
+ * @readonly
59
+ * @enum {number}
60
+ */
61
+ const BoostIO = {
62
+ MOTOR_WEDO: 0x01,
63
+ MOTOR_SYSTEM: 0x02,
64
+ BUTTON: 0x05,
65
+ LIGHT: 0x08,
66
+ VOLTAGE: 0x14,
67
+ CURRENT: 0x15,
68
+ PIEZO: 0x16,
69
+ LED: 0x17,
70
+ TILT_EXTERNAL: 0x22,
71
+ MOTION_SENSOR: 0x23,
72
+ COLOR: 0x25,
73
+ MOTOREXT: 0x26,
74
+ MOTORINT: 0x27,
75
+ TILT: 0x28
76
+ };
77
+
78
+ /**
79
+ * Enum for ids for various output command feedback types on the Boost.
80
+ * @readonly
81
+ * @enum {number}
82
+ */
83
+ const BoostPortFeedback = {
84
+ IN_PROGRESS: 0x01,
85
+ COMPLETED: 0x02,
86
+ DISCARDED: 0x04,
87
+ IDLE: 0x08,
88
+ BUSY_OR_FULL: 0x10
89
+ };
90
+
91
+ /**
92
+ * Enum for physical Boost Ports
93
+ * @readonly
94
+ * @enum {number}
95
+ */
96
+
97
+ const BoostPort10000223OrOlder = {
98
+ A: 55,
99
+ B: 56,
100
+ C: 1,
101
+ D: 2
102
+ };
103
+
104
+ const BoostPort10000224OrNewer = {
105
+ A: 0,
106
+ B: 1,
107
+ C: 2,
108
+ D: 3
109
+ };
110
+
111
+ // Set default port mapping to support the newer firmware
112
+ let BoostPort = BoostPort10000224OrNewer;
113
+
114
+ /**
115
+ * Ids for each color sensor value used by the extension.
116
+ * @readonly
117
+ * @enum {string}
118
+ */
119
+ const BoostColor = {
120
+ ANY: 'any',
121
+ NONE: 'none',
122
+ RED: 'red',
123
+ BLUE: 'blue',
124
+ GREEN: 'green',
125
+ YELLOW: 'yellow',
126
+ WHITE: 'white',
127
+ BLACK: 'black'
128
+ };
129
+
130
+ /**
131
+ * Enum for indices for each color sensed by the Boost vision sensor.
132
+ * @readonly
133
+ * @enum {number}
134
+ */
135
+ const BoostColorIndex = {
136
+ [BoostColor.NONE]: 255,
137
+ [BoostColor.RED]: 9,
138
+ [BoostColor.BLUE]: 3,
139
+ [BoostColor.GREEN]: 5,
140
+ [BoostColor.YELLOW]: 7,
141
+ [BoostColor.WHITE]: 10,
142
+ [BoostColor.BLACK]: 0
143
+ };
144
+
145
+ /**
146
+ * Enum for Message Types
147
+ * @readonly
148
+ * @enum {number}
149
+ */
150
+ const BoostMessage = {
151
+ HUB_PROPERTIES: 0x01,
152
+ HUB_ACTIONS: 0x02,
153
+ HUB_ALERTS: 0x03,
154
+ HUB_ATTACHED_IO: 0x04,
155
+ ERROR: 0x05,
156
+ PORT_INPUT_FORMAT_SETUP_SINGLE: 0x41,
157
+ PORT_INPUT_FORMAT_SETUP_COMBINED: 0x42,
158
+ PORT_INFORMATION: 0x43,
159
+ PORT_MODEINFORMATION: 0x44,
160
+ PORT_VALUE: 0x45,
161
+ PORT_VALUE_COMBINED: 0x46,
162
+ PORT_INPUT_FORMAT: 0x47,
163
+ PORT_INPUT_FORMAT_COMBINED: 0x48,
164
+ OUTPUT: 0x81,
165
+ PORT_FEEDBACK: 0x82
166
+ };
167
+
168
+ /**
169
+ * Enum for Hub Property Types
170
+ * @readonly
171
+ * @enum {number}
172
+ */
173
+
174
+ const BoostHubProperty = {
175
+ ADVERTISEMENT_NAME: 0x01,
176
+ BUTTON: 0x02,
177
+ FW_VERSION: 0x03,
178
+ HW_VERSION: 0x04,
179
+ RSSI: 0x05,
180
+ BATTERY_VOLTAGE: 0x06,
181
+ BATTERY_TYPE: 0x07,
182
+ MANUFACTURER_NAME: 0x08,
183
+ RADIO_FW_VERSION: 0x09,
184
+ LEGO_WP_VERSION: 0x0A,
185
+ SYSTEM_TYPE_ID: 0x0B,
186
+ HW_NETWORK_ID: 0x0C,
187
+ PRIMARY_MAC: 0x0D,
188
+ SECONDARY_MAC: 0x0E,
189
+ HW_NETWORK_FAMILY: 0x0F
190
+ };
191
+
192
+ /**
193
+ * Enum for Hub Property Operations
194
+ * @readonly
195
+ * @enum {number}
196
+ */
197
+
198
+ const BoostHubPropertyOperation = {
199
+ SET: 0x01,
200
+ ENABLE_UPDATES: 0x02,
201
+ DISABLE_UPDATES: 0x03,
202
+ RESET: 0x04,
203
+ REQUEST_UPDATE: 0x05,
204
+ UPDATE: 0x06
205
+ };
206
+
207
+ /**
208
+ * Enum for Motor Subcommands (for 0x81)
209
+ * @readonly
210
+ * @enum {number}
211
+ */
212
+ const BoostOutputSubCommand = {
213
+ START_POWER: 0x01,
214
+ START_POWER_PAIR: 0x02,
215
+ SET_ACC_TIME: 0x05,
216
+ SET_DEC_TIME: 0x06,
217
+ START_SPEED: 0x07,
218
+ START_SPEED_PAIR: 0x08,
219
+ START_SPEED_FOR_TIME: 0x09,
220
+ START_SPEED_FOR_TIME_PAIR: 0x0A,
221
+ START_SPEED_FOR_DEGREES: 0x0B,
222
+ START_SPEED_FOR_DEGREES_PAIR: 0x0C,
223
+ GO_TO_ABS_POSITION: 0x0D,
224
+ GO_TO_ABS_POSITION_PAIR: 0x0E,
225
+ PRESET_ENCODER: 0x14,
226
+ WRITE_DIRECT_MODE_DATA: 0x51
227
+ };
228
+
229
+ /**
230
+ * Enum for Startup/Completion information for an output command.
231
+ * Startup and completion bytes must be OR'ed to be combined to a single byte.
232
+ * @readonly
233
+ * @enum {number}
234
+ */
235
+ const BoostOutputExecution = {
236
+ // Startup information
237
+ BUFFER_IF_NECESSARY: 0x00,
238
+ EXECUTE_IMMEDIATELY: 0x10,
239
+ // Completion information
240
+ NO_ACTION: 0x00,
241
+ COMMAND_FEEDBACK: 0x01
242
+ };
243
+
244
+ /**
245
+ * Enum for Boost Motor end states
246
+ * @readonly
247
+ * @enum {number}
248
+ */
249
+ const BoostMotorEndState = {
250
+ FLOAT: 0,
251
+ HOLD: 126,
252
+ BRAKE: 127
253
+ };
254
+
255
+ /**
256
+ * Enum for Boost Motor acceleration/deceleration profiles
257
+ * @readyonly
258
+ * @enum {number}
259
+ */
260
+ const BoostMotorProfile = {
261
+ DO_NOT_USE: 0x00,
262
+ ACCELERATION: 0x01,
263
+ DECELERATION: 0x02
264
+ };
265
+
266
+ /**
267
+ * Enum for when Boost IO's are attached/detached
268
+ * @readonly
269
+ * @enum {number}
270
+ */
271
+ const BoostIOEvent = {
272
+ ATTACHED: 0x01,
273
+ DETACHED: 0x00,
274
+ ATTACHED_VIRTUAL: 0x02
275
+ };
276
+
277
+ /**
278
+ * Enum for selected sensor modes.
279
+ * @enum {number}
280
+ */
281
+ const BoostMode = {
282
+ TILT: 0, // angle (pitch/yaw)
283
+ LED: 1, // Set LED to accept RGB values
284
+ COLOR: 0, // Read indexed colors from Vision Sensor
285
+ MOTOR_SENSOR: 2, // Set motors to report their position
286
+ UNKNOWN: 0 // Anything else will use the default mode (mode 0)
287
+ };
288
+
289
+ /**
290
+ * Enum for Boost motor states.
291
+ * @param {number}
292
+ */
293
+ const BoostMotorState = {
294
+ OFF: 0,
295
+ ON_FOREVER: 1,
296
+ ON_FOR_TIME: 2,
297
+ ON_FOR_ROTATION: 3
298
+ };
299
+
300
+ /**
301
+ * Helper function for converting a JavaScript number to an INT32-number
302
+ * @param {number} number - a number
303
+ * @return {array} - a 4-byte array of Int8-values representing an INT32-number
304
+ */
305
+ const numberToInt32Array = function (number) {
306
+ const buffer = new ArrayBuffer(4);
307
+ const dataview = new DataView(buffer);
308
+ dataview.setInt32(0, number);
309
+ return [
310
+ dataview.getInt8(3),
311
+ dataview.getInt8(2),
312
+ dataview.getInt8(1),
313
+ dataview.getInt8(0)
314
+ ];
315
+ };
316
+
317
+ /**
318
+ * Helper function for converting a regular array to a Little Endian INT32-value
319
+ * @param {Array} array - an array containing UInt8-values
320
+ * @return {number} - a number
321
+ */
322
+ const int32ArrayToNumber = function (array) {
323
+ const i = Uint8Array.from(array);
324
+ const d = new DataView(i.buffer);
325
+ return d.getInt32(0, true);
326
+ };
327
+
328
+ /**
329
+ * Manage power, direction, position, and timers for one Boost motor.
330
+ */
331
+ class BoostMotor {
332
+ /**
333
+ * Construct a Boost Motor instance.
334
+ * @param {Boost} parent - the Boost peripheral which owns this motor.
335
+ * @param {int} index - the zero-based index of this motor on its parent peripheral.
336
+ */
337
+ constructor (parent, index) {
338
+ /**
339
+ * The Boost peripheral which owns this motor.
340
+ * @type {Boost}
341
+ * @private
342
+ */
343
+ this._parent = parent;
344
+
345
+ /**
346
+ * The zero-based index of this motor on its parent peripheral.
347
+ * @type {int}
348
+ * @private
349
+ */
350
+ this._index = index;
351
+
352
+ /**
353
+ * This motor's current direction: 1 for "this way" or -1 for "that way"
354
+ * @type {number}
355
+ * @private
356
+ */
357
+ this._direction = 1;
358
+
359
+ /**
360
+ * This motor's current power level, in the range [0,100].
361
+ * @type {number}
362
+ * @private
363
+ */
364
+ this._power = 50;
365
+
366
+ /**
367
+ * This motor's current relative position
368
+ * @type {number}
369
+ * @private
370
+ */
371
+ this._position = 0;
372
+
373
+ /**
374
+ * Is this motor currently moving?
375
+ * @type {boolean}
376
+ * @private
377
+ */
378
+ this._status = BoostMotorState.OFF;
379
+
380
+ /**
381
+ * If the motor has been turned on or is actively braking for a specific duration, this is the timeout ID for
382
+ * the end-of-action handler. Cancel this when changing plans.
383
+ * @type {Object}
384
+ * @private
385
+ */
386
+ this._pendingDurationTimeoutId = null;
387
+
388
+ /**
389
+ * The starting time for the pending duration timeout.
390
+ * @type {number}
391
+ * @private
392
+ */
393
+ this._pendingDurationTimeoutStartTime = null;
394
+
395
+ /**
396
+ * The delay/duration of the pending duration timeout.
397
+ * @type {number}
398
+ * @private
399
+ */
400
+ this._pendingDurationTimeoutDelay = null;
401
+
402
+ /**
403
+ * The target position of a turn-based command.
404
+ * @type {number}
405
+ * @private
406
+ */
407
+ this._pendingRotationDestination = null;
408
+
409
+ /**
410
+ * If the motor has been turned on run for a specific rotation, this is the function
411
+ * that will be called once Scratch VM gets a notification from the Move Hub.
412
+ * @type {Object}
413
+ * @private
414
+ */
415
+ this._pendingRotationPromise = null;
416
+
417
+ this.turnOff = this.turnOff.bind(this);
418
+ }
419
+
420
+ /**
421
+ * @return {int} - this motor's current direction: 1 for "this way" or -1 for "that way"
422
+ */
423
+ get direction () {
424
+ return this._direction;
425
+ }
426
+
427
+ /**
428
+ * @param {int} value - this motor's new direction: 1 for "this way" or -1 for "that way"
429
+ */
430
+ set direction (value) {
431
+ if (value < 0) {
432
+ this._direction = -1;
433
+ } else {
434
+ this._direction = 1;
435
+ }
436
+ }
437
+
438
+ /**
439
+ * @return {int} - this motor's current power level, in the range [0,100].
440
+ */
441
+ get power () {
442
+ return this._power;
443
+ }
444
+
445
+ /**
446
+ * @param {int} value - this motor's new power level, in the range [10,100].
447
+ */
448
+ set power (value) {
449
+ /**
450
+ * Scale the motor power to a range between 10 and 100,
451
+ * to make sure the motors will run with something built onto them.
452
+ */
453
+ if (value === 0) {
454
+ this._power = 0;
455
+ } else {
456
+ this._power = MathUtil.scale(value, 1, 100, 10, 100);
457
+ }
458
+ }
459
+
460
+ /**
461
+ * @return {int} - this motor's current position, in the range of [-MIN_INT32,MAX_INT32]
462
+ */
463
+ get position () {
464
+ return this._position;
465
+ }
466
+
467
+ /**
468
+ * @param {int} value - set this motor's current position.
469
+ */
470
+ set position (value) {
471
+ this._position = value;
472
+ }
473
+
474
+ /**
475
+ * @return {BoostMotorState} - the motor's current state.
476
+ */
477
+ get status () {
478
+ return this._status;
479
+ }
480
+
481
+ /**
482
+ * @param {BoostMotorState} value - set this motor's state.
483
+ */
484
+ set status (value) {
485
+ this._clearRotationState();
486
+ this._clearDurationTimeout();
487
+ this._status = value;
488
+ }
489
+
490
+ /**
491
+ * @return {number} - time, in milliseconds, of when the pending duration timeout began.
492
+ */
493
+ get pendingDurationTimeoutStartTime () {
494
+ return this._pendingDurationTimeoutStartTime;
495
+ }
496
+
497
+ /**
498
+ * @return {number} - delay, in milliseconds, of the pending duration timeout.
499
+ */
500
+ get pendingDurationTimeoutDelay () {
501
+ return this._pendingDurationTimeoutDelay;
502
+ }
503
+
504
+ /**
505
+ * @return {number} - target position, in degrees, of the pending rotation.
506
+ */
507
+ get pendingRotationDestination () {
508
+ return this._pendingRotationDestination;
509
+ }
510
+
511
+ /**
512
+ * @return {Promise} - the Promise function for the pending rotation.
513
+ */
514
+ get pendingRotationPromise () {
515
+ return this._pendingRotationPromise;
516
+ }
517
+
518
+ /**
519
+ * @param {function} func - function to resolve pending rotation Promise
520
+ */
521
+ set pendingRotationPromise (func) {
522
+ this._pendingRotationPromise = func;
523
+ }
524
+
525
+ /**
526
+ * Turn this motor on indefinitely
527
+ * @private
528
+ */
529
+ _turnOn () {
530
+ const cmd = this._parent.generateOutputCommand(
531
+ this._index,
532
+ BoostOutputExecution.EXECUTE_IMMEDIATELY,
533
+ BoostOutputSubCommand.START_SPEED,
534
+ [
535
+ this.power * this.direction,
536
+ MathUtil.clamp(this.power + BoostMotorMaxPowerAdd, 0, 100),
537
+ BoostMotorProfile.DO_NOT_USE
538
+ ]);
539
+
540
+ this._parent.send(BoostBLE.characteristic, cmd);
541
+ }
542
+
543
+ /**
544
+ * Turn this motor on indefinitely
545
+ */
546
+ turnOnForever () {
547
+ this.status = BoostMotorState.ON_FOREVER;
548
+ this._turnOn();
549
+ }
550
+
551
+ /**
552
+ * Turn this motor on for a specific duration.
553
+ * @param {number} milliseconds - run the motor for this long.
554
+ */
555
+ turnOnFor (milliseconds) {
556
+ milliseconds = Math.max(0, milliseconds);
557
+ this.status = BoostMotorState.ON_FOR_TIME;
558
+ this._turnOn();
559
+ this._setNewDurationTimeout(this.turnOff, milliseconds);
560
+ }
561
+
562
+ /**
563
+ * Turn this motor on for a specific rotation in degrees.
564
+ * @param {number} degrees - run the motor for this amount of degrees.
565
+ * @param {number} direction - rotate in this direction
566
+ */
567
+ turnOnForDegrees (degrees, direction) {
568
+ degrees = Math.max(0, degrees);
569
+
570
+ const cmd = this._parent.generateOutputCommand(
571
+ this._index,
572
+ (BoostOutputExecution.EXECUTE_IMMEDIATELY ^ BoostOutputExecution.COMMAND_FEEDBACK),
573
+ BoostOutputSubCommand.START_SPEED_FOR_DEGREES,
574
+ [
575
+ ...numberToInt32Array(degrees),
576
+ this.power * this.direction * direction,
577
+ MathUtil.clamp(this.power + BoostMotorMaxPowerAdd, 0, 100),
578
+ BoostMotorEndState.BRAKE,
579
+ BoostMotorProfile.DO_NOT_USE
580
+ ]
581
+ );
582
+
583
+ this.status = BoostMotorState.ON_FOR_ROTATION;
584
+ this._pendingRotationDestination = this.position + (degrees * this.direction * direction);
585
+ this._parent.send(BoostBLE.characteristic, cmd);
586
+ }
587
+
588
+ /**
589
+ * Turn this motor off.
590
+ * @param {boolean} [useLimiter=true] - if true, use the rate limiter
591
+ */
592
+ turnOff (useLimiter = true) {
593
+ const cmd = this._parent.generateOutputCommand(
594
+ this._index,
595
+ BoostOutputExecution.EXECUTE_IMMEDIATELY,
596
+ BoostOutputSubCommand.START_POWER,
597
+ [
598
+ BoostMotorEndState.FLOAT
599
+ ]
600
+ );
601
+
602
+ this.status = BoostMotorState.OFF;
603
+ this._parent.send(BoostBLE.characteristic, cmd, useLimiter);
604
+ }
605
+
606
+ /**
607
+ * Clear the motor action timeout, if any. Safe to call even when there is no pending timeout.
608
+ * @private
609
+ */
610
+ _clearDurationTimeout () {
611
+ if (this._pendingDurationTimeoutId !== null) {
612
+ clearTimeout(this._pendingDurationTimeoutId);
613
+ this._pendingDurationTimeoutId = null;
614
+ this._pendingDurationTimeoutStartTime = null;
615
+ this._pendingDurationTimeoutDelay = null;
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Set a new motor action timeout, after clearing an existing one if necessary.
621
+ * @param {Function} callback - to be called at the end of the timeout.
622
+ * @param {int} delay - wait this many milliseconds before calling the callback.
623
+ * @private
624
+ */
625
+ _setNewDurationTimeout (callback, delay) {
626
+ this._clearDurationTimeout();
627
+ const timeoutID = setTimeout(() => {
628
+ if (this._pendingDurationTimeoutId === timeoutID) {
629
+ this._pendingDurationTimeoutId = null;
630
+ this._pendingDurationTimeoutStartTime = null;
631
+ this._pendingDurationTimeoutDelay = null;
632
+ }
633
+ callback();
634
+ }, delay);
635
+ this._pendingDurationTimeoutId = timeoutID;
636
+ this._pendingDurationTimeoutStartTime = Date.now();
637
+ this._pendingDurationTimeoutDelay = delay;
638
+ }
639
+
640
+ /**
641
+ * Clear the motor states related to rotation-based commands, if any.
642
+ * Safe to call even when there is no pending promise function.
643
+ * @private
644
+ */
645
+ _clearRotationState () {
646
+ if (this._pendingRotationPromise !== null) {
647
+ this._pendingRotationPromise();
648
+ this._pendingRotationPromise = null;
649
+ }
650
+ this._pendingRotationDestination = null;
651
+ }
652
+ }
653
+
654
+ /**
655
+ * Manage communication with a Boost peripheral over a Bluetooth Low Energy client socket.
656
+ */
657
+ class Boost {
658
+
659
+ constructor (runtime, extensionId) {
660
+
661
+ /**
662
+ * The Scratch 3.0 runtime used to trigger the green flag button.
663
+ * @type {Runtime}
664
+ * @private
665
+ */
666
+ this._runtime = runtime;
667
+ this._runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this));
668
+
669
+ /**
670
+ * The id of the extension this peripheral belongs to.
671
+ */
672
+ this._extensionId = extensionId;
673
+
674
+ /**
675
+ * A list of the ids of the physical or virtual sensors.
676
+ * @type {string[]}
677
+ * @private
678
+ */
679
+ this._ports = [];
680
+
681
+ /**
682
+ * A list of motors registered by the Boost hardware.
683
+ * @type {BoostMotor[]}
684
+ * @private
685
+ */
686
+ this._motors = [];
687
+
688
+ /**
689
+ * The most recently received value for each sensor.
690
+ * @type {Object.<string, number>}
691
+ * @private
692
+ */
693
+ this._sensors = {
694
+ tiltX: 0,
695
+ tiltY: 0,
696
+ color: BoostColor.NONE,
697
+ previousColor: BoostColor.NONE
698
+ };
699
+
700
+ /**
701
+ * An array of values from the Boost Vision Sensor.
702
+ * @type {Array}
703
+ * @private
704
+ */
705
+ this._colorSamples = [];
706
+
707
+ /**
708
+ * The Bluetooth connection socket for reading/writing peripheral data.
709
+ * @type {BLE}
710
+ * @private
711
+ */
712
+ this._ble = null;
713
+ this._runtime.registerPeripheralExtension(extensionId, this);
714
+
715
+ /**
716
+ * A rate limiter utility, to help limit the rate at which we send BLE messages
717
+ * over the socket to Scratch Link to a maximum number of sends per second.
718
+ * @type {RateLimiter}
719
+ * @private
720
+ */
721
+ this._rateLimiter = new RateLimiter(BoostBLE.sendRateMax);
722
+
723
+ /**
724
+ * An interval id for the battery check interval.
725
+ * @type {number}
726
+ * @private
727
+ */
728
+ this._pingDeviceId = null;
729
+
730
+ this.reset = this.reset.bind(this);
731
+ this._onConnect = this._onConnect.bind(this);
732
+ this._onMessage = this._onMessage.bind(this);
733
+ this._pingDevice = this._pingDevice.bind(this);
734
+ }
735
+
736
+ /**
737
+ * @return {number} - the latest value received for the tilt sensor's tilt about the X axis.
738
+ */
739
+ get tiltX () {
740
+ return this._sensors.tiltX;
741
+ }
742
+
743
+ /**
744
+ * @return {number} - the latest value received for the tilt sensor's tilt about the Y axis.
745
+ */
746
+ get tiltY () {
747
+ return this._sensors.tiltY;
748
+ }
749
+
750
+ /**
751
+ * @return {number} - the latest color value received from the vision sensor.
752
+ */
753
+ get color () {
754
+ return this._sensors.color;
755
+ }
756
+
757
+ /**
758
+ * @return {number} - the previous color value received from the vision sensor.
759
+ */
760
+ get previousColor () {
761
+ return this._sensors.previousColor;
762
+ }
763
+
764
+ /**
765
+ * Look up the color id for an index received from the vision sensor.
766
+ * @param {number} index - the color index to look up.
767
+ * @return {BoostColor} the color id for this index.
768
+ */
769
+ boostColorForIndex (index) {
770
+ const colorForIndex = Object.keys(BoostColorIndex).find(key => BoostColorIndex[key] === index);
771
+ return colorForIndex || BoostColor.NONE;
772
+ }
773
+
774
+ /**
775
+ * Access a particular motor on this peripheral.
776
+ * @param {int} index - the index of the desired motor.
777
+ * @return {BoostMotor} - the BoostMotor instance, if any, at that index.
778
+ */
779
+ motor (index) {
780
+ return this._motors[index];
781
+ }
782
+
783
+ /**
784
+ * Stop all the motors that are currently running.
785
+ */
786
+ stopAllMotors () {
787
+ this._motors.forEach(motor => {
788
+ if (motor) {
789
+ // Send the motor off command without using the rate limiter.
790
+ // This allows the stop button to stop motors even if we are
791
+ // otherwise flooded with commands.
792
+ motor.turnOff(false);
793
+ }
794
+ });
795
+ }
796
+
797
+ /**
798
+ * Set the Boost peripheral's LED to a specific color.
799
+ * @param {int} inputRGB - a 24-bit RGB color in 0xRRGGBB format.
800
+ * @return {Promise} - a promise of the completion of the set led send operation.
801
+ */
802
+ setLED (inputRGB) {
803
+ const rgb = [
804
+ (inputRGB >> 16) & 0x000000FF,
805
+ (inputRGB >> 8) & 0x000000FF,
806
+ (inputRGB) & 0x000000FF
807
+ ];
808
+
809
+ const cmd = this.generateOutputCommand(
810
+ this._ports.indexOf(BoostIO.LED),
811
+ BoostOutputExecution.EXECUTE_IMMEDIATELY ^ BoostOutputExecution.COMMAND_FEEDBACK,
812
+ BoostOutputSubCommand.WRITE_DIRECT_MODE_DATA,
813
+ [BoostMode.LED,
814
+ ...rgb]
815
+ );
816
+
817
+ return this.send(BoostBLE.characteristic, cmd);
818
+ }
819
+
820
+ /**
821
+ * Sets the input mode of the LED to RGB.
822
+ * @return {Promise} - a promise returned by the send operation.
823
+ */
824
+ setLEDMode () {
825
+ const cmd = this.generateInputCommand(
826
+ this._ports.indexOf(BoostIO.LED),
827
+ BoostMode.LED,
828
+ 0,
829
+ false
830
+ );
831
+
832
+ return this.send(BoostBLE.characteristic, cmd);
833
+ }
834
+
835
+ /**
836
+ * Stop the motors on the Boost peripheral.
837
+ */
838
+ stopAll () {
839
+ if (!this.isConnected()) return;
840
+ this.stopAllMotors();
841
+ }
842
+
843
+ /**
844
+ * Called by the runtime when user wants to scan for a Boost peripheral.
845
+ */
846
+ scan () {
847
+ if (this._ble) {
848
+ this._ble.disconnect();
849
+ }
850
+ this._ble = new BLE(this._runtime, this._extensionId, {
851
+ filters: [{
852
+ services: [BoostBLE.service],
853
+ manufacturerData: {
854
+ 0x0397: {
855
+ dataPrefix: [0x00, 0x40],
856
+ mask: [0x00, 0xFF]
857
+ }
858
+ }
859
+ }],
860
+ optionalServices: []
861
+ }, this._onConnect, this.reset);
862
+ }
863
+
864
+ /**
865
+ * Called by the runtime when user wants to connect to a certain Boost peripheral.
866
+ * @param {number} id - the id of the peripheral to connect to.
867
+ */
868
+ connect (id) {
869
+ if (this._ble) {
870
+ this._ble.connectPeripheral(id);
871
+ }
872
+ }
873
+
874
+ /**
875
+ * Disconnects from the current BLE socket and resets state.
876
+ */
877
+ disconnect () {
878
+ if (this._ble) {
879
+ this._ble.disconnect();
880
+ }
881
+
882
+ this.reset();
883
+ }
884
+
885
+ /**
886
+ * Reset all the state and timeout/interval ids.
887
+ */
888
+ reset () {
889
+ this._ports = [];
890
+ this._motors = [];
891
+ this._sensors = {
892
+ tiltX: 0,
893
+ tiltY: 0,
894
+ color: BoostColor.NONE,
895
+ previousColor: BoostColor.NONE
896
+ };
897
+
898
+ if (this._pingDeviceId) {
899
+ window.clearInterval(this._pingDeviceId);
900
+ this._pingDeviceId = null;
901
+ }
902
+ }
903
+
904
+ /**
905
+ * Called by the runtime to detect whether the Boost peripheral is connected.
906
+ * @return {boolean} - the connected state.
907
+ */
908
+ isConnected () {
909
+ let connected = false;
910
+ if (this._ble) {
911
+ connected = this._ble.isConnected();
912
+ }
913
+ return connected;
914
+ }
915
+
916
+ /**
917
+ * Write a message to the Boost peripheral BLE socket.
918
+ * @param {number} uuid - the UUID of the characteristic to write to
919
+ * @param {Array} message - the message to write.
920
+ * @param {boolean} [useLimiter=true] - if true, use the rate limiter
921
+ * @return {Promise} - a promise result of the write operation
922
+ */
923
+ send (uuid, message, useLimiter = true) {
924
+ if (!this.isConnected()) return Promise.resolve();
925
+
926
+ if (useLimiter) {
927
+ if (!this._rateLimiter.okayToSend()) return Promise.resolve();
928
+ }
929
+
930
+ return this._ble.write(
931
+ BoostBLE.service,
932
+ uuid,
933
+ Base64Util.uint8ArrayToBase64(message),
934
+ 'base64'
935
+ );
936
+ }
937
+
938
+ /**
939
+ * Generate a Boost 'Output Command' in the byte array format
940
+ * (COMMON HEADER, PORT ID, EXECUTION BYTE, SUBCOMMAND ID, PAYLOAD).
941
+ *
942
+ * Payload is accepted as an array since these vary across different subcommands.
943
+ *
944
+ * @param {number} portID - the port (Connect ID) to send a command to.
945
+ * @param {number} execution - Byte containing startup/completion information
946
+ * @param {number} subCommand - the id of the subcommand byte.
947
+ * @param {array} payload - the list of bytes to send as subcommand payload
948
+ * @return {array} - a generated output command.
949
+ */
950
+ generateOutputCommand (portID, execution, subCommand, payload) {
951
+ const hubID = 0x00;
952
+ const command = [hubID, BoostMessage.OUTPUT, portID, execution, subCommand, ...payload];
953
+ command.unshift(command.length + 1); // Prepend payload with length byte;
954
+
955
+ return command;
956
+ }
957
+
958
+ /**
959
+ * Generate a Boost 'Input Command' in the byte array format
960
+ * (COMMAND ID, COMMAND TYPE, CONNECT ID, TYPE ID, MODE, DELTA INTERVAL (4 BYTES),
961
+ * UNIT, NOTIFICATIONS ENABLED).
962
+ *
963
+ * This sends a command to the Boost that sets that input format
964
+ * of the specified inputs and sets value change notifications.
965
+ *
966
+ * @param {number} portID - the port (Connect ID) to send a command to.
967
+ * @param {number} mode - the mode of the input sensor.
968
+ * @param {number} delta - the delta change needed to trigger notification.
969
+ * @param {boolean} enableNotifications - whether to enable notifications.
970
+ * @return {array} - a generated input command.
971
+ */
972
+ generateInputCommand (portID, mode, delta, enableNotifications) {
973
+ const command = [
974
+ 0x00, // Hub ID
975
+ BoostMessage.PORT_INPUT_FORMAT_SETUP_SINGLE,
976
+ portID,
977
+ mode
978
+ ].concat(numberToInt32Array(delta)).concat([
979
+ enableNotifications
980
+ ]);
981
+ command.unshift(command.length + 1); // Prepend payload with length byte;
982
+
983
+ return command;
984
+ }
985
+
986
+ /**
987
+ * Starts reading data from peripheral after BLE has connected.
988
+ * @private
989
+ */
990
+ _onConnect () {
991
+ this._ble.startNotifications(
992
+ BoostBLE.service,
993
+ BoostBLE.characteristic,
994
+ this._onMessage
995
+ );
996
+ this._pingDeviceId = window.setInterval(this._pingDevice, BoostPingInterval);
997
+
998
+ // Send a request for firmware version.
999
+ setTimeout(() => {
1000
+ const command = [
1001
+ 0x00, // Hub ID
1002
+ BoostMessage.HUB_PROPERTIES,
1003
+ BoostHubProperty.FW_VERSION,
1004
+ BoostHubPropertyOperation.REQUEST_UPDATE
1005
+ ];
1006
+ command.unshift(command.length + 1);
1007
+ this.send(BoostBLE.characteristic, command, false);
1008
+ }, 500);
1009
+
1010
+ }
1011
+
1012
+ /**
1013
+ * Process the sensor data from the incoming BLE characteristic.
1014
+ * @param {object} base64 - the incoming BLE data.
1015
+ * @private
1016
+ */
1017
+ _onMessage (base64) {
1018
+ const data = Base64Util.base64ToUint8Array(base64);
1019
+
1020
+ /**
1021
+ * First three bytes are the common header:
1022
+ * 0: Length of message
1023
+ * 1: Hub ID (always 0x00 at the moment, unused)
1024
+ * 2: Message Type
1025
+ * 3: Port ID
1026
+ * We base our switch-case on Message Type
1027
+ */
1028
+
1029
+ const messageType = data[2];
1030
+ const portID = data[3];
1031
+
1032
+ switch (messageType) {
1033
+
1034
+ case BoostMessage.HUB_PROPERTIES: {
1035
+ const property = data[3];
1036
+ switch (property) {
1037
+ case BoostHubProperty.FW_VERSION: {
1038
+ // Establish firmware version 1.0.00.0224 as a 32-bit signed integer (little endian)
1039
+ const fwVersion10000224 = int32ArrayToNumber([0x24, 0x02, 0x00, 0x10]);
1040
+ const fwHub = int32ArrayToNumber(data.slice(5, data.length));
1041
+ if (fwHub < fwVersion10000224) {
1042
+ BoostPort = BoostPort10000223OrOlder;
1043
+ log.info('Move Hub firmware older than version 1.0.00.0224 detected. Using old port mapping.');
1044
+ } else {
1045
+ BoostPort = BoostPort10000224OrNewer;
1046
+ }
1047
+ break;
1048
+ }
1049
+ }
1050
+ break;
1051
+ }
1052
+ case BoostMessage.HUB_ATTACHED_IO: { // IO Attach/Detach events
1053
+ const event = data[4];
1054
+ const typeId = data[5];
1055
+
1056
+ switch (event) {
1057
+ case BoostIOEvent.ATTACHED:
1058
+ this._registerSensorOrMotor(portID, typeId);
1059
+ break;
1060
+ case BoostIOEvent.DETACHED:
1061
+ this._clearPort(portID);
1062
+ break;
1063
+ case BoostIOEvent.ATTACHED_VIRTUAL:
1064
+ default:
1065
+ }
1066
+ break;
1067
+ }
1068
+ case BoostMessage.PORT_VALUE: {
1069
+ const type = this._ports[portID];
1070
+
1071
+ switch (type) {
1072
+ case BoostIO.TILT:
1073
+ this._sensors.tiltX = data[4];
1074
+ this._sensors.tiltY = data[5];
1075
+ break;
1076
+ case BoostIO.COLOR:
1077
+ this._colorSamples.unshift(data[4]);
1078
+ if (this._colorSamples.length > BoostColorSampleSize) {
1079
+ this._colorSamples.pop();
1080
+ if (this._colorSamples.every((v, i, arr) => v === arr[0])) {
1081
+ this._sensors.previousColor = this._sensors.color;
1082
+ this._sensors.color = this.boostColorForIndex(this._colorSamples[0]);
1083
+ } else {
1084
+ this._sensors.color = BoostColor.NONE;
1085
+ }
1086
+ } else {
1087
+ this._sensors.color = BoostColor.NONE;
1088
+ }
1089
+ break;
1090
+ case BoostIO.MOTOREXT:
1091
+ case BoostIO.MOTORINT:
1092
+ this.motor(portID).position = int32ArrayToNumber(data.slice(4, 8));
1093
+ break;
1094
+ case BoostIO.CURRENT:
1095
+ case BoostIO.VOLTAGE:
1096
+ case BoostIO.LED:
1097
+ break;
1098
+ default:
1099
+ log.warn(`Unknown sensor value! Type: ${type}`);
1100
+ }
1101
+ break;
1102
+ }
1103
+ case BoostMessage.PORT_FEEDBACK: {
1104
+ const feedback = data[4];
1105
+ const motor = this.motor(portID);
1106
+ if (motor) {
1107
+ // Makes sure that commands resolve both when they actually complete and when they fail
1108
+ const isBusy = feedback & BoostPortFeedback.IN_PROGRESS;
1109
+ const commandCompleted = feedback & (BoostPortFeedback.COMPLETED ^ BoostPortFeedback.DISCARDED);
1110
+ if (!isBusy && commandCompleted) {
1111
+ if (motor.status === BoostMotorState.ON_FOR_ROTATION) {
1112
+ motor.status = BoostMotorState.OFF;
1113
+ }
1114
+ }
1115
+ }
1116
+ break;
1117
+ }
1118
+ case BoostMessage.ERROR:
1119
+ log.warn(`Error reported by hub: ${data}`);
1120
+ break;
1121
+ }
1122
+ }
1123
+
1124
+ /**
1125
+ * Ping the Boost hub. If the Boost hub has disconnected
1126
+ * for some reason, the BLE socket will get an error back and automatically
1127
+ * close the socket.
1128
+ * @private
1129
+ */
1130
+ _pingDevice () {
1131
+ this._ble.read(
1132
+ BoostBLE.service,
1133
+ BoostBLE.characteristic,
1134
+ false
1135
+ );
1136
+ }
1137
+
1138
+ /**
1139
+ * Register a new sensor or motor connected at a port. Store the type of
1140
+ * sensor or motor internally, and then register for notifications on input
1141
+ * values if it is a sensor.
1142
+ * @param {number} portID - the port to register a sensor or motor on.
1143
+ * @param {number} type - the type ID of the sensor or motor
1144
+ * @private
1145
+ */
1146
+ _registerSensorOrMotor (portID, type) {
1147
+ // Record which port is connected to what type of device
1148
+ this._ports[portID] = type;
1149
+
1150
+ // Record motor port
1151
+ if (type === BoostIO.MOTORINT || type === BoostIO.MOTOREXT) {
1152
+ this._motors[portID] = new BoostMotor(this, portID);
1153
+ }
1154
+
1155
+ // Set input format for tilt or distance sensor
1156
+ let mode = null;
1157
+ let delta = 1;
1158
+
1159
+ switch (type) {
1160
+ case BoostIO.MOTORINT:
1161
+ case BoostIO.MOTOREXT:
1162
+ mode = BoostMode.MOTOR_SENSOR;
1163
+ break;
1164
+ case BoostIO.COLOR:
1165
+ mode = BoostMode.COLOR;
1166
+ delta = 0;
1167
+ break;
1168
+ case BoostIO.LED:
1169
+ mode = BoostMode.LED;
1170
+ /**
1171
+ * Sets the LED to blue to give an indication on the hub
1172
+ * that it has connected successfully.
1173
+ */
1174
+ this.setLEDMode();
1175
+ this.setLED(0x0000FF);
1176
+ break;
1177
+ case BoostIO.TILT:
1178
+ mode = BoostMode.TILT;
1179
+ break;
1180
+ default:
1181
+ mode = BoostMode.UNKNOWN;
1182
+ }
1183
+
1184
+ const cmd = this.generateInputCommand(
1185
+ portID,
1186
+ mode,
1187
+ delta,
1188
+ true // Receive feedback
1189
+ );
1190
+
1191
+ this.send(BoostBLE.characteristic, cmd);
1192
+ }
1193
+
1194
+ /**
1195
+ * Clear the sensors or motors present on the ports.
1196
+ * @param {number} portID - the port to clear.
1197
+ * @private
1198
+ */
1199
+ _clearPort (portID) {
1200
+ const type = this._ports[portID];
1201
+ if (type === BoostIO.TILT) {
1202
+ this._sensors.tiltX = this._sensors.tiltY = 0;
1203
+ }
1204
+ if (type === BoostIO.COLOR) {
1205
+ this._sensors.color = BoostColor.NONE;
1206
+ }
1207
+ this._ports[portID] = 'none';
1208
+ this._motors[portID] = null;
1209
+ }
1210
+ }
1211
+
1212
+ /**
1213
+ * Enum for motor specification.
1214
+ * @readonly
1215
+ * @enum {string}
1216
+ */
1217
+ const BoostMotorLabel = {
1218
+ A: 'A',
1219
+ B: 'B',
1220
+ C: 'C',
1221
+ D: 'D',
1222
+ AB: 'AB',
1223
+ ALL: 'ABCD'
1224
+ };
1225
+
1226
+ /**
1227
+ * Enum for motor direction specification.
1228
+ * @readonly
1229
+ * @enum {string}
1230
+ */
1231
+ const BoostMotorDirection = {
1232
+ FORWARD: 'this way',
1233
+ BACKWARD: 'that way',
1234
+ REVERSE: 'reverse'
1235
+ };
1236
+
1237
+ /**
1238
+ * Enum for tilt sensor direction.
1239
+ * @readonly
1240
+ * @enum {string}
1241
+ */
1242
+ const BoostTiltDirection = {
1243
+ UP: 'up',
1244
+ DOWN: 'down',
1245
+ LEFT: 'left',
1246
+ RIGHT: 'right',
1247
+ ANY: 'any'
1248
+ };
1249
+
1250
+ /**
1251
+ * Scratch 3.0 blocks to interact with a LEGO Boost peripheral.
1252
+ */
1253
+ class Scratch3BoostBlocks {
1254
+
1255
+ /**
1256
+ * @return {string} - the ID of this extension.
1257
+ */
1258
+ static get EXTENSION_ID () {
1259
+ return 'boost';
1260
+ }
1261
+
1262
+ /**
1263
+ * @return {number} - the tilt sensor counts as "tilted" if its tilt angle meets or exceeds this threshold.
1264
+ */
1265
+ static get TILT_THRESHOLD () {
1266
+ return 15;
1267
+ }
1268
+
1269
+ /**
1270
+ * Construct a set of Boost blocks.
1271
+ * @param {Runtime} runtime - the Scratch 3.0 runtime.
1272
+ */
1273
+ constructor (runtime) {
1274
+ /**
1275
+ * The Scratch 3.0 runtime.
1276
+ * @type {Runtime}
1277
+ */
1278
+ this.runtime = runtime;
1279
+
1280
+ // Create a new Boost peripheral instance
1281
+ this._peripheral = new Boost(this.runtime, Scratch3BoostBlocks.EXTENSION_ID);
1282
+ }
1283
+
1284
+ /**
1285
+ * @returns {object} metadata for this extension and its blocks.
1286
+ */
1287
+ getInfo () {
1288
+ return {
1289
+ id: Scratch3BoostBlocks.EXTENSION_ID,
1290
+ name: 'BOOST',
1291
+ blockIconURI: iconURI,
1292
+ showStatusButton: true,
1293
+ blocks: [
1294
+ {
1295
+ opcode: 'motorOnFor',
1296
+ text: formatMessage({
1297
+ id: 'boost.motorOnFor',
1298
+ default: 'turn motor [MOTOR_ID] for [DURATION] seconds',
1299
+ description: 'turn a motor on for some time'
1300
+ }),
1301
+ blockType: BlockType.COMMAND,
1302
+ arguments: {
1303
+ MOTOR_ID: {
1304
+ type: ArgumentType.STRING,
1305
+ menu: 'MOTOR_ID',
1306
+ defaultValue: BoostMotorLabel.A
1307
+ },
1308
+ DURATION: {
1309
+ type: ArgumentType.NUMBER,
1310
+ defaultValue: 1
1311
+ }
1312
+ }
1313
+ },
1314
+ {
1315
+ opcode: 'motorOnForRotation',
1316
+ text: formatMessage({
1317
+ id: 'boost.motorOnForRotation',
1318
+ default: 'turn motor [MOTOR_ID] for [ROTATION] rotations',
1319
+ description: 'turn a motor on for rotation'
1320
+ }),
1321
+ blockType: BlockType.COMMAND,
1322
+ arguments: {
1323
+ MOTOR_ID: {
1324
+ type: ArgumentType.STRING,
1325
+ menu: 'MOTOR_ID',
1326
+ defaultValue: BoostMotorLabel.A
1327
+ },
1328
+ ROTATION: {
1329
+ type: ArgumentType.NUMBER,
1330
+ defaultValue: 1
1331
+ }
1332
+ }
1333
+ },
1334
+ {
1335
+ opcode: 'motorOn',
1336
+ text: formatMessage({
1337
+ id: 'boost.motorOn',
1338
+ default: 'turn motor [MOTOR_ID] on',
1339
+ description: 'turn a motor on indefinitely'
1340
+ }),
1341
+ blockType: BlockType.COMMAND,
1342
+ arguments: {
1343
+ MOTOR_ID: {
1344
+ type: ArgumentType.STRING,
1345
+ menu: 'MOTOR_ID',
1346
+ defaultValue: BoostMotorLabel.A
1347
+ }
1348
+ }
1349
+ },
1350
+ {
1351
+ opcode: 'motorOff',
1352
+ text: formatMessage({
1353
+ id: 'boost.motorOff',
1354
+ default: 'turn motor [MOTOR_ID] off',
1355
+ description: 'turn a motor off'
1356
+ }),
1357
+ blockType: BlockType.COMMAND,
1358
+ arguments: {
1359
+ MOTOR_ID: {
1360
+ type: ArgumentType.STRING,
1361
+ menu: 'MOTOR_ID',
1362
+ defaultValue: BoostMotorLabel.A
1363
+ }
1364
+ }
1365
+ },
1366
+ {
1367
+ opcode: 'setMotorPower',
1368
+ text: formatMessage({
1369
+ id: 'boost.setMotorPower',
1370
+ default: 'set motor [MOTOR_ID] speed to [POWER] %',
1371
+ description: 'set the motor\'s speed without turning it on'
1372
+ }),
1373
+ blockType: BlockType.COMMAND,
1374
+ arguments: {
1375
+ MOTOR_ID: {
1376
+ type: ArgumentType.STRING,
1377
+ menu: 'MOTOR_ID',
1378
+ defaultValue: BoostMotorLabel.ALL
1379
+ },
1380
+ POWER: {
1381
+ type: ArgumentType.NUMBER,
1382
+ defaultValue: 100
1383
+ }
1384
+ }
1385
+ },
1386
+ {
1387
+ opcode: 'setMotorDirection',
1388
+ text: formatMessage({
1389
+ id: 'boost.setMotorDirection',
1390
+ default: 'set motor [MOTOR_ID] direction [MOTOR_DIRECTION]',
1391
+ description: 'set the motor\'s turn direction without turning it on'
1392
+ }),
1393
+ blockType: BlockType.COMMAND,
1394
+ arguments: {
1395
+ MOTOR_ID: {
1396
+ type: ArgumentType.STRING,
1397
+ menu: 'MOTOR_ID',
1398
+ defaultValue: BoostMotorLabel.A
1399
+ },
1400
+ MOTOR_DIRECTION: {
1401
+ type: ArgumentType.STRING,
1402
+ menu: 'MOTOR_DIRECTION',
1403
+ defaultValue: BoostMotorDirection.FORWARD
1404
+ }
1405
+ }
1406
+ },
1407
+ {
1408
+ opcode: 'getMotorPosition',
1409
+ text: formatMessage({
1410
+ id: 'boost.getMotorPosition',
1411
+ default: 'motor [MOTOR_REPORTER_ID] position',
1412
+ description: 'the position returned by the motor'
1413
+ }),
1414
+ blockType: BlockType.REPORTER,
1415
+ arguments: {
1416
+ MOTOR_REPORTER_ID: {
1417
+ type: ArgumentType.STRING,
1418
+ menu: 'MOTOR_REPORTER_ID',
1419
+ defaultValue: BoostMotorLabel.A
1420
+ }
1421
+ }
1422
+ },
1423
+ {
1424
+ opcode: 'whenColor',
1425
+ text: formatMessage({
1426
+ id: 'boost.whenColor',
1427
+ default: 'when [COLOR] brick seen',
1428
+ description: 'check for when color'
1429
+ }),
1430
+ blockType: BlockType.HAT,
1431
+ arguments: {
1432
+ COLOR: {
1433
+ type: ArgumentType.STRING,
1434
+ menu: 'COLOR',
1435
+ defaultValue: BoostColor.ANY
1436
+ }
1437
+ }
1438
+ },
1439
+ {
1440
+ opcode: 'seeingColor',
1441
+ text: formatMessage({
1442
+ id: 'boost.seeingColor',
1443
+ default: 'seeing [COLOR] brick?',
1444
+ description: 'is the color sensor seeing a certain color?'
1445
+ }),
1446
+ blockType: BlockType.BOOLEAN,
1447
+ arguments: {
1448
+ COLOR: {
1449
+ type: ArgumentType.STRING,
1450
+ menu: 'COLOR',
1451
+ defaultValue: BoostColor.ANY
1452
+ }
1453
+ }
1454
+ },
1455
+ {
1456
+ opcode: 'whenTilted',
1457
+ text: formatMessage({
1458
+ id: 'boost.whenTilted',
1459
+ default: 'when tilted [TILT_DIRECTION_ANY]',
1460
+ description: 'check when tilted in a certain direction'
1461
+ }),
1462
+ func: 'isTilted',
1463
+ blockType: BlockType.HAT,
1464
+ arguments: {
1465
+ TILT_DIRECTION_ANY: {
1466
+ type: ArgumentType.STRING,
1467
+ menu: 'TILT_DIRECTION_ANY',
1468
+ defaultValue: BoostTiltDirection.ANY
1469
+ }
1470
+ }
1471
+ },
1472
+ {
1473
+ opcode: 'getTiltAngle',
1474
+ text: formatMessage({
1475
+ id: 'boost.getTiltAngle',
1476
+ default: 'tilt angle [TILT_DIRECTION]',
1477
+ description: 'the angle returned by the tilt sensor'
1478
+ }),
1479
+ blockType: BlockType.REPORTER,
1480
+ arguments: {
1481
+ TILT_DIRECTION: {
1482
+ type: ArgumentType.STRING,
1483
+ menu: 'TILT_DIRECTION',
1484
+ defaultValue: BoostTiltDirection.UP
1485
+ }
1486
+ }
1487
+ },
1488
+ {
1489
+ opcode: 'setLightHue',
1490
+ text: formatMessage({
1491
+ id: 'boost.setLightHue',
1492
+ default: 'set light color to [HUE]',
1493
+ description: 'set the LED color'
1494
+ }),
1495
+ blockType: BlockType.COMMAND,
1496
+ arguments: {
1497
+ HUE: {
1498
+ type: ArgumentType.NUMBER,
1499
+ defaultValue: 50
1500
+ }
1501
+ }
1502
+ }
1503
+ ],
1504
+ menus: {
1505
+ MOTOR_ID: {
1506
+ acceptReporters: true,
1507
+ items: [
1508
+ {
1509
+ text: 'A',
1510
+ value: BoostMotorLabel.A
1511
+ },
1512
+ {
1513
+ text: 'B',
1514
+ value: BoostMotorLabel.B
1515
+ },
1516
+ {
1517
+ text: 'C',
1518
+ value: BoostMotorLabel.C
1519
+ },
1520
+ {
1521
+ text: 'D',
1522
+ value: BoostMotorLabel.D
1523
+ },
1524
+ {
1525
+ text: 'AB',
1526
+ value: BoostMotorLabel.AB
1527
+ },
1528
+ {
1529
+ text: 'ABCD',
1530
+ value: BoostMotorLabel.ALL
1531
+ }
1532
+ ]
1533
+ },
1534
+ MOTOR_REPORTER_ID: {
1535
+ acceptReporters: true,
1536
+ items: [
1537
+ {
1538
+ text: 'A',
1539
+ value: BoostMotorLabel.A
1540
+ },
1541
+ {
1542
+ text: 'B',
1543
+ value: BoostMotorLabel.B
1544
+ },
1545
+ {
1546
+ text: 'C',
1547
+ value: BoostMotorLabel.C
1548
+ },
1549
+ {
1550
+ text: 'D',
1551
+ value: BoostMotorLabel.D
1552
+ }
1553
+ ]
1554
+ },
1555
+ MOTOR_DIRECTION: {
1556
+ acceptReporters: true,
1557
+ items: [
1558
+ {
1559
+ text: formatMessage({
1560
+ id: 'boost.motorDirection.forward',
1561
+ default: 'this way',
1562
+ description:
1563
+ 'label for forward element in motor direction menu for LEGO Boost extension'
1564
+ }),
1565
+ value: BoostMotorDirection.FORWARD
1566
+ },
1567
+ {
1568
+ text: formatMessage({
1569
+ id: 'boost.motorDirection.backward',
1570
+ default: 'that way',
1571
+ description:
1572
+ 'label for backward element in motor direction menu for LEGO Boost extension'
1573
+ }),
1574
+ value: BoostMotorDirection.BACKWARD
1575
+ },
1576
+ {
1577
+ text: formatMessage({
1578
+ id: 'boost.motorDirection.reverse',
1579
+ default: 'reverse',
1580
+ description:
1581
+ 'label for reverse element in motor direction menu for LEGO Boost extension'
1582
+ }),
1583
+ value: BoostMotorDirection.REVERSE
1584
+ }
1585
+ ]
1586
+ },
1587
+ TILT_DIRECTION: {
1588
+ acceptReporters: true,
1589
+ items: [
1590
+ {
1591
+ text: formatMessage({
1592
+ id: 'boost.tiltDirection.up',
1593
+ default: 'up',
1594
+ description: 'label for up element in tilt direction menu for LEGO Boost extension'
1595
+ }),
1596
+ value: BoostTiltDirection.UP
1597
+ },
1598
+ {
1599
+ text: formatMessage({
1600
+ id: 'boost.tiltDirection.down',
1601
+ default: 'down',
1602
+ description: 'label for down element in tilt direction menu for LEGO Boost extension'
1603
+ }),
1604
+ value: BoostTiltDirection.DOWN
1605
+ },
1606
+ {
1607
+ text: formatMessage({
1608
+ id: 'boost.tiltDirection.left',
1609
+ default: 'left',
1610
+ description: 'label for left element in tilt direction menu for LEGO Boost extension'
1611
+ }),
1612
+ value: BoostTiltDirection.LEFT
1613
+ },
1614
+ {
1615
+ text: formatMessage({
1616
+ id: 'boost.tiltDirection.right',
1617
+ default: 'right',
1618
+ description: 'label for right element in tilt direction menu for LEGO Boost extension'
1619
+ }),
1620
+ value: BoostTiltDirection.RIGHT
1621
+ }
1622
+ ]
1623
+ },
1624
+ TILT_DIRECTION_ANY: {
1625
+ acceptReporters: true,
1626
+ items: [
1627
+ {
1628
+ text: formatMessage({
1629
+ id: 'boost.tiltDirection.up',
1630
+ default: 'up'
1631
+ }),
1632
+ value: BoostTiltDirection.UP
1633
+ },
1634
+ {
1635
+ text: formatMessage({
1636
+ id: 'boost.tiltDirection.down',
1637
+ default: 'down'
1638
+ }),
1639
+ value: BoostTiltDirection.DOWN
1640
+ },
1641
+ {
1642
+ text: formatMessage({
1643
+ id: 'boost.tiltDirection.left',
1644
+ default: 'left'
1645
+ }),
1646
+ value: BoostTiltDirection.LEFT
1647
+ },
1648
+ {
1649
+ text: formatMessage({
1650
+ id: 'boost.tiltDirection.right',
1651
+ default: 'right'
1652
+ }),
1653
+ value: BoostTiltDirection.RIGHT
1654
+ },
1655
+ {
1656
+ text: formatMessage({
1657
+ id: 'boost.tiltDirection.any',
1658
+ default: 'any',
1659
+ description: 'label for any element in tilt direction menu for LEGO Boost extension'
1660
+ }),
1661
+ value: BoostTiltDirection.ANY
1662
+ }
1663
+ ]
1664
+ },
1665
+ COLOR: {
1666
+ acceptReporters: true,
1667
+ items: [
1668
+ {
1669
+ text: formatMessage({
1670
+ id: 'boost.color.red',
1671
+ default: 'red',
1672
+ description: 'the color red'
1673
+ }),
1674
+ value: BoostColor.RED
1675
+ },
1676
+ {
1677
+ text: formatMessage({
1678
+ id: 'boost.color.blue',
1679
+ default: 'blue',
1680
+ description: 'the color blue'
1681
+ }),
1682
+ value: BoostColor.BLUE
1683
+ },
1684
+ {
1685
+ text: formatMessage({
1686
+ id: 'boost.color.green',
1687
+ default: 'green',
1688
+ description: 'the color green'
1689
+ }),
1690
+ value: BoostColor.GREEN
1691
+ },
1692
+ {
1693
+ text: formatMessage({
1694
+ id: 'boost.color.yellow',
1695
+ default: 'yellow',
1696
+ description: 'the color yellow'
1697
+ }),
1698
+ value: BoostColor.YELLOW
1699
+ },
1700
+ {
1701
+ text: formatMessage({
1702
+ id: 'boost.color.white',
1703
+ default: 'white',
1704
+ desription: 'the color white'
1705
+ }),
1706
+ value: BoostColor.WHITE
1707
+ },
1708
+ {
1709
+ text: formatMessage({
1710
+ id: 'boost.color.black',
1711
+ default: 'black',
1712
+ description: 'the color black'
1713
+ }),
1714
+ value: BoostColor.BLACK
1715
+ },
1716
+ {
1717
+ text: formatMessage({
1718
+ id: 'boost.color.any',
1719
+ default: 'any color',
1720
+ description: 'any color'
1721
+ }),
1722
+ value: BoostColor.ANY
1723
+ }
1724
+ ]
1725
+ }
1726
+ }
1727
+ };
1728
+ }
1729
+
1730
+ /**
1731
+ * Turn specified motor(s) on for a specified duration.
1732
+ * @param {object} args - the block's arguments.
1733
+ * @property {MotorID} MOTOR_ID - the motor(s) to activate.
1734
+ * @property {int} DURATION - the amount of time to run the motors.
1735
+ * @return {Promise} - a promise which will resolve at the end of the duration.
1736
+ */
1737
+ motorOnFor (args) {
1738
+ // TODO: cast args.MOTOR_ID?
1739
+ let durationMS = Cast.toNumber(args.DURATION) * 1000;
1740
+ durationMS = MathUtil.clamp(durationMS, 0, 15000);
1741
+ return new Promise(resolve => {
1742
+ this._forEachMotor(args.MOTOR_ID, motorIndex => {
1743
+ const motor = this._peripheral.motor(motorIndex);
1744
+ if (motor) motor.turnOnFor(durationMS);
1745
+ });
1746
+
1747
+ // Run for some time even when no motor is connected
1748
+ setTimeout(resolve, durationMS);
1749
+ });
1750
+ }
1751
+
1752
+ /**
1753
+ * Turn specified motor(s) on for a specified rotation in full rotations.
1754
+ * @param {object} args - the block's arguments.
1755
+ * @property {MotorID} MOTOR_ID - the motor(s) to activate.
1756
+ * @property {int} ROTATION - the amount of full rotations to turn the motors.
1757
+ * @return {Promise} - a promise which will resolve at the end of the duration.
1758
+ */
1759
+ motorOnForRotation (args) {
1760
+ // TODO: cast args.MOTOR_ID?
1761
+ let degrees = Cast.toNumber(args.ROTATION) * 360;
1762
+ // TODO: Clamps to 100 rotations. Consider changing.
1763
+ const sign = Math.sign(degrees);
1764
+ degrees = Math.abs(MathUtil.clamp(degrees, -360000, 360000));
1765
+
1766
+ const motors = [];
1767
+ this._forEachMotor(args.MOTOR_ID, motorIndex => {
1768
+ motors.push(motorIndex);
1769
+ });
1770
+
1771
+ /**
1772
+ * Checks that the motors given in args.MOTOR_ID exist,
1773
+ * and maps a promise for each of the motor-commands to an array.
1774
+ */
1775
+ const promises = motors.map(portID => {
1776
+ const motor = this._peripheral.motor(portID);
1777
+ if (motor) {
1778
+ // to avoid a hanging block if power is 0, return an immediately resolving promise.
1779
+ if (motor.power === 0) return Promise.resolve();
1780
+ return new Promise(resolve => {
1781
+ motor.turnOnForDegrees(degrees, sign);
1782
+ motor.pendingRotationPromise = resolve;
1783
+ });
1784
+ }
1785
+ return null;
1786
+ });
1787
+ /**
1788
+ * Make sure all promises are resolved, i.e. all motor-commands have completed.
1789
+ * To prevent the block from returning a value, an empty function is added to the .then
1790
+ */
1791
+ return Promise.all(promises).then(() => {});
1792
+ }
1793
+
1794
+ /**
1795
+ * Turn specified motor(s) on indefinitely.
1796
+ * @param {object} args - the block's arguments.
1797
+ * @property {MotorID} MOTOR_ID - the motor(s) to activate.
1798
+ * @return {Promise} - a Promise that resolves after some delay.
1799
+ */
1800
+ motorOn (args) {
1801
+ // TODO: cast args.MOTOR_ID?
1802
+ this._forEachMotor(args.MOTOR_ID, motorIndex => {
1803
+ const motor = this._peripheral.motor(motorIndex);
1804
+ if (motor) motor.turnOnForever();
1805
+ });
1806
+
1807
+ return new Promise(resolve => {
1808
+ window.setTimeout(() => {
1809
+ resolve();
1810
+ }, BoostBLE.sendInterval);
1811
+ });
1812
+ }
1813
+
1814
+ /**
1815
+ * Turn specified motor(s) off.
1816
+ * @param {object} args - the block's arguments.
1817
+ * @property {MotorID} MOTOR_ID - the motor(s) to deactivate.
1818
+ * @return {Promise} - a Promise that resolves after some delay.
1819
+ */
1820
+ motorOff (args) {
1821
+ // TODO: cast args.MOTOR_ID?
1822
+ this._forEachMotor(args.MOTOR_ID, motorIndex => {
1823
+ const motor = this._peripheral.motor(motorIndex);
1824
+ if (motor) motor.turnOff();
1825
+ });
1826
+
1827
+ return new Promise(resolve => {
1828
+ window.setTimeout(() => {
1829
+ resolve();
1830
+ }, BoostBLE.sendInterval);
1831
+ });
1832
+ }
1833
+
1834
+ /**
1835
+ * Set the power level of the specified motor(s).
1836
+ * @param {object} args - the block's arguments.
1837
+ * @property {MotorID} MOTOR_ID - the motor(s) to be affected.
1838
+ * @property {int} POWER - the new power level for the motor(s).
1839
+ * @return {Promise} - returns a promise to make sure the block yields.
1840
+ */
1841
+ setMotorPower (args) {
1842
+ // TODO: cast args.MOTOR_ID?
1843
+ this._forEachMotor(args.MOTOR_ID, motorIndex => {
1844
+ const motor = this._peripheral.motor(motorIndex);
1845
+ if (motor) {
1846
+ motor.power = MathUtil.clamp(Cast.toNumber(args.POWER), 0, 100);
1847
+ switch (motor.status) {
1848
+ case BoostMotorState.ON_FOREVER:
1849
+ motor.turnOnForever();
1850
+ break;
1851
+ case BoostMotorState.ON_FOR_TIME:
1852
+ motor.turnOnFor(motor.pendingDurationTimeoutStartTime +
1853
+ motor.pendingDurationTimeoutDelay - Date.now());
1854
+ break;
1855
+ }
1856
+ }
1857
+ });
1858
+ return new Promise(resolve => {
1859
+ window.setTimeout(() => {
1860
+ resolve();
1861
+ }, BoostBLE.sendInterval);
1862
+ });
1863
+ }
1864
+
1865
+ /**
1866
+ * Set the direction of rotation for specified motor(s).
1867
+ * If the direction is 'reverse' the motor(s) will be reversed individually.
1868
+ * @param {object} args - the block's arguments.
1869
+ * @property {MotorID} MOTOR_ID - the motor(s) to be affected.
1870
+ * @property {MotorDirection} MOTOR_DIRECTION - the new direction for the motor(s).
1871
+ * @return {Promise} - returns a promise to make sure the block yields.
1872
+ */
1873
+ setMotorDirection (args) {
1874
+ // TODO: cast args.MOTOR_ID?
1875
+ this._forEachMotor(args.MOTOR_ID, motorIndex => {
1876
+ const motor = this._peripheral.motor(motorIndex);
1877
+ if (motor) {
1878
+ switch (args.MOTOR_DIRECTION) {
1879
+ case BoostMotorDirection.FORWARD:
1880
+ motor.direction = 1;
1881
+ break;
1882
+ case BoostMotorDirection.BACKWARD:
1883
+ motor.direction = -1;
1884
+ break;
1885
+ case BoostMotorDirection.REVERSE:
1886
+ motor.direction = -motor.direction;
1887
+ break;
1888
+ default:
1889
+ log.warn(`Unknown motor direction in setMotorDirection: ${args.DIRECTION}`);
1890
+ break;
1891
+ }
1892
+ // keep the motor on if it's running, and update the pending timeout if needed
1893
+ if (motor) {
1894
+ switch (motor.status) {
1895
+ case BoostMotorState.ON_FOREVER:
1896
+ motor.turnOnForever();
1897
+ break;
1898
+ case BoostMotorState.ON_FOR_TIME:
1899
+ motor.turnOnFor(motor.pendingDurationTimeoutStartTime +
1900
+ motor.pendingDurationTimeoutDelay - Date.now());
1901
+ break;
1902
+ }
1903
+ }
1904
+ }
1905
+ });
1906
+ return new Promise(resolve => {
1907
+ window.setTimeout(() => {
1908
+ resolve();
1909
+ }, BoostBLE.sendInterval);
1910
+ });
1911
+ }
1912
+
1913
+ /**
1914
+ * @param {object} args - the block's arguments.
1915
+ * @return {number} - returns the motor's position.
1916
+ */
1917
+ getMotorPosition (args) {
1918
+ let portID = null;
1919
+ switch (args.MOTOR_REPORTER_ID) {
1920
+
1921
+ case BoostMotorLabel.A:
1922
+ portID = BoostPort.A;
1923
+ break;
1924
+ case BoostMotorLabel.B:
1925
+ portID = BoostPort.B;
1926
+ break;
1927
+ case BoostMotorLabel.C:
1928
+ portID = BoostPort.C;
1929
+ break;
1930
+ case BoostMotorLabel.D:
1931
+ portID = BoostPort.D;
1932
+ break;
1933
+ default:
1934
+ log.warn('Asked for a motor position that doesnt exist!');
1935
+ return false;
1936
+ }
1937
+ if (portID !== null && this._peripheral.motor(portID)) {
1938
+ let val = this._peripheral.motor(portID).position;
1939
+ // Boost motor A position direction is reversed by design
1940
+ // so we have to reverse the position here
1941
+ if (portID === BoostPort.A) {
1942
+ val *= -1;
1943
+ }
1944
+ return MathUtil.wrapClamp(val, 0, 360);
1945
+ }
1946
+ return 0;
1947
+ }
1948
+
1949
+ /**
1950
+ * Call a callback for each motor indexed by the provided motor ID.
1951
+ * @param {MotorID} motorID - the ID specifier.
1952
+ * @param {Function} callback - the function to call with the numeric motor index for each motor.
1953
+ * @private
1954
+ */
1955
+ _forEachMotor (motorID, callback) {
1956
+ let motors;
1957
+ switch (motorID) {
1958
+ case BoostMotorLabel.A:
1959
+ motors = [BoostPort.A];
1960
+ break;
1961
+ case BoostMotorLabel.B:
1962
+ motors = [BoostPort.B];
1963
+ break;
1964
+ case BoostMotorLabel.C:
1965
+ motors = [BoostPort.C];
1966
+ break;
1967
+ case BoostMotorLabel.D:
1968
+ motors = [BoostPort.D];
1969
+ break;
1970
+ case BoostMotorLabel.AB:
1971
+ motors = [BoostPort.A, BoostPort.B];
1972
+ break;
1973
+ case BoostMotorLabel.ALL:
1974
+ motors = [BoostPort.A, BoostPort.B, BoostPort.C, BoostPort.D];
1975
+ break;
1976
+ default:
1977
+ log.warn(`Invalid motor ID: ${motorID}`);
1978
+ motors = [];
1979
+ break;
1980
+ }
1981
+ for (const index of motors) {
1982
+ callback(index);
1983
+ }
1984
+ }
1985
+
1986
+ /**
1987
+ * Test whether the tilt sensor is currently tilted.
1988
+ * @param {object} args - the block's arguments.
1989
+ * @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any).
1990
+ * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
1991
+ */
1992
+ whenTilted (args) {
1993
+ return this._isTilted(args.TILT_DIRECTION_ANY);
1994
+ }
1995
+
1996
+ /**
1997
+ * Test whether the tilt sensor is currently tilted.
1998
+ * @param {object} args - the block's arguments.
1999
+ * @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any).
2000
+ * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
2001
+ */
2002
+ isTilted (args) {
2003
+ return this._isTilted(args.TILT_DIRECTION_ANY);
2004
+ }
2005
+
2006
+ /**
2007
+ * @param {object} args - the block's arguments.
2008
+ * @property {TiltDirection} TILT_DIRECTION - the direction (up, down, left, right) to check.
2009
+ * @return {number} - the tilt sensor's angle in the specified direction.
2010
+ * Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right).
2011
+ */
2012
+ getTiltAngle (args) {
2013
+ return this._getTiltAngle(args.TILT_DIRECTION);
2014
+ }
2015
+
2016
+ /**
2017
+ * Test whether the tilt sensor is currently tilted.
2018
+ * @param {TiltDirection} direction - the tilt direction to test (up, down, left, right, or any).
2019
+ * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
2020
+ * @private
2021
+ */
2022
+ _isTilted (direction) {
2023
+ switch (direction) {
2024
+ case BoostTiltDirection.ANY:
2025
+ return (Math.abs(this._peripheral.tiltX) >= Scratch3BoostBlocks.TILT_THRESHOLD) ||
2026
+ (Math.abs(this._peripheral.tiltY) >= Scratch3BoostBlocks.TILT_THRESHOLD);
2027
+ default:
2028
+ return this._getTiltAngle(direction) >= Scratch3BoostBlocks.TILT_THRESHOLD;
2029
+ }
2030
+ }
2031
+
2032
+ /**
2033
+ * @param {TiltDirection} direction - the direction (up, down, left, right) to check.
2034
+ * @return {number} - the tilt sensor's angle in the specified direction.
2035
+ * Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right).
2036
+ * @private
2037
+ */
2038
+ _getTiltAngle (direction) {
2039
+ switch (direction) {
2040
+ case BoostTiltDirection.UP:
2041
+ return this._peripheral.tiltY > 90 ? 256 - this._peripheral.tiltY : -this._peripheral.tiltY;
2042
+ case BoostTiltDirection.DOWN:
2043
+ return this._peripheral.tiltY > 90 ? this._peripheral.tiltY - 256 : this._peripheral.tiltY;
2044
+ case BoostTiltDirection.LEFT:
2045
+ return this._peripheral.tiltX > 90 ? this._peripheral.tiltX - 256 : this._peripheral.tiltX;
2046
+ case BoostTiltDirection.RIGHT:
2047
+ return this._peripheral.tiltX > 90 ? 256 - this._peripheral.tiltX : -this._peripheral.tiltX;
2048
+ default:
2049
+ log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`);
2050
+ }
2051
+ }
2052
+
2053
+ /**
2054
+ * Edge-triggering hat function, for when the vision sensor is detecting
2055
+ * a certain color.
2056
+ * @param {object} args - the block's arguments.
2057
+ * @return {boolean} - true when the color sensor senses the specified color.
2058
+ */
2059
+ whenColor (args) {
2060
+ if (args.COLOR === BoostColor.ANY) {
2061
+ // For "any" color, return true if the color is not "none", and
2062
+ // the color is different from the previous color detected. This
2063
+ // allows the hat to trigger when the color changes from one color
2064
+ // to another.
2065
+ return this._peripheral.color !== BoostColor.NONE &&
2066
+ this._peripheral.color !== this._peripheral.previousColor;
2067
+ }
2068
+
2069
+ return args.COLOR === this._peripheral.color;
2070
+ }
2071
+
2072
+ /**
2073
+ * A boolean reporter function, for whether the vision sensor is detecting
2074
+ * a certain color.
2075
+ * @param {object} args - the block's arguments.
2076
+ * @return {boolean} - true when the color sensor senses the specified color.
2077
+ */
2078
+ seeingColor (args) {
2079
+ if (args.COLOR === BoostColor.ANY) {
2080
+ return this._peripheral.color !== BoostColor.NONE;
2081
+ }
2082
+
2083
+ return args.COLOR === this._peripheral.color;
2084
+ }
2085
+
2086
+ /**
2087
+ * Set the LED's hue.
2088
+ * @param {object} args - the block's arguments.
2089
+ * @property {number} HUE - the hue to set, in the range [0,100].
2090
+ * @return {Promise} - a Promise that resolves after some delay.
2091
+ */
2092
+ setLightHue (args) {
2093
+ // Convert from [0,100] to [0,360]
2094
+ let inputHue = Cast.toNumber(args.HUE);
2095
+ inputHue = MathUtil.wrapClamp(inputHue, 0, 100);
2096
+ const hue = inputHue * 360 / 100;
2097
+
2098
+ const rgbObject = color.hsvToRgb({h: hue, s: 1, v: 1});
2099
+
2100
+ const rgbDecimal = color.rgbToDecimal(rgbObject);
2101
+
2102
+ this._peripheral._led = inputHue;
2103
+ this._peripheral.setLED(rgbDecimal);
2104
+
2105
+ return new Promise(resolve => {
2106
+ window.setTimeout(() => {
2107
+ resolve();
2108
+ }, BoostBLE.sendInterval);
2109
+ });
2110
+ }
2111
+ }
2112
+
2113
+ module.exports = Scratch3BoostBlocks;