diff --git a/package-lock.json b/package-lock.json index 2d230b7b7..e752fbaa2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1088,6 +1088,11 @@ } } }, + "@vernier/godirect": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@vernier/godirect/-/godirect-1.3.0.tgz", + "integrity": "sha512-vDdl1yCiKwPm/L/ca87cjSSDoFviWPx7IvT3SQxMqplYsACjCWrcW4yIECBfKuK9ok9cTFrXCSsmnpmDDApI8A==" + }, "@webassemblyjs/ast": { "version": "1.5.13", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.5.13.tgz", @@ -1537,7 +1542,7 @@ "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=", "dev": true }, "arr-union": { @@ -1713,7 +1718,7 @@ }, "aws4": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "resolved": "http://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" }, "babel-code-frame": { @@ -2177,7 +2182,7 @@ }, "bl": { "version": "1.2.2", - "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "dev": true, "requires": { @@ -2231,7 +2236,7 @@ "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "integrity": "sha1-LN4J617jQfSEdGuwMJsyU7GxRC8=", "dev": true }, "body-parser": { @@ -2338,7 +2343,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": { @@ -2470,7 +2475,7 @@ "buffer-indexof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "integrity": "sha1-Uvq8xqYG0aADAoAmSO9o9jnaJow=", "dev": true }, "buffer-shims": { @@ -2672,7 +2677,7 @@ "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=", "dev": true, "requires": { "inherits": "^2.0.1", @@ -2797,12 +2802,12 @@ "color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "integrity": "sha1-k4NDeaHMmgxh+C9S8NBDIiUb1aI=", "dev": true }, "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 }, @@ -2945,7 +2950,7 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", "dev": true }, "convert-source-map": { @@ -3286,7 +3291,7 @@ "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "integrity": "sha1-OWz58xN/A+S45TLFj2mCVOAPgOw=", "dev": true, "requires": { "browserify-cipher": "^1.0.0", @@ -4380,7 +4385,7 @@ "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "integrity": "sha1-f8vbGY3HGVlDLv4ThCaE4FJaywI=", "dev": true, "requires": { "md5.js": "^1.3.4", @@ -4950,7 +4955,7 @@ "dependencies": { "commander": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/commander/-/commander-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=", "dev": true } @@ -5315,7 +5320,7 @@ "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0", @@ -5509,14 +5514,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 +5534,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 +5661,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5674,7 +5673,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5689,7 +5687,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5697,14 +5694,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 +5718,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5804,8 +5798,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5817,7 +5810,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5939,7 +5931,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", @@ -6857,12 +6848,12 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=", "dev": true }, "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": { @@ -7631,7 +7622,7 @@ }, "magic-string": { "version": "0.22.5", - "resolved": "http://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", "dev": true, "requires": { @@ -7901,7 +7892,7 @@ "miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "integrity": "sha1-8IA1HIZbDcViqEYpZtqlNUPHik0=", "dev": true, "requires": { "bn.js": "^4.0.0", @@ -7911,7 +7902,7 @@ "mime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY=", "dev": true }, "mime-db": { @@ -8169,7 +8160,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": { @@ -8335,7 +8326,7 @@ "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", "dev": true, "requires": { "hosted-git-info": "^2.1.4", @@ -8419,7 +8410,6 @@ "version": "0.1.4", "bundled": true, "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -9602,8 +9592,7 @@ "longest": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "loose-envify": { "version": "1.3.1", @@ -11270,7 +11259,7 @@ "p-map": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "integrity": "sha1-5OlPMR6rvIYzoeeZCBZfyiYkG2s=", "dev": true }, "p-try": { @@ -11460,7 +11449,7 @@ "pluralize": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "integrity": "sha1-KYuJ34uTsCIdv0Ia0rGx6iP8Z3c=", "dev": true }, "pngjs": { @@ -12128,9 +12117,9 @@ } }, "scratch-audio": { - "version": "0.1.0-prerelease.20181023202904", - "resolved": "https://registry.npmjs.org/scratch-audio/-/scratch-audio-0.1.0-prerelease.20181023202904.tgz", - "integrity": "sha512-0cf+snpT04RFWFgMsMzbztzNVyh2PkUaT8mjlwNNoIRy5p7yDN3EMC2zGbR71nZMus1tO2EDxqrpan2ix4IWDw==", + "version": "0.1.0-prerelease.20190108181031", + "resolved": "https://registry.npmjs.org/scratch-audio/-/scratch-audio-0.1.0-prerelease.20190108181031.tgz", + "integrity": "sha512-Ygu+pN2u9det8HTIo+2wj8ibqe0QjAA624N9GxC62nrdGH39NxDRJyiwheeuZH/oEjM9RTsCSSOH+C9fXA9ekA==", "dev": true, "requires": { "audio-context": "1.0.1", @@ -12184,9 +12173,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.20190109203013", + "resolved": "https://registry.npmjs.org/scratch-render/-/scratch-render-0.1.0-prerelease.20190109203013.tgz", + "integrity": "sha512-yrkBuF1zLHrXEHQmbQhpATDxot/wqoN/oDW2aIjzV9ylxCy+zNdGI2XJ9tRy10bCcM5bdC879ROX9fCr+n6FwQ==", "dev": true, "requires": { "grapheme-breaker": "0.3.2", @@ -12196,8 +12185,29 @@ "minilog": "3.1.0", "raw-loader": "^0.5.1", "scratch-storage": "^1.0.0", - "scratch-svg-renderer": "0.2.0-prerelease.20181220183040", + "scratch-svg-renderer": "0.2.0-prerelease.20190109201344", "twgl.js": "4.4.0" + }, + "dependencies": { + "base64-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", + "dev": true + }, + "scratch-svg-renderer": { + "version": "0.2.0-prerelease.20190109201344", + "resolved": "https://registry.npmjs.org/scratch-svg-renderer/-/scratch-svg-renderer-0.2.0-prerelease.20190109201344.tgz", + "integrity": "sha512-pRMvQrM5UA2wcqleaXVpFx0Pi6Q3GsRA5elJ0tJksdr6k8HYm5D6sW62VtEtMHjnkQDa+EFyqfHq9IEPnzFjeQ==", + "dev": true, + "requires": { + "base64-js": "1.2.1", + "base64-loader": "1.0.0", + "minilog": "3.1.0", + "scratch-render-fonts": "1.0.0-prerelease.20180906193204", + "transformation-matrix": "1.14.1" + } + } } }, "scratch-render-fonts": { @@ -12450,7 +12460,7 @@ "setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=", "dev": true }, "sha.js": { @@ -12767,7 +12777,7 @@ "source-list-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", + "integrity": "sha1-qqR0A/eyRakvvJfqCPJQ1gh+0IU=", "dev": true }, "source-map": { @@ -13124,7 +13134,7 @@ "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0", @@ -13542,7 +13552,7 @@ "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", "dev": true, "requires": { "os-tmpdir": "~1.0.2" @@ -13863,7 +13873,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 } diff --git a/package.json b/package.json index 2a6a8c248..9dbfcc8e2 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"" }, "dependencies": { + "@vernier/godirect": "1.3.0", "arraybuffer-loader": "^1.0.6", "atob": "2.1.2", "btoa": "1.2.1", diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 86a335989..50f11dcdb 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 Scratch3GdxForBlocks = require('../extensions/scratch3_gdx_for'); const builtinExtensions = { pen: Scratch3PenBlocks, @@ -28,7 +29,8 @@ const builtinExtensions = { videoSensing: Scratch3VideoSensingBlocks, speech2text: Scratch3Speech2TextBlocks, ev3: Scratch3Ev3Blocks, - makeymakey: Scratch3MakeyMakeyBlocks + makeymakey: Scratch3MakeyMakeyBlocks, + gdxfor: Scratch3GdxForBlocks }; /** diff --git a/src/extensions/scratch3_gdx_for/index.js b/src/extensions/scratch3_gdx_for/index.js new file mode 100644 index 000000000..254a4ccc1 --- /dev/null +++ b/src/extensions/scratch3_gdx_for/index.js @@ -0,0 +1,632 @@ +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const log = require('../../util/log'); +const Cast = require('../../util/cast'); +const formatMessage = require('format-message'); +const MathUtil = require('../../util/math-util'); +const BLE = require('../../io/ble'); +const godirect = require('@vernier/godirect/dist/godirect.min.umd.js'); +const ScratchLinkDeviceAdapter = require('./scratch-link-device-adapter'); + +/** + * Icon png 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 blockIconURI = 'data:image/png;base64,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'; + +/** + * Enum for Vernier godirect protocol. + * @readonly + * @enum {string} + */ +const BLEUUID = { + service: 'd91714ef-28b9-4f91-ba16-f0d9a604f112', + commandChar: 'f4bf14a6-c7d5-4b6d-8aa8-df1a7c83adcb', + responseChar: 'b41e6675-a329-40e0-aa01-44d2f444babe' +}; + +/** + * Manage communication with a GDX-FOR peripheral over a Scratch Link client socket. + */ +class GdxFor { + + /** + * Construct a GDX-FOR communication object. + * @param {Runtime} runtime - the Scratch 3.0 runtime + * @param {string} extensionId - the id of the extension + */ + constructor (runtime, extensionId) { + + /** + * The Scratch 3.0 runtime used to trigger the green flag button. + * @type {Runtime} + * @private + */ + this._runtime = runtime; + + /** + * The BluetoothLowEnergy connection socket for reading/writing peripheral data. + * @type {BLE} + * @private + */ + this._scratchLinkSocket = null; + + /** + * An @vernier/godirect Device + * @type {Device} + * @private + */ + this._device = null; + + this._runtime.registerPeripheralExtension(extensionId, this); + + /** + * The id of the extension this peripheral belongs to. + */ + this._extensionId = extensionId; + + this.disconnect = this.disconnect.bind(this); + this._onConnect = this._onConnect.bind(this); + } + + + /** + * Called by the runtime when user wants to scan for a peripheral. + */ + scan () { + if (this._device) { + this._device.close(); + } + + this._scratchLinkSocket = new BLE(this._runtime, this._extensionId, { + filters: [ + {namePrefix: 'GDX-FOR'} + ], + optionalServices: [ + BLEUUID.service + ] + }, this._onConnect); + } + + /** + * Called by the runtime when user wants to connect to a certain peripheral. + * @param {number} id - the id of the peripheral to connect to. + */ + connect (id) { + if (this._scratchLinkSocket) { + this._scratchLinkSocket.connectPeripheral(id); + } + } + + /** + * Called by the runtime when a use exits the connection popup. + * Disconnect from the GDX FOR. + */ + disconnect () { + if (this._device) { + this._device.close(); + } + } + + /** + * Return true if connected to the goforce device. + * @return {boolean} - whether the goforce is connected. + */ + isConnected () { + let connected = false; + if (this._scratchLinkSocket) { + connected = this._scratchLinkSocket.isConnected(); + } + return connected; + } + + /** + * Starts reading data from peripheral after BLE has connected to it. + * @private + */ + _onConnect () { + const adapter = new ScratchLinkDeviceAdapter(this._scratchLinkSocket, BLEUUID); + godirect.createDevice(adapter, {open: true, startMeasurements: false}).then(device => { + this._device = device; + this._startMeasurements(); + }); + } + + /** + * Enable and begin reading measurements + * @private + */ + _startMeasurements () { + this._device.sensors.forEach(sensor => { + sensor.setEnabled(true); + + // For now, clear the save sensor values. The unlimited saving + // will be fixed in a future @vernier/godirect release. + sensor.on('value-changed', changedSensor => { + if (changedSensor.values.length > 1000) { + changedSensor.clear(); + } + }); + }); + this._device.start(10); // Set the period to 10 milliseconds + } + + + getForce () { + if (this.isConnected()) { + let force = this._device.getSensor(1).value; + // Normalize the force, which can be measured between -50 and 50 N, + // to be a value between -100 and 100. + force = MathUtil.clamp(force * 2, -100, 100); + return force; + } + return 0; + } + + getTiltX () { + if (this.isConnected()) { + let x = this.getAccelerationX(); + let y = this.getAccelerationY(); + let z = this.getAccelerationZ(); + + let xSign = 1; + let ySign = 1; + let zSign = 1; + + if (x < 0.0) { + x *= -1.0; xSign = -1; + } + if (y < 0.0) { + y *= -1.0; ySign = -1; + } + if (z < 0.0) { + z *= -1.0; zSign = -1; + } + + // Compute the yz unit vector + const z2 = z * z; + const y2 = y * y; + let value = z2 + y2; + value = Math.sqrt(value); + + // For sufficiently small zy vector values we are essentially at 90 degrees. + // The following snaps to 90 and avoids divide-by-zero errors. + // The snap factor was derived through observation -- just enough to + // still allow single degree steps up to 90 (..., 87, 88, 89, 90). + if (value < 0.35) { + value = 90; + } else { + // Compute the x-axis angle + value = x / value; + value = Math.atan(value); + value *= 57.2957795; // convert from rad to deg + } + // Manage the sign of the result + let yzSign = ySign; + if (z > y) yzSign = zSign; + if (yzSign === -1) value = 180.0 - value; + value *= xSign; + // Round the result to the nearest degree + value += 0.5; + return value; + } + return 0; + } + + getTiltY () { + if (this.isConnected()) { + let x = this.getAccelerationX(); + let y = this.getAccelerationY(); + let z = this.getAccelerationZ(); + + let xSign = 1; + let ySign = 1; + let zSign = 1; + + if (x < 0.0) { + x *= -1.0; xSign = -1; + } + if (y < 0.0) { + y *= -1.0; ySign = -1; + } + if (z < 0.0) { + z *= -1.0; zSign = -1; + } + + // Compute the yz unit vector + const z2 = z * z; + const x2 = x * x; + let value = z2 + x2; + value = Math.sqrt(value); + + // For sufficiently small zy vector values we are essentially at 90 degrees. + // The following snaps to 90 and avoids divide-by-zero errors. + // The snap factor was derived through observation -- just enough to + // still allow single degree steps up to 90 (..., 87, 88, 89, 90). + if (value < 0.35) { + value = 90; + } else { + // Compute the x-axis angle + value = y / value; + value = Math.atan(value); + value *= 57.2957795; // convert from rad to deg + } + // Manage the sign of the result + let xzSign = xSign; + if (z > x) xzSign = zSign; + if (xzSign === -1) value = 180.0 - value; + value *= ySign; + // Round the result to the nearest degree + value += 0.5; + return value; + } + return 0; + } + + + getAccelerationX () { + if (this.isConnected()) { + return this._device.getSensor(2).value; + } + return 0; + } + + getAccelerationY () { + if (this.isConnected()) { + return this._device.getSensor(3).value; + } + return 0; + } + + getAccelerationZ () { + if (this.isConnected()) { + return this._device.getSensor(4).value; + } + return 0; + } + + getSpinSpeedX () { + if (this.isConnected()) { + return this._device.getSensor(5).value * (180 / Math.PI); + } + return 0; + } + + getSpinSpeedY () { + if (this.isConnected()) { + return this._device.getSensor(6).value * (180 / Math.PI); + } + return 0; + } + + getSpinSpeedZ () { + if (this.isConnected()) { + return this._device.getSensor(7).value * (180 / Math.PI); + } + return 0; + } +} + +/** + * Enum for comparison operations. + * @readonly + * @enum {string} + */ +const ComparisonOptions = { + LESS_THAN: 'less_than', + GREATER_THAN: 'greater_than' +}; + +/** + * Scratch 3.0 blocks to interact with a GDX-FOR peripheral. + */ +class Scratch3GdxForBlocks { + + /** + * @return {string} - the name of this extension. + */ + static get EXTENSION_NAME () { + return 'GDX-FOR'; + } + + /** + * @return {string} - the ID of this extension. + */ + static get EXTENSION_ID () { + return 'gdxfor'; + } + + get DIRECTIONS_MENU () { + return [ + { + text: 'x', + value: 'x' + }, + { + text: 'y', + value: 'y' + }, + { + text: 'z', + value: 'z' + } + ]; + } + + get TILT_MENU () { + return [ + { + text: 'x', + value: 'x' + }, + { + text: 'y', + value: 'y' + } + ]; + } + + get COMPARE_MENU () { + return [ + { + text: '<', + value: ComparisonOptions.LESS_THAN + }, + { + text: '>', + value: ComparisonOptions.GREATER_THAN + } + ]; + } + + + /** + * Construct a set of GDX-FOR blocks. + * @param {Runtime} runtime - the Scratch 3.0 runtime. + */ + constructor (runtime) { + /** + * The Scratch 3.0 runtime. + * @type {Runtime} + */ + this.runtime = runtime; + + // Create a new GdxFor peripheral instance + this._peripheral = new GdxFor(this.runtime, Scratch3GdxForBlocks.EXTENSION_ID); + } + + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: Scratch3GdxForBlocks.EXTENSION_ID, + name: Scratch3GdxForBlocks.EXTENSION_NAME, + blockIconURI: blockIconURI, + showStatusButton: true, + blocks: [ + { + opcode: 'whenAccelerationCompare', + text: formatMessage({ + id: 'gdxfor.whenAccelerationCompare', + default: 'when acceleration [COMPARE] [VALUE]', + description: 'when the meters/second^2 value measured by the ' + + 'acceleration sensor is compared to some value' + }), + blockType: BlockType.HAT, + arguments: { + COMPARE: { + type: ArgumentType.STRING, + menu: 'compareOptions', + defaultValue: ComparisonOptions.GREATER_THAN + }, + VALUE: { + type: ArgumentType.NUMBER, + defaultValue: 5 + } + } + }, + { + opcode: 'whenSpinSpeedCompare', + text: formatMessage({ + id: 'gdxfor.whenSpinSpeedCompare', + default: 'when spin speed [COMPARE] [VALUE]', + description: 'when the degrees/second value measured by the ' + + 'gyroscope sensor is compared to some value' + }), + blockType: BlockType.HAT, + arguments: { + COMPARE: { + type: ArgumentType.STRING, + menu: 'compareOptions', + defaultValue: ComparisonOptions.GREATER_THAN + }, + VALUE: { + type: ArgumentType.NUMBER, + defaultValue: 5 + } + } + }, + { + opcode: 'whenForceCompare', + text: formatMessage({ + id: 'gdxfor.whenForceCompare', + default: 'when force [COMPARE] [VALUE]', + description: 'when the value measured by the force sensor is compared to some value' + }), + blockType: BlockType.HAT, + arguments: { + COMPARE: { + type: ArgumentType.STRING, + menu: 'compareOptions', + defaultValue: ComparisonOptions.GREATER_THAN + }, + VALUE: { + type: ArgumentType.NUMBER, + defaultValue: 5 + } + } + }, + { + opcode: 'getAcceleration', + text: formatMessage({ + id: 'gdxfor.getAcceleration', + default: 'acceleration [DIRECTION]', + description: 'gets acceleration' + }), + blockType: BlockType.REPORTER, + arguments: { + DIRECTION: { + type: ArgumentType.STRING, + menu: 'directionOptions', + defaultValue: 'x' + } + } + }, + { + opcode: 'getSpinSpeed', + text: formatMessage({ + id: 'gdxfor.getSpinSpeed', + default: 'spin speed [DIRECTION]', + description: 'gets spin speed' + }), + blockType: BlockType.REPORTER, + arguments: { + DIRECTION: { + type: ArgumentType.STRING, + menu: 'directionOptions', + defaultValue: 'x' + } + } + }, + { + opcode: 'getTilt', + text: formatMessage({ + id: 'gdxfor.getTilt', + default: 'tilt [TILT]', + description: 'gets tilt' + }), + blockType: BlockType.REPORTER, + arguments: { + TILT: { + type: ArgumentType.STRING, + menu: 'tiltOptions', + defaultValue: 'x' + } + } + }, + { + opcode: 'getForce', + text: formatMessage({ + id: 'gdxfor.getForce', + default: 'force', + description: 'gets force' + }), + blockType: BlockType.REPORTER + } + ], + menus: { + directionOptions: this.DIRECTIONS_MENU, + compareOptions: this.COMPARE_MENU, + tiltOptions: this.TILT_MENU + } + }; + } + + /** + * @param {number} x - x axis vector + * @param {number} y - y axis vector + * @param {number} z - z axis vector + * @return {number} - the magnitude of a three dimension vector. + */ + magnitude (x, y, z) { + return Math.sqrt((x * x) + (y * y) + (z * z)); + } + + + whenAccelerationCompare (args) { + const currentVal = this.magnitude( + this._peripheral.getAccelerationX(), + this._peripheral.getAccelerationY(), + this._peripheral.getAccelerationZ() + ); + + switch (args.COMPARE) { + case ComparisonOptions.LESS_THAN: + return currentVal < Cast.toNumber(args.VALUE); + case ComparisonOptions.GREATER_THAN: + return currentVal > Cast.toNumber(args.VALUE); + default: + log.warn(`Unknown comparison operator in whenAccelerationCompare: ${args.COMPARE}`); + return false; + } + } + whenSpinSpeedCompare (args) { + const currentVal = this.magnitude( + this._peripheral.getSpinSpeedX(), + this._peripheral.getSpinSpeedY(), + this._peripheral.getSpinSpeedZ() + ); + + switch (args.COMPARE) { + case ComparisonOptions.LESS_THAN: + return currentVal < Cast.toNumber(args.VALUE); + case ComparisonOptions.GREATER_THAN: + return currentVal > Cast.toNumber(args.VALUE); + default: + log.warn(`Unknown comparison operator in whenSpinSpeedCompare: ${args.COMPARE}`); + return false; + } + } + whenForceCompare (args) { + switch (args.COMPARE) { + case ComparisonOptions.LESS_THAN: + return this._peripheral.getForce() < Cast.toNumber(args.VALUE); + case ComparisonOptions.GREATER_THAN: + return this._peripheral.getForce() > Cast.toNumber(args.VALUE); + default: + log.warn(`Unknown comparison operator in whenForceCompare: ${args.COMPARE}`); + return false; + } + } + getAcceleration (args) { + switch (args.DIRECTION) { + case 'x': + return this._peripheral.getAccelerationX(); + case 'y': + return this._peripheral.getAccelerationY(); + case 'z': + return this._peripheral.getAccelerationZ(); + default: + log.warn(`Unknown direction in getAcceleration: ${args.DIRECTION}`); + } + } + getSpinSpeed (args) { + switch (args.DIRECTION) { + case 'x': + return this._peripheral.getSpinSpeedX(); + case 'y': + return this._peripheral.getSpinSpeedY(); + case 'z': + return this._peripheral.getSpinSpeedZ(); + default: + log.warn(`Unknown direction in getSpinSpeed: ${args.DIRECTION}`); + } + } + getTilt (args) { + switch (args.TILT) { + case 'x': + return this._peripheral.getTiltX(); + case 'y': + return this._peripheral.getTiltY(); + default: + log.warn(`Unknown direction in getTilt: ${args.TILT}`); + } + } + getForce () { + return this._peripheral.getForce(); + } +} + +module.exports = Scratch3GdxForBlocks; diff --git a/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js b/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js new file mode 100644 index 000000000..a9a61d4ae --- /dev/null +++ b/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js @@ -0,0 +1,48 @@ +const Base64Util = require('../../util/base64-util'); + +/** + * Adapter class + */ +class ScratchLinkDeviceAdapter { + constructor (scratchLinkSocket, {service, commandChar, responseChar}) { + this.scratchLinkSocket = scratchLinkSocket; + + this._service = service; + this._commandChar = commandChar; + this._responseChar = responseChar; + this._onResponse = this._onResponse.bind(this); + this._deviceOnResponse = null; + } + + get godirectAdapter () { + return true; + } + + writeCommand (commandBuffer) { + const data = Base64Util.uint8ArrayToBase64(commandBuffer); + + return this.scratchLinkSocket + .write(this._service, this._commandChar, data, 'base64', true); + } + + setup ({onResponse}) { + this._deviceOnResponse = onResponse; + return this.scratchLinkSocket + .startNotifications(this._service, this._responseChar, this._onResponse); + + // TODO: + // How do we find out from scratch link if communication closes? + } + + _onResponse (base64) { + const array = Base64Util.base64ToUint8Array(base64); + const response = new DataView(array.buffer); + return this._deviceOnResponse(response); + } + + close () { + return this.scratchLinkSocket.disconnect(); + } +} + +module.exports = ScratchLinkDeviceAdapter;