diff --git a/package-lock.json b/package-lock.json index 2d230b7b7..f91352f7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2193,7 +2193,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -2338,7 +2338,7 @@ }, "brfs": { "version": "1.6.1", - "resolved": "http://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz", + "resolved": "https://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz", "integrity": "sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==", "dev": true, "requires": { @@ -2388,7 +2388,7 @@ "browserify-des": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "integrity": "sha1-OvTx9Zg5QDVy8cZiBDdfen9wPpw=", "dev": true, "requires": { "cipher-base": "^1.0.1", @@ -2400,7 +2400,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=", "dev": true } } @@ -2802,7 +2802,7 @@ }, "colors": { "version": "0.6.2", - "resolved": "http://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", "dev": true }, @@ -2863,7 +2863,7 @@ "compression": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz", - "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==", + "integrity": "sha1-J+DhdqryYPfywoE8PkQK258Zk9s=", "dev": true, "requires": { "accepts": "~1.3.5", @@ -2893,7 +2893,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=", "dev": true } } @@ -3649,7 +3649,7 @@ "elliptic": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", - "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "integrity": "sha1-wtC3d2kRuGcixjLDwGxg8vgZk5o=", "dev": true, "requires": { "bn.js": "^4.4.0", @@ -4278,7 +4278,7 @@ "eslint-utils": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", - "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", + "integrity": "sha1-moUbqJ7nxGA0b5fPiTnHKYgn5RI=", "dev": true }, "eslint-visitor-keys": { @@ -4771,7 +4771,7 @@ "file-loader": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-2.0.0.tgz", - "integrity": "sha1-OXScgvAguehZAdz/mOgATmQBz94=", + "integrity": "sha512-YCsBfd1ZGCyonOKLxPiKPdu+8ld9HAaMEvJewzz+b2eTF7uL5Zm/HdBF6FjCrpCMRq25Mi0U1gl4pwn2TlH7hQ==", "dev": true, "requires": { "loader-utils": "^1.0.2", @@ -4781,7 +4781,7 @@ "ajv": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.4.tgz", - "integrity": "sha1-JH1SdBENtlNwa1UPzCt5fKKM/Fk=", + "integrity": "sha512-4Wyjt8+t6YszqaXnLDfMmG/8AlO5Zbcsy3ATHncCzjW/NoPzAId8AK6749Ybjmdt+kUY1gP60fCu46oDxPv/mg==", "dev": true, "requires": { "fast-deep-equal": "^2.0.1", @@ -4805,19 +4805,19 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha1-C3mpMgTXtgDUsoUNH2bCo0lRx3A=", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", "dev": true, "requires": { "ajv": "^6.1.0", @@ -4828,7 +4828,7 @@ "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "dev": true, "requires": { "punycode": "^2.1.0" @@ -5064,7 +5064,7 @@ "format-message": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/format-message/-/format-message-6.2.1.tgz", - "integrity": "sha1-kKJ9KJYNQRT5mRL9Uc2heJvdP/A=", + "integrity": "sha512-6gBXI+MOE9pu9QNeZf95V87GKYiLCdUvhisjts75xonaPAshtBu20NTZt2l8kbYcoMtxNyH9E9af+aieIRfVmw==", "requires": { "format-message-formats": "^6.2.0", "format-message-interpret": "^6.2.0", @@ -5471,7 +5471,7 @@ "fsevents": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "integrity": "sha1-9B3LGvJYKvNpLaNvxVy9jhBBxCY=", "dev": true, "optional": true, "requires": { @@ -5509,14 +5509,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5531,20 +5529,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5661,8 +5656,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5674,7 +5668,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5689,7 +5682,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5697,14 +5689,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -5723,7 +5713,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5804,8 +5793,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5817,7 +5805,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5939,7 +5926,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6387,7 +6373,7 @@ "hash.js": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", - "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", + "integrity": "sha1-44q0uF37HgxA/pJlwOm1SFTCOBI=", "dev": true, "requires": { "inherits": "^2.0.3", @@ -6862,7 +6848,7 @@ }, "is-builtin-module": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { @@ -8169,7 +8155,7 @@ }, "multipipe": { "version": "0.3.1", - "resolved": "http://registry.npmjs.org/multipipe/-/multipipe-0.3.1.tgz", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.3.1.tgz", "integrity": "sha1-kmJVJXYboE/qoJYFtjgrziyR8R8=", "dev": true, "requires": { @@ -8419,7 +8405,6 @@ "version": "0.1.4", "bundled": true, "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -9602,8 +9587,7 @@ "longest": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "loose-envify": { "version": "1.3.1", @@ -11182,7 +11166,7 @@ "original": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", - "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "integrity": "sha1-5EKmHP/hxf0gpl8yYcJmY7MD8l8=", "dev": true, "requires": { "url-parse": "^1.4.3" @@ -11550,7 +11534,7 @@ "proxy-addr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "integrity": "sha1-7PxzO/Iv+Mb0B/onUye5q2fki5M=", "dev": true, "requires": { "forwarded": "~0.1.2", @@ -11661,7 +11645,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -12173,7 +12157,7 @@ "dependencies": { "ajv": { "version": "6.3.0", - "resolved": "http://registry.npmjs.org/ajv/-/ajv-6.3.0.tgz", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.3.0.tgz", "integrity": "sha1-FlCkERTvAFdMrBC4Ay2PTBSBLac=", "requires": { "fast-deep-equal": "^1.0.0", @@ -12184,9 +12168,9 @@ } }, "scratch-render": { - "version": "0.1.0-prerelease.20181220195236", - "resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20181220195236.tgz", - "integrity": "sha512-FcYezDaztkoQifUG9k4uOsIFbelMt8JaCMpHrQis0QVJmjSuQrCDB3DRUjwpMuyUrmV7B7GdBUvwt65VYPZJ6g==", + "version": "0.1.0-prerelease.20190103190631", + "resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20190103190631.tgz", + "integrity": "sha512-DFh5dnvLQ0iv2UahMJgUrCqnMOPjM4NAN0fQUBtm4w5gskkFOem6QvTGPWcUf75II4MDwUbj+LGZclqEYWTnSw==", "dev": true, "requires": { "grapheme-breaker": "0.3.2", @@ -13008,7 +12992,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -13863,7 +13847,7 @@ "dependencies": { "pako": { "version": "0.2.9", - "resolved": "http://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", "dev": true } @@ -13977,7 +13961,7 @@ "upath": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", + "integrity": "sha1-NSVll+RqWB20eT0M5H+prr/J+r0=", "dev": true }, "uri-js": { @@ -14028,7 +14012,7 @@ "url-parse": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz", - "integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==", + "integrity": "sha1-v67kVciJAjIZ11fgRfpqaE7DbBU=", "dev": true, "requires": { "querystringify": "^2.0.0", diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 86a335989..de0093124 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -17,6 +17,7 @@ const Scratch3VideoSensingBlocks = require('../extensions/scratch3_video_sensing const Scratch3Speech2TextBlocks = require('../extensions/scratch3_speech2text'); const Scratch3Ev3Blocks = require('../extensions/scratch3_ev3'); const Scratch3MakeyMakeyBlocks = require('../extensions/scratch3_makeymakey'); +const Scratch3BoostBlocks = require('../extensions/scratch3_boost'); const builtinExtensions = { pen: Scratch3PenBlocks, @@ -28,7 +29,8 @@ const builtinExtensions = { videoSensing: Scratch3VideoSensingBlocks, speech2text: Scratch3Speech2TextBlocks, ev3: Scratch3Ev3Blocks, - makeymakey: Scratch3MakeyMakeyBlocks + makeymakey: Scratch3MakeyMakeyBlocks, + boost: Scratch3BoostBlocks }; /** diff --git a/src/extensions/scratch3_boost/index.js b/src/extensions/scratch3_boost/index.js new file mode 100644 index 000000000..970ba653f --- /dev/null +++ b/src/extensions/scratch3_boost/index.js @@ -0,0 +1,1780 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const Cast = require('../../util/cast'); +const formatMessage = require('format-message'); +const color = require('../../util/color'); +const BLE = require('../../io/ble'); +const Base64Util = require('../../util/base64-util'); +const MathUtil = require('../../util/math-util'); +const RateLimiter = require('../../util/rateLimiter.js'); +const log = require('../../util/log'); + +/** + * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. + * @type {string} + */ +// eslint-disable-next-line max-len +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=='; + +/** + * Boost BLE service UUID. + * @enum + */ +const BLEService = '00001623-1212-efde-1623-785feabcd123'; + +/** + * Boost BLE characteristic UUID. + * @enum + */ +const BLECharacteristic = '00001624-1212-efde-1623-785feabcd123'; + +/** + * A time interval to wait (in milliseconds) while a block that sends a BLE message is running. + * @type {number} + */ +const BLESendInterval = 100; + +/** + * A maximum number of BLE message sends per second, to be enforced by the rate limiter. + * @type {number} + */ +const BLESendRateMax = 20; + +/** + * Enum for Boost sensor and output types. + * @readonly + * @enum {number} + */ +const BoostDevice = { + MOTORINT: 0x27, + MOTOREXT: 0x26, + LED: 0x17, + TILT: 0x28, + COLOR: 0x25, + VOLTAGE: 0x14, + CURRENT: 0x15, +}; + +/** + * Enum for connection/port ids assigned to internal Boost output devices. + * @readonly + * @enum {number} + */ +// TODO: Check for these more accurately at startup? +const BoostConnectID = { + LED: 6, + PIEZO: 5 +}; + +/** + * Enum for ids for various output commands on the Boost. + * @readonly + * @enum {number} + */ +const BoostCommand = { + OUTPUT: 0x81, +}; + + +/** + * Enum for physical Boost Ports + * @readonly + * @enum {number} + */ +const BoostPort = { + A: 55, + B: 56, + C: 1, + D: 2 +}; + +const BoostColor = { + NONE: 255, + RED: 9, + BLUE: 3, + GREEN: 5, + YELLOW: 7, + WHITE: 10, + BLACK: 0 +} + +/** + * Enum for Message Types + * @readonly + * @enum {number} + */ + +const BoostMessageTypes = { + HUB_PROPERTIES: 0x01, + HUB_ACTIONS: 0x02, + HUB_ALERTS: 0x03, + HUB_ATTACHED_IO: 0x04, + ERROR: 0x05, + PORT_INFORMATION: 0x43, + PORT_MODEINFORMATION: 0x44, + PORT_VALUE: 0x45, + PORT_VALUE_COMBINED: 0x46, + PORT_INPUT_FORMAT: 0x47, + PORT_INPUT_FORMAT_COMBINED: 0x48, + PORT_OUTPUT_COMMAND_FEEDBACK: 0x82 +} + +/** + * Enum for Attached IO Message Lengths + * @readonly + * @enum {number} + */ + +const BoostIOEvent = { + ATTACHED: 0x01, + DETACHED: 0x00, + ATTACHED_VIRTUAL: 0x02 +} + +/** + * Enum for modes for input sensors on the Boost. + * @enum {number} + */ +const BoostMode = { + TILT: 0, // angle + DISTANCE: 0, // detect + LED: 1, // RGB + COLOR: 0, // Indexed colors + MOTOR: 2, // Position + UNKNOWN: 0 +}; + +/** + * Enum for units for input sensors on the Boost. + * + * 0 = raw + * 1 = percent + * + * @enum {number} + */ +const BoostUnit = { + TILT: 0, + DISTANCE: 1, + LED: 0, + MOTOR: 0, + COLOR: 0, + UNKNOWN:0 +}; + +function buf2hex(buffer) { // buffer is an ArrayBuffer + return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(' '); +} + +/** + * Manage power, direction, and timers for one Boost motor. + */ +class BoostMotor { + /** + * Construct a Boost Motor instance. + * @param {Boost} parent - the Boost peripheral which owns this motor. + * @param {int} index - the zero-based index of this motor on its parent peripheral. + */ + constructor (parent, index) { + /** + * The Boost peripheral which owns this motor. + * @type {Boost} + * @private + */ + this._parent = parent; + + /** + * The zero-based index of this motor on its parent peripheral. + * @type {int} + * @private + */ + this._index = index; + + /** + * This motor's current direction: 1 for "this way" or -1 for "that way" + * @type {number} + * @private + */ + this._direction = 1; + + /** + * This motor's current power level, in the range [0,100]. + * @type {number} + * @private + */ + this._power = 100; + + /** + * This motor's current relative position + * @type {number} + * @private + */ + this._position = 0; + + /** + * Is this motor currently moving? + * @type {boolean} + * @private + */ + this._isOn = false; + + /** + * If the motor has been turned on or is actively braking for a specific duration, this is the timeout ID for + * the end-of-action handler. Cancel this when changing plans. + * @type {Object} + * @private + */ + this._pendingTimeoutId = null; + + /** + * The starting time for the pending timeout. + * @type {Object} + * @private + */ + this._pendingTimeoutStartTime = null; + + /** + * The delay/duration of the pending timeout. + * @type {Object} + * @private + */ + this._pendingTimeoutDelay = null; + + this.startBraking = this.startBraking.bind(this); + this.turnOff = this.turnOff.bind(this); + } + + /** + * @return {number} - the duration of active braking after a call to startBraking(). Afterward, turn the motor off. + * @constructor + */ + static get BRAKE_TIME_MS () { + return 1000; + } + + /** + * @return {int} - this motor's current direction: 1 for "this way" or -1 for "that way" + */ + get direction () { + return this._direction; + } + + /** + * @param {int} value - this motor's new direction: 1 for "this way" or -1 for "that way" + */ + set direction (value) { + if (value < 0) { + this._direction = -1; + } else { + this._direction = 1; + } + } + + /** + * @return {int} - this motor's current power level, in the range [0,100]. + */ + get power () { + return this._power; + } + + /** + * @param {int} value - this motor's new power level, in the range [0,100]. + */ + set power (value) { + const p = Math.max(0, Math.min(value, 100)); + // Lego Boost hub only turns motors at power range [30 - 100], so + // map value from [0 - 100] to [30 - 100]. + if (p === 0) { + this._power = 0; + } else { + const delta = 100 / p; + this._power = 30 + (70 / delta); + } + } + + /** + * @return {int} - + */ + get position () { + return this._position; + } + + /** + * @param {int} value - + */ + set position (value) { + // Todo: wrap around rotation to avoid extremely large numbers + this._position = value; + } + + /** + * @return {boolean} - true if this motor is currently moving, false if this motor is off or braking. + */ + get isOn () { + return this._isOn; + } + + /** + * @return {boolean} - time, in milliseconds, of when the pending timeout began. + */ + get pendingTimeoutStartTime () { + return this._pendingTimeoutStartTime; + } + + /** + * @return {boolean} - delay, in milliseconds, of the pending timeout. + */ + get pendingTimeoutDelay () { + return this._pendingTimeoutDelay; + } + + /** + * Turn this motor on indefinitely. + */ + turnOn () { + if (this._power === 0) return; + //console.log(this._index) + //console.log(this._power * this._direction) + const cmd = this._parent.generateOutputCommand( + this._index, + 0x51, + [this._power * this._direction] // power in range 0-100 + ); + + this._parent.send(BLECharacteristic, cmd); + + this._isOn = true; + this._clearTimeout(); + } + + /** + * Turn this motor on for a specific duration. + * @param {number} milliseconds - run the motor for this long. + */ + turnOnFor (milliseconds) { + if (this._power === 0) return; + + milliseconds = Math.max(0, milliseconds); + this.turnOn(); + this._setNewTimeout(this.startBraking, milliseconds); + } + + /** + * Start active braking on this motor. After a short time, the motor will turn off. + * // TODO: rename this to coastAfter? + */ + startBraking () { + if (this._power === 0) return; + + const cmd = this._parent.generateOutputCommand( + this._index, + BoostCommand.MOTOR_POWER, + [127] // 127 = break + ); + + this._parent.send(BLECharacteristic, cmd); + + this._isOn = false; + this._setNewTimeout(this.turnOff, BoostMotor.BRAKE_TIME_MS); + } + + /** + * Turn this motor off. + * @param {boolean} [useLimiter=true] - if true, use the rate limiter + */ + turnOff (useLimiter = true) { + if (this._power === 0) return; + + const cmd = this._parent.generateOutputCommand( + this._index, + BoostCommand.MOTOR_POWER, + [0] // 0 = stop + ); + + this._parent.send(BLECharacteristic, cmd, useLimiter); + + this._isOn = false; + } + + /** + * Clear the motor action timeout, if any. Safe to call even when there is no pending timeout. + * @private + */ + _clearTimeout () { + if (this._pendingTimeoutId !== null) { + clearTimeout(this._pendingTimeoutId); + this._pendingTimeoutId = null; + this._pendingTimeoutStartTime = null; + this._pendingTimeoutDelay = null; + } + } + + /** + * Set a new motor action timeout, after clearing an existing one if necessary. + * @param {Function} callback - to be called at the end of the timeout. + * @param {int} delay - wait this many milliseconds before calling the callback. + * @private + */ + _setNewTimeout (callback, delay) { + this._clearTimeout(); + const timeoutID = setTimeout(() => { + if (this._pendingTimeoutId === timeoutID) { + this._pendingTimeoutId = null; + this._pendingTimeoutStartTime = null; + this._pendingTimeoutDelay = null; + } + callback(); + }, delay); + this._pendingTimeoutId = timeoutID; + this._pendingTimeoutStartTime = Date.now(); + this._pendingTimeoutDelay = delay; + } +} + +/** + * Manage communication with a Boost peripheral over a Bluetooth Low Energy client socket. + */ +class Boost { + + constructor (runtime, extensionId) { + + /** + * The Scratch 3.0 runtime used to trigger the green flag button. + * @type {Runtime} + * @private + */ + this._runtime = runtime; + this._runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this)); + + /** + * The id of the extension this peripheral belongs to. + */ + this._extensionId = extensionId; + + /** + * A list of the ids of the motors or sensors in ports A, B, C or D. + * @type {string[]} + * @private + */ + this._ports = []; + + /** + * The motors which this Boost could possibly have. + * @type {BoostMotor[]} + * @private + */ + this._motors = []; + + /** + * The most recently received value for each sensor. + * @type {Object.} + * @private + */ + this._sensors = { + tiltX: 0, + tiltY: 0, + distance: 0, + color: 0, + }; + + /** + * The Bluetooth connection socket for reading/writing peripheral data. + * @type {BLE} + * @private + */ + this._ble = null; + this._runtime.registerPeripheralExtension(extensionId, this); + + /** + * A rate limiter utility, to help limit the rate at which we send BLE messages + * over the socket to Scratch Link to a maximum number of sends per second. + * @type {RateLimiter} + * @private + */ + this._rateLimiter = new RateLimiter(BLESendRateMax); + + this._onConnect = this._onConnect.bind(this); + this._onMessage = this._onMessage.bind(this); + } + + /** + * @return {number} - the latest value received for the tilt sensor's tilt about the X axis. + */ + get tiltX () { + return this._sensors.tiltX; + } + + /** + * @return {number} - the latest value received for the tilt sensor's tilt about the Y axis. + */ + get tiltY () { + return this._sensors.tiltY; + } + + /** + * @return {number} - the latest value received from the distance sensor. + */ + get distance () { + return this._sensors.distance; + } + + /** + * @return {number} - the latest color value received from the vision sensor. + */ + get color () { + return this._sensors.color; + } + + /** + * Access a particular motor on this peripheral. + * @param {int} index - the index of the desired motor. + * @return {BoostMotor} - the BoostMotor instance, if any, at that index. + */ + motor (index) { + return this._motors[index]; + } + + /** + * Stop all the motors that are currently running. + */ + stopAllMotors () { + this._motors.forEach(motor => { + if (motor) { + // Send the motor off command without using the rate limiter. + // This allows the stop button to stop motors even if we are + // otherwise flooded with commands. + motor.turnOff(false); + } + }); + } + + /** + * Set the Boost peripheral's LED to a specific color. + * @param {int} inputRGB - a 24-bit RGB color in 0xRRGGBB format. + * @return {Promise} - a promise of the completion of the set led send operation. + */ + setLED (inputRGB) { + const rgb = [ + (inputRGB >> 16) & 0x000000FF, + (inputRGB >> 8) & 0x000000FF, + (inputRGB) & 0x000000FF + ]; + + const cmd = this.generateOutputCommand( + 50, + 0x51, + [0x51,0x01].concat(rgb) + ); + + return this.send(BLECharacteristic, cmd); + } + + /** + * Sets the input mode of the LED to RGB. + * @return {Promise} - a promise returned by the send operation. + */ + setLEDMode () { + const cmd = this.generateInputCommand( + this._ports.indexOf(BoostDevice.LED), + BoostMode.LED, + 0, + BoostUnit.LED, + false + ); + + return this.send(BLECharacteristic, cmd); + } + + /** + * Switch off the LED on the Boost. + * @return {Promise} - a promise of the completion of the stop led send operation. + */ + stopLED () { + const cmd = this.generateOutputCommand( + BoostConnectID.LED, + 0x32, + [0, 0, 0] + ); + + return this.send(BLECharacteristic, cmd); + } + + /** + * Stop the motors on the Boost peripheral. + */ + stopAll () { + if (!this.isConnected()) return; + this.stopAllMotors(); + } + + /** + * Called by the runtime when user wants to scan for a Boost peripheral. + */ + scan () { + if (this._ble) { + this._ble.disconnect(); + } + this._ble = new BLE(this._runtime, this._extensionId, { + filters: [{ + services: [BLEService] + }], + optionalServices: [] + }, this._onConnect); + } + + /** + * Called by the runtime when user wants to connect to a certain Boost peripheral. + * @param {number} id - the id of the peripheral to connect to. + */ + connect (id) { + if (this._ble) { + this._ble.connectPeripheral(id); + } + } + + /** + * Disconnects from the current BLE socket. + */ + disconnect () { + this._ports = []; + this._motors = []; + this._sensors = { + tiltX: 0, + tiltY: 0, + distance: 0, + color: 0 + }; + + if (this._ble) { + this._ble.disconnect(); + } + } + + /** + * Called by the runtime to detect whether the Boost peripheral is connected. + * @return {boolean} - the connected state. + */ + isConnected () { + let connected = false; + if (this._ble) { + connected = this._ble.isConnected(); + } + return connected; + } + + /** + * Write a message to the Boost peripheral BLE socket. + * @param {number} uuid - the UUID of the characteristic to write to + * @param {Array} message - the message to write. + * @param {boolean} [useLimiter=true] - if true, use the rate limiter + * @return {Promise} - a promise result of the write operation + */ + send (uuid, message, useLimiter = true) { + if (!this.isConnected()) return Promise.resolve(); + + if (useLimiter) { + if (!this._rateLimiter.okayToSend()) return Promise.resolve(); + } + + return this._ble.write( + BLEService, + uuid, + Base64Util.uint8ArrayToBase64(message), + 'base64' + ); + } + + /** + * Generate a Boost 'Output Command' in the byte array format + * (CONNECT ID, COMMAND ID, NUMBER OF BYTES, VALUES ...). + * + * This sends a command to the Boost to actuate the specified outputs. + * + * @param {number} connectID - the port (Connect ID) to send a command to. + * @param {number} commandID - the id of the byte command. + * @param {array} values - the list of values to write to the command. + * @return {array} - a generated output command. + */ + generateOutputCommand (connectID, subCommandID = 0x51, values = null) { + let command = [0x00, BoostCommand.OUTPUT]; + if (values) { + command = command.concat( + connectID + ).concat( + 0x00 // Execute immediately + ); + + if(subCommandID) { + command = command.concat(subCommandID); + } + command = command.concat(0x00) + + command = command/*.concat( + values.length + )*/.concat( + values + ); + } + command.unshift(command.length +1) + console.log(command) + return command; + } + + /** + * Generate a Boost 'Input Command' in the byte array format + * (COMMAND ID, COMMAND TYPE, CONNECT ID, TYPE ID, MODE, DELTA INTERVAL (4 BYTES), + * UNIT, NOTIFICATIONS ENABLED). + * + * This sends a command to the Boost that sets that input format + * of the specified inputs and sets value change notifications. + * + * @param {number} connectID - the port (Connect ID) to send a command to. + * @param {number} mode - the mode of the input sensor. + * @param {number} delta - the delta change needed to trigger notification. + * @param {array} units - the unit of the input sensor value. + * @param {boolean} enableNotifications - whether to enable notifications. + * @return {array} - a generated input command. + */ + generateInputCommand (connectID, mode, delta, units, enableNotifications) { + var command = [ + 0x00, // Hub ID + 0x41, // Message Type (Port Input Format Setup (Single)) + connectID, + mode, + delta, + enableNotifications + ]; + + command.unshift(command.length+1) // Prepend payload with length byte + + // Add checksum + console.log(buf2hex(command)) + //console.log(command) + + return command; + } + + /** + * Sets LED mode and initial color and starts reading data from peripheral after BLE has connected. + * @private + */ + _onConnect () { + //this.setLEDMode(); + //this.setLED(0x00FF00); + this._ble.startNotifications( + BLEService, + BLECharacteristic, + this._onMessage + ); + } + + /** + * Process the sensor data from the incoming BLE characteristic. + * @param {object} base64 - the incoming BLE data. + * @private + */ + _onMessage (base64) { + const data = Base64Util.base64ToUint8Array(base64); + //console.log(data) + // log.info(data); + + /** + * First three bytes are the common header: + * 0: Length of message + * 1: Hub ID (always 0x00 at the moment) + * 2: Message Type + * + * We base our switch-case on Message Type + */ + + switch (data[2]) { + case BoostMessageTypes.HUB_ATTACHED_IO: // IO Attach/Detach events + + /* + * 3: Port ID + * 4: Event + * 5: IO Type ID + */ + switch (data[4]) { + case BoostIOEvent.ATTACHED: + //case BoostIOEvent.ATTACHED_VIRTUAL: + //console.log("New sensor or motor registered!") + this._registerSensorOrMotor(data[3], data[5]) + //console.log(data[3] + "," + data[5]) + break; + case BoostIOEvent.DETACHED: + this._clearPort(data[3]); + break; + default: + console.log("No I/O Event case found!") + } + break; + case BoostMessageTypes.PORT_VALUE: + var type = this._ports[data[3]]; + var valueFormat = data.length + switch(valueFormat) { + // TODO: we can't know whether a 2- or 4-byte value is multiple values or a uint16/32-value respectively. We need to utilize the value format information provided in https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#value-format + case 5: + var value = data[4]; + break; + case 6: + var value = [data[4], data[5]]; + break; + case 8: + //console.log(Uint32Array(data.slice(4,8))) + //console.log(buf2hex(data)) + var dv = new DataView(data.slice(4,8)) + console.log(dv.getInt32); + //var value = new Uint32Array(data.slice(4,8))[0] + break; + default: + // Do nothing + } + switch(type) { + case BoostDevice.TILT: + this._sensors.tiltX = value[0] + this._sensors.tiltY = value[1] + break; + case BoostDevice.COLOR: + this._sensors.color = value; + break; + case BoostDevice.MOTOREXT: + // ToDo: Handle external motor sensor + break; + case BoostDevice.MOTORINT: + //console.log(data[3] + "," + valueFormat + ": " + value) + this._motors[data[3]]._position = value + break; + case BoostDevice.CURRENT: + case BoostDevice.VOLTAGE: + break; + // Do nothing + default: + console.log("Unknown sensor value! Type: " + type) + } + break; + case BoostMessageTypes.ERROR: + console.log("Error in BLE message! Errorneous command: " + data[3]) + break; + default: + //console.log("No case found for message:") + //console.log(data) + } + } + + /** + * Register a new sensor or motor connected at a port. Store the type of + * sensor or motor internally, and then register for notifications on input + * values if it is a sensor. + * @param {number} connectID - the port to register a sensor or motor on. + * @param {number} type - the type ID of the sensor or motor + * @private + */ + _registerSensorOrMotor (connectID, type) { + // Record which port is connected to what type of device + this._ports[connectID] = type; + + // Record motor port + if (type === BoostDevice.MOTORINT || type === BoostDevice.MOTOREXT) { + this._motors[connectID] = new BoostMotor(this, connectID); + } + + // Set input format for tilt or distance sensor + var typeString = '' + switch(type) { + case BoostDevice.MOTORINT: + case BoostDevice.MOTOREXT: + typeString = 'MOTOR' + break; + case BoostDevice.COLOR: + typeString = 'COLOR' + break; + case BoostDevice.LED: + typeString = 'LED' + console.log("NOW!") + console.log("LED: " + connectID + ", " + type) + this.setLEDMode(); + this.setLED(0x00FF00); + break; + case BoostDevice.TILT: + typeString = 'TILT' + break; + default: + typeString = 'UNKNOWN' + } + //const typeString = type === BoostDevice.DISTANCE ? 'DISTANCE' : 'TILT'; + const cmd = this.generateInputCommand( + connectID, + BoostMode[typeString], + 1, + BoostUnit[typeString], + true + ); + + this.send(BLECharacteristic, cmd); + } + + /** + * Clear the sensor or motor present at port 1 or 2. + * @param {number} connectID - the port to clear. + * @private + */ + _clearPort (connectID) { + const type = this._ports[connectID]; + if (type === BoostDevice.TILT) { + this._sensors.tiltX = this._sensors.tiltY = 0; + } + if (type === BoostDevice.DISTANCE) { + this._sensors.distance = 0; + } + this._ports[connectID] = 'none'; + this._motors[connectID] = null; + } +} + +/** + * Enum for motor specification. + * @readonly + * @enum {string} + */ +const BoostMotorLabel = { + DEFAULT: 'motor', + A: 'motor A', + B: 'motor B', + C: 'motor C', + D: 'motor D', + ALL: 'all motors' +}; + +/** + * Enum for motor direction specification. + * @readonly + * @enum {string} + */ +const BoostMotorDirection = { + FORWARD: 'this way', + BACKWARD: 'that way', + REVERSE: 'reverse' +}; + +/** + * Enum for tilt sensor direction. + * @readonly + * @enum {string} + */ +const BoostTiltDirection = { + UP: 'up', + DOWN: 'down', + LEFT: 'left', + RIGHT: 'right', + ANY: 'any' +}; + +/** + * Scratch 3.0 blocks to interact with a LEGO Boost peripheral. + */ +class Scratch3BoostBlocks { + + /** + * @return {string} - the ID of this extension. + */ + static get EXTENSION_ID () { + return 'boost'; + } + + /** + * @return {number} - the tilt sensor counts as "tilted" if its tilt angle meets or exceeds this threshold. + */ + static get TILT_THRESHOLD () { + return 15; + } + + /** + * Construct a set of Boost blocks. + * @param {Runtime} runtime - the Scratch 3.0 runtime. + */ + constructor (runtime) { + /** + * The Scratch 3.0 runtime. + * @type {Runtime} + */ + this.runtime = runtime; + + // Create a new Boost peripheral instance + this._peripheral = new Boost(this.runtime, Scratch3BoostBlocks.EXTENSION_ID); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: Scratch3BoostBlocks.EXTENSION_ID, + name: 'Boost', + blockIconURI: iconURI, + showStatusButton: true, + blocks: [ + { + opcode: 'motorOnFor', + text: formatMessage({ + id: 'boost.motorOnFor', + default: 'turn [MOTOR_ID] on for [DURATION] seconds', + description: 'turn a motor on for some time' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.DEFAULT + }, + DURATION: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'motorOn', + text: formatMessage({ + id: 'boost.motorOn', + default: 'turn [MOTOR_ID] on', + description: 'turn a motor on indefinitely' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.DEFAULT + } + } + }, + { + opcode: 'motorOff', + text: formatMessage({ + id: 'boost.motorOff', + default: 'turn [MOTOR_ID] off', + description: 'turn a motor off' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.DEFAULT + } + } + }, + { + opcode: 'startMotorPower', + text: formatMessage({ + id: 'boost.startMotorPower', + default: 'set [MOTOR_ID] power to [POWER]', + description: 'set the motor\'s power and turn it on' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.DEFAULT + }, + POWER: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + { + opcode: 'setMotorDirection', + text: formatMessage({ + id: 'boost.setMotorDirection', + default: 'set [MOTOR_ID] direction to [MOTOR_DIRECTION]', + description: 'set the motor\'s turn direction' + }), + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.DEFAULT + }, + MOTOR_DIRECTION: { + type: ArgumentType.STRING, + menu: 'MOTOR_DIRECTION', + defaultValue: BoostMotorDirection.FORWARD + } + } + }, + { + opcode: 'setLightHue', + text: formatMessage({ + id: 'boost.setLightHue', + default: 'set light color to [HUE]', + description: 'set the LED color' + }), + blockType: BlockType.COMMAND, + arguments: { + HUE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + }, + { + opcode: 'whenDistance', + text: formatMessage({ + id: 'boost.whenDistance', + default: 'when distance [OP] [REFERENCE]', + description: 'check for when distance is < or > than reference' + }), + blockType: BlockType.HAT, + arguments: { + OP: { + type: ArgumentType.STRING, + menu: 'OP', + defaultValue: '<' + }, + REFERENCE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + }, + { + opcode: 'whenColor', + text: formatMessage({ + id: 'boost.whenColor', + default: 'when color [COLOR]', + description: 'check for when color' + }), + blockType: BlockType.HAT, + arguments: { + COLOR: { + type: ArgumentType.STRING, + menu: 'COLOR', + defaultValue: BoostColor.RED + } + } + }, + { + opcode: 'whenTilted', + text: formatMessage({ + id: 'boost.whenTilted', + default: 'when tilted [TILT_DIRECTION_ANY]', + description: 'check when tilted in a certain direction' + }), + func: 'isTilted', + blockType: BlockType.HAT, + arguments: { + TILT_DIRECTION_ANY: { + type: ArgumentType.STRING, + menu: 'TILT_DIRECTION_ANY', + defaultValue: BoostTiltDirection.ANY + } + } + }, + { + opcode: 'getDistance', + text: formatMessage({ + id: 'boost.getDistance', + default: 'distance', + description: 'the value returned by the distance sensor' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'isTilted', + text: formatMessage({ + id: 'boost.isTilted', + default: 'tilted [TILT_DIRECTION_ANY]?', + description: 'whether the tilt sensor is tilted' + }), + blockType: BlockType.BOOLEAN, + arguments: { + TILT_DIRECTION_ANY: { + type: ArgumentType.STRING, + menu: 'TILT_DIRECTION_ANY', + defaultValue: BoostTiltDirection.ANY + } + } + }, + { + opcode: 'getTiltAngle', + text: formatMessage({ + id: 'boost.getTiltAngle', + default: 'tilt angle [TILT_DIRECTION]', + description: 'the angle returned by the tilt sensor' + }), + blockType: BlockType.REPORTER, + arguments: { + TILT_DIRECTION: { + type: ArgumentType.STRING, + menu: 'TILT_DIRECTION', + defaultValue: BoostTiltDirection.UP + } + } + }, + { + opcode: 'getColor', + text: formatMessage({ + id: 'boost.getColor', + default: 'color', + description: 'the color returned by the vision sensor' + }), + blockType: BlockType.REPORTER + }, + { + opcode: 'getMotorPosition', + text: formatMessage({ + id: 'boost.getMotorPosition', + default: 'motor position [MOTOR_ID]', + description: 'the position returned by the motor' + }), + blockType: BlockType.REPORTER, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'MOTOR_ID', + defaultValue: BoostMotorLabel.A + } + } + } + ], + menus: { + MOTOR_ID: [ + { + text: formatMessage({ + id: 'boost.motorId.default', + default: 'motor', + description: 'label for motor element in motor menu for LEGO Boost extension' + }), + value: BoostMotorLabel.DEFAULT + }, + { + text: formatMessage({ + id: 'boost.motorId.a', + default: 'motor A', + description: 'label for motor A element in motor menu for LEGO Boost extension' + }), + value: BoostMotorLabel.A + }, + { + text: formatMessage({ + id: 'boost.motorId.b', + default: 'motor B', + description: 'label for motor B element in motor menu for LEGO Boost extension' + }), + value: BoostMotorLabel.B + }, + { + text: formatMessage({ + id: 'boost.motorId.c', + default: 'motor C', + description: 'label for motor C element in motor menu for LEGO Boost extension' + }), + value: BoostMotorLabel.C + }, + { + text: formatMessage({ + id: 'boost.motorId.d', + default: 'motor D', + description: 'label for motor D element in motor menu for LEGO Boost extension' + }), + value: BoostMotorLabel.D + }, + { + text: formatMessage({ + id: 'boost.motorId.all', + default: 'all motors', + description: 'label for all motors element in motor menu for LEGO Boost extension' + }), + value: BoostMotorLabel.ALL + } + ], + MOTOR_DIRECTION: [ + { + text: formatMessage({ + id: 'boost.motorDirection.forward', + default: 'this way', + description: 'label for forward element in motor direction menu for LEGO Boost extension' + }), + value: BoostMotorDirection.FORWARD + }, + { + text: formatMessage({ + id: 'boost.motorDirection.backward', + default: 'that way', + description: 'label for backward element in motor direction menu for LEGO Boost extension' + }), + value: BoostMotorDirection.BACKWARD + }, + { + text: formatMessage({ + id: 'boost.motorDirection.reverse', + default: 'reverse', + description: 'label for reverse element in motor direction menu for LEGO Boost extension' + }), + value: BoostMotorDirection.REVERSE + } + ], + TILT_DIRECTION: [ + { + text: formatMessage({ + id: 'boost.tiltDirection.up', + default: 'up', + description: 'label for up element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.UP + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.down', + default: 'down', + description: 'label for down element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.DOWN + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.left', + default: 'left', + description: 'label for left element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.LEFT + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.right', + default: 'right', + description: 'label for right element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.RIGHT + } + ], + TILT_DIRECTION_ANY: [ + { + text: formatMessage({ + id: 'boost.tiltDirection.up', + default: 'up' + }), + value: BoostTiltDirection.UP + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.down', + default: 'down' + }), + value: BoostTiltDirection.DOWN + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.left', + default: 'left' + }), + value: BoostTiltDirection.LEFT + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.right', + default: 'right' + }), + value: BoostTiltDirection.RIGHT + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.any', + default: 'any', + description: 'label for any element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.ANY + } + ], + COLOR: [ + { + text: formatMessage({ + id: 'boost.color.red', + default: 'red' + }), + value: BoostColor.RED + }, + { + text: formatMessage({ + id: 'boost.color.blue', + default: 'blue' + }), + value: BoostColor.BLUE + }, + { + text: formatMessage({ + id: 'boost.color.green', + default: 'green' + }), + value: BoostColor.GREEN + }, + { + text: formatMessage({ + id: 'boost.color.yellow', + default: 'yellow' + }), + value: BoostColor.YELLOW + }, + { + text: formatMessage({ + id: 'boost.color.white', + default: 'white' + }), + value: BoostColor.WHITE + }, + { + text: formatMessage({ + id: 'boost.color.black', + default: 'black', + description: 'black' + }), + value: BoostColor.BLACK + } + ], + OP: ['<', '>'] + } + }; + } + + /** + * Turn specified motor(s) on for a specified duration. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to activate. + * @property {int} DURATION - the amount of time to run the motors. + * @return {Promise} - a promise which will resolve at the end of the duration. + */ + motorOnFor (args) { + // TODO: cast args.MOTOR_ID? + let durationMS = Cast.toNumber(args.DURATION) * 1000; + durationMS = MathUtil.clamp(durationMS, 0, 15000); + return new Promise(resolve => { + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.turnOnFor(durationMS); + } + }); + + // Run for some time even when no motor is connected + setTimeout(resolve, durationMS); + }); + } + + /** + * Turn specified motor(s) on indefinitely. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to activate. + * @return {Promise} - a Promise that resolves after some delay. + */ + motorOn (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.turnOn(); + } + }); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Turn specified motor(s) off. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to deactivate. + * @return {Promise} - a Promise that resolves after some delay. + */ + motorOff (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.turnOff(); + } + }); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Turn specified motor(s) off. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to be affected. + * @property {int} POWER - the new power level for the motor(s). + * @return {Promise} - a Promise that resolves after some delay. + */ + startMotorPower (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + motor.power = MathUtil.clamp(Cast.toNumber(args.POWER), 0, 100); + motor.turnOn(); + } + }); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Set the direction of rotation for specified motor(s). + * If the direction is 'reverse' the motor(s) will be reversed individually. + * @param {object} args - the block's arguments. + * @property {MotorID} MOTOR_ID - the motor(s) to be affected. + * @property {MotorDirection} MOTOR_DIRECTION - the new direction for the motor(s). + * @return {Promise} - a Promise that resolves after some delay. + */ + setMotorDirection (args) { + // TODO: cast args.MOTOR_ID? + this._forEachMotor(args.MOTOR_ID, motorIndex => { + const motor = this._peripheral.motor(motorIndex); + if (motor) { + switch (args.MOTOR_DIRECTION) { + case BoostMotorDirection.FORWARD: + motor.direction = 1; + break; + case BoostMotorDirection.BACKWARD: + motor.direction = -1; + break; + case BoostMotorDirection.REVERSE: + motor.direction = -motor.direction; + break; + default: + log.warn(`Unknown motor direction in setMotorDirection: ${args.DIRECTION}`); + break; + } + // keep the motor on if it's running, and update the pending timeout if needed + if (motor.isOn) { + if (motor.pendingTimeoutDelay) { + motor.turnOnFor(motor.pendingTimeoutStartTime + motor.pendingTimeoutDelay - Date.now()); + } else { + motor.turnOn(); + } + } + } + }); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Set the LED's hue. + * @param {object} args - the block's arguments. + * @property {number} HUE - the hue to set, in the range [0,100]. + * @return {Promise} - a Promise that resolves after some delay. + */ + setLightHue (args) { + // Convert from [0,100] to [0,360] + let inputHue = Cast.toNumber(args.HUE); + inputHue = MathUtil.wrapClamp(inputHue, 0, 100); + const hue = inputHue * 360 / 100; + + const rgbObject = color.hsvToRgb({h: hue, s: 1, v: 1}); + + const rgbDecimal = color.rgbToDecimal(rgbObject); + + this._peripheral.setLED(rgbDecimal); + + return new Promise(resolve => { + window.setTimeout(() => { + resolve(); + }, BLESendInterval); + }); + } + + /** + * Compare the distance sensor's value to a reference. + * @param {object} args - the block's arguments. + * @property {string} OP - the comparison operation: '<' or '>'. + * @property {number} REFERENCE - the value to compare against. + * @return {boolean} - the result of the comparison, or false on error. + */ + whenDistance (args) { + switch (args.OP) { + case '<': + return this._peripheral.distance < Cast.toNumber(args.REFERENCE); + case '>': + return this._peripheral.distance > Cast.toNumber(args.REFERENCE); + default: + log.warn(`Unknown comparison operator in whenDistance: ${args.OP}`); + return false; + } + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {object} args - the block's arguments. + * @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + */ + whenTilted (args) { + return this._isTilted(args.TILT_DIRECTION_ANY); + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {object} args - the block's arguments. + * @property {Color} COLOR - the color to test. + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + */ + whenColor (args) { + return this._isColor(args.COLOR); + } + + /** + * @return {number} - the distance sensor's value, scaled to the [0,100] range. + */ + getDistance () { + return this._peripheral.distance; + } + + /** + * @return {number} - the vision sensor's color value. Indexed LEGO brick colors. + */ + getColor () { + return this._peripheral.color; + } + + /** + * @return {number} + */ + getMotorPosition (args) { + switch(args.MOTOR_ID) { + case BoostMotorLabel.A: + return this._peripheral._motors[BoostPort.A].position + case BoostMotorLabel.B: + return this._peripheral._motors[BoostPort.B].position + case BoostMotorLabel.C: + return this._peripheral._motors[BoostPort.C].position + case BoostMotorLabel.D: + return this._peripheral._motors[BoostPort.D].position + default: + log.warn("Asked for a motor position that doesnt exist!") + return false; + } + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {object} args - the block's arguments. + * @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + */ + isTilted (args) { + return this._isTilted(args.TILT_DIRECTION_ANY); + } + + /** + * @param {object} args - the block's arguments. + * @property {TiltDirection} TILT_DIRECTION - the direction (up, down, left, right) to check. + * @return {number} - the tilt sensor's angle in the specified direction. + * Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right). + */ + getTiltAngle (args) { + return this._getTiltAngle(args.TILT_DIRECTION); + } + + /** + * Test whether the tilt sensor is currently tilted. + * @param {TiltDirection} direction - the tilt direction to test (up, down, left, right, or any). + * @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + * @private + */ + _isTilted (direction) { + switch (direction) { + case BoostTiltDirection.ANY: + return (Math.abs(this._peripheral.tiltX) >= Scratch3BoostBlocks.TILT_THRESHOLD) || + (Math.abs(this._peripheral.tiltY) >= Scratch3BoostBlocks.TILT_THRESHOLD); + default: + return this._getTiltAngle(direction) >= Scratch3BoostBlocks.TILT_THRESHOLD; + } + } + + /** + * Test whether the vision sensor is detecting a certain color. + * @param {Color} color - the color to test. + * @return {boolean} - true when the color sensor senses the specified color. + * @private + */ + _isColor (color) { + return this.getColor() === color; + } + + /** + * @param {TiltDirection} direction - the direction (up, down, left, right) to check. + * @return {number} - the tilt sensor's angle in the specified direction. + * Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right). + * @private + */ + _getTiltAngle (direction) { + switch (direction) { + case BoostTiltDirection.UP: + return this._peripheral.tiltY > 45 ? 256 - this._peripheral.tiltY : -this._peripheral.tiltY; + case BoostTiltDirection.DOWN: + return this._peripheral.tiltY > 45 ? this._peripheral.tiltY - 256 : this._peripheral.tiltY; + case BoostTiltDirection.LEFT: + return this._peripheral.tiltX > 45 ? 256 - this._peripheral.tiltX : -this._peripheral.tiltX; + case BoostTiltDirection.RIGHT: + return this._peripheral.tiltX > 45 ? this._peripheral.tiltX - 256 : this._peripheral.tiltX; + default: + log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`); + } + } + + /** + * Call a callback for each motor indexed by the provided motor ID. + * @param {MotorID} motorID - the ID specifier. + * @param {Function} callback - the function to call with the numeric motor index for each motor. + * @private + */ + _forEachMotor (motorID, callback) { + let motors; + switch (motorID) { + case BoostMotorLabel.A: + motors = [BoostPort.A]; + break; + case BoostMotorLabel.B: + motors = [BoostPort.B]; + break; + case BoostMotorLabel.C: + motors = [BoostPort.C]; + break; + case BoostMotorLabel.D: + motors = [BoostPort.D]; + break; + case BoostMotorLabel.ALL: + case BoostMotorLabel.DEFAULT: + motors = [BoostPort.A, BoostPort.B, BoostPort.C, BoostPort.D]; + break; + default: + log.warn(`Invalid motor ID: ${motorID}`); + motors = []; + break; + } + for (const index of motors) { + callback(index); + } + } +} + +module.exports = Scratch3BoostBlocks;