├── .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 | 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 | 16 | -------------------------------------------------------------------------------- /site/images/common_statusicon_cloudready_sml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /site/images/common_statusicon_wificonnect_med.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 20 | -------------------------------------------------------------------------------- /site/images/icon_accountManage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | -------------------------------------------------------------------------------- /site/images/icon_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /site/images/nav_icon_backpacklights_sml.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51 | -------------------------------------------------------------------------------- /site/images/onboarding_icon_plugincharger_med.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 33 | -------------------------------------------------------------------------------- /site/images/onboarding_icon_pressbackpack_med.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 60 | -------------------------------------------------------------------------------- /site/images/settings_icon_lock_mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /site/images/settings_icon_wifilife_1bars_mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /site/images/settings_icon_wifilife_2bars_mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /site/images/settings_icon_wifilife_3bars_mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /site/images/statlog_icon_checkmark_success_mini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | "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 |Choose the stack you want to use
97 |114 | There was an error loading the settings.json file. To view the 115 | file click here 116 |
117 |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 |Vector setup is complete!
461 |464 | Update completed. After Vector reboots, you will need to discover 465 | him again. 466 |
467 |