@scratch/scratch-vm 11.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/.jsdoc.json +20 -0
  2. package/.nvmrc +1 -0
  3. package/CHANGELOG.md +7004 -0
  4. package/LICENSE +12 -0
  5. package/README.md +120 -0
  6. package/TRADEMARK +1 -0
  7. package/commitlint.config.js +4 -0
  8. package/docs/extensions.md +527 -0
  9. package/package.json +103 -0
  10. package/release.config.js +13 -0
  11. package/src/blocks/scratch3_control.js +206 -0
  12. package/src/blocks/scratch3_core_example.js +69 -0
  13. package/src/blocks/scratch3_data.js +257 -0
  14. package/src/blocks/scratch3_event.js +137 -0
  15. package/src/blocks/scratch3_looks.js +612 -0
  16. package/src/blocks/scratch3_motion.js +288 -0
  17. package/src/blocks/scratch3_operators.js +154 -0
  18. package/src/blocks/scratch3_procedures.js +79 -0
  19. package/src/blocks/scratch3_sensing.js +336 -0
  20. package/src/blocks/scratch3_sound.js +350 -0
  21. package/src/dispatch/central-dispatch.js +143 -0
  22. package/src/dispatch/shared-dispatch.js +235 -0
  23. package/src/dispatch/worker-dispatch.js +110 -0
  24. package/src/engine/adapter.js +176 -0
  25. package/src/engine/block-utility.js +242 -0
  26. package/src/engine/blocks-execute-cache.js +19 -0
  27. package/src/engine/blocks-runtime-cache.js +78 -0
  28. package/src/engine/blocks.js +1299 -0
  29. package/src/engine/comment.js +56 -0
  30. package/src/engine/execute.js +580 -0
  31. package/src/engine/monitor-record.js +23 -0
  32. package/src/engine/mutation-adapter.js +48 -0
  33. package/src/engine/profiler.js +390 -0
  34. package/src/engine/runtime.js +2686 -0
  35. package/src/engine/scratch-blocks-constants.js +27 -0
  36. package/src/engine/sequencer.js +361 -0
  37. package/src/engine/stage-layering.js +29 -0
  38. package/src/engine/target.js +799 -0
  39. package/src/engine/thread.js +404 -0
  40. package/src/engine/variable.js +70 -0
  41. package/src/extension-support/argument-type.js +47 -0
  42. package/src/extension-support/block-type.js +50 -0
  43. package/src/extension-support/define-messages.js +18 -0
  44. package/src/extension-support/extension-manager.js +440 -0
  45. package/src/extension-support/extension-metadata.js +64 -0
  46. package/src/extension-support/extension-worker.js +59 -0
  47. package/src/extension-support/reporter-scope.js +18 -0
  48. package/src/extension-support/target-type.js +17 -0
  49. package/src/extensions/scratch3_boost/index.js +2113 -0
  50. package/src/extensions/scratch3_ev3/index.js +1355 -0
  51. package/src/extensions/scratch3_gdx_for/index.js +981 -0
  52. package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
  53. package/src/extensions/scratch3_makeymakey/index.js +396 -0
  54. package/src/extensions/scratch3_microbit/index.js +984 -0
  55. package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
  56. package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
  57. package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
  58. package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
  59. package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
  60. package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
  61. package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
  62. package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
  63. package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
  64. package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
  65. package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
  66. package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
  67. package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
  68. package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
  69. package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
  70. package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
  71. package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
  72. package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
  73. package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
  74. package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
  75. package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
  76. package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
  77. package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
  78. package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
  79. package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
  80. package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
  81. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
  82. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
  83. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
  84. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
  85. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
  86. package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
  87. package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
  88. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
  89. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
  90. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
  91. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
  92. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
  93. package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
  94. package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
  95. package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
  96. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
  97. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
  98. package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
  99. package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
  100. package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
  101. package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
  102. package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
  103. package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
  104. package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
  105. package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
  106. package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
  107. package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
  108. package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
  109. package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
  110. package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
  111. package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
  112. package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
  113. package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
  114. package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
  115. package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
  116. package/src/extensions/scratch3_music/index.js +1333 -0
  117. package/src/extensions/scratch3_music/manifest.js +63 -0
  118. package/src/extensions/scratch3_pen/index.js +770 -0
  119. package/src/extensions/scratch3_speech2text/index.js +700 -0
  120. package/src/extensions/scratch3_text2speech/index.js +766 -0
  121. package/src/extensions/scratch3_translate/index.js +286 -0
  122. package/src/extensions/scratch3_video_sensing/debug.js +13 -0
  123. package/src/extensions/scratch3_video_sensing/index.js +594 -0
  124. package/src/extensions/scratch3_video_sensing/library.js +384 -0
  125. package/src/extensions/scratch3_video_sensing/math.js +76 -0
  126. package/src/extensions/scratch3_video_sensing/view.js +509 -0
  127. package/src/extensions/scratch3_wedo2/index.js +1616 -0
  128. package/src/import/load-costume.js +416 -0
  129. package/src/import/load-sound.js +116 -0
  130. package/src/index.js +10 -0
  131. package/src/io/ble.js +256 -0
  132. package/src/io/bt.js +202 -0
  133. package/src/io/clock.js +39 -0
  134. package/src/io/cloud.js +169 -0
  135. package/src/io/keyboard.js +146 -0
  136. package/src/io/mouse.js +145 -0
  137. package/src/io/mouseWheel.js +28 -0
  138. package/src/io/userData.js +24 -0
  139. package/src/io/video.js +210 -0
  140. package/src/playground/benchmark.css +83 -0
  141. package/src/playground/benchmark.js +753 -0
  142. package/src/playground/index.html +111 -0
  143. package/src/playground/suite.css +78 -0
  144. package/src/playground/suite.html +25 -0
  145. package/src/playground/suite.js +544 -0
  146. package/src/playground/video-sensing.html +18 -0
  147. package/src/playground/video-sensing.js +133 -0
  148. package/src/serialization/deserialize-assets.js +178 -0
  149. package/src/serialization/sb2.js +1295 -0
  150. package/src/serialization/sb2_specmap.js +1818 -0
  151. package/src/serialization/sb3.js +1320 -0
  152. package/src/serialization/serialize-assets.js +60 -0
  153. package/src/sprites/rendered-target.js +1115 -0
  154. package/src/sprites/sprite.js +177 -0
  155. package/src/util/base64-util.js +48 -0
  156. package/src/util/cast.js +218 -0
  157. package/src/util/clone.js +17 -0
  158. package/src/util/color.js +204 -0
  159. package/src/util/fetch-with-timeout.js +53 -0
  160. package/src/util/get-monitor-id.js +33 -0
  161. package/src/util/jsonrpc.js +114 -0
  162. package/src/util/log.js +4 -0
  163. package/src/util/math-util.js +122 -0
  164. package/src/util/maybe-format-message.js +18 -0
  165. package/src/util/new-block-ids.js +33 -0
  166. package/src/util/rateLimiter.js +73 -0
  167. package/src/util/scratch-link-websocket.js +133 -0
  168. package/src/util/string-util.js +94 -0
  169. package/src/util/task-queue.js +203 -0
  170. package/src/util/timer.js +103 -0
  171. package/src/util/uid.js +29 -0
  172. package/src/util/variable-util.js +48 -0
  173. package/src/util/xml-escape.js +33 -0
  174. package/src/virtual-machine.js +1586 -0
  175. package/webpack.config.js +134 -0
@@ -0,0 +1,1355 @@
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 uid = require('../../util/uid');
6
+ const BT = require('../../io/bt');
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 blockIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNDBweCIgaGVpZ2h0PSI0MHB4IiB2aWV3Qm94PSIwIDAgNDAgNDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUwLjIgKDU1MDQ3KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5ldjMtYmxvY2staWNvbjwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJldjMtYmxvY2staWNvbiIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9ImV2MyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNS41MDAwMDAsIDMuNTAwMDAwKSIgZmlsbC1ydWxlPSJub256ZXJvIj4KICAgICAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZS1wYXRoIiBzdHJva2U9IiM3Qzg3QTUiIGZpbGw9IiNGRkZGRkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgeD0iMC41IiB5PSIzLjU5IiB3aWR0aD0iMjgiIGhlaWdodD0iMjUuODEiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgc3Ryb2tlPSIjN0M4N0E1IiBmaWxsPSIjRTZFN0U4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHg9IjIuNSIgeT0iMC41IiB3aWR0aD0iMjQiIGhlaWdodD0iMzIiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgc3Ryb2tlPSIjN0M4N0E1IiBmaWxsPSIjRkZGRkZGIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHg9IjIuNSIgeT0iMTQuNSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjEzIj48L3JlY3Q+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0xNC41LDEwLjUgTDE0LjUsMTQuNSIgaWQ9IlNoYXBlIiBzdHJva2U9IiM3Qzg3QTUiIGZpbGw9IiNFNkU3RTgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PC9wYXRoPgogICAgICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlLXBhdGgiIGZpbGw9IiM0MTQ3NTciIHg9IjQuNSIgeT0iMi41IiB3aWR0aD0iMjAiIGhlaWdodD0iMTAiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgZmlsbD0iIzdDODdBNSIgb3BhY2l0eT0iMC41IiB4PSIxMy41IiB5PSIyMC4xMyIgd2lkdGg9IjIiIGhlaWdodD0iMiIgcng9IjAuNSI+PC9yZWN0PgogICAgICAgICAgICA8cGF0aCBkPSJNOS4wNiwyMC4xMyBMMTAuNTYsMjAuMTMgQzEwLjgzNjE0MjQsMjAuMTMgMTEuMDYsMjAuMzUzODU3NiAxMS4wNiwyMC42MyBMMTEuMDYsMjEuNjMgQzExLjA2LDIxLjkwNjE0MjQgMTAuODM2MTQyNCwyMi4xMyAxMC41NiwyMi4xMyBMOS4wNiwyMi4xMyBDOC41MDc3MTUyNSwyMi4xMyA4LjA2LDIxLjY4MjI4NDcgOC4wNiwyMS4xMyBDOC4wNiwyMC41Nzc3MTUzIDguNTA3NzE1MjUsMjAuMTMgOS4wNiwyMC4xMyBaIiBpZD0iU2hhcGUiIGZpbGw9IiM3Qzg3QTUiIG9wYWNpdHk9IjAuNSI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMTguOTEsMjAuMTMgTDIwLjQyLDIwLjEzIEMyMC42OTYxNDI0LDIwLjEzIDIwLjkyLDIwLjM1Mzg1NzYgMjAuOTIsMjAuNjMgTDIwLjkyLDIxLjYzIEMyMC45MiwyMS45MDYxNDI0IDIwLjY5NjE0MjQsMjIuMTMgMjAuNDIsMjIuMTMgTDE4LjkyLDIyLjEzIEMxOC4zNjc3MTUzLDIyLjEzIDE3LjkyLDIxLjY4MjI4NDcgMTcuOTIsMjEuMTMgQzE3LjkxOTk3MjYsMjAuNTgxNTk3IDE4LjM2MTYyNDUsMjAuMTM1NDg0IDE4LjkxLDIwLjEzIFoiIGlkPSJTaGFwZSIgZmlsbD0iIzdDODdBNSIgb3BhY2l0eT0iMC41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxOS40MjAwMDAsIDIxLjEzMDAwMCkgcm90YXRlKC0xODAuMDAwMDAwKSB0cmFuc2xhdGUoLTE5LjQyMDAwMCwgLTIxLjEzMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik04LjIzLDE3LjUgTDUsMTcuNSBDNC43MjM4NTc2MywxNy41IDQuNSwxNy4yNzYxNDI0IDQuNSwxNyBMNC41LDE0LjUgTDEwLjUsMTQuNSBMOC42NSwxNy4yOCBDOC41NTQ2Njk2MSwxNy40MTc5MDgyIDguMzk3NjUwMDYsMTcuNTAwMTU2NiA4LjIzLDE3LjUgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjN0M4N0E1IiBvcGFjaXR5PSIwLjUiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggZD0iTTE4LjE1LDE4Ljg1IEwxNy42NSwxOS4zNSBDMTcuNTUyMzQxNiwxOS40NDQwNzU2IDE3LjQ5ODAzMzksMTkuNTc0NDE0MiAxNy41LDE5LjcxIEwxNy41LDIwIEMxNy41LDIwLjI3NjE0MjQgMTcuMjc2MTQyNCwyMC41IDE3LDIwLjUgTDE2LjUsMjAuNSBDMTYuMjIzODU3NiwyMC41IDE2LDIwLjI3NjE0MjQgMTYsMjAgQzE2LDE5LjcyMzg1NzYgMTUuNzc2MTQyNCwxOS41IDE1LjUsMTkuNSBMMTMuNSwxOS41IEMxMy4yMjM4NTc2LDE5LjUgMTMsMTkuNzIzODU3NiAxMywyMCBDMTMsMjAuMjc2MTQyNCAxMi43NzYxNDI0LDIwLjUgMTIuNSwyMC41IEwxMiwyMC41IEMxMS43MjM4NTc2LDIwLjUgMTEuNSwyMC4yNzYxNDI0IDExLjUsMjAgTDExLjUsMTkuNzEgQzExLjUwMTk2NjEsMTkuNTc0NDE0MiAxMS40NDc2NTg0LDE5LjQ0NDA3NTYgMTEuMzUsMTkuMzUgTDEwLjg1LDE4Ljg1IEMxMC42NTgyMTY3LDE4LjY1MjE4NjMgMTAuNjU4MjE2NywxOC4zMzc4MTM3IDEwLjg1LDE4LjE0IEwxMi4zNiwxNi42NSBDMTIuNDUwMjgwMywxNi41NTI4NjE3IDEyLjU3NzM5NjEsMTYuNDk4MzgzNSAxMi43MSwxNi41IEwxNi4yOSwxNi41IEMxNi40MjI2MDM5LDE2LjQ5ODM4MzUgMTYuNTQ5NzE5NywxNi41NTI4NjE3IDE2LjY0LDE2LjY1IEwxOC4xNSwxOC4xNCBDMTguMzQxNzgzMywxOC4zMzc4MTM3IDE4LjM0MTc4MzMsMTguNjUyMTg2MyAxOC4xNSwxOC44NSBaIiBpZD0iU2hhcGUiIGZpbGw9IiM3Qzg3QTUiIG9wYWNpdHk9IjAuNSI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMTAuODUsMjMuNDUgTDExLjM1LDIyLjk1IEMxMS40NDc2NTg0LDIyLjg1NTkyNDQgMTEuNTAxOTY2MSwyMi43MjU1ODU4IDExLjUsMjIuNTkgTDExLjUsMjIuMyBDMTEuNSwyMi4wMjM4NTc2IDExLjcyMzg1NzYsMjEuOCAxMiwyMS44IEwxMi41LDIxLjggQzEyLjc3NjE0MjQsMjEuOCAxMywyMi4wMjM4NTc2IDEzLDIyLjMgQzEzLDIyLjU3NjE0MjQgMTMuMjIzODU3NiwyMi44IDEzLjUsMjIuOCBMMTUuNSwyMi44IEMxNS43NzYxNDI0LDIyLjggMTYsMjIuNTc2MTQyNCAxNiwyMi4zIEMxNiwyMi4wMjM4NTc2IDE2LjIyMzg1NzYsMjEuOCAxNi41LDIxLjggTDE3LDIxLjggQzE3LjI3NjE0MjQsMjEuOCAxNy41LDIyLjAyMzg1NzYgMTcuNSwyMi4zIEwxNy41LDIyLjU5IEMxNy40OTgwMzM5LDIyLjcyNTU4NTggMTcuNTUyMzQxNiwyMi44NTU5MjQ0IDE3LjY1LDIyLjk1IEwxOC4xNSwyMy40NSBDMTguMzQwNTcxNCwyMy42NDQ0MjE4IDE4LjM0MDU3MTQsMjMuOTU1NTc4MiAxOC4xNSwyNC4xNSBMMTYuNjQsMjUuNjUgQzE2LjU0OTcxOTcsMjUuNzQ3MTM4MyAxNi40MjI2MDM5LDI1LjgwMTYxNjUgMTYuMjksMjUuOCBMMTIuNzEsMjUuOCBDMTIuNTc3Mzk2MSwyNS44MDE2MTY1IDEyLjQ1MDI4MDMsMjUuNzQ3MTM4MyAxMi4zNiwyNS42NSBMMTAuODUsMjQuMTUgQzEwLjY1OTQyODYsMjMuOTU1NTc4MiAxMC42NTk0Mjg2LDIzLjY0NDQyMTggMTAuODUsMjMuNDUgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjN0M4N0E1IiBvcGFjaXR5PSIwLjUiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggZD0iTTIxLjUsMjcuNSBMMjYuNSwyNy41IEwyNi41LDMxLjUgQzI2LjUsMzIuMDUyMjg0NyAyNi4wNTIyODQ3LDMyLjUgMjUuNSwzMi41IEwyMS41LDMyLjUgTDIxLjUsMjcuNSBaIiBpZD0iU2hhcGUiIHN0cm9rZT0iI0NDNEMyMyIgZmlsbD0iI0YxNUEyOSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48L3BhdGg+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4=';
18
+
19
+ /**
20
+ * String with Ev3 expected pairing pin.
21
+ * @readonly
22
+ */
23
+ const Ev3PairingPin = '1234';
24
+
25
+ /**
26
+ * A maximum number of BT message sends per second, to be enforced by the rate limiter.
27
+ * @type {number}
28
+ */
29
+ const BTSendRateMax = 40;
30
+
31
+ /**
32
+ * Enum for Ev3 parameter encodings of various argument and return values.
33
+ * Found in the 'EV3 Firmware Developer Kit', section4, page 9, at
34
+ * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits.
35
+ *
36
+ * The format for these values is:
37
+ * 0xxxxxxx for Short Format
38
+ * 1ttt-bbb for Long Format
39
+ *
40
+ * @readonly
41
+ * @enum {number}
42
+ */
43
+ const Ev3Encoding = {
44
+ ONE_BYTE: 0x81, // = 0b1000-001, "1 byte to follow"
45
+ TWO_BYTES: 0x82, // = 0b1000-010, "2 bytes to follow"
46
+ FOUR_BYTES: 0x83, // = 0b1000-011, "4 bytes to follow"
47
+ GLOBAL_VARIABLE_ONE_BYTE: 0xE1, // = 0b1110-001, "1 byte to follow"
48
+ GLOBAL_CONSTANT_INDEX_0: 0x20, // = 0b00100000
49
+ GLOBAL_VARIABLE_INDEX_0: 0x60 // = 0b01100000
50
+ };
51
+
52
+ /**
53
+ * Enum for Ev3 direct command types.
54
+ * Found in the 'EV3 Communication Developer Kit', section 4, page 24, at
55
+ * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits.
56
+ * @readonly
57
+ * @enum {number}
58
+ */
59
+ const Ev3Command = {
60
+ DIRECT_COMMAND_REPLY: 0x00,
61
+ DIRECT_COMMAND_NO_REPLY: 0x80,
62
+ DIRECT_REPLY: 0x02
63
+ };
64
+
65
+ /**
66
+ * Enum for Ev3 commands opcodes.
67
+ * Found in the 'EV3 Firmware Developer Kit', section 4, page 10, at
68
+ * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits.
69
+ * @readonly
70
+ * @enum {number}
71
+ */
72
+ const Ev3Opcode = {
73
+ OPOUTPUT_STEP_SPEED: 0xAE,
74
+ OPOUTPUT_TIME_SPEED: 0xAF,
75
+ OPOUTPUT_STOP: 0xA3,
76
+ OPOUTPUT_RESET: 0xA2,
77
+ OPOUTPUT_STEP_SYNC: 0xB0,
78
+ OPOUTPUT_TIME_SYNC: 0xB1,
79
+ OPOUTPUT_GET_COUNT: 0xB3,
80
+ OPSOUND: 0x94,
81
+ OPSOUND_CMD_TONE: 1,
82
+ OPSOUND_CMD_STOP: 0,
83
+ OPINPUT_DEVICE_LIST: 0x98,
84
+ OPINPUT_READSI: 0x9D
85
+ };
86
+
87
+ /**
88
+ * Enum for Ev3 values used as arguments to various opcodes.
89
+ * Found in the 'EV3 Firmware Developer Kit', section4, page 10-onwards, at
90
+ * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits.
91
+ * @readonly
92
+ * @enum {number}
93
+ */
94
+ const Ev3Args = {
95
+ LAYER: 0, // always 0, chained EV3s not supported
96
+ COAST: 0,
97
+ BRAKE: 1,
98
+ RAMP: 50, // time in milliseconds
99
+ DO_NOT_CHANGE_TYPE: 0,
100
+ MAX_DEVICES: 32 // 'Normally 32' from pg. 46
101
+ };
102
+
103
+ /**
104
+ * Enum for Ev3 device type numbers.
105
+ * Found in the 'EV3 Firmware Developer Kit', section 5, page 100, at
106
+ * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits.
107
+ * @readonly
108
+ * @enum {string}
109
+ */
110
+ const Ev3Device = {
111
+ 29: 'color',
112
+ 30: 'ultrasonic',
113
+ 32: 'gyro',
114
+ 16: 'touch',
115
+ 8: 'mediumMotor',
116
+ 7: 'largeMotor',
117
+ 126: 'none',
118
+ 125: 'none'
119
+ };
120
+
121
+ /**
122
+ * Enum for Ev3 device modes.
123
+ * Found in the 'EV3 Firmware Developer Kit', section 5, page 100, at
124
+ * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits.
125
+ * @readonly
126
+ * @enum {number}
127
+ */
128
+ const Ev3Mode = {
129
+ touch: 0, // touch
130
+ color: 1, // ambient
131
+ ultrasonic: 1, // inch
132
+ none: 0
133
+ };
134
+
135
+ /**
136
+ * Enum for Ev3 device labels used in the Scratch blocks/UI.
137
+ * @readonly
138
+ * @enum {string}
139
+ */
140
+ const Ev3Label = {
141
+ touch: 'button',
142
+ color: 'brightness',
143
+ ultrasonic: 'distance'
144
+ };
145
+
146
+ /**
147
+ * Manage power, direction, and timers for one EV3 motor.
148
+ */
149
+ class EV3Motor {
150
+
151
+ /**
152
+ * Construct a EV3 Motor instance, which could be of type 'largeMotor' or
153
+ * 'mediumMotor'.
154
+ *
155
+ * @param {EV3} parent - the EV3 peripheral which owns this motor.
156
+ * @param {int} index - the zero-based index of this motor on its parent peripheral.
157
+ * @param {string} type - the type of motor (i.e. 'largeMotor' or 'mediumMotor').
158
+ */
159
+ constructor (parent, index, type) {
160
+ /**
161
+ * The EV3 peripheral which owns this motor.
162
+ * @type {EV3}
163
+ * @private
164
+ */
165
+ this._parent = parent;
166
+
167
+ /**
168
+ * The zero-based index of this motor on its parent peripheral.
169
+ * @type {int}
170
+ * @private
171
+ */
172
+ this._index = index;
173
+
174
+ /**
175
+ * The type of EV3 motor this could be: 'largeMotor' or 'mediumMotor'.
176
+ * @type {string}
177
+ * @private
178
+ */
179
+ this._type = type;
180
+
181
+ /**
182
+ * This motor's current direction: 1 for "clockwise" or -1 for "counterclockwise"
183
+ * @type {number}
184
+ * @private
185
+ */
186
+ this._direction = 1;
187
+
188
+ /**
189
+ * This motor's current power level, in the range [0,100].
190
+ * @type {number}
191
+ * @private
192
+ */
193
+ this._power = 50;
194
+
195
+ /**
196
+ * This motor's current position, in the range [0,360].
197
+ * @type {number}
198
+ * @private
199
+ */
200
+ this._position = 0;
201
+
202
+ /**
203
+ * An ID for the current coast command, to help override multiple coast
204
+ * commands sent in succession.
205
+ * @type {number}
206
+ * @private
207
+ */
208
+ this._commandID = null;
209
+
210
+ /**
211
+ * A delay, in milliseconds, to add to coasting, to make sure that a brake
212
+ * first takes effect if one was sent.
213
+ * @type {number}
214
+ * @private
215
+ */
216
+ this._coastDelay = 1000;
217
+ }
218
+
219
+ /**
220
+ * @return {string} - this motor's type: 'largeMotor' or 'mediumMotor'
221
+ */
222
+ get type () {
223
+ return this._type;
224
+ }
225
+
226
+ /**
227
+ * @param {string} value - this motor's new type: 'largeMotor' or 'mediumMotor'
228
+ */
229
+ set type (value) {
230
+ this._type = value;
231
+ }
232
+
233
+ /**
234
+ * @return {int} - this motor's current direction: 1 for "clockwise" or -1 for "counterclockwise"
235
+ */
236
+ get direction () {
237
+ return this._direction;
238
+ }
239
+
240
+ /**
241
+ * @param {int} value - this motor's new direction: 1 for "clockwise" or -1 for "counterclockwise"
242
+ */
243
+ set direction (value) {
244
+ if (value < 0) {
245
+ this._direction = -1;
246
+ } else {
247
+ this._direction = 1;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * @return {int} - this motor's current power level, in the range [0,100].
253
+ */
254
+ get power () {
255
+ return this._power;
256
+ }
257
+
258
+ /**
259
+ * @param {int} value - this motor's new power level, in the range [0,100].
260
+ */
261
+ set power (value) {
262
+ this._power = value;
263
+ }
264
+
265
+ /**
266
+ * @return {int} - this motor's current position, in the range [-inf,inf].
267
+ */
268
+ get position () {
269
+ return this._position;
270
+ }
271
+
272
+ /**
273
+ * @param {int} array - this motor's new position, in the range [0,360].
274
+ */
275
+ set position (array) {
276
+ // tachoValue from Paula
277
+ let value = array[0] + (array[1] * 256) + (array[2] * 256 * 256) + (array[3] * 256 * 256 * 256);
278
+ if (value > 0x7fffffff) {
279
+ value = value - 0x100000000;
280
+ }
281
+ this._position = value;
282
+ }
283
+
284
+ /**
285
+ * Turn this motor on for a specific duration.
286
+ * Found in the 'EV3 Firmware Developer Kit', page 56, at
287
+ * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits.
288
+ *
289
+ * Opcode arguments:
290
+ * (Data8) LAYER – Specify chain layer number [0 - 3]
291
+ * (Data8) NOS – Output bit field [0x00 – 0x0F]
292
+ * (Data8) SPEED – Power level, [-100 – 100]
293
+ * (Data32) STEP1 – Time in milliseconds for ramp up
294
+ * (Data32) STEP2 – Time in milliseconds for continues run
295
+ * (Data32) STEP3 – Time in milliseconds for ramp down
296
+ * (Data8) BRAKE - Specify break level [0: Float, 1: Break]
297
+ *
298
+ * @param {number} milliseconds - run the motor for this long.
299
+ */
300
+ turnOnFor (milliseconds) {
301
+ if (this._power === 0) return;
302
+
303
+ const port = this._portMask(this._index);
304
+ let n = milliseconds;
305
+ let speed = this._power * this._direction;
306
+ const ramp = Ev3Args.RAMP;
307
+
308
+ let byteCommand = [];
309
+ byteCommand[0] = Ev3Opcode.OPOUTPUT_TIME_SPEED;
310
+
311
+ // If speed is less than zero, make it positive and multiply the input
312
+ // value by -1
313
+ if (speed < 0) {
314
+ speed = -1 * speed;
315
+ n = -1 * n;
316
+ }
317
+ // If the input value is less than 0
318
+ const dir = (n < 0) ? 0x100 - speed : speed; // step negative or positive
319
+ n = Math.abs(n);
320
+ // Setup motor run duration and ramping behavior
321
+ let rampup = ramp;
322
+ let rampdown = ramp;
323
+ let run = n - (ramp * 2);
324
+ if (run < 0) {
325
+ rampup = Math.floor(n / 2);
326
+ run = 0;
327
+ rampdown = n - rampup;
328
+ }
329
+ // Generate motor command values
330
+ const runcmd = this._runValues(run);
331
+ byteCommand = byteCommand.concat([
332
+ Ev3Args.LAYER,
333
+ port,
334
+ Ev3Encoding.ONE_BYTE,
335
+ dir & 0xff,
336
+ Ev3Encoding.ONE_BYTE,
337
+ rampup
338
+ ]).concat(runcmd.concat([
339
+ Ev3Encoding.ONE_BYTE,
340
+ rampdown,
341
+ Ev3Args.BRAKE
342
+ ]));
343
+
344
+ const cmd = this._parent.generateCommand(
345
+ Ev3Command.DIRECT_COMMAND_NO_REPLY,
346
+ byteCommand
347
+ );
348
+
349
+ this._parent.send(cmd);
350
+
351
+ this.coastAfter(milliseconds);
352
+ }
353
+
354
+ /**
355
+ * Set the motor to coast after a specified amount of time.
356
+ * @param {number} time - the time in milliseconds.
357
+ */
358
+ coastAfter (time) {
359
+ if (this._power === 0) return;
360
+
361
+ // Set the motor command id to check before starting coast
362
+ const commandId = uid();
363
+ this._commandID = commandId;
364
+
365
+ // Send coast message
366
+ setTimeout(() => {
367
+ // Do not send coast if another motor command changed the command id.
368
+ if (this._commandID === commandId) {
369
+ this.coast();
370
+ this._commandID = null;
371
+ }
372
+ }, time + this._coastDelay); // add a delay so the brake takes effect
373
+ }
374
+
375
+ /**
376
+ * Set the motor to coast.
377
+ */
378
+ coast () {
379
+ if (this._power === 0) return;
380
+
381
+ const cmd = this._parent.generateCommand(
382
+ Ev3Command.DIRECT_COMMAND_NO_REPLY,
383
+ [
384
+ Ev3Opcode.OPOUTPUT_STOP,
385
+ Ev3Args.LAYER,
386
+ this._portMask(this._index), // port output bit field
387
+ Ev3Args.COAST
388
+ ]
389
+ );
390
+
391
+ this._parent.send(cmd, false); // don't use rate limiter to ensure motor stops
392
+ }
393
+
394
+ /**
395
+ * Generate motor run values for a given input.
396
+ * @param {number} run - run input.
397
+ * @return {array} - run values as a byte array.
398
+ */
399
+ _runValues (run) {
400
+ // If run duration is less than max 16-bit integer
401
+ if (run < 0x7fff) {
402
+ return [
403
+ Ev3Encoding.TWO_BYTES,
404
+ run & 0xff,
405
+ (run >> 8) & 0xff
406
+ ];
407
+ }
408
+
409
+ // Run forever
410
+ return [
411
+ Ev3Encoding.FOUR_BYTES,
412
+ run & 0xff,
413
+ (run >> 8) & 0xff,
414
+ (run >> 16) & 0xff,
415
+ (run >> 24) & 0xff
416
+ ];
417
+ }
418
+
419
+ /**
420
+ * Return a port value for the EV3 that is in the format for 'output bit field'
421
+ * as 1/2/4/8, generally needed for motor ports, instead of the typical 0/1/2/3.
422
+ * The documentation in the 'EV3 Firmware Developer Kit' for motor port arguments
423
+ * is sometimes mistaken, but we believe motor ports are mostly addressed this way.
424
+ * @param {number} port - the port number to convert to an 'output bit field'.
425
+ * @return {number} - the converted port number.
426
+ */
427
+ _portMask (port) {
428
+ return Math.pow(2, port);
429
+ }
430
+ }
431
+
432
+ class EV3 {
433
+
434
+ constructor (runtime, extensionId) {
435
+
436
+ /**
437
+ * The Scratch 3.0 runtime used to trigger the green flag button.
438
+ * @type {Runtime}
439
+ * @private
440
+ */
441
+ this._runtime = runtime;
442
+ this._runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this));
443
+
444
+ /**
445
+ * The id of the extension this peripheral belongs to.
446
+ */
447
+ this._extensionId = extensionId;
448
+
449
+ /**
450
+ * A list of the names of the sensors connected in ports 1,2,3,4.
451
+ * @type {string[]}
452
+ * @private
453
+ */
454
+ this._sensorPorts = [];
455
+
456
+ /**
457
+ * A list of the names of the motors connected in ports A,B,C,D.
458
+ * @type {string[]}
459
+ * @private
460
+ */
461
+ this._motorPorts = [];
462
+
463
+ /**
464
+ * The state of all sensor values.
465
+ * @type {string[]}
466
+ * @private
467
+ */
468
+ this._sensors = {
469
+ distance: 0,
470
+ brightness: 0,
471
+ buttons: [0, 0, 0, 0]
472
+ };
473
+
474
+ /**
475
+ * The motors which this EV3 could possibly have connected.
476
+ * @type {string[]}
477
+ * @private
478
+ */
479
+ this._motors = [null, null, null, null];
480
+
481
+ /**
482
+ * The polling interval, in milliseconds.
483
+ * @type {number}
484
+ * @private
485
+ */
486
+ this._pollingInterval = 150;
487
+
488
+ /**
489
+ * The polling interval ID.
490
+ * @type {number}
491
+ * @private
492
+ */
493
+ this._pollingIntervalID = null;
494
+
495
+ /**
496
+ * The counter keeping track of polling cycles.
497
+ * @type {string[]}
498
+ * @private
499
+ */
500
+ this._pollingCounter = 0;
501
+
502
+ /**
503
+ * The Bluetooth socket connection for reading/writing peripheral data.
504
+ * @type {BT}
505
+ * @private
506
+ */
507
+ this._bt = null;
508
+ this._runtime.registerPeripheralExtension(extensionId, this);
509
+
510
+ /**
511
+ * A rate limiter utility, to help limit the rate at which we send BT messages
512
+ * over the socket to Scratch Link to a maximum number of sends per second.
513
+ * @type {RateLimiter}
514
+ * @private
515
+ */
516
+ this._rateLimiter = new RateLimiter(BTSendRateMax);
517
+
518
+ this.reset = this.reset.bind(this);
519
+ this._onConnect = this._onConnect.bind(this);
520
+ this._onMessage = this._onMessage.bind(this);
521
+ this._pollValues = this._pollValues.bind(this);
522
+ }
523
+
524
+ get distance () {
525
+ let value = this._sensors.distance > 100 ? 100 : this._sensors.distance;
526
+ value = value < 0 ? 0 : value;
527
+ value = Math.round(100 * value) / 100;
528
+
529
+ return value;
530
+ }
531
+
532
+ get brightness () {
533
+ return this._sensors.brightness;
534
+ }
535
+
536
+ /**
537
+ * Access a particular motor on this peripheral.
538
+ * @param {int} index - the zero-based index of the desired motor.
539
+ * @return {EV3Motor} - the EV3Motor instance, if any, at that index.
540
+ */
541
+ motor (index) {
542
+ return this._motors[index];
543
+ }
544
+
545
+ isButtonPressed (port) {
546
+ return this._sensors.buttons[port] === 1;
547
+ }
548
+
549
+ beep (freq, time) {
550
+ const cmd = this.generateCommand(
551
+ Ev3Command.DIRECT_COMMAND_NO_REPLY,
552
+ [
553
+ Ev3Opcode.OPSOUND,
554
+ Ev3Opcode.OPSOUND_CMD_TONE,
555
+ Ev3Encoding.ONE_BYTE,
556
+ 2,
557
+ Ev3Encoding.TWO_BYTES,
558
+ freq,
559
+ freq >> 8,
560
+ Ev3Encoding.TWO_BYTES,
561
+ time,
562
+ time >> 8
563
+ ]
564
+ );
565
+
566
+ this.send(cmd);
567
+ }
568
+
569
+ stopAll () {
570
+ this.stopAllMotors();
571
+ this.stopSound();
572
+ }
573
+
574
+ stopSound () {
575
+ const cmd = this.generateCommand(
576
+ Ev3Command.DIRECT_COMMAND_NO_REPLY,
577
+ [
578
+ Ev3Opcode.OPSOUND,
579
+ Ev3Opcode.OPSOUND_CMD_STOP
580
+ ]
581
+ );
582
+
583
+ this.send(cmd, false); // don't use rate limiter to ensure sound stops
584
+ }
585
+
586
+ stopAllMotors () {
587
+ this._motors.forEach(motor => {
588
+ if (motor) {
589
+ motor.coast();
590
+ }
591
+ });
592
+ }
593
+
594
+ /**
595
+ * Called by the runtime when user wants to scan for an EV3 peripheral.
596
+ */
597
+ scan () {
598
+ if (this._bt) {
599
+ this._bt.disconnect();
600
+ }
601
+ this._bt = new BT(this._runtime, this._extensionId, {
602
+ majorDeviceClass: 8,
603
+ minorDeviceClass: 1
604
+ }, this._onConnect, this.reset, this._onMessage);
605
+ }
606
+
607
+ /**
608
+ * Called by the runtime when user wants to connect to a certain EV3 peripheral.
609
+ * @param {number} id - the id of the peripheral to connect to.
610
+ */
611
+ connect (id) {
612
+ if (this._bt) {
613
+ this._bt.connectPeripheral(id, Ev3PairingPin);
614
+ }
615
+ }
616
+
617
+ /**
618
+ * Called by the runtime when user wants to disconnect from the EV3 peripheral.
619
+ */
620
+ disconnect () {
621
+ if (this._bt) {
622
+ this._bt.disconnect();
623
+ }
624
+
625
+ this.reset();
626
+ }
627
+
628
+ /**
629
+ * Reset all the state and timeout/interval ids.
630
+ */
631
+ reset () {
632
+ this._sensorPorts = [];
633
+ this._motorPorts = [];
634
+ this._sensors = {
635
+ distance: 0,
636
+ brightness: 0,
637
+ buttons: [0, 0, 0, 0]
638
+ };
639
+ this._motors = [null, null, null, null];
640
+
641
+ if (this._pollingIntervalID) {
642
+ window.clearInterval(this._pollingIntervalID);
643
+ this._pollingIntervalID = null;
644
+ }
645
+ }
646
+
647
+ /**
648
+ * Called by the runtime to detect whether the EV3 peripheral is connected.
649
+ * @return {boolean} - the connected state.
650
+ */
651
+ isConnected () {
652
+ let connected = false;
653
+ if (this._bt) {
654
+ connected = this._bt.isConnected();
655
+ }
656
+ return connected;
657
+ }
658
+
659
+ /**
660
+ * Send a message to the peripheral BT socket.
661
+ * @param {Uint8Array} message - the message to send.
662
+ * @param {boolean} [useLimiter=true] - if true, use the rate limiter
663
+ * @return {Promise} - a promise result of the send operation.
664
+ */
665
+ send (message, useLimiter = true) {
666
+ if (!this.isConnected()) return Promise.resolve();
667
+
668
+ if (useLimiter) {
669
+ if (!this._rateLimiter.okayToSend()) return Promise.resolve();
670
+ }
671
+
672
+ return this._bt.sendMessage({
673
+ message: Base64Util.uint8ArrayToBase64(message),
674
+ encoding: 'base64'
675
+ });
676
+ }
677
+
678
+ /**
679
+ * Genrates direct commands that are sent to the EV3 as a single or compounded byte arrays.
680
+ * See 'EV3 Communication Developer Kit', section 4, page 24 at
681
+ * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits.
682
+ *
683
+ * Direct commands are one of two types:
684
+ * DIRECT_COMMAND_NO_REPLY = a direct command where no reply is expected
685
+ * DIRECT_COMMAND_REPLY = a direct command where a reply is expected, and the
686
+ * number and length of returned values needs to be specified.
687
+ *
688
+ * The direct command byte array sent takes the following format:
689
+ * Byte 0 - 1: Command size, Little Endian. Command size not including these 2 bytes
690
+ * Byte 2 - 3: Message counter, Little Endian. Forth running counter
691
+ * Byte 4: Command type. Either DIRECT_COMMAND_REPLY or DIRECT_COMMAND_NO_REPLY
692
+ * Byte 5 - 6: Reservation (allocation) of global and local variables using a compressed format
693
+ * (globals reserved in byte 5 and the 2 lsb of byte 6, locals reserved in the upper
694
+ * 6 bits of byte 6) – see documentation for more details.
695
+ * Byte 7 - n: Byte codes as a single command or compound commands (I.e. more commands composed
696
+ * as a small program)
697
+ *
698
+ * @param {number} type - the direct command type.
699
+ * @param {string} byteCommands - a compound array of EV3 Opcode + arguments.
700
+ * @param {number} allocation - the allocation of global and local vars needed for replies.
701
+ * @return {array} - generated complete command byte array, with header and compounded commands.
702
+ */
703
+ generateCommand (type, byteCommands, allocation = 0) {
704
+
705
+ // Header (Bytes 0 - 6)
706
+ let command = [];
707
+ command[2] = 0; // Message counter unused for now
708
+ command[3] = 0; // Message counter unused for now
709
+ command[4] = type;
710
+ command[5] = allocation & 0xFF;
711
+ command[6] = allocation >> 8 && 0xFF;
712
+
713
+ // Bytecodes (Bytes 7 - n)
714
+ command = command.concat(byteCommands);
715
+
716
+ // Calculate command length minus first two header bytes
717
+ const len = command.length - 2;
718
+ command[0] = len & 0xFF;
719
+ command[1] = len >> 8 && 0xFF;
720
+
721
+ return command;
722
+ }
723
+
724
+ /**
725
+ * When the EV3 peripheral connects, start polling for sensor and motor values.
726
+ * @private
727
+ */
728
+ _onConnect () {
729
+ this._pollingIntervalID = window.setInterval(this._pollValues, this._pollingInterval);
730
+ }
731
+
732
+ /**
733
+ * Poll the EV3 for sensor and motor input values, based on the list of
734
+ * known connected sensors and motors. This is sent as many compound commands
735
+ * in a direct command, with a reply expected.
736
+ *
737
+ * See 'EV3 Firmware Developer Kit', section 4.8, page 46, at
738
+ * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits
739
+ * for a list of polling/input device commands and their arguments.
740
+ *
741
+ * @private
742
+ */
743
+ _pollValues () {
744
+ if (!this.isConnected()) {
745
+ window.clearInterval(this._pollingIntervalID);
746
+ return;
747
+ }
748
+
749
+ const cmds = []; // compound command
750
+ let allocation = 0;
751
+ let sensorCount = 0;
752
+
753
+ // Reset the list of devices every 20 counts
754
+ if (this._pollingCounter % 20 === 0) {
755
+ // GET DEVICE LIST
756
+ cmds[0] = Ev3Opcode.OPINPUT_DEVICE_LIST;
757
+ cmds[1] = Ev3Encoding.ONE_BYTE;
758
+ cmds[2] = Ev3Args.MAX_DEVICES;
759
+ cmds[3] = Ev3Encoding.GLOBAL_VARIABLE_INDEX_0;
760
+ cmds[4] = Ev3Encoding.GLOBAL_VARIABLE_ONE_BYTE;
761
+ cmds[5] = Ev3Encoding.GLOBAL_CONSTANT_INDEX_0;
762
+
763
+ // Command and payload lengths
764
+ allocation = 33;
765
+
766
+ this._updateDevices = true;
767
+ } else {
768
+ // GET SENSOR VALUES FOR CONNECTED SENSORS
769
+ let index = 0;
770
+ for (let i = 0; i < 4; i++) {
771
+ if (this._sensorPorts[i] !== 'none') {
772
+ cmds[index + 0] = Ev3Opcode.OPINPUT_READSI;
773
+ cmds[index + 1] = Ev3Args.LAYER;
774
+ cmds[index + 2] = i; // PORT
775
+ cmds[index + 3] = Ev3Args.DO_NOT_CHANGE_TYPE;
776
+ cmds[index + 4] = Ev3Mode[this._sensorPorts[i]];
777
+ cmds[index + 5] = Ev3Encoding.GLOBAL_VARIABLE_ONE_BYTE;
778
+ cmds[index + 6] = sensorCount * 4; // GLOBAL INDEX
779
+ index += 7;
780
+ }
781
+ sensorCount++;
782
+ }
783
+
784
+ // GET MOTOR POSITION VALUES, EVEN IF NO MOTOR PRESENT
785
+ for (let i = 0; i < 4; i++) {
786
+ cmds[index + 0] = Ev3Opcode.OPOUTPUT_GET_COUNT;
787
+ cmds[index + 1] = Ev3Args.LAYER;
788
+ cmds[index + 2] = i; // PORT (incorrectly specified as 'Output bit field' in LEGO docs)
789
+ cmds[index + 3] = Ev3Encoding.GLOBAL_VARIABLE_ONE_BYTE;
790
+ cmds[index + 4] = sensorCount * 4; // GLOBAL INDEX
791
+ index += 5;
792
+ sensorCount++;
793
+ }
794
+
795
+ // Command and payload lengths
796
+ allocation = sensorCount * 4;
797
+ }
798
+
799
+ const cmd = this.generateCommand(
800
+ Ev3Command.DIRECT_COMMAND_REPLY,
801
+ cmds,
802
+ allocation
803
+ );
804
+
805
+ this.send(cmd);
806
+
807
+ this._pollingCounter++;
808
+ }
809
+
810
+ /**
811
+ * Message handler for incoming EV3 reply messages, either a list of connected
812
+ * devices (sensors and motors) or the values of the connected sensors and motors.
813
+ *
814
+ * See 'EV3 Communication Developer Kit', section 4.1, page 24 at
815
+ * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits
816
+ * for more details on direct reply formats.
817
+ *
818
+ * The direct reply byte array sent takes the following format:
819
+ * Byte 0 – 1: Reply size, Little Endian. Reply size not including these 2 bytes
820
+ * Byte 2 – 3: Message counter, Little Endian. Equals the Direct Command
821
+ * Byte 4: Reply type. Either DIRECT_REPLY or DIRECT_REPLY_ERROR
822
+ * Byte 5 - n: Resonse buffer. I.e. the content of the by the Command reserved global variables.
823
+ * I.e. if the command reserved 64 bytes, these bytes will be placed in the reply
824
+ * packet as the bytes 5 to 68.
825
+ *
826
+ * See 'EV3 Firmware Developer Kit', section 4.8, page 56 at
827
+ * https://education.lego.com/en-us/support/mindstorms-ev3/developer-kits
828
+ * for direct response buffer formats for various commands.
829
+ *
830
+ * @param {object} params - incoming message parameters
831
+ * @private
832
+ */
833
+ _onMessage (params) {
834
+ const message = params.message;
835
+ const data = Base64Util.base64ToUint8Array(message);
836
+
837
+ if (data[4] !== Ev3Command.DIRECT_REPLY) {
838
+ return;
839
+ }
840
+
841
+ if (this._updateDevices) {
842
+
843
+ // PARSE DEVICE LIST
844
+ for (let i = 0; i < 4; i++) {
845
+ const deviceType = Ev3Device[data[i + 5]];
846
+ // if returned device type is null, use 'none'
847
+ this._sensorPorts[i] = deviceType ? deviceType : 'none';
848
+ }
849
+ for (let i = 0; i < 4; i++) {
850
+ const deviceType = Ev3Device[data[i + 21]];
851
+ // if returned device type is null, use 'none'
852
+ this._motorPorts[i] = deviceType ? deviceType : 'none';
853
+ }
854
+ for (let m = 0; m < 4; m++) {
855
+ const type = this._motorPorts[m];
856
+ if (type !== 'none' && !this._motors[m]) {
857
+ // add new motor if don't already have one
858
+ this._motors[m] = new EV3Motor(this, m, type);
859
+ }
860
+ if (type === 'none' && this._motors[m]) {
861
+ // clear old motor
862
+ this._motors[m] = null;
863
+ }
864
+ }
865
+ this._updateDevices = false;
866
+
867
+ // eslint-disable-next-line no-undefined
868
+ } else if (!this._sensorPorts.includes(undefined) && !this._motorPorts.includes(undefined)) {
869
+
870
+ // PARSE SENSOR VALUES
871
+ let offset = 5; // start reading sensor values at byte 5
872
+ for (let i = 0; i < 4; i++) {
873
+ // array 2 float
874
+ const buffer = new Uint8Array([
875
+ data[offset],
876
+ data[offset + 1],
877
+ data[offset + 2],
878
+ data[offset + 3]
879
+ ]).buffer;
880
+ const view = new DataView(buffer);
881
+ const value = view.getFloat32(0, true);
882
+
883
+ if (Ev3Label[this._sensorPorts[i]] === 'button') {
884
+ // Read a button value per port
885
+ this._sensors.buttons[i] = value ? value : 0;
886
+ } else if (Ev3Label[this._sensorPorts[i]]) { // if valid
887
+ // Read brightness / distance values and set to 0 if null
888
+ this._sensors[Ev3Label[this._sensorPorts[i]]] = value ? value : 0;
889
+ }
890
+ offset += 4;
891
+ }
892
+
893
+ // PARSE MOTOR POSITION VALUES, EVEN IF NO MOTOR PRESENT
894
+ for (let i = 0; i < 4; i++) {
895
+ const positionArray = [
896
+ data[offset],
897
+ data[offset + 1],
898
+ data[offset + 2],
899
+ data[offset + 3]
900
+ ];
901
+ if (this._motors[i]) {
902
+ this._motors[i].position = positionArray;
903
+ }
904
+ offset += 4;
905
+ }
906
+
907
+ }
908
+ }
909
+ }
910
+
911
+ /**
912
+ * Enum for motor port names.
913
+ * Note: if changed, will break compatibility with previously saved projects.
914
+ * @readonly
915
+ * @enum {string}
916
+ */
917
+ const Ev3MotorMenu = ['A', 'B', 'C', 'D'];
918
+
919
+ /**
920
+ * Enum for sensor port names.
921
+ * Note: if changed, will break compatibility with previously saved projects.
922
+ * @readonly
923
+ * @enum {string}
924
+ */
925
+ const Ev3SensorMenu = ['1', '2', '3', '4'];
926
+
927
+ class Scratch3Ev3Blocks {
928
+
929
+ /**
930
+ * The ID of the extension.
931
+ * @return {string} the id
932
+ */
933
+ static get EXTENSION_ID () {
934
+ return 'ev3';
935
+ }
936
+
937
+ /**
938
+ * Creates a new instance of the EV3 extension.
939
+ * @param {object} runtime VM runtime
940
+ * @constructor
941
+ */
942
+ constructor (runtime) {
943
+ /**
944
+ * The Scratch 3.0 runtime.
945
+ * @type {Runtime}
946
+ */
947
+ this.runtime = runtime;
948
+
949
+ // Create a new EV3 peripheral instance
950
+ this._peripheral = new EV3(this.runtime, Scratch3Ev3Blocks.EXTENSION_ID);
951
+
952
+ this._playNoteForPicker = this._playNoteForPicker.bind(this);
953
+ this.runtime.on('PLAY_NOTE', this._playNoteForPicker);
954
+ }
955
+
956
+ /**
957
+ * Define the EV3 extension.
958
+ * @return {object} Extension description.
959
+ */
960
+ getInfo () {
961
+ return {
962
+ id: Scratch3Ev3Blocks.EXTENSION_ID,
963
+ name: 'LEGO EV3',
964
+ blockIconURI: blockIconURI,
965
+ showStatusButton: true,
966
+ blocks: [
967
+ {
968
+ opcode: 'motorTurnClockwise',
969
+ text: formatMessage({
970
+ id: 'ev3.motorTurnClockwise',
971
+ default: 'motor [PORT] turn this way for [TIME] seconds',
972
+ description: 'turn a motor clockwise for some time'
973
+ }),
974
+ blockType: BlockType.COMMAND,
975
+ arguments: {
976
+ PORT: {
977
+ type: ArgumentType.STRING,
978
+ menu: 'motorPorts',
979
+ defaultValue: 0
980
+ },
981
+ TIME: {
982
+ type: ArgumentType.NUMBER,
983
+ defaultValue: 1
984
+ }
985
+ }
986
+ },
987
+ {
988
+ opcode: 'motorTurnCounterClockwise',
989
+ text: formatMessage({
990
+ id: 'ev3.motorTurnCounterClockwise',
991
+ default: 'motor [PORT] turn that way for [TIME] seconds',
992
+ description: 'turn a motor counter-clockwise for some time'
993
+ }),
994
+ blockType: BlockType.COMMAND,
995
+ arguments: {
996
+ PORT: {
997
+ type: ArgumentType.STRING,
998
+ menu: 'motorPorts',
999
+ defaultValue: 0
1000
+ },
1001
+ TIME: {
1002
+ type: ArgumentType.NUMBER,
1003
+ defaultValue: 1
1004
+ }
1005
+ }
1006
+ },
1007
+ {
1008
+ opcode: 'motorSetPower',
1009
+ text: formatMessage({
1010
+ id: 'ev3.motorSetPower',
1011
+ default: 'motor [PORT] set power [POWER] %',
1012
+ description: 'set a motor\'s power to some value'
1013
+ }),
1014
+ blockType: BlockType.COMMAND,
1015
+ arguments: {
1016
+ PORT: {
1017
+ type: ArgumentType.STRING,
1018
+ menu: 'motorPorts',
1019
+ defaultValue: 0
1020
+ },
1021
+ POWER: {
1022
+ type: ArgumentType.NUMBER,
1023
+ defaultValue: 100
1024
+ }
1025
+ }
1026
+ },
1027
+ {
1028
+ opcode: 'getMotorPosition',
1029
+ text: formatMessage({
1030
+ id: 'ev3.getMotorPosition',
1031
+ default: 'motor [PORT] position',
1032
+ description: 'get the measured degrees a motor has turned'
1033
+ }),
1034
+ blockType: BlockType.REPORTER,
1035
+ arguments: {
1036
+ PORT: {
1037
+ type: ArgumentType.STRING,
1038
+ menu: 'motorPorts',
1039
+ defaultValue: 0
1040
+ }
1041
+ }
1042
+ },
1043
+ {
1044
+ opcode: 'whenButtonPressed',
1045
+ text: formatMessage({
1046
+ id: 'ev3.whenButtonPressed',
1047
+ default: 'when button [PORT] pressed',
1048
+ description: 'when a button connected to a port is pressed'
1049
+ }),
1050
+ blockType: BlockType.HAT,
1051
+ arguments: {
1052
+ PORT: {
1053
+ type: ArgumentType.STRING,
1054
+ menu: 'sensorPorts',
1055
+ defaultValue: 0
1056
+ }
1057
+ }
1058
+ },
1059
+ {
1060
+ opcode: 'whenDistanceLessThan',
1061
+ text: formatMessage({
1062
+ id: 'ev3.whenDistanceLessThan',
1063
+ default: 'when distance < [DISTANCE]',
1064
+ description: 'when the value measured by the distance sensor is less than some value'
1065
+ }),
1066
+ blockType: BlockType.HAT,
1067
+ arguments: {
1068
+ DISTANCE: {
1069
+ type: ArgumentType.NUMBER,
1070
+ defaultValue: 5
1071
+ }
1072
+ }
1073
+ },
1074
+ {
1075
+ opcode: 'whenBrightnessLessThan',
1076
+ text: formatMessage({
1077
+ id: 'ev3.whenBrightnessLessThan',
1078
+ default: 'when brightness < [DISTANCE]',
1079
+ description: 'when value measured by brightness sensor is less than some value'
1080
+ }),
1081
+ blockType: BlockType.HAT,
1082
+ arguments: {
1083
+ DISTANCE: {
1084
+ type: ArgumentType.NUMBER,
1085
+ defaultValue: 50
1086
+ }
1087
+ }
1088
+ },
1089
+ {
1090
+ opcode: 'buttonPressed',
1091
+ text: formatMessage({
1092
+ id: 'ev3.buttonPressed',
1093
+ default: 'button [PORT] pressed?',
1094
+ description: 'is a button on some port pressed?'
1095
+ }),
1096
+ blockType: BlockType.BOOLEAN,
1097
+ arguments: {
1098
+ PORT: {
1099
+ type: ArgumentType.STRING,
1100
+ menu: 'sensorPorts',
1101
+ defaultValue: 0
1102
+ }
1103
+ }
1104
+ },
1105
+ {
1106
+ opcode: 'getDistance',
1107
+ text: formatMessage({
1108
+ id: 'ev3.getDistance',
1109
+ default: 'distance',
1110
+ description: 'gets measured distance'
1111
+ }),
1112
+ blockType: BlockType.REPORTER
1113
+ },
1114
+ {
1115
+ opcode: 'getBrightness',
1116
+ text: formatMessage({
1117
+ id: 'ev3.getBrightness',
1118
+ default: 'brightness',
1119
+ description: 'gets measured brightness'
1120
+ }),
1121
+ blockType: BlockType.REPORTER
1122
+ },
1123
+ {
1124
+ opcode: 'beep',
1125
+ text: formatMessage({
1126
+ id: 'ev3.beepNote',
1127
+ default: 'beep note [NOTE] for [TIME] secs',
1128
+ description: 'play some note on EV3 for some time'
1129
+ }),
1130
+ blockType: BlockType.COMMAND,
1131
+ arguments: {
1132
+ NOTE: {
1133
+ type: ArgumentType.NOTE,
1134
+ defaultValue: 60
1135
+ },
1136
+ TIME: {
1137
+ type: ArgumentType.NUMBER,
1138
+ defaultValue: 0.5
1139
+ }
1140
+ }
1141
+ }
1142
+ ],
1143
+ menus: {
1144
+ motorPorts: {
1145
+ acceptReporters: true,
1146
+ items: this._formatMenu(Ev3MotorMenu)
1147
+ },
1148
+ sensorPorts: {
1149
+ acceptReporters: true,
1150
+ items: this._formatMenu(Ev3SensorMenu)
1151
+ }
1152
+ }
1153
+ };
1154
+ }
1155
+
1156
+ motorTurnClockwise (args) {
1157
+ const port = Cast.toNumber(args.PORT);
1158
+ let time = Cast.toNumber(args.TIME) * 1000;
1159
+ time = MathUtil.clamp(time, 0, 15000);
1160
+
1161
+ return new Promise(resolve => {
1162
+ this._forEachMotor(port, motorIndex => {
1163
+ const motor = this._peripheral.motor(motorIndex);
1164
+ if (motor) {
1165
+ motor.direction = 1;
1166
+ motor.turnOnFor(time);
1167
+ }
1168
+ });
1169
+
1170
+ // Run for some time even when no motor is connected
1171
+ setTimeout(resolve, time);
1172
+ });
1173
+ }
1174
+
1175
+ motorTurnCounterClockwise (args) {
1176
+ const port = Cast.toNumber(args.PORT);
1177
+ let time = Cast.toNumber(args.TIME) * 1000;
1178
+ time = MathUtil.clamp(time, 0, 15000);
1179
+
1180
+ return new Promise(resolve => {
1181
+ this._forEachMotor(port, motorIndex => {
1182
+ const motor = this._peripheral.motor(motorIndex);
1183
+ if (motor) {
1184
+ motor.direction = -1;
1185
+ motor.turnOnFor(time);
1186
+ }
1187
+ });
1188
+
1189
+ // Run for some time even when no motor is connected
1190
+ setTimeout(resolve, time);
1191
+ });
1192
+ }
1193
+
1194
+ motorSetPower (args) {
1195
+ const port = Cast.toNumber(args.PORT);
1196
+ const power = MathUtil.clamp(Cast.toNumber(args.POWER), 0, 100);
1197
+
1198
+ this._forEachMotor(port, motorIndex => {
1199
+ const motor = this._peripheral.motor(motorIndex);
1200
+ if (motor) {
1201
+ motor.power = power;
1202
+ }
1203
+ });
1204
+ }
1205
+
1206
+ getMotorPosition (args) {
1207
+ const port = Cast.toNumber(args.PORT);
1208
+
1209
+ if (![0, 1, 2, 3].includes(port)) {
1210
+ return;
1211
+ }
1212
+
1213
+ const motor = this._peripheral.motor(port);
1214
+ let position = 0;
1215
+ if (motor) {
1216
+ position = MathUtil.wrapClamp(motor.position, 0, 360);
1217
+ }
1218
+
1219
+ return position;
1220
+ }
1221
+
1222
+ whenButtonPressed (args) {
1223
+ const port = Cast.toNumber(args.PORT);
1224
+
1225
+ if (![0, 1, 2, 3].includes(port)) {
1226
+ return;
1227
+ }
1228
+
1229
+ return this._peripheral.isButtonPressed(port);
1230
+ }
1231
+
1232
+ whenDistanceLessThan (args) {
1233
+ const distance = MathUtil.clamp(Cast.toNumber(args.DISTANCE), 0, 100);
1234
+
1235
+ return this._peripheral.distance < distance;
1236
+ }
1237
+
1238
+ whenBrightnessLessThan (args) {
1239
+ const brightness = MathUtil.clamp(Cast.toNumber(args.DISTANCE), 0, 100);
1240
+
1241
+ return this._peripheral.brightness < brightness;
1242
+ }
1243
+
1244
+ buttonPressed (args) {
1245
+ const port = Cast.toNumber(args.PORT);
1246
+
1247
+ if (![0, 1, 2, 3].includes(port)) {
1248
+ return;
1249
+ }
1250
+
1251
+ return this._peripheral.isButtonPressed(port);
1252
+ }
1253
+
1254
+ getDistance () {
1255
+ return this._peripheral.distance;
1256
+ }
1257
+
1258
+ getBrightness () {
1259
+ return this._peripheral.brightness;
1260
+ }
1261
+
1262
+ _playNoteForPicker (note, category) {
1263
+ if (category !== this.getInfo().name) return;
1264
+ this.beep({
1265
+ NOTE: note,
1266
+ TIME: 0.25
1267
+ });
1268
+ }
1269
+
1270
+ beep (args) {
1271
+ const note = MathUtil.clamp(Cast.toNumber(args.NOTE), 47, 99); // valid EV3 sounds
1272
+ let time = Cast.toNumber(args.TIME) * 1000;
1273
+ time = MathUtil.clamp(time, 0, 3000);
1274
+
1275
+ if (time === 0) {
1276
+ return; // don't send a beep time of 0
1277
+ }
1278
+
1279
+ return new Promise(resolve => {
1280
+ // https://en.wikipedia.org/wiki/MIDI_tuning_standard#Frequency_values
1281
+ const freq = Math.pow(2, ((note - 69 + 12) / 12)) * 440;
1282
+ this._peripheral.beep(freq, time);
1283
+
1284
+ // Run for some time even when no piezo is connected.
1285
+ setTimeout(resolve, time);
1286
+ });
1287
+ }
1288
+
1289
+ /**
1290
+ * Call a callback for each motor indexed by the provided motor ID.
1291
+ *
1292
+ * Note: This way of looping through motors is currently unnecessary, but could be
1293
+ * useful if an 'all motors' option is added in the future (see WeDo2 extension).
1294
+ *
1295
+ * @param {MotorID} motorID - the ID specifier.
1296
+ * @param {Function} callback - the function to call with the numeric motor index for each motor.
1297
+ * @private
1298
+ */
1299
+ _forEachMotor (motorID, callback) {
1300
+ let motors;
1301
+ switch (motorID) {
1302
+ case 0:
1303
+ motors = [0];
1304
+ break;
1305
+ case 1:
1306
+ motors = [1];
1307
+ break;
1308
+ case 2:
1309
+ motors = [2];
1310
+ break;
1311
+ case 3:
1312
+ motors = [3];
1313
+ break;
1314
+ default:
1315
+ log.warn(`Invalid motor ID: ${motorID}`);
1316
+ motors = [];
1317
+ break;
1318
+ }
1319
+ for (const index of motors) {
1320
+ callback(index);
1321
+ }
1322
+ }
1323
+
1324
+ /**
1325
+ * Formats menus into a format suitable for block menus, and loading previously
1326
+ * saved projects:
1327
+ * [
1328
+ * {
1329
+ * text: label,
1330
+ * value: index
1331
+ * },
1332
+ * {
1333
+ * text: label,
1334
+ * value: index
1335
+ * },
1336
+ * etc...
1337
+ * ]
1338
+ *
1339
+ * @param {array} menu - a menu to format.
1340
+ * @return {object} - a formatted menu as an object.
1341
+ * @private
1342
+ */
1343
+ _formatMenu (menu) {
1344
+ const m = [];
1345
+ for (let i = 0; i < menu.length; i++) {
1346
+ const obj = {};
1347
+ obj.text = menu[i];
1348
+ obj.value = i.toString();
1349
+ m.push(obj);
1350
+ }
1351
+ return m;
1352
+ }
1353
+ }
1354
+
1355
+ module.exports = Scratch3Ev3Blocks;