├── .nojekyll ├── public ├── robots.txt ├── favicon.ico ├── pwa-64x64.png ├── pwa-192x192.png ├── pwa-512x512.png ├── backpack_723137.png ├── backpack_723278.png ├── reciever_6276002.png ├── reciever_6276814.png ├── satellite_2637312.png ├── satellite_2637314.png ├── stopwatch_4354897.png ├── stopwatch_4355918.png ├── transmitter_6275858.png ├── transmitter_6276574.png ├── vr-glasses_8736938.png ├── vr-glasses_8737003.png ├── maskable-icon-512x512.png ├── apple-touch-icon-180x180.png └── favicon.svg ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .idea ├── .gitignore ├── vcs.xml ├── modules.xml ├── inspectionProfiles │ └── Project_Default.xml └── flasher.iml ├── mdns-proxy ├── README.md ├── elrs-proxy.py └── proxy.c ├── TODO.md ├── src ├── components │ ├── FanRuntime.vue │ ├── WiFiAutoOn.vue │ ├── RXOptions.vue │ ├── WiFiSettingsInput.vue │ ├── MelodyInput.vue │ ├── BindPhraseInput.vue │ ├── RXasTX.vue │ ├── FlashMethodSelect.vue │ ├── TXOptions.vue │ ├── ReloadPrompt.vue │ ├── RFSelect.vue │ └── HoverCard.vue ├── js │ ├── error.js │ ├── stlink │ │ ├── lib │ │ │ ├── package.js │ │ │ ├── semihosting.js │ │ │ ├── util.js │ │ │ ├── stlinkex.js │ │ │ ├── stlinkusb.js │ │ │ ├── stm32.js │ │ │ ├── stm32fs.js │ │ │ └── stlinkv2.js │ │ └── mutex.js │ ├── version.js │ ├── state.js │ ├── melody.js │ ├── phrase.js │ ├── firmware.js │ ├── serialex.js │ ├── espflasher.js │ ├── stlink.js │ ├── passthrough.js │ ├── configure.js │ └── xmodem.js ├── main.css ├── main.js ├── pages │ ├── BackpackOptions.vue │ ├── TransmitterOptions.vue │ ├── ReceiverOptions.vue │ ├── Download.vue │ ├── FirmwareSelect.vue │ ├── BackpackHardwareSelect.vue │ ├── STLinkFlash.vue │ ├── MainHardwareSelect.vue │ └── SerialFlash.vue └── App.vue ├── .run ├── run dev.run.xml └── get_artifacts.sh.run.xml ├── .gitignore ├── index.html ├── package.json ├── .github └── workflows │ ├── web.yml │ └── build.yml ├── README.md ├── get_artifacts.sh └── vite.config.js /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/pwa-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/pwa-64x64.png -------------------------------------------------------------------------------- /public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/pwa-192x192.png -------------------------------------------------------------------------------- /public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/pwa-512x512.png -------------------------------------------------------------------------------- /public/backpack_723137.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/backpack_723137.png -------------------------------------------------------------------------------- /public/backpack_723278.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/backpack_723278.png -------------------------------------------------------------------------------- /public/reciever_6276002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/reciever_6276002.png -------------------------------------------------------------------------------- /public/reciever_6276814.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/reciever_6276814.png -------------------------------------------------------------------------------- /public/satellite_2637312.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/satellite_2637312.png -------------------------------------------------------------------------------- /public/satellite_2637314.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/satellite_2637314.png -------------------------------------------------------------------------------- /public/stopwatch_4354897.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/stopwatch_4354897.png -------------------------------------------------------------------------------- /public/stopwatch_4355918.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/stopwatch_4355918.png -------------------------------------------------------------------------------- /public/transmitter_6275858.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/transmitter_6275858.png -------------------------------------------------------------------------------- /public/transmitter_6276574.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/transmitter_6276574.png -------------------------------------------------------------------------------- /public/vr-glasses_8736938.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/vr-glasses_8736938.png -------------------------------------------------------------------------------- /public/vr-glasses_8737003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/vr-glasses_8737003.png -------------------------------------------------------------------------------- /public/maskable-icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/maskable-icon-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpressLRS/web-flasher/HEAD/public/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /mdns-proxy/README.md: -------------------------------------------------------------------------------- 1 | Python 2 | === 3 | pip install zeroconf, gevent, bottle, requests 4 | 5 | C 6 | === 7 | MacosX/Linux 8 | cc -o epoxy proxy.c mdns.c 9 | 10 | Windows 11 | cl proxy.c mdns.c /link /out:epoxy.exe ws2_32.lib iphlpapi.lib 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "pwa-chrome", 5 | "name": "flasher", 6 | "request": "launch", 7 | "url": "http://localhost:8080" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "__config": "c", 4 | "locale": "c", 5 | "random": "c", 6 | "__functional_03": "c", 7 | "functional": "c", 8 | "complex": "c", 9 | "system_error": "c", 10 | "mdns.h": "c", 11 | "unistd.h": "c" 12 | } 13 | } -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [x] Implement ST-Link flashing 2 | - [x] Full chip-erase before flashing 3 | - [x] Advanced Options 4 | - [x] RX as TX 5 | - [x] Flashing branches 6 | - [x] Hide options other than bind-phrase & wifi settings 7 | - [x] (4) Flash Done 8 | - [x] With the buttons [Flash Another] [Back to Start] 9 | - [ ] WiFi flashing 10 | -------------------------------------------------------------------------------- /src/components/FanRuntime.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/js/error.js: -------------------------------------------------------------------------------- 1 | export class AlertError extends Error { 2 | constructor(title = undefined, message = undefined, type = 'error') { 3 | super(message) 4 | this.title = title 5 | this.type = type 6 | } 7 | } 8 | 9 | export class MismatchError extends Error { 10 | } 11 | 12 | export class PassthroughError extends Error { 13 | } 14 | 15 | export class WrongMCU extends Error { 16 | } -------------------------------------------------------------------------------- /src/components/WiFiAutoOn.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /.run/run dev.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /src/components/WiFiSettingsInput.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /src/components/MelodyInput.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 20 | -------------------------------------------------------------------------------- /src/components/BindPhraseInput.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | .containerMain { 2 | z-index: 9999; 3 | border: 1px solid #E8E8E8; 4 | background-color: #fff; 5 | padding: 40px; 6 | border-radius: 0.75rem; 7 | box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.16); 8 | } 9 | 10 | .containerHeader { 11 | margin-bottom: 40px; 12 | text-align: center; 13 | } 14 | 15 | .section { 16 | margin-top: -80px; 17 | display: flex; 18 | flex-direction: column; 19 | gap: 40px; 20 | position: relative; 21 | z-index: 1200; 22 | } 23 | 24 | @media (max-width: 640px) { 25 | .containerMain { 26 | padding: 20px; 27 | } 28 | 29 | .section { 30 | margin-top: -60px; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import '@mdi/font/css/materialdesignicons.css' 2 | import 'vuetify/styles' 3 | 4 | import {createApp} from 'vue' 5 | import {createVuetify} from 'vuetify' 6 | import * as vertical from 'vuetify/labs/VStepperVertical' 7 | 8 | import './main.css' 9 | import App from './App.vue' 10 | 11 | const vuetify = createVuetify({ 12 | components: {...vertical}, 13 | theme: { 14 | defaultTheme: 'light' 15 | }, 16 | defaults: { 17 | global: { 18 | density: "compact", 19 | }, 20 | VBtn: { 21 | density: "default" 22 | } 23 | } 24 | }) 25 | 26 | createApp(App) 27 | .use(vuetify) 28 | .mount('#app') 29 | -------------------------------------------------------------------------------- /src/components/RXasTX.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 19 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ExpressLRS Web Flasher 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/js/stlink/lib/package.js: -------------------------------------------------------------------------------- 1 | /* package.js 2 | * Module namespace for ST-Link library code 3 | * 4 | * Copyright Devan Lai 2017 5 | * 6 | */ 7 | 8 | import * as usb from './stlinkusb.js'; 9 | import * as exceptions from './stlinkex.js'; 10 | import Stlinkv2 from './stlinkv2.js'; 11 | import DEVICES from './stm32devices.js'; 12 | import * as semihosting from './semihosting.js'; 13 | 14 | 15 | import {Stm32} from './stm32.js'; 16 | import {Stm32FP, Stm32FPXL} from './stm32fp.js'; 17 | import {Stm32FS} from './stm32fs.js'; 18 | 19 | let drivers = { 20 | Stm32: Stm32, 21 | Stm32FP: Stm32FP, 22 | Stm32FPXL: Stm32FPXL, 23 | Stm32FS: Stm32FS 24 | }; 25 | 26 | export {exceptions, usb, drivers, Stlinkv2, DEVICES, semihosting}; 27 | -------------------------------------------------------------------------------- /src/js/stlink/lib/semihosting.js: -------------------------------------------------------------------------------- 1 | /* semihosting.js 2 | * ARM Semihosting debug I/O operations 3 | * 4 | * Copyright Devan Lai 2017 5 | * 6 | */ 7 | 8 | const opcodes = { 9 | SYS_OPEN: 0x01, 10 | SYS_CLOSE: 0x02, 11 | SYS_WRITEC: 0x03, 12 | SYS_WRITE0: 0x04, 13 | SYS_WRITE: 0x05, 14 | SYS_READ: 0x06, 15 | SYS_READC: 0x07, 16 | SYS_ISERROR: 0x08, 17 | SYS_ISTTY: 0x09, 18 | SYS_SEEK: 0x0A, 19 | SYS_FLEN: 0x0C, 20 | SYS_TMPNAM: 0x0D, 21 | SYS_REMOVE: 0x0E, 22 | SYS_RENAME: 0x0F, 23 | SYS_CLOCK: 0x10, 24 | SYS_TIME: 0x11, 25 | SYS_SYSTEM: 0x12, 26 | SYS_ERRNO: 0x13, 27 | SYS_GET_CMDLINE: 0x15, 28 | SYS_HEAPINFO: 0x016, 29 | SYS_ELAPSED: 0x030, 30 | SYS_TICKFREQ: 0x31, 31 | }; 32 | 33 | export {opcodes}; 34 | -------------------------------------------------------------------------------- /src/components/FlashMethodSelect.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /.run/get_artifacts.sh.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /src/components/TXOptions.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flasher2", 3 | "private": true, 4 | "version": "2.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@mdi/font": "7.4.47", 13 | "@zip.js/zip.js": "^2.8.11", 14 | "bluejay-rtttl-parse": "^2.0.2", 15 | "crypto-js": "^4.2.0", 16 | "esptool-js": "^0.5.7", 17 | "file-saver": "^2.0.5", 18 | "pako": "^2.1.0", 19 | "roboto-fontface": "*", 20 | "vue": "^3.5.25", 21 | "vuetify": "^3.11.3" 22 | }, 23 | "devDependencies": { 24 | "@types/file-saver": "^2.0.7", 25 | "@types/pako": "^2.0.4", 26 | "@types/w3c-web-serial": "^1.0.8", 27 | "@vitejs/plugin-vue": "^6.0.2", 28 | "sass": "1.95.1", 29 | "unplugin-fonts": "^1.4.0", 30 | "unplugin-vue-components": "^30.0.0", 31 | "vite-plugin-pwa": "^1.2.0", 32 | "vite-plugin-vuetify": "^2.1.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/ReloadPrompt.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 30 | 31 | -------------------------------------------------------------------------------- /src/js/stlink/lib/util.js: -------------------------------------------------------------------------------- 1 | /* util.js 2 | * Common helper functions 3 | * 4 | * Copyright Devan Lai 2017 5 | * 6 | */ 7 | function hex_octet(b) { 8 | return b.toString(16).padStart(2, "0"); 9 | } 10 | 11 | function hex_halfword(hw) { 12 | return hw.toString(16).padStart(4, "0"); 13 | } 14 | 15 | function hex_word(w) { 16 | return w.toString(16).padStart(8, "0"); 17 | } 18 | 19 | function hex_string(value, length) { 20 | return value.toString(16).padStart(length, "0"); 21 | } 22 | 23 | function hex_octet_array(arr) { 24 | return Array.from(arr, hex_octet); 25 | } 26 | 27 | function async_sleep(seconds) { 28 | return new Promise(function (resolve, reject) { 29 | setTimeout(resolve, seconds * 1000); 30 | }); 31 | } 32 | 33 | function async_timeout(seconds) { 34 | return new Promise(function (resolve, reject) { 35 | setTimeout(reject, seconds * 1000); 36 | }); 37 | } 38 | 39 | export { 40 | hex_octet, 41 | hex_halfword, 42 | hex_word, 43 | hex_string, 44 | hex_octet_array, 45 | async_sleep, 46 | async_timeout, 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/RFSelect.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 36 | -------------------------------------------------------------------------------- /src/js/stlink/lib/stlinkex.js: -------------------------------------------------------------------------------- 1 | /* stlinkex.js 2 | * ST-Link exception classes 3 | * 4 | * Copyright Devan Lai 2017 5 | * 6 | * Ported from lib/stlinkex.py in the pystlink project, 7 | * Copyright Pavel Revak 2015 8 | * 9 | */ 10 | 11 | import {hex_octet} from './util.js'; 12 | 13 | const Exception = class StlinkException extends Error { 14 | }; 15 | const Warning = class StlinkWarning extends Error { 16 | }; 17 | 18 | const UsbError = class StlinkUsbError extends Error { 19 | constructor(message, address, fatal = false) { 20 | super(message); 21 | if (message instanceof DOMException) { 22 | if (message.message == "Device unavailable.") { 23 | fatal = true; 24 | } 25 | } 26 | this.address = address; 27 | this.fatal = fatal; 28 | } 29 | 30 | toString() { 31 | if (this.address) { 32 | const addr_string = "0x" + hex_octet(this.address); 33 | return addr_string + ": " + this.message; 34 | } else { 35 | return this.message; 36 | } 37 | } 38 | }; 39 | 40 | export {Exception, Warning, UsbError}; 41 | -------------------------------------------------------------------------------- /src/js/version.js: -------------------------------------------------------------------------------- 1 | export const compareSemanticVersions = (a, b) => { 2 | // Split versions and discriminators 3 | const [v1, d1] = a.split('-') 4 | const [v2, d2] = b.split('-') 5 | 6 | // Split version sections 7 | const v1Sections = v1.split('.') 8 | const v2Sections = v2.split('.') 9 | 10 | // Compare main version numbers 11 | for (let i = 0; i < Math.max(v1Sections.length, v2Sections.length); i++) { 12 | const v1Section = parseInt(v1Sections[i] || 0, 10) 13 | const v2Section = parseInt(v2Sections[i] || 0, 10) 14 | 15 | if (v1Section > v2Section) return 1 16 | if (v1Section < v2Section) return -1 17 | } 18 | 19 | // If main versions are equal, compare discriminators 20 | if (!d1 && d2) return 1 // v1 is greater if it does not have discriminator 21 | if (d1 && !d2) return -1 // v2 is greater if it does not have a discriminator 22 | if (d1 && d2) return d1.localeCompare(d2) // Compare discriminators 23 | return 0 // Versions are equal 24 | } 25 | 26 | export const compareSemanticVersionsRC = (a, b) => { 27 | if (a === undefined || b === undefined) return 0; 28 | return compareSemanticVersions(a.replace(/-.*/, ''), b.replace(/-.*/, '')) 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/web.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master # Set a branch to deploy 7 | pull_request: 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | build: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Get supported target version artifacts 29 | run: | 30 | bash get_artifacts.sh 31 | GITHASH=`git rev-parse --short HEAD` 32 | sed -i~ "s/@GITHASH@/$GITHASH/" src/App.vue 33 | - name: Build 34 | run: npm install && npm run build 35 | - name: Setup Pages 36 | uses: actions/configure-pages@v5 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v3 39 | with: 40 | path: dist 41 | 42 | deploy: 43 | environment: 44 | name: github-pages 45 | url: ${{ steps.deployment.outputs.page_url }} 46 | runs-on: ubuntu-latest 47 | needs: build 48 | steps: 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v4 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExpressLRS Web Flasher 2 | 3 | This is a fully web-based flasher for ExpressLRS 3.x 4 | Currently supported flashing methods are: 5 | - UART (Receivers do not need to be in bootloader mode) 6 | - Betaflight passthrough 7 | - EdgeTX passthrough 8 | - STLink 9 | - ~~Wifi - with mdns lookup and 2.5 upgrade via a locally running proxy~~ 10 | 11 | Developed and maintained by **ExpressLRS LLC** and its passionate open source community, working together to advance reliable, high-performance radio control technology. 12 | 13 | # Developing and testing locally 14 | 15 | Checkout the git repository and run... 16 | ``` 17 | npm install 18 | ``` 19 | To start a development web server... 20 | ``` 21 | npm run dev 22 | ``` 23 | To build the distribution for stuffing on a web server 24 | ``` 25 | npm run build 26 | ``` 27 | # Firmware 28 | To actually test the code you will need a firmware folder at the root of the project. 29 | The firmware folder, with all it's accoutrement's can be downloaded from the ExpressLRS artifact repository by 30 | executing the `get_artifacts.sh` command. This will download all the release artifacts and put all the versions 31 | into the `index.js` file for testing locally. 32 | When committing you changes, you will note that there is a comment above where the versions were placed in the 33 | `index.js` file telling you not to commit changes to that line. So it is very important to revert the changes 34 | to the `versions` line before committing you changes. 35 | -------------------------------------------------------------------------------- /get_artifacts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p public/assets 4 | cd public/assets 5 | 6 | # Remove old firmware files 7 | rm -rf firmware backpack 8 | 9 | # Download main ELRS firmware, for each tagged version 10 | mkdir -p firmware 11 | cd firmware 12 | curl -L -o index.json https://artifactory.expresslrs.org/ExpressLRS/index.json 13 | for HASH in `cat index.json | jq '.tags,.branches | values[]' | sed 's/"//g' | sort -ru` ; do 14 | curl -L -o firmware.zip "https://artifactory.expresslrs.org/ExpressLRS/$HASH/firmware.zip" 15 | mkdir $HASH 16 | cd $HASH 17 | unzip -q ../firmware.zip 18 | mv firmware/* . 19 | rm -rf firmware 20 | cd .. 21 | rm firmware.zip 22 | done 23 | 24 | # Download the published hardware targets into the `firmware` directory 25 | mkdir hardware 26 | cd hardware 27 | curl -L -o hardware.zip https://artifactory.expresslrs.org/ExpressLRS/hardware.zip 28 | unzip -q hardware.zip 29 | rm hardware.zip 30 | 31 | # Download backpack firmware, for each tagged version 32 | cd ../.. 33 | mkdir -p backpack 34 | cd backpack 35 | curl -L -o index.json https://artifactory.expresslrs.org/Backpack/index.json 36 | for HASH in `cat index.json | jq '.tags,.branches | values[]' | sed 's/"//g' | sort -ru` ; do 37 | curl -L -o firmware.zip "https://artifactory.expresslrs.org/Backpack/$HASH/firmware.zip" 38 | mkdir $HASH 39 | cd $HASH 40 | unzip -q ../firmware.zip 41 | mv firmware/* . 42 | rm -rf firmware 43 | cd .. 44 | rm firmware.zip 45 | done 46 | -------------------------------------------------------------------------------- /src/js/stlink/mutex.js: -------------------------------------------------------------------------------- 1 | /* mutex.js 2 | * Mutex/Condition Variable for serializing access to a shared resource 3 | * 4 | * Copyright Devan Lai 2017 5 | * 6 | */ 7 | 8 | export default class Mutex { 9 | constructor() { 10 | this.queue = []; 11 | this.locked = false; 12 | this.destroyed = false; 13 | } 14 | 15 | async lock() { 16 | if (this.destroyed) { 17 | throw new Error("Mutex is no longer available"); 18 | } 19 | 20 | if (this.locked) { 21 | let promise = new Promise((resolve, reject) => { 22 | this.queue.push({"resolve": resolve, "reject": reject}); 23 | }); 24 | await promise; 25 | } else { 26 | this.locked = true; 27 | } 28 | } 29 | 30 | unlock() { 31 | // Signal the first waiting task that they can acquire the mutex 32 | if (this.locked) { 33 | if (this.queue.length > 0) { 34 | this.queue.shift().resolve(); 35 | } else { 36 | this.locked = false; 37 | } 38 | } else { 39 | throw new Error("Mutex was already unlocked"); 40 | } 41 | } 42 | 43 | destroy() { 44 | // Signal all waiting tasks that the resource protected by this 45 | // mutex is no longer accessible 46 | for (let promise of this.queue) { 47 | promise.reject(); 48 | } 49 | 50 | this.locked = true; 51 | this.destroyed = true; 52 | this.queue = []; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/pages/BackpackOptions.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /src/pages/TransmitterOptions.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/pages/ReceiverOptions.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: push 4 | 5 | defaults: 6 | run: 7 | shell: bash 8 | 9 | jobs: 10 | doze: 11 | name: Windows build 12 | runs-on: windows-2019 13 | steps: 14 | - name: Checkout Repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Add msbuild to PATH 18 | uses: ilammy/msvc-dev-cmd@v1 19 | 20 | - name: Compile a release 21 | run: | 22 | cd mdns-proxy 23 | cl proxy.c mdns.c -link ws2_32.lib iphlpapi.lib -out:epoxy-win.exe 24 | 25 | - name: upload artifact 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: upload-doze 29 | path: mdns-proxy/epoxy-win.exe 30 | 31 | mac: 32 | name: MacOS build 33 | runs-on: macos-13 34 | steps: 35 | - name: Checkout Repository 36 | uses: actions/checkout@v4 37 | 38 | - name: Compile a release 39 | run: | 40 | cd mdns-proxy 41 | cc -o epoxy-mac proxy.c mdns.c 42 | 43 | - name: upload artifact 44 | uses: actions/upload-artifact@v4 45 | with: 46 | name: upload-mac 47 | path: mdns-proxy/epoxy-mac 48 | 49 | linux: 50 | name: Linux build 51 | runs-on: ubuntu-20.04 52 | steps: 53 | - name: Checkout Repository 54 | uses: actions/checkout@v4 55 | 56 | - name: Compile a release 57 | run: | 58 | cd mdns-proxy 59 | cc -o epoxy-lin proxy.c mdns.c 60 | 61 | - name: upload artifact 62 | uses: actions/upload-artifact@v4 63 | with: 64 | name: upload-linux 65 | path: mdns-proxy/epoxy-lin 66 | 67 | merge: 68 | runs-on: ubuntu-latest 69 | needs: [doze, mac, linux] 70 | steps: 71 | - name: Merge Artifacts 72 | uses: actions/upload-artifact/merge@v4 73 | with: 74 | name: uploads 75 | pattern: upload-* 76 | -------------------------------------------------------------------------------- /src/js/state.js: -------------------------------------------------------------------------------- 1 | import {reactive} from 'vue' 2 | 3 | export const store = reactive({ 4 | currentStep: 1, 5 | firmware: null, 6 | folder: '', 7 | targetType: null, 8 | version: null, 9 | vendor: null, 10 | vendor_name: '', 11 | radio: null, 12 | target: null, 13 | name: '', 14 | options: { 15 | uid: null, 16 | region: 'FCC', 17 | domain: 1, 18 | ssid: null, 19 | password: null, 20 | wifiOnInternal: 60, 21 | tx: { 22 | telemetryInterval: 240, 23 | uartInverted: true, 24 | fanMinRuntime: 30, 25 | higherPower: false, 26 | melodyType: 3, 27 | melodyTune: null, 28 | }, 29 | rx: { 30 | uartBaud: 420000, 31 | lockOnFirstConnect: true, 32 | r9mmMiniSBUS: false, 33 | fanMinRuntime: 30, 34 | rxAsTx: false, 35 | rxAsTxType: 0 // 0 = Internal (Full-duplex), 1 = External (Half-duplex) 36 | }, 37 | flashMethod: null, 38 | } 39 | }) 40 | 41 | export function resetState() { 42 | store.currentStep = 1 43 | store.firmware = null 44 | store.folder = '' 45 | store.targetType = null 46 | store.version = null 47 | store.vendor = null 48 | store.radio = null 49 | store.target = null 50 | store.options = { 51 | uid: null, 52 | region: 'FCC', 53 | domain: 1, 54 | ssid: null, 55 | password: null, 56 | wifiOnInternal: 60, 57 | tx: { 58 | telemetryInterval: 240, 59 | uartInverted: true, 60 | fanMinRuntime: 30, 61 | higherPower: false, 62 | melodyType: 3, 63 | melodyTune: null, 64 | }, 65 | rx: { 66 | uartBaud: 420000, 67 | lockOnFirstConnect: true, 68 | r9mmMiniSBUS: false, 69 | fanMinRuntime: 30, 70 | }, 71 | flashMethod: null, 72 | } 73 | } 74 | 75 | export function hasFeature(feature) { 76 | return store.target?.config?.features?.includes(feature) 77 | } 78 | -------------------------------------------------------------------------------- /src/components/HoverCard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | 30 | -------------------------------------------------------------------------------- /src/js/melody.js: -------------------------------------------------------------------------------- 1 | import Rtttl from 'bluejay-rtttl-parse' 2 | 3 | export class MelodyParser { 4 | static #NOTES = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'] 5 | 6 | static #getFrequency(note, transposeBy = 0, A4 = 440) { 7 | // example note: A#5, meaning: 5th octave A sharp 8 | const octave = note.length === 3 ? Number(note[2]) : Number(note[1]) 9 | let keyNumber = this.#NOTES.indexOf(note.slice(0, -1)) 10 | if (keyNumber < 3) { 11 | keyNumber = keyNumber + 12 + ((octave - 1) * 12) + 1 12 | } else { 13 | keyNumber = keyNumber + ((octave - 1) * 12) + 1 14 | } 15 | keyNumber += transposeBy 16 | return Math.floor(A4 * 2 ** ((keyNumber - 49) / 12.0)) 17 | } 18 | 19 | static #getDurationInMs(bpm, duration) { 20 | return Math.floor((1000 * (60 * 4 / bpm)) / duration) 21 | } 22 | 23 | static #parseMelody(melodyString, bpm = 120, transposeBySemitones = 0) { 24 | // parse string to python list 25 | const tokenizedNotes = melodyString.split(' ') 26 | const operations = [] 27 | for (let i = 0; i < tokenizedNotes.length; i++) { 28 | const token = tokenizedNotes[i] 29 | const nextToken = tokenizedNotes[i + 1] 30 | console.log(token, nextToken) 31 | if (token[0] === 'P') { 32 | // Token is a pause operation, use frequency 0 33 | operations.push([0, this.#getDurationInMs(bpm, token.substring(1))]) 34 | } else if ('ABCDEFG'.indexOf(token[0]) !== -1) { 35 | // Token is a note; next token will be duration of this note 36 | const frequency = this.#getFrequency(token, transposeBySemitones) 37 | const duration = this.#getDurationInMs(bpm, nextToken) 38 | operations.push([frequency, duration]) 39 | } 40 | } 41 | return operations 42 | } 43 | 44 | static parseToArray(melodyOrRTTTL) { 45 | if (melodyOrRTTTL.indexOf('|') !== -1) { 46 | const defineValue = melodyOrRTTTL.split('|') 47 | const transposeBySemitones = defineValue.length > 2 ? Number(defineValue[2]) : 0 48 | return this.#parseMelody(defineValue[0].trim(), Number(defineValue[1]), transposeBySemitones) 49 | } else { 50 | const melody = Rtttl.parse(melodyOrRTTTL).melody.map((v) => [Math.floor(v.frequency), Math.floor(v.duration)]) 51 | if (melody.length > 32) melody.length = 32 52 | return melody 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/js/phrase.js: -------------------------------------------------------------------------------- 1 | const md5 = (function () { 2 | const k = [] 3 | let i = 0 4 | 5 | for (; i < 64;) { 6 | k[i] = 0 | (Math.abs(Math.sin(++i)) * 4294967296) 7 | } 8 | 9 | function calcMD5(str) { 10 | let b; 11 | let c; 12 | let d; 13 | let j 14 | const x = [] 15 | const str2 = unescape(encodeURI(str)) 16 | let a = str2.length 17 | const h = [b = 1732584193, c = -271733879, ~b, ~c] 18 | let i = 0 19 | 20 | for (; i <= a;) x[i >> 2] |= (str2.charCodeAt(i) || 128) << 8 * (i++ % 4) 21 | 22 | x[str = (a + 8 >> 6) * 16 + 14] = a * 8 23 | i = 0 24 | 25 | for (; i < str; i += 16) { 26 | a = h; 27 | j = 0 28 | for (; j < 64;) { 29 | a = [ 30 | d = a[3], 31 | ((b = a[1] | 0) + 32 | ((d = ( 33 | (a[0] + 34 | [ 35 | b & (c = a[2]) | ~b & d, 36 | d & b | ~d & c, 37 | b ^ c ^ d, 38 | c ^ (b | ~d) 39 | ][a = j >> 4] 40 | ) + 41 | (k[j] + 42 | (x[[ 43 | j, 44 | 5 * j + 1, 45 | 3 * j + 5, 46 | 7 * j 47 | ][a] % 16 + i] | 0) 48 | ) 49 | )) << (a = [ 50 | 7, 12, 17, 22, 51 | 5, 9, 14, 20, 52 | 4, 11, 16, 23, 53 | 6, 10, 15, 21 54 | ][4 * a + j++ % 4]) | d >>> 32 - a) 55 | ), 56 | b, 57 | c 58 | ] 59 | } 60 | for (j = 4; j;) h[--j] = h[j] + a[j] 61 | } 62 | 63 | str = [] 64 | for (; j < 32;) str.push(((h[j >> 3] >> ((1 ^ j++ & 7) * 4)) & 15) * 16 + ((h[j >> 3] >> ((1 ^ j++ & 7) * 4)) & 15)) 65 | 66 | return new Uint8Array(str) 67 | } 68 | 69 | return calcMD5 70 | }()) 71 | 72 | export function uidBytesFromText(text) { 73 | const bindingPhraseFull = `-DMY_BINDING_PHRASE="${text}"` 74 | const bindingPhraseHashed = md5(bindingPhraseFull) 75 | return bindingPhraseHashed.subarray(0, 6) 76 | } 77 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import vuetify from 'vite-plugin-vuetify' 4 | import { VitePWA } from 'vite-plugin-pwa' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | appType: 'mpa', 9 | plugins: [ 10 | vue(), 11 | vuetify(), 12 | VitePWA({ 13 | workbox: { 14 | globPatterns: ['**/*.{js,css,html,ico,png,svg}'], 15 | runtimeCaching: [ 16 | { 17 | // Cache firmware files for a device once they are requested the first time 18 | // This allows building again when being offline 19 | urlPattern: /\/assets\/(firmware|backpack)\/.*/i, 20 | handler: 'CacheFirst', 21 | options: { 22 | cacheName: 'firmwares', 23 | expiration: { 24 | maxEntries: 50, 25 | }, 26 | }, 27 | }, 28 | { 29 | // Cache fonts 30 | urlPattern: /\/assets\/.*\.(ttf|eot|woff|woff2)/i, 31 | handler: 'CacheFirst', 32 | options: { 33 | cacheName: 'fonts', 34 | expiration: { 35 | maxEntries: 50, 36 | }, 37 | }, 38 | }, 39 | ], 40 | }, 41 | // These files will always be available offline 42 | includeAssets: [ 43 | 'favicon.ico', 44 | 'apple-touch-icon.png', 45 | 'mask-icon.svg', 46 | 'assets/{firmware,backpack}/index.json', 47 | 'assets/{firmware,backpack}/**/targets.json', 48 | ], 49 | manifest: { 50 | name: 'ExpressLRS Web Flasher', 51 | short_name: 'ELRS Web Flasher', 52 | description: 'Web-hosted flasher for ExpressLRS version 3 firmware', 53 | theme_color: '#4a88ab', 54 | display_override: ['window-controls-overlay', 'standalone', 'browser'], 55 | icons: [ 56 | { 57 | src: 'pwa-64x64.png', 58 | sizes: '64x64', 59 | type: 'image/png' 60 | }, 61 | { 62 | src: 'pwa-192x192.png', 63 | sizes: '192x192', 64 | type: 'image/png' 65 | }, 66 | { 67 | src: 'pwa-512x512.png', 68 | sizes: '512x512', 69 | type: 'image/png', 70 | purpose: 'any' 71 | }, 72 | { 73 | src: 'maskable-icon-512x512.png', 74 | sizes: '512x512', 75 | type: 'image/png', 76 | purpose: 'maskable' 77 | } 78 | ], 79 | }, 80 | }) 81 | ], 82 | base: './' 83 | }) 84 | -------------------------------------------------------------------------------- /src/pages/Download.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 73 | -------------------------------------------------------------------------------- /src/js/firmware.js: -------------------------------------------------------------------------------- 1 | import {store} from "./state.js"; 2 | import {Configure} from "./configure.js"; 3 | 4 | const getSettings = async (deviceType) => { 5 | const options = { 6 | 'flash-discriminator': Math.floor(Math.random() * ((2 ** 31) - 2) + 1) 7 | } 8 | 9 | if (store.options.uid) { 10 | options.uid = store.options.uid 11 | } 12 | if (store.target.config.platform !== 'stm32') { 13 | options['wifi-on-interval'] = store.options.wifiOnInternal 14 | if (store.options.ssid) { 15 | options['wifi-ssid'] = store.options.ssid 16 | options['wifi-password'] = store.options.password 17 | } 18 | } 19 | let firmwareUrl 20 | if (store.firmware === 'firmware') { 21 | firmwareUrl = `./assets/firmware/${store.version}/${store.options.region}/${store.target.config.firmware}/firmware.bin` 22 | if (deviceType === 'RX' && !store.options.rx.rxAsTx) { 23 | options['rcvr-uart-baud'] = store.options.rx.uartBaud 24 | options['lock-on-first-connection'] = store.options.rx.lockOnFirstConnect 25 | } else { 26 | options['tlm-interval'] = store.options.tx.telemetryInterval 27 | options['fan-runtime'] = store.options.tx.fanMinRuntime 28 | options['uart-inverted'] = store.options.tx.uartInverted 29 | options['unlock-higher-power'] = store.options.tx.higherPower 30 | } 31 | if (store.radio.endsWith('_900') || store.radio.endsWith('_dual')) { 32 | options.domain = store.options.domain 33 | } 34 | if (store.target.config.features !== undefined && store.target.config.features.indexOf('buzzer') !== -1) { 35 | const beeptype = store.options.tx.melodyType 36 | options.beeptype = beeptype > 2 ? 2 : beeptype 37 | 38 | const melodyModule = await import('../js/melody.js') 39 | if (beeptype === 2) { 40 | options.melody = melodyModule.MelodyParser.parseToArray('A4 20 B4 20|60|0') 41 | } else if (beeptype === 3) { 42 | options.melody = melodyModule.MelodyParser.parseToArray('E5 40 E5 40 C5 120 E5 40 G5 22 G4 21|20|0') 43 | } else if (beeptype === 4) { 44 | options.melody = melodyModule.MelodyParser.parseToArray(store.options.tx.melodyTune) 45 | } else { 46 | options.melody = [] 47 | } 48 | } 49 | } else { 50 | options['product-name'] = store.target.config.product_name 51 | firmwareUrl = `./assets/backpack/${store.version}/${store.target.config.firmware}/firmware.bin` 52 | } 53 | let config = store.target.config 54 | return {config, firmwareUrl, options} 55 | } 56 | 57 | export async function generateFirmware() { 58 | let deviceType = store.targetType 59 | let radioType = null 60 | let txType = null 61 | if (store.firmware === 'firmware') { 62 | deviceType = store.targetType === 'tx' ? 'TX' : 'RX' 63 | radioType = store.radio.endsWith('_900') ? 'sx127x' : (store.radio.endsWith('_2400') ? 'sx128x' : 'lr1121') 64 | txType = undefined 65 | if (store.targetType === 'rx' && store.options.rx.rxAsTx) 66 | txType = store.options.rx.rxAsTxType ? 'external' : 'internal' 67 | } 68 | const {config, firmwareUrl, options} = await getSettings(deviceType) 69 | const firmwareFiles = await Configure.download(store.folder, store.version, deviceType, txType, radioType, config, firmwareUrl, options) 70 | return [ 71 | firmwareFiles, 72 | {config, firmwareUrl, options, deviceType, radioType, txType} 73 | ] 74 | } -------------------------------------------------------------------------------- /src/js/serialex.js: -------------------------------------------------------------------------------- 1 | import {Transport} from 'esptool-js' 2 | 3 | export class TransportEx extends Transport { 4 | ui8ToBstr(u8Array) { 5 | let bStr = '' 6 | for (let i = 0; i < u8Array.length; i++) { 7 | bStr += String.fromCharCode(u8Array[i]) 8 | } 9 | return bStr 10 | } 11 | 12 | bstrToUi8(bStr) { 13 | const len = bStr.length 14 | const u8array = new Uint8Array(len) 15 | for (let i = 0; i < len; i++) { 16 | u8array[i] = bStr.charCodeAt(i) 17 | } 18 | return u8array 19 | } 20 | 21 | set_delimiters(delimiters = ['\n', 'CCC']) { 22 | this.delimiters = [] 23 | for (const d of delimiters) { 24 | this.delimiters.push(this.bstrToUi8(d)) 25 | } 26 | } 27 | 28 | read_line = async (timeout = 0) => { 29 | console.log('Read with timeout ' + timeout) 30 | let t 31 | let packet = this.buffer 32 | this.buffer = new Uint8Array(0) 33 | const delimiters = this.delimiters 34 | 35 | function findDelimeter(packet) { 36 | const index = packet.findIndex((_, i, a) => { 37 | for (const d of delimiters) { 38 | if (d.every((v, j) => a[i + j] === v)) return true 39 | } 40 | return false 41 | }) 42 | if (index !== -1) { 43 | for (const d of delimiters) { 44 | if (d.every((v, j) => packet[index + j] === v)) return index + d.length 45 | } 46 | } 47 | return -1 48 | } 49 | 50 | let index = findDelimeter(packet) 51 | if (index === -1) { 52 | const reader = this.reader 53 | try { 54 | if (timeout > 0) { 55 | t = setTimeout(function () { 56 | reader.cancel() 57 | }, timeout) 58 | } 59 | do { 60 | const {value, done} = await reader.read() 61 | if (done) { 62 | await this.disconnect() 63 | await this.connect(this.baudrate) 64 | return '' 65 | } 66 | packet = this.appendArray(packet, value) 67 | index = findDelimeter(packet) 68 | } while (index === -1) 69 | } finally { 70 | if (timeout > 0) { 71 | clearTimeout(t) 72 | } 73 | } 74 | } 75 | this.buffer = packet.slice(index) 76 | packet = packet.slice(0, index) 77 | if (this.tracing) { 78 | console.log('Read bytes') 79 | console.log(this.hexConvert(packet)) 80 | } 81 | return this.ui8ToBstr(packet) 82 | } 83 | 84 | write_string = async (data) => { 85 | const writer = this.device.writable.getWriter() 86 | const out = this.bstrToUi8(data) 87 | if (this.tracing) { 88 | console.log('Write bytes') 89 | console.log(this.hexConvert(out)) 90 | } 91 | await writer.write(out.buffer) 92 | writer.releaseLock() 93 | } 94 | 95 | write_array = async (data) => { 96 | const writer = this.device.writable.getWriter() 97 | if (this.tracing) { 98 | console.log('Write bytes') 99 | console.log(this.hexConvert(data)) 100 | } 101 | await writer.write(data.buffer) 102 | writer.releaseLock() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/pages/FirmwareSelect.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 74 | 75 | -------------------------------------------------------------------------------- /mdns-proxy/elrs-proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from typing import Dict 4 | from zeroconf import ServiceBrowser, ServiceInfo, ServiceListener, Zeroconf 5 | from bottle import Bottle, request, response 6 | import requests 7 | import json 8 | import socket 9 | 10 | mdnsServices = {} 11 | 12 | class MDNSListener(ServiceListener): 13 | def convert(self, info: ServiceInfo) -> Dict: 14 | d = {} 15 | d['name'] = info.name 16 | d['address'] = socket.inet_ntoa(info.addresses[0]) 17 | d['port'] = info.port 18 | d['properties'] = {} 19 | for k,v in info.properties.items(): 20 | d['properties'][k.decode('utf-8')] = v.decode('utf-8') 21 | return d 22 | 23 | def update_service(self, zc: Zeroconf, type_: str, name: str) -> None: 24 | info = zc.get_service_info(type_, name) 25 | device = self.convert(info) 26 | if 'vendor' in device['properties'] and device['properties']['vendor'] == 'elrs': 27 | mdnsServices[name] = device 28 | print('Device updated: ' + name) 29 | 30 | def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: 31 | if mdnsServices.pop(name) != None: 32 | print('Device removed: ' + name) 33 | 34 | def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: 35 | info = zc.get_service_info(type_, name) 36 | device = self.convert(info) 37 | if 'vendor' in device['properties'] and device['properties']['vendor'] == 'elrs': 38 | mdnsServices[name] = device 39 | print('Device added: ' + name) 40 | 41 | zeroconf = Zeroconf() 42 | listener = MDNSListener() 43 | browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener) 44 | 45 | PORT = 9097 46 | 47 | app = Bottle() 48 | 49 | POST_HEADERS = { 50 | 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 51 | 'Access-Control-Allow-Headers': 'Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With, X-FileSize', 52 | } 53 | 54 | @app.get('/mdns', method=['GET']) 55 | def mdns(): 56 | response.status = 200 57 | response.set_header('Access-Control-Allow-Origin', '*') 58 | return json.dumps(mdnsServices) 59 | 60 | @app.get('/', method=['GET', 'OPTIONS']) 61 | def get(path): 62 | response.set_header('Access-Control-Allow-Origin', '*') 63 | qstring = request.query_string 64 | qstring = ('?' + qstring) if qstring else '' 65 | url = '{}://{}{}'.format(request.urlparts[0], path, qstring) 66 | ct = request.content_type 67 | header = {'Content-Type': ct} if ct else None 68 | 69 | if request.method == 'GET': 70 | r = requests.get(url, headers=header) 71 | response.status = r.status_code 72 | return r.text 73 | else: 74 | for k, v in POST_HEADERS.items(): 75 | response.set_header(k, v) 76 | return 77 | 78 | @app.get('/', method=['POST']) 79 | def get(path): 80 | response.set_header('Access-Control-Allow-Origin', '*') 81 | qstring = request.query_string 82 | qstring = ('?' + qstring) if qstring else '' 83 | url = '{}://{}{}'.format(request.urlparts[0], path, qstring) 84 | 85 | form_data = request.forms.dict 86 | file_data = request.files.get('upload') 87 | files = {file_data.name: (file_data.filename, file_data.file)} if file_data is not None else None 88 | headers = {} 89 | for k, v in request.headers.items(): 90 | headers[k] = v 91 | headers.pop('Content-Type') 92 | r = requests.post(url, data=form_data, files=files, headers=headers) 93 | 94 | response.status = r.status_code 95 | return r.text 96 | 97 | try: 98 | print('Starting ELRS proxy on port {}'.format(PORT)) 99 | print("Press ^C to exit...\n") 100 | app.run(host='0.0.0.0', port=PORT, quiet=True) 101 | finally: 102 | zeroconf.close() 103 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /src/pages/BackpackHardwareSelect.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | -------------------------------------------------------------------------------- /src/pages/STLinkFlash.vue: -------------------------------------------------------------------------------- 1 | 121 | 122 | 184 | -------------------------------------------------------------------------------- /src/js/espflasher.js: -------------------------------------------------------------------------------- 1 | import {TransportEx} from './serialex.js' 2 | import {CustomReset, ESPLoader} from 'esptool-js' 3 | import {Passthrough} from './passthrough.js' 4 | import CryptoJS from 'crypto-js' 5 | import {MismatchError, WrongMCU} from "./error.js"; 6 | 7 | export class ESPFlasher { 8 | constructor(device, type, method, config, options, firmwareUrl, term) { 9 | this.device = device 10 | this.type = type 11 | this.method = method 12 | this.config = config 13 | this.options = options 14 | this.firmwareUrl = firmwareUrl 15 | this.term = term 16 | this.mainFirmware = type === 'TX' || type === 'RX' 17 | } 18 | 19 | connect = async () => { 20 | let mode = 'default_reset' 21 | let baudrate = 460800 22 | let initbaud 23 | if (this.method === 'betaflight') { 24 | baudrate = 420000 25 | mode = 'no_reset' 26 | } else if (this.method === 'etx') { 27 | if (this.mainFirmware) { 28 | baudrate = 230400 29 | } 30 | mode = 'no_reset' 31 | } else if (this.method === 'passthru') { 32 | baudrate = 230400 33 | mode = 'no_reset' 34 | } else if (this.method === 'uart' && this.config.platform.startsWith('esp32')) { 35 | initbaud = 115200 36 | } 37 | if (this.config.baud) { 38 | baudrate = this.config.baud 39 | initbaud = this.config.baud 40 | } 41 | 42 | const transport = new TransportEx(this.device, false) 43 | const terminal = { 44 | clean: () => { 45 | }, 46 | writeLine: (data) => this.term.writeln(data), 47 | write: (data) => this.term.write(data) 48 | } 49 | this.esploader = new ESPLoader({ 50 | transport, 51 | baudrate, 52 | terminal, 53 | romBaudrate: initbaud === undefined ? baudrate : initbaud 54 | }) 55 | this.esploader.ESP_RAM_BLOCK = 0x0800 // we override otherwise flashing on BF will fail 56 | 57 | let hasError 58 | const passthrough = new Passthrough(transport, this.term, this.config.firmware, baudrate) 59 | try { 60 | if (this.method === 'uart') { 61 | if (this.type === 'RX' && !this.config.platform.startsWith('esp32')) { 62 | await transport.connect(baudrate) 63 | const ret = await this.esploader._connectAttempt(mode = 'no_reset', new CustomReset(transport, 'W0')) 64 | 65 | if (ret !== 'success') { 66 | await transport.disconnect() 67 | await transport.connect(420000) 68 | await passthrough.reset_to_bootloader() 69 | } 70 | } else { 71 | await transport.connect(115200) 72 | } 73 | } else if (this.method === 'betaflight') { 74 | await transport.connect(baudrate) 75 | await passthrough.betaflight() 76 | await passthrough.reset_to_bootloader() 77 | } else if (this.method === 'etx') { 78 | await transport.connect(baudrate) 79 | if (this.mainFirmware) { 80 | await passthrough.edgeTX() 81 | } else { 82 | await passthrough.edgeTXBP() 83 | } 84 | } else if (this.method === 'passthru') { 85 | await transport.connect(baudrate) 86 | await transport.setDTR(false) 87 | await transport.sleep(100) 88 | await transport.setRTS(false) 89 | await transport.sleep(5000) 90 | await transport.setDTR(true) 91 | await transport.sleep(200) 92 | await transport.setDTR(false) 93 | await transport.sleep(100) 94 | } 95 | } catch(e) { 96 | if (!(e instanceof MismatchError)) { 97 | throw e 98 | } 99 | hasError = e 100 | } 101 | 102 | await transport.disconnect() 103 | 104 | const chip = await this.esploader.main(mode) 105 | if ((this.esploader.chip.CHIP_NAME === 'ESP8266' && this.config.platform !== 'esp8285') || 106 | (this.esploader.chip.CHIP_NAME === 'ESP32-C3' && this.config.platform !== 'esp32-c3') || 107 | (this.esploader.chip.CHIP_NAME === 'ESP32-S3' && this.config.platform !== 'esp32-s3') || 108 | (this.esploader.chip.CHIP_NAME === 'ESP32' && this.config.platform !== 'esp32')) { 109 | throw new WrongMCU(`Wrong target selected, this device uses '${chip}' and the firmware is for '${this.config.platform}'`) 110 | } 111 | console.log(`Settings done for ${chip}`) 112 | 113 | if (hasError) { 114 | throw hasError 115 | } 116 | return this.esploader.chip.CHIP_NAME 117 | } 118 | 119 | flash = async (files, erase, progress) => { 120 | const loader = this.esploader 121 | if (this.method === 'etx' || this.method === 'betaflight') { 122 | loader.FLASH_WRITE_SIZE = 0x0800 123 | if (this.config.platform.startsWith('esp32') && this.method === 'betaflight') { 124 | files = files.slice(-1) 125 | } 126 | } 127 | 128 | const fileArray = files.map(v => ({data: loader.ui8ToBstr(v.data), address: v.address})) 129 | loader.IS_STUB = true 130 | return loader.writeFlash({ 131 | fileArray, 132 | flashSize: 'keep', 133 | flashMode: 'keep', 134 | flashFreq: 'keep', 135 | eraseAll: erase, 136 | compress: true, 137 | reportProgress: progress, 138 | calculateMD5Hash: (image) => CryptoJS.MD5(CryptoJS.enc.Latin1.parse(image)) 139 | }) 140 | .then(_ => { 141 | progress(fileArray.length - 1, 100, 100) 142 | if (this.config.platform.startsWith('esp32')) { 143 | return loader.after('hard_reset').catch(() => { 144 | }) 145 | } else { 146 | return loader.after('soft_reset').catch(() => { 147 | }) 148 | } 149 | }) 150 | } 151 | 152 | close = async () => { 153 | await this.esploader.transport.disconnect() 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/js/stlink/lib/stlinkusb.js: -------------------------------------------------------------------------------- 1 | /* stlinkusb.js 2 | * Low-level ST-Link USB communication wrapper class 3 | * 4 | * Copyright Devan Lai 2017 5 | * 6 | * Ported from lib/stlinkusb.py in the pystlink project, 7 | * Copyright Pavel Revak 2015 8 | * 9 | */ 10 | 11 | import {Exception, UsbError} from './stlinkex.js'; 12 | import {hex_halfword as H16, hex_octet_array} from './util.js'; 13 | 14 | const STLINK_CMD_SIZE_V2 = 16; 15 | const DEV_TYPES = [ 16 | { 17 | 'version': 'V2', 18 | 'idVendor': 0x0483, 19 | 'idProduct': 0x3748, 20 | 'outPipe': 0x02, 21 | 'inPipe': 0x81 22 | }, 23 | { 24 | 'version': 'V2-1', 25 | 'idVendor': 0x0483, 26 | 'idProduct': 0x374b, 27 | 'outPipe': 0x01, 28 | 'inPipe': 0x81 29 | } 30 | ]; 31 | 32 | var Connector = class StlinkUsbConnector { 33 | constructor(dev, dbg = null) { 34 | this._dbg = dbg; 35 | this._dev = null; 36 | this._dev_type = null; 37 | for (let dev_type of DEV_TYPES) { 38 | if (dev.vendorId == dev_type.idVendor && dev.productId == dev_type.idProduct) { 39 | this._dev = dev; 40 | this._dev_type = dev_type; 41 | return; 42 | } 43 | } 44 | 45 | throw new Exception(`Unknown ST-Link/V2 type ${H16(dev.vendorId)}:${H16(dev.productId)}`); 46 | } 47 | 48 | async connect() { 49 | await this._dev.open(); 50 | if (this._dev.configuration != 1) { 51 | await this._dev.selectConfiguration(1); 52 | } 53 | let intf = this._dev.configuration.interfaces[0]; 54 | if (!intf.claimed) { 55 | await this._dev.claimInterface(0); 56 | } 57 | if (intf.alternate === null || intf.alternate.alternateSetting != 0) { 58 | await this._dev.selectAlternateInterface(0, 0); 59 | } 60 | } 61 | 62 | async disconnect() { 63 | try { 64 | await this._dev.close(); 65 | } catch (error) { 66 | if (this._dbg) { 67 | this._dbg.debug("Error when disconnecting: " + error); 68 | } 69 | } 70 | } 71 | 72 | get version() { 73 | return this._dev_type.version; 74 | } 75 | 76 | get xfer_counter() { 77 | return this._xfer_counter; 78 | } 79 | 80 | _debug(msg) { 81 | if (this._dbg) { 82 | this._dbg.debug(msg); 83 | } 84 | } 85 | 86 | async _write(data) { 87 | if (this._dbg) { 88 | const bytes = new Uint8Array(data); 89 | const hex_bytes = hex_octet_array(bytes); 90 | this._dbg.debug(" USB > " + hex_bytes.join(" ")); 91 | } 92 | 93 | this._xfer_counter++; 94 | let result; 95 | try { 96 | result = await this._dev.transferOut(this._dev_type.outPipe, data); 97 | if (result.status != "ok") { 98 | throw result.status; 99 | } 100 | } catch (e) { 101 | if (e instanceof DOMException || e.constructor == String) { 102 | throw new UsbError(e, this._dev_type.outPipe); 103 | } else { 104 | throw e; 105 | } 106 | } 107 | 108 | if (result.bytesWritten != data.length) { 109 | throw new Exception(`Error, only ${result.bytesWritten} Bytes was transmitted to ST-Link instead of expected ${data.length}`); 110 | } 111 | } 112 | 113 | async _read(size) { 114 | let read_size = size; 115 | if (read_size < 64) { 116 | read_size = 64; 117 | } else if (read_size % 4) { 118 | read_size += 3; 119 | read_size &= 0xffc; 120 | } 121 | 122 | let result; 123 | try { 124 | result = await this._dev.transferIn(this._dev_type.inPipe & 0x7f, read_size); 125 | if (result.status != "ok") { 126 | throw result.status; 127 | } 128 | } catch (e) { 129 | if (e instanceof DOMException || e.constructor == String) { 130 | throw new UsbError(e, this._dev_type.inPipe); 131 | } else { 132 | throw e; 133 | } 134 | } 135 | 136 | if (this._dbg) { 137 | const bytes = new Uint8Array(result.data.buffer); 138 | const hex_bytes = hex_octet_array(bytes); 139 | this._dbg.debug(" USB < " + hex_bytes.join(" ")); 140 | } 141 | 142 | if (result.data.byteLength > size) { 143 | return new DataView(result.data.buffer, result.data.byteOffset, size); 144 | } else { 145 | return result.data; 146 | } 147 | } 148 | 149 | async xfer(cmd, {data = null, rx_len = null, retry = 0} = {}) { 150 | let src; 151 | if (cmd instanceof Array || cmd instanceof Uint8Array) { 152 | src = cmd; 153 | } else if (cmd instanceof ArrayBuffer) { 154 | src = new Uint8Array(cmd); 155 | } else if (cmd instanceof DataView) { 156 | src = new Uint8Array(cmd.buffer); 157 | } else { 158 | throw new Exception(`Unsupported command datatype ${typeof cmd}`); 159 | } 160 | 161 | if (src.length > STLINK_CMD_SIZE_V2) { 162 | throw new Exception(`Error too many Bytes in command: ${src.length}, maximum is ${STLINK_CMD_SIZE_V2}`); 163 | } 164 | 165 | // pad to 16 bytes 166 | let cmd_buffer = new Uint8Array(STLINK_CMD_SIZE_V2); 167 | src.forEach((v, i) => cmd_buffer[i] = v); 168 | 169 | while (true) { 170 | try { 171 | await this._write(cmd_buffer); 172 | if (data) { 173 | await this._write(data); 174 | } 175 | if (rx_len) { 176 | let rx = await this._read(rx_len); 177 | return rx; 178 | } 179 | } catch (e) { 180 | if ((e instanceof UsbError) && !e.fatal && (retry > 0)) { 181 | this._debug("Retrying xfer after " + e); 182 | retry--; 183 | continue; 184 | } else { 185 | throw e; 186 | } 187 | } 188 | return; 189 | } 190 | } 191 | }; 192 | 193 | const filters = DEV_TYPES.map( 194 | dev_type => ({ 195 | vendorId: dev_type.idVendor, 196 | productId: dev_type.idProduct 197 | }) 198 | ); 199 | 200 | export {Connector, filters}; 201 | -------------------------------------------------------------------------------- /src/js/stlink.js: -------------------------------------------------------------------------------- 1 | import * as libstlink from './stlink/lib/package.js' 2 | import WebStlink from './stlink/webstlink.js' 3 | 4 | export class STLink { 5 | constructor(term) { 6 | this.term = term 7 | this.stlink = null 8 | this.device = null 9 | } 10 | 11 | log(str) { 12 | this.term.writeln(str) 13 | } 14 | 15 | debug(msg) { 16 | } 17 | 18 | verbose(msg) { 19 | } 20 | 21 | info(msg) { 22 | } 23 | 24 | error(msg) { 25 | this.log('[ERROR] ' + msg) 26 | } 27 | 28 | warning(msg) { 29 | this.log('[WARN] ' + msg) 30 | } 31 | 32 | /* eslint-disable camelcase */ 33 | bargraph_start(msg, {value_min = 0, value_max = 100}) { 34 | this._msg = msg 35 | this._bargraph_min = value_min 36 | this._bargraph_max = value_max 37 | } 38 | 39 | /* eslint-enable camelcase */ 40 | 41 | bargraph_update({value = 0, percent = null}) { 42 | if (percent === null) { 43 | if ((this._bargraph_max - this._bargraph_min) > 0) { 44 | percent = Math.floor((100 * (value - this._bargraph_min)) / (this._bargraph_max - this._bargraph_min)) 45 | } else { 46 | percent = 0 47 | } 48 | } 49 | if (percent > 100) { 50 | percent = 100 51 | } 52 | this.progressCallback(this.fileNumber, percent, 100, this._msg) 53 | } 54 | 55 | bargraph_done() { 56 | this.progressCallback(this.fileNumber, 100, 100) 57 | } 58 | 59 | update_debugger_info(stlink, device) { 60 | const version = 'ST-Link/' + stlink._stlink.ver_str 61 | this.log(`Debugger - ${version} - Connected`) 62 | this.log(device.productName) 63 | this.log(device.manufacturerName) 64 | this.log(device.serialNumber) 65 | } 66 | 67 | update_target_status(status, target = null) { 68 | if (target !== null) { 69 | const fields = [ 70 | ['type', 'MCU Type', ''], 71 | ['core', 'Core', ''], 72 | ['dev_id', 'Device ID', ''], 73 | ['flash_size', 'Flash Size', 'KiB'], 74 | ['sram_size', 'SRAM Size', 'KiB'] 75 | ] 76 | if (target.eeprom_size > 0) { 77 | fields.push(['eeprom_size', 'EEPROM Size', 'KiB']) 78 | } 79 | for (const [key, title, suffix] of fields) { 80 | this.log(title + ': ' + target[key] + suffix) 81 | } 82 | } 83 | 84 | const haltState = status.halted ? 'Halted' : 'Running' 85 | const debugState = 'Debugging ' + (status.debug ? 'Enabled' : 'Disabled') 86 | 87 | this.log(`${haltState}, ${debugState}`) 88 | } 89 | 90 | on_successful_attach = async (stlink, device) => { 91 | // Export for manual debugging 92 | this.stlink = stlink 93 | this.device = device 94 | 95 | this.update_debugger_info(stlink, device) 96 | 97 | // Detect attached target CPU 98 | this.target = await stlink.detect_cpu(this.config.stlink.cpus, null) 99 | 100 | // Attach UI callbacks for whenever the CPU state is inspected 101 | const that = this 102 | 103 | function updateOnInspection(status) { 104 | that.update_target_status(status, null) 105 | } 106 | 107 | stlink.add_callback('halted', updateOnInspection) 108 | stlink.add_callback('resumed', updateOnInspection) 109 | 110 | // Update the UI with detected target info and debug state 111 | let status = await stlink.inspect_cpu() 112 | if (!status.debug) { 113 | // Automatically enable debugging 114 | await stlink.set_debug_enable(true) 115 | status = await stlink.inspect_cpu() 116 | } 117 | 118 | this.update_target_status(status, this.target) 119 | 120 | // Set the read memory address to the SRAM start 121 | this.log('SRAM address = 0x' + this.target.sram_start.toString(16)) 122 | 123 | // Set the flash write address to the Flash start 124 | this.log('Flash adddress = 0x' + this.target.flash_start.toString(16)) 125 | } 126 | 127 | on_disconnect = () => { 128 | this.info('Device disconnected') 129 | 130 | this.stlink = null 131 | this.device = null 132 | } 133 | 134 | connect = async (config, handler) => { 135 | this.config = config 136 | if (this.stlink !== null) { 137 | await this.stlink.detach() 138 | this.on_disconnect() 139 | } 140 | try { 141 | const device = await navigator.usb.requestDevice({ 142 | filters: libstlink.usb.filters 143 | }) 144 | navigator.usb.ondisconnect = e => { 145 | if (e.device === device) handler() 146 | } 147 | const nextStlink = new WebStlink(this) 148 | await nextStlink.attach(device, this) 149 | this.stlink = nextStlink 150 | this.device = device 151 | } catch (err) { 152 | this.error(err) 153 | throw err 154 | } 155 | if (this.stlink !== null) { 156 | await this.on_successful_attach(this.stlink, this.device) 157 | } 158 | } 159 | 160 | // PK pass in bootloader binary if we want to flash that! 161 | flash = async (binary, bootloader, progressCallback) => { 162 | this.progressCallback = progressCallback 163 | if (this.stlink !== null && this.stlink.connected) { 164 | this.fileNumber = 0 165 | if (bootloader) { 166 | this.log('Flash bootloader') 167 | try { 168 | await this.stlink.halt() 169 | await this.stlink.flash(this.target.flash_start, bootloader) 170 | } catch (err) { 171 | this.error(err) 172 | throw err 173 | } 174 | this.fileNumber++ 175 | } 176 | 177 | const addr = parseInt(this.config.stlink.offset, 16) 178 | this.log('Flash ExpressLRS') 179 | try { 180 | await this.stlink.halt() 181 | await this.stlink.flash(this.target.flash_start + addr, binary[0].data) 182 | } catch (err) { 183 | this.error(err) 184 | throw err 185 | } 186 | } 187 | } 188 | 189 | close = async () => { 190 | this.stlink.detach() 191 | this.stlink = null 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/pages/MainHardwareSelect.vue: -------------------------------------------------------------------------------- 1 | 160 | 161 | -------------------------------------------------------------------------------- /src/pages/SerialFlash.vue: -------------------------------------------------------------------------------- 1 | 170 | 171 | 248 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 110 | 111 | -------------------------------------------------------------------------------- /src/js/stlink/lib/stm32.js: -------------------------------------------------------------------------------- 1 | /* stm32.js 2 | * Generic STM32 CPU access and base class for STM32 CPUs 3 | * 4 | * Copyright Devan Lai 2017 5 | * 6 | * Ported from lib/stm32.py in the pystlink project, 7 | * Copyright Pavel Revak 2015 8 | * 9 | */ 10 | 11 | import {Exception} from './stlinkex.js'; 12 | import {hex_octet as H8, hex_word as H32} from './util.js'; 13 | 14 | const REGISTERS = [ 15 | 'R0', 'R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9', 16 | 'R10', 'R11', 'R12', 'SP', 'LR', 'PC', 'PSR', 'MSP', 'PSP' 17 | ]; 18 | 19 | const SRAM_START = 0x20000000; 20 | const FLASH_START = 0x08000000; 21 | 22 | const AIRCR_REG = 0xe000ed0c; 23 | const DHCSR_REG = 0xe000edf0; 24 | const DEMCR_REG = 0xe000edfc; 25 | 26 | const AIRCR_KEY = 0x05fa0000; 27 | const AIRCR_SYSRESETREQ_BIT = 0x00000004; 28 | const AIRCR_SYSRESETREQ = AIRCR_KEY | AIRCR_SYSRESETREQ_BIT; 29 | 30 | const DHCSR_KEY = 0xa05f0000; 31 | const DHCSR_DEBUGEN_BIT = 0x00000001; 32 | const DHCSR_HALT_BIT = 0x00000002; 33 | const DHCSR_STEP_BIT = 0x00000004; 34 | const DHCSR_STATUS_HALT_BIT = 0x00020000; 35 | const DHCSR_STATUS_LOCKUP_BIT = 0x00080000; 36 | const DHCSR_DEBUGDIS = DHCSR_KEY; 37 | const DHCSR_DEBUGEN = DHCSR_KEY | DHCSR_DEBUGEN_BIT; 38 | const DHCSR_HALT = DHCSR_KEY | DHCSR_DEBUGEN_BIT | DHCSR_HALT_BIT; 39 | const DHCSR_STEP = DHCSR_KEY | DHCSR_DEBUGEN_BIT | DHCSR_STEP_BIT; 40 | 41 | const DEMCR_RUN_AFTER_RESET = 0x00000000; 42 | const DEMCR_HALT_AFTER_RESET = 0x00000001; 43 | 44 | class Stm32 { 45 | constructor(stlink, dbg) { 46 | this._stlink = stlink; 47 | this._dbg = dbg; 48 | this.FLASH_START = FLASH_START; 49 | this.SRAM_START = SRAM_START; 50 | } 51 | 52 | is_reg(reg) { 53 | return REGISTERS.includes(reg.toUpperCase()); 54 | } 55 | 56 | async get_reg_all() { 57 | let registers = {}; 58 | for (let reg of REGISTERS) { 59 | registers[reg] = await this.get_reg(reg); 60 | } 61 | return registers; 62 | } 63 | 64 | get_reg(reg) { 65 | this._dbg.debug(`Stm32.get_reg(${reg})`); 66 | let index = REGISTERS.indexOf(reg.toUpperCase()); 67 | if (index != -1) { 68 | return this._stlink.get_reg(index); 69 | } 70 | throw new Exception(`Wrong register name ${reg}`); 71 | } 72 | 73 | set_reg(reg, value) { 74 | this._dbg.debug(`Stm32.set_reg(${reg}, 0x${H32(value)})`); 75 | let index = REGISTERS.indexOf(reg.toUpperCase()); 76 | if (index != -1) { 77 | return this._stlink.set_reg(index, value); 78 | } 79 | throw new Exception("Wrong register name"); 80 | } 81 | 82 | async get_mem(addr, size) { 83 | this._dbg.debug(`Stm32.get_mem(0x${H32(addr)}, ${size})`); 84 | if (size == 0) { 85 | return []; 86 | } 87 | let total = 0; 88 | let chunks = []; 89 | if (addr % 4) { 90 | let read_size = Math.min((4 - (addr % 4)), size); 91 | let chunk = await this._stlink.get_mem8(addr, read_size); 92 | total += chunk.byteLength; 93 | chunks.push(chunk); 94 | } 95 | while (true) { 96 | this._dbg.bargraph_update({"value": total}); 97 | let read_size = Math.min(((size - total) & 0xfffffff8), (this._stlink.maximum_transfer_size * 2)); 98 | if (read_size == 0) { 99 | break; 100 | } 101 | if (read_size > 64) { 102 | read_size = Math.floor(read_size / 2); 103 | let chunk = await this._stlink.get_mem32((addr + total), read_size); 104 | total += chunk.byteLength; 105 | chunks.push(chunk); 106 | chunk = await this._stlink.get_mem32((addr + total), read_size); 107 | total += chunk.byteLength; 108 | chunks.push(chunk); 109 | } else { 110 | let chunk = await this._stlink.get_mem32((addr + total), read_size); 111 | total += chunk.byteLength; 112 | chunks.push(chunk); 113 | } 114 | } 115 | if (total < size) { 116 | let read_size = (size - total); 117 | let chunk = await this._stlink.get_mem8((addr + total), read_size); 118 | total += chunk.byteLength; 119 | chunks.push(chunk); 120 | } 121 | this._dbg.bargraph_done(); 122 | 123 | let data = new Uint8Array(total); 124 | let i = 0; 125 | for (let chunk of chunks) { 126 | for (let b of new Uint8Array(chunk.buffer)) { 127 | data[i++] = b; 128 | } 129 | } 130 | 131 | return data; 132 | } 133 | 134 | async set_mem(addr, data) { 135 | this._dbg.debug(`Stm32.set_mem(0x${H32(addr)}, [data:${data.length}Bytes])`); 136 | if (data.length == 0) { 137 | return; 138 | } 139 | let written_size = 0; 140 | if (addr % 4) { 141 | let write_size = Math.min((4 - (addr % 4)), data.length); 142 | await this._stlink.set_mem8(addr, data.slice(0, write_size)); 143 | written_size = write_size; 144 | } 145 | while (true) { 146 | this._dbg.bargraph_update({"value": written_size}); 147 | let write_size = Math.min(((data.length - written_size) & 0xfffffff8), (this._stlink.maximum_transfer_size * 2)); 148 | if (write_size == 0) { 149 | break; 150 | } 151 | if (write_size > 64) { 152 | write_size = Math.floor(write_size / 2); 153 | await this._stlink.set_mem32((addr + written_size), data.slice(written_size, (written_size + write_size))); 154 | written_size += write_size; 155 | await this._stlink.set_mem32((addr + written_size), data.slice(written_size, (written_size + write_size))); 156 | written_size += write_size; 157 | } else { 158 | await this._stlink.set_mem32((addr + written_size), data.slice(written_size, (written_size + write_size))); 159 | written_size += write_size; 160 | } 161 | } 162 | if (written_size < data.length) { 163 | await this._stlink.set_mem8((addr + written_size), data.slice(written_size)); 164 | } 165 | this._dbg.bargraph_done(); 166 | 167 | } 168 | 169 | fill_mem(addr, size, pattern) { 170 | if (pattern > 0xff) { 171 | throw new Exception("Fill pattern can by 8 bit number"); 172 | } 173 | this._dbg.debug(`Stm32.fill_mem(0x${H32(addr)}, 0x${H8(pattern)}d)`); 174 | if (size == 0) { 175 | return; 176 | } 177 | const data = new Uint8Array(size).fill(pattern); 178 | return this.set_mem(addr, data); 179 | } 180 | 181 | core_status() { 182 | this._dbg.debug("Stm32.core_status()"); 183 | return this._stlink.get_debugreg32(DHCSR_REG); 184 | } 185 | 186 | async core_reset() { 187 | this._dbg.debug("Stm32.core_reset()"); 188 | await this._stlink.set_debugreg32(DEMCR_REG, DEMCR_RUN_AFTER_RESET); 189 | await this._stlink.set_debugreg32(AIRCR_REG, AIRCR_SYSRESETREQ); 190 | await this._stlink.get_debugreg32(AIRCR_REG); 191 | } 192 | 193 | async core_reset_halt() { 194 | this._dbg.debug("Stm32.core_reset_halt()"); 195 | await this._stlink.set_debugreg32(DHCSR_REG, DHCSR_HALT); 196 | await this._stlink.set_debugreg32(DEMCR_REG, DEMCR_HALT_AFTER_RESET); 197 | await this._stlink.set_debugreg32(AIRCR_REG, AIRCR_SYSRESETREQ); 198 | await this._stlink.get_debugreg32(AIRCR_REG); 199 | } 200 | 201 | async core_halt() { 202 | this._dbg.debug("Stm32.core_halt()"); 203 | await this._stlink.set_debugreg32(DHCSR_REG, DHCSR_HALT); 204 | } 205 | 206 | async core_step() { 207 | this._dbg.debug("Stm32.core_step()"); 208 | await this._stlink.set_debugreg32(DHCSR_REG, DHCSR_STEP); 209 | } 210 | 211 | async core_run() { 212 | this._dbg.debug("Stm32.core_run()"); 213 | await this._stlink.set_debugreg32(DHCSR_REG, DHCSR_DEBUGEN); 214 | } 215 | 216 | async core_nodebug() { 217 | this._dbg.debug("Stm32.core_nodebug()"); 218 | await this._stlink.set_debugreg32(DHCSR_REG, DHCSR_DEBUGDIS); 219 | } 220 | 221 | async flash_erase_all() { 222 | this._dbg.debug("Stm32.flash_mass_erase()"); 223 | throw new Exception("Erasing FLASH is not implemented for this MCU"); 224 | } 225 | 226 | async flash_write(addr, data, {erase = false, verify = false, erase_sizes = null}) { 227 | let addr_str = (addr !== null) ? `0x{H32(addr)}` : 'None'; 228 | this._dbg.debug(`Stm32.flash_write(${addr_str}, [data:${data.length}Bytes], erase=${erase}, verify=${verify}, erase_sizes=${erase_sizes})`); 229 | throw new Exception("Programing FLASH is not implemented for this MCU"); 230 | } 231 | } 232 | 233 | Stm32.SRAM_START = SRAM_START; 234 | Stm32.FLASH_START = FLASH_START; 235 | 236 | Stm32.AIRCR_REG = AIRCR_REG; 237 | Stm32.DHCSR_REG = DHCSR_REG; 238 | Stm32.DEMCR_REG = DEMCR_REG; 239 | 240 | Stm32.AIRCR_KEY = AIRCR_KEY; 241 | Stm32.AIRCR_SYSRESETREQ_BIT = AIRCR_SYSRESETREQ_BIT; 242 | Stm32.AIRCR_SYSRESETREQ = AIRCR_SYSRESETREQ; 243 | 244 | Stm32.DHCSR_KEY = DHCSR_KEY; 245 | Stm32.DHCSR_DEBUGEN_BIT = DHCSR_DEBUGEN_BIT; 246 | Stm32.DHCSR_HALT_BIT = DHCSR_HALT_BIT; 247 | Stm32.DHCSR_STEP_BIT = DHCSR_STEP_BIT; 248 | Stm32.DHCSR_STATUS_HALT_BIT = DHCSR_STATUS_HALT_BIT; 249 | Stm32.DHCSR_STATUS_LOCKUP_BIT = DHCSR_STATUS_LOCKUP_BIT; 250 | Stm32.DHCSR_DEBUGDIS = DHCSR_DEBUGDIS; 251 | Stm32.DHCSR_DEBUGEN = DHCSR_DEBUGEN; 252 | Stm32.DHCSR_HALT = DHCSR_HALT; 253 | Stm32.DHCSR_STEP = DHCSR_STEP; 254 | 255 | Stm32.DEMCR_RUN_AFTER_RESET = DEMCR_RUN_AFTER_RESET; 256 | Stm32.DEMCR_HALT_AFTER_RESET = DEMCR_HALT_AFTER_RESET; 257 | 258 | export {Stm32}; 259 | -------------------------------------------------------------------------------- /src/js/passthrough.js: -------------------------------------------------------------------------------- 1 | import {MismatchError, PassthroughError} from './error.js' 2 | 3 | export class Bootloader { 4 | static INIT_SEQ = { 5 | CRSF: [0xEC, 0x04, 0x32, this.ord('b'), this.ord('l')], 6 | GHST: [0x89, 0x04, 0x32, this.ord('b'), this.ord('l')] 7 | } 8 | 9 | static BIND_SEQ = { 10 | CRSF: [0xEC, 0x04, 0x32, this.ord('b'), this.ord('d')], 11 | GHST: [0x89, 0x04, 0x32, this.ord('b'), this.ord('d')] 12 | } 13 | 14 | static ord(s) { 15 | return s.charCodeAt(0) 16 | } 17 | 18 | static calc_crc8(payload, poly = 0xD5) { 19 | let crc = 0 20 | for (let pos = 0; pos < payload.byteLength; pos++) { 21 | crc ^= payload[pos] 22 | for (let j = 0; j < 8; ++j) { 23 | if ((crc & 0x80) !== 0) { 24 | crc = ((crc << 1) ^ poly) % 256 25 | } else { 26 | crc = (crc << 1) % 256 27 | } 28 | } 29 | } 30 | return crc 31 | } 32 | 33 | static get_telemetry_seq(seq, key = null) { 34 | const payload = new Uint8Array(seq) 35 | let u8array = new Uint8Array(0) 36 | if (key != null) { 37 | const len = key.length 38 | u8array = new Uint8Array(len) 39 | for (let i = 0; i < len; i++) { 40 | u8array[i] = key.charCodeAt(i) 41 | } 42 | } 43 | const tmp = new Uint8Array(payload.byteLength + u8array.byteLength + 1) 44 | tmp.set(payload, 0) 45 | tmp.set([payload[1] + u8array.byteLength], 1) 46 | tmp.set(u8array, payload.byteLength) 47 | const crc = this.calc_crc8(tmp.slice(2, tmp.byteLength - 1)) 48 | tmp.set([crc], payload.byteLength + u8array.byteLength) 49 | return tmp 50 | } 51 | 52 | static get_init_seq(module, key = null) { 53 | return this.get_telemetry_seq(this.INIT_SEQ[module], key) 54 | } 55 | 56 | static get_bind_seq(module, key = null) { 57 | return this.get_telemetry_seq(this.BIND_SEQ[module], key) 58 | } 59 | } 60 | 61 | export class Passthrough { 62 | constructor(transport, terminal, flashTarget, baudrate, halfDuplex = false, uploadforce = false) { 63 | this.transport = transport 64 | this.terminal = terminal 65 | this.flash_target = flashTarget 66 | this.baudrate = baudrate 67 | this.half_duplex = halfDuplex 68 | this.uploadforce = uploadforce 69 | } 70 | 71 | _validate_serialrx = async (config, expected) => { 72 | await this.transport.write_string(`get ${config}\r\n`) 73 | const line = await this.transport.read_line(100) 74 | console.log(line) 75 | for (const key of expected) { 76 | if (line.trim().indexOf(` = ${key}`) !== -1) { 77 | console.log('found') 78 | return true 79 | } 80 | } 81 | console.log('NOT found') 82 | return false 83 | } 84 | 85 | _sleep(ms) { 86 | return new Promise(resolve => setTimeout(resolve, ms)) 87 | } 88 | 89 | log(str) { 90 | this.terminal.writeln(str) 91 | } 92 | 93 | sendExpect = async (send, expect, delay) => { 94 | await this.transport.write_string(send) 95 | const line = await this.transport.read_line(100) 96 | 97 | if (line.indexOf(expect) === -1) { 98 | this.log('Failed passthrough initialisation') 99 | this.log(`Wanted '${expect}', but not found in response '${line}'`) 100 | throw new PassthroughError() 101 | } 102 | await this._sleep(delay) 103 | } 104 | 105 | 106 | edgeTXBP = async () => { 107 | this.log('Initializing EdgeTX backpack passthrough') 108 | 109 | this.transport.set_delimiters(['> ']) 110 | await this.sendExpect('set rfmod 0 power off\n', 'set: ', 100) 111 | await this.sendExpect('set pulses 0\n', 'set: ', 500) 112 | await this.sendExpect('set rfmod 0 power on\n', 'set: ', 2500) 113 | await this.sendExpect('set rfmod 0 bootpin 1\n', 'set: ', 100) 114 | await this.sendExpect('set rfmod 0 bootpin 0\n', 'set: ', 100) 115 | 116 | this.log('Enabling serial passthrough...') 117 | this.transport.set_delimiters(['\n']) 118 | const cmd = `serialpassthrough rfmod 0 ${this.transport.baudrate.toString()}` 119 | this.log(` CMD: '${cmd}`) 120 | await this.transport.write_string(`${cmd}\n`) 121 | await this.transport.read_line(200) 122 | 123 | this.log('Passthrough initialization complete') 124 | } 125 | 126 | edgeTX = async () => { 127 | this.log('Initializing EdgeTX passthrough') 128 | 129 | this.transport.set_delimiters(['> ']) 130 | await this.sendExpect('set pulses 0\n', 'set: ', 500) 131 | await this.sendExpect('set rfmod 0 power off\n', 'set: ', 500) 132 | await this.sendExpect('set rfmod 0 bootpin 1\n', 'set: ', 100) 133 | await this.sendExpect('set rfmod 0 power on\n', 'set: ', 100) 134 | await this.sendExpect('set rfmod 0 bootpin 0\n', 'set: ', 0) 135 | 136 | this.log('Enabling serial passthrough...') 137 | this.transport.set_delimiters(['\n']) 138 | const cmd = `serialpassthrough rfmod 0 ${this.transport.baudrate.toString()}` 139 | this.log(` CMD: '${cmd}`) 140 | await this.transport.write_string(`${cmd}\n`) 141 | await this.transport.read_line(200) 142 | 143 | this.log('Passthrough initialization complete') 144 | } 145 | 146 | betaflight = async () => { 147 | this.log('Initializing FC passthrough') 148 | 149 | await this.transport.write_string('#') 150 | this.transport.set_delimiters(['# ', 'CCC']) 151 | const line = await this.transport.read_line(200) 152 | if (line.indexOf('CCC') !== -1) { 153 | this.log('Passthrough already enabled and bootloader active') 154 | return 155 | } 156 | if (!line.trim().endsWith('#')) { 157 | this.log('No CLI available. Already in passthrough mode?, If this fails reboot FC and try again!') 158 | return 159 | } 160 | 161 | this.transport.set_delimiters(['# ']) 162 | 163 | let waitfor 164 | if (this.half_duplex) { 165 | waitfor = ['GHST'] 166 | } else { 167 | waitfor = ['CRSF', 'ELRS'] 168 | } 169 | const serialCheck = [] 170 | 171 | if (!await this._validate_serialrx('serialrx_provider', waitfor)) { 172 | serialCheck.push('Serial Receiver Protocol is not set to CRSF! Hint: set serialrx_provider = CRSF') 173 | } 174 | if (!await this._validate_serialrx('serialrx_inverted', ['OFF'])) { 175 | serialCheck.push('Serial Receiver UART is inverted! Hint: set serialrx_inverted = OFF') 176 | } 177 | if (!await this._validate_serialrx('serialrx_halfduplex', ['OFF', 'AUTO'])) { 178 | serialCheck.push('Serial Receiver UART is not in full duplex! Hint: set serialrx_halfduplex = OFF') 179 | } 180 | if (serialCheck.length > 0) { 181 | if (await this._validate_serialrx('rx_spi_protocol', ['EXPRESSLRS'])) { 182 | serialCheck.push('ExpressLRS SPI RX detected:') 183 | serialCheck.push('Update via betaflight to flash your RX') 184 | serialCheck.push('See https://www.expresslrs.org/2.0/hardware/spi-receivers/') 185 | } 186 | this.log('[ERROR] Invalid serial RX configuration detected:') 187 | for (const err of serialCheck) { 188 | this.log(` ${err}`) 189 | } 190 | this.log('Please change the configuration and try again!') 191 | throw new PassthroughError() 192 | } 193 | 194 | this.log('\nAttempting to detect FC UART configuration...') 195 | await this.transport.write_string('serial\r\n') 196 | 197 | this.transport.set_delimiters(['\n']) 198 | let index = false 199 | while (true) { 200 | const line = await this.transport.read_line(200) 201 | if (line === '') { 202 | break 203 | } 204 | if (line.startsWith('serial')) { 205 | const regexp = /serial (?(UART)?[0-9]+) (?[0-9]+) / 206 | const config = line.match(regexp) 207 | if (config && config.groups && config.groups.port && config.groups.port_cfg && (config.groups.port_cfg & 64) === 64) { 208 | index = config.groups.port 209 | break 210 | } 211 | } 212 | } 213 | if (!index) { 214 | this.log('!!! RX Serial not found !!!!') 215 | this.log('Check configuration and try again...') 216 | throw new PassthroughError() 217 | } 218 | 219 | await this.transport.write_string(`serialpassthrough ${index} ${this.transport.baudrate}\r\n`) 220 | await this._sleep(200) 221 | 222 | try { 223 | for (let i = 0; i < 10; i++) { 224 | await this.transport.read_line(200) 225 | } 226 | } catch (e) { 227 | } 228 | this.log('Passthrough initialization complete') 229 | } 230 | 231 | reset_to_bootloader = async () => { 232 | this.log('Reset to bootloader') 233 | 234 | if (this.half_duplex) { 235 | this.log('Using half duplex (GHST)') 236 | await this.transport.write_array(Bootloader.get_init_seq('GHST')) 237 | } else { 238 | this.log('Using full duplex (CRSF)') 239 | while (await this.transport.read_line(100) !== '') {} 240 | const train = new Uint8Array(32) 241 | train.fill(0x55) 242 | await this.transport.write_array(new Uint8Array([0x07, 0x07, 0x12, 0x20])) 243 | await this.transport.write_array(train) 244 | await this._sleep(200) 245 | await this.transport.write_array(Bootloader.get_init_seq('CRSF')) 246 | await this._sleep(200) 247 | } 248 | 249 | this.transport.set_delimiters(['\n']) 250 | const rxTarget = (await this.transport.read_line(200)).trim() 251 | 252 | console.log(`rxtarget ${rxTarget}`) 253 | 254 | if (rxTarget === '') { 255 | this.log('Cannot detect RX target, blindly flashing!') 256 | } else if (this.uploadforce) { 257 | this.log(`Force flashing ${this.flash_target}, detected ${rxTarget}`) 258 | } else if (rxTarget.toUpperCase() !== this.flash_target.toUpperCase()) { 259 | this.log(`Wrong target selected your RX is '${rxTarget}', trying to flash '${this.flash_target}'`) 260 | throw new MismatchError() 261 | } else if (this.flash_target !== '') { 262 | this.log(`Verified RX target '${this.flash_target}'`) 263 | } 264 | this.log('Bootloader enabled') 265 | await this._sleep(500) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/js/configure.js: -------------------------------------------------------------------------------- 1 | import {compareSemanticVersions} from "./version.js"; 2 | import {store} from "./state.js"; 3 | 4 | export class Configure { 5 | static #MAGIC = new Uint8Array([0xBE, 0xEF, 0xBA, 0xBE, 0xCA, 0xFE, 0xF0, 0x0D]) 6 | 7 | static #find_patch_location(binary) { 8 | return binary.findIndex((_, i, a) => { 9 | let j = 0 10 | while (j < Configure.#MAGIC.length && a[i + j] === Configure.#MAGIC[j]) { 11 | j++ 12 | } 13 | return j === Configure.#MAGIC.length 14 | }) 15 | } 16 | 17 | static #write32(binary, pos, val) { 18 | if (val !== undefined) { 19 | binary[pos + 0] = (val >> 0) & 0xFF 20 | binary[pos + 1] = (val >> 8) & 0xFF 21 | binary[pos + 2] = (val >> 16) & 0xFF 22 | binary[pos + 3] = (val >> 24) & 0xFF 23 | } 24 | return pos + 4 25 | } 26 | 27 | static #patch_buzzer(binary, pos, options) { 28 | binary[pos] = options.beeptype 29 | pos += 1 30 | for (let i = 0; i < 32 * 4; i++) { 31 | binary[pos + i] = 0 32 | } 33 | const melody = options.melody 34 | if (melody) { 35 | for (let i = 0; i < melody.length; i++) { 36 | binary[pos + i * 4 + 0] = melody[i][0] & 0xFF 37 | binary[pos + i * 4 + 1] = (melody[i][0] >> 8) & 0xFF 38 | binary[pos + i * 4 + 2] = melody[i][1] & 0xFF 39 | binary[pos + i * 4 + 3] = (melody[i][1] >> 8) & 0xFF 40 | } 41 | } 42 | pos += 32 * 4 43 | return pos 44 | } 45 | 46 | static #patch_tx_params(binary, pos, options, version) { 47 | pos = this.#write32(binary, pos, options['tlm-report']) 48 | if (compareSemanticVersions(store.version, '3.5') < 0) { 49 | pos = this.#write32(binary, pos, options['fan-runtime']) 50 | } 51 | let val = binary[pos] 52 | if (options['uart-inverted']) { 53 | val &= ~1 54 | val |= options['uart-inverted'] ? 1 : 0 55 | } 56 | if (options['unlock-higher-power']) { 57 | val &= ~2 58 | val |= options['unlock-higher-power'] ? 2 : 0 59 | } 60 | binary[pos] = val 61 | return pos + 1 62 | } 63 | 64 | static #patch_rx_params(binary, pos, options) { 65 | pos = this.#write32(binary, pos, options['rcvr-uart-baud']) 66 | let val = binary[pos] 67 | if (options['rcvr-invert-tx']) { 68 | val &= ~1 69 | val |= options['rcvr-invert-tx'] ? 1 : 0 70 | } 71 | if (options['lock-on-first-connection']) { 72 | val &= ~2 73 | val |= options['lock-on-first-connection'] ? 2 : 0 74 | } 75 | if (options['r9mm-mini-sbus']) { 76 | val &= ~4 77 | val |= options['r9mm-mini-sbus'] ? 4 : 0 78 | } 79 | binary[pos] = val 80 | return pos + 1 81 | } 82 | 83 | static #configureSTM32(binary, deviceType, radioType, options) { 84 | let pos = this.#find_patch_location(binary) 85 | if (pos === -1) throw new Error('Configuration magic not found in firmware file. Is this a 3.x firmware?') 86 | 87 | pos += 8 // Skip magic 88 | const version = binary[pos] + binary[pos + 1] << 8 89 | pos += 2 // Skip version 90 | if (version === 0) { 91 | pos += 1 // Skip the (old) hardware flag 92 | } 93 | 94 | // Poke in the domain 95 | if (radioType === 'sx127x' && options.domain) { 96 | binary[pos] = options.domain 97 | } 98 | pos += 1 99 | 100 | // Poke in the UID (if there is one) 101 | if (options.uid) { 102 | binary[pos] = 1 103 | for (let i = 0; i < 6; i++) { 104 | binary[pos + 1 + i] = options.uid[i] 105 | } 106 | } else { 107 | binary[pos] = 0 108 | } 109 | pos += 7 110 | 111 | if (compareSemanticVersions(store.version, '3.4') >= 0) { 112 | pos = this.#write32(binary, pos, options['flash-discriminator']) 113 | } 114 | 115 | if (compareSemanticVersions(store.version, '3.5') >= 0) { 116 | pos = this.#write32(binary, pos, options['fan-runtime']) 117 | } 118 | 119 | if (deviceType === 'TX') { // TX target 120 | pos = this.#patch_tx_params(binary, pos, options, version) 121 | if (options.beeptype) { // Has a Buzzer 122 | pos = this.#patch_buzzer(binary, pos, options) 123 | } 124 | } else if (deviceType === 'RX') { // RX target 125 | pos = this.#patch_rx_params(binary, pos, options) 126 | } 127 | 128 | return binary 129 | } 130 | 131 | static #checkStatus = (response) => { 132 | if (!response.ok) { 133 | throw new Error(`HTTP ${response.status} - ${response.statusText}`) 134 | } 135 | return response 136 | } 137 | 138 | static #fetch_file = async (file, address, transform = (e) => e) => { 139 | const response = this.#checkStatus(await fetch(file)) 140 | const blob = await response.blob() 141 | const arrayBuffer = await blob.arrayBuffer() 142 | const dataArray = new Uint8Array(arrayBuffer) 143 | const data = transform(dataArray) 144 | return {data, address} 145 | } 146 | 147 | static #findFirmwareEnd = (binary, config) => { 148 | let pos = 0x0 149 | if (config.platform === 'esp8285') pos = 0x1000 150 | if (binary[pos] !== 0xE9) throw new Error('The file provided does not the right magic for a firmware file!') 151 | let segments = binary[pos + 1] 152 | if (config.platform.startsWith('esp32')) pos = 24 153 | else pos = 0x1008 154 | while (segments--) { 155 | const size = binary[pos + 4] + (binary[pos + 5] << 8) + (binary[pos + 6] << 16) + (binary[pos + 7] << 24) 156 | pos += 8 + size 157 | } 158 | pos = (pos + 16) & ~15 159 | if (config.platform.startsWith('esp32')) pos += 32 160 | return pos 161 | } 162 | 163 | static #appendArray = (...args) => { 164 | const totalLength = args.reduce((acc, value) => acc + value.length, 0) 165 | const c = new Uint8Array(totalLength) 166 | args.reduce((acc, value) => { 167 | c.set(value, acc) 168 | return acc + value.length 169 | }, 0) 170 | return c 171 | } 172 | 173 | static #ui8ToBstr = (u8Array) => { 174 | const len = u8Array.length 175 | let bStr = '' 176 | for (let i = 0; i < len; i++) { 177 | bStr += String.fromCharCode(u8Array[i]) 178 | } 179 | return bStr 180 | } 181 | 182 | static #bstrToUi8 = (bStr) => { 183 | const len = bStr.length 184 | const u8array = new Uint8Array(len) 185 | for (let i = 0; i < len; i++) { 186 | u8array[i] = bStr.charCodeAt(i) 187 | } 188 | return u8array 189 | } 190 | 191 | static #configureESP = (deviceType, binary, config, options) => { 192 | const end = this.#findFirmwareEnd(binary, config) 193 | if (deviceType === 'RX' || deviceType === 'TX') { 194 | return this.#appendArray( 195 | binary.slice(0, end), 196 | this.#bstrToUi8(config.product_name.padEnd(128, '\x00')), 197 | this.#bstrToUi8(config.lua_name.padEnd(16, '\x00')), 198 | this.#bstrToUi8(JSON.stringify(options).padEnd(512, '\x00')) 199 | ) 200 | } else { 201 | return this.#appendArray( 202 | binary.slice(0, end), 203 | this.#bstrToUi8(JSON.stringify(options).padEnd(512, '\x00')) 204 | ) 205 | } 206 | } 207 | 208 | static download = async (folder, version, deviceType, rxAsTxType, radioType, config, firmwareUrl, options) => { 209 | if (rxAsTxType) firmwareUrl = firmwareUrl.replace('_RX', '_TX') 210 | if (config.platform === 'stm32') { 211 | const entry = await this.#fetch_file(firmwareUrl, 0, (bin) => this.#configureSTM32(bin, deviceType, radioType, options)) 212 | return [entry] 213 | } else { 214 | const list = [] 215 | 216 | let hardwareLayoutData 217 | if (config.custom_layout) { 218 | hardwareLayoutData = this.#bstrToUi8(JSON.stringify(config.custom_layout)) 219 | } else if (config.layout_file) { 220 | // get layout from version specific folder OR fall back to global folder 221 | const hardwareLayoutFile = await this.#fetch_file(`${folder}/${version}/hardware/${deviceType}/${config.layout_file}`, 0) 222 | .catch(() => this.#fetch_file(`${folder}/hardware/${deviceType}/${config.layout_file}`, 0)) 223 | let layout = JSON.parse(this.#ui8ToBstr(hardwareLayoutFile.data)) 224 | if (config.overlay) { 225 | layout = { 226 | ...layout, 227 | ...config.overlay 228 | } 229 | } 230 | if (rxAsTxType === 'external') layout['serial_rx'] = layout['serial_tx'] 231 | hardwareLayoutData = this.#bstrToUi8(JSON.stringify(layout)) 232 | } else { 233 | hardwareLayoutData = new Uint8Array(0) 234 | } 235 | 236 | if (config.platform.startsWith('esp32')) { 237 | let startAddress = 0x1000 238 | if (config.platform.startsWith('esp32-')) { 239 | startAddress = 0x0000 240 | } 241 | list.push(this.#fetch_file(firmwareUrl.replace('firmware.bin', 'bootloader.bin'), startAddress)) 242 | list.push(this.#fetch_file(firmwareUrl.replace('firmware.bin', 'partitions.bin'), 0x8000)) 243 | list.push(this.#fetch_file(firmwareUrl.replace('firmware.bin', 'boot_app0.bin'), 0xE000)) 244 | list.push(this.#fetch_file(firmwareUrl, 0x10000, (bin) => Configure.#configureESP(deviceType, bin, config, options))) 245 | } else if (config.platform === 'esp8285') { 246 | list.push(this.#fetch_file(firmwareUrl, 0x0, (bin) => Configure.#configureESP(deviceType, bin, config, options))) 247 | } 248 | 249 | const files = await Promise.all(list) 250 | let logoFile = {data: new Uint8Array(0), address: 0} 251 | if (config.logo_file) { 252 | // get logo from version specific folder OR fall back to global folder 253 | logoFile = await this.#fetch_file(`${folder}/${version}/hardware/logo/${config.logo_file}`, 0) 254 | .catch(() => this.#fetch_file(`${folder}/hardware/logo/${config.logo_file}`, 0)) 255 | } 256 | files[files.length - 1].data = this.#appendArray( 257 | files[files.length - 1].data, 258 | hardwareLayoutData, 259 | (new Uint8Array(2048 - hardwareLayoutData.length)).fill(0), 260 | logoFile.data 261 | ) 262 | return files 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /mdns-proxy/proxy.c: -------------------------------------------------------------------------------- 1 | #ifdef _WIN32 2 | #define _CRT_SECURE_NO_WARNINGS 1 3 | #endif 4 | 5 | #include 6 | 7 | #include "mdns.h" 8 | #include "hashmap.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #ifdef _WIN32 15 | #include 16 | #else 17 | #include 18 | #define closesocket close 19 | #endif 20 | 21 | extern int* open_mdns_sockets(); 22 | extern const char *send_mdns_query(mdns_query_t query); 23 | 24 | extern const char * 25 | send_mdns_query_old(mdns_query_t* query, size_t count); 26 | void process_mdns_response(int isock, void (*handler)(const char *name, const char *json)); 27 | 28 | int running = 1; 29 | fd_set active_fd_set, response_fd_set, mdns_fd_set; 30 | struct { 31 | int peer; 32 | unsigned int started: 1; 33 | } connection[FD_SETSIZE]; 34 | 35 | struct hashmap_s hashmap; 36 | 37 | 38 | int output(int conn, const char *buffer, int nbytes) 39 | { 40 | int sent = 0; 41 | while(nbytes) { 42 | int w = write(conn, buffer + sent, nbytes); 43 | if (w == -1) return -1; 44 | sent += w; 45 | nbytes -= w; 46 | } 47 | return sent; 48 | } 49 | 50 | void query_mdns() 51 | { 52 | mdns_query_t query; 53 | 54 | query.name = "_http._tcp.local."; 55 | query.type = MDNS_RECORDTYPE_PTR; 56 | query.length = strlen(query.name); 57 | 58 | send_mdns_query(query); 59 | } 60 | 61 | void mdns_handler(const char *name, const char *json) 62 | { 63 | // add to map 64 | if (0 != hashmap_put(&hashmap, name, strlen(name), strdup(json))) { 65 | // error! 66 | } 67 | } 68 | 69 | static int outcount = 0; 70 | static int iterate(void* const context, void* const value) 71 | { 72 | if (outcount != 0) { 73 | output(*(int*)context, (char *)",", 1); 74 | } 75 | output(*(int*)context, (char *)value, strlen((char *)value)); 76 | outcount++; 77 | return 1; 78 | } 79 | 80 | int do_mdns_query(int conn) 81 | { 82 | const char *header = "HTTP/1.1 200 OK\r\n" 83 | "Content-Type: application/json\r\n" 84 | "Connection: close\r\n" 85 | "Accept-Ranges: none\r\n" 86 | "Access-Control-Allow-Origin: *\r\n\r\n{"; 87 | 88 | output(conn, header, strlen(header)); 89 | outcount = 0; 90 | hashmap_iterate(&hashmap, iterate, &conn); 91 | output(conn, "}", 1); 92 | FD_CLR(conn, &active_fd_set); 93 | closesocket(conn); 94 | return 0; 95 | } 96 | 97 | int connect_client(int conn, const char *dest) 98 | { 99 | int client; 100 | if((client = socket(AF_INET, SOCK_STREAM, 0)) < 0) 101 | { 102 | printf("Error : Could not create socket\n"); 103 | return -1; 104 | } 105 | 106 | struct sockaddr_in serv_addr; 107 | memset(&serv_addr, 0, sizeof(serv_addr)); 108 | 109 | serv_addr.sin_family = AF_INET; 110 | serv_addr.sin_port = htons(80); 111 | 112 | if(inet_pton(AF_INET, dest, &serv_addr.sin_addr)<=0) 113 | { 114 | printf("inet_pton error occurred\n"); 115 | return -1; 116 | } 117 | 118 | if(connect(client, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) 119 | { 120 | printf("Error : Connect Failed\n"); 121 | return -1; 122 | } 123 | 124 | connection[conn].peer = client; 125 | connection[conn].started = 1; 126 | connection[client].peer = conn; 127 | connection[client].started = 0; 128 | FD_SET(client, &active_fd_set); 129 | FD_SET(client, &response_fd_set); 130 | return client; 131 | } 132 | 133 | int process_request(int conn) 134 | { 135 | char buffer[2048]; 136 | int nbytes; 137 | 138 | nbytes = read (conn, buffer, 2048); 139 | if (nbytes < 0) { 140 | perror ("read"); 141 | exit (EXIT_FAILURE); 142 | } else if (nbytes == 0) { 143 | return -1; 144 | } else { 145 | if (!connection[conn].started) { 146 | if (strncmp(buffer, "GET /mdns", 9) == 0) { 147 | return do_mdns_query(conn); 148 | } else if (strncmp(buffer, "POST ", 5) == 0 || strncmp(buffer, "GET ", 4) == 0) { 149 | // parse URL for target, and adjust and send data to peer 150 | char *space1 = strchr(buffer, ' '); 151 | char *slash = strchr(space1 + 2, '/'); 152 | char dest[80]={0}; 153 | memcpy(dest, space1+2, slash-(space1+2)); 154 | nbytes -= slash-(space1+1); 155 | memcpy(space1+1, slash, buffer+2048-slash); 156 | int client = connect_client(conn, dest); 157 | if (client == -1) { 158 | return -1; 159 | } 160 | } else if (strncmp(buffer, "OPTIONS ", 8) == 0) { 161 | // send back header only response for CORS 162 | const char *cors = "HTTP/1.1 204 No Content\r\n" 163 | "Connection: keep-alive\r\n" 164 | "Access-Control-Allow-Origin: *\r\n" 165 | "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n" 166 | "Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With, X-FileSize\r\n" 167 | "Access-Control-Max-Age: 86400\r\n\r\n"; 168 | return output(conn, cors, strlen(cors)); 169 | } 170 | } 171 | return output(connection[conn].peer, buffer, nbytes); 172 | } 173 | } 174 | 175 | int process_response(int conn) 176 | { 177 | char buffer[2049]; 178 | int nbytes; 179 | 180 | nbytes = read (conn, buffer, 2048); 181 | buffer[nbytes] = 0; 182 | if (nbytes < 0) { 183 | perror ("read"); 184 | exit (EXIT_FAILURE); 185 | } else if (nbytes == 0) { 186 | return -1; 187 | } else { 188 | int peer = connection[conn].peer; 189 | // forward data to peer 190 | if (!connection[conn].started) { 191 | // add in CORS header if theres not one already 192 | if (strstr(buffer, "Access-Control-Allow-Origin:") == 0) { 193 | const char *header = "\r\nAccess-Control-Allow-Origin: *"; 194 | const char *end = strstr(buffer, "\r\n\r\n"); 195 | if (output(peer, buffer, end - buffer) == -1) return -1; 196 | if (output(peer, header, strlen(header)) == -1) return -1; 197 | return output(peer, end, nbytes - (end - buffer)); 198 | } 199 | } 200 | return output(peer, buffer, nbytes); 201 | } 202 | return -1; 203 | } 204 | 205 | int process_data(int conn) 206 | { 207 | if (FD_ISSET(conn, &response_fd_set)) 208 | return process_response(conn); 209 | return process_request(conn); 210 | } 211 | 212 | void startServer() 213 | { 214 | int sockfd; 215 | struct sockaddr_in servaddr; 216 | 217 | fd_set read_fd_set; 218 | struct sockaddr_in clientname; 219 | socklen_t size; 220 | 221 | // socket create and verification 222 | sockfd = socket(AF_INET, SOCK_STREAM, 0); 223 | if (sockfd == -1) { 224 | printf("socket creation failed...\n"); 225 | exit(0); 226 | } else printf("Socket successfully created..\n"); 227 | memset(&servaddr, 0, sizeof(servaddr)); 228 | 229 | // assign IP, PORT 230 | servaddr.sin_family = AF_INET; 231 | servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 232 | servaddr.sin_port = htons(9097); 233 | 234 | // Binding newly created socket to given IP and verification 235 | if ((bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) != 0) { 236 | printf("socket bind failed...\n"); 237 | exit(0); 238 | } else printf("Socket successfully bound..\n"); 239 | 240 | // Now server is ready to listen and verification 241 | if ((listen(sockfd, 5)) != 0) { 242 | printf("Listen failed...\n"); 243 | exit(0); 244 | } else printf("Server listening..\n"); 245 | 246 | /* Initialize the set of active sockets. */ 247 | FD_ZERO(&active_fd_set); 248 | FD_ZERO(&response_fd_set); 249 | FD_ZERO(&mdns_fd_set); 250 | FD_SET(sockfd, &active_fd_set); 251 | 252 | int *sockets = open_mdns_sockets(); 253 | while(*sockets != -1) { 254 | FD_SET(*sockets, &active_fd_set); 255 | FD_SET(*sockets, &mdns_fd_set); 256 | sockets++; 257 | } 258 | 259 | query_mdns(); 260 | 261 | while (running) { 262 | struct timeval timeout; 263 | timeout.tv_sec = 10; 264 | timeout.tv_usec = 0; 265 | 266 | 267 | /* Block until input arrives on one or more active sockets. */ 268 | read_fd_set = active_fd_set; 269 | int s = select(FD_SETSIZE, &read_fd_set, NULL, NULL, &timeout); 270 | if (s < 0) { 271 | perror("select"); 272 | return; 273 | } 274 | if (s == 0 || time(0) % 10 == 0) query_mdns(); 275 | if (s == 0) continue; 276 | 277 | /* Service all the sockets with input pending. */ 278 | for (int i = 0; i < FD_SETSIZE; ++i) { 279 | if (FD_ISSET(i, &read_fd_set)) { 280 | if (i == sockfd) { 281 | /* Connection request on original socket. */ 282 | int client; 283 | size = sizeof(clientname); 284 | client = accept(sockfd, (struct sockaddr *)&clientname, &size); 285 | if (client < 0) { 286 | perror("accept"); 287 | return; 288 | } 289 | fprintf(stderr, "Server: connect from host %s, port %hd.\n", inet_ntoa(clientname.sin_addr), ntohs(clientname.sin_port)); 290 | FD_SET(client, &active_fd_set); 291 | connection[client].started = 0; 292 | connection[client].peer = -1; 293 | } else if (FD_ISSET(i, &mdns_fd_set)) { 294 | // process MDNS response 295 | process_mdns_response(i, mdns_handler); 296 | } else { 297 | /* Data arriving on an already-connected socket. */ 298 | if (process_data(i) < 0) { 299 | closesocket(i); 300 | FD_CLR(i, &active_fd_set); 301 | FD_CLR(i, &response_fd_set); 302 | // get peer and close that too 303 | int peer = connection[i].peer; 304 | if (peer >= 0) { 305 | closesocket(peer); 306 | FD_CLR(peer, &active_fd_set); 307 | FD_CLR(peer, &response_fd_set); 308 | } 309 | } 310 | } 311 | } 312 | } 313 | } 314 | } 315 | 316 | #ifdef _WIN32 317 | BOOL console_handler(DWORD signal) { 318 | if (signal == CTRL_C_EVENT) { 319 | running = 0; 320 | } 321 | return TRUE; 322 | } 323 | #else 324 | void signal_handler(int signal) { 325 | running = 0; 326 | } 327 | #endif 328 | 329 | int main(int argc, char **argv) 330 | { 331 | #ifdef _WIN32 332 | WORD versionWanted = MAKEWORD(1, 1); 333 | WSADATA wsaData; 334 | if (WSAStartup(versionWanted, &wsaData)) { 335 | printf("Failed to initialize WinSock\n"); 336 | return -1; 337 | } 338 | SetConsoleCtrlHandler(console_handler, TRUE); 339 | #else 340 | signal(SIGINT, signal_handler); 341 | #endif 342 | if (0 != hashmap_create(8, &hashmap)) { 343 | perror("Failed to create hashmap"); 344 | exit(1); 345 | } 346 | 347 | startServer(); 348 | for (int i=0 ; i 48 | 0x00, 0x2b, // 0x2b00 // cmp r3, //0 49 | 0x04, 0xd1, // 0xd104 // bne 50 | 0x01, 0x30, // 0x3001 // adds r0, //1 51 | 0x01, 0x31, // 0x3101 // adds r1, //1 52 | 0x01, 0x3a, // 0x3a01 // subs r2, //1 53 | 0x00, 0x2a, // 0x2a00 // cmp r2, //0 54 | 0xf3, 0xd1, // 0xd1f3 // bne 55 | // exit: 56 | 0x00, 0xbe, // 0xbe00 // bkpt 0x00 57 | ]); 58 | 59 | const FLASH_WRITER_F4_CODE_X16 = new Uint8Array([ 60 | // write: 61 | 0x03, 0x88, // 0x8803 // ldrh r3, [r0] 62 | 0x0b, 0x80, // 0x800b // strh r3, [r1] 63 | // test_busy: 64 | 0x23, 0x68, // 0x6823 // ldr r3, [r4] 65 | 0x2b, 0x42, // 0x422b // tst r3, r5 66 | 0xfc, 0xd1, // 0xd1fc // bne 67 | 0x00, 0x2b, // 0x2b00 // cmp r3, //0 68 | 0x04, 0xd1, // 0xd104 // bne 69 | 0x02, 0x30, // 0x3002 // adds r0, //2 70 | 0x02, 0x31, // 0x3102 // adds r1, //2 71 | 0x02, 0x3a, // 0x3a02 // subs r2, //2 72 | 0x00, 0x2a, // 0x2a00 // cmp r2, //0 73 | 0xf3, 0xd1, // 0xd1f3 // bne 74 | // exit: 75 | 0x00, 0xbe, // 0xbe00 // bkpt 0x00 76 | ]); 77 | 78 | const FLASH_WRITER_F4_CODE_X32 = new Uint8Array([ 79 | // write: 80 | 0x03, 0x68, // 0x6803 // ldr r3, [r0] 81 | 0x0b, 0x60, // 0x600b // str r3, [r1] 82 | // test_busy: 83 | 0x23, 0x68, // 0x6823 // ldr r3, [r4] 84 | 0x2b, 0x42, // 0x422b // tst r3, r5 85 | 0xfc, 0xd1, // 0xd1fc // bne 86 | 0x00, 0x2b, // 0x2b00 // cmp r3, //0 87 | 0x04, 0xd1, // 0xd104 // bne 88 | 0x04, 0x30, // 0x3004 // adds r0, //4 89 | 0x04, 0x31, // 0x3104 // adds r1, //4 90 | 0x04, 0x3a, // 0x3a04 // subs r2, //4 91 | 0x00, 0x2a, // 0x2a00 // cmp r2, //0 92 | 0xf3, 0xd1, // 0xd1f3 // bne 93 | // exit: 94 | 0x00, 0xbe, // 0xbe00 // bkpt 0x00 95 | ]); 96 | 97 | const VOLTAGE_DEPENDEND_PARAMS = [ 98 | { 99 | 'min_voltage': 2.7, 100 | 'max_mass_erase_time': 16, 101 | 'max_erase_time': {16: .5, 64: 1.1, 128: 2}, 102 | 'FLASH_CR_PSIZE': FLASH_CR_PSIZE_X32, 103 | 'FLASH_WRITER_CODE': FLASH_WRITER_F4_CODE_X32, 104 | }, 105 | { 106 | 'min_voltage': 2.1, 107 | 'max_mass_erase_time': 22, 108 | 'max_erase_time': {16: .6, 64: 1.4, 128: 2.6}, 109 | 'FLASH_CR_PSIZE': FLASH_CR_PSIZE_X16, 110 | 'FLASH_WRITER_CODE': FLASH_WRITER_F4_CODE_X16, 111 | }, 112 | { 113 | 'min_voltage': 1.8, 114 | 'max_mass_erase_time': 32, 115 | 'max_erase_time': {16: .8, 64: 2.4, 128: 4}, 116 | 'FLASH_CR_PSIZE': FLASH_CR_PSIZE_X8, 117 | 'FLASH_WRITER_CODE': FLASH_WRITER_F4_CODE_X8, 118 | } 119 | ]; 120 | 121 | class Flash { 122 | constructor(driver, stlink, dbg) { 123 | this._driver = driver; 124 | this._stlink = stlink; 125 | this._dbg = dbg; 126 | this._params = null; 127 | } 128 | 129 | async init() { 130 | this._params = await this.get_voltage_dependend_params(); 131 | await this.unlock(); 132 | } 133 | 134 | async get_voltage_dependend_params() { 135 | await this._stlink.read_target_voltage(); 136 | let voltage = this._stlink.target_voltage; 137 | let params = VOLTAGE_DEPENDEND_PARAMS.find( 138 | params => (voltage > params["min_voltage"]) 139 | ); 140 | if (params) { 141 | return params; 142 | } 143 | throw new Exception(`Supply voltage is ${voltage}V, but minimum for FLASH program or erase is 1.8V`); 144 | } 145 | 146 | async unlock() { 147 | await this._driver.core_reset_halt(); 148 | // programming locked 149 | let cr_reg = await this._stlink.get_debugreg32(FLASH_CR_REG); 150 | if (cr_reg & FLASH_CR_LOCK_BIT) { 151 | // unlock keys 152 | await this._stlink.set_debugreg32(FLASH_KEYR_REG, 0x45670123); 153 | await this._stlink.set_debugreg32(FLASH_KEYR_REG, 0xcdef89ab); 154 | } 155 | cr_reg = await this._stlink.get_debugreg32(FLASH_CR_REG); 156 | // programming locked 157 | if (cr_reg & FLASH_CR_LOCK_BIT) { 158 | throw new Exception("Error unlocking FLASH"); 159 | } 160 | } 161 | 162 | async lock() { 163 | await this._stlink.set_debugreg32(FLASH_CR_REG, FLASH_CR_LOCK_BIT); 164 | await this._driver.core_reset_halt(); 165 | } 166 | 167 | async erase_all() { 168 | await this._stlink.set_debugreg32(FLASH_CR_REG, FLASH_CR_MER_BIT); 169 | await this._stlink.set_debugreg32(FLASH_CR_REG, (FLASH_CR_MER_BIT | FLASH_CR_STRT_BIT)); 170 | await this.wait_busy(this._params["max_mass_erase_time"], "Erasing FLASH"); 171 | } 172 | 173 | async erase_sector(sector, erase_size) { 174 | let flash_cr_value = FLASH_CR_SER_BIT; 175 | flash_cr_value |= (this._params["FLASH_CR_PSIZE"] | (sector << FLASH_CR_SNB_BITINDEX)); 176 | await this._stlink.set_debugreg32(FLASH_CR_REG, flash_cr_value); 177 | await this._stlink.set_debugreg32(FLASH_CR_REG, (flash_cr_value | FLASH_CR_STRT_BIT)); 178 | await this.wait_busy(this._params["max_erase_time"][erase_size]); 179 | } 180 | 181 | async erase_sectors(flash_start, erase_sizes, addr, size) { 182 | let erase_addr = flash_start; 183 | this._dbg.bargraph_start("Erasing FLASH", {"value_min": flash_start, "value_max": (flash_start + size)}); 184 | let sector = 0; 185 | while (true) { 186 | for (let erase_size of erase_sizes) { 187 | if (addr < (erase_addr + erase_size)) { 188 | this._dbg.bargraph_update({"value": erase_addr}); 189 | await this.erase_sector(sector, erase_size); 190 | } 191 | erase_addr += erase_size; 192 | if ((addr + size) < erase_addr) { 193 | this._dbg.bargraph_done(); 194 | return; 195 | } 196 | sector += 1; 197 | } 198 | } 199 | } 200 | 201 | async init_write(sram_offset) { 202 | this._flash_writer_offset = sram_offset; 203 | this._flash_data_offset = (sram_offset + 256); 204 | await this._stlink.set_mem8(this._flash_writer_offset, this._params["FLASH_WRITER_CODE"]); 205 | // set configuration for flash writer 206 | await this._driver.set_reg("R4", FLASH_SR_REG); 207 | await this._driver.set_reg("R5", FLASH_SR_BUSY_BIT); 208 | // enable PG 209 | await this._stlink.set_debugreg32(FLASH_CR_REG, (FLASH_CR_PG_BIT | this._params["FLASH_CR_PSIZE"])); 210 | } 211 | 212 | async write(addr, block) { 213 | // if all data are 0xff then will be not written 214 | if (block.every(b => (b == 0xff))) { 215 | return; 216 | } 217 | await this._stlink.set_mem32(this._flash_data_offset, block); 218 | await this._driver.set_reg("PC", this._flash_writer_offset); 219 | await this._driver.set_reg("R0", this._flash_data_offset); 220 | await this._driver.set_reg("R1", addr); 221 | await this._driver.set_reg("R2", block.length); 222 | await this._driver.core_run(); 223 | await this.wait_for_breakpoint(0.2); 224 | } 225 | 226 | async wait_busy(wait_time, bargraph_msg = null) { 227 | const end_time = (Date.now() + (wait_time * 1.5 * 1000)); 228 | if (bargraph_msg) { 229 | this._dbg.bargraph_start(bargraph_msg, { 230 | "value_min": Date.now() / 1000.0, 231 | "value_max": (Date.now() / 1000.0 + wait_time) 232 | }); 233 | } 234 | while (Date.now() < end_time) { 235 | if (bargraph_msg) { 236 | this._dbg.bargraph_update({"value": Date.now() / 1000.0}); 237 | } 238 | let status = await this._stlink.get_debugreg32(FLASH_SR_REG); 239 | if (!(status & FLASH_SR_BUSY_BIT)) { 240 | this.end_of_operation(status); 241 | if (bargraph_msg) { 242 | this._dbg.bargraph_done(); 243 | } 244 | return; 245 | } 246 | await async_sleep(wait_time / 20); 247 | } 248 | throw new Exception("Operation timeout"); 249 | } 250 | 251 | async wait_for_breakpoint(wait_time) { 252 | const end_time = Date.now() + (wait_time * 1000); 253 | do { 254 | let dhcsr = await this._stlink.get_debugreg32(Stm32.DHCSR_REG); 255 | if (dhcsr & Stm32.DHCSR_STATUS_HALT_BIT) { 256 | break; 257 | } 258 | await async_sleep(wait_time / 20); 259 | } while (Date.now() < end_time); 260 | 261 | let sr = await this._stlink.get_debugreg32(FLASH_SR_REG); 262 | this.end_of_operation(sr); 263 | } 264 | 265 | end_of_operation(status) { 266 | if (status) { 267 | throw new Exception("Error writing FLASH with status (FLASH_SR) " + H32(status)); 268 | } 269 | } 270 | } 271 | 272 | // support all STM32F MCUs with sector access access to FLASH 273 | // (STM32F2xx, STM32F4xx) 274 | class Stm32FS extends Stm32 { 275 | async flash_erase_all() { 276 | this._dbg.debug("Stm32FS.flash_erase_all()"); 277 | let flash = new Flash(this, this._stlink, this._dbg); 278 | await flash.init(); 279 | await flash.erase_all(); 280 | await flash.lock(); 281 | } 282 | 283 | async flash_write(addr, data, {erase = false, verify = false, erase_sizes = null}) { 284 | let addr_str = (addr !== null) ? `0x${H32(addr)}` : 'None'; 285 | this._dbg.debug(`Stm32FS.flash_write(${addr_str}, [data:${data.length}Bytes], erase=${erase}, verify=${verify}, erase_sizes=${erase_sizes})`); 286 | if (addr === null) { 287 | addr = this.FLASH_START; 288 | } 289 | if (addr % 4) { 290 | throw new Exception("Start address is not aligned to word"); 291 | } 292 | // align data 293 | if (data.length % 4) { 294 | let padded_data = new Uint8Array(data.length + (4 - (data.length % 4))); 295 | data.forEach((b, i) => padded_data[i] = b); 296 | padded_data.fill(0xff, data.length); 297 | data = padded_data; 298 | } 299 | let flash = new Flash(this, this._stlink, this._dbg); 300 | await flash.init(); 301 | if (erase) { 302 | if (erase_sizes) { 303 | await flash.erase_sectors(this.FLASH_START, erase_sizes, addr, data.length); 304 | } else { 305 | await flash.erase_all(); 306 | } 307 | } 308 | this._dbg.bargraph_start("Writing FLASH", { 309 | "value_min": addr, 310 | "value_max": (addr + data.length) 311 | }); 312 | await flash.init_write(Stm32FS.SRAM_START); 313 | while (data.length > 0) { 314 | this._dbg.bargraph_update({"value": addr}); 315 | let block = data.slice(0, this._stlink.maximum_transfer_size); 316 | data = data.slice(this._stlink.maximum_transfer_size); 317 | await flash.write(addr, block); 318 | if (verify) { 319 | let flashed_data = await this._stlink.get_mem32(addr, block.length); 320 | let flashed_block = new Uint8Array(flashed_data.buffer); 321 | let verified = false; 322 | if (flashed_block.length == block.length) { 323 | verified = flashed_block.every((octet, index) => octet == block[index]); 324 | } 325 | if (!verified) { 326 | throw new Exception("Verify error at block address: 0x" + H32(addr)); 327 | } 328 | } 329 | addr += block.length; 330 | } 331 | await flash.lock(); 332 | this._dbg.bargraph_done(); 333 | } 334 | } 335 | 336 | export {Stm32FS}; 337 | -------------------------------------------------------------------------------- /src/js/stlink/lib/stlinkv2.js: -------------------------------------------------------------------------------- 1 | /* stlinkv2.js 2 | * ST-Link probe API class 3 | * 4 | * Copyright Devan Lai 2017 5 | * 6 | * Ported from lib/stlinkv2.py in the pystlink project, 7 | * Copyright Pavel Revak 2015 8 | * 9 | */ 10 | 11 | import {Exception, Warning} from './stlinkex.js'; 12 | 13 | const STLINK_GET_VERSION = 0xf1; 14 | const STLINK_DEBUG_COMMAND = 0xf2; 15 | const STLINK_DFU_COMMAND = 0xf3; 16 | const STLINK_SWIM_COMMAND = 0xf4; 17 | const STLINK_GET_CURRENT_MODE = 0xf5; 18 | const STLINK_GET_TARGET_VOLTAGE = 0xf7; 19 | 20 | const STLINK_MODE_DFU = 0x00; 21 | const STLINK_MODE_MASS = 0x01; 22 | const STLINK_MODE_DEBUG = 0x02; 23 | const STLINK_MODE_SWIM = 0x03; 24 | const STLINK_MODE_BOOTLOADER = 0x04; 25 | 26 | const STLINK_DFU_EXIT = 0x07; 27 | 28 | const STLINK_SWIM_ENTER = 0x00; 29 | const STLINK_SWIM_EXIT = 0x01; 30 | 31 | const STLINK_DEBUG_ENTER_JTAG = 0x00; 32 | const STLINK_DEBUG_STATUS = 0x01; 33 | const STLINK_DEBUG_FORCEDEBUG = 0x02; 34 | const STLINK_DEBUG_APIV1_RESETSYS = 0x03; 35 | const STLINK_DEBUG_APIV1_READALLREGS = 0x04; 36 | const STLINK_DEBUG_APIV1_READREG = 0x05; 37 | const STLINK_DEBUG_APIV1_WRITEREG = 0x06; 38 | const STLINK_DEBUG_READMEM_32BIT = 0x07; 39 | const STLINK_DEBUG_WRITEMEM_32BIT = 0x08; 40 | const STLINK_DEBUG_RUNCORE = 0x09; 41 | const STLINK_DEBUG_STEPCORE = 0x0a; 42 | const STLINK_DEBUG_APIV1_SETFP = 0x0b; 43 | const STLINK_DEBUG_READMEM_8BIT = 0x0c; 44 | const STLINK_DEBUG_WRITEMEM_8BIT = 0x0d; 45 | const STLINK_DEBUG_APIV1_CLEARFP = 0x0e; 46 | const STLINK_DEBUG_APIV1_WRITEDEBUGREG = 0x0f; 47 | const STLINK_DEBUG_APIV1_SETWATCHPOINT = 0x10; 48 | const STLINK_DEBUG_APIV1_ENTER = 0x20; 49 | const STLINK_DEBUG_EXIT = 0x21; 50 | const STLINK_DEBUG_READCOREID = 0x22; 51 | const STLINK_DEBUG_APIV2_ENTER = 0x30; 52 | const STLINK_DEBUG_APIV2_READ_IDCODES = 0x31; 53 | const STLINK_DEBUG_APIV2_RESETSYS = 0x32; 54 | const STLINK_DEBUG_APIV2_READREG = 0x33; 55 | const STLINK_DEBUG_APIV2_WRITEREG = 0x34; 56 | const STLINK_DEBUG_APIV2_WRITEDEBUGREG = 0x35; 57 | const STLINK_DEBUG_APIV2_READDEBUGREG = 0x36; 58 | const STLINK_DEBUG_APIV2_READALLREGS = 0x3a; 59 | const STLINK_DEBUG_APIV2_GETLASTRWSTATUS = 0x3b; 60 | const STLINK_DEBUG_APIV2_DRIVE_NRST = 0x3c; 61 | const STLINK_DEBUG_SYNC = 0x3e; 62 | const STLINK_DEBUG_APIV2_START_TRACE_RX = 0x40; 63 | const STLINK_DEBUG_APIV2_STOP_TRACE_RX = 0x41; 64 | const STLINK_DEBUG_APIV2_GET_TRACE_NB = 0x42; 65 | const STLINK_DEBUG_APIV2_SWD_SET_FREQ = 0x43; 66 | const STLINK_DEBUG_ENTER_SWD = 0xa3; 67 | 68 | const STLINK_DEBUG_APIV2_DRIVE_NRST_LOW = 0x00; 69 | const STLINK_DEBUG_APIV2_DRIVE_NRST_HIGH = 0x01; 70 | const STLINK_DEBUG_APIV2_DRIVE_NRST_PULSE = 0x02; 71 | 72 | const STLINK_MAXIMUM_TRANSFER_SIZE = 1024; 73 | 74 | const STLINK_DEBUG_APIV2_SWD_SET_FREQ_MAP = [ 75 | [4000000, 0], 76 | [1800000, 1], // default 77 | [1200000, 2], 78 | [950000, 3], 79 | [480000, 7], 80 | [240000, 15], 81 | [125000, 31], 82 | [100000, 40], 83 | [50000, 79], 84 | [25000, 158], 85 | [15000, 265], 86 | [5000, 798] 87 | ]; 88 | 89 | export default class Stlink { 90 | constructor(connector, dbg) { 91 | this._connector = connector; 92 | this._dbg = dbg; 93 | } 94 | 95 | _debug(msg) { 96 | if (this._dbg) { 97 | this._dbg.debug(msg); 98 | } 99 | } 100 | 101 | async init(swd_frequency = 1800000) { 102 | await this.read_version(); 103 | await this.leave_state(); 104 | await this.read_target_voltage(); 105 | if (this._ver_jtag >= 22) { 106 | await this.set_swd_freq(swd_frequency); 107 | } 108 | await this.enter_debug_swd(); 109 | await this.read_coreid(); 110 | } 111 | 112 | async clean_exit() { 113 | // WORKAROUND for OS/X 10.11+ 114 | // ... read from ST-Link, must be performed even times 115 | // call this function after last send command 116 | if (this._connector.xfer_counter & 1) { 117 | await this._connector.xfer([STLINK_GET_CURRENT_MODE], {"rx_len": 2}); 118 | } 119 | } 120 | 121 | async read_version() { 122 | // WORKAROUNF for OS/X 10.11+ 123 | // ... retry XFER if first is timeout. 124 | // only during this command it is necessary 125 | let rx = await this._connector.xfer([STLINK_GET_VERSION, 0x80], {"rx_len": 6, "retry": 2}); 126 | let ver = rx.getUint16(0); 127 | 128 | let dev_ver = this._connector.version; 129 | this._ver_stlink = ((ver >> 12) & 0x0f); 130 | this._ver_jtag = ((ver >> 6) & 0x3f); 131 | this._ver_swim = ((dev_ver === "V2") ? (ver & 0x3f) : null); 132 | this._ver_mass = ((dev_ver === "V2-1") ? (ver & 0x3f) : null); 133 | this._ver_api = ((this._ver_jtag > 11) ? 2 : 1); 134 | this._ver_str = `${dev_ver} V${this._ver_stlink}J${this._ver_jtag}`; 135 | if (dev_ver === "V2") { 136 | this._ver_str += ("S" + this._ver_swim); 137 | } 138 | if (dev_ver === "V2-1") { 139 | this._ver_str += ("M" + this._ver_mass); 140 | } 141 | if (this.ver_api === 1) { 142 | throw new Warning(`ST-Link/${this._ver_str} is not supported, please upgrade firmware.`); 143 | } 144 | if (this.ver_jtag < 21) { 145 | throw new Warning(`ST-Link/${this._ver_str} is not recent firmware, please upgrade first - functionality is not guaranteed.`); 146 | } 147 | } 148 | 149 | get maximum_transfer_size() { 150 | return STLINK_MAXIMUM_TRANSFER_SIZE; 151 | } 152 | 153 | get ver_stlink() { 154 | return this._ver_stlink; 155 | } 156 | 157 | get ver_jtag() { 158 | return this._ver_jtag; 159 | } 160 | 161 | get ver_mass() { 162 | return this._ver_mass; 163 | } 164 | 165 | get ver_swim() { 166 | return this._ver_swim; 167 | } 168 | 169 | get ver_api() { 170 | return this._ver_api; 171 | } 172 | 173 | get ver_str() { 174 | return this._ver_str; 175 | } 176 | 177 | async read_target_voltage() { 178 | let rx = await this._connector.xfer([STLINK_GET_TARGET_VOLTAGE], {"rx_len": 8}); 179 | let a0 = rx.getUint32(0, true); 180 | let a1 = rx.getUint32(4, true); 181 | this._target_voltage = (a0 !== 0) ? (2 * a1 * 1.2 / a0) : null; 182 | } 183 | 184 | get target_voltage() { 185 | return this._target_voltage; 186 | } 187 | 188 | async read_coreid() { 189 | let rx = await this._connector.xfer([STLINK_DEBUG_COMMAND, STLINK_DEBUG_READCOREID], {"rx_len": 4}); 190 | this._coreid = rx.getUint32(0, true); 191 | } 192 | 193 | get coreid() { 194 | return this._coreid; 195 | } 196 | 197 | async leave_state() { 198 | let rx = await this._connector.xfer([STLINK_GET_CURRENT_MODE], {"rx_len": 2}); 199 | let state = rx.getUint8(0); 200 | 201 | if (state === STLINK_MODE_DFU) { 202 | this._debug("Leaving state DFU"); 203 | await this._connector.xfer([STLINK_DFU_COMMAND, STLINK_DFU_EXIT]); 204 | } else if (state === STLINK_MODE_DEBUG) { 205 | this._debug("Leaving state DEBUG"); 206 | await this._connector.xfer([STLINK_DEBUG_COMMAND, STLINK_DEBUG_EXIT]); 207 | } else if (state === STLINK_MODE_SWIM) { 208 | this._debug("Leaving state is SWIM"); 209 | await this._connector.xfer([STLINK_SWIM_COMMAND, STLINK_SWIM_EXIT]); 210 | } 211 | } 212 | 213 | async set_swd_freq(freq = 1800000) { 214 | for (let [f, divisor] of STLINK_DEBUG_APIV2_SWD_SET_FREQ_MAP) { 215 | if (freq >= f) { 216 | let cmd = [STLINK_DEBUG_COMMAND, STLINK_DEBUG_APIV2_SWD_SET_FREQ, divisor]; 217 | let rx = await this._connector.xfer(cmd, {"rx_len": 2}); 218 | let status = rx.getUint8(0); 219 | if (status !== 0x80) { 220 | throw new Exception("Error switching SWD frequency"); 221 | } 222 | this._debug(`Set SWD frequency to ${f}Hz`); 223 | return; 224 | } 225 | } 226 | throw new Exception("Selected SWD frequency is too low"); 227 | } 228 | 229 | async enter_debug_swd() { 230 | await this._connector.xfer([STLINK_DEBUG_COMMAND, STLINK_DEBUG_APIV2_ENTER, STLINK_DEBUG_ENTER_SWD], {"rx_len": 2}); 231 | this._debug("Entered SWD debug mode"); 232 | } 233 | 234 | async debug_resetsys() { 235 | await this._connector.xfer([STLINK_DEBUG_COMMAND, STLINK_DEBUG_APIV2_RESETSYS], {"rx_len": 2}); 236 | this._debug("Sent reset"); 237 | } 238 | 239 | set_debugreg32(addr, data) { 240 | if (addr % 4) { 241 | throw new Exception("get_mem_short address is not in multiples of 4"); 242 | } 243 | let cmd = new ArrayBuffer(10); 244 | let view = new DataView(cmd); 245 | view.setUint8(0, STLINK_DEBUG_COMMAND); 246 | view.setUint8(1, STLINK_DEBUG_APIV2_WRITEDEBUGREG); 247 | view.setUint32(2, addr, true); 248 | view.setUint32(6, data, true); 249 | return this._connector.xfer(cmd, {"rx_len": 2}); 250 | } 251 | 252 | async get_debugreg32(addr) { 253 | if (addr % 4) { 254 | throw new Exception("get_mem_short address is not in multiples of 4"); 255 | } 256 | let cmd = new ArrayBuffer(6); 257 | let view = new DataView(cmd); 258 | view.setUint8(0, STLINK_DEBUG_COMMAND); 259 | view.setUint8(1, STLINK_DEBUG_APIV2_READDEBUGREG); 260 | view.setUint32(2, addr, true); 261 | let rx = await this._connector.xfer(cmd, {"rx_len": 8}); 262 | return rx.getUint32(4, true); 263 | } 264 | 265 | async get_debugreg16(addr) { 266 | if (addr % 2) { 267 | throw new Exception("get_mem_short address is not in even"); 268 | } 269 | let val = await this.get_debugreg32(addr & 0xfffffffc); 270 | if (addr % 4) { 271 | val >>= 16; 272 | } 273 | return (val & 0xffff); 274 | } 275 | 276 | async get_debugreg8(addr) { 277 | let val = await this.get_debugreg32(addr & 0xfffffffc); 278 | val >>= (addr % 4) << 3; 279 | return (val & 0xff); 280 | } 281 | 282 | async get_reg(reg) { 283 | let cmd = [STLINK_DEBUG_COMMAND, STLINK_DEBUG_APIV2_READREG, reg]; 284 | let rx = await this._connector.xfer(cmd, {"rx_len": 8}); 285 | return rx.getUint32(4, true); 286 | } 287 | 288 | set_reg(reg, data) { 289 | let cmd = new ArrayBuffer(7); 290 | let view = new DataView(cmd); 291 | view.setUint8(0, STLINK_DEBUG_COMMAND); 292 | view.setUint8(1, STLINK_DEBUG_APIV2_WRITEREG); 293 | view.setUint8(2, reg); 294 | view.setUint32(3, data, true); 295 | return this._connector.xfer(cmd, {"rx_len": 2}); 296 | } 297 | 298 | get_mem32(addr, size) { 299 | if (addr % 4) { 300 | throw new Exception("get_mem32: Address must be in multiples of 4"); 301 | } 302 | if (size % 4) { 303 | throw new Exception("get_mem32: Size must be in multiples of 4"); 304 | } 305 | if (size > STLINK_MAXIMUM_TRANSFER_SIZE) { 306 | throw new Exception(`get_mem32: Size for reading is ${size} but maximum can be ${STLINK_MAXIMUM_TRANSFER_SIZE}`); 307 | } 308 | let cmd = new ArrayBuffer(10); 309 | let view = new DataView(cmd); 310 | view.setUint8(0, STLINK_DEBUG_COMMAND); 311 | view.setUint8(1, STLINK_DEBUG_READMEM_32BIT); 312 | view.setUint32(2, addr, true); 313 | view.setUint32(6, size, true); 314 | return this._connector.xfer(cmd, {"rx_len": size}); 315 | } 316 | 317 | set_mem32(addr, data) { 318 | if (addr % 4) { 319 | throw new Exception("set_mem32: Address must be in multiples of 4"); 320 | } 321 | if (data.length % 4) { 322 | throw new Exception("set_mem32: Size must be in multiples of 4"); 323 | } 324 | if (data.length > STLINK_MAXIMUM_TRANSFER_SIZE) { 325 | throw new Exception(`set_mem32: Size for writing is ${data.length} but maximum can be ${STLINK_MAXIMUM_TRANSFER_SIZE}`); 326 | } 327 | let cmd = new ArrayBuffer(10); 328 | let view = new DataView(cmd); 329 | view.setUint8(0, STLINK_DEBUG_COMMAND); 330 | view.setUint8(1, STLINK_DEBUG_WRITEMEM_32BIT); 331 | view.setUint32(2, addr, true); 332 | view.setUint32(6, data.length, true); 333 | return this._connector.xfer(cmd, {"data": data}); 334 | } 335 | 336 | get_mem8(addr, size) { 337 | if (size > 64) { 338 | throw new Exception(`get_mem8: Size for reading is ${size} but maximum can be 64`); 339 | } 340 | let cmd = new ArrayBuffer(10); 341 | let view = new DataView(cmd); 342 | view.setUint8(0, STLINK_DEBUG_COMMAND); 343 | view.setUint8(1, STLINK_DEBUG_READMEM_8BIT); 344 | view.setUint32(2, addr, true); 345 | view.setUint32(6, size, true); 346 | return this._connector.xfer(cmd, {"rx_len": size}); 347 | } 348 | 349 | set_mem8(addr, data) { 350 | if (data.length > 64) { 351 | throw new Exception(`set_mem8: Size for writing is ${data.length} but maximum can be 64`); 352 | } 353 | let cmd = new ArrayBuffer(10); 354 | let view = new DataView(cmd); 355 | view.setUint8(0, STLINK_DEBUG_COMMAND); 356 | view.setUint8(1, STLINK_DEBUG_WRITEMEM_8BIT); 357 | view.setUint32(2, addr, true); 358 | view.setUint32(6, data.length, true); 359 | return this._connector.xfer(cmd, {"data": data}); 360 | } 361 | } 362 | 363 | -------------------------------------------------------------------------------- /src/js/xmodem.js: -------------------------------------------------------------------------------- 1 | import {TransportEx} from './serialex.js' 2 | import {Bootloader, Passthrough} from './passthrough.js' 3 | import {MismatchError, PassthroughError} from "./error.js"; 4 | 5 | const log = { 6 | info: function () { 7 | }, warn: function () { 8 | } 9 | } 10 | 11 | const SOH = 0x01 12 | const EOT = 0x04 13 | const ACK = 0x06 14 | const NAK = 0x15 15 | const FILLER = 0x1A 16 | const CRC_MODE = 0x43 // 'C' 17 | 18 | class Xmodem { 19 | XMODEM_OP_MODE = 'crc' 20 | XMODEM_START_BLOCK = 1 21 | block_size = 128 22 | 23 | constructor(device, logger) { 24 | this.device = device 25 | this.logger = logger 26 | } 27 | 28 | emit = (msg, obj) => { 29 | console.log(`${msg}: ${obj}`) 30 | } 31 | 32 | crc16xmodem = function (buf) { 33 | let crc = 0x0 34 | 35 | for (let index = 0; index < buf.length; index++) { 36 | const byte = buf[index] 37 | let code = (crc >>> 8) & 0xff 38 | 39 | code ^= byte & 0xff 40 | code ^= code >>> 4 41 | crc = (crc << 8) & 0xffff 42 | crc ^= code 43 | code = (code << 5) & 0xffff 44 | crc ^= code 45 | code = (code << 7) & 0xffff 46 | crc ^= code 47 | } 48 | 49 | return crc 50 | } 51 | 52 | send = async (dataBuffer, progress) => { 53 | const _self = this 54 | const packagedBuffer = [] 55 | let blockNumber = this.XMODEM_START_BLOCK 56 | let sentEof = false 57 | 58 | log.info(dataBuffer.length) 59 | 60 | // FILLER 61 | for (let i = 0; i < this.XMODEM_START_BLOCK; i++) { 62 | packagedBuffer.push('') 63 | } 64 | 65 | while (dataBuffer.length > 0) { 66 | const chunk = dataBuffer.slice(0, this.block_size) 67 | const currentBlock = new Uint8Array(this.block_size) 68 | currentBlock.set(chunk, 0) 69 | for (let i = chunk.length; i < this.block_size; i++) { 70 | currentBlock[i] = FILLER 71 | } 72 | dataBuffer = dataBuffer.slice(this.block_size) 73 | packagedBuffer.push(currentBlock) 74 | } 75 | 76 | let sending = true 77 | 78 | _self.emit('ready', packagedBuffer.length - 1) // We don't count the filler 79 | 80 | const sendBlock = this.sendBlock 81 | const write = this.write 82 | const sendData = async (data) => { 83 | /* 84 | * Here we handle the beginning of the transmission 85 | * The receiver initiates the transfer by either calling 86 | * checksum mode or CRC mode. 87 | */ 88 | if (data[0] === CRC_MODE && blockNumber === _self.XMODEM_START_BLOCK) { 89 | log.info('[SEND] - received C byte for CRC transfer!') 90 | _self.XMODEM_OP_MODE = 'crc' 91 | if (packagedBuffer.length > blockNumber) { 92 | /* 93 | * Transmission Start event. A successful start of transmission. 94 | * @event Xmodem#start 95 | * @property {string} - Indicates transmission mode 'crc' or 'normal' 96 | */ 97 | _self.emit('start', _self.XMODEM_OP_MODE) 98 | await sendBlock(blockNumber, packagedBuffer[blockNumber], _self.XMODEM_OP_MODE) 99 | _self.emit('send', blockNumber) 100 | blockNumber++ 101 | } 102 | } else if (data[0] === NAK && blockNumber === _self.XMODEM_START_BLOCK) { 103 | log.info('[SEND] - received NAK byte for standard checksum transfer!') 104 | _self.XMODEM_OP_MODE = 'normal' 105 | if (packagedBuffer.length > blockNumber) { 106 | _self.emit('start', _self.XMODEM_OP_MODE) 107 | await sendBlock(blockNumber, packagedBuffer[blockNumber], _self.XMODEM_OP_MODE) 108 | _self.emit('send', blockNumber) 109 | blockNumber++ 110 | } 111 | } else if (data[0] === ACK && blockNumber > _self.XMODEM_START_BLOCK) { 112 | /* 113 | * Here we handle the actual transmission of data and 114 | * retransmission in case the block was not accepted. 115 | */ 116 | // Woohooo we are ready to send the next block! :) 117 | log.info('ACK RECEIVED') 118 | _self.emit('recv', 'ACK') 119 | if (packagedBuffer.length > blockNumber) { 120 | await sendBlock(blockNumber, packagedBuffer[blockNumber], _self.XMODEM_OP_MODE) 121 | _self.emit('send', blockNumber) 122 | blockNumber++ 123 | if (blockNumber % 10 === 0) { 124 | const percent = Math.floor(blockNumber * 100 / packagedBuffer.length) 125 | progress(1, percent, 100) 126 | _self.logger.log(`${percent}% uploaded...`) 127 | } 128 | } else if (packagedBuffer.length === blockNumber) { 129 | // We are EOT 130 | if (sentEof === false) { 131 | sentEof = true 132 | log.info('WE HAVE RUN OUT OF STUFF TO SEND, EOT EOT!') 133 | _self.emit('send', 'EOT') 134 | await write(new Uint8Array([EOT])) 135 | } else { 136 | // We are finished! 137 | log.info('[SEND] - Finished!') 138 | _self.emit('stop', 0) 139 | progress(1, 100, 100) 140 | sending = false 141 | } 142 | } 143 | } else if (data[0] === NAK && blockNumber > _self.XMODEM_START_BLOCK) { 144 | if (blockNumber === packagedBuffer.length && sentEof) { 145 | log.info('[SEND] - Resending EOT, because receiver responded with NAK.') 146 | _self.emit('send', 'EOT') 147 | await write(new Uint8Array([EOT])) 148 | } else { 149 | log.info('[SEND] - Packet corruption detected, resending previous block.') 150 | _self.emit('recv', 'NAK') 151 | blockNumber-- 152 | if (packagedBuffer.length > blockNumber) { 153 | await sendBlock(blockNumber, packagedBuffer[blockNumber], _self.XMODEM_OP_MODE) 154 | _self.emit('send', blockNumber) 155 | blockNumber++ 156 | } 157 | } 158 | } else { 159 | log.warn('GOT SOME UNEXPECTED DATA which was not handled properly!') 160 | log.warn('===>') 161 | log.warn(data) 162 | log.warn('<===') 163 | log.warn('blockNumber: ' + blockNumber) 164 | } 165 | } 166 | 167 | // eslint-disable-next-line no-unmodified-loop-condition 168 | while (sending) { 169 | const reader = this.device.readable.getReader() 170 | // PAK need a timeout and handler 171 | const {value, done} = await reader.read() 172 | if (done) { 173 | reader.releaseLock() 174 | throw new Error('cancelled') 175 | } 176 | reader.releaseLock() 177 | await sendData(value) 178 | } 179 | this.logger.log('Flash complete!') 180 | } 181 | 182 | sendBlock = async (blockNr, blockData, mode) => { 183 | function _appendBuffer(buffer1, buffer2) { 184 | const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength) 185 | tmp.set(buffer1, 0) 186 | tmp.set(buffer2, buffer1.byteLength) 187 | return tmp 188 | } 189 | 190 | let crcCalc = 0 191 | let sendBuffer = _appendBuffer(new Uint8Array([SOH, blockNr, (0xFF - blockNr)]), blockData) 192 | log.info('SENDBLOCK! Data length: ' + blockData.byteLength) 193 | log.info(sendBuffer) 194 | if (mode === 'crc') { 195 | const crc = this.crc16xmodem(blockData) 196 | sendBuffer = _appendBuffer(sendBuffer, new Uint8Array([(crc >>> 8) & 0xff, crc & 0xff])) 197 | } else { 198 | // Count only the blockData into the checksum 199 | for (let i = 3; i < sendBuffer.byteLength; i++) { 200 | crcCalc = crcCalc + sendBuffer.readUInt8(i) 201 | } 202 | crcCalc = crcCalc % 256 203 | sendBuffer = _appendBuffer(sendBuffer, new Uint8Array([crcCalc])) 204 | } 205 | log.info('Sending buffer with total length: ' + sendBuffer.length) 206 | await this.write(sendBuffer) 207 | } 208 | 209 | write = async (buf) => { 210 | const writer = this.device.writable.getWriter() 211 | await writer.write(buf.buffer) 212 | writer.releaseLock() 213 | } 214 | } 215 | 216 | export class XmodemFlasher { 217 | constructor(device, deviceType, method, config, options, firmwareUrl, terminal) { 218 | this.device = device 219 | this.config = config 220 | this.options = options 221 | this.firmwareUrl = firmwareUrl 222 | this.terminal = terminal 223 | this.xmodem = new Xmodem(this.device, this) 224 | } 225 | 226 | _sleep(ms) { 227 | return new Promise(resolve => setTimeout(resolve, ms)) 228 | } 229 | 230 | log(str) { 231 | this.terminal.writeln(str) 232 | } 233 | 234 | connect = async () => { 235 | if (this.config.firmware.startsWith('GHOST')) { 236 | this.init_seq1 = Bootloader.get_init_seq('GHST') 237 | } else { 238 | this.init_seq1 = Bootloader.get_init_seq('CRSF') 239 | } 240 | 241 | this.transport = new TransportEx(this.device, true) 242 | await this.transport.connect(420000) 243 | this.passthrough = new Passthrough(this.transport, this.terminal, this.config.firmware, 420000) 244 | await this.startBootloader() 245 | return 'XModem Flasher' 246 | } 247 | 248 | startBootloader = async (force = false) => { 249 | this.transport.set_delimiters(['CCC']) 250 | const data = await this.transport.read_line(2000) 251 | let gotBootloader = data.endsWith('CCC') 252 | if (!gotBootloader) { 253 | let delaySeq2 = 500 254 | await this.passthrough.betaflight() 255 | this.transport.set_delimiters(['CCC']) 256 | const data = await this.transport.read_line(2000) 257 | gotBootloader = data.endsWith('CCC') 258 | if (!gotBootloader) { 259 | this.transport.set_delimiters(['\n', 'CCC']) 260 | let currAttempt = 0 261 | this.log('Attempting to reboot into bootloader...') 262 | while (!gotBootloader) { 263 | currAttempt++ 264 | if (currAttempt > 10) { 265 | throw new Error('Failed to enter bootloader mode in a reasonable time') 266 | } 267 | this.log(`[${currAttempt}] retry...`) 268 | 269 | await this.transport.write(this.init_seq1) 270 | 271 | const start = Date.now() 272 | do { 273 | const line = await this.transport.read_line(2000) 274 | if (line === '') { 275 | continue 276 | } 277 | 278 | if (line.indexOf('BL_TYPE') !== -1) { 279 | const blType = line.substring(8).trim() 280 | this.log(`Bootloader type found : '${blType}`) 281 | delaySeq2 = 100 282 | continue 283 | } 284 | 285 | const versionMatch = line.match(/=== (?[vV].*) ===/) 286 | if (versionMatch && versionMatch.groups && versionMatch.groups.version) { 287 | this.log(`Bootloader version found : '${versionMatch.groups.version}'`) 288 | } else if (line.indexOf('hold down button') !== -1) { 289 | await this._sleep(delaySeq2) 290 | await this.transport.write_string('bbbbbb') 291 | gotBootloader = true 292 | break 293 | } else if (line.indexOf('CCC') !== -1) { 294 | gotBootloader = true 295 | break 296 | } else if (line.indexOf('_RX_') !== -1) { 297 | const flashTarget = this.config.firmware.toUpperCase() 298 | 299 | if (line.trim() !== flashTarget && !force) { 300 | this.log(`Wrong target selected your RX is '${line.trim()}', trying to flash '${flashTarget}'`) 301 | throw new MismatchError() 302 | } else if (flashTarget !== '') { 303 | this.log(`Verified RX target '${flashTarget}'`) 304 | } 305 | } 306 | } while (Date.now() - start < 2000) 307 | } 308 | this.log(`Got into bootloader after: ${currAttempt} attempts`) 309 | this.log('Waiting for sync...') 310 | this.transport.set_delimiters(['CCC']) 311 | const data = await this.transport.read_line(15000) 312 | if (data.indexOf('CCC') === -1) { 313 | this.log('[FAILED] Unable to communicate with bootloader...') 314 | throw new PassthroughError() 315 | } 316 | this.log('Sync OK') 317 | } 318 | } 319 | } 320 | 321 | flash = async (binary, force = false, progress) => { 322 | await this.startBootloader(true); 323 | this.log('Beginning flash...') 324 | return this.xmodem.send(binary[0].data, progress) 325 | } 326 | } 327 | --------------------------------------------------------------------------------