├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── create-release.yml │ ├── npmjs-release.yml │ ├── stale.yml │ └── update-firmware-links.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── bin ├── dev.cmd ├── dev.js ├── run.cmd └── run.js ├── biome.json ├── firmware-links-v3.json ├── firmware-links.json ├── package-lock.json ├── package.json ├── src ├── commands │ ├── bootloader │ │ └── index.ts │ ├── monitor │ │ └── index.ts │ ├── router │ │ └── index.ts │ ├── sniff │ │ └── index.ts │ ├── stack │ │ └── index.ts │ └── utils │ │ └── index.ts ├── index.ts └── utils │ ├── bootloader.ts │ ├── consts.ts │ ├── cpc.ts │ ├── ember.ts │ ├── enums.ts │ ├── port.ts │ ├── router-endpoints.ts │ ├── spinel.ts │ ├── transport.ts │ ├── types.ts │ ├── update-firmware-links.ts │ ├── utils.ts │ ├── wireshark.ts │ └── xmodem.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Nerivec 4 | buy_me_a_coffee: Nerivec 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Do this 16 | 2. Do that 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Setup (please complete the following information):** 26 | - Machine: [e.g. Intel NUC, RPI] 27 | - OS: [e.g. debian 12.9] 28 | - Container: [e.g. no, Docker 27] 29 | - Ember ZLI: [e.g. 2.11.0] 30 | 31 | 39 | 40 | **Coordinator/Adapter (please complete the following information):** 41 | - Manufacturer: [e.g. Silabs, TI] 42 | - Firmware Version (or link to firmware file): [e.g. 2.5.2.0_GitHub-1fceb225b] 43 | 44 | **Additional context** 45 | 46 | **Logs** 47 | Add relevant `debug` logs 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | commit-message: 5 | prefix: fix 6 | versioning-strategy: increase 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | groups: 11 | production-dependencies: 12 | applies-to: version-updates 13 | dependency-type: "production" 14 | update-types: 15 | - "minor" 16 | - "patch" 17 | development-dependencies: 18 | applies-to: version-updates 19 | dependency-type: "development" 20 | update-types: 21 | - "minor" 22 | - "patch" 23 | - package-ecosystem: github-actions 24 | commit-message: 25 | prefix: chore 26 | directory: "/" 27 | schedule: 28 | interval: weekly 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | ci: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v5 16 | 17 | - uses: actions/setup-node@v6 18 | with: 19 | node-version-file: "package.json" 20 | 21 | - run: npm ci 22 | - run: npm run check:ci 23 | - run: npm run build 24 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create tag and tarballs release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | inputs: 8 | force: 9 | description: "Force update release if tag already exists (re-adds tarballs from scratch)" 10 | required: false 11 | default: false 12 | type: boolean 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | check-version: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v5 22 | 23 | - name: Check if version already exists 24 | id: version-check 25 | run: | 26 | tag="v$(jq -r < package.json .version)" 27 | exists=$(gh api repos/${{ github.repository }}/releases/tags/$tag >/dev/null 2>&1 && echo "true" || echo "") 28 | 29 | if [ -n "$exists" ]; 30 | then 31 | echo "Version $tag already exists" 32 | echo "::warning file=package.json,line=1::Version $tag already exists." 33 | echo "skip=true" >> $GITHUB_OUTPUT 34 | else 35 | echo "Version $tag does not exist." 36 | echo "skip=false" >> $GITHUB_OUTPUT 37 | fi 38 | 39 | echo "tag=$tag" >> $GITHUB_OUTPUT 40 | env: 41 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | outputs: 43 | skip: ${{ steps.version-check.outputs.skip }} 44 | tag: ${{ steps.version-check.outputs.tag }} 45 | 46 | create-release: 47 | runs-on: ubuntu-latest 48 | permissions: 49 | contents: write 50 | needs: [check-version] 51 | if: | 52 | !fromJSON(needs.check-version.outputs.skip) || fromJSON(inputs.force || false) 53 | steps: 54 | - uses: actions/checkout@v5 55 | 56 | - uses: actions/setup-node@v6 57 | with: 58 | node-version-file: "package.json" 59 | 60 | - run: npm install -g oclif 61 | 62 | - run: npm ci 63 | - run: npm run build:prod 64 | 65 | - run: oclif pack tarballs -r . --prune-lockfiles 66 | 67 | - name: Create Github Release 68 | uses: ncipollo/release-action@v1 69 | with: 70 | name: ${{ needs.check-version.outputs.tag }} 71 | tag: ${{ needs.check-version.outputs.tag }} 72 | commit: ${{ github.ref_name }} 73 | generateReleaseNotes: true 74 | makeLatest: true 75 | allowUpdates: true 76 | removeArtifacts: true 77 | artifacts: "./dist/ember-zli-*" 78 | -------------------------------------------------------------------------------- /.github/workflows/npmjs-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to npmjs 2 | 3 | on: 4 | # workflow_run: 5 | # workflows: [Create tag and tarballs release] 6 | # types: 7 | # - completed 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | id-token: write 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} 18 | permissions: 19 | contents: read 20 | id-token: write 21 | steps: 22 | - uses: actions/checkout@v5 23 | 24 | # Setup .npmrc file to publish to npm 25 | - uses: actions/setup-node@v6 26 | with: 27 | node-version: 24 28 | registry-url: "https://registry.npmjs.org" 29 | 30 | - name: Update NPM 31 | run: npm install -g npm@latest 32 | 33 | - run: npm ci 34 | 35 | - run: npm publish --provenance --access public 36 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues and PRs" 2 | on: 3 | schedule: 4 | - cron: "30 4 * * *" 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | stale: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | # contents: write # only for delete-branch option 14 | issues: write 15 | pull-requests: write 16 | steps: 17 | - uses: actions/stale@v10 18 | with: 19 | stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." 20 | stale-pr-message: "This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." 21 | days-before-stale: 30 22 | days-before-close: 5 23 | exempt-issue-labels: dont-stale 24 | exempt-pr-labels: dont-stale 25 | -------------------------------------------------------------------------------- /.github/workflows/update-firmware-links.yml: -------------------------------------------------------------------------------- 1 | name: Update firmware links 2 | 3 | on: 4 | schedule: 5 | - cron: "0 12 1 * *" 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | update-fw-links: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | steps: 17 | - uses: actions/checkout@v5 18 | 19 | - uses: actions/setup-node@v6 20 | with: 21 | node-version-file: "package.json" 22 | 23 | - run: npm ci 24 | - run: npm run build 25 | - run: npm run update-fw-links 26 | 27 | - name: Commit changes 28 | run: | 29 | git config --global user.name 'github-actions[bot]' 30 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 31 | git add . 32 | git commit -m "Update firmware links" || echo 'Nothing to commit' 33 | git push 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | **/.DS_Store 4 | /.idea 5 | /dist 6 | /tmp 7 | /node_modules 8 | /coverage 9 | oclif.manifest.json 10 | *.tsbuildinfo 11 | 12 | 13 | yarn.lock 14 | pnpm-lock.yaml 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ember ZLI 2 | ================= 3 | 4 | Interact with EmberZNet-based adapters using zigbee-herdsman 'ember' driver. Also supports bootloading to/from CPC and Spinel protocols. 5 | 6 | [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) 7 | [![Version](https://img.shields.io/npm/v/ember-zli.svg)](https://npmjs.org/package/ember-zli) 8 | [![Downloads](https://img.shields.io/npm/dt/ember-zli.svg)](https://npmjs.org/package/ember-zli) 9 | [![ci](https://github.com/Nerivec/ember-zli/actions/workflows/ci.yml/badge.svg)](https://github.com/Nerivec/ember-zli/actions/workflows/ci.yml) 10 | 11 | > [!IMPORTANT] 12 | > `ember-zli` uses the `ember` driver from [zigbee-herdsman](https://github.com/Koenkk/zigbee-herdsman) under the hood. As such, it roughly has the same firmware requirements as [Zigbee2MQTT ember](https://www.zigbee2mqtt.io/guide/adapters/emberznet.html); firmware 7.4.x minimum. 13 | 14 | ### Available Interactive Menus 15 | 16 | See https://github.com/Nerivec/ember-zli/wiki 17 | 18 | # ToC 19 | 20 | 21 | * [ToC](#toc) 22 | * [Usage](#usage) 23 | * [Commands](#commands) 24 | 25 | # Usage 26 | 27 | ```sh-session 28 | $ npm install -g ember-zli 29 | $ ember-zli COMMAND 30 | running command... 31 | $ ember-zli (--version) 32 | ember-zli/3.2.0 linux-x64 node-v24.9.0 33 | $ ember-zli --help [COMMAND] 34 | USAGE 35 | $ ember-zli COMMAND 36 | ... 37 | ``` 38 | 39 | # Commands 40 | 41 | * [`ember-zli bootloader`](#ember-zli-bootloader) 42 | * [`ember-zli help [COMMAND]`](#ember-zli-help-command) 43 | * [`ember-zli monitor`](#ember-zli-monitor) 44 | * [`ember-zli router`](#ember-zli-router) 45 | * [`ember-zli sniff`](#ember-zli-sniff) 46 | * [`ember-zli stack`](#ember-zli-stack) 47 | * [`ember-zli utils`](#ember-zli-utils) 48 | * [`ember-zli version`](#ember-zli-version) 49 | 50 | ## `ember-zli bootloader` 51 | 52 | Interact with the Gecko bootloader in the adapter. 53 | 54 | ``` 55 | USAGE 56 | $ ember-zli bootloader 57 | 58 | DESCRIPTION 59 | Interact with the Gecko bootloader in the adapter. 60 | 61 | EXAMPLES 62 | $ ember-zli bootloader 63 | ``` 64 | 65 | _See code: [src/commands/bootloader/index.ts](https://github.com/Nerivec/ember-zli/blob/v3.2.0/src/commands/bootloader/index.ts)_ 66 | 67 | ## `ember-zli help [COMMAND]` 68 | 69 | Display help for ember-zli. 70 | 71 | ``` 72 | USAGE 73 | $ ember-zli help [COMMAND...] [-n] 74 | 75 | ARGUMENTS 76 | [COMMAND...] Command to show help for. 77 | 78 | FLAGS 79 | -n, --nested-commands Include all nested commands in the output. 80 | 81 | DESCRIPTION 82 | Display help for ember-zli. 83 | ``` 84 | 85 | _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.34/src/commands/help.ts)_ 86 | 87 | ## `ember-zli monitor` 88 | 89 | Monitor the chosen port in the console. 90 | 91 | ``` 92 | USAGE 93 | $ ember-zli monitor 94 | 95 | DESCRIPTION 96 | Monitor the chosen port in the console. 97 | 98 | EXAMPLES 99 | $ ember-zli monitor 100 | ``` 101 | 102 | _See code: [src/commands/monitor/index.ts](https://github.com/Nerivec/ember-zli/blob/v3.2.0/src/commands/monitor/index.ts)_ 103 | 104 | ## `ember-zli router` 105 | 106 | Use a coordinator firmware as a router and interact with the joined network. 107 | 108 | ``` 109 | USAGE 110 | $ ember-zli router 111 | 112 | DESCRIPTION 113 | Use a coordinator firmware as a router and interact with the joined network. 114 | 115 | EXAMPLES 116 | $ ember-zli router 117 | ``` 118 | 119 | _See code: [src/commands/router/index.ts](https://github.com/Nerivec/ember-zli/blob/v3.2.0/src/commands/router/index.ts)_ 120 | 121 | ## `ember-zli sniff` 122 | 123 | Sniff Zigbee traffic (to Wireshark, to PCAP file, to custom handler or just log raw data). 124 | 125 | ``` 126 | USAGE 127 | $ ember-zli sniff 128 | 129 | DESCRIPTION 130 | Sniff Zigbee traffic (to Wireshark, to PCAP file, to custom handler or just log raw data). 131 | 132 | EXAMPLES 133 | $ ember-zli sniff 134 | ``` 135 | 136 | _See code: [src/commands/sniff/index.ts](https://github.com/Nerivec/ember-zli/blob/v3.2.0/src/commands/sniff/index.ts)_ 137 | 138 | ## `ember-zli stack` 139 | 140 | Interact with the EmberZNet stack in the adapter. 141 | 142 | ``` 143 | USAGE 144 | $ ember-zli stack 145 | 146 | DESCRIPTION 147 | Interact with the EmberZNet stack in the adapter. 148 | 149 | EXAMPLES 150 | $ ember-zli stack 151 | ``` 152 | 153 | _See code: [src/commands/stack/index.ts](https://github.com/Nerivec/ember-zli/blob/v3.2.0/src/commands/stack/index.ts)_ 154 | 155 | ## `ember-zli utils` 156 | 157 | Execute various utility commands. 158 | 159 | ``` 160 | USAGE 161 | $ ember-zli utils 162 | 163 | DESCRIPTION 164 | Execute various utility commands. 165 | 166 | EXAMPLES 167 | $ ember-zli utils 168 | ``` 169 | 170 | _See code: [src/commands/utils/index.ts](https://github.com/Nerivec/ember-zli/blob/v3.2.0/src/commands/utils/index.ts)_ 171 | 172 | ## `ember-zli version` 173 | 174 | ``` 175 | USAGE 176 | $ ember-zli version [--json] [--verbose] 177 | 178 | FLAGS 179 | --verbose Show additional information about the CLI. 180 | 181 | GLOBAL FLAGS 182 | --json Format output as json. 183 | 184 | FLAG DESCRIPTIONS 185 | --verbose Show additional information about the CLI. 186 | 187 | Additionally shows the architecture, node version, operating system, and versions of plugins that the CLI is using. 188 | ``` 189 | 190 | _See code: [@oclif/plugin-version](https://github.com/oclif/plugin-version/blob/v2.2.35/src/commands/version.ts)_ 191 | 192 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* 4 | -------------------------------------------------------------------------------- /bin/dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning 2 | 3 | import { execute } from "@oclif/core"; 4 | 5 | await execute({ development: true, dir: import.meta.url }); 6 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /bin/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { execute } from "@oclif/core"; 4 | 5 | await execute({ dir: import.meta.url }); 6 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.3.1/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "includes": ["**", "!firmware-links*.json", "!package.json", "!package-lock.json"] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space", 15 | "indentWidth": 4, 16 | "lineWidth": 150, 17 | "lineEnding": "lf" 18 | }, 19 | "assist": { 20 | "actions": { 21 | "source": { 22 | "organizeImports": "on" 23 | } 24 | } 25 | }, 26 | "linter": { 27 | "enabled": true, 28 | "rules": { 29 | "recommended": true, 30 | "style": { 31 | "noNonNullAssertion": "off", 32 | "noParameterAssign": "off", 33 | "useAsConstAssertion": "error", 34 | "useDefaultParameterLast": "error", 35 | "useEnumInitializers": "error", 36 | "useSelfClosingElements": "error", 37 | "useSingleVarDeclarator": "error", 38 | "noUnusedTemplateLiteral": "error", 39 | "useNumberNamespace": "error", 40 | "noInferrableTypes": "error", 41 | "noUselessElse": "error" 42 | }, 43 | "suspicious": { 44 | "noAssignInExpressions": "off" 45 | } 46 | } 47 | }, 48 | "javascript": { 49 | "formatter": { 50 | "quoteStyle": "double" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /firmware-links-v3.json: -------------------------------------------------------------------------------- 1 | { 2 | "official": { 3 | "Nabu Casa SkyConnect": "https://github.com/NabuCasa/silabs-firmware-builder/releases/download/v2025.09.30/skyconnect_zigbee_ncp_7.4.4.3.gbl", 4 | "Nabu Casa Yellow": "https://github.com/NabuCasa/silabs-firmware-builder/releases/download/v2025.09.30/yellow_zigbee_ncp_7.4.4.3.gbl", 5 | "Nabu Casa ZBT-2": "https://github.com/NabuCasa/silabs-firmware-builder/releases/download/v2025.09.30/zbt2_zigbee_ncp_7.4.4.3_20250911_final.gbl", 6 | "SMLight SLZB06-M": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06m_zigbee_ncp_8.0.3.0_sw_flow_115200.gbl", 7 | "SMLight SLZB06mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06Mg24_zigbee_ncp_8.0.3.0_115200.gbl", 8 | "SMLight SLZB07": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07_zigbee_ncp_8.0.3.0_115200.gbl", 9 | "SMLight SLZB07mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07Mg24_zigbee_ncp_8.0.3.0_115200.gbl", 10 | "Sonoff ZBDongle-E": "https://github.com/itead/Sonoff_Zigbee_Dongle_Firmware/raw/refs/heads/master/Dongle-E/NCP_7.4.4/ncp-uart-sw_EZNet7.4.4_V1.0.0.gbl", 11 | "TubeZB MGM24": "https://github.com/tube0013/tube_gateways/raw/refs/heads/main/models/current/tubeszb-efr32-MGM24/firmware/mgm24/ncp/4.4.4/tubeszb-mgm24-hw-max_ncp-uart-hw_7.4.4.0.gbl", 12 | "ROUTER - SMLight SLZB06-M": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06m_zigbee_router_8.0.3.0_115200.gbl", 13 | "ROUTER - SMLight SLZB06mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06Mg24_zigbee_router_8.0.3.0_115200.gbl", 14 | "ROUTER - SMLight SLZB07": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07_zigbee_router_8.0.3.0_115200.gbl", 15 | "ROUTER - SMLight SLZB07mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07Mg24_zigbee_router_8.0.3.0_115200.gbl", 16 | "ROUTER - Sonoff ZBDongle-E": "https://github.com/itead/Sonoff_Zigbee_Dongle_Firmware/raw/refs/heads/master/Dongle-E/Router/Z3RouterUSBDonlge_EZNet6.10.3_V1.0.0.gbl" 17 | }, 18 | "darkxst": { 19 | "Aeotec Zi-Stick (ZGA008)": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/zga008_zigbee_ncp_8.0.3.0_115200.gbl", 20 | "EasyIOT ZB-GW04 v1.1": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/zb-gw04-1v1_zigbee_ncp_8.0.3.0_sw_flow_115200.gbl", 21 | "EasyIOT ZB-GW04 v1.2": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/zb-gw04-1v2_zigbee_ncp_8.0.3.0_115200.gbl", 22 | "SMLight SLZB06-M": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06m_zigbee_ncp_8.0.3.0_sw_flow_115200.gbl", 23 | "SMLight SLZB06mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06Mg24_zigbee_ncp_8.0.3.0_115200.gbl", 24 | "SMLight SLZB07": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07_zigbee_ncp_8.0.3.0_115200.gbl", 25 | "SMLight SLZB07mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07Mg24_zigbee_ncp_8.0.3.0_115200.gbl", 26 | "Sonoff ZBDongle-E": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/zbdonglee_zigbee_ncp_8.0.3.0_sw_flow_115200.gbl", 27 | "SparkFun MGM240p": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/mgm240p_zigbee_ncp_8.0.3.0_sw_flow_115200.gbl", 28 | "ROUTER - SMLight SLZB06-M": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06m_zigbee_router_8.0.3.0_115200.gbl", 29 | "ROUTER - SMLight SLZB06mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06Mg24_zigbee_router_8.0.3.0_115200.gbl", 30 | "ROUTER - SMLight SLZB07": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07_zigbee_router_8.0.3.0_115200.gbl", 31 | "ROUTER - SMLight SLZB07mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07Mg24_zigbee_router_8.0.3.0_115200.gbl" 32 | }, 33 | "nerivec": { 34 | "Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/aeotec_zga008_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 35 | "EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/easyiot_zb-gw04-1v1_zigbee_ncp_8.0.2.0_115200_sw_flow.gbl", 36 | "EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/easyiot_zb-gw04-1v2_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 37 | "Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/nabucasa_skyconnect_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 38 | "Nabu Casa Yellow": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/nabucasa_yellow_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 39 | "Nabu Casa ZBT-2": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/nabucasa_zbt-2_zigbee_ncp_8.0.2.0_460800_hw_flow.gbl", 40 | "SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb06m_zigbee_ncp_8.0.2.0_115200_sw_flow.gbl", 41 | "SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb06Mg24_zigbee_ncp_8.0.2.0_115200_sw_flow.gbl", 42 | "SMLight SLZB06mg26": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb06Mg26_zigbee_ncp_8.0.2.0_115200_sw_flow.gbl", 43 | "SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb07_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 44 | "SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb07Mg24_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 45 | "Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/sonoff_zbdonglee_zigbee_ncp_8.0.2.0_115200_sw_flow.gbl", 46 | "SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/sparkfun_mgm240p_zigbee_ncp_8.0.2.0_115200_sw_flow.gbl", 47 | "TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/tubeszb-mgm24-zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 48 | "ROUTER - Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/aeotec_zga008_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 49 | "ROUTER - EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/easyiot_zb-gw04-1v1_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 50 | "ROUTER - EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/easyiot_zb-gw04-1v2_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 51 | "ROUTER - Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/nabucasa_skyconnect_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 52 | "ROUTER - Nabu Casa ZBT-2": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/nabucasa_zbt-2_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 53 | "ROUTER - SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb06m_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 54 | "ROUTER - SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb06Mg24_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 55 | "ROUTER - SMLight SLZB06mg26": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb06Mg26_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 56 | "ROUTER - SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb07_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 57 | "ROUTER - SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb07Mg24_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 58 | "ROUTER - Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/sonoff_zbdonglee_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 59 | "ROUTER - SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/sparkfun_mgm240p_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 60 | "ROUTER - TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/tubeszb-mgm24-zigbee_router_8.0.2.0_115200_hw_flow.gbl" 61 | }, 62 | "nvm3_32768_clear": { 63 | "Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F1024IM32_nvm3_clear_0_1048576_8192_16384_32768.gbl", 64 | "EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 65 | "EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 66 | "Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F512IM32_nvm3_clear_0_524288_8192_16384_32768.gbl", 67 | "Nabu Casa ZBT-2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A420F1536IM40_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 68 | "SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 69 | "SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_32768.gbl", 70 | "SMLight SLZB06mg26": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG26B420F3200IM48_nvm3_clear_134217728_3276800_8192_134242304_32768.gbl", 71 | "SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 72 | "SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_32768.gbl", 73 | "Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 74 | "SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNA_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 75 | "TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PA32VNN_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 76 | "TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNN_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 77 | "ROUTER - Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F1024IM32_nvm3_clear_0_1048576_8192_16384_32768.gbl", 78 | "ROUTER - EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 79 | "ROUTER - EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 80 | "ROUTER - Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F512IM32_nvm3_clear_0_524288_8192_16384_32768.gbl", 81 | "ROUTER - Nabu Casa ZBT-2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A420F1536IM40_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 82 | "ROUTER - SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 83 | "ROUTER - SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_32768.gbl", 84 | "ROUTER - SMLight SLZB06mg26": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG26B420F3200IM48_nvm3_clear_134217728_3276800_8192_134242304_32768.gbl", 85 | "ROUTER - SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 86 | "ROUTER - SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_32768.gbl", 87 | "ROUTER - Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 88 | "ROUTER - SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNA_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 89 | "ROUTER - TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PA32VNN_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 90 | "ROUTER - TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNN_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl" 91 | }, 92 | "nvm3_40960_clear": { 93 | "Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F1024IM32_nvm3_clear_0_1048576_8192_16384_40960.gbl", 94 | "EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 95 | "EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 96 | "Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F512IM32_nvm3_clear_0_524288_8192_16384_40960.gbl", 97 | "Nabu Casa ZBT-2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A420F1536IM40_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 98 | "SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 99 | "SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_40960.gbl", 100 | "SMLight SLZB06mg26": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG26B420F3200IM48_nvm3_clear_134217728_3276800_8192_134242304_40960.gbl", 101 | "SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 102 | "SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_40960.gbl", 103 | "Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 104 | "SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNA_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 105 | "TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PA32VNN_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 106 | "TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNN_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 107 | "ROUTER - Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F1024IM32_nvm3_clear_0_1048576_8192_16384_40960.gbl", 108 | "ROUTER - EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 109 | "ROUTER - EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 110 | "ROUTER - Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F512IM32_nvm3_clear_0_524288_8192_16384_40960.gbl", 111 | "ROUTER - Nabu Casa ZBT-2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A420F1536IM40_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 112 | "ROUTER - SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 113 | "ROUTER - SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_40960.gbl", 114 | "ROUTER - SMLight SLZB06mg26": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG26B420F3200IM48_nvm3_clear_134217728_3276800_8192_134242304_40960.gbl", 115 | "ROUTER - SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 116 | "ROUTER - SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_40960.gbl", 117 | "ROUTER - Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 118 | "ROUTER - SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNA_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 119 | "ROUTER - TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PA32VNN_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 120 | "ROUTER - TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNN_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl" 121 | }, 122 | "app_clear": { 123 | "Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F1024IM32_app_clear_0_1048576_8192_16384.gbl", 124 | "EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 125 | "EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 126 | "Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F512IM32_app_clear_0_524288_8192_16384.gbl", 127 | "Nabu Casa ZBT-2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A420F1536IM40_app_clear_134217728_1572864_8192_134242304.gbl", 128 | "SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 129 | "SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_app_clear_134217728_1048576_8192_134242304.gbl", 130 | "SMLight SLZB06mg26": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG26B420F3200IM48_app_clear_134217728_3276800_8192_134242304.gbl", 131 | "SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 132 | "SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_app_clear_134217728_1048576_8192_134242304.gbl", 133 | "Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 134 | "SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNA_app_clear_134217728_1572864_8192_134242304.gbl", 135 | "TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PA32VNN_app_clear_134217728_1572864_8192_134242304.gbl", 136 | "TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNN_app_clear_134217728_1572864_8192_134242304.gbl", 137 | "ROUTER - Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F1024IM32_app_clear_0_1048576_8192_16384.gbl", 138 | "ROUTER - EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 139 | "ROUTER - EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 140 | "ROUTER - Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F512IM32_app_clear_0_524288_8192_16384.gbl", 141 | "ROUTER - Nabu Casa ZBT-2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A420F1536IM40_app_clear_134217728_1572864_8192_134242304.gbl", 142 | "ROUTER - SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 143 | "ROUTER - SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_app_clear_134217728_1048576_8192_134242304.gbl", 144 | "ROUTER - SMLight SLZB06mg26": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG26B420F3200IM48_app_clear_134217728_3276800_8192_134242304.gbl", 145 | "ROUTER - SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 146 | "ROUTER - SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG24A020F1024IM40_app_clear_134217728_1048576_8192_134242304.gbl", 147 | "ROUTER - Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 148 | "ROUTER - SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNA_app_clear_134217728_1572864_8192_134242304.gbl", 149 | "ROUTER - TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PA32VNN_app_clear_134217728_1572864_8192_134242304.gbl", 150 | "ROUTER - TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.3.0/MGM240PB32VNN_app_clear_134217728_1572864_8192_134242304.gbl" 151 | } 152 | } -------------------------------------------------------------------------------- /firmware-links.json: -------------------------------------------------------------------------------- 1 | { 2 | "latest": { 3 | "Aeotec Zi-Stick (ZGA008)": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/zga008_zigbee_ncp_8.0.3.0_115200.gbl", 4 | "EasyIOT ZB-GW04 v1.1": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/zb-gw04-1v1_zigbee_ncp_8.0.3.0_sw_flow_115200.gbl", 5 | "EasyIOT ZB-GW04 v1.2": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/zb-gw04-1v2_zigbee_ncp_8.0.3.0_115200.gbl", 6 | "Nabu Casa SkyConnect": "https://github.com/NabuCasa/silabs-firmware-builder/releases/download/v2025.04.04-1/skyconnect_zigbee_ncp_7.4.4.1.gbl", 7 | "Nabu Casa Yellow": "https://github.com/NabuCasa/silabs-firmware-builder/releases/download/v2025.04.04-1/yellow_zigbee_ncp_7.4.4.1.gbl", 8 | "SMLight SLZB06-M": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06m_zigbee_ncp_8.0.3.0_sw_flow_115200.gbl", 9 | "SMLight SLZB06mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06Mg24_zigbee_ncp_8.0.3.0_115200.gbl", 10 | "SMLight SLZB07": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07_zigbee_ncp_8.0.3.0_115200.gbl", 11 | "SMLight SLZB07mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07Mg24_zigbee_ncp_8.0.3.0_115200.gbl", 12 | "Sonoff ZBDongle-E": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/zbdonglee_zigbee_ncp_8.0.3.0_sw_flow_115200.gbl", 13 | "SparkFun MGM240p": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/mgm240p_zigbee_ncp_8.0.3.0_sw_flow_115200.gbl", 14 | "TubeZB MGM24": "https://github.com/tube0013/tube_gateways/raw/refs/heads/main/models/current/tubeszb-efr32-MGM24/firmware/mgm24/ncp/4.4.4/tubeszb-mgm24-hw-max_ncp-uart-hw_7.4.4.0.gbl", 15 | "ROUTER - SMLight SLZB06-M": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06m_zigbee_router_8.0.3.0_115200.gbl", 16 | "ROUTER - SMLight SLZB06mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06Mg24_zigbee_router_8.0.3.0_115200.gbl", 17 | "ROUTER - SMLight SLZB07": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07_zigbee_router_8.0.3.0_115200.gbl", 18 | "ROUTER - SMLight SLZB07mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07Mg24_zigbee_router_8.0.3.0_115200.gbl" 19 | }, 20 | "official": { 21 | "Nabu Casa SkyConnect": "https://github.com/NabuCasa/silabs-firmware-builder/releases/download/v2025.04.04-1/skyconnect_zigbee_ncp_7.4.4.1.gbl", 22 | "Nabu Casa Yellow": "https://github.com/NabuCasa/silabs-firmware-builder/releases/download/v2025.04.04-1/yellow_zigbee_ncp_7.4.4.1.gbl", 23 | "SMLight SLZB06-M": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06m_zigbee_ncp_8.0.3.0_sw_flow_115200.gbl", 24 | "SMLight SLZB06mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06Mg24_zigbee_ncp_8.0.3.0_115200.gbl", 25 | "SMLight SLZB07": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07_zigbee_ncp_8.0.3.0_115200.gbl", 26 | "SMLight SLZB07mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07Mg24_zigbee_ncp_8.0.3.0_115200.gbl", 27 | "Sonoff ZBDongle-E": "https://github.com/itead/Sonoff_Zigbee_Dongle_Firmware/raw/refs/heads/master/Dongle-E/NCP_7.4.4/ncp-uart-sw_EZNet7.4.4_V1.0.0.gbl", 28 | "TubeZB MGM24": "https://github.com/tube0013/tube_gateways/raw/refs/heads/main/models/current/tubeszb-efr32-MGM24/firmware/mgm24/ncp/4.4.4/tubeszb-mgm24-hw-max_ncp-uart-hw_7.4.4.0.gbl", 29 | "ROUTER - SMLight SLZB06-M": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06m_zigbee_router_8.0.3.0_115200.gbl", 30 | "ROUTER - SMLight SLZB06mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb06Mg24_zigbee_router_8.0.3.0_115200.gbl", 31 | "ROUTER - SMLight SLZB07": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07_zigbee_router_8.0.3.0_115200.gbl", 32 | "ROUTER - SMLight SLZB07mg24": "https://github.com/darkxst/silabs-firmware-builder/releases/download/20250627/slzb07Mg24_zigbee_router_8.0.3.0_115200.gbl", 33 | "ROUTER - Sonoff ZBDongle-E": "https://github.com/itead/Sonoff_Zigbee_Dongle_Firmware/raw/refs/heads/master/Dongle-E/Router/Z3RouterUSBDonlge_EZNet6.10.3_V1.0.0.gbl" 34 | }, 35 | "experimental": { 36 | "Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/aeotec_zga008_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 37 | "EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/easyiot_zb-gw04-1v1_zigbee_ncp_8.0.2.0_115200_sw_flow.gbl", 38 | "EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/easyiot_zb-gw04-1v2_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 39 | "Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/nabucasa_skyconnect_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 40 | "Nabu Casa Yellow": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/nabucasa_yellow_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 41 | "SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb06m_zigbee_ncp_8.0.2.0_115200_sw_flow.gbl", 42 | "SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb06Mg24_zigbee_ncp_8.0.2.0_115200_sw_flow.gbl", 43 | "SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb07_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 44 | "SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb07Mg24_zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 45 | "Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/sonoff_zbdonglee_zigbee_ncp_8.0.2.0_115200_sw_flow.gbl", 46 | "SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/sparkfun_mgm240p_zigbee_ncp_8.0.2.0_115200_sw_flow.gbl", 47 | "TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/tubeszb-mgm24-zigbee_ncp_8.0.2.0_115200_hw_flow.gbl", 48 | "ROUTER - Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/aeotec_zga008_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 49 | "ROUTER - EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/easyiot_zb-gw04-1v1_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 50 | "ROUTER - EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/easyiot_zb-gw04-1v2_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 51 | "ROUTER - Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/nabucasa_skyconnect_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 52 | "ROUTER - SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb06m_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 53 | "ROUTER - SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb06Mg24_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 54 | "ROUTER - SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb07_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 55 | "ROUTER - SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/smlight_slzb07Mg24_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 56 | "ROUTER - Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/sonoff_zbdonglee_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 57 | "ROUTER - SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/sparkfun_mgm240p_zigbee_router_8.0.2.0_115200_sw_flow.gbl", 58 | "ROUTER - TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-builder/releases/download/v2024.6.2-update7/tubeszb-mgm24-zigbee_router_8.0.2.0_115200_hw_flow.gbl" 59 | }, 60 | "nvm3_32768_clear": { 61 | "Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F1024IM32_nvm3_clear_0_1048576_8192_16384_32768.gbl", 62 | "EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 63 | "EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 64 | "Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F512IM32_nvm3_clear_0_524288_8192_16384_32768.gbl", 65 | "SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 66 | "SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_32768.gbl", 67 | "SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 68 | "SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_32768.gbl", 69 | "Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 70 | "SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNA_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 71 | "TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PA32VNN_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 72 | "TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNN_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 73 | "ROUTER - Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F1024IM32_nvm3_clear_0_1048576_8192_16384_32768.gbl", 74 | "ROUTER - EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 75 | "ROUTER - EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 76 | "ROUTER - Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F512IM32_nvm3_clear_0_524288_8192_16384_32768.gbl", 77 | "ROUTER - SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 78 | "ROUTER - SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_32768.gbl", 79 | "ROUTER - SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 80 | "ROUTER - SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_32768.gbl", 81 | "ROUTER - Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_32768.gbl", 82 | "ROUTER - SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNA_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 83 | "ROUTER - TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PA32VNN_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl", 84 | "ROUTER - TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNN_nvm3_clear_134217728_1572864_8192_134242304_32768.gbl" 85 | }, 86 | "nvm3_40960_clear": { 87 | "Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F1024IM32_nvm3_clear_0_1048576_8192_16384_40960.gbl", 88 | "EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 89 | "EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 90 | "Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F512IM32_nvm3_clear_0_524288_8192_16384_40960.gbl", 91 | "SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 92 | "SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_40960.gbl", 93 | "SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 94 | "SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_40960.gbl", 95 | "Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 96 | "SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNA_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 97 | "TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PA32VNN_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 98 | "TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNN_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 99 | "ROUTER - Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F1024IM32_nvm3_clear_0_1048576_8192_16384_40960.gbl", 100 | "ROUTER - EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 101 | "ROUTER - EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 102 | "ROUTER - Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F512IM32_nvm3_clear_0_524288_8192_16384_40960.gbl", 103 | "ROUTER - SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 104 | "ROUTER - SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_40960.gbl", 105 | "ROUTER - SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 106 | "ROUTER - SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_nvm3_clear_134217728_1048576_8192_134242304_40960.gbl", 107 | "ROUTER - Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_nvm3_clear_0_786432_8192_16384_40960.gbl", 108 | "ROUTER - SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNA_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 109 | "ROUTER - TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PA32VNN_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl", 110 | "ROUTER - TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNN_nvm3_clear_134217728_1572864_8192_134242304_40960.gbl" 111 | }, 112 | "app_clear": { 113 | "Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F1024IM32_app_clear_0_1048576_8192_16384.gbl", 114 | "EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 115 | "EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 116 | "Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F512IM32_app_clear_0_524288_8192_16384.gbl", 117 | "SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 118 | "SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_app_clear_134217728_1048576_8192_134242304.gbl", 119 | "SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 120 | "SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_app_clear_134217728_1048576_8192_134242304.gbl", 121 | "Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 122 | "SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNA_app_clear_134217728_1572864_8192_134242304.gbl", 123 | "TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PA32VNN_app_clear_134217728_1572864_8192_134242304.gbl", 124 | "TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNN_app_clear_134217728_1572864_8192_134242304.gbl", 125 | "ROUTER - Aeotec Zi-Stick (ZGA008)": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F1024IM32_app_clear_0_1048576_8192_16384.gbl", 126 | "ROUTER - EasyIOT ZB-GW04 v1.1": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 127 | "ROUTER - EasyIOT ZB-GW04 v1.2": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 128 | "ROUTER - Nabu Casa SkyConnect": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F512IM32_app_clear_0_524288_8192_16384.gbl", 129 | "ROUTER - SMLight SLZB06-M": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 130 | "ROUTER - SMLight SLZB06mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_app_clear_134217728_1048576_8192_134242304.gbl", 131 | "ROUTER - SMLight SLZB07": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 132 | "ROUTER - SMLight SLZB07mg24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG24A020F1024IM40_app_clear_134217728_1048576_8192_134242304.gbl", 133 | "ROUTER - Sonoff ZBDongle-E": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/EFR32MG21A020F768IM32_app_clear_0_786432_8192_16384.gbl", 134 | "ROUTER - SparkFun MGM240p": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNA_app_clear_134217728_1572864_8192_134242304.gbl", 135 | "ROUTER - TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PA32VNN_app_clear_134217728_1572864_8192_134242304.gbl", 136 | "ROUTER - TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.1.0/MGM240PB32VNN_app_clear_134217728_1572864_8192_134242304.gbl" 137 | } 138 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-zli", 3 | "description": "Interact with EmberZNet-based adapters using zigbee-herdsman 'ember' driver", 4 | "version": "3.2.0", 5 | "author": "Nerivec", 6 | "bin": { 7 | "ember-zli": "bin/run.js" 8 | }, 9 | "bugs": "https://github.com/Nerivec/ember-zli/issues", 10 | "dependencies": { 11 | "@inquirer/prompts": "^7.9.0", 12 | "@oclif/core": "^4.7.2", 13 | "@oclif/plugin-help": "^6.2.34", 14 | "@oclif/plugin-not-found": "^3.2.71", 15 | "@oclif/plugin-version": "^2.2.35", 16 | "bonjour-service": "^1.3.0", 17 | "cli-progress": "^3.12.0", 18 | "winston": "^3.18.3", 19 | "zigbee-herdsman": "^6.3.2", 20 | "zigbee-on-host": "^0.1.13" 21 | }, 22 | "devDependencies": { 23 | "@biomejs/biome": "^2.3.1", 24 | "@types/cli-progress": "^3.11.6", 25 | "@types/node": "^24.9.1", 26 | "oclif": "^4.22.37", 27 | "ts-node": "^10.9.2", 28 | "typescript": "^5.9.3" 29 | }, 30 | "engines": { 31 | "node": ">=20.15.0" 32 | }, 33 | "files": [ 34 | "/bin", 35 | "/dist", 36 | "/oclif.manifest.json" 37 | ], 38 | "homepage": "https://github.com/Nerivec/ember-zli", 39 | "keywords": [ 40 | "zigbee2mqtt", 41 | "z2m", 42 | "zigbee-herdsman", 43 | "herdsman", 44 | "ember", 45 | "emberznet", 46 | "ezsp", 47 | "silabs", 48 | "zigbee" 49 | ], 50 | "license": "GPL-3.0-or-later", 51 | "main": "dist/index.js", 52 | "type": "module", 53 | "oclif": { 54 | "bin": "ember-zli", 55 | "dirname": "ember-zli", 56 | "commands": "./dist/commands", 57 | "plugins": [ 58 | "@oclif/plugin-help", 59 | "@oclif/plugin-version", 60 | "@oclif/plugin-not-found" 61 | ], 62 | "topicSeparator": " ", 63 | "topics": {} 64 | }, 65 | "repository": { 66 | "type": "git", 67 | "url": "git+https://github.com/Nerivec/ember-zli.git" 68 | }, 69 | "scripts": { 70 | "build": "tsc -b", 71 | "build:prod": "npm run prepack && npm run postpack", 72 | "clean": "rm -rf dist tsconfig.tsbuildinfo", 73 | "update-fw-links": "node ./dist/utils/update-firmware-links.js", 74 | "check": "biome check --write", 75 | "check:ci": "biome check", 76 | "postpack": "rm -f oclif.manifest.json", 77 | "prepack": "npm run clean && npm run build && oclif manifest && oclif readme", 78 | "version": "oclif readme && git add README.md" 79 | }, 80 | "types": "dist/index.d.ts" 81 | } 82 | -------------------------------------------------------------------------------- /src/commands/bootloader/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { confirm, input, select } from "@inquirer/prompts"; 3 | import { Command } from "@oclif/core"; 4 | import { Presets, SingleBar } from "cli-progress"; 5 | import { DEFAULT_FIRMWARE_GBL_PATH, logger } from "../../index.js"; 6 | import { BootloaderEvent, BootloaderMenu, GeckoBootloader } from "../../utils/bootloader.js"; 7 | import { ADAPTER_MODELS, PRE_DEFINED_FIRMWARE_LINKS_URL } from "../../utils/consts.js"; 8 | import { FirmwareValidation } from "../../utils/enums.js"; 9 | import { getPortConf } from "../../utils/port.js"; 10 | import type { AdapterModel, FirmwareLinks, FirmwareVariant, SelectChoices } from "../../utils/types.js"; 11 | import { browseToFile, fetchJson } from "../../utils/utils.js"; 12 | 13 | export default class Bootloader extends Command { 14 | static override args = {}; 15 | static override description = "Interact with the Gecko bootloader in the adapter."; 16 | static override examples = ["<%= config.bin %> <%= command.id %>"]; 17 | 18 | public async run(): Promise { 19 | const portConf = await getPortConf(); 20 | logger.debug(`Using port conf: ${JSON.stringify(portConf)}`); 21 | 22 | const adapterModelChoices: SelectChoices = [{ name: "Not in this list", value: undefined }]; 23 | 24 | for (const model of ADAPTER_MODELS) { 25 | adapterModelChoices.push({ name: model, value: model }); 26 | } 27 | 28 | const adapterModel = await select({ 29 | choices: adapterModelChoices, 30 | message: "Adapter model", 31 | }); 32 | 33 | const gecko = new GeckoBootloader(portConf, adapterModel); 34 | const progressBar = new SingleBar({ clearOnComplete: true, format: "{bar} {percentage}%" }, Presets.shades_classic); 35 | 36 | gecko.on(BootloaderEvent.FAILED, () => { 37 | this.exit(1); 38 | }); 39 | 40 | gecko.on(BootloaderEvent.CLOSED, () => { 41 | this.exit(0); 42 | }); 43 | 44 | gecko.on(BootloaderEvent.UPLOAD_START, () => { 45 | progressBar.start(100, 0); 46 | }); 47 | 48 | gecko.on(BootloaderEvent.UPLOAD_STOP, () => { 49 | progressBar.stop(); 50 | }); 51 | 52 | gecko.on(BootloaderEvent.UPLOAD_PROGRESS, (percent) => { 53 | progressBar.update(percent); 54 | }); 55 | 56 | await gecko.connect(); 57 | 58 | let exit = false; 59 | 60 | while (!exit) { 61 | exit = await this.navigateMenu(gecko); 62 | } 63 | 64 | await gecko.transport.close(false); 65 | 66 | return this.exit(0); 67 | } 68 | 69 | private async navigateMenu(gecko: GeckoBootloader): Promise { 70 | const answer = await select<-1 | BootloaderMenu>({ 71 | choices: [ 72 | { name: "Get info", value: BootloaderMenu.INFO }, 73 | { name: "Update firmware", value: BootloaderMenu.UPLOAD_GBL }, 74 | { 75 | name: "Clear NVM3 (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#nvm3-clear)", 76 | value: BootloaderMenu.CLEAR_NVM3, 77 | disabled: !gecko.adapterModel, 78 | }, 79 | { 80 | name: "Clear APP (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#app-clear)", 81 | value: BootloaderMenu.CLEAR_APP, 82 | disabled: !gecko.adapterModel, 83 | }, 84 | { name: "Exit bootloader (run firmware)", value: BootloaderMenu.RUN }, 85 | { name: "Force close", value: -1 }, 86 | ], 87 | message: "Menu", 88 | }); 89 | 90 | if (answer === -1) { 91 | logger.warning("Force closing... You may need to unplug/replug the adapter."); 92 | return true; 93 | } 94 | 95 | let firmware: Buffer | undefined; 96 | 97 | if (answer === BootloaderMenu.UPLOAD_GBL) { 98 | let validFirmware: FirmwareValidation = FirmwareValidation.INVALID; 99 | 100 | while (validFirmware !== FirmwareValidation.VALID) { 101 | firmware = await this.selectFirmware(gecko); 102 | 103 | validFirmware = await gecko.validateFirmware(firmware); 104 | 105 | if (validFirmware === FirmwareValidation.CANCELLED) { 106 | return false; 107 | } 108 | } 109 | } else if (answer === BootloaderMenu.CLEAR_NVM3) { 110 | const confirmed = await confirm({ 111 | default: false, 112 | message: `Confirm adapter is: ${gecko.adapterModel}?`, 113 | }); 114 | 115 | if (!confirmed) { 116 | logger.warning("Cancelled NVM3 clearing."); 117 | return false; 118 | } 119 | 120 | const nvm3Size = await select({ 121 | choices: [ 122 | { name: "32768", value: 32768 }, 123 | { name: "40960", value: 40960 }, 124 | ], 125 | message: "NVM3 Size (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#nvm3-clear)", 126 | }); 127 | const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL); 128 | const variant = nvm3Size === 32768 ? "nvm3_32768_clear" : "nvm3_40960_clear"; 129 | firmware = await this.downloadFirmware(firmwareLinks[variant][gecko.adapterModel!]!); 130 | } else if (answer === BootloaderMenu.CLEAR_APP) { 131 | const confirmed = await confirm({ 132 | default: false, 133 | message: `Confirm adapter is: ${gecko.adapterModel}?`, 134 | }); 135 | 136 | if (!confirmed) { 137 | logger.warning("Cancelled APP clearing."); 138 | return false; 139 | } 140 | 141 | const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL); 142 | firmware = await this.downloadFirmware(firmwareLinks.app_clear[gecko.adapterModel!]!); 143 | } 144 | 145 | return await gecko.navigate(answer, firmware); 146 | } 147 | 148 | private async downloadFirmware(url: string): Promise { 149 | try { 150 | logger.info(`Downloading firmware from ${url}.`); 151 | 152 | const response = await fetch(url); 153 | 154 | if (!response.ok) { 155 | throw new Error(`${response.status}`); 156 | } 157 | 158 | const arrayBuffer = await response.arrayBuffer(); 159 | 160 | return Buffer.from(arrayBuffer); 161 | } catch (error) { 162 | logger.error(`Failed to download firmware file from ${url} with error ${error}.`); 163 | } 164 | 165 | return undefined; 166 | } 167 | 168 | private async selectFirmware(gecko: GeckoBootloader): Promise { 169 | enum FirmwareSource { 170 | PRE_DEFINED = 0, 171 | URL = 1, 172 | FILE = 2, 173 | } 174 | const firmwareSource = await select({ 175 | choices: [ 176 | { 177 | name: `Use pre-defined firmware (using ${PRE_DEFINED_FIRMWARE_LINKS_URL})`, 178 | value: FirmwareSource.PRE_DEFINED, 179 | disabled: gecko.adapterModel === undefined, 180 | }, 181 | { name: "Provide URL", value: FirmwareSource.URL }, 182 | { name: "Browse to file", value: FirmwareSource.FILE }, 183 | ], 184 | message: "Firmware source", 185 | }); 186 | 187 | switch (firmwareSource) { 188 | case FirmwareSource.PRE_DEFINED: { 189 | const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL); 190 | // valid adapterModel since select option disabled if not 191 | const official = firmwareLinks.official[gecko.adapterModel!]; 192 | const darkxst = firmwareLinks.darkxst[gecko.adapterModel!]; 193 | const nerivec = firmwareLinks.nerivec[gecko.adapterModel!]; 194 | const firmwareVariant = await select({ 195 | choices: [ 196 | { 197 | name: "Latest from manufacturer", 198 | value: "official", 199 | description: official, 200 | disabled: !official, 201 | }, 202 | { 203 | name: "Latest from @darkxst", 204 | value: "darkxst", 205 | description: darkxst, 206 | disabled: !darkxst, 207 | }, 208 | { 209 | name: "Latest from @Nerivec", 210 | value: "nerivec", 211 | description: nerivec, 212 | disabled: !nerivec, 213 | }, 214 | ], 215 | message: "Firmware version", 216 | }); 217 | const firmwareUrl = firmwareLinks[firmwareVariant][gecko.adapterModel!]; 218 | 219 | // just in case (and to pass linter) 220 | if (!firmwareUrl) { 221 | return undefined; 222 | } 223 | 224 | return await this.downloadFirmware(firmwareUrl); 225 | } 226 | 227 | case FirmwareSource.URL: { 228 | const url = await input({ 229 | message: "Enter the URL to the firmware file", 230 | validate(value) { 231 | try { 232 | new URL(value); 233 | return true; 234 | } catch { 235 | return false; 236 | } 237 | }, 238 | }); 239 | 240 | return await this.downloadFirmware(url); 241 | } 242 | 243 | case FirmwareSource.FILE: { 244 | const firmwareFile = await browseToFile("Firmware file", DEFAULT_FIRMWARE_GBL_PATH); 245 | 246 | return readFileSync(firmwareFile); 247 | } 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/commands/monitor/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@oclif/core"; 2 | 3 | import { SLStatus } from "zigbee-herdsman/dist/adapter/ember/enums.js"; 4 | 5 | import { logger } from "../../index.js"; 6 | import { getPortConf } from "../../utils/port.js"; 7 | import { Transport, TransportEvent } from "../../utils/transport.js"; 8 | 9 | export default class Monitor extends Command { 10 | static override args = {}; 11 | static override description = "Monitor the chosen port in the console."; 12 | static override examples = ["<%= config.bin %> <%= command.id %>"]; 13 | 14 | private logBuffer: Buffer = Buffer.alloc(0); 15 | 16 | public async run(): Promise { 17 | const portConf = await getPortConf(); 18 | logger.debug(`Using port conf: ${JSON.stringify(portConf)}`); 19 | 20 | const transport = new Transport(portConf); 21 | 22 | try { 23 | await transport.initPort(); 24 | } catch (error) { 25 | logger.error(`Failed to open port: ${error}.`); 26 | 27 | await transport.close(false, false); // force failed below 28 | 29 | return this.exit(1); 30 | } 31 | 32 | logger.info("Started monitoring. Press any key to stop."); 33 | 34 | transport.on(TransportEvent.FAILED, () => this.exit(1)); 35 | transport.on(TransportEvent.DATA, this.onTransportData.bind(this)); 36 | 37 | process.stdin.setRawMode(true); 38 | process.stdin.resume(); 39 | 40 | await new Promise((resolve) => { 41 | process.stdin.once("data", () => { 42 | process.stdin.setRawMode(false); 43 | resolve(); 44 | }); 45 | }); 46 | 47 | return this.exit(0); 48 | } 49 | 50 | private onTransportData(received: Buffer): void { 51 | // concat received to previous to ensure lines are outputted properly 52 | let data = Buffer.concat([this.logBuffer, received]); 53 | let position: number; 54 | 55 | while ((position = data.indexOf("\r\n")) !== -1) { 56 | // take everything up to '\r\n' (excluded) 57 | const line = data.subarray(0, position); 58 | 59 | // skip blank lines 60 | if (line.length > 0) { 61 | let asciiLine = line.toString("ascii"); 62 | // append SLStatus at end of line if detected hex for it 63 | // - "Join network complete: 0x18" 64 | // - "Join network start: 0x0" 65 | // XXX: format seems pretty standard throughout the SDK, but this might create some false matches (hence leaving the hex too) 66 | const endStatusMatch = asciiLine.match(/ (0x\d+)$/); 67 | 68 | if (endStatusMatch) { 69 | asciiLine += ` (${SLStatus[Number.parseInt(endStatusMatch[1], 16)]})`; 70 | } 71 | 72 | logger.info(asciiLine); 73 | } 74 | 75 | // remove the line from internal buffer (set below), this time include '\r\n' 76 | data = data.subarray(position + 2); 77 | } 78 | 79 | this.logBuffer = data; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/commands/sniff/index.ts: -------------------------------------------------------------------------------- 1 | import { createSocket, type Socket } from "node:dgram"; 2 | import { createWriteStream, existsSync, type WriteStream } from "node:fs"; 3 | import { join } from "node:path"; 4 | import { pathToFileURL } from "node:url"; 5 | 6 | import { confirm, input, select } from "@inquirer/prompts"; 7 | import { Command } from "@oclif/core"; 8 | import type { Logger } from "winston"; 9 | 10 | import { ZSpec } from "zigbee-herdsman"; 11 | import { SLStatus } from "zigbee-herdsman/dist/adapter/ember/enums.js"; 12 | import type { Ezsp } from "zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js"; 13 | 14 | import { DATA_FOLDER, DEFAULT_PCAP_PATH, logger } from "../../index.js"; 15 | import { emberStart, emberStop } from "../../utils/ember.js"; 16 | import { getPortConf } from "../../utils/port.js"; 17 | import { browseToFile, computeCRC16CITTKermit } from "../../utils/utils.js"; 18 | import { createPcapFileHeader, createPcapPacketRecordMs, createWiresharkZEPFrame, PCAP_MAGIC_NUMBER_MS } from "../../utils/wireshark.js"; 19 | 20 | enum SniffMenu { 21 | START_SNIFFING = 0, 22 | } 23 | 24 | const DEFAULT_WIRESHARK_IP_ADDRESS = "127.0.0.1"; 25 | const DEFAULT_ZEP_UDP_PORT = 17754; 26 | 27 | export default class Sniff extends Command { 28 | static override args = {}; 29 | static override description = "Sniff Zigbee traffic (to Wireshark, to PCAP file, to custom handler or just log raw data)."; 30 | static override examples = ["<%= config.bin %> <%= command.id %>"]; 31 | static override flags = {}; 32 | 33 | public ezsp: Ezsp | undefined; 34 | public sequence = 0; 35 | public sniffing = false; 36 | public udpSocket: Socket | undefined; 37 | public pcapFileStream: WriteStream | undefined; 38 | public wiresharkIPAddress: string = DEFAULT_WIRESHARK_IP_ADDRESS; 39 | public zepUDPPort: number = DEFAULT_ZEP_UDP_PORT; 40 | 41 | private customHandler: ((cmd: Command, logger: Logger, linkQuality: number, rssi: number, packetContents: Buffer) => void) | undefined; 42 | 43 | public async run(): Promise { 44 | // const { args, flags } = await this.parse(Sniff) 45 | const portConf = await getPortConf(); 46 | logger.debug(`Using port conf: ${JSON.stringify(portConf)}`); 47 | 48 | this.ezsp = await emberStart(portConf); 49 | let exit = false; 50 | 51 | while (!exit) { 52 | exit = await this.navigateMenu(); 53 | 54 | if (exit && this.sniffing) { 55 | exit = await confirm({ message: "Sniffing is currently running. Confirm exit?", default: false }); 56 | } 57 | } 58 | 59 | this.udpSocket?.close(); 60 | this.pcapFileStream?.close(); 61 | await emberStop(this.ezsp); 62 | 63 | return this.exit(0); 64 | } 65 | 66 | private async menuStartSniffing(): Promise { 67 | if (!this.ezsp) { 68 | logger.error("Invalid state, no EZSP layer available."); 69 | return this.exit(1); 70 | } 71 | 72 | enum SniffDestination { 73 | LOG_FILE = 0, 74 | WIRESHARK = 1, 75 | PCAP_FILE = 2, 76 | } 77 | const sniffDestination = await select({ 78 | choices: [ 79 | { name: "Wireshark", value: SniffDestination.WIRESHARK, description: "Write to Wireshark ZEP UDP Protocol" }, 80 | { name: "PCAP file", value: SniffDestination.PCAP_FILE, description: "Write to a PCAP file for later use or sharing." }, 81 | { name: "Log", value: SniffDestination.LOG_FILE, description: "Write raw data to log file." }, 82 | ], 83 | message: "Destination (Note: if present, custom handler is always used, regardless of the selected destination)", 84 | }); 85 | 86 | switch (sniffDestination) { 87 | case SniffDestination.WIRESHARK: { 88 | this.wiresharkIPAddress = await input({ message: "Wireshark IP address", default: DEFAULT_WIRESHARK_IP_ADDRESS }); 89 | this.zepUDPPort = Number.parseInt(await input({ message: "Wireshark ZEP UDP port", default: `${DEFAULT_ZEP_UDP_PORT}` }), 10); 90 | this.udpSocket = createSocket("udp4"); 91 | 92 | this.udpSocket.bind(this.zepUDPPort); 93 | 94 | break; 95 | } 96 | 97 | case SniffDestination.PCAP_FILE: { 98 | const pcapFilePath = await browseToFile("PCAP file", DEFAULT_PCAP_PATH, true); 99 | this.pcapFileStream = createWriteStream(pcapFilePath, "utf8"); 100 | 101 | this.pcapFileStream.on("error", (error) => { 102 | logger.error(error); 103 | 104 | return true; 105 | }); 106 | 107 | const fileHeader = createPcapFileHeader(PCAP_MAGIC_NUMBER_MS); 108 | 109 | this.pcapFileStream.write(fileHeader); 110 | 111 | break; 112 | } 113 | } 114 | 115 | // set desired tx power before scan 116 | const radioTxPower = Number.parseInt( 117 | await input({ 118 | default: "5", 119 | message: "Radio transmit power [-128-127]", 120 | validate(value: string) { 121 | if (/\./.test(value)) { 122 | return false; 123 | } 124 | 125 | const v = Number.parseInt(value, 10); 126 | 127 | return v >= -128 && v <= 127; 128 | }, 129 | }), 130 | 10, 131 | ); 132 | 133 | let status = await this.ezsp.ezspSetRadioPower(radioTxPower); 134 | 135 | if (status !== SLStatus.OK) { 136 | logger.error(`Failed to set transmit power to ${radioTxPower} status=${SLStatus[status]}.`); 137 | return true; 138 | } 139 | 140 | const channel = await select({ 141 | choices: ZSpec.ALL_802_15_4_CHANNELS.map((c) => ({ name: c.toString(), value: c })), 142 | message: "Channel to sniff", 143 | }); 144 | const eui64 = await this.ezsp.ezspGetEui64(); 145 | const deviceId = Number.parseInt(eui64.slice(-4), 16); 146 | 147 | status = await this.ezsp.mfglibInternalStart(true); 148 | 149 | if (status !== SLStatus.OK) { 150 | logger.error(`Failed to start listening for packets with status=${SLStatus[status]}.`); 151 | return true; 152 | } 153 | 154 | status = await this.ezsp.mfglibInternalSetChannel(channel); 155 | 156 | if (status !== SLStatus.OK) { 157 | logger.error(`Failed to set channel with status=${SLStatus[status]}.`); 158 | return true; 159 | } 160 | 161 | this.sniffing = true; 162 | 163 | const handlerFile = join(DATA_FOLDER, "ezspMfglibRxHandler.mjs"); 164 | 165 | if (existsSync(handlerFile)) { 166 | try { 167 | const importedScript = await import(pathToFileURL(handlerFile).toString()); 168 | 169 | if (typeof importedScript.default !== "function") { 170 | throw new TypeError("Not a function."); 171 | } 172 | 173 | this.customHandler = importedScript.default; 174 | 175 | logger.info("Loaded custom handler."); 176 | } catch (error) { 177 | logger.error(`Failed to load custom handler. ${error}`); 178 | } 179 | } 180 | 181 | // XXX: this is currently not restored, but not a problem since only possible menu is exit 182 | const ezspMfglibRxHandlerOriginal = this.ezsp.ezspMfglibRxHandler; 183 | 184 | this.ezsp.ezspMfglibRxHandler = (linkQuality: number, rssi: number, packetContents: Buffer): void => { 185 | if (this.customHandler) { 186 | this.customHandler(this, logger, linkQuality, rssi, packetContents); 187 | } 188 | 189 | switch (sniffDestination) { 190 | case SniffDestination.WIRESHARK: { 191 | try { 192 | const wsZEPFrame = createWiresharkZEPFrame(channel, deviceId, linkQuality, rssi, this.sequence, packetContents); 193 | this.sequence += 1; 194 | 195 | if (this.sequence > 0xffffffff) { 196 | // wrap if necessary... 197 | this.sequence = 0; 198 | } 199 | 200 | if (this.udpSocket) { 201 | this.udpSocket.send(wsZEPFrame, this.zepUDPPort, this.wiresharkIPAddress); 202 | } 203 | } catch (error) { 204 | logger.debug(error); 205 | } 206 | 207 | break; 208 | } 209 | 210 | case SniffDestination.PCAP_FILE: { 211 | if (this.pcapFileStream) { 212 | // fix static CRC used in EZSP >= v8 213 | packetContents.set(computeCRC16CITTKermit(packetContents.subarray(0, -2)), packetContents.length - 2); 214 | 215 | const packet = createPcapPacketRecordMs(packetContents); 216 | 217 | this.pcapFileStream.write(packet); 218 | } 219 | 220 | break; 221 | } 222 | 223 | case SniffDestination.LOG_FILE: { 224 | ezspMfglibRxHandlerOriginal(linkQuality, rssi, packetContents); 225 | 226 | break; 227 | } 228 | } 229 | }; 230 | 231 | logger.info("Sniffing started."); 232 | 233 | return false; 234 | } 235 | 236 | private async navigateMenu(): Promise { 237 | const answer = await select<-1 | SniffMenu>({ 238 | choices: [ 239 | { name: "Start sniffing", value: SniffMenu.START_SNIFFING, disabled: this.sniffing }, 240 | { name: "Exit", value: -1 }, 241 | ], 242 | message: "Menu", 243 | }); 244 | 245 | switch (answer) { 246 | case SniffMenu.START_SNIFFING: { 247 | return await this.menuStartSniffing(); 248 | } 249 | } 250 | 251 | return true; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/commands/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, readFileSync, rmSync } from "node:fs"; 2 | import { join } from "node:path"; 3 | 4 | import { select } from "@inquirer/prompts"; 5 | import { Command } from "@oclif/core"; 6 | 7 | import { DEFAULT_TOKENS_BACKUP_PATH, LOGS_FOLDER, logger } from "../../index.js"; 8 | import { parseTokenData } from "../../utils/ember.js"; 9 | import { NVM3ObjectKey } from "../../utils/enums.js"; 10 | import { browseToFile } from "../../utils/utils.js"; 11 | 12 | enum UtilsMenu { 13 | PARSE_TOKENS_BACKUP_FILE = 10, 14 | PURGE_LOG_FILES = 90, 15 | } 16 | 17 | export default class Utils extends Command { 18 | static override args = {}; 19 | static override description = "Execute various utility commands."; 20 | static override examples = ["<%= config.bin %> <%= command.id %>"]; 21 | static override flags = {}; 22 | 23 | public async run(): Promise { 24 | // const {args, flags} = await this.parse(Utils) 25 | let exit = false; 26 | 27 | while (!exit) { 28 | exit = await this.navigateMenu(); 29 | } 30 | 31 | return this.exit(0); 32 | } 33 | 34 | private async menuParseTokensBackupFile(): Promise { 35 | const backupFile = await browseToFile("Tokens backup file location", DEFAULT_TOKENS_BACKUP_PATH); 36 | const tokensBuf = Buffer.from(readFileSync(backupFile, "utf8"), "hex"); 37 | 38 | if (tokensBuf.length === 0) { 39 | logger.error("Tokens file invalid or empty."); 40 | 41 | return true; 42 | } 43 | 44 | let readOffset = 0; 45 | const inTokenCount = tokensBuf.readUInt8(readOffset++); 46 | 47 | for (let i = 0; i < inTokenCount; i++) { 48 | const nvm3Key = tokensBuf.readUInt32LE(readOffset); // 4 bytes Token Key/Creator 49 | readOffset += 4; 50 | const size = tokensBuf.readUInt8(readOffset++); // 1 byte token size 51 | const arraySize = tokensBuf.readUInt8(readOffset++); // 1 byte array size. 52 | 53 | for (let arrayIndex = 0; arrayIndex < arraySize; arrayIndex++) { 54 | const parsedTokenData = parseTokenData(nvm3Key, tokensBuf.subarray(readOffset, readOffset + size)); 55 | 56 | logger.info(`Token nvm3Key=${NVM3ObjectKey[nvm3Key]} size=${size} token=[${parsedTokenData}]`); 57 | 58 | readOffset += size; 59 | } 60 | } 61 | 62 | return false; 63 | } 64 | 65 | private async menuPurgeLogFiles(): Promise { 66 | const olderThan = await select({ 67 | choices: [ 68 | { name: "Older than 30 days", value: 3600000 * 24 * 30 }, 69 | { name: "Older than 7 days", value: 3600000 * 24 * 7 }, 70 | { name: "Older than 1 day", value: 3600000 * 24 }, 71 | { name: "Older than 1 hour", value: 3600000 }, 72 | { name: "All", value: -1 }, 73 | ], 74 | message: "Timeframe", 75 | }); 76 | 77 | let count = 0; 78 | 79 | // -1 == never process last (currently used) 80 | for (const file of readdirSync(LOGS_FOLDER).slice(0, -1)) { 81 | const match = file.match(/^ember-zli-(\d+)\.log$/); 82 | 83 | if (match) { 84 | if (olderThan === -1 || Number.parseInt(match[1], 10) < Date.now() - olderThan) { 85 | rmSync(join(LOGS_FOLDER, file), { force: true }); 86 | 87 | count++; 88 | } 89 | } 90 | } 91 | 92 | logger.info(`Purged ${count} log files.`); 93 | 94 | return false; 95 | } 96 | 97 | private async navigateMenu(): Promise { 98 | const answer = await select<-1 | UtilsMenu>({ 99 | choices: [ 100 | { name: "Parse NVM3 tokens backup file", value: UtilsMenu.PARSE_TOKENS_BACKUP_FILE }, 101 | { name: "Purge log files", value: UtilsMenu.PURGE_LOG_FILES }, 102 | { name: "Exit", value: -1 }, 103 | ], 104 | message: "Menu", 105 | }); 106 | 107 | switch (answer) { 108 | case UtilsMenu.PARSE_TOKENS_BACKUP_FILE: { 109 | return await this.menuParseTokensBackupFile(); 110 | } 111 | 112 | case UtilsMenu.PURGE_LOG_FILES: { 113 | return await this.menuPurgeLogFiles(); 114 | } 115 | } 116 | 117 | return true; // exit 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync } from "node:fs"; 2 | import { homedir } from "node:os"; 3 | import { join } from "node:path"; 4 | 5 | import { config, createLogger, format, transports } from "winston"; 6 | 7 | import { setLogger as zhSetLogger } from "zigbee-herdsman"; 8 | 9 | export const DATA_FOLDER = join(homedir(), "ember-zli"); 10 | export const LOGS_FOLDER = join(DATA_FOLDER, "logs"); 11 | 12 | export const CONF_PORT_PATH = join(DATA_FOLDER, "conf_port.json"); 13 | export const CONF_NETWORK_PATH = join(DATA_FOLDER, "conf_network.json"); 14 | export const CONF_STACK = join(DATA_FOLDER, "conf_stack.json"); 15 | 16 | export const DEFAULT_STACK_CONFIG_PATH = join(DATA_FOLDER, "stack_config.json"); 17 | export const DEFAULT_NETWORK_BACKUP_PATH = join(DATA_FOLDER, "coordinator_backup.json"); 18 | export const DEFAULT_TOKENS_BACKUP_PATH = join(DATA_FOLDER, "tokens_backup.nvm3"); 19 | export const DEFAULT_ROUTER_TOKENS_BACKUP_PATH = join(DATA_FOLDER, "router_tokens_backup.nvm3"); 20 | export const DEFAULT_CONFIGURATION_YAML_PATH = join(DATA_FOLDER, "configuration.yaml"); 21 | export const DEFAULT_FIRMWARE_GBL_PATH = join(DATA_FOLDER, "firmware.gbl"); 22 | export const DEFAULT_ROUTER_SCRIPT_MJS_PATH = join(DATA_FOLDER, "router_script.mjs"); 23 | 24 | export const DEFAULT_PCAP_PATH = join(DATA_FOLDER, "sniff.pcap"); 25 | 26 | if (!existsSync(DATA_FOLDER)) { 27 | mkdirSync(DATA_FOLDER); 28 | } 29 | 30 | if (!existsSync(LOGS_FOLDER)) { 31 | mkdirSync(LOGS_FOLDER); 32 | } 33 | 34 | export const logger = createLogger({ 35 | format: format.combine( 36 | format.errors({ stack: true }), 37 | format.timestamp({ 38 | format: "YYYY-MM-DD hh:mm:ss.SSS", 39 | }), 40 | format.printf((info) => `[${info.timestamp}] ${info.level}: \t${info.namespace ?? "cli"}: ${info.message}`), 41 | ), 42 | levels: config.syslog.levels, 43 | transports: [ 44 | new transports.Console({ 45 | format: format.colorize({ all: true, colors: { debug: "blue", error: "red", info: "green", warning: "yellow" } }), 46 | level: "info", 47 | }), 48 | new transports.File({ 49 | filename: join(LOGS_FOLDER, `ember-zli-${Date.now()}.log`), 50 | level: "debug", 51 | }), 52 | ], 53 | }); 54 | 55 | const getZHMessage = (messageOrLambda: string | (() => string)): string => { 56 | return messageOrLambda instanceof Function ? messageOrLambda() : messageOrLambda; 57 | }; 58 | 59 | zhSetLogger({ 60 | debug(message, namespace) { 61 | logger.debug(getZHMessage(message), { namespace }); 62 | }, 63 | error(message, namespace) { 64 | logger.error(getZHMessage(message), { namespace }); 65 | }, 66 | info(message, namespace) { 67 | logger.info(getZHMessage(message), { namespace }); 68 | }, 69 | warning(message, namespace) { 70 | logger.warning(getZHMessage(message), { namespace }); 71 | }, 72 | }); 73 | 74 | logger.info(`Data folder: ${DATA_FOLDER}.`); 75 | 76 | export { run } from "@oclif/core"; 77 | -------------------------------------------------------------------------------- /src/utils/consts.ts: -------------------------------------------------------------------------------- 1 | import { EmberApsOption } from "zigbee-herdsman/dist/adapter/ember/enums.js"; 2 | import type { AdapterModel } from "./types.js"; 3 | 4 | export const PRE_DEFINED_FIRMWARE_LINKS_URL = "https://github.com/Nerivec/ember-zli/raw/refs/heads/main/firmware-links-v3.json"; 5 | export const ADAPTER_MODELS: ReadonlyArray = [ 6 | "Aeotec Zi-Stick (ZGA008)", 7 | "EasyIOT ZB-GW04 v1.1", 8 | "EasyIOT ZB-GW04 v1.2", 9 | "Nabu Casa SkyConnect", 10 | "Nabu Casa Yellow", 11 | "Nabu Casa ZBT-2", 12 | "SMLight SLZB06-M", 13 | "SMLight SLZB06mg24", 14 | "SMLight SLZB06mg26", 15 | "SMLight SLZB07", 16 | "SMLight SLZB07mg24", 17 | "Sonoff ZBDongle-E", 18 | "SparkFun MGM240p", 19 | "TubeZB MGM24", 20 | "TubeZB MGM24PB", 21 | "ROUTER - Aeotec Zi-Stick (ZGA008)", 22 | "ROUTER - EasyIOT ZB-GW04 v1.1", 23 | "ROUTER - EasyIOT ZB-GW04 v1.2", 24 | "ROUTER - Nabu Casa SkyConnect", 25 | "ROUTER - Nabu Casa Yellow", 26 | "ROUTER - Nabu Casa ZBT-2", 27 | "ROUTER - SMLight SLZB06-M", 28 | "ROUTER - SMLight SLZB06mg24", 29 | "ROUTER - SMLight SLZB06mg26", 30 | "ROUTER - SMLight SLZB07", 31 | "ROUTER - SMLight SLZB07mg24", 32 | "ROUTER - Sonoff ZBDongle-E", 33 | "ROUTER - SparkFun MGM240p", 34 | "ROUTER - TubeZB MGM24", 35 | "ROUTER - TubeZB MGM24PB", 36 | ]; 37 | export const TCP_REGEX = /^tcp:\/\/[\w.-]+:\d+$/; 38 | export const BAUDRATES = [115200, 230400, 460800, 921600]; 39 | /** Read/write max bytes count at stream level */ 40 | export const CONFIG_HIGHWATER_MARK = 256; 41 | 42 | /** Default behavior is to disable app key requests */ 43 | export const ALLOW_APP_KEY_REQUESTS = false; 44 | 45 | export const DEFAULT_APS_OPTIONS = EmberApsOption.RETRY | EmberApsOption.ENABLE_ROUTE_DISCOVERY | EmberApsOption.ENABLE_ADDRESS_DISCOVERY; 46 | 47 | export const APPLICATION_ZDO_SEQUENCE_MASK = 0x7f; 48 | export const DEFAULT_ZDO_REQUEST_RADIUS = 0xff; 49 | 50 | export const TOUCHLINK_CHANNELS = [11, 15, 20, 25]; 51 | 52 | export const CPC_PAYLOAD_LENGTH_MAX = 16; 53 | export const CPC_SYSTEM_COMMAND_HEADER_SIZE = 4; 54 | 55 | export const CPC_HDLC_FLAG_POS = 0; 56 | export const CPC_HDLC_ADDRESS_POS = 1; 57 | export const CPC_HDLC_LENGTH_POS = 2; 58 | export const CPC_HDLC_CONTROL_POS = 4; 59 | export const CPC_HDLC_HCS_POS = 5; 60 | 61 | export const CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT = 6; 62 | export const CPC_HDLC_CONTROL_P_F_SHIFT = 3; 63 | export const CPC_HDLC_CONTROL_SEQ_SHIFT = 4; 64 | export const CPC_HDLC_CONTROL_SUPERVISORY_FNCT_ID_SHIFT = 4; 65 | export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_SHIFT = 0; 66 | 67 | export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_MASK = 0x37; 68 | 69 | export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_INFORMATION = 0x00; 70 | export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_POLL_FINAL = 0x04; 71 | export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_RESET_SEQ = 0x31; 72 | export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_ACKNOWLEDGE = 0x0e; 73 | 74 | export const CPC_HDLC_FLAG_VAL = 0x14; 75 | export const CPC_HDLC_HEADER_SIZE = 5; 76 | export const CPC_HDLC_HEADER_RAW_SIZE = 7; 77 | export const CPC_HDLC_HCS_SIZE = CPC_HDLC_HEADER_RAW_SIZE - CPC_HDLC_HEADER_SIZE; 78 | export const CPC_HDLC_FCS_SIZE = 2; 79 | 80 | export const CPC_HDLC_FRAME_TYPE_UNNUMBERED = 3; 81 | 82 | export const CPC_DEFAULT_COMMAND_TIMEOUT = 1000; 83 | 84 | /** At the next reboot bootloader is executed */ 85 | export const CPC_SYSTEM_REBOOT_MODE_BOOTLOADER = 1; 86 | 87 | export const CPC_PROPERTY_ID_SECONDARY_CPC_VERSION = 0x03; 88 | export const CPC_PROPERTY_ID_BOOTLOADER_REBOOT_MODE = 0x202; 89 | 90 | export const CPC_FLAG_UNNUMBERED_POLL_FINAL = 0x01 << 2; 91 | 92 | export const CPC_SERVICE_ENDPOINT_ID_SYSTEM = 0; 93 | 94 | export const CREATOR_STACK_RESTORED_EUI64 = 0xe12a; 95 | -------------------------------------------------------------------------------- /src/utils/cpc.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "node:events"; 2 | import { EzspBuffalo } from "zigbee-herdsman/dist/adapter/ember/ezsp/buffalo.js"; 3 | import { logger } from "../index.js"; 4 | import { 5 | CPC_DEFAULT_COMMAND_TIMEOUT, 6 | CPC_FLAG_UNNUMBERED_POLL_FINAL, 7 | CPC_HDLC_ADDRESS_POS, 8 | CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT, 9 | CPC_HDLC_CONTROL_POS, 10 | CPC_HDLC_CONTROL_UNNUMBERED_TYPE_SHIFT, 11 | CPC_HDLC_FCS_SIZE, 12 | CPC_HDLC_FLAG_POS, 13 | CPC_HDLC_FLAG_VAL, 14 | CPC_HDLC_FRAME_TYPE_UNNUMBERED, 15 | CPC_HDLC_HCS_POS, 16 | CPC_HDLC_HEADER_RAW_SIZE, 17 | CPC_HDLC_HEADER_SIZE, 18 | CPC_HDLC_LENGTH_POS, 19 | CPC_PAYLOAD_LENGTH_MAX, 20 | CPC_PROPERTY_ID_BOOTLOADER_REBOOT_MODE, 21 | CPC_PROPERTY_ID_SECONDARY_CPC_VERSION, 22 | CPC_SERVICE_ENDPOINT_ID_SYSTEM, 23 | CPC_SYSTEM_COMMAND_HEADER_SIZE, 24 | CPC_SYSTEM_REBOOT_MODE_BOOTLOADER, 25 | } from "./consts.js"; 26 | import { CpcSystemCommandId, CpcSystemStatus } from "./enums.js"; 27 | import { Transport, TransportEvent } from "./transport.js"; 28 | import type { CpcSystemCommand, FirmwareVersionShort, PortConf } from "./types.js"; 29 | import { computeCRC16 } from "./utils.js"; 30 | 31 | const NS = { namespace: "cpc" }; 32 | 33 | export enum CpcEvent { 34 | FAILED = "failed", 35 | } 36 | 37 | interface CpcEventMap { 38 | [CpcEvent.FAILED]: []; 39 | } 40 | 41 | export class Cpc extends EventEmitter { 42 | public readonly transport: Transport; 43 | private buffalo: EzspBuffalo; 44 | private sequence: number; 45 | private waiter: 46 | | { 47 | /** Expected to return true if properly resolved, false if timed out and timeout not considered hard-fail */ 48 | resolve: (value: CpcSystemCommand | PromiseLike) => void; 49 | sequence: number; 50 | timeout: NodeJS.Timeout; 51 | } 52 | | undefined; 53 | 54 | constructor(portConf: PortConf) { 55 | super(); 56 | 57 | this.sequence = 0; 58 | this.waiter = undefined; 59 | this.transport = new Transport(portConf); 60 | this.buffalo = new EzspBuffalo(Buffer.alloc(CPC_PAYLOAD_LENGTH_MAX), 0); 61 | 62 | this.transport.on(TransportEvent.FAILED, this.onTransportFailed.bind(this)); 63 | this.transport.on(TransportEvent.DATA, this.onTransportData.bind(this)); 64 | } 65 | 66 | public async cpcGetVersion(): Promise { 67 | this.buffalo.setPosition(0); 68 | this.buffalo.writeUInt32(CPC_PROPERTY_ID_SECONDARY_CPC_VERSION); 69 | 70 | // req: 14 00 0a00 c4 55d3 02 01 0400 03000000 baaa 71 | // rsp: 14 00 1600 c4 57e5 06 01 1000 03000000 04000000 05000000 00000000 6d3c 72 | const result = await this.sendSystemUFrame(CpcSystemCommandId.PROP_VALUE_GET); 73 | 74 | if (!result) { 75 | throw new Error("Invalid result from PROP_VALUE_GET(SECONDARY_CPC_VERSION) response"); 76 | } 77 | 78 | // const propertyId = result.payload.readUInt32LE(0) 79 | const major = result.payload.readUInt32LE(4); 80 | const minor = result.payload.readUInt32LE(8); 81 | const patch = result.payload.readUInt32LE(12); 82 | 83 | return `${major}.${minor}.${patch}`; 84 | } 85 | 86 | public async cpcLaunchStandaloneBootloader(): Promise { 87 | this.buffalo.setPosition(0); 88 | this.buffalo.writeUInt32(CPC_PROPERTY_ID_BOOTLOADER_REBOOT_MODE); 89 | this.buffalo.writeUInt32(CPC_SYSTEM_REBOOT_MODE_BOOTLOADER); 90 | 91 | // req: 14 00 0e00 c4 950f 02 01 0800 02020000 01000000 190d 92 | // rsp: 14 00 0e00 c4 950f 06 01 0800 02020000 01000000 cd00 93 | const result = await this.sendSystemUFrame(CpcSystemCommandId.PROP_VALUE_SET); 94 | 95 | if (!result) { 96 | throw new Error("Invalid result from PROP_VALUE_SET(BOOTLOADER_REBOOT_MODE) response."); 97 | } 98 | 99 | const status: CpcSystemStatus = result.payload[0]; 100 | 101 | // as of 4.5.0, this is actually returning UNIMPLEMENTED 102 | if (status !== CpcSystemStatus.OK && status !== CpcSystemStatus.UNIMPLEMENTED) { 103 | return status; 104 | } 105 | 106 | this.buffalo.setPosition(0); 107 | // don't want to parse anything coming in after RESET is sent 108 | this.transport.removeAllListeners(TransportEvent.DATA); 109 | // req: 14 00 0300 90 b557 06 c660 110 | await this.sendSystemUFrame(CpcSystemCommandId.RESET, true); 111 | await new Promise((resolve) => { 112 | setTimeout(resolve, 500); 113 | }); 114 | 115 | return CpcSystemStatus.OK; 116 | } 117 | 118 | public receiveSystemUFrame(data: Buffer): void { 119 | if (data.length < CPC_HDLC_HEADER_RAW_SIZE) { 120 | throw new Error(`Received invalid System UFrame length=${data.length} [${data.toString("hex")}].`); 121 | } 122 | 123 | const flag = data.readUInt8(CPC_HDLC_FLAG_POS); 124 | 125 | if (flag !== CPC_HDLC_FLAG_VAL) { 126 | throw new Error(`Received invalid System UFrame flag=${CPC_HDLC_FLAG_VAL}.`); 127 | } 128 | 129 | // const address = data.readUInt8(CPC_HDLC_ADDRESS_POS) 130 | const frameLength = data.readUInt16LE(CPC_HDLC_LENGTH_POS); 131 | const expectedFrameLength = data.length - CPC_HDLC_HEADER_RAW_SIZE; 132 | 133 | if (expectedFrameLength !== frameLength) { 134 | throw new Error(`Received invalid System UFrame length=${data.length} expected=${expectedFrameLength}.`); 135 | } 136 | 137 | const control = data.readUInt8(CPC_HDLC_CONTROL_POS); 138 | const frameType = control >> CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT; 139 | 140 | if (frameType !== CPC_HDLC_FRAME_TYPE_UNNUMBERED) { 141 | throw new Error(`Unsupported frame type ${frameType}.`); 142 | } 143 | 144 | // const unnumberedType = (control >> CPC_HDLC_CONTROL_UNNUMBERED_TYPE_SHIFT) & CPC_HDLC_CONTROL_UNNUMBERED_TYPE_MASK 145 | const headerChecksum = data.readUInt16LE(CPC_HDLC_HEADER_SIZE); 146 | const expectedHeaderChecksum = computeCRC16(data.subarray(0, CPC_HDLC_HEADER_SIZE)).readUInt16BE(); 147 | 148 | if (headerChecksum !== expectedHeaderChecksum) { 149 | throw new Error(`Received invalid System UFrame headerChecksum=${headerChecksum} expected=${expectedHeaderChecksum}.`); 150 | } 151 | 152 | let i = CPC_HDLC_HEADER_RAW_SIZE; 153 | const commandId = data.readUInt8(i++); 154 | const seq = data.readUInt8(i++); 155 | const length = data.readUInt8(i); 156 | i += 2; 157 | const payload = data.subarray(i, -CPC_HDLC_FCS_SIZE); 158 | const frameChecksum = data.readUInt16LE(i + payload.length); 159 | const expectedFrameChecksum = computeCRC16(data.subarray(CPC_HDLC_HEADER_RAW_SIZE, -CPC_HDLC_FCS_SIZE)).readUInt16BE(); 160 | 161 | if (frameChecksum !== expectedFrameChecksum) { 162 | throw new Error(`Received invalid System UFrame frameChecksum=${frameChecksum} expected=${expectedFrameChecksum}.`); 163 | } 164 | 165 | const command: CpcSystemCommand = { commandId, seq, length, payload }; 166 | 167 | logger.debug(`Received System UFrame: ${JSON.stringify(command)}.`); 168 | 169 | this.resolveSequence(command); 170 | } 171 | 172 | public async sendSystemUFrame(commandId: CpcSystemCommandId, noResponse = false): Promise { 173 | const payload = this.buffalo.getWritten(); 174 | this.sequence = (this.sequence + 1) & 0xff; 175 | 176 | const header = Buffer.alloc(CPC_HDLC_HEADER_SIZE); 177 | header.writeUInt8(CPC_HDLC_FLAG_VAL, CPC_HDLC_FLAG_POS); 178 | header.writeUInt8(CPC_SERVICE_ENDPOINT_ID_SYSTEM, CPC_HDLC_ADDRESS_POS); 179 | header.writeUInt16LE(CPC_SYSTEM_COMMAND_HEADER_SIZE + payload.length + CPC_HDLC_FCS_SIZE, CPC_HDLC_LENGTH_POS); 180 | header.writeUInt8( 181 | (CPC_HDLC_FRAME_TYPE_UNNUMBERED << CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT) | 182 | (CPC_FLAG_UNNUMBERED_POLL_FINAL << CPC_HDLC_CONTROL_UNNUMBERED_TYPE_SHIFT), 183 | CPC_HDLC_CONTROL_POS, 184 | ); 185 | 186 | const buffer = Buffer.alloc(CPC_HDLC_HEADER_RAW_SIZE + CPC_SYSTEM_COMMAND_HEADER_SIZE + payload.length + CPC_HDLC_FCS_SIZE); 187 | 188 | buffer.set(header, 0); 189 | 190 | const headerChecksum = computeCRC16(header).readUInt16BE(); 191 | 192 | buffer.writeUInt16LE(headerChecksum, CPC_HDLC_HCS_POS); 193 | 194 | let i = CPC_HDLC_HEADER_RAW_SIZE; 195 | buffer.writeUInt8(commandId, i++); 196 | buffer.writeUInt8(this.sequence, i++); 197 | buffer.writeUInt16LE(payload.length, i); 198 | i += 2; 199 | buffer.set(payload, i); 200 | i += payload.length; 201 | 202 | const frameChecksum = computeCRC16(buffer.subarray(CPC_HDLC_HEADER_RAW_SIZE, i)).readUInt16BE(); 203 | 204 | buffer.writeUInt16LE(frameChecksum, i); 205 | 206 | this.transport.write(buffer); 207 | 208 | if (noResponse) { 209 | return undefined; 210 | } 211 | 212 | return await this.waitForSequence(this.sequence, CPC_DEFAULT_COMMAND_TIMEOUT); 213 | } 214 | 215 | public async start(): Promise { 216 | return await this.transport.initPort(); 217 | } 218 | 219 | public async stop(): Promise { 220 | await this.transport.close(false); 221 | } 222 | 223 | private async onTransportData(received: Buffer): Promise { 224 | logger.debug(`Received transport data: ${received.toString("hex")}.`, NS); 225 | 226 | this.receiveSystemUFrame(received); 227 | } 228 | 229 | private onTransportFailed(): void { 230 | this.emit(CpcEvent.FAILED); 231 | } 232 | 233 | private resolveSequence(command: CpcSystemCommand): void { 234 | if (this.waiter?.sequence === command.seq) { 235 | clearTimeout(this.waiter.timeout); 236 | this.waiter.resolve(command); 237 | 238 | this.waiter = undefined; 239 | } 240 | } 241 | 242 | private waitForSequence(sequence: number, timeout: number = CPC_DEFAULT_COMMAND_TIMEOUT): Promise { 243 | return new Promise((resolve) => { 244 | this.waiter = { 245 | resolve, 246 | sequence, 247 | timeout: setTimeout(() => { 248 | const msg = `Timed out waiting for sequence(${sequence}) after ${timeout}ms.`; 249 | this.waiter = undefined; 250 | 251 | logger.error(msg, NS); 252 | this.emit(CpcEvent.FAILED); 253 | }, timeout), 254 | }; 255 | }); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/utils/ember.ts: -------------------------------------------------------------------------------- 1 | import { type Zcl, ZSpec } from "zigbee-herdsman"; 2 | import type { DEFAULT_STACK_CONFIG } from "zigbee-herdsman/dist/adapter/ember/adapter/emberAdapter.js"; 3 | import { FIXED_ENDPOINTS } from "zigbee-herdsman/dist/adapter/ember/adapter/endpoints.js"; 4 | import { 5 | EMBER_HIGH_RAM_CONCENTRATOR, 6 | EMBER_LOW_RAM_CONCENTRATOR, 7 | SECURITY_LEVEL_Z3, 8 | STACK_PROFILE_ZIGBEE_PRO, 9 | } from "zigbee-herdsman/dist/adapter/ember/consts.js"; 10 | import { 11 | EmberKeyStructBitmask, 12 | EmberLibraryId, 13 | EmberLibraryStatus, 14 | EmberNetworkInitBitmask, 15 | EmberSourceRouteDiscoveryMode, 16 | EmberVersionType, 17 | EzspStatus, 18 | IEEE802154CcaMode, 19 | SLStatus, 20 | } from "zigbee-herdsman/dist/adapter/ember/enums.js"; 21 | import { EZSP_MIN_PROTOCOL_VERSION, EZSP_PROTOCOL_VERSION, EZSP_STACK_TYPE_MESH } from "zigbee-herdsman/dist/adapter/ember/ezsp/consts.js"; 22 | import { EzspConfigId, EzspDecisionId, EzspPolicyId, EzspValueId } from "zigbee-herdsman/dist/adapter/ember/ezsp/enums.js"; 23 | import { Ezsp } from "zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js"; 24 | import type { EmberMulticastId, EmberMulticastTableEntry, EmberNetworkInitStruct } from "zigbee-herdsman/dist/adapter/ember/types.js"; 25 | import { lowHighBytes } from "zigbee-herdsman/dist/adapter/ember/utils/math.js"; 26 | import { logger } from "../index.js"; 27 | import { NVM3ObjectKey } from "./enums.js"; 28 | import { ROUTER_FIXED_ENDPOINTS } from "./router-endpoints.js"; 29 | import type { EmberFullVersion, PortConf } from "./types.js"; 30 | 31 | const NS = { namespace: "ember" }; 32 | export let emberFullVersion: EmberFullVersion = { 33 | ezsp: -1, 34 | revision: "unknown", 35 | build: -1, 36 | major: -1, 37 | minor: -1, 38 | patch: -1, 39 | special: -1, 40 | type: EmberVersionType.PRE_RELEASE, 41 | }; 42 | 43 | export const waitForStackStatus = async (ezsp: Ezsp, status: SLStatus, timeout = 10000): Promise => 44 | await new Promise((resolve, reject) => { 45 | const timeoutHandle = setTimeout(() => { 46 | ezsp.removeListener("stackStatus", onStackStatus); 47 | return reject(new Error(`Timed out waiting for stack status '${SLStatus[status]}'.`)); 48 | }, timeout); 49 | const onStackStatus = (receivedStatus: SLStatus): void => { 50 | logger.debug(`Received stack status ${receivedStatus} while waiting for ${status}.`, NS); 51 | 52 | if (status === receivedStatus) { 53 | clearTimeout(timeoutHandle); 54 | ezsp.removeListener("stackStatus", onStackStatus); 55 | resolve(); 56 | } 57 | }; 58 | 59 | ezsp.on("stackStatus", onStackStatus); 60 | }); 61 | 62 | export const emberStart = async (portConf: PortConf): Promise => { 63 | const ezsp = new Ezsp({ adapter: "ember", ...portConf }); 64 | 65 | // NOTE: something deep in this call can throw too 66 | const startResult = await ezsp.start(); 67 | 68 | if (startResult !== 0) { 69 | throw new Error(`Failed to start EZSP layer with status=${EzspStatus[startResult]}.`); 70 | } 71 | 72 | // call before any other command, else fails 73 | emberFullVersion = await emberVersion(ezsp); 74 | 75 | return ezsp; 76 | }; 77 | 78 | export const emberStop = async (ezsp: Ezsp): Promise => { 79 | // workaround to remove ASH COUNTERS logged on stop 80 | // @ts-expect-error workaround (overriding private) 81 | ezsp.ash.logCounters = (): void => {}; 82 | 83 | await ezsp.stop(); 84 | }; 85 | 86 | export const emberVersion = async (ezsp: Ezsp): Promise => { 87 | // send the Host version number to the NCP. 88 | // The NCP returns the EZSP version that the NCP is running along with the stackType and stackVersion 89 | let [ncpEzspProtocolVer, ncpStackType, ncpStackVer] = await ezsp.ezspVersion(EZSP_PROTOCOL_VERSION); 90 | 91 | // verify that the stack type is what is expected 92 | if (ncpStackType !== EZSP_STACK_TYPE_MESH) { 93 | throw new Error(`Stack type ${ncpStackType} is not expected!`); 94 | } 95 | 96 | if (ncpEzspProtocolVer === EZSP_PROTOCOL_VERSION) { 97 | logger.debug(`NCP EZSP protocol version (${ncpEzspProtocolVer}) matches Host.`, NS); 98 | } else if (ncpEzspProtocolVer < EZSP_PROTOCOL_VERSION && ncpEzspProtocolVer >= EZSP_MIN_PROTOCOL_VERSION) { 99 | [ncpEzspProtocolVer, ncpStackType, ncpStackVer] = await ezsp.ezspVersion(ncpEzspProtocolVer); 100 | 101 | logger.info(`NCP EZSP protocol version (${ncpEzspProtocolVer}) lower than Host. Switched.`, NS); 102 | } else { 103 | throw new Error( 104 | `NCP EZSP protocol version (${ncpEzspProtocolVer}) is not supported by Host [${EZSP_MIN_PROTOCOL_VERSION}-${EZSP_PROTOCOL_VERSION}].`, 105 | ); 106 | } 107 | 108 | ezsp.setProtocolVersion(ncpEzspProtocolVer); 109 | logger.debug(`NCP info: EZSPVersion=${ncpEzspProtocolVer} StackType=${ncpStackType} StackVersion=${ncpStackVer}`, NS); 110 | 111 | const [status, versionStruct] = await ezsp.ezspGetVersionStruct(); 112 | 113 | if (status !== SLStatus.OK) { 114 | // Should never happen with support of only EZSP v13+ 115 | throw new Error("NCP has old-style version number. Not supported."); 116 | } 117 | 118 | const version: EmberFullVersion = { 119 | ezsp: ncpEzspProtocolVer, 120 | revision: `${versionStruct.major}.${versionStruct.minor}.${versionStruct.patch} [${EmberVersionType[versionStruct.type]}]`, 121 | ...versionStruct, 122 | }; 123 | 124 | if (versionStruct.type !== EmberVersionType.GA) { 125 | logger.warning(`NCP is running a non-GA version (${EmberVersionType[versionStruct.type]}).`, NS); 126 | } 127 | 128 | logger.info(`NCP version: ${JSON.stringify(version)}`, NS); 129 | 130 | return version; 131 | }; 132 | 133 | export const emberNetworkInit = async (ezsp: Ezsp, wasConfigured = false): Promise => { 134 | if (!wasConfigured) { 135 | // minimum required for proper network init 136 | const status = await ezsp.ezspSetConfigurationValue(EzspConfigId.STACK_PROFILE, STACK_PROFILE_ZIGBEE_PRO); 137 | 138 | if (status !== SLStatus.OK) { 139 | throw new Error(`Failed to set stack profile with status=${SLStatus[status]}.`); 140 | } 141 | } 142 | 143 | const networkInitStruct: EmberNetworkInitStruct = { 144 | bitmask: EmberNetworkInitBitmask.PARENT_INFO_IN_TOKEN | EmberNetworkInitBitmask.END_DEVICE_REJOIN_ON_REBOOT, 145 | }; 146 | 147 | return await ezsp.ezspNetworkInit(networkInitStruct); 148 | }; 149 | 150 | export const emberNetworkConfig = async ( 151 | ezsp: Ezsp, 152 | stackConf: typeof DEFAULT_STACK_CONFIG, 153 | manufacturerCode: Zcl.ManufacturerCode, 154 | ): Promise => { 155 | /** The address cache needs to be initialized and used with the source routing code for the trust center to operate properly. */ 156 | await ezsp.ezspSetConfigurationValue(EzspConfigId.TRUST_CENTER_ADDRESS_CACHE_SIZE, 2); 157 | /** MAC indirect timeout should be 7.68 secs (STACK_PROFILE_ZIGBEE_PRO) */ 158 | await ezsp.ezspSetConfigurationValue(EzspConfigId.INDIRECT_TRANSMISSION_TIMEOUT, 7680); 159 | /** Max hops should be 2 * nwkMaxDepth, where nwkMaxDepth is 15 (STACK_PROFILE_ZIGBEE_PRO) */ 160 | await ezsp.ezspSetConfigurationValue(EzspConfigId.MAX_HOPS, 30); 161 | await ezsp.ezspSetConfigurationValue(EzspConfigId.SUPPORTED_NETWORKS, 1); 162 | // allow other devices to modify the binding table 163 | await ezsp.ezspSetPolicy(EzspPolicyId.BINDING_MODIFICATION_POLICY, EzspDecisionId.CHECK_BINDING_MODIFICATIONS_ARE_VALID_ENDPOINT_CLUSTERS); 164 | // return message tag only in ezspMessageSentHandler() 165 | await ezsp.ezspSetPolicy(EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY, EzspDecisionId.MESSAGE_TAG_ONLY_IN_CALLBACK); 166 | await ezsp.ezspSetValue(EzspValueId.TRANSIENT_DEVICE_TIMEOUT, 2, lowHighBytes(stackConf.TRANSIENT_DEVICE_TIMEOUT)); 167 | await ezsp.ezspSetManufacturerCode(manufacturerCode); 168 | // network security init 169 | await ezsp.ezspSetConfigurationValue(EzspConfigId.STACK_PROFILE, STACK_PROFILE_ZIGBEE_PRO); 170 | await ezsp.ezspSetConfigurationValue(EzspConfigId.SECURITY_LEVEL, SECURITY_LEVEL_Z3); 171 | // common configs 172 | await ezsp.ezspSetConfigurationValue(EzspConfigId.MAX_END_DEVICE_CHILDREN, stackConf.MAX_END_DEVICE_CHILDREN); 173 | await ezsp.ezspSetConfigurationValue(EzspConfigId.END_DEVICE_POLL_TIMEOUT, stackConf.END_DEVICE_POLL_TIMEOUT); 174 | await ezsp.ezspSetConfigurationValue(EzspConfigId.TRANSIENT_KEY_TIMEOUT_S, stackConf.TRANSIENT_KEY_TIMEOUT_S); 175 | // XXX: temp-fix: forces a side-effect in the firmware that prevents broadcast issues in environments with unusual interferences 176 | await ezsp.ezspSetValue(EzspValueId.CCA_THRESHOLD, 1, [0]); 177 | 178 | if (stackConf.CCA_MODE) { 179 | // validated in `loadStackConfig` 180 | await ezsp.ezspSetRadioIeee802154CcaMode(IEEE802154CcaMode[stackConf.CCA_MODE]); 181 | } 182 | }; 183 | 184 | export const emberRegisterFixedEndpoints = async (ezsp: Ezsp, multicastTable: EmberMulticastId[], router = false): Promise => { 185 | for (const ep of router ? ROUTER_FIXED_ENDPOINTS : FIXED_ENDPOINTS) { 186 | if (ep.networkIndex !== 0x00) { 187 | logger.debug(`Multi-network not currently supported. Skipping endpoint ${JSON.stringify(ep)}.`, NS); 188 | continue; 189 | } 190 | 191 | const [epStatus] = await ezsp.ezspGetEndpointFlags(ep.endpoint); 192 | 193 | // endpoint already registered 194 | if (epStatus === SLStatus.OK) { 195 | logger.debug(`Endpoint '${ep.endpoint}' already registered.`, NS); 196 | } else { 197 | // check to see if ezspAddEndpoint needs to be called 198 | // if ezspInit is called without NCP reset, ezspAddEndpoint is not necessary and will return an error 199 | const status = await ezsp.ezspAddEndpoint( 200 | ep.endpoint, 201 | ep.profileId, 202 | ep.deviceId, 203 | ep.deviceVersion, 204 | [...ep.inClusterList], // copy 205 | [...ep.outClusterList], // copy 206 | ); 207 | 208 | if (status === SLStatus.OK) { 209 | logger.debug(`Registered endpoint '${ep.endpoint}'.`, NS); 210 | } else { 211 | throw new Error(`Failed to register endpoint '${ep.endpoint}' with status=${SLStatus[status]}.`); 212 | } 213 | } 214 | 215 | for (const multicastId of ep.multicastIds) { 216 | const multicastEntry: EmberMulticastTableEntry = { 217 | multicastId, 218 | endpoint: ep.endpoint, 219 | networkIndex: ep.networkIndex, 220 | }; 221 | 222 | const status = await ezsp.ezspSetMulticastTableEntry(multicastTable.length, multicastEntry); 223 | 224 | if (status !== SLStatus.OK) { 225 | throw new Error(`Failed to register group '${multicastId}' in multicast table with status=${SLStatus[status]}.`); 226 | } 227 | 228 | logger.debug(`Registered multicast table entry (${multicastTable.length}): ${JSON.stringify(multicastEntry)}.`, NS); 229 | multicastTable.push(multicastEntry.multicastId); 230 | } 231 | } 232 | }; 233 | 234 | export const emberSetConcentrator = async (ezsp: Ezsp, stackConf: typeof DEFAULT_STACK_CONFIG): Promise => { 235 | const status = await ezsp.ezspSetConcentrator( 236 | true, 237 | stackConf.CONCENTRATOR_RAM_TYPE === "low" ? EMBER_LOW_RAM_CONCENTRATOR : EMBER_HIGH_RAM_CONCENTRATOR, 238 | stackConf.CONCENTRATOR_MIN_TIME, 239 | stackConf.CONCENTRATOR_MAX_TIME, 240 | stackConf.CONCENTRATOR_ROUTE_ERROR_THRESHOLD, 241 | stackConf.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, 242 | stackConf.CONCENTRATOR_MAX_HOPS, 243 | ); 244 | 245 | if (status !== SLStatus.OK) { 246 | throw new Error(`[CONCENTRATOR] Failed to set concentrator with status=${SLStatus[status]}.`); 247 | } 248 | 249 | const remainTilMTORR = await ezsp.ezspSetSourceRouteDiscoveryMode(EmberSourceRouteDiscoveryMode.RESCHEDULE); 250 | 251 | logger.info(`[CONCENTRATOR] Started source route discovery. ${remainTilMTORR}ms until next broadcast.`, NS); 252 | }; 253 | 254 | // -- Utils 255 | 256 | export const getLibraryStatus = (id: EmberLibraryId, status: EmberLibraryStatus): string => { 257 | if (status === EmberLibraryStatus.LIBRARY_ERROR) { 258 | return "ERROR"; 259 | } 260 | 261 | let statusStr = "NOT_PRESENT"; 262 | const present = Boolean(status & EmberLibraryStatus.LIBRARY_PRESENT_MASK); 263 | 264 | if (present) { 265 | statusStr = "PRESENT"; 266 | 267 | if (id === EmberLibraryId.ZIGBEE_PRO) { 268 | statusStr += status & EmberLibraryStatus.ZIGBEE_PRO_LIBRARY_HAVE_ROUTER_CAPABILITY ? " / ROUTER_CAPABILITY" : " / END_DEVICE_ONLY"; 269 | 270 | if (status & EmberLibraryStatus.ZIGBEE_PRO_LIBRARY_ZLL_SUPPORT) { 271 | statusStr += " / ZLL_SUPPORT"; 272 | } 273 | } 274 | 275 | if (id === EmberLibraryId.SECURITY_CORE) { 276 | statusStr += status & EmberLibraryStatus.SECURITY_LIBRARY_HAVE_ROUTER_SUPPORT ? " / ROUTER_SUPPORT" : " / END_DEVICE_ONLY"; 277 | } 278 | 279 | if (id === EmberLibraryId.PACKET_VALIDATE) { 280 | statusStr += status & EmberLibraryStatus.PACKET_VALIDATE_LIBRARY_ENABLED ? " / ENABLED" : " / DISABLED"; 281 | } 282 | } 283 | 284 | return statusStr; 285 | }; 286 | 287 | export const getKeyStructBitmask = (bitmask: EmberKeyStructBitmask): string => { 288 | const bitmaskValues: string[] = []; 289 | 290 | for (const key in EmberKeyStructBitmask) { 291 | const val = EmberKeyStructBitmask[key as keyof typeof EmberKeyStructBitmask]; 292 | 293 | if (typeof val !== "number") { 294 | continue; 295 | } 296 | 297 | if (bitmask & val) { 298 | bitmaskValues.push(key); 299 | } 300 | } 301 | 302 | return bitmaskValues.join("|"); 303 | }; 304 | 305 | export const parseTokenData = (nvm3Key: NVM3ObjectKey, data: Buffer): string => { 306 | switch (nvm3Key) { 307 | case NVM3ObjectKey.STACK_BOOT_COUNTER: 308 | case NVM3ObjectKey.STACK_NONCE_COUNTER: 309 | case NVM3ObjectKey.STACK_ANALYSIS_REBOOT: 310 | case NVM3ObjectKey.MULTI_NETWORK_STACK_NONCE_COUNTER: 311 | case NVM3ObjectKey.STACK_APS_FRAME_COUNTER: 312 | case NVM3ObjectKey.STACK_GP_INCOMING_FC: 313 | case NVM3ObjectKey.STACK_GP_INCOMING_FC_IN_SINK: { 314 | return `${data.readUIntLE(0, data.length)}`; 315 | } 316 | 317 | case NVM3ObjectKey.STACK_MIN_RECEIVED_RSSI: { 318 | return `${data.readIntLE(0, data.length)}`; 319 | } 320 | 321 | case NVM3ObjectKey.STACK_CHILD_TABLE: { 322 | // TODO 323 | return `EUI64: ${data.subarray(0, 8).toString("hex")} | ${data.subarray(8).toString("hex")}`; 324 | } 325 | 326 | // TODO: 327 | // case NVM3ObjectKey.STACK_BINDING_TABLE: {} 328 | 329 | // TODO: 330 | // case NVM3ObjectKey.STACK_KEY_TABLE: {} 331 | 332 | case NVM3ObjectKey.STACK_TRUST_CENTER: { 333 | // TODO 334 | return `${data.subarray(0, 2).toString("hex")} | EUI64: ${data.subarray(2, 10).toString("hex")} | Link Key: ${data.subarray(10).toString("hex")}`; 335 | } 336 | 337 | case NVM3ObjectKey.STACK_KEYS: 338 | case NVM3ObjectKey.STACK_ALTERNATE_KEY: { 339 | // TODO 340 | return `Network Key: ${data.subarray(0, -1).toString("hex")} | Sequence Number: ${data.readUInt8(16)}`; 341 | } 342 | 343 | case NVM3ObjectKey.STACK_NODE_DATA: { 344 | // TODO 345 | // [4-5] === network join status? 346 | return ( 347 | `PAN ID: ${data.subarray(0, 2).toString("hex")} | Radio TX Power ${data.readUInt8(2)} | Radio Channel ${data.readUInt8(3)} ` + 348 | `| ${data.subarray(4, 8).toString("hex")} | Ext PAN ID: ${data.subarray(8, 16).toString("hex")}` 349 | ); 350 | } 351 | 352 | case NVM3ObjectKey.STACK_NETWORK_MANAGEMENT: { 353 | // TODO 354 | return `Channels: ${ZSpec.Utils.uint32MaskToChannels(data.readUInt32LE(0))} | ${data.subarray(4).toString("hex")}`; 355 | } 356 | 357 | default: { 358 | return data.toString("hex"); 359 | } 360 | } 361 | }; 362 | -------------------------------------------------------------------------------- /src/utils/enums.ts: -------------------------------------------------------------------------------- 1 | export enum FirmwareValidation { 2 | VALID = 0, 3 | INVALID = 1, 4 | CANCELLED = 2, 5 | } 6 | 7 | /** 8 | * The NVM3 object key is used as a distinct identifier tag for a token stored in NVM3. 9 | */ 10 | export enum NVM3ObjectKey { 11 | // STACK KEYS 12 | STACK_NVDATA_VERSION = 0x10000 | 0xff01, 13 | STACK_BOOT_COUNTER = 0x10000 | 0xe263, 14 | STACK_NONCE_COUNTER = 0x10000 | 0xe563, 15 | STACK_ANALYSIS_REBOOT = 0x10000 | 0xe162, 16 | STACK_KEYS = 0x10000 | 0xeb79, 17 | STACK_NODE_DATA = 0x10000 | 0xee64, 18 | STACK_CLASSIC_DATA = 0x10000 | 0xe364, 19 | STACK_ALTERNATE_KEY = 0x10000 | 0xe475, 20 | STACK_APS_FRAME_COUNTER = 0x10000 | 0xe123, 21 | STACK_TRUST_CENTER = 0x10000 | 0xe124, 22 | STACK_NETWORK_MANAGEMENT = 0x10000 | 0xe125, 23 | STACK_PARENT_INFO = 0x10000 | 0xe126, 24 | STACK_PARENT_ADDITIONAL_INFO = 0x10000 | 0xe127, 25 | STACK_MULTI_PHY_NWK_INFO = 0x10000 | 0xe128, 26 | STACK_MIN_RECEIVED_RSSI = 0x10000 | 0xe129, 27 | // Restored EUI64 28 | STACK_RESTORED_EUI64 = 0x10000 | 0xe12a, 29 | 30 | // MULTI-NETWORK STACK KEYS 31 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 32 | MULTI_NETWORK_STACK_KEYS = 0x10000 | 0x0000, 33 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 34 | MULTI_NETWORK_STACK_NODE_DATA = 0x10000 | 0x0080, 35 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 36 | MULTI_NETWORK_STACK_ALTERNATE_KEY = 0x10000 | 0x0100, 37 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 38 | MULTI_NETWORK_STACK_TRUST_CENTER = 0x10000 | 0x0180, 39 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 40 | MULTI_NETWORK_STACK_NETWORK_MANAGEMENT = 0x10000 | 0x0200, 41 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 42 | MULTI_NETWORK_STACK_PARENT_INFO = 0x10000 | 0x0280, 43 | 44 | // Temporary solution for multi-network nwk counters: 45 | // This counter will be used on the network with index 1. 46 | MULTI_NETWORK_STACK_NONCE_COUNTER = 0x10000 | 0xe220, 47 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved 48 | MULTI_NETWORK_STACK_PARENT_ADDITIONAL_INFO = 0x10000 | 0x0300, 49 | 50 | // GP stack tokens. 51 | STACK_GP_DATA = 0x10000 | 0xe258, 52 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 53 | STACK_GP_PROXY_TABLE = 0x10000 | 0x0380, 54 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 55 | STACK_GP_SINK_TABLE = 0x10000 | 0x0400, 56 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved 57 | STACK_GP_INCOMING_FC = 0x10000 | 0x0480, 58 | 59 | // APP KEYS 60 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 61 | STACK_BINDING_TABLE = 0x10000 | 0x0500, 62 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 63 | STACK_CHILD_TABLE = 0x10000 | 0x0580, 64 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 65 | STACK_KEY_TABLE = 0x10000 | 0x0600, 66 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 67 | STACK_CERTIFICATE_TABLE = 0x10000 | 0x0680, 68 | STACK_ZLL_DATA = 0x10000 | 0xe501, 69 | STACK_ZLL_SECURITY = 0x10000 | 0xe502, 70 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved. 71 | STACK_ADDITIONAL_CHILD_DATA = 0x10000 | 0x0700, 72 | 73 | // This key is used for an indexed token and the subsequent 0x7F keys are also reserved 74 | STACK_GP_INCOMING_FC_IN_SINK = 0x10000 | 0x0780, 75 | } 76 | 77 | /** Enumeration representing spinel protocol status code. uint32_t */ 78 | export enum CpcSystemStatus { 79 | /** Operation has completed successfully. */ 80 | OK = 0, 81 | /** Operation has failed for some undefined reason. */ 82 | FAILURE = 1, 83 | /** The given operation has not been implemented. */ 84 | UNIMPLEMENTED = 2, 85 | /** An argument to the given operation is invalid. */ 86 | INVALID_ARGUMENT = 3, 87 | /** The given operation is invalid for the current state of the device. */ 88 | INVALID_STATE = 4, 89 | /** The given command is not recognized. */ 90 | INVALID_COMMAND = 5, 91 | /** The given Spinel interface is not supported. */ 92 | INVALID_INTERFACE = 6, 93 | /** An internal runtime error has occurred. */ 94 | INTERNAL_ERROR = 7, 95 | /** A security or authentication error has occurred. */ 96 | SECURITY_ERROR = 8, 97 | /** An error has occurred while parsing the command. */ 98 | PARSE_ERROR = 9, 99 | /** The operation is in progress and will be completed asynchronously. */ 100 | IN_PROGRESS = 10, 101 | /** The operation has been prevented due to memory pressure. */ 102 | NOMEM = 11, 103 | /** The device is currently performing a mutually exclusive operation. */ 104 | BUSY = 12, 105 | /** The given property is not recognized. */ 106 | PROP_NOT_FOUND = 13, 107 | /** The packet was dropped. */ 108 | PACKET_DROPPED = 14, 109 | /** The result of the operation is empty. */ 110 | EMPTY = 15, 111 | /** The command was too large to fit in the internal buffer. */ 112 | CMD_TOO_BIG = 16, 113 | /** The packet was not acknowledged. */ 114 | NO_ACK = 17, 115 | /** The packet was not sent due to a CCA failure. */ 116 | CCA_FAILURE = 18, 117 | /** The operation is already in progress or the property was already set to the given value. */ 118 | ALREADY = 19, 119 | /** The given item could not be found in the property. */ 120 | ITEM_NOT_FOUND = 20, 121 | /** The given command cannot be performed on this property. */ 122 | INVALID_COMMAND_FOR_PROP = 21, 123 | // 22-111 : RESERVED 124 | RESET_POWER_ON = 112, 125 | RESET_EXTERNAL = 113, 126 | RESET_SOFTWARE = 114, 127 | RESET_FAULT = 115, 128 | RESET_CRASH = 116, 129 | RESET_ASSERT = 117, 130 | RESET_OTHER = 118, 131 | RESET_UNKNOWN = 119, 132 | RESET_WATCHDOG = 120, 133 | // 121-127 : RESERVED-RESET-CODES 134 | // 128 - 15,359: UNALLOCATED 135 | // 15,360 - 16,383: Vendor-specific 136 | // 16,384 - 1,999,999: UNALLOCATED 137 | // 2,000,000 - 2,097,151: Experimental Use Only (MUST NEVER be used in production!) 138 | } 139 | 140 | export enum CpcSystemCommandId { 141 | NOOP = 0x00, 142 | RESET = 0x01, 143 | PROP_VALUE_GET = 0x02, 144 | PROP_VALUE_SET = 0x03, 145 | PROP_VALUE_IS = 0x06, 146 | INVALID = 0xff, 147 | } 148 | -------------------------------------------------------------------------------- /src/utils/port.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, writeFileSync } from "node:fs"; 2 | import { confirm, input, select } from "@inquirer/prompts"; 3 | import { Bonjour } from "bonjour-service"; 4 | import { SerialPort } from "zigbee-herdsman/dist/adapter/serialPort.js"; 5 | import { CONF_PORT_PATH, logger } from "../index.js"; 6 | import { BAUDRATES, TCP_REGEX } from "./consts.js"; 7 | import type { BaudRate, PortConf, PortType, SelectChoices } from "./types.js"; 8 | 9 | async function findmDNSAdapters(): Promise> { 10 | logger.info("Starting mDNS discovery..."); 11 | 12 | const bonjour = new Bonjour(); 13 | const adapters: SelectChoices = [{ name: "Not in this list", value: undefined }]; 14 | const browser = bonjour.find(null, (service) => { 15 | if (service.txt && service.txt.radio_type === "ezsp") { 16 | logger.debug(`Found matching service: ${JSON.stringify(service)}`); 17 | 18 | const path = `tcp://${service.addresses?.[0] ?? service.host}:${service.port}`; 19 | 20 | adapters.push({ name: `${service.name ?? service.txt.name ?? "Unknown"} (${path})`, value: path }); 21 | } 22 | }); 23 | 24 | browser.start(); 25 | 26 | return await new Promise((resolve) => { 27 | setTimeout(() => { 28 | browser.stop(); 29 | bonjour.destroy(); 30 | resolve(adapters); 31 | }, 2000); 32 | }); 33 | } 34 | 35 | export const getPortConfFile = async (): Promise => { 36 | if (!existsSync(CONF_PORT_PATH)) { 37 | return undefined; 38 | } 39 | 40 | const file = readFileSync(CONF_PORT_PATH, "utf8"); 41 | const conf: PortConf = JSON.parse(file); 42 | 43 | if (!conf.path) { 44 | logger.error("Cached config does not include a valid path value."); 45 | return undefined; 46 | } 47 | 48 | if (!TCP_REGEX.test(conf.path)) { 49 | // serial-only validation 50 | if (!conf.baudRate || !BAUDRATES.includes(conf.baudRate)) { 51 | logger.error("Cached config does not include a valid baudrate value."); 52 | return undefined; 53 | } 54 | 55 | const portList = await SerialPort.list(); 56 | 57 | if (portList.length === 0) { 58 | logger.error("Cached config is using serial, no serial device currently connected."); 59 | return undefined; 60 | } 61 | 62 | if (!portList.some((p) => p.path === conf.path)) { 63 | logger.error("Cached config path does not match a currently connected serial device."); 64 | return undefined; 65 | } 66 | 67 | if (conf.rtscts !== true && conf.rtscts !== false) { 68 | logger.error("Cached config does not include a valid rtscts value."); 69 | return undefined; 70 | } 71 | 72 | if (conf.xon !== true && conf.xon !== false) { 73 | conf.xon = !conf.rtscts; 74 | logger.debug(`Cached config does not include a valid xon value. Derived from rtscts (will be ${conf.xon}).`); 75 | } 76 | 77 | if (conf.xoff !== true && conf.xoff !== false) { 78 | conf.xoff = !conf.rtscts; 79 | logger.debug(`Cached config does not include a valid xoff value. Derived from rtscts (will be ${conf.xoff}).`); 80 | } 81 | } 82 | 83 | return conf; 84 | }; 85 | 86 | export const getPortConf = async (): Promise => { 87 | const portConfFile = await getPortConfFile(); 88 | 89 | if (portConfFile !== undefined) { 90 | const isTcp = TCP_REGEX.test(portConfFile.path); 91 | const usePortConfFile = await confirm({ 92 | default: true, 93 | message: `Path: ${portConfFile.path}${isTcp ? "" : `, Baudrate: ${portConfFile.baudRate}, RTS/CTS: ${portConfFile.rtscts}`}. Use this config?`, 94 | }); 95 | 96 | if (usePortConfFile) { 97 | return portConfFile; 98 | } 99 | } 100 | 101 | const type = await select({ 102 | choices: [ 103 | { name: "Serial", value: "serial" }, 104 | { name: "TCP", value: "tcp" }, 105 | ], 106 | message: "Adapter connection type", 107 | }); 108 | 109 | let baudRate = BAUDRATES[0]; 110 | let path = null; 111 | let rtscts = false; 112 | 113 | switch (type) { 114 | case "serial": { 115 | const baudrateChoices = []; 116 | 117 | for (const v of BAUDRATES) { 118 | baudrateChoices.push({ name: v.toString(), value: v }); 119 | } 120 | 121 | baudRate = await select({ 122 | choices: baudrateChoices, 123 | message: "Adapter firmware baudrate", 124 | }); 125 | 126 | const portList = await SerialPort.list(); 127 | 128 | if (portList.length === 0) { 129 | throw new Error("No serial device found."); 130 | } 131 | 132 | path = await select({ 133 | choices: portList.map((p) => ({ 134 | // @ts-expect-error friendlyName windows only 135 | name: `${p.manufacturer} ${p.friendlyName ?? ""} ${p.pnpId} (${p.path})`, 136 | value: p.path, 137 | })), 138 | message: "Serial port", 139 | }); 140 | 141 | const fcChoices = [ 142 | { name: "Software Flow Control (rtscts=false)", value: false }, 143 | { name: "Hardware Flow Control (rtscts=true)", value: true }, 144 | ]; 145 | rtscts = await select({ 146 | choices: fcChoices, 147 | message: "Flow control", 148 | }); 149 | 150 | break; 151 | } 152 | 153 | case "tcp": { 154 | const discover = await confirm({ message: "Try to discover adapter?", default: true }); 155 | 156 | if (discover) { 157 | const choices = await findmDNSAdapters(); 158 | 159 | path = await select({ message: "Select adapter", choices }); 160 | } 161 | 162 | if (!discover || !path) { 163 | path = await input({ 164 | message: `TCP path ('tcp://:')`, 165 | validate(value) { 166 | return TCP_REGEX.test(value); 167 | }, 168 | }); 169 | } 170 | 171 | break; 172 | } 173 | } 174 | 175 | if (!path) { 176 | throw new Error("Invalid port path."); 177 | } 178 | 179 | const conf = { baudRate, path, rtscts, xon: !rtscts, xoff: !rtscts }; 180 | 181 | try { 182 | writeFileSync(CONF_PORT_PATH, JSON.stringify(conf, null, 2), "utf8"); 183 | } catch { 184 | logger.error(`Could not write port conf to ${CONF_PORT_PATH}.`); 185 | } 186 | 187 | return conf; 188 | }; 189 | -------------------------------------------------------------------------------- /src/utils/router-endpoints.ts: -------------------------------------------------------------------------------- 1 | import { Zcl, ZSpec } from "zigbee-herdsman"; 2 | import type { EmberMulticastId } from "zigbee-herdsman/dist/adapter/ember/types.js"; 3 | import type { ClusterId, ProfileId } from "zigbee-herdsman/dist/zspec/tstypes.js"; 4 | 5 | type FixedEndpointInfo = { 6 | /** Actual Zigbee endpoint number. uint8_t */ 7 | endpoint: number; 8 | /** Profile ID of the device on this endpoint. */ 9 | profileId: ProfileId; 10 | /** Device ID of the device on this endpoint. uint16_t */ 11 | deviceId: number; 12 | /** Version of the device. uint8_t */ 13 | deviceVersion: number; 14 | /** List of server clusters. */ 15 | inClusterList: readonly ClusterId[]; 16 | /** List of client clusters. */ 17 | outClusterList: readonly ClusterId[]; 18 | /** Network index for this endpoint. uint8_t */ 19 | networkIndex: number; 20 | /** Multicast group IDs to register in the multicast table */ 21 | multicastIds: readonly EmberMulticastId[]; 22 | }; 23 | 24 | /** 25 | * List of endpoints to register. 26 | * 27 | * Index 0 is used as default and expected to be the primary network. 28 | */ 29 | export const ROUTER_FIXED_ENDPOINTS: readonly FixedEndpointInfo[] = [ 30 | { 31 | // primary network 32 | endpoint: 1, 33 | profileId: ZSpec.HA_PROFILE_ID, 34 | deviceId: 0x08, // HA-rangeextender 35 | deviceVersion: 1, 36 | inClusterList: [Zcl.Clusters.genBasic.ID, Zcl.Clusters.touchlink.ID], 37 | outClusterList: [Zcl.Clusters.genOta.ID], 38 | networkIndex: 0x00, 39 | // - Cluster spec 3.7.2.4.1: group identifier 0x0000 is reserved for the global scene used by the OnOff cluster. 40 | // - 901: defaultBindGroup 41 | multicastIds: [0, 901], 42 | }, 43 | { 44 | // green power 45 | endpoint: ZSpec.GP_ENDPOINT, 46 | profileId: ZSpec.GP_PROFILE_ID, 47 | deviceId: 0x66, // GP-combo-basic 48 | deviceVersion: 1, 49 | inClusterList: [Zcl.Clusters.greenPower.ID], 50 | outClusterList: [Zcl.Clusters.greenPower.ID], 51 | networkIndex: 0x00, 52 | multicastIds: [0x0b84], 53 | }, 54 | ]; 55 | -------------------------------------------------------------------------------- /src/utils/spinel.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "node:events"; 2 | import { OTRCPDriver } from "zigbee-on-host"; 3 | import { DATA_FOLDER, logger } from "../index.js"; 4 | import { Transport, TransportEvent } from "./transport.js"; 5 | import type { PortConf } from "./types.js"; 6 | 7 | const NS = { namespace: "spinel" }; 8 | 9 | export enum MinimalSpinelEvent { 10 | FAILED = "failed", 11 | } 12 | 13 | interface MinimalSpinelEventMap { 14 | [MinimalSpinelEvent.FAILED]: []; 15 | } 16 | 17 | export class MinimalSpinel extends EventEmitter { 18 | public readonly driver: OTRCPDriver; 19 | private readonly transport: Transport; 20 | 21 | constructor(portConf: PortConf) { 22 | super(); 23 | 24 | this.driver = new OTRCPDriver( 25 | // @ts-expect-error none of these params are needed for this minimal use 26 | {}, 27 | {}, 28 | DATA_FOLDER, 29 | ); 30 | this.transport = new Transport(portConf); 31 | 32 | this.transport.on(TransportEvent.FAILED, this.onTransportFailed.bind(this)); 33 | this.transport.on(TransportEvent.DATA, (b) => { 34 | logger.debug(`Received transport data: ${b.toString("hex")}.`, NS); 35 | 36 | this.driver.parser._transform(b, "utf8", () => {}); 37 | }); 38 | this.driver.parser.on("data", this.driver.onFrame.bind(this.driver)); 39 | } 40 | 41 | public async start(): Promise { 42 | await this.transport.initPort(this.driver.writer); 43 | 44 | this.transport.write(Buffer.from([0x7e /* HDLC FLAG */])); 45 | } 46 | 47 | public async stop(): Promise { 48 | await this.transport.close(false); 49 | } 50 | 51 | private onTransportFailed(): void { 52 | this.emit(MinimalSpinelEvent.FAILED); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/transport.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "node:events"; 2 | import { Socket } from "node:net"; 3 | import { Readable } from "node:stream"; 4 | import { SerialPort } from "zigbee-herdsman/dist/adapter/serialPort.js"; 5 | import { logger } from "../index.js"; 6 | import { CONFIG_HIGHWATER_MARK, TCP_REGEX } from "./consts.js"; 7 | import type { PortConf } from "./types.js"; 8 | 9 | const NS = { namespace: "transport" }; 10 | 11 | type SetOptions = { 12 | brk?: boolean; 13 | cts?: boolean; 14 | dsr?: boolean; 15 | dtr?: boolean; 16 | rts?: boolean; 17 | }; 18 | 19 | class TransportWriter extends Readable { 20 | public writeBuffer(buffer: Buffer): void { 21 | this.emit("data", buffer); 22 | } 23 | 24 | public _read(): void {} 25 | } 26 | 27 | export enum TransportEvent { 28 | CLOSED = "closed", 29 | DATA = "data", 30 | FAILED = "failed", 31 | } 32 | 33 | interface SerialEventMap { 34 | [TransportEvent.CLOSED]: []; 35 | [TransportEvent.DATA]: [data: Buffer]; 36 | [TransportEvent.FAILED]: []; 37 | } 38 | 39 | /** 40 | * Serial or Socket based transport based on passed conf. 41 | */ 42 | export class Transport extends EventEmitter { 43 | public connected: boolean; 44 | public readonly portConf: PortConf; 45 | public portWriter: TransportWriter | undefined; 46 | private portSerial: SerialPort | undefined; 47 | private portSocket: Socket | undefined; 48 | 49 | constructor(portConf: PortConf) { 50 | super(); 51 | 52 | this.connected = false; 53 | this.portConf = portConf; 54 | } 55 | 56 | get isSerial(): boolean { 57 | return Boolean(this.portSerial); 58 | } 59 | 60 | public async close(emitClosed: boolean, emitFailed = true): Promise { 61 | if (this.portSerial?.isOpen) { 62 | logger.info("Closing serial connection...", NS); 63 | 64 | try { 65 | await this.portSerial.asyncFlushAndClose(); 66 | } catch (error) { 67 | logger.error(`Failed to close port: ${error}.`, NS); 68 | this.portSerial.removeAllListeners(); 69 | 70 | if (emitFailed) { 71 | this.emit(TransportEvent.FAILED); 72 | } 73 | 74 | return; 75 | } 76 | 77 | this.portSerial.removeAllListeners(); 78 | } else if (this.portSocket !== undefined && !this.portSocket.closed) { 79 | logger.info("Closing socket connection...", NS); 80 | this.portSocket.destroy(); 81 | this.portSocket.removeAllListeners(); 82 | } 83 | 84 | if (emitClosed) { 85 | this.emit(TransportEvent.CLOSED); 86 | } 87 | } 88 | 89 | public async initPort(customPortWriter?: TransportWriter): Promise { 90 | // will do nothing if nothing's open 91 | await this.close(false); 92 | 93 | if (TCP_REGEX.test(this.portConf.path)) { 94 | const info = new URL(this.portConf.path); 95 | logger.debug(`Opening TCP socket with ${info.hostname}:${info.port}`, NS); 96 | 97 | this.portSocket = new Socket(); 98 | 99 | this.portSocket.setNoDelay(true); 100 | this.portSocket.setKeepAlive(true, 15000); 101 | 102 | this.portWriter = customPortWriter ?? new TransportWriter({ highWaterMark: CONFIG_HIGHWATER_MARK }); 103 | 104 | this.portWriter.pipe(this.portSocket); 105 | this.portSocket.on("data", this.emitData.bind(this)); 106 | 107 | return await new Promise((resolve, reject): void => { 108 | const openError = async (err: Error): Promise => { 109 | reject(err); 110 | }; 111 | 112 | if (this.portSocket === undefined) { 113 | reject(new Error("Invalid socket")); 114 | return; 115 | } 116 | 117 | this.portSocket.on("connect", () => { 118 | logger.debug("Socket connected", NS); 119 | }); 120 | this.portSocket.on("ready", (): void => { 121 | logger.info("Socket ready", NS); 122 | this.portSocket!.removeListener("error", openError); 123 | this.portSocket!.once("close", this.onPortClose.bind(this)); 124 | this.portSocket!.on("error", this.onPortError.bind(this)); 125 | 126 | this.connected = true; 127 | 128 | resolve(); 129 | }); 130 | this.portSocket.once("error", openError); 131 | this.portSocket.connect(Number.parseInt(info.port, 10), info.hostname); 132 | }); 133 | } 134 | 135 | const serialOpts = { 136 | autoOpen: false, 137 | baudRate: this.portConf.baudRate, 138 | dataBits: 8 as const, 139 | parity: "none" as const, 140 | path: this.portConf.path, 141 | rtscts: this.portConf.rtscts, 142 | stopBits: 1 as const, 143 | xoff: this.portConf.xoff, 144 | xon: this.portConf.xon, 145 | }; 146 | 147 | logger.debug(`Opening serial port with ${JSON.stringify(serialOpts)}`, NS); 148 | 149 | this.portSerial = new SerialPort(serialOpts); 150 | this.portWriter = customPortWriter ?? new TransportWriter({ highWaterMark: CONFIG_HIGHWATER_MARK }); 151 | 152 | this.portWriter.pipe(this.portSerial); 153 | this.portSerial.on("data", this.emitData.bind(this)); 154 | 155 | await this.portSerial.asyncOpen(); 156 | logger.info("Serial port opened", NS); 157 | 158 | this.portSerial.once("close", this.onPortClose.bind(this)); 159 | this.portSerial.on("error", this.onPortError.bind(this)); 160 | 161 | this.connected = true; 162 | } 163 | 164 | public async serialSet(options: SetOptions, afterDelayMS?: number): Promise { 165 | await new Promise((resolve, reject) => { 166 | const fn = (): void => this.portSerial?.set(options, (error) => (error ? reject(error) : resolve())); 167 | 168 | if (afterDelayMS) { 169 | setTimeout(fn, afterDelayMS); 170 | } else { 171 | fn(); 172 | } 173 | }); 174 | } 175 | 176 | public write(buffer: Buffer): void { 177 | if (this.portWriter === undefined) { 178 | logger.error("No port available to write.", NS); 179 | this.emit(TransportEvent.FAILED); 180 | } else { 181 | logger.debug(`Sending transport data: ${buffer.toString("hex")}.`, NS); 182 | this.portWriter.writeBuffer(buffer); 183 | } 184 | } 185 | 186 | private emitData(data: Buffer): void { 187 | this.emit(TransportEvent.DATA, data); 188 | } 189 | 190 | private onPortClose(error: Error): void { 191 | logger.info("Transport closed.", NS); 192 | 193 | if (error && this.connected) { 194 | logger.info(`Transport close ${error}`, NS); 195 | this.emit(TransportEvent.FAILED); 196 | } else { 197 | this.emit(TransportEvent.CLOSED); 198 | } 199 | } 200 | 201 | private onPortError(error: Error): void { 202 | this.connected = false; 203 | 204 | logger.info(`Transport ${error}`, NS); 205 | this.emit(TransportEvent.FAILED); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type { checkbox, select } from "@inquirer/prompts"; 2 | import type { EmberKeyData, EmberVersion } from "zigbee-herdsman/dist/adapter/ember/types.js"; 3 | import type { Eui64 } from "zigbee-herdsman/dist/zspec/tstypes.js"; 4 | 5 | import type { BAUDRATES } from "./consts.js"; 6 | import type { CpcSystemCommandId } from "./enums.js"; 7 | 8 | // https://github.com/microsoft/TypeScript/issues/24509 9 | export type Mutable = { 10 | -readonly [P in keyof T]: T[P] extends ReadonlyArray ? Mutable[] : Mutable; 11 | }; 12 | 13 | // types from inquirer/prompts are not exported 14 | export type CheckboxChoices = Mutable>[0]["choices"]>; 15 | export type SelectChoices = Mutable>[0]["choices"]>; 16 | 17 | export type AdapterModel = 18 | | "Aeotec Zi-Stick (ZGA008)" 19 | | "EasyIOT ZB-GW04 v1.1" 20 | | "EasyIOT ZB-GW04 v1.2" 21 | | "Nabu Casa SkyConnect" 22 | | "Nabu Casa Yellow" 23 | | "Nabu Casa ZBT-2" 24 | | "SMLight SLZB06-M" 25 | | "SMLight SLZB06mg24" 26 | | "SMLight SLZB06mg26" 27 | | "SMLight SLZB07" 28 | | "SMLight SLZB07mg24" 29 | | "Sonoff ZBDongle-E" 30 | | "SparkFun MGM240p" 31 | | "TubeZB MGM24" 32 | | "TubeZB MGM24PB" 33 | | "ROUTER - Aeotec Zi-Stick (ZGA008)" 34 | | "ROUTER - EasyIOT ZB-GW04 v1.1" 35 | | "ROUTER - EasyIOT ZB-GW04 v1.2" 36 | | "ROUTER - Nabu Casa SkyConnect" 37 | | "ROUTER - Nabu Casa Yellow" 38 | | "ROUTER - Nabu Casa ZBT-2" 39 | | "ROUTER - SMLight SLZB06-M" 40 | | "ROUTER - SMLight SLZB06mg24" 41 | | "ROUTER - SMLight SLZB06mg26" 42 | | "ROUTER - SMLight SLZB07" 43 | | "ROUTER - SMLight SLZB07mg24" 44 | | "ROUTER - Sonoff ZBDongle-E" 45 | | "ROUTER - SparkFun MGM240p" 46 | | "ROUTER - TubeZB MGM24" 47 | | "ROUTER - TubeZB MGM24PB"; 48 | 49 | export type PortType = "serial" | "tcp"; 50 | export type BaudRate = (typeof BAUDRATES)[number]; 51 | 52 | export type PortConf = { 53 | baudRate: number; 54 | path: string; 55 | rtscts: boolean; 56 | xon: boolean; 57 | xoff: boolean; 58 | }; 59 | 60 | export type EmberFullVersion = { ezsp: number; revision: string } & EmberVersion; 61 | export type ConfigValue = { [key: string]: string }; 62 | 63 | export type FirmwareVariant = "official" | "darkxst" | "nerivec" | "nvm3_32768_clear" | "nvm3_40960_clear" | "app_clear"; 64 | export type FirmwareVersion = `${number}.${number}.${number}.${number}`; 65 | export type FirmwareVersionShort = `${number}.${number}.${number}`; 66 | export type FirmwareFilename = `${string}.gbl`; 67 | export type FirmwareURL = `https://${string}/${FirmwareFilename}`; 68 | 69 | export type FirmwareFileMetadata = { 70 | metadata_version: number; // 1 71 | sdk_version: FirmwareVersionShort; // '5.0.1' 72 | fw_type: "ncp-uart-hw" | "ncp-uart-sw" | "rcp-uart-802154" | "rcp-uart-802154-blehci"; 73 | baudrate: number; // 115200 74 | ezsp_version?: FirmwareVersion; // '8.0.1.0' 75 | ot_version?: FirmwareVersion; // '2.5.1.0' 76 | ble_version?: FirmwareVersionShort; // '8.1.0' 77 | cpc_version?: FirmwareVersion; // '5.0.1' 78 | }; 79 | 80 | export type FirmwareLinks = Record>>; 81 | 82 | export type TokensInfo = { 83 | nvm3Key: string; // keyof typeof NVM3ObjectKey 84 | size: number; 85 | arraySize: number; 86 | data: string[]; 87 | }[]; 88 | 89 | /** 90 | * Use for a link key backup. 91 | * 92 | * Each entry notes the EUI64 of the device it is paired to and the key data. 93 | * This key may be hashed and not the actual link key currently in use. 94 | */ 95 | export type LinkKeyBackupData = { 96 | deviceEui64: Eui64; 97 | key: EmberKeyData; 98 | outgoingFrameCounter: number; 99 | incomingFrameCounter: number; 100 | }; 101 | 102 | export type CpcSystemCommand = { 103 | /** Identifier of the command. uint8_t */ 104 | commandId: CpcSystemCommandId; 105 | /** Command sequence number. uint8_t */ 106 | seq: number; 107 | /** Length of the payload in bytes. uint16_t */ 108 | length: number; 109 | /** Command payload. uint8_t[PAYLOAD_LENGTH_MAX] */ 110 | payload: Buffer; 111 | }; 112 | 113 | export type GithubReleaseAssetJson = { 114 | url: string; 115 | id: number; 116 | node_id: string; 117 | name: string; 118 | label: null; 119 | uploader: Record; 120 | content_type: string; 121 | state: string; 122 | size: number; 123 | download_count: number; 124 | created_at: string; 125 | updated_at: string; 126 | browser_download_url: string; 127 | }; 128 | 129 | export type GithubReleaseJson = { 130 | url: string; 131 | assets_url: string; 132 | upload_url: string; 133 | html_url: string; 134 | id: number; 135 | author: Record; 136 | node_id: string; 137 | tag_name: string; 138 | target_commitish: string; 139 | name: string; 140 | draft: boolean; 141 | prerelease: boolean; 142 | created_at: string; 143 | published_at: string; 144 | assets: GithubReleaseAssetJson[]; 145 | tarball_url: string; 146 | zipball_url: string; 147 | body: string; 148 | reactions: Record; 149 | }; 150 | -------------------------------------------------------------------------------- /src/utils/update-firmware-links.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from "node:fs"; 2 | import path from "node:path"; 3 | import type { AdapterModel, FirmwareVariant, GithubReleaseJson } from "./types.js"; 4 | 5 | import { fetchJson } from "./utils.js"; 6 | 7 | const GITHUB_REPOS_API = "https://api.github.com/repos/"; 8 | const GITHUB_RELEASES_ENDPOINT = "/releases"; 9 | 10 | const NABUCASA_REPO = "NabuCasa/silabs-firmware-builder"; 11 | const DARKXST_REPO = "darkxst/silabs-firmware-builder"; 12 | const NERIVEC_REPO = "Nerivec/silabs-firmware-builder"; 13 | const NERIVEC_RECOVERY_REPO = "Nerivec/silabs-firmware-recovery"; 14 | // const TUBE0013_REPO = "tube0013/silabs-firmware-builder" 15 | 16 | // const FIRMWARE_BOOTLOADER = "bootloader" 17 | const FIRMWARE_ZIGBEE_NCP = "zigbee_ncp"; 18 | const FIRMWARE_ZIGBEE_ROUTER = "zigbee_router"; 19 | 20 | const NABUCASA_RELEASE = await getLatestGithubRelease(NABUCASA_REPO); 21 | const DARKXST_RELEASE = await getLatestGithubRelease(DARKXST_REPO); 22 | const NERIVEC_RELEASE = await getLatestGithubRelease(NERIVEC_REPO); 23 | const NERIVEC_RECOVERY_RELEASE = await getLatestGithubRelease(NERIVEC_RECOVERY_REPO); 24 | // const TUBE0013_REPO = await getLatestGithubRelease(TUBE0013_REPO) 25 | 26 | async function getLatestGithubRelease(repo: string): Promise { 27 | const response = await fetchJson(GITHUB_REPOS_API + path.posix.join(repo, GITHUB_RELEASES_ENDPOINT)); 28 | let i = 0; 29 | let release = response[i++]; 30 | 31 | while (release.prerelease || release.draft) { 32 | release = response[i++]; 33 | } 34 | 35 | return release; 36 | } 37 | 38 | function findFirmware(release: GithubReleaseJson, model: string, include: string | string[]): string | undefined { 39 | const includeArr = Array.isArray(include) ? include : [include]; 40 | const firmware = release.assets.find((asset) => asset.name.startsWith(model) && includeArr.every((i) => asset.name.includes(i))); 41 | 42 | return firmware?.browser_download_url; 43 | } 44 | 45 | const firmwareLinks: Record> = { 46 | official: { 47 | //-- FIRMWARE_ZIGBEE_NCP 48 | "Aeotec Zi-Stick (ZGA008)": undefined, 49 | 50 | "EasyIOT ZB-GW04 v1.1": undefined, 51 | "EasyIOT ZB-GW04 v1.2": undefined, 52 | 53 | "Nabu Casa SkyConnect": findFirmware(NABUCASA_RELEASE, "skyconnect", FIRMWARE_ZIGBEE_NCP), 54 | "Nabu Casa Yellow": findFirmware(NABUCASA_RELEASE, "yellow", FIRMWARE_ZIGBEE_NCP), 55 | "Nabu Casa ZBT-2": findFirmware(NABUCASA_RELEASE, "zbt2", FIRMWARE_ZIGBEE_NCP), 56 | 57 | "SMLight SLZB06-M": findFirmware(DARKXST_RELEASE, "slzb06m", FIRMWARE_ZIGBEE_NCP), 58 | "SMLight SLZB06mg24": findFirmware(DARKXST_RELEASE, "slzb06Mg24", FIRMWARE_ZIGBEE_NCP), 59 | "SMLight SLZB06mg26": findFirmware(DARKXST_RELEASE, "slzb06Mg26", FIRMWARE_ZIGBEE_NCP), 60 | // avoid matching on mg24 variant with `_` 61 | "SMLight SLZB07": findFirmware(DARKXST_RELEASE, "slzb07_", FIRMWARE_ZIGBEE_NCP), 62 | "SMLight SLZB07mg24": findFirmware(DARKXST_RELEASE, "slzb07Mg24", FIRMWARE_ZIGBEE_NCP), 63 | 64 | "Sonoff ZBDongle-E": 65 | "https://github.com/itead/Sonoff_Zigbee_Dongle_Firmware/raw/refs/heads/master/Dongle-E/NCP_7.4.4/ncp-uart-sw_EZNet7.4.4_V1.0.0.gbl", 66 | 67 | "SparkFun MGM240p": undefined, 68 | 69 | // avoid matching on PB variant with `-` 70 | "TubeZB MGM24": 71 | "https://github.com/tube0013/tube_gateways/raw/refs/heads/main/models/current/tubeszb-efr32-MGM24/firmware/mgm24/ncp/4.4.4/tubeszb-mgm24-hw-max_ncp-uart-hw_7.4.4.0.gbl", // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24-', FIRMWARE_ZIGBEE_NCP), 72 | "TubeZB MGM24PB": undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24pb-', FIRMWARE_ZIGBEE_NCP), 73 | 74 | //-- FIRMWARE_ZIGBEE_ROUTER 75 | "ROUTER - Aeotec Zi-Stick (ZGA008)": undefined, 76 | 77 | "ROUTER - EasyIOT ZB-GW04 v1.1": undefined, 78 | "ROUTER - EasyIOT ZB-GW04 v1.2": undefined, 79 | 80 | "ROUTER - Nabu Casa SkyConnect": undefined, // findFirmware(NABUCASA_RELEASE, 'skyconnect', FIRMWARE_ZIGBEE_ROUTER), 81 | "ROUTER - Nabu Casa Yellow": undefined, // findFirmware(NABUCASA_RELEASE, 'yellow', FIRMWARE_ZIGBEE_ROUTER), 82 | "ROUTER - Nabu Casa ZBT-2": undefined, // findFirmware(NABUCASA_RELEASE, 'zbt2', FIRMWARE_ZIGBEE_ROUTER), 83 | 84 | "ROUTER - SMLight SLZB06-M": findFirmware(DARKXST_RELEASE, "slzb06m", FIRMWARE_ZIGBEE_ROUTER), 85 | "ROUTER - SMLight SLZB06mg24": findFirmware(DARKXST_RELEASE, "slzb06Mg24", FIRMWARE_ZIGBEE_ROUTER), 86 | "ROUTER - SMLight SLZB06mg26": findFirmware(DARKXST_RELEASE, "slzb06Mg26", FIRMWARE_ZIGBEE_ROUTER), 87 | // avoid matching on mg24 variant with `_` 88 | "ROUTER - SMLight SLZB07": findFirmware(DARKXST_RELEASE, "slzb07_", FIRMWARE_ZIGBEE_ROUTER), 89 | "ROUTER - SMLight SLZB07mg24": findFirmware(DARKXST_RELEASE, "slzb07Mg24", FIRMWARE_ZIGBEE_ROUTER), 90 | 91 | "ROUTER - Sonoff ZBDongle-E": 92 | "https://github.com/itead/Sonoff_Zigbee_Dongle_Firmware/raw/refs/heads/master/Dongle-E/Router/Z3RouterUSBDonlge_EZNet6.10.3_V1.0.0.gbl", 93 | 94 | "ROUTER - SparkFun MGM240p": undefined, 95 | 96 | // avoid matching on variants with `-` 97 | "ROUTER - TubeZB MGM24": undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24-', FIRMWARE_ZIGBEE_ROUTER), 98 | "ROUTER - TubeZB MGM24PB": undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24PB-', FIRMWARE_ZIGBEE_ROUTER), 99 | }, 100 | darkxst: { 101 | //-- FIRMWARE_ZIGBEE_NCP 102 | "Aeotec Zi-Stick (ZGA008)": findFirmware(DARKXST_RELEASE, "zga008", FIRMWARE_ZIGBEE_NCP), 103 | 104 | "EasyIOT ZB-GW04 v1.1": findFirmware(DARKXST_RELEASE, "zb-gw04-1v1", FIRMWARE_ZIGBEE_NCP), 105 | "EasyIOT ZB-GW04 v1.2": findFirmware(DARKXST_RELEASE, "zb-gw04-1v2", FIRMWARE_ZIGBEE_NCP), 106 | 107 | "Nabu Casa SkyConnect": undefined, 108 | "Nabu Casa Yellow": undefined, 109 | "Nabu Casa ZBT-2": undefined, 110 | 111 | "SMLight SLZB06-M": findFirmware(DARKXST_RELEASE, "slzb06m", FIRMWARE_ZIGBEE_NCP), 112 | "SMLight SLZB06mg24": findFirmware(DARKXST_RELEASE, "slzb06Mg24", FIRMWARE_ZIGBEE_NCP), 113 | "SMLight SLZB06mg26": findFirmware(DARKXST_RELEASE, "slzb06Mg26", FIRMWARE_ZIGBEE_NCP), 114 | // avoid matching on mg24 variant with `_` 115 | "SMLight SLZB07": findFirmware(DARKXST_RELEASE, "slzb07_", FIRMWARE_ZIGBEE_NCP), 116 | "SMLight SLZB07mg24": findFirmware(DARKXST_RELEASE, "slzb07Mg24", FIRMWARE_ZIGBEE_NCP), 117 | 118 | "Sonoff ZBDongle-E": findFirmware(DARKXST_RELEASE, "zbdonglee", FIRMWARE_ZIGBEE_NCP), 119 | 120 | "SparkFun MGM240p": findFirmware(DARKXST_RELEASE, "mgm240p", FIRMWARE_ZIGBEE_NCP), 121 | 122 | // avoid matching on PB variant with `-` 123 | "TubeZB MGM24": undefined, 124 | "TubeZB MGM24PB": undefined, 125 | 126 | //-- FIRMWARE_ZIGBEE_ROUTER 127 | "ROUTER - Aeotec Zi-Stick (ZGA008)": undefined, 128 | 129 | "ROUTER - EasyIOT ZB-GW04 v1.1": undefined, 130 | "ROUTER - EasyIOT ZB-GW04 v1.2": undefined, 131 | 132 | "ROUTER - Nabu Casa SkyConnect": undefined, 133 | "ROUTER - Nabu Casa Yellow": undefined, 134 | "ROUTER - Nabu Casa ZBT-2": undefined, 135 | 136 | "ROUTER - SMLight SLZB06-M": findFirmware(DARKXST_RELEASE, "slzb06m", FIRMWARE_ZIGBEE_ROUTER), 137 | "ROUTER - SMLight SLZB06mg24": findFirmware(DARKXST_RELEASE, "slzb06Mg24", FIRMWARE_ZIGBEE_ROUTER), 138 | "ROUTER - SMLight SLZB06mg26": findFirmware(DARKXST_RELEASE, "slzb06Mg26", FIRMWARE_ZIGBEE_ROUTER), 139 | // avoid matching on mg24 variant with `_` 140 | "ROUTER - SMLight SLZB07": findFirmware(DARKXST_RELEASE, "slzb07_", FIRMWARE_ZIGBEE_ROUTER), 141 | "ROUTER - SMLight SLZB07mg24": findFirmware(DARKXST_RELEASE, "slzb07Mg24", FIRMWARE_ZIGBEE_ROUTER), 142 | 143 | "ROUTER - Sonoff ZBDongle-E": undefined, 144 | 145 | "ROUTER - SparkFun MGM240p": undefined, 146 | 147 | // avoid matching on variants with `-` 148 | "ROUTER - TubeZB MGM24": undefined, 149 | "ROUTER - TubeZB MGM24PB": undefined, 150 | }, 151 | nerivec: { 152 | //-- FIRMWARE_ZIGBEE_NCP 153 | "Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RELEASE, "aeotec_zga008", FIRMWARE_ZIGBEE_NCP), 154 | 155 | "EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RELEASE, "easyiot_zb-gw04-1v1", FIRMWARE_ZIGBEE_NCP), 156 | "EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RELEASE, "easyiot_zb-gw04-1v2", FIRMWARE_ZIGBEE_NCP), 157 | 158 | "Nabu Casa SkyConnect": findFirmware(NERIVEC_RELEASE, "nabucasa_skyconnect", FIRMWARE_ZIGBEE_NCP), 159 | "Nabu Casa Yellow": findFirmware(NERIVEC_RELEASE, "nabucasa_yellow", FIRMWARE_ZIGBEE_NCP), 160 | "Nabu Casa ZBT-2": findFirmware(NERIVEC_RELEASE, "nabucasa_zbt-2", FIRMWARE_ZIGBEE_NCP), 161 | 162 | "SMLight SLZB06-M": findFirmware(NERIVEC_RELEASE, "smlight_slzb06m", FIRMWARE_ZIGBEE_NCP), 163 | "SMLight SLZB06mg24": findFirmware(NERIVEC_RELEASE, "smlight_slzb06Mg24", FIRMWARE_ZIGBEE_NCP), 164 | "SMLight SLZB06mg26": findFirmware(NERIVEC_RELEASE, "smlight_slzb06Mg26", FIRMWARE_ZIGBEE_NCP), 165 | // avoid matching on mg24 variant with `_` 166 | "SMLight SLZB07": findFirmware(NERIVEC_RELEASE, "smlight_slzb07_", FIRMWARE_ZIGBEE_NCP), 167 | "SMLight SLZB07mg24": findFirmware(NERIVEC_RELEASE, "smlight_slzb07Mg24", FIRMWARE_ZIGBEE_NCP), 168 | 169 | "Sonoff ZBDongle-E": findFirmware(NERIVEC_RELEASE, "sonoff_zbdonglee", FIRMWARE_ZIGBEE_NCP), 170 | 171 | "SparkFun MGM240p": findFirmware(NERIVEC_RELEASE, "sparkfun_mgm240p", FIRMWARE_ZIGBEE_NCP), 172 | // avoid matching on variants with `-` 173 | "TubeZB MGM24": findFirmware(NERIVEC_RELEASE, "tubeszb-mgm24-", FIRMWARE_ZIGBEE_NCP), 174 | "TubeZB MGM24PB": findFirmware(NERIVEC_RELEASE, "tubeszb-mgm24PB-", FIRMWARE_ZIGBEE_NCP), 175 | 176 | //-- FIRMWARE_ZIGBEE_ROUTER 177 | "ROUTER - Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RELEASE, "aeotec_zga008", FIRMWARE_ZIGBEE_ROUTER), 178 | 179 | "ROUTER - EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RELEASE, "easyiot_zb-gw04-1v1", FIRMWARE_ZIGBEE_ROUTER), 180 | "ROUTER - EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RELEASE, "easyiot_zb-gw04-1v2", FIRMWARE_ZIGBEE_ROUTER), 181 | 182 | "ROUTER - Nabu Casa SkyConnect": findFirmware(NERIVEC_RELEASE, "nabucasa_skyconnect", FIRMWARE_ZIGBEE_ROUTER), 183 | "ROUTER - Nabu Casa Yellow": findFirmware(NERIVEC_RELEASE, "nabucasa_yellow", FIRMWARE_ZIGBEE_ROUTER), 184 | "ROUTER - Nabu Casa ZBT-2": findFirmware(NERIVEC_RELEASE, "nabucasa_zbt-2", FIRMWARE_ZIGBEE_ROUTER), 185 | 186 | "ROUTER - SMLight SLZB06-M": findFirmware(NERIVEC_RELEASE, "smlight_slzb06m", FIRMWARE_ZIGBEE_ROUTER), 187 | "ROUTER - SMLight SLZB06mg24": findFirmware(NERIVEC_RELEASE, "smlight_slzb06Mg24", FIRMWARE_ZIGBEE_ROUTER), 188 | "ROUTER - SMLight SLZB06mg26": findFirmware(NERIVEC_RELEASE, "smlight_slzb06Mg26", FIRMWARE_ZIGBEE_ROUTER), 189 | // avoid matching on mg24 variant with `_` 190 | "ROUTER - SMLight SLZB07": findFirmware(NERIVEC_RELEASE, "smlight_slzb07_", FIRMWARE_ZIGBEE_ROUTER), 191 | "ROUTER - SMLight SLZB07mg24": findFirmware(NERIVEC_RELEASE, "smlight_slzb07Mg24", FIRMWARE_ZIGBEE_ROUTER), 192 | 193 | "ROUTER - Sonoff ZBDongle-E": findFirmware(NERIVEC_RELEASE, "sonoff_zbdonglee", FIRMWARE_ZIGBEE_ROUTER), 194 | 195 | "ROUTER - SparkFun MGM240p": findFirmware(NERIVEC_RELEASE, "sparkfun_mgm240p", FIRMWARE_ZIGBEE_ROUTER), 196 | 197 | // avoid matching on variants with `-` 198 | "ROUTER - TubeZB MGM24": findFirmware(NERIVEC_RELEASE, "tubeszb-mgm24-", FIRMWARE_ZIGBEE_ROUTER), 199 | "ROUTER - TubeZB MGM24PB": findFirmware(NERIVEC_RELEASE, "tubeszb-mgm24PB-", FIRMWARE_ZIGBEE_ROUTER), 200 | }, 201 | nvm3_32768_clear: { 202 | "Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", ["nvm3_clear", "32768.gbl"]), 203 | 204 | "EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), 205 | "EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), 206 | 207 | "Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", ["nvm3_clear", "32768.gbl"]), 208 | "Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", ["nvm3_clear", "32768.gbl"]), 209 | "Nabu Casa ZBT-2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A420F1536IM40", ["nvm3_clear", "32768.gbl"]), 210 | 211 | "SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), 212 | "SMLight SLZB06mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "32768.gbl"]), 213 | "SMLight SLZB06mg26": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG26B420F3200IM48", ["nvm3_clear", "32768.gbl"]), 214 | "SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), 215 | "SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "32768.gbl"]), 216 | 217 | "Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), 218 | 219 | "SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", ["nvm3_clear", "32768.gbl"]), 220 | 221 | "TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", ["nvm3_clear", "32768.gbl"]), 222 | "TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", ["nvm3_clear", "32768.gbl"]), 223 | 224 | "ROUTER - Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", ["nvm3_clear", "32768.gbl"]), 225 | 226 | "ROUTER - EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), 227 | "ROUTER - EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), 228 | 229 | "ROUTER - Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", ["nvm3_clear", "32768.gbl"]), 230 | "ROUTER - Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", ["nvm3_clear", "32768.gbl"]), 231 | "ROUTER - Nabu Casa ZBT-2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A420F1536IM40", ["nvm3_clear", "32768.gbl"]), 232 | 233 | "ROUTER - SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), 234 | "ROUTER - SMLight SLZB06mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "32768.gbl"]), 235 | "ROUTER - SMLight SLZB06mg26": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG26B420F3200IM48", ["nvm3_clear", "32768.gbl"]), 236 | "ROUTER - SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), 237 | "ROUTER - SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "32768.gbl"]), 238 | 239 | "ROUTER - Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), 240 | 241 | "ROUTER - SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", ["nvm3_clear", "32768.gbl"]), 242 | 243 | "ROUTER - TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", ["nvm3_clear", "32768.gbl"]), 244 | "ROUTER - TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", ["nvm3_clear", "32768.gbl"]), 245 | }, 246 | nvm3_40960_clear: { 247 | "Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", ["nvm3_clear", "40960.gbl"]), 248 | 249 | "EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), 250 | "EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), 251 | 252 | "Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", ["nvm3_clear", "40960.gbl"]), 253 | "Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", ["nvm3_clear", "40960.gbl"]), 254 | "Nabu Casa ZBT-2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A420F1536IM40", ["nvm3_clear", "40960.gbl"]), 255 | 256 | "SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), 257 | "SMLight SLZB06mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "40960.gbl"]), 258 | "SMLight SLZB06mg26": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG26B420F3200IM48", ["nvm3_clear", "40960.gbl"]), 259 | "SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), 260 | "SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "40960.gbl"]), 261 | 262 | "Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), 263 | 264 | "SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", ["nvm3_clear", "40960.gbl"]), 265 | 266 | "TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", ["nvm3_clear", "40960.gbl"]), 267 | "TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", ["nvm3_clear", "40960.gbl"]), 268 | 269 | "ROUTER - Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", ["nvm3_clear", "40960.gbl"]), 270 | 271 | "ROUTER - EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), 272 | "ROUTER - EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), 273 | 274 | "ROUTER - Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", ["nvm3_clear", "40960.gbl"]), 275 | "ROUTER - Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", ["nvm3_clear", "40960.gbl"]), 276 | "ROUTER - Nabu Casa ZBT-2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A420F1536IM40", ["nvm3_clear", "40960.gbl"]), 277 | 278 | "ROUTER - SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), 279 | "ROUTER - SMLight SLZB06mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "40960.gbl"]), 280 | "ROUTER - SMLight SLZB06mg26": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG26B420F3200IM48", ["nvm3_clear", "40960.gbl"]), 281 | "ROUTER - SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), 282 | "ROUTER - SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "40960.gbl"]), 283 | 284 | "ROUTER - Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), 285 | 286 | "ROUTER - SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", ["nvm3_clear", "40960.gbl"]), 287 | 288 | "ROUTER - TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", ["nvm3_clear", "40960.gbl"]), 289 | "ROUTER - TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", ["nvm3_clear", "40960.gbl"]), 290 | }, 291 | app_clear: { 292 | "Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", "app_clear"), 293 | 294 | "EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), 295 | "EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), 296 | 297 | "Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", "app_clear"), 298 | "Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", "app_clear"), 299 | "Nabu Casa ZBT-2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A420F1536IM40", "app_clear"), 300 | 301 | "SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), 302 | "SMLight SLZB06mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", "app_clear"), 303 | "SMLight SLZB06mg26": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG26B420F3200IM48", "app_clear"), 304 | "SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), 305 | "SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", "app_clear"), 306 | 307 | "Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), 308 | 309 | "SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", "app_clear"), 310 | 311 | "TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", "app_clear"), 312 | "TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", "app_clear"), 313 | 314 | "ROUTER - Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", "app_clear"), 315 | 316 | "ROUTER - EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), 317 | "ROUTER - EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), 318 | 319 | "ROUTER - Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", "app_clear"), 320 | "ROUTER - Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", "app_clear"), 321 | "ROUTER - Nabu Casa ZBT-2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A420F1536IM40", "app_clear"), 322 | 323 | "ROUTER - SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), 324 | "ROUTER - SMLight SLZB06mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", "app_clear"), 325 | "ROUTER - SMLight SLZB06mg26": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG26B420F3200IM48", "app_clear"), 326 | "ROUTER - SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), 327 | "ROUTER - SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", "app_clear"), 328 | 329 | "ROUTER - Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), 330 | 331 | "ROUTER - SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", "app_clear"), 332 | 333 | "ROUTER - TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", "app_clear"), 334 | "ROUTER - TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", "app_clear"), 335 | }, 336 | }; 337 | 338 | writeFileSync("firmware-links-v3.json", JSON.stringify(firmwareLinks, undefined, 4), "utf8"); 339 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readdirSync, readFileSync, renameSync, statSync } from "node:fs"; 2 | import { dirname, extname, join } from "node:path"; 3 | import { input, select } from "@inquirer/prompts"; 4 | import { DEFAULT_STACK_CONFIG } from "zigbee-herdsman/dist/adapter/ember/adapter/emberAdapter.js"; 5 | import { IEEE802154CcaMode } from "zigbee-herdsman/dist/adapter/ember/enums.js"; 6 | import { halCommonCrc16, highByte, lowByte } from "zigbee-herdsman/dist/adapter/ember/utils/math.js"; 7 | import type { Backup } from "zigbee-herdsman/dist/models/backup.js"; 8 | import type { UnifiedBackupStorage } from "zigbee-herdsman/dist/models/backup-storage-unified.js"; 9 | import { fromUnifiedBackup } from "zigbee-herdsman/dist/utils/backup.js"; 10 | import { CONF_STACK, DATA_FOLDER, logger } from "../index.js"; 11 | import type { SelectChoices } from "./types.js"; 12 | 13 | // @from zigbee2mqtt-frontend 14 | export const toHex = (input: number, padding = 4): string => { 15 | const padStr = "0".repeat(padding); 16 | return `0x${(padStr + input.toString(16)).slice(-1 * padding).toUpperCase()}`; 17 | }; 18 | 19 | export const loadStackConfig = (): typeof DEFAULT_STACK_CONFIG => { 20 | try { 21 | const customConfig = JSON.parse(readFileSync(CONF_STACK, "utf8")); 22 | // set any undefined config to default 23 | const config = { ...DEFAULT_STACK_CONFIG, ...customConfig }; 24 | 25 | const inRange = (value: number, min: number, max: number): boolean => !(value == null || value < min || value > max); 26 | 27 | if (!["high", "low"].includes(config.CONCENTRATOR_RAM_TYPE)) { 28 | config.CONCENTRATOR_RAM_TYPE = DEFAULT_STACK_CONFIG.CONCENTRATOR_RAM_TYPE; 29 | logger.error("[CONF STACK] Invalid CONCENTRATOR_RAM_TYPE, using default."); 30 | } 31 | 32 | if (!inRange(config.CONCENTRATOR_MIN_TIME, 1, 60) || config.CONCENTRATOR_MIN_TIME >= config.CONCENTRATOR_MAX_TIME) { 33 | config.CONCENTRATOR_MIN_TIME = DEFAULT_STACK_CONFIG.CONCENTRATOR_MIN_TIME; 34 | logger.error("[CONF STACK] Invalid CONCENTRATOR_MIN_TIME, using default."); 35 | } 36 | 37 | if (!inRange(config.CONCENTRATOR_MAX_TIME, 30, 300) || config.CONCENTRATOR_MAX_TIME <= config.CONCENTRATOR_MIN_TIME) { 38 | config.CONCENTRATOR_MAX_TIME = DEFAULT_STACK_CONFIG.CONCENTRATOR_MAX_TIME; 39 | logger.error("[CONF STACK] Invalid CONCENTRATOR_MAX_TIME, using default."); 40 | } 41 | 42 | if (!inRange(config.CONCENTRATOR_ROUTE_ERROR_THRESHOLD, 1, 100)) { 43 | config.CONCENTRATOR_ROUTE_ERROR_THRESHOLD = DEFAULT_STACK_CONFIG.CONCENTRATOR_ROUTE_ERROR_THRESHOLD; 44 | logger.error("[CONF STACK] Invalid CONCENTRATOR_ROUTE_ERROR_THRESHOLD, using default."); 45 | } 46 | 47 | if (!inRange(config.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, 1, 100)) { 48 | config.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD = DEFAULT_STACK_CONFIG.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD; 49 | logger.error("[CONF STACK] Invalid CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, using default."); 50 | } 51 | 52 | if (!inRange(config.CONCENTRATOR_MAX_HOPS, 0, 30)) { 53 | config.CONCENTRATOR_MAX_HOPS = DEFAULT_STACK_CONFIG.CONCENTRATOR_MAX_HOPS; 54 | logger.error("[CONF STACK] Invalid CONCENTRATOR_MAX_HOPS, using default."); 55 | } 56 | 57 | if (!inRange(config.MAX_END_DEVICE_CHILDREN, 6, 64)) { 58 | config.MAX_END_DEVICE_CHILDREN = DEFAULT_STACK_CONFIG.MAX_END_DEVICE_CHILDREN; 59 | logger.error("[CONF STACK] Invalid MAX_END_DEVICE_CHILDREN, using default."); 60 | } 61 | 62 | if (!inRange(config.TRANSIENT_DEVICE_TIMEOUT, 0, 65535)) { 63 | config.TRANSIENT_DEVICE_TIMEOUT = DEFAULT_STACK_CONFIG.TRANSIENT_DEVICE_TIMEOUT; 64 | logger.error("[CONF STACK] Invalid TRANSIENT_DEVICE_TIMEOUT, using default."); 65 | } 66 | 67 | if (!inRange(config.END_DEVICE_POLL_TIMEOUT, 0, 14)) { 68 | config.END_DEVICE_POLL_TIMEOUT = DEFAULT_STACK_CONFIG.END_DEVICE_POLL_TIMEOUT; 69 | logger.error("[CONF STACK] Invalid END_DEVICE_POLL_TIMEOUT, using default."); 70 | } 71 | 72 | if (!inRange(config.TRANSIENT_KEY_TIMEOUT_S, 0, 65535)) { 73 | config.TRANSIENT_KEY_TIMEOUT_S = DEFAULT_STACK_CONFIG.TRANSIENT_KEY_TIMEOUT_S; 74 | logger.error("[CONF STACK] Invalid TRANSIENT_KEY_TIMEOUT_S, using default."); 75 | } 76 | 77 | config.CCA_MODE = config.CCA_MODE ?? undefined; // always default to undefined 78 | 79 | if (config.CCA_MODE && IEEE802154CcaMode[config.CCA_MODE] === undefined) { 80 | config.CCA_MODE = undefined; 81 | logger.error("[STACK CONFIG] Invalid CCA_MODE, ignoring."); 82 | } 83 | 84 | logger.info(`Using stack config ${JSON.stringify(config)}.`); 85 | 86 | return config; 87 | } catch { 88 | /* empty */ 89 | } 90 | 91 | logger.info("Using default stack config."); 92 | 93 | return DEFAULT_STACK_CONFIG; 94 | }; 95 | 96 | export const browseToFile = async (message: string, defaultValue: string, toWrite = false): Promise => { 97 | const pathOpt = await select({ 98 | choices: [ 99 | { name: `Use default (${defaultValue})`, value: 0 }, 100 | { name: "Enter path manually", value: 1 }, 101 | { name: `Select in data folder (${DATA_FOLDER})`, value: 2 }, 102 | ], 103 | message, 104 | }); 105 | let filepath: string = defaultValue; 106 | 107 | switch (pathOpt) { 108 | case 1: { 109 | filepath = await input({ 110 | message: "Enter path to file", 111 | validate(value) { 112 | return existsSync(dirname(value)) && extname(value) === extname(defaultValue); 113 | }, 114 | }); 115 | 116 | break; 117 | } 118 | 119 | case 2: { 120 | const files = readdirSync(DATA_FOLDER); 121 | const fileChoices: SelectChoices = [{ name: "Go back", value: "-1" }]; 122 | 123 | for (const file of files) { 124 | if (extname(file) === extname(defaultValue)) { 125 | const { size, mtime, birthtime } = statSync(join(DATA_FOLDER, file)); 126 | 127 | fileChoices.push({ 128 | name: file, 129 | value: file, 130 | description: `Size: ${size} bytes | Created: ${birthtime.toISOString()} | Last Modified: ${mtime.toISOString()}`, 131 | }); 132 | } 133 | } 134 | 135 | let chosenFile = "-1"; 136 | 137 | if (fileChoices.length === 1) { 138 | logger.error(`Found no file in '${DATA_FOLDER}'.`); 139 | } else { 140 | chosenFile = await select({ choices: fileChoices, message }); 141 | } 142 | 143 | filepath = chosenFile === "-1" ? await browseToFile(message, defaultValue, toWrite) : join(DATA_FOLDER, chosenFile); 144 | 145 | break; 146 | } 147 | } 148 | 149 | if (toWrite && existsSync(filepath)) { 150 | const rename = await select({ 151 | choices: [ 152 | { name: "Overwrite", value: 0 }, 153 | { name: "Rename", value: 1 }, 154 | ], 155 | message: "File already exists", 156 | }); 157 | 158 | if (rename === 1) { 159 | const renamed = `${filepath}-${Date.now()}.old`; 160 | 161 | logger.info(`Renaming existing file to '${renamed}'.`); 162 | renameSync(filepath, renamed); 163 | } 164 | } 165 | 166 | return filepath; 167 | }; 168 | 169 | export const getBackupFromFile = (backupFile: string): Backup | undefined => { 170 | try { 171 | const data: UnifiedBackupStorage = JSON.parse(readFileSync(backupFile, "utf8")); 172 | 173 | if (data.metadata.format === "zigpy/open-coordinator-backup") { 174 | if (data.metadata.version !== 1) { 175 | logger.error(`Unsupported open coordinator backup version (version=${data.metadata.version}).`); 176 | return undefined; 177 | } 178 | 179 | return fromUnifiedBackup(data); 180 | } 181 | 182 | logger.error("Unknown backup format."); 183 | } catch (error) { 184 | logger.error(`Not valid backup found. ${error}`); 185 | } 186 | 187 | return undefined; 188 | }; 189 | 190 | export const computeCRC16 = (data: Buffer, init = 0): Buffer => { 191 | let crc = init; 192 | 193 | for (const byte of data) { 194 | crc = halCommonCrc16(byte, crc); 195 | } 196 | 197 | return Buffer.from([highByte(crc), lowByte(crc)]); 198 | }; 199 | 200 | export const computeCRC16CITTKermit = (data: Buffer, init = 0): Buffer => { 201 | let crc = init; 202 | 203 | for (const byte of data) { 204 | let t = crc ^ byte; 205 | t = (t ^ (t << 4)) & 0xff; 206 | crc = (crc >> 8) ^ (t << 8) ^ (t >> 4) ^ (t << 3); 207 | } 208 | 209 | return Buffer.from([lowByte(crc), highByte(crc)]); 210 | }; 211 | 212 | export async function fetchJson(pageUrl: string): Promise { 213 | const response = await fetch(pageUrl); 214 | 215 | if (!response.ok || !response.body) { 216 | throw new Error(`Invalid response from ${pageUrl} status=${response.status}.`); 217 | } 218 | 219 | return (await response.json()) as T; 220 | } 221 | -------------------------------------------------------------------------------- /src/utils/wireshark.ts: -------------------------------------------------------------------------------- 1 | import { EZSP_MAX_FRAME_LENGTH } from "zigbee-herdsman/dist/adapter/ember/ezsp/consts.js"; 2 | 3 | /** 4 | * @see https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-zep.c 5 | * @see https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-ieee802154.c 6 | * @see https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-zbee-nwk.c 7 | *------------------------------------------------------------ 8 | * 9 | * ZEP Packets must be received in the following format: 10 | * |UDP Header| ZEP Header |IEEE 802.15.4 Packet| 11 | * | 8 bytes | 16/32 bytes | <= 127 bytes | 12 | *------------------------------------------------------------ 13 | * 14 | * ZEP v1 Header will have the following format: 15 | * |Preamble|Version|Channel ID|Device ID|CRC/LQI Mode|LQI Val|Reserved|Length| 16 | * |2 bytes |1 byte | 1 byte | 2 bytes | 1 byte |1 byte |7 bytes |1 byte| 17 | * 18 | * ZEP v2 Header will have the following format (if type=1/Data): 19 | * |Preamble|Version| Type |Channel ID|Device ID|CRC/LQI Mode|LQI Val|NTP Timestamp|Sequence#|Reserved|Length| 20 | * |2 bytes |1 byte |1 byte| 1 byte | 2 bytes | 1 byte |1 byte | 8 bytes | 4 bytes |10 bytes|1 byte| 21 | * 22 | * ZEP v2 Header will have the following format (if type=2/Ack): 23 | * |Preamble|Version| Type |Sequence#| 24 | * |2 bytes |1 byte |1 byte| 4 bytes | 25 | *------------------------------------------------------------ 26 | */ 27 | const ZEP_PREAMBLE = "EX"; 28 | const ZEP_PROTOCOL_VERSION = 2; 29 | const ZEP_PROTOCOL_TYPE = 1; 30 | /** Baseline NTP time if bit-0=0 -> 7-Feb-2036 @ 06:28:16 UTC */ 31 | const NTP_MSB_0_BASE_TIME = 2085978496000n; 32 | /** Baseline NTP time if bit-0=1 -> 1-Jan-1900 @ 01:00:00 UTC */ 33 | const NTP_MSB_1_BASE_TIME = -2208988800000n; 34 | 35 | const getZepTimestamp = (): bigint => { 36 | const now = BigInt(Date.now()); 37 | const useBase1 = now < NTP_MSB_0_BASE_TIME; // time < Feb-2036 38 | // MSB_1_BASE_TIME: dates <= Feb-2036, MSB_0_BASE_TIME: if base0 needed for dates >= Feb-2036 39 | const baseTime = now - (useBase1 ? NTP_MSB_1_BASE_TIME : NTP_MSB_0_BASE_TIME); 40 | let seconds = baseTime / 1000n; 41 | const fraction = ((baseTime % 1000n) * 0x100000000n) / 1000n; 42 | 43 | if (useBase1) { 44 | seconds |= 0x80000000n; // set high-order bit if MSB_1_BASE_TIME 1900 used 45 | } 46 | 47 | return BigInt.asIntN(64, (seconds << 32n) | fraction); 48 | }; 49 | 50 | export const createWiresharkZEPFrame = ( 51 | channelId: number, 52 | deviceId: number, 53 | lqi: number, 54 | rssi: number, 55 | sequence: number, 56 | data: Buffer, 57 | lqiMode = false, 58 | ): Buffer => { 59 | const buffer = Buffer.alloc(167); 60 | let offset = 0; 61 | 62 | // The IEEE 802.15.4 packet encapsulated in the ZEP frame must have the "TI CC24xx" format 63 | // See figure 21 on page 24 of the CC2420 datasheet: https://www.ti.com/lit/ds/symlink/cc2420.pdf 64 | // So, two bytes must be added at the end: 65 | // * First byte: RSSI value as a signed 8 bits integer (range -128 to 127) 66 | // * Second byte: 67 | // - the most significant bit is set to 1 if the CRC of the frame is correct 68 | // - the 7 least significant bits contain the LQI value as a unsigned 7 bits integer (range 0 to 127) 69 | data[data.length - 2] = rssi; 70 | data[data.length - 1] = 0x80 | ((lqi >> 1) & 0x7f); 71 | 72 | // Protocol ID String | Character string | 2.0.3 to 4.2.5 73 | buffer.write(ZEP_PREAMBLE, offset); 74 | offset += 2; 75 | 76 | // Protocol Version | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 77 | buffer.writeUInt8(ZEP_PROTOCOL_VERSION, offset++); 78 | // Type | Unsigned integer (8 bits) | 1.2.0 to 1.8.15, 1.12.0 to 4.2.5 79 | buffer.writeUInt8(ZEP_PROTOCOL_TYPE, offset++); 80 | // Channel ID | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 81 | buffer.writeUInt8(channelId, offset++); 82 | // Device ID | Unsigned integer (16 bits) | 1.2.0 to 4.2.5 83 | buffer.writeUint16BE(deviceId, offset); 84 | offset += 2; 85 | 86 | // LQI/CRC Mode | Boolean | 1.2.0 to 4.2.5 87 | buffer.writeUInt8(lqiMode ? 1 : 0, offset++); 88 | // Link Quality Indication | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 89 | buffer.writeUInt8(lqi, offset++); 90 | 91 | // Timestamp | Date and time | 1.2.0 to 4.2.5 92 | buffer.writeBigInt64BE(getZepTimestamp(), offset); 93 | offset += 8; 94 | 95 | // Sequence Number | Unsigned integer (32 bits) | 1.2.0 to 4.2.5 96 | buffer.writeUint32BE(sequence, offset); 97 | offset += 4; 98 | 99 | // Reserved Fields | Byte sequence | 2.0.0 to 4.2.5 100 | offset += 10; 101 | 102 | // Length | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 103 | buffer.writeUInt8(data.length, offset++); 104 | 105 | buffer.set(data, offset); 106 | offset += data.length; 107 | 108 | return buffer.subarray(0, offset); // increased to "beyond last" above 109 | }; 110 | 111 | /** 112 | * @see https://datatracker.ietf.org/doc/id/draft-gharris-opsawg-pcap-00.html 113 | */ 114 | 115 | /** seconds + microseconds */ 116 | export const PCAP_MAGIC_NUMBER_MS = 0xa1b2c3d4; 117 | /** seconds + nanoseconds */ 118 | export const PCAP_MAGIC_NUMBER_NS = 0xa1b23c4d; 119 | const PCAP_VERSION_MAJOR = 2; 120 | const PCAP_VERSION_MINOR = 4; 121 | /** IEEE 802.15.4 Low-Rate Wireless Networks, with each packet having the FCS at the end of the frame. */ 122 | const PCAP_LINKTYPE_IEEE802_15_4_WITH_FCS = 195; 123 | 124 | export const createPcapFileHeader = (magicNumber: number = PCAP_MAGIC_NUMBER_MS): Buffer => { 125 | const fileHeader = Buffer.alloc(24); 126 | 127 | /** 128 | * An unsigned magic number, whose value is either the hexadecimal number 0xA1B2C3D4 or the hexadecimal number 0xA1B23C4D. 129 | * If the value is 0xA1B2C3D4, time stamps in Packet Records (see Figure 2) are in seconds and microseconds; 130 | * if it is 0xA1B23C4D, time stamps in Packet Records are in seconds and nanoseconds. 131 | * These numbers can be used to distinguish sections that have been saved on little-endian machines from the ones saved on big-endian machines, 132 | * and to heuristically identify pcap files. 133 | * 32 bits 134 | * */ 135 | fileHeader.writeUInt32LE(magicNumber, 0); 136 | /** 137 | * An unsigned value, giving the number of the current major version of the format. 138 | * The value for the current version of the format is 2. 139 | * This value should change if the format changes in such a way that code that reads the new format could not read the old format 140 | * (i.e., code to read both formats would have to check the version number and use different code paths for the two formats) 141 | * and code that reads the old format could not read the new format. 142 | * 16 bits 143 | */ 144 | fileHeader.writeUInt16LE(PCAP_VERSION_MAJOR, 4); 145 | /** 146 | * An unsigned value, giving the number of the current minor version of the format. 147 | * The value is for the current version of the format is 4. 148 | * This value should change if the format changes in such a way that code that reads the new format could read the old format 149 | * without checking the version number but code that reads the old format could not read all files in the new format. 150 | * 16 bits 151 | */ 152 | fileHeader.writeUInt16LE(PCAP_VERSION_MINOR, 6); 153 | /** 154 | * Not used - SHOULD be filled with 0 by pcap file writers, and MUST be ignored by pcap file readers. 155 | * This value was documented by some older implementations as "gmt to local correction". 156 | * Some older pcap file writers stored non-zero values in this field. 157 | * 32 bits 158 | */ 159 | fileHeader.writeUInt32LE(0, 8); 160 | /** 161 | * Not used - SHOULD be filled with 0 by pcap file writers, and MUST be ignored by pcap file readers. 162 | * This value was documented by some older implementations as "accuracy of timestamps". 163 | * Some older pcap file writers stored non-zero values in this field. 164 | * 32 bits 165 | */ 166 | fileHeader.writeUInt32LE(0, 12); 167 | /** 168 | * An unsigned value indicating the maximum number of octets captured from each packet. 169 | * The portion of each packet that exceeds this value will not be stored in the file. 170 | * This value MUST NOT be zero; if no limit was specified, the value should be a number greater than or equal 171 | * to the largest packet length in the file. 172 | * 32 bits 173 | */ 174 | fileHeader.writeUInt32LE(EZSP_MAX_FRAME_LENGTH, 16); 175 | /** 176 | * An unsigned value that defines, in the lower 28 bits, the link layer type of packets in the file. 177 | * 32 bits 178 | */ 179 | fileHeader.writeUInt32LE(PCAP_LINKTYPE_IEEE802_15_4_WITH_FCS, 20); 180 | 181 | return fileHeader; 182 | }; 183 | 184 | export const createPcapPacketRecordMs = (packetData: Buffer): Buffer => { 185 | const packetHeader = Buffer.alloc(16); 186 | const timestamp = (Date.now() * 1000) / 1000000; 187 | const timestampSec = Math.trunc(timestamp); 188 | 189 | /** 32-bit unsigned integer that represents the number of seconds that have elapsed since 1970-01-01 00:00:00 UTC */ 190 | packetHeader.writeUInt32LE(timestampSec, 0); 191 | /** Number of microseconds or nanoseconds that have elapsed since that seconds. */ 192 | packetHeader.writeUInt32LE(Math.trunc((timestamp - timestampSec) * 1000000.0), 4); 193 | /** 194 | * Unsigned value that indicates the number of octets captured from the packet (i.e. the length of the Packet Data field). 195 | * It will be the minimum value among the Original Packet Length and the snapshot length for the interface (SnapLen, defined in Figure 1). 196 | * 32 bits 197 | */ 198 | packetHeader.writeUInt32LE(packetData.length, 8); 199 | /** 200 | * Unsigned value that indicates the actual length of the packet when it was transmitted on the network. 201 | * It can be different from the Captured Packet Length if the packet has been truncated by the capture process. 202 | * 32 bits 203 | */ 204 | packetHeader.writeUInt32LE(packetData.length, 12); 205 | 206 | return Buffer.concat([packetHeader, packetData]); 207 | }; 208 | -------------------------------------------------------------------------------- /src/utils/xmodem.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "node:events"; 2 | 3 | import { logger } from "../index.js"; 4 | import { computeCRC16 } from "./utils.js"; 5 | 6 | const NS = { namespace: "xmodemcrc" }; 7 | const FILLER = 0xff; 8 | 9 | /** First block number. */ 10 | const XMODEM_START_BLOCK = 1; 11 | /** Bytes in each block (header and checksum not included) */ 12 | const BLOCK_SIZE = 128; 13 | /** Maximum retries to send block before giving up */ 14 | const MAX_RETRIES = 10; 15 | 16 | export enum XSignal { 17 | /** Start of Header */ 18 | SOH = 0x01, 19 | /** End of Transmission */ 20 | EOT = 0x04, 21 | /** Acknowledge */ 22 | ACK = 0x06, 23 | /** Not Acknowledge */ 24 | NAK = 0x15, 25 | /** End of Transmission Block / File done */ 26 | ETB = 0x17, 27 | /** Cancel */ 28 | CAN = 0x18, 29 | /** Block OK */ 30 | BOK = 0x19, 31 | /** 'C' */ 32 | CRC = 0x43, 33 | } 34 | 35 | export enum XExitStatus { 36 | SUCCESS = 0, 37 | FAIL = 1, 38 | CANCEL = 2, 39 | } 40 | 41 | export enum XEvent { 42 | /** C byte received */ 43 | START = "start", 44 | STOP = "stop", 45 | /** Data to write */ 46 | DATA = "data", 47 | } 48 | 49 | interface XModemCRCEventMap { 50 | [XEvent.DATA]: [buffer: Buffer, progressPc: number]; 51 | [XEvent.START]: []; 52 | [XEvent.STOP]: [status: XExitStatus]; 53 | } 54 | 55 | export class XModemCRC extends EventEmitter { 56 | private blockNum: number = XMODEM_START_BLOCK; 57 | private blocks: Buffer[] = []; 58 | private retries: number = MAX_RETRIES; 59 | private sentEOF = false; 60 | private waitForBlock: number = XMODEM_START_BLOCK; 61 | 62 | public init(buffer: Buffer): void { 63 | this.blockNum = XMODEM_START_BLOCK; 64 | this.blocks = [Buffer.from([])]; // filler for start block offset 65 | this.retries = MAX_RETRIES; 66 | this.sentEOF = false; 67 | this.waitForBlock = XMODEM_START_BLOCK; 68 | let currentBlock = Buffer.alloc(BLOCK_SIZE); 69 | 70 | while (buffer.length > 0) { 71 | for (let i = 0; i < BLOCK_SIZE; i++) { 72 | currentBlock[i] = buffer[i] === undefined ? FILLER : buffer[i]; 73 | } 74 | 75 | buffer = buffer.subarray(BLOCK_SIZE); 76 | this.blocks.push(currentBlock); 77 | currentBlock = Buffer.alloc(BLOCK_SIZE); 78 | } 79 | 80 | const blocksCount = this.blocks.length - XMODEM_START_BLOCK; 81 | 82 | logger.debug(`Outgoing blocks count=${blocksCount}, size=${blocksCount * BLOCK_SIZE}.`, NS); 83 | } 84 | 85 | public process(recdData: Buffer): void { 86 | if (this.waitForBlock !== this.blockNum) { 87 | logger.warning( 88 | `Received out of sequence data: ${recdData.toString("hex")} (blockNum=${this.blockNum}, expected=${this.waitForBlock}).`, 89 | NS, 90 | ); 91 | this.retries--; 92 | 93 | if (this.retries === 0) { 94 | logger.error(`Maximum retries ${MAX_RETRIES} reached. Giving up.`, NS); 95 | this.emit(XEvent.STOP, XExitStatus.FAIL); 96 | } 97 | 98 | return; 99 | } 100 | 101 | logger.debug(`Current block ${this.blockNum}. Received data: ${recdData.toString("hex")}.`, NS); 102 | 103 | switch (recdData[0]) { 104 | case XSignal.CRC: { 105 | if (this.blockNum === XMODEM_START_BLOCK) { 106 | logger.debug("Received C byte, starting transfer...", NS); 107 | 108 | if (this.blocks.length > this.blockNum) { 109 | this.emit(XEvent.START); 110 | this.emitBlock(this.blockNum, this.blocks[this.blockNum]); 111 | 112 | this.blockNum++; 113 | } 114 | } 115 | 116 | break; 117 | } 118 | 119 | case XSignal.ACK: { 120 | if (this.blockNum > XMODEM_START_BLOCK) { 121 | this.retries = MAX_RETRIES; 122 | 123 | logger.debug("ACK received.", NS); 124 | 125 | if (this.blocks.length > this.blockNum) { 126 | this.emitBlock(this.blockNum, this.blocks[this.blockNum]); 127 | 128 | this.blockNum++; 129 | } else if (this.blocks.length === this.blockNum) { 130 | if (this.sentEOF === false) { 131 | this.sentEOF = true; 132 | 133 | logger.debug("Sending End of Transmission.", NS); 134 | this.emit(XEvent.DATA, Buffer.from([XSignal.EOT]), 100); 135 | } else { 136 | logger.debug("Done.", NS); 137 | this.emit(XEvent.STOP, XExitStatus.SUCCESS); 138 | } 139 | } 140 | } 141 | 142 | break; 143 | } 144 | 145 | case XSignal.NAK: { 146 | if (this.blockNum > XMODEM_START_BLOCK) { 147 | this.retries--; 148 | 149 | logger.debug("NAK received.", NS); 150 | 151 | if (this.retries === 0) { 152 | logger.error(`Maximum retries ${MAX_RETRIES} reached. Giving up.`, NS); 153 | this.emit(XEvent.STOP, XExitStatus.FAIL); 154 | } else if (this.blockNum === this.blocks.length && this.sentEOF) { 155 | logger.warning("Received NAK, resending EOT.", NS); 156 | this.emit(XEvent.DATA, Buffer.from([XSignal.EOT]), 0); 157 | } else { 158 | logger.warning("Packet corrupted, resending previous block.", NS); 159 | 160 | this.blockNum--; 161 | 162 | if (this.blocks.length > this.blockNum) { 163 | this.emitBlock(this.blockNum, this.blocks[this.blockNum]); 164 | 165 | this.blockNum++; 166 | } 167 | } 168 | } 169 | 170 | break; 171 | } 172 | 173 | case XSignal.CAN: { 174 | logger.error("Received cancel.", NS); 175 | this.emit(XEvent.STOP, XExitStatus.CANCEL); 176 | 177 | break; 178 | } 179 | 180 | default: { 181 | logger.debug(`Unrecognized data received for block ${this.blockNum}. Ignoring.`, NS); 182 | 183 | break; 184 | } 185 | } 186 | } 187 | 188 | private emitBlock(blockNum: number, blockData: Buffer): void { 189 | const progressPc = Math.round((blockNum / (this.blocks.length - XMODEM_START_BLOCK)) * 100); 190 | this.waitForBlock = blockNum + 1; 191 | blockNum &= 0xff; // starts at 1, goes to 255, then wraps back to 0 (XModem spec) 192 | 193 | logger.debug(`Sending block ${blockNum}.`, NS); 194 | 195 | this.emit( 196 | XEvent.DATA, 197 | Buffer.concat([Buffer.from([XSignal.SOH, blockNum, 0xff - blockNum]), blockData, computeCRC16(blockData)]), 198 | progressPc, 199 | ); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "dist", 5 | "module": "nodenext", 6 | "moduleResolution": "nodenext", 7 | "target": "ESNext", 8 | "lib": ["ESNext"], 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "composite": true, 14 | "checkJs": true 15 | }, 16 | "include": ["src/**/*"], 17 | "ts-node": { 18 | "esm": true 19 | } 20 | } 21 | --------------------------------------------------------------------------------