├── .drone.yml
├── .github
└── workflows
│ ├── build-develop.yml
│ └── build-master.yml
├── .gitignore
├── .husky
└── pre-commit
├── .prettierrc
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── electron-builder.yml
├── electron.vite.config.ts
├── electron
├── icon.png
├── icon.svg
├── main
│ └── index.ts
└── preload
│ ├── index.d.ts
│ └── index.ts
├── env.d.ts
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── favicon.svg
├── gt20.glb
├── osd
│ ├── betaflight.png
│ ├── bold.png
│ ├── clarity.png
│ ├── default.png
│ ├── digital.png
│ ├── extra_large.png
│ ├── hdzero_quic.png
│ ├── impact.png
│ ├── impact_mini.png
│ ├── large.png
│ └── vision.png
├── osd_background.jpg
├── osd_logo.png
├── pwa.png
└── robots.txt
├── script
└── extract-tooltips.js
├── src
├── App.vue
├── assets
│ ├── Logo_Clean.svg
│ ├── Logo_Develop_Text.svg
│ ├── Logo_Text.svg
│ ├── props_view.svg
│ └── tooltips.json
├── components
│ ├── AlertPortal.vue
│ ├── InputSelect.vue
│ ├── LineChart.vue
│ ├── ModalPortal.vue
│ ├── RealtimePlot.vue
│ ├── SelectModal.vue
│ ├── SpinnerBtn.vue
│ ├── TemplateCard.vue
│ └── Tooltip.vue
├── log.ts
├── main.ts
├── mixin
│ ├── chart.ts
│ ├── filters.ts
│ ├── icons.ts
│ └── modal.ts
├── panel
│ ├── AuxChannels.vue
│ ├── ESCSettings.vue
│ ├── FilterSettings.vue
│ ├── Flash.vue
│ ├── GyroModel.vue
│ ├── Info.vue
│ ├── Motor.vue
│ ├── MotorPins.vue
│ ├── MotorTest.vue
│ ├── OSDElements.vue
│ ├── OSDElementsLegacy.vue
│ ├── OSDFont.vue
│ ├── PIDRates.vue
│ ├── ProfileMetadata.vue
│ ├── RCChannels.vue
│ ├── ReceiverSettings.vue
│ ├── ReceiverSettingsLegacy.vue
│ ├── Serial.vue
│ ├── SerialPassthrough.vue
│ ├── StickRates.vue
│ ├── StickRatesLegacy.vue
│ ├── Target.vue
│ ├── ThrottleSettings.vue
│ ├── VTX.vue
│ └── Voltage.vue
├── router.ts
├── store
│ ├── bind.ts
│ ├── blackbox.ts
│ ├── constants.ts
│ ├── default_profile.ts
│ ├── flash.ts
│ ├── flash
│ │ ├── dfu.ts
│ │ ├── flash.ts
│ │ └── ihex.ts
│ ├── index.ts
│ ├── info.ts
│ ├── motor.ts
│ ├── osd.ts
│ ├── perf.ts
│ ├── profile.ts
│ ├── root.ts
│ ├── serial.ts
│ ├── serial
│ │ ├── cbor.ts
│ │ ├── quic.ts
│ │ ├── serial.ts
│ │ ├── settings.ts
│ │ └── webserial.ts
│ ├── state.ts
│ ├── target.ts
│ ├── templates.ts
│ ├── types.ts
│ ├── util
│ │ ├── blackbox.ts
│ │ ├── github.ts
│ │ ├── index.ts
│ │ ├── osd.ts
│ │ └── updater.ts
│ └── vtx.ts
├── style.scss
├── sw.ts
├── switch.scss
└── views
│ ├── Blackbox.vue
│ ├── Home.vue
│ ├── Motor.vue
│ ├── OSD.vue
│ ├── Perf.vue
│ ├── Profile.vue
│ ├── Rates.vue
│ ├── Receiver.vue
│ ├── Setup.vue
│ ├── State.vue
│ └── Templates.vue
├── tsconfig.json
├── tsconfig.vite-config.json
└── vite.config.ts
/.drone.yml:
--------------------------------------------------------------------------------
1 | ---
2 | kind: pipeline
3 | type: docker
4 | name: default
5 |
6 | steps:
7 | - name: web
8 | image: hanfer/node-wine:16
9 | commands:
10 | - npm install
11 | - npm run build
12 | - name: publish-github
13 | image: plugins/github-release
14 | settings:
15 | api_key:
16 | from_secret: github_token
17 | files:
18 | - build/quic-config*.zip
19 | - build/quic-config*.exe
20 | when:
21 | event:
22 | - tag
23 |
--------------------------------------------------------------------------------
/.github/workflows/build-develop.yml:
--------------------------------------------------------------------------------
1 | name: build-develop
2 | on:
3 | push:
4 | branches:
5 | - "develop"
6 |
7 | permissions:
8 | contents: write
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: checkout
15 | uses: actions/checkout@v4
16 |
17 | - name: cache node_modules
18 | uses: actions/cache@v4
19 | with:
20 | path: node_modules
21 | key: ${{ runner.os }}-develop-node_modules
22 |
23 | - name: install npm packages
24 | run: npm install
25 |
26 | - name: build gh-pages
27 | run: |
28 | npm run build:gh-pages
29 |
30 | - name: upload artifacts
31 | uses: actions/upload-artifact@v4
32 | with:
33 | name: gh-pages
34 | path: docs
35 |
36 | deploy:
37 | needs: build
38 | runs-on: ubuntu-latest
39 | steps:
40 | - name: checkout
41 | uses: actions/checkout@v4
42 |
43 | - name: checkout gh-pages
44 | run: |
45 | git config --local user.email "action@github.com"
46 | git config --local user.name "GitHub Action"
47 | git fetch
48 | git checkout gh-pages
49 |
50 | - uses: actions/download-artifact@v4
51 | with:
52 | name: gh-pages
53 | path: develop
54 |
55 | - name: update develop gh-pages
56 | run: |
57 | git add .
58 | git commit -m "GitHub Pages $GITHUB_SHA" || exit 0
59 | git remote set-url --push origin https://actions:$GITHUB_TOKEN@github.com/BossHobby/Configurator
60 | git push -f
61 |
--------------------------------------------------------------------------------
/.github/workflows/build-master.yml:
--------------------------------------------------------------------------------
1 | name: build-master
2 | on:
3 | push:
4 | branches:
5 | - "master"
6 | tags:
7 | - v*
8 |
9 | permissions:
10 | contents: write
11 |
12 | jobs:
13 | web:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: checkout
17 | uses: actions/checkout@v4
18 |
19 | - name: cache node_modules
20 | uses: actions/cache@v4
21 | with:
22 | path: node_modules
23 | key: ${{ runner.os }}-develop-node_modules
24 |
25 | - name: install npm packages
26 | run: npm install
27 |
28 | - name: build
29 | run: |
30 | npm run build:gh-pages
31 | echo 'config.bosshobby.com' > docs/CNAME
32 |
33 | - name: upload artifacts
34 | uses: actions/upload-artifact@v4
35 | with:
36 | name: gh-pages
37 | path: docs
38 |
39 | linux:
40 | runs-on: ubuntu-latest
41 | needs: web
42 | if: startsWith(github.ref, 'refs/tags/')
43 | steps:
44 | - name: checkout
45 | uses: actions/checkout@v4
46 |
47 | - name: cache node_modules
48 | uses: actions/cache@v4
49 | with:
50 | path: node_modules
51 | key: ${{ runner.os }}-develop-node_modules
52 |
53 | - name: install npm packages
54 | run: npm install
55 |
56 | - name: build
57 | run: |
58 | npm run build:linux
59 |
60 | - name: upload artifacts
61 | uses: actions/upload-artifact@v4
62 | with:
63 | name: quic-config-linux
64 | path: build/*.AppImage
65 |
66 | windows:
67 | runs-on: windows-latest
68 | needs: web
69 | if: startsWith(github.ref, 'refs/tags/')
70 | steps:
71 | - name: checkout
72 | uses: actions/checkout@v4
73 |
74 | - name: cache node_modules
75 | uses: actions/cache@v4
76 | with:
77 | path: node_modules
78 | key: ${{ runner.os }}-develop-node_modules
79 |
80 | - name: install npm packages
81 | run: npm install
82 |
83 | - name: build
84 | run: |
85 | npm run build:windows
86 |
87 | - name: upload artifacts
88 | uses: actions/upload-artifact@v4
89 | with:
90 | name: quic-config-windows
91 | path: build/*.exe
92 |
93 | mac:
94 | runs-on: macos-latest
95 | needs: web
96 | if: startsWith(github.ref, 'refs/tags/')
97 | steps:
98 | - name: checkout
99 | uses: actions/checkout@v4
100 |
101 | - name: cache node_modules
102 | uses: actions/cache@v4
103 | with:
104 | path: node_modules
105 | key: ${{ runner.os }}-develop-node_modules
106 |
107 | - name: install npm packages
108 | run: npm install
109 |
110 | - name: build
111 | run: |
112 | npm run build:mac
113 |
114 | - name: upload artifacts
115 | uses: actions/upload-artifact@v4
116 | with:
117 | name: quic-config-mac
118 | path: build/*.dmg
119 |
120 | deploy:
121 | runs-on: ubuntu-latest
122 | needs: web
123 | steps:
124 | - name: checkout
125 | uses: actions/checkout@v4
126 |
127 | - name: checkout gh-pages
128 | run: |
129 | git config --local user.email "action@github.com"
130 | git config --local user.name "GitHub Action"
131 | git fetch
132 | git checkout gh-pages
133 |
134 | - uses: actions/download-artifact@v4
135 | with:
136 | name: gh-pages
137 |
138 | - name: update gh-pages
139 | run: |
140 | git add .
141 | git commit -m "GitHub Pages $GITHUB_SHA" || exit 0
142 | git remote set-url --push origin https://actions:$GITHUB_TOKEN@github.com/BossHobby/Configurator
143 | git push -f
144 |
145 | release:
146 | runs-on: ubuntu-latest
147 | needs: [linux, windows, mac]
148 | if: startsWith(github.ref, 'refs/tags/')
149 | steps:
150 | - uses: actions/download-artifact@v4
151 |
152 | - name: release
153 | uses: softprops/action-gh-release@v2
154 | with:
155 | files: |
156 | quic-config-linux/*
157 | quic-config-mac/*
158 | quic-config-windows/*
159 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /web/dist
4 | /dev-dist
5 |
6 | # local env files
7 | .envrc
8 | .env.local
9 | .env.*.local
10 |
11 | # Log files
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
25 | *.perf
26 |
27 | __debug*
28 | /quic-config*
29 | /misc
30 | /pkg/statik
31 | /cmd/quic-config/rsrc.syso
32 | quicksilver.log
33 |
34 | /dist
35 | /build
36 | /out
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npm run pre-commit
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.vscode-typescript-vue-plugin"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "editor.formatOnSave": true,
4 | "editor.formatOnSaveMode": "file"
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Benedikt Kleiner
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # QUICKSILVER Configurator
4 |
5 | Configurator for the [QUICKSILVER Flight Controller Firmware](https://github.com/BossHobby/QUICKSILVER).
6 | A web version is available at [config.bosshobby.com](https://config.bosshobby.com).
7 | Standalone versions can be found in the [github releases](https://github.com/BossHobby/Configurator/releases).
8 | The Web version might or might not work with an Android device and an OTG cable.
9 |
10 | ## Building
11 |
12 | ```
13 | npm install
14 | npm run serve # run in a local browser
15 | npm run start # run in nwjs
16 | ```
17 |
18 | ## Contributing
19 |
20 | Contributions are welcome and encouraged.
21 |
--------------------------------------------------------------------------------
/electron-builder.yml:
--------------------------------------------------------------------------------
1 | appId: com.bosshobby.config
2 | productName: quic-config
3 | directories:
4 | output: build
5 | buildResources: public
6 | files:
7 | - "out/**/*"
8 | - package-lock.json
9 | - package.json
10 | - README.md
11 | win:
12 | target: portable
13 | artifactName: ${name}-${version}.${ext}
14 | dmg:
15 | artifactName: ${name}-${version}.${ext}
16 | linux:
17 | target:
18 | - AppImage
19 | artifactName: ${name}-${version}.${ext}
20 |
--------------------------------------------------------------------------------
/electron.vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "path";
2 | import { defineConfig } from "electron-vite";
3 | import VueConfig from "./vite.config";
4 |
5 | export default defineConfig({
6 | main: {
7 | build: {
8 | rollupOptions: {
9 | input: {
10 | index: resolve(__dirname, "electron/main/index.ts"),
11 | },
12 | },
13 | },
14 | },
15 | preload: {
16 | build: {
17 | rollupOptions: {
18 | input: {
19 | index: resolve(__dirname, "electron/preload/index.ts"),
20 | },
21 | },
22 | },
23 | },
24 | renderer: {
25 | root: ".",
26 | build: {
27 | rollupOptions: {
28 | input: {
29 | index: resolve(__dirname, "index.html"),
30 | },
31 | },
32 | },
33 | envPrefix: "VITE_",
34 | ...VueConfig,
35 | },
36 | });
37 |
--------------------------------------------------------------------------------
/electron/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/electron/icon.png
--------------------------------------------------------------------------------
/electron/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/electron/main/index.ts:
--------------------------------------------------------------------------------
1 | import { app, shell, BrowserWindow, ipcMain } from "electron";
2 | import { join } from "path";
3 | import { electronApp, optimizer, is } from "@electron-toolkit/utils";
4 |
5 | import icon from "../icon.png?asset";
6 |
7 | const USB_DEVICE_FILTER = [
8 | { vendorId: 0x0483, productId: 0xdf11 },
9 | { vendorId: 0x2e3c, productId: 0xdf11 },
10 | ];
11 |
12 | function createWindow(): void {
13 | // Create the browser window.
14 | const mainWindow = new BrowserWindow({
15 | width: 1280,
16 | height: 720,
17 | show: false,
18 | autoHideMenuBar: true,
19 | ...(process.platform === "linux" ? { icon } : {}),
20 | webPreferences: {
21 | preload: join(__dirname, "../preload/index.mjs"),
22 | sandbox: false,
23 | },
24 | });
25 |
26 | mainWindow.on("ready-to-show", () => {
27 | mainWindow.show();
28 | });
29 |
30 | mainWindow.webContents.setWindowOpenHandler((details) => {
31 | shell.openExternal(details.url);
32 | return { action: "deny" };
33 | });
34 |
35 | mainWindow.webContents.session.on(
36 | "select-serial-port",
37 | (event, portList, webContents, callback) => {
38 | event.preventDefault();
39 | mainWindow.webContents.send("select-serial", portList);
40 | ipcMain.once("serial", (_event, port) => {
41 | callback(port ? port : "");
42 | });
43 | },
44 | );
45 |
46 | mainWindow.webContents.session.on(
47 | "select-usb-device",
48 | (event, details, callback) => {
49 | event.preventDefault();
50 |
51 | const devices = details.deviceList.filter((d) => {
52 | return USB_DEVICE_FILTER.some(
53 | (f) => f.vendorId == d.vendorId && f.productId == d.productId,
54 | );
55 | });
56 |
57 | mainWindow.webContents.send("select-usb-device", devices);
58 | ipcMain.once("usb-device", (_event, dev) => {
59 | if (dev) {
60 | callback(dev);
61 | } else {
62 | callback();
63 | }
64 | });
65 | },
66 | );
67 |
68 | mainWindow.webContents.session.setPermissionCheckHandler(
69 | (webContents, permission, requestingOrigin, details) => {
70 | if (permission === "serial" || permission === "usb") {
71 | return true;
72 | }
73 |
74 | return false;
75 | },
76 | );
77 |
78 | mainWindow.webContents.session.setDevicePermissionHandler((details) => {
79 | if (details.deviceType === "serial" || details.deviceType === "usb") {
80 | return true;
81 | }
82 |
83 | return false;
84 | });
85 |
86 | // HMR for renderer base on electron-vite cli.
87 | // Load the remote URL for development or the local html file for production.
88 | if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
89 | mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
90 | } else {
91 | mainWindow.loadFile(join(__dirname, "../renderer/index.html"));
92 | }
93 | }
94 |
95 | // This method will be called when Electron has finished
96 | // initialization and is ready to create browser windows.
97 | // Some APIs can only be used after this event occurs.
98 | app.whenReady().then(() => {
99 | // Set app user model id for windows
100 | electronApp.setAppUserModelId("com.electron");
101 |
102 | // Default open or close DevTools by F12 in development
103 | // and ignore CommandOrControl + R in production.
104 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
105 | app.on("browser-window-created", (_, window) => {
106 | optimizer.watchWindowShortcuts(window);
107 | });
108 |
109 | createWindow();
110 |
111 | app.on("activate", function () {
112 | // On macOS it's common to re-create a window in the app when the
113 | // dock icon is clicked and there are no other windows open.
114 | if (BrowserWindow.getAllWindows().length === 0) createWindow();
115 | });
116 | });
117 |
118 | // Quit when all windows are closed, except on macOS. There, it's common
119 | // for applications and their menu bar to stay active until the user quits
120 | // explicitly with Cmd + Q.
121 | app.on("window-all-closed", () => {
122 | if (process.platform !== "darwin") {
123 | app.quit();
124 | }
125 | });
126 |
127 | // In this file you can include the rest of your app"s specific main process
128 | // code. You can also put them in separate files and require them here.
129 |
--------------------------------------------------------------------------------
/electron/preload/index.d.ts:
--------------------------------------------------------------------------------
1 | import { ElectronAPI } from "@electron-toolkit/preload";
2 |
3 | declare global {
4 | interface Window {
5 | electron: ElectronAPI;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/electron/preload/index.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge } from "electron";
2 | import { electronAPI } from "@electron-toolkit/preload";
3 |
4 | // Use `contextBridge` APIs to expose Electron APIs to
5 | // renderer only if context isolation is enabled, otherwise
6 | // just add to the DOM global.
7 | if (process.contextIsolated) {
8 | try {
9 | contextBridge.exposeInMainWorld("electron", electronAPI);
10 | } catch (error) {
11 | console.error(error);
12 | }
13 | } else {
14 | // @ts-ignore (define in dts)
15 | window.electron = electronAPI;
16 | }
17 |
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | import { ElectronAPI } from "@electron-toolkit/preload";
2 |
3 | ///
4 | ///
5 |
6 | declare global {
7 | interface Window {
8 | electron?: ElectronAPI;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import pluginVue from "eslint-plugin-vue";
2 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
3 |
4 | export default [
5 | ...pluginVue.configs["flat/recommended"],
6 | eslintPluginPrettierRecommended,
7 | {
8 | rules: {
9 | // override/add rules settings here, such as:
10 | // 'vue/no-unused-vars': 'error'
11 | },
12 | },
13 | ];
14 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | QUICKSILVER Configurator
10 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "quic-config",
3 | "version": "0.9.1",
4 | "private": true,
5 | "main": "./out/main/index.js",
6 | "type": "module",
7 | "scripts": {
8 | "serve": "vite",
9 | "build": "npm run build:clean && npm run build:vue && npm run build:electron",
10 | "lint": "eslint src",
11 | "postinstall": "electron-builder install-app-deps",
12 | "build:clean": "rimraf ./dist ./out ./build",
13 | "build:vue": "vite build",
14 | "build:gh-pages": "env DEPLOYMENT=gh-pages vite build --outDir docs",
15 | "build:linux": "electron-vite build && electron-builder --publish=never --linux --config",
16 | "build:windows": "electron-vite build && electron-builder --publish=never --win --config",
17 | "build:mac": "electron-vite build && electron-builder --publish=never --mac --config",
18 | "pwa": "env DEPLOYMENT=local vite build && npx http-server ./dist",
19 | "start": "electron-vite preview",
20 | "dev": "electron-vite dev",
21 | "extract-tooltips": "node script/extract-tooltips.js",
22 | "prepare": "husky",
23 | "pre-commit": "pretty-quick --staged"
24 | },
25 | "dependencies": {
26 | "@electron-toolkit/preload": "^3.0.1",
27 | "@electron-toolkit/utils": "^3.0.0",
28 | "@fortawesome/fontawesome-free": "^6.1.1",
29 | "@fortawesome/fontawesome-svg-core": "^6.1.1",
30 | "@fortawesome/free-brands-svg-icons": "^6.1.1",
31 | "@fortawesome/free-regular-svg-icons": "^6.1.1",
32 | "@fortawesome/free-solid-svg-icons": "^6.1.1",
33 | "@fortawesome/vue-fontawesome": "^3.0.1",
34 | "@zip.js/zip.js": "^2.7.14",
35 | "bulma": "^1.0.2",
36 | "chart.js": "^4.4.2",
37 | "chartjs-adapter-dayjs-4": "^1.0.4",
38 | "dayjs": "^1.11.13",
39 | "fuse.js": "^7.0.0",
40 | "isomorphic-fetch": "^3.0.0",
41 | "md5": "^2.3.0",
42 | "octokit": "^4.0.2",
43 | "pinia": "^2.0.15",
44 | "semver": "^7.3.5",
45 | "three": "^0.167.1",
46 | "ts-enum-util": "^4.0.2",
47 | "vue": "^3.1.0",
48 | "vue-chartjs": "^5.3.1",
49 | "vue-router": "^4.0.0",
50 | "web-serial-polyfill": "github:BossHobby/web-serial-polyfill",
51 | "yaml": "^2.1.1"
52 | },
53 | "devDependencies": {
54 | "@electron-toolkit/tsconfig": "^1.0.1",
55 | "@electron/notarize": "^2.3.0",
56 | "@types/dom-serial": "^1.0.2",
57 | "@types/node": "^18.15.3",
58 | "@types/semver": "^7.3.9",
59 | "@types/w3c-web-usb": "^1.0.6",
60 | "@vitejs/plugin-vue": "^5.0.4",
61 | "@vue/tsconfig": "^0.5.1",
62 | "electron": "^30.0.2",
63 | "electron-builder": "^24.13.3",
64 | "electron-vite": "^2.2.0",
65 | "eslint": "^9.2.0",
66 | "eslint-config-prettier": "^9.1.0",
67 | "eslint-plugin-prettier": "^5.1.3",
68 | "eslint-plugin-vue": "^9.25.0",
69 | "glob": "^10.2.7",
70 | "http-server": "^14.1.0",
71 | "husky": "^9.0.11",
72 | "prettier": "3.2.5",
73 | "pretty-quick": "^4.0.0",
74 | "rimraf": "^5.0.1",
75 | "sass": "^1.52.1",
76 | "typescript": "~5.4.5",
77 | "vite": "^5.2.11",
78 | "vite-plugin-pwa": "^0.20.0",
79 | "vite-plugin-webfont-dl": "^3.4.2",
80 | "vite-svg-loader": "^5.1.0",
81 | "workbox-precaching": "^7.0.0",
82 | "workbox-routing": "^7.3.0",
83 | "workbox-strategies": "^7.3.0",
84 | "workbox-expiration": "^7.3.0"
85 | },
86 | "browserslist": [
87 | "> 1%",
88 | "last 2 versions",
89 | "not dead"
90 | ]
91 | }
92 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
75 |
--------------------------------------------------------------------------------
/public/gt20.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/gt20.glb
--------------------------------------------------------------------------------
/public/osd/betaflight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd/betaflight.png
--------------------------------------------------------------------------------
/public/osd/bold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd/bold.png
--------------------------------------------------------------------------------
/public/osd/clarity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd/clarity.png
--------------------------------------------------------------------------------
/public/osd/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd/default.png
--------------------------------------------------------------------------------
/public/osd/digital.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd/digital.png
--------------------------------------------------------------------------------
/public/osd/extra_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd/extra_large.png
--------------------------------------------------------------------------------
/public/osd/hdzero_quic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd/hdzero_quic.png
--------------------------------------------------------------------------------
/public/osd/impact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd/impact.png
--------------------------------------------------------------------------------
/public/osd/impact_mini.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd/impact_mini.png
--------------------------------------------------------------------------------
/public/osd/large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd/large.png
--------------------------------------------------------------------------------
/public/osd/vision.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd/vision.png
--------------------------------------------------------------------------------
/public/osd_background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd_background.jpg
--------------------------------------------------------------------------------
/public/osd_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/osd_logo.png
--------------------------------------------------------------------------------
/public/pwa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BossHobby/Configurator/912a3b92b528014d5bf068395880faf918c1b77d/public/pwa.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
--------------------------------------------------------------------------------
/script/extract-tooltips.js:
--------------------------------------------------------------------------------
1 | const { globSync } = require("glob");
2 | const fs = require("fs");
3 |
4 | const regex = //gm;
5 | const whitelist = ["channel."];
6 | const tooltips = {};
7 |
8 | const files = globSync("src/**/*.vue");
9 |
10 | for (const f of files) {
11 | const data = fs.readFileSync(f, "utf8");
12 | var match;
13 | while ((match = regex.exec(data))) {
14 | tooltips[match[1]] = {};
15 | }
16 | }
17 |
18 | const existing = JSON.parse(
19 | fs.readFileSync("src/assets/tooltips.json", "utf8"),
20 | );
21 | for (const key of Object.keys(tooltips)) {
22 | tooltips[key] = {
23 | ...existing[key],
24 | };
25 | }
26 | for (const w of whitelist) {
27 | for (const key of Object.keys(existing)) {
28 | if (key.startsWith(w)) {
29 | tooltips[key] = {
30 | ...existing[key],
31 | };
32 | }
33 | }
34 | }
35 |
36 | const ordered = Object.keys(tooltips)
37 | .sort()
38 | .reduce((obj, key) => {
39 | obj[key] = tooltips[key];
40 | return obj;
41 | }, {});
42 | fs.writeFileSync("src/assets/tooltips.json", JSON.stringify(ordered, null, 2));
43 |
--------------------------------------------------------------------------------
/src/assets/Logo_Clean.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/AlertPortal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 | {{ alert.msg }}
12 |
13 |
14 |
15 |
16 |
17 |
55 |
56 |
75 |
--------------------------------------------------------------------------------
/src/components/InputSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/LineChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
68 |
--------------------------------------------------------------------------------
/src/components/ModalPortal.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/components/RealtimePlot.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
163 |
--------------------------------------------------------------------------------
/src/components/SelectModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Select {{ title }}
5 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |
34 |
35 |
36 |
37 |
50 |
51 |
63 |
--------------------------------------------------------------------------------
/src/components/SpinnerBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/components/TemplateCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ tmpl.name }}
7 |
8 |
9 |
12 |
13 | by {{ tmpl.author }}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
{{ tmpl.desc }}
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
38 |
43 |
44 |
45 |
46 | Please select an option!
47 |
48 |
52 | {{ selectedValues[o.name]?.desc }}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
72 |
73 |
74 |
75 |
208 |
209 |
215 |
--------------------------------------------------------------------------------
/src/components/Tooltip.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
17 |
22 |
23 |
24 |
25 |
26 |
31 |
32 | {{ tooltip.text }}
33 |
36 |
37 | Missing tooltip entry {{ entry }}
38 |
39 |
40 |
41 |
42 |
43 |
132 |
133 |
235 |
--------------------------------------------------------------------------------
/src/log.ts:
--------------------------------------------------------------------------------
1 | export enum LogLevel {
2 | Trace,
3 | Debug,
4 | Info,
5 | Warning,
6 | Error,
7 | }
8 |
9 | export const LevelNames = {
10 | [LogLevel.Trace]: "trace",
11 | [LogLevel.Debug]: "debug",
12 | [LogLevel.Info]: "info",
13 | [LogLevel.Warning]: "warning",
14 | [LogLevel.Error]: "error",
15 | };
16 |
17 | export class Log {
18 | public static level = import.meta.env.DEV ? LogLevel.Trace : LogLevel.Info;
19 | public static history: string[] = [];
20 |
21 | public static trace(prefix: string, ...data: any[]) {
22 | Log.log(LogLevel.Trace, prefix, ...data);
23 | }
24 |
25 | public static debug(prefix: string, ...data: any[]) {
26 | Log.log(LogLevel.Debug, prefix, ...data);
27 | }
28 |
29 | public static info(prefix: string, ...data: any[]) {
30 | Log.log(LogLevel.Info, prefix, ...data);
31 | }
32 |
33 | public static warn(prefix: string, ...data: any[]) {
34 | Log.log(LogLevel.Warning, prefix, ...data);
35 | }
36 |
37 | public static warning(prefix: string, ...data: any[]) {
38 | Log.log(LogLevel.Warning, prefix, ...data);
39 | }
40 |
41 | public static error(prefix: string, ...data: any[]) {
42 | Log.log(LogLevel.Error, prefix, ...data);
43 | }
44 |
45 | private static logFmtStr(
46 | level: LogLevel,
47 | prefix: string | undefined,
48 | data: any[],
49 | ): string {
50 | let str = "[" + LevelNames[level] + "]";
51 | if (prefix && prefix.length) {
52 | str += "[" + prefix + "]";
53 | }
54 | str += " ";
55 | while (typeof data[0] == "string") {
56 | str += data.shift();
57 | }
58 | return str;
59 | }
60 |
61 | private static logToFile(level: LogLevel, fmt: string, ...data: any[]) {
62 | for (const d of data) {
63 | fmt += " ";
64 |
65 | if (typeof d == "string") {
66 | fmt += d;
67 | } else {
68 | fmt += JSON.stringify(d);
69 | }
70 | }
71 | Log.history.push(fmt);
72 | }
73 |
74 | public static log(level: LogLevel, prefix?: string, ...data: any[]) {
75 | if (level < Log.level) {
76 | return;
77 | }
78 |
79 | switch (level) {
80 | case LogLevel.Trace: {
81 | const str = this.logFmtStr(level, prefix, data);
82 | console.debug(str, ...data);
83 | this.logToFile(level, str, ...data);
84 | break;
85 | }
86 | case LogLevel.Debug:
87 | case LogLevel.Info: {
88 | const str = this.logFmtStr(level, prefix, data);
89 | console.log(str, ...data);
90 | this.logToFile(level, str, ...data);
91 | break;
92 | }
93 | case LogLevel.Warning: {
94 | const str = this.logFmtStr(level, prefix, data);
95 | console.warn(str, ...data);
96 | this.logToFile(level, str, ...data);
97 | break;
98 | }
99 | case LogLevel.Error: {
100 | const str = this.logFmtStr(level, prefix, data);
101 | console.error(str, ...data);
102 | this.logToFile(level, str, ...data);
103 | break;
104 | }
105 | default:
106 | break;
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import App from "./App.vue";
4 | import router from "./router";
5 | import pinia from "./store";
6 |
7 | import SpinnerBtn from "./components/SpinnerBtn.vue";
8 | import Tooltip from "./components/Tooltip.vue";
9 | import InputSelect from "./components/InputSelect.vue";
10 | import FontAwesomeIcon from "./mixin/icons";
11 | import { ModalPlugin } from "./mixin/modal";
12 |
13 | import "./style.scss";
14 | import "./mixin/chart.ts";
15 |
16 | const app = createApp(App);
17 |
18 | app.component("spinner-btn", SpinnerBtn);
19 | app.component("tooltip", Tooltip);
20 | app.component("input-select", InputSelect);
21 | app.component("FontAwesomeIcon", FontAwesomeIcon);
22 |
23 | app.use(pinia);
24 | app.use(router);
25 | app.use(ModalPlugin);
26 |
27 | app.mount("#app");
28 |
--------------------------------------------------------------------------------
/src/mixin/chart.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Chart as ChartJS,
3 | Title,
4 | LinearScale,
5 | PointElement,
6 | LineElement,
7 | TimeScale,
8 | Legend,
9 | Tooltip,
10 | CategoryScale,
11 | } from "chart.js";
12 | import "chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm";
13 |
14 | ChartJS.register(
15 | Title,
16 | Legend,
17 | Tooltip,
18 | PointElement,
19 | LineElement,
20 | TimeScale,
21 | LinearScale,
22 | CategoryScale,
23 | );
24 |
--------------------------------------------------------------------------------
/src/mixin/filters.ts:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import relativeTime from "dayjs/plugin/relativeTime";
3 |
4 | dayjs.extend(relativeTime);
5 |
6 | export function timeAgo(time) {
7 | return dayjs(time).fromNow();
8 | }
9 |
10 | export function humanFileSize(bytes: number, si = false, dp = 1): string {
11 | const thresh = si ? 1000 : 1024;
12 |
13 | if (Math.abs(bytes) < thresh) {
14 | return bytes + " B";
15 | }
16 |
17 | const units = si
18 | ? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
19 | : ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
20 | let u = -1;
21 | const r = 10 ** dp;
22 |
23 | do {
24 | bytes /= thresh;
25 | ++u;
26 | } while (
27 | Math.round(Math.abs(bytes) * r) / r >= thresh &&
28 | u < units.length - 1
29 | );
30 |
31 | return bytes.toFixed(dp) + " " + units[u];
32 | }
33 |
--------------------------------------------------------------------------------
/src/mixin/icons.ts:
--------------------------------------------------------------------------------
1 | import { library } from "@fortawesome/fontawesome-svg-core";
2 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
3 |
4 | import {
5 | faCircleQuestion,
6 | faTriangleExclamation,
7 | faUpload,
8 | faCloudMoon,
9 | faCloudSun,
10 | faDownload,
11 | faFileExport,
12 | faSpinner,
13 | } from "@fortawesome/free-solid-svg-icons";
14 |
15 | import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
16 |
17 | library.add(
18 | faCircleQuestion,
19 | faTriangleExclamation,
20 | faUpload,
21 | faCloudMoon,
22 | faCloudSun,
23 | faDownload,
24 | faPenToSquare,
25 | faFileExport,
26 | faSpinner,
27 | );
28 |
29 | export default FontAwesomeIcon;
30 |
--------------------------------------------------------------------------------
/src/mixin/modal.ts:
--------------------------------------------------------------------------------
1 | import type { App } from "vue";
2 | import { shallowRef } from "vue";
3 | import { reactive } from "vue";
4 |
5 | declare module "vue" {
6 | interface ComponentCustomProperties {
7 | $modal: ModalService;
8 | }
9 | }
10 |
11 | class ModalService {
12 | private state = reactive({
13 | isShown: false,
14 | resolve: undefined as any,
15 | component: undefined,
16 | props: {},
17 | });
18 |
19 | public get isShown() {
20 | return this.state.isShown;
21 | }
22 |
23 | public get component() {
24 | return this.state.component;
25 | }
26 |
27 | public get props() {
28 | return this.state.props;
29 | }
30 |
31 | public show(component: any, props: any) {
32 | this.state.props = props || {};
33 | this.state.component = shallowRef(component);
34 | this.state.isShown = true;
35 | return new Promise((resolve) => {
36 | this.state.resolve = resolve;
37 | });
38 | }
39 |
40 | public close(data: any) {
41 | if (this.state.resolve) {
42 | this.state.resolve(data);
43 | }
44 | this.state.isShown = false;
45 | this.state.component = undefined;
46 | this.state.resolve = undefined;
47 | this.state.props = {};
48 | }
49 | }
50 |
51 | export const ModalPlugin = {
52 | install(app: App): void {
53 | app.config.globalProperties.$modal = new ModalService();
54 | },
55 | };
56 |
--------------------------------------------------------------------------------
/src/panel/AuxChannels.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
25 |
26 |
38 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
52 |
{{ v.text }}
53 |
54 |
62 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
137 |
--------------------------------------------------------------------------------
/src/panel/ESCSettings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
14 |
19 |
20 |
21 |
{{ m.label }}
22 |
23 |
24 | {{ trim(motor.settings[mapPin(m)].LAYOUT) }}
25 | -
26 | {{ trim(motor.settings[mapPin(m)].NAME) }},
27 | {{ motor.settings[mapPin(m)].MAIN_REVISION }}.{{
28 | motor.settings[mapPin(m)].SUB_REVISION
29 | }}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
49 | Settings not loaded
50 |
51 |
52 |
53 |
54 |
74 |
75 |
76 |
77 |
114 |
--------------------------------------------------------------------------------
/src/panel/GyroModel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 |
Model: TKS GT20 by Tarkusx
16 |
17 |
18 |
19 |
20 |
21 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/src/panel/Info.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | QUICKSILVER
11 | {{ appVersion }}
12 |
13 |
14 | Checkout our
15 |
20 | Docs
22 | for help on getting started.
23 |
24 |
New Version available!
25 |
26 | Update Now
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
66 |
67 |
75 |
--------------------------------------------------------------------------------
/src/panel/MotorPins.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
23 |
24 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
71 |
--------------------------------------------------------------------------------
/src/panel/MotorTest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Motor Test disabled
58 |
59 |
60 |
61 |
62 |
63 |
70 |
71 |
72 |
73 |
106 |
--------------------------------------------------------------------------------
/src/panel/ProfileMetadata.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
51 |
52 |
53 |
54 |
69 |
70 |
71 |
72 |
73 |
74 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/src/panel/RCChannels.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
27 |
28 |
29 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
51 |
52 |
53 | min
54 |
55 |
56 |
57 |
58 |
67 |
68 |
69 | max
70 |
71 |
72 |
73 |
74 |
75 |
76 | {{ Math.floor(state.rx_filtered[i] * (i != 3 ? 50 : 100)) }}
77 |
78 |
79 |
80 |
81 |
82 |
83 | Stick Calibration Wizard
84 | {{ wizardStates[state.stick_calibration_wizard] }}
85 | Continuing in {{ timerCount }}s..
86 |
87 |
88 |
92 | Calibrate
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
194 |
195 |
216 |
--------------------------------------------------------------------------------
/src/panel/Serial.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
28 |
29 |
30 |
31 |
32 |
36 |
37 |
49 |
50 |
51 |
52 |
53 |
57 |
58 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
110 |
--------------------------------------------------------------------------------
/src/panel/SerialPassthrough.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
51 |
52 |
53 |
54 |
111 |
--------------------------------------------------------------------------------
/src/panel/Target.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
86 |
87 |
88 |
89 |
90 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
175 |
176 |
181 |
--------------------------------------------------------------------------------
/src/panel/ThrottleSettings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
33 |
34 |
35 |
36 |
37 |
41 |
42 |
57 |
58 |
59 |
60 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
142 |
--------------------------------------------------------------------------------
/src/router.ts:
--------------------------------------------------------------------------------
1 | import { useSerialStore } from "./store/serial";
2 | import { createRouter, createWebHashHistory } from "vue-router";
3 |
4 | import Setup from "./views/Setup.vue";
5 | import Rates from "./views/Rates.vue";
6 | import Receiver from "./views/Receiver.vue";
7 | import OSD from "./views/OSD.vue";
8 | import Motor from "./views/Motor.vue";
9 | import Blackbox from "./views/Blackbox.vue";
10 | import State from "./views/State.vue";
11 | import Perf from "./views/Perf.vue";
12 | import Profile from "./views/Profile.vue";
13 | import Home from "./views/Home.vue";
14 | import Templates from "./views/Templates.vue";
15 |
16 | const router = createRouter({
17 | history: createWebHashHistory(import.meta.env.BASE_URL),
18 | routes: [
19 | {
20 | path: "/",
21 | redirect: () => {
22 | const serial = useSerialStore();
23 | if (serial.is_connected) {
24 | return "/profile";
25 | }
26 | return "/home";
27 | },
28 | },
29 | {
30 | path: "/home",
31 | name: "home",
32 | component: Home,
33 | },
34 | {
35 | path: "/templates",
36 | name: "templates",
37 | component: Templates,
38 | },
39 | {
40 | path: "/profile",
41 | name: "profile",
42 | component: Profile,
43 | },
44 | {
45 | path: "/setup",
46 | name: "setup",
47 | component: Setup,
48 | },
49 | {
50 | path: "/rates",
51 | name: "rates",
52 | component: Rates,
53 | },
54 | {
55 | path: "/receiver",
56 | name: "receiver",
57 | component: Receiver,
58 | },
59 | {
60 | path: "/osd",
61 | name: "osd",
62 | component: OSD,
63 | },
64 | {
65 | path: "/motor",
66 | name: "motor",
67 | component: Motor,
68 | },
69 | {
70 | path: "/blackbox",
71 | name: "blackbox",
72 | component: Blackbox,
73 | },
74 | {
75 | path: "/state",
76 | name: "state",
77 | component: State,
78 | },
79 | {
80 | path: "/perf",
81 | name: "perf",
82 | component: Perf,
83 | },
84 | ],
85 | });
86 |
87 | router.beforeEach((to, from, next) => {
88 | const serial = useSerialStore();
89 | if (serial.is_connected) {
90 | if (to.name === "home") {
91 | next({ name: "profile" });
92 | } else {
93 | next();
94 | }
95 | } else {
96 | if (to.name !== "home" && to.name !== "flash" && to.name !== "log") {
97 | next({ name: "home" });
98 | } else {
99 | next();
100 | }
101 | }
102 | });
103 |
104 | export default router;
105 |
--------------------------------------------------------------------------------
/src/store/bind.ts:
--------------------------------------------------------------------------------
1 | import { serial } from "./serial/serial";
2 | import { QuicVal } from "./serial/quic";
3 | import { Log } from "@/log";
4 | import { defineStore } from "pinia";
5 | import { useRootStore } from "./root";
6 |
7 | export const useBindStore = defineStore("bind", {
8 | state: () => ({
9 | info: {
10 | bind_saved: 0,
11 | raw: new Uint8Array(),
12 | },
13 | }),
14 | actions: {
15 | fetch_bind_info() {
16 | return serial.get(QuicVal.BindInfo).then((b) => (this.info = b));
17 | },
18 | apply_bind_info(info) {
19 | const root = useRootStore();
20 |
21 | return serial
22 | .set(QuicVal.BindInfo, info)
23 | .then((b) => (this.info = b))
24 | .then(() => root.set_needs_reboot())
25 | .then(() =>
26 | root.append_alert({ type: "success", msg: "Bind info applied!" }),
27 | )
28 | .catch((err) => {
29 | Log.error(err);
30 | root.append_alert({
31 | type: "danger",
32 | msg: "Apply failed! " + err,
33 | });
34 | });
35 | },
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/src/store/constants.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 | import semver from "semver";
3 | import { useInfoStore } from "./info";
4 | import { useProfileStore } from "./profile";
5 |
6 | enum Features {
7 | BRUSHLESS = 1 << 1,
8 | OSD = 1 << 2,
9 | BLACKBOX = 1 << 3,
10 | DEBUG = 1 << 4,
11 | }
12 |
13 | enum GyroRotation {
14 | ROTATE_NONE = 0x0,
15 | ROTATE_45_CCW = 0x1,
16 | ROTATE_45_CW = 0x2,
17 | ROTATE_90_CW = 0x4,
18 | ROTATE_90_CCW = 0x8,
19 | ROTATE_180 = 0x10,
20 | FLIP_180 = 0x20,
21 | }
22 |
23 | enum GyroType {
24 | INVALID,
25 |
26 | MPU6000,
27 | MPU6500,
28 |
29 | ICM20601,
30 | ICM20602,
31 | ICM20608,
32 | ICM20649,
33 | ICM20689,
34 |
35 | ICM42605,
36 | ICM42688P,
37 |
38 | BMI270,
39 | }
40 |
41 | enum GyroTypeV021 {
42 | INVALID,
43 |
44 | MPU6000,
45 | MPU6500,
46 |
47 | ICM20601,
48 | ICM20602,
49 | ICM20608,
50 | ICM20689,
51 |
52 | ICM42605,
53 | ICM42688P,
54 |
55 | BMI270,
56 | BMI323,
57 | }
58 |
59 | enum AuxChannels {
60 | CHANNEL_5,
61 | CHANNEL_6,
62 | CHANNEL_7,
63 | CHANNEL_8,
64 | CHANNEL_9,
65 | CHANNEL_10,
66 | CHANNEL_11,
67 | CHANNEL_12,
68 | CHANNEL_13,
69 | CHANNEL_14,
70 | CHANNEL_15,
71 | CHANNEL_16,
72 | OFF,
73 | ON,
74 | }
75 |
76 | enum AuxFunctionsV010 {
77 | AUX_ARMING,
78 | AUX_IDLE_UP,
79 | AUX_LEVELMODE,
80 | AUX_RACEMODE,
81 | AUX_HORIZON,
82 | AUX_STICK_BOOST_PROFILE,
83 | _AUX_RATE_PROFILE,
84 | AUX_BUZZER_ENABLE,
85 | AUX_TURTLE,
86 | AUX_MOTOR_TEST,
87 | AUX_RSSI,
88 | AUX_FPV_SWITCH,
89 | AUX_BLACKBOX,
90 | }
91 |
92 | enum AuxFunctionsV011 {
93 | AUX_ARMING,
94 | AUX_IDLE_UP,
95 | AUX_LEVELMODE,
96 | AUX_RACEMODE,
97 | AUX_HORIZON,
98 | AUX_STICK_BOOST_PROFILE,
99 | _AUX_RATE_PROFILE,
100 | AUX_BUZZER_ENABLE,
101 | AUX_TURTLE,
102 | AUX_MOTOR_TEST,
103 | AUX_RSSI,
104 | AUX_FPV_SWITCH,
105 | AUX_BLACKBOX,
106 | AUX_PREARM,
107 | }
108 |
109 | enum AuxFunctionsV025 {
110 | AUX_ARMING,
111 | AUX_IDLE_UP,
112 | AUX_LEVELMODE,
113 | AUX_RACEMODE,
114 | AUX_HORIZON,
115 | AUX_STICK_BOOST_PROFILE,
116 | _AUX_RATE_PROFILE,
117 | AUX_BUZZER_ENABLE,
118 | AUX_TURTLE,
119 | AUX_MOTOR_TEST,
120 | AUX_RSSI,
121 | AUX_FPV_SWITCH,
122 | AUX_BLACKBOX,
123 | AUX_PREARM,
124 | AUX_OSD_PROFILE,
125 | }
126 |
127 | enum RXProtocolV5 {
128 | INVALID,
129 | UNIFIED_SERIAL,
130 | SBUS,
131 | CRSF,
132 | IBUS,
133 | FPORT,
134 | DSMX_2048,
135 | DSM2_1024,
136 | NRF24_BAYANG_TELEMETRY,
137 | BAYANG_PROTOCOL_BLE_BEACON,
138 | BAYANG_PROTOCOL_TELEMETRY_AUTOBIND,
139 | FRSKY_D8,
140 | FRSKY_D16,
141 | REDPINE,
142 | EXPRESS_LRS,
143 | }
144 |
145 | enum RXProtocolV010 {
146 | INVALID,
147 | UNIFIED_SERIAL,
148 | SBUS,
149 | CRSF,
150 | IBUS,
151 | FPORT,
152 | DSM,
153 | NRF24_BAYANG_TELEMETRY,
154 | BAYANG_PROTOCOL_BLE_BEACON,
155 | BAYANG_PROTOCOL_TELEMETRY_AUTOBIND,
156 | FRSKY_D8,
157 | FRSKY_D16,
158 | REDPINE,
159 | EXPRESS_LRS,
160 | }
161 |
162 | enum RXProtocolV011 {
163 | INVALID,
164 | UNIFIED_SERIAL,
165 | SBUS,
166 | CRSF,
167 | IBUS,
168 | FPORT,
169 | DSM,
170 | NRF24_BAYANG_TELEMETRY,
171 | BAYANG_PROTOCOL_BLE_BEACON,
172 | BAYANG_PROTOCOL_TELEMETRY_AUTOBIND,
173 | FRSKY_D8,
174 | FRSKY_D16_FCC,
175 | FRSKY_D16_LBT,
176 | REDPINE,
177 | EXPRESS_LRS,
178 | FLYSKY_AFHDS,
179 | FLYSKY_AFHDS2A,
180 | }
181 |
182 | enum RXSerialProtocol {
183 | INVALID,
184 | DSM,
185 | SBUS,
186 | IBUS,
187 | FPORT,
188 | CRSF,
189 | REDPINE,
190 | SBUS_INVERTED,
191 | FPORT_INVERTED,
192 | REDPINE_INVERTED,
193 | }
194 |
195 | export enum StickWizardState {
196 | STICK_WIZARD_INACTIVE,
197 | STICK_WIZARD_SUCCESS,
198 | STICK_WIZARD_FAILED,
199 | STICK_WIZARD_START,
200 | STICK_WIZARD_CAPTURE_STICKS,
201 | STICK_WIZARD_WAIT_FOR_CONFIRM,
202 | STICK_WIZARD_CONFIRMED,
203 | STICK_WIZARD_TIMEOUT,
204 | }
205 |
206 | enum Failloop {
207 | FAILLOOP_NONE = 0,
208 | FAILLOOP_LOW_BATTERY = 2, // - low battery at powerup - currently unused
209 | FAILLOOP_RADIO = 3, // - radio chip not found
210 | FAILLOOP_GYRO = 4, // - gyro not found
211 | FAILLOOP_FAULT = 5, // - clock, intterrupts, systick, gcc bad code, bad memory access (code issues like bad pointers) - this should not come up
212 | FAILLOOP_LOOPTIME = 6, // - loop time issue - if loop time exceeds 20mS
213 | FAILLOOP_SPI = 7, // - spi error - triggered by hardware spi driver only
214 | FAILLOOP_SPI_MAIN = 8, // - spi error main loop - triggered by hardware spi driver only
215 | }
216 |
217 | // These should align with 'blackbox_t' and 'blackbox_field_t' in Quicksilver source 'blackbox.h'
218 | export enum BlackboxField {
219 | LOOP,
220 | TIME,
221 | PID_P_TERM,
222 | PID_I_TERM,
223 | PID_D_TERM,
224 | RX,
225 | SETPOINT,
226 | ACCEL_RAW,
227 | ACCEL_FILTER,
228 | GYRO_RAW,
229 | GYRO_FILTER,
230 | MOTOR,
231 | CPU_LOAD,
232 | DEBUG,
233 | }
234 |
235 | export enum LQISource {
236 | PACKET_RATE,
237 | CHANNEL,
238 | DIRECT,
239 | }
240 |
241 | export const FailloopMessages = {
242 | [Failloop.FAILLOOP_NONE]: "",
243 | [Failloop.FAILLOOP_LOW_BATTERY]: "low battery at powerup - currently unused",
244 | [Failloop.FAILLOOP_RADIO]: "radio chip not found",
245 | [Failloop.FAILLOOP_GYRO]: "gyro not found",
246 | [Failloop.FAILLOOP_FAULT]:
247 | "clock, intterrupts, systick, gcc bad code, bad memory access (code issues like bad pointers) - this should not come up",
248 | [Failloop.FAILLOOP_LOOPTIME]: "loop time issue - if loop time exceeds 20mS",
249 | [Failloop.FAILLOOP_SPI]: "spi error - triggered by hardware spi driver only",
250 | [Failloop.FAILLOOP_SPI_MAIN]:
251 | "spi error main loop - triggered by hardware spi driver only",
252 | };
253 |
254 | export const useConstantStore = defineStore("constant", {
255 | state: () => ({
256 | Features,
257 | GyroRotation,
258 | AuxChannels,
259 | RXSerialProtocol,
260 | StickWizardState,
261 | Failloop,
262 | LQISource,
263 | }),
264 | getters: {
265 | RXProtocol() {
266 | const info = useInfoStore();
267 |
268 | if (semver.gt(info.quic_protocol_semver, "0.1.0")) {
269 | return RXProtocolV011;
270 | }
271 | if (info.quic_protocol_version > 5) {
272 | return RXProtocolV010;
273 | }
274 | return RXProtocolV5;
275 | },
276 | AuxFunctions() {
277 | const profile = useProfileStore();
278 | if (profile.profileVersionGt("0.2.4")) {
279 | return AuxFunctionsV025;
280 | }
281 | const info = useInfoStore();
282 | if (semver.gt(info.quic_protocol_semver, "0.1.0")) {
283 | return AuxFunctionsV011;
284 | }
285 | return AuxFunctionsV010;
286 | },
287 | GyroType() {
288 | const info = useInfoStore();
289 |
290 | if (semver.gt(info.quic_protocol_semver, "0.2.0")) {
291 | return GyroTypeV021;
292 | }
293 | return GyroType;
294 | },
295 | },
296 | actions: {},
297 | });
298 |
--------------------------------------------------------------------------------
/src/store/default_profile.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 | import semver from "semver";
3 | import { QuicVal } from "./serial/quic";
4 | import { serial } from "./serial/serial";
5 | import { decodeSemver } from "./util";
6 |
7 | export const useDefaultProfileStore = defineStore("default_profile", {
8 | state: () => ({
9 | serial: {
10 | rx: 0,
11 | smart_audio: 0,
12 | hdzero: 0,
13 | },
14 | filter: {
15 | gyro: [{}, {}],
16 | dterm: [{}, {}],
17 | },
18 | osd: {
19 | callsign: "",
20 | elements: [],
21 | elements_hd: [],
22 | },
23 | meta: {
24 | version: 0,
25 | datetime: 0,
26 | },
27 | motor: {
28 | invert_yaw: 1,
29 | },
30 | rate: {
31 | mode: 0,
32 | silverware: {},
33 | betaflight: {},
34 | },
35 | voltage: {},
36 | receiver: {
37 | lqi_source: -1,
38 | aux: [],
39 | },
40 | pid: {
41 | pid_profile: 0,
42 | pid_rates: [{}],
43 | stick_profile: 0,
44 | stick_rates: [{}],
45 | big_angle: {},
46 | small_angle: {},
47 | throttle_dterm_attenuation: {},
48 | },
49 | }),
50 | getters: {
51 | has_legacy_stickrates(state) {
52 | return semver.lte(decodeSemver(state.meta.version), "v0.1.0");
53 | },
54 | has_legacy_osd(state) {
55 | return semver.lte(decodeSemver(state.meta.version), "v0.2.4");
56 | },
57 | },
58 | actions: {
59 | fetch_default_profile() {
60 | return serial.get(QuicVal.DefaultProfile).then((profile) => {
61 | profile.meta.name = profile.meta.name.replace(/\0/g, "");
62 | this.$patch(profile);
63 | });
64 | },
65 | },
66 | });
67 |
--------------------------------------------------------------------------------
/src/store/flash.ts:
--------------------------------------------------------------------------------
1 | import YAML from "yaml";
2 | import { defineStore } from "pinia";
3 | import { github } from "./util/github";
4 | import { CBOR } from "./serial/cbor";
5 |
6 | export interface RuntimeTarget {
7 | name: string;
8 | target: string;
9 | manufacturer: string;
10 | mcu: string;
11 | }
12 |
13 | export const TARGET_URL =
14 | "https://raw.githubusercontent.com/BossHobby/Targets/targets/";
15 |
16 | export const useFlashStore = defineStore("flash", {
17 | state: () => ({
18 | releases: {},
19 | branches: {},
20 | pullRequests: {},
21 | targets: [] as RuntimeTarget[],
22 | manufacturers: {},
23 | }),
24 | actions: {
25 | fetchTargets() {
26 | return fetch(TARGET_URL + "_index.json")
27 | .then((res) => res.json())
28 | .then((res) => {
29 | this.targets = res.targets;
30 | this.manufacturers = res.manufacturers;
31 | });
32 | },
33 | fetchReleases() {
34 | return github.fetchReleases().then((r) => (this.releases = r));
35 | },
36 | fetchBranches() {
37 | return github.fetchBranches().then((b) => (this.branches = b));
38 | },
39 | fetchPullRequests() {
40 | return github.fetchPullRequests().then((p) => (this.pullRequests = p));
41 | },
42 | async fetch(source: string) {
43 | if (this.targets.length == 0) {
44 | await this.fetchTargets();
45 | }
46 | switch (source) {
47 | case "release":
48 | return this.fetchReleases();
49 | case "branch":
50 | return this.fetchBranches();
51 | case "pull_request":
52 | return this.fetchPullRequests();
53 | default:
54 | break;
55 | }
56 | },
57 | fetchRuntimeConfig(target: string) {
58 | return fetch(TARGET_URL + target + ".yaml")
59 | .then((res) => {
60 | if (!res.ok) {
61 | return Promise.reject(res);
62 | }
63 | return res.text();
64 | })
65 | .then((res) => YAML.parse(res))
66 | .then((target) => CBOR.encode(target));
67 | },
68 | },
69 | });
70 |
--------------------------------------------------------------------------------
/src/store/flash/flash.ts:
--------------------------------------------------------------------------------
1 | import { DFU } from "./dfu";
2 | import { IntelHEX } from "./ihex";
3 |
4 | const USB_DEVICE_FILTER: USBDeviceFilter[] = [
5 | { vendorId: 0x0483, productId: 0xdf11 },
6 | { vendorId: 0x2e3c, productId: 0xdf11 },
7 | ];
8 |
9 | export interface FlashProgress {
10 | task: string;
11 | current: number;
12 | total: number;
13 | }
14 |
15 | export type FlashProgressCallback = (p: FlashProgress) => void;
16 |
17 | export class Flasher {
18 | private device?: USBDevice;
19 | private progressCallback?: FlashProgressCallback;
20 |
21 | public onProgress(cb: FlashProgressCallback) {
22 | this.progressCallback = cb;
23 | }
24 |
25 | public async connect() {
26 | this.device = await navigator.usb.requestDevice({
27 | filters: USB_DEVICE_FILTER,
28 | });
29 | }
30 |
31 | public async flash(hex: IntelHEX) {
32 | if (!this.device) {
33 | return;
34 | }
35 |
36 | const dfu = new DFU(this.device);
37 | dfu.onProgress((p) => {
38 | if (this.progressCallback) {
39 | this.progressCallback(p);
40 | }
41 | });
42 |
43 | await dfu.open();
44 | await dfu.flash(hex);
45 | await dfu.close();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/store/flash/ihex.ts:
--------------------------------------------------------------------------------
1 | import { concatUint8Array } from "../util";
2 |
3 | enum Record {
4 | DATA = 0,
5 | END_OF_FILE = 1,
6 | EXT_SEGMENT_ADDR = 2,
7 | START_SEGMENT_ADDR = 3,
8 | EXT_LINEAR_ADDR = 4,
9 | START_LINEAR_ADDR = 5,
10 | }
11 |
12 | const CONFIG_MAGIC = new Uint8Array([0x01, 0x00, 0xaa, 0x12]);
13 |
14 | export const ConfigOffsets = {
15 | stm32f411: 0x0800c000,
16 | stm32f765: 0x08018000,
17 | stm32f745: 0x08018000,
18 | stm32h743: 0x08020000,
19 | stm32f405: 0x0800c000,
20 | stm32g473: 0x0800c000,
21 | stm32f722: 0x0800c000,
22 | at32f435: 0x080f0000,
23 | at32f435m: 0x080f0000,
24 | };
25 |
26 | export class IntelHEX {
27 | public start_linear_address: number;
28 | public start_segment_address: number;
29 | public segments: { address: number; data: Uint8Array }[] = [];
30 |
31 | public get linear_bytes_total() {
32 | return this.end_address - this.start_address;
33 | }
34 |
35 | public get segment_bytes_total() {
36 | return this.segments.reduce((p, c) => (p += c.data.length), 0);
37 | }
38 |
39 | public get start_address() {
40 | return this.segments[0].address;
41 | }
42 |
43 | public get end_address() {
44 | return (
45 | this.segments[this.segments.length - 1].address +
46 | this.segments[this.segments.length - 1].data.length
47 | );
48 | }
49 |
50 | constructor(start_linear_address: number, start_segment_address: number) {
51 | this.start_linear_address = start_linear_address;
52 | this.start_segment_address = start_segment_address;
53 | }
54 |
55 | private findSegment(offset: number) {
56 | for (let i = 0; i < this.segments.length; i++) {
57 | if (
58 | this.segments[i].address >= offset &&
59 | offset <= this.segments[i].address + this.segments[i].data.byteLength
60 | ) {
61 | return this.segments[i];
62 | }
63 | }
64 |
65 | this.segments.push({ address: offset, data: new Uint8Array() });
66 | return this.segments[this.segments.length - 1];
67 | }
68 |
69 | public patch(offset: number, data: Uint8Array) {
70 | data = concatUint8Array(CONFIG_MAGIC, data);
71 |
72 | const seg = this.findSegment(offset);
73 |
74 | const segOffset = seg.address - offset;
75 | const segSize = segOffset + data.byteLength;
76 | if (seg.data.byteLength < segSize) {
77 | seg.data = concatUint8Array(
78 | seg.data,
79 | new Uint8Array(segSize - seg.data.byteLength),
80 | );
81 | }
82 |
83 | for (let i = segOffset; i < segSize; i++) {
84 | seg.data[i] = data[i - segOffset];
85 | }
86 | }
87 |
88 | public static parse(data: string): IntelHEX {
89 | let eofReached = false;
90 | let highAddr = 0;
91 | let lastAddr = 0;
92 |
93 | const result = new IntelHEX(0, 0);
94 |
95 | const lines = data.split(/\r?\n/);
96 | for (const line of lines) {
97 | if (line.length == 0 || line == "") {
98 | continue;
99 | }
100 |
101 | const byteCount = parseInt(line.substr(1, 2), 16);
102 | const address = parseInt(line.substr(3, 4), 16);
103 | const recordType = parseInt(line.substr(7, 2), 16);
104 | const dataStr = line.substr(9, byteCount * 2);
105 | const dataBuffer = IntelHEX.fromHex(dataStr);
106 | const checksum = parseInt(line.substr(9 + byteCount * 2, 2), 16);
107 |
108 | let calcChecksum =
109 | (byteCount + (address >> 8) + address + recordType) & 0xff;
110 | for (let i = 0; i < byteCount; i++)
111 | calcChecksum = (calcChecksum + dataBuffer[i]) & 0xff;
112 | calcChecksum = (0x100 - calcChecksum) & 0xff;
113 |
114 | if (checksum != calcChecksum) {
115 | throw new Error("invalid checksum");
116 | }
117 |
118 | switch (recordType) {
119 | // data record
120 | case Record.DATA: {
121 | const absoluteAddress = highAddr + address;
122 |
123 | if (lastAddr == 0 || absoluteAddress != lastAddr) {
124 | result.segments.push({
125 | address: absoluteAddress,
126 | data: new Uint8Array(),
127 | });
128 | }
129 |
130 | lastAddr = absoluteAddress + byteCount;
131 | result.segments[result.segments.length - 1].data = concatUint8Array(
132 | result.segments[result.segments.length - 1].data,
133 | dataBuffer,
134 | );
135 |
136 | break;
137 | }
138 |
139 | case Record.END_OF_FILE:
140 | if (byteCount != 0) {
141 | throw new Error("invalid END_OF_FILE");
142 | }
143 | eofReached = true;
144 | break;
145 |
146 | case Record.EXT_SEGMENT_ADDR:
147 | if (byteCount != 2 || address != 0) {
148 | throw new Error("invalid EXT_SEGMENT_ADDR");
149 | }
150 | highAddr = parseInt(dataStr, 16) << 4;
151 | break;
152 |
153 | case Record.START_SEGMENT_ADDR:
154 | if (byteCount != 4 || address != 0) {
155 | throw new Error("invalid START_SEGMENT_ADDR");
156 | }
157 | result.start_segment_address = parseInt(dataStr, 16);
158 | break;
159 |
160 | case Record.EXT_LINEAR_ADDR:
161 | if (byteCount != 2 || address != 0) {
162 | throw new Error("invalid EXT_SEGMENT_ADDR");
163 | }
164 | highAddr = parseInt(dataStr, 16) << 16;
165 | break;
166 |
167 | case Record.START_LINEAR_ADDR:
168 | if (byteCount != 4 || address != 0) {
169 | throw new Error("invalid START_LINEAR_ADDR");
170 | }
171 | result.start_linear_address = parseInt(dataStr, 16);
172 | break;
173 | }
174 | }
175 |
176 | if (!eofReached) {
177 | throw new Error("no END_OF_FILE record");
178 | }
179 |
180 | return result;
181 | }
182 |
183 | private static fromHex(str: string) {
184 | const buffer = new Uint8Array(Math.ceil(str.length / 2));
185 | for (let i = 0; i < buffer.length; i++) {
186 | buffer[i] = parseInt(str.substr(i * 2, 2), 16);
187 | }
188 | return buffer;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { createPinia } from "pinia";
2 | import { useRootStore } from "./root";
3 |
4 | const pinia = createPinia();
5 |
6 | const applyNeededStores = ["profile"];
7 |
8 | pinia.use(({ store }) => {
9 | if (!applyNeededStores.includes(store.$id)) {
10 | return;
11 | }
12 |
13 | store.$subscribe((mutation) => {
14 | if (mutation.type != "direct") {
15 | return;
16 | }
17 | const root = useRootStore();
18 | root.set_needs_apply();
19 | });
20 | });
21 |
22 | export default pinia;
23 |
--------------------------------------------------------------------------------
/src/store/info.ts:
--------------------------------------------------------------------------------
1 | import { useStateStore } from "./state";
2 | import semver from "semver";
3 | import { decodeSemver } from "@/store/util";
4 | import { defineStore } from "pinia";
5 | import type { target_info_t } from "./types";
6 | import { $enum } from "ts-enum-util";
7 | import { useConstantStore } from "./constants";
8 | import { useDefaultProfileStore } from "./default_profile";
9 |
10 | export interface local_target_info_t extends target_info_t {
11 | quic_protocol_semver: string;
12 | rx_protocol?: number;
13 | gyro_name: string;
14 | }
15 |
16 | export const useInfoStore = defineStore("info", {
17 | state: (): local_target_info_t => ({
18 | usart_ports: [],
19 | motor_pins: [],
20 |
21 | mcu: "",
22 | target_name: "",
23 | git_version: "",
24 |
25 | quic_protocol_version: 0,
26 | quic_protocol_semver: "v0.0.0",
27 |
28 | gyro_id: 0,
29 | gyro_name: "",
30 | rx_protocol: 0,
31 | rx_protocols: [],
32 | features: 0,
33 | }),
34 | getters: {
35 | has_feature(state) {
36 | return (feature) => {
37 | if (state.features == null) {
38 | return true;
39 | }
40 | return state.features & feature;
41 | };
42 | },
43 | quic_semver_gt(state) {
44 | return (version) => {
45 | return semver.gt(state.quic_protocol_semver, version);
46 | };
47 | },
48 | quic_semver_gte(state) {
49 | return (version) => {
50 | return semver.gte(state.quic_protocol_semver, version);
51 | };
52 | },
53 | version_too_old(state) {
54 | const default_profile = useDefaultProfileStore();
55 | return (
56 | state.quic_protocol_version < 5 ||
57 | semver.lte(decodeSemver(default_profile.meta.version), "v0.1.0")
58 | ); // unsupported osd
59 | },
60 | is_read_only(state) {
61 | const fwstate = useStateStore();
62 | return this.version_too_old || fwstate.failloop > 0;
63 | },
64 | },
65 | actions: {
66 | set_info(info) {
67 | this.$patch(info);
68 | if (this.quic_protocol_version) {
69 | this.quic_protocol_semver = decodeSemver(this.quic_protocol_version);
70 | }
71 |
72 | const constants = useConstantStore();
73 | this.gyro_name = $enum(constants.GyroType).getKeys()[this.gyro_id];
74 | },
75 | },
76 | });
77 |
--------------------------------------------------------------------------------
/src/store/motor.ts:
--------------------------------------------------------------------------------
1 | import { QuicCmd, QuicMotor, QuicVal } from "./serial/quic";
2 | import { serial } from "./serial/serial";
3 | import { Log } from "@/log";
4 | import { defineStore } from "pinia";
5 | import { useRootStore } from "./root";
6 | import { useProfileStore } from "./profile";
7 |
8 | export const useMotorStore = defineStore("motor", {
9 | state: () => ({
10 | loading: false,
11 | test: {
12 | active: 0,
13 | value: new Array(),
14 | },
15 | settings: null as any,
16 | _pins: [
17 | {
18 | index: 1,
19 | id: "MOTOR_FL",
20 | label: "Front Left",
21 | },
22 | {
23 | index: 3,
24 | id: "MOTOR_FR",
25 | label: "Front Right",
26 | },
27 | {
28 | index: 0,
29 | id: "MOTOR_BL",
30 | label: "Back Left",
31 | },
32 | {
33 | index: 2,
34 | id: "MOTOR_BR",
35 | label: "Back Right",
36 | },
37 | ],
38 | }),
39 | getters: {
40 | pins(state) {
41 | const profile = useProfileStore();
42 | return state._pins.map((p) => {
43 | return {
44 | ...p,
45 | pin: profile.motor.motor_pins[p.index],
46 | };
47 | });
48 | },
49 | },
50 | actions: {
51 | fetch_motor_test() {
52 | return serial.command(QuicCmd.Motor, QuicMotor.TestStatus).then((p) => {
53 | this.test = p.payload[0];
54 | });
55 | },
56 | fetch_motor_settings() {
57 | const root = useRootStore();
58 | this.loading = true;
59 |
60 | return serial
61 | .get(QuicVal.BLHeliSettings)
62 | .then((settings) => {
63 | this.settings = settings;
64 | })
65 | .catch((err) => {
66 | root.append_alert({
67 | type: "danger",
68 | msg: "Loading motor settings failed!",
69 | });
70 | Log.error("motor", err);
71 | })
72 | .finally(() => {
73 | this.loading = false;
74 | });
75 | },
76 | apply_motor_settings(settings) {
77 | const root = useRootStore();
78 | this.loading = true;
79 |
80 | return serial
81 | .set(QuicVal.BLHeliSettings, ...settings)
82 | .then(() => {
83 | this.settings = settings;
84 | root.append_alert({
85 | type: "success",
86 | msg: "Motor settings applied!",
87 | });
88 | })
89 | .catch((err) => {
90 | Log.error("motor", err);
91 | root.append_alert({
92 | type: "danger",
93 | msg: "Failed to apply motor settings!",
94 | });
95 | })
96 | .finally(() => {
97 | this.loading = false;
98 | });
99 | },
100 | async motor_test_toggle() {
101 | await this.fetch_motor_test();
102 | return serial
103 | .command(
104 | QuicCmd.Motor,
105 | this.test.active ? QuicMotor.TestDisable : QuicMotor.TestEnable,
106 | )
107 | .then(() => {
108 | this.test.active = this.test.active ? 0 : 1;
109 | });
110 | },
111 | motor_test_set_value(value) {
112 | return serial
113 | .command(QuicCmd.Motor, QuicMotor.TestSetValue, value)
114 | .then((p) => {
115 | this.test.value = p.payload[0];
116 | });
117 | },
118 | },
119 | });
120 |
--------------------------------------------------------------------------------
/src/store/osd.ts:
--------------------------------------------------------------------------------
1 | import { serial } from "./serial/serial";
2 | import { QuicCmd, QuicOSD, QuicVal } from "./serial/quic";
3 | import { defineStore } from "pinia";
4 | import { OSD } from "./util/osd";
5 | import { useInfoStore } from "./info";
6 |
7 | export const useOSDStore = defineStore("osd", {
8 | state: () => ({
9 | font_raw: undefined as number[][] | undefined,
10 | font_bitmap: undefined as ImageBitmap | undefined,
11 | font_bitmap_inverted: undefined as ImageBitmap | undefined,
12 | }),
13 | actions: {
14 | async fetch_sd_osd_font() {
15 | const info = useInfoStore();
16 | if (!info.quic_semver_gte("0.2.0")) {
17 | return serial.get(QuicVal.OSDFont).then((font) => {
18 | this.font_raw = font;
19 | this.font_bitmap = OSD.unpackFontBitmap(font);
20 | this.font_bitmap_inverted = OSD.unpackFontBitmap(font, true);
21 | });
22 | }
23 |
24 | const font: number[][] = [];
25 | for (let i = 0; i < 256; i++) {
26 | const res = await serial.command(QuicCmd.OSD, QuicOSD.ReadChar, i);
27 | font[i] = res.payload[0];
28 | }
29 | this.font_raw = font;
30 | this.font_bitmap = OSD.unpackFontBitmap(font);
31 | this.font_bitmap_inverted = OSD.unpackFontBitmap(font, true);
32 | },
33 | async apply_font(font: Uint8Array[]) {
34 | const info = useInfoStore();
35 | if (!info.quic_semver_gte("0.2.0")) {
36 | return serial.set(QuicVal.OSDFont, ...font);
37 | }
38 |
39 | for (let i = 0; i < 256; i++) {
40 | await serial.command(QuicCmd.OSD, QuicOSD.WriteChar, i, font[i]);
41 | }
42 | },
43 | fetch_hd_osd_font() {
44 | return new Promise((resolve, reject) => {
45 | const image = new Image();
46 | image.onload = () => {
47 | resolve(image);
48 | };
49 | image.src = "osd/hdzero_quic.png";
50 | })
51 | .then((img: any) => createImageBitmap(img))
52 | .then((img) => {
53 | this.font_raw = undefined;
54 | this.font_bitmap = img;
55 | this.font_bitmap_inverted = undefined;
56 | });
57 | },
58 | },
59 | });
60 |
--------------------------------------------------------------------------------
/src/store/perf.ts:
--------------------------------------------------------------------------------
1 | import { serial } from "./serial/serial";
2 | import { QuicVal } from "./serial/quic";
3 | import { defineStore } from "pinia";
4 |
5 | export const usePerfStore = defineStore("perf", {
6 | state: () => ({
7 | counters: [] as any[],
8 | }),
9 | actions: {
10 | fetch_perf_counters() {
11 | return serial
12 | .get(QuicVal.PerfCounters)
13 | .then((update) => (this.counters = update));
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/src/store/root.ts:
--------------------------------------------------------------------------------
1 | import type { pid_rate_preset_t } from "./types";
2 | import { defineStore } from "pinia";
3 | import { serial } from "./serial/serial";
4 | import { QuicCmd, QuicVal } from "./serial/quic";
5 |
6 | export const useRootStore = defineStore("root", {
7 | state: () => ({
8 | needs_apply: false,
9 | needs_reboot: false,
10 |
11 | alerts: [] as any[],
12 |
13 | pid_rate_presets: [] as pid_rate_preset_t[],
14 | }),
15 | actions: {
16 | append_alert(alert) {
17 | this.alerts = [
18 | ...this.alerts,
19 | {
20 | id: Date.now().toString() + "-" + this.alerts.length.toString(),
21 | ...alert,
22 | },
23 | ];
24 | },
25 | pop_alert(id) {
26 | this.alerts = [...this.alerts.filter((a) => a.id != id)];
27 | },
28 |
29 | set_needs_apply() {
30 | this.needs_apply = true;
31 | },
32 | set_needs_reboot() {
33 | this.needs_apply = true;
34 | this.needs_reboot = true;
35 | },
36 |
37 | reset_needs_apply() {
38 | this.needs_apply = false;
39 | },
40 | reset_needs_reboot() {
41 | this.needs_apply = false;
42 | this.needs_reboot = false;
43 | },
44 |
45 | fetch_pid_rate_presets() {
46 | return serial
47 | .get(QuicVal.PidRatePresets)
48 | .then((p) => (this.pid_rate_presets = p));
49 | },
50 | cal_imu() {
51 | return serial.command(QuicCmd.CalImu);
52 | },
53 | cal_sticks() {
54 | return serial.command(QuicCmd.CalSticks);
55 | },
56 | },
57 | });
58 |
--------------------------------------------------------------------------------
/src/store/serial.ts:
--------------------------------------------------------------------------------
1 | import { useVTXStore } from "./vtx";
2 | import { useProfileStore } from "./profile";
3 | import { useDefaultProfileStore } from "./default_profile";
4 | import { usePerfStore } from "./perf";
5 | import { useBindStore } from "./bind";
6 | import { Log } from "@/log";
7 | import router from "@/router";
8 | import { defineStore } from "pinia";
9 | import { useRootStore } from "./root";
10 | import { QuicCmd } from "./serial/quic";
11 | import { serial } from "./serial/serial";
12 | import { settings } from "./serial/settings";
13 | import { useInfoStore } from "./info";
14 | import { useMotorStore } from "./motor";
15 | import { useStateStore } from "./state";
16 | import { useBlackboxStore } from "./blackbox";
17 | import { useTargetStore } from "./target";
18 | import { asyncDelay } from "./util";
19 | import { WebSerial } from "./serial/webserial";
20 |
21 | let interval: any = null;
22 | let intervalCounter = 0;
23 |
24 | function stopInterval() {
25 | clearInterval(interval);
26 | interval = null;
27 | intervalCounter = 0;
28 | }
29 |
30 | function startInterval(fn: any) {
31 | stopInterval();
32 |
33 | interval = setInterval(async () => {
34 | await fn(intervalCounter);
35 | intervalCounter++;
36 | }, settings.serial.updateInterval);
37 | }
38 |
39 | export const useSerialStore = defineStore("serial", {
40 | state: () => ({
41 | is_connected: false,
42 | is_connecting: false,
43 | }),
44 | actions: {
45 | async poll_serial(counter: number) {
46 | if (!this.is_connected) {
47 | return;
48 | }
49 |
50 | const bind = useBindStore();
51 | const perf = usePerfStore();
52 | const state = useStateStore();
53 | const vtx = useVTXStore();
54 |
55 | await state.fetch_state();
56 | if (counter % 4) {
57 | if (router.currentRoute.value.fullPath == "/receiver") {
58 | await bind.fetch_bind_info();
59 | }
60 | if (router.currentRoute.value.fullPath == "/perf") {
61 | await perf.fetch_perf_counters();
62 | }
63 | if (router.currentRoute.value.fullPath == "/setup") {
64 | await vtx.update_vtx_settings();
65 | }
66 | }
67 | },
68 | async soft_reboot() {
69 | await this.disconnect();
70 |
71 | this.is_connecting = true;
72 | await serial.softReboot();
73 | for (let i = 0; i < 10; i++) {
74 | const ports = await WebSerial.getPorts();
75 | if (!ports.length) {
76 | break;
77 | }
78 | await asyncDelay(100);
79 | }
80 | for (let i = 0; i < 10; i++) {
81 | const ports = await WebSerial.getPorts();
82 | if (ports.length) {
83 | break;
84 | }
85 | await asyncDelay(100);
86 | }
87 |
88 | await this.connect(
89 | serial.connectFirstPort((err) => {
90 | Log.error("serial", err);
91 | this.disconnect();
92 | return serial.close();
93 | }),
94 | );
95 | },
96 | serial_passthrough({ port, baudrate, half_duplex, stop_bits }) {
97 | const root = useRootStore();
98 |
99 | return serial
100 | .command(
101 | QuicCmd.Serial,
102 | 0,
103 | port,
104 | baudrate,
105 | half_duplex ? 1 : 0,
106 | stop_bits,
107 | )
108 | .then(() => serial.close())
109 | .then(() => this.toggle_connection())
110 | .then(() => {
111 | root.append_alert({
112 | type: "success",
113 | msg: "Serial passthrough successful!",
114 | });
115 | })
116 | .catch((err) => {
117 | Log.error("serial", err);
118 | root.append_alert({
119 | type: "danger",
120 | msg: "Serial passthrough failed",
121 | });
122 | });
123 | },
124 | hard_reboot() {
125 | const root = useRootStore();
126 |
127 | return serial
128 | .hardReboot()
129 | .then((target) => {
130 | root.append_alert({
131 | type: "success",
132 | msg: "Reset to bootloader successful!",
133 | });
134 | return target;
135 | })
136 | .catch((err) => {
137 | Log.error("serial", err);
138 | root.append_alert({
139 | type: "danger",
140 | msg: "Reset to bootloader failed",
141 | });
142 | return undefined;
143 | });
144 | },
145 | disconnect() {
146 | if (!this.is_connected) return;
147 |
148 | this.is_connected = false;
149 | this.is_connecting = false;
150 |
151 | stopInterval();
152 |
153 | const root = useRootStore();
154 | root.reset_needs_reboot();
155 |
156 | if (router.currentRoute.value.fullPath != "/home") {
157 | router.push("/home");
158 | }
159 | },
160 | async connect(infoPromise: Promise) {
161 | const bb = useBlackboxStore();
162 | const default_profile = useDefaultProfileStore();
163 | const info = useInfoStore();
164 | const motor = useMotorStore();
165 | const profile = useProfileStore();
166 | const root = useRootStore();
167 | const target = useTargetStore();
168 | const vtx = useVTXStore();
169 |
170 | try {
171 | const i = await infoPromise;
172 |
173 | info.$reset();
174 | motor.$reset();
175 | vtx.$reset();
176 | bb.$reset();
177 | default_profile.$reset();
178 | profile.$reset();
179 | target.$reset();
180 |
181 | this.is_connected = true;
182 | info.set_info(i);
183 |
184 | if (info.quic_semver_gte("0.2.0")) {
185 | target.fetch();
186 | }
187 |
188 | default_profile.fetch_default_profile();
189 | root.fetch_pid_rate_presets();
190 | profile.fetch_profile();
191 | vtx.update_vtx_settings();
192 |
193 | startInterval((c) => this.poll_serial(c));
194 |
195 | if (router.currentRoute.value.fullPath != "/profile") {
196 | router.push("/profile");
197 | }
198 | } catch (err) {
199 | Log.error("serial", err);
200 | this.is_connected = false;
201 | root.reset_needs_reboot();
202 | root.append_alert({
203 | type: "danger",
204 | msg: "Connection to the board failed",
205 | });
206 | } finally {
207 | this.is_connecting = false;
208 | }
209 | },
210 | async toggle_connection() {
211 | if (this.is_connected) {
212 | this.disconnect();
213 | return serial.close();
214 | }
215 |
216 | this.is_connecting = true;
217 | return this.connect(
218 | serial.connect((err) => {
219 | Log.error("serial", err);
220 | this.disconnect();
221 | return serial.close();
222 | }),
223 | );
224 | },
225 | },
226 | });
227 |
--------------------------------------------------------------------------------
/src/store/serial/quic.ts:
--------------------------------------------------------------------------------
1 | export const QUIC_MAGIC = "#".charCodeAt(0);
2 | export const QUIC_HEADER_LEN = 4;
3 |
4 | export enum QuicCmd {
5 | Invalid,
6 | Get,
7 | Set,
8 | Log,
9 | CalImu,
10 | Blackbox,
11 | Motor,
12 | CalSticks,
13 | Serial,
14 | OSD,
15 | Max,
16 | }
17 |
18 | export enum QuicVal {
19 | Invalid,
20 | Info,
21 | Profile,
22 | DefaultProfile,
23 | State,
24 | PidRatePresets,
25 | VtxSettings,
26 | OSDFont,
27 | BLHeliSettings,
28 | BindInfo,
29 | PerfCounters,
30 | BlackboxPresets,
31 | Target,
32 | }
33 |
34 | export enum QuicBlackbox {
35 | Reset,
36 | List,
37 | Get,
38 | }
39 |
40 | export enum QuicMotor {
41 | TestStatus,
42 | TestEnable,
43 | TestDisable,
44 | TestSetValue,
45 | Esc4WayIf,
46 | }
47 |
48 | export enum QuicOSD {
49 | ReadChar,
50 | WriteChar,
51 | }
52 |
53 | export enum QuicFlag {
54 | None,
55 | Error,
56 | Streaming,
57 | Exit,
58 | }
59 |
60 | export interface QuicHeader {
61 | cmd: QuicCmd;
62 | flag: QuicFlag;
63 | len: number;
64 | }
65 |
66 | export interface QuicPacket extends QuicHeader {
67 | payload: any;
68 | }
69 |
--------------------------------------------------------------------------------
/src/store/serial/settings.ts:
--------------------------------------------------------------------------------
1 | const isAndroid = /(android)/i.test(navigator.userAgent);
2 |
3 | const androidSerialSettings = {
4 | baudRate: 921600,
5 | bufferSize: 4 * 1024 * 1024,
6 | updateInterval: 1000,
7 | };
8 |
9 | const desktopSerialSettings = {
10 | baudRate: 921600,
11 | bufferSize: 4 * 1024 * 1024,
12 | updateInterval: 250,
13 | };
14 |
15 | export const settings = {
16 | websocketUrl() {
17 | return new URL(document.location.toString()).searchParams.get("ws");
18 | },
19 | serial: isAndroid ? androidSerialSettings : desktopSerialSettings,
20 | };
21 |
--------------------------------------------------------------------------------
/src/store/serial/webserial.ts:
--------------------------------------------------------------------------------
1 | import { serial } from "web-serial-polyfill";
2 |
3 | export const WebSerial = navigator.serial ? navigator.serial : serial;
4 |
--------------------------------------------------------------------------------
/src/store/state.ts:
--------------------------------------------------------------------------------
1 | import { QuicVal } from "./serial/quic";
2 | import { serial } from "./serial/serial";
3 | import { Log } from "@/log";
4 | import { FailloopMessages } from "./constants";
5 | import { defineStore } from "pinia";
6 |
7 | export const useStateStore = defineStore("state", {
8 | state: () => ({
9 | looptime_autodetect: 0,
10 | cpu_load: 0.0,
11 | cpu_temp: 0.0,
12 | vbat_filtered: 0.0,
13 | vbattfilt: null,
14 | ibat_filtered: 0.0,
15 | ibat_drawn: 0.0,
16 | rx: [],
17 | rx_filtered: [],
18 | rx_status: 0,
19 | rx_rssi: 0,
20 | gyro_temp: 0,
21 | GEstG: null,
22 | accel: null,
23 | angle_error: [],
24 | stick_vector: [],
25 | aux: [],
26 | stick_calibration_wizard: 0,
27 | failloop: 0,
28 | pidoutput: null,
29 | accel_raw: null,
30 | gyro: null,
31 | gyro_raw: null,
32 | }),
33 | getters: {
34 | vbat(state) {
35 | return state.vbattfilt || state.vbat_filtered;
36 | },
37 | failloopMessage(state) {
38 | return FailloopMessages[state.failloop];
39 | },
40 | },
41 | actions: {
42 | fetch_state() {
43 | return serial
44 | .get(QuicVal.State)
45 | .then((update) => this.$patch(update))
46 | .catch((err) => Log.warn("state", err));
47 | },
48 | },
49 | });
50 |
--------------------------------------------------------------------------------
/src/store/target.ts:
--------------------------------------------------------------------------------
1 | import { QuicVal } from "./serial/quic";
2 | import { serial } from "./serial/serial";
3 | import { defineStore } from "pinia";
4 | import type { target_t } from "./types";
5 |
6 | import YAML from "yaml";
7 | import { useInfoStore } from "./info";
8 | import { useRootStore } from "./root";
9 |
10 | export function skipEmpty(val: any) {
11 | if (val === undefined) {
12 | return undefined;
13 | }
14 | if (val === "NONE") {
15 | return undefined;
16 | }
17 | if (Array.isArray(val)) {
18 | for (let i = val.length - 1; i >= 0; i--) {
19 | val[i] = skipEmpty(val[i]);
20 | if (!val[i]) {
21 | val.splice(i, 1);
22 | }
23 | }
24 | if (val.length == 0) {
25 | return undefined;
26 | }
27 | }
28 | if (Object.getPrototypeOf(val) === Object.prototype) {
29 | const keys = Object.keys(val);
30 | for (let i = keys.length - 1; i >= 0; i--) {
31 | val[keys[i]] = skipEmpty(val[keys[i]]);
32 | if (!val[keys[i]]) {
33 | delete val[keys[i]];
34 | }
35 | }
36 | if (Object.keys(val).length == 0) {
37 | return undefined;
38 | }
39 | }
40 | return val;
41 | }
42 |
43 | export const useTargetStore = defineStore("target", {
44 | state: (): target_t => ({
45 | name: "",
46 |
47 | brushless: true,
48 |
49 | gyro_orientation: 0,
50 |
51 | leds: [],
52 | serial_ports: [],
53 | serial_soft_ports: [],
54 | spi_ports: [],
55 |
56 | motor_pins: [],
57 | }),
58 | getters: {
59 | yaml(store) {
60 | const info = useInfoStore();
61 | return YAML.stringify(
62 | skipEmpty({ mcu: info.mcu, ...(store as any).$state }),
63 | );
64 | },
65 | serial_port_names(store): { [index: string]: number } {
66 | const res = {};
67 |
68 | const info = useInfoStore();
69 | if (info.quic_semver_gte("0.2.0")) {
70 | for (const p of store.serial_ports) {
71 | if (p.index == 0) {
72 | continue;
73 | }
74 |
75 | res[`SERIAL_PORT${p.index}`] = p.index;
76 | }
77 | for (const p of store.serial_soft_ports) {
78 | if (p.index == 0) {
79 | continue;
80 | }
81 |
82 | res[`SERIAL_SOFT_PORT${p.index}`] = 100 + p.index;
83 | }
84 | } else {
85 | for (let i = 1; i < info.usart_ports.length; i++) {
86 | res[info.usart_ports[i]] = i;
87 | }
88 | }
89 | return res;
90 | },
91 | motor_pin_names(store): string[] {
92 | const info = useInfoStore();
93 | if (info.quic_semver_gte("0.2.0")) {
94 | return store.motor_pins;
95 | } else {
96 | return info.motor_pins;
97 | }
98 | },
99 | },
100 | actions: {
101 | fetch() {
102 | return serial.get(QuicVal.Target).then((target) => this.$patch(target));
103 | },
104 | apply(target: target_t) {
105 | const root = useRootStore();
106 | return serial
107 | .set(QuicVal.Target, target)
108 | .then(() => this.fetch())
109 | .then(() =>
110 | root.append_alert({ type: "success", msg: "Target applied!" }),
111 | );
112 | },
113 | },
114 | });
115 |
--------------------------------------------------------------------------------
/src/store/templates.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 |
3 | const isDevelop = import.meta.env.VITE_BRANCH_NAME;
4 |
5 | const DEVELOP_TEMPLATE_URL =
6 | "https://raw.githubusercontent.com/BossHobby/Templates/develop-deploy/";
7 | const MASTER_TEMPLATE_URL =
8 | "https://raw.githubusercontent.com/BossHobby/Templates/master-deploy/";
9 |
10 | export interface TemplateOptionEntry {
11 | title?: string;
12 | name: string;
13 | desc: string;
14 | selector?: any;
15 | file: string;
16 | }
17 |
18 | export interface TemplateOption {
19 | name: string;
20 | title: string;
21 | desc: string;
22 | default: string;
23 | entries: TemplateOptionEntry[];
24 | }
25 |
26 | export interface TemplateMutation {
27 | name: string;
28 | options: TemplateMutationOption[];
29 | }
30 |
31 | export interface TemplateMutationOption {
32 | name: string;
33 | selector: { [key: string]: string[] };
34 | profile: any;
35 | }
36 |
37 | export interface TemplateEntry {
38 | name: string;
39 | desc: string;
40 | author: string;
41 | options?: TemplateOption[];
42 | mutations?: TemplateMutation[];
43 | category: string;
44 | profile: string;
45 | image: string;
46 | }
47 |
48 | export function templateUrl(file: string) {
49 | const url = isDevelop ? DEVELOP_TEMPLATE_URL : MASTER_TEMPLATE_URL;
50 | return url + file;
51 | }
52 |
53 | export const useTemplatesStore = defineStore("templates", {
54 | state: () => ({
55 | index: [] as TemplateEntry[],
56 | }),
57 | actions: {
58 | fetch_templates() {
59 | const url = isDevelop ? DEVELOP_TEMPLATE_URL : MASTER_TEMPLATE_URL;
60 | return fetch(url + "index.json")
61 | .then((res) => res.json())
62 | .then((index) => {
63 | this.index = index.map((e) => {
64 | if (e.image) {
65 | e.image = templateUrl(e.image);
66 | }
67 | e.profile = templateUrl(e.profile);
68 | return e;
69 | });
70 | });
71 | },
72 | },
73 | });
74 |
--------------------------------------------------------------------------------
/src/store/types.ts:
--------------------------------------------------------------------------------
1 | export enum rate_modes_t {
2 | RATE_MODE_SILVERWARE,
3 | RATE_MODE_BETAFLIGHT,
4 | RATE_MODE_ACTUAL,
5 | }
6 |
7 | export enum silverware_rates_t {
8 | SILVERWARE_MAX_RATE,
9 | SILVERWARE_ACRO_EXPO,
10 | SILVERWARE_ANGLE_EXPO,
11 | }
12 |
13 | export enum betaflight_rates_t {
14 | BETAFLIGHT_RC_RATE,
15 | BETAFLIGHT_SUPER_RATE,
16 | BETAFLIGHT_EXPO,
17 | }
18 |
19 | export enum actual_rates_t {
20 | ACTUAL_CENTER_SENSITIVITY,
21 | ACTUAL_MAX_RATE,
22 | ACTUAL_EXPO,
23 | }
24 |
25 | export enum rate_profiles_t {
26 | STICK_RATE_PROFILE_1,
27 | STICK_RATE_PROFILE_2,
28 | STICK_RATE_PROFILE_MAX,
29 | }
30 |
31 | export enum gyro_rotation_t {
32 | GYRO_ROTATE_NONE = 0x0,
33 | GYRO_ROTATE_45_CCW = 0x1,
34 | GYRO_ROTATE_45_CW = 0x2,
35 | GYRO_ROTATE_90_CW = 0x4,
36 | GYRO_ROTATE_90_CCW = 0x8,
37 | GYRO_ROTATE_180 = 0x10,
38 | GYRO_FLIP_180 = 0x20,
39 | }
40 |
41 | export enum dshot_time_t {
42 | DSHOT_TIME_150 = 150,
43 | DSHOT_TIME_300 = 300,
44 | DSHOT_TIME_600 = 600,
45 | }
46 |
47 | export enum stick_profile_t {
48 | STICK_PROFILE_OFF,
49 | STICK_PROFILE_ON,
50 | STICK_PROFILE_MAX,
51 | }
52 |
53 | export enum tda_active_t {
54 | THROTTLE_D_ATTENTUATION_NONE,
55 | THROTTLE_D_ATTENUATION_ACTIVE,
56 | THROTTLE_D_ATTENUATION_MAX,
57 | }
58 |
59 | export enum pid_profile_t {
60 | PID_PROFILE_1,
61 | PID_PROFILE_2,
62 | PID_PROFILE_MAX,
63 | }
64 |
65 | export enum pid_voltage_compensation_t {
66 | PID_VOLTAGE_COMPENSATION_NONE,
67 | PID_VOLTAGE_COMPENSATION_ACTIVE,
68 | }
69 |
70 | export type vec3_t = number[];
71 |
72 | export interface rate_t {
73 | mode: rate_modes_t;
74 | rate: vec3_t[];
75 | }
76 |
77 | export interface profile_rate_t {
78 | profile: rate_profiles_t;
79 | rates: rate_t[];
80 | level_max_angle: number;
81 | sticks_deadband: number;
82 | throttle_mid: number;
83 | throttle_expo: number;
84 | low_rate_mulitplier?: number;
85 | }
86 |
87 | export interface pid_rate_t {
88 | kp: vec3_t;
89 | ki: vec3_t;
90 | kd: vec3_t;
91 | }
92 |
93 | export interface angle_pid_rate_t {
94 | kp: number;
95 | kd: number;
96 | }
97 |
98 | export interface pid_rate_preset_t {
99 | index: number;
100 | name: string;
101 | rate: pid_rate_t;
102 | }
103 |
104 | export interface stick_rate_t {
105 | accelerator: vec3_t;
106 | transition: vec3_t;
107 | }
108 |
109 | export interface throttle_dterm_attenuation_t {
110 | tda_active: tda_active_t;
111 | tda_breakpoint: number;
112 | tda_percent: number;
113 | }
114 |
115 | export interface profile_pid_t {
116 | pid_profile: pid_profile_t;
117 | pid_rates: pid_rate_t[];
118 | stick_profile: stick_profile_t;
119 | stick_rates: stick_rate_t[];
120 | big_angle: angle_pid_rate_t;
121 | small_angle: angle_pid_rate_t;
122 | throttle_dterm_attenuation: throttle_dterm_attenuation_t;
123 | }
124 |
125 | export interface profile_motor_t {
126 | digital_idle: number;
127 | motor_limit: number;
128 | dshot_time: dshot_time_t;
129 | invert_yaw: number;
130 | gyro_orientation: number;
131 | torque_boost: number;
132 | throttle_boost: number;
133 | motor_pins: number[];
134 | turtle_throttle_percent: number;
135 | }
136 |
137 | export interface profile_voltage_t {
138 | lipo_cell_count: number;
139 | pid_voltage_compensation: pid_voltage_compensation_t;
140 | vbattlow: number;
141 | actual_battery_voltage: number;
142 | reported_telemetry_voltage: number;
143 | use_filtered_voltage_for_warnings: number;
144 | vbat_scale: number;
145 | ibat_scale: number;
146 | }
147 |
148 | export interface profile_stick_calibration_limits_t {
149 | min: number;
150 | max: number;
151 | }
152 |
153 | export interface profile_receiver_t {
154 | protocol: number;
155 | aux: number[];
156 | lqi_source: number;
157 | channel_mapping: number;
158 | stick_calibration_limits: profile_stick_calibration_limits_t[];
159 | }
160 |
161 | export interface profile_serial_t {
162 | rx: number;
163 | smart_audio: number;
164 | hdzero: number;
165 | }
166 |
167 | export interface profile_osd_t {
168 | guac_mode: number;
169 | callsign: string;
170 | elements: number[];
171 | elements_hd: number[];
172 | }
173 |
174 | export interface profile_filter_parameter_t {
175 | type: number;
176 | cutoff_freq: number;
177 | }
178 |
179 | export interface profile_filter_t {
180 | gyro: profile_filter_parameter_t[];
181 | dterm: profile_filter_parameter_t[];
182 | dterm_dynamic_enable: number;
183 | dterm_dynamic_min: number;
184 | dterm_dynamic_max: number;
185 | }
186 |
187 | export interface profile_blackbox_t {
188 | field_flags: number;
189 | sample_rate_hz: number;
190 | }
191 |
192 | export interface blackbox_preset_t {
193 | field_flags: number;
194 | sample_rate_hz: number;
195 | name: string;
196 | name_osd: string;
197 | }
198 |
199 | export interface profile_metadata_t {
200 | version: number;
201 | name: string;
202 | datetime: number;
203 | }
204 |
205 | export interface profile_t {
206 | meta: profile_metadata_t;
207 | motor: profile_motor_t;
208 | serial: profile_serial_t;
209 | filter: profile_filter_t;
210 | osd: profile_osd_t;
211 | rate: profile_rate_t;
212 | receiver: profile_receiver_t;
213 | pid: profile_pid_t;
214 | voltage: profile_voltage_t;
215 | blackbox: profile_blackbox_t;
216 | }
217 |
218 | export type gpio_pins_t = string;
219 |
220 | export interface target_led_t {
221 | pin: gpio_pins_t;
222 | invert: boolean;
223 | }
224 |
225 | export interface target_invert_pin_t {
226 | pin: gpio_pins_t;
227 | invert: boolean;
228 | }
229 |
230 | export interface target_serial_port_t {
231 | index: number;
232 | rx: gpio_pins_t;
233 | tx: gpio_pins_t;
234 | inverter: gpio_pins_t;
235 | }
236 |
237 | export interface target_spi_port_t {
238 | index: number;
239 | miso: gpio_pins_t;
240 | mosi: gpio_pins_t;
241 | sck: gpio_pins_t;
242 | }
243 |
244 | export interface target_spi_device_t {
245 | port: number;
246 | nss: gpio_pins_t;
247 | }
248 |
249 | export interface target_rx_spi_device_t {
250 | port: number;
251 | nss: gpio_pins_t;
252 | exti?: gpio_pins_t;
253 | ant_sel?: gpio_pins_t;
254 | lna_en?: gpio_pins_t;
255 | tx_en?: gpio_pins_t;
256 | busy?: gpio_pins_t;
257 | busy_exti?: boolean;
258 | reset?: gpio_pins_t;
259 | }
260 |
261 | export interface target_t {
262 | name: string;
263 |
264 | brushless: boolean;
265 |
266 | leds: target_led_t[];
267 | serial_ports: target_serial_port_t[];
268 | serial_soft_ports: target_serial_port_t[];
269 | spi_ports: target_spi_port_t[];
270 |
271 | gyro?: target_spi_device_t;
272 | gyro_orientation: number;
273 | osd?: target_spi_device_t;
274 | flash?: target_spi_device_t;
275 | sdcard?: target_spi_device_t;
276 | rx_spi?: target_rx_spi_device_t;
277 |
278 | usb_detect?: gpio_pins_t;
279 | fpv?: gpio_pins_t;
280 | vbat?: gpio_pins_t;
281 | ibat?: gpio_pins_t;
282 |
283 | sdcard_detect?: target_invert_pin_t;
284 | buzzer?: target_invert_pin_t;
285 | motor_pins: gpio_pins_t[];
286 | }
287 |
288 | export enum target_feature_t {
289 | FEATURE_BRUSHLESS = 1 << 1,
290 | FEATURE_OSD = 1 << 2,
291 | FEATURE_BLACKBOX = 1 << 3,
292 | FEATURE_DEBUG = 1 << 4,
293 | }
294 |
295 | export interface target_info_t {
296 | target_name: string;
297 | mcu: string;
298 | git_version: string;
299 |
300 | features: number;
301 | rx_protocols: number[];
302 | quic_protocol_version: number;
303 |
304 | motor_pins: string[];
305 | usart_ports: string[];
306 |
307 | gyro_id: number;
308 | }
309 |
--------------------------------------------------------------------------------
/src/store/util/index.ts:
--------------------------------------------------------------------------------
1 | import semver from "semver";
2 |
3 | export function concatUint8Array(a: Uint8Array, b: Uint8Array): Uint8Array {
4 | const res = new Uint8Array(a.length + b.length);
5 | res.set(a);
6 | res.set(b, a.length);
7 | return res;
8 | }
9 |
10 | export function stringToUint8Array(str: string): Uint8Array {
11 | const res = new Uint8Array(str.length);
12 | for (let i = 0; i < str.length; i++) {
13 | res[i] = str.charCodeAt(i);
14 | }
15 | return res;
16 | }
17 |
18 | export class ArrayWriter {
19 | private offset = 0;
20 | private buf = new ArrayBuffer(4);
21 | private view = new DataView(this.buf);
22 |
23 | public get length() {
24 | return this.offset;
25 | }
26 |
27 | public reset() {
28 | this.offset = 0;
29 | }
30 |
31 | public get(index: number): number {
32 | return this.view.getUint8(index);
33 | }
34 |
35 | public writeUint8(v: number) {
36 | this.grow();
37 |
38 | this.view.setUint8(this.offset, v);
39 | this.offset++;
40 | }
41 |
42 | public writeUint8s(values: ArrayLike) {
43 | this.grow(values.length);
44 | new Uint8Array(this.buf).set(Uint8Array.from(values), this.offset);
45 | this.offset += values.length;
46 | }
47 |
48 | public writeFloat32(v: number) {
49 | this.grow(4);
50 |
51 | this.view.setFloat32(this.offset, v);
52 | this.offset += 4;
53 | }
54 |
55 | public array(): Uint8Array {
56 | return new Uint8Array(this.buf, 0, this.offset);
57 | }
58 |
59 | private grow(num = 1) {
60 | if (this.offset + num >= this.buf.byteLength) {
61 | let newSize = this.buf.byteLength;
62 | while (newSize < this.offset + num) {
63 | newSize *= 2;
64 | }
65 |
66 | const newBuf = new ArrayBuffer(newSize);
67 | new Uint8Array(newBuf).set(new Uint8Array(this.buf));
68 | this.buf = newBuf;
69 | this.view = new DataView(this.buf);
70 | }
71 | }
72 | }
73 |
74 | export class ArrayReader {
75 | private offset = 0;
76 | private buf = new ArrayBuffer(0);
77 | private view = new DataView(this.buf);
78 |
79 | constructor(array?: Uint8Array) {
80 | if (array) {
81 | this.buf = array.buffer.slice(0, array.length);
82 | this.view = new DataView(this.buf);
83 | }
84 | }
85 |
86 | public advance(num: number) {
87 | this.offset += num;
88 | }
89 |
90 | public remaining(): number {
91 | return this.buf.byteLength - this.offset;
92 | }
93 |
94 | public peekUint8(): number {
95 | return this.view.getUint8(this.offset);
96 | }
97 |
98 | public peekUint16(): number {
99 | return this.view.getUint16(this.offset);
100 | }
101 |
102 | public peekFloat32(): number {
103 | return this.view.getFloat32(this.offset);
104 | }
105 |
106 | public peekFloat64(): number {
107 | return this.view.getFloat64(this.offset);
108 | }
109 | }
110 |
111 | export function decodeSemver(v: number): string {
112 | return `v${(v >> 16) & 0xff}.${(v >> 8) & 0xff}.${(v >> 0) & 0xff}`;
113 | }
114 |
115 | export function encodeSemver(version: string): number {
116 | const v = semver.parse(version)!;
117 | return (v.major << 16) | (v.minor << 8) | (v.patch & 0xff);
118 | }
119 |
120 | export function asyncDelay(timeout: number) {
121 | return new Promise((resolve) => {
122 | setTimeout(resolve, timeout);
123 | });
124 | }
125 |
--------------------------------------------------------------------------------
/src/store/util/updater.ts:
--------------------------------------------------------------------------------
1 | function newElectronUpdater() {
2 | // TODO: electron
3 | }
4 |
5 | function newPWAUpdater() {
6 | class Updater {
7 | private hasUpdate = false;
8 | private updateSW?: (reloadPage?: boolean) => Promise;
9 | private updateCallback?: (v: any) => Promise;
10 |
11 | constructor() {}
12 |
13 | public updatePreparing() {
14 | return false;
15 | }
16 |
17 | public updatePending() {
18 | return false;
19 | }
20 |
21 | public async checkForUpdate(
22 | currentVersion: string,
23 | updateCallback: (v: any) => Promise,
24 | ) {
25 | this.updateCallback = updateCallback;
26 | if (this.updateSW) {
27 | return;
28 | }
29 |
30 | try {
31 | const updater = this;
32 | const { registerSW } = await import("virtual:pwa-register");
33 | this.updateSW = registerSW({
34 | immediate: true,
35 | onOfflineReady() {
36 | console.log("PWA offline ready");
37 | },
38 | onNeedRefresh() {
39 | updater.hasUpdate = true;
40 |
41 | if (updater.updateCallback) {
42 | updater.updateCallback(updater.hasUpdate);
43 | }
44 | },
45 | onRegistered(swRegistration) {},
46 | onRegisterError(e) {},
47 | });
48 | } catch {
49 | console.log("PWA disabled");
50 | }
51 | }
52 |
53 | public async update(release: any) {
54 | if (this.hasUpdate) {
55 | this.hasUpdate = false;
56 | this.updateSW && this.updateSW(true);
57 | }
58 | }
59 |
60 | public async finishUpdate() {}
61 | }
62 |
63 | return new Updater();
64 | }
65 |
66 | function newUpdater() {
67 | try {
68 | throw new Error("new.App undefined");
69 | return newElectronUpdater();
70 | } catch {
71 | console.log("Registering PWA updater");
72 | return newPWAUpdater();
73 | }
74 | }
75 |
76 | export const updater = newUpdater();
77 |
--------------------------------------------------------------------------------
/src/store/vtx.ts:
--------------------------------------------------------------------------------
1 | import { QuicVal } from "./serial/quic";
2 | import { serial } from "./serial/serial";
3 | import { defineStore } from "pinia";
4 | import { useRootStore } from "./root";
5 |
6 | export const useVTXStore = defineStore("vtx", {
7 | state: () => ({
8 | settings: {
9 | protocol: 0,
10 | detected: 0,
11 | channel: 0,
12 | band: 0,
13 | power_table: {
14 | levels: 0,
15 | labels: [],
16 | values: [],
17 | },
18 | },
19 | }),
20 | actions: {
21 | apply_vtx_settings(vtx_settings) {
22 | const root = useRootStore();
23 |
24 | if (vtx_settings.power_table) {
25 | for (let i = 0; i < vtx_settings.power_table.labels.length; i++) {
26 | while (vtx_settings.power_table.labels[i].length < 3) {
27 | vtx_settings.power_table.labels[i] += " ";
28 | }
29 | }
30 | }
31 |
32 | return serial
33 | .set(QuicVal.VtxSettings, vtx_settings)
34 | .then((v) => (this.settings = v))
35 | .then(() => {
36 | root.append_alert({ type: "success", msg: "Apply successful!" });
37 | })
38 | .catch(() => {
39 | root.append_alert({ type: "danger", msg: "Apply failed" });
40 | });
41 | },
42 | update_vtx_settings(force = false) {
43 | if (this.settings.detected == 0 || force) {
44 | return serial.get(QuicVal.VtxSettings).then((settings) => {
45 | const protocol =
46 | settings.detected == 0 ? this.settings.protocol : settings.protocol;
47 | this.settings = {
48 | ...settings,
49 | protocol,
50 | };
51 | });
52 | }
53 | },
54 | },
55 | });
56 |
--------------------------------------------------------------------------------
/src/style.scss:
--------------------------------------------------------------------------------
1 | $green: hsl(96deg 53% 43%);
2 | $green-light: hsl(96deg 53% 48%);
3 | $green-dark: hsl(96deg 53% 40%);
4 |
5 | $grey: hsl(0, 0%, 19%);
6 | $grey-dark: hsl(0, 0%, 16%);
7 | $grey-darker: hsl(0, 0%, 3%);
8 | $grey-light: hsl(0, 0%, 42%);
9 | $grey-lighter: hsl(0, 0%, 73%);
10 |
11 | $white: rgb(255, 255, 255);
12 | $white-off: hsl(0, 0%, 95%);
13 | $white-offer: hsl(0, 0%, 75%);
14 |
15 | $danger: hsl(348, 86%, 61%);
16 |
17 | @use "bulma/sass" with (
18 | $family-primary: '"Roboto", sans-serif',
19 | $primary: $green,
20 | $danger: $danger,
21 | $link: $white,
22 | $success: $green-light,
23 |
24 | $grey: $grey,
25 | $grey-dark: $grey-dark,
26 | $grey-darker: $grey-darker,
27 | $grey-light: $grey-light,
28 | $grey-lighter: $grey-lighter
29 | );
30 | @use "bulma/sass/utilities/css-variables" as cs;
31 | @import "switch.scss";
32 |
33 | @mixin register-var($name, $value) {
34 | @include cs.register-var($name, $value);
35 | @include cs.register-hsl($name, $value);
36 | }
37 |
38 | :root {
39 | --bulma-primary-invert-l: var(--bulma-primary-100-l);
40 | --bulma-danger-invert-l: var(--bulma-danger-100-l);
41 |
42 | .navbar {
43 | --bulma-navbar-burger-color: #{$white-off};
44 | }
45 | }
46 |
47 | .theme-dark {
48 | --bulma-text: #{$white-off};
49 | --bulma-text-strong: #{$white};
50 | --bulma-text-light: #{$white-offer};
51 |
52 | --bulma-link: #{$green};
53 | --bulma-link-visited: #{$green};
54 | --bulma-link-hover: #{$grey-lighter};
55 | --bulma-link-invert: #{$grey-lighter};
56 | --bulma-link-focus: #{$grey-lighter};
57 |
58 | @include register-var("scheme", $grey-dark);
59 | @include register-var("scheme-main", $grey-dark);
60 | @include register-var("scheme-main-bis", $grey);
61 | @include register-var("scheme-main-ter", $grey);
62 | @include register-var("scheme-invert", $white-off);
63 | @include register-var("scheme-invert-bis", $white);
64 | @include register-var("scheme-invert-ter", $white);
65 |
66 | --bulma-background: #{$grey};
67 | --bulma-body-background-color: #{$grey};
68 |
69 | --bulma-border: #{$grey-light};
70 | --bulma-border-hover: #{$grey-lighter};
71 | --bulma-border-light: #{$grey-light};
72 |
73 | --bulma-shadow: #{0 0.5em 1em -0.125em rgba($grey-darker, 0.5)},
74 | #{0 0px 0 1px rgba($grey-darker, 0.2)};
75 | --bulma-card-header-shadow: 0 0.125em 0.25em #{rgba($grey-darker, 0.5)};
76 | --bulma-navbar-bottom-box-shadow-size: 0 -0.1em 0.125em;
77 | --bulma-navbar-box-shadow-color: rgb(8 8 8 / 30%);
78 |
79 | .modal {
80 | --bulma-modal-background-background-color: hsla(
81 | var(--bulma-scheme-h),
82 | var(--bulma-scheme-s),
83 | var(--bulma-scheme-main-l),
84 | 0.86
85 | );
86 | }
87 | }
88 |
89 | html,
90 | body,
91 | #app {
92 | font-family: "Roboto", sans-serif;
93 | font-feature-settings: "kern", "liga";
94 |
95 | -webkit-font-smoothing: antialiased;
96 | -moz-osx-font-smoothing: grayscale;
97 |
98 | max-width: 100%;
99 | overflow-x: hidden;
100 | }
101 |
102 | .column-narrow {
103 | .column {
104 | padding-top: 8px;
105 | padding-bottom: 8px;
106 | }
107 | }
108 |
109 | .field-label {
110 | align-self: center;
111 | }
112 |
113 | .field-is-5 {
114 | .field-label {
115 | flex-grow: 5;
116 | }
117 | }
118 |
119 | .field-is-4 {
120 | .field-label {
121 | flex-grow: 4;
122 | }
123 | }
124 |
125 | .field-is-3 {
126 | .field-label {
127 | flex-grow: 3;
128 | }
129 | }
130 |
131 | .field-is-2 {
132 | .field-label {
133 | flex-grow: 2;
134 | }
135 | }
136 |
137 | .card-header-button {
138 | margin: 10px;
139 | }
140 |
141 | .logo-animation {
142 | @keyframes logo-wiggle {
143 | 0% {
144 | transform: translateY(-70%) rotate(20deg);
145 | transform-origin: center;
146 | }
147 | 4% {
148 | transform: translateY(0%) rotate(16deg) scaleY(0.999);
149 | transform-origin: center;
150 | }
151 | 6% {
152 | transform: translateY(-2%) rotate(14deg) scaleY(1);
153 | transform-origin: center;
154 | }
155 | 8% {
156 | transform: translateY(0) rotate(12deg);
157 | transform-origin: center;
158 | }
159 | 18% {
160 | transform: rotate(-5deg);
161 | }
162 | 28% {
163 | transform: rotate(5deg);
164 | }
165 | 38% {
166 | transform: rotate(-1.5deg);
167 | }
168 | 44% {
169 | transform: rotate(0deg);
170 | }
171 | 100% {
172 | transform: rotate(0deg);
173 | }
174 | }
175 |
176 | .avocado {
177 | transform-origin: bottom center;
178 | animation: logo-wiggle 10s cubic-bezier(0.45, 0.2, 0.45, 1) 1;
179 | }
180 | }
181 |
182 | .text-animation {
183 | @keyframes text-wiggle {
184 | 0% {
185 | transform: translateY(-70%);
186 | transform-origin: center;
187 | }
188 | 4% {
189 | transform: translateY(0%) scaleY(0.999);
190 | transform-origin: center;
191 | }
192 | 6% {
193 | transform: translateY(-2%) scaleY(1);
194 | transform-origin: center;
195 | }
196 | 8% {
197 | transform: translateY(0);
198 | transform-origin: center;
199 | }
200 | }
201 |
202 | .tagline {
203 | transform-origin: bottom center;
204 | animation: text-wiggle 10s cubic-bezier(0.45, 0.2, 0.45, 1) 1;
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/src/sw.ts:
--------------------------------------------------------------------------------
1 | import { clientsClaim } from "workbox-core";
2 | import { registerRoute } from "workbox-routing";
3 | import { CacheFirst } from "workbox-strategies";
4 | import { ExpirationPlugin } from "workbox-expiration";
5 | import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching";
6 |
7 | declare let self: ServiceWorkerGlobalScope;
8 |
9 | const cacheUrls = [
10 | "raw.githubusercontent.com",
11 | "api.github.com",
12 | "cors.bubblesort.me",
13 | ];
14 |
15 | self.__WB_DISABLE_DEV_LOGS = true;
16 | self.addEventListener("message", (event) => {
17 | if (event.data && event.data.type === "SKIP_WAITING") {
18 | event.waitUntil(
19 | caches
20 | .keys()
21 | .then((keys) => Promise.all(keys.map((key) => caches.delete(key)))),
22 | );
23 | self.skipWaiting();
24 | }
25 | });
26 |
27 | clientsClaim();
28 | cleanupOutdatedCaches();
29 |
30 | registerRoute(
31 | ({ url }) => cacheUrls.includes(url.hostname),
32 | new CacheFirst({
33 | cacheName: "github-cache",
34 | matchOptions: {
35 | ignoreVary: true,
36 | },
37 | plugins: [
38 | new ExpirationPlugin({
39 | matchOptions: {
40 | ignoreVary: true,
41 | },
42 | purgeOnQuotaError: true,
43 | maxAgeSeconds: 5 * 60,
44 | }),
45 | ],
46 | }),
47 | );
48 | precacheAndRoute(self.__WB_MANIFEST);
49 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
25 |
--------------------------------------------------------------------------------
/src/views/Motor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
37 |
--------------------------------------------------------------------------------
/src/views/OSD.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
43 |
--------------------------------------------------------------------------------
/src/views/Perf.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
35 |
--------------------------------------------------------------------------------
/src/views/Profile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Incompatible Firmware!
7 | Please update to be able to change settings.
8 | Your current profile can be exported and loaded.
9 |
10 |
11 | Faillop {{ state.failloopMessage }} ({{ state.failloop }}) Detected!
12 |
13 | Please fix the issue to be able to change settings.
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
54 |
--------------------------------------------------------------------------------
/src/views/Rates.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/views/Receiver.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/views/Setup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
25 |
26 |
27 |
52 |
--------------------------------------------------------------------------------
/src/views/State.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
19 |
20 |
21 |
22 |
23 |
24 |
104 |
--------------------------------------------------------------------------------
/src/views/Templates.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Templates provide a way to apply configurations supplied by the
6 | community.
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
40 |
41 |
50 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.dom.json",
3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
4 | "compilerOptions": {
5 | "baseUrl": ".",
6 | "paths": {
7 | "@/*": ["./src/*"]
8 | },
9 | "types": [
10 | "node",
11 | "dom-serial",
12 | "w3c-web-usb",
13 | "semver",
14 | "vite-plugin-pwa/client"
15 | ],
16 | "lib": ["ES2020", "DOM", "DOM.Iterable", "WebWorker"],
17 | "allowJs": true,
18 | "noImplicitAny": false
19 | },
20 | "references": [
21 | {
22 | "path": "./tsconfig.vite-config.json"
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.vite-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.json",
3 | "include": ["vite.config.*"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "types": ["node"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from "url";
2 |
3 | import { defineConfig } from "vite";
4 | import { VitePWA } from "vite-plugin-pwa";
5 | import vue from "@vitejs/plugin-vue";
6 | import { execSync } from "child_process";
7 | import svgLoader from "vite-svg-loader";
8 | import webfontDownload from "vite-plugin-webfont-dl";
9 |
10 | const branch = execSync("git rev-parse --abbrev-ref HEAD").toString().trimEnd();
11 |
12 | let base = "/";
13 |
14 | if (process.env.NODE_ENV === "production") {
15 | if (
16 | process.env.DEPLOYMENT === "gh-pages" ||
17 | process.env.DEPLOYMENT === "local"
18 | ) {
19 | if (branch == "develop") {
20 | base = "/develop/";
21 | } else {
22 | base = "/";
23 | }
24 | } else {
25 | base = "/dist/";
26 | }
27 | }
28 |
29 | process.env.VITE_BRANCH_NAME = branch;
30 | process.env.VITE_APP_VERSION = require("./package.json").version;
31 |
32 | // https://vitejs.dev/config/
33 | export default defineConfig({
34 | base: base,
35 | plugins: [
36 | vue(),
37 | svgLoader(),
38 | webfontDownload([
39 | "https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap",
40 | ]),
41 | VitePWA({
42 | strategies: "injectManifest",
43 | srcDir: "src",
44 | filename: "sw.ts",
45 | manifest: {
46 | name: "QUICKSILVER Configurator",
47 | short_name: "QUICKSILVER",
48 | description:
49 | "Configurator for the QUICKSILVER flight-controller firmware",
50 | theme_color: "#3c7317",
51 | icons: [
52 | {
53 | src: "pwa.png",
54 | sizes: "512x512",
55 | type: "image/png",
56 | },
57 | ],
58 | display: "standalone",
59 | background_color: "#FFFFFF",
60 | },
61 | devOptions: {
62 | enabled: true,
63 | type: "module",
64 | },
65 | workbox: {
66 | sourcemap: process.env.NODE_ENV !== "production",
67 | mode: process.env.NODE_ENV,
68 | },
69 | injectManifest: {
70 | globPatterns: [
71 | "**/*.{css,glb,html,ico,jpg,js,png,svg,txt,webmanifest,woff2}",
72 | ],
73 | rollupFormat: "iife",
74 | },
75 | }),
76 | ],
77 | server: {
78 | port: 8080,
79 | },
80 | resolve: {
81 | alias: {
82 | "node-fetch": "isomorphic-fetch",
83 | "@": fileURLToPath(new URL("./src", import.meta.url)),
84 | },
85 | },
86 | define: {
87 | "process.env": {},
88 | },
89 | });
90 |
--------------------------------------------------------------------------------