├── .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 |
4 |
5 |
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 |
8 |
11 |
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 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/run dev.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | mdns-proxy/mdns.dSYM
15 | mdns-proxy/mdns
16 |
17 | # Firmware files
18 | public/assets/
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | .DS_Store
25 | *.suo
26 | *.ntvs*
27 | *.njsproj
28 | *.sln
29 | *.sw?
30 |
--------------------------------------------------------------------------------
/.idea/flasher.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/RXOptions.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/WiFiSettingsInput.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/MelodyInput.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/BindPhraseInput.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
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 |
16 |
17 |
18 |
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 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.run/get_artifacts.sh.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/TXOptions.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
10 |
12 |
13 |
15 |
18 |
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 |
17 |
18 |
19 | App ready to work offline
20 |
21 |
22 | New content available, reload the page?
23 |
24 |
25 | Reload
26 | Dismiss
27 |
28 |
29 |
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 |
33 |
34 |
35 |
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 |
26 |
27 | Backpack Options
28 | Set the flashing options and method for your {{ store.name }}
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/pages/TransmitterOptions.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 | Transmitter Options
14 | Set the flashing options and method for your {{ store.target?.config?.product_name }}
15 |
16 |
17 |
18 |
19 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/pages/ReceiverOptions.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 | Receiver Options
17 | Set the flashing options and method for your {{ store.target?.config?.product_name }}
18 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/.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 |
17 |
18 |
19 |
20 |
![]()
21 |
![]()
22 |
23 | {{ title }}
24 | {{ text }}
25 |
26 |
27 |
28 |
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 |
52 |
53 | Download Firmware File(s)
54 | The firmware file(s) have been configured for your {{ store.target?.config?.product_name }} with
55 | the specified options.
56 |
57 | To flash the firmware file to your device, put it into WiFi mode and connect to it via the browser
58 | then upload the firmware.bin{{ store.target.config.platform === 'esp8285' ? '.gz' : '' }} file on the
59 | Update tab.
60 |
61 |
62 | The firmware file firmware.bin.gz should be flashed as-is, do NOT decompress or unzip the file or you will
63 | receive an error.
64 |
65 |
66 | The firmware files are contained in the firmware.zip file and should be extracted before being uploaded to
67 | the device for flashing.
68 |
69 |
70 | Download
71 |
72 |
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 |
16 |
17 |
21 |
22 |
23 |
28 |
29 |
30 |
34 |
35 |
36 |
37 |
38 |
42 |
43 |
44 |
49 |
50 |
51 |
56 |
57 |
58 |
63 |
64 |
65 |
70 |
71 |
72 |
73 |
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 |
15 |
--------------------------------------------------------------------------------
/src/pages/BackpackHardwareSelect.vue:
--------------------------------------------------------------------------------
1 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Transmitter Hardware Selection
107 | Choose the transmitter module that is having it's backpack flashed
108 |
109 |
110 | VRx Hardware Selection
111 | Choose the video receiver type and hardware to be flashed
112 |
113 |
114 | Antenna Tracker Hardware Selection
115 | Choose the antenna tracker type and hardware to be flashed
116 |
117 |
118 | Race Timer Hardware Selection
119 | Choose the race timer and hardware to be flashed
120 |
121 |
122 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/src/pages/STLinkFlash.vue:
--------------------------------------------------------------------------------
1 |
121 |
122 |
123 |
124 | Flash Firmware File(s)
125 | The firmware file(s) have been configured for your {{ store.target?.config?.product_name }} with
126 | the specified options.
127 |
128 |
129 |
130 |
132 | Connect
133 |
134 |
136 |
137 | {{ line }}
138 |
139 |
140 | Flash
141 | Flash Anyway
142 | Try Again
143 |
144 |
146 |
147 |
148 | Flashing file {{ progressText }}
149 |
150 |
152 | {{ progress }} %
153 |
154 |
155 | Flash failed
156 |
157 | Try Again
158 |
159 |
160 |
161 |
162 |
164 |
165 |
166 |
167 | Flash Another
168 |
169 |
170 | Back to Start
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | No Device Selected
179 |
180 | A serial device must be selected to perform flashing.
181 |
182 |
183 |
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 |
162 |
163 |
164 |
165 |
166 |
167 | Hardware Selection
168 | Choose the vendor specific hardware that you are flashing, if the hardware is not in the list then the
169 | hardware is unsupported.
170 |
171 |
172 |
173 |
175 |
177 |
179 |
180 | Download ELRS Lua Script
181 |
182 |
183 |
--------------------------------------------------------------------------------
/src/pages/SerialFlash.vue:
--------------------------------------------------------------------------------
1 |
170 |
171 |
172 |
173 | Flash Firmware File(s)
174 | The firmware file(s) have been configured for your {{ store.target?.config?.product_name }} with
175 | the specified options.
176 |
177 |
178 |
179 |
181 | Connect
182 |
183 |
185 |
186 | {{ line }}
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | Flash
197 |
198 |
199 | Flash Anyway
200 |
201 |
202 | Try Again
203 |
204 |
205 |
206 |
207 |
209 |
210 |
211 | Erasing flash, please wait...
212 | Flashing file {{ progressText }}
213 |
214 |
216 | {{ progress }} %
217 |
218 |
219 | Flash failed
220 |
221 | Try Again
222 |
223 |
224 |
225 |
226 |
228 |
229 |
230 |
231 | Flash Another
232 |
233 |
234 | Back to Start
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 | No Device Selected
243 |
244 | A serial device must be selected to perform flashing.
245 |
246 |
247 |
248 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
62 |
63 |
64 |
ExpressLRS™
65 | WEB FLASHER
66 |
67 |
68 |
69 | Git: @GITHASH@
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
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 |
--------------------------------------------------------------------------------