├── bridge ├── .gitignore ├── entrypoint.sh ├── embed.go ├── go.mod ├── Dockerfile ├── Makefile ├── main.go ├── README.md ├── build.sh ├── go.sum ├── mdns.go ├── websocket.go └── routes.go ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── 03-feature-request.yml │ ├── 04-firmware-request.yml │ ├── 02-bridge-issue.yml │ └── 01-flashing-issue.yml ├── badges │ └── ghcr-downloads.json ├── workflows │ ├── draft-release-notes.yml │ ├── ghcr-downloads-badge.yml │ ├── notify-release.yml │ └── build-binaries.yml ├── FUNDING.yml ├── release-drafter.yml └── scripts │ └── ghcr_downloads.py ├── .vscode ├── settings.json └── tasks.json ├── docs ├── imgs │ ├── dark.png │ ├── light.png │ └── telegram_banner.png ├── how-to │ ├── imgs │ │ ├── zadig.jpg │ │ ├── TLSRPGM_dark.png │ │ ├── TLSRPGM_light.png │ │ ├── TlsrComProg_dark.png │ │ ├── debugger-pinout.png │ │ ├── device_manager.jpg │ │ └── TlsrComProg_light.png │ ├── readme.md │ ├── cc_loader.md │ ├── cc_debuger.md │ └── telink.md ├── readme.md └── devices.md ├── web-page ├── favicon │ ├── icon.png │ ├── logo.png │ ├── favicon-data.json │ ├── favicon-settings.json │ └── logo.svg ├── static │ ├── imgs │ │ ├── si.png │ │ ├── ti.png │ │ ├── esp.png │ │ ├── arduino.png │ │ └── telink.png │ ├── bins │ │ ├── floader_825x.bin │ │ ├── floader_826x.bin │ │ └── uart2swire.bin │ └── fonts │ │ └── bootstrap-icons.woff2 ├── .gitignore ├── tsconfig.json ├── eslint.config.mts ├── src │ ├── utils │ │ ├── index.ts │ │ ├── xmodem.ts │ │ ├── crc.ts │ │ ├── http.ts │ │ ├── intelhex.ts │ │ └── control.ts │ ├── types │ │ ├── web-serial.d.ts │ │ └── index.ts │ ├── transport │ │ ├── tcp.ts │ │ └── serial.ts │ ├── tools │ │ └── spinel.ts │ └── style.css ├── purgecss.config.js ├── bs-config.js ├── package.json ├── scripts │ └── inject-commit.js └── README.md ├── xzg-multi-tool-addon ├── icon.png ├── config.json ├── run.sh ├── README.md ├── DOCS.md └── CHANGELOG.md ├── repository.json ├── .dockerignore ├── .gitignore ├── LICENSE └── README.md /bridge/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | web/ 3 | xzg-mt-bridge -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | } -------------------------------------------------------------------------------- /docs/imgs/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/docs/imgs/dark.png -------------------------------------------------------------------------------- /docs/imgs/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/docs/imgs/light.png -------------------------------------------------------------------------------- /docs/how-to/imgs/zadig.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/docs/how-to/imgs/zadig.jpg -------------------------------------------------------------------------------- /web-page/favicon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/web-page/favicon/icon.png -------------------------------------------------------------------------------- /web-page/favicon/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/web-page/favicon/logo.png -------------------------------------------------------------------------------- /web-page/static/imgs/si.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/web-page/static/imgs/si.png -------------------------------------------------------------------------------- /web-page/static/imgs/ti.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/web-page/static/imgs/ti.png -------------------------------------------------------------------------------- /docs/imgs/telegram_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/docs/imgs/telegram_banner.png -------------------------------------------------------------------------------- /web-page/static/imgs/esp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/web-page/static/imgs/esp.png -------------------------------------------------------------------------------- /xzg-multi-tool-addon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/xzg-multi-tool-addon/icon.png -------------------------------------------------------------------------------- /.github/badges/ghcr-downloads.json: -------------------------------------------------------------------------------- 1 | {"schemaVersion": 1, "label": "ghcr pulls", "message": "3.9k", "color": "blue"} -------------------------------------------------------------------------------- /web-page/static/imgs/arduino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/web-page/static/imgs/arduino.png -------------------------------------------------------------------------------- /web-page/static/imgs/telink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/web-page/static/imgs/telink.png -------------------------------------------------------------------------------- /docs/how-to/imgs/TLSRPGM_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/docs/how-to/imgs/TLSRPGM_dark.png -------------------------------------------------------------------------------- /docs/how-to/imgs/TLSRPGM_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/docs/how-to/imgs/TLSRPGM_light.png -------------------------------------------------------------------------------- /docs/how-to/imgs/TlsrComProg_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/docs/how-to/imgs/TlsrComProg_dark.png -------------------------------------------------------------------------------- /docs/how-to/imgs/debugger-pinout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/docs/how-to/imgs/debugger-pinout.png -------------------------------------------------------------------------------- /docs/how-to/imgs/device_manager.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/docs/how-to/imgs/device_manager.jpg -------------------------------------------------------------------------------- /web-page/static/bins/floader_825x.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/web-page/static/bins/floader_825x.bin -------------------------------------------------------------------------------- /web-page/static/bins/floader_826x.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/web-page/static/bins/floader_826x.bin -------------------------------------------------------------------------------- /web-page/static/bins/uart2swire.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/web-page/static/bins/uart2swire.bin -------------------------------------------------------------------------------- /docs/how-to/imgs/TlsrComProg_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/docs/how-to/imgs/TlsrComProg_light.png -------------------------------------------------------------------------------- /web-page/static/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xyzroe/XZG-MT/HEAD/web-page/static/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /repository.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XZG Multi-tool", 3 | "url": "https://github.com/xyzroe/XZG-MT", 4 | "maintainer": "xyzroe " 5 | } 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | web-page/node_modules 4 | bridge/node_modules 5 | 6 | # Build output 7 | dist 8 | 9 | # Workspace and misc 10 | .DS_Store 11 | .env 12 | npm-debug.log 13 | yarn.lock 14 | pnpm-lock.yaml 15 | README.md 16 | legacy/serialprebuilds 17 | -------------------------------------------------------------------------------- /web-page/.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | node_modules/ 3 | dist/**/* 4 | 5 | # Logs 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | pnpm-debug.log* 10 | *.log 11 | 12 | # Env 13 | .env 14 | .env.* 15 | *.local 16 | 17 | # Tooling caches 18 | .eslintcache 19 | *.tsbuildinfo 20 | 21 | # IDE 22 | .vscode/ 23 | .idea/ 24 | -------------------------------------------------------------------------------- /web-page/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "lib": ["DOM", "ES2020"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "outDir": "./dist", 12 | "rootDir": "./src" 13 | }, 14 | "include": ["src/**/*", "src/types/**/*.d.ts"], 15 | "exclude": ["node_modules", "**/*.spec.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /bridge/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # Entrypoint wrapper: 5 | # - If running as HA addon (presence of /data/options.json) -> run wrapper script as root 6 | # - Otherwise run the compiled binary as the non-root user 'xzg' using su-exec when available 7 | 8 | if [ -f /data/options.json ]; then 9 | exec ./run.sh "$@" 10 | else 11 | if command -v su-exec >/dev/null 2>&1; then 12 | exec su-exec xzg /app/xzg-mt-bridge "$@" 13 | else 14 | exec /app/xzg-mt-bridge "$@" 15 | fi 16 | fi 17 | -------------------------------------------------------------------------------- /web-page/eslint.config.mts: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import tseslint from "typescript-eslint"; 4 | import { defineConfig } from "eslint/config"; 5 | 6 | export default defineConfig([ 7 | { 8 | ignores: ["dist/**", "build/**", "scripts/**", "coverage/**", "**/*.min.js"], 9 | }, 10 | { 11 | files: ["src/**/*.{js,mjs,cjs,ts,mts,cts}"], 12 | plugins: { js }, 13 | extends: ["js/recommended"], 14 | languageOptions: { globals: globals.browser }, 15 | }, 16 | tseslint.configs.recommended, 17 | ]); 18 | -------------------------------------------------------------------------------- /.github/workflows/draft-release-notes.yml: -------------------------------------------------------------------------------- 1 | name: Update Release Draft 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize, closed] 6 | workflow_dispatch: 7 | release: 8 | types: [published] 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | jobs: 15 | update-draft: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Update release draft 19 | uses: release-drafter/release-drafter@v6 20 | with: 21 | config-name: release-drafter.yml 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /web-page/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms)); 2 | 3 | export function concat(...arrs: Uint8Array[]): Uint8Array { 4 | const total = arrs.reduce((a, b) => a + b.length, 0); 5 | const out = new Uint8Array(total); 6 | let off = 0; 7 | for (const a of arrs) { 8 | out.set(a, off); 9 | off += a.length; 10 | } 11 | return out; 12 | } 13 | 14 | export function toHex(v: number, w = 2) { 15 | return "0x" + v.toString(16).toUpperCase().padStart(w, "0"); 16 | } 17 | 18 | export function bufToHex(buf: Uint8Array): string { 19 | return Array.from(buf) 20 | .map((b) => b.toString(16).padStart(2, "0")) 21 | .join(" "); 22 | } 23 | -------------------------------------------------------------------------------- /web-page/purgecss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["dist/index.html", "dist/flasher.js"], 3 | css: ["dist/css/bootstrap.min.css"], 4 | safelist: { 5 | standard: [ 6 | // Bootstrap tooltip classes 7 | "tooltip", 8 | "tooltip-inner", 9 | "tooltip-arrow", 10 | "bs-tooltip-top", 11 | "bs-tooltip-bottom", 12 | "bs-tooltip-start", 13 | "bs-tooltip-end", 14 | "bs-tooltip-auto", 15 | "fade", 16 | "show", 17 | // Modal classes (if needed) 18 | "modal-backdrop", 19 | "modal-open", 20 | ], 21 | // Preserve all classes that start with these patterns 22 | greedy: [/^tooltip/, /^bs-tooltip/], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: xyzroe 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: xyzroe 14 | 15 | 16 | -------------------------------------------------------------------------------- /web-page/bs-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | server: { 3 | baseDir: "dist", 4 | index: "index.html", 5 | serveStaticOptions: { 6 | extensions: ["html"], 7 | }, 8 | }, 9 | files: ["dist/**/*"], 10 | watchEvents: ["change", "add", "unlink", "addDir", "unlinkDir"], 11 | watch: true, 12 | port: 3000, 13 | open: true, 14 | cors: true, 15 | notify: false, 16 | ui: false, 17 | ghostMode: false, 18 | reloadOnRestart: true, 19 | logLevel: "info", 20 | middleware: [ 21 | function nocache(req, res, next) { 22 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); 23 | res.setHeader("Pragma", "no-cache"); 24 | res.setHeader("Expires", "0"); 25 | res.setHeader("Surrogate-Control", "no-store"); 26 | next(); 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /web-page/favicon/favicon-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "markups": [ 3 | "", 4 | "", 5 | "", 6 | "", 7 | "", 8 | "" 9 | ], 10 | "cssSelectors": [ 11 | "link[rel=\"icon\"][type=\"image/png\"]", 12 | "link[rel=\"icon\"][type=\"image/svg\\+xml\"]", 13 | "link[rel=\"shortcut icon\"]", 14 | "link[rel=\"apple-touch-icon\"]", 15 | "meta[name=\"apple-mobile-web-app-title\"]", 16 | "link[rel=\"manifest\"]" 17 | ] 18 | } -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | categories: 4 | - title: '🚀 Features' 5 | labels: 6 | - 'feature' 7 | - 'feat' 8 | - title: '🐛 Bug Fixes' 9 | labels: 10 | - 'bug' 11 | - 'fix' 12 | - title: '📘 Documentation' 13 | labels: 14 | - 'chore' 15 | - 'docs' 16 | - 'documentation' 17 | change-template: '- $TITLE (#$NUMBER) by @$AUTHOR' 18 | template: | 19 | $CHANGES 20 | 21 | autolabeler: 22 | - label: 'feature' 23 | title: 24 | - '/^feat:/i' 25 | - '/^feature:/i' 26 | - label: 'fix' 27 | title: 28 | - '/^fix:/i' 29 | - '/^bug:/i' 30 | - label: 'documentation' 31 | title: 32 | - '/^docs:/i' 33 | - '/^chore:/i' 34 | files: 35 | - '*.md' 36 | - 'docs/**/*' 37 | - 'README*' -------------------------------------------------------------------------------- /web-page/src/types/web-serial.d.ts: -------------------------------------------------------------------------------- 1 | // Minimal Web Serial typings for TS 2 | interface SerialOptions { 3 | baudRate: number; 4 | } 5 | 6 | interface SerialPort { 7 | open(options: SerialOptions): Promise; 8 | close(): Promise; 9 | setSignals?: (signals: SerialSignals) => Promise; 10 | readable?: ReadableStream; 11 | writable?: WritableStream; 12 | } 13 | 14 | interface Navigator { 15 | serial: { 16 | requestPort(options?: SerialPortRequestOptions): Promise; 17 | getPorts?(): Promise; 18 | }; 19 | } 20 | 21 | interface SerialPortRequestOptions { 22 | filters?: SerialPortFilter[]; 23 | } 24 | 25 | interface SerialPortFilter { 26 | usbVendorId?: number; 27 | usbProductId?: number; 28 | } 29 | 30 | interface SerialSignals { 31 | dataTerminalReady?: boolean; 32 | requestToSend?: boolean; 33 | break?: boolean; 34 | } 35 | -------------------------------------------------------------------------------- /bridge/embed.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "path" 6 | "strings" 7 | ) 8 | 9 | //go:embed web/* 10 | var webFiles embed.FS 11 | 12 | // getEmbeddedFile returns the content of an embedded file if it exists 13 | func getEmbeddedFile(relPath string) (string, bool) { 14 | // Remove leading slash 15 | relPath = strings.TrimPrefix(relPath, "/") 16 | 17 | // Default to index.html 18 | if relPath == "" { 19 | relPath = "index.html" 20 | } 21 | 22 | // Normalize path separators - embed.FS always uses forward slashes 23 | relPath = strings.ReplaceAll(relPath, "\\", "/") 24 | 25 | // Use path.Join instead of filepath.Join for embed.FS 26 | // embed.FS always uses forward slashes regardless of OS 27 | embeddedPath := path.Join("web", relPath) 28 | 29 | // Try to read the file 30 | content, err := webFiles.ReadFile(embeddedPath) 31 | if err != nil { 32 | return "", false 33 | } 34 | 35 | return string(content), true 36 | } 37 | -------------------------------------------------------------------------------- /xzg-multi-tool-addon/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XZG Multi-tool", 3 | "version": "0.3.4", 4 | "slug": "xzg-multi-tool-addon", 5 | "description": "Simple WebSocket <-> TCP bridge with optional mDNS and local serial exposure", 6 | "image": "ghcr.io/xyzroe/xzg-mt", 7 | "arch": [ 8 | "amd64", 9 | "armv7", 10 | "aarch64" 11 | ], 12 | "startup": "services", 13 | "webui": "http://[HOST]:[PORT:8765]", 14 | "ingress": true, 15 | "ingress_port": 8765, 16 | "panel_icon": "mdi:memory", 17 | "panel_title": "XZG MT", 18 | "boot": "auto", 19 | "init": false, 20 | "options": { 21 | "port": 8765, 22 | "advertise_host": "", 23 | "debug_mode": false 24 | }, 25 | "schema": { 26 | "port": "int", 27 | "advertise_host": "str?", 28 | "debug_mode": "bool?" 29 | }, 30 | "url": "https://github.com/xyzroe/XZG-MT", 31 | "map": [ 32 | "config:rw" 33 | ], 34 | "host_network": true, 35 | "uart": true 36 | } 37 | -------------------------------------------------------------------------------- /bridge/go.mod: -------------------------------------------------------------------------------- 1 | module xzg-mt-bridge 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.5.1 7 | github.com/grandcat/zeroconf v1.0.0 8 | github.com/labstack/echo/v4 v4.11.4 9 | go.bug.st/serial v1.6.2 10 | ) 11 | 12 | require ( 13 | github.com/cenkalti/backoff v2.2.1+incompatible // indirect 14 | github.com/creack/goselect v0.1.2 // indirect 15 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 16 | github.com/labstack/gommon v0.4.2 // indirect 17 | github.com/mattn/go-colorable v0.1.13 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/miekg/dns v1.1.41 // indirect 20 | github.com/valyala/bytebufferpool v1.0.0 // indirect 21 | github.com/valyala/fasttemplate v1.2.2 // indirect 22 | golang.org/x/crypto v0.17.0 // indirect 23 | golang.org/x/net v0.19.0 // indirect 24 | golang.org/x/sys v0.15.0 // indirect 25 | golang.org/x/text v0.14.0 // indirect 26 | golang.org/x/time v0.5.0 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /web-page/favicon/favicon-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": { 3 | "desktop": { 4 | "regularIconTransformation": { 5 | "type": "background", 6 | "backgroundColor": "#ffffff", 7 | "backgroundRadius": 0.7, 8 | "imageScale": 0.9 9 | }, 10 | "darkIconType": "none" 11 | }, 12 | "touch": { 13 | "transformation": { 14 | "type": "background", 15 | "backgroundColor": "#ffffff", 16 | "backgroundRadius": 0, 17 | "imageScale": 0.8 18 | }, 19 | "appTitle": "XZG MT" 20 | }, 21 | "webAppManifest": { 22 | "transformation": { 23 | "type": "background", 24 | "backgroundColor": "#ffffff", 25 | "backgroundRadius": 0, 26 | "imageScale": 0.8 27 | }, 28 | "backgroundColor": "#ffffff", 29 | "name": "XZG Multi-tool", 30 | "shortName": "XZG MT", 31 | "themeColor": "#ffffff" 32 | } 33 | }, 34 | "path": "fav/" 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Node dependencies 4 | node_modules/ 5 | 6 | 7 | # Logs 8 | logs/ 9 | *.log 10 | *.log.* 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Runtime data / pids 17 | pids/ 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Diagnostic reports (Node.js) 23 | report.*.json 24 | 25 | # Coverage 26 | coverage/ 27 | .nyc_output/ 28 | 29 | # Build outputs 30 | dist/ 31 | /build/ 32 | /tmp/ 33 | /temp/ 34 | .cache/ 35 | web/ 36 | serialprebuilds/ 37 | 38 | # Environment files 39 | .env 40 | .env.* 41 | !.env.example 42 | 43 | # OS / Editor cruft 44 | .DS_Store 45 | Thumbs.db 46 | .idea/ 47 | .vscode/* 48 | !.vscode/settings.json 49 | !.vscode/tasks.json 50 | !.vscode/launch.json 51 | !.vscode/extensions.json 52 | *.psd 53 | 54 | # Swap files 55 | *.swp 56 | *.swo 57 | 58 | # Binary artifacts (usually generated) 59 | *.dll 60 | *.so 61 | *.dylib 62 | *.exe 63 | 64 | # Scripts 65 | clean.sh 66 | 67 | # Local markdown files 68 | commit.md 69 | to-do.md -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # 📚 Documentation 2 | 3 | This directory contains all the documentation for the XZG Multi-tool project, including user guides, device specifications, and reference materials. 4 | 5 | ## 📁 Contents 6 | 7 | ### Files 8 | 9 | - **`devices.md`**: Comprehensive list of supported devices, features, and device-specific notes. This is the go-to resource for understanding what chips and tools XZG-MT can work with. 10 | 11 | ### Directories 12 | 13 | - **`how-to/`**: Step-by-step guides for using XZG-MT with various devices and tools. Includes tutorials for flashing firmware, debugging, and troubleshooting. 14 | - **`imgs/`**: Shared images and assets used across the documentation. 15 | 16 | ## 🤝 Contributing 17 | 18 | If you have documentation to add or improve: 19 | 20 | 1. Follow the existing structure and style. 21 | 2. Use Markdown formatting. 22 | 3. Add relevant images to the `imgs/` directory. 23 | 4. Submit a pull request with your changes. 24 | 25 | For more information about the project, see the [main README](../README.md). 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 xyzroe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ghcr-downloads-badge.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/ghcr-downloads-badge.yml 2 | name: Update GHCR downloads badge 3 | 4 | permissions: 5 | contents: write 6 | 7 | on: 8 | schedule: 9 | - cron: '0 3 * * *' # once a day 10 | workflow_dispatch: 11 | 12 | jobs: 13 | update-badge: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-python@v5 21 | with: 22 | python-version: '3.x' 23 | 24 | - run: pip install requests beautifulsoup4 25 | 26 | - name: Generate badge JSON 27 | env: 28 | GH_OWNER: xyzroe 29 | GH_REPO: XZG-MT 30 | GH_IMAGE: xzg-mt 31 | run: | 32 | mkdir -p .github/badges 33 | python .github/scripts/ghcr_downloads.py > .github/badges/ghcr-downloads.json 34 | 35 | - name: Commit badge 36 | uses: stefanzweifel/git-auto-commit-action@v5 37 | with: 38 | commit_message: "chore: update GHCR downloads badge" 39 | file_pattern: ".github/badges/ghcr-downloads.json" 40 | -------------------------------------------------------------------------------- /web-page/src/utils/xmodem.ts: -------------------------------------------------------------------------------- 1 | import { crc16 } from "./crc"; 2 | 3 | export const XMODEM_BLOCK_SIZE = 128; 4 | 5 | export enum XModemPacketType { 6 | SOH = 0x01, // Start of Header 7 | EOT = 0x04, // End of Transmission 8 | ACK = 0x06, // Acknowledge 9 | NAK = 0x15, // Not Acknowledge 10 | CAN = 0x18, // Cancel 11 | } 12 | 13 | export class XmodemCRCPacket { 14 | constructor( 15 | public number: number, // Packet number (1-255, wraps around) 16 | public payload: Uint8Array // Must be exactly XMODEM_BLOCK_SIZE bytes 17 | ) { 18 | if (payload.length !== XMODEM_BLOCK_SIZE) { 19 | throw new Error(`Payload must be ${XMODEM_BLOCK_SIZE} bytes`); 20 | } 21 | } 22 | 23 | serialize(): Uint8Array { 24 | const crc = crc16(this.payload); 25 | const packet = new Uint8Array(3 + XMODEM_BLOCK_SIZE + 2); 26 | 27 | packet[0] = XModemPacketType.SOH; 28 | packet[1] = this.number & 0xff; 29 | packet[2] = (0xff - this.number) & 0xff; 30 | packet.set(this.payload, 3); 31 | packet[3 + XMODEM_BLOCK_SIZE] = (crc >> 8) & 0xff; // CRC high byte 32 | packet[3 + XMODEM_BLOCK_SIZE + 1] = crc & 0xff; // CRC low byte 33 | 34 | return packet; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web-page/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Transport = "serial" | "tcp"; 2 | 3 | export type Link = { 4 | write: (d: Uint8Array) => Promise; 5 | onData: (cb: (d: Uint8Array) => void) => void; 6 | offData?: (cb: (d: Uint8Array) => void) => void; 7 | }; 8 | 9 | export interface TcpConnectParams { 10 | host: string; 11 | port: number; 12 | } 13 | export interface SerialOpenParams { 14 | path?: string; 15 | bitrate: number; 16 | } 17 | 18 | export interface FlashOptions { 19 | erase: boolean; 20 | verify: boolean; 21 | address?: number; // start address override; default comes from HEX 22 | } 23 | 24 | export interface HexImage { 25 | startAddress: number; 26 | data: Uint8Array; // linear, dense image with gaps padded as 0xFF 27 | } 28 | 29 | export enum VerifyMethod { 30 | BY_READ = "read", 31 | BY_CRC = "crc", 32 | } 33 | 34 | export enum WriteMethod { 35 | FAST = "fast", 36 | SLOW = "slow", 37 | } 38 | 39 | export enum EraseMethod { 40 | FULL = "full", 41 | SECTOR = "sector", 42 | } 43 | 44 | export enum TelinkFamily { 45 | TLSR825X = 8250, 46 | TLSR826X = 8260, 47 | } 48 | 49 | export enum TelinkMethod { 50 | UART = "uart", 51 | SWIRE = "swire", 52 | } 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03-feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature Request / Improvement 2 | description: Suggest an idea or improvement for the Multi-Tool 3 | title: "[Idea] " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for suggesting an improvement! 10 | 11 | - type: dropdown 12 | id: category 13 | attributes: 14 | label: Feature Category 15 | description: What area does this relate to? 16 | options: 17 | - User Interface 18 | - Flashing/Programming 19 | - Bridge 20 | - Device Support 21 | - Firmware Management 22 | - Documentation 23 | - Other 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | id: problem 29 | attributes: 30 | label: Problem or Need 31 | description: Is your feature request related to a problem? Describe it 32 | validations: 33 | required: true 34 | 35 | - type: textarea 36 | id: solution 37 | attributes: 38 | label: Proposed Solution 39 | description: Describe your idea or suggestion 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | id: alternatives 45 | attributes: 46 | label: Alternative Solutions 47 | description: Have you considered any alternative approaches? 48 | 49 | - type: textarea 50 | id: usecase 51 | attributes: 52 | label: Use Case 53 | description: How would this feature be used? Who would benefit? 54 | 55 | - type: textarea 56 | id: additional 57 | attributes: 58 | label: Additional Context 59 | description: Add mockups, screenshots, links, or other relevant information 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/04-firmware-request.yml: -------------------------------------------------------------------------------- 1 | name: 📦 Firmware Request 2 | description: Request to add a new firmware to the cloud repository 3 | title: "[Firmware] " 4 | labels: ["firmware"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for suggesting a firmware to add! 10 | 11 | - type: input 12 | id: firmware-name 13 | attributes: 14 | label: Firmware Name 15 | placeholder: ex. Z2M Coordinator, Thread Border Router 16 | validations: 17 | required: true 18 | 19 | - type: input 20 | id: target-device 21 | attributes: 22 | label: Target Device/Chip 23 | placeholder: ex. CC2652P, EFR32MG21, ESP32 24 | validations: 25 | required: true 26 | 27 | - type: input 28 | id: project-source 29 | attributes: 30 | label: Project/Source 31 | description: Link to original project or repository 32 | placeholder: https://github.com/... 33 | validations: 34 | required: true 35 | 36 | - type: input 37 | id: version 38 | attributes: 39 | label: Firmware Version 40 | placeholder: ex. v1.2.3 41 | 42 | - type: textarea 43 | id: why 44 | attributes: 45 | label: Why Add This Firmware? 46 | description: Explain why this firmware should be included 47 | validations: 48 | required: true 49 | 50 | - type: textarea 51 | id: special-requirements 52 | attributes: 53 | label: Special Requirements 54 | description: Any special flashing requirements or notes 55 | 56 | - type: textarea 57 | id: additional 58 | attributes: 59 | label: Related Links 60 | description: Add any relevant links (project homepage, documentation, GitHub repo, etc.) 61 | -------------------------------------------------------------------------------- /web-page/src/utils/crc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * CRC16-CCITT implementation 3 | * Polynomial: 0x1021 4 | * Final XOR: 0x0000 5 | * No input/output reversal 6 | * 7 | * @param data - Input data 8 | * @param initialValue - Initial CRC value (0x0000 for XMODEM, 0xFFFF for EZSP) 9 | */ 10 | export function crc16(data: Uint8Array, initialValue: number = 0x0000): number { 11 | let crc = initialValue; 12 | 13 | for (let i = 0; i < data.length; i++) { 14 | crc ^= data[i] << 8; 15 | 16 | for (let j = 0; j < 8; j++) { 17 | if (crc & 0x8000) { 18 | crc = (crc << 1) ^ 0x1021; 19 | } else { 20 | crc = crc << 1; 21 | } 22 | } 23 | } 24 | 25 | return crc & 0xffff; 26 | } 27 | 28 | /** 29 | * CRC32 implementation (standard IEEE 802.3 polynomial) 30 | * Polynomial: 0xEDB88320 (reversed 0x04C11DB7) 31 | * Initial value: 0xFFFFFFFF 32 | * Final XOR: 0xFFFFFFFF 33 | * Input/output bit reversal 34 | * 35 | * Compatible with Python's binascii.crc32() and standard CRC32 36 | */ 37 | export function crc32(data: Uint8Array): number { 38 | let crc = 0xffffffff; 39 | 40 | for (let i = 0; i < data.length; i++) { 41 | crc ^= data[i]; 42 | for (let j = 0; j < 8; j++) { 43 | if (crc & 1) { 44 | crc = (crc >>> 1) ^ 0xedb88320; 45 | } else { 46 | crc = crc >>> 1; 47 | } 48 | } 49 | } 50 | 51 | return (crc ^ 0xffffffff) >>> 0; 52 | } 53 | 54 | /** 55 | * Pad data to a multiple of blockSize with padding byte 56 | */ 57 | export function padToMultiple(data: Uint8Array, blockSize: number, padding: number): Uint8Array { 58 | if (data.length % blockSize === 0) { 59 | return data; 60 | } 61 | 62 | const numCompleteBlocks = Math.floor(data.length / blockSize); 63 | const paddedSize = blockSize * (numCompleteBlocks + 1); 64 | const result = new Uint8Array(paddedSize); 65 | 66 | result.set(data); 67 | result.fill(padding, data.length); 68 | 69 | return result; 70 | } 71 | -------------------------------------------------------------------------------- /docs/devices.md: -------------------------------------------------------------------------------- 1 | ## 💻 Supported Chips 2 | 3 | | Manufacturer | Model | Notes | Interface | Detect | Erase | Write | Verify | Read | Copy IEEE | NVRAM | Local files | Cloud FWs | 4 | | :---------------- | :--------------------- | :----------------------------- | :-------: | :----: | :---: | :---: | :----: | :--: | :-------: | :---: | :------------: | :-------: | 5 | | Texas Instruments | CC2538, CC1352, CC2652 | with BSL loader | 🔌 / 🌐 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | `.hex`, `.bin` | ✅ | 6 | | Silicon Labs | EFR32MG21 series | with Gecko Bootloader | 🔌 / 🌐 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | `.ota`, `.gbl` | ✅ | 7 | | Espressif | ESP8266, ESP32 series | almost any chip | 🔌 | ✅ | ✅ | ✅ | ❌ | ❌ | ◻️ | ◻️ | `.bin` | ⚠️ | 8 | | Texas Instruments | CC253X, CC254X\* | using TI CC Debugger | 🧰 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | `.hex`, `.bin` | ❌ | 9 | | Texas Instruments | CC253X, CC254X\* | using CC Loader FW | 🔌 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | `.hex`, `.bin` | ❌ | 10 | | Arduino | Nano, Uno, Pro Mini | any ATmega328P | 🔌 | ✅ | ◻️ | ✅ | ✅ | ✅ | ◻️ | ◻️ | `.hex` | ⚠️ | 11 | | Telink | TLSR825X, TLSR826X\*\* | swire emulation and uart2swire | 🔌 | ✅ | ✅ | ✅ | ✅ | ✅ | ◻️ | ◻️ | `.bin` | ❌ | 12 | 13 | \* CC2530, CC2531, CC2533, CC2540, CC2541, CC2543, CC2544, CC2545 14 | \*\* TLSR8250, TLSR8251, TLSR8253, TLSR8258, TLSR8266, TLSR8269 15 | 16 | Legend: 🔌 Web Serial, 🧰 Web USB, 🌐 WS-TCP bridge, ✅ full support, ⚠️ partial support, ❌ not implemented, ◻️ not applicable 17 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build app", 6 | "type": "shell", 7 | "command": "npm", 8 | "args": [ 9 | "run", 10 | "build" 11 | ] 12 | }, 13 | { 14 | "label": "Build app", 15 | "type": "shell", 16 | "command": "npm", 17 | "args": [ 18 | "run", 19 | "build" 20 | ], 21 | "isBackground": false, 22 | "problemMatcher": [ 23 | "$eslint - stylish" 24 | ], 25 | "group": "build" 26 | }, 27 | { 28 | "label": "Build app", 29 | "type": "shell", 30 | "command": "npm", 31 | "args": [ 32 | "run", 33 | "build" 34 | ] 35 | }, 36 | { 37 | "label": "Build app", 38 | "type": "shell", 39 | "command": "npm", 40 | "args": [ 41 | "run", 42 | "build:lite" 43 | ] 44 | }, 45 | { 46 | "label": "Build app", 47 | "type": "shell", 48 | "command": "npm", 49 | "args": [ 50 | "run", 51 | "build" 52 | ], 53 | "problemMatcher": [ 54 | "$eslint-stylish" 55 | ] 56 | }, 57 | { 58 | "label": "Build app (lite)", 59 | "type": "shell", 60 | "command": "npm", 61 | "args": [ 62 | "run", 63 | "build:lite" 64 | ], 65 | "isBackground": false, 66 | "problemMatcher": [ 67 | "$tsc" 68 | ], 69 | "group": "build" 70 | }, 71 | { 72 | "label": "Build app (lite)", 73 | "type": "shell", 74 | "command": "npm", 75 | "args": [ 76 | "run", 77 | "build:lite" 78 | ], 79 | "isBackground": false, 80 | "problemMatcher": [ 81 | "$tsc" 82 | ], 83 | "group": "build" 84 | }, 85 | { 86 | "label": "Build app (lite)", 87 | "type": "shell", 88 | "command": "npm", 89 | "args": [ 90 | "run", 91 | "build:lite" 92 | ], 93 | "isBackground": false, 94 | "problemMatcher": [ 95 | "$tsc" 96 | ], 97 | "group": "build" 98 | } 99 | ] 100 | } -------------------------------------------------------------------------------- /docs/how-to/readme.md: -------------------------------------------------------------------------------- 1 | # How-To Guides 2 | 3 | ## 📖 Introduction 4 | 5 | This section provides detailed, step-by-step guides for using XZG-MT to accomplish various tasks. These guides will help you get started and troubleshoot common issues. XZG-MT supports a wide range of chips and tools, and these instructions are designed to be user-friendly for beginners and experts alike. 6 | 7 | ## 📋 Existing Guides 8 | 9 | Here are the guides currently available: 10 | 11 | - [Using CC Debugger with CC2530 chips](cc_debuger.md) - Learn how to use TI CC Debugger or SmartRF04EB to program CC2530 family chips. 12 | - [Using CC Loader with CC2530 chips](cc_loader.md) - Instruction for using Arduino/ESP-based CC Loader as an alternative to dedicated debuggers. 13 | - [Working with Telink Devices](telink.md) - Flash Telink chips using UART-based SWire emulation or the UART2SWire programmer. 14 | 15 | ## 🚀 Planned Guides 16 | 17 | I plan to add more guides in the future. 18 | 19 | If you have a specific guide in mind, check the [GitHub issues](https://github.com/xyzroe/XZG-MT/issues) and suggest it there! 20 | 21 | ## 🤝 Contribute 22 | 23 | The XZG-MT community thrives on contributions! If you've successfully used XZG-MT for a task not covered here, or if you have expertise in a particular area, please share your knowledge by creating a new guide. 24 | 25 | ### How to Contribute: 26 | 27 | 1. Fork the [XZG-MT repository](https://github.com/xyzroe/XZG-MT). 28 | 2. Create a new Markdown file in the `docs/how-to/` directory (e.g., `your_guide.md`). 29 | 3. Follow the structure of existing guides: include an introduction, required hardware, step-by-step instructions, troubleshooting, and a "If the Problem Persists" section. 30 | 4. Add emojis to H2 headings for visual appeal. 31 | 5. Update this index.md file to include your new guide in the "Existing Guides" list. 32 | 6. Submit a pull request with a clear description of your changes. 33 | 34 | We appreciate your help in making XZG-MT more accessible to everyone! 35 | -------------------------------------------------------------------------------- /xzg-multi-tool-addon/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail 3 | 4 | OPTIONS_FILE="/data/options.json" 5 | 6 | PORT=8765 7 | ADVERTISE_HOST="" 8 | DEBUG_MODE="false" 9 | # SERIAL_SCAN_INTERVAL=5000 10 | 11 | if [ -f "$OPTIONS_FILE" ]; then 12 | if command -v jq >/dev/null 2>&1; then 13 | PORT=$(jq -r '.port // 8765' "$OPTIONS_FILE") 14 | ADVERTISE_HOST=$(jq -r '.advertise_host // ""' "$OPTIONS_FILE") 15 | DEBUG_MODE=$(jq -r '.debug_mode // false' "$OPTIONS_FILE") 16 | # SERIAL_SCAN_INTERVAL=$(jq -r '.serial_scan_interval // 5000' "$OPTIONS_FILE") 17 | else 18 | PORT=$(grep -oP '"port"\s*:\s*\K[0-9]+' "$OPTIONS_FILE" || echo 8765) 19 | ADVERTISE_HOST=$(grep -oP '"advertise_host"\s*:\s*"\K[^"]+' "$OPTIONS_FILE" || true) 20 | if grep -q '"debug_mode"\s*:\s*true' "$OPTIONS_FILE"; then DEBUG_MODE=true; fi 21 | # SERIAL_SCAN_INTERVAL=$(grep -oP '"serial_scan_interval"\s*:\s*\K[0-9]+' "$OPTIONS_FILE" || echo 5000) 22 | fi 23 | fi 24 | 25 | export PORT 26 | # export SERIAL_SCAN_INTERVAL 27 | if [ -n "$ADVERTISE_HOST" ] && [ "$ADVERTISE_HOST" != "null" ]; then 28 | export ADVERTISE_HOST 29 | fi 30 | if [ "$DEBUG_MODE" = "true" ]; then 31 | export DEBUG_MODE=1 32 | fi 33 | 34 | echo "Starting bridge on port ${PORT} (ADVERTISE_HOST=${ADVERTISE_HOST:-})" # , SERIAL_SCAN_INTERVAL=${SERIAL_SCAN_INTERVAL})" 35 | # Decide what to execute: prefer compiled Go binary if present, otherwise fall back to Node + bridge.js 36 | if [ -x "/app/xzg-mt-bridge" ]; then 37 | echo "Found Go binary /app/xzg-mt-bridge, launching it" 38 | exec /app/xzg-mt-bridge "$PORT" # "$SERIAL_SCAN_INTERVAL" 39 | elif command -v node >/dev/null 2>&1 && [ -f "/app/bridge.js" ]; then 40 | echo "Found Node and /app/bridge.js, launching Node" 41 | exec node /app/bridge.js "$PORT" # "$SERIAL_SCAN_INTERVAL" 42 | else 43 | echo "ERROR: No runtime available. Expected /app/xzg-mt-bridge (executable) or node + /app/bridge.js" >&2 44 | exit 1 45 | fi 46 | -------------------------------------------------------------------------------- /bridge/Dockerfile: -------------------------------------------------------------------------------- 1 | # Multi-stage Dockerfile for the Go bridge 2 | # - builder: compile a static linux binary 3 | # - runtime: small alpine image with HA-friendly utilities 4 | 5 | 6 | FROM --platform=$BUILDPLATFORM golang:1.20-alpine AS builder 7 | ARG VERSION=0.0.0 8 | ARG TARGETOS 9 | ARG TARGETARCH 10 | 11 | WORKDIR /src 12 | 13 | # git may be needed for some modules 14 | RUN apk add --no-cache git 15 | 16 | # Download modules first for better caching 17 | COPY bridge/go.mod bridge/go.sum ./ 18 | RUN go mod download 19 | 20 | # Copy full source and build 21 | COPY bridge . 22 | 23 | # Build a static binary for the target platform supplied by buildx. 24 | # When using docker buildx, TARGETOS and TARGETARCH are passed automatically. 25 | RUN CGO_ENABLED=0 \ 26 | GOOS=${TARGETOS:-linux} \ 27 | GOARCH=${TARGETARCH:-amd64} \ 28 | go build -v -ldflags "-s -w -X main.VERSION=${VERSION}" -o /src/xzg-mt-bridge . 29 | 30 | FROM alpine:3.18 31 | 32 | # Install runtime tools useful for HA addon wrapper and healthchecks 33 | RUN apk add --no-cache jq su-exec netcat-openbsd eudev ca-certificates 34 | 35 | # Create non-root user (for standalone mode) and add to dialout for serial access 36 | RUN addgroup -g 1001 -S xzg && \ 37 | adduser -S xzg -u 1001 && \ 38 | addgroup xzg dialout 39 | 40 | WORKDIR /app 41 | 42 | # Copy the compiled binary from the builder stage 43 | COPY --from=builder /src/xzg-mt-bridge ./xzg-mt-bridge 44 | 45 | # Copy HA addon wrapper script (optional; exists only when used as addon) 46 | COPY xzg-multi-tool-addon/run.sh ./run.sh 47 | RUN chmod +x ./run.sh ./xzg-mt-bridge || true 48 | 49 | # Add a small entrypoint wrapper to choose HA wrapper vs standalone binary 50 | COPY bridge/entrypoint.sh ./entrypoint.sh 51 | RUN chmod +x ./entrypoint.sh || true 52 | 53 | # Default environment 54 | ENV PORT=8765 55 | 56 | # Expose default WS port 57 | EXPOSE 8765 58 | 59 | # Health check - fast port probe 60 | HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \ 61 | CMD nc -z localhost ${PORT} || exit 1 62 | 63 | # Smart entrypoint: use `entrypoint.sh` to choose HA wrapper vs standalone runtime 64 | ENTRYPOINT ["/bin/sh", "-c", "./entrypoint.sh \"$@\"", "--"] 65 | -------------------------------------------------------------------------------- /xzg-multi-tool-addon/README.md: -------------------------------------------------------------------------------- 1 | # XZG Multi-tool Home Assistant Add-on 2 | 3 |
4 | GitHub version 5 | CI status 6 | GitHub downloads 7 | GHCR pulls 8 | GitHub Issues or Pull Requests 9 | License 10 |
11 | 12 | ### Docker 13 | 14 |
15 | Supports amd64 Architecture 16 | Supports arm64 Architecture 17 | Supports armv7 Architecture 18 | Supports armv6 Architecture 19 | Supports 386 Architecture 20 |

21 |
22 | 23 | 24 | 25 | Tiny Home Assistant add-on wrapper for the `bridge` project — a WebSocket ↔ TCP bridge with mDNS discovery and optional local serial port exposure. 26 | 27 | See Documentation tab or [`DOCS.md`](https://github.com/xyzroe/XZG-MT/blob/main/xzg-multi-tool-addon/DOCS.md) for installation, configuration and usage details, and [`CHANGELOG.md`](https://github.com/xyzroe/XZG-MT/blob/main/xzg-multi-tool-addon/CHANGELOG.md) for release notes. 28 | 29 | Main repo [XZG-MT](https://github.com/xyzroe/XZG-MT) 🚀 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02-bridge-issue.yml: -------------------------------------------------------------------------------- 1 | name: 🌉 Bridge Issue 2 | description: Report a problem with the bridge application 3 | title: "[Bridge] " 4 | labels: ["bug", "bridge"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for reporting a bridge issue! 10 | 11 | - type: input 12 | id: version 13 | attributes: 14 | label: Bridge Version 15 | placeholder: ex. v0.3.2 16 | validations: 17 | required: true 18 | 19 | - type: dropdown 20 | id: installation 21 | attributes: 22 | label: Installation Method 23 | options: 24 | - Standalone binary 25 | - Docker 26 | - Home Assistant Add-on 27 | validations: 28 | required: true 29 | 30 | - type: input 31 | id: platform 32 | attributes: 33 | label: OS/Platform 34 | placeholder: ex. Windows, macOS, Linux, Raspberry Pi, HA OS 35 | validations: 36 | required: true 37 | 38 | - type: dropdown 39 | id: architecture 40 | attributes: 41 | label: Architecture 42 | options: 43 | - amd64 44 | - arm64 45 | - armv7 46 | - Other 47 | 48 | - type: dropdown 49 | id: access-type 50 | attributes: 51 | label: Access Type 52 | options: 53 | - Local network 54 | - Same machine 55 | - Other 56 | 57 | - type: textarea 58 | id: description 59 | attributes: 60 | label: Problem Description 61 | description: Describe what's wrong with the bridge 62 | validations: 63 | required: true 64 | 65 | - type: textarea 66 | id: steps 67 | attributes: 68 | label: Steps to Reproduce 69 | placeholder: | 70 | 1. 71 | 2. 72 | 3. 73 | validations: 74 | required: true 75 | 76 | - type: textarea 77 | id: logs 78 | attributes: 79 | label: Bridge Logs 80 | description: Please provide bridge console output or logs 81 | render: shell 82 | validations: 83 | required: true 84 | 85 | - type: textarea 86 | id: additional 87 | attributes: 88 | label: Additional Context 89 | description: Add configuration files (remove sensitive data), screenshots, or other info 90 | -------------------------------------------------------------------------------- /.github/scripts/ghcr_downloads.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os, re, requests 3 | from bs4 import BeautifulSoup 4 | 5 | OWNER = os.environ["GH_OWNER"] 6 | REPO = os.environ["GH_REPO"] 7 | IMAGE = os.environ["GH_IMAGE"] 8 | 9 | url = f"https://github.com/{OWNER}/{REPO}/pkgs/container/{IMAGE}" 10 | r = requests.get(url) 11 | r.raise_for_status() 12 | 13 | soup = BeautifulSoup(r.text, "html.parser") 14 | total_downloads = 0 15 | 16 | # Iterate all

elements and look for a nearby with label "Total downloads". 17 | # Stop at the first matching card; do NOT fallback to the old span-based search. 18 | total_downloads = 0 19 | found = False 20 | for h3 in soup.find_all("h3"): 21 | parent = h3.parent 22 | if not parent: 23 | continue 24 | # Look for a sibling/child span in the same parent containing the label 25 | for sp in parent.find_all("span"): 26 | if "total downloads" in sp.get_text(strip=True).lower(): 27 | # Found the correct card — extract the number 28 | title_val = h3.get("title") 29 | if title_val and re.match(r"^\d+$", title_val): 30 | total_downloads = int(title_val) 31 | found = True 32 | break 33 | # Parse human-readable like "2.88K" 34 | txt = h3.get_text(strip=True) 35 | m = re.match(r"([0-9]*\.?[0-9]+)\s*([KMkm])?", txt) 36 | if m: 37 | num = float(m.group(1)) 38 | suf = m.group(2) 39 | if suf: 40 | if suf.upper() == "K": 41 | num *= 1000 42 | elif suf.upper() == "M": 43 | num *= 1000000 44 | total_downloads = int(num) 45 | found = True 46 | break 47 | if found: 48 | break 49 | 50 | def _fmt_compact(n: int) -> str: 51 | if n >= 1_000_000: 52 | val = n / 1_000_000.0 53 | s = f"{val:.1f}" + 'm' 54 | return s 55 | if n >= 1_000: 56 | val = n / 1_000.0 57 | s = f"{val:.1f}" + 'k' 58 | return s 59 | return str(n) 60 | 61 | message = _fmt_compact(total_downloads) 62 | 63 | badge = { 64 | "schemaVersion": 1, 65 | "label": "ghcr pulls", 66 | "message": message, 67 | "color": "blue", 68 | } 69 | 70 | import json, sys 71 | json.dump(badge, sys.stdout) 72 | -------------------------------------------------------------------------------- /bridge/Makefile: -------------------------------------------------------------------------------- 1 | # XZG-MT Go Bridge Makefile 2 | 3 | #read version from # read from ../web-page/package.json or set 0.0.0 if error 4 | VERSION := $(shell cat ../web-page/package.json | jq -r .version || echo "0.0.0") 5 | BUILD_DIR := dist 6 | MAIN_FILE := main.go 7 | 8 | # Default target 9 | .PHONY: all 10 | all: build 11 | 12 | # Build all targets 13 | .PHONY: build 14 | build: 15 | @echo "Building XZG-MT Go Bridge v$(VERSION)" 16 | @echo "==================================" 17 | @mkdir -p $(BUILD_DIR) 18 | @USE_UPX=1 ./build.sh 19 | 20 | # Build for current platform only 21 | .PHONY: build-local 22 | build-local: 23 | @echo "Building for current platform..." 24 | @mkdir -p $(BUILD_DIR) 25 | @go build -ldflags "-s -w -X main.VERSION=$(VERSION)" -o $(BUILD_DIR)/XZG-MT-local . 26 | @echo "✓ Built $(BUILD_DIR)/XZG-MT-local" 27 | 28 | # Run locally 29 | .PHONY: run 30 | run: 31 | @echo "Running XZG-MT Go Bridge locally..." 32 | @go run -ldflags "-X main.VERSION=$(VERSION)" . 33 | 34 | # Clean build artifacts 35 | .PHONY: clean 36 | clean: 37 | @echo "Cleaning build artifacts..." 38 | @rm -rf $(BUILD_DIR) 39 | @echo "✓ Cleaned" 40 | 41 | # Install dependencies 42 | .PHONY: deps 43 | deps: 44 | @echo "Installing dependencies..." 45 | @go mod tidy 46 | @go mod download 47 | @echo "✓ Dependencies installed" 48 | 49 | # Run tests 50 | .PHONY: test 51 | test: 52 | @echo "Running tests..." 53 | @go test ./... 54 | @echo "✓ Tests completed" 55 | 56 | # Format code 57 | .PHONY: fmt 58 | fmt: 59 | @echo "Formatting code..." 60 | @go fmt ./... 61 | @echo "✓ Code formatted" 62 | 63 | # Lint code 64 | .PHONY: lint 65 | lint: 66 | @echo "Linting code..." 67 | @go vet ./... 68 | @echo "✓ Code linted" 69 | 70 | # Show help 71 | .PHONY: help 72 | help: 73 | @echo "XZG-MT Go Bridge - Available targets:" 74 | @echo "" 75 | @echo " build - Build binaries for all platforms" 76 | @echo " build-local - Build binary for current platform only" 77 | @echo " run - Run the application locally" 78 | @echo " clean - Clean build artifacts" 79 | @echo " deps - Install dependencies" 80 | @echo " test - Run tests" 81 | @echo " fmt - Format code" 82 | @echo " lint - Lint code" 83 | @echo " help - Show this help" 84 | @echo "" 85 | @echo "Examples:" 86 | @echo " make run # Run locally" 87 | @echo " make build-local # Build for current platform" 88 | @echo " make build # Build for all platforms" 89 | @echo " make clean && make build # Clean and rebuild" 90 | -------------------------------------------------------------------------------- /web-page/favicon/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /web-page/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xzg-multi-tool", 3 | "version": "0.3.4", 4 | "description": "Web-based multi-tool for flashing and managing various chips via local or remote connection", 5 | "main": "dist/flasher.js", 6 | "engines": { 7 | "node": ">=20.18.0" 8 | }, 9 | "scripts": { 10 | "clean": "rm -rf dist", 11 | "esbuild:base": "esbuild src/flasher.ts --bundle --outfile=dist/flasher.js --platform=browser --target=es2020 --minify --tree-shaking=true --legal-comments=none", 12 | "esbuild:watch": "npm run esbuild:base -- --watch", 13 | "copy:static": "copyfiles -u 1 src/index.html src/style.css src/index.js dist && copyfiles -u 1 static/**/* dist", 14 | "optimize:css": "purgecss --config purgecss.config.js && purgecss --css dist/css/bootstrap-icons.min.css --content dist/index.html dist/flasher.js --output dist/css/", 15 | "copy:ready": "copyfiles -u 1 dist/* ../bridge/web/", 16 | "fav:gen": "realfavicon generate favicon/logo.svg favicon/favicon-settings.json favicon/favicon-data.json dist/fav", 17 | "fav:inject": "realfavicon inject favicon/favicon-data.json dist dist/index.html", 18 | "inject:commit": "node scripts/inject-commit.js", 19 | "build:lite": "npm run clean && npm run esbuild:base && npm run copy:static && npm run optimize:css", 20 | "build": "npm run build:lite && npm run inject:commit && npm run fav:gen && npm run fav:inject && npm run copy:ready", 21 | "dev:static": "nodemon -q --watch src --ext html,css,js --exec \"npm run copy:static && npm run optimize:css && npm run fav:inject\"", 22 | "dev:fav": "nodemon -q --watch favicon/favicon-settings.json --ext json --exec \"npm run fav:gen && npm run fav:inject\"", 23 | "dev": "concurrently -n FLASHER,STATIC,FAV,SERVER \"npm run esbuild:watch\" \"npm run dev:static\" \"npm run dev:fav\" \"npm:preview\"", 24 | "preview": "browser-sync start --config bs-config.js", 25 | "lint": "eslint .", 26 | "typecheck": "tsc --noEmit" 27 | }, 28 | "author": "xyzroe", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "@eslint/css": "^0.14.1", 32 | "@eslint/js": "^9.39.1", 33 | "@eslint/json": "^0.14.0", 34 | "@types/chrome": "^0.0.269", 35 | "browser-sync": "^2.29.3", 36 | "concurrently": "^9.0.0", 37 | "copyfiles": "^2.4.1", 38 | "esbuild": "^0.23.0", 39 | "eslint": "^9.39.1", 40 | "globals": "^16.5.0", 41 | "nodemon": "^3.1.4", 42 | "purgecss": "^7.0.2", 43 | "realfavicon": "^0.4.18", 44 | "typescript": "^5.5.4", 45 | "typescript-eslint": "^8.47.0" 46 | }, 47 | "dependencies": { 48 | "esptool-js": "^0.5.7", 49 | "jiti": "^2.6.1", 50 | "sharp": "^0.33.5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /web-page/src/transport/tcp.ts: -------------------------------------------------------------------------------- 1 | // TCP bridge over WebSocket 2 | // You need a local TCP bridge server that accepts a target host:port param and pipes bytes over the WS. 3 | // Example: ws://127.0.0.1:8765/connect?host=192.168.1.100&port=6638 4 | export class TcpClient { 5 | private ws: WebSocket | null = null; 6 | private onDataCbs: Array<(data: Uint8Array) => void> = []; 7 | private onTxCb: ((data: Uint8Array) => void) | null = null; 8 | private wsBase: string; 9 | 10 | constructor(wsBase?: string) { 11 | this.wsBase = 12 | wsBase || 13 | `ws://${localStorage.getItem("bridgeHost") || "127.0.0.1"}:${ 14 | Number(localStorage.getItem("bridgePort") || 8765) || 8765 15 | }`; 16 | } 17 | 18 | async connect(host: string, port: number): Promise { 19 | // Bridge URL; can be made configurable via settings 20 | const url = `${this.wsBase}/connect?host=${encodeURIComponent(host)}&port=${port}`; 21 | await new Promise((resolve, reject) => { 22 | const ws = new WebSocket(url); 23 | ws.binaryType = "arraybuffer"; 24 | ws.onopen = () => { 25 | this.ws = ws; 26 | resolve(); 27 | }; 28 | ws.onerror = () => reject(new Error("WebSocket error")); 29 | ws.onmessage = (ev) => { 30 | const data = ev.data instanceof ArrayBuffer ? new Uint8Array(ev.data) : new Uint8Array(); 31 | if (data.length === 0) return; 32 | for (const cb of this.onDataCbs) { 33 | try { 34 | cb(data); 35 | } catch { 36 | // ignore 37 | } 38 | } 39 | }; 40 | ws.onclose = (ev) => { 41 | if (this.ws === null) { 42 | // Closed before open => connection failed 43 | reject(new Error(`WebSocket closed (${ev.code})`)); 44 | } 45 | }; 46 | }); 47 | } 48 | 49 | async write(data: Uint8Array): Promise { 50 | if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error("tcp not connected"); 51 | try { 52 | this.onTxCb?.(data); 53 | } catch { 54 | // ignore 55 | } 56 | if (data.length === 0) return; 57 | this.ws.send(data); 58 | } 59 | 60 | onData(cb: (data: Uint8Array) => void) { 61 | this.onDataCbs.push(cb); 62 | } 63 | 64 | offData(cb: (data: Uint8Array) => void) { 65 | const idx = this.onDataCbs.indexOf(cb); 66 | if (idx >= 0) { 67 | this.onDataCbs.splice(idx, 1); 68 | } 69 | } 70 | 71 | onTx(cb: (data: Uint8Array) => void) { 72 | this.onTxCb = cb; 73 | } 74 | 75 | close() { 76 | try { 77 | this.ws?.close(); 78 | } catch { 79 | // ignore 80 | } 81 | this.ws = null; 82 | this.onDataCbs = []; 83 | this.onTxCb = null; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /xzg-multi-tool-addon/DOCS.md: -------------------------------------------------------------------------------- 1 | ## Options 2 | 3 | - PORT (int) — WebSocket/HTTP server port. Default: 8765. 4 | - ADVERTISE_HOST (string) — advertised host/IP. Optional; if empty the host is auto-detected. 5 | - DEBUG_MODE (bool) — enable debug logs. Default: false. 6 | 7 | ## Web interface 8 | 9 | - Served on the same HTTP port as the WebSocket server: http://:/ 10 | - The UI uses the WebSocket bridge to connect to targets. 11 | - No authentication by default; intended for local networks only. 12 | 13 | ## WebSocket bridge 14 | 15 | URL format: 16 | 17 | ``` 18 | ws://:/?host=&port= 19 | ``` 20 | 21 | Behavior: 22 | 23 | - Forward all WS frames to the TCP socket. 24 | - Send TCP data back as WS binary frames. 25 | - Server listens on 0.0.0.0 by default. 26 | 27 | ## HTTP endpoints 28 | 29 | All endpoints respond with JSON and include CORS headers. 30 | 31 | ### GET /mdns 32 | 33 | Purpose: discover mDNS services and optionally expose local serial ports as ephemeral TCP servers. 34 | 35 | Query parameters: 36 | 37 | - types (string) — comma-separated service types (e.g. `_http._tcp`, `_zigstar_gw._tcp.local.`). To include local serial ports, use one of: `local`, `local.serial`, `local-serial`, `local:serial`. 38 | - timeout (int) — scan timeout in ms (500–10000). Default: 2000. 39 | 40 | Response schema: 41 | 42 | ```json 43 | { 44 | "devices": [ 45 | { 46 | "name": "string", 47 | "host": "string", 48 | "port": 1234, 49 | "type": "string", 50 | "protocol": "tcp|serial", 51 | "fqdn": "string", 52 | "txt": { "k": "v" } 53 | } 54 | ] 55 | } 56 | ``` 57 | 58 | Notes: 59 | 60 | - When local serial is requested each port is bound to 0.0.0.0 on an ephemeral TCP port. 61 | - The advertised `host` field is ADVERTISE_HOST if set, otherwise the host primary IPv4. 62 | - Default serial baud: 115200. 63 | 64 | ### GET /sc 65 | 66 | Purpose: set DTR/RTS or change baud on a local serial port (identified by `path` or an exposed TCP `port`). 67 | 68 | Query parameters (one of `path` or `port` required): 69 | 70 | - path (string) — serial device path (e.g. `/dev/ttyUSB0`, `COM3`). 71 | - port (int) — TCP port of the serial server (from `/mdns`). 72 | - dtr (1|0|true|false) — optional. 73 | - rts (1|0|true|false) — optional. 74 | - baud (int) — optional; applied immediately and used for subsequent reconnects. 75 | 76 | Response schema: 77 | 78 | ```json 79 | { "ok": true, "path": "/dev/tty...", "tcpPort": 50123, "set": { "dtr": true, "rts": false, "baud": 115200 } } 80 | ``` 81 | 82 | ## Serial over TCP 83 | 84 | - Request `/mdns?types=local` to create per-device TCP servers for local serial ports. 85 | - Connect via WebSocket to the advertised TCP port using the WebSocket bridge URL above. 86 | 87 | ## Notes 88 | 89 | - mDNS in Linux containers require host networking (`--network host`). 90 | - To expose host serial devices to a container, pass `--device /dev/ttyUSB0:/dev/ttyUSB0`. 91 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01-flashing-issue.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Flashing Issue 2 | description: Report a problem during device flashing 3 | title: "[Flash] " 4 | labels: ["bug", "flashing"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to report a flashing issue! 10 | 11 | - type: dropdown 12 | id: chip-family 13 | attributes: 14 | label: Chip Family 15 | description: Select your device chip family 16 | options: 17 | - TI CCXX52 (CC2652, CC1352, CC2538) 18 | - Silabs EFR32 19 | - TI CC25XX (CC2530, CC2531 and so on) 20 | - Arduino 21 | - ESP8266 / ESP32 22 | - Other 23 | validations: 24 | required: true 25 | 26 | - type: input 27 | id: device-model 28 | attributes: 29 | label: Device Model 30 | description: Specific model (e.g., ZigStar UZG-01, SONOFF ZBDongle-E, Arduino Uno) 31 | validations: 32 | required: true 33 | 34 | - type: dropdown 35 | id: connection-type 36 | attributes: 37 | label: Connection Type 38 | options: 39 | - Local USB 40 | - Remote via Bridge 41 | - CC Debugger 42 | - CC Loader 43 | validations: 44 | required: true 45 | 46 | - type: input 47 | id: browser 48 | attributes: 49 | label: Browser 50 | description: Browser name and version 51 | placeholder: ex. Chrome 120 52 | validations: 53 | required: true 54 | 55 | - type: input 56 | id: os 57 | attributes: 58 | label: Operating System 59 | placeholder: ex. Windows 11, macOS 14, Ubuntu 22.04 60 | validations: 61 | required: true 62 | 63 | - type: dropdown 64 | id: firmware-source 65 | attributes: 66 | label: Firmware Source 67 | options: 68 | - Local file 69 | - Cloud repository 70 | 71 | - type: input 72 | id: firmware-file 73 | attributes: 74 | label: Firmware File 75 | description: Filename or cloud firmware selection 76 | 77 | - type: textarea 78 | id: description 79 | attributes: 80 | label: Problem Description 81 | description: Describe what happened during flashing 82 | validations: 83 | required: true 84 | 85 | - type: textarea 86 | id: steps 87 | attributes: 88 | label: Steps to Reproduce 89 | description: How can we reproduce this issue? 90 | placeholder: | 91 | 1. 92 | 2. 93 | 3. 94 | validations: 95 | required: true 96 | 97 | - type: textarea 98 | id: logs 99 | attributes: 100 | label: Console Logs 101 | description: Please copy logs from the tool (disable "Show TX/RX") 102 | render: shell 103 | validations: 104 | required: true 105 | 106 | - type: textarea 107 | id: additional 108 | attributes: 109 | label: Additional Context 110 | description: Add any other context, screenshots, or information 111 | -------------------------------------------------------------------------------- /web-page/scripts/inject-commit.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process"); 2 | const { readFileSync, writeFileSync, existsSync } = require("fs"); 3 | const path = require("path"); 4 | 5 | try { 6 | const repoRoot = path.resolve(__dirname, ".."); 7 | const distPath = path.join(repoRoot, "dist", "index.html"); 8 | 9 | if (!existsSync(distPath)) { 10 | console.error("dist/index.html not found — run the build step that creates it first."); 11 | process.exit(0); // don't fail the build if file is missing (optional) — can exit with code 1 if strict behavior is needed 12 | } 13 | 14 | // Prefer commit SHA from environment (CI/build systems). 15 | let sha = process.env.COMMIT_SHA || process.env.GIT_COMMIT || process.env.GITHUB_SHA || process.env.GIT_SHA || null; 16 | 17 | // If no env var, attempt to discover git commit by trying multiple candidate 18 | // working directories. This helps when the script is invoked from a different 19 | // CWD (Docker build, npm prefix, CI helpers, etc.). We check for a .git 20 | // directory near the web-page folder and walk up a couple of parents, then 21 | // fallback to the current process.cwd(). If git is not available or no repo 22 | // is present, we fall back to 'unknown' without failing the build. 23 | if (!sha) { 24 | const candidates = []; 25 | // primary candidate: the repo root relative to this script 26 | candidates.push(repoRoot); 27 | // walk up a couple of levels (in case web-page is nested) 28 | candidates.push(path.resolve(repoRoot, "..")); 29 | candidates.push(path.resolve(repoRoot, "..", "..")); 30 | // also try the current working directory 31 | candidates.push(process.cwd()); 32 | 33 | for (const c of candidates) { 34 | try { 35 | const gitDir = path.join(c, ".git"); 36 | if (existsSync(gitDir)) { 37 | // run git in the candidate cwd 38 | sha = execSync("git rev-parse --short HEAD", { cwd: c, encoding: "utf8" }).trim(); 39 | if (sha) break; 40 | } else { 41 | // if .git not present, we can still try git in case the environment 42 | // has the repo elsewhere; wrap in try/catch to avoid throwing. 43 | try { 44 | sha = execSync("git rev-parse --short HEAD", { cwd: c, encoding: "utf8" }).trim(); 45 | if (sha) break; 46 | } catch (e) { 47 | // ignore and continue 48 | } 49 | } 50 | } catch (err) { 51 | // ignore and continue to next candidate 52 | } 53 | } 54 | } 55 | 56 | if (!sha) { 57 | sha = "unknown"; 58 | console.warn('No git repository found and no commit env var set — injecting "unknown"'); 59 | } 60 | 61 | //read version from package.json 62 | let version; 63 | try { 64 | const pkg = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf8")); 65 | version = pkg.version; 66 | } catch (e) { 67 | version = "unknown"; 68 | console.warn('No package.json found or invalid — injecting "unknown"'); 69 | } 70 | 71 | const html = readFileSync(distPath, "utf8"); 72 | const updated = html.replace(/COMMIT_PLH/g, sha).replace(/VER_PLH/g, version); 73 | writeFileSync(distPath, updated, "utf8"); 74 | console.log("Injected commit SHA:", sha); 75 | console.log("Injected version:", version); 76 | } catch (err) { 77 | console.error("Failed to inject commit SHA:", err.message || err); 78 | console.error("Failed to inject version:", err.message || err); 79 | process.exit(1); 80 | } 81 | -------------------------------------------------------------------------------- /web-page/src/utils/http.ts: -------------------------------------------------------------------------------- 1 | // Fire a GET with a timeout; on CORS-related failures, retry with no-cors and accept opaque response. 2 | export async function httpGetWithFallback( 3 | url: string, 4 | timeoutMs = 8000 5 | ): Promise<{ text: string | null; opaque: boolean }> { 6 | const controller = new AbortController(); 7 | const timer = setTimeout(() => controller.abort(), timeoutMs); 8 | try { 9 | const resp = await fetch(url, { signal: controller.signal }); 10 | if (!resp.ok) { 11 | const body = await resp.text().catch(() => ""); 12 | throw new Error(`HTTP ${resp.status} ${resp.statusText}${body ? ` - ${body}` : ""}`); 13 | } 14 | if (resp.type === "opaque") return { text: null, opaque: true }; 15 | const text = await resp.text(); 16 | return { text, opaque: false }; 17 | } catch (e: unknown) { 18 | const err = e as Error; 19 | const msg = err?.message || String(e); 20 | if (/Failed to fetch|TypeError|CORS|NetworkError/i.test(msg)) { 21 | try { 22 | await fetch(url, { mode: "no-cors", signal: controller.signal }); 23 | return { text: null, opaque: true }; 24 | } catch { 25 | throw e; 26 | } 27 | } 28 | throw e; 29 | } finally { 30 | clearTimeout(timer); 31 | } 32 | } 33 | 34 | /** 35 | * Save data to a file with auto-generated filename 36 | * @param data - Data to save (Uint8Array for binary, string for text) 37 | * @param mimeType - MIME type (e.g., "application/octet-stream", "text/plain", "application/json") 38 | * @param extension - File extension without dot (e.g., "bin", "hex", "json") 39 | * @param prefix - Filename prefix (e.g., "dump", "NVRAM") 40 | * @param chipModel - Chip model name (optional) 41 | * @param ieeeAddress - IEEE MAC address (optional) 42 | * @returns The generated filename 43 | */ 44 | export function saveToFile( 45 | data: Uint8Array | string, 46 | mimeType: string, 47 | extension: string, 48 | prefix: string, 49 | chipModel?: string, 50 | ieeeAddress?: string, 51 | extraInfo?: string 52 | ): string { 53 | // Sanitize chip model: replace spaces with dashes, remove non-alphanumeric chars 54 | const modelSafe = (chipModel || "unknown") 55 | .trim() 56 | .replace(/\s+/g, "-") 57 | .replace(/[^A-Za-z0-9._-]/g, ""); 58 | 59 | // Sanitize IEEE address: keep only hex chars (remove colons, etc.) 60 | const ieeeSafe = (ieeeAddress || "unknown").toUpperCase().replace(/[^A-F0-9]/g, ""); 61 | 62 | // Generate timestamp: YYYY-MM-DDTHH-MM-SS 63 | const now = new Date(); 64 | const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, -5); 65 | 66 | // Build filename 67 | let filename = `${prefix}_${modelSafe}`; 68 | if (ieeeAddress) { 69 | filename += `_${ieeeSafe}`; 70 | } 71 | if (extraInfo) { 72 | filename += `_${extraInfo}`; 73 | } 74 | 75 | filename += `_${timestamp}.${extension}`; 76 | 77 | // Create blob based on data type 78 | let blob: Blob; 79 | if (data instanceof Uint8Array) { 80 | const copy = new Uint8Array(data); 81 | blob = new Blob([copy], { type: mimeType }); 82 | } else { 83 | blob = new Blob([data], { type: mimeType }); 84 | } 85 | 86 | // Create download link and trigger download 87 | const url = URL.createObjectURL(blob); 88 | const a = document.createElement("a"); 89 | a.href = url; 90 | a.download = filename; 91 | document.body.appendChild(a); 92 | a.click(); 93 | a.remove(); 94 | setTimeout(() => URL.revokeObjectURL(url), 1000); 95 | 96 | return filename; 97 | } 98 | -------------------------------------------------------------------------------- /bridge/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | 12 | "github.com/labstack/echo/v4" 13 | "github.com/labstack/echo/v4/middleware" 14 | ) 15 | 16 | // VERSION is set at build time. Use go build -ldflags "-X main.VERSION=1.2.3" 17 | // to override the default value during the build process. 18 | var VERSION = "0.0.0" 19 | 20 | const ( 21 | DEFAULT_WS_PORT = 8765 22 | ) 23 | 24 | var ( 25 | wsPort int 26 | advertiseHost string 27 | debugMode bool 28 | ) 29 | 30 | func main() { 31 | // Parse command line arguments 32 | flag.IntVar(&wsPort, "port", DEFAULT_WS_PORT, "WebSocket server port") 33 | flag.StringVar(&advertiseHost, "advertise-host", "", "Advertise host for mDNS") 34 | flag.BoolVar(&debugMode, "debug", false, "Enable debug mode") 35 | flag.Parse() 36 | 37 | log.SetFlags(log.Ltime | log.Lmicroseconds) 38 | // Override with environment variables 39 | if port := os.Getenv("PORT"); port != "" { 40 | if p, err := fmt.Sscanf(port, "%d", &wsPort); err == nil && p == 1 { 41 | // Successfully parsed 42 | } 43 | } 44 | if host := os.Getenv("ADVERTISE_HOST"); host != "" { 45 | advertiseHost = host 46 | } 47 | if debug := os.Getenv("DEBUG_MODE"); debug == "1" || debug == "true" || debug == "yes" || debug == "on" { 48 | debugMode = true 49 | 50 | } 51 | 52 | log.Printf("[XZG-MT] Local Bridge Server v%s\n", VERSION) 53 | log.Printf("[XZG-MT] access UI at http://%s:%d\n", getAdvertiseHost(), wsPort) 54 | 55 | if debugMode { 56 | log.Println("[XZG-MT] debug mode enabled") 57 | } 58 | // Create Echo instance 59 | e := echo.New() 60 | e.HideBanner = true 61 | 62 | // Middleware 63 | // if debugMode { 64 | // e.Use(middleware.Logger()) 65 | // } 66 | e.Use(middleware.Recover()) 67 | e.Use(middleware.CORS()) 68 | 69 | // Routes 70 | setupRoutes(e) 71 | 72 | // Start serial monitor 73 | //go startSerialMonitor() 74 | 75 | // Start server 76 | go func() { 77 | if err := e.Start(fmt.Sprintf(":%d", wsPort)); err != nil { 78 | log.Fatal(err) 79 | } 80 | }() 81 | 82 | // Wait for interrupt signal to gracefully shutdown the server 83 | quit := make(chan os.Signal, 1) 84 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 85 | <-quit 86 | 87 | log.Println("[shutdown] graceful shutdown starting...") 88 | 89 | // Stop serial monitor 90 | //stopSerialMonitor() 91 | 92 | // Close all serial servers 93 | closeAllSerialServers() 94 | 95 | log.Println("[shutdown] done") 96 | } 97 | 98 | func getAdvertiseHost() string { 99 | if advertiseHost != "" { 100 | return advertiseHost 101 | } 102 | return getPrimaryIPv4() 103 | } 104 | 105 | func getPrimaryIPv4() string { 106 | // Find first active non-loopback interface with an IPv4 address 107 | ifaces, err := net.Interfaces() 108 | if err != nil { 109 | return "127.0.0.1" 110 | } 111 | 112 | for _, iface := range ifaces { 113 | // skip interfaces that are down or loopback 114 | if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { 115 | continue 116 | } 117 | 118 | addrs, err := iface.Addrs() 119 | if err != nil { 120 | continue 121 | } 122 | 123 | for _, a := range addrs { 124 | var ip net.IP 125 | switch v := a.(type) { 126 | case *net.IPNet: 127 | ip = v.IP 128 | case *net.IPAddr: 129 | ip = v.IP 130 | } 131 | if ip == nil || ip.IsLoopback() { 132 | continue 133 | } 134 | ip4 := ip.To4() 135 | if ip4 == nil { 136 | continue // not IPv4 137 | } 138 | // skip link-local addresses (169.254.x.x) 139 | if ip4.IsLinkLocalUnicast() { 140 | continue 141 | } 142 | return ip4.String() 143 | } 144 | } 145 | 146 | // fallback 147 | return "127.0.0.1" 148 | } 149 | -------------------------------------------------------------------------------- /bridge/README.md: -------------------------------------------------------------------------------- 1 | # XZG-MT Bridge 2 | 3 | A Go implementation of the XZG Multi-tool Bridge server. This is a WebSocket-TCP bridge with mDNS discovery and local serial port exposure capabilities. 4 | 5 | ## ⭐ Features 6 | 7 | - **WebSocket ↔ TCP Bridge**: Forward WebSocket connections to TCP devices 8 | - **mDNS Discovery**: Automatically discover devices on the local network 9 | - **Serial Port Support**: Expose local serial ports as TCP servers 10 | - **HTTP Control**: Control DTR/RTS pins, general GPIOs (only 🐧) and baud rates via HTTP API 11 | - **Embedded Web UI**: Built-in web interface for device management 12 | - **Cross-Platform**: Builds for: **Linux**: (amd64, arm64, 386, arm, mips, mipsle, mips64, mips64le); **macOS** (darwin): (amd64, arm64); **Windows** (amd64, 386, arm64); 13 | 14 | ## 🏗️ Architecture 15 | 16 | - **HTTP Server**: Serves the web UI and API endpoints 17 | - **WebSocket Handler**: Manages WebSocket connections and forwards to TCP 18 | - **Serial Manager**: Handles serial port discovery and TCP server creation 19 | - **mDNS Scanner**: Discovers devices on the local network 20 | - **Embedded Assets**: Web UI files are embedded in the binary 21 | 22 | ## 🚀 Quick Start 23 | 24 | The easiest way is to use prebuild binaries, ready yo use Docker images or even HomeAssist Add-on. 25 | More info can be found in the [main readme](../README.md#-remote-tcp-or-remote-usbserial) 26 | 27 | ## 🛠️ Build from the source 28 | 29 | ### Prerequisites 30 | 31 | - Go 1.21 or later 32 | - Git 33 | 34 | ### Installation 35 | 36 | 1. Clone the repository: 37 | 38 | ```bash 39 | git clone https://github.com/xyzroe/XZG-MT.git 40 | cd XZG-MT/bridge 41 | ``` 42 | 43 | 2. Install dependencies: 44 | 45 | ```bash 46 | make deps 47 | ``` 48 | 49 | 3. Run locally: 50 | 51 | ```bash 52 | make run 53 | ``` 54 | 55 | 4. Open your browser to `http://localhost:8765` 56 | 57 | ### Building Binaries 58 | 59 | Build for all platforms: 60 | 61 | ```bash 62 | make build 63 | ``` 64 | 65 | Build for current platform only: 66 | 67 | ```bash 68 | make build-local 69 | ``` 70 | 71 | ### Building Docker image 72 | 73 | ```bash 74 | docker buildx build --platform linux/amd64 --build-arg VERSION=dev -t xzg-mt-bridge:dev --load -f bridge/Dockerfile . 75 | ``` 76 | 77 | ## 📖 Usage 78 | 79 | ### Command Line Options 80 | 81 | ```bash 82 | ./XZG-MT-linux-amd64 [options] 83 | ``` 84 | 85 | Options: 86 | 87 | - `-port`: WebSocket server port (default: 8765) 88 | - `-advertise-host`: Host to advertise for mDNS (default: auto-detect) 89 | - `-debug`: Enable debug mode (default: no) 90 | 91 | ### Environment Variables 92 | 93 | - `PORT`: WebSocket server port 94 | - `ADVERTISE_HOST`: Host to advertise for mDNS 95 | - `DEBUG_MODE`: Enable debug mode (1, true, yes, on) 96 | 97 | ## 🔌 API Endpoints 98 | 99 | #### WebSocket Bridge 100 | 101 | - `GET /ws?host=&port=`: WebSocket bridge to TCP device 102 | 103 | #### mDNS Discovery 104 | 105 | - `GET /mdns?types=&timeout=`: Discover devices via mDNS 106 | 107 | #### Serial Control 108 | 109 | - `GET /sc?path=&dtr=<0|1>&rts=<0|1>&baud=`: Control serial port 110 | 111 | #### GPIO Control 112 | 113 | - `GET /gpio?path=&set=<0|1>`: Control GPIO port 114 | 115 | #### Static Files 116 | 117 | - `GET /*`: Serve embedded web interface 118 | 119 | ## 📁 Project Structure 120 | 121 | ``` 122 | bridge/ 123 | ├── main.go # Main application entry point 124 | ├── routes.go # HTTP route handlers 125 | ├── websocket.go # WebSocket connection handling 126 | ├── serial.go # Serial port management 127 | ├── mdns.go # mDNS discovery 128 | ├── embed.go # Embedded file handling 129 | ├── go.mod # Go module definition 130 | ├── build.sh # Build script 131 | ├── Makefile # Build automation 132 | ├── web/ # Web UI files (embedded) 133 | └── dist/ # Built binaries (created during build) 134 | ``` 135 | -------------------------------------------------------------------------------- /docs/how-to/cc_loader.md: -------------------------------------------------------------------------------- 1 | # How to Use CCLoader for Flashing CC2530 Chips 2 | 3 | ## 📖 Introduction 4 | 5 | The CC2530 is a System-on-Chip (SoC) from Texas Instruments, commonly used in Zigbee applications, smart home devices, and IoT projects. CCLoader is an ESP/Arduino-based device with special firmware that allows programming CC2530 family chips without the need for dedicated debuggers like TI CC Debugger. This guide covers using CCLoader to program CC2530 chips using the XZG-MT tool. 6 | 7 | ## 🔧 Required Hardware 8 | 9 | - **CCLoader**: An Arduino/ESP8266/ESP32 module flashed with the special CCLoader firmware. 10 | - **CC2530 device**: The target device to be flashed. 11 | - **USB Cable**: For connecting the CCLoader to your host device. 12 | - **Host device**: Computer (Windows, Linux, macOS) 13 | 14 | ## 💻 Preparing CCLoader Firmware 15 | 16 | Before using CCLoader, you need to flash the Arduino/ESP with the special CCLoader firmware. This can be done using XZG-MT: 17 | 18 | 1. **Open XZG-MT**: 19 | 20 | - Open the [XZG-MT](https://mt.xyzroe.cc) on your computer. 21 | 22 | 2. **Select Chip Family**: 23 | 24 | - In XZG-MT, select the `Arduino` or `ESP` in the Family section. 25 | 26 | 3. **Connect to Device**: 27 | 28 | - Connect your board to your computer via USB. 29 | - Click the `Connect` button in XZG-MT. 30 | - The web browser should display a list of available serial ports; select yours and click `Connect`. 31 | 32 | 4. **Select Firmware**: 33 | 34 | - In the cloud firmware list, select the corresponding CCLoader firmware for your board and pinout. 35 | 36 | _For pinout information on how to connect the board to the CC2530, click the info button after selecting the firmware in the cloud list. A popup will display the required connections._ 37 | 38 | 5. **Flash the Firmware**: 39 | - Click the `Flash` button in XZG-MT. 40 | - Wait for the process to complete. The progress should be displayed in the interface. 41 | 6. **Disconnect the serial connection**: 42 | 43 | - Click `Disconnect` button. 44 | 45 | ## ⚡ Flashing Procedure 46 | 47 | 1. **Prepare Your Setup**: 48 | 49 | - Ensure the CC2530 chip is properly connected to the CCLoader according to the pinout provided in the firmware info popup. 50 | - Power on the target device if required. 51 | 52 | 2. **Open XZG-MT**: 53 | 54 | - Open the [XZG-MT](https://mt.xyzroe.cc) on your computer. 55 | 56 | 3. **Select Chip Family**: 57 | 58 | - In XZG-MT, select the `TI CC25XX` in the Family section. 59 | 60 | 4. **Connect CCLoader**: 61 | 62 | - Click the `Connect Loader` button. 63 | - Web browser should display a list of available serial ports, select your CCLoader and click `Connect`. 64 | - XZG-MT will detect the CCLoader and the connected CC2530 chip's model and IEEE. 65 | 66 | 5. **Load Firmware**: 67 | 68 | - Select the firmware file (`.hex` or `.bin`) you want to flash. 69 | 70 | 6. **Flash the Chip**: 71 | 72 | - Select the options you need (Write and Verify). Erase will be done in any case. 73 | - Click the `Start` button in XZG-MT. 74 | - Wait for the process to complete. The progress should be displayed in the interface. More information can be found in `Logs` section. 75 | 76 | 7. **Verify**: 77 | - After flashing, verify that the device functions as expected. 78 | 79 | ## 💾 Dump flash to a file 80 | 81 | XZG-MT allows to read device's flash and save it to a local file. 82 | 83 | 1. Connect to device as described above (1-4) 84 | 2. Click on `Dump flash` button in the `Actions` section. 85 | 3. Wait until all data will be read. The progress should be displayed in the interface. More information can be found in `Logs` section. 86 | 4. Save file to your computer. 87 | 88 | ## 🛠️ Troubleshooting 89 | 90 | - **Device Not Recognized**: Try a different USB port or cable. 91 | - **Connection Failed**: Check physical connections between CCLoader and CC2530 according to the pinout. Ensure the chip is powered. Ensure the CCLoader is properly flashed with CCLoader firmware. 92 | - **CC2530 Flashing Errors**: Verify the firmware file is compatible with CC2530. Check for voltage issues or faulty hardware. 93 | - **CCLoader Flashing Issues**: Ensure you selected the correct Arduino / ESP board and firmware in XZG-MT. 94 | 95 | ## 🆘 If the Problem Persists 96 | 97 | If the problem persists after trying the troubleshooting steps, please open an issue on the [XZG-MT GitHub repository](https://github.com/xyzroe/XZG-MT/issues). Provide detailed information about your setup, operating system, error messages, and steps you've taken. 98 | -------------------------------------------------------------------------------- /docs/how-to/cc_debuger.md: -------------------------------------------------------------------------------- 1 | # How to Use CC Debugger for Flashing CC2530 Chips 2 | 3 | ## 📖 Introduction 4 | 5 | The CC2530 is a System-on-Chip (SoC) from Texas Instruments, commonly used in Zigbee applications, smart home devices, and IoT projects. The best way to flash firmware onto CC2530 chips requires a special programmer. This guide covers using the TI CC Debugger or SmartRF04EB to program CC2530 family chips using the XZG-MT. 6 | 7 | ## 🔧 Required Hardware 8 | 9 | - **Programmer**: Texas Instruments CC Debugger or SmartRF04EB 10 | - **CC2530 device**: The target device to be flashed. 11 | - **USB Cable**: For connecting the debugger to your host device. 12 | - **Host device**: Computer (Windows, Linux, macOS) or Android device 13 | 14 | ## 💻 Driver Installation 15 | 16 | ### Linux, macOS, and Android 17 | 18 | These operating systems support plug-and-play for the CC Debugger and SmartRF04EB. No additional drivers are required. 19 | 20 | 1. Connect the debugger to your host device via USB. 21 | 2. The device should be recognized automatically. 22 | 23 | ### Windows 24 | 25 | Windows requires installing the WinUSB driver for proper communication with the debugger. 26 | 27 | 1. Download Zadig from the [official website](https://zadig.akeo.ie/). 28 | 2. Run Zadig as an administrator. 29 | 3. Connect your CC Debugger or SmartRF04EB to your computer. 30 | 4. In Zadig, select the device from the dropdown (it may appear as "Texas Instruments CC Debugger" or similar). 31 | 5. Choose "WinUSB" from the driver list. 32 | 6. Click "Install Driver" or "Replace Driver". 33 | 34 | If you have previously installed the SmartRF Flash Programmer from TI, your system may have the CEBAL driver installed. You need to replace it with WinUSB using Zadig as described above. 35 | 36 | ![Zadig with successfully installed WinUSB](./imgs/zadig.jpg) 37 | 38 | After installation, verify in Device Manager that the device is listed under "Universal Serial Bus devices" with "WinUSB" driver. 39 | 40 | ![Device Manager with successfully installed WinUSB](./imgs/device_manager.jpg) 41 | 42 | ## ⚡ Flashing Procedure 43 | 44 | 1. **Prepare Your Setup**: 45 | 46 | - Ensure the CC2530 chip is properly connected to the debugger. 47 | ![Debugger pinout](./imgs/debugger-pinout.png) 48 | - Power on the target device if required. 49 | 50 | 2. **Open XZG-MT**: 51 | 52 | - Open the [XZG-MT](https://mt.xyzroe.cc) on your host device. 53 | 54 | 3. **Select Chip Family**: 55 | 56 | - In XZG-MT, select the `TI CC25XX` in Family section. 57 | 58 | 4. **Connect Debugger**: 59 | 60 | - Click the `Connect Debugger` button. 61 | - Web browser should display list of compatible devices, select yours and click `Connect`. 62 | - XZG-MT will detect programmer's model and firmware version and chip's model and IEEE. 63 | 64 | 5. **Load Firmware**: 65 | 66 | - Select the firmware file (`.hex` or `.bin`) you want to flash. 67 | 68 | 6. **Flash the Chip**: 69 | 70 | - Select the options you need (Erase, Write and Verify). 71 | - Select writing and verifying methods. 72 | - Click the `Start` button in XZG-MT. 73 | - Wait for the process to complete. The progress should be displayed in the interface. More information can be found in `Logs` section. 74 | 75 | 7. **Verify**: 76 | - After flashing, verify that the device functions as expected. 77 | 78 | ## 💾 Dump flash to a file 79 | 80 | XZG-MT allows to read device's flash and save it to a local file. 81 | 82 | 1. Connect to device as described above (1-4) 83 | 2. Click on `Dump flash` button in the `Actions` section. 84 | 3. Wait until all data will be read. The progress should be displayed in the interface. More information can be found in `Logs` section. 85 | 4. Save file to your host device. 86 | 87 | ## 🛠️ Troubleshooting 88 | 89 | - **Device Not Recognized**: Ensure drivers are installed correctly (especially on Windows). Try a different USB port or cable. 90 | - **Access denied (Windows)**: Ensure that you have installed WinUSB driver and your device is listed under "Universal Serial Bus devices" in Device Manager. 91 | - **Connection Failed**: Check physical connections between debugger and CC2530. Ensure the chip is powered. 92 | - **Flashing Errors**: Verify the firmware file is compatible with CC2530. Check for voltage issues or faulty hardware. 93 | - **Zadig Issues**: Run Zadig as administrator. If the device doesn't appear, try unplugging and replugging the debugger. If it still doesn't show, click Options and select "List All Devices". 94 | 95 | ## 🆘 If the Problem Persists 96 | 97 | If the problem persists after trying the troubleshooting steps, please open an issue on the [XZG-MT GitHub repository](https://github.com/xyzroe/XZG-MT/issues). Provide detailed information about your setup, operating system, error messages, and steps you've taken. 98 | -------------------------------------------------------------------------------- /.github/workflows/notify-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Notification 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | prepare: 13 | name: Prepare Notification Assets 14 | runs-on: ubuntu-latest 15 | outputs: 16 | version: ${{ steps.release_data.outputs.version }} 17 | url: ${{ steps.release_data.outputs.url }} 18 | title: ${{ steps.release_data.outputs.title }} 19 | steps: 20 | - name: Get release data 21 | id: release_data 22 | run: | 23 | API_RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/${{ github.repository }}/releases/latest") 24 | VERSION=$(echo "$API_RESPONSE" | jq -r '.tag_name') 25 | RELEASE_URL=$(echo "$API_RESPONSE" | jq -r '.html_url') 26 | 27 | echo "version=$VERSION" >> "$GITHUB_OUTPUT" 28 | echo "url=$RELEASE_URL" >> "$GITHUB_OUTPUT" 29 | echo "title=🎉 New Release of MT" >> "$GITHUB_OUTPUT" 30 | 31 | - name: Download OpenGraph image 32 | run: | 33 | IMAGE_URL="https://opengraph.githubassets.com/0.1.0/${{ github.repository }}/releases/tag/${{ steps.release_data.outputs.version }}" 34 | echo "📥 Downloading OpenGraph image from: $IMAGE_URL" 35 | 36 | # Try to download with retries (in case image is still generating) 37 | for i in {1..5}; do 38 | if curl -s -L -o release-image.png "$IMAGE_URL"; then 39 | if [ -s release-image.png ]; then 40 | if file --mime-type release-image.png | grep -q "image/"; then 41 | echo "✅ Image downloaded successfully (attempt $i)" 42 | ls -lh release-image.png 43 | break 44 | else 45 | echo "⚠️ Downloaded file is not an image (attempt $i)" 46 | cat release-image.png 47 | fi 48 | fi 49 | fi 50 | echo "⏳ Attempt $i failed, waiting $((i*3)) seconds..." 51 | sleep $((i*3)) 52 | done 53 | 54 | if [ ! -f release-image.png ] || ! file --mime-type release-image.png | grep -q "image/"; then 55 | echo "❌ Failed to download a valid image." 56 | exit 1 57 | fi 58 | 59 | - name: Upload release image 60 | uses: actions/upload-artifact@v4 61 | with: 62 | name: release-image 63 | path: release-image.png 64 | retention-days: 1 65 | 66 | notify-telegram: 67 | name: Send Telegram Notification 68 | needs: prepare 69 | runs-on: ubuntu-latest 70 | if: success() 71 | env: 72 | TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} 73 | TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} 74 | TELEGRAM_THREAD_ID: ${{ secrets.TELEGRAM_THREAD_ID }} 75 | steps: 76 | - name: Download release image 77 | uses: actions/download-artifact@v4 78 | with: 79 | name: release-image 80 | 81 | - name: Send Telegram Notification about release 82 | if: env.TELEGRAM_BOT_TOKEN != '' && env.TELEGRAM_CHAT_ID != '' && env.TELEGRAM_THREAD_ID != '' 83 | run: | 84 | CAPTION=$'*${{ needs.prepare.outputs.title }}: ${{ needs.prepare.outputs.version }}*\n\n[View on GitHub](${{ needs.prepare.outputs.url }})' 85 | 86 | curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendPhoto" \ 87 | -F chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \ 88 | -F message_thread_id="${{ secrets.TELEGRAM_THREAD_ID }}" \ 89 | -F parse_mode="Markdown" \ 90 | -F photo="@release-image.png" \ 91 | -F caption="$CAPTION" 92 | 93 | notify-discord: 94 | name: Send Discord Notification 95 | needs: prepare 96 | runs-on: ubuntu-latest 97 | if: success() 98 | env: 99 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 100 | steps: 101 | - name: Download release image 102 | uses: actions/download-artifact@v4 103 | with: 104 | name: release-image 105 | 106 | - name: Send Discord Notification 107 | if: env.DISCORD_WEBHOOK != '' 108 | run: | 109 | # Construct JSON payload using jq to handle escaping safely 110 | JSON_PAYLOAD=$(jq -n \ 111 | --arg title "${{ needs.prepare.outputs.title }}" \ 112 | --arg version "${{ needs.prepare.outputs.version }}" \ 113 | --arg url "${{ needs.prepare.outputs.url }}" \ 114 | '{ 115 | embeds: [{ 116 | title: $title, 117 | description: ($version + "\n\n[View on GitHub](" + $url + ")"), 118 | color: 3066993, 119 | image: { url: "attachment://release-image.png" } 120 | }] 121 | }') 122 | 123 | curl -s -X POST "${{ secrets.DISCORD_WEBHOOK }}" \ 124 | -F "file=@release-image.png" \ 125 | -F "payload_json=$JSON_PAYLOAD" 126 | -------------------------------------------------------------------------------- /web-page/src/utils/intelhex.ts: -------------------------------------------------------------------------------- 1 | export function parseIntelHex(fullText: string, fillByte: number = 0x00): { startAddress: number; data: Uint8Array } { 2 | const lines = fullText 3 | .split(/\r?\n/) 4 | .map((l) => l.trim()) 5 | .filter((l) => l.length > 0); 6 | type Rec = { absBase: number; addr: number; bytes: number[]; type: number }; 7 | const recs: Rec[] = []; 8 | 9 | let base = 0; // current base (set by type 04 <<16 or type 02 <<4) 10 | let minAddr = Number.POSITIVE_INFINITY; 11 | let maxAddr = 0; 12 | 13 | for (let i = 0; i < lines.length; i++) { 14 | const l = lines[i]; 15 | if (!l.startsWith(":")) throw new Error(`Invalid HEX line ${i + 1}`); 16 | const byteCount = parseInt(l.substr(1, 2), 16); 17 | const addr = parseInt(l.substr(3, 4), 16); 18 | const type = parseInt(l.substr(7, 2), 16); 19 | const dataHex = l.substr(9, byteCount * 2); 20 | // checksum omitted here (could validate if needed) 21 | 22 | if (type === 0x00) { 23 | const bytes: number[] = []; 24 | for (let j = 0; j < byteCount; j++) { 25 | bytes.push(parseInt(dataHex.substr(j * 2, 2), 16)); 26 | } 27 | const absBase = base; 28 | const absAddr = absBase + addr; 29 | recs.push({ absBase, addr, bytes, type }); 30 | minAddr = Math.min(minAddr, absAddr); 31 | maxAddr = Math.max(maxAddr, absAddr + bytes.length - 1); 32 | } else if (type === 0x01) { 33 | // EOF record 34 | break; 35 | } else if (type === 0x02) { 36 | // Extended Segment Address Record: bits 4..19 -> shift left 4 37 | const seg = parseInt(dataHex, 16) & 0xffff; 38 | base = (seg << 4) >>> 0; 39 | } else if (type === 0x04) { 40 | // Extended Linear Address Record: upper 16 bits 41 | const upper = parseInt(dataHex, 16) & 0xffff; 42 | base = (upper << 16) >>> 0; 43 | } else { 44 | // other record types (03,05) — ignore but keep base as is 45 | } 46 | } 47 | 48 | if (minAddr === Number.POSITIVE_INFINITY) { 49 | // no data records 50 | return { startAddress: 0, data: new Uint8Array(0) }; 51 | } 52 | 53 | const size = maxAddr - minAddr + 1; 54 | if (size <= 0) return { startAddress: 0, data: new Uint8Array(0) }; 55 | const out = new Uint8Array(size); 56 | // fill with 0x00 to represent erased flash 57 | //out.fill(0x00); 58 | out.fill(fillByte); 59 | 60 | // write records 61 | for (const r of recs) { 62 | const abs = r.absBase + r.addr; 63 | const offset = abs - minAddr; 64 | out.set(Uint8Array.from(r.bytes), offset); 65 | } 66 | 67 | return { startAddress: minAddr >>> 0, data: out }; 68 | } 69 | 70 | // Generate Intel HEX format from binary data 71 | export function generateHex(data: Uint8Array, baseAddress: number = 0): string { 72 | const lines: string[] = []; 73 | const BYTES_PER_LINE = 16; 74 | 75 | // Helper to calculate checksum 76 | function calculateChecksum(bytes: number[]): number { 77 | let sum = 0; 78 | for (const b of bytes) { 79 | sum += b; 80 | } 81 | return -sum & 0xff; 82 | } 83 | 84 | // Helper to format hex byte 85 | function toHex(value: number, digits: number = 2): string { 86 | return value.toString(16).toUpperCase().padStart(digits, "0"); 87 | } 88 | 89 | // Helper to check if a line contains only 0xFF (empty flash) 90 | function isEmptyLine(data: Uint8Array, offset: number, count: number): boolean { 91 | for (let i = 0; i < count; i++) { 92 | if (data[offset + i] !== 0xff) { 93 | return false; 94 | } 95 | } 96 | return true; 97 | } 98 | 99 | let currentExtendedAddress = -1; 100 | 101 | for (let offset = 0; offset < data.length; offset += BYTES_PER_LINE) { 102 | const count = Math.min(BYTES_PER_LINE, data.length - offset); 103 | 104 | // Skip lines that contain only 0xFF (unprogrammed flash) 105 | if (isEmptyLine(data, offset, count)) { 106 | continue; 107 | } 108 | 109 | const address = baseAddress + offset; 110 | const highAddress = (address >> 16) & 0xffff; 111 | 112 | // Emit Extended Linear Address record if needed 113 | if (highAddress !== currentExtendedAddress) { 114 | currentExtendedAddress = highAddress; 115 | const recordData = [ 116 | 0x02, // byte count 117 | 0x00, 118 | 0x00, // address (always 0000 for type 04) 119 | 0x04, // record type (Extended Linear Address) 120 | (highAddress >> 8) & 0xff, 121 | highAddress & 0xff, 122 | ]; 123 | const checksum = calculateChecksum(recordData); 124 | lines.push(`:02000004${toHex(highAddress, 4)}${toHex(checksum)}`); 125 | } 126 | 127 | // Emit data record 128 | const lineAddress = address & 0xffff; 129 | const recordData = [ 130 | count, 131 | (lineAddress >> 8) & 0xff, 132 | lineAddress & 0xff, 133 | 0x00, // record type (Data) 134 | ]; 135 | 136 | let dataHex = ""; 137 | for (let i = 0; i < count; i++) { 138 | const byte = data[offset + i]; 139 | recordData.push(byte); 140 | dataHex += toHex(byte); 141 | } 142 | 143 | const checksum = calculateChecksum(recordData); 144 | lines.push(`:${toHex(count)}${toHex(lineAddress, 4)}00${dataHex}${toHex(checksum)}`); 145 | } 146 | 147 | // Emit EOF record 148 | lines.push(":00000001FF"); 149 | 150 | return lines.join("\n") + "\n"; 151 | } 152 | -------------------------------------------------------------------------------- /bridge/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build script for XZG-MT Go Bridge 4 | # Creates binaries for multiple operating systems and architectures 5 | 6 | set -e 7 | 8 | 9 | 10 | BUILD_DIR="dist" 11 | MAIN_FILE="main.go" 12 | 13 | # Size-reduction defaults. Can be overridden via environment. 14 | # Set USE_UPX=1 to try to compress with upx if it's installed. 15 | USE_UPX=${USE_UPX:-0} 16 | 17 | # Switch to the directory of script 18 | cd "$(dirname "$0")" 19 | 20 | #VERSION="1.0.0" 21 | #don't use static version 22 | # read from ../web-page/package.json 23 | if [ -f "../web-page/package.json" ]; then 24 | VERSION=$(jq -r '.version' ../web-page/package.json) 25 | else 26 | # echo "Warning: ../package.json not found. Using default version." 27 | VERSION="0.0.0" 28 | fi 29 | 30 | 31 | # Create build directory 32 | mkdir -p $BUILD_DIR 33 | 34 | # Build targets 35 | TARGETS=( 36 | "linux/amd64" 37 | "linux/arm64" 38 | "linux/386" 39 | "linux/arm" 40 | "linux/mips" 41 | "linux/mipsle" 42 | "linux/mips64" 43 | "linux/mips64le" 44 | "darwin/amd64" 45 | "darwin/arm64" 46 | "windows/amd64" 47 | "windows/386" 48 | "windows/arm64" 49 | ) 50 | 51 | # Copy web assets with progress 52 | echo "Copying web assets..." 53 | # Copy web assets without rsync (portable) 54 | SRC="../web-page/dist" 55 | DST="./web" 56 | 57 | if [ ! -d "$SRC" ]; then 58 | echo "Warning: source directory $SRC does not exist. Skipping copy." 59 | else 60 | mkdir -p "$DST" 61 | 62 | total_files=$(find "$SRC" -type f | wc -l | tr -d ' ') 63 | total_size=$(du -sh "$SRC" 2>/dev/null | cut -f1 || echo "N/A") 64 | echo "Found $total_files files (≈ $total_size) to copy." 65 | 66 | if [ "$total_files" -eq 0 ]; then 67 | echo "No files to copy." 68 | else 69 | i=0 70 | # copy each file preserving directory structure 71 | while IFS= read -r -d '' file; do 72 | rel="${file#$SRC/}" 73 | mkdir -p "$(dirname "$DST/$rel")" 74 | # try to preserve mode/timestamps; fall back to plain cp if needed 75 | cp -p -- "$file" "$DST/$rel" 2>/dev/null || cp -- "$file" "$DST/$rel" 76 | i=$((i+1)) 77 | percent=$((i * 100 / total_files)) 78 | printf "\rCopying files: %d/%d (%d%%)" "$i" "$total_files" "$percent" 79 | done < <(find "$SRC" -type f -print0) 80 | echo 81 | fi 82 | 83 | # Summary 84 | copied_files=$(find "$DST" -type f | wc -l | tr -d ' ') 85 | copied_size=$(du -sh "$DST" 2>/dev/null | cut -f1 || echo "N/A") 86 | echo "Copy complete: $copied_files files (≈ $copied_size) in $DST" 87 | fi 88 | 89 | 90 | # echo "Building XZG-MT Go Bridge v$VERSION" 91 | # echo "==================================" 92 | 93 | 94 | 95 | # Build for each target 96 | for target in "${TARGETS[@]}"; do 97 | IFS='/' read -r os arch <<< "$target" 98 | 99 | echo "Building for $os/$arch..." 100 | 101 | # Set output filename 102 | output_name="XZG-MT-${os}-${arch}" 103 | if [ "$os" = "windows" ]; then 104 | output_name="${output_name}.exe" 105 | fi 106 | 107 | # Set GOOS and GOARCH 108 | export GOOS=$os 109 | export GOARCH=$arch 110 | 111 | # Disable CGO for all builds to create static binaries 112 | export CGO_ENABLED=0 113 | 114 | # Set GOMIPS for MIPS architectures to ensure compatibility with older processors like MT7688 115 | if [ "$arch" = "mips" ] || [ "$arch" = "mipsle" ]; then 116 | export GOMIPS=softfloat 117 | echo " → Using GOMIPS=softfloat for MIPS compatibility" 118 | elif [ "$arch" = "arm" ]; then 119 | export GOARM=7 # ARMv7 with VFPv3 support (most common) 120 | echo " → Using GOARM=7 for ARM compatibility" 121 | elif [ "$arch" = "386" ]; then 122 | export GO386=sse2 # Use SSE2 instructions for better compatibility 123 | echo " → Using GO386=sse2 for i386 compatibility" 124 | else 125 | unset GOMIPS 126 | unset GOARM 127 | unset GO386 128 | fi 129 | 130 | # Common ldflags to strip debug and symbol tables. 131 | LDFLAGS="-s -w -X main.VERSION=$VERSION" 132 | 133 | # Additional flags for smaller builds and reproducibility 134 | # -trimpath removes file system paths from the binary (supported on modern Go versions) 135 | # Keep flags minimal and portable: avoid -buildvcs (not available in older toolchains) 136 | # GC/ASM flags to trim paths as well 137 | GCFLAGS="all=-trimpath=$(pwd)" 138 | ASMFLAGS="all=-trimpath=$(pwd)" 139 | 140 | # Build the binary 141 | echo " → go build (GOOS=$GOOS GOARCH=$GOARCH)" 142 | go build -trimpath -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -asmflags "$ASMFLAGS" -o "$BUILD_DIR/$output_name" . 143 | 144 | # Get file size before optional compression 145 | size_before=$(du -h "$BUILD_DIR/$output_name" | cut -f1) 146 | echo " ✓ Built $output_name (before compression: $size_before)" 147 | 148 | # Optional: compress the executable with upx (if requested and available) 149 | if [ "$USE_UPX" = "1" ] && command -v upx >/dev/null 2>&1; then 150 | echo -n " → compressing with upx... " 151 | # use maximum compression; skip if upx fails 152 | if upx --best --lzma -- "$BUILD_DIR/$output_name" >/dev/null 2>&1; then 153 | size_after=$(du -h "$BUILD_DIR/$output_name" | cut -f1) 154 | echo "done (after: $size_after)" 155 | else 156 | echo "failed (upx returned non-zero)" 157 | fi 158 | fi 159 | done 160 | 161 | echo "" 162 | echo "Build completed! Binaries are in the $BUILD_DIR directory:" 163 | ls -la $BUILD_DIR/ 164 | -------------------------------------------------------------------------------- /web-page/src/transport/serial.ts: -------------------------------------------------------------------------------- 1 | type NativeSerialPort = globalThis.SerialPort; 2 | 3 | export class SerialPort { 4 | private port: NativeSerialPort | null = null; 5 | private reader: ReadableStreamDefaultReader | null = null; 6 | private writer: WritableStreamDefaultWriter | null = null; 7 | private onDataCbs: Array<(data: Uint8Array) => void> = []; 8 | private onTxCb: ((data: Uint8Array) => void) | null = null; 9 | private readonly bitrate: number; 10 | 11 | constructor(bitrate: number) { 12 | this.bitrate = bitrate; 13 | } 14 | 15 | static isSupported(): boolean { 16 | return typeof navigator !== "undefined" && !!navigator.serial; 17 | } 18 | 19 | private startIO(): void { 20 | if (!this.port) return; 21 | // Start read loop 22 | const readable = this.port.readable; 23 | if (readable) { 24 | this.reader = readable.getReader(); 25 | (async () => { 26 | try { 27 | while (true) { 28 | const r = this.reader; 29 | if (!r) break; 30 | const { value, done } = await r.read(); 31 | if (done) break; 32 | if (value) { 33 | for (const cb of this.onDataCbs) { 34 | try { 35 | cb(value); 36 | } catch { 37 | // ignore 38 | } 39 | } 40 | } 41 | } 42 | } catch { 43 | // reader canceled/closed 44 | } 45 | })(); 46 | } 47 | const writable = this.port.writable; 48 | if (writable) this.writer = writable.getWriter(); 49 | } 50 | 51 | async requestAndOpen(): Promise { 52 | // Must be called from a user gesture (click) to show chooser 53 | const port = await navigator.serial.requestPort(); 54 | await port.open({ baudRate: this.bitrate }); 55 | this.port = port; 56 | this.startIO(); 57 | } 58 | 59 | async openGranted(): Promise { 60 | const ports = await navigator.serial.getPorts?.(); 61 | if (!ports || ports.length === 0) throw new Error("No previously granted serial ports"); 62 | const port = ports[0]; 63 | await port.open({ baudRate: this.bitrate }); 64 | this.port = port; 65 | this.startIO(); 66 | } 67 | 68 | useExistingPortAndStart(port: NativeSerialPort): void { 69 | this.port = port; 70 | this.startIO(); 71 | } 72 | 73 | // async reopenWithBaudrate(baud: number): Promise { 74 | // const p: any = this.port as any; 75 | // if (!p) throw new Error("serial not open"); 76 | // // Tear down current IO and re-open at new baudrate 77 | // try { await this.reader?.cancel(); } catch {} 78 | // try { await this.writer?.close(); } catch {} 79 | // try { await p.close?.(); } catch {} 80 | // await p.open?.({ baudRate: baud }); 81 | // this.startIO(); 82 | // } 83 | 84 | async reopenWithBaudrate(baud: number): Promise { 85 | const p = this.port; 86 | if (!p) throw new Error("serial not open"); 87 | // Tear down current IO and re-open at new baudrate 88 | try { 89 | if (this.reader) { 90 | try { 91 | await this.reader.cancel(); 92 | } catch { 93 | // ignore 94 | } 95 | try { 96 | this.reader.releaseLock?.(); 97 | } catch { 98 | // ignore 99 | } 100 | this.reader = null; 101 | } 102 | if (this.writer) { 103 | try { 104 | await this.writer.close(); 105 | } catch { 106 | // ignore 107 | } 108 | try { 109 | this.writer.releaseLock?.(); 110 | } catch { 111 | // ignore 112 | } 113 | this.writer = null; 114 | } 115 | try { 116 | await p.close(); 117 | } catch { 118 | // ignore 119 | } 120 | await p.open({ baudRate: baud }); 121 | this.startIO(); 122 | } catch (err) { 123 | // leave object in consistent state on error 124 | this.reader = null; 125 | this.writer = null; 126 | throw err; 127 | } 128 | } 129 | 130 | // async openByPath(_path?: string): Promise { 131 | // // Backwards-compat: behave like requestAndOpen; web-serial has no system path 132 | // await this.requestAndOpen(); 133 | // } 134 | 135 | async write(data: Uint8Array): Promise { 136 | if (!this.writer) throw new Error("serial not open"); 137 | try { 138 | this.onTxCb?.(data); 139 | } catch { 140 | // ignore 141 | } 142 | await this.writer.write(data); 143 | } 144 | 145 | async setSignals(signals: SerialSignals): Promise { 146 | const p = this.port; 147 | if (!p || !p.setSignals) return; // not supported on some platforms 148 | await p.setSignals(signals); 149 | } 150 | 151 | onData(cb: (data: Uint8Array) => void) { 152 | this.onDataCbs.push(cb); 153 | } 154 | 155 | offData(cb: (data: Uint8Array) => void) { 156 | const idx = this.onDataCbs.indexOf(cb); 157 | if (idx >= 0) { 158 | this.onDataCbs.splice(idx, 1); 159 | } 160 | } 161 | 162 | onTx(cb: (data: Uint8Array) => void) { 163 | this.onTxCb = cb; 164 | } 165 | 166 | async close() { 167 | try { 168 | await this.reader?.cancel(); 169 | } catch { 170 | // ignore 171 | } 172 | try { 173 | await this.writer?.close(); 174 | } catch { 175 | // ignore 176 | } 177 | try { 178 | await this.port?.close(); 179 | } catch { 180 | // ignore 181 | } 182 | this.reader = null; 183 | this.writer = null; 184 | this.port = null; 185 | this.onDataCbs = []; 186 | this.onTxCb = null; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /web-page/README.md: -------------------------------------------------------------------------------- 1 | # Web Page - Developer Documentation 2 | 3 | This directory contains the web frontend for XZG Multi-tool - a TypeScript-based web application for flashing various devices. 4 | 5 | ## 📋 Requirements 6 | 7 | - Node.js >= 20.18.0 8 | - npm (comes with Node.js) 9 | 10 | ## ⚙️ Development Setup 11 | 12 | 1. **Install dependencies:** 13 | 14 | ```bash 15 | npm install 16 | ``` 17 | 18 | 2. **Start development server:** 19 | 20 | ```bash 21 | npm run dev 22 | ``` 23 | 24 | This will: 25 | 26 | - Build TypeScript files with watch mode 27 | - Watch HTML/CSS/JS files for changes 28 | - Generate favicons when settings change 29 | - Start a local development server with live reload 30 | 31 | 3. **Access the development server:** 32 | Open http://localhost:3000 in your browser 33 | 34 | ## 🏗️ Build Commands 35 | 36 | ### Full Production Build 37 | 38 | ```bash 39 | npm run build 40 | ``` 41 | 42 | This creates a complete production build with: 43 | 44 | - TypeScript compilation and bundling 45 | - Static file copying 46 | - Commit hash injection 47 | - Favicon generation and injection 48 | - Assets copying to bridge/web/ 49 | 50 | ### Lite Build (Development) 51 | 52 | ```bash 53 | npm run build:lite 54 | ``` 55 | 56 | Creates a minimal build without favicons and commit injection. 57 | 58 | ### Individual Build Steps 59 | 60 | - `npm run clean` - Remove dist directory 61 | - `npm run copy:static` - Copy HTML, CSS, JS files 62 | - `npm run fav:gen` - Generate favicons from logo.svg 63 | - `npm run fav:inject` - Inject favicon tags into HTML 64 | - `npm run inject:commit` - Inject current git commit hash 65 | 66 | ## 🔄 Development Workflow 67 | 68 | ### Making Changes 69 | 70 | 1. **TypeScript files:** Edit files in `src/` directory 71 | 2. **Styles:** Modify `src/style.css` 72 | 3. **HTML:** Update `src/index.html` 73 | 4. **Types:** Add/modify types in `src/types/` 74 | 75 | ### Testing Changes 76 | 77 | 1. The development server automatically reloads on changes 78 | 2. Check browser console for TypeScript errors 79 | 3. Test with both Web Serial and TCP transport modes 80 | 81 | ### Code Quality 82 | 83 | ```bash 84 | # Type checking 85 | npm run typecheck 86 | 87 | # Linting 88 | npm run lint 89 | ``` 90 | 91 | ## 🏛️ Architecture Overview 92 | 93 | ### Main Components 94 | 95 | - **flasher.ts** - Main application entry point and logic 96 | - **ui.ts** - UI elements and event handling 97 | - **netfw.ts** - Network firmware fetching and parsing 98 | - **tools/** - Device specific protocols 99 | - **ti.ts** - TI CC device communication protocol 100 | - **sl.ts** - Silabs device communication protocol 101 | - **cc-debugger.ts** - CC Debugger communication protocol 102 | - **cc-loader.ts** - CC Loader communication protocol 103 | - **transport/** - Abstracted transport layer for serial/TCP communication 104 | - **utils/** - Shared utility functions 105 | 106 | ### Transport Layer 107 | 108 | The application supports two transport modes: 109 | 110 | - **Web Serial** (`transport/serial.ts`) - Direct USB connection via Web Serial API 111 | - **TCP/WebSocket** (`transport/tcp.ts`) - Remote connection via bridge 112 | 113 | ### Build System 114 | 115 | - **esbuild** - Fast TypeScript bundler 116 | - **realfavicon** - Favicon generation 117 | - **browser-sync** - Development server with live reload 118 | - **concurrently** - Run multiple build processes 119 | 120 | ## 🚀 Deployment 121 | 122 | ### Static Hosting 123 | 124 | The built files in `dist/` can be served by any static web server. The application requires HTTPS for Web Serial API functionality. 125 | 126 | ### Integration with Bridge 127 | 128 | The `npm run copy:ready` command copies built files to `../bridge/web/` for embedding in the Go bridge binary. 129 | 130 | ## 🤝 Contributing Guidelines 131 | 132 | ### Code Style 133 | 134 | - Follow existing TypeScript conventions 135 | - Use meaningful variable and function names 136 | - Add JSDoc comments for public APIs 137 | - Keep functions focused and small 138 | 139 | ### Commit Messages 140 | 141 | Use conventional commit format: 142 | 143 | ``` 144 | feat: add new firmware upload feature 145 | fix: resolve serial connection timeout 146 | docs: update API documentation 147 | ``` 148 | 149 | ### Testing 150 | 151 | Before submitting changes: 152 | 153 | 1. Run `npm run typecheck` - ensure no TypeScript errors 154 | 2. Run `npm run lint` - check code style 155 | 3. Test functionality in multiple browsers 156 | 4. Verify both transport modes work 157 | 158 | ### Adding New Features 159 | 160 | 1. Create feature branch from `main` 161 | 2. Implement changes with proper typing 162 | 3. Update documentation if needed 163 | 4. Test thoroughly 164 | 5. Submit pull request 165 | 166 | ## 🛠️ Troubleshooting 167 | 168 | ### Development Issues 169 | 170 | **Build fails with TypeScript errors:** 171 | 172 | - Run `npm run typecheck` to see detailed errors 173 | - Ensure all imports have proper type definitions 174 | 175 | **Development server not updating:** 176 | 177 | - Check if files are being watched correctly 178 | - Restart the dev server: `npm run dev` 179 | 180 | **Favicon not updating:** 181 | 182 | - Modify `favicon/favicon-settings.json` 183 | - Run `npm run fav:gen && npm run fav:inject` 184 | 185 | ### Browser Compatibility 186 | 187 | - Web Serial API requires Chrome/Edge (not Firefox/Safari) 188 | - HTTPS is required for Web Serial functionality 189 | - Modern ES2020 features are used 190 | 191 | ### Performance Considerations 192 | 193 | - Large firmware files are handled in chunks 194 | - Progress callbacks prevent UI freezing 195 | - Transport layer handles connection timeouts 196 | 197 | --- 198 | 199 | For more information about the overall project, see the [main README](../README.md). 200 | -------------------------------------------------------------------------------- /web-page/src/utils/control.ts: -------------------------------------------------------------------------------- 1 | // Control configuration and helpers extracted from flasher.ts 2 | 3 | import { log } from "../ui"; 4 | import { setLines } from "../flasher"; 5 | import { sleep } from "../utils/index"; 6 | 7 | export type ControlConfig = { 8 | pinMode?: boolean; 9 | bslValue?: string; 10 | rstValue?: string; 11 | baudValue?: string; 12 | invertLevel?: boolean; 13 | }; 14 | 15 | export const CONTROL_PRESETS: Array<{ 16 | name: string; 17 | test: (meta: { type?: string; protocol?: string }) => boolean; 18 | config: ControlConfig; 19 | }> = [ 20 | { 21 | name: "ZigStar/XZG HTTP", 22 | test: (m) => /^(zigstar_gw|zig_star_gw|uzg-01|xzg)$/i.test(m.type || ""), 23 | config: { 24 | pinMode: true, 25 | bslValue: "url:cmdZigBSL", 26 | rstValue: "url:cmdZigRST", 27 | }, 28 | }, 29 | { 30 | name: "TubesZB HTTP (ESPHome)", 31 | test: (m) => /^(tubeszb|tubes_zb)$/i.test(m.type || ""), 32 | config: { 33 | pinMode: false, 34 | bslValue: "url:switch/zBSL", 35 | rstValue: "url:switch/zRST_gpio", 36 | }, 37 | }, 38 | { 39 | name: "Local USB via Bridge", 40 | test: (m) => (m.type || "").toLowerCase() === "local" && (m.protocol || "").toLowerCase() === "usb", 41 | config: { 42 | pinMode: false, 43 | bslValue: "sp:dtr", 44 | rstValue: "sp:rts", 45 | baudValue: "bridge", 46 | }, 47 | }, 48 | { 49 | name: "Local Serial via Bridge", 50 | test: (m) => (m.type || "").toLowerCase() === "local" && (m.protocol || "").toLowerCase() === "serial", 51 | config: { 52 | pinMode: false, 53 | baudValue: "bridge", 54 | }, 55 | }, 56 | ]; 57 | 58 | export function deriveControlConfig(meta: { type?: string; protocol?: string }): ControlConfig { 59 | for (const p of CONTROL_PRESETS) { 60 | try { 61 | if (p.test(meta)) return p.config; 62 | } catch { 63 | // ignore 64 | } 65 | } 66 | return {}; 67 | } 68 | 69 | export async function enterBootloader(implyGate: boolean) { 70 | log("Universal entry bootloader, implyGate=" + implyGate); 71 | 72 | // Assume standard scheme: 73 | // RTS controls RESET (Active Low - 0 resets) 74 | // DTR controls BOOT/GPIO (Active Low - 0 activates bootloader) 75 | 76 | // 1. Initial state: All released (High) 77 | // (RTS=1, DTR=1) -> (false, false) in await setLines logic, if false=High/Inactive 78 | // In await setLines: rstLevel=true -> RTS=1 (High), rstLevel=false -> RTS=0 (Low) 79 | // Usually: true = active level (Low for reset), false = inactive (High) 80 | // But let's check your await setLines logic. 81 | // In flasher.ts: log(`CTRL(tcp): setting RTS=${rst ? "1" : "0"} ...`) 82 | // Usually USB-UART adapters invert signals, but drivers operate with logical levels. 83 | 84 | // Let's try the classic sequence for "bare" UART (without auto-reset scheme like ESP): 85 | 86 | if (!implyGate) { 87 | // Step 0: Make sure everything is at high level (VCC), chip is running 88 | // RTS=0 (High/3.3V), DTR=0 (High/3.3V) 89 | await setLines(false, false); 90 | await sleep(300); 91 | 92 | // Step 1: Press RESET (RTS -> Low/GND) 93 | // Don't touch DTR yet (or keep High) 94 | // RTS=1 (Low/GND), DTR=0 (High/3.3V) 95 | await setLines(true, false); 96 | await sleep(300); 97 | 98 | // Step 2: Press BOOT (DTR -> Low/GND), while RESET is still pressed 99 | // RTS=1 (Low/GND), DTR=1 (Low/GND) 100 | await setLines(true, true); 101 | await sleep(300); 102 | 103 | // Step 3: Release RESET (RTS -> High/3.3V), but keep BOOT pressed! 104 | // Chip wakes up, sees pressed BOOT and enters bootloader. 105 | // RTS=0 (High/3.3V), DTR=1 (Low/GND) 106 | await setLines(false, true); 107 | await sleep(600); // Give time for bootloader to initialize 108 | 109 | // Step 4: Release BOOT (DTR -> High/3.3V) 110 | // RTS=0 (High/3.3V), DTR=0 (High/3.3V) 111 | await setLines(false, false); 112 | await sleep(300); 113 | } 114 | 115 | // Logic for scheme with two transistors: 116 | 117 | // Truth table for such scheme: 118 | // DTR=0, RTS=0 -> Idle (VCC, VCC) 119 | // DTR=0, RTS=1 -> Reset (VCC, GND) -> CHIP IN RESET 120 | // DTR=1, RTS=0 -> Boot (GND, VCC) -> CHIP IN BOOT MODE 121 | // DTR=1, RTS=1 -> Idle (VCC, VCC) -> Protection from simultaneous pressing 122 | if (implyGate) { 123 | // 1. Initial state (Idle) 124 | await setLines(false, false); 125 | await sleep(300); 126 | 127 | // 2. Press RESET (RTS=True, DTR=False) 128 | // Chip stops 129 | await setLines(true, false); 130 | await sleep(300); 131 | 132 | // 3. Switch to BOOT mode (RTS=False, DTR=True) 133 | // At this moment Reset is released (becomes High), and Boot is pressed to ground (Low). 134 | // Chip starts, sees low level on Boot pin and enters bootloader. 135 | await setLines(false, true); 136 | await sleep(600); // Give time for bootloader to initialize 137 | 138 | // 4. Release everything (Idle) 139 | // Boot pin returns to VCC 140 | await setLines(false, false); 141 | await sleep(300); 142 | } 143 | await sleep(1000); 144 | } 145 | 146 | export async function makeReset(implyGate: boolean) { 147 | log("Universal reset, implyGate=" + implyGate); 148 | 149 | if (!implyGate) { 150 | // Just pull Reset 151 | await setLines(false, false); // Release Reset 152 | await sleep(300); 153 | await setLines(true, false); // Press Reset 154 | await sleep(300); 155 | await setLines(false, false); // Release Reset 156 | await sleep(300); 157 | } 158 | 159 | if (implyGate) { 160 | // Simple reset for transistor scheme 161 | // 1. Idle 162 | await setLines(false, false); 163 | await sleep(300); 164 | 165 | // 2. Reset (RTS=True, DTR=False) 166 | await setLines(true, false); 167 | await sleep(300); 168 | 169 | // 3. Back to Idle 170 | await setLines(false, false); 171 | await sleep(300); 172 | } 173 | 174 | await sleep(1000); 175 | } 176 | -------------------------------------------------------------------------------- /bridge/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= 2 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 3 | github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= 4 | github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 8 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 9 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 10 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 11 | github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= 12 | github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= 13 | github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= 14 | github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= 15 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 16 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 17 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 18 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 19 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 20 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 21 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 22 | github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 23 | github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= 24 | github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= 25 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 26 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 30 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 31 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 32 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 33 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 34 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 35 | go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= 36 | go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= 37 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 38 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 39 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 40 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 41 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 42 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 43 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 44 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 45 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 46 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 47 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 48 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 49 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 51 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 52 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 53 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 60 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 61 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 62 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 63 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 64 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 65 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 66 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 67 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 68 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 69 | golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 70 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 71 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 72 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 73 | -------------------------------------------------------------------------------- /docs/how-to/telink.md: -------------------------------------------------------------------------------- 1 | # How to Flash Telink TLSR825x/TLSR826x Chips 2 | 3 | ## 📖 Introduction 4 | 5 | Telink TLSR825x and TLSR826x are System-on-Chip (SoC) solutions commonly used in Zigbee, Bluetooth Low Energy (BLE), and smart home devices. This guide covers two methods for flashing firmware onto Telink chips using XZG-MT. 6 | 7 | ## 🔌 Connection Methods 8 | 9 | XZG-MT supports two connection methods for programming Telink chips: 10 | 11 | ### Method 1: UART SWire Emulation (TlsrComProg) 12 | 13 | This method emulates SWire protocol over a standard UART interface using a special connection scheme. It uses direct SWire access through UART TX/RX pins connected in a specific way. 14 | 15 | This method is especially convenient for development boards that already have a built-in USB-to-UART converter, such as **TB-03F-KIT** or **TB-04-KIT**. 16 | 17 |
18 | 19 | 20 | 21 | 22 | UART SWire Emulation Connection Diagram 23 | 24 | 25 |
26 | 27 | **Requirements:** 28 | 29 | - USB-to-UART adapter (CH340 or PL2303HX) or development board with built-in converter (TB-03F-KIT, TB-04-KIT) 30 | - Target Telink device 31 | - Wires connected according to the diagram (TX, RX tied together to SWS) 32 | 33 | **Pros:** 34 | 35 | - Simple hardware — uses common USB-to-UART adapters 36 | - Ideal for boards with built-in USB-UART (TB-03F-KIT, TB-04-KIT) 37 | - Can be used to create a UART2SWire programmer from these boards 38 | 39 | ### Method 2: UART2SWire Programmer (TlsrPgm) 40 | 41 | This method uses a dedicated UART2SWire programmer board that converts UART commands to native SWire protocol. The programmer can be created from **TB-03F-KIT** or **TB-04-KIT** boards using XZG-MT (see above). 42 | 43 |
44 | 45 | 46 | 47 | 48 | UART2SWire Programmer Connection Diagram 49 | 50 | 51 |
52 | 53 | **Requirements:** 54 | 55 | - UART2SWire programmer (created from TB-03F-KIT or TB-04-KIT) 56 | - Target Telink device 57 | - Wires for SWS (SWire), RST, GND, and VCC connections 58 | 59 | **Pros:** 60 | 61 | - Universal — automatically detects chip family (no need to select 825X/826X) 62 | - Native SWire protocol — more reliable communication 63 | - Can program any Telink chip 64 | 65 | ## ⚡ Flashing Procedure 66 | 67 | ### Using UART Method 68 | 69 | 1. **Prepare Your Setup**: 70 | 71 | - Connect USB-to-UART adapter to your computer (or use built-in converter on TB-03F-KIT/TB-04-KIT) 72 | - Wire the connections according to the diagram above 73 | - The key is connecting TX and RX together to the chip's SWS pin 74 | 75 | 2. **Open XZG-MT**: 76 | 77 | - Open [XZG-MT](https://mt.xyzroe.cc) in your browser 78 | 79 | 3. **Select Chip Family**: 80 | 81 | - In XZG-MT, select `Telink` in the Family section 82 | 83 | 4. **Configure Connection**: 84 | 85 | - Set Method to `UART` 86 | - Select chip Family: `825X` or `826X` (must match your target chip) 87 | 88 | 5. **Connect to Device**: 89 | 90 | - Click `Choose Serial` button 91 | - Select your USB-to-UART adapter from the list 92 | - XZG-MT will detect the chip and display its information 93 | 94 | 6. **Load and Flash Firmware**: 95 | - Select the firmware file (`.bin`) you want to flash 96 | - Configure flash options (Erase, Verify) 97 | - Click `Start` to begin flashing 98 | - Wait for the process to complete 99 | 100 | #### Creating a UART2SWire Programmer 101 | 102 | You can create a UART2SWire programmer from a **TB-03F-KIT** or **TB-04-KIT** board using UART method: 103 | 104 | 1. Connect the TB-03F-KIT or TB-04-KIT to your computer via USB 105 | 2. Open [XZG-MT](https://mt.xyzroe.cc) and select `Telink` family 106 | 3. Set Method to `UART` and select chip family `825X` 107 | 4. Connect to the board using `Choose Serial` 108 | 5. In the `Actions` section, click `Flash uart2swire` button 109 | 6. Wait for the firmware to be flashed 110 | 7. Your board is now a UART2SWire programmer! 111 | 112 | ### Using SWire Method 113 | 114 | 1. **Prepare Your Setup**: 115 | 116 | - Connect UART2SWire programmer to your computer via USB 117 | - Connect programmer's SWS output to target chip's SWS pin 118 | - Connect RST, GND, and VCC appropriately 119 | - Power on the target device 120 | 121 | 2. **Open XZG-MT**: 122 | 123 | - Open [XZG-MT](https://mt.xyzroe.cc) in your browser 124 | 125 | 3. **Select Chip Family**: 126 | 127 | - In XZG-MT, select `Telink` in the Family section 128 | 129 | 4. **Configure Connection**: 130 | 131 | - Set Method to `Swire` 132 | - No need to select chip family — it will be detected automatically 133 | 134 | 5. **Connect to Programmer**: 135 | 136 | - Click `Choose Serial` button 137 | - Select your UART2SWire programmer from the list 138 | - XZG-MT will communicate with the programmer and detect the target chip 139 | 140 | 6. **Load and Flash Firmware**: 141 | - Select the firmware file (`.bin`) you want to flash 142 | - Configure flash options (Erase, Verify) 143 | - Click `Start` to begin flashing 144 | - Wait for the process to complete 145 | 146 | ## 💾 Dump Flash to a File 147 | 148 | XZG-MT allows reading the device's flash memory and saving it to a local file. 149 | 150 | 1. Connect to the device using either method described above 151 | 2. Click on `Dump flash` button in the `Actions` section 152 | 3. Wait until all data is read. Progress is displayed in the interface 153 | 4. Save the file to your computer 154 | 155 | ## 🛠️ Troubleshooting 156 | 157 | ### UART Method Issues 158 | 159 | - **Device Not Detected**: 160 | 161 | - Verify the wiring matches the diagram (TX and RX should be connected together to SWS) 162 | - Ensure you selected the correct chip family (825X or 826X) 163 | 164 | - **Communication Errors**: 165 | - Ensure proper GND connection 166 | - Check for loose wires 167 | 168 | ### SWire Method Issues 169 | 170 | - **Programmer Not Responding**: 171 | 172 | - Verify the programmer was flashed correctly 173 | - Try reconnecting the USB 174 | - Check that the programmer has power 175 | 176 | - **Target Chip Not Detected**: 177 | - Verify SWS pin connection 178 | - Check RST connection 179 | - Ensure target chip is powered 180 | - Check GND connection between programmer and target 181 | 182 | ### General Issues 183 | 184 | - **Flashing Errors**: Verify the firmware file is compatible with your chip model 185 | - **Verification Failed**: Try erasing the chip first, then flash again 186 | - **Bricked Device**: Use SWire method with UART2SWire programmer to recover 187 | 188 | ## 🆘 If the Problem Persists 189 | 190 | If the problem persists after trying the troubleshooting steps, please open an issue on the [XZG-MT GitHub repository](https://github.com/xyzroe/XZG-MT/issues). Provide detailed information about your setup, operating system, error messages, and steps you've taken. 191 | -------------------------------------------------------------------------------- /bridge/mdns.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sort" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/grandcat/zeroconf" 13 | ) 14 | 15 | type ServiceType struct { 16 | Type string 17 | Protocol string 18 | } 19 | 20 | type ServiceInfo struct { 21 | Name string `json:"name"` 22 | Host string `json:"host"` 23 | Port int `json:"port"` 24 | Type string `json:"type"` 25 | Protocol string `json:"protocol"` 26 | FQDN string `json:"fqdn"` 27 | TXT map[string]string `json:"txt"` 28 | } 29 | 30 | func parseServiceType(full string) *ServiceType { 31 | full = strings.ToLower(full) 32 | 33 | // Special tokens for local serial 34 | if isLocalSerialToken(full) { 35 | return &ServiceType{Type: "local", Protocol: "serial"} 36 | } 37 | 38 | // Parse mDNS service type format: _service._tcp.local 39 | if strings.HasPrefix(full, "_") && strings.Contains(full, "._tcp") { 40 | parts := strings.Split(full, ".") 41 | if len(parts) >= 2 { 42 | serviceType := strings.TrimPrefix(parts[0], "_") 43 | return &ServiceType{Type: serviceType, Protocol: "tcp"} 44 | } 45 | } 46 | 47 | if strings.HasPrefix(full, "_") && strings.Contains(full, "._udp") { 48 | parts := strings.Split(full, ".") 49 | if len(parts) >= 2 { 50 | serviceType := strings.TrimPrefix(parts[0], "_") 51 | return &ServiceType{Type: serviceType, Protocol: "udp"} 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func isLocalSerialToken(s string) bool { 59 | localTokens := []string{"local.serial", "local:serial", "local-serial", "local"} 60 | s = strings.ToLower(s) 61 | for _, token := range localTokens { 62 | if s == token { 63 | return true 64 | } 65 | } 66 | return false 67 | } 68 | 69 | func scanMdns(typeList []ServiceType, timeoutMs int) []ServiceInfo { 70 | 71 | var results []ServiceInfo 72 | foundDevices := make(map[string]ServiceInfo) 73 | var mu sync.Mutex 74 | // includeLocal := false 75 | 76 | // // Check whether to include local devices 77 | // for _, serviceType := range typeList { 78 | // if serviceType.Protocol == "serial" || serviceType.Type == "local" { 79 | // includeLocal = true 80 | // break 81 | // } 82 | // } 83 | 84 | // Create a WaitGroup to synchronize goroutines 85 | var wg sync.WaitGroup 86 | 87 | //Print one message before starting scan with all requested types 88 | if len(typeList) > 0 { 89 | var typeNames []string 90 | for _, t := range typeList { 91 | typeNames = append(typeNames, fmt.Sprintf("%s.%s", t.Type, t.Protocol)) 92 | } 93 | log.Printf("[mdns] scanning for: %s with timeout %d ms\n", strings.Join(typeNames, ", "), timeoutMs) 94 | } else { 95 | log.Printf("[mdns] no valid services requested for scan\n") 96 | return results 97 | } 98 | 99 | // Start a search for each service type 100 | for _, serviceType := range typeList { 101 | // Skip non-network services 102 | if serviceType.Protocol != "tcp" && serviceType.Protocol != "udp" { 103 | continue 104 | } 105 | 106 | wg.Add(1) 107 | go func(st ServiceType) { 108 | defer wg.Done() 109 | 110 | serviceName := fmt.Sprintf("_%s._%s", st.Type, st.Protocol) 111 | 112 | // Create a new resolver for each service 113 | resolver, err := zeroconf.NewResolver(nil) 114 | if err != nil { 115 | log.Printf("[mdns] failed to create resolver for %s: %v\n", serviceName, err) 116 | return 117 | } 118 | 119 | // Create a context with timeout for this specific service 120 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond) 121 | defer cancel() 122 | 123 | // Channel to receive discovered devices 124 | entries := make(chan *zeroconf.ServiceEntry, 10) 125 | 126 | // Run Browse in a separate goroutine 127 | go func() { 128 | defer func() { 129 | if r := recover(); r != nil { 130 | // Ignore panic from the zeroconf library 131 | log.Printf("[mdns] recovered from panic in %s: %v\n", serviceName, r) 132 | } 133 | }() 134 | 135 | err := resolver.Browse(ctx, serviceName, "local.", entries) 136 | if err != nil && err != context.Canceled && err != context.DeadlineExceeded { 137 | log.Printf("[mdns] browse error for %s: %v\n", serviceName, err) 138 | } 139 | }() 140 | 141 | // Process discovered devices 142 | for { 143 | select { 144 | case entry, ok := <-entries: 145 | if !ok { 146 | return 147 | } 148 | 149 | mu.Lock() 150 | 151 | // Determine host 152 | host := "" 153 | if len(entry.AddrIPv4) > 0 { 154 | host = entry.AddrIPv4[0].String() 155 | } else if len(entry.AddrIPv6) > 0 { 156 | host = entry.AddrIPv6[0].String() 157 | } else if entry.HostName != "" { 158 | host = entry.HostName 159 | } else { 160 | host = getAdvertiseHost() 161 | } 162 | 163 | key := fmt.Sprintf("%s|%s|%d", entry.Instance, host, entry.Port) 164 | 165 | // Avoid duplication 166 | if _, exists := foundDevices[key]; !exists { 167 | // Parse TXT records 168 | txtMap := make(map[string]string) 169 | for _, txt := range entry.Text { 170 | parts := strings.SplitN(txt, "=", 2) 171 | if len(parts) == 2 { 172 | txtMap[parts[0]] = parts[1] 173 | } 174 | } 175 | 176 | service := ServiceInfo{ 177 | Name: entry.Instance, 178 | Host: host, 179 | Port: entry.Port, 180 | Type: st.Type, 181 | Protocol: st.Protocol, 182 | FQDN: entry.Instance, 183 | TXT: txtMap, 184 | } 185 | 186 | foundDevices[key] = service 187 | log.Printf("[mdns] found: %s on %s:%d (%s, %s)\n", st.Type, host, entry.Port, txtMap["board"], txtMap["serial_number"]) 188 | } 189 | 190 | mu.Unlock() 191 | 192 | case <-ctx.Done(): 193 | return 194 | } 195 | } 196 | }(serviceType) 197 | } 198 | 199 | // Wait for all goroutines to finish or for a global timeout 200 | done := make(chan bool) 201 | go func() { 202 | wg.Wait() 203 | done <- true 204 | }() 205 | 206 | select { 207 | case <-done: 208 | // All goroutines finished 209 | case <-time.After(time.Duration(timeoutMs) * time.Millisecond): 210 | // Global timeout 211 | if debugMode { 212 | log.Printf("[mdns] scan timeout reached\n") 213 | } 214 | } 215 | 216 | // Collect results 217 | mu.Lock() 218 | for _, service := range foundDevices { 219 | results = append(results, service) 220 | } 221 | mu.Unlock() 222 | 223 | // Sort mDNS results deterministically by name then host:port 224 | sort.Slice(results, func(i, j int) bool { 225 | if results[i].Name != results[j].Name { 226 | return results[i].Name < results[j].Name 227 | } 228 | if results[i].Host != results[j].Host { 229 | return results[i].Host < results[j].Host 230 | } 231 | return results[i].Port < results[j].Port 232 | }) 233 | 234 | // If requested, add local serial ports as services 235 | // if includeLocal { 236 | // local := listLocalSerialAsServices() 237 | // if len(local) > 0 { 238 | // log.Printf("[mdns] adding %d local serial services\n", len(local)) 239 | // results = append(results, local...) 240 | // } 241 | // } 242 | 243 | log.Printf("[mdns] scan done, found %d\n", len(results)) 244 | return results 245 | } 246 | 247 | func listLocalSerialAsServices() []ServiceInfo { 248 | var services []ServiceInfo 249 | hostIP := getAdvertiseHost() 250 | // Collect keys then iterate in sorted order to ensure deterministic output 251 | serialMutex.RLock() 252 | keys := make([]string, 0, len(serialServers)) 253 | for pathName := range serialServers { 254 | keys = append(keys, pathName) 255 | } 256 | serialMutex.RUnlock() 257 | 258 | sort.Strings(keys) 259 | 260 | serialMutex.RLock() 261 | for _, pathName := range keys { 262 | info := serialServers[pathName] 263 | details := serialPortDetails[pathName] 264 | 265 | proto := "serial" 266 | if strings.Contains(pathName, "USB") || strings.Contains(pathName, "usb") { 267 | proto = "usb" 268 | } 269 | 270 | service := ServiceInfo{ 271 | Name: pathName, 272 | Host: hostIP, 273 | Port: info.Port, 274 | Type: "local", 275 | Protocol: proto, 276 | FQDN: pathName, 277 | TXT: map[string]string{ 278 | "board": details.Manufacturer, 279 | "serial_number": details.SerialNumber, 280 | "vendor_id": details.VendorID, 281 | "product_id": details.ProductID, 282 | }, 283 | } 284 | services = append(services, service) 285 | } 286 | serialMutex.RUnlock() 287 | 288 | return services 289 | } 290 | -------------------------------------------------------------------------------- /bridge/websocket.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | "strconv" 8 | "sync" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | // minimal net.Addr implementation for wrapper 15 | type wsAddr struct { 16 | network string 17 | addr string 18 | } 19 | 20 | func (a wsAddr) Network() string { return a.network } 21 | func (a wsAddr) String() string { return a.addr } 22 | 23 | // wsNetConn implements net.Conn over a websocket connection (stream-like). 24 | // It pumps incoming WS messages into an io.Pipe for Read(), and implements Write() by 25 | // using NextWriter(websocket.BinaryMessage) for atomic writes. 26 | type wsNetConn struct { 27 | ws *websocket.Conn 28 | pr *io.PipeReader 29 | pw *io.PipeWriter 30 | writeMu sync.Mutex 31 | closeMu sync.Mutex 32 | closed bool 33 | local net.Addr 34 | remote net.Addr 35 | readDLMu sync.Mutex 36 | writeDLM sync.Mutex 37 | } 38 | 39 | func newWsNetConn(ws *websocket.Conn, localAddr, remoteAddr string) *wsNetConn { 40 | pr, pw := io.Pipe() 41 | conn := &wsNetConn{ 42 | ws: ws, 43 | pr: pr, 44 | pw: pw, 45 | local: wsAddr{"ws", localAddr}, 46 | remote: wsAddr{"ws", remoteAddr}, 47 | } 48 | // pump WS -> pipe writer 49 | go func() { 50 | defer pw.Close() 51 | // buffer incoming frame data and emit complete logical packets. 52 | // Special-case for protocol starting with 0x00 0xCC: third byte is length (L), 53 | // total packet size = 3 + L. Adjust/extend rules for other protocols as needed. 54 | buf := make([]byte, 0, 8192) 55 | for { 56 | mt, r, err := ws.NextReader() 57 | if err != nil { 58 | _ = pw.CloseWithError(err) 59 | return 60 | } 61 | if mt != websocket.BinaryMessage && mt != websocket.TextMessage { 62 | continue 63 | } 64 | // read full frame into tmp slice 65 | data, err := io.ReadAll(r) 66 | if err != nil { 67 | _ = pw.CloseWithError(err) 68 | return 69 | } 70 | if len(data) == 0 { 71 | // nothing to append, continue 72 | continue 73 | } 74 | buf = append(buf, data...) 75 | 76 | // try to flush complete logical packets from buffer 77 | for { 78 | if len(buf) == 0 { 79 | break 80 | } 81 | // protocol: 0x00 0xCC 82 | if len(buf) >= 3 && buf[0] == 0x00 && buf[1] == 0xCC { 83 | plen := int(buf[2]) 84 | total := 3 + plen 85 | if len(buf) >= total { 86 | if _, err := pw.Write(buf[:total]); err != nil { 87 | _ = pw.CloseWithError(err) 88 | return 89 | } 90 | // drop emitted bytes 91 | buf = buf[total:] 92 | continue 93 | } 94 | // not enough bytes yet — wait for next frame 95 | break 96 | } 97 | // no known header — flush everything (or could implement other rules) 98 | if len(buf) > 0 { 99 | if _, err := pw.Write(buf); err != nil { 100 | _ = pw.CloseWithError(err) 101 | return 102 | } 103 | buf = buf[:0] 104 | } 105 | } 106 | } 107 | }() 108 | return conn 109 | } 110 | 111 | func (c *wsNetConn) Read(b []byte) (int, error) { 112 | return c.pr.Read(b) 113 | } 114 | 115 | func (c *wsNetConn) Write(b []byte) (int, error) { 116 | c.writeMu.Lock() 117 | defer c.writeMu.Unlock() 118 | 119 | // ensure we don't block forever on NextWriter/Close 120 | _ = c.ws.SetWriteDeadline(time.Now().Add(10 * time.Second)) 121 | 122 | w, err := c.ws.NextWriter(websocket.BinaryMessage) 123 | if err != nil { 124 | // clear deadline on error (best-effort) 125 | _ = c.ws.SetWriteDeadline(time.Time{}) 126 | return 0, err 127 | } 128 | // write full payload 129 | n, err := w.Write(b) 130 | closeErr := w.Close() 131 | // clear deadline after finished 132 | _ = c.ws.SetWriteDeadline(time.Time{}) 133 | if err != nil { 134 | return n, err 135 | } 136 | if closeErr != nil { 137 | return n, closeErr 138 | } 139 | return n, nil 140 | } 141 | 142 | func (c *wsNetConn) Close() error { 143 | c.closeMu.Lock() 144 | defer c.closeMu.Unlock() 145 | if c.closed { 146 | return nil 147 | } 148 | c.closed = true 149 | // close underlying websocket and pipe 150 | _ = c.ws.Close() 151 | // ensure pipe readers are unblocked with an error so io.Copy returns 152 | _ = c.pw.CloseWithError(io.EOF) 153 | _ = c.pr.Close() 154 | return nil 155 | } 156 | 157 | func (c *wsNetConn) LocalAddr() net.Addr { return c.local } 158 | func (c *wsNetConn) RemoteAddr() net.Addr { return c.remote } 159 | 160 | func (c *wsNetConn) SetDeadline(t time.Time) error { 161 | if err := c.SetReadDeadline(t); err != nil { 162 | return err 163 | } 164 | return c.SetWriteDeadline(t) 165 | } 166 | 167 | func (c *wsNetConn) SetReadDeadline(t time.Time) error { 168 | c.readDLMu.Lock() 169 | defer c.readDLMu.Unlock() 170 | return c.ws.SetReadDeadline(t) 171 | } 172 | 173 | func (c *wsNetConn) SetWriteDeadline(t time.Time) error { 174 | c.writeDLM.Lock() 175 | defer c.writeDLM.Unlock() 176 | return c.ws.SetWriteDeadline(t) 177 | } 178 | 179 | func handleWebSocketConnection(ws *websocket.Conn, targetHost string, targetPort int) { 180 | target := net.JoinHostPort(targetHost, strconv.Itoa(targetPort)) 181 | if debugMode { 182 | log.Printf("[websocket] establishing TCP connection to %s\n", target) 183 | } 184 | 185 | // Create TCP connection to target 186 | tcpConn, err := net.Dial("tcp", target) 187 | if err != nil { 188 | log.Printf("[websocket] failed to connect to %s: %v\n", target, err) 189 | _ = ws.Close() 190 | return 191 | } 192 | // ensure cleanup 193 | defer tcpConn.Close() 194 | defer ws.Close() 195 | 196 | // try optimize TCP 197 | if tcp, ok := tcpConn.(*net.TCPConn); ok { 198 | _ = tcp.SetNoDelay(true) 199 | _ = tcp.SetKeepAlive(true) 200 | _ = tcp.SetKeepAlivePeriod(30 * time.Second) 201 | } 202 | 203 | log.Printf("[websocket] TCP connection established to %s\n", target) 204 | 205 | // wrap websocket as net.Conn 206 | wsConn := newWsNetConn(ws, ws.LocalAddr().String(), ws.RemoteAddr().String()) 207 | 208 | // Try to set TCP_NODELAY on websocket underlying TCP conn (reduce buffering) 209 | if u := ws.UnderlyingConn(); u != nil { 210 | if tcpU, ok := u.(*net.TCPConn); ok { 211 | _ = tcpU.SetNoDelay(true) 212 | } 213 | } 214 | 215 | // set reasonable deadlines / ping-pong on websocket side 216 | ws.SetReadLimit(4 * 1024 * 1024) 217 | ws.SetReadDeadline(time.Now().Add(60 * time.Second)) 218 | ws.SetPongHandler(func(string) error { 219 | _ = ws.SetReadDeadline(time.Now().Add(60 * time.Second)) 220 | return nil 221 | }) 222 | 223 | // Start ping routine 224 | pingTicker := time.NewTicker(20 * time.Second) 225 | defer pingTicker.Stop() 226 | go func() { 227 | for range pingTicker.C { 228 | _ = ws.SetWriteDeadline(time.Now().Add(10 * time.Second)) 229 | _ = ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)) 230 | } 231 | }() 232 | 233 | // Bidirectional copy (stream-like) with coalescing for tcp->ws 234 | errCh := make(chan error, 2) 235 | 236 | // ws -> tcp (keep simple: ensure full write loop) 237 | go func() { 238 | _, err := io.Copy(tcpConn, wsConn) 239 | errCh <- err 240 | }() 241 | 242 | // tcp -> ws with small coalescing window 243 | go func() { 244 | readBuf := make([]byte, 4096) 245 | for { 246 | // block until first chunk 247 | n, rerr := tcpConn.Read(readBuf) 248 | if n > 0 { 249 | out := make([]byte, 0, n) 250 | out = append(out, readBuf[:n]...) 251 | // coalesce additional immediately-available bytes with short deadline 252 | _ = tcpConn.SetReadDeadline(time.Now().Add(5 * time.Millisecond)) 253 | for { 254 | m, err2 := tcpConn.Read(readBuf) 255 | if m > 0 { 256 | out = append(out, readBuf[:m]...) 257 | // prevent unbounded growth 258 | if len(out) >= 64*1024 { 259 | break 260 | } 261 | continue 262 | } 263 | if err2 != nil { 264 | // timeout or real error: break to send what's collected 265 | break 266 | } 267 | } 268 | _ = tcpConn.SetReadDeadline(time.Time{}) // clear deadline 269 | 270 | // write as a single websocket frame 271 | _, werr := wsConn.Write(out) 272 | if werr != nil { 273 | errCh <- werr 274 | return 275 | } 276 | } 277 | if rerr != nil { 278 | errCh <- rerr 279 | return 280 | } 281 | } 282 | }() 283 | 284 | // wait for first error/close 285 | err = <-errCh 286 | if err != nil && err != io.EOF { 287 | if debugMode { 288 | log.Printf("[websocket] proxy error: %v\n", err) 289 | } 290 | } 291 | // wake up/blocking ops: set immediate deadlines so blocked reads/writes unblock 292 | _ = tcpConn.SetDeadline(time.Now()) 293 | _ = ws.SetWriteDeadline(time.Now()) 294 | _ = ws.SetReadDeadline(time.Now()) 295 | 296 | // ensure close both sides 297 | _ = tcpConn.Close() 298 | _ = wsConn.Close() 299 | // extra log: if ws returned a close code -- it often appears in error string, print it 300 | // (websocket library returns CloseError in some cases) 301 | if ce, ok := err.(*websocket.CloseError); ok { 302 | log.Printf("[websocket] remote close code=%d text=%s\n", ce.Code, ce.Text) 303 | } 304 | log.Printf("[websocket] connection closing for %s\n", target) 305 | } 306 | -------------------------------------------------------------------------------- /bridge/routes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/gorilla/websocket" 12 | "github.com/labstack/echo/v4" 13 | ) 14 | 15 | func setupRoutes(e *echo.Echo) { 16 | // CORS middleware 17 | e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { 18 | return func(c echo.Context) error { 19 | c.Response().Header().Set("Access-Control-Allow-Origin", "*") 20 | c.Response().Header().Set("Access-Control-Allow-Credentials", "true") 21 | c.Response().Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS") 22 | c.Response().Header().Set("Access-Control-Allow-Headers", "Content-Type,Accept,Origin,X-Requested-With,Authorization") 23 | c.Response().Header().Set("Access-Control-Allow-Private-Network", "true") 24 | c.Response().Header().Set("Access-Control-Max-Age", "86400") 25 | 26 | if c.Request().Method == "OPTIONS" { 27 | return c.NoContent(204) 28 | } 29 | 30 | return next(c) 31 | } 32 | }) 33 | 34 | // WebSocket upgrade handlers 35 | e.GET("/ws", handleWebSocketUpgrade) 36 | e.GET("/connect", handleWebSocketUpgrade) 37 | 38 | // mDNS discovery endpoint 39 | e.GET("/mdns", handleMdnsScan) 40 | 41 | // Serial control endpoint 42 | e.GET("/sc", handleSerialControl) 43 | 44 | // GPIO control endpoint 45 | e.GET("/gpio", handleGpioControl) 46 | 47 | // GPIO list endpoint 48 | e.GET("/gl", handleGpioList) 49 | 50 | // Static file serving 51 | e.GET("/*", handleStaticFiles) 52 | } 53 | 54 | func handleWebSocketUpgrade(c echo.Context) error { 55 | // Get target host and port from query parameters 56 | host := c.QueryParam("host") 57 | portStr := c.QueryParam("port") 58 | 59 | if host == "" || portStr == "" { 60 | return c.String(http.StatusBadRequest, "Missing host or port parameter") 61 | } 62 | 63 | port, err := strconv.Atoi(portStr) 64 | if err != nil { 65 | return c.String(http.StatusBadRequest, "Invalid port parameter") 66 | } 67 | 68 | // Upgrade to WebSocket 69 | upgrader := websocket.Upgrader{ 70 | CheckOrigin: func(r *http.Request) bool { 71 | return true // Allow all origins 72 | }, 73 | } 74 | 75 | ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil) 76 | if err != nil { 77 | return err 78 | } 79 | defer ws.Close() 80 | 81 | // Handle WebSocket connection 82 | handleWebSocketConnection(ws, host, port) 83 | 84 | return nil 85 | } 86 | 87 | func handleMdnsScan(c echo.Context) error { 88 | typesParam := c.QueryParam("types") 89 | timeoutStr := c.QueryParam("timeout") 90 | 91 | timeout := 2000 // default timeout 92 | if timeoutStr != "" { 93 | if t, err := strconv.Atoi(timeoutStr); err == nil { 94 | timeout = t 95 | } 96 | } 97 | 98 | // Ensure timeout is within reasonable bounds 99 | if timeout < 500 { 100 | timeout = 500 101 | } else if timeout > 10000 { 102 | timeout = 10000 103 | } 104 | 105 | types := strings.Split(typesParam, ",") 106 | var normalizedTypes []ServiceType 107 | var wantsLocalSerial bool 108 | 109 | for _, t := range types { 110 | t = strings.TrimSpace(t) 111 | if t == "" { 112 | continue 113 | } 114 | 115 | if isLocalSerialToken(t) { 116 | wantsLocalSerial = true 117 | } else { 118 | if st := parseServiceType(t); st != nil { 119 | normalizedTypes = append(normalizedTypes, *st) 120 | } 121 | } 122 | } 123 | 124 | var results []ServiceInfo 125 | 126 | // Scan mDNS services 127 | if len(normalizedTypes) > 0 { 128 | results = scanMdns(normalizedTypes, timeout) 129 | } 130 | 131 | // Add local serial services 132 | if wantsLocalSerial { 133 | scanAndSyncSerialPorts() 134 | locals := listLocalSerialAsServices() 135 | results = append(results, locals...) 136 | } 137 | 138 | response := map[string]interface{}{ 139 | "devices": results, 140 | } 141 | 142 | return c.JSON(http.StatusOK, response) 143 | } 144 | 145 | func handleGpioControl(c echo.Context) error { 146 | // Handle GPIO control logic here 147 | path := c.QueryParam("path") 148 | setStr := c.QueryParam("set") 149 | 150 | if path == "" || setStr == "" { 151 | return c.JSON(http.StatusBadRequest, map[string]string{ 152 | "error": "Missing path or set parameter", 153 | }) 154 | } 155 | 156 | // Trim surrounding quotes/spaces and clean the path 157 | path = strings.TrimSpace(path) 158 | path = strings.Trim(path, "\"' ") 159 | path = filepath.Clean(path) 160 | 161 | var setValue int 162 | var err error 163 | setValue, err = strconv.Atoi(setStr) 164 | if err != nil { 165 | return c.JSON(http.StatusBadRequest, map[string]string{ 166 | "error": "Invalid set value", 167 | }) 168 | } 169 | if setValue < 0 || setValue > 1 { 170 | return c.JSON(http.StatusBadRequest, map[string]string{ 171 | "error": "Invalid set value", 172 | }) 173 | } 174 | 175 | // Set GPIO state here 176 | err = setGpioState(path, setValue) 177 | 178 | ok := (err == nil) 179 | resp := map[string]interface{}{ 180 | "ok": ok, 181 | "path": path, 182 | "set": setValue, 183 | } 184 | if err != nil { 185 | resp["error"] = err.Error() 186 | } 187 | 188 | return c.JSON(http.StatusOK, resp) 189 | } 190 | 191 | func handleGpioList(c echo.Context) error { 192 | // Return only already-exported GPIOs and LEDs in compact format 193 | type SimpleEntry struct { 194 | Path string `json:"path"` 195 | Label string `json:"label"` 196 | Value string `json:"value"` 197 | } 198 | var gpioOut []SimpleEntry 199 | entries, _ := os.ReadDir("/sys/class/gpio") 200 | for _, entry := range entries { 201 | name := entry.Name() 202 | // skip gpiochip* and export/unexport entries 203 | if strings.HasPrefix(name, "gpiochip") || name == "export" || name == "unexport" { 204 | continue 205 | } 206 | valPath := filepath.Join("/sys/class/gpio", name, "value") 207 | val := "" 208 | if b, err := os.ReadFile(valPath); err == nil { 209 | val = strings.TrimSpace(string(b)) 210 | } 211 | label := name 212 | gpioOut = append(gpioOut, SimpleEntry{Path: valPath, Label: label, Value: val}) 213 | } 214 | var ledsOut []SimpleEntry 215 | leds, _ := os.ReadDir("/sys/class/leds/") 216 | for _, led := range leds { 217 | name := led.Name() 218 | bPath := filepath.Join("/sys/class/leds", name, "brightness") 219 | val := "" 220 | if b, err := os.ReadFile(bPath); err == nil { 221 | val = strings.TrimSpace(string(b)) 222 | } 223 | ledsOut = append(ledsOut, SimpleEntry{Path: bPath, Label: name, Value: val}) 224 | } 225 | 226 | resp := map[string]interface{}{ 227 | "gpio": gpioOut, 228 | "leds": ledsOut, 229 | } 230 | 231 | return c.JSON(http.StatusOK, resp) 232 | } 233 | 234 | func handleSerialControl(c echo.Context) error { 235 | path := c.QueryParam("path") 236 | tcpPortStr := c.QueryParam("port") 237 | dtrStr := c.QueryParam("dtr") 238 | rtsStr := c.QueryParam("rts") 239 | baudStr := c.QueryParam("baud") 240 | 241 | // Get path from TCP port if not provided directly 242 | if path == "" && tcpPortStr != "" { 243 | if tcpPort, err := strconv.Atoi(tcpPortStr); err == nil { 244 | path = getSerialPathFromTcpPort(tcpPort) 245 | } 246 | } 247 | 248 | if path == "" || (dtrStr == "" && rtsStr == "" && baudStr == "") { 249 | return c.JSON(http.StatusBadRequest, map[string]string{ 250 | "error": "Missing path/tcpPort or dtr/rts/baud param", 251 | }) 252 | } 253 | 254 | // Parse baud rate if provided 255 | var baud int 256 | if baudStr != "" { 257 | var err error 258 | baud, err = strconv.Atoi(baudStr) 259 | if err != nil { 260 | return c.JSON(http.StatusBadRequest, map[string]string{ 261 | "error": "Invalid baud rate", 262 | }) 263 | } 264 | 265 | if !isValidBaudRate(baud) { 266 | return c.JSON(http.StatusBadRequest, map[string]interface{}{ 267 | "error": "Invalid baud rate", 268 | "validRates": validRates, 269 | }) 270 | } 271 | } 272 | 273 | // Get current state 274 | currentState := getSerialPortState(path) 275 | 276 | // Handle baud rate change 277 | if baud > 0 && baud != currentState.BaudRate { 278 | if !reopenSerialPort(path, baud) { 279 | return c.JSON(http.StatusInternalServerError, map[string]string{ 280 | "error": "Failed to reopen port with new baud rate", 281 | }) 282 | } 283 | currentState.BaudRate = baud 284 | 285 | // Reopen immediately to ensure it's ready 286 | if _, err := ensureSerialPort(path, baud); err != nil { 287 | log.Printf("[routes] failed to reopen port %s at %d: %v\n", path, baud, err) 288 | } 289 | } 290 | 291 | // Update state 292 | setObj := currentState 293 | if dtrStr != "" { 294 | setObj.DTR = dtrStr == "1" || dtrStr == "true" 295 | } 296 | if rtsStr != "" { 297 | setObj.RTS = rtsStr == "1" || rtsStr == "true" 298 | } 299 | if baud > 0 { 300 | setObj.BaudRate = baud 301 | } 302 | 303 | setSerialPortState(path, setObj) 304 | 305 | // Apply DTR/RTS if they were changed 306 | if dtrStr != "" || rtsStr != "" { 307 | // Use ensureSerialPort to safely get or open the port 308 | serial, err := ensureSerialPort(path, currentState.BaudRate) 309 | if err != nil { 310 | // Log error but continue? Or return error? 311 | // For now, just log and fail the DTR/RTS part 312 | log.Printf("[routes] failed to ensure port for %s: %v\n", path, err) 313 | } else if serial != nil { 314 | // Set both DTR and RTS simultaneously for better timing 315 | //setSerialDTRRTS(serial, setObj.DTR, setObj.RTS) 316 | if dtrStr != "" && rtsStr != "" { 317 | setSerialDTRRTS(serial, setObj.DTR, setObj.RTS) 318 | 319 | } 320 | if dtrStr != "" && rtsStr == "" { 321 | setSerialDTR(serial, setObj.DTR) 322 | } 323 | if rtsStr != "" && dtrStr == "" { 324 | setSerialRTS(serial, setObj.RTS) 325 | } 326 | } 327 | } 328 | 329 | response := map[string]interface{}{ 330 | "ok": true, 331 | "path": path, 332 | "tcpPort": getTcpPortFromPath(path), 333 | "set": setObj, 334 | } 335 | 336 | return c.JSON(http.StatusOK, response) 337 | } 338 | 339 | func handleStaticFiles(c echo.Context) error { 340 | path := c.Request().URL.Path 341 | 342 | // Remove leading slash 343 | path = strings.TrimPrefix(path, "/") 344 | 345 | // Default to index.html 346 | if path == "" { 347 | path = "index.html" 348 | } 349 | 350 | // Try to get embedded file 351 | if content, found := getEmbeddedFile(path); found { 352 | contentType := getContentType(path) 353 | c.Response().Header().Set("Content-Type", contentType) 354 | return c.String(http.StatusOK, content) 355 | } 356 | 357 | // File not found 358 | return c.String(http.StatusNotFound, "File not found") 359 | } 360 | 361 | func getContentType(path string) string { 362 | ext := strings.ToLower(getFileExtension(path)) 363 | switch ext { 364 | case ".html": 365 | return "text/html" 366 | case ".js": 367 | return "text/javascript" 368 | case ".css": 369 | return "text/css" 370 | case ".json": 371 | return "application/json" 372 | case ".png": 373 | return "image/png" 374 | case ".jpg", ".jpeg": 375 | return "image/jpeg" 376 | case ".gif": 377 | return "image/gif" 378 | case ".svg": 379 | return "image/svg+xml" 380 | case ".ico": 381 | return "image/x-icon" 382 | default: 383 | return "application/octet-stream" 384 | } 385 | } 386 | 387 | func getFileExtension(path string) string { 388 | if idx := strings.LastIndex(path, "."); idx != -1 { 389 | return path[idx:] 390 | } 391 | return "" 392 | } 393 | -------------------------------------------------------------------------------- /web-page/src/tools/spinel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Spinel Protocol Client for OpenThread RCP 3 | * Used by both Silicon Labs and Texas Instruments chips 4 | * OpenThread RCP uses Spinel protocol over HDLC-Lite 5 | */ 6 | 7 | import { Link } from "../types/index"; 8 | 9 | // Spinel constants 10 | export const SPINEL_HEADER_FLAG = 0x80; 11 | export const SPINEL_CMD_PROP_VALUE_GET = 0x02; 12 | export const SPINEL_CMD_PROP_VALUE_IS = 0x06; 13 | export const SPINEL_PROP_PROTOCOL_VERSION = 0x01; 14 | export const SPINEL_PROP_NCP_VERSION = 0x02; 15 | export const SPINEL_PROP_CAPS = 0x05; 16 | export const SPINEL_PROP_HWADDR = 0x08; // EUI-64 17 | 18 | // HDLC constants 19 | export const HDLC_FLAG = 0x7e; 20 | export const HDLC_ESCAPE = 0x7d; 21 | 22 | // FCS-16 lookup table for HDLC 23 | export const FCS_TABLE = new Uint16Array([ 24 | 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 25 | 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 26 | 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 27 | 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 28 | 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 29 | 0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, 30 | 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, 31 | 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 32 | 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 33 | 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 34 | 0x4e64, 0x5fed, 0x6d76, 0x7cff, 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 35 | 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 36 | 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 37 | 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, 38 | 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 39 | 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 40 | 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 41 | 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 42 | 0x3de3, 0x2c6a, 0x1ef1, 0x0f78, 43 | ]); 44 | 45 | /** 46 | * Calculate FCS-16 checksum for HDLC 47 | */ 48 | export function fcs16(data: Uint8Array): number { 49 | let fcs = 0xffff; 50 | for (const byte of data) { 51 | fcs = (fcs >> 8) ^ FCS_TABLE[(fcs ^ byte) & 0xff]; 52 | } 53 | return fcs ^ 0xffff; 54 | } 55 | 56 | export interface OpenThreadRcpInfo { 57 | version: string; 58 | rawVersion: string; 59 | } 60 | 61 | /** 62 | * Spinel Client for OpenThread RCP communication 63 | * Works over HDLC-Lite framing 64 | */ 65 | export class SpinelClient { 66 | private link: Link; 67 | private buffer: number[] = []; 68 | private pendingResponse: { 69 | resolve: (payload: Uint8Array) => void; 70 | reject: (err: Error) => void; 71 | timer: number; 72 | } | null = null; 73 | private disposed = false; 74 | private tid = 1; 75 | private logger: (msg: string) => void = () => {}; 76 | 77 | constructor(link: Link) { 78 | this.link = link; 79 | } 80 | 81 | public setLogger(logger: (msg: string) => void) { 82 | this.logger = logger; 83 | } 84 | 85 | public dispose() { 86 | this.disposed = true; 87 | if (this.pendingResponse) { 88 | window.clearTimeout(this.pendingResponse.timer); 89 | this.pendingResponse.reject(new Error("Spinel client disposed")); 90 | this.pendingResponse = null; 91 | } 92 | } 93 | 94 | public handleData(chunk: Uint8Array): void { 95 | if (this.disposed) return; 96 | for (const byte of chunk) { 97 | this.buffer.push(byte); 98 | } 99 | this.processBuffer(); 100 | } 101 | 102 | private processBuffer() { 103 | while (true) { 104 | const startIdx = this.buffer.indexOf(HDLC_FLAG); 105 | if (startIdx === -1) { 106 | this.buffer = []; 107 | return; 108 | } 109 | 110 | if (startIdx > 0) { 111 | this.buffer = this.buffer.slice(startIdx); 112 | } 113 | 114 | const endIdx = this.buffer.indexOf(HDLC_FLAG, 1); 115 | if (endIdx === -1) return; 116 | 117 | const frameBytes = this.buffer.slice(1, endIdx); 118 | this.buffer = this.buffer.slice(endIdx + 1); 119 | 120 | if (frameBytes.length < 4) continue; // Too short 121 | 122 | try { 123 | const unstuffed = this.hdlcUnstuff(new Uint8Array(frameBytes)); 124 | // Verify FCS 125 | if (unstuffed.length < 3) continue; 126 | const payload = unstuffed.slice(0, unstuffed.length - 2); 127 | const receivedFcs = (unstuffed[unstuffed.length - 2] | (unstuffed[unstuffed.length - 1] << 8)) & 0xffff; 128 | const calculatedFcs = fcs16(payload); 129 | if (receivedFcs !== calculatedFcs) { 130 | this.logger(`FCS mismatch: received ${receivedFcs.toString(16)}, calculated ${calculatedFcs.toString(16)}`); 131 | continue; 132 | } 133 | this.handleFrame(payload); 134 | } catch (e) { 135 | this.logger(`Spinel frame error: ${e}`); 136 | } 137 | } 138 | } 139 | 140 | private hdlcUnstuff(data: Uint8Array): Uint8Array { 141 | const out: number[] = []; 142 | let escaped = false; 143 | for (const byte of data) { 144 | if (escaped) { 145 | out.push(byte ^ 0x20); 146 | escaped = false; 147 | } else if (byte === HDLC_ESCAPE) { 148 | escaped = true; 149 | } else { 150 | out.push(byte); 151 | } 152 | } 153 | return new Uint8Array(out); 154 | } 155 | 156 | private hdlcStuff(data: Uint8Array): Uint8Array { 157 | const out: number[] = []; 158 | for (const byte of data) { 159 | if (byte === HDLC_FLAG || byte === HDLC_ESCAPE || byte < 0x20) { 160 | out.push(HDLC_ESCAPE, byte ^ 0x20); 161 | } else { 162 | out.push(byte); 163 | } 164 | } 165 | return new Uint8Array(out); 166 | } 167 | 168 | private handleFrame(payload: Uint8Array) { 169 | if (payload.length < 2) return; 170 | 171 | const cmd = payload[1]; 172 | // this.logger(`RX frame: cmd=${cmd.toString(16)}, len=${payload.length}`); 173 | 174 | if (cmd === SPINEL_CMD_PROP_VALUE_IS && this.pendingResponse) { 175 | window.clearTimeout(this.pendingResponse.timer); 176 | this.pendingResponse.resolve(payload.slice(2)); 177 | this.pendingResponse = null; 178 | } 179 | } 180 | 181 | private encodeVarint(value: number): Uint8Array { 182 | if (value < 127) { 183 | return new Uint8Array([value]); 184 | } 185 | const bytes: number[] = []; 186 | while (value > 0) { 187 | let b = value & 0x7f; 188 | value >>= 7; 189 | if (value > 0) b |= 0x80; 190 | bytes.push(b); 191 | } 192 | return new Uint8Array(bytes); 193 | } 194 | 195 | private decodeVarint(data: Uint8Array, offset: number): [number, number] { 196 | let value = 0; 197 | let shift = 0; 198 | let idx = offset; 199 | while (idx < data.length) { 200 | const b = data[idx++]; 201 | value |= (b & 0x7f) << shift; 202 | if ((b & 0x80) === 0) break; 203 | shift += 7; 204 | } 205 | return [value, idx]; 206 | } 207 | 208 | public async sendCommand(cmd: number, propId: number, value?: Uint8Array, timeoutMs = 3000): Promise { 209 | if (this.pendingResponse) { 210 | throw new Error("Request already in flight"); 211 | } 212 | 213 | const header = SPINEL_HEADER_FLAG | (this.tid & 0x0f); 214 | this.tid = (this.tid + 1) & 0x0f || 1; 215 | 216 | const propBytes = this.encodeVarint(propId); 217 | const payloadLen = 2 + propBytes.length + (value?.length || 0); 218 | const payload = new Uint8Array(payloadLen); 219 | payload[0] = header; 220 | payload[1] = cmd; 221 | payload.set(propBytes, 2); 222 | if (value) { 223 | payload.set(value, 2 + propBytes.length); 224 | } 225 | 226 | const fcsVal = fcs16(payload); 227 | const withFcs = new Uint8Array(payload.length + 2); 228 | withFcs.set(payload); 229 | withFcs[payload.length] = fcsVal & 0xff; 230 | withFcs[payload.length + 1] = (fcsVal >> 8) & 0xff; 231 | 232 | const stuffed = this.hdlcStuff(withFcs); 233 | const frame = new Uint8Array(stuffed.length + 2); 234 | frame[0] = HDLC_FLAG; 235 | frame.set(stuffed, 1); 236 | frame[frame.length - 1] = HDLC_FLAG; 237 | 238 | // this.logger( 239 | // `TX: ${Array.from(frame) 240 | // .map((b) => b.toString(16).padStart(2, "0")) 241 | // .join(" ")}` 242 | // ); 243 | 244 | const responsePromise = new Promise((resolve, reject) => { 245 | const timer = window.setTimeout(() => { 246 | if (this.pendingResponse) { 247 | this.pendingResponse.reject(new Error("Spinel timeout")); 248 | this.pendingResponse = null; 249 | } 250 | }, timeoutMs); 251 | this.pendingResponse = { resolve, reject, timer }; 252 | }); 253 | 254 | await this.link.write(frame); 255 | return responsePromise; 256 | } 257 | 258 | /** 259 | * Get EUI-64 hardware address 260 | */ 261 | public async getEui64(): Promise { 262 | const response = await this.sendCommand(SPINEL_CMD_PROP_VALUE_GET, SPINEL_PROP_HWADDR); 263 | const [, dataOffset] = this.decodeVarint(response, 0); 264 | const eui64 = response.slice(dataOffset, dataOffset + 8); 265 | 266 | if (eui64.length < 8) { 267 | throw new Error("Invalid EUI64 response"); 268 | } 269 | 270 | return Array.from(eui64) 271 | .map((b) => b.toString(16).padStart(2, "0").toUpperCase()) 272 | .join(":"); 273 | } 274 | 275 | /** 276 | * Get NCP version string 277 | */ 278 | public async getVersion(): Promise { 279 | const response = await this.sendCommand(SPINEL_CMD_PROP_VALUE_GET, SPINEL_PROP_NCP_VERSION); 280 | const [, dataOffset] = this.decodeVarint(response, 0); 281 | // Version is null-terminated string 282 | const versionBytes = response.slice(dataOffset); 283 | const nullIdx = versionBytes.indexOf(0); 284 | const str = new TextDecoder().decode(nullIdx >= 0 ? versionBytes.slice(0, nullIdx) : versionBytes); 285 | this.logger(`Spinel Version string: ${str}`); 286 | return str; 287 | } 288 | 289 | /** 290 | * Get OpenThread RCP info with parsed version 291 | */ 292 | public async getOpenThreadInfo(): Promise { 293 | try { 294 | const rawVersion = await this.getVersion(); 295 | 296 | if (!rawVersion) { 297 | return null; 298 | } 299 | 300 | let version = rawVersion; 301 | 302 | // Try to extract just the OpenThread version part 303 | // Formats: "OPENTHREAD/20191113-01234; EFR32; ..." or "bla-bla-openthread/version; ..." 304 | const match = rawVersion.match(/openthread\/([^;]+)/i); 305 | if (match) { 306 | version = match[1].trim(); 307 | } 308 | 309 | // this.logger(`OpenThread RCP version: ${version}`); 310 | return { version, rawVersion }; 311 | } catch (e) { 312 | this.logger(`getOpenThreadInfo error: ${e}`); 313 | return null; 314 | } 315 | } 316 | 317 | /** 318 | * Ping the device by requesting version 319 | */ 320 | public async ping(timeoutMs = 1000): Promise { 321 | try { 322 | await this.sendCommand(SPINEL_CMD_PROP_VALUE_GET, SPINEL_PROP_NCP_VERSION, undefined, timeoutMs); 323 | return true; 324 | } catch { 325 | return false; 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /.github/workflows/build-binaries.yml: -------------------------------------------------------------------------------- 1 | name: Build and release workflow 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | version-bump: 13 | name: Update version numbers 14 | runs-on: ubuntu-latest 15 | if: startsWith(github.ref, 'refs/tags/v') 16 | outputs: 17 | version: ${{ steps.setver.outputs.version }} 18 | new_sha: ${{ steps.newsha.outputs.new_sha }} 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | - name: Parse version from tag 26 | id: setver 27 | shell: bash 28 | run: | 29 | set -e 30 | ref="${GITHUB_REF#refs/tags/}" 31 | version="${ref#v}" 32 | echo "version=$version" >> "$GITHUB_OUTPUT" 33 | echo "Using version: $version" 34 | - name: Checkout main branch 35 | shell: bash 36 | run: | 37 | git checkout main 38 | - name: Update versions in files 39 | shell: bash 40 | run: | 41 | set -e 42 | ver='${{ steps.setver.outputs.version }}' 43 | echo "Updating version to: $ver" 44 | 45 | cd web-page 46 | # Update root package.json 47 | echo "Before updating package.json:" 48 | cat package.json | jq '.version' 49 | tmp=$(mktemp) 50 | jq --arg v "$ver" '.version=$v' package.json > "$tmp" && mv "$tmp" package.json 51 | echo "After updating package.json:" 52 | cat package.json | jq '.version' 53 | 54 | # Update add-on config.json 55 | echo "Before updating xzg-multi-tool-addon/config.json:" 56 | cat ../xzg-multi-tool-addon/config.json | jq '.version' 57 | tmp=$(mktemp) 58 | jq --arg v "$ver" '.version=$v' ../xzg-multi-tool-addon/config.json > "$tmp" && mv "$tmp" ../xzg-multi-tool-addon/config.json 59 | echo "After updating xzg-multi-tool-addon/config.json:" 60 | cat ../xzg-multi-tool-addon/config.json | jq '.version' 61 | 62 | echo "Git status after updates:" 63 | git status --porcelain 64 | 65 | - name: Fetch draft release notes from Release Drafter and prepend to CHANGELOG 66 | id: fetch_draft 67 | shell: bash 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | run: | 71 | set -e 72 | owner_repo="${GITHUB_REPOSITORY}" 73 | # get first draft release body (release-drafter keeps an editable draft release) 74 | draft_body=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/$owner_repo/releases" | jq -r 'map(select(.draft==true))[0].body // empty') 75 | if [ -z "$draft_body" ]; then 76 | echo "No draft release found; skipping changelog update" 77 | echo "draft_found=false" >> "$GITHUB_OUTPUT" 78 | exit 0 79 | fi 80 | echo "draft_found=true" >> "$GITHUB_OUTPUT" 81 | 82 | ver='${{ steps.setver.outputs.version }}' 83 | header="## v$ver" 84 | tmpfile=$(mktemp) 85 | printf "%s\n\n%s\n\n" "$header" "$draft_body" > "$tmpfile" 86 | if [ -f xzg-multi-tool-addon/CHANGELOG.md ]; then 87 | cat xzg-multi-tool-addon/CHANGELOG.md >> "$tmpfile" 88 | fi 89 | mv "$tmpfile" xzg-multi-tool-addon/CHANGELOG.md 90 | git add xzg-multi-tool-addon/CHANGELOG.md 91 | echo "Prepended xzg-multi-tool-addon/CHANGELOG.md with draft release notes." 92 | 93 | - name: Commit version bumps 94 | uses: stefanzweifel/git-auto-commit-action@v5 95 | with: 96 | commit_message: "chore(release): bump version to ${{ steps.setver.outputs.version }}" 97 | branch: main 98 | file_pattern: "web-page/package.json xzg-multi-tool-addon/config.json xzg-multi-tool-addon/CHANGELOG.md" 99 | 100 | - name: Get bumped commit SHA 101 | id: newsha 102 | run: | 103 | # ensure we have the latest remote ref (commit just pushed by previous step) 104 | git fetch origin main 105 | sha=$(git rev-parse origin/main) 106 | echo "new_sha=$sha" >> "$GITHUB_OUTPUT" 107 | 108 | build-web-deploy: 109 | name: Build & Deploy Web 110 | runs-on: ubuntu-latest 111 | needs: [version-bump] 112 | steps: 113 | - name: Checkout code 114 | uses: actions/checkout@v4 115 | with: 116 | ref: ${{ needs.version-bump.outputs.new_sha }} 117 | fetch-depth: 0 118 | 119 | - name: Ensure full git history and export short SHA 120 | shell: bash 121 | run: | 122 | git fetch --prune --unshallow || true 123 | short_sha=$(git rev-parse --short HEAD) 124 | echo "sha=$short_sha" >> "$GITHUB_OUTPUT" 125 | echo "COMMIT_SHA=$short_sha" >> "$GITHUB_ENV" 126 | id: short_sha 127 | 128 | - name: Setup Node.js 129 | uses: actions/setup-node@v4 130 | with: 131 | node-version: '20' 132 | 133 | - name: Install dependencies 134 | run: npm ci 135 | working-directory: web-page 136 | 137 | - name: Build project 138 | run: npm run build 139 | working-directory: web-page 140 | 141 | - name: Upload web-page artifacts 142 | uses: actions/upload-artifact@v4 143 | with: 144 | name: web-page-artifacts 145 | path: | 146 | web-page/dist/** 147 | if-no-files-found: error 148 | 149 | - name: Create CNAME for GitHub Pages 150 | shell: bash 151 | run: | 152 | mkdir -p web-page/dist 153 | echo 'mt.xyzroe.cc' > web-page/dist/CNAME 154 | 155 | - name: Deploy to GitHub Pages 156 | uses: peaceiris/actions-gh-pages@v4 157 | with: 158 | github_token: ${{ secrets.GITHUB_TOKEN }} 159 | publish_dir: ./web-page/dist 160 | publish_branch: web 161 | force_orphan: true 162 | 163 | release: 164 | name: Publish Release 165 | runs-on: ubuntu-latest 166 | needs: [build-go, version-bump] 167 | steps: 168 | - name: Checkout 169 | uses: actions/checkout@v4 170 | with: 171 | ref: ${{ needs.version-bump.outputs.new_sha }} 172 | fetch-depth: 0 173 | 174 | - name: Download go artifacts 175 | uses: actions/download-artifact@v4 176 | with: 177 | name: go-binaries 178 | path: dist-release 179 | 180 | - name: Show downloaded files 181 | run: ls -Rla dist-release || true 182 | 183 | - name: Get draft release body (Release Drafter) 184 | id: get_draft 185 | env: 186 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 187 | run: | 188 | set -e 189 | owner_repo="${GITHUB_REPOSITORY}" 190 | draft_body=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/$owner_repo/releases" | jq -r 'map(select(.draft==true))[0].body // ""') 191 | # write multiline output 192 | echo "body<> "$GITHUB_OUTPUT" 193 | printf "%s\n" "$draft_body" >> "$GITHUB_OUTPUT" 194 | echo "EOF" >> "$GITHUB_OUTPUT" 195 | 196 | - name: Create GitHub Release and upload assets files (use draft body) 197 | uses: softprops/action-gh-release@v2 198 | with: 199 | tag_name: v${{ needs.version-bump.outputs.version }} 200 | name: v${{ needs.version-bump.outputs.version }} 201 | body: ${{ steps.get_draft.outputs.body }} 202 | files: dist-release/** 203 | env: 204 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 205 | 206 | build-go: 207 | name: Build Go binaries (macos-latest) 208 | runs-on: macos-latest 209 | needs: [version-bump, build-web-deploy] 210 | steps: 211 | - name: Checkout 212 | uses: actions/checkout@v4 213 | with: 214 | ref: ${{ needs.version-bump.outputs.new_sha }} 215 | fetch-depth: 0 216 | 217 | - name: Ensure full git history and export short SHA 218 | shell: bash 219 | run: | 220 | git fetch --prune --unshallow || true 221 | short_sha=$(git rev-parse --short HEAD) 222 | echo "sha=$short_sha" >> "$GITHUB_OUTPUT" 223 | echo "COMMIT_SHA=$short_sha" >> "$GITHUB_ENV" 224 | id: short_sha 225 | 226 | - name: Download web-page artifacts 227 | uses: actions/download-artifact@v4 228 | with: 229 | name: web-page-artifacts 230 | path: tmp/web-artifacts 231 | if-no-files-found: error 232 | 233 | - name: Copy web assets into bridge/web 234 | shell: bash 235 | run: | 236 | set -e 237 | mkdir -p bridge/web 238 | cp -a tmp/web-artifacts/. bridge/web/ || true 239 | ls -la bridge/web || true 240 | 241 | - name: Setup Go 242 | uses: actions/setup-go@v4 243 | with: 244 | go-version: '1.21' 245 | 246 | - name: Install upx 247 | run: | 248 | echo "Installing upx for binary compression" 249 | brew install upx || true 250 | 251 | - name: Build go binaries for multiple platforms 252 | working-directory: bridge 253 | env: 254 | USE_UPX: '1' 255 | run: | 256 | make deps 257 | make build 258 | 259 | - name: Upload go binaries 260 | uses: actions/upload-artifact@v4 261 | with: 262 | name: go-binaries 263 | path: | 264 | bridge/dist/** 265 | if-no-files-found: error 266 | 267 | docker-go: 268 | name: Build & Push Docker image (bridge) 269 | runs-on: ubuntu-latest 270 | needs: [version-bump, build-web-deploy] 271 | permissions: 272 | contents: read 273 | packages: write 274 | steps: 275 | - name: Checkout 276 | uses: actions/checkout@v4 277 | with: 278 | ref: ${{ needs.version-bump.outputs.new_sha }} 279 | fetch-depth: 0 280 | 281 | - name: Download web-page artifacts 282 | uses: actions/download-artifact@v4 283 | with: 284 | name: web-page-artifacts 285 | path: tmp/web-artifacts 286 | if-no-files-found: error 287 | 288 | - name: Copy web assets into bridge/web 289 | shell: bash 290 | run: | 291 | set -e 292 | mkdir -p bridge/web 293 | cp -a tmp/web-artifacts/. bridge/web/ || true 294 | ls -la bridge/web || true 295 | 296 | - name: Set up QEMU 297 | uses: docker/setup-qemu-action@v3 298 | 299 | - name: Set up Docker Buildx 300 | uses: docker/setup-buildx-action@v3 301 | 302 | - name: Log in to GHCR 303 | uses: docker/login-action@v3 304 | with: 305 | registry: ghcr.io 306 | username: ${{ github.actor }} 307 | password: ${{ secrets.GITHUB_TOKEN }} 308 | 309 | - name: Extract metadata (tags, labels) for bridge 310 | id: meta-go 311 | uses: docker/metadata-action@v5 312 | with: 313 | images: | 314 | ghcr.io/${{ github.repository }} 315 | tags: | 316 | type=raw,value=${{ needs.version-bump.outputs.version }} 317 | type=raw,value=latest 318 | 319 | - name: Compute short commit SHA 320 | id: short_sha_go 321 | run: | 322 | echo "sha=$(echo ${GITHUB_SHA} | cut -c1-8)" >> "$GITHUB_OUTPUT" 323 | 324 | - name: Build and push bridge Docker image (multi-arch) 325 | uses: docker/build-push-action@v6 326 | with: 327 | context: . 328 | file: ./bridge/Dockerfile 329 | push: true 330 | platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6,linux/386 331 | tags: ${{ steps.meta-go.outputs.tags }} 332 | labels: ${{ steps.meta-go.outputs.labels }} 333 | cache-from: type=gha 334 | cache-to: type=gha,mode=max 335 | build-args: | 336 | VERSION=${{ needs.version-bump.outputs.version }} 337 | COMMIT_SHA=${{ steps.short_sha_go.outputs.sha }} 338 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XZG Multi-tool 2 | 3 |
4 | XZG Multi-tool 5 |
6 | 7 | --- 8 | 9 |
10 | GitHub version 11 | GitHub Actions Workflow Status 12 | GitHub download 13 | GHCR pulls 14 | GitHub Issues or Pull Requests 15 | License 16 |
17 | 18 | ## 📖 About 19 | 20 | The XZG Multi-Tool is a browser-based flashing solution that enables hobbyists and developers to program Texas Instruments, Silicon Labs, Espressif, Arduino and Telink devices. It provides a simple, polished web UI that enables users to flash adapters directly from the browser, eliminating the need for client software installation. 21 | 22 | The web front end performs local flashing via the WebSerial and WebUSB APIs, offering automatic device detection and convenient firmware flashing features. The bridge component (WebSocket ↔ TCP) enables headless or remote hosts to expose local serial ports via TCP and connect them to the web UI. The bridge also supports connecting to remote TCP-based adapters, enabling access to networked adapters from any browser. 23 | 24 | ## ⭐ Features 25 | 26 | - 🔌 Work with various devices locally via WebSerial/WebUSB or remotely via bridge. 27 | - 📂 Flash firmware from a local file or select from a provided list. 28 | - 📝 List of cloud firmware with descriptions 29 | - 🦾 Automatically detects chip model, flash size, IEEE, and firmware version 30 | - 💾 Backup, restore, and erase NVRAM 31 | 32 | ## 💻 Supported Chips 33 | 34 | For a complete, up-to-date list of supported devices, features, and device-specific notes, see the [devices table](/docs/devices.md). 35 | 36 | ## 🏗️ Architecture 37 | 38 |
39 | 40 | 41 | 42 | 43 | Block Diagram 44 | 45 | 46 |
47 | 48 | ## 🚀 Quick start 49 | 50 | ### 🔌 Local USB 51 | 52 |
53 | 🌐 Open: mt.xyzroe.cc
54 | from Chrome or Edge 55 |
56 | 57 | ### 📡 Remote (TCP or remote USB/serial) 58 | 59 | Because browsers don't support TCP connections you need to use WebSocket ↔ TCP bridge that can forward WebSocket clients to TCP hosts and as option expose local serial ports over TCP. 60 | 61 | You have some options: 62 | 63 | #### 🏠 Home Assistant Add-On 64 | 65 |
66 | Home-Assistant add repository sticker 67 |
68 | 69 | Just click on the button above or add this repository to your Home Assistant add-on store manually and then install the add-on to expose remote TCP / host serial devices to the web UI. 70 | 71 | #### 🐳 Docker images 72 | 73 | Prebuilt multi-arch images are published to [GHCR](https://github.com/xyzroe/XZG-MT/pkgs/container/xzg-mt) on each release. 74 | 75 | Latest image: `ghcr.io/xyzroe/XZG-MT:latest` 76 | Special version image: `ghcr.io/xyzroe/XZG-MT:` (e.g. `v0.1.1`) 77 | 78 |
79 | Running instructions: 80 | - Run (basic): 81 | 82 | ```bash 83 | docker run --rm -p 8765:8765 ghcr.io/xyzroe/XZG-MT:latest 84 | ``` 85 | 86 | - Run with mDNS: 87 | 88 | ```bash 89 | docker run --rm --network host ghcr.io/xyzroe/XZG-MT:latest 90 | ``` 91 | 92 | - To expose a host serial device to the container add `--device` (Linux): 93 | 94 | ```bash 95 | docker run --rm --network host \ 96 | --device /dev/ttyUSB0:/dev/ttyUSB0 \ 97 | ghcr.io/xyzroe/XZG-MT:latest 98 | ``` 99 | 100 | - Customize port, advertised host, disable serial scan and enable debug logs: 101 | 102 | ```bash 103 | docker run --rm \ 104 | -e PORT=9000 \ 105 | -e ADVERTISE_HOST=192.168.1.42 \ 106 | -e DEBUG_MODE=true \ 107 | -p 9000:9000 \ 108 | ghcr.io/xyzroe/XZG-MT:latest 109 | ``` 110 | 111 |
112 | 113 | #### 📦 Prebuilt binaries 114 | 115 | Download a ready-to-run binary from [Releases](https://github.com/xyzroe/XZG-MT/releases), make it executable (Linux/macOS), and run. 116 | 117 |
118 | ⚡ How to run: 119 | 120 | ##### Windows: 121 | 122 | - Run: `XZG-MT-windows-*.exe` or double click 123 | 124 | ##### Linux: 125 | 126 |
127 | Select the correct binary for your platform 128 | 129 | - linux/arm64 — aarch64_generic, aarch64, arm64 130 | - linux/arm — armhf, arm_cortex-a7_neon-vfpv4, arm_cortex-a9_neon 131 | - linux/amd64 — amd64 132 | - linux/386 — i386 / 32-bit x86 133 | - linux/mips — mips_24kc 134 | - linux/mipsle — mipsel_24kc 135 | - linux/mips64 — mips64 136 | - linux/mips64le — mips64le 137 | 138 | Note: linux/arm targets ARMv7 (GOARM=7). MIPS and MIPSLE builds use GOMIPS=softfloat for compatibility with older devices (for example, MT7688). 139 | 140 |
141 |
142 | 143 | 144 | 1. Make executable: 145 | 146 | ```bash 147 | chmod +x ./XZG-MT-linux-* 148 | ``` 149 | 150 | 2. Run: `./XZG-MT-linux-*` or double click 151 | 152 | ##### macOS: 153 | 154 | 1. Make executable and remove quarantine: 155 | 156 | ```bash 157 | chmod +x ./XZG-MT-darwin-* 158 | xattr -d com.apple.quarantine ./XZG-MT-darwin-* 159 | ``` 160 | 161 | 2. Run: `./XZG-MT-darwin-*` or double click 162 | 163 | To run on custom port: `./XZG-MT-* 9999` 164 | 165 |
166 | 167 | ## 📚 Where to read more 168 | 169 | For step-by-step guides and detailed documentation, explore the following: 170 | 171 | - 📚 How-To Guides: [Start here](docs/how-to/readme.md) 172 | - 🌐 Web UI: [README](web-page/README.md) 173 | - 🚀 WebSocket bridge — [README](bridge/README.md) 174 | - 🏠 Home Assistant add-on: [README](xzg-multi-tool-addon/README.md) 175 | - 🤖 AI Generated Wiki: [DeepWiki](https://deepwiki.com/xyzroe/XZG-MT) 176 | 177 | ## 👥 Community 178 | 179 |
180 | Telegram 181 | Discord 182 |
183 | 184 | ## 💖 Support 185 | 186 | If you find this project useful and want to support further development, you can sponsor or donate to the author: 187 | 188 |
189 | GitHub Sponsors 190 | Buy Me a Coffee 191 | PayPal 192 | NOWPayments 193 |
194 |
195 | 196 | Thank you — every little contribution helps keep the project alive and maintained. 🙏 197 | 198 | ## 🌟 Star History 199 | 200 | If you find this project useful, please consider giving it a ⭐ on GitHub! 201 | 202 | 211 | 212 | ## 🛠️ Tech badges 213 | 214 | Below are key technologies used across the projects (click the badges for quick context): 215 | 216 |
217 | Node.js 218 | TypeScript 219 | esbuild
220 | Web Serial API 221 | Web USB 222 | WebSocket
223 | Go 224 | mDNS (zeroconf) 225 | Docker 226 |
227 | 228 | ## 📁 Repository structure 229 | 230 | - web-page/ — The web frontend. Contains source TypeScript, build scripts, favicon and static assets. 231 | - bridge/ - The small Go app that bridges WebSocket ↔ TCP, supports mDNS discovery and exposing local serial ports as TCP servers. 232 | - xzg-multi-tool-addon/ — Home Assistant add-on wrapper for the bridge. 233 | - docs - Folder consisting the documentation about this project. 234 | - LICENSE — License for the whole repository (MIT). 235 | - repository.json — repository metadata. 236 | 237 | ## 📜 License 238 | 239 | MIT — see [`LICENSE`](LICENSE) for details. 240 | 241 | ## 🙏 Acknowledgements 242 | 243 | Built on the shoulders of giants: 244 | 245 | - **Texas Instruments CCXX52 and CC2538** — inspired by 246 | - [cc2538-bsl](https://github.com/JelmerT/cc2538-bsl) by Jelmer Tiete 247 | - [zigpy-znp](https://github.com/zigpy/zigpy-znp) by Open Home Foundation 248 | - **Silicon Labs** — inspired by 249 | - [universal-silabs-flasher](https://github.com/NabuCasa/universal-silabs-flasher) by Nabu Casa 250 | - **Espressif Systems** — powered by 251 | - [esptool-js](https://github.com/espressif/esptool-js) by 252 | Espressif Systems 253 | - **Texas Instruments CC25XX** 254 | - **СС Debugger** — inspired by 255 | - [cc-tool](https://github.com/scott-42/cc-tool) by Scott Gustafson 256 | - **СС Loader** — inspired by 257 | - [CC Loader](https://github.com/RedBearLab/CCLoader) by RedBearLab 258 | - [CC Loader fork](https://github.com/tjko/CCLoader) by Timo Kokkonen 259 | - **Arduino** — inspired by 260 | - [arduino-web-uploader](https://github.com/dbuezas/arduino-web-uploader) by David Buezas 261 | - **Telink** - inspired by 262 | - [TlsrComProg825x](https://github.com/pvvx/TlsrComProg825x) by pvvx Viktor 263 | - [TlsrComProg](https://github.com/pvvx/TlsrComProg) by pvvx Viktor 264 | - [TLSRPGM](https://github.com/pvvx/TLSRPGM) by pvvx Viktor 265 | 266 | --- 267 | 268 |
269 | Made with from Berlin! 270 |
271 | 272 | --- 273 | -------------------------------------------------------------------------------- /web-page/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg: #ffffff; 3 | --fg: #212529; 4 | --section-bg: #ffffff; 5 | --border: #e1e4e8; 6 | --console-bg: #f8f9fa; 7 | --console-fg: #212529; 8 | --input-bg: #ffffff; 9 | --input-fg: #212529; 10 | --input-border: #ced4da; 11 | --input-focus: #86b7fe; 12 | --muted: #6c757d; 13 | --progress-track: #e9ecef; 14 | --progress-text: #ffffff; 15 | 16 | /* Tooltip */ 17 | --tooltip-bg: #212529; 18 | --tooltip-fg: #ffffff; 19 | 20 | } 21 | 22 | html.dark { 23 | --bg: #20242b; 24 | --fg: #c9d1d9; 25 | --section-bg: #1a1f26; 26 | --border: #22303c; 27 | /* keep console as dark */ 28 | --console-bg: #0d1117; 29 | --console-fg: #c9d1d9; 30 | --input-bg: #1a1f26; 31 | --input-fg: #e6edf3; 32 | --input-border: #2b3a48; 33 | --input-focus: #3b82f6; 34 | --muted: #8b949e; 35 | --progress-track: #1f2a33; 36 | --progress-text: #c9d1d9; 37 | 38 | .bg-warning-subtle { 39 | background-color: #ffc10773 !important; 40 | } 41 | .form-check-input:not(:checked) { 42 | background-color: #ffffff; 43 | } 44 | 45 | /* Tooltip */ 46 | --tooltip-bg: #e6edf3; 47 | --tooltip-fg: #0f141a; 48 | 49 | } 50 | 51 | /* Gradient text animation */ 52 | .gradient-text { 53 | background: linear-gradient( 54 | 90deg, 55 | #667eea 0%, 56 | #764ba2 20%, 57 | #f093fb 40%, 58 | #4facfe 60%, 59 | #00f2fe 80%, 60 | #667eea 100% 61 | ); 62 | background-size: 200% auto; 63 | background-clip: text; 64 | -webkit-background-clip: text; 65 | -webkit-text-fill-color: transparent; 66 | animation: gradient-shift 5s ease-in-out infinite; 67 | font-weight: 500; 68 | } 69 | 70 | @keyframes gradient-shift { 71 | 0% { 72 | background-position: 0% center; 73 | } 74 | 100% { 75 | background-position: 200% center; 76 | } 77 | } 78 | 79 | /* Fallback for non-Bootstrap tooltip implementations */ 80 | .tooltip .tooltip-inner { background-color: var(--tooltip-bg); color: var(--tooltip-fg); } 81 | 82 | /* Bootstrap tooltip theming override: ensure arrow matches background */ 83 | .tooltip { 84 | --bs-tooltip-bg: var(--tooltip-bg); 85 | --bs-tooltip-color: var(--tooltip-fg); 86 | --bs-tooltip-opacity: 1; 87 | --bs-tooltip-arrow-color: var(--tooltip-bg); 88 | } 89 | 90 | body { 91 | padding: 16px; 92 | background: var(--bg); 93 | color: var(--fg); 94 | } 95 | 96 | /* Custom Dropdown */ 97 | .custom-dropdown { 98 | position: relative; 99 | width: 250px; 100 | } 101 | 102 | .custom-dropdown-toggle { 103 | width: 100%; 104 | padding: 8px 12px; 105 | background: var(--input-bg); 106 | border: 1px solid var(--input-border); 107 | border-radius: 10px; 108 | color: var(--fg); 109 | display: flex; 110 | align-items: center; 111 | gap: 8px; 112 | cursor: pointer; 113 | transition: all 0.2s; 114 | } 115 | 116 | .custom-dropdown-toggle:hover { 117 | border-color: var(--input-focus); 118 | } 119 | 120 | .custom-dropdown-toggle:focus { 121 | outline: none; 122 | border-color: var(--input-focus); 123 | box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); 124 | } 125 | 126 | .custom-dropdown.open .custom-dropdown-toggle { 127 | border-color: var(--input-focus); 128 | } 129 | 130 | .dropdown-icon { 131 | height: 2em; 132 | width: auto; 133 | vertical-align: middle; 134 | } 135 | 136 | .dropdown-text { 137 | flex: 1; 138 | } 139 | 140 | .custom-dropdown-menu { 141 | position: absolute; 142 | top: calc(100% + 4px); 143 | left: 0; 144 | width: 100%; 145 | background: var(--input-bg); 146 | border: 1px solid var(--input-border); 147 | border-radius: 6px; 148 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 149 | z-index: 1000; 150 | display: none; 151 | overflow: hidden; 152 | } 153 | 154 | .custom-dropdown.open .custom-dropdown-menu { 155 | display: block; 156 | } 157 | 158 | .custom-dropdown-item { 159 | padding: 8px 12px; 160 | display: flex; 161 | align-items: center; 162 | gap: 8px; 163 | cursor: pointer; 164 | transition: background-color 0.15s; 165 | } 166 | 167 | .custom-dropdown-item:hover { 168 | background-color: rgba(13, 110, 253, 0.1); 169 | } 170 | 171 | .custom-dropdown-item.selected { 172 | background-color: rgba(13, 110, 253, 0.15); 173 | font-weight: 500; 174 | } 175 | 176 | .section { 177 | border: 1px solid var(--border); 178 | padding: 12px; 179 | border-radius: 6px; 180 | margin-bottom: 12px; 181 | background: var(--section-bg); 182 | } 183 | 184 | .console-wrap { 185 | resize: vertical; 186 | overflow: auto; 187 | border: 1px solid var(--border); 188 | border-radius: 6px; 189 | background: var(--console-bg); 190 | height: 220px; 191 | } 192 | 193 | /* Hide RX/TX lines when toggles are off */ 194 | .console-wrap.hide-rx .log-rx { display: none; } 195 | .console-wrap.hide-tx .log-tx { display: none; } 196 | 197 | #log { 198 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 199 | font-size: 12px; 200 | color: var(--console-fg); 201 | padding: 8px; 202 | } 203 | 204 | .log-line { white-space: pre; } 205 | 206 | /* Log colors - light default */ 207 | .log-app { color: #0d6efd; } 208 | .log-rx { color: #198754; } 209 | .log-tx { color: #b8860b; } 210 | 211 | /* Log colors - dark overrides */ 212 | html.dark .log-app { color: #9cdcfe; } 213 | html.dark .log-rx { color: #7ee787; } 214 | html.dark .log-tx { color: #e3b341; } 215 | 216 | .form-readonly input { background: var(--input-bg); color: var(--input-fg); } 217 | 218 | .btn { display: inline-flex; align-items: center; white-space: nowrap; } 219 | .btn .bi { line-height: 1; } 220 | 221 | /* Lightweight modal */ 222 | .modal-backdrop-lite { 223 | position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); 224 | display: flex; align-items: center; justify-content: center; z-index: 1050; 225 | } 226 | 227 | .modal-card-lite { 228 | background: var(--section-bg); 229 | border-radius: 8px; 230 | box-shadow: 0 10px 30px rgba(0, 0, 0, .2); 231 | width: min(640px, calc(100vw - 32px)); 232 | max-width: 70vw; 233 | max-height: 70vh; 234 | width: 100%; 235 | height: auto; 236 | overflow: hidden; 237 | } 238 | 239 | .modal-card-lite .modal-header { padding: .75rem 1rem; border-bottom: 1px solid var(--border); } 240 | .modal-card-lite .modal-body { padding: 1rem; } 241 | .modal-card-lite .modal-footer { padding: .75rem 1rem; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: .5rem; } 242 | 243 | /* Theme toggle icons */ 244 | .theme-icon-moon { display: none; } 245 | html.dark .theme-icon-sun { display: none; } 246 | html.dark .theme-icon-moon { display: inline; } 247 | 248 | /* Inputs/selects in themes */ 249 | .form-control, .form-select { 250 | background-color: var(--input-bg); 251 | color: var(--input-fg); 252 | border-color: var(--input-border); 253 | } 254 | 255 | .form-control:focus, .form-select:focus { 256 | border-color: var(--input-focus); 257 | box-shadow: 0 0 0 .2rem rgba(13, 110, 253, 0.25); 258 | background-color: var(--input-bg); 259 | color: var(--input-fg); 260 | caret-color: var(--input-fg); 261 | } 262 | .form-control:active { background-color: var(--input-bg); color: var(--input-fg); } 263 | 264 | .form-control::placeholder { color: color-mix(in srgb, var(--input-fg) 50%, transparent); opacity: .7; } 265 | 266 | .form-control:disabled, .form-control[readonly] { 267 | background-color: color-mix(in srgb, var(--input-bg) 90%, transparent); 268 | color: color-mix(in srgb, var(--input-fg) 70%, transparent); 269 | border-color: var(--input-border); 270 | opacity: 1; 271 | } 272 | 273 | .form-select:disabled { 274 | background-color: color-mix(in srgb, var(--input-bg) 90%, transparent); 275 | color: color-mix(in srgb, var(--input-fg) 70%, transparent); 276 | border-color: var(--input-border); 277 | } 278 | 279 | /* Checkbox/switch theming */ 280 | .form-check-input { background-color: var(--input-bg); border-color: var(--input-border); } 281 | .form-check-input:checked { background-color: #0d6efd; border-color: #0d6efd; } 282 | .form-check-input:disabled { background-color: color-mix(in srgb, var(--input-bg) 90%, transparent); } 283 | 284 | /* Light theme: keep disabled switches visible with subtle primary tint */ 285 | html:not(.dark) .form-check-input:disabled { background-color: #cfe2ff; border-color: #cfe2ff; opacity: 1; } 286 | html:not(.dark) .form-check-input:checked:disabled { background-color: #9ec5fe; border-color: #9ec5fe; } 287 | 288 | /* Muted text in dark */ 289 | .text-muted { color: var(--muted) !important; } 290 | 291 | /* Make dark buttons readable in dark theme */ 292 | html.dark .btn-dark { background-color: #f8f9fa; color: #212529; border-color: #f8f9fa; } 293 | html.dark .btn-dark:hover { background-color: #e9ecef; border-color: #e9ecef; } 294 | html.dark .btn-outline-dark { color: #f8f9fa; border-color: #f8f9fa; } 295 | html.dark .btn-outline-dark:hover { background-color: #f8f9fa; color: #212529; border-color: #f8f9fa; } 296 | 297 | /* Large slider toggle with sun/moon */ 298 | .theme-toggle { position: relative; width: 64px; height: 32px; } 299 | .theme-toggle input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; z-index: 2; } 300 | .theme-toggle-label { display: flex; align-items: center; justify-content: space-between; width: 100%; height: 100%; padding: 2px 8px 0 6px; border: 1px solid var(--border); border-radius: 999px; background: var(--section-bg); position: relative; user-select: none; } 301 | .theme-toggle-label .bi { font-size: 1.1rem; line-height: 1; } 302 | .theme-toggle .handle { position: absolute; top: 2px; left: 2px; width: 26px; height: 26px; border-radius: 50%; background: var(--bg); border: 1px solid var(--border); transition: transform .2s ease-in-out; } 303 | .theme-toggle input:checked + .theme-toggle-label .handle { transform: translateX(32px); } 304 | 305 | /* Hide native file button; keep only the field */ 306 | input[type="file"].form-control::file-selector-button { display: none; } 307 | input[type="file"].form-control::-webkit-file-upload-button { visibility: hidden; } 308 | input[type="file"].form-control { color: var(--input-fg); background-color: var(--input-bg); } 309 | input[type="file"].form-control:focus { background-color: var(--input-bg); color: var(--input-fg); } 310 | 311 | /* Autofill theming to avoid white background */ 312 | input:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill { 313 | -webkit-text-fill-color: var(--input-fg); 314 | -webkit-box-shadow: 0 0 0px 1000px var(--input-bg) inset; 315 | box-shadow: 0 0 0px 1000px var(--input-bg) inset; 316 | transition: background-color 9999s ease-out 0s; 317 | } 318 | 319 | /* Buttons disabled state readable in dark */ 320 | .btn:disabled, .btn.disabled { opacity: .6; } 321 | 322 | /* Progress bars track and text */ 323 | .progress { background-color: var(--progress-track); } 324 | .progress .progress-bar { color: var(--progress-text); } 325 | 326 | /* Footer */ 327 | .app-footer { border-top: 1px solid var(--border); color: var(--muted); } 328 | .app-footer a { color: inherit; } 329 | .app-footer a:hover { color: var(--fg); } 330 | .app-footer .bi { font-size: 1.1rem; } 331 | 332 | /* Actions spacing when wrapped */ 333 | .actions-grid > .btn { margin-bottom: 1rem;} 334 | 335 | .radio-group { 336 | display: flex; 337 | flex-wrap: wrap; 338 | justify-content: center; 339 | gap: 1rem; 340 | } 341 | 342 | .radio-group > .d-flex { 343 | justify-content: center; 344 | min-width: 180px; 345 | } 346 | 347 | @media (max-width: 640px) { 348 | .radio-group { 349 | flex-direction: column; 350 | align-items: stretch; 351 | } 352 | .radio-group > .d-flex { 353 | width: 100%; 354 | } 355 | } 356 | 357 | 358 | /* Mobile tweaks */ 359 | @media (max-width: 576px) { 360 | /* 1. NVRAM buttons centered in column with auto width */ 361 | .nv-actions { 362 | flex-direction: column; 363 | align-items: center; 364 | } 365 | .nv-actions > .btn { 366 | width: auto; 367 | margin-bottom: 0.5rem; 368 | } 369 | 370 | /* 2. Actions buttons spacing already handled via margin */ 371 | 372 | /* 3. Modal fits screen */ 373 | .modal-card-lite { 374 | width: calc(100vw - 16px); 375 | margin: 0 8px; 376 | max-width: 98vw; 377 | max-height: 98vh; 378 | } 379 | 380 | /* 4. Serial: hide controls, show note */ 381 | /* .serial-controls { display: none; } */ 382 | .serial-note { display: block !important; } 383 | 384 | /* 5. Mobile: show TCP panel by default; user can hide it by adding .tcp-hidden on or */ 385 | /* #tcpSettingsPanel { display: block; } 386 | html.tcp-hidden #tcpSettingsPanel, 387 | body.tcp-hidden #tcpSettingsPanel { display: none !important; } */ 388 | } 389 | -------------------------------------------------------------------------------- /xzg-multi-tool-addon/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.3.4 2 | 3 | ## 🚀 Features 4 | 5 | - feat: Telink UART-SWire emulation protocol (Telink) (#91) by @xyzroe 6 | - feat: Telink UART2SWire protocol (Telink) (#91) by @xyzroe 7 | - feat: universal makeReset and enterBootloader functions, more robust work (TI & SL). Thanks [FerrumLogic](https://github.com/FerrumLogic) https://github.com/xyzroe/XZG-MT/issues/90 8 | 9 | ## 🐛 Bug Fixes 10 | 11 | - fix: TCP bridge work (TI). Thanks [axodentally](https://github.com/axodentally) https://github.com/xyzroe/XZG-MT/issues/90 12 | - fix: small UI improvements (#91) by @xyzroe 13 | - fix: some code cleanup (#91) by @xyzroe 14 | 15 | ## 📘 Documentation 16 | 17 | - docs: how-to about Telink devices (#91) by @xyzroe 18 | 19 | 20 | ## v0.3.3 21 | 22 | ## 🚀 Features 23 | 24 | - feat: add CSS and JS optimization, deleted unnecessary files to reduce total size (#88) by @xyzroe 25 | 26 | ## 🐛 Bug Fixes 27 | 28 | - fix: update CSS optimization script and add purgecss configuration (#89) by @xyzroe 29 | 30 | ## 📘 Documentation 31 | 32 | - docs: add issue templates (#87) by @xyzroe 33 | 34 | ## v0.3.2 35 | 36 | ## 🚀 Features 37 | 38 | - feat: Arduino flashing support (#86) by @xyzroe 39 | - feat: dropdown in family selection (#86) by @xyzroe 40 | 41 | ## 🐛 Bug Fixes 42 | 43 | - fix: small UI fixes (#86) by @xyzroe 44 | 45 | ## 📘 Documentation 46 | 47 | - docs: update CC Loader how-to about Arduino (#86) by @xyzroe 48 | 49 | ## v0.3.1 50 | 51 | ## 🚀 Features 52 | 53 | - feat: added IEEE address read/write support for TI CC2538/CC2652 (#84) by @xyzroe 54 | - feat: add read/write IEEE (SL); (#85) by @xyzroe 55 | - feat: manufacturer and model determine (SL, only NCP fw support) (#85) by @xyzroe 56 | - feat: removed external style/font dependencies (#84) by @xyzroe 57 | 58 | ## 📘 Documentation 59 | 60 | - feat: manufacturer and model determine (SL, only NCP fw support) (#85) by @xyzroe 61 | 62 | ## v0.3.0 63 | 64 | ## 🚀 Features 65 | 66 | - feat: retry connect with different option if chip id read error (TI & SL) (#83) by @xyzroe 67 | - feat: version detect for MultiPAN (CPC), OpenThred (Spinel) and Router firmwares (SL) (#83) by @xyzroe 68 | - feat: verison detect for OpenThread firmwares (TI) (#83) by @xyzroe 69 | - feat: auto find baud rate (SL) (#83) by @xyzroe 70 | - feat: save and load find baud rate toggle in cookies (#83) by @xyzroe 71 | - feat: save baud rate value in cookies (#83) by @xyzroe 72 | 73 | ## 🐛 Bug Fixes 74 | 75 | - fix: improve auto find baud rate (TI) (#83) by @xyzroe 76 | - fix: setLinesHandler instead of direct import (SL & CCLoader) (#83) by @xyzroe 77 | - fix: some code cleanup (#83) by @xyzroe 78 | - fix: implement saveToFile utility for unify file downloads (#83) by @xyzroe 79 | - 80 | 81 | ## v0.2.24 82 | 83 | ## 🚀 Features 84 | 85 | - feat: add dump flash option to CCXX52 devices (#82) by @xyzroe 86 | - feat: hide unrelated option in ESP section (#79) by @xyzroe 87 | 88 | ## 🐛 Bug Fixes 89 | 90 | - fix: massive code cleanup (#82) by @xyzroe 91 | - fix: improve progress logging and update checkbox handling for CC Debugger (#81) by @xyzroe 92 | 93 | ## 📘 Documentation 94 | 95 | - docs: update release drafter version and make auto labeler work (#80) by @xyzroe 96 | - docs: update release drafter configuration (#78) by @xyzroe 97 | 98 | ## v0.2.23 99 | 100 | ## 🚀 Features 101 | 102 | - feat: add separate imply gate and original logic to SL reset and boot loader (#77) by @xyzroe 103 | - feat: bridge: separate function to trigger DTR and RTS (#77) by @xyzroe 104 | - feat: bridge: support trigger both using one request (#77) by @xyzroe 105 | - feat: enhance debugger and loader connection UI, update firmware source links (#70) by @xyzroe 106 | - feat: add workflow to update GHCR downloads (#66) by @xyzroe 107 | - feat: enhance debugger and loader connection UI, update firmware source links (#70) by @xyzroe 108 | 109 | ## 🐛 Bug Fixes 110 | 111 | - fix: make possible to flash dongles with imply gate logic vie bridge (#77) by @xyzroe 112 | - fix: refactor SL tools and TI tools integration (#77) by @xyzroe 113 | - fix: small UI enhancements (#77) by @xyzroe 114 | - fix: bridge: logs with timestamp (#77) by @xyzroe 115 | 116 | ## 📘 Documentation 117 | 118 | - docs: some docs improvements (#77) by @xyzroe 119 | - docs: update .gitignore and update telegram banner (#76) by @xyzroe 120 | - docs: refactor documentation (#75) by @xyzroe 121 | - docs: update how-to guides structure by renaming index.md (#74) by @xyzroe 122 | - docs: update READMEs for improved clarity and consistency in section headings (#73) by @xyzroe 123 | - docs: First how-to guides, bridge diagram, rework some files (#72) by @xyzroe 124 | - docs: move supported chips list and notes to a separate file (#71) by @xyzroe 125 | - docs: improve documentation and add GHCR pulls badge (#69) by @xyzroe 126 | - docs: update badge message formatting for total downloads (#68) by @xyzroe 127 | - docs: correct script path in GHCR downloads badge workflow (#67) by @xyzroe 128 | - docs: reorder and clean up CHANGELOG entries for clarity (#65) by @xyzroe 129 | 130 | ## v0.2.22 131 | 132 | ## 🚀 Features 133 | 134 | - feat: Implement CC Loader module for flashing CC2530 family devices via ESP board as flasher interface. (#64) by @xyzroe 135 | - feat: Integrate cloud firmware repository listing for ESP platforms (currently scoped to CC Loader). (#64) by @xyzroe 136 | - feat: enhance mobile view (#63) by @xyzroe 137 | - feat: update footer with trademark notice and adjust dark theme colors (#57) by @xyzroe 138 | - feat: SmartRF04EB support and SLS presets (#56) by @xyzroe 139 | 140 | ## 🐛 Bug Fixes 141 | 142 | - fix: Reset flash option checkboxes upon deselection of local firmware file (#64) by @xyzroe 143 | - fix: comment out serial controls and note in CSS (#59) by @xyzroe 144 | - fix: update .gitignore to include some local files (#58) by @xyzroe 145 | 146 | ##📘 Documentation 147 | 148 | - docs: Update README and UI with additional information on CC253X Debugger and Loader support (#64) by @xyzroe 149 | 150 | ## v0.2.21 151 | 152 | ## 🚀 Features 153 | 154 | - feat: implement support for the CC2530 family (#55) by @xyzroe 155 | 156 | ## v0.2.20 157 | 158 | ## 🚀 Features 159 | 160 | - feat: automatic select BSL and RST GPIOs if existing (#54) by @xyzroe 161 | - feat: categories inside cloud FW list (#54) by @xyzroe 162 | - feat: individual accepted local file extensions for each family (#54) by @xyzroe 163 | - feat: cloud firmware list for Silicon Labs chips (#54) by @xyzroe 164 | - feat: update firmware manifest URL and add CC2538 support for cloud firmware list (#51) by @xyzroe 165 | 166 | ## 🐛 Bug Fixes 167 | 168 | - fix: reworked mechanism of applying URLs based on templates (#54) by @xyzroe 169 | - fix: clear devices list if no connection to the bridge (#54) by @xyzroe 170 | - fix: cloud firmware list sorting. newest > oldest. (#54) by @xyzroe 171 | - fix: improved SL flashing process. (#54) by @xyzroe 172 | - fix: some code clean up (#54) by @xyzroe 173 | - small fixes in bridge, code cleanup (#53) by @xyzroe 174 | - fix: closing serial port after socket disconnect (#52) by @xyzroe 175 | 176 | ## 📘 Documentation 177 | 178 | - docs: update README to include ESP32 support and improve project structure (#50) by @xyzroe 179 | 180 | ## v0.2.19 181 | 182 | ## 🚀 Features 183 | 184 | - feat: Initial support of all ESP32 chips (#49) by @xyzroe 185 | 186 | ## 🐛 Bug Fixes 187 | 188 | - fix: code clean up and reorganization (#49) by @xyzroe 189 | - fix: many lint errors (#49) by @xyzroe 190 | 191 | ## v0.2.18 192 | 193 | ## 🚀 Features 194 | 195 | - feat: add support of TI CC2538 (#47) by @xyzroe 196 | 197 | ## 🐛 Bug Fixes 198 | 199 | - fix: verify CRC after flashing TI chips (#47) by @xyzroe 200 | - fix: some code cleanup (#47) by @xyzroe 201 | 202 | ## v0.2.17 203 | 204 | ## 🚀 Features 205 | 206 | #### Web UI 207 | 208 | - feat: one global "invert levels" switch, instead of two separate (#46) by @xyzroe 209 | 210 | #### GitHub 211 | 212 | - feat: implement separate task for release notification (#41) by @xyzroe 213 | 214 | ## 🐛 Bug Fixes 215 | 216 | #### Web UI 217 | 218 | - fix: GPIOs group title in drop down lists (#46) by @xyzroe 219 | - fix: improve BSL and RST logic for remote connections (#46) by @xyzroe 220 | - fix: remove reset BSL and RST URLs while changing the port. (#46) by @xyzroe 221 | 222 | #### Bridge 223 | 224 | - fix: deprecate SERIAL_SCAN_INTERVAL option and update related documentation (#45) by @xyzroe 225 | - fix: don't showing non-existent serial ports (#45) by @xyzroe 226 | - fix: errors during intensive serial-tcp communication (#45) by @xyzroe 227 | 228 | #### GitHub 229 | 230 | - fix: update Telegram notification message format to use MarkdownV2 (#44) by @xyzroe 231 | - fix: correct photo URL in Telegram and Discord notifications (#43) by @xyzroe 232 | - fix: enhance notification to include photo and update message format (#42) by @xyzroe 233 | 234 | ## v0.2.16 235 | 236 | ## 🚀 Features 237 | 238 | - initial support of SL (#40) by @xyzroe 239 | - feat: add notification step for Telegram and Discord after release (#39) by @xyzroe 240 | 241 | ## 🐛 Bug Fixes 242 | 243 | - initial support of SL (#40) by @xyzroe 244 | - refactor: remove legacy version update steps from build workflow (#38) by @xyzroe 245 | - refactor: update ControlConfig to use pinControl instead of remote (#37) by @xyzroe 246 | - remove node.js bridge (#36) by @xyzroe 247 | 248 | ## v0.2.14 249 | 250 | ## 🚀 Features 251 | 252 | - feat: some small adjustments (#34) by @xyzroe 253 | - feat: add support for CC1352P7 chip (#32) by @xyzroe 254 | 255 | ## 🐛 Bug Fixes 256 | 257 | - feat: some small adjustments (#34) by @xyzroe 258 | - fix: improve HEX parsing logic (#33) by @xyzroe 259 | 260 | ## v0.2.13 261 | 262 | ## 🚀 Features 263 | 264 | - feat: add support for CC1352P7 chip (#32) by @xyzroe 265 | 266 | ## 🐛 Bug Fixes 267 | 268 | - fix: add support for custom GPIOs' paths (#31) by @xyzroe 269 | 270 | ## v0.2.12 271 | 272 | ## 🐛 Bug Fixes 273 | 274 | - fix: add support for custom GPIOs' paths (#31) by @xyzroe 275 | 276 | ## v0.2.11 277 | 278 | - docs: enhance README files (#28) by @xyzroe 279 | 280 | ## 🚀 Features 281 | 282 | - feat: add imply gate logic for BSL enter (#30) by @xyzroe 283 | - feat: more comfortable selection of GPIOs (#27) by @xyzroe 284 | 285 | ## v0.2.10 286 | 287 | ## 🚀 Features 288 | 289 | - feat: more comfortable selection of GPIOs (#27) by @xyzroe 290 | 291 | ## v0.2.8 292 | 293 | ## 🚀 Features 294 | 295 | - feat: update release drafter configuration and streamline build workflow (#25) by @xyzroe 296 | - feat: add more build configurations (MIPS, ARM) for binaries and Docker images (#24) by @xyzroe 297 | 298 | ## 🐛 Bug Fixes 299 | 300 | - fix: embed handling on Windows (#24) by @xyzroe 301 | - fix: Print version while start in Docker images (#18) by @xyzroe 302 | 303 | ## 📚 Documentation 304 | 305 | - chore: update README.md to enhance project description (#19, #20, #21, #22, #23) by @xyzroe 306 | - chore: update CHANGELOG for v0.2.7 release (#17) by @xyzroe 307 | 308 | ## v0.2.8 309 | 310 | ## 📚 Documentation 311 | 312 | - chore: update README.md to enhance project description (#19, #20, #21, #22, #23) by @xyzroe 313 | - chore: update CHANGELOG for v0.2.7 release (#17) by @xyzroe 314 | 315 | ## 🚀 Features 316 | 317 | - feat: add more build configurations (MIPS, ARM) for binaries and Docker images (#24) by @xyzroe 318 | 319 | ## 🐛 Bug Fixes 320 | 321 | - fix: embed handling on Windows (#24) by @xyzroe 322 | - fix: Print version while start in Docker images (#18) by @xyzroe 323 | 324 | ## v0.2.7 325 | 326 | ### 🚀 Features 327 | 328 | - feat: First Go only bridge release 329 | 330 | ### 🐛 Bug Fixes 331 | 332 | - fix: update output filename format in build script (#16) by @xyzroe 333 | 334 | ## v0.2.6 335 | 336 | ### 🚀 Features 337 | 338 | - feat: Add a Go implementation of Bridge. Reduce size! Binaries and Docker images (#12) by @xyzroe. 339 | - feat: UI impoves (#11) by @xyzroe 340 | 341 | ### 🐛 Bug Fixes 342 | 343 | - fix: update Go version to 1.21 in build workflow (#15) by @xyzroe 344 | - fix: update artifact download steps to specify names for node and go (#14) by @xyzroe 345 | - fix: update job dependencies in build workflow and (#13) by @xyzroe 346 | - feat: UI impoves (#11) by @xyzroe 347 | 348 | ## v0.2.3 349 | 350 | ### 🚀 Features 351 | 352 | - fix: use actual version while web page build (#10) by @xyzroe 353 | 354 | ### 🐛 Bug Fixes 355 | 356 | - fix: use actual version while web page build (#10) by @xyzroe 357 | 358 | ## v0.2.2 359 | 360 | ### 🐛 Bug Fixes 361 | 362 | - fix: update checkout references to use new SHA from version bump step (#9) by @xyzroe 363 | 364 | ## v0.2.1 365 | 366 | - fix: serial module import; feature: some UI enhancements (#8) by @xyzroe 367 | 368 | ### 🐛 Bug Fixes 369 | 370 | - fix: update name-template in release-drafter.yml (#7) by @xyzroe 371 | - fix: prepend 'v' to version headers and some cleanup in CHANGELOG (#6) by @xyzroe 372 | - fix: correct workflow_dispatch indentation in draft-release-notes.yml (#5) by @xyzroe 373 | - fix: update protocol options in documentation to remove mistakes (#4) by @xyzroe 374 | 375 | ## v0.2.0 376 | 377 | ### 🚀 Features 378 | 379 | - fix: update permissions to include pull-requests read access (#1) by @xyzroe 380 | 381 | ### 🐛 Bug Fixes 382 | 383 | - fix: conditionally log debug messages (#2) by @xyzroe 384 | 385 | ## v0.1.8 386 | 387 | - Initial public release 388 | --------------------------------------------------------------------------------