├── .gitignore ├── src ├── img │ ├── d3vl.png │ ├── logo.png │ ├── logo_dark.png │ ├── buyusacoffee.png │ ├── google_play.png │ └── models │ │ ├── avata.png │ │ └── dji-fpv.png ├── main.js ├── vue │ ├── commands │ │ ├── CeLockGen1.vue │ │ └── FccUnlockGen1.vue │ ├── components │ │ ├── Logo.vue │ │ ├── ConnectionSteps │ │ │ ├── GogglesUsb.vue │ │ │ ├── AvataPower.vue │ │ │ ├── DjiFpvPower.vue │ │ │ ├── GogglesPower.vue │ │ │ └── InstallBulkDriver.vue │ │ ├── Alert.vue │ │ ├── ModelBox.vue │ │ └── Footer.vue │ ├── App.vue │ ├── ConnectToSerial.vue │ ├── Start.vue │ ├── ModelPicker.vue │ ├── ConnectToBulk.vue │ └── ConnectionFlow.vue ├── index.html ├── js │ ├── config │ │ ├── commands.js │ │ └── models.js │ ├── class │ │ ├── usb │ │ │ ├── AndroidAccessory.js │ │ │ ├── BaseUsb.js │ │ │ ├── Serial.js │ │ │ └── Bulk.js │ │ └── duml │ │ │ ├── Session.js │ │ │ └── Packer.js │ └── index.js └── scss │ └── main.scss ├── webpack.config.js ├── README.md ├── package.json ├── LICENCE └── gulpfile.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | dist -------------------------------------------------------------------------------- /src/img/d3vl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D3VL/B3YOND-WEB-APP/HEAD/src/img/d3vl.png -------------------------------------------------------------------------------- /src/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D3VL/B3YOND-WEB-APP/HEAD/src/img/logo.png -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // proxy to index.js in src/js 2 | module.exports = require('./js/index.js'); -------------------------------------------------------------------------------- /src/img/logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D3VL/B3YOND-WEB-APP/HEAD/src/img/logo_dark.png -------------------------------------------------------------------------------- /src/vue/commands/CeLockGen1.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/buyusacoffee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D3VL/B3YOND-WEB-APP/HEAD/src/img/buyusacoffee.png -------------------------------------------------------------------------------- /src/img/google_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D3VL/B3YOND-WEB-APP/HEAD/src/img/google_play.png -------------------------------------------------------------------------------- /src/img/models/avata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D3VL/B3YOND-WEB-APP/HEAD/src/img/models/avata.png -------------------------------------------------------------------------------- /src/vue/commands/FccUnlockGen1.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/models/dji-fpv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D3VL/B3YOND-WEB-APP/HEAD/src/img/models/dji-fpv.png -------------------------------------------------------------------------------- /src/vue/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/vue/components/ConnectionSteps/GogglesUsb.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/vue/components/ConnectionSteps/AvataPower.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/vue/components/ConnectionSteps/DjiFpvPower.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/vue/components/ConnectionSteps/GogglesPower.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/vue/components/ConnectionSteps/InstallBulkDriver.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/vue/App.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/vue/ConnectToSerial.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/vue/Start.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/vue/components/Alert.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | B3YOND APP 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/vue/components/ModelBox.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/vue/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /src/js/config/commands.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | wm170: [ 3 | { 4 | name: "FCC Unlock", 5 | description: "Unlocks full transmission power", 6 | image: "https://i.imgur.com/8Z7ZQ9M.png", 7 | commandPage: "FccUnlockGen1", 8 | }, 9 | { 10 | name: "CE Lock", 11 | description: "Locks transmission power to 25mW", 12 | image: "https://i.imgur.com/8Z7ZQ9M.png", 13 | commandPage: "CeLockGen1", 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import '../../node_modules/bootstrap/scss/bootstrap.scss'; 2 | * { 3 | box-sizing: border-box !important; 4 | } 5 | 6 | .container { 7 | padding-top: 10px; 8 | } 9 | 10 | .logo { 11 | max-width: 330px; 12 | margin: 15px 0; 13 | } 14 | 15 | .d3vl-link { 16 | color: #ff3e3e; 17 | text-decoration: none; 18 | } 19 | 20 | .ModelBox { 21 | margin: 11px; 22 | 23 | .card { 24 | 25 | &:hover { 26 | background-color: rgb(237, 237, 237); 27 | } 28 | 29 | img { 30 | width: 180px; 31 | height: 180px; 32 | } 33 | } 34 | 35 | width: 180px; 36 | 37 | text-decoration: none; 38 | color: black; 39 | text-align: center; 40 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | const { VueLoaderPlugin } = require('vue-loader') 3 | 4 | module.exports = { 5 | entry: './src/main.js', 6 | output: { 7 | path: path.resolve(__dirname, 'dist/assets/js'), 8 | filename: 'app.js', 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.vue$/, 14 | loader: 'vue-loader' 15 | }, 16 | { 17 | test: /\.js$/, 18 | exclude: /node_modules/, 19 | loader: 'babel-loader', 20 | options: { 21 | presets: [ 22 | ['@babel/preset-env', { targets: "defaults" }] 23 | ] 24 | } 25 | } 26 | ] 27 | }, 28 | plugins: [ 29 | new VueLoaderPlugin() 30 | ] 31 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | B3YOND LOGO 2 | 3 | # The DJI Multitool 4 | 5 | This is a Work In Progress, the goal is to create an easily extendible web app for general DUML commands, including FCC unlocking, DroneID Disabling, parameter modification and more! 6 | 7 | ## Installation 8 | ```bash 9 | # Install dependencies 10 | npm install 11 | 12 | # Build the app 13 | npx gulp 14 | 15 | # Run the dev server 16 | npx gulp dev 17 | ``` 18 | 19 | ## Contributing 20 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 21 | At present this is a very early skeleton! 22 | 23 | ## License 24 | [MIT](https://choosealicense.com/licenses/mit/) with [Commons Clause](https://commonsclause.com/) 25 | 26 | 27 | © D3VL LTD 2022 -------------------------------------------------------------------------------- /src/vue/ModelPicker.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "b3yond-tool", 3 | "version": "1.0.0", 4 | "description": "b3yond tool", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "npx gulp dev", 8 | "build": "npx gulp" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bootstrap": "^5.2.0", 14 | "vue": "^3.2.38", 15 | "vue-resource": "^1.5.3", 16 | "vue-router": "^4.1.5" 17 | }, 18 | "devDependencies": { 19 | "@babel/preset-env": "^7.18.10", 20 | "babel-loader": "^8.2.5", 21 | "browser-sync": "^2.27.10", 22 | "del": "^3.0.0", 23 | "gulp": "^4.0.2", 24 | "gulp-autoprefixer": "^8.0.0", 25 | "gulp-clean-css": "3.9.4", 26 | "gulp-concat": "^2.6.1", 27 | "gulp-ejs": "^5.1.0", 28 | "gulp-rename": "^1.4.0", 29 | "gulp-sass": "^5.1.0", 30 | "gulp-sourcemaps": "^2.6.5", 31 | "sass": "^1.49.0", 32 | "vue-loader": "^17.0.0", 33 | "webpack-cli": "^4.10.0", 34 | "webpack-stream": "^7.0.0" 35 | } 36 | } -------------------------------------------------------------------------------- /src/js/class/usb/AndroidAccessory.js: -------------------------------------------------------------------------------- 1 | const UsbBase = require("./BaseUsb"); 2 | 3 | // this is used to communicate with the android app wrapper, as it uses AOA. 4 | 5 | const injectedFunctions = ["accessoryBridgeConnect", "accessoryBridgeRequestDevice", "accessoryBridgeWrite", "accessoryBridgeRead"] 6 | 7 | class AndroidAccessory extends UsbBase { 8 | constructor(filters = []) { 9 | super(filters); 10 | if (navigator.userAgentData.platform !== "Android") throw new Error("Not supported"); 11 | // check that the injected functions are available 12 | for (const func of injectedFunctions) { 13 | if (typeof window[func] !== "function") throw new Error(`Function ${func} not available`); 14 | } 15 | } 16 | 17 | async requestDevice() { 18 | const device = await accessoryBridgeRequestDevice(); 19 | return device; 20 | } 21 | 22 | async connect(device) { 23 | 24 | } 25 | } 26 | 27 | module.exports = AndroidAccessory; -------------------------------------------------------------------------------- /src/js/class/usb/BaseUsb.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class UsbBase { 4 | 5 | constructor(filters = []) { 6 | this.listeners = []; 7 | this.filters = filters; 8 | } 9 | 10 | isAvailable() { 11 | return false; 12 | } 13 | 14 | async requestDevice() { 15 | return []; 16 | } 17 | 18 | async grantedDevices() { 19 | return []; 20 | } 21 | 22 | async connect() { 23 | throw new Error("Not supported"); 24 | } 25 | 26 | async write(data) { 27 | // if (this.session) { 28 | // this.session.write(transmit); 29 | // } 30 | throw new Error("Not supported"); 31 | } 32 | 33 | async linkSession(session) { 34 | console.log("Linking session"); 35 | this.session = session; 36 | this.on('data', (data) => { 37 | try { 38 | this.session.receive(data); 39 | } catch (error) { 40 | console.error(error); 41 | } 42 | }); 43 | } 44 | 45 | // super simple event emitter 46 | on(event, callback) { 47 | if (!event || !callback) throw new Error("Invalid arguments"); 48 | this.listeners.push({ event, callback, once: false, id: this.listeners.length }); 49 | } 50 | 51 | _emit(event, data = {}) { 52 | if (!event || !this.listeners) return; 53 | 54 | for (const listener of this.listeners) { 55 | try { 56 | if (listener.event === event) listener.callback(data); 57 | } catch (error) { 58 | console.error(error); 59 | } 60 | } 61 | 62 | return Promise.resolve(); 63 | } 64 | } 65 | 66 | module.exports = UsbBase; -------------------------------------------------------------------------------- /src/vue/ConnectToBulk.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 D3VL LTD 4 | 5 | “Commons Clause” License Condition v1.0 6 | 7 | The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition. 8 | 9 | Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software. 10 | 11 | For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice. 12 | 13 | Software: B3YOND WEB APP 14 | 15 | License: MIT with “Commons Clause” License Condition v1.0 16 | 17 | Licensor: D3VL LTD 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in all 27 | copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createRouter, createWebHistory } from 'vue-router' 3 | 4 | import App from '../vue/App.vue' 5 | 6 | 7 | // Components 8 | import Logo from '../vue/components/Logo.vue' 9 | import Footer from '../vue/components/Footer.vue' 10 | import Alert from '../vue/components/Alert.vue' 11 | 12 | const duml = { 13 | Session: require('./class/duml/Session.js'), 14 | Packer: require('./class/duml/Packer.js'), 15 | } 16 | 17 | // Set up router 18 | const router = createRouter({ 19 | history: createWebHistory(), 20 | 21 | routes: [ 22 | { 23 | path: '/start', 24 | name: 'start', 25 | component: require('../vue/Start.vue').default 26 | }, 27 | { 28 | path: '/models', 29 | name: 'model_list', 30 | component: require('../vue/ModelPicker.vue').default 31 | }, 32 | { 33 | path: '/model/:slug', 34 | name: 'model_connect', 35 | component: require('../vue/ConnectionFlow.vue').default 36 | }, 37 | { 38 | path: '/connect/bulk/:slug', 39 | name: 'model_connect_bulk', 40 | component: require('../vue/ConnectToBulk.vue').default 41 | }, 42 | { 43 | path: '/connect/serial/:slug', 44 | name: 'model_connect_serial', 45 | component: require('../vue/ConnectToSerial.vue').default 46 | }, 47 | { path: '/:pathMatch(.*)*', redirect: '/start' } 48 | ] 49 | }) 50 | 51 | const app = createApp(App) 52 | 53 | // @TODO: make this match the GitHub build 54 | app.config.globalProperties.$build = '0.0.0' 55 | 56 | // global class instances 57 | const dumlSession = new duml.Session(); 58 | app.config.globalProperties.session = dumlSession; 59 | // app.config.globalProperties.session.setUnmatchedListener(console.warn); 60 | 61 | // left here for debugging 62 | window.session = dumlSession; 63 | window.duml = duml; 64 | 65 | // add global components 66 | app.component('Logo', Logo) 67 | app.component('Footer', Footer) 68 | app.component('Alert', Alert) 69 | 70 | // add router 71 | app.use(router) 72 | 73 | // Start up app 74 | app.mount('#app') 75 | 76 | -------------------------------------------------------------------------------- /src/js/class/usb/Serial.js: -------------------------------------------------------------------------------- 1 | const UsbBase = require("./BaseUsb"); 2 | 3 | 4 | class Serial extends UsbBase { 5 | constructor(filters = [ 6 | { usbVendorId: 0x2CA3 }, 7 | ]) { 8 | super(filters); 9 | 10 | if (!navigator.serial) throw new Error("WebSerial not supported"); 11 | 12 | this.port = null; 13 | 14 | this.readStream = null; 15 | this.writeStream = null; 16 | } 17 | 18 | isAvailable() { 19 | return this.port !== null; 20 | } 21 | 22 | async requestDevice() { 23 | const port = await navigator.serial.requestPort({ filters: this.filters }); 24 | return port; 25 | } 26 | 27 | async grantedDevices() { 28 | const ports = await navigator.serial.getPorts({ filters: this.filters }); 29 | return ports; 30 | } 31 | 32 | async connect(port) { 33 | try { 34 | 35 | await port.open({ baudRate: 115200 }); 36 | this._emit("connected", { port }); 37 | this.port = port; 38 | 39 | if (port.readable && port.writable) { 40 | this.port = port 41 | this.readStream = this.port.readable.getReader(); 42 | this.writeStream = this.port.writable.getWriter(); 43 | } else { 44 | throw new Error("Could not get reader/writer"); 45 | } 46 | 47 | this.readLoop(); 48 | 49 | } catch (error) { 50 | throw error; 51 | } 52 | 53 | } 54 | 55 | async write(data) { 56 | if (!this.port) throw new Error("Not connected"); 57 | if (!this.writeStream) throw new Error("Write stream not available"); 58 | 59 | this.writeStream.write(data); 60 | 61 | return Promise.resolve(); 62 | } 63 | 64 | async readLoop() { 65 | try { 66 | while (true) { 67 | const { data, done } = await this.readStream.read(); 68 | 69 | if (done) { 70 | this.readStream.releaseLock(); 71 | break; 72 | } 73 | 74 | if (data) this._emit("data", data); 75 | } 76 | } catch (error) { 77 | this._emit("error", error); 78 | this.readLoop(); 79 | } finally { 80 | this.readStream.releaseLock(); 81 | } 82 | } 83 | 84 | 85 | } 86 | 87 | module.exports = Serial; -------------------------------------------------------------------------------- /src/vue/ConnectionFlow.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const gulp = require('gulp'), 4 | sass = require('gulp-sass')(require('sass')), 5 | del = require('del'), 6 | cleanCSS = require('gulp-clean-css'), 7 | rename = require("gulp-rename"), 8 | webpack = require('webpack-stream'), 9 | autoprefixer = require('gulp-autoprefixer'), 10 | browserSync = require('browser-sync').create(); 11 | 12 | // delete the dist folder 13 | gulp.task('clean', function () { 14 | return del(['dist']); 15 | }); 16 | 17 | // Compile SCSS(SASS) files to CSS and copy to dist/css 18 | gulp.task('scss', function () { 19 | return gulp.src(['./src/scss/*.scss']) 20 | .pipe(sass.sync({ 21 | outputStyle: 'expanded' 22 | }).on('error', sass.logError)) 23 | .pipe(autoprefixer()) 24 | .pipe(cleanCSS()) 25 | .pipe(rename({ 26 | suffix: '.min' 27 | })) 28 | .pipe(gulp.dest('./dist/assets/css')) 29 | .pipe(browserSync.stream()); 30 | }); 31 | 32 | // Build Js 33 | gulp.task('webpack', function () { 34 | return gulp.src('src/js/index.js') 35 | .pipe(webpack(require('./webpack.config.js'))) 36 | .pipe(gulp.dest('./dist/assets/js')) 37 | .pipe(browserSync.stream()); 38 | }); 39 | 40 | // Copy Images 41 | gulp.task('images', function () { 42 | return gulp.src("src/*.html").pipe(gulp.dest('./dist/')); 43 | }); 44 | 45 | // Copy HTML 46 | gulp.task('html', function () { 47 | return gulp.src("src/img/**").pipe(gulp.dest('./dist/assets/img')); 48 | }); 49 | 50 | 51 | // Configure the browserSync task and watch file path for change 52 | gulp.task('dev', function browserDev(done) { 53 | 54 | browserSync.init({ 55 | server: { 56 | baseDir: "./dist" 57 | } 58 | }, (err, bs) => { 59 | bs.addMiddleware("*", (req, res) => { 60 | res.writeHead(302, { 61 | location: "/" 62 | }); 63 | res.end("Redirecting!"); 64 | }); 65 | }); 66 | 67 | gulp.watch(['src/scss/*.scss', 'src/scss/**/*.scss', '!src/scss/bootstrap/**'], gulp.series('scss', function cssBrowserReload(done) { 68 | // browserSync.reload(); 69 | done(); //Async callback for completion. 70 | })); 71 | 72 | gulp.watch(['src/js/*.js', 'src/js/**/*.js'], gulp.series('webpack', function jsBrowserReload(done) { 73 | browserSync.reload(); 74 | done(); 75 | })); 76 | 77 | gulp.watch(['src/vue/*.vue', 'src/vue/**/*.vue'], gulp.series('webpack', function jsBrowserReload(done) { 78 | browserSync.reload(); 79 | done(); 80 | })); 81 | 82 | 83 | done(); 84 | }); 85 | 86 | // Build task 87 | gulp.task("build", gulp.parallel('scss', 'webpack', 'images', 'html')); 88 | 89 | // Default task 90 | gulp.task("default", gulp.series('clean', 'build')); -------------------------------------------------------------------------------- /src/js/class/usb/Bulk.js: -------------------------------------------------------------------------------- 1 | const UsbBase = require("./BaseUsb"); 2 | 3 | // this was using private members, but that's not supported with vue3 proxies 🙄 4 | // read -> https://github.com/tc39/proposal-class-fields/issues/106 5 | 6 | class Bulk extends UsbBase { 7 | constructor(filters = [ 8 | { usbVendorId: 0x2CA3 }, 9 | ]) { 10 | super(filters); 11 | 12 | if (!navigator.usb) throw new Error("WebUSB not supported"); 13 | 14 | this.device = null; 15 | 16 | this.readStream = null; 17 | this.writeStream = null; 18 | 19 | this.doTransfer = true; 20 | } 21 | 22 | // https://github.com/o-gs/dji-firmware-tools/blob/08ccc8d84b3776f53a27a9d220fcb943734a8284/comm_serialtalk.py#L143 23 | getDumlInterface(interfaces) { 24 | const foundInterface = interfaces.filter(iface => { 25 | return iface.alternate.endpoints.some(endpoint => { 26 | return (endpoint.endpointNumber === 5 && endpoint.direction === "in") 27 | }) 28 | }) 29 | return foundInterface[0] || null; 30 | } 31 | 32 | isAvailable() { 33 | return this.device !== null; 34 | } 35 | 36 | async requestDevice() { 37 | const device = await navigator.usb.requestDevice({ filters: this.filters }); 38 | return device; 39 | } 40 | 41 | async grantedDevices() { 42 | const devices = await navigator.usb.getDevices({ filters: this.filters }); 43 | return devices; 44 | } 45 | 46 | async connect(device) { 47 | try { 48 | await device.open(); 49 | await device.selectConfiguration(1); 50 | 51 | let iface = this.getDumlInterface(device.configurations[0].interfaces); 52 | if (!iface) { 53 | throw new Error("Could not find DUML interface"); 54 | } 55 | 56 | this.readStream = iface.alternate.endpoints.find(endpoint => endpoint.direction === 'in'); 57 | this.writeStream = iface.alternate.endpoints.find(endpoint => endpoint.direction === 'out'); 58 | 59 | // sometimes, we can't claim the interface, this is either because it's already claimed or it's not the WinUSB driver on windows! 60 | let hasClaimedInterface = false; 61 | setTimeout(() => { 62 | if (!hasClaimedInterface) throw new Error("Could not claim interface"); 63 | }, 1000); 64 | 65 | await device.claimInterface(iface.interfaceNumber); 66 | hasClaimedInterface = true; 67 | 68 | this._emit("connected", { device }); 69 | 70 | await device.selectAlternateInterface(iface.interfaceNumber, iface.alternate.alternateSetting); 71 | 72 | this.device = device; 73 | 74 | this.readLoop(); 75 | 76 | } catch (error) { 77 | throw error; 78 | } 79 | } 80 | 81 | async write(data) { 82 | if (!this.device) throw new Error("Not connected"); 83 | if (!this.writeStream) throw new Error("Write stream not available"); 84 | 85 | await this.device.transferOut(this.writeStream.endpointNumber, data); 86 | } 87 | 88 | async readLoop() { 89 | if (!this.doTransfer) return; 90 | try { 91 | const result = await this.device.transferIn(this.readStream.endpointNumber, this.readStream.packetSize); 92 | 93 | if (result.status === 'stall') { 94 | console.warn('Endpoint stalled. Clearing.'); 95 | await this.device.clearHalt('in', this.readStream.endpointNumber); 96 | } 97 | 98 | if (result.data.byteLength > 0) { 99 | // console.log("emit data", result.data); 100 | this._emit("data", new Uint8Array(result.data.buffer)); 101 | } 102 | 103 | // console.info("read", toHexString(new Uint8Array(result.data.buffer))); 104 | 105 | return this.readLoop(); 106 | } catch (error) { 107 | this._emit("error", error); 108 | } 109 | } 110 | } 111 | 112 | module.exports = Bulk; -------------------------------------------------------------------------------- /src/js/class/duml/Session.js: -------------------------------------------------------------------------------- 1 | const Packer = require('./Packer') 2 | 3 | const hexToUint8Array = (hex) => { 4 | if (!hex || hex.length === 0) return null; 5 | return new Uint8Array(hex.match(/.{1,2}/g).map((i) => parseInt(i, 16))); 6 | } 7 | 8 | const decodeBytes = (buffer, offset, length) => { 9 | let value = 0; 10 | for (let i = (offset + length) - 1; i >= offset; i--) { 11 | value = (value << 8) | (buffer[i] & 255); 12 | } 13 | return value; 14 | } 15 | 16 | class Session { 17 | constructor() { 18 | this.sentPackets = []; 19 | this.receiveBuffer = new Uint8Array(0); 20 | this.lastSeenSeq = 0; 21 | this.unmatchedListener = () => { }; 22 | } 23 | 24 | setUnmatchedListener(callback) { 25 | if (typeof callback === 'function') this.unmatchedListener = callback; 26 | } 27 | 28 | receive(data) { 29 | // if the data is not a Uint8Array, convert it 30 | if (!(data instanceof Uint8Array)) { 31 | data = hexToUint8Array(data); 32 | } 33 | 34 | // append data to the receive buffer 35 | const removeCount = (this.receiveBuffer.length > 300) ? 50 : 0; 36 | const bytesToCopy = this.receiveBuffer.length - removeCount; 37 | 38 | const newBuffer = new Uint8Array(bytesToCopy + data.length); 39 | 40 | newBuffer.set(this.receiveBuffer.subarray(removeCount, bytesToCopy)); 41 | newBuffer.set(data, bytesToCopy); 42 | 43 | this.receiveBuffer = newBuffer; 44 | 45 | // process the buffer 46 | while (this.receiveBuffer.length > 3) { 47 | if (this.receiveBuffer[0] != 85) { 48 | this.receiveBuffer = this.receiveBuffer.subarray(1); 49 | continue; 50 | } 51 | 52 | const length = decodeBytes(this.receiveBuffer, 1, 2) & 0x3FF; 53 | 54 | if (length > 300) { // sanity limit 55 | this.receiveBuffer = this.receiveBuffer.subarray(3); 56 | continue; 57 | } 58 | 59 | if (this.receiveBuffer.length < length) break; 60 | 61 | const packet = new Packer(); 62 | packet.unpack(this.receiveBuffer.subarray(0, length)); 63 | 64 | // check if packet is valid 65 | if (!packet.validate()) { 66 | this.receiveBuffer = this.receiveBuffer.subarray(length - 1); 67 | continue; 68 | } 69 | 70 | 71 | if (packet.buffer != null) { 72 | // we can assume that the packet is valid 73 | this.receiveBuffer = this.receiveBuffer.subarray(length); 74 | 75 | this.lastSeenSeq = packet.seq; 76 | 77 | // send packet to be matched up 78 | const matched = this.sentPackets.find(lookup => (lookup.seq == packet.seq)); 79 | if (matched != null) { 80 | // we have a match 81 | this.sentPackets.splice(this.sentPackets.indexOf(matched), 1); 82 | 83 | matched.timeout && clearTimeout(matched.timeout); 84 | 85 | // send the callback 86 | matched.Promise.resolve(packet); 87 | } else { 88 | // we don't have a match, send to unmatched callback 89 | this.unmatchedListener(packet); 90 | } 91 | } 92 | } 93 | } 94 | 95 | transmit(PackedPacket, ttl = 1000) { 96 | return new Promise(function (resolve, reject) { 97 | // check if PackedPacket requires a response 98 | 99 | PackedPacket.Promise = { resolve, reject }; 100 | // set a timeout to reject the promise 101 | 102 | PackedPacket.timeout = setTimeout(function () { 103 | 104 | // timeout the promise 105 | PackedPacket.Promise.reject(new Error("Timeout")); 106 | // remove the packet from the sent list 107 | this.sentPackets.splice(this.sentPackets.indexOf(PackedPacket), 1); 108 | }.bind(this), ttl); 109 | 110 | this.sentPackets.push(PackedPacket); 111 | }.bind(this)); 112 | } 113 | } 114 | 115 | module.exports = Session 116 | 117 | -------------------------------------------------------------------------------- /src/js/config/models.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | wm169: { 3 | codename: "wm169", 4 | name: "DJI Avata", 5 | image: "/assets/img/models/avata.png", 6 | type: "drone", 7 | slug: "dji-avata", 8 | isEnabled: true, 9 | methods: ["Bulk"], 10 | connectionSteps: ["GogglesPower", "AvataPower", "GogglesUsb", "InstallBulkDriver"], 11 | filters: [ 12 | { vendorId: 0x2ca3, productId: 0x0020 }, 13 | ] 14 | }, 15 | zv900: { 16 | codename: "zv900", 17 | name: "Goggles 2", 18 | image: "/", 19 | type: "glass", 20 | slug: "dji-goggles-2", 21 | isEnabled: false, 22 | methods: ["Bulk"], 23 | connectionSteps: ["GogglesPower", "GogglesUsb"], 24 | filters: [ 25 | { vendorId: 0x2ca3, productId: 0x0020 }, 26 | ] 27 | }, 28 | wm170: { 29 | codename: "wm170", 30 | name: "DJI FPV", 31 | image: "/assets/img/models/dji-fpv.png", 32 | type: "drone", 33 | slug: "dji-fpv", 34 | isEnabled: true, 35 | methods: ["Serial"], 36 | connectionSteps: ["GogglesPower", "DjiFpvPower", "GogglesUsb"], 37 | filters: [ 38 | { vendorId: 0x2ca3 }, 39 | ] 40 | }, 41 | gl170: { 42 | codename: "gl170", 43 | name: "FPV Goggles V2 (DJI FPV)", 44 | image: "/", 45 | type: "glass", 46 | slug: "dji-goggles-v2-fpv", 47 | isEnabled: false, 48 | methods: ["Serial"], 49 | connectionSteps: ["GogglesPower", "GogglesUsb"], 50 | filters: [ 51 | { vendorId: 0x2ca3 }, 52 | ] 53 | }, 54 | xxx: { 55 | codename: "xxx", 56 | name: "Generic Bulk", 57 | image: "/", 58 | type: "glass", 59 | slug: "fpv-goggles-v2-diy", 60 | isEnabled: false, 61 | methods: ["Serial"], 62 | connectionSteps: ["GogglesPower", "GogglesUsb"], 63 | filters: [ 64 | { vendorId: 0x2ca3 }, 65 | ] 66 | }, 67 | } 68 | 69 | 70 | 71 | // list from Dank Drone Downloader 72 | // a2 - A3 Flight Controller 73 | // ac103 - Action 2 74 | // tp703 - Aeroscope 75 | // ag600 - AG600 Gimball 76 | // ag406 - AGRAS MG-1A 77 | // ag407 - AGRAS MG-1P RTK 78 | // ag405 - AGRAS MG-1S 79 | // ag500 - AGRAS T10 80 | // ag410 - AGRAS T20 81 | // ag501 - AGRAS T30 82 | // wm169 - Avata 83 | // cs550 - Crystalsky 5.50 Inch 84 | // cs785 - Crystalsky 7.85 Inch 85 | // r400 - D-RTK GNSS 86 | // zv900 - DJI Goggles 2 87 | // rm330 - DJI RC 88 | // rm700 - DJI RC Plus 89 | // gl150 - FPV Goggles V1 90 | // gp150 - FPV Goggles V2 91 | // wm170 - FPV Racer 92 | // wm150 - FPV System - Air Unit 93 | // lt150 - FPV System - Air Unit Lite 94 | // rc150 - FPV System - RC 95 | // gl170 - Goggles - FPV Racer 96 | // gl811 - Goggles - Racing Edition 97 | // p1gs - Goggles - Standard 98 | // wm600 - Inspire 1 99 | // wm610 - Inspire 1 Pro 100 | // wm620 - Inspire 2 101 | // m100 - Matrice 100 102 | // pm410 - Matrice 200 103 | // pm420 - Matrice 200 V2 104 | // pm430 - Matrice 300 105 | // m601 - Matrice 600 106 | // m600 - Matrice 600 Pro 107 | // wm230 - Mavic Air 108 | // wm231 - Mavic Air 2 109 | // wm232 - Mavic Air 2s 110 | // wm160 - Mavic Mini 111 | // wm161 - Mavic Mini 2 112 | // wm162 - Mavic Mini 3 113 | // wm1605 - Mavic Mini SE 114 | // wm220 - Mavic Pro 1 - Incl Platinum and Alpine 115 | // wm240 - Mavic Pro 2 - Incl Zoom 116 | // wm245 - Mavic Pro 2 Enterprise 117 | // wm246 - Mavic Pro 2 Enterprise Dual 118 | // wm247 - Mavic Pro 2 Enterprise RTK 119 | // wm260 - Mavic Pro 3 120 | // dlg30a - N3 Flight Controller 121 | // zv811 - Ocusync Air System 122 | // ac101 - Osmo - Action 123 | // hg200 - Osmo - Incl Pro Raw and Standard 124 | // hg210 - Osmo - Pocket 125 | // hg211 - Osmo - Pocket 2 126 | // wm325 - Phantom 3 - 4K 127 | // wm322 - Phantom 3 - Advanced 128 | // wm323 - Phantom 3 - Professional 129 | // wm321 - Phantom 3 - Standard 130 | // wm332 - Phantom 4 - Advanced 131 | // wm336 - Phantom 4 - Multispectral 132 | // wm331 - Phantom 4 - Professional 133 | // wm335 - Phantom 4 - Professional 2.0 134 | // wm330 - Phantom 4 - Standard 135 | // wm334 - Phantom 4 RTK 136 | // wm334 - Phantom 4 RTK - China Only Version 137 | // xw607 - Robomaster S1 138 | // rm500 - Smart Controller 139 | // rm510 - Smart Controller 2021 140 | // wm100 - Spark 141 | // ag411 - Unknown Aircraft 142 | // ag601 - Unknown Aircraft 143 | // wm222 - Unknown Aircraft 144 | -------------------------------------------------------------------------------- /src/js/class/duml/Packer.js: -------------------------------------------------------------------------------- 1 | const CRC8 = new Int8Array([0, 94, 188, 226, 97, 63, 221, 131, 194, 156, 126, 32, 163, 253, 31, 65, 157, 195, 33, 127, 252, 162, 64, 30, 95, 1, 227, 189, 62, 96, 130, 220, 35, 125, 159, 193, 66, 28, 254, 160, 225, 191, 93, 3, 128, 222, 60, 98, 190, 224, 2, 92, 223, 129, 99, 61, 124, 34, 192, 158, 29, 67, 161, 255, 70, 24, 250, 164, 39, 121, 155, 197, 132, 218, 56, 102, 229, 187, 89, 7, 219, 133, 103, 57, 186, 228, 6, 88, 25, 71, 165, 251, 120, 38, 196, 154, 101, 59, 217, 135, 4, 90, 184, 230, 167, 249, 27, 69, 198, 152, 122, 36, 248, 166, 68, 26, 153, 199, 37, 123, 58, 100, 134, 216, 91, 5, 231, 185, 140, 210, 48, 110, 237, 179, 81, 15, 78, 16, 242, 172, 47, 113, 147, 205, 17, 79, 173, 243, 112, 46, 204, 146, 211, 141, 111, 49, 178, 236, 14, 80, 175, 241, 19, 77, 206, 144, 114, 44, 109, 51, 209, 143, 12, 82, 176, 238, 50, 108, 142, 208, 83, 13, 239, 177, 240, 174, 76, 18, 145, 207, 45, 115, 202, 148, 118, 40, 171, 245, 23, 73, 8, 86, 180, 234, 105, 55, 213, 139, 87, 9, 235, 181, 54, 104, 138, 212, 149, 203, 41, 119, 244, 170, 72, 22, 233, 183, 85, 11, 136, 214, 52, 106, 43, 117, 151, 201, 74, 20, 246, 168, 116, 42, 200, 150, 21, 75, 169, 247, 182, 232, 10, 84, 215, 137, 107, 53,]) 2 | const CRC16 = new Int16Array([0, 4489, 8978, 12955, 17956, 22445, 25910, 29887, 35912, 40385, 44890, 48851, 51820, 56293, 59774, 63735, 4225, 264, 13203, 8730, 22181, 18220, 30135, 25662, 40137, 36160, 49115, 44626, 56045, 52068, 63999, 59510, 8450, 12427, 528, 5017, 26406, 30383, 17460, 21949, 44362, 48323, 36440, 40913, 60270, 64231, 51324, 55797, 12675, 8202, 4753, 792, 30631, 26158, 21685, 17724, 48587, 44098, 40665, 36688, 64495, 60006, 55549, 51572, 16900, 21389, 24854, 28831, 1056, 5545, 10034, 14011, 52812, 57285, 60766, 64727, 34920, 39393, 43898, 47859, 21125, 17164, 29079, 24606, 5281, 1320, 14259, 9786, 57037, 53060, 64991, 60502, 39145, 35168, 48123, 43634, 25350, 29327, 16404, 20893, 9506, 13483, 1584, 6073, 61262, 65223, 52316, 56789, 43370, 47331, 35448, 39921, 29575, 25102, 20629, 16668, 13731, 9258, 5809, 1848, 65487, 60998, 56541, 52564, 47595, 43106, 39673, 35696, 33800, 38273, 42778, 46739, 49708, 54181, 57662, 61623, 2112, 6601, 11090, 15067, 20068, 24557, 28022, 31999, 38025, 34048, 47003, 42514, 53933, 49956, 61887, 57398, 6337, 2376, 15315, 10842, 24293, 20332, 32247, 27774, 42250, 46211, 34328, 38801, 58158, 62119, 49212, 53685, 10562, 14539, 2640, 7129, 28518, 32495, 19572, 24061, 46475, 41986, 38553, 34576, 62383, 57894, 53437, 49460, 14787, 10314, 6865, 2904, 32743, 28270, 23797, 19836, 50700, 55173, 58654, 62615, 32808, 37281, 41786, 45747, 19012, 23501, 26966, 30943, 3168, 7657, 12146, 16123, 54925, 50948, 62879, 58390, 37033, 33056, 46011, 41522, 23237, 19276, 31191, 26718, 7393, 3432, 16371, 11898, 59150, 63111, 50204, 54677, 41258, 45219, 33336, 37809, 27462, 31439, 18516, 23005, 11618, 15595, 3696, 8185, 63375, 58886, 54429, 50452, 45483, 40994, 37561, 33584, 31687, 27214, 22741, 18780, 15843, 11370, 7921, 3960]) 3 | 4 | 5 | const calcCrc16 = (array, length) => { 6 | let seed = 0x3692; 7 | 8 | // seed = 0x1012 // Naza M 9 | // seed = 0x1013 // Phantom 2 10 | // seed = 0x7000 // Naza M V2 11 | // seed = 0x3692 // P3/P4/Mavic/Later 12 | 13 | for (let pos = 0; pos < length; pos++) { 14 | seed = (CRC16[((seed ^ array[pos]) & 0xff)] & 0xffff) ^ (seed >> 8); 15 | } 16 | return seed; 17 | } 18 | 19 | const calcCrc8 = (array, length) => { 20 | let crc = 0x77; 21 | for (let pos = 0; pos < length; pos++) { 22 | crc = CRC8[(crc ^ array[pos]) & 0xff]; 23 | } 24 | return crc; 25 | } 26 | 27 | class pack { 28 | constructor(inputArray = null) { 29 | this.buffer; 30 | this.sof = 85; 31 | this.ccode; 32 | this.cmdId; 33 | this.cmdSet; 34 | this.cmdType; 35 | this.crc16; 36 | this.crc8; 37 | this.encryptType; 38 | this.isNeedAck; 39 | this.length; 40 | this.receiverId; 41 | this.receiverType; 42 | this.senderId; 43 | this.senderType; 44 | this.seq; 45 | this.data; 46 | this.version = 1; 47 | 48 | this.packRepeatTimes = 2; 49 | this.packTimeOut = 1000; 50 | this.repeatTimes = 2; 51 | this.timeOut = 1000; 52 | 53 | if (inputArray != null) { 54 | this.unpack(inputArray); 55 | } 56 | } 57 | 58 | 59 | 60 | getLength() { 61 | return this.length; 62 | } 63 | 64 | encodeSequenceNo() { 65 | let sequence = new Uint8Array(2); 66 | sequence[0] = (this.seq & 255); 67 | sequence[1] = ((this.seq & 65280) >> 8); 68 | return sequence; 69 | } 70 | 71 | decodeBytes(offset, length) { 72 | let value = 0; 73 | for (let i = (offset + length) - 1; i >= offset; i--) { 74 | value = (value << 8) | (this.buffer[i] & 255); 75 | } 76 | return value; 77 | } 78 | 79 | reCrc() { 80 | if (this.buffer != null) { 81 | let endOfBuffer = this.buffer.length - 2; 82 | let crcs = calcCrc16(this.buffer, endOfBuffer) 83 | this.buffer[endOfBuffer - 0] = (crcs & 255); 84 | this.buffer[endOfBuffer + 1] = ((65280 & crcs) >> 8); 85 | } 86 | } 87 | 88 | validate() { 89 | // check the last two bytes of the buffer are equal to the last 2 after reCrc 90 | if (this.buffer != null) { 91 | let endOfBuffer = this.buffer.length - 2; 92 | let crcs = calcCrc16(this.buffer, endOfBuffer) 93 | return (this.buffer[endOfBuffer - 0] == (crcs & 255) && this.buffer[endOfBuffer + 1] == ((65280 & crcs) >> 8)) 94 | } else return false; 95 | } 96 | 97 | // isNeedCcode() { 98 | // // NOT IMPLEMENTED 99 | // // try { 100 | // // this.cmdSetObj = CmdSet.find(this.cmdSet); 101 | // // if (this.cmdSetObj == null || this.cmdSetObj.cmdIdClass() == null) { 102 | 103 | // // } else { 104 | // // this.isNeedCcode = this.cmdSetObj.cmdIdClass().isNeedCcode(this.cmdId); 105 | // // } 106 | // // } catch (e) { 107 | 108 | // // } 109 | // } 110 | 111 | pack() { 112 | if (this.data == null) { 113 | this.length = 13; 114 | } else { 115 | this.length = this.data.length + 13; 116 | } 117 | this.buffer = new Uint8Array(this.length); 118 | let box_head = this.buffer 119 | box_head[0] = this.sof; 120 | box_head[1] = (this.length & 255); 121 | box_head[2] = ((this.length >> 8) & 3); 122 | box_head[2] = (box_head[2] | 4); 123 | box_head[3] = calcCrc8(box_head, 3); 124 | this.crc8 = box_head[3]; 125 | box_head[4] = ((this.senderId << 5) | this.senderType); 126 | box_head[5] = ((this.receiverId << 5) | this.receiverType); 127 | box_head[6] = this.encodeSequenceNo()[0]; 128 | box_head[7] = this.encodeSequenceNo()[1]; 129 | box_head[8] = ((this.cmdType << 7) | (this.isNeedAck << 5) | this.encryptType); 130 | box_head[9] = this.cmdSet; 131 | box_head[10] = this.cmdId; 132 | 133 | if (this.data != null) { 134 | this.data.forEach((e, i) => { 135 | box_head[i + 11] = e 136 | }); 137 | } 138 | 139 | let endOfBuffer = this.length - 2; 140 | let crcs = calcCrc16(box_head, endOfBuffer) 141 | this.crc16 = crcs; 142 | box_head[this.length - 2] = (crcs & 255); 143 | box_head[this.length - 1] = ((65280 & crcs) >> 8); 144 | 145 | this.buffer = box_head; 146 | } 147 | 148 | unpack(buffer) { 149 | if (buffer != null && buffer.length >= 13) { 150 | this.buffer = buffer; 151 | this.sof = buffer[0]; 152 | 153 | let VL = this.decodeBytes(1, 2); 154 | this.version = VL >> 10; 155 | this.length = VL & 1023; 156 | 157 | this.crc8 = this.buffer[3]; 158 | this.senderId = parseInt(this.buffer[4]) >> 5; 159 | this.senderType = parseInt(this.buffer[4]) & 31; 160 | this.receiverId = parseInt(this.buffer[5]) >> 5; 161 | this.receiverType = parseInt(this.buffer[5]) & 31; 162 | this.seq = this.decodeBytes(6, 2); 163 | this.cmdType = parseInt(this.buffer[8]) >> 7; 164 | this.isNeedAck = (parseInt(this.buffer[8]) >> 5) & 3; 165 | this.encryptType = parseInt(this.buffer[8]) & 7; 166 | this.cmdSet = parseInt(this.buffer[9]); 167 | this.cmdId = parseInt(this.buffer[10]); 168 | 169 | if (this.cmdType == 1) { 170 | this.ccode = parseInt(this.buffer[11]); 171 | } 172 | 173 | let dataLen = (this.buffer.length - 11) - 2; 174 | if (dataLen > 0) { 175 | this.data = new Uint8Array(dataLen); 176 | 177 | this.data.forEach((e, i) => { 178 | this.data[i] = this.buffer[i + 11] 179 | }); 180 | 181 | } 182 | this.crc16 = this.decodeBytes(this.buffer.length - 2, 2); //BytesUtil.getInt(buffer, buffer.length - 2, 2); 183 | } 184 | } 185 | 186 | resetData() { 187 | this.isNeedCcode = true; 188 | this.buffer = null; 189 | this.sof = 0; 190 | this.version = 1; 191 | this.length = 0; 192 | this.crc8 = 0; 193 | this.senderId = 0; 194 | this.senderType = 0; 195 | this.receiverId = 0; 196 | this.receiverType = 0; 197 | this.seq = 0; 198 | this.cmdType = 0; 199 | this.isNeedAck = 0; 200 | this.encryptType = 0; 201 | this.cmdSet = 0; 202 | this.cmdId = 0; 203 | this.ccode = 0; 204 | this.data = null; 205 | this.crc16 = 0; 206 | } 207 | 208 | toString() { 209 | if (this.buffer != null) { 210 | return Array.prototype.map.call(new Uint8Array(this.buffer), x => ('00' + x.toString(16)).slice(-2)).join(''); 211 | } else return ""; 212 | } 213 | 214 | toUint8Array() { 215 | return new Uint8Array(this.buffer); 216 | } 217 | 218 | toObject() { 219 | return { 220 | sof: this.sof, 221 | version: this.version, 222 | ccode: this.ccode, 223 | cmdId: this.cmdId, 224 | cmdSet: this.cmdSet, 225 | cmdType: this.cmdType, 226 | crc16: this.crc16, 227 | crc8: this.crc8, 228 | encryptType: this.encryptType, 229 | isNeedAck: this.isNeedAck, 230 | length: this.length, 231 | receiverId: this.receiverId, 232 | receiverType: this.receiverType, 233 | senderId: this.senderId, 234 | senderType: this.senderType, 235 | seq: this.seq, 236 | data: this.data, 237 | buffer: this.buffer 238 | 239 | } 240 | } 241 | 242 | static calcCrc16 = calcCrc16; 243 | static calcCrc8 = calcCrc8; 244 | } 245 | 246 | 247 | module.exports = pack; --------------------------------------------------------------------------------