├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── Dockerfile ├── FAQ.md ├── LICENSE ├── README.md ├── default-data ├── inventory.json └── settings.json ├── less └── pterm.less ├── node-client ├── .gitignore ├── install.sh ├── package.json └── src │ ├── sessions.js │ ├── vec-cli.js │ └── vectorBluetooth.js ├── package.json ├── rts-js ├── bleMessageProtocol.js ├── blesh.js ├── clad.js ├── main.js ├── messageExternalComms.js ├── rtsCliUtil.js ├── rtsV2Handler.js ├── rtsV3Handler.js ├── rtsV4Handler.js ├── rtsV5Handler.js ├── rtsV6Handler.js ├── sessions.js ├── settings.js ├── stack.js └── vectorBluetooth.js ├── site ├── css │ ├── bootstrap │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ └── vecSetup.css ├── images │ ├── AnkiLogo.svg │ ├── VectorLogo.svg │ ├── chrome.png │ ├── common_icon_bluetooth_med.svg │ ├── common_icon_updatecloud_sml.svg │ ├── common_statusicon_cloudready_sml.svg │ ├── common_statusicon_wificonnect_med.svg │ ├── ddl_logo.png │ ├── ddl_logo_sm.png │ ├── fontawesome │ │ ├── bluetooth-brands.svg │ │ ├── cloud-download-alt-solid.svg │ │ ├── exclamation-solid.svg │ │ ├── eye-slash-solid.svg │ │ ├── eye-solid.svg │ │ ├── layer-group-solid.svg │ │ ├── sd-card-solid.svg │ │ ├── sliders-h-solid.svg │ │ ├── user-circle-solid.svg │ │ └── wifi-solid.svg │ ├── friends_icon_profileheadshot_sml.svg │ ├── icon_accountManage.svg │ ├── icon_settings.svg │ ├── nav_icon_backpacklights_sml.svg │ ├── onboarding_icon_plugincharger_med.svg │ ├── onboarding_icon_pressbackpack_med.svg │ ├── settings_icon_lock_mini.svg │ ├── settings_icon_wifilife_1bars_mini.svg │ ├── settings_icon_wifilife_2bars_mini.svg │ ├── settings_icon_wifilife_3bars_mini.svg │ └── statlog_icon_checkmark_success_mini.svg ├── index.html └── js │ ├── bootstrap │ ├── bootstrap.bundle.min.js │ └── bootstrap.bundle.min.js.map │ ├── jquery.min.3.3.1.js │ ├── pterm.js │ └── sodium.js ├── src-node-client └── blesh.js ├── templates └── main.ejs ├── test └── vector-setup │ ├── addOta.e2e.js │ ├── approveOta.e2e.js │ └── index.js ├── tools ├── common.js ├── configure.js ├── inventory │ ├── firmwareStore.js │ ├── index.js │ └── ota.js ├── ota.js └── run.js ├── utils ├── clicmd.js ├── fsPromise.js ├── hash.js ├── ip.js └── rmdir.js └── vector-web-setup.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | site/js/rts.js 3 | site/data/* 4 | site/firmware/* 5 | site/js/bundle-data.js 6 | site/css/pterm.css 7 | .vscode 8 | **/node_modules 9 | **/package-lock.json 10 | 11 | # Local .terraform directories 12 | **/.terraform/* 13 | 14 | # .tfstate files 15 | *.tfstate 16 | *.tfstate.* 17 | 18 | .prettierignore 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | * Added inventory.json file check in serve command 2 | 3 | 1.1.0 4 | ===== 5 | 6 | * Updated login screen to clarify that your username is your email. 7 | 8 | * When settings.json has a single stack, stack selection screen is 9 | skipped automatically . 10 | 11 | * Fized typo so npm run vector-setup works. 12 | 13 | * Added a stock Docker file for users who prefer docker deployments. 14 | 15 | * Users can now initiate vector authorization by 16 | pressing enter or clicking on sign-in. 17 | 18 | * Fixed usage string to reflect use of packaged node.js app rather 19 | than source checkout execution. 20 | 21 | * Fix for an issue where some computer network settings explicitly 22 | require binding to 0.0.0.0 to expose the web app to Vector robots 23 | and added an option to override the 0.0.0.0 IP for users with unique 24 | network configuration. 25 | 26 | * Users can now specify the TCP/IP port to run the app on to avoid conflicts 27 | with existing applications that use port 8000. 28 | 29 | * We now issue a graceful error message when the program attempts to 30 | bind to a TCP/IP port in use by another process. 31 | 32 | * Fixed documentation error that used the incorrect command for 33 | `vector-web-setup ota-approve`. 34 | 35 | 1.0.0 36 | ===== 37 | 38 | * Initial public release. -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | In addition to the staff at Anki and Digital Dream Labs, the following people have provided code to improve the software: 4 | 5 | * Lee Alexis 6 | * Gavin Hamill 7 | * Anant Kaushik -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts 2 | RUN npm install -g digital-dream-labs/vector-web-setup 3 | RUN vector-web-setup configure 4 | RUN vector-web-setup ota-sync 5 | ENV PORT 8000 6 | EXPOSE 8000 7 | CMD vector-web-setup serve -p $PORT 8 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## Why is Chrome the only browser supported? 4 | 5 | We use the BluetoothLE standard to communicate with Vector. Chrome provides the most comprehensive support for the big four browers (Chrome, IE, Safari, Firefox) and is the only one that is functional as of the time of writing. We do make a **capabilities check** for Bluetooth support and not a **software check** for Chrome so if another less known browser supports the standard, or another browser implements the capabilities in the future the software should run and bypass the screen saying you need to use chrome. 6 | 7 | ## Why doesn't my connection work when serving the software from a location other than http://localhost:8000 ? 8 | 9 | There are two conflicting security concerns: 10 | 11 | 1. Chrome's implementation of Bluetooth only allows it to run on https so that someone can not exploit a bluetooth connection. However for development purposes they let a developer have access to the functionality on localhost, but **only on http**. so we can either run without a TLS connection on localhost, or with one on a real public-facing domain. 12 | 13 | 2. Our cloud-side stack implements CORS based security which allows restricted access to the resources based on the domain name that contacts us. For our stack we only want to allow authorized clients we know about, or someone running locally on their machine where they understand the risks. 14 | 15 | When future users are running the Escape Pod and variant software they will have control over CORS settings and will be able to configure their domains appropriately. 16 | 17 | ## But why do you use Bluetooth and not ssh/https/ftp/etc when it has all these limitations ? 18 | 19 | There are two reasons for this: 20 | 21 | 1. Security is a paramount concern for a fleet of over a million robots that sit in people's homes and have a camera and microphone. We take security and privacy very seriously. Using bluetooth combined with a pairing protocol ensures that the device is being configured by a real person in physical proximity of the robot and not a malicious actor, spyware, virus, etc, from a country of unknown origin. 22 | 23 | 2. Although later versions of Vector's software allow limited admin communications via an https interface this is not installed on the **factory firmware** that is initially loaded on to the Vector. This software is intentionally minimal and we keep the same version on every robot so that upgrades will behave predictably. It is important that our software is always able to do a full update on a Vector with the factory firmware so we always assume we have a minimal set of capabilities. 24 | 25 | ## I've installed my favorite old version of the software, but it gets reset to the latest the next day. Can I stop this? 26 | 27 | There is currently no means to disable automatic updates. This will change as Digital Dream Labs roles out the full escape pod. For now advanced users can disable automatic updates by modifying their local DNS server or modifying a PiHole to block the endpoints where the vector tries to receive the update so it can't download a new file. If you understand what all this means the domains to block are: 28 | 29 | * ota.global.anki-services.com 30 | * ota-cdn.anki.com 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ----------- 3 | 4 | Copyright (c) 2019-2020 Digital Dream Labs (https://www.digitaldreamlabs.com/) 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vector Web Setup 2 | 3 | Vector Web Setup provides an open source tool to allow users of Vector 4 | to configure their robot without relying on the proprietary phone 5 | application that previously provided the only method a user could use 6 | to configure their robot. 7 | 8 | As Digital Dream Labs releases both the Escape Pod and OSKR code it is 9 | anticipated this tool will become an important part of the system by 10 | which users deploy both their own server side code, and their own 11 | custom software images to the robot. 12 | 13 | For now it simply provides an alternative to the existing phone 14 | application and allows users to maintain local copies of the operating 15 | system images for redundancy purposes. 16 | 17 | The software is written in [Node.js](https://nodejs.org) and should run anywhere you can 18 | run Node.js. It is tested on Windows, Mac OSX, and Linux. 19 | 20 | ## Normal End-User Usage 21 | 22 | Most users will simply want to run a copy of the web server locally to 23 | interact with their robot. They will not need to use github to do 24 | this. 25 | 26 | One-time install: 27 | 28 | 1. Install [Node.js](https://nodejs.org/en/download/), however that is done on their system. 29 | 1. Install vector-web-setup package: `npm install -g vector-web-setup` 30 | 1. Perform an initial configuration: `vector-web-setup configure` 31 | 1. Perform a local sync of software files: `vector-web-setup ota-sync` 32 | 33 | Daily usage: 34 | 35 | 1. Start the web-server: `vector-web-setup serve` 36 | 1. Open a Chrome Browser and go to http://localhost:8000/. 37 | 1. Follow the instructions provided by the web application. 38 | 39 | > NOTE: 40 | The application talks to the robot via Bluetooth Low-Energe protocol (BLE). There is a 41 | standard for browsers to support this but it is currently only 42 | **implemented on Chrome**. Until that changes, use of the Chrome browser is 43 | required. BLE is only enabled on `https://` sites or `http://localhost`. 44 | 45 | ## Advanced - Admin usage 46 | 47 | As we release firmware to unlock OSKR robots or other alternate 48 | firmwares users may wish to install different firmwares for 49 | installation. There is a two-step process for this. First a file is 50 | downloaded and included in the manifest. After verifying that the file 51 | has downloaded correctly and completely it is signed with a 52 | checksum. This allows future users to distribute their own 53 | configurations to other users. 54 | 55 | You can view all supported capabilities by using --help parameter: `vector-web-setup --help` 56 | 57 | ### Custom port 58 | 59 | You can override the default 8000 port with your own by specifying a non-zero number up to 65535 (avoid using [reserved ports](https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers) 60 | 61 | Example: `vector-web-setup serve -p 7010` will serve the website at http://localhost:7010. 62 | 63 | 64 | ## **Examples** 65 | 66 | ### Example: use GooeyChickenman archives 67 | 68 | There are backups of the firmware available via the user 69 | GooeyChickenman on github. Lets pretend that for some reason the 70 | official copies of the firmware are down, and you want to use the 71 | GooeyChickenman files as a replacement: 72 | 73 | 1. Add the new file to the inventory: `vector-web-setup ota-add https://github.com/GooeyChickenman/victor/raw/master/firmware/prod/1.6.0.3331.ota` 74 | 1. Download the file: `vector-web-setup ota-sync` 75 | 1. Install it on a robot by running the software and selecting the new 76 | file. 77 | 1. Sign the file after you've verified it's good: `vector-web-setup ota-approve 1.6.0.3331.ota` 78 | 79 | ### Example: Add OSKR image locally 80 | 81 | ### Example: Distribute Your configuration to another user 82 | 83 | ### Example: Install another user's configuration 84 | 85 | ## Contributions 86 | 87 | Contributions from the community are always welcome! 88 | 89 | For something simple such as fixing a typo or adjusting the css layout 90 | for a certain device simply create a pull request and we'll take a 91 | look. 92 | 93 | If you have more substantial customizations or redesign it is highly 94 | recommended that you open an advisory issue to discuss with the team 95 | **before** spending significant time developing a solution that may be 96 | rejected for various reasons. 97 | 98 | Any submitted pull request should pass the test suite run with `npm 99 | test` and will hopefully have additional tests as needed. It should 100 | also include a friendly entry in `CHANGELOG.md` describing the 101 | change/enhancement/fix. 102 | 103 | And as always, the project can be forked permanently if you want to make 104 | significant changes without needing our permission! 105 | 106 | -------------------------------------------------------------------------------- /default-data/inventory.json: -------------------------------------------------------------------------------- 1 | { 2 | "prod": [ 3 | { 4 | "url": " http://ota.global.anki-services.com/vic/prod/full/latest.ota", 5 | "name": "latest.ota", 6 | "checksum": "e01e49e7183f6e0b279d875bcd6c0e39daea0ba6a418330ed3f7d14aeb850434" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /default-data/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "stacks": { 3 | "prod": { 4 | "accountEndpoints": "https://accounts.api.anki.com", 5 | "apiKeys": "luyain9ep5phahP8aph8xa" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /less/pterm.less: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details */ 2 | @size: 8px; 3 | 4 | .pterm-main { 5 | font-family: 'Ubuntu Mono', monospace; 6 | padding:20px; 7 | color:#4FED94; 8 | height:100%; 9 | box-sizing:border-box; 10 | overflow-y:scroll; 11 | } 12 | 13 | .pterm-line { 14 | display:flex; 15 | margin-bottom: 4px; 16 | } 17 | 18 | .pterm-text { 19 | white-space:pre; 20 | } 21 | 22 | .pterm-text::selection { 23 | color:#00672d; 24 | background: #FFF; 25 | } 26 | 27 | .pterm-prompt { 28 | font-size:@size * 2; 29 | margin-right:@size; 30 | } 31 | 32 | .pterm-row { 33 | font-size:@size * 2; 34 | height: @size * 2; 35 | overflow: hidden; 36 | position: relative; 37 | min-width:@size; 38 | } 39 | 40 | .pterm-cursor { 41 | background-color:#4FED94; 42 | width:@size; 43 | height:@size * 2; 44 | position:absolute; 45 | top:0; 46 | left:0; 47 | animation-name: pterm-cursor-blink; 48 | animation-duration: 1s; 49 | animation-iteration-count: infinite; 50 | animation-direction: alternate; 51 | animation-timing-function: ease-in; 52 | } 53 | 54 | .pterm-red { 55 | line-height:@size * 2; 56 | color:#ff7070; 57 | } 58 | 59 | .pterm-yellow { 60 | line-height:@size * 2; 61 | color:#f8ff86; 62 | } 63 | 64 | .pterm-green { 65 | line-height:@size * 2; 66 | color:#0F0; 67 | } 68 | 69 | .pterm-blue { 70 | line-height:@size * 2; 71 | color:#78aeff; 72 | } 73 | 74 | .pterm-row.pterm-full { 75 | width:100%; 76 | } 77 | 78 | .pterm-progress-bar { 79 | height:@size * 2; 80 | width:100%; 81 | background-color:#1e5737; 82 | } 83 | 84 | .pterm-progress-bar-track { 85 | height:@size * 2; 86 | width:0%; 87 | background-color:#4FED94; 88 | transition:width .5s; 89 | } 90 | 91 | .pterm-button { 92 | background-color:#4FED94; 93 | color:#000; 94 | height:@size * 2; 95 | border-radius:4px; 96 | } 97 | 98 | .pterm-no-transition { 99 | -webkit-transition: none !important; 100 | -moz-transition: none !important; 101 | -o-transition: none !important; 102 | transition: none !important; 103 | } 104 | 105 | @keyframes pterm-cursor-blink { 106 | from {background-color: rgba(79,237,148,0.8);} 107 | to {background-color:rgba(0,0,0,0);} 108 | } -------------------------------------------------------------------------------- /node-client/.gitignore: -------------------------------------------------------------------------------- 1 | vec-cli 2 | settings.json 3 | generated 4 | -------------------------------------------------------------------------------- /node-client/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details 4 | 5 | npm install . 6 | 7 | echo "NODE_CLIENT_PATH=\"\`dirname \\\"\$0\\\"\`\"; node \$NODE_CLIENT_PATH/src/vec-cli.js $@\$@" > vec-cli 8 | chmod +x vec-cli 9 | 10 | # src files to copy 11 | RTS_FILES=(bleMessageProtocol.js clad.js messageExternalComms.js rtsCliUtil.js \ 12 | rtsV2Handler.js rtsV3Handler.js rtsV4Handler.js rtsV5Handler.js rtsV6Handler.js) 13 | 14 | mkdir -p generated 15 | 16 | for i in "${RTS_FILES[@]}" 17 | do 18 | cp ../rts-js/$i generated/$i 19 | done 20 | 21 | cp -R ../src-node-client/. generated/ 22 | -------------------------------------------------------------------------------- /node-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cli-progress": "^2.1.1", 4 | "commander": "^2.19.0", 5 | "libsodium": "^0.7.3", 6 | "libsodium-wrappers": "^0.7.3", 7 | "mdns-js": "^1.0.3", 8 | "nconf": "^0.10.0", 9 | "noble-mac": "github:Timeular/noble-mac#master", 10 | "node-wifi": "^2.0.5", 11 | "ping": "^0.2.2", 12 | "request": "^2.88.0", 13 | "string-argv": "^0.1.1", 14 | "xpc-connection": "github:sandeepmistry/node-xpc-connection#pull/26/head" 15 | }, 16 | "resolutions": { 17 | "noble-mac": "Timeular/noble-mac#head", 18 | "xpc-connection": "sandeepmistry/node-xpc-connection#26/head" 19 | } 20 | "author": "Digital Dream Labs", 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /node-client/src/sessions.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details */ 2 | var nconf = require('nconf'); 3 | const { RtsCliUtil } = require('../generated/rtsCliUtil.js'); 4 | 5 | class Sessions { 6 | constructor() { 7 | this.sessions = {}; 8 | this.getSessions(); 9 | } 10 | 11 | getSessions() { 12 | nconf.use('file', { file: './settings.json'}); 13 | nconf.load(); 14 | let sessionStr = nconf.get("sessions"); 15 | 16 | if(sessionStr != null) { 17 | this.sessions = JSON.parse(sessionStr); 18 | if('remote-keys' in this.sessions) { 19 | let remoteKeys = Object.keys(this.sessions['remote-keys']); 20 | for(let i = 0; i < remoteKeys.length; i++) { 21 | this.sessions['remote-keys'][remoteKeys[i]].tx = 22 | Sessions.keyDictToArray(this.sessions['remote-keys'][remoteKeys[i]].tx); 23 | this.sessions['remote-keys'][remoteKeys[i]].rx = 24 | Sessions.keyDictToArray(this.sessions['remote-keys'][remoteKeys[i]].rx); 25 | } 26 | } 27 | 28 | if('id-keys' in this.sessions) { 29 | this.sessions['id-keys'].publicKey = 30 | Sessions.keyDictToArray(this.sessions['id-keys'].publicKey); 31 | this.sessions['id-keys'].privateKey = 32 | Sessions.keyDictToArray(this.sessions['id-keys'].privateKey); 33 | } 34 | } else { 35 | this.sessions = {}; 36 | } 37 | } 38 | 39 | // --------------------------------------------------------------------------- 40 | 41 | setLastVector(name) { 42 | this.sessions['last-vec'] = name; 43 | } 44 | 45 | getLastVector() { 46 | return this.sessions['last-vec']; 47 | } 48 | 49 | // --------------------------------------------------------------------------- 50 | 51 | setEnv(env) { 52 | this.sessions['env'] = env; 53 | } 54 | 55 | getEnv() { 56 | return this.sessions['env']; 57 | } 58 | 59 | // --------------------------------------------------------------------------- 60 | 61 | setKeys(publicKey, privateKey) { 62 | this.sessions['id-keys'] = { "publicKey":publicKey, "privateKey":privateKey }; 63 | } 64 | 65 | getKeys() { 66 | return this.sessions['id-keys']; 67 | } 68 | 69 | // --------------------------------------------------------------------------- 70 | 71 | setSession(remoteKey, name, encryptKey, decryptKey) { 72 | if(!('remote-keys' in this.sessions)) { 73 | this.sessions['remote-keys'] = {}; 74 | } 75 | 76 | this.sessions['remote-keys'][RtsCliUtil.keyToHexStr(remoteKey)] = { 77 | name:name, 78 | tx:encryptKey, 79 | rx:decryptKey 80 | }; 81 | } 82 | 83 | getSession(remoteKey) { 84 | if(!('remote-keys' in this.sessions)) { 85 | return null; 86 | } 87 | 88 | if(RtsCliUtil.keyToHexStr(remoteKey) in this.sessions['remote-keys']) { 89 | return this.sessions['remote-keys'][RtsCliUtil.keyToHexStr(remoteKey)]; 90 | } 91 | 92 | return null; 93 | } 94 | 95 | clearSessions() { 96 | this.sessions['remote-keys'] = {}; 97 | } 98 | 99 | deleteSession(remoteKey) { 100 | if(!('remote-keys' in this.sessions)) { 101 | return; 102 | } 103 | 104 | if(RtsCliUtil.keyToHexStr(remoteKey) in this.sessions['remote-keys']) { 105 | delete this.sessions['remote-keys']; 106 | } 107 | } 108 | 109 | // --------------------------------------------------------------------------- 110 | 111 | 112 | // --------------------------------------------------------------------------- 113 | 114 | save() { 115 | nconf.use('file', { file: './settings.json'}); 116 | nconf.load(); 117 | nconf.set("sessions", JSON.stringify(this.sessions)); 118 | nconf.save(); 119 | } 120 | 121 | static keyDictToArray(dict) { 122 | let dKeys = Object.keys(dict); 123 | let ret = new Uint8Array(dKeys.length); 124 | 125 | for(let j = 0; j < dKeys.length; j++) { 126 | ret[j] = dict[dKeys[j]]; 127 | } 128 | 129 | return ret; 130 | } 131 | } 132 | 133 | module.exports = { Sessions }; 134 | -------------------------------------------------------------------------------- /node-client/src/vec-cli.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details */ 2 | 3 | var readline = require('readline'); 4 | var program = require('commander'); 5 | var stringArgv = require('string-argv'); 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | const { Sessions } = require('./sessions.js'); 9 | const _vectorBle = require('./vectorBluetooth.js'); 10 | const { IntBuffer } = require('../generated/clad.js'); 11 | const _sodium = require('libsodium-wrappers'); 12 | const { RtsV2Handler } = require('../generated/rtsV2Handler.js'); 13 | const { RtsV3Handler } = require('../generated/rtsV3Handler.js'); 14 | const { RtsV4Handler } = require('../generated/rtsV4Handler.js'); 15 | const { RtsV5Handler } = require('../generated/rtsV5Handler.js'); 16 | const { RtsV6Handler } = require('../generated/rtsV6Handler.js'); 17 | const _cliProgress = require('cli-progress'); 18 | 19 | let v = 0; 20 | let rtsHandler = null; 21 | let vectorBle = null; 22 | let sessions = new Sessions(); 23 | let progressBar = null; 24 | let rl = readline.createInterface({ 25 | input: process.stdin, 26 | output: process.stdout 27 | }); 28 | let autoScript; 29 | let pinKey = null; 30 | 31 | var script = null; 32 | var config = {}; 33 | 34 | function main() { 35 | program 36 | .version('0.0.2', '-v, --verison') 37 | .option('-f, --filter [type]', 'Filter BLE scan for specific Vector.', list) 38 | .option('-t, --test [file]', 'Test script file.') 39 | .option('-c, --config [file]', 'Test script config data.') 40 | //.option('-d, --debug', 'Debug logs.') 41 | .option('-p, --protocol', 'Force a specific RTS protocol version.') 42 | .option('-k, --pin [type]', 'Force a specific RTS protocol version.') 43 | .option('-o, --output [type]', 'Output directory') 44 | .parse(process.argv); 45 | 46 | if(program.test != null) { 47 | let testPath = program.test; 48 | 49 | if(!fs.existsSync(testPath)) { 50 | let pathRel = path.relative(__dirname, process.cwd()); 51 | testPath = path.join(pathRel, program.test); 52 | } 53 | 54 | script = require(testPath); 55 | } 56 | 57 | if(program.config != null) { 58 | let configJson = fs.readFileSync(program.config); 59 | config = JSON.parse(configJson); 60 | } 61 | 62 | if(program.output != null) { 63 | config['output'] = program.output; 64 | } 65 | 66 | if(program.pin != null) { 67 | pinKey = program.pin; 68 | } 69 | 70 | startBleComms(program.filter); 71 | } 72 | 73 | function list(val) { 74 | return val.split(','); 75 | } 76 | 77 | async function initializeSodium() { 78 | await _sodium.ready; 79 | } 80 | 81 | function generateHandshakeMessage(version) { 82 | let buffer = IntBuffer.Int32ToLE(version); 83 | return [1].concat(buffer); 84 | } 85 | 86 | function saveSession() { 87 | // Save session 88 | let remoteKey = rtsHandler.remoteKeys.publicKey; 89 | let name = vectorBle.bleName; 90 | let encryptKey = rtsHandler.cryptoKeys.encrypt; 91 | let decryptKey = rtsHandler.cryptoKeys.decrypt; 92 | sessions.setSession(remoteKey, name, encryptKey, decryptKey); 93 | sessions.setKeys(rtsHandler.keys.publicKey, rtsHandler.keys.privateKey); 94 | sessions.save(); 95 | } 96 | 97 | function handleHandshake(version) { 98 | if(rtsHandler != null) { 99 | console.log(''); 100 | 101 | rtsHandler.cleanup(); 102 | rtsHandler = null; 103 | } 104 | 105 | console.log(`Vector is requesting RTS v${version}`); 106 | 107 | v = version; 108 | 109 | switch(version) { 110 | case 6: 111 | // RTSv6 112 | rtsHandler = new RtsV6Handler(vectorBle, _sodium, sessions); 113 | break; 114 | case 5: 115 | // RTSv5 116 | rtsHandler = new RtsV5Handler(vectorBle, _sodium, sessions); 117 | break; 118 | case 4: 119 | // RTSv4 120 | rtsHandler = new RtsV4Handler(vectorBle, _sodium, sessions); 121 | break; 122 | case 3: 123 | rtsHandler = new RtsV3Handler(vectorBle, _sodium, sessions); 124 | // RTSv3 (Dev) 125 | break; 126 | case 2: 127 | // RTSv2 (Factory) 128 | rtsHandler = new RtsV2Handler(vectorBle, _sodium, sessions); 129 | break; 130 | default: 131 | // Unknown 132 | console.log("Unknown Rts version"); 133 | return; 134 | } 135 | 136 | rtsHandler.onReadyForPin(function() { 137 | if(pinKey) { 138 | rtsHandler.enterPin(pinKey); 139 | return; 140 | } 141 | 142 | rl.question("Enter pin: ", function(pin) { 143 | rtsHandler.enterPin(pin); 144 | }); 145 | }); 146 | 147 | rtsHandler.onEncryptedConnection(function() { 148 | if(script != null) { 149 | autoScript = new script.AutoScript(config); 150 | autoScript.run(rtsHandler); 151 | return; 152 | } 153 | 154 | rtsHandler.doStatus() 155 | .then(function(m) { 156 | if((v == 2 || v == 3) || 157 | (!m.value.hasOwner || m.value.isCloudAuthed)) { 158 | // RtsV2 or Cloud authorized 159 | // Save session 160 | saveSession(); 161 | } 162 | }); 163 | cmdPrompt(); 164 | }); 165 | 166 | if(rtsHandler.onCloudAuthorized) { 167 | rtsHandler.onCloudAuthorized(function(value) { 168 | if(value.success) { 169 | // save session 170 | saveSession(); 171 | } 172 | }); 173 | } 174 | 175 | rtsHandler.onCliResponse(function(output) { 176 | if(progressBar != null) { 177 | progressBar.stop(); 178 | progressBar = null; 179 | } 180 | console.log(output); 181 | cmdPrompt(); 182 | }); 183 | 184 | rtsHandler.onPrint(function(output) { 185 | console.log(output);; 186 | }); 187 | 188 | rtsHandler.onCommandDone(function() { 189 | // do nothing 190 | }); 191 | 192 | rtsHandler.onNewProgressBar(function() { 193 | // 194 | progressBar = new _cliProgress.Bar({}, _cliProgress.Presets.shades_classic); 195 | progressBar.start(100, 0); 196 | }); 197 | 198 | rtsHandler.onUpdateProgressBar(function(value, total) { 199 | // 200 | if(progressBar != null) { 201 | progressBar.update((value/total)*100); 202 | } 203 | }); 204 | 205 | rtsHandler.onLogsDownloaded(function(name, logFile) { 206 | // write logs folder/file 207 | if (!fs.existsSync('./tmp-logs')){ 208 | fs.mkdirSync('./tmp-logs'); 209 | } 210 | 211 | fs.writeFileSync('./tmp-logs/' + name, Buffer.from(logFile)); 212 | }); 213 | 214 | vectorBle.send(generateHandshakeMessage(version)); 215 | } 216 | 217 | function cmdPrompt() { 218 | let prompt = vectorBle.name.split(' ')[1]; 219 | rl.question(`\u001b[32;1m[v${v}] ${prompt}\x1b[0m$ `, function(line) { 220 | if(rtsHandler != null) { 221 | let args = stringArgv(line); 222 | let ready = rtsHandler.handleCli(args); 223 | 224 | if(ready) { 225 | cmdPrompt(); 226 | } 227 | } 228 | }); 229 | } 230 | 231 | function startBleComms(filter) { 232 | initializeSodium().then(function() { 233 | // Start cli 234 | vectorBle = new _vectorBle.VectorBluetooth(); 235 | let handshakeHandler = {}; 236 | 237 | handshakeHandler.receive = function(data) { 238 | if(data[0] == 1 && data.length == 5) { 239 | // This message is a handshake from Vector 240 | let v = IntBuffer.BufferToUInt32(data.slice(1)); 241 | handleHandshake(v); 242 | } else { 243 | // Received message after version handler exists 244 | 245 | } 246 | }; 247 | 248 | vectorBle.onReceive(handshakeHandler); 249 | vectorBle.tryConnect(filter); 250 | }); 251 | } 252 | 253 | main(); 254 | -------------------------------------------------------------------------------- /node-client/src/vectorBluetooth.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details */ 2 | 3 | const _noble = require('noble-mac'); 4 | const _bleProtocol = require('../generated/bleMessageProtocol.js'); 5 | 6 | class VectorBluetooth { 7 | constructor() { 8 | this.vectorService = "FEE3"; 9 | this.readCharService = "7D2A4BDAD29B4152B7252491478C5CD7"; 10 | this.writeCharService = "30619F2D0F5441BDA65A7588D8C85B45"; 11 | this.pairingChar = "p".charCodeAt(0); 12 | this.maxPacketSize = 20; 13 | this.bleMsgProtocol = null; 14 | this.readChar; 15 | this.writeChar; 16 | this.onReceiveEvent = []; 17 | this.sessions = {}; 18 | this.peripheral = null; 19 | 20 | this.initializeBleProtocol(); 21 | } 22 | 23 | stop() { 24 | this.onReceiveEvent = []; 25 | } 26 | 27 | initializeBleProtocol() { 28 | let self = this; 29 | this.bleMsgProtocol = new _bleProtocol.BleMessageProtocol(this.maxPacketSize); 30 | this.bleMsgProtocol.setDelegate(this); 31 | this.bleMsgProtocol.onSendRaw(function(buffer) { 32 | self.sendMessage(Buffer.from(buffer), false); 33 | }); 34 | } 35 | 36 | send(arr) { 37 | this.bleMsgProtocol.sendMessage(arr); 38 | } 39 | 40 | onReceive(fnc) { 41 | this.onReceiveEvent.push(fnc); 42 | } 43 | 44 | onReceiveUnsubscribe(obj) { 45 | for(let i = 0; i < this.onReceiveEvent.length; i++) { 46 | if(obj == this.onReceiveEvent[i]) { 47 | this.onReceiveEvent.splice(i, 1); 48 | return; 49 | } 50 | } 51 | } 52 | 53 | handleReceive(data) { 54 | let listeners = this.onReceiveEvent.slice(0); 55 | 56 | for(let i = 0; i < listeners.length; i++) { 57 | listeners[i].receive(data); 58 | } 59 | } 60 | 61 | tryConnect(vectorFilter) { 62 | console.log("Scanning..."); 63 | let self = this; 64 | _noble.on('discover', function(peripheral) { 65 | if(vectorFilter != null && vectorFilter.length > 0) { 66 | let containsFilter = false; 67 | 68 | for(let i = 0; i < vectorFilter.length; i++) { 69 | if(peripheral.advertisement.localName.includes(vectorFilter[i])) { 70 | containsFilter = true; 71 | break; 72 | } 73 | } 74 | 75 | if(!containsFilter) { 76 | return; 77 | } 78 | } 79 | 80 | console.log("Connecting to " + peripheral.advertisement.localName + "... "); 81 | let isPairing = peripheral.advertisement.manufacturerData[3] == self.pairingChar; 82 | 83 | peripheral.once('connect', function() { 84 | self.peripheral = peripheral; 85 | self.onConnect(peripheral); 86 | }); 87 | peripheral.once('disconnect', function() { console.log("peripheralDisconnecting..."); process.exit(); }); 88 | 89 | peripheral.connect(); 90 | _noble.stopScanning(); 91 | }); 92 | 93 | _noble.on('stateChange', function(state) { 94 | if(state == "poweredOn") { 95 | _noble.startScanning([ self.vectorService ], false); 96 | } 97 | }); 98 | } 99 | 100 | tryDisconnect() { 101 | if(this.peripheral) { 102 | this.peripheral.disconnect(); 103 | } 104 | } 105 | 106 | sendMessage(msg) { 107 | this.readChar.write(msg, true); 108 | } 109 | 110 | onConnect(peripheral) { 111 | let self = this; 112 | this.name = peripheral.advertisement.localName; 113 | this.discoverServices(peripheral).then(function(characteristics) { 114 | // Finished discovering 115 | self.writeChar.on('data', function(data, isNotification) { 116 | self.bleMsgProtocol.receiveRawBuffer(Array.from(data)); 117 | }); 118 | }); 119 | } 120 | 121 | discoverServices(peripheral) { 122 | // Discovering services 123 | let self = this; 124 | 125 | return new Promise(function(resolve, reject) { 126 | peripheral.discoverServices([self.vectorService], function(error, services) { 127 | // Discovering characteristics 128 | 129 | let streamPromise = new Promise(function(resolve, reject) { 130 | services[0].discoverCharacteristics([self.writeCharService, self.readCharService], function(error, characteristics) { 131 | if(error != undefined) { 132 | reject(); 133 | } else { 134 | self.writeChar = characteristics[0]; 135 | self.readChar = characteristics[1]; 136 | 137 | self.readChar.subscribe(); 138 | self.writeChar.subscribe(); 139 | 140 | resolve([self.writeChar, self.readChar]); 141 | } 142 | }); 143 | }); 144 | 145 | Promise.all([streamPromise]).then(function(data) { 146 | resolve([data[0], data[1]]); 147 | }); 148 | }); 149 | }); 150 | } 151 | } 152 | 153 | module.exports = { VectorBluetooth }; 154 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vector-web-setup", 3 | "version": "1.1.0", 4 | "dependencies": { 5 | "axios": "^0.19.2", 6 | "body-parser": "^1.19.0", 7 | "browserify": "^16.5.1", 8 | "commander": "^5.1.0", 9 | "cors": "^2.8.5", 10 | "ejs": "^3.1.3", 11 | "express": "^4.17.1", 12 | "less": "^3.11.2", 13 | "lessc": "^1.0.2" 14 | }, 15 | "main": "vector-web-setup.js", 16 | "bin": "vector-web-setup.js", 17 | "devDependencies": { 18 | "chai": "^4.2.0", 19 | "chai-as-promised": "^7.1.1", 20 | "cross-spawn": "^7.0.3", 21 | "mocha": "^8.0.1" 22 | }, 23 | "scripts": { 24 | "vector-setup": "node vector-web-setup.js", 25 | "test": "mocha test/*" 26 | }, 27 | "author": "Digital Dream Labs", 28 | "license": "MIT" 29 | } 30 | -------------------------------------------------------------------------------- /rts-js/bleMessageProtocol.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details */ 2 | 3 | class BleMessageProtocol { 4 | constructor(maxSize) { 5 | this.kMsgStart = 0b10; 6 | this.kMsgContinue = 0b00; 7 | this.kMsgEnd = 0b01; 8 | this.kMsgSolo = 0b11; 9 | this.kMsgBits = 0b11 << 6; 10 | 11 | this.maxSize = maxSize; 12 | this.sendRawEvent; 13 | this.delegate; 14 | 15 | this.state = this.kMsgStart; 16 | this.buffer = []; 17 | } 18 | 19 | onSendRaw(fnc) { 20 | this.sendRawEvent = fnc; 21 | } 22 | 23 | setDelegate(delegate) { 24 | this.delegate = delegate; 25 | } 26 | 27 | receiveRawBuffer(buffer) { 28 | if (buffer.length < 1) { 29 | return; 30 | } 31 | 32 | let headerByte = buffer[0]; 33 | let sizeByte = BleMessageProtocol.getSize(headerByte); 34 | let multipartState = BleMessageProtocol.getMultipartBits(headerByte); 35 | 36 | if (sizeByte != buffer.length - 1) { 37 | console.log("Size failure " + sizeByte + ", " + (buffer.length - 1)); 38 | return; 39 | } 40 | 41 | switch (multipartState) { 42 | case this.kMsgStart: { 43 | if (this.state != this.kMsgStart) { 44 | // error 45 | } 46 | 47 | this.buffer = []; 48 | this.append(buffer); 49 | this.state = this.kMsgContinue; 50 | 51 | break; 52 | } 53 | case this.kMsgContinue: { 54 | if (this.state != this.kMsgContinue) { 55 | // error 56 | } 57 | 58 | this.append(buffer); 59 | this.state = this.kMsgContinue; 60 | break; 61 | } 62 | case this.kMsgEnd: { 63 | if (this.state != this.kMsgContinue) { 64 | // error 65 | } 66 | 67 | this.append(buffer); 68 | if (this.delegate != null) { 69 | this.delegate.handleReceive(this.buffer); 70 | } 71 | this.state = this.kMsgStart; 72 | break; 73 | } 74 | case this.kMsgSolo: { 75 | if (this.state != this.kMsgStart) { 76 | // error 77 | } 78 | 79 | if (this.delegate != null) { 80 | buffer.splice(0, 1); 81 | this.delegate.handleReceive(buffer); 82 | } 83 | this.state = this.kMsgStart; 84 | break; 85 | } 86 | } 87 | } 88 | 89 | sendMessage(buffer) { 90 | let sizeRemaining = buffer.length; 91 | 92 | if (buffer.length < this.maxSize) { 93 | this.sendRawMessage(this.kMsgSolo, buffer); 94 | } else { 95 | while (sizeRemaining > 0) { 96 | let offset = buffer.length - sizeRemaining; 97 | 98 | if (sizeRemaining == buffer.length) { 99 | let msgSize = this.maxSize - 1; 100 | this.sendRawMessage( 101 | this.kMsgStart, 102 | buffer.slice(offset, msgSize + offset) 103 | ); 104 | sizeRemaining -= msgSize; 105 | } else if (sizeRemaining < this.maxSize) { 106 | this.sendRawMessage( 107 | this.kMsgEnd, 108 | buffer.slice(offset, sizeRemaining + offset) 109 | ); 110 | sizeRemaining = 0; 111 | } else { 112 | let msgSize = this.maxSize - 1; 113 | this.sendRawMessage( 114 | this.kMsgContinue, 115 | buffer.slice(offset, msgSize + offset) 116 | ); 117 | sizeRemaining -= msgSize; 118 | } 119 | } 120 | } 121 | } 122 | 123 | append(buffer) { 124 | this.buffer = this.buffer.concat(buffer.slice(1)); 125 | } 126 | 127 | sendRawMessage(multipart, buffer) { 128 | let arr = [BleMessageProtocol.getHeaderByte(multipart, buffer.length)]; 129 | 130 | let sendBuffer = arr.concat(buffer); 131 | 132 | if (this.sendRawEvent != null) { 133 | this.sendRawEvent(sendBuffer); 134 | } 135 | } 136 | 137 | static kMsgBits() { 138 | return 0b11 << 6; 139 | } 140 | 141 | static getMultipartBits(headerByte) { 142 | return (headerByte >> 6) & 0xff; 143 | } 144 | 145 | static getHeaderByte(multipart, size) { 146 | return ((multipart << 6) | (size & ~BleMessageProtocol.kMsgBits())) & 0xff; 147 | } 148 | 149 | static getSize(headerByte) { 150 | return headerByte & ~BleMessageProtocol.kMsgBits(); 151 | } 152 | } 153 | 154 | module.exports = { BleMessageProtocol }; 155 | -------------------------------------------------------------------------------- /rts-js/blesh.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | class Blesh { 3 | constructor() {} 4 | 5 | static isSupported() { 6 | return false; 7 | } 8 | 9 | onReceiveData(fnc) {} 10 | 11 | send(data) {} 12 | 13 | start(port) { 14 | let p = new Promise(function (resolve, reject) { 15 | resolve(false); 16 | }); 17 | 18 | return p; 19 | } 20 | } 21 | 22 | module.exports = { Blesh }; 23 | -------------------------------------------------------------------------------- /rts-js/clad.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | class Clad { 3 | constructor() {} 4 | 5 | pack() { 6 | return null; 7 | } 8 | 9 | unpack(buffer) { 10 | return null; 11 | } 12 | 13 | unpackFromClad(cladBuffer) { 14 | let buf = cladBuffer.buffer.slice(cladBuffer.index); 15 | this.unpack(buf); 16 | cladBuffer.index += this.size; 17 | } 18 | 19 | get size() { 20 | return 0; 21 | } 22 | } 23 | 24 | class CladBuffer { 25 | constructor(buffer) { 26 | this.index = 0; 27 | this.buffer = buffer; 28 | } 29 | 30 | readBool() { 31 | let ret = this.buffer.slice(this.index, this.index + 1) != 0; 32 | this.index++; 33 | return ret; 34 | } 35 | 36 | readUint8() { 37 | let ret = IntBuffer.BufferToUInt8( 38 | this.buffer.slice(this.index, this.index + 1) 39 | ); 40 | this.index += 1; 41 | return ret; 42 | } 43 | 44 | readInt8() { 45 | let ret = IntBuffer.BufferToInt8( 46 | this.buffer.slice(this.index, this.index + 1) 47 | ); 48 | this.index += 1; 49 | return ret; 50 | } 51 | 52 | readUint16() { 53 | let ret = IntBuffer.BufferToUInt16( 54 | this.buffer.slice(this.index, this.index + 2) 55 | ); 56 | this.index += 2; 57 | return ret; 58 | } 59 | 60 | readInt16() { 61 | let ret = IntBuffer.BufferToInt16( 62 | this.buffer.slice(this.index, this.index + 2) 63 | ); 64 | this.index += 2; 65 | return ret; 66 | } 67 | 68 | readUint32() { 69 | let ret = IntBuffer.BufferToUInt32( 70 | this.buffer.slice(this.index, this.index + 4) 71 | ); 72 | this.index += 4; 73 | return ret; 74 | } 75 | 76 | readInt32() { 77 | let ret = IntBuffer.BufferToInt32( 78 | this.buffer.slice(this.index, this.index + 4) 79 | ); 80 | this.index += 4; 81 | return ret; 82 | } 83 | 84 | readBigInt64() { 85 | let ret = IntBuffer.LE64ToBigInt( 86 | this.buffer.slice(this.index, this.index + 8) 87 | ); 88 | this.index += 8; 89 | return ret; 90 | } 91 | 92 | readBigUint64() { 93 | let ret = IntBuffer.LE64ToBigUInt( 94 | this.buffer.slice(this.index, this.index + 8) 95 | ); 96 | this.index += 8; 97 | return ret; 98 | } 99 | 100 | readFloat32() { 101 | let byteArray = this.buffer.slice(this.index, this.index + 4); 102 | 103 | if (IntBuffer.IsHostLittleEndian()) { 104 | byteArray.reverse(); 105 | } 106 | 107 | let ret = IntBuffer.BufferToFloat32(byteArray); 108 | this.index += 4; 109 | return ret; 110 | } 111 | 112 | readFloat64() { 113 | let byteArray = this.buffer.slice(this.index, this.index + 8); 114 | 115 | if (IntBuffer.IsHostLittleEndian()) { 116 | byteArray.reverse(); 117 | } 118 | 119 | let ret = IntBuffer.BufferToFloat64(byteArray); 120 | this.index += 8; 121 | return ret; 122 | } 123 | 124 | readFArray(isFloat, type, capacity, signed) { 125 | if (type == 1) { 126 | let ret = this.buffer.slice(this.index, this.index + capacity); 127 | this.index += capacity; 128 | return signed ? new Int8Array(ret) : new Uint8Array(ret); 129 | } else { 130 | let ret = this.buffer.slice(this.index, this.index + capacity * type); 131 | this.index += capacity * type; 132 | 133 | let typedArray; 134 | 135 | if (isFloat) { 136 | switch (type) { 137 | case 4: 138 | typedArray = new Float32Array(capacity); 139 | break; 140 | case 8: 141 | typedArray = new Float64Array(capacity); 142 | break; 143 | default: 144 | console.error("Unhandled array type."); 145 | return null; 146 | } 147 | 148 | for (let i = 0; i < capacity; i++) { 149 | let buf = ret.slice(i * type, i * type + type); 150 | let numArr; 151 | 152 | if (IntBuffer.IsHostLittleEndian()) { 153 | buf.reverse(); 154 | } 155 | 156 | switch (type) { 157 | case 4: 158 | numArr = [IntBuffer.BufferToFloat32(buf)]; 159 | break; 160 | case 8: 161 | numArr = [IntBuffer.BufferToFloat64(buf)]; 162 | break; 163 | default: 164 | return null; 165 | } 166 | 167 | typedArray.set(numArr, i); 168 | } 169 | } else { 170 | switch (type) { 171 | case 2: 172 | typedArray = signed 173 | ? new Int16Array(capacity) 174 | : new Uint16Array(capacity); 175 | break; 176 | case 4: 177 | typedArray = signed 178 | ? new Int32Array(capacity) 179 | : new Uint32Array(capacity); 180 | break; 181 | default: 182 | console.error("Unhandled array type."); 183 | return null; 184 | } 185 | 186 | for (let i = 0; i < capacity; i++) { 187 | let buf = ret.slice(i * type, i * type + type); 188 | let numArr; 189 | 190 | switch (type) { 191 | case 2: 192 | numArr = signed 193 | ? [IntBuffer.BufferToInt16(buf)] 194 | : [IntBuffer.BufferToUInt16(buf)]; 195 | break; 196 | case 4: 197 | numArr = signed 198 | ? [IntBuffer.BufferToInt32(buf)] 199 | : [IntBuffer.BufferToUInt32(buf)]; 200 | break; 201 | default: 202 | return null; 203 | } 204 | 205 | typedArray.set(numArr, i); 206 | } 207 | } 208 | 209 | return typedArray; 210 | } 211 | } 212 | 213 | readVArray(isFloat, type, sizeType, signed) { 214 | let vArrayLength = 0; 215 | 216 | switch (sizeType) { 217 | case 1: 218 | vArrayLength = IntBuffer.BufferToUInt8( 219 | this.buffer.slice(this.index, this.index + 1) 220 | ); 221 | this.index++; 222 | break; 223 | case 2: 224 | vArrayLength = IntBuffer.BufferToUInt16( 225 | this.buffer.slice(this.index, this.index + 2) 226 | ); 227 | this.index += 2; 228 | break; 229 | case 4: 230 | vArrayLength = IntBuffer.BufferToUInt32( 231 | this.buffer.slice(this.index, this.index + 4) 232 | ); 233 | this.index += 4; 234 | break; 235 | case 8: 236 | vArrayLength = IntBuffer.BufferToUInt32( 237 | this.buffer.slice(this.index, this.index + 4) 238 | ); 239 | 240 | let bigNumber = false; 241 | for (let i = 0; i < 4; i++) { 242 | bigNumber |= this.buffer[this.index + i] != 0; 243 | } 244 | 245 | if (bigNumber) { 246 | console.log( 247 | "Warning! readVArray is reading a uint_64 type that is larger than uint_32, which is unsupported in JS_emitter.py" 248 | ); 249 | } 250 | 251 | this.index += 8; 252 | break; 253 | } 254 | 255 | return this.readFArray(isFloat, type, vArrayLength, signed); 256 | } 257 | 258 | readString(type) { 259 | let buffer = this.readVArray(false, 1, type, false); 260 | return String.fromCharCode.apply(String, buffer); 261 | } 262 | 263 | readStringFArray(isFloat, type, capacity) { 264 | let array = []; 265 | 266 | for (let i = 0; i < capacity; i++) { 267 | array.push(this.readString(type)); 268 | } 269 | 270 | return array; 271 | } 272 | 273 | readStringVArray(isFloat, type, sizeType) { 274 | let vArrayLength = 0; 275 | let array = []; 276 | 277 | switch (sizeType) { 278 | case 1: 279 | vArrayLength = IntBuffer.BufferToUInt8( 280 | this.buffer.slice(this.index, this.index + 1) 281 | ); 282 | this.index++; 283 | break; 284 | case 2: 285 | vArrayLength = IntBuffer.BufferToUInt16( 286 | this.buffer.slice(this.index, this.index + 2) 287 | ); 288 | this.index += 2; 289 | break; 290 | case 4: 291 | vArrayLength = IntBuffer.BufferToUInt32( 292 | this.buffer.slice(this.index, this.index + 4) 293 | ); 294 | this.index += 4; 295 | break; 296 | case 8: 297 | vArrayLength = IntBuffer.BufferToUInt32( 298 | this.buffer.slice(this.index, this.index + 4) 299 | ); 300 | this.index += 8; 301 | break; 302 | } 303 | 304 | for (let i = 0; i < vArrayLength; i++) { 305 | array.push(this.readString(type)); 306 | } 307 | 308 | return array; 309 | } 310 | 311 | /// 312 | /// Write operations 313 | /// 314 | write(array) { 315 | this.buffer.set(array, this.index); 316 | this.index += array.length; 317 | } 318 | 319 | writeBool(value) { 320 | this.buffer.set([value], this.index); 321 | this.index++; 322 | } 323 | 324 | writeUint8(value) { 325 | this.buffer.set([value], this.index); 326 | this.index++; 327 | } 328 | 329 | writeInt8(value) { 330 | this.buffer.set([value], this.index); 331 | this.index++; 332 | } 333 | 334 | writeUint16(value) { 335 | this.buffer.set(IntBuffer.Int16ToLE(value), this.index); 336 | this.index += 2; 337 | } 338 | 339 | writeInt16(value) { 340 | this.buffer.set(IntBuffer.Int16ToLE(value), this.index); 341 | this.index += 2; 342 | } 343 | 344 | writeUint32(value) { 345 | this.buffer.set(IntBuffer.Int32ToLE(value), this.index); 346 | this.index += 4; 347 | } 348 | 349 | writeInt32(value) { 350 | this.buffer.set(IntBuffer.Int32ToLE(value), this.index); 351 | this.index += 4; 352 | } 353 | 354 | writeBigUint64(value) { 355 | this.write(IntBuffer.BigIntToLE64(value)); 356 | } 357 | 358 | writeBigInt64(value) { 359 | this.write(IntBuffer.BigIntToLE64(value)); 360 | } 361 | 362 | writeFloat32(value) { 363 | this.buffer.set(IntBuffer.Float32ToLE(value), this.index); 364 | this.index += 4; 365 | } 366 | 367 | writeFloat64(value) { 368 | this.buffer.set(IntBuffer.Float64ToLE(value), this.index); 369 | this.index += 8; 370 | } 371 | 372 | writeFArray(array) { 373 | this.buffer.set(IntBuffer.TypedArrayToByteArray(array), this.index); 374 | this.index += array.length * array.BYTES_PER_ELEMENT; 375 | } 376 | 377 | writeVArray(array, sizeType) { 378 | switch (sizeType) { 379 | case 1: 380 | this.buffer.set([array.length], this.index); 381 | break; 382 | case 2: 383 | this.buffer.set(IntBuffer.Int16ToLE(array.length), this.index); 384 | break; 385 | case 4: 386 | this.buffer.set(IntBuffer.Int32ToLE(array.length), this.index); 387 | break; 388 | case 8: 389 | this.buffer.set(IntBuffer.Int32ToLE(array.length), this.index); 390 | break; 391 | default: 392 | console.error("Unsupported size type."); 393 | break; 394 | } 395 | 396 | this.index += sizeType; 397 | 398 | this.buffer.set(IntBuffer.TypedArrayToByteArray(array), this.index); 399 | this.index += array.length * array.BYTES_PER_ELEMENT; 400 | } 401 | 402 | writeString(value, sizeType) { 403 | let stringBuffer = new Uint8Array(value.length); 404 | 405 | for (let i = 0; i < value.length; i++) { 406 | stringBuffer.set([value.charCodeAt(i)], i); 407 | } 408 | 409 | this.writeVArray(stringBuffer, sizeType); 410 | } 411 | 412 | writeStringFArray(value, capacity, sizeType) { 413 | for (let i = 0; i < capacity; i++) { 414 | this.writeString(value[i], sizeType); 415 | } 416 | } 417 | 418 | writeStringVArray(array, arrayType, sizeType) { 419 | switch (arrayType) { 420 | case 1: 421 | this.buffer.set([array.length], this.index); 422 | break; 423 | case 2: 424 | this.buffer.set(IntBuffer.Int16ToLE(array.length), this.index); 425 | break; 426 | case 4: 427 | this.buffer.set(IntBuffer.Int32ToLE(array.length), this.index); 428 | break; 429 | case 8: 430 | this.buffer.set(IntBuffer.Int32ToLE(array.length), this.index); 431 | break; 432 | default: 433 | console.error("Unsupported size type."); 434 | break; 435 | } 436 | 437 | this.index += arrayType; 438 | 439 | for (let i = 0; i < array.length; i++) { 440 | this.writeString(array[i], sizeType); 441 | } 442 | } 443 | } 444 | 445 | class IntBuffer { 446 | static IsHostLittleEndian() { 447 | let buffer = new ArrayBuffer(2); 448 | let byteArray = new Uint8Array(buffer); 449 | let shortArray = new Uint16Array(buffer); 450 | byteArray[0] = 0x11; 451 | byteArray[1] = 0x22; 452 | 453 | return shortArray[0] == 0x2211; 454 | } 455 | 456 | static Int32ToLE(number) { 457 | let buffer = new Array(4); 458 | buffer[0] = number & 0x000000ff; 459 | buffer[1] = (number >> 8) & 0x0000ff; 460 | buffer[2] = (number >> 16) & 0x00ff; 461 | buffer[3] = number >> 24; 462 | return buffer; 463 | } 464 | 465 | static Int16ToLE(number) { 466 | let buffer = new Array(2); 467 | buffer[0] = number & 0x00ff; 468 | buffer[1] = number >> 8; 469 | return buffer; 470 | } 471 | 472 | static Float32ToLE(number) { 473 | let buffer = new Float32Array(1); 474 | buffer[0] = number; 475 | let byteArray = new Int8Array(buffer.buffer); 476 | 477 | if (!IntBuffer.IsHostLittleEndian()) { 478 | byteArray.reverse(); 479 | } 480 | 481 | return Array.from(byteArray); 482 | } 483 | 484 | static Float64ToLE(number) { 485 | let buffer = new Float64Array(1); 486 | buffer[0] = number; 487 | let byteArray = new Int8Array(buffer.buffer); 488 | 489 | if (!IntBuffer.IsHostLittleEndian()) { 490 | byteArray.reverse(); 491 | } 492 | 493 | return Array.from(byteArray); 494 | } 495 | 496 | static BufferToInt8(buffer) { 497 | var buf = new ArrayBuffer(1); 498 | var view = new DataView(buf); 499 | 500 | buffer.forEach(function (b, i) { 501 | view.setUint8(i, b); 502 | }); 503 | 504 | return view.getInt8(0); 505 | } 506 | 507 | static BufferToUInt8(buffer) { 508 | var buf = new ArrayBuffer(1); 509 | var view = new DataView(buf); 510 | 511 | buffer.forEach(function (b, i) { 512 | view.setUint8(i, b); 513 | }); 514 | 515 | return view.getUint8(0); 516 | } 517 | 518 | static BufferToInt16(buffer) { 519 | var buf = new ArrayBuffer(2); 520 | var view = new DataView(buf); 521 | 522 | buffer.forEach(function (b, i) { 523 | view.setUint8(i, b); 524 | }); 525 | 526 | return view.getInt16(0, true); 527 | } 528 | 529 | static BufferToUInt16(buffer) { 530 | var buf = new ArrayBuffer(2); 531 | var view = new DataView(buf); 532 | 533 | buffer.forEach(function (b, i) { 534 | view.setUint8(i, b); 535 | }); 536 | 537 | return view.getUint16(0, true); 538 | } 539 | 540 | static BufferToInt32(buffer) { 541 | var buf = new ArrayBuffer(4); 542 | var view = new DataView(buf); 543 | 544 | buffer.forEach(function (b, i) { 545 | view.setUint8(i, b); 546 | }); 547 | 548 | return view.getInt32(0, true); 549 | } 550 | 551 | static BufferToUInt32(buffer) { 552 | var buf = new ArrayBuffer(4); 553 | var view = new DataView(buf); 554 | 555 | buffer.forEach(function (b, i) { 556 | view.setUint8(i, b); 557 | }); 558 | 559 | return view.getUint32(0, true); 560 | } 561 | 562 | static BufferToFloat32(buffer) { 563 | // Create a buffer 564 | var buf = new ArrayBuffer(4); 565 | // Create a data view of it 566 | var view = new DataView(buf); 567 | 568 | // set bytes 569 | buffer.forEach(function (b, i) { 570 | view.setUint8(i, b); 571 | }); 572 | 573 | // Read the bits as a float; note that by doing this, we're implicitly 574 | // converting it from a 32-bit float into JavaScript's native 64-bit double 575 | return view.getFloat32(0); 576 | } 577 | 578 | static BufferToFloat64(buffer) { 579 | // Create a buffer 580 | var buf = new ArrayBuffer(8); 581 | // Create a data view of it 582 | var view = new DataView(buf); 583 | 584 | // set bytes 585 | buffer.forEach(function (b, i) { 586 | view.setUint8(i, b); 587 | }); 588 | 589 | // Read the bits as a float; note that by doing this, we're implicitly 590 | // converting it from a 32-bit float into JavaScript's native 64-bit double 591 | return view.getFloat64(0); 592 | } 593 | 594 | static TypedArrayToByteArray(typedArray) { 595 | let elementSize = typedArray.BYTES_PER_ELEMENT; 596 | let buffer = new Uint8Array(elementSize * typedArray.length); 597 | 598 | if (typedArray.constructor.name.indexOf("Float") == 0) { 599 | // type is float array 600 | switch (elementSize) { 601 | case 4: 602 | for (let i = 0; i < typedArray.length; i++) { 603 | buffer.set(IntBuffer.Float32ToLE(typedArray[i]), i * 4); 604 | } 605 | break; 606 | case 8: 607 | for (let i = 0; i < typedArray.length; i++) { 608 | buffer.set(IntBuffer.Float64ToLE(typedArray[i]), i * 8); 609 | } 610 | break; 611 | } 612 | } else { 613 | switch (elementSize) { 614 | case 1: 615 | for (let i = 0; i < typedArray.length; i++) { 616 | buffer.set([typedArray[i]], i); 617 | } 618 | break; 619 | case 2: 620 | for (let i = 0; i < typedArray.length; i++) { 621 | buffer.set(IntBuffer.Int16ToLE(typedArray[i]), i * 2); 622 | } 623 | break; 624 | case 4: 625 | for (let i = 0; i < typedArray.length; i++) { 626 | buffer.set(IntBuffer.Int32ToLE(typedArray[i]), i * 4); 627 | } 628 | break; 629 | case 8: 630 | // not yet supported 631 | break; 632 | } 633 | } 634 | 635 | return buffer; 636 | } 637 | 638 | static BigIntToLE64(bigInt) { 639 | let big64bit = BigInt.asUintN(64, bigInt); 640 | let high = Number(big64bit & 0xffffffffn); 641 | let low = Number(big64bit >> 32n); 642 | 643 | let bufferHigh = IntBuffer.Int32ToLE(high); 644 | let bufferLow = IntBuffer.Int32ToLE(low); 645 | 646 | return bufferHigh.concat(bufferLow); 647 | } 648 | 649 | static LE64ToBigInt(buffer) { 650 | let low = buffer.slice(0, 4); 651 | let high = buffer.slice(4, 8); 652 | 653 | let lowInt = IntBuffer.BufferToUInt32(low); 654 | let highInt = IntBuffer.BufferToUInt32(high); 655 | 656 | let bigInt = (BigInt(highInt) << 32n) | BigInt(lowInt); 657 | 658 | return BigInt.asIntN(64, bigInt); 659 | } 660 | 661 | static LE64ToBigUInt(buffer) { 662 | let low = buffer.slice(0, 4); 663 | let high = buffer.slice(4, 8); 664 | 665 | let lowInt = IntBuffer.BufferToUInt32(low); 666 | let highInt = IntBuffer.BufferToUInt32(high); 667 | 668 | let bigInt = (BigInt(highInt) << 32n) | BigInt(lowInt); 669 | 670 | return BigInt.asUintN(64, bigInt); 671 | } 672 | } 673 | 674 | module.exports = { Clad, CladBuffer, IntBuffer }; 675 | -------------------------------------------------------------------------------- /rts-js/rtsCliUtil.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | class RtsCliUtil { 4 | static msgToStr(msg) { 5 | let str = ""; 6 | 7 | switch (msg.type()) { 8 | case "RtsWifiScanResponse_3": { 9 | str = RtsCliUtil.rtsWifiScanResponseStr(msg, 3); 10 | break; 11 | } 12 | case "RtsWifiScanResponse_2": { 13 | str = RtsCliUtil.rtsWifiScanResponseStr(msg, 2); 14 | break; 15 | } 16 | case "RtsWifiConnectResponse_3": { 17 | str = RtsCliUtil.rtsWifiConnectResponseStr(msg, 3); 18 | break; 19 | } 20 | case "RtsWifiConnectResponse": { 21 | str = RtsCliUtil.rtsWifiConnectResponseStr(msg, 2); 22 | break; 23 | } 24 | case "RtsStatusResponse_2": { 25 | str = RtsCliUtil.rtsStatusResponseStr(msg, 2); 26 | break; 27 | } 28 | case "RtsStatusResponse_4": { 29 | str = RtsCliUtil.rtsStatusResponseStr(msg, 4); 30 | break; 31 | } 32 | case "RtsStatusResponse_5": { 33 | str = RtsCliUtil.rtsStatusResponseStr(msg, 5); 34 | break; 35 | } 36 | case "RtsWifiForgetResponse": { 37 | str = RtsCliUtil.rtsWifiForgetResponseStr(msg); 38 | break; 39 | } 40 | case "RtsWifiAccessPointResponse": { 41 | str = RtsCliUtil.rtsWifiAccessPointResponseStr(msg); 42 | break; 43 | } 44 | case "RtsWifiIpResponse": { 45 | str = RtsCliUtil.rtsWifiIpResponseStr(msg); 46 | break; 47 | } 48 | case "RtsCloudSessionResponse": { 49 | str = RtsCliUtil.rtsCloudSessionResponseStr(msg); 50 | break; 51 | } 52 | case "RtsOtaUpdateResponse": { 53 | str = RtsCliUtil.rtsOtaUpdateResponseStr(msg); 54 | break; 55 | } 56 | case "RtsSdkProxyResponse": { 57 | str = RtsCliUtil.rtsSdkProxyResponseStr(msg); 58 | break; 59 | } 60 | case "RtsResponse": { 61 | str = RtsCliUtil.rtsResponseStr(msg); 62 | break; 63 | } 64 | case "RtsFileDownload": { 65 | str = "Successfully downloaded logs."; 66 | break; 67 | } 68 | default: 69 | break; 70 | } 71 | 72 | return str; 73 | } 74 | 75 | static padEnd(str, targetLength, padString) { 76 | str = str + ""; 77 | targetLength = targetLength >> 0; //floor if number or convert non-number to 0; 78 | padString = String(typeof padString !== "undefined" ? padString : " "); 79 | if (str.length > targetLength) { 80 | return String(str); 81 | } else { 82 | targetLength = targetLength - str.length; 83 | if (targetLength > padString.length) { 84 | padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed 85 | } 86 | return String(str) + padString.slice(0, targetLength); 87 | } 88 | } 89 | 90 | static padStart(str, targetLength, padString) { 91 | str = str + ""; 92 | targetLength = targetLength >> 0; //truncate if number, or convert non-number to 0; 93 | padString = String(typeof padString !== "undefined" ? padString : " "); 94 | if (str.length >= targetLength) { 95 | return String(str); 96 | } else { 97 | targetLength = targetLength - str.length; 98 | if (targetLength > padString.length) { 99 | padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed 100 | } 101 | return padString.slice(0, targetLength) + String(str); 102 | } 103 | } 104 | 105 | static replaceAll(base, str1, str2, ignore) { 106 | return base.replace( 107 | new RegExp( 108 | str1.replace(/([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|\<\>\-\&])/g, "\\$&"), 109 | ignore ? "gi" : "g" 110 | ), 111 | typeof str2 == "string" ? str2.replace(/\$/g, "$$$$") : str2 112 | ); 113 | } 114 | 115 | static removeBackspace(str) { 116 | let hexLine = RtsCliUtil.convertStrToHex(str); 117 | let hexArr = []; 118 | 119 | for (let i = 0; i < hexLine.length; i += 2) { 120 | hexArr.push(hexLine.substring(i, i + 2)); 121 | } 122 | 123 | while (hexArr.indexOf("7F") != -1) { 124 | let idx = hexArr.indexOf("7F"); 125 | 126 | hexArr.splice(idx, 1); 127 | 128 | if (idx != 0) { 129 | hexArr.splice(idx - 1, 1); 130 | } 131 | } 132 | 133 | let hexStr = ""; 134 | for (let i = 0; i < hexArr.length; i++) { 135 | hexStr += hexArr[i]; 136 | } 137 | 138 | return RtsCliUtil.convertHexToStr(hexStr); 139 | } 140 | 141 | static convertHexToStr(hexString) { 142 | let ssid = ""; 143 | 144 | if (hexString.length % 2 != 0) { 145 | return null; 146 | } 147 | 148 | for (let i = 0; i < hexString.length; i += 2) { 149 | let code = hexString.charAt(i) + hexString.charAt(i + 1); 150 | try { 151 | let intFromCode = parseInt("0x" + code); 152 | if (isNaN(intFromCode)) { 153 | return null; 154 | } 155 | 156 | ssid += String.fromCharCode(parseInt("0x" + code)); 157 | } catch { 158 | return null; 159 | } 160 | } 161 | 162 | return ssid; 163 | } 164 | 165 | static convertStrToHex(str) { 166 | let hex = ""; 167 | 168 | for (let i = 0; i < str.length; i++) { 169 | hex += RtsCliUtil.padStart( 170 | str.charCodeAt(i).toString(16).toUpperCase(), 171 | 2, 172 | "0" 173 | ); 174 | } 175 | 176 | return hex; 177 | } 178 | 179 | static rtsWifiScanResponseStr(msg, version) { 180 | let str = ""; 181 | 182 | let statusStr = ""; 183 | 184 | switch (msg.statusCode) { 185 | case 0: 186 | statusStr = "success"; 187 | break; 188 | case 100: 189 | statusStr = "error_getting_proxy"; 190 | break; 191 | case 101: 192 | statusStr = "error_scanning"; 193 | break; 194 | case 102: 195 | statusStr = "failed_scanning"; 196 | break; 197 | case 103: 198 | statusStr = "error_getting_manager"; 199 | break; 200 | case 104: 201 | statusStr = "error_getting_services"; 202 | break; 203 | case 105: 204 | statusStr = "failed_getting_services"; 205 | break; 206 | default: 207 | statusStr = "?"; 208 | break; 209 | } 210 | 211 | str += `status: ${statusStr}\n`; 212 | str += `scanned ${msg.scanResult.length} network(s)...\n\n`; 213 | 214 | str += RtsCliUtil.padEnd("Auth", 12, " "); 215 | str += RtsCliUtil.padEnd("Signal", 6, " "); 216 | str += RtsCliUtil.padEnd("", 4, " "); 217 | str += RtsCliUtil.padEnd("", 4, " "); 218 | str += "SSID\n"; 219 | 220 | for (let i = 0; i < msg.scanResult.length; i++) { 221 | let authType = ""; 222 | let r = msg.scanResult[i]; 223 | switch (r.authType) { 224 | case 0: 225 | authType = "none"; 226 | break; 227 | case 1: 228 | authType = "WEP"; 229 | break; 230 | case 2: 231 | authType = "WEP_SHARED"; 232 | break; 233 | case 3: 234 | authType = "IEEE8021X"; 235 | break; 236 | case 4: 237 | authType = "WPA_PSK"; 238 | break; 239 | case 5: 240 | authType = "WPA_EAP"; 241 | break; 242 | case 6: 243 | authType = "WPA2_PSK"; 244 | break; 245 | case 7: 246 | authType = "WPA2_EAP"; 247 | break; 248 | } 249 | 250 | str += RtsCliUtil.padEnd(authType, 12, " "); 251 | let signalStr = RtsCliUtil.padEnd(r.signalStrength, 6, " "); 252 | 253 | if (version < 3) { 254 | switch (r.signalStrength) { 255 | case 0: 256 | case 1: 257 | r.signalStrength = 30; 258 | break; 259 | case 2: 260 | r.signalStrength = 70; 261 | break; 262 | default: 263 | case 3: 264 | r.signalStrength = 100; 265 | break; 266 | } 267 | } 268 | 269 | if (0 <= r.signalStrength && r.signalStrength <= 30) { 270 | signalStr = "\x1b[91m" + RtsCliUtil.padEnd("#", 6, " ") + "\x1b[0m"; 271 | } else if (30 < r.signalStrength && r.signalStrength <= 70) { 272 | signalStr = "\x1b[93m" + RtsCliUtil.padEnd("##", 6, " ") + "\x1b[0m"; 273 | } else if (70 < r.signalStrength && r.signalStrength <= 100) { 274 | signalStr = "\x1b[92m" + RtsCliUtil.padEnd("###", 6, " ") + "\x1b[0m"; 275 | } 276 | 277 | str += signalStr; 278 | str += RtsCliUtil.padEnd(r.hidden ? "H" : "", 4, " "); 279 | 280 | let p = ""; 281 | if (version >= 3) { 282 | p = r.provisioned ? "*" : ""; 283 | } 284 | str += RtsCliUtil.padEnd(p, 4, " "); 285 | 286 | if (r.wifiSsidHex == "hidden") { 287 | str += "hidden\n"; 288 | } else { 289 | str += RtsCliUtil.convertHexToStr(r.wifiSsidHex) + "\n"; 290 | } 291 | } 292 | 293 | return str; 294 | } 295 | 296 | static rtsWifiConnectResponseStr(msg, version) { 297 | let str = ""; 298 | let wifiState = ""; 299 | let result = ""; 300 | 301 | switch (msg.wifiState) { 302 | case 0: 303 | wifiState = "\x1b[91munknown\x1b[0m"; 304 | break; 305 | case 1: 306 | wifiState = "\x1b[92monline\x1b[0m"; 307 | break; 308 | case 2: 309 | wifiState = "\x1b[93mconnected\x1b[0m"; 310 | break; 311 | case 3: 312 | wifiState = "\x1b[91mdisconnected\x1b[0m"; 313 | break; 314 | default: 315 | wifiState = "?"; 316 | break; 317 | } 318 | 319 | str += 320 | `WiFi connection:\n` + 321 | `${RtsCliUtil.padStart("wifi state: ", 14, " ") + wifiState}\n`; 322 | 323 | if (version >= 3) { 324 | switch (msg.connectResult) { 325 | case 0: 326 | result = "success"; 327 | break; 328 | case 1: 329 | result = "failure"; 330 | break; 331 | case 2: 332 | result = "invalid password"; 333 | break; 334 | case 255: 335 | result = "none"; 336 | break; 337 | default: 338 | result = "?"; 339 | break; 340 | } 341 | str += `${RtsCliUtil.padStart("result: ", 14, " ") + result}\n`; 342 | } 343 | 344 | return str; 345 | } 346 | 347 | static rtsStatusResponseStr(msg, version) { 348 | let str = ""; 349 | let wifiSsid = 350 | msg.wifiSsidHex != "" ? RtsCliUtil.convertHexToStr(msg.wifiSsidHex) : ""; 351 | let wifiState = ""; 352 | let apStr = msg.accessPoint ? "on" : "off"; 353 | let v = msg.version; 354 | let otaStr = msg.otaInProgress ? "yes" : "no"; 355 | let esn, ownerStr, cloudAuthed; 356 | 357 | switch (msg.wifiState) { 358 | case 0: 359 | wifiState = "\x1b[91munknown"; 360 | break; 361 | case 1: 362 | wifiState = "\x1b[92monline"; 363 | break; 364 | case 2: 365 | wifiState = "\x1b[93mconnected"; 366 | break; 367 | case 3: 368 | wifiState = "\x1b[91mdisconnected"; 369 | break; 370 | default: 371 | wifiState = "?"; 372 | break; 373 | } 374 | 375 | if (version >= 4) { 376 | esn = msg.esn; 377 | ownerStr = msg.hasOwner ? "yes" : "no"; 378 | } 379 | 380 | if (version >= 5) { 381 | cloudAuthed = msg.isCloudAuthed ? "yes" : "no"; 382 | } 383 | 384 | str += 385 | `${RtsCliUtil.padStart("ssid: ", 20, " ") + wifiSsid}\n` + 386 | `${ 387 | RtsCliUtil.padStart("wifi state: ", 20, " ") + wifiState + "\x1b[0m" 388 | }\n` + 389 | `${RtsCliUtil.padStart("access point: ", 20, " ") + apStr}\n` + 390 | `${RtsCliUtil.padStart("build version: ", 20, " ") + v}\n` + 391 | `${RtsCliUtil.padStart("is ota updating: ", 20, " ") + otaStr}\n`; 392 | 393 | if (version >= 4) { 394 | str += 395 | `${RtsCliUtil.padStart("serial number: ", 20, " ") + esn}\n` + 396 | `${RtsCliUtil.padStart("has cloud owner: ", 20, " ") + ownerStr}\n`; 397 | } 398 | 399 | if (version >= 5) { 400 | str += `${ 401 | RtsCliUtil.padStart("is cloud authed: ", 20, " ") + cloudAuthed 402 | }\n`; 403 | } 404 | 405 | return str; 406 | } 407 | 408 | static rtsWifiIpResponseStr(msg) { 409 | let str = ""; 410 | 411 | if (msg.hasIpV4) { 412 | // add ipv4 413 | str += "IPv4: "; 414 | 415 | for (let i = 0; i < 4; i++) { 416 | str += msg.ipV4[i]; 417 | if (i < 4 - 1) { 418 | str += "."; 419 | } 420 | } 421 | str += "\n"; 422 | } 423 | 424 | if (msg.hasIpV6) { 425 | // add ipv6 426 | str += "IPv6: "; 427 | 428 | for (let i = 0; i < 16; i += 2) { 429 | str += RtsCliUtil.padStart(msg.ipV6[i].toString(16), 2, "0"); 430 | str += RtsCliUtil.padStart(msg.ipV6[i + 1].toString(16), 2, "0"); 431 | if (i < 16 - 3) { 432 | str += ":"; 433 | } 434 | } 435 | str += "\n"; 436 | } 437 | 438 | return str; 439 | } 440 | 441 | static rtsCloudSessionResponseStr(msg) { 442 | let str = ""; 443 | let statusStr = ""; 444 | 445 | str += RtsCliUtil.padStart("result: ", 10, " "); 446 | 447 | if (msg.success) { 448 | str += "\x1b[92msuccessfully authorized\n" + "\x1b[0m"; 449 | } else { 450 | str += "\x1b[91mfailed to authorize\n" + "\x1b[0m"; 451 | } 452 | 453 | switch (msg.statusCode) { 454 | case 0: 455 | statusStr = "UnknownError"; 456 | break; 457 | case 1: 458 | statusStr = "ConnectionError"; 459 | break; 460 | case 2: 461 | statusStr = "WrongAccount"; 462 | break; 463 | case 3: 464 | statusStr = "InvalidSessionToken"; 465 | break; 466 | case 4: 467 | statusStr = "AuthorizedAsPrimary"; 468 | break; 469 | case 5: 470 | statusStr = "AuthorizedAsSecondary"; 471 | break; 472 | case 6: 473 | statusStr = "ReassociatedPrimary"; 474 | break; 475 | } 476 | 477 | str += RtsCliUtil.padStart("status: ", 10, " ") + statusStr + "\n"; 478 | str += RtsCliUtil.padStart("token: ", 10, " ") + msg.clientTokenGuid + "\n"; 479 | 480 | return str; 481 | } 482 | 483 | static rtsWifiAccessPointResponseStr(msg) { 484 | let str = ""; 485 | 486 | if (msg.ssid == "") { 487 | str += "AP Disabled"; 488 | } else { 489 | str += RtsCliUtil.padStart("ssid: ", 15, " ") + msg.ssid + "\n"; 490 | str += RtsCliUtil.padStart("password: ", 15, " ") + msg.password + "\n"; 491 | } 492 | 493 | return str; 494 | } 495 | 496 | static rtsSdkProxyResponseStr(msg) { 497 | let str = ""; 498 | 499 | str += RtsCliUtil.padStart("messageId: ", 15, " ") + msg.messageId + "\n"; 500 | str += RtsCliUtil.padStart("statusCode: ", 15, " ") + msg.statusCode + "\n"; 501 | str += 502 | RtsCliUtil.padStart("responseType: ", 15, " ") + msg.responseType + "\n"; 503 | str += 504 | RtsCliUtil.padStart("responseBody: ", 15, " ") + msg.responseBody + "\n"; 505 | 506 | return str; 507 | } 508 | 509 | static rtsWifiForgetResponseStr(msg) { 510 | let str = ""; 511 | 512 | str += "status: " + (msg.didDelete ? "deleted" : "no delete"); 513 | 514 | return str; 515 | } 516 | 517 | static rtsResponseStr(msg) { 518 | if (msg.code == 0) { 519 | return 'Error: Not cloud authorized. Do "anki-auth SESSION_TOKEN"'; 520 | } 521 | 522 | return "Unknown error..."; 523 | } 524 | 525 | static rtsOtaUpdateResponseStr(msg) { 526 | let n = 0; 527 | if (msg.expected > 0) { 528 | n = Number(msg.current / msg.expected); 529 | } 530 | return `status:${msg.status}\nprogress:${100 * n}% (${msg.current} / ${ 531 | msg.expected 532 | })`; 533 | } 534 | 535 | static addTimeout(promise) { 536 | let timeout = new Promise((resolve, reject) => { 537 | let t = setTimeout(() => { 538 | clearTimeout(t); 539 | reject(); 540 | }, 5000); 541 | }); 542 | 543 | return Promise.race([promise, timeout]); 544 | } 545 | 546 | static makeId() { 547 | let ret = ""; 548 | let chars = "abcdefghijklmnopqrstuvwxyz"; 549 | 550 | for (let i = 0; i < 24; i++) { 551 | ret += chars.charAt(Math.floor(Math.random() * chars.length)); 552 | } 553 | 554 | return ret; 555 | } 556 | 557 | static getDateString() { 558 | let d = new Date(Date.now()); 559 | let year = "" + d.getFullYear(); 560 | let month = ("" + (d.getMonth() + 1)).padStart(2, "0"); 561 | let day = ("" + d.getDate()).padStart(2, "0"); 562 | let hours = ("" + d.getHours()).padStart(2, "0"); 563 | let mins = ("" + d.getMinutes()).padStart(2, "0"); 564 | let secs = ("" + d.getSeconds()).padStart(2, "0"); 565 | 566 | return ( 567 | year + "-" + month + "-" + day + "-" + hours + "-" + mins + "-" + secs 568 | ); 569 | } 570 | 571 | static byteToHexStr(n) { 572 | let s = n.toString(16).toUpperCase(); 573 | return "0".repeat(2 - s.length) + s; 574 | } 575 | 576 | static keyToHexStr(arr) { 577 | let str = ""; 578 | for (let i = 0; i < arr.length; i++) { 579 | str += RtsCliUtil.byteToHexStr(arr[i]); 580 | } 581 | return str; 582 | } 583 | 584 | static printHelp(args) { 585 | let keys = Object.keys(args); 586 | let p = ""; 587 | for (let i = 0; i < keys.length; i++) { 588 | p += keys[i] + " ".repeat(24 - keys[i].length) + args[keys[i]].des + "\n"; 589 | p += " ".repeat(24) + args[keys[i]].help + "\n\n"; 590 | } 591 | 592 | return p; 593 | } 594 | } 595 | 596 | module.exports = { RtsCliUtil }; 597 | -------------------------------------------------------------------------------- /rts-js/sessions.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | var { RtsCliUtil } = require("./rtsCliUtil.js"); 4 | 5 | class Sessions { 6 | constructor() { 7 | this.sessions = {}; 8 | this.getSessions(); 9 | } 10 | 11 | getSessions() { 12 | try { 13 | this.sessions = JSON.parse(Sessions.getCookie("sessions")); 14 | if ("remote-keys" in this.sessions) { 15 | let remoteKeys = Object.keys(this.sessions["remote-keys"]); 16 | for (let i = 0; i < remoteKeys.length; i++) { 17 | this.sessions["remote-keys"][ 18 | remoteKeys[i] 19 | ].tx = Sessions.keyDictToArray( 20 | this.sessions["remote-keys"][remoteKeys[i]].tx 21 | ); 22 | this.sessions["remote-keys"][ 23 | remoteKeys[i] 24 | ].rx = Sessions.keyDictToArray( 25 | this.sessions["remote-keys"][remoteKeys[i]].rx 26 | ); 27 | } 28 | } 29 | 30 | if ("id-keys" in this.sessions) { 31 | this.sessions["id-keys"].publicKey = Sessions.keyDictToArray( 32 | this.sessions["id-keys"].publicKey 33 | ); 34 | this.sessions["id-keys"].privateKey = Sessions.keyDictToArray( 35 | this.sessions["id-keys"].privateKey 36 | ); 37 | } 38 | } catch (e) { 39 | console.log(e); 40 | this.sessions = {}; 41 | } 42 | } 43 | 44 | // --------------------------------------------------------------------------- 45 | 46 | setLastVector(name) { 47 | this.sessions["last-vec"] = name; 48 | } 49 | 50 | getLastVector() { 51 | return this.sessions["last-vec"]; 52 | } 53 | 54 | // --------------------------------------------------------------------------- 55 | 56 | setEnv(env) { 57 | this.sessions["env"] = env; 58 | } 59 | 60 | getEnv() { 61 | return this.sessions["env"]; 62 | } 63 | 64 | // --------------------------------------------------------------------------- 65 | 66 | setKeys(publicKey, privateKey) { 67 | this.sessions["id-keys"] = { publicKey: publicKey, privateKey: privateKey }; 68 | } 69 | 70 | getKeys() { 71 | return this.sessions["id-keys"]; 72 | } 73 | 74 | // --------------------------------------------------------------------------- 75 | 76 | setViewMode(view) { 77 | this.sessions["view-mode"] = view; 78 | } 79 | 80 | getViewMode() { 81 | let mode = this.sessions["view-mode"]; 82 | if (mode == null) { 83 | mode = 1; 84 | } 85 | 86 | return mode; 87 | } 88 | 89 | // --------------------------------------------------------------------------- 90 | 91 | setSession(remoteKey, name, encryptKey, decryptKey) { 92 | if (!("remote-keys" in this.sessions)) { 93 | this.sessions["remote-keys"] = {}; 94 | } 95 | 96 | this.sessions["remote-keys"][RtsCliUtil.keyToHexStr(remoteKey)] = { 97 | name: name, 98 | tx: encryptKey, 99 | rx: decryptKey, 100 | }; 101 | } 102 | 103 | getSession(remoteKey) { 104 | if (!("remote-keys" in this.sessions)) { 105 | return null; 106 | } 107 | 108 | if (RtsCliUtil.keyToHexStr(remoteKey) in this.sessions["remote-keys"]) { 109 | return this.sessions["remote-keys"][RtsCliUtil.keyToHexStr(remoteKey)]; 110 | } 111 | 112 | return null; 113 | } 114 | 115 | clearSessions() { 116 | this.sessions["remote-keys"] = {}; 117 | } 118 | 119 | deleteSession(remoteKey) { 120 | if (!("remote-keys" in this.sessions)) { 121 | return; 122 | } 123 | 124 | if (RtsCliUtil.keyToHexStr(remoteKey) in this.sessions["remote-keys"]) { 125 | delete this.sessions["remote-keys"]; 126 | } 127 | } 128 | 129 | // --------------------------------------------------------------------------- 130 | 131 | // --------------------------------------------------------------------------- 132 | 133 | save() { 134 | Sessions.setCookie("sessions", JSON.stringify(this.sessions)); 135 | } 136 | 137 | static keyDictToArray(dict) { 138 | let dKeys = Object.keys(dict); 139 | let ret = new Uint8Array(dKeys.length); 140 | 141 | for (let j = 0; j < dKeys.length; j++) { 142 | ret[j] = dict[dKeys[j]]; 143 | } 144 | 145 | return ret; 146 | } 147 | 148 | static getCookie(cname) { 149 | var name = cname + "="; 150 | var ca = document.cookie.split(";"); 151 | for (var i = 0; i < ca.length; i++) { 152 | var c = ca[i]; 153 | while (c.charAt(0) == " ") { 154 | c = c.substring(1); 155 | } 156 | if (c.indexOf(name) == 0) { 157 | return c.substring(name.length, c.length); 158 | } 159 | } 160 | return ""; 161 | } 162 | 163 | static setCookie(cname, cvalue) { 164 | document.cookie = cname + "=" + cvalue + ";"; 165 | } 166 | } 167 | 168 | module.exports = { Sessions }; 169 | -------------------------------------------------------------------------------- /rts-js/settings.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | var { Stack } = require("./stack.js"); 4 | const STACK = "stacks"; 5 | 6 | class Settings { 7 | constructor(settingsJson) { 8 | this.stackDict = {}; 9 | this.parse(settingsJson); 10 | } 11 | 12 | parse(json) { 13 | var stackJson = json[STACK]; 14 | 15 | if (stackJson !== undefined) { 16 | for (const name in stackJson) { 17 | this.stackDict[name] = new Stack(name, stackJson[name]); 18 | } 19 | } 20 | } 21 | 22 | getStackNames() { 23 | return Object.keys(this.stackDict); 24 | } 25 | 26 | getStack(name) { 27 | return this.stackDict[name]; 28 | } 29 | } 30 | 31 | module.exports = { Settings }; 32 | -------------------------------------------------------------------------------- /rts-js/stack.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | const ACCOUNT_ENDPOINTS = "accountEndpoints"; 4 | const API_KEYS = "apiKeys"; 5 | 6 | const TYPE = { 7 | CLOUD: "cloud", 8 | LOCAL: "local", 9 | }; 10 | 11 | class Stack { 12 | constructor(name, stackJson) { 13 | this.name = name; 14 | this.apiKeys = null; 15 | this.accountEndpoints = null; 16 | this.parse(stackJson); 17 | } 18 | 19 | parse(json) { 20 | if (json[API_KEYS] !== undefined) { 21 | this.apiKeys = json[API_KEYS]; 22 | } 23 | 24 | if (json[ACCOUNT_ENDPOINTS] !== undefined) { 25 | this.accountEndpoints = json[ACCOUNT_ENDPOINTS]; 26 | } 27 | } 28 | 29 | getAccountEndpoints() { 30 | return this.accountEndpoints; 31 | } 32 | 33 | getApiKeys() { 34 | return this.apiKeys; 35 | } 36 | } 37 | 38 | module.exports = { Stack, TYPE }; 39 | -------------------------------------------------------------------------------- /rts-js/vectorBluetooth.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | var { BleMessageProtocol } = require("./bleMessageProtocol.js"); 4 | 5 | class VectorBluetooth { 6 | constructor() { 7 | this.vectorService = 0xfee3; 8 | this.readCharService = "7d2a4bda-d29b-4152-b725-2491478c5cd7"; 9 | this.writeCharService = "30619f2d-0f54-41bd-a65a-7588d8c85b45"; 10 | this.pairingChar = "p".charCodeAt(0); 11 | this.maxPacketSize = 20; 12 | this.bleMsgProtocol = null; 13 | this.readChar; 14 | this.writeChar; 15 | this.onReceiveEvent = []; 16 | this.onCancelSelectEvent = []; 17 | this.onDisconnectedEvent = []; 18 | this.writeQueue = []; 19 | this.writeReady = true; 20 | let self = this; 21 | this.tickInterval = window.setInterval(function () { 22 | self.tick(); 23 | }, 70); 24 | this.sessions = {}; 25 | 26 | this.initializeBleProtocol(); 27 | } 28 | 29 | initializeBleProtocol() { 30 | let self = this; 31 | this.bleMsgProtocol = new BleMessageProtocol(this.maxPacketSize); 32 | this.bleMsgProtocol.setDelegate(this); 33 | this.bleMsgProtocol.onSendRaw(function (buffer) { 34 | self.sendMessage(Uint8Array.from(buffer), false); 35 | }); 36 | } 37 | 38 | send(arr) { 39 | this.bleMsgProtocol.sendMessage(arr); 40 | } 41 | 42 | onReceive(fnc) { 43 | this.onReceiveEvent.push(fnc); 44 | } 45 | 46 | onCancelSelect(fnc) { 47 | this.onCancelSelectEvent.push(fnc); 48 | } 49 | 50 | onDisconnected(fnc) { 51 | this.onDisconnectedEvent.push(fnc); 52 | } 53 | 54 | onReceiveUnsubscribe(obj) { 55 | for (let i = 0; i < this.onReceiveEvent.length; i++) { 56 | if (obj == this.onReceiveEvent[i]) { 57 | this.onReceiveEvent.splice(i, 1); 58 | return; 59 | } 60 | } 61 | } 62 | 63 | handleReceive(data) { 64 | let listeners = this.onReceiveEvent.slice(0); 65 | 66 | for (let i = 0; i < listeners.length; i++) { 67 | listeners[i].receive(data); 68 | } 69 | } 70 | 71 | handleDisconnected() { 72 | this.bleName = ""; 73 | this.bleDevice = null; 74 | 75 | for (let i = 0; i < this.onDisconnectedEvent.length; i++) { 76 | this.onDisconnectedEvent[i](); 77 | } 78 | } 79 | 80 | tryConnect(vectorFilter) { 81 | let self = this; 82 | let f = { services: [this.vectorService] }; 83 | if (vectorFilter != null) { 84 | f["name"] = vectorFilter; 85 | } 86 | 87 | navigator.bluetooth 88 | .requestDevice({ 89 | filters: [f], 90 | optionalServices: [], 91 | }) 92 | .then( 93 | (device) => { 94 | self.bleName = device.name; 95 | self.bleDevice = device; 96 | self.bleDevice.addEventListener( 97 | "gattserverdisconnected", 98 | function () { 99 | self.handleDisconnected(); 100 | } 101 | ); 102 | self.connectToDevice(device); 103 | }, 104 | (error) => { 105 | // user didn't select any peripherals 106 | for (let i = 0; i < this.onCancelSelectEvent.length; i++) { 107 | this.onCancelSelectEvent[i](); 108 | } 109 | } 110 | ); 111 | } 112 | 113 | tryDisconnect() { 114 | if (this.bleDevice) { 115 | this.bleDevice.gatt.disconnect(); 116 | } 117 | } 118 | 119 | connectToDevice(device) { 120 | device.gatt 121 | .connect() 122 | .then((server) => { 123 | return server.getPrimaryService(this.vectorService); 124 | }) 125 | .then((service) => { 126 | let readChar = service.getCharacteristic(this.readCharService); 127 | let writeChar = service.getCharacteristic(this.writeCharService); 128 | return Promise.all([readChar, writeChar]); 129 | }) 130 | .then((characteristics) => { 131 | let self = this; 132 | self.readChar = characteristics[0]; 133 | self.writeChar = characteristics[1]; 134 | 135 | characteristics[1].startNotifications().then((ch) => { 136 | ch.addEventListener("characteristicvaluechanged", function (event) { 137 | self.bleMsgProtocol.receiveRawBuffer( 138 | Array.from(new Uint8Array(event.target.value.buffer)) 139 | ); 140 | }); 141 | }); 142 | }); 143 | } 144 | 145 | forceSendMsg() { 146 | if (this.writeQueue.length > 0) { 147 | let msg = this.writeQueue[0]; 148 | this.writeReady = false; 149 | let self = this; 150 | this.readChar.writeValue(msg).then( 151 | function () { 152 | self.writeReady = true; 153 | }, 154 | function (err) { 155 | console.log(err); 156 | } 157 | ); 158 | this.writeQueue.shift(); 159 | } 160 | } 161 | 162 | trySendMsg() { 163 | if (this.writeReady) { 164 | this.forceSendMsg(); 165 | } 166 | } 167 | 168 | sendMessage(msg) { 169 | this.writeQueue.push(msg); 170 | this.trySendMsg(); 171 | } 172 | 173 | tick() { 174 | this.trySendMsg(); 175 | } 176 | } 177 | 178 | module.exports = { VectorBluetooth }; 179 | -------------------------------------------------------------------------------- /site/css/vecSetup.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | html, 4 | body { 5 | color: #fff; 6 | background-color: #000; 7 | width: 100%; 8 | height: 100%; 9 | font-family: "Open Sans", sans-serif; 10 | font-size: 10pt; 11 | margin: 0px; 12 | } 13 | 14 | body { 15 | position: relative; 16 | } 17 | 18 | a { 19 | color: #4fed94; 20 | } 21 | 22 | a:visited { 23 | color: #777; 24 | } 25 | 26 | .vec-table::-webkit-scrollbar { 27 | width: 10px; 28 | } 29 | 30 | .vec-table::-webkit-scrollbar-thumb { 31 | background: #4fed94; 32 | border-radius: 10px; 33 | } 34 | 35 | .vec-table::-webkit-scrollbar-track { 36 | background: #444; 37 | border-radius: 10px; 38 | } 39 | 40 | .vec-ota-table { 41 | max-height: 70vh; 42 | overflow-y: auto; 43 | overflow-x: hidden; 44 | } 45 | 46 | select.vec-field { 47 | width: 240px; 48 | } 49 | 50 | .chrome-img { 51 | width: 128px; 52 | margin: 20px; 53 | } 54 | 55 | .vec-env-container { 56 | padding: 10%; 57 | display: flex; 58 | flex-wrap: wrap; 59 | justify-content: center; 60 | align-items: center; 61 | } 62 | 63 | .vec-env { 64 | cursor: pointer; 65 | } 66 | 67 | .vec-env-select-btn { 68 | margin: 10px; 69 | padding-top: 10px; 70 | padding-bottom: 10px; 71 | height: auto !important; 72 | } 73 | 74 | .vec-env-error-icon { 75 | height: 80px; 76 | } 77 | 78 | .vec-instruction { 79 | display: flex; 80 | height: 120px; 81 | } 82 | 83 | .vec-instruction > div { 84 | line-height: 120px; 85 | } 86 | 87 | .vec-row { 88 | display: flex; 89 | align-items: center; 90 | justify-content: center; 91 | } 92 | 93 | .vec-card { 94 | width: 100px; 95 | padding: 10px; 96 | } 97 | 98 | .vec-wifi-signal { 99 | width: 40px; 100 | } 101 | 102 | .vec-ota-type { 103 | height: 30px; 104 | } 105 | 106 | .vec-icon { 107 | width: 30px; 108 | filter: opacity(25%); 109 | transition: filter 0.5s; 110 | margin: 5px; 111 | } 112 | 113 | #iconComplete { 114 | filter: hue-rotate(50deg); 115 | } 116 | 117 | #wifiScanTable { 118 | max-height: 300px; 119 | overflow-y: auto; 120 | } 121 | 122 | .vec-icon-done { 123 | filter: opacity(100%) brightness(0.8) sepia(1) saturate(10000%) 124 | hue-rotate(76deg); 125 | } 126 | 127 | .vec-icon-active { 128 | animation-name: vec-icon-anim; 129 | animation-duration: 1s; 130 | animation-iteration-count: infinite; 131 | animation-timing-function: linear; 132 | animation-direction: alternate; 133 | } 134 | 135 | .vec-status { 136 | position: absolute; 137 | top: 0px; 138 | display: flex; 139 | height: 50px; 140 | } 141 | 142 | .logo-anki { 143 | position: absolute; 144 | bottom: 0px; 145 | left: 0px; 146 | width: 100px; 147 | padding: 10px; 148 | } 149 | 150 | .logo-vector { 151 | position: absolute; 152 | bottom: 0px; 153 | right: 0px; 154 | width: 150px; 155 | padding: 4px; 156 | } 157 | 158 | .vec-spinner { 159 | height: 34px; 160 | width: 34px; 161 | border-radius: 100%; 162 | background: -webkit-linear-gradient(left top, #4fed94 9%, #000 45%); 163 | padding: 2px; 164 | animation-name: vec-spinner-anim; 165 | animation-duration: 1s; 166 | animation-iteration-count: infinite; 167 | animation-timing-function: linear; 168 | } 169 | 170 | .vec-spinner-container { 171 | height: 80px; 172 | width: 80px; 173 | display: flex; 174 | align-items: center; 175 | justify-content: center; 176 | } 177 | 178 | .vec-spinner-inner { 179 | background-color: #000; 180 | height: 34px; 181 | width: 34px; 182 | border-radius: 100%; 183 | } 184 | 185 | @keyframes vec-spinner-anim { 186 | from { 187 | transform: rotate(0deg); 188 | } 189 | to { 190 | transform: rotate(360deg); 191 | } 192 | } 193 | 194 | @keyframes vec-icon-anim { 195 | from { 196 | filter: opacity(25%); 197 | } 198 | to { 199 | filter: opacity(100%); 200 | } 201 | } 202 | 203 | .vec-container { 204 | visibility: hidden; 205 | display: none; 206 | opacity: 0; 207 | transition: opacity 500ms; 208 | overflow-y: auto; 209 | max-height: 100%; 210 | padding: 0px 15px; 211 | } 212 | 213 | .vec-container.vec-current { 214 | visibility: visible; 215 | display: block; 216 | } 217 | 218 | .vec-btn { 219 | outline: none; 220 | color: #fff; 221 | background-color: #000; 222 | height: 40px; 223 | border-radius: 20px; 224 | min-width: 200px; 225 | text-transform: uppercase; 226 | border: 2px solid #fff; 227 | margin-bottom: 3px; 228 | transition: background-color 500ms, border 500ms, color 500ms; 229 | cursor: pointer; 230 | } 231 | 232 | .vec-field { 233 | outline: none; 234 | color: #fff; 235 | box-sizing: border-box; 236 | padding: 0 12px; 237 | background-color: #000; 238 | height: 40px; 239 | border-radius: 5px; 240 | min-width: 200px; 241 | text-transform: uppercase; 242 | border: 2px solid #fff; 243 | margin-bottom: 3px; 244 | transition: background-color 500ms, border 500ms, color 500ms; 245 | } 246 | 247 | .vec-field.readonly { 248 | border: none !important; 249 | } 250 | 251 | .vec-field[type="button"] { 252 | border-radius: 20px; 253 | cursor: pointer; 254 | } 255 | 256 | .vec-field.vec-default { 257 | text-transform: none; 258 | } 259 | 260 | .vec-field:focus { 261 | border: 2px solid #4fed94; 262 | color: #4fed94; 263 | } 264 | 265 | .vec-progress-bar { 266 | width: 100%; 267 | height: 40px; 268 | position: relative; 269 | margin: 10px; 270 | left: -10px; 271 | min-width: 240px; 272 | } 273 | 274 | .vec-progress-bar-fill { 275 | left: 0px; 276 | right: 0px; 277 | height: 30px; 278 | box-sizing: border-box; 279 | position: absolute; 280 | border-radius: 20px; 281 | background-color: #4fed94; 282 | margin: 5px; 283 | } 284 | 285 | .vec-progress-bar-border { 286 | width: 100%; 287 | height: 100%; 288 | border: 2px solid #fff; 289 | box-sizing: border-box; 290 | border-radius: 20px; 291 | position: absolute; 292 | } 293 | 294 | .vec-progress-bar-mask { 295 | background-color: #000; 296 | width: 100%; 297 | height: 40px; 298 | right: 0; 299 | box-sizing: border-box; 300 | position: absolute; 301 | transition: width 1s; 302 | } 303 | 304 | .vec-progress-bar.vec-sm { 305 | height: 16px; 306 | position: relative; 307 | margin: 10px; 308 | left: -10px; 309 | width: 100%; 310 | min-width: 0px !important; 311 | } 312 | 313 | .vec-sm > .vec-progress-bar-fill { 314 | left: 0px; 315 | right: 0px; 316 | height: 8px; 317 | box-sizing: border-box; 318 | position: absolute; 319 | border-radius: 4px; 320 | background-color: #4fed94; 321 | margin: 4px; 322 | } 323 | 324 | .vec-sm > .vec-progress-bar-border { 325 | width: 100%; 326 | height: 100%; 327 | border: 2px solid #fff; 328 | box-sizing: border-box; 329 | border-radius: 8px; 330 | position: absolute; 331 | } 332 | 333 | .vec-sm > .vec-progress-bar-mask { 334 | background-color: #000; 335 | width: 100%; 336 | height: 16px; 337 | right: 0; 338 | box-sizing: border-box; 339 | position: absolute; 340 | transition: width 1s; 341 | } 342 | 343 | .vec-btn:hover { 344 | border: 2px solid #4fed94; 345 | color: #4fed94; 346 | } 347 | 348 | .vec-wifi-row { 349 | box-sizing: border-box; 350 | height: 60px; 351 | display: flex; 352 | padding: 10px; 353 | min-width: 300px; 354 | text-align: center; 355 | transition: color 500ms, border 500ms; 356 | cursor: pointer; 357 | border-top: 1px solid rgba(0, 0, 0, 0); 358 | border-bottom: 1px solid rgba(0, 0, 0, 0); 359 | } 360 | 361 | .vec-wifi-ssid { 362 | line-height: 40px; 363 | } 364 | 365 | .vec-wifi-row:hover, 366 | .vec-ota-row:hover { 367 | color: #4fed94; 368 | border-top: 1px solid #4fed94; 369 | border-bottom: 1px solid #4fed94; 370 | } 371 | 372 | .vec-ota-row { 373 | box-sizing: border-box; 374 | padding: 5px; 375 | min-width: 300px; 376 | transition: color 500ms, border 500ms; 377 | cursor: pointer; 378 | border-top: 1px solid rgba(0, 0, 0, 0); 379 | border-bottom: 1px solid rgba(0, 0, 0, 0); 380 | } 381 | 382 | .vec-ota-name { 383 | font-size: medium; 384 | } 385 | 386 | .vec-ota-host { 387 | font-size: small; 388 | } 389 | 390 | .vec-format { 391 | text-align: center; 392 | } 393 | 394 | .vec-link { 395 | color: #fff; 396 | text-decoration: underline; 397 | transition: color 500ms; 398 | cursor: pointer; 399 | } 400 | 401 | div.vec-link { 402 | text-align: center; 403 | } 404 | 405 | .vec-shell { 406 | width: 100%; 407 | height: 100%; 408 | background-color: rgba(2, 23, 11, 0.9); 409 | flex: 0; 410 | } 411 | 412 | .vec-link:hover { 413 | color: #4fed94; 414 | } 415 | 416 | .vec-error { 417 | color: #f77; 418 | margin: 10px; 419 | } 420 | 421 | .vec-hidden { 422 | display: none; 423 | } 424 | 425 | .vec-vector-status { 426 | position: absolute; 427 | min-width: 120px; 428 | height: auto; 429 | right: 10px; 430 | top: 10px; 431 | } 432 | 433 | .vec-info-box { 434 | min-width: 120px; 435 | height: auto; 436 | border-radius: 20px; 437 | border: 2px solid #4fed94; 438 | padding: 5px 20px; 439 | background-color: #000; 440 | margin: 3px 0px; 441 | } 442 | 443 | .vec-status-icon { 444 | height: 11px; 445 | margin-right: 4px; 446 | } 447 | 448 | .vec-status-none { 449 | width: 0px; 450 | display: inline-block; 451 | } 452 | 453 | .vec-info-title { 454 | color: #4fed94; 455 | text-align: center; 456 | margin-bottom: 5px; 457 | font-weight: 700; 458 | } 459 | 460 | .vec-eyecon { 461 | height: 16px; 462 | } 463 | 464 | .vec-password-block { 465 | position: relative; 466 | display: inline-block; 467 | } 468 | 469 | .vec-eye-toggle { 470 | position: absolute; 471 | top: 12px; 472 | right: 10px; 473 | } 474 | 475 | .vec-link { 476 | cursor: pointer; 477 | transition: color 0.5s; 478 | } 479 | 480 | .vec-link:hover { 481 | color: #4fed94; 482 | } 483 | 484 | .vec-panel { 485 | position: relative; 486 | flex: 0 0 0%; 487 | width: 0px; 488 | transition: flex 0.2s, width 0.2s; 489 | overflow: hidden; 490 | } 491 | 492 | .vec-panel-ui { 493 | align-items: center; 494 | justify-content: center; 495 | display: flex; 496 | } 497 | 498 | .vec-panel-container { 499 | display: flex; 500 | width: 100%; 501 | height: 100%; 502 | overflow: hidden; 503 | } 504 | 505 | .vec-panel-footer { 506 | bottom: 10px; 507 | position: absolute; 508 | font-size: 8pt; 509 | } 510 | 511 | .vec-ui-footer { 512 | display: flex; 513 | position: absolute; 514 | left: 0px; 515 | right: 0px; 516 | bottom: 0px; 517 | height: 50px; 518 | } 519 | 520 | .vec-ui-footer-side { 521 | flex: 0 1 auto; 522 | display: flex; 523 | } 524 | 525 | .vec-ui-footer-side > img { 526 | width: 61px; 527 | padding: 10px; 528 | justify-content: center; 529 | align-items: center; 530 | } 531 | 532 | .vec-ui-footer-middle { 533 | flex: 1 0 auto; 534 | display: flex; 535 | justify-content: center; 536 | align-items: center; 537 | text-align: center; 538 | color: #999; 539 | font-size: 8pt; 540 | } 541 | 542 | .vec-ui-footer-middle * a { 543 | color: #999; 544 | } 545 | 546 | .vec-ui-footer-middle:visited * a { 547 | color: #999; 548 | } 549 | 550 | .vec-action { 551 | cursor: pointer; 552 | } 553 | -------------------------------------------------------------------------------- /site/images/AnkiLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | 11 | 15 | 19 | 21 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /site/images/VectorLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 25 | -------------------------------------------------------------------------------- /site/images/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digital-dream-labs/vector-web-setup/0637cf72b61bafdabde2f1a2b5776ec03daf405a/site/images/chrome.png -------------------------------------------------------------------------------- /site/images/common_icon_updatecloud_sml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /site/images/common_statusicon_cloudready_sml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /site/images/common_statusicon_wificonnect_med.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /site/images/ddl_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digital-dream-labs/vector-web-setup/0637cf72b61bafdabde2f1a2b5776ec03daf405a/site/images/ddl_logo.png -------------------------------------------------------------------------------- /site/images/ddl_logo_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digital-dream-labs/vector-web-setup/0637cf72b61bafdabde2f1a2b5776ec03daf405a/site/images/ddl_logo_sm.png -------------------------------------------------------------------------------- /site/images/fontawesome/bluetooth-brands.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/images/fontawesome/cloud-download-alt-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/images/fontawesome/exclamation-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/images/fontawesome/eye-slash-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/images/fontawesome/eye-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/images/fontawesome/layer-group-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/images/fontawesome/sd-card-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/images/fontawesome/sliders-h-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/images/fontawesome/user-circle-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/images/fontawesome/wifi-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /site/images/friends_icon_profileheadshot_sml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 12 | 13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /site/images/icon_accountManage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 13 | 14 | 15 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /site/images/icon_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /site/images/nav_icon_backpacklights_sml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 13 | 14 | 16 | 18 | 19 | 24 | 30 | 31 | 32 | 35 | 37 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | -------------------------------------------------------------------------------- /site/images/onboarding_icon_plugincharger_med.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 14 | 16 | 19 | 20 | 21 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /site/images/onboarding_icon_pressbackpack_med.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 12 | 21 | 23 | 25 | 26 | 29 | 31 | 36 | 41 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /site/images/settings_icon_lock_mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /site/images/settings_icon_wifilife_1bars_mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /site/images/settings_icon_wifilife_2bars_mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /site/images/settings_icon_wifilife_3bars_mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /site/images/statlog_icon_checkmark_success_mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /site/js/pterm.js: -------------------------------------------------------------------------------- 1 | let pterm_current_line = ""; 2 | let pterm_current_pos = 0; 3 | let pterm_history_list = []; 4 | let pterm_history_pos = 0; 5 | let pterm_cmd_event = []; 6 | let pterm_env_event = []; 7 | let pterm_handled = false; 8 | let pterm_prompt = "$"; 9 | let pterm_env = {}; 10 | let pterm_editing = true; 11 | let pterm_prompt_promise = null; 12 | const pterm_env_var_regex = /([A-Z_]?[A-Z0-9_]*)/ 13 | const pterm_quote_regex = /(["'])(.*)(?" + 18 | "
" + pterm_prompt + "
" + 19 | "
" + 20 | ""; 21 | pterm_current_line = ""; 22 | pterm_current_pos = 0; 23 | pterm_history_pos = pterm_history_list.length; 24 | let p = $(".pterm-current"); 25 | p.removeClass("pterm-current"); 26 | p.children(".pterm-cursor").remove(); 27 | let m = $(".pterm-main"); 28 | m.append(line); 29 | m.scrollTop(m.prop("scrollHeight")); 30 | $(".pterm-cursor").css("visibility", "visible"); 31 | pterm_editing = true; 32 | } 33 | 34 | function pterm_set(name, value) { 35 | pterm_env[name] = value; 36 | } 37 | 38 | function pterm_on(event, callback) { 39 | switch(event) { 40 | case 'cmd': 41 | pterm_cmd_event.push(callback); 42 | break; 43 | case 'env': 44 | pterm_env_event.push(callback); 45 | break; 46 | default: 47 | break; 48 | } 49 | } 50 | 51 | function execute(event_list, args) { 52 | if(pterm_prompt_promise) { 53 | pterm_prompt_promise.resolve(args); 54 | pterm_prompt_promise = null; 55 | newLine(); 56 | } else { 57 | for(let i = 0; i < event_list.length; i++) { 58 | event_list[i](args); 59 | } 60 | } 61 | 62 | if(event_list.length == 0) { 63 | pterm_handled = true; 64 | } 65 | } 66 | 67 | // 68 | // ------------------------------------------------------------------------------------------------ 69 | // 70 | function getArgsFromLine(line) { 71 | let c_pos = 0; 72 | let args = []; 73 | let parsing = false; 74 | let s_pos = 0; 75 | 76 | while(c_pos < line.length) { 77 | let c = line.charAt(c_pos); 78 | if(c == ' ') { 79 | // skip over whitespace 80 | c_pos++; 81 | continue; 82 | } 83 | 84 | if(!parsing) { 85 | s_pos = c_pos; 86 | parsing = true; 87 | } 88 | 89 | if(c == '"' || c == "'") { 90 | let arg = parseQuote(line.substring(c_pos)); 91 | c_pos += arg.length + 1; 92 | } 93 | 94 | // advance character 95 | c_pos++; 96 | 97 | // peak 98 | if((line.charAt(c_pos) == ' ' || c_pos == line.length) && parsing) { 99 | args.push(line.substring(s_pos, c_pos)); 100 | parsing = false; 101 | } 102 | } 103 | 104 | for(let i = 0; i < args.length; i++) { 105 | let valueMatchString = new RegExp('^' + pterm_quote_regex.source + '$'); 106 | let valueResult = args[i].match(valueMatchString); 107 | 108 | if(valueResult != null) { 109 | args[i] = valueResult[2]; 110 | } 111 | 112 | if(args[i].length > 0 && args[i].charAt(0) == '$') { 113 | let k = args[i].substring(1); 114 | if(k in pterm_env) { 115 | args[i] = pterm_env[k]; 116 | } else { 117 | args[i] = ""; 118 | } 119 | } 120 | } 121 | 122 | return args; 123 | } 124 | 125 | function parseQuote(line) { 126 | let c_pos = 1; 127 | let c = line.charAt(0); 128 | 129 | while(c_pos < line.length) { 130 | if(line.charAt(c_pos) == c) { 131 | return line.substring(1, c_pos); 132 | } 133 | c_pos++; 134 | } 135 | 136 | return line.substring(1); 137 | } 138 | 139 | function pterm_changeprompt(str, color) { 140 | if(color) { 141 | str = "" + str + ""; 142 | } 143 | 144 | pterm_prompt = str + '$'; 145 | 146 | $(".pterm-current").siblings(".pterm-prompt").html(pterm_prompt); 147 | } 148 | 149 | function pterm_changeprompt_once(str, color) { 150 | if(color) { 151 | str = "" + str + ""; 152 | } 153 | 154 | $(".pterm-current").siblings(".pterm-prompt").html(str); 155 | } 156 | 157 | function pterm_read(input) { 158 | let p = new Promise(function(resolve, reject) { 159 | pterm_prompt_promise = { resolve:resolve, reject:reject }; 160 | newLine(); 161 | pterm_changeprompt_once(input, null); 162 | }); 163 | 164 | return p; 165 | } 166 | 167 | function pterm_print(text) { 168 | while(text.includes("\x1b[")) { 169 | text = text.replace("\x1b[0m", ""); 170 | text = text.replace("\x1b[91m", ""); 171 | text = text.replace("\x1b[92m", ""); 172 | text = text.replace("\x1b[93m", ""); 173 | } 174 | 175 | let lines = text.split("\n"); 176 | 177 | for(let i = 0; i < lines.length; i++) { 178 | let txt = "
" + 179 | "
" + 180 | lines[i] + "
"; 181 | 182 | $(".pterm-main").append(txt); 183 | } 184 | } 185 | 186 | function pterm_print_overwrite(text) { 187 | while(text.includes("\x1b[")) { 188 | text = text.replace("\x1b[0m", "
"); 189 | text = text.replace("\x1b[91m", ""); 190 | text = text.replace("\x1b[92m", ""); 191 | text = text.replace("\x1b[93m", ""); 192 | } 193 | 194 | let lines = text.split("\n"); 195 | 196 | for(let i = 0; i < lines.length; i++) { 197 | if(i == 0) { 198 | $(".pterm-row.pterm-text").last().html(txt); 199 | } else { 200 | let txt = "
" + 201 | "
" + 202 | lines[i] + "
"; 203 | $(".pterm-main").append(txt); 204 | } 205 | } 206 | } 207 | 208 | function pterm_new_progress_bar() { 209 | let txt = "
" + 210 | "
" + 211 | "
" + 212 | "
" + 213 | "
"; 214 | 215 | $(".pterm-main").append(txt); 216 | let m = $(".pterm-main"); 217 | m.scrollTop(m.prop("scrollHeight")); 218 | } 219 | 220 | function pterm_new_button(value) { 221 | let id = "id-" + Math.floor(Math.random() * 10000000); 222 | let txt = "
" + 223 | "
" + 224 | "" 225 | "
"; 226 | 227 | $(".pterm-main").append(txt); 228 | return id; 229 | } 230 | 231 | function pterm_set_progress_bar(val, max) { 232 | let p = (val/max) * 100; 233 | $(".pterm-progress-bar-track").last().width(p + "%"); 234 | } 235 | 236 | // 237 | // ------------------------------------------------------------------------------------------------ 238 | // 239 | 240 | function pterm_insert_history(line) { 241 | pterm_history_list.push(line); 242 | pterm_history_pos = pterm_history_list.length; 243 | } 244 | 245 | function processLine(line) { 246 | if(line == "") { 247 | return; 248 | } 249 | 250 | let args = getArgsFromLine(line); 251 | 252 | if(args[0] == "echo") { 253 | if(args[1] != null) { 254 | pterm_print(args[1]); 255 | } 256 | pterm_handled = true; 257 | } else if(args[0] == "printenv") { 258 | let envKeys = Object.keys(pterm_env); 259 | for(let i = 0; i < envKeys.length; i++) { 260 | pterm_print(envKeys[i] + "=" + pterm_env[envKeys[i]]); 261 | } 262 | pterm_handled = true; 263 | } else if(args[0] == "export" || args[0] == "set") { 264 | let matchString = new RegExp('^' + pterm_env_var_regex.source + '=' + '(.+)' + '$'); 265 | 266 | for(let i = 1; i < args.length; i++) { 267 | let result = args[i].match(matchString); 268 | 269 | if(result != null) { 270 | // found a match to export 271 | let valueMatchString = new RegExp('^' + pterm_quote_regex.source + '$'); 272 | let valueResult = result[2].match(valueMatchString); 273 | 274 | let value = result[2]; 275 | 276 | if(valueResult != null) { 277 | value = valueResult[2]; 278 | } 279 | 280 | pterm_set(result[1], value); 281 | } 282 | } 283 | 284 | for(let i = 0; i < pterm_env_event.length; i++) { 285 | pterm_env_event[i](); 286 | } 287 | 288 | pterm_handled = true; 289 | } else if(args[0] == "unset") { 290 | if(args[1] in pterm_env) { 291 | delete pterm_env[args[1]]; 292 | for(let i = 0; i < pterm_env_event.length; i++) { 293 | pterm_env_event[i](); 294 | } 295 | } 296 | pterm_handled = true; 297 | } else { 298 | $(".pterm-cursor").css("visibility", "hidden"); 299 | pterm_editing = false; 300 | execute(pterm_cmd_event, args); 301 | } 302 | 303 | pterm_insert_history(line); 304 | } 305 | 306 | $(document).ready(function() { 307 | $(document).keydown(function(event) { 308 | if($("*").is(":focus")) { 309 | return; 310 | } 311 | 312 | pterm_process_key(event); 313 | }); 314 | }); 315 | 316 | function pterm_process_key(event) { 317 | // don't scroll on arrow or space 318 | if([32, 37, 38, 39, 40].indexOf(event.keyCode) > -1) { 319 | event.preventDefault(); 320 | } 321 | 322 | if(event.metaKey) { 323 | if(event.key == "k") { 324 | $(".pterm-line:not(:last)").remove(); 325 | } 326 | } 327 | else if(event.altKey) { 328 | if(event.key == "ArrowLeft") { 329 | if(pterm_current_pos == 0) { 330 | return; 331 | } 332 | 333 | pterm_current_pos--; 334 | 335 | let c = pterm_current_line.charAt(pterm_current_pos); 336 | 337 | // 1. if c is space, then continue until non-space, 338 | // and then just right of next space 339 | // 2. if c is non-space, then continue until just right of next space 340 | 341 | if(c == ' ') { 342 | while((c=pterm_current_line.charAt(pterm_current_pos)) == ' ' && pterm_current_pos > 0) { 343 | pterm_current_pos--; 344 | } 345 | } 346 | 347 | while((c=pterm_current_line.charAt(pterm_current_pos)) != ' ' && pterm_current_pos > 0) { 348 | pterm_current_pos--; 349 | } 350 | 351 | if(pterm_current_pos != 0 || pterm_current_line.charAt(0) == ' ') { 352 | pterm_current_pos++; 353 | } 354 | } 355 | else if(event.key == "ArrowRight") { 356 | if(pterm_current_pos == pterm_current_line.length) { 357 | return; 358 | } 359 | 360 | let c = pterm_current_line.charAt(pterm_current_pos); 361 | 362 | // 1. if c is space, then continue until non-space, 363 | // 2. if c is non-space, then continue until just right of next space 364 | 365 | if(c != ' ') { 366 | while((c=pterm_current_line.charAt(pterm_current_pos)) != ' ' && pterm_current_pos < pterm_current_line.length) { 367 | pterm_current_pos++; 368 | } 369 | } 370 | 371 | while((c=pterm_current_line.charAt(pterm_current_pos)) == ' ' && pterm_current_pos < pterm_current_line.length) { 372 | pterm_current_pos++; 373 | } 374 | } 375 | } 376 | else if(event.ctrlKey) { 377 | if(event.key == "a") { 378 | pterm_current_pos = 0; 379 | } 380 | else if(event.key == "e") { 381 | pterm_current_pos = pterm_current_line.length; 382 | } 383 | else if(event.key == "c") { 384 | newLine(); 385 | } 386 | else if(event.key == "r") { 387 | // reverse search 388 | } 389 | } 390 | else if(event.key == "ArrowLeft") { 391 | if(pterm_current_pos > 0) { 392 | pterm_current_pos--; 393 | } 394 | } 395 | else if(event.key == "ArrowRight") { 396 | if(pterm_current_pos < pterm_current_line.length) { 397 | pterm_current_pos++; 398 | } 399 | } 400 | else if(event.key == "ArrowUp") { 401 | if(pterm_history_pos == 0) { 402 | return; 403 | } 404 | 405 | let line = pterm_history_list[--pterm_history_pos]; 406 | pterm_current_line = line; 407 | pterm_current_pos = line.length; 408 | } 409 | else if(event.key == "ArrowDown") { 410 | if(pterm_history_pos >= pterm_history_list.length - 1) { 411 | return; 412 | } 413 | 414 | let line = pterm_history_list[++pterm_history_pos]; 415 | pterm_current_line = line; 416 | pterm_current_pos = line.length; 417 | } 418 | else if(event.key == "Delete") { 419 | // delete 420 | if(pterm_current_pos < pterm_current_line.length) { 421 | pterm_current_line = pterm_current_line.substring(0, pterm_current_pos) + 422 | pterm_current_line.substring(pterm_current_pos + 1); 423 | } 424 | } 425 | else if(event.key == "Enter") { 426 | // handle Enter 427 | pterm_handled = false; 428 | processLine(pterm_current_line); 429 | if(pterm_handled) { 430 | newLine(); 431 | } 432 | } 433 | else if(event.keyCode == 8) { 434 | if(pterm_current_pos > 0) { 435 | pterm_current_line = pterm_current_line.substring(0, pterm_current_pos - 1) + 436 | pterm_current_line.substring(pterm_current_pos); 437 | pterm_current_pos--; 438 | } 439 | } 440 | else if((32 <= event.keyCode && event.keyCode <= 126) || (event.key.length == 1)) { 441 | let key = event.key; 442 | 443 | pterm_current_line = pterm_current_line.substring(0, pterm_current_pos) + 444 | key + 445 | pterm_current_line.substring(pterm_current_pos); 446 | pterm_current_pos++; 447 | } 448 | else { 449 | // console.log(event); 450 | } 451 | 452 | let w = $(".pterm-cursor").width(); 453 | $(".pterm-current > .pterm-text").html(pterm_current_line); 454 | $(".pterm-current").width(w * (pterm_current_line.length + 1)); 455 | $(".pterm-cursor").css("left", w * pterm_current_pos); 456 | } 457 | 458 | $(document).on("paste", function(event){ 459 | // get clipboard data 460 | let data = event.originalEvent.clipboardData.getData('text'); 461 | pterm_current_line = pterm_current_line.substring(0, pterm_current_pos) + 462 | data + 463 | pterm_current_line.substring(pterm_current_pos); 464 | pterm_current_pos += data.length; 465 | 466 | let w = $(".pterm-cursor").width(); 467 | $(".pterm-current > .pterm-text").html(pterm_current_line); 468 | $(".pterm-current").width(w * (pterm_current_line.length + 1)); 469 | $(".pterm-cursor").css("left", w * pterm_current_pos); 470 | }); -------------------------------------------------------------------------------- /src-node-client/blesh.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | let net = require('net'); 4 | 5 | class Blesh { 6 | constructor() { 7 | this.onReceiveDataEvent = null; 8 | this.server = null; 9 | this.socket = null; 10 | this.writeQueue = new Uint8Array(0); 11 | } 12 | 13 | static isSupported() { 14 | return true; 15 | } 16 | 17 | onReceiveData(fnc) { 18 | this.onReceiveDataEvent = fnc; 19 | } 20 | 21 | send(data) { 22 | if(this.socket != null) { 23 | if(this.writeQueue.length > 0) { 24 | this.socket.write(this.writeQueue); 25 | this.writeQueue = new Uint8Array(0); 26 | } 27 | 28 | this.socket.write(data); 29 | } else { 30 | // append to the writeQueue 31 | let tmp = new Uint8Array(this.writeQueue.length + data.length); 32 | tmp.set(this.writeQueue); 33 | tmp.set(data, this.writeQueue.length); 34 | this.writeQueue = tmp; 35 | } 36 | } 37 | 38 | stop() { 39 | if(this.socket != null) { 40 | this.socket.end(); 41 | this.socket = null; 42 | } 43 | 44 | if(this.server != null) { 45 | this.server.close(); 46 | this.server = null; 47 | } 48 | } 49 | 50 | start(port) { 51 | let self = this; 52 | 53 | let p = new Promise(function(resolve, reject) { 54 | self.server = net.createServer((c) => { 55 | // 'connection' listener 56 | console.log('* SSH client connected'); 57 | self.socket = c; 58 | 59 | c.on('end', () => { 60 | console.log('* SSH client disconnected.'); 61 | }); 62 | 63 | c.on('data', function(r) { 64 | self.onReceiveDataEvent(r); 65 | }); 66 | }); 67 | 68 | self.server.on('error', (err) => { 69 | throw err; 70 | }); 71 | 72 | self.server.listen(port, () => { 73 | console.log("* SSH tunnel server started."); 74 | }); 75 | 76 | resolve(true); 77 | }); 78 | 79 | return p; 80 | } 81 | } 82 | 83 | module.exports = { Blesh }; 84 | -------------------------------------------------------------------------------- /templates/main.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vector Setup 9 | 13 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 | 37 | 43 | 49 | 55 | 61 | 67 |
68 |
73 |
74 | 75 |
76 |
77 |

78 | Vector Web Setup uses Web Bluetooth which requires Google Chrome. 79 | See 80 | 83 | Mozilla's compatibility table 84 | 85 | for details. 86 |

87 |
88 |
89 |
94 |
95 |
96 |

Choose the stack you want to use

97 |
98 | 99 |

100 | 101 |
102 |
103 |
104 |
105 |
106 | 111 |
112 |
113 |

114 | There was an error loading the settings.json file. To view the 115 | file click here 116 |

117 |
118 |
119 |
120 |
121 |
122 | 126 |
1. Place Vector on his charger.
127 |
128 |
129 | 133 |
134 | 135 | 2. Double-press Vector's button and then click "PAIR WITH 136 | VECTOR." 137 | 138 |
139 |
140 |
141 |
142 |
143 | 147 |
148 | Wait until Vector finishes restarting, then click "PAIR WITH 149 | VECTOR." If Vector doesn't connect automatically, try 150 | double-pressing Vector's button. 151 |
152 |
153 |
154 |
155 | 161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
178 | Enable auto-setup flow.
185 | 191 |
192 |
193 |
194 | Error trying to connect Vector to wifi. 195 |
196 |
197 |
198 |

199 |
200 | 201 | Don't see your network? Click to join custom. 202 | 203 |
204 |
205 |
206 |
213 |
220 | 226 |
227 |
233 |
234 | Authorize vector with the account
used in Vector Mobile app

235 |
236 | Error. 237 |
238 |
245 |
246 | 253 |
259 | 263 |
264 |
265 |

266 |
272 | Reset Password
273 | Create Account 274 |
275 |
276 | Create a new account

277 |
278 | Error. 279 |
280 | 281 |
289 |
296 |
297 | 304 |
310 | 314 |
315 |
316 |

317 |
323 |
329 |
330 |
331 | 332 |
333 | Choose a timezone
334 | 379 |

380 | Choose temperature unit
381 | 385 |

386 | Choose distance system
387 | 391 |

392 | Enable Amazon Alexa.
398 | Allow Digital Dream Labs to upload anonymized logs to improve 404 | Vector.

405 | 411 |
412 |
413 |
414 |
Choose the image to upload
415 |
416 |
417 |
418 |
419 |
420 | 425 |
426 |
427 |

428 | No OTA's present in firmware folder. Try "vector-setup ota-sync" 429 | to sync all otas present in inventory.json file. To view the 430 | file click here 431 |

432 |
433 |
434 |
435 |
436 | Error while updating Vector. 437 |
438 |
439 | 440 | Updating Vector...
441 |
442 |
443 |
444 |
445 |
446 | 452 |
453 |
454 |
455 |
460 |

Vector setup is complete!

461 |
462 |
463 |

464 | Update completed. After Vector reboots, you will need to discover 465 | him again. 466 |

467 |
468 | 474 |
475 | 476 |
477 |
478 |
479 |
480 | hello 484 |
485 |
486 |
487 |
488 |
489 |
490 | 495 |
496 |
497 | 501 |
502 |
503 |
504 |
505 |
506 | 507 |
508 | Downloading logs... 509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 |
518 | 519 | 536 |
537 |
<%= serverIp %>
538 |
<%= networkIp %>
539 |
<%= port %>
540 |
541 |
542 |
543 |
$
544 |
545 | 546 |
547 |
548 |
549 |
550 |
551 |
552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | -------------------------------------------------------------------------------- /test/vector-setup/addOta.e2e.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | const { expect } = require("chai"); 6 | const should = require("chai").should(); 7 | 8 | const path = require("path"); 9 | 10 | const clicmd = require("../../utils/clicmd"); 11 | const fs = require("../../utils/fsPromise"); 12 | const { VECTOR_WEB_SETUP } = require("../../tools/common.js").filePath; 13 | 14 | const cliProcess = clicmd.create(VECTOR_WEB_SETUP, "."); // this will return a new object { execute } 15 | const testOta = "https://ddltestota.s3.amazonaws.com/test.ota"; 16 | 17 | const getTestPath = (stage) => 18 | path.join(VECTOR_WEB_SETUP, `../site/firmware/${stage}/test.ota`); 19 | 20 | const inventoryPath = path.join( 21 | VECTOR_WEB_SETUP, 22 | `../site/data/inventory.json` 23 | ); 24 | 25 | module.exports = () => { 26 | describe("when using ota-add", () => { 27 | let originalJson = {}; 28 | 29 | before(async () => { 30 | originalJson = await fs.readFile(inventoryPath); 31 | }); 32 | 33 | after(async () => { 34 | await fs.writeFile(inventoryPath, originalJson); 35 | }); 36 | 37 | beforeEach(async () => { 38 | await fs.writeFile(inventoryPath, originalJson); 39 | }); 40 | 41 | it("should throw error when url in not given", async () => { 42 | try { 43 | await cliProcess.execute(["ota-add"]); 44 | } catch (err) { 45 | should.exist(err); 46 | } 47 | }); 48 | 49 | it("should add ota to prod when no other option is used", async () => { 50 | try { 51 | await cliProcess.execute(["ota-add", testOta]); 52 | 53 | let testPath = getTestPath("prod"); 54 | expect(await fs.exists(testPath)).to.be.true; 55 | 56 | //Cleanup 57 | fs.unlink(testPath); 58 | } catch (err) { 59 | should.not.exist(err); 60 | } 61 | }); 62 | 63 | it("should add ota to a specific stack", async () => { 64 | try { 65 | await cliProcess.execute(["ota-add", testOta, "-e", "test"]); 66 | 67 | let testPath = getTestPath("test"); 68 | expect(await fs.exists(testPath)).to.be.true; 69 | 70 | //Cleanup 71 | fs.unlink(testPath); 72 | } catch (err) { 73 | should.not.exist(err); 74 | } 75 | }); 76 | 77 | it("should add ota with a specific name", async () => { 78 | try { 79 | await cliProcess.execute(["ota-add", testOta, "-n", "abc"]); 80 | 81 | let testPath = path.join( 82 | VECTOR_WEB_SETUP, 83 | `../site/firmware/prod/abc.ota` 84 | ); 85 | expect(await fs.exists(testPath)).to.be.true; 86 | 87 | //Cleanup 88 | fs.unlink(testPath); 89 | } catch (err) { 90 | should.not.exist(err); 91 | } 92 | }); 93 | }); 94 | }; 95 | -------------------------------------------------------------------------------- /test/vector-setup/approveOta.e2e.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | const chai = require("chai"); 6 | const chaiAsPromised = require("chai-as-promised"); 7 | const expect = chai.expect; 8 | const should = chai.should(); 9 | chai.use(chaiAsPromised); 10 | 11 | const path = require("path"); 12 | 13 | const clicmd = require("../../utils/clicmd"); 14 | const fs = require("../../utils/fsPromise"); 15 | const rmdir = require("../../utils/rmdir"); 16 | 17 | const { prompt, filePath } = require("../../tools/common"); 18 | const { VECTOR_WEB_SETUP } = filePath; 19 | 20 | const cliProcess = clicmd.create(VECTOR_WEB_SETUP, "."); // this will return a new object { execute } 21 | const testOta = "https://ddltestota.s3.amazonaws.com/test.ota"; 22 | 23 | const testStore = path.join(VECTOR_WEB_SETUP, `../site/firmware/test`); 24 | 25 | const inventoryPath = path.join( 26 | VECTOR_WEB_SETUP, 27 | `../site/data/inventory.json` 28 | ); 29 | 30 | module.exports = () => { 31 | describe("when using ota-approve", () => { 32 | let originalJson = {}; 33 | 34 | before(async () => { 35 | originalJson = await fs.readFile(inventoryPath); 36 | rmdir(testStore); 37 | }); 38 | 39 | after(async () => { 40 | await fs.writeFile(inventoryPath, originalJson); 41 | }); 42 | 43 | beforeEach(async () => { 44 | await fs.writeFile(inventoryPath, originalJson); 45 | }); 46 | 47 | afterEach(() => { 48 | rmdir(testStore); 49 | }); 50 | 51 | it("should add checksum if the checksum doesn't exist", async () => { 52 | await expect( 53 | cliProcess.execute(["ota-add", testOta, "-e", "test"]) 54 | ).to.be.eventually.contain("Downloading ota. It can take some minutes."); 55 | 56 | let process = cliProcess.execute(["ota-approve", "test/test.ota"]); 57 | 58 | await expect(process).to.be.eventually.contain( 59 | prompt.OTA_APPROVE_SUCCESS 60 | ); 61 | }); 62 | 63 | it("should throw error if the store doesn't exists", async () => { 64 | let process = cliProcess.execute([ 65 | "ota-approve", 66 | "non_existent/test.ota", 67 | ]); 68 | 69 | await expect(process).to.be.eventually.contain(prompt.STORE_NOT_EXISTS); 70 | }); 71 | 72 | it("should not approve again if the checksum exist", async () => { 73 | await expect( 74 | cliProcess.execute(["ota-add", testOta, "-e", "test"]) 75 | ).to.be.eventually.contain("Downloading ota. It can take some minutes."); 76 | 77 | await expect( 78 | cliProcess.execute(["ota-approve", "test/test.ota"]) 79 | ).to.be.eventually.contain(prompt.OTA_APPROVE_SUCCESS); 80 | 81 | await expect( 82 | cliProcess.execute(["ota-approve", "test/test.ota"]) 83 | ).to.be.eventually.contain(prompt.CHECKSUM_EXITS); 84 | }); 85 | }); 86 | }; 87 | -------------------------------------------------------------------------------- /test/vector-setup/index.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | describe("The vector-web-setup end to end", () => { 6 | require("./addOta.e2e")(); 7 | require("./approveOta.e2e")(); 8 | }); 9 | -------------------------------------------------------------------------------- /tools/common.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | var path = require("path"); 6 | 7 | const pathJoin = (str) => path.join(__dirname, str); 8 | 9 | module.exports = { 10 | filePath: { 11 | DEF_SETTINGS_FILE: pathJoin("../default-data/settings.json"), 12 | DEF_INVENTORY_FILE: pathJoin("../default-data/inventory.json"), 13 | PTERM_LESS_FILE: pathJoin("../less/pterm.less"), 14 | RTS_MAIN_JS: pathJoin("../rts-js/main.js"), 15 | 16 | DATA_FOLDER: pathJoin("../site/data"), 17 | SETTINGS_FILE: pathJoin("../site/data/settings.json"), 18 | INVENTORY_FILE: pathJoin("../site/data/inventory.json"), 19 | 20 | PTERM_CSS_FILE: pathJoin("../site/css/pterm.css"), 21 | RTS_JS_FILE: pathJoin("../site/js/rts.js"), 22 | 23 | FIRMWARE_FOLDER: pathJoin("../site/firmware"), 24 | 25 | VECTOR_WEB_SETUP: pathJoin("../vector-web-setup.js"), 26 | }, 27 | 28 | prompt: { 29 | STORE_NOT_EXISTS: "Inventory store doesn't exists", 30 | 31 | OTA_APPROVE_SUCCESS: "Ota approved", 32 | OTA_NOT_EXISTS: "Ota doesn't exists", 33 | CHECKSUM_EXITS: "Checksum already present", 34 | }, 35 | 36 | DEFUALT_ENV: "prod", 37 | }; 38 | -------------------------------------------------------------------------------- /tools/configure.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | var commander = require("commander"); 6 | var fs = require("fs"); 7 | var less = require("less"); 8 | var browserify = require("browserify"); 9 | 10 | const { 11 | DATA_FOLDER, 12 | SETTINGS_FILE, 13 | INVENTORY_FILE, 14 | 15 | DEF_SETTINGS_FILE, 16 | DEF_INVENTORY_FILE, 17 | 18 | PTERM_LESS_FILE, 19 | PTERM_CSS_FILE, 20 | RTS_JS_FILE, 21 | RTS_MAIN_JS, 22 | } = require("./common.js").filePath; 23 | 24 | const ensureDataFolderExists = () => { 25 | if (!fs.existsSync(DATA_FOLDER)) { 26 | fs.mkdirSync(DATA_FOLDER); 27 | } 28 | }; 29 | const processSettings = (force) => { 30 | try { 31 | ensureDataFolderExists(); 32 | console.log("Checking Settings file at " + SETTINGS_FILE); 33 | 34 | if (force) { 35 | console.log( 36 | "Overriding Settings file using file from " + DEF_SETTINGS_FILE 37 | ); 38 | fs.copyFileSync(DEF_SETTINGS_FILE, SETTINGS_FILE); 39 | } else if (fs.existsSync(SETTINGS_FILE)) { 40 | console.log("Settings file exist. No action taken"); 41 | } else { 42 | console.log("Adding Settings file using file from " + DEF_SETTINGS_FILE); 43 | fs.copyFileSync(DEF_SETTINGS_FILE, SETTINGS_FILE); 44 | } 45 | } catch (err) { 46 | console.log(err); 47 | } 48 | }; 49 | 50 | const processInventory = (force) => { 51 | try { 52 | ensureDataFolderExists(); 53 | console.log("Checking Inventory file at " + INVENTORY_FILE); 54 | 55 | if (force) { 56 | console.log( 57 | "Overriding Inventory file using file from " + DEF_INVENTORY_FILE 58 | ); 59 | fs.copyFileSync(DEF_INVENTORY_FILE, INVENTORY_FILE); 60 | } else if (fs.existsSync(INVENTORY_FILE)) { 61 | console.log("Inventory file exists. No action taken"); 62 | } else { 63 | console.log( 64 | "Adding Inventory file using file from " + DEF_INVENTORY_FILE 65 | ); 66 | fs.copyFileSync(DEF_INVENTORY_FILE, INVENTORY_FILE); 67 | } 68 | } catch (err) { 69 | console.log(err); 70 | } 71 | }; 72 | 73 | const processLess = async () => { 74 | console.log("Adding css files at " + PTERM_LESS_FILE); 75 | const content = fs.readFileSync(PTERM_LESS_FILE, "utf-8"); 76 | const result = await less.render(content); 77 | fs.writeFileSync(PTERM_CSS_FILE, result.css, "utf-8"); 78 | }; 79 | 80 | const processRts = () => { 81 | console.log("Adding rts.js file at " + RTS_JS_FILE); 82 | 83 | return new Promise((resolve, reject) => { 84 | browserify() 85 | .add(RTS_MAIN_JS) 86 | .bundle() 87 | .pipe(fs.createWriteStream(RTS_JS_FILE)) 88 | .on("finish", resolve) 89 | .on("error", reject); 90 | }); 91 | }; 92 | 93 | const configure = new commander.Command("configure"); 94 | 95 | configure 96 | .description("configure the settings and assets for websetup") 97 | .option("-rs, --reset-setting", "Use to force override settings.json") 98 | .option("-ri, --reset-inventory", "Use to force override inventory.json") 99 | .action(async (cmd) => { 100 | console.log("Running configures..."); 101 | 102 | processSettings(cmd.resetSetting); 103 | processInventory(cmd.resetInventory); 104 | processLess(); 105 | await processRts(); 106 | 107 | console.log("Done."); 108 | }); 109 | 110 | module.exports = configure; 111 | -------------------------------------------------------------------------------- /tools/inventory/firmwareStore.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | const axios = require("axios"); 6 | const fs = require("../../utils/fsPromise"); 7 | 8 | const { filePath, prompt } = require("../common.js"); 9 | const { Ota } = require("./ota"); 10 | const { generateChecksum } = require("../../utils/hash"); 11 | 12 | const healthStatus = { 13 | checksum: { 14 | OK: "ok", 15 | MISMATCH: "mismatch", 16 | NOT_DEFINED: "checksum not defined", 17 | }, 18 | 19 | filing: { 20 | OK: "ok", 21 | ABSENT: "absent", 22 | }, 23 | }; 24 | 25 | class FirmwareStore { 26 | constructor(env, otas) { 27 | this.location = filePath.FIRMWARE_FOLDER + "/" + env; 28 | this.otas = otas; 29 | this.name = env; 30 | } 31 | 32 | async setup() { 33 | try { 34 | const storeExists = await fs.exists(this.location); 35 | if (!storeExists) { 36 | await fs.mkdir(this.location); 37 | } 38 | } catch (err) { 39 | if (err.code == "ENOENT") { 40 | let frimwareFolderExists = await fs.exists(filePath.FIRMWARE_FOLDER); 41 | if (!frimwareFolderExists) { 42 | await fs.mkdir(filePath.FIRMWARE_FOLDER); 43 | await this.setup(); 44 | } 45 | } else { 46 | this.log(ota, err); 47 | } 48 | } 49 | } 50 | 51 | async downloadAndSaveConfig(ota, options) { 52 | let force = options.force == undefined ? false : options.force; 53 | 54 | if (!(await this.exists(ota)) || force) { 55 | await this.download(ota); 56 | } else { 57 | this.log(ota, "Ota already exists. No action was taken"); 58 | } 59 | } 60 | 61 | async download(ota) { 62 | if (ota === undefined) { 63 | this.log(ota, prompt.OTA_NOT_EXISTS); 64 | return; 65 | } 66 | 67 | this.log(ota, "Downloading ota. It can take some minutes."); 68 | 69 | const writer = fs.createWriteStream(this.getPath(ota)); 70 | const respose = await axios.get(ota.url, { responseType: "stream" }); 71 | respose.data.pipe(writer); 72 | 73 | return new Promise((resolve, reject) => { 74 | writer.on("finish", () => { 75 | if (this.otas.filter((o) => ota.compare(o)).length > 0) { 76 | this.log(ota, "The ota is already present in inventory manifest"); 77 | } else { 78 | this.otas.push(ota); 79 | } 80 | 81 | resolve(); 82 | }); 83 | 84 | writer.on("error", () => 85 | reject("There was an error while downlaoding the ota. Please the url") 86 | ); 87 | }); 88 | } 89 | 90 | exists(ota) { 91 | return fs.exists(this.getPath(ota)); 92 | } 93 | 94 | getPath(ota) { 95 | return this.location + `/${ota.name}`; 96 | } 97 | 98 | getOta(name) { 99 | const ota = this.otas.filter((o) => o.name === name)[0]; 100 | 101 | if (ota === undefined) { 102 | throw new Error( 103 | `[${this.name} - ${name}] Ota doesn't exists in inventory. You need to add the ota using 'vector-setup ota-add'.` 104 | ); 105 | } 106 | 107 | return ota; 108 | } 109 | 110 | async sync(name) { 111 | let ota = this.getOta(name); 112 | 113 | await this.syncOta(ota); 114 | } 115 | 116 | async syncAll() { 117 | this.otas.map(async (ota) => { 118 | this.log(ota, "Starting Sync"); 119 | await this.syncOta(ota); 120 | }); 121 | } 122 | 123 | async syncOta(ota) { 124 | if (!(await this.exists(ota))) { 125 | await this.download(ota); 126 | } 127 | 128 | if (ota.checksum === undefined) { 129 | this.log( 130 | ota, 131 | "WARN: No checksum defined for the ota. The ota could be harmful" 132 | ); 133 | } else { 134 | let generatedChecksum = await generateChecksum(this.getPath(ota)); 135 | generatedChecksum === ota.checksum 136 | ? this.log(ota, "Checksum matched. Ota is safe to use") 137 | : this.log(ota, "Checksum mismatched. Delete the ota and retry"); 138 | } 139 | } 140 | 141 | async addChecksum(name) { 142 | let ota = this.getOta(name); 143 | 144 | if (ota.checksum != undefined) { 145 | this.log(ota, prompt.CHECKSUM_EXITS); 146 | return; 147 | } 148 | 149 | let checksum = await generateChecksum(this.getPath(ota)); 150 | ota.checksum = checksum; 151 | 152 | this.log(ota, prompt.OTA_APPROVE_SUCCESS); 153 | } 154 | 155 | log(ota, txt) { 156 | console.log(`[${this.name} - ${ota.name}] ${txt}`); 157 | } 158 | } 159 | 160 | FirmwareStore.freeze = (store) => { 161 | let frozenOtas = store.otas.reduce((acc, ota) => { 162 | acc.push(Ota.freeze(ota)); 163 | return acc; 164 | }, []); 165 | 166 | return frozenOtas; 167 | }; 168 | 169 | FirmwareStore.revive = (env, json) => { 170 | if (!Array.isArray(json)) { 171 | throw new Error(`${this.env} is not saved as array`); 172 | } 173 | 174 | let otas = []; 175 | json.map((o) => { 176 | otas.push(Ota.thaw(o)); 177 | }); 178 | 179 | return new FirmwareStore(env, otas); 180 | }; 181 | 182 | const getStoreFor = async (env, json) => { 183 | try { 184 | let store = FirmwareStore.revive(env, json); 185 | await store.setup(); 186 | return store; 187 | } catch (err) { 188 | console.log(err); 189 | } 190 | }; 191 | 192 | module.exports = { 193 | getStoreFor, 194 | freezeStore: (store) => FirmwareStore.freeze(store), 195 | healthStatus, 196 | }; 197 | -------------------------------------------------------------------------------- /tools/inventory/index.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | const fs = require("../../utils/fsPromise"); 6 | 7 | const { filePath, prompt } = require("../common.js"); 8 | const { Ota } = require("./ota"); 9 | const { getStoreFor, freezeStore, healthStatus } = require("./firmwareStore"); 10 | 11 | const DEFAULT_INV = JSON.stringify({}); 12 | 13 | class Inventory { 14 | constructor() { 15 | this.stores = {}; 16 | } 17 | 18 | async setup() { 19 | try { 20 | await fs.readFile(filePath.INVENTORY_FILE); 21 | } catch (err) { 22 | if (err.code == "ENOENT") { 23 | let dataFolderExists = await fs.exists(filePath.DATA_FOLDER); 24 | if (!dataFolderExists) { 25 | await fs.mkdir(filePath.DATA_FOLDER); 26 | } 27 | 28 | await fs.writeFile(filePath.INVENTORY_FILE, DEFAULT_INV); 29 | } 30 | } 31 | } 32 | 33 | async addOta(url, options) { 34 | try { 35 | let env = options.environment; 36 | let name = options.otaName; 37 | let ota = new Ota(url, name); 38 | 39 | let store = this.stores[env]; 40 | 41 | // The store for the environment doesn't exist 42 | if (store == undefined) { 43 | store = await getStoreFor(env, []); 44 | this.stores[env] = store; 45 | } 46 | 47 | await store.downloadAndSaveConfig(ota, { force: options.force }); 48 | } catch (err) { 49 | console.log(err.message); 50 | } 51 | } 52 | 53 | async syncOta(name, env) { 54 | try { 55 | let store = this.stores[env]; 56 | await store.sync(name); 57 | this.stores[env] = store; 58 | } catch (err) { 59 | console.log(err.message); 60 | } 61 | } 62 | 63 | async syncAll() { 64 | try { 65 | for (const key in this.stores) { 66 | const store = this.stores[key]; 67 | await store.syncAll(); 68 | } 69 | } catch (err) { 70 | console.log(err.message); 71 | } 72 | } 73 | 74 | async approveOta(name, env) { 75 | try { 76 | let store = this.stores[env]; 77 | if (store == undefined) { 78 | console.log(prompt.STORE_NOT_EXISTS); 79 | return; 80 | } 81 | 82 | await store.addChecksum(name); 83 | 84 | this.stores[env] = store; 85 | } catch (err) { 86 | console.log(err.message); 87 | } 88 | } 89 | 90 | async getInv() { 91 | let rawData = await fs.readFile(filePath.INVENTORY_FILE); 92 | let json = JSON.parse(rawData); 93 | return json; 94 | } 95 | 96 | async saveInv(json) { 97 | await fs.writeFile(filePath.INVENTORY_FILE, JSON.stringify(json)); 98 | } 99 | 100 | async cryo() { 101 | let json = Object.keys(this.stores).reduce((acc, key) => { 102 | acc[key] = freezeStore(this.stores[key]); 103 | return acc; 104 | }, {}); 105 | 106 | await this.saveInv(json); 107 | } 108 | 109 | async revive() { 110 | try { 111 | let json = await this.getInv(); 112 | for (var env in json) { 113 | this.stores[env] = await getStoreFor(env, json[env]); 114 | } 115 | } catch (err) { 116 | console.log(err.message); 117 | } 118 | } 119 | } 120 | 121 | module.exports = { 122 | getInventory: async () => { 123 | try { 124 | let vectorInventory = new Inventory(); 125 | await vectorInventory.setup(); 126 | await vectorInventory.revive(); 127 | 128 | return vectorInventory; 129 | } catch (err) { 130 | console.log(err.message); 131 | } 132 | }, 133 | }; 134 | -------------------------------------------------------------------------------- /tools/inventory/ota.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | function parseURL(url) { 6 | let obj = new URL(url); 7 | obj.filename = url.substring(url.lastIndexOf("/") + 1); 8 | return obj; 9 | } 10 | 11 | function parseName(name) { 12 | return name.substring(name.lastIndexOf(".") + 1) == "ota" 13 | ? name 14 | : name + ".ota"; 15 | } 16 | 17 | class Ota { 18 | constructor(url, name, checksum) { 19 | this.url = url; 20 | this.name = name != undefined ? parseName(name) : parseURL(url).filename; 21 | this.checksum = checksum; 22 | } 23 | 24 | toString() { 25 | return JSON.stringify(this); 26 | } 27 | 28 | compare(obj) { 29 | return obj.name === this.name; 30 | } 31 | } 32 | 33 | // Static method to convert to a basic structure with a class identifier 34 | Ota.freeze = (ota) => ({ 35 | url: ota.url, 36 | name: ota.name, 37 | checksum: ota.checksum, 38 | }); 39 | 40 | // Static method to reconstitute a Ota from the basic structure 41 | Ota.thaw = (json) => new Ota(json.url, json.name, json.checksum); 42 | 43 | module.exports = { Ota }; 44 | -------------------------------------------------------------------------------- /tools/ota.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | const commander = require("commander"); 6 | const fs = require("fs"); 7 | 8 | const { getInventory } = require("./inventory"); 9 | const { DEFUALT_ENV } = require("./common"); 10 | 11 | const { filePath } = require("./common.js"); 12 | 13 | const otaAdd = new commander.Command("ota-add"); 14 | otaAdd 15 | .arguments("") 16 | .description("Add a cloud ota (url) as part of your local inventory.") 17 | .option("-e, --environment ", "set environment", DEFUALT_ENV) 18 | .option("-f, --force", "Override the existing ota") 19 | .option("-n, --ota-name ", "Save the ota with a specific name") 20 | .action(async (url, options) => { 21 | if (!ensureInventoryFileExists()) { 22 | return; 23 | } 24 | 25 | let inventory = await getInventory(); 26 | await inventory.addOta(url, options); 27 | await inventory.cryo(); 28 | }); 29 | 30 | const otaSync = new commander.Command("ota-sync"); 31 | otaSync 32 | .arguments("[ota_file]") 33 | .description( 34 | "Check for new version of an ota based on checksum. For ota-sync dev/test.ota" 35 | ) 36 | .action(async (otaFile, options) => { 37 | if (!ensureInventoryFileExists()) { 38 | return; 39 | } 40 | 41 | let inventory = await getInventory(); 42 | 43 | if (otaFile !== undefined) { 44 | const { name, env } = parseFile(otaFile); 45 | await inventory.syncOta(name, env); 46 | } else { 47 | await inventory.syncAll(); 48 | } 49 | }); 50 | 51 | const parseFile = (otaFile) => { 52 | otaFile = otaFile.endsWith(".ota") ? otaFile : otaFile + ".ota"; 53 | let name = otaFile; 54 | let env = DEFUALT_ENV; 55 | 56 | if (otaFile.includes("/")) { 57 | name = otaFile.substring(otaFile.lastIndexOf("/") + 1); 58 | env = otaFile.substring(0, otaFile.lastIndexOf("/")); 59 | } 60 | 61 | return { name, env }; 62 | }; 63 | 64 | const otaApprove = new commander.Command("ota-approve"); 65 | otaApprove 66 | .arguments("") 67 | .description("Add a checksum to ota. For ota-approve prod/test.ota") 68 | .action(async (otaFile, options) => { 69 | if (!ensureInventoryFileExists()) { 70 | return; 71 | } 72 | 73 | const { name, env } = parseFile(otaFile); 74 | 75 | let inventory = await getInventory(); 76 | await inventory.approveOta(name, env); 77 | await inventory.cryo(); 78 | }); 79 | 80 | const ensureInventoryFileExists = () => { 81 | if (!fs.existsSync(filePath.INVENTORY_FILE)) { 82 | console.log("Seems like you have missed this step 'configure'!"); 83 | console.log("E.g. 'vector-setup configure'"); 84 | return false; 85 | } 86 | return true; 87 | }; 88 | 89 | module.exports = { 90 | otaAdd, 91 | otaSync, 92 | otaApprove, 93 | }; 94 | -------------------------------------------------------------------------------- /tools/run.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | const express = require("express"); 6 | const path = require("path"); 7 | const bodyParser = require("body-parser"); 8 | const fs = require("fs"); 9 | const cors = require("cors"); 10 | 11 | const fsPromise = require("../utils/fsPromise"); 12 | const { getNetworkIp } = require("../utils/ip"); 13 | 14 | const { filePath } = require("./common.js"); 15 | 16 | const app = express(); 17 | 18 | let port = undefined; 19 | let networkIp = getNetworkIp(); 20 | let serverIp = undefined; 21 | 22 | app.set("view engine", "ejs"); 23 | app.use("/static", express.static(path.join(__dirname, "../site"))); 24 | app.use(bodyParser.json()); 25 | app.use(bodyParser.urlencoded({ extended: false })); 26 | app.use(cors()); 27 | 28 | app.get("/", (req, res) => { 29 | res.render(path.join(__dirname, "../templates/main.ejs"), { 30 | networkIp: networkIp, 31 | serverIp: serverIp, 32 | port: port, 33 | }); 34 | }); 35 | 36 | app.post("/firmware", async (req, res) => { 37 | try { 38 | const env = req.body.env; 39 | const loc = filePath.FIRMWARE_FOLDER + "/" + env; 40 | if (!(await fsPromise.exists(loc))) { 41 | return res.json({ message: "Store doesn't exists" }); 42 | } else { 43 | const result = await fsPromise.readdir(loc); 44 | return res.json({ message: result }); 45 | } 46 | } catch (err) { 47 | console.log(err); 48 | } 49 | }); 50 | 51 | const startServer = (portReq, ipReq) => { 52 | port = portReq === undefined ? 8000 : portReq; 53 | serverIp = ipReq === undefined ? "0.0.0.0" : ipReq; 54 | 55 | app 56 | .listen(port, serverIp, () => { 57 | console.log(`Server running on ip ${serverIp} and port ${port}`); 58 | 59 | let ipToShow = serverIp; 60 | if ("0.0.0.0" == serverIp || serverIp.startsWith("127")) { 61 | ipToShow = "localhost"; 62 | } else { 63 | console.log( 64 | `WARN: Server is running on an ip that might not be accessible to bot.` 65 | ); 66 | } 67 | 68 | console.log(`Server running. Go to http://${ipToShow}:${port} to use it`); 69 | }) 70 | .on("error", (err) => { 71 | switch (err.code) { 72 | case "EADDRNOTAVAIL": 73 | console.log(`Unable to bind to IP ${serverIp} on this device`); 74 | return; 75 | case "EACCES": 76 | console.log( 77 | `Permission denied to bind to IP ${serverIp} on PORT ${port} on this device.` 78 | ); 79 | return; 80 | case "EADDRINUSE": 81 | console.log( 82 | `Permission denied to bind to IP ${serverIp} on PORT ${port} on this device. The address is already in use.` 83 | ); 84 | return; 85 | default: 86 | console.log(err); 87 | return; 88 | } 89 | }); 90 | }; 91 | 92 | module.exports = (portReq, ipReq) => { 93 | try { 94 | if ( 95 | fs.existsSync(filePath.SETTINGS_FILE) && 96 | fs.existsSync(filePath.INVENTORY_FILE) 97 | ) { 98 | startServer(portReq, ipReq); 99 | } else { 100 | console.log("Seems like you have missed this step 'configure'!"); 101 | console.log("E.g. 'vector-web-setup configure'"); 102 | } 103 | } catch (err) { 104 | console.log(err); 105 | } 106 | }; 107 | -------------------------------------------------------------------------------- /utils/clicmd.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | const { constants } = require("os"); 6 | const spawn = require("cross-spawn"); 7 | const concat = require("concat-stream"); 8 | 9 | const fs = require("./fsPromise"); 10 | 11 | const PATH = process.env.PATH; 12 | 13 | const createProcess = async (processPath, args = [], env = null) => { 14 | // Ensure that path exists 15 | if (!processPath || !(await fs.exists(processPath))) { 16 | throw new Error("Invalid process path"); 17 | } 18 | 19 | args = [processPath].concat(args); 20 | 21 | return spawn("node", args, { 22 | env: Object.assign( 23 | { 24 | NODE_ENV: "test", 25 | preventAutoStart: false, 26 | PATH, // This is needed in order to get all the binaries in your current terminal 27 | }, 28 | env 29 | ), 30 | stdio: [null, null, null, "ipc"], // This enables interprocess communication (IPC) 31 | }); 32 | }; 33 | 34 | const executeWithInput = async ( 35 | processPath, 36 | args = [], 37 | inputs = [], 38 | opts = {} 39 | ) => { 40 | if (!Array.isArray(inputs)) { 41 | opts = inputs; 42 | inputs = []; 43 | } 44 | 45 | const { env = null, timeout = 100, maxTimeout = 10000 } = opts; 46 | const childProcess = await createProcess(processPath, args, env); 47 | childProcess.stdin.setEncoding("utf-8"); 48 | 49 | let currentInputTimeout, killIOTimeout; 50 | 51 | const loop = (inputs) => { 52 | if (killIOTimeout) { 53 | clearTimeout(killIOTimeout); 54 | } 55 | 56 | if (!inputs.length) { 57 | childProcess.stdin.end(); 58 | // Set a timeout to wait for CLI response. If CLI takes longer than 59 | // maxTimeout to respond, kill the childProcess and notify user 60 | killIOTimeout = setTimeout(() => { 61 | console.error("Error: Reached I/O timeout"); 62 | childProcess.kill(constants.signals.SIGTERM); 63 | }, maxTimeout); 64 | 65 | return; 66 | } 67 | 68 | currentInputTimeout = setTimeout(() => { 69 | childProcess.stdin.write(inputs[0]); 70 | // Log debug I/O statements on tests 71 | if (env && env.DEBUG) { 72 | console.log("input:", inputs[0]); 73 | } 74 | loop(inputs.slice(1)); 75 | }, timeout); 76 | }; 77 | 78 | const promise = new Promise((resolve, reject) => { 79 | // Get errors from CLI 80 | childProcess.stderr.on("data", (data) => { 81 | // Log debug I/O statements on tests 82 | if (env && env.DEBUG) { 83 | console.log("error:", data.toString()); 84 | } 85 | }); 86 | 87 | // Get output from CLI 88 | childProcess.stdout.on("data", (data) => { 89 | // Log debug I/O statements on tests 90 | if (env && env.DEBUG) { 91 | console.log("output:", data.toString()); 92 | } 93 | }); 94 | 95 | childProcess.stderr.once("data", (err) => { 96 | childProcess.stdin.end(); 97 | 98 | if (currentInputTimeout) { 99 | clearTimeout(currentInputTimeout); 100 | inputs = []; 101 | } 102 | reject(err.toString()); 103 | }); 104 | 105 | childProcess.on("error", reject); 106 | 107 | // Kick off the process 108 | loop(inputs); 109 | 110 | childProcess.stdout.pipe( 111 | concat((result) => { 112 | if (killIOTimeout) { 113 | clearTimeout(killIOTimeout); 114 | } 115 | 116 | resolve(result.toString()); 117 | }) 118 | ); 119 | }); 120 | 121 | // Appending the process to the promise, in order to 122 | // add additional parameters or behavior (such as IPC communication) 123 | promise.attachedProcess = childProcess; 124 | 125 | return promise; 126 | }; 127 | 128 | module.exports = { 129 | createProcess, 130 | 131 | create: (processPath) => { 132 | const fn = (...args) => executeWithInput(processPath, ...args); 133 | 134 | return { 135 | execute: fn, 136 | }; 137 | }, 138 | }; 139 | -------------------------------------------------------------------------------- /utils/fsPromise.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | const fs = require("fs"); 4 | const { promisify } = require("util"); 5 | 6 | module.exports = Object.keys(fs).reduce((acc, fn) => { 7 | if (fn.match(/(stream|sync)/gi)) { 8 | acc[fn] = fs[fn]; 9 | } else { 10 | try { 11 | acc[fn] = promisify(fs[fn]); 12 | } catch (err) { 13 | acc[fn] = fs[fn]; 14 | } 15 | } 16 | return acc; 17 | }, {}); 18 | -------------------------------------------------------------------------------- /utils/hash.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | "use strict"; 4 | 5 | const crypto = require("crypto"); 6 | const fsPromise = require("./fsPromise"); 7 | 8 | const generateChecksum = async (path) => { 9 | let data = await fsPromise.readFile(path); 10 | 11 | return crypto.createHash("sha256").update(data).digest("hex"); 12 | }; 13 | 14 | module.exports = { generateChecksum }; 15 | -------------------------------------------------------------------------------- /utils/ip.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | const ifaces = require("os").networkInterfaces(); 4 | 5 | let address; 6 | 7 | Object.keys(ifaces).forEach((dev) => { 8 | ifaces[dev].filter((details) => { 9 | if (details.family === "IPv4" && details.internal === false) { 10 | address = details.address; 11 | } 12 | }); 13 | }); 14 | 15 | const getNetworkIp = () => { 16 | return address; 17 | }; 18 | 19 | module.exports = { 20 | getNetworkIp, 21 | }; 22 | -------------------------------------------------------------------------------- /utils/rmdir.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 2 | 3 | const fs = require("fs"); 4 | 5 | module.exports = function (path) { 6 | var files = []; 7 | if (fs.existsSync(path)) { 8 | files = fs.readdirSync(path); 9 | files.forEach(function (file, index) { 10 | var curPath = path + "/" + file; 11 | if (fs.lstatSync(curPath).isDirectory()) { 12 | // recurse 13 | deleteFolderRecursive(curPath); 14 | } else { 15 | // delete file 16 | fs.unlinkSync(curPath); 17 | } 18 | }); 19 | fs.rmdirSync(path); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /vector-web-setup.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* Copyright (c) 2019-2020 Digital Dream Labs. See LICENSE file for details. */ 4 | 5 | var commander = require("commander"); 6 | 7 | const program = new commander.Command(); 8 | 9 | program 10 | .version("1.0.0", "-v, --version") 11 | .description( 12 | " __ ________ _____ _______ ____ _____ \n" + 13 | " \\ \\ / / ____/ ____|__ __/ __ \\| __ \\ \n" + 14 | " \\ \\ / /| |__ | | | | | | | | |__) |\n" + 15 | " \\ \\/ / | __|| | | | | | | | _ / \n" + 16 | " \\ / | |___| |____ | | | |__| | | \\ \\ \n" + 17 | " \\/ |______\\_____| |_| \\____/|_| \\_\\ \n\n" 18 | ) 19 | .addCommand(require("./tools/configure.js")) 20 | .addCommand(require("./tools/ota.js").otaAdd) 21 | .addCommand(require("./tools/ota.js").otaSync) 22 | .addCommand(require("./tools/ota.js").otaApprove) 23 | .exitOverride(() => { 24 | process.exit(0); 25 | }); 26 | 27 | program 28 | .command("serve") 29 | .description("Serve the vector websetup") 30 | .option("-p, --port ", "port to serve the setup on", 8000) 31 | .option( 32 | "-ip, --ip-address ", 33 | "address to serve the setup on", 34 | "0.0.0.0" 35 | ) 36 | .action((options) => { 37 | try { 38 | require("./tools/run.js")(options.port, options.ipAddress); 39 | } catch (err) { 40 | console.log(err); 41 | } 42 | }); 43 | 44 | program.parse(process.argv); 45 | --------------------------------------------------------------------------------