├── .brilliant └── device-files.json ├── .gitignore ├── media ├── brilliant.png ├── icons │ ├── left.png │ ├── line.png │ ├── rect.png │ ├── text.png │ ├── top.png │ ├── bottom.png │ ├── center.png │ ├── eraser.png │ ├── middle.png │ ├── paint.png │ ├── polygon.png │ ├── right.png │ ├── polyline.png │ ├── paintbrush.png │ ├── thickness_icon.png │ ├── rect.svg │ ├── polyline.svg │ ├── line.svg │ ├── polygon.svg │ ├── center.svg │ ├── left.svg │ ├── right.svg │ ├── text.svg │ ├── thickness_icon.svg │ ├── top.svg │ ├── bottom.svg │ └── middle.svg ├── vscode-ext-repl.gif ├── vscode-ext-custom-fpga.gif ├── vscode-ext-upload-file.gif ├── vscode-ext-api-drag-drop.gif ├── vscode-ext-drag-drop-GUI.png ├── vscode-ext-custom-projects.gif ├── JetBrains_Mono │ ├── JetBrainsMono-VariableFont_wght.ttf │ ├── README.txt │ └── OFL.txt ├── Monocle_Icon.svg └── main.css ├── .vscodeignore ├── snippets ├── variables.json ├── fpga.json ├── loops.json ├── led.json ├── update.json ├── uhashlib.json ├── microphone.json ├── camera.json ├── ustruct.json ├── ujson.json ├── bluetooth.json ├── touch.json ├── logic.json ├── gc.json ├── ure.json ├── urandom.json ├── time.json ├── device.json ├── uselect.json ├── math.json ├── uasyncio.json └── display.json ├── src ├── ble │ ├── index.ts │ └── helpers.ts ├── test │ ├── suite │ │ ├── extension.test.ts │ │ └── index.ts │ └── runTest.ts ├── snippets │ ├── index.ts │ └── provider.ts ├── projects.ts ├── nordicdfu.ts ├── update.ts ├── bluetooth.ts ├── fileSystemProvider.ts └── UIEditorPanel.ts ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── .eslintrc.json ├── tsconfig.json ├── LICENSE.md ├── .github └── workflows │ ├── universal.yml │ ├── CI.yml │ └── main.yml ├── CHANGELOG.md ├── vsc-extension-quickstart.md ├── README.md └── package.json /.brilliant/device-files.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /media/brilliant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/brilliant.png -------------------------------------------------------------------------------- /media/icons/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/left.png -------------------------------------------------------------------------------- /media/icons/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/line.png -------------------------------------------------------------------------------- /media/icons/rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/rect.png -------------------------------------------------------------------------------- /media/icons/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/text.png -------------------------------------------------------------------------------- /media/icons/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/top.png -------------------------------------------------------------------------------- /media/icons/bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/bottom.png -------------------------------------------------------------------------------- /media/icons/center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/center.png -------------------------------------------------------------------------------- /media/icons/eraser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/eraser.png -------------------------------------------------------------------------------- /media/icons/middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/middle.png -------------------------------------------------------------------------------- /media/icons/paint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/paint.png -------------------------------------------------------------------------------- /media/icons/polygon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/polygon.png -------------------------------------------------------------------------------- /media/icons/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/right.png -------------------------------------------------------------------------------- /media/icons/polyline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/polyline.png -------------------------------------------------------------------------------- /media/vscode-ext-repl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/vscode-ext-repl.gif -------------------------------------------------------------------------------- /media/icons/paintbrush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/paintbrush.png -------------------------------------------------------------------------------- /media/icons/thickness_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/icons/thickness_icon.png -------------------------------------------------------------------------------- /media/vscode-ext-custom-fpga.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/vscode-ext-custom-fpga.gif -------------------------------------------------------------------------------- /media/vscode-ext-upload-file.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/vscode-ext-upload-file.gif -------------------------------------------------------------------------------- /media/vscode-ext-api-drag-drop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/vscode-ext-api-drag-drop.gif -------------------------------------------------------------------------------- /media/vscode-ext-drag-drop-GUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/vscode-ext-drag-drop-GUI.png -------------------------------------------------------------------------------- /media/vscode-ext-custom-projects.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/vscode-ext-custom-projects.gif -------------------------------------------------------------------------------- /media/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brilliantlabsAR/ar-studio-for-vscode/main/media/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /media/icons/rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /media/icons/polyline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /snippets/variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "if": { 3 | "prefix": "if", 4 | "body": "\nif ${1:condition}:\n\t${2:pass}$0", 5 | "description" : "Code snippet for the if statement." 6 | } 7 | } -------------------------------------------------------------------------------- /media/icons/line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /media/icons/polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/ble/index.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | helpers: require('./helpers'), 3 | get webbluetooth() { 4 | var bluetooth = require('./webbluetooth'); 5 | require('./adapter.noble')(bluetooth); 6 | return bluetooth; 7 | } 8 | }; -------------------------------------------------------------------------------- /media/icons/center.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /media/icons/left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /media/icons/right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ], 7 | "permissions": [ 8 | "bluetooth" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /media/icons/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /media/icons/thickness_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /snippets/fpga.json: -------------------------------------------------------------------------------- 1 | { 2 | "read(addr, n)": { 3 | "prefix": "read", 4 | "body": "fpga.read(${1:addr}, ${2:n})", 5 | "description" : "Reads n number of bytes from the 16-bit address addr, and returns a bytes object." 6 | }, 7 | "write(addr,bytes[])": { 8 | "prefix": "reset", 9 | "body": "fpga.write(${1:addr},${2:bytes})", 10 | "description" : "Writes all bytes from a given bytes object bytes[] to the 16-bit address addr" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /media/icons/top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /media/icons/bottom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /snippets/loops.json: -------------------------------------------------------------------------------- 1 | { 2 | "for": { 3 | "prefix": "for", 4 | "body": "\nfor ${1:item} in ${2:items}:\n\t${3:print($1)}", 5 | "description" : "Code snippet to create a for loop structure." 6 | }, 7 | "while": { 8 | "prefix": "while", 9 | "body": "\nwhile ${1:condition}:\n\t${2:print('inside loop')}", 10 | "description" : "Code snippet to create a while loop structure." 11 | }, 12 | "for range": { 13 | "prefix": "for", 14 | "body": "\nfor ${1:item} in range(${2:10}):\n\t${3:print($1)}", 15 | "description" : "Code snippet to create a for loop with a range." 16 | } 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | 7 | "lib": [ 8 | "ES2020", 9 | "ES2021", 10 | "DOM" 11 | ], 12 | "sourceMap": true, 13 | "rootDir": "src", 14 | "strict": true /* enable all strict type-checking options */ 15 | /* Additional Checks */ 16 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 17 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 18 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests', err); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /snippets/led.json: -------------------------------------------------------------------------------- 1 | { 2 | "on(color)": { 3 | "prefix": "on", 4 | "body": "led.on(led.${1|GREEN,RED|})", 5 | "description" : "Illuminates an led. color can be either led.RED or led.GREEN." 6 | }, 7 | "off(color)": { 8 | "prefix": "off", 9 | "body": "device.off(led.${1|GREEN,RED|}))", 10 | "description" : "Turns off an led. color can be either led.RED or led.GREEN. " 11 | }, 12 | "RED": { 13 | "prefix": "red", 14 | "body": "led.RED", 15 | "description" :"String constant which represents the red led." 16 | }, 17 | "GREEN": { 18 | "prefix": "green", 19 | "body": "led.GREEN", 20 | "description" :"String constant which represents the green led." 21 | } 22 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2023 Brilliant Labs Ltd. 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /snippets/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "micropython()": { 3 | "prefix": "micropython", 4 | "body": "update.micropython()", 5 | "description" : "Puts the device into over-the-air device firmware update mode. The device will stay in DFU mode for 5 minutes or until an update is finished being performed. The device will then restart with an updated MicroPython firmware." 6 | }, 7 | "Fpga": { 8 | "prefix": "Fpga", 9 | "body": "update.Fpga", 10 | "description" : "Fpga contains three low level functions which are used to update the FPGA. Fpga.erase() erases the existing application, Fpga.write(bytes[]) sequentially writes in the new application, and Fpga.read(address, length) can be used to read back and verify the application. For details on how the FPGA application is updated," 11 | } 12 | } -------------------------------------------------------------------------------- /snippets/uhashlib.json: -------------------------------------------------------------------------------- 1 | { 2 | "sha256([data])": { 3 | "prefix": "sha256", 4 | "body": "hashlib.sha256(${1:[data]})", 5 | "description" : "Create an SHA256 hasher object and optionally feed data into it." 6 | }, 7 | "sha1([data])": { 8 | "prefix": "sha1", 9 | "body": "hashlib.sha1(${1:[data]})", 10 | "description" : "Create an SHA1 hasher object and optionally feed data into it." 11 | }, 12 | 13 | "md5([data])": { 14 | "prefix": "md5", 15 | "body": "hashlib.md5(${1:[data]})", 16 | "description" : "Create an MD5 hasher object and optionally feed data into it." 17 | }, 18 | "update(data)": { 19 | "prefix": "update", 20 | "body": "hash.update(data)", 21 | "description" : "Feed more binary data into hash." 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /snippets/microphone.json: -------------------------------------------------------------------------------- 1 | { 2 | "record(seconds,sample_rate,bit_depth)": { 3 | "prefix": "record", 4 | "body": "microphone.record(seconds=${1:5.0},sample_rate=${2|16000,8000|},bit_depth=${3|16,8|})", 5 | "description": "Issues an instruction to the FPGA to start recording audio for a number of seconds. Always clears previously recorded audio. While recording is ongoing, data can be read out using read(). sample_rate can be either 16000 or 8000. bit_depth can be either 16 or 8." 6 | }, 7 | "read(samples)": { 8 | "prefix": "read", 9 | "body": "microphone.read(${1:127})", 10 | "description": "Reads out a number of recorded audio samples from the FPGA as a list. Samples are signed 16bit values, and up to 127 samples can be read at a time. Once all samples have been read, read() will return None." 11 | } 12 | } -------------------------------------------------------------------------------- /snippets/camera.json: -------------------------------------------------------------------------------- 1 | { 2 | "capture()": { 3 | "prefix": "capture", 4 | "body": "camera.capture()", 5 | "description": "Captures an image and returns it to a device over Bluetooth." 6 | }, 7 | "output(x,y,format)": { 8 | "prefix": "output", 9 | "body": "camera.output(${1:640}, ${2:400}, ${3|camera.YUV,camera.RGB,camera.JPEG|})", 10 | "description": "Set the output resolution and format. x and y represent the output resolution in pixels. format can be either RGB, 'YUV' or 'JPEG'. The default output settings are camera.output(640, 400, 'YUV')." 11 | }, 12 | "zoom(multiplier)": { 13 | "prefix": "zoom", 14 | "body": "camera.zoom(${1|1,2,3,4,5,6,7,8|})", 15 | "description": "Sets the zoom level of the camera. Multiplier can be any floating point number between 1 and 8." 16 | } 17 | } -------------------------------------------------------------------------------- /media/icons/middle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /media/Monocle_Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /snippets/ustruct.json: -------------------------------------------------------------------------------- 1 | { 2 | "calcsize(fmt)": { 3 | "prefix": "calcsize", 4 | "body": "struct.calcsize(${1:fmt})", 5 | "description": "Return the number of bytes needed to store the given fmt." 6 | }, 7 | "pack(fmt, v1, v2,v3,...)": { 8 | "prefix": "pack", 9 | "body": "struct.pack(${1:fmt},${2:v1},${3:v2},${2:v3},...)", 10 | "description": "Pack the values v1, v2, … according to the format string fmt. The return value is a bytes object encoding the values." 11 | }, 12 | "pack_into(fmt, buffer, offset, v1, v2, ...)": { 13 | "prefix": "pack_into", 14 | "body": "struct.pack_into(${1:fmt},${2:buffer},${3:offset},${2:v1},${2:v2}...)", 15 | "description": "Pack the values v1, v2, … according to the format string fmt into a buffer starting at offset. offset may be negative to count from the end of buffer." 16 | }, 17 | "unpack_from(fmt, data, offset)": { 18 | "prefix": "unpack_from", 19 | "body": "struct.unpack_from(${1:fmt},${2:data},${3:offset})", 20 | "description": "Unpack from the data according to the format string fmt. The return value is a tuple of the unpacked values." 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/snippets/index.ts: -------------------------------------------------------------------------------- 1 | export const snippets:any= { 2 | bluetooth:require('../../snippets/bluetooth.json'), 3 | camera: require('../../snippets/camera.json'), 4 | device:require('../../snippets/device.json'), 5 | microphone:require('../../snippets/microphone.json'), 6 | display: require('../../snippets/display.json'), 7 | fpga: require('../../snippets/fpga.json'), 8 | gc:require('../../snippets/gc.json'), 9 | led: require('../../snippets/led.json'), 10 | logic: require('../../snippets/logic.json'), 11 | loops: require('../../snippets/loops.json'), 12 | math: require('../../snippets/math.json'), 13 | time: require('../../snippets/time.json'), 14 | touch: require('../../snippets/touch.json'), 15 | // uasyncio: require('../../snippets/uasyncio.json'), 16 | uhashlib: require('../../snippets/uhashlib.json'), 17 | ujson: require('../../snippets/ujson.json'), 18 | // update:require('../../snippets/update.json'), 19 | urandom: require('../../snippets/urandom.json'), 20 | ure:require('../../snippets/ure.json'), 21 | uselect:require('../../snippets/uselect.json'), 22 | ustruct:require('../../snippets/ustruct.json'), 23 | // varibles:require('../../snippets/variables.json'), 24 | 25 | 26 | }; -------------------------------------------------------------------------------- /snippets/ujson.json: -------------------------------------------------------------------------------- 1 | { 2 | "dump(obj, stream, separators)": { 3 | "prefix": "dump", 4 | "body": "json.dump(${1:obj}, ${2:stream}, ${2:separators})", 5 | "description": "Serialise obj to a JSON string, writing it to the given stream. If specified, separators should be an (item_separator, key_separator) tuple. The default is (', ', ': '). To get the most compact JSON representation, you should specify (',', ':') to eliminate whitespace." 6 | }, 7 | "dumps(obj, separators)": { 8 | "prefix": "dumps", 9 | "body": "json.dumps(${1:obj},${2:separators})", 10 | "description": "Return obj represented as a JSON string.The arguments have the same meaning as in dump." 11 | }, 12 | "load(stream)": { 13 | "prefix": "load", 14 | "body": "json.load(${1:stream})", 15 | "description": "Parse the given stream, interpreting it as a JSON string and deserialising the data to a Python object. The resulting object is returned. Parsing continues until end-of-file is encountered. A ValueError is raised if the data in stream is not correctly formed." 16 | }, 17 | "loads(str)": { 18 | "prefix": "loads", 19 | "body": "json.loads(${1:str})", 20 | "description": "Parse the JSON str and return an object. Raises ValueError if the string is not correctly formed." 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /snippets/bluetooth.json: -------------------------------------------------------------------------------- 1 | { 2 | "send(bytes[])": { 3 | "prefix": "send", 4 | "body": "bluetooth.send(${1:bytes})", 5 | "description" : "Sends data from a bytes object bytes[] over Bluetooth using the raw data service. The length of bytes[] must be equal to or less than the value returned by the bluetooth.max_length() function." 6 | }, 7 | "receive_callback(callback)": { 8 | "prefix": "receive_callback", 9 | "body": "bluetooth.receive_callback(${1:callback})", 10 | "description" : "Assigns a callback to receive data over the raw data service. callback must be a predefined function taking one argument. The value of the argument will be a bytes object bytes[] containing the received data. To unbind the callback, issue this function with callback set as None. If callback isn’t given when issuing this function, the currently set callback will be returned if it is set." 11 | }, 12 | "connected()": { 13 | "prefix": "connected", 14 | "body": "bluetooth.connected()", 15 | "description" : "Returns True if the raw data service is connected, otherwise returns False." 16 | }, 17 | "max_length()": { 18 | "prefix": "max_length", 19 | "body": "bluetooth.max_length()", 20 | "description" : "Returns the maximum data size the Bluetooth host allows for single transfers." 21 | 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /.github/workflows/universal.yml: -------------------------------------------------------------------------------- 1 | name: CI Universal 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | include: 12 | - os: macos-latest 13 | platform: darwin 14 | arch: arm64 15 | npm_config_arch: arm64 16 | branch: mac_win 17 | 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v2 21 | with: 22 | ref: ${{ matrix.branch }} # Use the dynamically set branch 23 | - uses: actions/setup-node@v2 24 | with: 25 | node-version: 16.x 26 | - run: | 27 | python3 -m pip install setuptools --break-system-packages 28 | npm install 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | npm_config_arch: ${{ matrix.npm_config_arch }} 32 | - shell: pwsh 33 | run: echo "target=${{ matrix.platform }}-${{ matrix.arch }}" >> $env:GITHUB_ENV 34 | - run: npx vsce package 35 | - uses: actions/upload-artifact@v2 36 | with: 37 | name: ${{ env.target }} 38 | path: "*.vsix" 39 | 40 | publish: 41 | runs-on: ubuntu-latest 42 | needs: build 43 | if: success() 44 | steps: 45 | - uses: actions/download-artifact@v2 46 | - run: npx vsce publish --packagePath $(find . -iname *.vsix) 47 | env: 48 | VSCE_PAT: ${{ secrets.VSCE_TOKEN }} 49 | -------------------------------------------------------------------------------- /snippets/touch.json: -------------------------------------------------------------------------------- 1 | { 2 | "callback(pad,callback)": { 3 | "prefix": "callback", 4 | "body": "touch.callback(touch.${1|A,B,BOTH|}, ${2:callback})", 5 | "description" : "Attaches a callback handler to one or both of the touch pads. If pad is given as touch.BOTH, a single callback will be used to capture both touch events. Otherwise touch.A or touch.B can be used to assign separate callback functions if desired. callback should be a predefined function taking one argument. This argument will equal the pad which triggered the callback. To unassign a callback, issue touch.callback() with None in the callback argument. To view the currently assigned callback, issue touch.callback() without the callback argument." 6 | }, 7 | "state(pad)": { 8 | "prefix": "reset", 9 | "body": "touch.state(touch.${1|A,B,BOTH|}, ${2:callback})", 10 | "description" : "Returns the current touch state of the touch pads. If pad is not specified, either 'A', 'B', 'BOTH' or None will be returned depending on which pads are currently touched. If pad is specified, then True or False will be returned for the touch state of that pad." 11 | }, 12 | "A": { 13 | "prefix": "A", 14 | "body": "touch.A", 15 | "description" :"String constant which represents Pad A." 16 | }, 17 | "B": { 18 | "prefix": "B", 19 | "body": "touch.B", 20 | "description" :"String constant which represents Pad B." 21 | }, 22 | "BOTH": { 23 | "prefix": "BOTH", 24 | "body": "touch.BOTH", 25 | "description" :"String constant which represents both pads." 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI MAC 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | include: 12 | - os: macos-latest 13 | platform: darwin 14 | arch: x64 15 | npm_config_arch: x64 16 | branch: mac_win 17 | - os: macos-latest 18 | platform: darwin 19 | arch: arm64 20 | npm_config_arch: arm64 21 | branch: mac_win 22 | 23 | runs-on: ${{ matrix.os }} 24 | steps: 25 | - uses: actions/checkout@v2 26 | with: 27 | ref: ${{ matrix.branch }} # Use the dynamically set branch 28 | - uses: actions/setup-node@v2 29 | with: 30 | node-version: 16.x 31 | - run: | 32 | python3 -m pip install setuptools --break-system-packages 33 | npm install 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | npm_config_arch: ${{ matrix.npm_config_arch }} 37 | - shell: pwsh 38 | run: echo "target=${{ matrix.platform }}-${{ matrix.arch }}" >> $env:GITHUB_ENV 39 | - run: npx vsce package --target ${{ env.target }} 40 | - uses: actions/upload-artifact@v2 41 | with: 42 | name: ${{ env.target }} 43 | path: "*.vsix" 44 | 45 | publish: 46 | runs-on: ubuntu-latest 47 | needs: build 48 | if: success() 49 | steps: 50 | - uses: actions/download-artifact@v2 51 | - run: npx vsce publish --packagePath $(find . -iname *.vsix) 52 | env: 53 | VSCE_PAT: ${{ secrets.VSCE_TOKEN }} 54 | -------------------------------------------------------------------------------- /snippets/logic.json: -------------------------------------------------------------------------------- 1 | { 2 | "if": { 3 | "prefix": "if", 4 | "body": "\nif ${1:condition}:\n\t${2:pass}", 5 | "description" : "Code snippet for the if statement." 6 | }, 7 | "elif": { 8 | "prefix": "elif", 9 | "body": "\nif ${1:condition1}:\n\t${2:pass}\nelif ${3:condition2}:\n\t${4:pass}", 10 | "description" : "Code snippet for the elif statement." 11 | }, 12 | "if else": { 13 | "prefix": "ifelse", 14 | "body": "\nif ${1:condition}:\n\t${2:pass}\nelse:\n\t${3:pass}", 15 | "description" : "Code snippet for the if else statement." 16 | }, 17 | "and": { 18 | "prefix": "and", 19 | "body": "${1:value1} and ${2:value2}", 20 | "description" : "Code snippet for the and logic." 21 | }, 22 | "or": { 23 | "prefix": "or", 24 | "body": "${1:value1} or ${2:value2}", 25 | "description" : "Code snippet for the or logic." 26 | }, 27 | "not": { 28 | "prefix": "not", 29 | "body": "\nnot ${1:value}", 30 | "description" : "Code snippet for the equal not logic." 31 | }, 32 | "in": { 33 | "prefix": "in", 34 | "body": "in ${1:list}", 35 | "description" : "Code snippet for the equal in logic." 36 | }, 37 | "equal": { 38 | "prefix": "==", 39 | "body": "${1:value1} == ${2:value2}", 40 | "description" : "Code snippet for the equal (==) operator." 41 | }, 42 | "not equal": { 43 | "prefix": "!=", 44 | "body": "${1:value1} != ${2:value2}", 45 | "description" : "Code snippet for the not equal (!=) operator." 46 | } 47 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI LINUX 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | include: 12 | - os: ubuntu-latest 13 | platform: linux 14 | arch: x64 15 | npm_config_arch: x64 16 | branch: linux 17 | - os: ubuntu-latest 18 | platform: linux 19 | arch: arm64 20 | npm_config_arch: arm64 21 | branch: linux 22 | - os: ubuntu-latest 23 | platform: linux 24 | arch: armhf 25 | npm_config_arch: arm 26 | branch: linux 27 | - os: ubuntu-latest 28 | platform: alpine 29 | arch: x64 30 | npm_config_arch: x64 31 | branch: linux 32 | runs-on: ${{ matrix.os }} 33 | steps: 34 | - uses: actions/checkout@v2 35 | with: 36 | ref: ${{ matrix.branch }} # Use the dynamically set branch 37 | - uses: actions/setup-node@v2 38 | with: 39 | node-version: 16.x 40 | - run: npm install 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | npm_config_arch: ${{ matrix.npm_config_arch }} 44 | - shell: pwsh 45 | run: echo "target=${{ matrix.platform }}-${{ matrix.arch }}" >> $env:GITHUB_ENV 46 | - run: npx vsce package --target ${{ env.target }} 47 | - uses: actions/upload-artifact@v2 48 | with: 49 | name: ${{ env.target }} 50 | path: "*.vsix" 51 | 52 | publish: 53 | runs-on: ubuntu-latest 54 | needs: build 55 | if: success() 56 | steps: 57 | - uses: actions/download-artifact@v2 58 | - run: npx vsce publish --packagePath $(find . -iname *.vsix) 59 | env: 60 | VSCE_PAT: ${{ secrets.VSCE_TOKEN }} 61 | -------------------------------------------------------------------------------- /snippets/gc.json: -------------------------------------------------------------------------------- 1 | { 2 | "enable()": { 3 | "prefix": "enable", 4 | "body": "gc.enable()", 5 | "description" : "Enable automatic garbage collection." 6 | }, 7 | "disable()": { 8 | "prefix": "disable", 9 | "body": "gc.disable()", 10 | "description" : "Disable automatic garbage collection. Heap memory can still be allocated, and garbage collection can still be initiated manually using gc.collect()." 11 | }, 12 | "collect()": { 13 | "prefix": "collect", 14 | "body": "gc.collect()", 15 | "description" : "Run a garbage collection." 16 | }, 17 | "mem_alloc()": { 18 | "prefix": "mem_alloc", 19 | "body": "gc.mem_alloc()", 20 | "description" : "Return the number of bytes of heap RAM that are allocated." 21 | }, 22 | 23 | "mem_free()": { 24 | "prefix": "mem_free", 25 | "body": "gc.mem_free()", 26 | "description" : "Return the number of bytes of available heap RAM, or -1 if this amount is not known." 27 | }, 28 | "threshold([amount])": { 29 | "prefix": "threshold", 30 | "body": "gc.threshold([amount])", 31 | "description" : "Set or query the additional GC allocation threshold. Normally, a collection is triggered only when a new allocation cannot be satisfied, i.e. on an out-of-memory (OOM) condition. If this function is called, in addition to OOM, a collection will be triggered each time after amount bytes have been allocated (in total, since the previous time such an amount of bytes have been allocated). amount is usually specified as less than the full heap size, with the intention to trigger a collection earlier than when the heap becomes exhausted, and in the hope that an early collection will prevent excessive memory fragmentation. This is a heuristic measure, the effect of which will vary from application to application, as well as the optimal value of the amount parameter." 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "brilliant-ar-studio" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - 0.0.1 10 | 11 | ### added 12 | 13 | - extension icon 14 | 15 | ## [1.0.2] - 2023-04-21 16 | 17 | ### fixed 18 | 19 | - extension icon 20 | - fpga update issue fixed 21 | 22 | ## [1.0.2] - 2023-04-21 23 | 24 | ### added 25 | 26 | - directory upload to device 27 | 28 | ## [1.0.3] - 29 | 30 | ### fixed 31 | 32 | - REPL, file operation speed inrease by 10x 33 | 34 | ## [1.0.3] - 35 | 36 | ### fixed 37 | 38 | - REPL, file size limit increased 39 | 40 | ## [1.5.0] - 41 | 42 | ### fixed 43 | 44 | - REPL, file create bug and dfu update adjusted with new display 45 | 46 | ## [1.7.0] - 47 | 48 | ### ADDED 49 | 50 | - Drag and Drop UI for screen code generation 51 | 52 | ## [1.8.0] - 2023-05-16 53 | 54 | ### IMPROVEMENT 55 | 56 | - dfu and FPGA with proper progress, file updating loader, block terminal while background process, 57 | - snippets more like intellisense 58 | 59 | ## [1.12.0] - 2023-06-06 60 | 61 | ### ADDED 62 | 63 | - LINUX support, 64 | - RAW DATA log, RAW DATA send options 65 | - Select from multiple monocle 66 | 67 | ## [1.14.0] - 2023-06-12 68 | 69 | ### ADDED 70 | 71 | - uploading file from anywhere in workspace 72 | - Building with ctrl+shift+b 73 | - fixed file transfer bugs 74 | - context menu in device files 75 | 76 | ## [1.19.0] - 2023-06-23 77 | 78 | ### ADDED Device Info tab 79 | 80 | - uploading file from anywhere in workspace 81 | - Building with ctrl+shift+b 82 | - fixed file transfer bugs 83 | - context menu in device files 84 | 85 | ## [1.19.3] - 2023-07-13 86 | 87 | 88 | ## [1.20.0] - 2024-05-27 89 | 90 | - Added Frame support 91 | - Smooth file operations for monocle and frame 92 | - more visible error messages 93 | 94 | ## [1.20.0] - 2024-05-28 95 | - Firmware update fix for frame 96 | -------------------------------------------------------------------------------- /snippets/ure.json: -------------------------------------------------------------------------------- 1 | { 2 | "compile(regex_str,[flags])": { 3 | "prefix": "calcsize", 4 | "body": "re.compile(${1:regex_str}, ${2:[flags]})", 5 | "description": "Compile regular expression, return regex object." 6 | }, 7 | "match(regex_str, string)": { 8 | "prefix": "match", 9 | "body": "re.match(${1:regex_str},${2:string})", 10 | "description": "Compile regex_str and match against string. Match always happens from starting position in a string." 11 | }, 12 | "search(regex_str, string)": { 13 | "prefix": "search", 14 | "body": "sre.search(${1:regex_str}, ${2:string})", 15 | "description": "Compile regex_str and search it in a string. Unlike match, this will search string for first position which matches regex (which still may be 0 if regex is anchored)." 16 | }, 17 | "sub(regex_str, replace, string, count,flags)": { 18 | "prefix": "sub", 19 | "body": "re.sub(${1:regex_str}, ${2:replace}, ${3:string}, ${4:count}, ${5:flags})", 20 | "description": "Unpack from the data according to the format string fmt. The return value is a tuple of the unpacked values." 21 | }, 22 | "group(index)": { 23 | "prefix": "group", 24 | "body": "match.group(${1:index})", 25 | "description": "Return matching (sub)string. index is 0 for entire match, 1 and above for each capturing group. Only numeric groups are supported." 26 | }, 27 | "groups()": { 28 | "prefix": "groups", 29 | "body": "match.groups()", 30 | "description": "Return a tuple containing all the substrings of the groups of the match." 31 | }, 32 | "start([index])": { 33 | "prefix": "start", 34 | "body": "match.start(${[index]})", 35 | "description": "" 36 | }, 37 | "end([index])": { 38 | "prefix": "end", 39 | "body": "match.end(${[index]})", 40 | "description": "Return the index in the original string of the start or end of the substring group that was matched. index defaults to the entire group, otherwise it will select a group." 41 | }, 42 | "span([index])": { 43 | "prefix": "espannd", 44 | "body": "match.span(${[index]})", 45 | "description": "Returns the 2-tuple (match.start(index), match.end(index))." 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /snippets/urandom.json: -------------------------------------------------------------------------------- 1 | { 2 | "random()": { 3 | "prefix": "random", 4 | "body": "random.random()", 5 | "description": "Return a random floating point number in the range [0.0, 1.0)." 6 | }, 7 | "uniform(a, b)": { 8 | "prefix": "uniform", 9 | "body": "random.uniform(${1:a},${2:b})", 10 | "description": "Return a random floating point number N such that a <= N <= b for a <= b, and b <= N <= a for b < a." 11 | }, 12 | "getrandbits(n)": { 13 | "prefix": "getrandbits", 14 | "body": "random.getrandbits(${1:n})", 15 | "description": "Return an integer with n random bits (0 <= n <= 32)" 16 | }, 17 | "randint(a, b)": { 18 | "prefix": "randint", 19 | "body": "random.randint(${1:a}, ${2:b})", 20 | "description": "Return a random integer in the range [a, b]." 21 | }, 22 | "randrange(stop)": { 23 | "prefix": "randrange", 24 | "body": "random.randrange(${1:stop})", 25 | "description": "returns a random integer from the range [0, stop)" 26 | }, 27 | "randrange(start, stop,[step])": { 28 | "prefix": "randrange", 29 | "body": "random.randrange(${1:start}, ${2:stop},${3:step})", 30 | "description": "eturns a random integer from the range [start, stop) in steps of step. For instance, calling randrange(1, 10, 2) will return odd numbers between 1 and 9 inclusive" 31 | }, 32 | "randrange(start, stop)": { 33 | "prefix": "randrange", 34 | "body": "random.randrange(${1:start}, ${2:stop})", 35 | "description": "returns a random integer from the range [start, stop) in steps of step. For instance, calling randrange(1, 10, 2) will return odd numbers between 1 and 9 inclusive." 36 | }, 37 | 38 | "seed(n)": { 39 | "prefix": "seed", 40 | "body": "random.seed(${1:n})", 41 | "description": "Initialise the random number generator module with the seed n which should be an integer. When no argument (or None) is passed in it will (if supported by the port) initialise the PRNG with a true random number (usually a hardware generated random number)." 42 | }, 43 | "choice(sequence)": { 44 | "prefix": "choice", 45 | "body": "random.choice(${1:sequence})", 46 | "description": "Chooses and returns one item at random from sequence (tuple, list or any object that supports the subscript operation)." 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /snippets/time.json: -------------------------------------------------------------------------------- 1 | { 2 | "time(secs)": { 3 | "prefix": "time", 4 | "body": "time.time(${1:secs})", 5 | "description" : "Sets or gets the current system time in seconds. If secs is not provided, the current time is returned, otherwise the time is set according to the value of secs. secs should be referenced from midnight on the 1st of January 1970." 6 | }, 7 | "now(epoch)": { 8 | "prefix": "now", 9 | "body": "time.now(${1:epoch})", 10 | "description" : "Returns a dictionary containing a human readable date and time. If no argument is provided, the current system time is used. If epoch is provided, that value will be used to generate the dictionary. The epoch should be referenced from midnight on the 1st of January 1970." 11 | }, 12 | "zone(offset)": { 13 | "prefix": "zone", 14 | "body": "time.zone(${1:offset})", 15 | "description" : "Sets or gets the time zone offset from GMT as a string. If offset is provided, the timezone is set to the new value. offset must be provided as a string, eg “8:00”, or “-06:30”." 16 | }, 17 | "mktime(dict)": { 18 | "prefix": "mktime", 19 | "body": "time.mktime(${1:dict})", 20 | "description" : "The inverse of now(). Converts a dictionary provided into an epoch timestamp. The returned epoch value will be referenced from midnight on the 1st of January 1970." 21 | }, 22 | "ticks_add(ticks, delta)": { 23 | "prefix": "ticks_add", 24 | "body": "time.ticks_add(${1:ticks} , ${2:delta})", 25 | "description" : "Offsets a timestamp value by a given number. Can be positive or negative." 26 | }, 27 | "ticks_diff(ticks1,ticks2)": { 28 | "prefix": "ticks_diff", 29 | "body": "time.ticks_diff(${1:ticks1} , ${2:ticks2})", 30 | "description" : "Returns the difference between two timestamps. i.e. ticks1 - ticks2, but takes into consideration if the numbers wrap around." 31 | }, 32 | "sleep(secs)": { 33 | "prefix": "sleep", 34 | "body": "time.sleep(${1:secs})", 35 | "description" : "Sleep for a given number of seconds." 36 | }, 37 | "sleep_ms(msecs)": { 38 | "prefix": "sleep_ms", 39 | "body": "time.sleep_ms(${1:msecs})", 40 | "description" : "Sleep for a given number of milliseconds." 41 | }, 42 | "ticks_ms()": { 43 | "prefix": "ticks_ms", 44 | "body": "time.ticks_ms()", 45 | "description" : "Returns a timestamp in milliseconds since power on." 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /snippets/device.json: -------------------------------------------------------------------------------- 1 | { 2 | "NAME": { 3 | "prefix": "monocle", 4 | "body": "device.NAME", 5 | "description" : "Device name is monocle" 6 | }, 7 | "reset()": { 8 | "prefix": "reset", 9 | "body": "device.reset()", 10 | "description" : "Resets the device" 11 | }, 12 | "VERSION": { 13 | "prefix": "VERSION", 14 | "body": "device.VERSION", 15 | "description" :"Constant containing the firmware version as a 12 character string." 16 | }, 17 | "GIT_TAG": { 18 | "prefix": "GIT_TAG", 19 | "body": "device.GIT_TAG", 20 | "description" :"Constant containing the build git tag as a 9 character string." 21 | }, 22 | "GIT_REPO": { 23 | "prefix": "GIT_REPO", 24 | "body": "device.GIT_REPO", 25 | "description" :"Constant containing the project git repo as a string" 26 | }, 27 | "battery_level()": { 28 | "prefix": "battery_level", 29 | "body": "device.battery_level()", 30 | "description" :"Returns the battery level percentage as an integer." 31 | }, 32 | "reset_cause()": { 33 | "prefix": "reset_cause", 34 | "body": "device.reset_cause()", 35 | "description" :"Returns the reason for the previous reset or startup state. These can be either:-'POWERED_ON' if the device was powered on normally- 'SOFTWARE_RESET' if device.reset() was called- 'CRASHED' if the device had crashed." 36 | }, 37 | "prevent_sleep(enable)": { 38 | "prefix": "prevent_sleep", 39 | "body": "device.prevent_sleep(${1|False,True|})", 40 | "description" :"Enables or disables sleeping of the device when put back into the charging case. Sleeping is enabled by default. If no argument is given. The currently set value is returned. WARNING: Running monocle for prolonged periods may result in display burn in, as well as reduced lifetime of components." 41 | }, 42 | "force_sleep()": { 43 | "prefix": "force_sleep", 44 | "body": "device.force_sleep()", 45 | "description" :"Puts the device to sleep. All hardware components are shut down, and Bluetooth is disconnected. Upon touching either of the touch pads, the device will wake up, and reset." 46 | }, 47 | "storage(start,length)": { 48 | "prefix": "storage", 49 | "body": "device.storage(${1:start}, ${2:length},)", 50 | "description" :"The Storage class is used internally for initializing and accessing the file system. This class shouldn’t be accessed, unless you want to reformat the internal storage." 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | 26 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 27 | 28 | ## Run tests 29 | 30 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 31 | * Press `F5` to run the tests in a new window with your extension loaded. 32 | * See the output of the test result in the debug console. 33 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 34 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 35 | * You can create folders inside the `test` folder to structure your tests any way you want. 36 | 37 | ## Go further 38 | 39 | * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns. 40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 43 | -------------------------------------------------------------------------------- /src/snippets/provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | import {snippets} from '.'; 6 | export class SnippetProvider implements vscode.TreeDataProvider, vscode.TreeDragAndDropController { 7 | 8 | dropMimeTypes = []; 9 | dragMimeTypes = ['application/vnd.code.tree.snippettemplates']; 10 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 11 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 12 | 13 | private dragDataEmitter = new vscode.EventEmitter(); 14 | public readonly onDragData = this.dragDataEmitter.event; 15 | 16 | 17 | // for drag 18 | public async handleDrag(source: Snippet[], treeDataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { 19 | treeDataTransfer.set('application/vnd.code.tree.snippettemplates', new vscode.DataTransferItem(source[0].label)); 20 | return ; 21 | } 22 | 23 | returnData(){ 24 | return this.dropMimeTypes; 25 | } 26 | 27 | refresh(): void { 28 | this._onDidChangeTreeData.fire(); 29 | } 30 | 31 | getTreeItem(element: Snippet): vscode.TreeItem { 32 | return element; 33 | } 34 | 35 | getChildren(element?: Snippet): Thenable { 36 | // return Promise.resolve(categories); 37 | if (element) { 38 | let snippetCodes = snippets[element.label]; 39 | let snippetItems:Snippet[] = []; 40 | Object.keys(snippetCodes).forEach((item:any)=>{ 41 | let cmd = { 42 | command: 'editor.action.insertSnippet', 43 | title: 'Use Snippet', 44 | arguments: [{ langId: "python", name: item }] 45 | }; 46 | snippetItems.push(new Snippet(item,vscode.TreeItemCollapsibleState.None,snippetCodes[item].description,cmd,snippetCodes[item].id)); 47 | }); 48 | return Promise.resolve(snippetItems); 49 | } else { 50 | let categories:Snippet[] = []; 51 | // console.log(JSON.stringify(snippets.display)); 52 | Object.keys(snippets).forEach(key=>{ 53 | categories.push(new Snippet(key,vscode.TreeItemCollapsibleState.Collapsed,key)); 54 | }); 55 | return Promise.resolve(categories); 56 | } 57 | } 58 | } 59 | 60 | export class Snippet extends vscode.TreeItem { 61 | 62 | constructor( 63 | public readonly label: string, 64 | public readonly collapsibleState: vscode.TreeItemCollapsibleState, 65 | public readonly description?: string, 66 | public readonly command?: vscode.Command, 67 | public readonly id?: string, 68 | 69 | ) { 70 | super(label, collapsibleState); 71 | this.id = "snippet_"+label; 72 | this.tooltip = `${this.description}`; 73 | this.description = this.description; 74 | } 75 | 76 | 77 | contextValue = 'snippet'; 78 | } 79 | -------------------------------------------------------------------------------- /media/JetBrains_Mono/README.txt: -------------------------------------------------------------------------------- 1 | JetBrains Mono Variable Font 2 | ============================ 3 | 4 | This download contains JetBrains Mono as both variable fonts and static fonts. 5 | 6 | JetBrains Mono is a variable font with this axis: 7 | wght 8 | 9 | This means all the styles are contained in these files: 10 | JetBrainsMono-VariableFont_wght.ttf 11 | JetBrainsMono-Italic-VariableFont_wght.ttf 12 | 13 | If your app fully supports variable fonts, you can now pick intermediate styles 14 | that aren’t available as static fonts. Not all apps support variable fonts, and 15 | in those cases you can use the static font files for JetBrains Mono: 16 | static/JetBrainsMono-Thin.ttf 17 | static/JetBrainsMono-ExtraLight.ttf 18 | static/JetBrainsMono-Light.ttf 19 | static/JetBrainsMono-Regular.ttf 20 | static/JetBrainsMono-Medium.ttf 21 | static/JetBrainsMono-SemiBold.ttf 22 | static/JetBrainsMono-Bold.ttf 23 | static/JetBrainsMono-ExtraBold.ttf 24 | static/JetBrainsMono-ThinItalic.ttf 25 | static/JetBrainsMono-ExtraLightItalic.ttf 26 | static/JetBrainsMono-LightItalic.ttf 27 | static/JetBrainsMono-Italic.ttf 28 | static/JetBrainsMono-MediumItalic.ttf 29 | static/JetBrainsMono-SemiBoldItalic.ttf 30 | static/JetBrainsMono-BoldItalic.ttf 31 | static/JetBrainsMono-ExtraBoldItalic.ttf 32 | 33 | Get started 34 | ----------- 35 | 36 | 1. Install the font files you want to use 37 | 38 | 2. Use your app's font picker to view the font family and all the 39 | available styles 40 | 41 | Learn more about variable fonts 42 | ------------------------------- 43 | 44 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts 45 | https://variablefonts.typenetwork.com 46 | https://medium.com/variable-fonts 47 | 48 | In desktop apps 49 | 50 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc 51 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts 52 | 53 | Online 54 | 55 | https://developers.google.com/fonts/docs/getting_started 56 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide 57 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts 58 | 59 | Installing fonts 60 | 61 | MacOS: https://support.apple.com/en-us/HT201749 62 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux 63 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows 64 | 65 | Android Apps 66 | 67 | https://developers.google.com/fonts/docs/android 68 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts 69 | 70 | License 71 | ------- 72 | Please read the full license text (OFL.txt) to understand the permissions, 73 | restrictions and requirements for usage, redistribution, and modification. 74 | 75 | You can use them in your products & projects – print or digital, 76 | commercial or otherwise. 77 | 78 | This isn't legal advice, please consider consulting a lawyer and see the full 79 | license for all details. 80 | -------------------------------------------------------------------------------- /snippets/uselect.json: -------------------------------------------------------------------------------- 1 | { 2 | "poll()": { 3 | "prefix": "poll", 4 | "body": "select.poll()", 5 | "description": "Create an instance of the Poll class." 6 | }, 7 | "select(rlist, wlist, xlist,[timeout])": { 8 | "prefix": "select", 9 | "body": "select.select(${1:rlist}, ${2:wlist}, ${3:xlist},${4:[timeout]})", 10 | "description": "Wait for activity on a set of objects.This function is provided by some MicroPython ports for compatibility and is not efficient. Usage of Poll is recommended instead." 11 | }, 12 | "register(obj[, eventmask])": { 13 | "prefix": "register", 14 | "body": "poll.register(${1:obj}, ${2:[eventmask]})", 15 | "description": "Register stream obj for polling. eventmask is logical OR of: select.POLLIN - data available for reading select.POLLOUT - more data can be written Note that flags like select.POLLHUP and select.POLLERR are not valid as input eventmask (these are unsolicited events which will be returned from poll() regardless of whether they are asked for). This semantics is per POSIX.eventmask defaults to select.POLLIN | select.POLLOUT. It is OK to call this function multiple times for the same obj. Successive calls will update obj’s eventmask to the value of eventmask (i.e. will behave as modify())." 16 | }, 17 | "unregister(obj)": { 18 | "prefix": "unregister", 19 | "body": "poll.unregister(${1:obj})", 20 | "description": "Modify the eventmask for obj. If obj is not registered, OSError is raised with error of ENOENT" 21 | }, 22 | "modify(obj, eventmask)": { 23 | "prefix": "modify", 24 | "body": "poll.modify(${1:obj}, ${1:eventmask})", 25 | "description": "Unregister obj from polling." 26 | }, 27 | "poll(timeout)": { 28 | "prefix": "poll", 29 | "body": "poll.poll(${1:timeout})", 30 | "description": "Wait for at least one of the registered objects to become ready or have an exceptional condition, with optional timeout in milliseconds (if timeout arg is not specified or -1, there is no timeout).Returns list of (obj, event, …) tuples. There may be other elements in tuple, depending on a platform and version, so don’t assume that its size is 2. The event element specifies which events happened with a stream and is a combination of select.POLL* constants described above. Note that flags select.POLLHUP and select.POLLERR can be returned at any time (even if were not asked for), and must be acted on accordingly (the corresponding stream unregistered from poll and likely closed), because otherwise all further invocations of poll() may return immediately with these flags set for this stream again." 31 | }, 32 | "poll.ipoll(timeout,flags)": { 33 | "prefix": "ipoll", 34 | "body": "poll.ipoll(${1:timeout},${2:flags})", 35 | "description": "Like poll.poll(), but instead returns an iterator which yields a callee-owned tuple. This function provides an efficient, allocation-free way to poll on streams. If flags is 1, one-shot behaviour for events is employed: streams for which events happened will have their event masks automatically reset (equivalent to poll.modify(obj, 0)), so new events for such a stream won’t be processed until new mask is set with poll.modify(). This behaviour is useful for asynchronous I/O schedulers." 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /media/JetBrains_Mono/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brilliant AR Studio 2 | 3 | Build and test your Monocle AR apps with ease using MicroPython! 🐍 4 | 5 | Check out the full documentation the [docs pages 📚](https://docs.brilliant.xyz), or [contribute 🧑‍💻](https://github.com/brilliantlabsAR/ar-studio-for-vscode) to make this extension even better! 6 | 7 | ## Features 8 | 9 | Loading and saving Python files to your device: 10 | 11 | ![Animation of loading and saving files to Monocle](./media/vscode-ext-upload-file.gif) 12 | 13 | Access the on-device REPL: 14 | 15 | ![Animation of the Monocle REPL](./media/vscode-ext-repl.gif) 16 | 17 | Easily browse the various MicroPython modules: 18 | 19 | ![Animation of drag and drop editor](./media/vscode-ext-api-drag-drop.gif) 20 | 21 | Update your FPGA binaries: 22 | 23 | ![Image of the FPGA update buttons](./media/vscode-ext-custom-fpga.gif) 24 | 25 | Browse community projects, or publish your own: 26 | 27 | ![Animation of user projects](./media/vscode-ext-custom-projects.gif) 28 | 29 | ### UI instructions 30 | 31 | 1. Make sure a project is initialized (you can see 'device files' in File Explorer) 32 | if not setup, do with command `Brilliant AR Studio: Initialize new project folder` 33 | choose project name and directory and you are good to go. 34 | 35 | 2. connect monocle from one of the ways 36 | 37 | 1. command `Brilliant AR Studio: Connect` 38 | 2. status bar button `Monocle` 39 | 3. file explorer -> Brilliant AR Studio: device files -> connect 40 | 4. (optional) If multiple device present select your device from the dropdown that appears after scan. more value of RSSI with negative sign means device is farther and vice versa 41 | 42 | 3. after connect you can see REPL and all files listed in `file explorer` -> `Brilliant AR Studio: device files` 43 | 44 | 4. Now open main.py and edit it. 45 | you can create any file transferred to device. 46 | you can also upload files/dir from your project dir by 47 | right click and `Brilliant AR Studio: Upload File To Device` then type path of file at which you want to upload. 48 | all uploaded files will be tracked with `.brilliant/device-files.json` with relative source and destination. 49 | 50 | 5. Auto run: it means as soon as you save all files will be transferred and main.py will run. default is always on you turn off by 51 | button at right corner of editor or command `Brilliant AR Studio: Stop auto run`. 52 | to start again click on play button at right corner of editor or `Brilliant AR Studio: start auto run` 53 | (this feture discontinued from v1.15) 54 | 55 | 6. Run in Device: with this all file will be transferred and excuted at REPL run time 56 | quite useful to test files without uploading. 57 | 58 | use with button at at right corner of editor or `Brilliant AR Studio: Run in Device` 59 | 60 | 7. Build: when you are done with editing you can build with `ctrl+shift+b`. It will transfer all earlier files that are uploaded 61 | and apply soft reset that will execute main.py. 62 | 63 | 8. ONE output channel and TWO Terminal 64 | 65 | 1. (OUTPUT CHANNEL) RAW-REPL: for REPL internal communication (allows to debug issues) 66 | 2. (TERMNAL) REPL: for Interactive python terminal access of Device 67 | 3. (TERMNAL) RAW-DATA: for RAW data communication over data service ( such as you can send microphone bytes to this service) 68 | 69 | 9. Send Raw Data: data can be directly send at data service to monocle. This data can be received inside monocle by `bluetooth.receive_callback()`. To use the play button at title bar of `file explorer` -> `Brilliant AR Studio: device files` or command `Brilliant AR Studio: Send Raw Data` 70 | 71 | 10. Context Menu: on right clicking on device files in `file explorer` -> `Brilliant AR Studio: device files` you can perform: 72 | a. Download of files from Device 73 | b. Rename of files 74 | c. Delete from device 75 | 76 | # Create and Edit display screens with DRAG-DROP UI: 77 | 78 | ![Drag Drop UI](./media/vscode-ext-drag-drop-GUI.png) 79 | 80 | ### DRAG DROP instructions 81 | 82 | 1. Start a new screen with Brilliant AR Studio -> screens -> click on plus icon 83 | or with command 'Brilliant AR Studio: Open New GUI Screen' 84 | 85 | 2. Two columns of editor will appear one for python code and other the GUI. dont change python code 86 | 87 | 3. Draw from GUI with and after every change python file will change 88 | and you can test with Run in Device button or command 89 | 90 | 4. Drawing instrctions and shorcuts: 91 | 92 | 1. To draw Rectangle select recatangle and drag and then click to select, transform or move 93 | 2. To draw Line select Line and click on two points then click to select or drag a selection , adjust anchors 94 | 3. To draw Polyline or polygon click select button and click multiple points and to finish double click or 95 | to cancel press Esc. to adjust select and move anchor points 96 | 4. To write text select T and drag to make box, double click to edit and enter or outside click to finish. shift + enter to new line 97 | 5. you can move any object with arrow keys after select. ctrl + arrow keys to move with precise 98 | 6. to delete select and press delete button, multiple selected objects can be deleted 99 | 7. ctrl + A to select all 100 | 8. ctrl + D to duplicate selected 101 | 102 | TIP: To snap line, polyline, polygon to nearest multiple of 45 degrees hold ctrl or shift 103 | 104 | ## Requirements 105 | 106 | - Windows or MacOS 107 | - Bluetooth hardware correctly installed on your system. 108 | 109 | #### for MacOS 110 | 111 | - Make Sure OSX is installed and all bluetooth permissions are given to vs code 112 | 113 | #### for Linux (currently tested on Debian) 114 | 115 | - Make sure bluetooth is on and the user have permissions to access Bluetooth 116 | - To give permissions you try adding user to bluetooth group with following command 117 | `sudo usermod -aG bluetooth $USER` 118 | - then log off/log on or try a reboot 119 | -------------------------------------------------------------------------------- /snippets/math.json: -------------------------------------------------------------------------------- 1 | { 2 | "acos(x)": { 3 | "prefix": "acos", 4 | "body": "math.acos(${1:x})", 5 | "description": "Return the inverse cosine of x." 6 | }, 7 | "acosh(x)": { 8 | "prefix": "acosh", 9 | "body": "math.acosh(${1:x})", 10 | "description": "Return the inverse hyperbolic cosine of x" 11 | }, 12 | "asin(x)": { 13 | "prefix": "asin", 14 | "body": "math.asin(${1:x})", 15 | "description": "Return the inverse sine of x." 16 | }, 17 | "asinh(x)": { 18 | "prefix": "asinh", 19 | "body": "math.asinh(${1:x})", 20 | "description": "Return the inverse hyperbolic sine of x." 21 | }, 22 | "atan(x)": { 23 | "prefix": "atan", 24 | "body": "math.atan(${1:x})", 25 | "description": "Return the inverse tangent of x." 26 | }, 27 | "atan2(y, x)": { 28 | "prefix": "atan2", 29 | "body": "math.atan2(${1:y},${2:x})", 30 | "description": "Return the principal value of the inverse tangent of y/x." 31 | }, 32 | 33 | "ceil(x)": { 34 | "prefix": "ceil", 35 | "body": "math.ceil(${1:x})", 36 | "description": "Return an integer, being x rounded towards positive infinity." 37 | }, 38 | "copysign(x,y)": { 39 | "prefix": "copysign", 40 | "body": "math.copysign(${1:x},${2:y})", 41 | "description": "Return x with the sign of y." 42 | }, 43 | "cos(x)": { 44 | "prefix": "cos", 45 | "body": "math.asin(${1:x})", 46 | "description": "Return the cosine of x" 47 | }, 48 | "cosh(x)": { 49 | "prefix": "cosh", 50 | "body": "math.cosh(${1:x})", 51 | "description": "Return the hyperbolic cosine of x" 52 | }, 53 | "degrees(x)": { 54 | "prefix": "degrees", 55 | "body": "math.degrees(${1:x})", 56 | "description": "Return radians x converted to degrees." 57 | }, 58 | "erf(x)": { 59 | "prefix": "erf", 60 | "body": "math.erf(${1:x})", 61 | "description": "Return the error function of x." 62 | }, 63 | 64 | "erfc(x)": { 65 | "prefix": "erfc", 66 | "body": "math.erfc(${1:x})", 67 | "description": "Return the complementary error function of x." 68 | }, 69 | "exp(x)": { 70 | "prefix": "exp", 71 | "body": "math.exp(${1:x})", 72 | "description": "Return the exponential of x." 73 | }, 74 | "expm1(x)": { 75 | "prefix": "expm1", 76 | "body": "math.expm1(${1:x})", 77 | "description": "Return radians x converted to degrees." 78 | }, 79 | "fabs(x)": { 80 | "prefix": "fabs", 81 | "body": "math.fabs(${1:x})", 82 | "description": "Return the absolute value of x." 83 | }, 84 | 85 | "floor(x)": { 86 | "prefix": "floor", 87 | "body": "math.floor(${1:x})", 88 | "description": "Return an integer, being x rounded towards negative infinity." 89 | }, 90 | "fmod(x,y)": { 91 | "prefix": "fmod", 92 | "body": "math.fmod(${1:x},${2:y})", 93 | "description": "Return the remainder of x/y." 94 | }, 95 | "frexp(x)": { 96 | "prefix": "frexp", 97 | "body": "math.frexp(${1:x})", 98 | "description": "Decomposes a floating-point number into its mantissa and exponent. The returned value is the tuple (m, e) such that x == m * 2**e exactly. If x == 0 then the function returns (0.0, 0), otherwise the relation 0.5 <= abs(m) < 1 holds." 99 | }, 100 | "gamma(x)": { 101 | "prefix": "gamma", 102 | "body": "math.gamma(${1:x})", 103 | "description": "Return the gamma function of x." 104 | }, 105 | 106 | "isfinite(x)": { 107 | "prefix": "isfinite", 108 | "body": "math.isfinite(${1:x})", 109 | "description": "Return True if x is finite." 110 | }, 111 | "isinf(x)": { 112 | "prefix": "fmod", 113 | "body": "math.fmod(${1:x})", 114 | "description": "Return True if x is infinite." 115 | }, 116 | "isnan(x)": { 117 | "prefix": "isnan", 118 | "body": "math.isnan(${1:x})", 119 | "description": "Return True if x is not-a-number" 120 | }, 121 | "ldexp(x,exp)": { 122 | "prefix": "gamma", 123 | "body": "math.gamma(${1:x},${2:exp})", 124 | "description": "Return x * (2**exp)." 125 | }, 126 | "lgamma(x)": { 127 | "prefix": "lgamma", 128 | "body": "math.lgamma(${1:x})", 129 | "description": "Return the natural logarithm of the gamma function of x." 130 | }, 131 | "log(x)": { 132 | "prefix": "log", 133 | "body": "math.log(${1:x})", 134 | "description": "Return the natural logarithm of x." 135 | }, 136 | "log10(x)": { 137 | "prefix": "log10", 138 | "body": "math.log10(${1:x})", 139 | "description": "Return the base-10 logarithm of x" 140 | }, 141 | "log2(x)": { 142 | "prefix": "log2", 143 | "body": "math.log2(${1:x})", 144 | "description": "Return the base-2 logarithm of x" 145 | }, 146 | "modf(x)": { 147 | "prefix": "modf", 148 | "body": "math.modf(${1:x})", 149 | "description": "Return a tuple of two floats, being the fractional and integral parts of x. Both return values have the same sign as x" 150 | }, 151 | "pow(x,y)": { 152 | "prefix": "pow", 153 | "body": "math.pow(${1:x},${2:y})", 154 | "description": "Returns x to the power of y." 155 | }, 156 | "radians(x)": { 157 | "prefix": "radians", 158 | "body": "math.radians(${1:x})", 159 | "description": "Return degrees x converted to radians." 160 | }, 161 | "sin(x)": { 162 | "prefix": "sin", 163 | "body": "math.sin(${1:x})", 164 | "description": "Return the sine of x." 165 | }, 166 | "sinh(x)": { 167 | "prefix": "sinh", 168 | "body": "math.sinh(${1:x})", 169 | "description": "Return the hyperbolic sine of x." 170 | }, 171 | "sqrt(x)": { 172 | "prefix": "sqrt", 173 | "body": "math.sqrt(${1:x})", 174 | "description": "Return the square root of x." 175 | }, 176 | "tan(x)": { 177 | "prefix": "tan", 178 | "body": "math.tan(${1:x})", 179 | "description": "Return the tangent of x." 180 | }, 181 | "tanh(x)": { 182 | "prefix": "tanh", 183 | "body": "math.tanh(${1:x})", 184 | "description": "Return the hyperbolic tangent of x" 185 | }, 186 | "trunc(x)": { 187 | "prefix": "trunc", 188 | "body": "math.trunc(${1:x})", 189 | "description": "Return an integer, being x rounded towards 0" 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /media/main.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'JetBrains Mono'; 3 | src: url(./JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf); 4 | } 5 | 6 | /* root { 7 | --vscode-foreground: #cccccc; 8 | --vscode-disabledForeground: rgba(204, 204, 204, 0.5); 9 | --vscode-errorForeground: #f85149; 10 | --vscode-descriptionForeground: #8b949e; 11 | --vscode-icon-foreground: #cccccc; 12 | --vscode-focusBorder: #0078d4; 13 | --vscode-textSeparator-foreground: #21262d; 14 | --vscode-textLink-foreground: #40a6ff; 15 | --vscode-textLink-activeForeground: #40a6ff; 16 | --vscode-textPreformat-foreground: #d7ba7d; 17 | --vscode-textBlockQuote-background: #010409; 18 | --vscode-textBlockQuote-border: rgba(255, 255, 255, 0.08); 19 | --vscode-textCodeBlock-background: rgba(110, 118, 129, 0.4); 20 | } */ 21 | 22 | .main { 23 | padding: 10px; 24 | /* height: 100vh; 25 | width: 100vw; */ 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | flex-direction: column; 30 | /* background: black; */ 31 | } 32 | 33 | .tools { 34 | display: flex; 35 | align-items: center; 36 | justify-content: space-between; 37 | flex-wrap: wrap; 38 | -webkit-user-select: none; 39 | /* Safari */ 40 | -ms-user-select: none; 41 | /* IE 10 and IE 11 */ 42 | user-select: none; 43 | /* Standard syntax */ 44 | /* align-items: center; */ 45 | margin-bottom: 10px; 46 | max-width: 1280px; 47 | width: 100%; 48 | } 49 | 50 | .tools span.tool_title { 51 | display: block; 52 | color: #E0E6E5; 53 | margin: 8px 0; 54 | text-align: left; 55 | font-family: 'JetBrains Mono'; 56 | font-size: 14px; 57 | text-transform: capitalize; 58 | } 59 | 60 | .tools .tools_items, 61 | .tools .shape_items { 62 | display: flex; 63 | flex-wrap: wrap; 64 | margin: 0 -5px; 65 | align-items: center; 66 | } 67 | 68 | .tools .tools_items .item, 69 | .tools .shape_items .item { 70 | padding: 5px; 71 | } 72 | 73 | .tools button { 74 | padding: 2px; 75 | height: 35px; 76 | width: 35px; 77 | font-size: 14px; 78 | cursor: pointer; 79 | background-color: transparent; 80 | outline: none; 81 | border: none; 82 | display: inline-flex; 83 | justify-content: center; 84 | align-items: center; 85 | border-radius: 5px; 86 | transition: 0.3s all; 87 | } 88 | 89 | .tools img { 90 | width: auto; 91 | height: 16px; 92 | } 93 | 94 | .tools input { 95 | /* height: 30px; 96 | width: 30px; */ 97 | /* padding: 2px; */ 98 | height: 26px; 99 | width: 26px; 100 | cursor: pointer; 101 | background-color: transparent; 102 | outline: none; 103 | border: none; 104 | } 105 | 106 | .shape-btn.active { 107 | background-color: rgba(0, 0, 0, 0.5) !important; 108 | } 109 | 110 | .alignBtn { 111 | background-color: transparent !important; 112 | } 113 | 114 | .alignBtn.active { 115 | background-color: rgba(0, 0, 0, 0.5) !important; 116 | } 117 | 118 | .thickness_dropdown { 119 | position: relative; 120 | } 121 | 122 | #myDropdown { 123 | display: inline-block; 124 | position: absolute; 125 | left: 0; 126 | top: 35px; 127 | width: 100%; 128 | z-index: 1000; 129 | 130 | } 131 | 132 | #myDropdown ul { 133 | list-style: none; 134 | background: #333; 135 | margin: 0; 136 | padding: 5px; 137 | width: 30px; 138 | text-align: center; 139 | border-radius: 5px; 140 | box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.9); 141 | } 142 | 143 | #myDropdown ul li { 144 | padding: 5px; 145 | margin: 0; 146 | border-radius: 3px; 147 | /* color: rgb(23, 22, 22); */ 148 | color: #ccc; 149 | font-family: 'JetBrains Mono'; 150 | transition: 0.3s all; 151 | background: transparent; 152 | } 153 | 154 | li.t-op:hover { 155 | cursor: pointer; 156 | background-color: #111 !important; 157 | } 158 | 159 | .title { 160 | font-family: 'JetBrains Mono'; 161 | text-align: center; 162 | font-size: 14px; 163 | margin: 0; 164 | color: #E0E6E5; 165 | margin-bottom: 0.6rem; 166 | -webkit-user-select: none; 167 | /* Safari */ 168 | -ms-user-select: none; 169 | /* IE 10 and IE 11 */ 170 | user-select: none; 171 | /* Standard syntax */ 172 | 173 | } 174 | 175 | .box { 176 | -webkit-clip-path: polygon(0 0, 100% 35%, 100% 65%, 0% 100%); 177 | clip-path: polygon(0 0, 100% 35%, 100% 65%, 0% 100%); 178 | height: 30px; 179 | width: 40px; 180 | background: #E0E6E5; 181 | margin-right: 5px; 182 | } 183 | 184 | .thickness { 185 | width: 40px !important; 186 | } 187 | 188 | .thickness img { 189 | width: 50% !important; 190 | } 191 | 192 | .thickness span { 193 | color: #E0E6E5; 194 | font-size: 16px; 195 | margin-left: 2px; 196 | font-family: 'JetBrains Mono'; 197 | } 198 | 199 | /* .thickness{ 200 | border: 1px solid black; 201 | text-align: left; 202 | background-color: gray; 203 | } */ 204 | .color_picker_box { 205 | position: relative; 206 | } 207 | 208 | .color_picker_box .color_pick { 209 | width: 20px; 210 | height: 20px; 211 | border-radius: 5px; 212 | background: #fff; 213 | cursor: pointer; 214 | } 215 | 216 | .color_picker_box .picker_box { 217 | position: absolute; 218 | width: 200px; 219 | height: auto; 220 | display: flex; 221 | flex-wrap: wrap; 222 | background: #0c0a0a; 223 | border-radius: 10px; 224 | right: 0; 225 | /* transform: translateX(-50%); */ 226 | top: 30px; 227 | transition: 0.3s all; 228 | opacity: 0; 229 | visibility: hidden; 230 | padding: 7px; 231 | box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.9); 232 | } 233 | 234 | .pickbox_show { 235 | opacity: 1 !important; 236 | visibility: visible !important; 237 | z-index: 1000; 238 | } 239 | 240 | .color_picker_box .picker_box .item { 241 | width: 25%; 242 | height: 50%; 243 | padding: 0; 244 | display: flex; 245 | justify-content: center; 246 | align-items: center; 247 | } 248 | 249 | .color_picker_box .picker_box .item .pigment_box { 250 | margin: 7px; 251 | width: 35px; 252 | height: 35px; 253 | border-radius: 5px; 254 | cursor: pointer; 255 | } 256 | 257 | .colorselection { 258 | /* display: none; */ 259 | } 260 | #selected_color_custom{ 261 | margin-right: 6px; 262 | } 263 | .cpic-box{ 264 | position: relative; 265 | z-index: 1; 266 | margin-right: 8px; 267 | } 268 | 269 | .img_brush{ 270 | left: 0; 271 | top: 0; 272 | width: 100%; 273 | height: 100%; 274 | z-index: 1; 275 | opacity: 0; 276 | 277 | } 278 | 279 | 280 | .d-flex { 281 | display: flex; 282 | } 283 | 284 | .color_picker_box .d-flex img { 285 | height: 20px; 286 | margin-right: 8px; 287 | } 288 | 289 | .aic { 290 | align-items: center; 291 | } -------------------------------------------------------------------------------- /src/projects.ts: -------------------------------------------------------------------------------- 1 | import { request } from "@octokit/request"; 2 | import * as vscode from 'vscode'; 3 | let JSZIP = require("jszip"); 4 | // import fs from 'fs'; 5 | const TOPIC = 'monocle-ar-project'; 6 | export async function getRepoList(){ 7 | let resp = await request("GET /search/repositories?q={q}", { 8 | q: TOPIC+" in:topics", 9 | }).catch((err:any)=>console.log(err)); 10 | // console.log(resp); 11 | return resp; 12 | } 13 | 14 | export class ProjectProvider implements vscode.TreeDataProvider, vscode.TreeDragAndDropController { 15 | 16 | dropMimeTypes = []; 17 | dragMimeTypes = ['application/vnd.code.tree.snippettemplates']; 18 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 19 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 20 | 21 | 22 | // for drag 23 | public async handleDrag(source: Project[], treeDataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { 24 | treeDataTransfer.set('application/vnd.code.tree.snippettemplates', new vscode.DataTransferItem(source[0].label)); 25 | return ; 26 | } 27 | 28 | refresh(): void { 29 | this._onDidChangeTreeData.fire(); 30 | } 31 | 32 | getTreeItem(element: Project): vscode.TreeItem { 33 | return element; 34 | } 35 | search(term:string){ 36 | console.log(term); 37 | } 38 | 39 | 40 | async getChildren(element?: Project): Promise { 41 | // return Promise.resolve(categories); 42 | if (element) { 43 | return [element]; 44 | 45 | } else { 46 | let categories:Project[] = []; 47 | let repoList:any = await getRepoList(); 48 | // console.log(repoList); 49 | if(repoList?.data.items.length>0){ 50 | repoList.data.items.forEach((repo:any)=>{ 51 | let desc:string = repo.description?repo.full_name+" "+repo.description:repo.full_name; 52 | categories.push(new Project(repo.name, repo.owner.avatar_url, repo.clone_url, desc)); 53 | }); 54 | } 55 | 56 | return categories; 57 | } 58 | 59 | } 60 | 61 | } 62 | 63 | export class Project extends vscode.TreeItem { 64 | 65 | constructor( 66 | public readonly label: string, 67 | public readonly icon :string, 68 | public readonly cloneurl: string, 69 | public readonly description?: string, 70 | public readonly collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.None, 71 | public readonly command?: vscode.Command 72 | ) { 73 | super(label, collapsibleState); 74 | this.id = "project_"+label; 75 | this.tooltip = `${this.description}`; 76 | this.description = this.description; 77 | this.iconPath = icon; 78 | this.cloneurl = cloneurl; 79 | } 80 | 81 | 82 | contextValue = this.cloneurl; 83 | } 84 | export class GitOperation { 85 | scopes = ['read:user', 'user:email', 'repo', 'workflow']; 86 | auth:vscode.AuthenticationSession|null=null; 87 | 88 | async init(){ 89 | this.auth = await vscode.authentication.getSession('github',this.scopes,{createIfNone:true}); 90 | } 91 | getOwnerRepo(pushUrl:string){ 92 | let ownerRepo = pushUrl.replace('https://github.com/','').replace('.git','').split('/'); 93 | return {owner:ownerRepo[0],repo:ownerRepo[1]}; 94 | } 95 | async publishProject(pushUrl:string,unpublish=false){ 96 | let {owner,repo} = this.getOwnerRepo(pushUrl); 97 | let names = [ 98 | TOPIC, 99 | ]; 100 | if(unpublish){ 101 | names = []; 102 | } 103 | await request('PUT /repos/{owner}/{repo}/topics', { 104 | owner, 105 | repo, 106 | names:names, 107 | headers: await this.getHeader() 108 | }).catch(this.onError); 109 | 110 | 111 | } 112 | async checkPublisStatus(pushUrl:string) { 113 | let {owner,repo} = this.getOwnerRepo(pushUrl); 114 | let resp = await request('GET /repos/{owner}/{repo}/topics', { 115 | owner, 116 | repo, 117 | headers: await this.getHeader() 118 | }).catch(this.onError); 119 | 120 | if(resp?.data.names.includes(TOPIC)){ 121 | return true; 122 | }else{ 123 | return false; 124 | } 125 | } 126 | 127 | async createFork(cloneurl:string,name:string) { 128 | 129 | let {owner,repo} = this.getOwnerRepo(cloneurl); 130 | let resp:any = await request('POST /repos/{owner}/{repo}/forks', { 131 | owner, 132 | repo, 133 | name, 134 | default_branch_only: true, 135 | headers: await this.getHeader() 136 | }).catch(this.onError); 137 | return resp; 138 | } 139 | async getArchiveZip(cloneurl:string,localPath:vscode.Uri){ 140 | let {owner,repo} = this.getOwnerRepo(cloneurl); 141 | let resp:any = await request('GET /repos/{owner}/{repo}/zipball/{ref}', { 142 | owner, 143 | repo, 144 | ref: 'master', 145 | headers: await this.getHeader() 146 | }); 147 | let zip = await JSZIP.loadAsync(resp.data); 148 | // console.log(zip); 149 | zip.forEach(async (fileName:any)=>{ 150 | let filepathname = fileName.split("/").slice(1).join("/"); 151 | if(!filepathname){return;}; 152 | let localPath_ = vscode.Uri.joinPath(localPath,filepathname); 153 | if(zip.file(fileName)===null){ 154 | vscode.workspace.fs.createDirectory(localPath_); 155 | return; 156 | } 157 | vscode.workspace.fs.writeFile(localPath_, Buffer.from(await zip.file(fileName).async('arraybuffer'))); 158 | }); 159 | // const folderName = path.basename(localPath.path); 160 | vscode.commands.executeCommand('vscode.openFolder',localPath); 161 | // vscode.workspace.updateWorkspaceFolders(0, null, { uri:localPath, name:folderName}); 162 | } 163 | private async getHeader(){ 164 | if(!this.auth){ 165 | await this.init(); 166 | } 167 | if(!this.auth){return;} 168 | return { 169 | authorization: "token "+ this.auth.accessToken, 170 | 'X-GitHub-Api-Version': '2022-11-28' 171 | }; 172 | } 173 | onError(err:any){ 174 | console.log(err); 175 | // vscode.window.showErrorMessage("could"); 176 | } 177 | } 178 | 179 | 180 | 181 | import { spawn } from 'child_process'; 182 | 183 | export function cloneAndOpenRepo(repoUrl:string,uri:vscode.Uri): void { 184 | 185 | 186 | // Clone the repository 187 | const gitClone = spawn('git', ['clone', repoUrl, uri.fsPath]); 188 | 189 | gitClone.on('exit', () => { 190 | // Open the cloned repository in VS Code 191 | // const workspaceFolder = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0; 192 | // const folderName = path.basename(uri.path); 193 | vscode.commands.executeCommand('vscode.openFolder',uri); 194 | }); 195 | } 196 | 197 | 198 | -------------------------------------------------------------------------------- /snippets/uasyncio.json: -------------------------------------------------------------------------------- 1 | { 2 | "create_task(coro)": { 3 | "prefix": "create_task", 4 | "body": "uasyncio.create_task(${1:coro})", 5 | "description" : "Create a new task from the given coroutine and schedule it to run.Returns the corresponding Task object." 6 | }, 7 | "current_task()": { 8 | "prefix": "current_task", 9 | "body": "uasyncio.current_task()", 10 | "description" : "Return the Task object associated with the currently running task." 11 | }, 12 | "run(coro)": { 13 | "prefix": "run", 14 | "body": "uasyncio.run(${1:coro})", 15 | "description" : "Create a new task from the given coroutine and run it until it completes.Returns the value returned by coro." 16 | }, 17 | "sleep(t)": { 18 | "prefix": "sleep", 19 | "body": "uasyncio.sleep(${1:t})", 20 | "description" : "The inverse of now(). Converts a dictionary provided into an epoch timestamp. The returned epoch value will be referenced from midnight on the 1st of January 1970." 21 | }, 22 | "sleep_ms(t)": { 23 | "prefix": "sleep_ms", 24 | "body": "uasyncio.sleep_ms(${1:t})", 25 | "description" : "Sleep for t milliseconds.This is a coroutine, and a MicroPython extension." 26 | }, 27 | "wait_for(awaitable, timeout)": { 28 | "prefix": "wait_for", 29 | "body": "uasyncio.wait_for(${1:awaitable} , ${2:timeout})", 30 | "description" : "Wait for the awaitable to complete, but cancel it if it takes longer than timeout seconds. If awaitable is not a task then a task will be created from it.If a timeout occurs, it cancels the task and raises uasyncio.TimeoutError: this should be trapped by the caller. The task receives uasyncio.CancelledError which may be ignored or trapped using try...except or try...finally to run cleanup code.Returns the return value of awaitable." 31 | }, 32 | "wait_for_ms(awaitable, timeout)": { 33 | "prefix": "wait_for_ms", 34 | "body": "uasyncio.wait_for_ms(${1:awaitable},${2:timeout})", 35 | "description" : "Similar to wait_for but timeout is an integer in milliseconds. This is a coroutine, and a MicroPython extension." 36 | }, 37 | "gather(*awaitables, return_exceptions)": { 38 | "prefix": "gather", 39 | "body": "uasyncio.gather(${1:awaitables}, ${2:return_exceptions})", 40 | "description" : "Run all awaitables concurrently. Any awaitables that are not tasks are promoted to tasks.Returns a list of return values of all awaitables." 41 | }, 42 | "cancel()": { 43 | "prefix": "cancel", 44 | "body": "Task.cancel()", 45 | "description" : "Cancel the task by injecting uasyncio.CancelledError into it. The task may ignore this exception. Cleanup code may be run by trapping it, or via try ... finally." 46 | }, 47 | 48 | "is_set()": { 49 | "prefix": "is_set", 50 | "body": "Event.is_set()", 51 | "description" : "Returns True if the event is set, False otherwise." 52 | }, 53 | 54 | "set()": { 55 | "prefix": "set", 56 | "body": "Event.set()", 57 | "description" : "Set the event. Any tasks waiting on the event will be scheduled to run. Note: This must be called from within a task. It is not safe to call this from an IRQ, scheduler callback, or other thread. See ThreadSafeFlag." 58 | }, 59 | 60 | "clear()": { 61 | "prefix": "clear", 62 | "body": "Event.clear()", 63 | "description" : "Clear the event." 64 | }, 65 | "wait()": { 66 | "prefix": "wait", 67 | "body": "Event.wait()", 68 | "description" : "Wait for the event to be set. If the event is already set then it returns immediately.This is a coroutine." 69 | }, 70 | "locked()": { 71 | "prefix": "locked", 72 | "body": "Lock.locked()", 73 | "description" : "Returns True if the lock is locked, otherwise False." 74 | }, 75 | "acquire()": { 76 | "prefix": "acquire", 77 | "body": "Lock.acquire()", 78 | "description" : "Wait for the lock to be in the unlocked state and then lock it in an atomic way. Only one task can acquire the lock at any one time." 79 | }, 80 | "release()": { 81 | "prefix": "release", 82 | "body": "Lock.release()", 83 | "description" : "Release the lock. If any tasks are waiting on the lock then the next one in the queue is scheduled to run and the lock remains locked. Otherwise, no tasks are waiting an the lock becomes unlocked." 84 | }, 85 | "open_connection(host,port)": { 86 | "prefix": "open_connection", 87 | "body": "uasyncio.open_connection(${1:host}, ${2:port})", 88 | "description" : "Returns a pair of streams: a reader and a writer stream. Will raise a socket-specific OSError if the host could not be resolved or if the connection could not be made." 89 | }, 90 | "start_server(callback, host, port, backlog)": { 91 | "prefix": "start_server", 92 | "body": "uasyncio.start_server(${1:callback}, ${2:host},${3:port},${4:backlog})", 93 | "description" : "Start a TCP server on the given host and port. The callback will be called with incoming, accepted connections, and be passed 2 arguments: reader and writer streams for the connection.Returns a Server object." 94 | }, 95 | "get_extra_info(v)": { 96 | "prefix": "get_extra_info", 97 | "body": "Stream.get_extra_info(${1:v})", 98 | "description" : "Start a TCP server on the given host and port. The callback will be called with incoming, accepted connections, and be passed 2 arguments: reader and writer streams for the connection.Returns a Server object." 99 | }, 100 | "close()": { 101 | "prefix": "close", 102 | "body": "Stream.close()", 103 | "description" : "Close the stream." 104 | }, 105 | "wait_closed()": { 106 | "prefix": "wait_closed", 107 | "body": "Lock.wait_closed()", 108 | "description" : "WWait for the stream to close. This is a coroutine." 109 | }, 110 | "read(n)": { 111 | "prefix": "read", 112 | "body": "Stream.read(${1:n})", 113 | "description" : "Release the lock. If any tasks are waiting on the lock then the next one in the queue is scheduled to run and the lock remains locked. Otherwise, no tasks are waiting an the lock becomes unlocked." 114 | }, 115 | "readinto(buf)": { 116 | "prefix": "readinto", 117 | "body": "Stream.readinto(${1:buf})", 118 | "description" : "Read up to n bytes into buf with n being equal to the length of buf.Return the number of bytes read into buf.This is a coroutine, and a MicroPython extension." 119 | }, 120 | "readexactly(n)": { 121 | "prefix": "readexactly", 122 | "body": "Stream.readexactly(${1:n})", 123 | "description" : "Read exactly n bytes and return them as a bytes object.Raises an EOFError exception if the stream ends before reading n bytes.This is a coroutine." 124 | }, 125 | "readline()": { 126 | "prefix": "readline", 127 | "body": "Stream.readline()", 128 | "description" : "Read a line and return it.This is a coroutine." 129 | }, 130 | "write(buf)": { 131 | "prefix": "write", 132 | "body": "Stream.write(${1:buf})", 133 | "description" : "Accumulated buf to the output buffer. The data is only flushed when Stream.drain is called. It is recommended to call Stream.drain immediately after calling this function." 134 | }, 135 | "drain()": { 136 | "prefix": "drain", 137 | "body": "Stream.drain()", 138 | "description" : "Drain (write) all buffered output data out to the stream.This is a coroutine." 139 | }, 140 | "Server.close()": { 141 | "prefix": "close", 142 | "body": "Server.close()", 143 | "description" : "Close the server." 144 | }, 145 | "Server.wait_closed()": { 146 | "prefix": "wait_closed", 147 | "body": "Server.wait_closed()", 148 | "description" : "Wait for the server to close.This is a coroutine." 149 | }, 150 | "get_event_loop()": { 151 | "prefix": "get_event_loop", 152 | "body": "uasyncio.get_event_loop()", 153 | "description" : "Return the event loop used to schedule and run tasks. See Loop. " 154 | }, 155 | "new_event_loop()": { 156 | "prefix": "new_event_loop", 157 | "body": "uasyncio.new_event_loop()", 158 | "description" : "Reset the event loop and return it." 159 | } 160 | } 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /src/nordicdfu.ts: -------------------------------------------------------------------------------- 1 | // Details on how this works: 2 | // https://infocenter.nordicsemi.com/topic/sdk_nrf5_v17.1.0/lib_dfu_transport_ble.html 3 | 4 | import { transmitNordicDfuControlData, transmitNordicDfuPacketData } from "./bluetooth"; 5 | import { outputChannel } from "./extension"; 6 | import { micropythonGit } from "./update"; 7 | import { reportUpdatePercentage } from "./repl"; 8 | import * as vscode from 'vscode'; 9 | import { request } from "@octokit/request"; 10 | let JSZIP = require("jszip"); 11 | let fetch = require('node-fetch'); 12 | let controlResponseCallback:any; 13 | 14 | export async function startNordicDFU(device:any="frame") { 15 | try { 16 | 17 | outputChannel.appendLine('Entering nRF52 DFU'); 18 | 19 | let files:any = await obtainFiles(device); 20 | 21 | await transferFile(files.dat, 'init'); 22 | await transferFile(files.bin, 'image'); 23 | await new Promise(r => setTimeout(r, 500)); 24 | outputChannel.appendLine('Leaving nRF52 DFU'); 25 | return Promise.resolve("completed"); 26 | } catch (error) { 27 | return Promise.resolve(error); 28 | } 29 | } 30 | 31 | export async function nordicDfuSendControl(bytes:any) { 32 | 33 | outputChannel.appendLine('DFU control ⬆️: ' + bytes); 34 | 35 | transmitNordicDfuControlData(bytes); 36 | 37 | // Return a promise which calls a function that'll eventually run when the 38 | // response handler calls the function associated with controlResponseCallback 39 | return new Promise(resolve => { 40 | controlResponseCallback = function (responseBytes:Uint8Array) { 41 | outputChannel.appendLine('DFU control ⬇️: ' + new Uint8Array(responseBytes.buffer)); 42 | resolve(responseBytes); 43 | }; 44 | setTimeout(() => { 45 | resolve(""); 46 | }, 1000); 47 | }); 48 | } 49 | 50 | export function nordicDfuHandleControlResponse(bytes:Uint8Array) { 51 | controlResponseCallback(bytes); 52 | } 53 | 54 | export async function nordicDfuSendPacket(bytes:Uint8Array) { 55 | const payload = new Uint8Array(bytes); 56 | outputChannel.appendLine('DFU packet ⬆️: ' + payload); 57 | await transmitNordicDfuPacketData(payload); 58 | // Wait a little while as this is a write without response 59 | await new Promise(r => setTimeout(r, 10)); 60 | } 61 | 62 | async function obtainFiles(device:string) { 63 | 64 | if (!micropythonGit.owner || !micropythonGit.repo) { 65 | // TODO 66 | micropythonGit.owner = 'brilliantlabsAR'; 67 | micropythonGit.repo = 'monocle-micropython'; 68 | } 69 | if(device==="frame"){ 70 | micropythonGit.owner = 'brilliantlabsAR'; 71 | micropythonGit.repo = 'frame-codebase'; 72 | } 73 | //TODO take from git repo 74 | 75 | outputChannel.appendLine("Downloading latest release from: github.com/" + 76 | micropythonGit.owner + "/" + micropythonGit.repo); 77 | let headers = { 78 | // authorization: "token "+ (await vscode.authentication.getSession('github',['read:user', 'user:email', 'repo', 'workflow'],{createIfNone:true})).accessToken, 79 | 'X-GitHub-Api-Version': '2022-11-28' 80 | }; 81 | let response:any = await request("GET /repos/{owner}/{repo}/releases/latest", { 82 | owner: micropythonGit.owner, 83 | repo: micropythonGit.repo, 84 | // headers 85 | }); 86 | 87 | let assetId; 88 | response.data.assets.forEach((item:any, index:Number) => { 89 | if (item.content_type === 'application/zip') { 90 | assetId = item.id; 91 | } 92 | }); 93 | 94 | response = await request("GET /repos/{owner}/{repo}/releases/assets/{assetId}", { 95 | owner: micropythonGit.owner, 96 | repo: micropythonGit.repo, 97 | assetId: assetId, 98 | // headers 99 | }); 100 | 101 | // Annoyingly we have to fetch the data via a cors proxy 102 | 103 | let download = await fetch('https://api.brilliant.xyz/firmware?url=' + response.data.browser_download_url); 104 | let blob = await download.blob(); 105 | let buffer = await blob.arrayBuffer(); 106 | 107 | let zip = await JSZIP.loadAsync(buffer); 108 | 109 | let manifest = await zip.file('manifest.json').async('string'); 110 | let dat = await zip.file('application.dat').async('arraybuffer'); 111 | let bin = await zip.file('application.bin').async('arraybuffer'); 112 | 113 | return { manifest, dat, bin }; 114 | } 115 | 116 | async function transferFile(file:any, type:any) { 117 | 118 | let response:any; 119 | 120 | // Select command 121 | switch (type) { 122 | case 'init': 123 | outputChannel.appendLine('Transferring init file'); 124 | response = await nordicDfuSendControl([0x06, 0x01]); 125 | break; 126 | case 'image': 127 | outputChannel.appendLine('Transferring image file'); 128 | response = await nordicDfuSendControl([0x06, 0x02]); 129 | break; 130 | 131 | default: 132 | break; 133 | } 134 | 135 | const fileSize = file.byteLength; 136 | 137 | outputChannel.appendLine("fileSize: " + fileSize); 138 | 139 | const maxSize = response.getUint32(3, true); 140 | const offset = response.getUint32(7, true); 141 | const crc = response.getUint32(11, true); 142 | 143 | outputChannel.appendLine("maxSize: " + maxSize + ", offset: " + offset + ", crc: " + crc); 144 | 145 | let chunks = Math.ceil(fileSize / maxSize); 146 | outputChannel.appendLine("Sending file as " + chunks + " chunks"); 147 | 148 | let fileOffset = 0; 149 | for (let chk = 0; chk < chunks; chk++) { 150 | 151 | let chunkSize = Math.min(fileSize, maxSize); 152 | 153 | // The last chunk could be smaller 154 | if (chk === chunks - 1 && fileSize % maxSize) { 155 | chunkSize = fileSize % maxSize; 156 | } 157 | 158 | const chunkCrc = crc32(new Uint8Array(file) 159 | .slice(0, fileOffset + chunkSize)); 160 | 161 | outputChannel.appendLine( 162 | "chunk " + chk + 163 | ", fileOffset: " + fileOffset + 164 | ", chunkSize: " + chunkSize + ", chunkCrc: " + chunkCrc); 165 | 166 | // Create command with size 167 | let chunkSizeAsBytes = 168 | [chunkSize & 0xFF, chunkSize >> 8 & 0xFF, 169 | chunkSize >> 16 & 0xff, chunkSize >> 24 & 0xff]; 170 | 171 | if (type === 'init') { 172 | await nordicDfuSendControl([0x01, 0x01].concat(chunkSizeAsBytes)); 173 | } 174 | if (type === 'image') { 175 | await nordicDfuSendControl([0x01, 0x02].concat(chunkSizeAsBytes)); 176 | } 177 | 178 | // Send packets as maximum 100 byte payloads (assume max 100 byte MTU) 179 | const packets = Math.ceil(chunkSize / 100); 180 | for (let pkt = 0; pkt < packets; pkt++) { 181 | 182 | // The last packet could be smaller 183 | let packetLength = 100; 184 | if (pkt === packets - 1 && chunkSize % 100) { 185 | packetLength = chunkSize % 100; 186 | } 187 | 188 | const fileSlice = file.slice(fileOffset, fileOffset + packetLength); 189 | fileOffset += fileSlice.byteLength; 190 | 191 | await nordicDfuSendPacket(fileSlice); 192 | // .report({ increment: Math.round((100 / fileSize) * fileOffset) }); 193 | if(type==="image"){ 194 | reportUpdatePercentage((100 / fileSize) * fileOffset); 195 | 196 | } 197 | } 198 | 199 | // Calculate CRC 200 | response = await nordicDfuSendControl([0x03]); 201 | let returnedOffset = response.getUint32(3, true); 202 | let returnedCrc = response.getUint32(7, true); 203 | 204 | outputChannel.appendLine("returnedOffset: " + returnedOffset + ", returnedCrc: " + returnedCrc); 205 | 206 | if (returnedCrc !== chunkCrc) { 207 | outputChannel.appendLine('CRC mismatch after sending this chunk. Expected: ' + chunkCrc); 208 | // vscode.window.showErrorMessage('CRC mismatch after sending this chunk. Expected: ' + chunkCrc); 209 | chk--; 210 | continue; 211 | // return Promise.reject(''); 212 | } 213 | 214 | // Execute command 215 | await nordicDfuSendControl([0x04]); 216 | } 217 | } 218 | 219 | 220 | function crc32(r:any) { 221 | for (var a, o = [], c = 0; c < 256; c++) { 222 | a = c; 223 | for (var f = 0; f < 8; f++) { 224 | a = 1 & a ? 3988292384 ^ a >>> 1 : a >>> 1; 225 | } 226 | o[c] = a; 227 | } 228 | for (var n = -1, t = 0; t < r.length; t++) { 229 | n = n >>> 8 ^ o[255 & (n ^ r[t])]; 230 | } 231 | return (-1 ^ n) >>> 0; 232 | } 233 | 234 | -------------------------------------------------------------------------------- /src/update.ts: -------------------------------------------------------------------------------- 1 | import { 2 | replSend, 3 | replRawMode, 4 | ensureConnected, 5 | reportUpdatePercentage, 6 | frameSend, 7 | enterRawReplInternal, 8 | exitRawReplInternal 9 | } from "./repl"; 10 | import { outputChannel } from "./extension"; 11 | import { request } from "@octokit/request"; 12 | import { disconnect, isConnected,convertToLittleEndian } from "./bluetooth"; 13 | let fetch = require("node-fetch"); 14 | 15 | export let micropythonGit: any = {}; 16 | export let fpgaGit: any = {}; 17 | 18 | // declare a type for update details with optional properties 19 | type UpdateDetails = { 20 | firmwareVersion?: string; 21 | macAddress?: string; 22 | fpgaVersion?: string; 23 | firmwareUpdate?: string; 24 | fpgaUpdate?: string; 25 | message?: string; 26 | }; 27 | export async function checkForUpdates() { 28 | try { 29 | await replRawMode(true); 30 | // Short delay to throw away bluetooth data received upon connection 31 | let message = await getUpdateInfo(); 32 | await replRawMode(false); 33 | return Promise.resolve(message); 34 | } catch (error: any) { 35 | outputChannel.appendLine(error); 36 | await replRawMode(false); 37 | } 38 | } 39 | export async function checkForFrameUpdates() { 40 | try { 41 | let updateDetails:UpdateDetails = {}; 42 | let response:any = await frameSend("print(frame.FIRMWARE_VERSION);",2); 43 | let macAddress:any = await frameSend("print(frame.bluetooth.address());",2); 44 | updateDetails.firmwareVersion = response||'Unknown'; 45 | updateDetails.macAddress = macAddress||'Unknown'; 46 | updateDetails.firmwareUpdate = 'Unknown'; 47 | micropythonGit.owner = "brilliantlabsAR"; 48 | micropythonGit.repo = "frame-codebase"; 49 | let getTag = await request("GET /repos/{owner}/{repo}/releases/latest", { 50 | owner: micropythonGit.owner, 51 | repo: micropythonGit.repo, 52 | }); 53 | let latestVersion = getTag.data.tag_name; 54 | 55 | if (response !== latestVersion) { 56 | updateDetails.firmwareUpdate= latestVersion; 57 | updateDetails.message = `New firmware ([${latestVersion}](${getTag.url})) update available, Do you want to update?`; 58 | } 59 | 60 | return Promise.resolve(updateDetails); 61 | } catch (error: any) { 62 | outputChannel.appendLine(error); 63 | } 64 | } 65 | async function getUpdateInfo() { 66 | // Check nRF firmware 67 | let updateDetails:UpdateDetails = {}; 68 | let getTag:any; 69 | let response: any = await replSend("import device;print(device.VERSION);print('MACADDR#'+device.mac_address())"); 70 | if (response && response.includes("Error")) { 71 | updateDetails.message= "Could not detect the firmware version. You may have to update" + 72 | " manually. Try typing: import update;update.micropython()"; 73 | updateDetails.firmwareVersion = "Unknown"; 74 | return updateDetails; 75 | }else if(response){ 76 | let currentVersion = response.substring( 77 | response.indexOf("v"), 78 | response.indexOf("MACADDR#")-2 79 | ); 80 | let macAddress = response.substring( 81 | response.indexOf("MACADDR#")+8, 82 | response.lastIndexOf(">")-4 83 | ); 84 | updateDetails.firmwareVersion = currentVersion; 85 | updateDetails.macAddress = convertToLittleEndian(macAddress.trim()).toUpperCase(); 86 | response = await replSend("print(device.GIT_REPO);del(device)"); 87 | if (response.includes("Error")) { 88 | updateDetails.message = "Could not detect the device. Current version is: " + 89 | currentVersion + 90 | ". You may have to update manually. Try typing: " + 91 | "import update;update.micropython()"; 92 | 93 | }else{ 94 | let gitRepoLink = response.substring( 95 | response.indexOf("https"), 96 | response.lastIndexOf("\r\n") 97 | ); 98 | 99 | micropythonGit.owner = gitRepoLink.split("/")[3]; 100 | micropythonGit.repo = gitRepoLink.split("/")[4]; 101 | getTag = await request("GET /repos/{owner}/{repo}/releases/latest", { 102 | owner: micropythonGit.owner, 103 | repo: micropythonGit.repo, 104 | }); 105 | let latestVersion = getTag.data.tag_name; 106 | 107 | if (currentVersion !== latestVersion) { 108 | updateDetails.firmwareUpdate= latestVersion; 109 | updateDetails.message = `New firmware ([${latestVersion}](${getTag.url})) update available, Do you want to update?`; 110 | } 111 | } 112 | return updateDetails; 113 | } 114 | 115 | 116 | // Check FPGA image 117 | fpgaGit.owner = micropythonGit.owner; 118 | fpgaGit.repo = micropythonGit.repo.replace("micropython", "fpga"); 119 | getTag = await request("GET /repos/{owner}/{repo}/releases/latest", { 120 | owner: fpgaGit.owner, 121 | repo: fpgaGit.repo, 122 | }); 123 | let latestVersion = getTag.data.tag_name; 124 | 125 | response = await replSend( 126 | "import fpga;" + "print(fpga.read(2,12));" + "del(fpga)" 127 | ); 128 | if (response && response.includes("Error")) { 129 | updateDetails.message ="Could not detect the FPGA image, check manually. "; 130 | updateDetails.fpgaVersion ="Unknown"; 131 | }else if(response){ 132 | let currentVersion = response.substring( 133 | response.indexOf("OKb"), 134 | response.lastIndexOf("\r\n") 135 | ); 136 | if(currentVersion.includes('\\x00\\x00\\x00\\x00')){ 137 | updateDetails.fpgaVersion = "Uknown"; 138 | }else{ 139 | updateDetails.fpgaVersion = currentVersion; 140 | } 141 | 142 | if (!response.includes(latestVersion)) { 143 | updateDetails.fpgaUpdate = latestVersion; 144 | updateDetails.message = `New FPGA image ([${latestVersion}](${getTag.url})) available, Do you want to update?`; 145 | } 146 | updateDetails.message = ''; 147 | } 148 | return updateDetails; 149 | 150 | } 151 | 152 | export async function startFirmwareUpdate(device:string="monocle") { 153 | if (device==="frame"){ 154 | await enterRawReplInternal(); 155 | await frameSend("frame.display.clear()",0.1); 156 | await frameSend("frame.display.text('Updating firmware...', 120, 180, {align='MIDDLE_LEFT'});\r",0.1); 157 | await frameSend("frame.display.show()",0.1); 158 | await frameSend("frame.update()",0.1); 159 | await exitRawReplInternal(); 160 | }else{ 161 | 162 | await replRawMode(true); 163 | await replSend( 164 | "import display as d;" + 165 | "m= d.Text('Updating firmware...',120,180,0xffffff,justify=d.MIDDLE_LEFT);" + 166 | "d.show(m);" + 167 | "import update;" + 168 | "update.micropython()" 169 | ); 170 | await replRawMode(false); 171 | 172 | } 173 | // try to connect after 1 sec 174 | await disconnect(); 175 | await ensureConnected(); 176 | } 177 | 178 | 179 | export async function updateFPGA(file: ArrayBuffer) { 180 | outputChannel.appendLine("Starting FPGA update"); 181 | outputChannel.appendLine( 182 | "Converting " + file.byteLength + " bytes of file to base64" 183 | ); 184 | let asciiFile = Buffer.from(file).toString("base64"); 185 | await replSend("import ubinascii, update, device, bluetooth"); 186 | let response: any = await replSend("print(bluetooth.max_length())"); 187 | const maxMtu = parseInt(response.match(/\d/g).join(""), 10); 188 | 189 | // 45 is the string length of the update string. Calculates base64 chunk length 190 | let chunksize = Math.floor(Math.floor((maxMtu - 45) / 3) / 4) * 4 * 3; 191 | let chunks = Math.ceil(asciiFile.length / chunksize); 192 | outputChannel.appendLine( 193 | "Chunk size = " + chunksize + ". Total chunks = " + chunks 194 | ); 195 | 196 | await replSend("update.Fpga.erase()"); 197 | for (let chk = 0; chk < chunks; chk++) { 198 | response = await replSend( 199 | "update.Fpga.write(ubinascii.a2b_base64(b'" + 200 | asciiFile.slice(chk * chunksize, chk * chunksize + chunksize) + 201 | "'))" 202 | ); 203 | 204 | if (response && response.includes("Error")) { 205 | outputChannel.appendLine("Retrying this chunk"); 206 | chk--; 207 | } else if (response === null) { 208 | return Promise.reject("Not responding"); 209 | } 210 | reportUpdatePercentage((100 / asciiFile.length) * chk * chunksize); 211 | } 212 | 213 | await replSend("update.Fpga.write(b'done')"); 214 | await replSend("device.reset()"); 215 | outputChannel.appendLine("Completed FPGA update. Resetting"); 216 | } 217 | 218 | export async function downloadLatestFpgaImage() { 219 | let chiprev = "revC"; 220 | 221 | // let chipresponse:any = await replSend("import fpga;print(fpga.version())"); 222 | // if (chipresponse && chipresponse.includes("revB")) { 223 | // chiprev = "revB"; 224 | // } 225 | 226 | outputChannel.appendLine( 227 | "Downloading latest release from: github.com/" + 228 | fpgaGit.owner + 229 | "/" + 230 | fpgaGit.repo 231 | ); 232 | 233 | let response: any = await request( 234 | "GET /repos/{owner}/{repo}/releases/latest", 235 | { 236 | owner: fpgaGit.owner, 237 | repo: fpgaGit.repo, 238 | } 239 | ); 240 | 241 | let assetId; 242 | response.data.assets.forEach((item: any, index: number) => { 243 | // if (item.name.includes('revC.bin') && chiprev === "revC") { 244 | if (item.name.includes('.bin')) { 245 | assetId = item.id; 246 | } 247 | // } 248 | 249 | // if (item.name.includes('revB.bin') && chiprev === "revB") { 250 | // assetId = item.id; 251 | // } 252 | }); 253 | 254 | response = await request( 255 | "GET /repos/{owner}/{repo}/releases/assets/{assetId}", 256 | { 257 | owner: fpgaGit.owner, 258 | repo: fpgaGit.repo, 259 | assetId: assetId, 260 | } 261 | ); 262 | 263 | // Annoyingly we have to fetch the data via a cors proxy 264 | let download = await fetch( 265 | "https://api.brilliant.xyz/firmware?url=" + 266 | response.data.browser_download_url 267 | ); 268 | let blob = await download.blob(); 269 | let bin = await blob.arrayBuffer(); 270 | 271 | return bin; 272 | } 273 | -------------------------------------------------------------------------------- /snippets/display.json: -------------------------------------------------------------------------------- 1 | { 2 | "Text(string, x, y, color, justify)": { 3 | "prefix": "Text", 4 | "body": "display.Text(${1:string}, ${2:x}, ${3:y}, ${4:color}, justify=display.${5|TOP_LEFT,MIDDLE_LEFT,BOTTOM_LEFT,TOP_CENTER,BOTTOM_CENTER,TOP_RIGHT,MIDDLE_CENTER,MIDDLE_RIGHT,BOTTOM_RIGHT|})", 5 | "description": "Creates a text object at the coordinate x, y which can be passed to display.show(). string can be any string, and color can be any color from the available color palette. If the justify parameter is given, the text will be justified accordingly from the x, y coordinate." 6 | }, 7 | "Rectangle(x1, y1, x2, y2, color)": { 8 | "prefix": "Rectangle", 9 | "body": "display.Rectangle(${1:x1}, ${2:y1}, ${3:x2}, ${4:y2},${5:color})", 10 | "description": "Creates a rectangle object which can be passed to display.show(). x1, y1 and x2, y2 define each corner of the rectangle. color can be any color from the available color palette." 11 | }, 12 | "Line(x1, y1, x2, y2, color, thickness=1)": { 13 | "prefix": "Line", 14 | "body": "display.Line(${1:x1}, ${2:y1}, ${3:x2}, ${4:y2}, ${5:color},thickness=${6:1})", 15 | "description": "Creates a line object from x1, y1 to x2, y2 which can be passed to display.show(). color can be any color from the available color palette, and thickness can optionally be provided to override the default line thickness in pixels." 16 | }, 17 | "Polygon([x1, y1, ... xn, yn], color)": { 18 | "prefix": "Polygon", 19 | "body": "display.Polygon([${1:x1}, ${2:y1},${3:x2},${4:y2}], ${5:color})", 20 | "description": "Creates a polygon object which can be passed to display.show(). The first parameter should be a list of coordinates, and color can be any color from the available color palette. Polygons are always closed shapes, therefore if the last coordinate does not close the shape, it will be closed automatically." 21 | }, 22 | "Polyline([x1,y1,...xn,yn],color,thickness=1)": { 23 | "prefix": "Polyline", 24 | "body": "display.Polyline([${1:x1}, ${2:y1}, ${3:x2},${4:y2}], ${4:color}],${5:color},thickness=${6:1})", 25 | "description": "Similar to the Polygon object, Polyline creates a shape based on a list of coordinates. Unlike Polygon, Polyline does not need to be a closed shape. color can be any color from the available color palette, and thickness can optionally be provided to override the default line thickness in pixels." 26 | }, 27 | "show(object_1,object_2...)": { 28 | "prefix": "show", 29 | "body": "display.show(${1:object_1},${2:object_2})", 30 | "description": "Prints to the display. The passed arguments can be any number of Text, Line, Rectangle, Polygon, or Polyline objects, or any number of lists containing such objects. Objects are layered front to back, i.e. object_1 is shown on top of object_n." 31 | }, 32 | "move(x, y)": { 33 | "prefix": "move", 34 | "body": "display.move(${1:x}, ${2:y})", 35 | "description": "move() can be called as a class method on any printable object to translate its position. x and y will move the object relative to its current position." 36 | }, 37 | "move([objects], x, y)": { 38 | "prefix": "move", 39 | "body": "display.move([${1:objects}], ${2:x}, ${3:y})", 40 | "description": "Additionally, move() can be called as a standard function to move a list of objects together. This is useful for grouping printable objects together and moving them as layers." 41 | }, 42 | "color(color)": { 43 | "prefix": "color", 44 | "body": "display.color(${1:color})", 45 | "description": "color() can be called as a class method on any printable object to change its color. color can be any color from the available color palette." 46 | }, 47 | "color([objects], color)": { 48 | "prefix": "color", 49 | "body": "display.color([${1:objects}], ${2:color})", 50 | "description": "Additionally, color() can be called as a standard function to change the color on a whole list of objects. color can be any color from the available color palette." 51 | }, 52 | "brightness(level)": { 53 | "prefix": "brightness", 54 | "body": "display.brightness(${1|1,2,3,4|})", 55 | "description": "Sets the display’s brightness. level can be 0 (dimmest), 1, 2, 3, or 4 (brightest). Level 3 is the default." 56 | }, 57 | "CLEAR": { 58 | "prefix": "CLEAR", 59 | "body": "display.CLEAR", 60 | "description": "If using the default color palette, this constant indexes the color #000000. Note, color indexes may be overridden by the user." 61 | }, 62 | "RED": { 63 | "prefix": "RED", 64 | "body": "display.RED", 65 | "description": "If using the default color palette, this constant indexes the color #ad2323. Note, color indexes may be overridden by the user." 66 | }, 67 | "GREEN": { 68 | "prefix": "GREEN", 69 | "body": "display.GREEN", 70 | "description": "If using the default color palette, this constant indexes the color #1d6914. Note, color indexes may be overridden by the user." 71 | }, 72 | "BLUE": { 73 | "prefix": "BLUE", 74 | "body": "display.BLUE", 75 | "description": "If using the default color palette, this constant indexes the color #2a4bd7. Note, color indexes may be overridden by the user." 76 | }, 77 | "CYAN": { 78 | "prefix": "CYAN", 79 | "body": "display.CYAN", 80 | "description": "If using the default color palette, this constant indexes the color #29d0d0. Note, color indexes may be overridden by the user." 81 | }, 82 | "MAGENTA": { 83 | "prefix": "MAGENTA", 84 | "body": "display.MAGENTA", 85 | "description": "If using the default color palette, this constant indexes the color #8126c0. Note, color indexes may be overridden by the user." 86 | }, 87 | "YELLOW": { 88 | "prefix": "YELLOW", 89 | "body": "display.YELLOW", 90 | "description": "If using the default color palette, this constant indexes the color #ffee33. Note, color indexes may be overridden by the user." 91 | }, 92 | "WHITE": { 93 | "prefix": "WHITE", 94 | "body": "display.WHITE", 95 | "description": "If using the default color palette, this constant indexes the color #ffffff. Note, color indexes may be overridden by the user." 96 | }, 97 | "GRAY1": { 98 | "prefix": "GRAY1", 99 | "body": "display.GRAY1", 100 | "description": "If using the default color palette, this constant indexes the color #1c1c1c. Note, color indexes may be overridden by the user." 101 | }, 102 | "GRAY2": { 103 | "prefix": "GRAY2", 104 | "body": "display.GRAY2", 105 | "description": "If using the default color palette, this constant indexes the color #383838. Note, color indexes may be overridden by the user." 106 | }, 107 | "GRAY3": { 108 | "prefix": "GRAY3", 109 | "body": "display.GRAY3", 110 | "description": "If using the default color palette, this constant indexes the color #555555. Note, color indexes may be overridden by the user." 111 | }, 112 | "GRAY4": { 113 | "prefix": "GRAY4", 114 | "body": "display.GRAY4", 115 | "description": "If using the default color palette, this constant indexes the color #717171. Note, color indexes may be overridden by the user." 116 | }, 117 | "GRAY5": { 118 | "prefix": "GRAY5", 119 | "body": "display.GRAY5", 120 | "description": "If using the default color palette, this constant indexes the color #8d8d8d. Note, color indexes may be overridden by the user." 121 | }, 122 | "GRAY6": { 123 | "prefix": "GRAY6", 124 | "body": "display.GRAY6", 125 | "description": "If using the default color palette, this constant indexes the color #aaaaaa. Note, color indexes may be overridden by the user." 126 | }, 127 | "GRAY7": { 128 | "prefix": "GRAY7", 129 | "body": "display.GRAY7", 130 | "description": "If using the default color palette, this constant indexes the color #c6c6c6. Note, color indexes may be overridden by the user." 131 | }, 132 | "GRAY8": { 133 | "prefix": "GRAY8", 134 | "body": "display.GRAY8", 135 | "description": "If using the default color palette, this constant indexes the color #e2e2e2. Note, color indexes may be overridden by the user." 136 | }, 137 | "TOP_LEFT": { 138 | "prefix": "TOP_LEFT", 139 | "body": "display.TOP_LEFT", 140 | "description": "Justifies a text object on its x, y coordinate to the top left." 141 | }, 142 | "MIDDLE_LEFT": { 143 | "prefix": "MIDDLE_LEFT", 144 | "body": "display.MIDDLE_LEFT", 145 | "description": "Justifies a text object on its x, y coordinate to the middle left" 146 | }, 147 | "BOTTOM_LEFT": { 148 | "prefix": "BOTTOM_LEFT", 149 | "body": "display.BOTTOM_LEFT", 150 | "description": "Justifies a text object on its x, y coordinate to the bottom left." 151 | }, 152 | "TOP_CENTER": { 153 | "prefix": "TOP_CENTER", 154 | "body": "display.TOP_CENTER", 155 | "description": "Justifies a text object on its x, y coordinate to the top center." 156 | }, 157 | "BOTTOM_CENTER": { 158 | "prefix": "BOTTOM_CENTER", 159 | "body": "display.BOTTOM_CENTER", 160 | "description": "Justifies a text object on its x, y coordinate to the middle cente." 161 | }, 162 | "TOP_RIGHT": { 163 | "prefix": "TOP_RIGHT", 164 | "body": "display.TOP_RIGHT", 165 | "description": "Justifies a text object on its x, y coordinate to the bottom center." 166 | }, 167 | "MIDDLE_CENTER": { 168 | "prefix": "MIDDLE_CENTER", 169 | "body": "display.MIDDLE_CENTER", 170 | "description": "Justifies a text object on its x, y coordinate to the top right." 171 | }, 172 | "MIDDLE_RIGHT": { 173 | "prefix": "MIDDLE_RIGHT", 174 | "body": "display.MIDDLE_RIGHT", 175 | "description": "Justifies a text object on its x, y coordinate to the middle right." 176 | }, 177 | "BOTTOM_RIGHT": { 178 | "prefix": "BOTTOM_RIGHT", 179 | "body": "display.BOTTOM_RIGHT", 180 | "description": "Justifies a text object on its x, y coordinate to the bottom right." 181 | }, 182 | "WIDTH": { 183 | "prefix": "WIDTH", 184 | "body": "display.WIDTH", 185 | "description": "The display width in pixels. Equal to 640." 186 | }, 187 | "HEIGHT": { 188 | "prefix": "HEIGHT", 189 | "body": "display.HEIGHT", 190 | "description": "Justifies a text object on its x, y coordinate to the top right." 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/ble/helpers.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | /* @license 4 | * 5 | * BLE Abstraction Tool: bluetooth helpers 6 | * 7 | * The MIT License (MIT) 8 | * 9 | * Copyright (c) 2016 Rob Moran 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy 12 | * of this software and associated documentation files (the "Software"), to deal 13 | * in the Software without restriction, including without limitation the rights 14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | * copies of the Software, and to permit persons to whom the Software is 16 | * furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included in all 19 | * copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | * SOFTWARE. 28 | */ 29 | 30 | // https://github.com/umdjs/umd 31 | (function (root, factory) { 32 | if (typeof define === 'function' && define.amd) { 33 | // AMD. Register as an anonymous module. 34 | define(factory); 35 | } else if (typeof exports === 'object') { 36 | // Node. Does not work with strict CommonJS 37 | module.exports = factory(); 38 | } else { 39 | // Browser globals with support for web workers (root is window) 40 | root.bleatHelpers = factory(); 41 | } 42 | }(this, function() { 43 | "use strict"; 44 | 45 | var bluetoothServices = { 46 | "alert_notification": 0x1811, 47 | "automation_io": 0x1815, 48 | "battery_service": 0x180F, 49 | "blood_pressure": 0x1810, 50 | "body_composition": 0x181B, 51 | "bond_management": 0x181E, 52 | "continuous_glucose_monitoring": 0x181F, 53 | "current_time": 0x1805, 54 | "cycling_power": 0x1818, 55 | "cycling_speed_and_cadence": 0x1816, 56 | "device_information": 0x180A, 57 | "environmental_sensing": 0x181A, 58 | "generic_access": 0x1800, 59 | "generic_attribute": 0x1801, 60 | "glucose": 0x1808, 61 | "health_thermometer": 0x1809, 62 | "heart_rate": 0x180D, 63 | "human_interface_device": 0x1812, 64 | "immediate_alert": 0x1802, 65 | "indoor_positioning": 0x1821, 66 | "internet_protocol_support": 0x1820, 67 | "link_loss": 0x1803, 68 | "location_and_navigation": 0x1819, 69 | "next_dst_change": 0x1807, 70 | "phone_alert_status": 0x180E, 71 | "pulse_oximeter": 0x1822, 72 | "reference_time_update": 0x1806, 73 | "running_speed_and_cadence": 0x1814, 74 | "scan_parameters": 0x1813, 75 | "tx_power": 0x1804, 76 | "user_data": 0x181C, 77 | "weight_scale": 0x181D 78 | }; 79 | 80 | var bluetoothCharacteristics = { 81 | "aerobic_heart_rate_lower_limit": 0x2A7E, 82 | "aerobic_heart_rate_upper_limit": 0x2A84, 83 | "aerobic_threshold": 0x2A7F, 84 | "age": 0x2A80, 85 | "aggregate": 0x2A5A, 86 | "alert_category_id": 0x2A43, 87 | "alert_category_id_bit_mask": 0x2A42, 88 | "alert_level": 0x2A06, 89 | "alert_notification_control_point": 0x2A44, 90 | "alert_status": 0x2A3F, 91 | "altitude": 0x2AB3, 92 | "anaerobic_heart_rate_lower_limit": 0x2A81, 93 | "anaerobic_heart_rate_upper_limit": 0x2A82, 94 | "anaerobic_threshold": 0x2A83, 95 | "analog": 0x2A58, 96 | "apparent_wind_direction": 0x2A73, 97 | "apparent_wind_speed": 0x2A72, 98 | "gap.appearance": 0x2A01, 99 | "barometric_pressure_trend": 0x2AA3, 100 | "battery_level": 0x2A19, 101 | "blood_pressure_feature": 0x2A49, 102 | "blood_pressure_measurement": 0x2A35, 103 | "body_composition_feature": 0x2A9B, 104 | "body_composition_measurement": 0x2A9C, 105 | "body_sensor_location": 0x2A38, 106 | "bond_management_control_point": 0x2AA4, 107 | "bond_management_feature": 0x2AA5, 108 | "boot_keyboard_input_report": 0x2A22, 109 | "boot_keyboard_output_report": 0x2A32, 110 | "boot_mouse_input_report": 0x2A33, 111 | "gap.central_address_resolution_support": 0x2AA6, 112 | "cgm_feature": 0x2AA8, 113 | "cgm_measurement": 0x2AA7, 114 | "cgm_session_run_time": 0x2AAB, 115 | "cgm_session_start_time": 0x2AAA, 116 | "cgm_specific_ops_control_point": 0x2AAC, 117 | "cgm_status": 0x2AA9, 118 | "csc_feature": 0x2A5C, 119 | "csc_measurement": 0x2A5B, 120 | "current_time": 0x2A2B, 121 | "cycling_power_control_point": 0x2A66, 122 | "cycling_power_feature": 0x2A65, 123 | "cycling_power_measurement": 0x2A63, 124 | "cycling_power_vector": 0x2A64, 125 | "database_change_increment": 0x2A99, 126 | "date_of_birth": 0x2A85, 127 | "date_of_threshold_assessment": 0x2A86, 128 | "date_time": 0x2A08, 129 | "day_date_time": 0x2A0A, 130 | "day_of_week": 0x2A09, 131 | "descriptor_value_changed": 0x2A7D, 132 | "gap.device_name": 0x2A00, 133 | "dew_point": 0x2A7B, 134 | "digital": 0x2A56, 135 | "dst_offset": 0x2A0D, 136 | "elevation": 0x2A6C, 137 | "email_address": 0x2A87, 138 | "exact_time_256": 0x2A0C, 139 | "fat_burn_heart_rate_lower_limit": 0x2A88, 140 | "fat_burn_heart_rate_upper_limit": 0x2A89, 141 | "firmware_revision_string": 0x2A26, 142 | "first_name": 0x2A8A, 143 | "five_zone_heart_rate_limits": 0x2A8B, 144 | "floor_number": 0x2AB2, 145 | "gender": 0x2A8C, 146 | "glucose_feature": 0x2A51, 147 | "glucose_measurement": 0x2A18, 148 | "glucose_measurement_context": 0x2A34, 149 | "gust_factor": 0x2A74, 150 | "hardware_revision_string": 0x2A27, 151 | "heart_rate_control_point": 0x2A39, 152 | "heart_rate_max": 0x2A8D, 153 | "heart_rate_measurement": 0x2A37, 154 | "heat_index": 0x2A7A, 155 | "height": 0x2A8E, 156 | "hid_control_point": 0x2A4C, 157 | "hid_information": 0x2A4A, 158 | "hip_circumference": 0x2A8F, 159 | "humidity": 0x2A6F, 160 | "ieee_11073-20601_regulatory_certification_data_list": 0x2A2A, 161 | "indoor_positioning_configuration": 0x2AAD, 162 | "intermediate_blood_pressure": 0x2A36, 163 | "intermediate_temperature": 0x2A1E, 164 | "irradiance": 0x2A77, 165 | "language": 0x2AA2, 166 | "last_name": 0x2A90, 167 | "latitude": 0x2AAE, 168 | "ln_control_point": 0x2A6B, 169 | "ln_feature": 0x2A6A, 170 | "local_east_coordinate.xml": 0x2AB1, 171 | "local_north_coordinate": 0x2AB0, 172 | "local_time_information": 0x2A0F, 173 | "location_and_speed": 0x2A67, 174 | "location_name": 0x2AB5, 175 | "longitude": 0x2AAF, 176 | "magnetic_declination": 0x2A2C, 177 | "magnetic_flux_density_2D": 0x2AA0, 178 | "magnetic_flux_density_3D": 0x2AA1, 179 | "manufacturer_name_string": 0x2A29, 180 | "maximum_recommended_heart_rate": 0x2A91, 181 | "measurement_interval": 0x2A21, 182 | "model_number_string": 0x2A24, 183 | "navigation": 0x2A68, 184 | "new_alert": 0x2A46, 185 | "gap.peripheral_preferred_connection_parameters": 0x2A04, 186 | "gap.peripheral_privacy_flag": 0x2A02, 187 | "plx_continuous_measurement": 0x2A5F, 188 | "plx_features": 0x2A60, 189 | "plx_spot_check_measurement": 0x2A5E, 190 | "pnp_id": 0x2A50, 191 | "pollen_concentration": 0x2A75, 192 | "position_quality": 0x2A69, 193 | "pressure": 0x2A6D, 194 | "protocol_mode": 0x2A4E, 195 | "rainfall": 0x2A78, 196 | "gap.reconnection_address": 0x2A03, 197 | "record_access_control_point": 0x2A52, 198 | "reference_time_information": 0x2A14, 199 | "report": 0x2A4D, 200 | "report_map": 0x2A4B, 201 | "resting_heart_rate": 0x2A92, 202 | "ringer_control_point": 0x2A40, 203 | "ringer_setting": 0x2A41, 204 | "rsc_feature": 0x2A54, 205 | "rsc_measurement": 0x2A53, 206 | "sc_control_point": 0x2A55, 207 | "scan_interval_window": 0x2A4F, 208 | "scan_refresh": 0x2A31, 209 | "sensor_location": 0x2A5D, 210 | "serial_number_string": 0x2A25, 211 | "gatt.service_changed": 0x2A05, 212 | "software_revision_string": 0x2A28, 213 | "sport_type_for_aerobic_and_anaerobic_thresholds": 0x2A93, 214 | "supported_new_alert_category": 0x2A47, 215 | "supported_unread_alert_category": 0x2A48, 216 | "system_id": 0x2A23, 217 | "temperature": 0x2A6E, 218 | "temperature_measurement": 0x2A1C, 219 | "temperature_type": 0x2A1D, 220 | "three_zone_heart_rate_limits": 0x2A94, 221 | "time_accuracy": 0x2A12, 222 | "time_source": 0x2A13, 223 | "time_update_control_point": 0x2A16, 224 | "time_update_state": 0x2A17, 225 | "time_with_dst": 0x2A11, 226 | "time_zone": 0x2A0E, 227 | "true_wind_direction": 0x2A71, 228 | "true_wind_speed": 0x2A70, 229 | "two_zone_heart_rate_limit": 0x2A95, 230 | "tx_power_level": 0x2A07, 231 | "uncertainty": 0x2AB4, 232 | "unread_alert_status": 0x2A45, 233 | "user_control_point": 0x2A9F, 234 | "user_index": 0x2A9A, 235 | "uv_index": 0x2A76, 236 | "vo2_max": 0x2A96, 237 | "waist_circumference": 0x2A97, 238 | "weight": 0x2A98, 239 | "weight_measurement": 0x2A9D, 240 | "weight_scale_feature": 0x2A9E, 241 | "wind_chill": 0x2A79 242 | }; 243 | 244 | var bluetoothDescriptors = { 245 | "gatt.characteristic_extended_properties": 0x2900, 246 | "gatt.characteristic_user_description": 0x2901, 247 | "gatt.client_characteristic_configuration": 0x2902, 248 | "gatt.server_characteristic_configuration": 0x2903, 249 | "gatt.characteristic_presentation_format": 0x2904, 250 | "gatt.characteristic_aggregate_format": 0x2905, 251 | "valid_range": 0x2906, 252 | "external_report_reference": 0x2907, 253 | "report_reference": 0x2908, 254 | "number_of_digitals": 0x2909, 255 | "value_trigger_setting": 0x290A, 256 | "es_configuration": 0x290B, 257 | "es_measurement": 0x290C, 258 | "es_trigger_setting": 0x290D, 259 | "time_trigger_setting": 0x290E 260 | }; 261 | 262 | function getCanonicalUUID(uuid) { 263 | if (typeof uuid === "number") uuid = uuid.toString(16); 264 | uuid = uuid.toLowerCase(); 265 | if (uuid.length <= 8) uuid = ("00000000" + uuid).slice(-8) + "-0000-1000-8000-00805f9b34fb"; 266 | if (uuid.length === 32) uuid = uuid.match(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/).splice(1).join("-"); 267 | return uuid; 268 | } 269 | 270 | function getServiceUUID(uuid) { 271 | if (bluetoothServices[uuid]) uuid = bluetoothServices[uuid]; 272 | return getCanonicalUUID(uuid); 273 | } 274 | 275 | function getCharacteristicUUID(uuid) { 276 | if (bluetoothCharacteristics[uuid]) uuid = bluetoothCharacteristics[uuid]; 277 | return getCanonicalUUID(uuid); 278 | } 279 | 280 | function getDescriptorUUID(uuid) { 281 | if (bluetoothDescriptors[uuid]) uuid = bluetoothDescriptors[uuid]; 282 | return getCanonicalUUID(uuid); 283 | } 284 | 285 | return { 286 | Services: bluetoothServices, 287 | Characteristics: bluetoothCharacteristics, 288 | Descriptors: bluetoothDescriptors, 289 | getCanonicalUUID: getCanonicalUUID, 290 | getServiceUUID: getServiceUUID, 291 | getCharacteristicUUID: getCharacteristicUUID, 292 | getDescriptorUUID: getDescriptorUUID 293 | }; 294 | })); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brilliant-ar-studio", 3 | "displayName": "Brilliant AR Studio", 4 | "description": "Develop AR applications for Monocle within the Brilliant AR Studio.", 5 | "publisher": "brilliantlabs", 6 | "repository": { 7 | "url": "https://github.com/brilliantlabsAR/ar-studio-for-vscode" 8 | }, 9 | "icon": "media/brilliant.png", 10 | "version": "1.20.1", 11 | "engines": { 12 | "vscode": "^1.76.0" 13 | }, 14 | "categories": [ 15 | "Other" 16 | ], 17 | "activationEvents": [ 18 | "onStartupFinished" 19 | ], 20 | "main": "./out/extension.js", 21 | "contributes": { 22 | "viewsContainers": { 23 | "activitybar": [ 24 | { 25 | "id": "brilliantsar", 26 | "title": "Brilliant AR Studio", 27 | "icon": "media/Monocle_Icon.svg" 28 | } 29 | ] 30 | }, 31 | "views": { 32 | "brilliantsar": [ 33 | { 34 | "id": "snippetTemplates", 35 | "name": "Code templates", 36 | "icon": "media/Monocle_Icon.svg", 37 | "when": "!frame.deviceConnected" 38 | }, 39 | { 40 | "id": "screens", 41 | "name": "screens", 42 | "icon": "media/Monocle_Icon.svg", 43 | "when": "!frame.deviceConnected" 44 | }, 45 | { 46 | "id": "fpga", 47 | "name": "Device Status", 48 | "icon": "media/Monocle_Icon.svg", 49 | "type": "webview", 50 | "when": "monocle.deviceConnected || frame.deviceConnected" 51 | }, 52 | { 53 | "id": "projects", 54 | "name": "Community projects", 55 | "icon": "media/Monocle_Icon.svg", 56 | "when": "!frame.deviceConnected" 57 | } 58 | ], 59 | "explorer": [ 60 | { 61 | "id": "fileExplorer", 62 | "name": "Brilliant AR Studio: Device Files", 63 | "icon": "media/Monocle_Icon.svg" 64 | } 65 | ] 66 | }, 67 | "viewsWelcome": [ 68 | { 69 | "view": "fpga", 70 | "contents": "Device: Monocle \nMAC address: ...\nFirmware version: v23.117.1100 (Up to date) \nFPGA image: v23.130.1100 (Update available)", 71 | "when": "monocle.deviceConnected || frame.deviceConnected" 72 | }, 73 | { 74 | "view": "fpga", 75 | "contents": "[Update FPGA](command:brilliant-ar-studio.fpgaUpdate)", 76 | "when": "monocle.fpgaAvailable || !monocle.deviceConnected" 77 | }, 78 | { 79 | "view": "fpga", 80 | "contents": "[Custom FPGA](command:brilliant-ar-studio.fpgaUpdateCustom)" 81 | }, 82 | { 83 | "view": "fileExplorer", 84 | "contents": "[Connect](command:brilliant-ar-studio.connect)", 85 | "when": "!monocle.deviceConnected && !frame.deviceConnected" 86 | } 87 | ], 88 | "commands": [ 89 | { 90 | "command": "brilliant-ar-studio.openDeviceFile", 91 | "title": "Brilliant AR Studio: Open Device file in editor", 92 | "enablement": "monocle.deviceConnected || frame.deviceConnected" 93 | }, 94 | { 95 | "command": "fileExplorer.openFile", 96 | "title": "Brilliant AR Studio: openFile", 97 | "enablement": "monocle.deviceConnected || frame.deviceConnected" 98 | }, 99 | { 100 | "command": "brilliant-ar-studio.disconnect", 101 | "title": "Brilliant AR Studio: Disconnect", 102 | "enablement": "monocle.deviceConnected || frame.deviceConnected" 103 | }, 104 | { 105 | "command": "brilliant-ar-studio.connect", 106 | "title": "Brilliant AR Studio: Connect", 107 | "enablement": "!monocle.deviceConnected && !frame.deviceConnected" 108 | }, 109 | { 110 | "command": "brilliant-ar-studio.runFile", 111 | "title": "Brilliant AR Studio: Run in Device", 112 | "icon": "$(play-circle)", 113 | "enablement": "monocle.deviceConnected" 114 | }, 115 | { 116 | "command": "brilliant-ar-studio.refreshDeviceFiles", 117 | "title": "Brilliant AR Studio: Refresh", 118 | "icon": "$(refresh)" 119 | }, 120 | { 121 | "command": "brilliant-ar-studio.syncFiles", 122 | "title": "Brilliant AR Studio: Start auto run", 123 | "icon": "$(play-circle)", 124 | "enablement": "monocle.deviceConnected || frame.deviceConnected" 125 | }, 126 | { 127 | "command": "brilliant-ar-studio.deleteDeviceFile", 128 | "title": "Brilliant AR Studio: Delete file from device", 129 | "icon": "$(trash)" 130 | }, 131 | { 132 | "command": "brilliant-ar-studio.syncStop", 133 | "title": "Brilliant AR Studio: Stop auto run", 134 | "icon": "$(debug-stop)" 135 | }, 136 | { 137 | "command": "brilliant-ar-studio.fpgaUpdate", 138 | "title": "Brilliant AR Studio: FPGA Update", 139 | "icon": "$(cloud)", 140 | "enablement": "monocle.deviceConnected" 141 | }, 142 | { 143 | "command": "brilliant-ar-studio.fpgaUpdateCustom", 144 | "title": "Brilliant AR Studio: Custom FPGA Update", 145 | "icon": "$(cloud)", 146 | "enablement": "monocle.deviceConnected" 147 | }, 148 | { 149 | "command": "brilliant-ar-studio.getPublicApps", 150 | "title": "Brilliant AR Studio: Get public apps", 151 | "icon": "$(refresh)" 152 | }, 153 | { 154 | "command": "brilliant-ar-studio.publishMonocleApp", 155 | "title": "Brilliant AR Studio: Publish app", 156 | "icon": "$(cloud-upload)" 157 | }, 158 | { 159 | "command": "brilliant-ar-studio.UnPublishMonocleApp", 160 | "title": "Brilliant AR Studio: Unpublish app", 161 | "icon": "$(star-full)" 162 | }, 163 | { 164 | "command": "brilliant-ar-studio.forkProject", 165 | "title": "Brilliant AR Studio: Fork to Edit", 166 | "icon": "$(repo-forked)" 167 | }, 168 | { 169 | "command": "brilliant-ar-studio.copyProject", 170 | "title": "Brilliant AR Studio: Copy To Local", 171 | "icon": "$(explorer-view-icon)" 172 | }, 173 | { 174 | "command": "brilliant-ar-studio.setDeviceLocalPath", 175 | "title": "Brilliant AR Studio: Initialize new project folder", 176 | "icon": "$(explorer-view-icon)" 177 | }, 178 | { 179 | "command": "brilliant-ar-studio.uploadFilesToDevice", 180 | "title": "Brilliant AR Studio: Upload File To Device", 181 | "icon": "$(save)", 182 | "enablement": "monocle.deviceConnected || frame.deviceConnected" 183 | }, 184 | { 185 | "command": "brilliant-ar-studio.openUIEditor", 186 | "title": "Brilliant AR Studio: Open New GUI Screen", 187 | "icon": "$(plus)", 188 | "enablement": "!frame.deviceConnected" 189 | }, 190 | { 191 | "command": "brilliant-ar-studio.editUIEditor", 192 | "title": "Brilliant AR Studio:Edit GUI Screen", 193 | "icon": "$(edit)", 194 | "enablement": "!frame.deviceConnected" 195 | }, 196 | { 197 | "command": "brilliant-ar-studio.sendRawData", 198 | "title": "Brilliant AR Studio: Send Raw Data", 199 | "icon": "$(play)", 200 | "enablement": "monocle.deviceConnected" 201 | }, 202 | { 203 | "command": "brilliant-ar-studio.syncAllFiles", 204 | "title": "Brilliant AR Studio: Build", 205 | "icon": "$(sync)", 206 | "enablement": "monocle.deviceConnected || frame.deviceConnected" 207 | }, 208 | { 209 | "command": "brilliant-ar-studio.renameDeviceFile", 210 | "title": "Brilliant AR Studio: Rename Device File", 211 | "icon": "$(sync)", 212 | "enablement": "monocle.deviceConnected || frame.deviceConnected" 213 | }, 214 | { 215 | "command": "brilliant-ar-studio.downloadDeviceFile", 216 | "title": "Brilliant AR Studio: Download Device File", 217 | "icon": "$(cloud-download)", 218 | "enablement": "monocle.deviceConnected || frame.deviceConnected" 219 | } 220 | ], 221 | "menus": { 222 | "view/title": [ 223 | { 224 | "command": "brilliant-ar-studio.refreshDeviceFiles", 225 | "when": "view == fileExplorer && (monocle.deviceConnected || frame.deviceConnected)", 226 | "group": "navigation@1" 227 | }, 228 | { 229 | "command": "brilliant-ar-studio.sendRawData", 230 | "when": "view == fileExplorer && monocle.deviceConnected", 231 | "group": "navigation@0" 232 | }, 233 | { 234 | "command": "brilliant-ar-studio.publishMonocleApp", 235 | "when": "view == projects && workspaceFolderCount !=0 && !monocle.published", 236 | "group": "navigation@3", 237 | "enablement": "monocle.deviceConnected" 238 | }, 239 | { 240 | "command": "brilliant-ar-studio.UnPublishMonocleApp", 241 | "when": "view == projects && workspaceFolderCount !=0 && monocle.published==true", 242 | "group": "navigation@3", 243 | "enablement": "monocle.deviceConnected" 244 | }, 245 | { 246 | "command": "brilliant-ar-studio.openUIEditor", 247 | "when": "view == screens", 248 | "group": "navigation@0", 249 | "enablement": "!frame.deviceConnected" 250 | } 251 | ], 252 | "view/item/context": [ 253 | { 254 | "command": "brilliant-ar-studio.copyProject", 255 | "when": "view == projects", 256 | "group": "inline" 257 | }, 258 | { 259 | "command": "brilliant-ar-studio.forkProject", 260 | "when": "view == projects", 261 | "group": "inline" 262 | }, 263 | { 264 | "command": "brilliant-ar-studio.downloadDeviceFile", 265 | "when": "view == fileExplorer", 266 | "group": "navigation@0" 267 | }, 268 | { 269 | "command": "brilliant-ar-studio.renameDeviceFile", 270 | "when": "view == fileExplorer", 271 | "group": "navigation@1" 272 | }, 273 | { 274 | "command": "brilliant-ar-studio.deleteDeviceFile", 275 | "when": "view == fileExplorer", 276 | "group": "navigation@2" 277 | } 278 | ], 279 | "editor/title": [ 280 | { 281 | "when": "resourceLangId == python && monocle.deviceConnected", 282 | "command": "brilliant-ar-studio.runFile", 283 | "group": "navigation@0" 284 | } 285 | ], 286 | "explorer/context": [ 287 | { 288 | "command": "brilliant-ar-studio.uploadFilesToDevice", 289 | "group": "navigation@0" 290 | } 291 | ] 292 | }, 293 | "keybindings": [ 294 | { 295 | "command": "brilliant-ar-studio.syncAllFiles", 296 | "key": "ctrl+shift+b", 297 | "when": "monocle.deviceConnected || frame.deviceConnected" 298 | } 299 | ], 300 | "snippets": [ 301 | { 302 | "language": "python", 303 | "path": "./snippets/bluetooth.json" 304 | }, 305 | { 306 | "language": "python", 307 | "path": "./snippets/camera.json" 308 | }, 309 | { 310 | "language": "python", 311 | "path": "./snippets/device.json" 312 | }, 313 | { 314 | "language": "python", 315 | "path": "./snippets/display.json" 316 | }, 317 | { 318 | "language": "python", 319 | "path": "./snippets/fpga.json" 320 | }, 321 | { 322 | "language": "python", 323 | "path": "./snippets/gc.json" 324 | }, 325 | { 326 | "language": "python", 327 | "path": "./snippets/led.json" 328 | }, 329 | { 330 | "language": "python", 331 | "path": "./snippets/logic.json" 332 | }, 333 | { 334 | "language": "python", 335 | "path": "./snippets/loops.json" 336 | }, 337 | { 338 | "language": "python", 339 | "path": "./snippets/math.json" 340 | }, 341 | { 342 | "language": "python", 343 | "path": "./snippets/time.json" 344 | }, 345 | { 346 | "language": "python", 347 | "path": "./snippets/touch.json" 348 | }, 349 | { 350 | "language": "python", 351 | "path": "./snippets/uhashlib.json" 352 | }, 353 | { 354 | "language": "python", 355 | "path": "./snippets/ujson.json" 356 | }, 357 | { 358 | "language": "python", 359 | "path": "./snippets/update.json" 360 | }, 361 | { 362 | "language": "python", 363 | "path": "./snippets/urandom.json" 364 | }, 365 | { 366 | "language": "python", 367 | "path": "./snippets/ure.json" 368 | }, 369 | { 370 | "language": "python", 371 | "path": "./snippets/uselect.json" 372 | }, 373 | { 374 | "language": "python", 375 | "path": "./snippets/ustruct.json" 376 | }, 377 | { 378 | "language": "python", 379 | "path": "./snippets/microphone.json" 380 | } 381 | ] 382 | }, 383 | "scripts": { 384 | "vscode:prepublish": "npm run compile", 385 | "compile": "tsc -p ./", 386 | "watch": "tsc -watch -p ./", 387 | "pretest": "npm run compile && npm run lint", 388 | "lint": "eslint src --ext ts", 389 | "test": "node ./out/test/runTest.js" 390 | }, 391 | "devDependencies": { 392 | "@types/glob": "^8.1.0", 393 | "@types/mocha": "^10.0.1", 394 | "@types/node": "16.x", 395 | "@types/vscode": "^1.76.0", 396 | "@typescript-eslint/eslint-plugin": "^5.53.0", 397 | "@typescript-eslint/parser": "^5.53.0", 398 | "@vscode/test-electron": "^2.2.3", 399 | "eslint": "^8.34.0", 400 | "glob": "^8.1.0", 401 | "mocha": "^10.2.0", 402 | "typescript": "^4.9.5" 403 | }, 404 | "dependencies": { 405 | "@octokit/request": "^6.2.3", 406 | "froala-editor": "^4.0.18", 407 | "jszip": "^3.10.1", 408 | "node-ble": "^1.9.0" 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/bluetooth.ts: -------------------------------------------------------------------------------- 1 | 2 | import { replHandleResponse, onDisconnect, colorText, frameHandleResponse, setDeviceConnected } from "./repl"; 3 | import { nordicDfuHandleControlResponse } from './nordicdfu'; 4 | import { writeEmitter, writeEmitterRaw } from './extension'; 5 | import * as vscode from 'vscode'; 6 | 7 | var util = require('util'); 8 | let device: any = null; 9 | var bluetooth = require('./ble/index').webbluetooth; 10 | let nordicDfuControlCharacteristic: any = null; 11 | let nordicDfuPacketCharacteristic: any = null; 12 | const nordicDfuServiceUuid = 0xfe59; 13 | const nordicDfuControlCharacteristicUUID = '8ec90001-f315-4f60-9fb8-838830daea50'; 14 | const nordicDfuPacketCharacteristicUUID = '8ec90002-f315-4f60-9fb8-838830daea50'; 15 | 16 | let replRxCharacteristic: any = null; 17 | let replTxCharacteristic: any = null; 18 | const replDataServiceUuid = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; 19 | const replRxCharacteristicUuid = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; 20 | const replTxCharacteristicUuid = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; 21 | 22 | let frameRxCharacteristic: any = null; 23 | let frameTxCharacteristic: any = null; 24 | const frameServiceUUID = "7a230001-5475-a6a4-654c-8431f6ad49c4"; 25 | const frameRxCharacteristicsUUID = "7a230002-5475-a6a4-654c-8431f6ad49c4"; 26 | const frameTxCharacteristicsUUID = "7a230003-5475-a6a4-654c-8431f6ad49c4"; 27 | 28 | let rawDataRxCharacteristic: any = null; 29 | let rawDataTxCharacteristic = null; 30 | const rawDataServiceUuid = "e5700001-7bac-429a-b4ce-57ff900f479d"; 31 | const rawDataRxCharacteristicUuid = "e5700002-7bac-429a-b4ce-57ff900f479d"; 32 | const rawDataTxCharacteristicUuid = "e5700003-7bac-429a-b4ce-57ff900f479d"; 33 | 34 | export const replDataTxQueue = []; 35 | export const frameDataTxQueue = []; 36 | export const rawDataTxQueue = []; 37 | type DeviceInfo = { 38 | macAddress?: string, 39 | name?: string, 40 | fpgaStatu?: boolean, 41 | fpgaMessage?: string 42 | }; 43 | export var deviceInfo: DeviceInfo = {}; 44 | let replTxTaskIntervalId: any = null; 45 | let frameTxTaskIntervalId: any = null; 46 | let replDataTxInProgress = false; 47 | let frameDataTxInProgress = false; 48 | 49 | // Web-Bluetooth doesn't have any MTU API, so we just set it to something reasonable 50 | export const maxmtu: any = 100; 51 | export function convertToLittleEndian(macAddress: string): string { 52 | // Split the MAC address into an array of hexadecimal values 53 | var macAddressBytes = macAddress.split(":").map((hex: any) => parseInt(hex, 16)); 54 | 55 | // Check if the current endianness is big-endian 56 | var isBigEndian = (macAddressBytes[0] & 1) === 0; 57 | 58 | if (isBigEndian) { 59 | // Reverse the array to convert to little-endian 60 | macAddressBytes.reverse(); 61 | 62 | // Join the array back into a string with colons 63 | var littleEndianMAC = macAddressBytes.map((byt: any) => { 64 | var hex = byt.toString(16).toUpperCase(); 65 | return hex.length === 1 ? "0" + hex : hex; 66 | }).join(":"); 67 | 68 | return littleEndianMAC; 69 | } else { 70 | // If it's already little-endian, no need to change anything 71 | return macAddress; 72 | } 73 | } 74 | 75 | 76 | export function isConnected() { 77 | 78 | if (device && device.gatt.connected) { 79 | return true; 80 | } 81 | 82 | return false; 83 | } 84 | let connectionInProgress = 0; 85 | let currentSelectionTimeout: any; 86 | export async function connect() { 87 | try { 88 | // connection timeout = 10s 89 | if (connectionInProgress && (Date.now() - connectionInProgress) > 10200) { 90 | connectionInProgress = 0; 91 | } 92 | if (connectionInProgress) { 93 | return Promise.resolve("inprogress"); 94 | } 95 | connectionInProgress = Date.now(); 96 | setTimeout(() => { 97 | bluetooth.cancelRequest(); 98 | if (!isConnected() && !currentSelectionTimeout) { 99 | onDisconnect(); 100 | vscode.window.showWarningMessage("No device found"); 101 | } 102 | 103 | }, 10000); 104 | const quickPick = vscode.window.createQuickPick(); 105 | quickPick.items = []; 106 | let allDevices: any = {}; 107 | device = await bluetooth.requestDevice({ 108 | filters: [ 109 | { services: [replDataServiceUuid] }, 110 | { services: [nordicDfuServiceUuid] }, 111 | { services: [frameServiceUUID] } 112 | ], 113 | optionalServices: [rawDataServiceUuid], 114 | deviceFound: function (bleDevice: any, selectFn: any) { 115 | let mac = bleDevice.id.toUpperCase(); 116 | 117 | if (bleDevice.id.length < 20) { 118 | mac = convertToLittleEndian(bleDevice.id).toUpperCase(); 119 | } 120 | allDevices[mac] = bleDevice; 121 | quickPick.items = [...quickPick.items, { label: bleDevice.name + ' RSSI: ' + bleDevice.adData.rssi || "can't detect", description: mac }]; 122 | quickPick.onDidChangeSelection(selection => { 123 | if (selection[0] && selection[0].description) { 124 | selectFn(allDevices[selection[0]?.description]); 125 | quickPick.dispose(); 126 | } 127 | }); 128 | quickPick.onDidHide(() => { 129 | quickPick.dispose(); 130 | }); 131 | if (currentSelectionTimeout) { 132 | clearTimeout(currentSelectionTimeout); 133 | } 134 | currentSelectionTimeout = setTimeout(() => { 135 | if (Object.keys(allDevices).length > 1) { 136 | quickPick.show(); 137 | 138 | } else if (Object.keys(allDevices).length === 1) { 139 | selectFn(allDevices[Object.keys(allDevices)[0]]); 140 | allDevices = {}; 141 | } 142 | clearTimeout(currentSelectionTimeout); 143 | }, 3000); 144 | 145 | } 146 | }).catch(console.log); 147 | // } 148 | 149 | const server = await device.gatt.connect(); 150 | console.log('Connected to ' + device.name); 151 | let _time = Date.now(); 152 | device.addEventListener('gattserverdisconnected', disconnect); 153 | deviceInfo.macAddress = device.id; 154 | if (String(device.id).length > 20) { 155 | deviceInfo.macAddress = "Uknown"; 156 | } else { 157 | deviceInfo.macAddress = convertToLittleEndian(device.id).toUpperCase(); 158 | } 159 | 160 | deviceInfo.name = device.name; 161 | const nordicDfuService = await server.getPrimaryService(nordicDfuServiceUuid) 162 | .catch(() => { }); 163 | const replService = await server.getPrimaryService(replDataServiceUuid) 164 | .catch((err: any) => { console.log(err); }); 165 | const frameService = await server.getPrimaryService(frameServiceUUID) 166 | .catch((err: any) => { console.log(err); }); 167 | const rawDataService = await server.getPrimaryService(rawDataServiceUuid) 168 | .catch(() => { }); 169 | 170 | if (nordicDfuService) { 171 | nordicDfuControlCharacteristic = await nordicDfuService.getCharacteristic(nordicDfuControlCharacteristicUUID); 172 | nordicDfuPacketCharacteristic = await nordicDfuService.getCharacteristic(nordicDfuPacketCharacteristicUUID); 173 | await nordicDfuControlCharacteristic.startNotifications(); 174 | nordicDfuControlCharacteristic.addEventListener('characteristicvaluechanged', receiveNordicDfuControlData); 175 | connectionInProgress = 0; 176 | if (device.name = "Frame Update") { 177 | return Promise.resolve("frame update connected"); 178 | } else { 179 | return Promise.resolve("dfu connected"); 180 | } 181 | } 182 | 183 | if (frameService) { 184 | setDeviceConnected("Frame"); 185 | frameRxCharacteristic = await frameService.getCharacteristic(frameRxCharacteristicsUUID); 186 | frameTxCharacteristic = await frameService.getCharacteristic(frameTxCharacteristicsUUID); 187 | await frameTxCharacteristic.startNotifications(); 188 | frameTxCharacteristic.addEventListener('characteristicvaluechanged', receiveFrameData); 189 | frameTxTaskIntervalId = setInterval(transmitFrameData); 190 | connectionInProgress = 0; 191 | return Promise.resolve("frame connected"); 192 | } 193 | if (replService) { 194 | replRxCharacteristic = await replService.getCharacteristic(replRxCharacteristicUuid); 195 | replTxCharacteristic = await replService.getCharacteristic(replTxCharacteristicUuid); 196 | await replTxCharacteristic.startNotifications(); 197 | replTxCharacteristic.addEventListener('characteristicvaluechanged', receiveReplData); 198 | replTxTaskIntervalId = setInterval(transmitReplData); 199 | 200 | } 201 | 202 | if (rawDataService) { 203 | rawDataRxCharacteristic = await rawDataService.getCharacteristic(rawDataRxCharacteristicUuid); 204 | rawDataTxCharacteristic = await rawDataService.getCharacteristic(rawDataTxCharacteristicUuid); 205 | await rawDataTxCharacteristic.startNotifications(); 206 | rawDataTxCharacteristic.addEventListener('characteristicvaluechanged', receiveRawData); 207 | } 208 | connectionInProgress = 0; 209 | return Promise.resolve("repl connected"); 210 | } catch (error) { 211 | connectionInProgress = 0; 212 | console.log(error); 213 | } 214 | 215 | 216 | } 217 | 218 | export async function disconnect() { 219 | 220 | if (device && device.gatt.connected) { 221 | await device.gatt.disconnect(); 222 | } 223 | 224 | // Stop transmitting data 225 | clearInterval(replTxTaskIntervalId); 226 | clearInterval(frameTxTaskIntervalId); 227 | 228 | writeEmitter.fire("\r\nDisconnected \r\n"); 229 | // Callback to main.js 230 | onDisconnect(); 231 | } 232 | 233 | function receiveNordicDfuControlData(event: any) { 234 | nordicDfuHandleControlResponse(event.target.value); 235 | } 236 | 237 | export async function transmitNordicDfuControlData(bytes: any) { 238 | await nordicDfuControlCharacteristic.writeValue(new Uint8Array(bytes)); 239 | } 240 | 241 | export async function transmitNordicDfuPacketData(bytes: any) { 242 | await nordicDfuPacketCharacteristic.writeValueWithoutResponse(new Uint8Array(bytes)); 243 | } 244 | 245 | function receiveReplData(event: any) { 246 | // console.log(event); 247 | // Decode the byte array into a UTF-8 string 248 | const decoder = new util.TextDecoder('utf-8'); 249 | replHandleResponse(decoder.decode(event.target.value)); 250 | } 251 | function receiveFrameData(event: any) { 252 | const decoder = new util.TextDecoder('utf-8'); 253 | frameHandleResponse(decoder.decode(event.target.value)); 254 | } 255 | async function transmitFrameData() { 256 | if (frameDataTxInProgress === true) { 257 | return; 258 | } 259 | 260 | if (frameDataTxQueue.length === 0) { 261 | return; 262 | } 263 | 264 | frameDataTxInProgress = true; 265 | 266 | const payload = frameDataTxQueue.slice(0, maxmtu); 267 | 268 | await frameRxCharacteristic.writeValueWithoutResponse(new Uint8Array(payload)) 269 | .then(() => { 270 | frameDataTxQueue.splice(0, payload.length); 271 | frameDataTxInProgress = false; 272 | return; 273 | }) 274 | .catch((error: any) => { 275 | console.log(error); 276 | if (error === "NetworkError: GATT operation already in progress.") { 277 | // Ignore busy errors. Just wait and try again later 278 | } 279 | else { 280 | // Discard data on other types of error 281 | frameDataTxQueue.splice(0, payload.length); 282 | frameDataTxInProgress = false; 283 | // return Promise.reject(error); 284 | } 285 | }); 286 | } 287 | async function transmitReplData() { 288 | 289 | if (replDataTxInProgress === true) { 290 | return; 291 | } 292 | 293 | if (replDataTxQueue.length === 0) { 294 | return; 295 | } 296 | 297 | replDataTxInProgress = true; 298 | 299 | const payload = replDataTxQueue.slice(0, maxmtu); 300 | 301 | await replRxCharacteristic.writeValueWithoutResponse(new Uint8Array(payload)) 302 | .then(() => { 303 | replDataTxQueue.splice(0, payload.length); 304 | replDataTxInProgress = false; 305 | return; 306 | }) 307 | .catch((error: any) => { 308 | if (error === "NetworkError: GATT operation already in progress.") { 309 | // Ignore busy errors. Just wait and try again later 310 | } 311 | else { 312 | // Discard data on other types of error 313 | replDataTxQueue.splice(0, payload.length); 314 | replDataTxInProgress = false; 315 | // return Promise.reject(error); 316 | } 317 | }); 318 | 319 | } 320 | 321 | function bufferToHex(buffer: ArrayBuffer) { 322 | return [...new Uint8Array(buffer)] 323 | .map(b => b.toString(16).padStart(2, "0")); 324 | } 325 | function hexArrayToAscii(hexArray: string[], colorIndex: number) { 326 | let result = ""; 327 | for (let i = 0; i < hexArray.length; i++) { 328 | let code = parseInt(hexArray[i], 16); 329 | if (code >= 32 && code <= 126) { 330 | result += colorText(String.fromCharCode(code), colorIndex); 331 | 332 | } else { 333 | result += colorText(".", 1); 334 | } 335 | } 336 | return result; 337 | } 338 | 339 | export function receiveRawData(data: any) { 340 | // console.log(data); 341 | printToRawChannel(data.target.value.buffer); 342 | } 343 | 344 | 345 | export async function sendRawData(data: string) { 346 | // let buffer = Buffer.from(data,'utf-8').buffer; 347 | const encoder = new util.TextEncoder('utf-8'); 348 | let chars: any = []; 349 | chars.push.apply(chars, encoder.encode(data)); 350 | await rawDataRxCharacteristic.writeValueWithoutResponse(new Uint8Array(chars)) 351 | .then(() => { 352 | printToRawChannel(chars, false); 353 | }) 354 | .catch((error: any) => { 355 | // return Promise.reject(error); 356 | }); 357 | } 358 | 359 | const printToRawChannel = function (buff: ArrayBuffer, rx = true) { 360 | 361 | let buffArray = bufferToHex(buff); 362 | let colorIndex = 2; 363 | let outputmsg = 'RX '; 364 | if (!rx) { 365 | colorIndex = 3; 366 | outputmsg = 'TX '; 367 | } 368 | outputmsg = colorText(outputmsg, colorIndex); 369 | for (let row = 0; row < buffArray.length; row += 8) { 370 | const thisRow = buffArray.slice(row, row + 8); 371 | if (row !== 0) { 372 | outputmsg += ' '; 373 | } 374 | outputmsg += colorText(thisRow.join(' '), colorIndex); 375 | outputmsg += ' ' + ' '.repeat((8 - thisRow.length) * 3) + hexArrayToAscii(thisRow, colorIndex) + '\r\n'; 376 | 377 | } 378 | // outputChannelData.appendLine(outputmsg); 379 | writeEmitterRaw.fire(outputmsg + '\r\n'); 380 | }; -------------------------------------------------------------------------------- /src/fileSystemProvider.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as vscode from 'vscode'; 3 | import { 4 | listFilesDevice, 5 | createDirectoryDevice, 6 | creatUpdateFileDevice, 7 | deleteFilesDevice, 8 | renameFileDevice, 9 | readFileDevice, 10 | uploadFileBulkDevice, 11 | FileMaps, 12 | buildMappedFiles 13 | } from './repl'; 14 | import { startFirmwareUpdate } from './update'; 15 | import {deviceTreeProvider, isPathExist, monocleFolder,configScreenReadUpdate} from './extension'; 16 | import { isConnected,deviceInfo } from './bluetooth'; 17 | 18 | export class File extends vscode.TreeItem implements vscode.FileStat { 19 | 20 | type: vscode.FileType; 21 | ctime: number; 22 | mtime: number; 23 | size: number; 24 | name: string; 25 | data?: Uint8Array; 26 | path?:string = ""; 27 | constructor(name: string,path?:string,public readonly collapsibleState?: vscode.TreeItemCollapsibleState,public readonly command?: vscode.Command) { 28 | super(name, collapsibleState||vscode.TreeItemCollapsibleState.None); 29 | this.type = vscode.FileType.File; 30 | this.ctime = Date.now(); 31 | this.mtime = Date.now(); 32 | this.size = 0; 33 | this.name = name; 34 | this.path = path; 35 | this.iconPath =vscode.ThemeIcon.File; 36 | this.command = {command:"brilliant-ar-studio.openDeviceFile",title:"Open In Editor",arguments:[{"path":this.path}]}; 37 | this.contextValue = `devicefile_active`; 38 | 39 | } 40 | contextValue='devicefile_active'; 41 | 42 | // contextValue = `devicefile#${this.path}`; 43 | } 44 | 45 | export class Directory extends vscode.TreeItem implements vscode.FileStat { 46 | 47 | type: vscode.FileType; 48 | ctime: number; 49 | mtime: number; 50 | size: number; 51 | path?:string = ''; 52 | name: string; 53 | entries: Map; 54 | 55 | constructor(name: string, path?:string, public readonly collapsibleState?: vscode.TreeItemCollapsibleState) { 56 | super(name, collapsibleState||vscode.TreeItemCollapsibleState.None); 57 | this.type = vscode.FileType.Directory; 58 | this.ctime = Date.now(); 59 | this.mtime = Date.now(); 60 | this.size = 0; 61 | this.name = name; 62 | this.path = path; 63 | this.entries = new Map(); 64 | this.iconPath =vscode.ThemeIcon.Folder; 65 | this.contextValue = `devicefile_active`; 66 | 67 | } 68 | contextValue='devicefile_active'; 69 | 70 | } 71 | 72 | export type MonocleFile = File | Directory ; 73 | 74 | export class DeviceFs implements vscode.TreeDataProvider,vscode.TextDocumentContentProvider , vscode.TreeDragAndDropController { 75 | dropMimeTypes = ['application/vnd.code.workbench.explorer.fileView']; 76 | dragMimeTypes = []; 77 | root = new Directory(''); 78 | // --- manage file metadata 79 | data:Map = new Map(); 80 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 81 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 82 | private dragDataEmitter = new vscode.EventEmitter(); 83 | public readonly onDragData = this.dragDataEmitter.event; 84 | refresh(): void { 85 | this.data = new Map(); 86 | this._onDidChangeTreeData.fire(); 87 | } 88 | 89 | public async handleDrop(target: any, sources: any, token: vscode.CancellationToken): Promise { 90 | const transferItem = sources.get('application/vnd.code.tree.explorer'); 91 | if (!transferItem) { 92 | return; 93 | } 94 | } 95 | // for drag 96 | public async handleDrag(source: File[], treeDataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { 97 | // treeDataTransfer.set('application/vnd.code.tree.snippettemplates', new vscode.DataTransferItem(source[0].label)); 98 | return ; 99 | } 100 | async addFile(uri:vscode.Uri,devicePath:string){ 101 | const basename = path.posix.basename(devicePath); 102 | let file = await vscode.workspace.fs.stat(uri); 103 | 104 | vscode.window.withProgress({ 105 | location: {viewId:"fileExplorer"}, 106 | cancellable: false, 107 | }, async (progress,canceled) => { 108 | if(file.type===vscode.FileType.Directory){ 109 | // here needs to create directory in monocle 110 | if(await createDirectoryDevice(devicePath)){ 111 | this.refresh(); 112 | } 113 | }else if(file.type===vscode.FileType.File){ 114 | // here needs to create file in monocle 115 | if(await creatUpdateFileDevice(uri, devicePath)){ 116 | this.refresh(); 117 | } 118 | } 119 | }); 120 | } 121 | async updateFile(uri:vscode.Uri,devicePath:string,refresh=false){ 122 | vscode.window.withProgress({ 123 | location: {viewId:"fileExplorer"}, 124 | cancellable: false, 125 | }, async (progress,canceled) => { 126 | if(await creatUpdateFileDevice(uri, devicePath)){ 127 | if(refresh){ 128 | this.refresh(); 129 | } 130 | } 131 | }); 132 | } 133 | async buildFiles(fileMAps:FileMaps[],refresh=false){ 134 | vscode.window.withProgress({ 135 | location: {viewId:"fileExplorer"}, 136 | cancellable: false, 137 | }, async (progress,canceled) => { 138 | if(await buildMappedFiles(fileMAps)){ 139 | if(refresh){ 140 | this.refresh(); 141 | } 142 | } 143 | }); 144 | } 145 | async updateFileBulk(files:vscode.Uri[],devicePath:string){ 146 | vscode.window.withProgress({ 147 | location: {viewId:"fileExplorer"}, 148 | cancellable: false, 149 | }, async (progress,canceled) => { 150 | if(await uploadFileBulkDevice(files,devicePath)){ 151 | this.refresh(); 152 | }; 153 | }); 154 | 155 | } 156 | async renameFile (oldDevicePath:string,newDevicePath:string){ 157 | vscode.window.withProgress({ 158 | location: {viewId:"fileExplorer"}, 159 | cancellable: false, 160 | }, async (progress,canceled) => { 161 | if(await renameFileDevice(oldDevicePath, newDevicePath)){ 162 | this.refresh(); 163 | } 164 | }); 165 | 166 | 167 | } 168 | private async _dowloadRecursive(devicePath:string, localPath:vscode.Uri){ 169 | if(this.data.get(devicePath) instanceof Directory){ 170 | let subDirectory = await listFilesDevice(devicePath); 171 | for (let index = 0; index < subDirectory.length; index++) { 172 | const dFile:any = subDirectory[index]; 173 | const rootPath = devicePath+"/"+dFile.name; 174 | const _localPath = vscode.Uri.joinPath(localPath, rootPath); 175 | if(dFile.file){ 176 | let content = await this.readFile(rootPath); 177 | if(content!=='NOTFOUND' && typeof content!=='boolean'){ 178 | await vscode.workspace.fs.writeFile(_localPath,Buffer.from(content)); 179 | } 180 | }else{ 181 | await this._dowloadRecursive(rootPath,localPath); 182 | } 183 | 184 | } 185 | } 186 | } 187 | async downloadDirectory(devicePath:string, localPath:vscode.Uri){ 188 | vscode.window.withProgress({ 189 | location: {viewId:"fileExplorer"}, 190 | cancellable: false, 191 | }, async (progress,canceled) => { 192 | await this._dowloadRecursive(devicePath,localPath); 193 | }); 194 | } 195 | async readFile (devicePath:string):Promise{ 196 | 197 | let data = await readFileDevice(devicePath); 198 | if(typeof data ==='string'){ 199 | return data; 200 | }else{ 201 | 202 | return false; 203 | } 204 | 205 | 206 | } 207 | 208 | async deleteFile(devicePath:string){ 209 | 210 | vscode.window.withProgress({ 211 | location: {viewId:"fileExplorer"}, 212 | cancellable: false, 213 | }, async (progress,canceled) => { 214 | if(await deleteFilesDevice(devicePath)){ 215 | this.data.delete(devicePath); 216 | this.refresh(); 217 | } 218 | }); 219 | 220 | } 221 | async provideTextDocumentContent(uri: vscode.Uri): Promise { 222 | // simply invoke cowsay, use uri-path as text 223 | let data = await this.readFile(uri.path); 224 | if(typeof data === 'string'){ 225 | return data; 226 | }else{ 227 | vscode.window.showErrorMessage('couldn\'t read file'); 228 | return "NOTFOUND"; 229 | } 230 | } 231 | getTreeItem(element: MonocleFile): vscode.TreeItem { 232 | return element; 233 | } 234 | getParent(element: MonocleFile): vscode.ProviderResult { 235 | if(element.path){ 236 | let paths = element.path.split('/'); 237 | if(paths.length>1){ 238 | return this.data.get(paths.slice(0,paths.length-1).join("/")); 239 | }else{ 240 | return this.data.get(paths[0]); 241 | } 242 | } 243 | return undefined; 244 | } 245 | async getChildren(element?: MonocleFile): Promise { 246 | let files:MonocleFile[]= []; 247 | if(!isConnected()){ 248 | return []; 249 | } 250 | let rootPath = ""; 251 | if (element) { 252 | if (element instanceof Directory) { 253 | let subDirectory = await listFilesDevice(element.path); 254 | 255 | subDirectory.forEach((f:any)=>{ 256 | if(element.path){ 257 | rootPath = element.path+"/"+f.name; 258 | } 259 | let entry:MonocleFile; 260 | f.file? entry = new File(f.name,rootPath,vscode.TreeItemCollapsibleState.None) 261 | :entry = new Directory(f.name,rootPath,vscode.TreeItemCollapsibleState.Collapsed); 262 | files.push(entry); 263 | this.data.set(rootPath,entry); 264 | }); 265 | 266 | return files; 267 | }else{ 268 | return [element]; 269 | }; 270 | 271 | } 272 | 273 | 274 | let topDirectory = await listFilesDevice(); 275 | 276 | topDirectory.forEach((f:any)=>{ 277 | rootPath = f.name; 278 | let entry:MonocleFile; 279 | f.file? entry = new File(f.name,rootPath,vscode.TreeItemCollapsibleState.None) 280 | :entry = new Directory(f.name,rootPath,vscode.TreeItemCollapsibleState.Collapsed); 281 | files.push(entry); 282 | this.data.set(rootPath,entry); 283 | }); 284 | return files; 285 | 286 | } 287 | 288 | } 289 | 290 | 291 | export class ScreenProvider implements vscode.TreeDataProvider{ 292 | 293 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 294 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 295 | 296 | refresh(): void { 297 | this._onDidChangeTreeData.fire(); 298 | } 299 | getTreeItem(element: vscode.TreeItem): vscode.TreeItem | Thenable { 300 | return element; 301 | } 302 | getChildren(element?: vscode.TreeItem | undefined): vscode.ProviderResult { 303 | if(element){ 304 | return []; 305 | } 306 | return this.getAllScreens(); 307 | } 308 | private async getAllScreens(){ 309 | if(vscode.workspace.workspaceFolders){ 310 | const rootUri = vscode.workspace.workspaceFolders[0].uri; 311 | // const localFiles = vscode.Uri.joinPath(rootUri,screenFolder); 312 | 313 | if(await isPathExist(rootUri)){ 314 | // const screenFiles = new vscode.RelativePattern(rootUri, '*_screen.py'); 315 | let finalList:vscode.TreeItem[]=[]; 316 | let screensObjs = await configScreenReadUpdate(); 317 | for (const [key, value] of Object.entries(screensObjs)) { 318 | try { 319 | let uri = vscode.Uri.joinPath(rootUri,value.filePath); 320 | if(await isPathExist(uri)){ 321 | let newitem = new vscode.TreeItem(key.replace('.py',''),vscode.TreeItemCollapsibleState.None); 322 | newitem.iconPath = vscode.ThemeIcon.File; 323 | newitem.command = {command:"brilliant-ar-studio.editUIEditor",title:"Edit In UI Editor",arguments:[{"uri":uri,"name":key.replace('.py','')}]}; 324 | finalList.push(newitem); 325 | } 326 | } catch (error) { 327 | console.log(error); 328 | } 329 | 330 | } 331 | return finalList; 332 | }else{ 333 | 334 | // vscode.window.showWarningMessage("Project not initialized!"); 335 | return []; 336 | } 337 | }else{ 338 | return []; 339 | } 340 | 341 | } 342 | } 343 | 344 | 345 | export class DeviceInfoProvider implements vscode.WebviewViewProvider{ 346 | 347 | private _view?: vscode.WebviewView; 348 | private _currentInfo:any; 349 | constructor( 350 | private readonly _extensionUri: vscode.Uri, 351 | ) { 352 | this._extensionUri = _extensionUri; 353 | } 354 | public updateValues(data:object){ 355 | this._view?.webview.postMessage(data); 356 | this._currentInfo= data; 357 | } 358 | 359 | 360 | private _setWebviewMessageListener(webview: vscode.Webview) { 361 | webview.onDidReceiveMessage( 362 | (message: any) => { 363 | switch (message) { 364 | case "fpgaUpdate": 365 | vscode.commands.executeCommand('brilliant-ar-studio.fpgaUpdate'); 366 | break; 367 | case "customFpga": 368 | vscode.commands.executeCommand('brilliant-ar-studio.fpgaUpdateCustom'); 369 | break; 370 | case "firmwareUpdate": 371 | let deviceName = String(deviceInfo.name).toLowerCase(); 372 | startFirmwareUpdate(deviceName); 373 | break; 374 | default: 375 | break; 376 | } 377 | } 378 | ); 379 | } 380 | public resolveWebviewView (webviewView: vscode.WebviewView, contextWebview: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken){ 381 | webviewView.webview.options={ 382 | enableScripts:true,localResourceRoots: [ 383 | this._extensionUri, 384 | ] 385 | }; 386 | let updateScript:string = ""; 387 | if(this._currentInfo){ 388 | updateScript = 'updateUi(JSON.parse('+JSON.stringify(this._currentInfo)+'))'; 389 | } 390 | this._view = webviewView; 391 | webviewView.webview.html=` 392 | 393 | 394 | 395 | 396 | Device Status 397 | 435 | 436 | 437 | 438 |
439 | Device:
440 | MAC address:
441 | Firmware version:
442 | 443 | FPGA image:
444 | 445 | Custom FPGA 446 |
447 | 507 | 508 | `; 509 | this._setWebviewMessageListener(webviewView.webview); 510 | setTimeout(()=>this.updateValues(this._currentInfo),500); 511 | } 512 | 513 | }; 514 | -------------------------------------------------------------------------------- /src/UIEditorPanel.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { isPathExist } from "./extension"; 3 | const CUSTOMCOLOR:any = { 4 | RED: '#ad2323', 5 | GREEN: '#1d6914', 6 | BLUE: '#2a4bd7', 7 | CYAN: '#29d0d0', 8 | MAGENTA: '#8126c0', 9 | YELLOW: '#ffee33', 10 | WHITE: '#ffffff', 11 | GRAY1: '#1c1c1c', 12 | GRAY2: '#383838', 13 | GRAY3: '#555555', 14 | GRAY4: '#717171', 15 | GRAY5: '#8d8d8d', 16 | GRAY6: '#aaaaaa', 17 | GRAY7: '#c6c6c6', 18 | GRAY8: '#e2e2e2', 19 | }; 20 | export class UIEditorPanel { 21 | public static currentPanel: UIEditorPanel | undefined; 22 | private readonly _panel: vscode.WebviewPanel; 23 | private _disposables: vscode.Disposable[] = []; 24 | private screenName: string; 25 | private screenPath: vscode.Uri; 26 | 27 | private constructor( 28 | panel: vscode.WebviewPanel, 29 | extensionUri: vscode.Uri, 30 | screenName: string, 31 | screenPath: vscode.Uri 32 | ) { 33 | this._panel = panel; 34 | this.screenName = screenName; 35 | this.screenPath = screenPath; 36 | this._panel.onDidDispose(() => this.dispose(), null, this._disposables); 37 | this._panel.webview.html = this._getWebviewContent( 38 | this._panel.webview, 39 | extensionUri 40 | ); 41 | this._setWebviewMessageListener(this._panel.webview); 42 | this.updatePy([], true); 43 | this._disposables.push( 44 | vscode.workspace.onDidSaveTextDocument((e:vscode.TextDocument)=>{ 45 | if (e.uri.fsPath===this.screenPath.fsPath){ 46 | this.updateGUI(); 47 | } 48 | }) 49 | ); 50 | } 51 | 52 | public static render( 53 | extensionUri: vscode.Uri, 54 | screenName: string, 55 | screenPath: vscode.Uri 56 | ) { 57 | if (UIEditorPanel.currentPanel) { 58 | UIEditorPanel.currentPanel.dispose(); 59 | } 60 | 61 | const panel = vscode.window.createWebviewPanel( 62 | screenName, 63 | screenName, 64 | vscode.ViewColumn.Two, 65 | { 66 | // Enable javascript in the webview 67 | enableScripts: true, 68 | // Restrict the webview to only load resources from the `out` directory 69 | localResourceRoots: [vscode.Uri.joinPath(extensionUri, "media")], 70 | } 71 | ); 72 | 73 | UIEditorPanel.currentPanel = new UIEditorPanel( 74 | panel, 75 | extensionUri, 76 | screenName, 77 | screenPath 78 | ); 79 | } 80 | public dispose() { 81 | UIEditorPanel.currentPanel = undefined; 82 | 83 | this._panel.dispose(); 84 | 85 | while (this._disposables.length) { 86 | const disposable = this._disposables.pop(); 87 | if (disposable) { 88 | disposable.dispose(); 89 | } 90 | } 91 | } 92 | private _getWebviewContent( 93 | webview: vscode.Webview, 94 | extensionUri: vscode.Uri 95 | ) { 96 | // Tip: Install the es6-string-html VS Code extension to enable code highlighting below 97 | const webviewUri = getUri(webview, extensionUri, ["media", "conva.min.js"]); 98 | const mainJsUri = getUri(webview, extensionUri, ["media", "main.js"]); 99 | const nonce = getNonce(); 100 | const stylesMainUri = getUri(webview, extensionUri, ["media", "main.css"]); 101 | // const fontawesomeUri = getUri(webview, extensionUri, ["media" ,"fontawesome.css"]); 102 | const thickness = getUri(webview, extensionUri, [ 103 | "media", 104 | "icons", 105 | "thickness_icon.svg", 106 | ]); 107 | const top = getUri(webview, extensionUri, ["media", "icons", "top.svg"]); 108 | const left = getUri(webview, extensionUri, ["media", "icons", "left.svg"]); 109 | const center = getUri(webview, extensionUri, [ 110 | "media", 111 | "icons", 112 | "center.svg", 113 | ]); 114 | const middle = getUri(webview, extensionUri, [ 115 | "media", 116 | "icons", 117 | "middle.svg", 118 | ]); 119 | const bottom = getUri(webview, extensionUri, [ 120 | "media", 121 | "icons", 122 | "bottom.svg", 123 | ]); 124 | const right = getUri(webview, extensionUri, [ 125 | "media", 126 | "icons", 127 | "right.svg", 128 | ]); 129 | const rect = getUri(webview, extensionUri, ["media", "icons", "rect.svg"]); 130 | const polygon = getUri(webview, extensionUri, [ 131 | "media", 132 | "icons", 133 | "polygon.png", 134 | ]); 135 | const line = getUri(webview, extensionUri, ["media", "icons", "line.svg"]); 136 | const polyline = getUri(webview, extensionUri, [ 137 | "media", 138 | "icons", 139 | "polyline.png", 140 | ]); 141 | const text = getUri(webview, extensionUri, ["media", "icons", "text.svg"]); 142 | const eraser = getUri(webview, extensionUri, [ 143 | "media", 144 | "icons", 145 | "eraser.png", 146 | ]); 147 | const paintBrush = getUri(webview, extensionUri, [ 148 | "media", 149 | "icons", 150 | "paintbrush.png", 151 | ]); 152 | 153 | // const fontUri = getUri(webview, extensionUri, ["media" ,"JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf"]); 154 | return /*html*/ ` 155 | 156 | 157 | 158 | 159 | 160 | Konva Select and Transform Demo 161 | 162 | 163 | 164 |
165 |
166 |
167 | Shapes 168 |
169 |
170 | 171 |
172 |
173 | 174 |
175 |
176 | 177 |
178 |
179 | 180 |
181 |
182 | 184 |
185 |
186 |
187 |
188 | tools and options 189 |
190 |
191 |
192 |
193 |
194 | 195 | 196 | 197 |
198 |
199 | 200 | 201 |
202 | 203 |
204 |
205 | 206 | 207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 | 254 |
255 |
256 |
257 |
258 |
259 | 263 | 276 |
277 |
278 |
279 | 280 |
281 |
282 | 283 |
284 |
285 | 286 |
287 |
288 | 289 |
290 |
291 | 292 |
293 |
294 | 295 |
296 |
297 | 298 |
299 |
300 |
301 | 302 |
303 |
304 |
305 | 306 | 307 | 308 | 309 | 310 | `; 311 | } 312 | private _setWebviewMessageListener(webview: vscode.Webview) { 313 | webview.onDidReceiveMessage( 314 | (message: any) => { 315 | this.updatePy(message); 316 | }, 317 | undefined, 318 | this._disposables 319 | ); 320 | } 321 | 322 | public async updateGUI() { 323 | let existingDataBin = await vscode.workspace.fs.readFile(this.screenPath); 324 | try { 325 | let existingData = existingDataBin.toString(); 326 | let allblocks = existingData.slice( 327 | existingData.indexOf("screen=[") + 8, 328 | existingData.indexOf("]##") 329 | ); 330 | let allblocksArray = allblocks 331 | .split("\t\td.") 332 | .filter((d) => d !== "") 333 | .map((d) => d.trim()); 334 | let uiObjs: object[] = []; 335 | allblocksArray.forEach((block) => { 336 | let shape = block.slice(0, block.indexOf("(")); 337 | if (shape === "Rectangle") { 338 | let attrs = block 339 | .slice(block.indexOf("(") + 1, block.indexOf(")")) 340 | .split(",") 341 | .map((d) => d.trim()); 342 | let colorname = String(attrs[4]).includes("d.")?String(attrs[4]).replace("d.",""):false; 343 | let colorvalue = "#" + attrs[4].replace("0x", ""); 344 | if(colorname){ 345 | colorvalue= CUSTOMCOLOR[colorname]; 346 | } 347 | uiObjs.push({ 348 | name: "rect", 349 | x: parseInt(attrs[0]), 350 | y: parseInt(attrs[1]), 351 | width: parseInt(attrs[2]) - parseInt(attrs[0]), 352 | height: parseInt(attrs[3]) - parseInt(attrs[1]), 353 | fill:colorvalue, 354 | draggable: true, 355 | colorname 356 | }); 357 | } 358 | if (shape === "Line") { 359 | let attrs = block 360 | .slice(block.indexOf("(") + 1, block.indexOf(")")) 361 | .split(",") 362 | .map((d) => d.trim()); 363 | let colorname = String(attrs[4]).includes("d.")?String(attrs[4]).replace("d.",""):false; 364 | let colorvalue = "#" + attrs[4].replace("0x", ""); 365 | if(colorname){ 366 | colorvalue= CUSTOMCOLOR[colorname]; 367 | } 368 | uiObjs.push({ 369 | name: "line", 370 | points: [ 371 | parseInt(attrs[0]), 372 | parseInt(attrs[1]), 373 | parseInt(attrs[2]), 374 | parseInt(attrs[3]), 375 | ], 376 | stroke: colorvalue, 377 | strokeWidth: parseInt( 378 | attrs[5].replaceAll(" ", "").replace("thickness=", "") 379 | ), 380 | colorname 381 | }); 382 | } 383 | if (shape === "Polyline") { 384 | let points = block.slice(block.indexOf("["), block.indexOf("]") + 1); 385 | let newblock = block.replace(points, ""); 386 | let attrs = newblock 387 | .slice(newblock.indexOf("(") + 1, newblock.indexOf(")")) 388 | .split(",") 389 | .filter((d) => d !== "") 390 | .map((d) => d.trim()); 391 | let colorname = String(attrs[0]).includes("d.")?String(attrs[0]).replace("d.",""):false; 392 | let colorvalue = "#" + attrs[0].replace("0x", ""); 393 | if(colorname){ 394 | colorvalue= CUSTOMCOLOR[colorname]; 395 | } 396 | uiObjs.push({ 397 | name: "polyline", 398 | points: JSON.parse(points), 399 | stroke: colorvalue, 400 | strokeWidth: parseInt( 401 | attrs[1].replaceAll(" ", "").replace("thickness=", "") 402 | ), 403 | colorname 404 | }); 405 | } 406 | if (shape === "Polygon") { 407 | let points = block.slice(block.indexOf("["), block.indexOf("]") + 1); 408 | let newblock = block.replace(points, ""); 409 | let attrs = newblock 410 | .slice(newblock.indexOf("(") + 1, newblock.indexOf(")")) 411 | .split(",") 412 | .filter((d) => d !== "") 413 | .map((d) => d.trim()); 414 | let colorname = String(attrs[0]).includes("d.")?String(attrs[0]).replace("d.",""):false; 415 | let colorvalue = "#" + attrs[0].replace("0x", ""); 416 | if(colorname){ 417 | colorvalue= CUSTOMCOLOR[colorname]; 418 | } 419 | uiObjs.push({ 420 | name: "polygone", 421 | points: JSON.parse(points), 422 | fill: colorvalue, 423 | stroke: colorvalue, 424 | strokeWidth: 1, 425 | closed: true, 426 | colorname 427 | }); 428 | } 429 | if (shape === "Text") { 430 | let attrs = block 431 | .slice(block.indexOf("(") + 1, block.indexOf(")")) 432 | .split(",") 433 | .map((d) => d.trim()); 434 | let colorname = String(attrs[3]).includes("d.")?String(attrs[3]).replace("d.",""):false; 435 | let colorvalue = "#" + attrs[3].replace("0x", ""); 436 | if(colorname){ 437 | colorvalue= CUSTOMCOLOR[colorname]; 438 | } 439 | uiObjs.push({ 440 | name: "text", 441 | text: attrs[0].slice(1, attrs[0].length - 1), 442 | x: parseInt(attrs[1]), 443 | y: parseInt(attrs[2]), 444 | fill: colorvalue, 445 | alignment: attrs[4].replaceAll(" ", "").replace("justify=d.", ""), 446 | draggable: true, 447 | colorname 448 | }); 449 | } 450 | }); 451 | this._panel.webview.postMessage(uiObjs); 452 | } catch (error) { 453 | console.log(error); 454 | vscode.window.showErrorMessage( 455 | "File parsing failed! probably unknown structure" 456 | ); 457 | } 458 | } 459 | public async updatePy(data: object[] = [], firstload = false) { 460 | if ( 461 | (await isPathExist(this.screenPath)) && 462 | data.length === 0 && 463 | firstload 464 | ) { 465 | this.updateGUI(); 466 | } else { 467 | let pystring = gUItoPython(data, this.screenName); 468 | vscode.workspace.fs.writeFile(this.screenPath, Buffer.from(pystring)); 469 | } 470 | } 471 | } 472 | 473 | export function getUri( 474 | webview: vscode.Webview, 475 | extensionUri: vscode.Uri, 476 | pathList: string[] 477 | ) { 478 | return webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, ...pathList)); 479 | } 480 | export function getNonce() { 481 | let text = ""; 482 | const possible = 483 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 484 | for (let i = 0; i < 32; i++) { 485 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 486 | } 487 | return text; 488 | } 489 | 490 | function gUItoPython(data: object[], screenName: string) { 491 | const initialMessage = 492 | "# GENERATED BY BRILLIANT AR STUDIO Do not modify this file directly\nimport display as d\n\n"; 493 | let finalPyString = ""; 494 | if (data.length === 0) { 495 | finalPyString += 496 | initialMessage + 497 | "class " + 498 | screenName + 499 | ":\n\tblocks=[]## dont't remove this #"; 500 | } else { 501 | finalPyString += initialMessage + "class " + screenName + ":\n\tscreen=["; 502 | } 503 | data.forEach((uiElement: any, index: number) => { 504 | let colorname= uiElement.colorname||undefined; 505 | let colorvalue = ''; 506 | if(uiElement.name==='line' || uiElement.name==='polyline'){ 507 | colorvalue = "0x"+uiElement.stroke.replace("#",""); 508 | }else{ 509 | colorvalue = "0x"+uiElement.fill.replace("#",""); 510 | } 511 | if(colorname){ 512 | colorvalue = "d."+colorname; 513 | } 514 | if (uiElement.name === "rect") { 515 | finalPyString += `\n\t\td.Rectangle(${Math.round( 516 | uiElement.x 517 | )}, ${Math.round(uiElement.y)}, ${Math.round( 518 | uiElement.x + uiElement.width 519 | )}, ${Math.round( 520 | uiElement.y + uiElement.height 521 | )}, ${colorvalue}),`; 522 | } 523 | if (uiElement.name === "line") { 524 | finalPyString += `\n\t\td.Line(${Math.round( 525 | uiElement.points[0] 526 | )}, ${Math.round(uiElement.points[1])}, ${Math.round( 527 | uiElement.points[2] 528 | )}, ${Math.round(uiElement.points[3])}, ${colorvalue}, thickness=${uiElement.strokeWidth}),`; 529 | } 530 | if (uiElement.name === "polyline") { 531 | finalPyString += `\n\t\td.Polyline([${uiElement.points 532 | .map((point: number) => Math.round(point)) 533 | .join(",")}], ${colorvalue}, thickness=${ 534 | uiElement.strokeWidth 535 | }),`; 536 | } 537 | if (uiElement.name === "polygone") { 538 | finalPyString += `\n\t\td.Polygon([${uiElement.points 539 | .map((point: number) => Math.round(point)) 540 | .join(",")}], ${colorvalue}),`; 541 | } 542 | if (uiElement.name === "text") { 543 | finalPyString += `\n\t\td.Text('${uiElement.text}', ${Math.round( 544 | uiElement.x 545 | )}, ${Math.round(uiElement.y)}, ${colorvalue}, justify=d.${uiElement.alignment}),`; 546 | } 547 | }); 548 | if (data.length !== 0) { 549 | finalPyString += "\n\t]## dont't remove this #"; 550 | } 551 | // finalPyString += `\n\tdef __init__(self):\n\t\td.show(self.blocks)`; 552 | 553 | // finalPyString += 554 | // "\n\n\n# To use this in main.py screen import into main.py or copy below code in main.py\n# from screens." + 555 | // screenName + 556 | // "_screen import " + 557 | // screenName + 558 | // "\nif __name__=='__main__':\n\t" + 559 | // screenName + 560 | // "()"; 561 | return finalPyString; 562 | } 563 | 564 | // 577 | --------------------------------------------------------------------------------