@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,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;