From b953431eafd93043e9bdb22bc7463ffc19482dc3 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:02:10 +0100 Subject: [PATCH] Switch to biomejs --- .github/workflows/ci.yml | 18 + .github/workflows/test.yml | 23 - .mocharc.json | 15 - README.md | 4 +- bin/dev.js | 4 +- bin/run.js | 4 +- biome.json | 40 + eslint.config.mjs | 33 - firmware-links.json | 2 +- package-lock.json | 2343 ++---------------------- package.json | 150 +- prettier.config.mjs | 28 - src/commands/bootloader/index.ts | 206 +-- src/commands/monitor/index.ts | 74 +- src/commands/router/index.ts | 783 ++++---- src/commands/sniff/index.ts | 232 +-- src/commands/stack/index.ts | 1130 ++++++------ src/commands/utils/index.ts | 104 +- src/index.ts | 70 +- src/utils/bootloader.ts | 544 +++--- src/utils/consts.ts | 132 +- src/utils/cpc.ts | 216 +-- src/utils/ember.ts | 244 +-- src/utils/port.ts | 164 +- src/utils/router-endpoints.ts | 26 +- src/utils/transport.ts | 198 +- src/utils/types.ts | 214 +-- src/utils/update-firmware-links.ts | 342 ++-- src/utils/utils.ts | 192 +- src/utils/wireshark.ts | 124 +- src/utils/xmodem.ts | 152 +- test/commands/bootloader/index.test.ts | 1 - test/commands/monitor/index.test.ts | 1 - test/commands/router/index.test.ts | 1 - test/commands/sniff/index.test.ts | 1 - test/commands/stack/index.test.ts | 1 - test/commands/utils/index.test.ts | 1 - test/tsconfig.json | 10 - tsconfig.json | 36 +- 39 files changed, 2878 insertions(+), 4985 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/test.yml delete mode 100644 .mocharc.json create mode 100644 biome.json delete mode 100644 eslint.config.mjs delete mode 100644 prettier.config.mjs delete mode 100644 test/commands/bootloader/index.test.ts delete mode 100644 test/commands/monitor/index.test.ts delete mode 100644 test/commands/router/index.test.ts delete mode 100644 test/commands/sniff/index.test.ts delete mode 100644 test/commands/stack/index.test.ts delete mode 100644 test/commands/utils/index.test.ts delete mode 100644 test/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d009ed6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: ci +on: + push: + workflow_dispatch: + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: 'package.json' + + - run: npm install + - run: npm run check:ci + - run: npm run build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 1d156cf..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: tests -on: - push: - branches-ignore: [main] - workflow_dispatch: - -jobs: - unit-tests: - strategy: - matrix: - os: ['ubuntu-latest', 'windows-latest'] - node_version: [lts/-1, lts/*, latest] - fail-fast: false - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node_version }} - cache: npm - - run: npm install - - run: npm run build - - run: npm run test diff --git a/.mocharc.json b/.mocharc.json deleted file mode 100644 index fa25f20..0000000 --- a/.mocharc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "require": [ - "ts-node/register" - ], - "watch-extensions": [ - "ts" - ], - "recursive": true, - "reporter": "spec", - "timeout": 60000, - "node-option": [ - "loader=ts-node/esm", - "experimental-specifier-resolution=node" - ] -} diff --git a/README.md b/README.md index 3a02309..af90e20 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ DESCRIPTION Display help for ember-zli. ``` -_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.20/src/commands/help.ts)_ +_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.26/src/commands/help.ts)_ ## `ember-zli monitor` @@ -186,5 +186,5 @@ FLAG DESCRIPTIONS Additionally shows the architecture, node version, operating system, and versions of plugins that the CLI is using. ``` -_See code: [@oclif/plugin-version](https://github.com/oclif/plugin-version/blob/v2.2.19/src/commands/version.ts)_ +_See code: [@oclif/plugin-version](https://github.com/oclif/plugin-version/blob/v2.2.25/src/commands/version.ts)_ diff --git a/bin/dev.js b/bin/dev.js index 9d7ce3c..94b0b3e 100644 --- a/bin/dev.js +++ b/bin/dev.js @@ -1,5 +1,5 @@ #!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning -import {execute} from '@oclif/core' +import { execute } from "@oclif/core"; -await execute({development: true, dir: import.meta.url}) +await execute({ development: true, dir: import.meta.url }); diff --git a/bin/run.js b/bin/run.js index dd50271..94a0d6f 100644 --- a/bin/run.js +++ b/bin/run.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -import {execute} from '@oclif/core' +import { execute } from "@oclif/core"; -await execute({dir: import.meta.url}) +await execute({ dir: import.meta.url }); diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..692c881 --- /dev/null +++ b/biome.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 4, + "lineWidth": 150, + "lineEnding": "lf" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noNonNullAssertion": "off", + "noParameterAssign": "off" + }, + "suspicious": { + "noAssignInExpressions": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index b05abcb..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,33 +0,0 @@ -import eslint from '@eslint/js'; -import tseslint from 'typescript-eslint'; -import prettier from 'eslint-config-prettier'; - -export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.recommended, - prettier, - { - files: ['**/*.ts'], - languageOptions: { - ecmaVersion: 'latest', - sourceType: 'script', - parserOptions: { - projectService: { - allowDefaultProject: ['eslint.config.mjs', 'prettier.config.mjs'], - }, - }, - }, - rules: { - '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/ban-ts-comment': 'error', - '@typescript-eslint/explicit-function-return-type': 'error', - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-unused-vars': 'error', - '@typescript-eslint/return-await': ['error', 'always'], - '@typescript-eslint/no-floating-promises': 'error', - }, - }, - { - ignores: ['dist', 'tmp', 'coverage'], - }, -); diff --git a/firmware-links.json b/firmware-links.json index e7d7db5..643f361 100644 --- a/firmware-links.json +++ b/firmware-links.json @@ -119,4 +119,4 @@ "ROUTER - TubeZB MGM24": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.0.1/MGM240PA32VNN_app_clear_134217728_1572864_8192_134242304.gbl", "ROUTER - TubeZB MGM24PB": "https://github.com/Nerivec/silabs-firmware-recovery/releases/download/v0.0.1/MGM240PB32VNN_app_clear_134217728_1572864_8192_134242304.gbl" } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 2c23fe7..d907f0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,23 +23,13 @@ "ember-zli": "bin/run.js" }, "devDependencies": { - "@eslint/js": "^9", - "@ianvs/prettier-plugin-sort-imports": "^4", - "@oclif/prettier-config": "^0", - "@oclif/test": "^4", + "@biomejs/biome": "1.9.4", "@types/cli-progress": "^3", - "@types/eslint__js": "^8", - "@types/mocha": "^10", "@types/node": "^20", - "eslint": "^9", - "eslint-config-prettier": "^10", - "mocha": "^11", "oclif": "^4", - "prettier": "^3", "shx": "^0", "ts-node": "^10", - "typescript": "^5", - "typescript-eslint": "^8" + "typescript": "^5" }, "engines": { "node": ">=20.15.0" @@ -996,120 +986,168 @@ "node": ">=18.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" + "node": ">=14.21.3" } }, - "node_modules/@babel/generator": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", - "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" + "node": ">=14.21.3" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=14.21.3" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=14.21.3" } }, - "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.9" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0.0" + "node": ">=14.21.3" } }, - "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" - }, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=14.21.3" } }, - "node_modules/@babel/traverse": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", - "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.9", - "@babel/parser": "^7.26.9", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">=14.21.3" } }, - "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">=14.21.3" } }, "node_modules/@colors/colors": { @@ -1156,284 +1194,6 @@ "kuler": "^2.0.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", - "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", - "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.12.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@ianvs/prettier-plugin-sort-imports": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.4.1.tgz", - "integrity": "sha512-F0/Hrcfpy8WuxlQyAWJTEren/uxKhYonOGY4OyWmwRdeTvkh9mMSCxowZLjNkhwi/2ipqCgtXwwOk7tW0mWXkA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@babel/generator": "^7.26.2", - "@babel/parser": "^7.26.2", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", - "semver": "^7.5.2" - }, - "peerDependencies": { - "@vue/compiler-sfc": "2.7.x || 3.x", - "prettier": "2 || 3" - }, - "peerDependenciesMeta": { - "@vue/compiler-sfc": { - "optional": true - } - } - }, "node_modules/@inquirer/checkbox": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.2.tgz", @@ -1738,133 +1498,23 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=6.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true, "license": "MIT" }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -2031,41 +1681,6 @@ "node": ">=18.0.0" } }, - "node_modules/@oclif/prettier-config": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@oclif/prettier-config/-/prettier-config-0.2.1.tgz", - "integrity": "sha512-XB8kwQj8zynXjIIWRm+6gO/r8Qft2xKtwBMSmq1JRqtA6TpwpqECqiu8LosBCyg2JBXuUy2lU23/L98KIR7FrQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@oclif/test": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@oclif/test/-/test-4.1.11.tgz", - "integrity": "sha512-j689R13E2so1Rj6jJUfQ67yJ4N7u6L5KFzv87cvUfD9AZ79xAtCxGrd34/iOLUDJmv1huFt/0QumBcjKoWUSYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansis": "^3.16.0", - "debug": "^4.4.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@oclif/core": ">= 3.0.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -3014,34 +2629,6 @@ "@types/node": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint__js": { - "version": "8.42.3", - "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", - "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -3049,20 +2636,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mocha": { - "version": "10.0.10", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", - "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/mute-stream": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", @@ -3096,186 +2669,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz", - "integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/type-utils": "8.25.0", - "@typescript-eslint/utils": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz", - "integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/typescript-estree": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz", - "integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz", - "integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.25.0", - "@typescript-eslint/utils": "8.25.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz", - "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz", - "integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/visitor-keys": "8.25.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz", - "integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.25.0", - "@typescript-eslint/types": "8.25.0", - "@typescript-eslint/typescript-estree": "8.25.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz", - "integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.25.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -3289,16 +2682,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -3312,33 +2695,6 @@ "node": ">=0.4.0" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3354,19 +2710,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3391,20 +2734,6 @@ "node": ">=14" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3412,13 +2741,6 @@ "dev": true, "license": "MIT" }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -3450,19 +2772,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bonjour-service": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", @@ -3501,13 +2810,6 @@ "node": ">=8" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" - }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -3537,16 +2839,6 @@ "node": ">=14.16" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/camel-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", @@ -3558,19 +2850,6 @@ "tslib": "^2.0.3" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/capital-case": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", @@ -3638,44 +2917,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "license": "MIT" }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/clean-stack": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", @@ -3704,80 +2945,24 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" } }, "node_modules/color": { @@ -3889,21 +3074,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/debounce": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", @@ -3933,19 +3103,6 @@ } } }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -3975,13 +3132,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -4015,16 +3165,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4060,13 +3200,6 @@ "tslib": "^2.0.3" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -4104,16 +3237,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4126,197 +3249,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", - "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.0", - "@eslint/js": "9.21.0", - "@eslint/plugin-kit": "^0.2.7", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", - "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "build/bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -4365,13 +3297,6 @@ "node": ">= 6" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-levenshtein": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", @@ -4428,19 +3353,6 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -4474,23 +3386,6 @@ "node": ">=8" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/find-yarn-workspace-root": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", @@ -4501,60 +3396,12 @@ "micromatch": "^4.0.2" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data-encoder": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", @@ -4587,21 +3434,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4612,16 +3444,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -4674,50 +3496,6 @@ "dev": true, "license": "ISC" }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -4769,14 +3547,7 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" + "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", @@ -4800,16 +3571,6 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/header-case": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", @@ -4894,33 +3655,6 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -4972,19 +3706,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -5090,19 +3811,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -5115,29 +3823,6 @@ "node": ">=8" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -5178,39 +3863,6 @@ "node": "*" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5225,20 +3877,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -5265,20 +3903,6 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -5291,22 +3915,6 @@ "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -5314,30 +3922,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -5452,16 +4036,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/mixin-deep": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-2.0.1.tgz", @@ -5471,55 +4045,6 @@ "node": ">=6" } }, - "node_modules/mocha": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", - "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5548,13 +4073,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -5601,16 +4119,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/normalize-url": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", @@ -5827,31 +4335,6 @@ "fn.name": "1.x.x" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/optionator/node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -5871,45 +4354,6 @@ "node": ">=12.20" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -5921,19 +4365,6 @@ "tslib": "^2.0.3" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -5970,16 +4401,6 @@ "tslib": "^2.0.3" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -5990,16 +4411,6 @@ "node": ">=0.10.0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -6007,23 +4418,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6033,13 +4427,6 @@ "node": ">=8" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -6052,32 +4439,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", - "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -6085,16 +4446,6 @@ "dev": true, "license": "ISC" }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6128,16 +4479,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -6152,19 +4493,6 @@ "node": ">= 6" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -6190,16 +4518,6 @@ "node": ">=14" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -6228,16 +4546,6 @@ "dev": true, "license": "MIT" }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/responselike": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", @@ -6356,39 +4664,6 @@ "upper-case-first": "^2.0.2" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/shelljs": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", @@ -6618,45 +4893,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -6678,59 +4914,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strnum": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.1.tgz", @@ -6869,19 +5052,6 @@ "node": ">= 14.0.0" } }, - "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -6956,19 +5126,6 @@ "node": "*" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", @@ -6995,29 +5152,6 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.25.0.tgz", - "integrity": "sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.25.0", - "@typescript-eslint/parser": "8.25.0", - "@typescript-eslint/utils": "8.25.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -7055,16 +5189,6 @@ "tslib": "^2.0.3" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7113,22 +5237,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", @@ -7177,29 +5285,12 @@ "node": ">= 12.0.0" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "license": "MIT" }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -7214,48 +5305,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -7284,71 +5333,6 @@ "dev": true, "license": "ISC" }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser/node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -7359,19 +5343,6 @@ "node": ">=6" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/yoctocolors-cjs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", diff --git a/package.json b/package.json index 21cac03..e99c384 100644 --- a/package.json +++ b/package.json @@ -1,92 +1,62 @@ { - "name": "ember-zli", - "description": "Interact with EmberZNet-based adapters using zigbee-herdsman 'ember' driver", - "version": "2.10.2", - "author": "Nerivec", - "bin": { - "ember-zli": "bin/run.js" - }, - "bugs": "https://github.com/Nerivec/ember-zli/issues", - "dependencies": { - "@inquirer/prompts": "^7", - "@oclif/core": "^4", - "@oclif/plugin-help": "^6", - "@oclif/plugin-not-found": "^3", - "@oclif/plugin-version": "^2", - "bonjour-service": "^1", - "cli-progress": "^3", - "winston": "^3", - "zigbee-herdsman": "3.2.6" - }, - "devDependencies": { - "@eslint/js": "^9", - "@ianvs/prettier-plugin-sort-imports": "^4", - "@oclif/prettier-config": "^0", - "@oclif/test": "^4", - "@types/cli-progress": "^3", - "@types/eslint__js": "^8", - "@types/mocha": "^10", - "@types/node": "^20", - "eslint": "^9", - "eslint-config-prettier": "^10", - "mocha": "^11", - "oclif": "^4", - "prettier": "^3", - "shx": "^0", - "ts-node": "^10", - "typescript": "^5", - "typescript-eslint": "^8" - }, - "engines": { - "node": ">=20.15.0" - }, - "files": [ - "/bin", - "/dist", - "/oclif.manifest.json" - ], - "homepage": "https://github.com/Nerivec/ember-zli", - "keywords": [ - "zigbee2mqtt", - "z2m", - "zigbee-herdsman", - "herdsman", - "ember", - "emberznet", - "ezsp", - "silabs", - "zigbee" - ], - "license": "GPL-3.0-or-later", - "main": "dist/index.js", - "type": "module", - "oclif": { - "bin": "ember-zli", - "dirname": "ember-zli", - "commands": "./dist/commands", - "plugins": [ - "@oclif/plugin-help", - "@oclif/plugin-version", - "@oclif/plugin-not-found" - ], - "topicSeparator": " ", - "topics": {} - }, - "repository": { - "type": "git", - "url": "git+https://github.com/Nerivec/ember-zli.git" - }, - "scripts": { - "build": "shx rm -rf dist tsconfig.tsbuildinfo && tsc -b", - "build:run": "npm run build && npm run prepack && npm run postpack", - "update-fw-links": "node ./dist/utils/update-firmware-links.js", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "lint": "eslint .", - "postpack": "shx rm -f oclif.manifest.json", - "xposttest": "npm run lint", - "prepack": "oclif manifest && oclif readme", - "test": "mocha --forbid-only \"test/**/*.test.ts\"", - "version": "oclif readme && git add README.md" - }, - "types": "dist/index.d.ts" + "name": "ember-zli", + "description": "Interact with EmberZNet-based adapters using zigbee-herdsman 'ember' driver", + "version": "2.10.2", + "author": "Nerivec", + "bin": { + "ember-zli": "bin/run.js" + }, + "bugs": "https://github.com/Nerivec/ember-zli/issues", + "dependencies": { + "@inquirer/prompts": "^7", + "@oclif/core": "^4", + "@oclif/plugin-help": "^6", + "@oclif/plugin-not-found": "^3", + "@oclif/plugin-version": "^2", + "bonjour-service": "^1", + "cli-progress": "^3", + "winston": "^3", + "zigbee-herdsman": "3.2.6" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/cli-progress": "^3", + "@types/node": "^20", + "oclif": "^4", + "shx": "^0", + "ts-node": "^10", + "typescript": "^5" + }, + "engines": { + "node": ">=20.15.0" + }, + "files": ["/bin", "/dist", "/oclif.manifest.json"], + "homepage": "https://github.com/Nerivec/ember-zli", + "keywords": ["zigbee2mqtt", "z2m", "zigbee-herdsman", "herdsman", "ember", "emberznet", "ezsp", "silabs", "zigbee"], + "license": "GPL-3.0-or-later", + "main": "dist/index.js", + "type": "module", + "oclif": { + "bin": "ember-zli", + "dirname": "ember-zli", + "commands": "./dist/commands", + "plugins": ["@oclif/plugin-help", "@oclif/plugin-version", "@oclif/plugin-not-found"], + "topicSeparator": " ", + "topics": {} + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Nerivec/ember-zli.git" + }, + "scripts": { + "build": "shx rm -rf dist tsconfig.tsbuildinfo && tsc -b", + "build:run": "npm run build && npm run prepack && npm run postpack", + "update-fw-links": "node ./dist/utils/update-firmware-links.js", + "check": "biome check --write", + "check:ci": "biome check", + "postpack": "shx rm -f oclif.manifest.json", + "prepack": "oclif manifest && oclif readme", + "version": "oclif readme && git add README.md" + }, + "types": "dist/index.d.ts" } diff --git a/prettier.config.mjs b/prettier.config.mjs deleted file mode 100644 index 594b800..0000000 --- a/prettier.config.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import oclifPrettierConfig from '@oclif/prettier-config'; -import sortImports from '@ianvs/prettier-plugin-sort-imports'; - -export default { - ...oclifPrettierConfig, - plugins: [sortImports], - singleQuote: true, - printWidth: 150, - bracketSpacing: true, - endOfLine: "lf", - tabWidth: 4, - importOrder: [ - "", - "^(node:)", - "", - "", - "", - "^[.]", - "", - "", - "", - "", - "", - "^zigbee", - "", - "^[.]", - ], -}; diff --git a/src/commands/bootloader/index.ts b/src/commands/bootloader/index.ts index 21ba836..5df69c9 100644 --- a/src/commands/bootloader/index.ts +++ b/src/commands/bootloader/index.ts @@ -1,175 +1,175 @@ -import type { AdapterModel, FirmwareLinks, FirmwareVariant, SelectChoices } from '../../utils/types.js' +import type { AdapterModel, FirmwareLinks, FirmwareVariant, SelectChoices } from "../../utils/types.js"; -import { readFileSync } from 'node:fs' +import { readFileSync } from "node:fs"; -import { confirm, input, select } from '@inquirer/prompts' -import { Command } from '@oclif/core' -import { Presets, SingleBar } from 'cli-progress' +import { confirm, input, select } from "@inquirer/prompts"; +import { Command } from "@oclif/core"; +import { Presets, SingleBar } from "cli-progress"; -import { DEFAULT_FIRMWARE_GBL_PATH, logger } from '../../index.js' -import { BootloaderEvent, BootloaderMenu, GeckoBootloader } from '../../utils/bootloader.js' -import { ADAPTER_MODELS, PRE_DEFINED_FIRMWARE_LINKS_URL } from '../../utils/consts.js' -import { FirmwareValidation } from '../../utils/enums.js' -import { getPortConf } from '../../utils/port.js' -import { browseToFile, fetchJson } from '../../utils/utils.js' +import { DEFAULT_FIRMWARE_GBL_PATH, logger } from "../../index.js"; +import { BootloaderEvent, BootloaderMenu, GeckoBootloader } from "../../utils/bootloader.js"; +import { ADAPTER_MODELS, PRE_DEFINED_FIRMWARE_LINKS_URL } from "../../utils/consts.js"; +import { FirmwareValidation } from "../../utils/enums.js"; +import { getPortConf } from "../../utils/port.js"; +import { browseToFile, fetchJson } from "../../utils/utils.js"; export default class Bootloader extends Command { - static override args = {} - static override description = 'Interact with the Gecko bootloader in the adapter.' - static override examples = ['<%= config.bin %> <%= command.id %>'] + static override args = {}; + static override description = "Interact with the Gecko bootloader in the adapter."; + static override examples = ["<%= config.bin %> <%= command.id %>"]; public async run(): Promise { - const portConf = await getPortConf() - logger.debug(`Using port conf: ${JSON.stringify(portConf)}`) + const portConf = await getPortConf(); + logger.debug(`Using port conf: ${JSON.stringify(portConf)}`); - const adapterModelChoices: SelectChoices = [{ name: 'Not in this list', value: undefined }] + const adapterModelChoices: SelectChoices = [{ name: "Not in this list", value: undefined }]; for (const model of ADAPTER_MODELS) { - adapterModelChoices.push({ name: model, value: model }) + adapterModelChoices.push({ name: model, value: model }); } const adapterModel = await select({ choices: adapterModelChoices, - message: 'Adapter model', - }) + message: "Adapter model", + }); - const gecko = new GeckoBootloader(portConf, adapterModel) - const progressBar = new SingleBar({ clearOnComplete: true, format: '{bar} {percentage}%' }, Presets.shades_classic) + const gecko = new GeckoBootloader(portConf, adapterModel); + const progressBar = new SingleBar({ clearOnComplete: true, format: "{bar} {percentage}%" }, Presets.shades_classic); gecko.on(BootloaderEvent.FAILED, () => { - this.exit(1) - }) + this.exit(1); + }); gecko.on(BootloaderEvent.CLOSED, () => { - this.exit(0) - }) + this.exit(0); + }); gecko.on(BootloaderEvent.UPLOAD_START, () => { - progressBar.start(100, 0) - }) + progressBar.start(100, 0); + }); gecko.on(BootloaderEvent.UPLOAD_STOP, () => { - progressBar.stop() - }) + progressBar.stop(); + }); gecko.on(BootloaderEvent.UPLOAD_PROGRESS, (percent) => { - progressBar.update(percent) - }) + progressBar.update(percent); + }); - await gecko.connect() + await gecko.connect(); - let exit: boolean = false + let exit = false; while (!exit) { - exit = await this.navigateMenu(gecko) + exit = await this.navigateMenu(gecko); } - await gecko.transport.close(false) + await gecko.transport.close(false); - return this.exit(0) + return this.exit(0); } private async navigateMenu(gecko: GeckoBootloader): Promise { const answer = await select<-1 | BootloaderMenu>({ choices: [ - { name: 'Get info', value: BootloaderMenu.INFO }, - { name: 'Update firmware', value: BootloaderMenu.UPLOAD_GBL }, + { name: "Get info", value: BootloaderMenu.INFO }, + { name: "Update firmware", value: BootloaderMenu.UPLOAD_GBL }, { - name: 'Clear NVM3 (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#nvm3-clear)', + name: "Clear NVM3 (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#nvm3-clear)", value: BootloaderMenu.CLEAR_NVM3, disabled: !gecko.adapterModel, }, { - name: 'Clear APP (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#app-clear)', + name: "Clear APP (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#app-clear)", value: BootloaderMenu.CLEAR_APP, disabled: !gecko.adapterModel, }, - { name: 'Exit bootloader (run firmware)', value: BootloaderMenu.RUN }, - { name: 'Force close', value: -1 }, + { name: "Exit bootloader (run firmware)", value: BootloaderMenu.RUN }, + { name: "Force close", value: -1 }, ], - message: 'Menu', - }) + message: "Menu", + }); if (answer === -1) { - logger.warning(`Force closing... You may need to unplug/replug the adapter.`) - return true + logger.warning("Force closing... You may need to unplug/replug the adapter."); + return true; } - let firmware: Buffer | undefined = undefined + let firmware: Buffer | undefined = undefined; if (answer === BootloaderMenu.UPLOAD_GBL) { - let validFirmware: FirmwareValidation = FirmwareValidation.INVALID + let validFirmware: FirmwareValidation = FirmwareValidation.INVALID; while (validFirmware !== FirmwareValidation.VALID) { - firmware = await this.selectFirmware(gecko) + firmware = await this.selectFirmware(gecko); - validFirmware = await gecko.validateFirmware(firmware) + validFirmware = await gecko.validateFirmware(firmware); if (validFirmware === FirmwareValidation.CANCELLED) { - return false + return false; } } } else if (answer === BootloaderMenu.CLEAR_NVM3) { const confirmed = await confirm({ default: false, message: `Confirm adapter is: ${gecko.adapterModel}?`, - }) + }); if (!confirmed) { - logger.warning(`Cancelled NVM3 clearing.`) - return false + logger.warning("Cancelled NVM3 clearing."); + return false; } const nvm3Size = await select({ choices: [ - { name: '32768', value: 32768 }, - { name: '40960', value: 40960 }, + { name: "32768", value: 32768 }, + { name: "40960", value: 40960 }, ], - message: 'NVM3 Size (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#nvm3-clear)', - }) - const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL) - const variant = nvm3Size === 32768 ? `nvm3_32768_clear` : `nvm3_40960_clear` - firmware = await this.downloadFirmware(firmwareLinks[variant][gecko.adapterModel!]!) + message: "NVM3 Size (https://github.com/Nerivec/silabs-firmware-recovery?tab=readme-ov-file#nvm3-clear)", + }); + const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL); + const variant = nvm3Size === 32768 ? "nvm3_32768_clear" : "nvm3_40960_clear"; + firmware = await this.downloadFirmware(firmwareLinks[variant][gecko.adapterModel!]!); } else if (answer === BootloaderMenu.CLEAR_APP) { const confirmed = await confirm({ default: false, message: `Confirm adapter is: ${gecko.adapterModel}?`, - }) + }); if (!confirmed) { - logger.warning(`Cancelled APP clearing.`) - return false + logger.warning("Cancelled APP clearing."); + return false; } - const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL) - firmware = await this.downloadFirmware(firmwareLinks.app_clear[gecko.adapterModel!]!) + const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL); + firmware = await this.downloadFirmware(firmwareLinks.app_clear[gecko.adapterModel!]!); } - return await gecko.navigate(answer, firmware) + return await gecko.navigate(answer, firmware); } private async downloadFirmware(url: string): Promise { try { - logger.info(`Downloading firmware from ${url}.`) + logger.info(`Downloading firmware from ${url}.`); - const response = await fetch(url) + const response = await fetch(url); if (!response.ok) { - throw new Error(`${response.status}`) + throw new Error(`${response.status}`); } - const arrayBuffer = await response.arrayBuffer() + const arrayBuffer = await response.arrayBuffer(); - return Buffer.from(arrayBuffer) + return Buffer.from(arrayBuffer); } catch (error) { - logger.error(`Failed to download firmware file from ${url} with error ${error}.`) + logger.error(`Failed to download firmware file from ${url} with error ${error}.`); } - return undefined + return undefined; } private async selectFirmware(gecko: GeckoBootloader): Promise { - const enum FirmwareSource { + enum FirmwareSource { PRE_DEFINED = 0, URL = 1, FILE = 2, @@ -181,72 +181,72 @@ export default class Bootloader extends Command { value: FirmwareSource.PRE_DEFINED, disabled: gecko.adapterModel === undefined, }, - { name: 'Provide URL', value: FirmwareSource.URL }, - { name: `Browse to file`, value: FirmwareSource.FILE }, + { name: "Provide URL", value: FirmwareSource.URL }, + { name: "Browse to file", value: FirmwareSource.FILE }, ], - message: 'Firmware source', - }) + message: "Firmware source", + }); switch (firmwareSource) { case FirmwareSource.PRE_DEFINED: { - const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL) + const firmwareLinks = await fetchJson(PRE_DEFINED_FIRMWARE_LINKS_URL); // valid adapterModel since select option disabled if not - const recommended = firmwareLinks.latest[gecko.adapterModel!] - const official = firmwareLinks.official[gecko.adapterModel!] - const experimental = firmwareLinks.experimental[gecko.adapterModel!] + const recommended = firmwareLinks.latest[gecko.adapterModel!]; + const official = firmwareLinks.official[gecko.adapterModel!]; + const experimental = firmwareLinks.experimental[gecko.adapterModel!]; const firmwareVariant = await select({ choices: [ { - name: `Latest`, - value: 'latest', + name: "Latest", + value: "latest", description: recommended ? recommended : undefined, disabled: !recommended, }, { - name: `Latest from manufacturer`, - value: 'official', + name: "Latest from manufacturer", + value: "official", description: official ? official : undefined, disabled: !official, }, { - name: `Experimental`, - value: 'experimental', + name: "Experimental", + value: "experimental", description: experimental ? experimental : undefined, disabled: !experimental, }, ], - message: 'Firmware version', - }) - const firmwareUrl = firmwareLinks[firmwareVariant][gecko.adapterModel!] + message: "Firmware version", + }); + const firmwareUrl = firmwareLinks[firmwareVariant][gecko.adapterModel!]; // just in case (and to pass linter) if (!firmwareUrl) { - return undefined + return undefined; } - return await this.downloadFirmware(firmwareUrl) + return await this.downloadFirmware(firmwareUrl); } case FirmwareSource.URL: { const url = await input({ - message: 'Enter the URL to the firmware file', + message: "Enter the URL to the firmware file", validate(value) { try { - new URL(value) - return true + new URL(value); + return true; } catch { - return false + return false; } }, - }) + }); - return await this.downloadFirmware(url) + return await this.downloadFirmware(url); } case FirmwareSource.FILE: { - const firmwareFile = await browseToFile('Firmware file', DEFAULT_FIRMWARE_GBL_PATH) + const firmwareFile = await browseToFile("Firmware file", DEFAULT_FIRMWARE_GBL_PATH); - return readFileSync(firmwareFile) + return readFileSync(firmwareFile); } } } diff --git a/src/commands/monitor/index.ts b/src/commands/monitor/index.ts index 78a66a8..541d088 100644 --- a/src/commands/monitor/index.ts +++ b/src/commands/monitor/index.ts @@ -1,81 +1,81 @@ -import { Command } from '@oclif/core' +import { Command } from "@oclif/core"; -import { SLStatus } from 'zigbee-herdsman/dist/adapter/ember/enums.js' +import { SLStatus } from "zigbee-herdsman/dist/adapter/ember/enums.js"; -import { logger } from '../../index.js' -import { getPortConf } from '../../utils/port.js' -import { Transport, TransportEvent } from '../../utils/transport.js' +import { logger } from "../../index.js"; +import { getPortConf } from "../../utils/port.js"; +import { Transport, TransportEvent } from "../../utils/transport.js"; export default class Monitor extends Command { - static override args = {} - static override description = 'Monitor the chosen port in the console.' - static override examples = ['<%= config.bin %> <%= command.id %>'] + static override args = {}; + static override description = "Monitor the chosen port in the console."; + static override examples = ["<%= config.bin %> <%= command.id %>"]; - private logBuffer: Buffer = Buffer.alloc(0) + private logBuffer: Buffer = Buffer.alloc(0); public async run(): Promise { - const portConf = await getPortConf() - logger.debug(`Using port conf: ${JSON.stringify(portConf)}`) + const portConf = await getPortConf(); + logger.debug(`Using port conf: ${JSON.stringify(portConf)}`); - const transport = new Transport(portConf) + const transport = new Transport(portConf); try { - await transport.initPort() + await transport.initPort(); } catch (error) { - logger.error(`Failed to open port: ${error}.`) + logger.error(`Failed to open port: ${error}.`); - await transport.close(false, false) // force failed below + await transport.close(false, false); // force failed below - return this.exit(1) + return this.exit(1); } - logger.info(`Started monitoring. Press any key to stop.`) + logger.info("Started monitoring. Press any key to stop."); - transport.on(TransportEvent.FAILED, () => this.exit(1)) - transport.on(TransportEvent.DATA, this.onTransportData.bind(this)) + transport.on(TransportEvent.FAILED, () => this.exit(1)); + transport.on(TransportEvent.DATA, this.onTransportData.bind(this)); - process.stdin.setRawMode(true) - process.stdin.resume() + process.stdin.setRawMode(true); + process.stdin.resume(); await new Promise((resolve) => { - process.stdin.once('data', () => { - process.stdin.setRawMode(false) - resolve() - }) - }) + process.stdin.once("data", () => { + process.stdin.setRawMode(false); + resolve(); + }); + }); - return this.exit(0) + return this.exit(0); } private onTransportData(received: Buffer): void { // concat received to previous to ensure lines are outputted properly - let data = Buffer.concat([this.logBuffer, received]) - let position: number + let data = Buffer.concat([this.logBuffer, received]); + let position: number; - while ((position = data.indexOf('\r\n')) !== -1) { + while ((position = data.indexOf("\r\n")) !== -1) { // take everything up to '\r\n' (excluded) - const line = data.subarray(0, position) + const line = data.subarray(0, position); // skip blank lines if (line.length > 0) { - let asciiLine = line.toString('ascii') + let asciiLine = line.toString("ascii"); // append SLStatus at end of line if detected hex for it // - "Join network complete: 0x18" // - "Join network start: 0x0" // XXX: format seems pretty standard throughout the SDK, but this might create some false matches (hence leaving the hex too) - const endStatusMatch = asciiLine.match(/ (0x\d+)$/) + const endStatusMatch = asciiLine.match(/ (0x\d+)$/); if (endStatusMatch) { - asciiLine += ` (${SLStatus[Number.parseInt(endStatusMatch[1], 16)]})` + asciiLine += ` (${SLStatus[Number.parseInt(endStatusMatch[1], 16)]})`; } - logger.info(asciiLine) + logger.info(asciiLine); } // remove the line from internal buffer (set below), this time include '\r\n' - data = data.subarray(position + 2) + data = data.subarray(position + 2); } - this.logBuffer = data + this.logBuffer = data; } } diff --git a/src/commands/router/index.ts b/src/commands/router/index.ts index 047f86e..8269dee 100644 --- a/src/commands/router/index.ts +++ b/src/commands/router/index.ts @@ -4,24 +4,24 @@ import type { EmberMulticastId, EmberMulticastTableEntry, EmberZigbeeNetwork, -} from 'zigbee-herdsman/dist/adapter/ember/types.js' -import type { EUI64, NodeId, PanId } from 'zigbee-herdsman/dist/zspec/tstypes.js' - -import { existsSync, readFileSync, writeFileSync } from 'node:fs' -import { join } from 'node:path' -import { pathToFileURL } from 'node:url' - -import { checkbox, confirm, input, select } from '@inquirer/prompts' -import { Command } from '@oclif/core' -import { Presets, SingleBar } from 'cli-progress' -import { Logger } from 'winston' - -import { Zcl, Zdo, ZSpec } from 'zigbee-herdsman' -import { DEFAULT_STACK_CONFIG } from 'zigbee-herdsman/dist/adapter/ember/adapter/emberAdapter.js' -import { EmberTokensManager } from 'zigbee-herdsman/dist/adapter/ember/adapter/tokensManager.js' -import { EMBER_MIN_BROADCAST_ADDRESS, STACK_PROFILE_ZIGBEE_PRO } from 'zigbee-herdsman/dist/adapter/ember/consts.js' +} from "zigbee-herdsman/dist/adapter/ember/types.js"; +import type { EUI64, NodeId, PanId } from "zigbee-herdsman/dist/zspec/tstypes.js"; + +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +import { checkbox, confirm, input, select } from "@inquirer/prompts"; +import { Command } from "@oclif/core"; +import { Presets, SingleBar } from "cli-progress"; +import type { Logger } from "winston"; + +import { ZSpec, Zcl, Zdo } from "zigbee-herdsman"; +import { DEFAULT_STACK_CONFIG } from "zigbee-herdsman/dist/adapter/ember/adapter/emberAdapter.js"; +import { EmberTokensManager } from "zigbee-herdsman/dist/adapter/ember/adapter/tokensManager.js"; +import { EMBER_MIN_BROADCAST_ADDRESS, STACK_PROFILE_ZIGBEE_PRO } from "zigbee-herdsman/dist/adapter/ember/consts.js"; import { - EmberApsOption, + type EmberApsOption, EmberCounterType, EmberExtendedSecurityBitmask, EmberIncomingMessageType, @@ -32,13 +32,13 @@ import { EzspNetworkScanType, EzspStatus, SLStatus, -} from 'zigbee-herdsman/dist/adapter/ember/enums.js' -import { Ezsp } from 'zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js' -import { DataType } from 'zigbee-herdsman/dist/zspec/zcl/index.js' -import { BuffaloZdo } from 'zigbee-herdsman/dist/zspec/zdo/buffaloZdo.js' +} from "zigbee-herdsman/dist/adapter/ember/enums.js"; +import type { Ezsp } from "zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js"; +import type { DataType } from "zigbee-herdsman/dist/zspec/zcl/index.js"; +import { BuffaloZdo } from "zigbee-herdsman/dist/zspec/zdo/buffaloZdo.js"; -import { DATA_FOLDER, DEFAULT_ROUTER_SCRIPT_MJS_PATH, DEFAULT_ROUTER_TOKENS_BACKUP_PATH, logger } from '../../index.js' -import { APPLICATION_ZDO_SEQUENCE_MASK, DEFAULT_APS_OPTIONS, DEFAULT_ZDO_REQUEST_RADIUS } from '../../utils/consts.js' +import { DATA_FOLDER, DEFAULT_ROUTER_SCRIPT_MJS_PATH, DEFAULT_ROUTER_TOKENS_BACKUP_PATH, logger } from "../../index.js"; +import { APPLICATION_ZDO_SEQUENCE_MASK, DEFAULT_APS_OPTIONS, DEFAULT_ZDO_REQUEST_RADIUS } from "../../utils/consts.js"; import { emberFullVersion, emberNetworkConfig, @@ -48,10 +48,10 @@ import { emberStart, emberStop, waitForStackStatus, -} from '../../utils/ember.js' -import { getPortConf } from '../../utils/port.js' -import { ROUTER_FIXED_ENDPOINTS } from '../../utils/router-endpoints.js' -import { browseToFile, loadStackConfig, toHex } from '../../utils/utils.js' +} from "../../utils/ember.js"; +import { getPortConf } from "../../utils/port.js"; +import { ROUTER_FIXED_ENDPOINTS } from "../../utils/router-endpoints.js"; +import { browseToFile, loadStackConfig, toHex } from "../../utils/utils.js"; type CustomEventHandlers = { onIncomingMessage?: ( @@ -62,7 +62,7 @@ type CustomEventHandlers = { lastHopLqi: number, sender: NodeId, messageContents: Buffer, - ) => Promise + ) => Promise; onMessageSent?: ( cmd: Command, logger: Logger, @@ -71,8 +71,8 @@ type CustomEventHandlers = { indexOrDestination: number, apsFrame: EmberApsFrame, messageTag: number, - ) => Promise - onStackStatus?: (cmd: Command, logger: Logger, status: SLStatus) => Promise + ) => Promise; + onStackStatus?: (cmd: Command, logger: Logger, status: SLStatus) => Promise; onTouchlinkMessage?: ( cmd: Command, logger: Logger, @@ -81,11 +81,11 @@ type CustomEventHandlers = { groupId: null | number, lastHopLqi: number, messageContents: Buffer, - ) => Promise - onZDOResponse?: (cmd: Command, logger: Logger, apsFrame: EmberApsFrame, sender: NodeId, messageContents: Buffer) => Promise -} + ) => Promise; + onZDOResponse?: (cmd: Command, logger: Logger, apsFrame: EmberApsFrame, sender: NodeId, messageContents: Buffer) => Promise; +}; -const enum RouterMenu { +enum RouterMenu { NETWORK_JOIN = 0, NETWORK_REJOIN = 1, NETWORK_LEAVE = 2, @@ -111,14 +111,14 @@ enum RouterState { } export default class Router extends Command { - static override args = {} - static override description = 'Use a coordinator firmware as a router and interact with the joined network.' - static override examples = ['<%= config.bin %> <%= command.id %>'] - static override flags = {} + static override args = {}; + static override description = "Use a coordinator firmware as a router and interact with the joined network."; + static override examples = ["<%= config.bin %> <%= command.id %>"]; + static override flags = {}; - public ezsp: Ezsp | undefined - public multicastTable: EmberMulticastId[] = [] - public routerState: RouterState = RouterState.UNKNOWN + public ezsp: Ezsp | undefined; + public multicastTable: EmberMulticastId[] = []; + public routerState: RouterState = RouterState.UNKNOWN; private customEventHandlers: CustomEventHandlers = { onIncomingMessage: undefined, @@ -126,69 +126,69 @@ export default class Router extends Command { onStackStatus: undefined, onTouchlinkMessage: undefined, onZDOResponse: undefined, - } + }; - private manufacturerCode: Zcl.ManufacturerCode = Zcl.ManufacturerCode.SILICON_LABORATORIES - private stackConfig: typeof DEFAULT_STACK_CONFIG = DEFAULT_STACK_CONFIG - private zdoRequestSequence: number = 0 + private manufacturerCode: Zcl.ManufacturerCode = Zcl.ManufacturerCode.SILICON_LABORATORIES; + private stackConfig: typeof DEFAULT_STACK_CONFIG = DEFAULT_STACK_CONFIG; + private zdoRequestSequence = 0; public async run(): Promise { // const {flags} = await this.parse(Router) - const portConf = await getPortConf() - logger.debug(`Using port conf: ${JSON.stringify(portConf)}`) + const portConf = await getPortConf(); + logger.debug(`Using port conf: ${JSON.stringify(portConf)}`); - this.ezsp = await emberStart(portConf) + this.ezsp = await emberStart(portConf); - this.ezsp.on('ncpNeedsResetAndInit', (status: EzspStatus): void => { - logger.error(`Adapter needs restarting: status=${EzspStatus[status]}`) + this.ezsp.on("ncpNeedsResetAndInit", (status: EzspStatus): void => { + logger.error(`Adapter needs restarting: status=${EzspStatus[status]}`); - return this.exit(1) - }) - this.ezsp.on('incomingMessage', this.onIncomingMessage.bind(this)) - this.ezsp.on('messageSent', this.onMessageSent.bind(this)) - this.ezsp.on('stackStatus', this.onStackStatus.bind(this)) - this.ezsp.on('touchlinkMessage', this.onTouchlinkMessage.bind(this)) - this.ezsp.on('zdoResponse', this.onZDOResponse.bind(this)) + this.exit(1); + }); + this.ezsp.on("incomingMessage", this.onIncomingMessage.bind(this)); + this.ezsp.on("messageSent", this.onMessageSent.bind(this)); + this.ezsp.on("stackStatus", this.onStackStatus.bind(this)); + this.ezsp.on("touchlinkMessage", this.onTouchlinkMessage.bind(this)); + this.ezsp.on("zdoResponse", this.onZDOResponse.bind(this)); - await this.loadCustomEventHandlers() + await this.loadCustomEventHandlers(); - this.stackConfig = loadStackConfig() + this.stackConfig = loadStackConfig(); - await emberNetworkConfig(this.ezsp, this.stackConfig, this.manufacturerCode) - await emberRegisterFixedEndpoints(this.ezsp, this.multicastTable /* IN/OUT */, true) + await emberNetworkConfig(this.ezsp, this.stackConfig, this.manufacturerCode); + await emberRegisterFixedEndpoints(this.ezsp, this.multicastTable /* IN/OUT */, true); - let exit: boolean = false + let exit = false; while (!exit) { - exit = await this.navigateMenu() + exit = await this.navigateMenu(); if (exit && this.routerState === RouterState.RUNNING) { - exit = await confirm({ message: 'Router is currently running. Confirm exit?', default: false }) + exit = await confirm({ message: "Router is currently running. Confirm exit?", default: false }); } } - await emberStop(this.ezsp) + await emberStop(this.ezsp); - return this.exit(0) + return this.exit(0); } private async loadCustomEventHandlers(): Promise { for (const handler in this.customEventHandlers) { - const handlerFile = join(DATA_FOLDER, `${handler}.mjs`) + const handlerFile = join(DATA_FOLDER, `${handler}.mjs`); if (existsSync(handlerFile)) { try { - const importedScript = await import(pathToFileURL(handlerFile).toString()) + const importedScript = await import(pathToFileURL(handlerFile).toString()); - if (typeof importedScript.default !== 'function') { - throw new TypeError(`Not a function.`) + if (typeof importedScript.default !== "function") { + throw new TypeError("Not a function."); } - this.customEventHandlers[handler as keyof CustomEventHandlers] = importedScript.default + this.customEventHandlers[handler as keyof CustomEventHandlers] = importedScript.default; - logger.info(`Loaded custom handler for '${handler}'.`) + logger.info(`Loaded custom handler for '${handler}'.`); } catch (error) { - logger.error(`Failed to load custom handler for '${handler}'. ${error}`) + logger.error(`Failed to load custom handler for '${handler}'. ${error}`); } } } @@ -196,88 +196,88 @@ export default class Router extends Command { private async menuNetworkInfo(): Promise { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } - const [npStatus, nodeType, netParams] = await this.ezsp.ezspGetNetworkParameters() + const [npStatus, nodeType, netParams] = await this.ezsp.ezspGetNetworkParameters(); if (npStatus !== SLStatus.OK) { - logger.error(`Failed to get network parameters with status=${SLStatus[npStatus]}.`) - return true + logger.error(`Failed to get network parameters with status=${SLStatus[npStatus]}.`); + return true; } - const eui64 = await this.ezsp.ezspGetEui64() - const nodeId = await this.ezsp.ezspGetNodeId() + const eui64 = await this.ezsp.ezspGetEui64(); + const nodeId = await this.ezsp.ezspGetNodeId(); - logger.info(`Node ID=${toHex(nodeId)}/${nodeId} EUI64=${eui64} type=${EmberNodeType[nodeType]}.`) - logger.info(`Network parameters:`) - logger.info(` - PAN ID: ${netParams.panId} (${toHex(netParams.panId)})`) - logger.info(` - Extended PAN ID: ${netParams.extendedPanId}`) - logger.info(` - Radio Channel: ${netParams.radioChannel}`) - logger.info(` - Radio Power: ${netParams.radioTxPower} dBm`) - logger.info(` - Preferred Channels: ${ZSpec.Utils.uint32MaskToChannels(netParams.channels).join(',')}`) + logger.info(`Node ID=${toHex(nodeId)}/${nodeId} EUI64=${eui64} type=${EmberNodeType[nodeType]}.`); + logger.info("Network parameters:"); + logger.info(` - PAN ID: ${netParams.panId} (${toHex(netParams.panId)})`); + logger.info(` - Extended PAN ID: ${netParams.extendedPanId}`); + logger.info(` - Radio Channel: ${netParams.radioChannel}`); + logger.info(` - Radio Power: ${netParams.radioTxPower} dBm`); + logger.info(` - Preferred Channels: ${ZSpec.Utils.uint32MaskToChannels(netParams.channels).join(",")}`); - return false + return false; } private async menuNetworkJoin(): Promise { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } - let status = await emberNetworkInit(this.ezsp, true) - const notJoined = status === SLStatus.NOT_JOINED + let status = await emberNetworkInit(this.ezsp, true); + const notJoined = status === SLStatus.NOT_JOINED; - this.setRouterState(notJoined ? RouterState.NOT_JOINED : RouterState.UNKNOWN) + this.setRouterState(notJoined ? RouterState.NOT_JOINED : RouterState.UNKNOWN); if (!notJoined && status !== SLStatus.OK) { - logger.error(`Failed network init request with status=${SLStatus[status]}.`) - return true + logger.error(`Failed network init request with status=${SLStatus[status]}.`); + return true; } if (!notJoined) { - const overwrite = await confirm({ default: false, message: 'A network is present in the adapter. Leave and continue join?' }) + const overwrite = await confirm({ default: false, message: "A network is present in the adapter. Leave and continue join?" }); if (!overwrite) { - logger.info(`Join cancelled.`) - return false + logger.info("Join cancelled."); + return false; } - const status = await this.ezsp.ezspLeaveNetwork() + const status = await this.ezsp.ezspLeaveNetwork(); if (status !== SLStatus.OK) { - logger.error(`Failed to leave network with status=${SLStatus[status]}.`) - return true + logger.error(`Failed to leave network with status=${SLStatus[status]}.`); + return true; } - await waitForStackStatus(this.ezsp, SLStatus.NETWORK_DOWN) + await waitForStackStatus(this.ezsp, SLStatus.NETWORK_DOWN); } // set desired tx power before scan const radioTxPower = Number.parseInt( await input({ - default: '5', - message: 'Radio transmit power [-128-127]', + default: "5", + message: "Radio transmit power [-128-127]", validate(value) { if (/\./.test(value)) { - return false + return false; } - const v = Number.parseInt(value, 10) + const v = Number.parseInt(value, 10); - return v >= -128 && v <= 127 + return v >= -128 && v <= 127; }, }), 10, - ) + ); - status = await this.ezsp.ezspSetRadioPower(radioTxPower) + status = await this.ezsp.ezspSetRadioPower(radioTxPower); if (status !== SLStatus.OK) { - logger.error(`Failed to set transmit power to '${radioTxPower}' status=${SLStatus[status]}.`) - return true + logger.error(`Failed to set transmit power to '${radioTxPower}' status=${SLStatus[status]}.`); + return true; } const channels = await checkbox({ @@ -286,76 +286,76 @@ export default class Router extends Command { value: c, checked: ZSpec.PREFERRED_802_15_4_CHANNELS.includes(c), })), - message: 'Channels to scan', + message: "Channels to scan", required: true, - }) + }); - const progressBar = new SingleBar({ clearOnComplete: true, format: '{bar} {percentage}% | ETA: {eta}s' }, Presets.shades_classic) - const duration = 4 + const progressBar = new SingleBar({ clearOnComplete: true, format: "{bar} {percentage}% | ETA: {eta}s" }, Presets.shades_classic); + const duration = 4; // a symbol is 16 microseconds, a scan period is 960 symbols - const totalTime = (((2 ** duration + 1) * (16 * 960)) / 1000) * channels.length - let scanCompleted: (value: PromiseLike | void) => void - const joinableNetworks: { networkFound: EmberZigbeeNetwork; lastHopLqi: number; lastHopRssi: number }[] = [] + const totalTime = (((2 ** duration + 1) * (16 * 960)) / 1000) * channels.length; + let scanCompleted: (value: PromiseLike | void) => void; + const joinableNetworks: { networkFound: EmberZigbeeNetwork; lastHopLqi: number; lastHopRssi: number }[] = []; // NOTE: expanding zigbee-herdsman - const ezspNetworkFoundHandlerOriginal = this.ezsp.ezspNetworkFoundHandler - const ezspScanCompleteHandlerOriginal = this.ezsp.ezspScanCompleteHandler + const ezspNetworkFoundHandlerOriginal = this.ezsp.ezspNetworkFoundHandler; + const ezspScanCompleteHandlerOriginal = this.ezsp.ezspScanCompleteHandler; this.ezsp.ezspNetworkFoundHandler = (networkFound: EmberZigbeeNetwork, lastHopLqi: number, lastHopRssi: number): void => { - logger.debug(`ezspNetworkFoundHandler: ${JSON.stringify({ networkFound, lastHopLqi, lastHopRssi })}`) + logger.debug(`ezspNetworkFoundHandler: ${JSON.stringify({ networkFound, lastHopLqi, lastHopRssi })}`); // don't want networks we can't join or wrong profile if (networkFound.allowingJoin && networkFound.stackProfile === STACK_PROFILE_ZIGBEE_PRO) { - joinableNetworks.push({ networkFound, lastHopLqi, lastHopRssi }) + joinableNetworks.push({ networkFound, lastHopLqi, lastHopRssi }); } - } + }; this.ezsp.ezspScanCompleteHandler = (channel: number, status: SLStatus): void => { - logger.debug(`ezspScanCompleteHandler: ${JSON.stringify({ channel, status })}`) - progressBar.stop() - clearInterval(progressInterval) + logger.debug(`ezspScanCompleteHandler: ${JSON.stringify({ channel, status })}`); + progressBar.stop(); + clearInterval(progressInterval); if (status === SLStatus.OK) { if (scanCompleted) { - scanCompleted() + scanCompleted(); } } else { - logger.error(`Failed to scan ${channel} with status=${SLStatus[status]}.`) + logger.error(`Failed to scan ${channel} with status=${SLStatus[status]}.`); } - } + }; - const startScanStatus = await this.ezsp.ezspStartScan(EzspNetworkScanType.ACTIVE_SCAN, ZSpec.Utils.channelsToUInt32Mask(channels), duration) + const startScanStatus = await this.ezsp.ezspStartScan(EzspNetworkScanType.ACTIVE_SCAN, ZSpec.Utils.channelsToUInt32Mask(channels), duration); if (startScanStatus !== SLStatus.OK) { - logger.error(`Failed start scan request with status=${SLStatus[startScanStatus]}.`) + logger.error(`Failed start scan request with status=${SLStatus[startScanStatus]}.`); // restore zigbee-herdsman default - this.ezsp.ezspNetworkFoundHandler = ezspNetworkFoundHandlerOriginal - this.ezsp.ezspScanCompleteHandler = ezspScanCompleteHandlerOriginal - return true + this.ezsp.ezspNetworkFoundHandler = ezspNetworkFoundHandlerOriginal; + this.ezsp.ezspScanCompleteHandler = ezspScanCompleteHandlerOriginal; + return true; } - progressBar.start(totalTime, 0) + progressBar.start(totalTime, 0); const progressInterval = setInterval(() => { - progressBar.increment(500) - }, 500) + progressBar.increment(500); + }, 500); await new Promise((resolve) => { - scanCompleted = resolve - }) + scanCompleted = resolve; + }); // restore zigbee-herdsman default - this.ezsp.ezspNetworkFoundHandler = ezspNetworkFoundHandlerOriginal - this.ezsp.ezspScanCompleteHandler = ezspScanCompleteHandlerOriginal + this.ezsp.ezspNetworkFoundHandler = ezspNetworkFoundHandlerOriginal; + this.ezsp.ezspScanCompleteHandler = ezspScanCompleteHandlerOriginal; if (joinableNetworks.length === 0) { - logger.error(`Found no network available to join.`) - return false + logger.error("Found no network available to join."); + return false; } // sort network found by RSSI - joinableNetworks.sort((a, b) => b.lastHopLqi - a.lastHopLqi) + joinableNetworks.sort((a, b) => b.lastHopLqi - a.lastHopLqi); - const networkChoices = [] + const networkChoices = []; for (const { networkFound, lastHopLqi, lastHopRssi } of joinableNetworks) { networkChoices.push({ @@ -363,11 +363,11 @@ export default class Router extends Command { `PAN ID: ${networkFound.panId} | Ext PAN ID: ${networkFound.extendedPanId} | ` + `Channel: ${networkFound.channel} | LQI: ${lastHopLqi} | RSSI: ${lastHopRssi}`, value: networkFound, - }) + }); } - const networkToJoin = await select({ choices: networkChoices, message: 'Available networks' }) - const defaultLinkKey = Buffer.from(ZSpec.INTEROPERABILITY_LINK_KEY) + const networkToJoin = await select({ choices: networkChoices, message: "Available networks" }); + const defaultLinkKey = Buffer.from(ZSpec.INTEROPERABILITY_LINK_KEY); const state: EmberInitialSecurityState = { bitmask: EmberInitialSecurityBitmask.TRUST_CENTER_GLOBAL_LINK_KEY | @@ -378,35 +378,35 @@ export default class Router extends Command { networkKey: { contents: Buffer.alloc(16) }, // blank networkKeySequenceNumber: 0, preconfiguredTrustCenterEui64: ZSpec.BLANK_EUI64, - } + }; - status = await this.ezsp.ezspSetInitialSecurityState(state) + status = await this.ezsp.ezspSetInitialSecurityState(state); if (status !== SLStatus.OK) { - logger.error(`Failed to set initial security state with status=${SLStatus[status]}.`) - return true + logger.error(`Failed to set initial security state with status=${SLStatus[status]}.`); + return true; } const extended: EmberExtendedSecurityBitmask = - EmberExtendedSecurityBitmask.JOINER_GLOBAL_LINK_KEY | EmberExtendedSecurityBitmask.EXT_NO_FRAME_COUNTER_RESET - status = await this.ezsp.ezspSetExtendedSecurityBitmask(extended) + EmberExtendedSecurityBitmask.JOINER_GLOBAL_LINK_KEY | EmberExtendedSecurityBitmask.EXT_NO_FRAME_COUNTER_RESET; + status = await this.ezsp.ezspSetExtendedSecurityBitmask(extended); if (status !== SLStatus.OK) { - logger.error(`Failed to set extended security bitmask to ${extended} with status=${SLStatus[status]}.`) - return true + logger.error(`Failed to set extended security bitmask to ${extended} with status=${SLStatus[status]}.`); + return true; } - status = await this.ezsp.ezspClearKeyTable() + status = await this.ezsp.ezspClearKeyTable(); if (status !== SLStatus.OK) { - logger.error(`Failed to clear key table with status=${SLStatus[status]}.`) + logger.error(`Failed to clear key table with status=${SLStatus[status]}.`); } - status = await this.ezsp.ezspImportTransientKey(ZSpec.BLANK_EUI64, { contents: defaultLinkKey }) + status = await this.ezsp.ezspImportTransientKey(ZSpec.BLANK_EUI64, { contents: defaultLinkKey }); if (status !== SLStatus.OK) { - logger.error(`Failed to import transient key with status=${SLStatus[status]}.`) - return true + logger.error(`Failed to import transient key with status=${SLStatus[status]}.`); + return true; } status = await this.ezsp.ezspJoinNetwork(EmberNodeType.ROUTER, { @@ -418,134 +418,134 @@ export default class Router extends Command { nwkManagerId: 0, nwkUpdateId: networkToJoin.nwkUpdateId, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, - }) + }); if (status !== SLStatus.OK) { - logger.error(`Failed to join specified network with status=${SLStatus[status]}.`) - return true + logger.error(`Failed to join specified network with status=${SLStatus[status]}.`); + return true; } - await waitForStackStatus(this.ezsp, SLStatus.NETWORK_UP) - await emberSetConcentrator(this.ezsp, this.stackConfig) - this.setRouterState(RouterState.RUNNING) + await waitForStackStatus(this.ezsp, SLStatus.NETWORK_UP); + await emberSetConcentrator(this.ezsp, this.stackConfig); + this.setRouterState(RouterState.RUNNING); - const permitJoining = await confirm({ default: true, message: 'Permit joining to extend network?' }) + const permitJoining = await confirm({ default: true, message: "Permit joining to extend network?" }); if (permitJoining) { - const [status] = await this.permitJoining(180, true) + const [status] = await this.permitJoining(180, true); if (status !== SLStatus.OK) { - logger.error(`Failed to permit joining with status=${SLStatus[status]}.`) + logger.error(`Failed to permit joining with status=${SLStatus[status]}.`); } } - return false + return false; } private async menuNetworkLeave(): Promise { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } const confirmed = await confirm({ default: false, - message: 'Confirm leave network? (Cannot be undone without a backup.)', - }) + message: "Confirm leave network? (Cannot be undone without a backup.)", + }); if (!confirmed) { - logger.info(`Network leave cancelled.`) - return false + logger.info("Network leave cancelled."); + return false; } if (this.routerState !== RouterState.RUNNING) { - const initStatus = await emberNetworkInit(this.ezsp, true) + const initStatus = await emberNetworkInit(this.ezsp, true); if (initStatus === SLStatus.NOT_JOINED) { - logger.info(`No network present.`) - return false + logger.info("No network present."); + return false; } if (initStatus !== SLStatus.OK) { - logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`) - return true + logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`); + return true; } - await waitForStackStatus(this.ezsp, SLStatus.NETWORK_UP) + await waitForStackStatus(this.ezsp, SLStatus.NETWORK_UP); // NOTE: explicitly not set since we don't want to consider this "running" // this.setRouterState(RouterState.RUNNING) } - const leaveStatus = await this.ezsp.ezspLeaveNetwork() + const leaveStatus = await this.ezsp.ezspLeaveNetwork(); if (leaveStatus !== SLStatus.OK) { - logger.error(`Failed to leave network with status=${SLStatus[leaveStatus]}.`) - return true + logger.error(`Failed to leave network with status=${SLStatus[leaveStatus]}.`); + return true; } - await waitForStackStatus(this.ezsp, SLStatus.NETWORK_DOWN) - this.setRouterState(RouterState.NOT_JOINED) - logger.info(`Left network.`) + await waitForStackStatus(this.ezsp, SLStatus.NETWORK_DOWN); + this.setRouterState(RouterState.NOT_JOINED); + logger.info("Left network."); - return false + return false; } private async menuNetworkRejoin(): Promise { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } - const initStatus = await emberNetworkInit(this.ezsp, true) - const notJoined = initStatus === SLStatus.NOT_JOINED + const initStatus = await emberNetworkInit(this.ezsp, true); + const notJoined = initStatus === SLStatus.NOT_JOINED; - this.setRouterState(notJoined ? RouterState.NOT_JOINED : RouterState.UNKNOWN) + this.setRouterState(notJoined ? RouterState.NOT_JOINED : RouterState.UNKNOWN); if (!notJoined && initStatus !== SLStatus.OK) { - logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`) - return true + logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`); + return true; } if (notJoined) { - logger.info(`No network present in the adapter, cannot rejoin.`) - return false + logger.info("No network present in the adapter, cannot rejoin."); + return false; } - await waitForStackStatus(this.ezsp, SLStatus.NETWORK_UP) - await emberSetConcentrator(this.ezsp, this.stackConfig) + await waitForStackStatus(this.ezsp, SLStatus.NETWORK_UP); + await emberSetConcentrator(this.ezsp, this.stackConfig); - const [npStatus, nodeType, netParams] = await this.ezsp.ezspGetNetworkParameters() + const [npStatus, nodeType, netParams] = await this.ezsp.ezspGetNetworkParameters(); if (npStatus !== SLStatus.OK) { - logger.error(`Failed to get network parameters with status=${SLStatus[npStatus]}.`) - return true + logger.error(`Failed to get network parameters with status=${SLStatus[npStatus]}.`); + return true; } if (nodeType !== EmberNodeType.ROUTER) { - logger.error(`Current network is not router: nodeType=${EmberNodeType[nodeType]}`) - return true + logger.error(`Current network is not router: nodeType=${EmberNodeType[nodeType]}`); + return true; } - logger.info(`Current adapter network: ${JSON.stringify(netParams)}`) - this.setRouterState(RouterState.RUNNING) + logger.info(`Current adapter network: ${JSON.stringify(netParams)}`); + this.setRouterState(RouterState.RUNNING); - const permitJoining = await confirm({ default: true, message: 'Permit joining to extend network?' }) + const permitJoining = await confirm({ default: true, message: "Permit joining to extend network?" }); if (permitJoining) { - const [status] = await this.permitJoining(180, true) + const [status] = await this.permitJoining(180, true); if (status !== SLStatus.OK) { - logger.error(`Failed to permit joining with status=${SLStatus[status]}.`) + logger.error(`Failed to permit joining with status=${SLStatus[status]}.`); } } - return false + return false; } private async menuPingCoordinator(): Promise { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } const [status] = await this.ezsp.send( @@ -561,74 +561,74 @@ export default class Router extends Command { sequence: 0, }, // type 'readResponse', cluster 'genBasic', data '{"zclVersion":8}' - Buffer.from('1801010000002008', 'hex'), + Buffer.from("1801010000002008", "hex"), 0, 0, - ) + ); if (status !== SLStatus.OK) { - logger.error(`Failed to ping coordinator with status=${SLStatus[status]}.`) + logger.error(`Failed to ping coordinator with status=${SLStatus[status]}.`); } - return false + return false; } private async menuReadCounters(): Promise { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } - const alsoClear = await confirm({ message: 'Clear counters after read?', default: true }) - const counters = alsoClear ? await this.ezsp.ezspReadAndClearCounters() : await this.ezsp.ezspReadCounters() + const alsoClear = await confirm({ message: "Clear counters after read?", default: true }); + const counters = alsoClear ? await this.ezsp.ezspReadAndClearCounters() : await this.ezsp.ezspReadCounters(); for (let i = 0; i < EmberCounterType.COUNT; i++) { - logger.info(`Counter ${EmberCounterType[i]}=${counters[i]}`) + logger.info(`Counter ${EmberCounterType[i]}=${counters[i]}`); } - return false + return false; } private async menuReloadEventHandlers(): Promise { - await this.loadCustomEventHandlers() + await this.loadCustomEventHandlers(); - return false + return false; } private async menuRunScript(): Promise { - const jsFile = await browseToFile('File to run', DEFAULT_ROUTER_SCRIPT_MJS_PATH) + const jsFile = await browseToFile("File to run", DEFAULT_ROUTER_SCRIPT_MJS_PATH); try { - const scriptToRun = await import(pathToFileURL(jsFile).toString()) + const scriptToRun = await import(pathToFileURL(jsFile).toString()); - scriptToRun.default(this, logger) + scriptToRun.default(this, logger); } catch (error) { - logger.error(error) + logger.error(error); } - return false + return false; } private async menuSetManufacturerCode(): Promise { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } - const enum Source { + enum Source { ZCL_LIST = 0, INPUT = 1, } const source = await select<-1 | Source>({ choices: [ - { name: 'From ZCL list (long)', value: Source.ZCL_LIST }, - { name: 'From manual input', value: Source.INPUT }, - { name: 'Go Back', value: -1 }, + { name: "From ZCL list (long)", value: Source.ZCL_LIST }, + { name: "From manual input", value: Source.INPUT }, + { name: "Go Back", value: -1 }, ], - message: 'Source for the manufacturer code', - }) + message: "Source for the manufacturer code", + }); - let newCode: Zcl.ManufacturerCode = this.manufacturerCode + let newCode: Zcl.ManufacturerCode = this.manufacturerCode; switch (source) { case Source.ZCL_LIST: { @@ -637,189 +637,189 @@ export default class Router extends Command { name: k, value: Zcl.ManufacturerCode[k as keyof typeof Zcl.ManufacturerCode], })), - message: 'Select manufacturer', - }) + message: "Select manufacturer", + }); - break + break; } case Source.INPUT: { newCode = Number.parseInt( await input({ default: Zcl.ManufacturerCode.SILICON_LABORATORIES.toString(), - message: 'Code [0-65535/0x0000-0xFFFF]', + message: "Code [0-65535/0x0000-0xFFFF]", validate(value) { if (/\./.test(value)) { - return false + return false; } - const v = Number.parseInt(value, value.startsWith('0x') ? 16 : 10) - return v >= 0 && v <= 65535 + const v = Number.parseInt(value, value.startsWith("0x") ? 16 : 10); + return v >= 0 && v <= 65535; }, }), 10, - ) + ); - break + break; } case -1: { - return false + return false; } } - this.manufacturerCode = newCode + this.manufacturerCode = newCode; - await this.ezsp.ezspSetManufacturerCode(newCode) + await this.ezsp.ezspSetManufacturerCode(newCode); - return false + return false; } private async menuTokensBackup(): Promise { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } - const saveFile = await browseToFile('Router tokens backup save file', DEFAULT_ROUTER_TOKENS_BACKUP_PATH, true) - const eui64 = await this.ezsp.ezspGetEui64() - const tokensBuf = await EmberTokensManager.saveTokens(this.ezsp, Buffer.from(eui64.slice(2 /* 0x */), 'hex').reverse()) + const saveFile = await browseToFile("Router tokens backup save file", DEFAULT_ROUTER_TOKENS_BACKUP_PATH, true); + const eui64 = await this.ezsp.ezspGetEui64(); + const tokensBuf = await EmberTokensManager.saveTokens(this.ezsp, Buffer.from(eui64.slice(2 /* 0x */), "hex").reverse()); if (tokensBuf) { - writeFileSync(saveFile, tokensBuf.toString('hex'), 'utf8') + writeFileSync(saveFile, tokensBuf.toString("hex"), "utf8"); - logger.info(`Tokens backup written to '${saveFile}'.`) + logger.info(`Tokens backup written to '${saveFile}'.`); } else { - logger.error(`Failed to backup tokens.`) + logger.error("Failed to backup tokens."); } - return false + return false; } private async menuTokensReset(): Promise { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } const confirmed = await confirm({ default: false, - message: 'Confirm tokens reset? (Cannot be undone without a backup.)', - }) + message: "Confirm tokens reset? (Cannot be undone without a backup.)", + }); if (!confirmed) { - logger.info(`Tokens reset cancelled.`) - return false + logger.info("Tokens reset cancelled."); + return false; } const options = await checkbox({ choices: [ - { name: 'Exclude network and APS outgoing frame counter tokens?', value: 'excludeOutgoingFC', checked: false }, - { name: 'Exclude stack boot counter token?', value: 'excludeBootCounter', checked: false }, + { name: "Exclude network and APS outgoing frame counter tokens?", value: "excludeOutgoingFC", checked: false }, + { name: "Exclude stack boot counter token?", value: "excludeBootCounter", checked: false }, ], - message: 'Reset options', - }) + message: "Reset options", + }); - await this.ezsp.ezspTokenFactoryReset(options.includes('excludeOutgoingFC'), options.includes('excludeBootCounter')) + await this.ezsp.ezspTokenFactoryReset(options.includes("excludeOutgoingFC"), options.includes("excludeBootCounter")); - return true + return true; } private async menuTokensRestore(): Promise { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } - const backupFile = await browseToFile('Router tokens backup file location', DEFAULT_ROUTER_TOKENS_BACKUP_PATH) - const tokensBuf = Buffer.from(readFileSync(backupFile, 'utf8'), 'hex') - const status = await EmberTokensManager.restoreTokens(this.ezsp, tokensBuf) + const backupFile = await browseToFile("Router tokens backup file location", DEFAULT_ROUTER_TOKENS_BACKUP_PATH); + const tokensBuf = Buffer.from(readFileSync(backupFile, "utf8"), "hex"); + const status = await EmberTokensManager.restoreTokens(this.ezsp, tokensBuf); if (status === SLStatus.OK) { - logger.info(`Restored router tokens.`) + logger.info("Restored router tokens."); } else { - logger.error(`Failed to restore router tokens.`) + logger.error("Failed to restore router tokens."); } - return true + return true; } private async navigateMenu(): Promise { - const notRunning = this.routerState !== RouterState.RUNNING + const notRunning = this.routerState !== RouterState.RUNNING; const answer = await select<-1 | RouterMenu>({ choices: [ - { name: 'Join network', value: RouterMenu.NETWORK_JOIN }, + { name: "Join network", value: RouterMenu.NETWORK_JOIN }, { - name: 'Rejoin network', + name: "Rejoin network", value: RouterMenu.NETWORK_REJOIN, disabled: this.routerState === RouterState.NOT_JOINED || this.routerState !== RouterState.UNKNOWN, }, - { name: 'Leave network', value: RouterMenu.NETWORK_LEAVE }, - { name: 'Backup NVM3 tokens', value: RouterMenu.TOKENS_BACKUP }, - { name: 'Restore NVM3 tokens', value: RouterMenu.TOKENS_RESTORE }, - { name: 'Reset NVM3 tokens', value: RouterMenu.TOKENS_RESET }, - { name: 'Get network info', value: RouterMenu.NETWORK_INFO, disabled: notRunning }, - { name: 'Set manufacturer code', value: RouterMenu.SET_MANUFACTURER_CODE, disabled: notRunning }, - { name: 'Read counters', value: RouterMenu.PING_COORDINATOR, disabled: notRunning }, - { name: 'Ping coordinator', value: RouterMenu.READ_COUNTERS, disabled: notRunning }, - { name: 'Reload custom event handlers', value: RouterMenu.RELOAD_EVENT_HANDLERS }, - { name: 'Run custom script', value: RouterMenu.RUN_SCRIPT, disabled: notRunning }, - { name: 'Exit', value: -1 }, + { name: "Leave network", value: RouterMenu.NETWORK_LEAVE }, + { name: "Backup NVM3 tokens", value: RouterMenu.TOKENS_BACKUP }, + { name: "Restore NVM3 tokens", value: RouterMenu.TOKENS_RESTORE }, + { name: "Reset NVM3 tokens", value: RouterMenu.TOKENS_RESET }, + { name: "Get network info", value: RouterMenu.NETWORK_INFO, disabled: notRunning }, + { name: "Set manufacturer code", value: RouterMenu.SET_MANUFACTURER_CODE, disabled: notRunning }, + { name: "Read counters", value: RouterMenu.PING_COORDINATOR, disabled: notRunning }, + { name: "Ping coordinator", value: RouterMenu.READ_COUNTERS, disabled: notRunning }, + { name: "Reload custom event handlers", value: RouterMenu.RELOAD_EVENT_HANDLERS }, + { name: "Run custom script", value: RouterMenu.RUN_SCRIPT, disabled: notRunning }, + { name: "Exit", value: -1 }, ], - message: 'Menu', - }) + message: "Menu", + }); switch (answer) { case RouterMenu.NETWORK_JOIN: { - return await this.menuNetworkJoin() + return await this.menuNetworkJoin(); } case RouterMenu.NETWORK_REJOIN: { - return await this.menuNetworkRejoin() + return await this.menuNetworkRejoin(); } case RouterMenu.NETWORK_LEAVE: { - return await this.menuNetworkLeave() + return await this.menuNetworkLeave(); } case RouterMenu.TOKENS_BACKUP: { - return await this.menuTokensBackup() + return await this.menuTokensBackup(); } case RouterMenu.TOKENS_RESTORE: { - return await this.menuTokensRestore() + return await this.menuTokensRestore(); } case RouterMenu.TOKENS_RESET: { - return await this.menuTokensReset() + return await this.menuTokensReset(); } case RouterMenu.NETWORK_INFO: { - return await this.menuNetworkInfo() + return await this.menuNetworkInfo(); } case RouterMenu.SET_MANUFACTURER_CODE: { - return await this.menuSetManufacturerCode() + return await this.menuSetManufacturerCode(); } case RouterMenu.READ_COUNTERS: { - return await this.menuReadCounters() + return await this.menuReadCounters(); } case RouterMenu.PING_COORDINATOR: { - return await this.menuPingCoordinator() + return await this.menuPingCoordinator(); } case RouterMenu.RELOAD_EVENT_HANDLERS: { - return await this.menuReloadEventHandlers() + return await this.menuReloadEventHandlers(); } case RouterMenu.RUN_SCRIPT: { - return await this.menuRunScript() + return await this.menuRunScript(); } } - return true // exit + return true; // exit } private async onIncomingMessage( @@ -837,71 +837,70 @@ export default class Router extends Command { apsFrame.destinationEndpoint === ROUTER_FIXED_ENDPOINTS[0].endpoint && apsFrame.sourceEndpoint === ROUTER_FIXED_ENDPOINTS[0].endpoint ) { - const header = Zcl.Header.fromBuffer(messageContents) + const header = Zcl.Header.fromBuffer(messageContents); if ( - header && - header.isGlobal && + header?.isGlobal && header.frameControl.direction === Zcl.Direction.CLIENT_TO_SERVER && header.commandIdentifier === Zcl.Foundation.read.ID ) { // handle replying to Z2M interview + ping attribute reads - const frame = Zcl.Frame.fromBuffer(apsFrame.clusterId, header, messageContents, {}) + const frame = Zcl.Frame.fromBuffer(apsFrame.clusterId, header, messageContents, {}); const replyPayload: { attrId: number; status: Zcl.Status; dataType?: DataType; attrData?: number | string } = { attrId: frame.payload[0].attrId, status: Zcl.Status.SUCCESS, dataType: undefined, attrData: undefined, - } + }; switch (replyPayload.attrId) { case Zcl.Clusters.genBasic.attributes.zclVersion.ID: { - replyPayload.dataType = Zcl.Clusters.genBasic.attributes.zclVersion.type - replyPayload.attrData = 8 // DataType.UINT8 - break + replyPayload.dataType = Zcl.Clusters.genBasic.attributes.zclVersion.type; + replyPayload.attrData = 8; // DataType.UINT8 + break; } case Zcl.Clusters.genBasic.attributes.appVersion.ID: { - replyPayload.dataType = Zcl.Clusters.genBasic.attributes.appVersion.type - replyPayload.attrData = emberFullVersion.ezsp // DataType.UINT8 - break + replyPayload.dataType = Zcl.Clusters.genBasic.attributes.appVersion.type; + replyPayload.attrData = emberFullVersion.ezsp; // DataType.UINT8 + break; } case Zcl.Clusters.genBasic.attributes.stackVersion.ID: { - replyPayload.dataType = Zcl.Clusters.genBasic.attributes.stackVersion.type - replyPayload.attrData = emberFullVersion.major // DataType.UINT8 - break + replyPayload.dataType = Zcl.Clusters.genBasic.attributes.stackVersion.type; + replyPayload.attrData = emberFullVersion.major; // DataType.UINT8 + break; } case Zcl.Clusters.genBasic.attributes.manufacturerName.ID: { - replyPayload.dataType = Zcl.Clusters.genBasic.attributes.manufacturerName.type - replyPayload.attrData = Zcl.ManufacturerCode[this.manufacturerCode] // DataType.CHAR_STR - break + replyPayload.dataType = Zcl.Clusters.genBasic.attributes.manufacturerName.type; + replyPayload.attrData = Zcl.ManufacturerCode[this.manufacturerCode]; // DataType.CHAR_STR + break; } case Zcl.Clusters.genBasic.attributes.modelId.ID: { - replyPayload.dataType = Zcl.Clusters.genBasic.attributes.modelId.type - replyPayload.attrData = 'Ember ZLI Router' // DataType.CHAR_STR - break + replyPayload.dataType = Zcl.Clusters.genBasic.attributes.modelId.type; + replyPayload.attrData = "Ember ZLI Router"; // DataType.CHAR_STR + break; } case Zcl.Clusters.genBasic.attributes.dateCode.ID: { - replyPayload.dataType = Zcl.Clusters.genBasic.attributes.dateCode.type - replyPayload.attrData = emberFullVersion.revision // DataType.CHAR_STR - break + replyPayload.dataType = Zcl.Clusters.genBasic.attributes.dateCode.type; + replyPayload.attrData = emberFullVersion.revision; // DataType.CHAR_STR + break; } case Zcl.Clusters.genBasic.attributes.powerSource.ID: { - replyPayload.dataType = Zcl.Clusters.genBasic.attributes.powerSource.type + replyPayload.dataType = Zcl.Clusters.genBasic.attributes.powerSource.type; // Mains - replyPayload.attrData = 1 // DataType.ENUM8 - break + replyPayload.attrData = 1; // DataType.ENUM8 + break; } case Zcl.Clusters.genBasic.attributes.swBuildId.ID: { - replyPayload.dataType = Zcl.Clusters.genBasic.attributes.swBuildId.type - replyPayload.attrData = emberFullVersion.revision // DataType.CHAR_STR - break + replyPayload.dataType = Zcl.Clusters.genBasic.attributes.swBuildId.type; + replyPayload.attrData = emberFullVersion.revision; // DataType.CHAR_STR + break; } } @@ -916,23 +915,23 @@ export default class Router extends Command { Zcl.Clusters.genBasic.ID, [replyPayload], // repetitive strategy, wrap in array {}, - ) + ); logger.debug( `~~~> [ZCL to=${ZSpec.COORDINATOR_ADDRESS} apsFrame=${JSON.stringify(apsFrame)} header=${JSON.stringify(zclFrame.header)}]`, - ) + ); try { - await this.ezsp!.send(EmberOutgoingMessageType.DIRECT, ZSpec.COORDINATOR_ADDRESS, apsFrame, zclFrame.toBuffer(), 0, 0) + await this.ezsp!.send(EmberOutgoingMessageType.DIRECT, ZSpec.COORDINATOR_ADDRESS, apsFrame, zclFrame.toBuffer(), 0, 0); } catch (error) { - logger.debug(error) + logger.debug(error); } } } } if (this.customEventHandlers.onIncomingMessage) { - await this.customEventHandlers.onIncomingMessage(this, logger, type, apsFrame, lastHopLqi, sender, messageContents) + await this.customEventHandlers.onIncomingMessage(this, logger, type, apsFrame, lastHopLqi, sender, messageContents); } } @@ -948,9 +947,9 @@ export default class Router extends Command { // no ACK was received from the destination logger.error( `Delivery of ${EmberOutgoingMessageType[type]} failed for '${indexOrDestination}' [apsFrame=${JSON.stringify(apsFrame)} messageTag=${messageTag}]`, - ) + ); - break + break; } case SLStatus.OK: { @@ -961,49 +960,49 @@ export default class Router extends Command { !this.multicastTable.includes(apsFrame.groupId) ) { // workaround for devices using multicast for state update (coordinator passthrough) - const tableIdx = this.multicastTable.length + const tableIdx = this.multicastTable.length; const multicastEntry: EmberMulticastTableEntry = { multicastId: apsFrame.groupId, endpoint: ROUTER_FIXED_ENDPOINTS[0].endpoint, networkIndex: ROUTER_FIXED_ENDPOINTS[0].networkIndex, - } + }; // set immediately to avoid potential race - this.multicastTable.push(multicastEntry.multicastId) + this.multicastTable.push(multicastEntry.multicastId); try { - const status = await this.ezsp!.ezspSetMulticastTableEntry(tableIdx, multicastEntry) + const status = await this.ezsp!.ezspSetMulticastTableEntry(tableIdx, multicastEntry); if (status !== SLStatus.OK) { throw new Error( `Failed to register group '${multicastEntry.multicastId}' in multicast table with status=${SLStatus[status]}.`, - ) + ); } - logger.debug(`Registered multicast table entry (${tableIdx}): ${JSON.stringify(multicastEntry)}.`) + logger.debug(`Registered multicast table entry (${tableIdx}): ${JSON.stringify(multicastEntry)}.`); } catch (error) { // remove to allow retry on next occurrence - this.multicastTable.splice(tableIdx, 1) - logger.error(`${error}`) + this.multicastTable.splice(tableIdx, 1); + logger.error(`${error}`); } } - break + break; } } // shouldn't be any other status if (this.customEventHandlers.onMessageSent) { - await this.customEventHandlers.onMessageSent(this, logger, status, type, indexOrDestination, apsFrame, messageTag) + await this.customEventHandlers.onMessageSent(this, logger, status, type, indexOrDestination, apsFrame, messageTag); } } private async onStackStatus(status: SLStatus): Promise { if (status === SLStatus.NETWORK_DOWN) { - this.setRouterState(RouterState.NOT_JOINED) + this.setRouterState(RouterState.NOT_JOINED); } if (this.customEventHandlers.onStackStatus) { - await this.customEventHandlers.onStackStatus(this, logger, status) + await this.customEventHandlers.onStackStatus(this, logger, status); } } @@ -1015,13 +1014,13 @@ export default class Router extends Command { messageContents: Buffer, ): Promise { if (this.customEventHandlers.onTouchlinkMessage) { - await this.customEventHandlers.onTouchlinkMessage(this, logger, sourcePanId, sourceAddress, groupId, lastHopLqi, messageContents) + await this.customEventHandlers.onTouchlinkMessage(this, logger, sourcePanId, sourceAddress, groupId, lastHopLqi, messageContents); } } private async onZDOResponse(apsFrame: EmberApsFrame, sender: NodeId, messageContents: Buffer): Promise { if (this.customEventHandlers.onZDOResponse) { - await this.customEventHandlers.onZDOResponse(this, logger, apsFrame, sender, messageContents) + await this.customEventHandlers.onZDOResponse(this, logger, apsFrame, sender, messageContents); } } @@ -1030,28 +1029,28 @@ export default class Router extends Command { broadcastMgmtPermitJoin: boolean, ): Promise<[SLStatus, apsFrame: EmberApsFrame | undefined, messageTag: number | undefined]> { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } - let status = await this.ezsp.ezspPermitJoining(duration) - let apsFrame: EmberApsFrame | undefined - let messageTag: number | undefined + let status = await this.ezsp.ezspPermitJoining(duration); + let apsFrame: EmberApsFrame | undefined; + let messageTag: number | undefined; - logger.debug(`Permit joining for ${duration} sec. status=${[status]}`) + logger.debug(`Permit joining for ${duration} sec. status=${[status]}`); if (broadcastMgmtPermitJoin) { // `authentication`: TC significance always 1 (zb specs) - const zdoPayload = BuffaloZdo.buildRequest(true, Zdo.ClusterId.PERMIT_JOINING_REQUEST, duration, 1, []) - ;[status, apsFrame, messageTag] = await this.sendZDORequest( + const zdoPayload = BuffaloZdo.buildRequest(true, Zdo.ClusterId.PERMIT_JOINING_REQUEST, duration, 1, []); + [status, apsFrame, messageTag] = await this.sendZDORequest( ZSpec.BroadcastAddress.DEFAULT, Zdo.ClusterId.PERMIT_JOINING_REQUEST, zdoPayload, DEFAULT_APS_OPTIONS, - ) + ); } - return [status, apsFrame, messageTag] + return [status, apsFrame, messageTag]; } private async sendZDORequest( @@ -1061,13 +1060,13 @@ export default class Router extends Command { options: EmberApsOption, ): Promise<[SLStatus, apsFrame: EmberApsFrame | undefined, messageTag: number | undefined]> { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } - this.zdoRequestSequence = ++this.zdoRequestSequence & APPLICATION_ZDO_SEQUENCE_MASK - const messageTag = this.zdoRequestSequence - messageContents[0] = messageTag + this.zdoRequestSequence = ++this.zdoRequestSequence & APPLICATION_ZDO_SEQUENCE_MASK; + const messageTag = this.zdoRequestSequence; + messageContents[0] = messageTag; const apsFrame: EmberApsFrame = { profileId: Zdo.ZDO_PROFILE_ID, @@ -1077,7 +1076,7 @@ export default class Router extends Command { options, groupId: 0, sequence: 0, // set by stack - } + }; if ( destination === ZSpec.BroadcastAddress.DEFAULT || @@ -1086,8 +1085,8 @@ export default class Router extends Command { ) { logger.debug( `~~~> [ZDO ${Zdo.ClusterId[clusterId]} BROADCAST to=${destination} messageTag=${messageTag} ` + - `messageContents=${messageContents.toString('hex')}]`, - ) + `messageContents=${messageContents.toString("hex")}]`, + ); const [status, apsSequence] = await this.ezsp.ezspSendBroadcast( ZSpec.NULL_NODE_ID, // alias @@ -1097,17 +1096,17 @@ export default class Router extends Command { DEFAULT_ZDO_REQUEST_RADIUS, messageTag, messageContents, - ) - apsFrame.sequence = apsSequence + ); + apsFrame.sequence = apsSequence; - logger.debug(`~~~> [SENT ZDO type=BROADCAST apsSequence=${apsSequence} messageTag=${messageTag} status=${SLStatus[status]}`) - return [status, apsFrame, messageTag] + logger.debug(`~~~> [SENT ZDO type=BROADCAST apsSequence=${apsSequence} messageTag=${messageTag} status=${SLStatus[status]}`); + return [status, apsFrame, messageTag]; } logger.debug( `~~~> [ZDO ${Zdo.ClusterId[clusterId]} UNICAST to=${destination} messageTag=${messageTag} ` + - `messageContents=${messageContents.toString('hex')}]`, - ) + `messageContents=${messageContents.toString("hex")}]`, + ); const [status, apsSequence] = await this.ezsp.ezspSendUnicast( EmberOutgoingMessageType.DIRECT, @@ -1115,20 +1114,20 @@ export default class Router extends Command { apsFrame, messageTag, messageContents, - ) - apsFrame.sequence = apsSequence + ); + apsFrame.sequence = apsSequence; - logger.debug(`~~~> [SENT ZDO type=DIRECT apsSequence=${apsSequence} messageTag=${messageTag} status=${SLStatus[status]}`) - return [status, apsFrame, messageTag] + logger.debug(`~~~> [SENT ZDO type=DIRECT apsSequence=${apsSequence} messageTag=${messageTag} status=${SLStatus[status]}`); + return [status, apsFrame, messageTag]; } private setRouterState(newState: RouterState): void { if (newState === this.routerState) { - return + return; } - logger.info(`Router state changed: previous=${RouterState[this.routerState]} new=${RouterState[newState]}.`) + logger.info(`Router state changed: previous=${RouterState[this.routerState]} new=${RouterState[newState]}.`); - this.routerState = newState + this.routerState = newState; } } diff --git a/src/commands/sniff/index.ts b/src/commands/sniff/index.ts index be04052..9e5033f 100644 --- a/src/commands/sniff/index.ts +++ b/src/commands/sniff/index.ts @@ -1,253 +1,253 @@ -import { createSocket, Socket } from 'node:dgram' -import { createWriteStream, existsSync, WriteStream } from 'node:fs' -import { join } from 'node:path' -import { pathToFileURL } from 'node:url' - -import { confirm, input, select } from '@inquirer/prompts' -import { Command } from '@oclif/core' -import { Logger } from 'winston' - -import { ZSpec } from 'zigbee-herdsman' -import { SLStatus } from 'zigbee-herdsman/dist/adapter/ember/enums.js' -import { Ezsp } from 'zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js' - -import { DATA_FOLDER, DEFAULT_PCAP_PATH, logger } from '../../index.js' -import { emberStart, emberStop } from '../../utils/ember.js' -import { getPortConf } from '../../utils/port.js' -import { browseToFile, computeCRC16CITTKermit } from '../../utils/utils.js' -import { createPcapFileHeader, createPcapPacketRecordMs, createWiresharkZEPFrame, PCAP_MAGIC_NUMBER_MS } from '../../utils/wireshark.js' - -const enum SniffMenu { +import { type Socket, createSocket } from "node:dgram"; +import { type WriteStream, createWriteStream, existsSync } from "node:fs"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +import { confirm, input, select } from "@inquirer/prompts"; +import { Command } from "@oclif/core"; +import type { Logger } from "winston"; + +import { ZSpec } from "zigbee-herdsman"; +import { SLStatus } from "zigbee-herdsman/dist/adapter/ember/enums.js"; +import type { Ezsp } from "zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js"; + +import { DATA_FOLDER, DEFAULT_PCAP_PATH, logger } from "../../index.js"; +import { emberStart, emberStop } from "../../utils/ember.js"; +import { getPortConf } from "../../utils/port.js"; +import { browseToFile, computeCRC16CITTKermit } from "../../utils/utils.js"; +import { PCAP_MAGIC_NUMBER_MS, createPcapFileHeader, createPcapPacketRecordMs, createWiresharkZEPFrame } from "../../utils/wireshark.js"; + +enum SniffMenu { START_SNIFFING = 0, } -const DEFAULT_WIRESHARK_IP_ADDRESS = '127.0.0.1' -const DEFAULT_ZEP_UDP_PORT = 17754 +const DEFAULT_WIRESHARK_IP_ADDRESS = "127.0.0.1"; +const DEFAULT_ZEP_UDP_PORT = 17754; export default class Sniff extends Command { - static override args = {} - static override description = 'Sniff Zigbee traffic (to Wireshark, to PCAP file, to custom handler or just log raw data).' - static override examples = ['<%= config.bin %> <%= command.id %>'] - static override flags = {} + static override args = {}; + static override description = "Sniff Zigbee traffic (to Wireshark, to PCAP file, to custom handler or just log raw data)."; + static override examples = ["<%= config.bin %> <%= command.id %>"]; + static override flags = {}; - public ezsp: Ezsp | undefined - public sequence: number = 0 - public sniffing: boolean = false - public udpSocket: Socket | undefined - public pcapFileStream: WriteStream | undefined - public wiresharkIPAddress: string = DEFAULT_WIRESHARK_IP_ADDRESS - public zepUDPPort: number = DEFAULT_ZEP_UDP_PORT + public ezsp: Ezsp | undefined; + public sequence = 0; + public sniffing = false; + public udpSocket: Socket | undefined; + public pcapFileStream: WriteStream | undefined; + public wiresharkIPAddress: string = DEFAULT_WIRESHARK_IP_ADDRESS; + public zepUDPPort: number = DEFAULT_ZEP_UDP_PORT; - private customHandler: ((cmd: Command, logger: Logger, linkQuality: number, rssi: number, packetContents: Buffer) => void) | undefined + private customHandler: ((cmd: Command, logger: Logger, linkQuality: number, rssi: number, packetContents: Buffer) => void) | undefined; public async run(): Promise { // const { args, flags } = await this.parse(Sniff) - const portConf = await getPortConf() - logger.debug(`Using port conf: ${JSON.stringify(portConf)}`) + const portConf = await getPortConf(); + logger.debug(`Using port conf: ${JSON.stringify(portConf)}`); - this.ezsp = await emberStart(portConf) - let exit: boolean = false + this.ezsp = await emberStart(portConf); + let exit = false; while (!exit) { - exit = await this.navigateMenu() + exit = await this.navigateMenu(); if (exit && this.sniffing) { - exit = await confirm({ message: 'Sniffing is currently running. Confirm exit?', default: false }) + exit = await confirm({ message: "Sniffing is currently running. Confirm exit?", default: false }); } } - this.udpSocket?.close() - this.pcapFileStream?.close() - await emberStop(this.ezsp) + this.udpSocket?.close(); + this.pcapFileStream?.close(); + await emberStop(this.ezsp); - return this.exit(0) + return this.exit(0); } private async menuStartSniffing(): Promise { if (!this.ezsp) { - logger.error(`Invalid state, no EZSP layer available.`) - return this.exit(1) + logger.error("Invalid state, no EZSP layer available."); + return this.exit(1); } - const enum SniffDestination { + enum SniffDestination { LOG_FILE = 0, WIRESHARK = 1, PCAP_FILE = 2, } const sniffDestination = await select({ choices: [ - { name: 'Wireshark', value: SniffDestination.WIRESHARK, description: 'Write to Wireshark ZEP UDP Protocol' }, - { name: 'PCAP file', value: SniffDestination.PCAP_FILE, description: 'Write to a PCAP file for later use or sharing.' }, - { name: 'Log', value: SniffDestination.LOG_FILE, description: 'Write raw data to log file.' }, + { name: "Wireshark", value: SniffDestination.WIRESHARK, description: "Write to Wireshark ZEP UDP Protocol" }, + { name: "PCAP file", value: SniffDestination.PCAP_FILE, description: "Write to a PCAP file for later use or sharing." }, + { name: "Log", value: SniffDestination.LOG_FILE, description: "Write raw data to log file." }, ], - message: 'Destination (Note: if present, custom handler is always used, regardless of the selected destination)', - }) + message: "Destination (Note: if present, custom handler is always used, regardless of the selected destination)", + }); switch (sniffDestination) { case SniffDestination.WIRESHARK: { - this.wiresharkIPAddress = await input({ message: 'Wireshark IP address', default: DEFAULT_WIRESHARK_IP_ADDRESS }) - this.zepUDPPort = Number.parseInt(await input({ message: 'Wireshark ZEP UDP port', default: `${DEFAULT_ZEP_UDP_PORT}` }), 10) - this.udpSocket = createSocket('udp4') + this.wiresharkIPAddress = await input({ message: "Wireshark IP address", default: DEFAULT_WIRESHARK_IP_ADDRESS }); + this.zepUDPPort = Number.parseInt(await input({ message: "Wireshark ZEP UDP port", default: `${DEFAULT_ZEP_UDP_PORT}` }), 10); + this.udpSocket = createSocket("udp4"); - this.udpSocket.bind(this.zepUDPPort) + this.udpSocket.bind(this.zepUDPPort); - break + break; } case SniffDestination.PCAP_FILE: { - const pcapFilePath = await browseToFile('PCAP file', DEFAULT_PCAP_PATH, true) - this.pcapFileStream = createWriteStream(pcapFilePath, 'utf8') + const pcapFilePath = await browseToFile("PCAP file", DEFAULT_PCAP_PATH, true); + this.pcapFileStream = createWriteStream(pcapFilePath, "utf8"); - this.pcapFileStream.on('error', (error) => { - logger.error(error) + this.pcapFileStream.on("error", (error) => { + logger.error(error); - return true - }) + return true; + }); - const fileHeader = createPcapFileHeader(PCAP_MAGIC_NUMBER_MS) + const fileHeader = createPcapFileHeader(PCAP_MAGIC_NUMBER_MS); - this.pcapFileStream.write(fileHeader) + this.pcapFileStream.write(fileHeader); - break + break; } } // set desired tx power before scan const radioTxPower = Number.parseInt( await input({ - default: '5', - message: 'Radio transmit power [-128-127]', + default: "5", + message: "Radio transmit power [-128-127]", validate(value: string) { if (/\./.test(value)) { - return false + return false; } - const v = Number.parseInt(value, 10) + const v = Number.parseInt(value, 10); - return v >= -128 && v <= 127 + return v >= -128 && v <= 127; }, }), 10, - ) + ); - let status = await this.ezsp.ezspSetRadioPower(radioTxPower) + let status = await this.ezsp.ezspSetRadioPower(radioTxPower); if (status !== SLStatus.OK) { - logger.error(`Failed to set transmit power to ${radioTxPower} status=${SLStatus[status]}.`) - return true + logger.error(`Failed to set transmit power to ${radioTxPower} status=${SLStatus[status]}.`); + return true; } const channel = await select({ choices: ZSpec.ALL_802_15_4_CHANNELS.map((c) => ({ name: c.toString(), value: c })), - message: 'Channel to sniff', - }) - const eui64 = await this.ezsp.ezspGetEui64() - const deviceId = Number.parseInt(eui64.slice(-4), 16) + message: "Channel to sniff", + }); + const eui64 = await this.ezsp.ezspGetEui64(); + const deviceId = Number.parseInt(eui64.slice(-4), 16); - status = await this.ezsp.mfglibInternalStart(true) + status = await this.ezsp.mfglibInternalStart(true); if (status !== SLStatus.OK) { - logger.error(`Failed to start listening for packets with status=${SLStatus[status]}.`) - return true + logger.error(`Failed to start listening for packets with status=${SLStatus[status]}.`); + return true; } - status = await this.ezsp.mfglibInternalSetChannel(channel) + status = await this.ezsp.mfglibInternalSetChannel(channel); if (status !== SLStatus.OK) { - logger.error(`Failed to set channel with status=${SLStatus[status]}.`) - return true + logger.error(`Failed to set channel with status=${SLStatus[status]}.`); + return true; } - this.sniffing = true + this.sniffing = true; - const handlerFile = join(DATA_FOLDER, `ezspMfglibRxHandler.mjs`) + const handlerFile = join(DATA_FOLDER, "ezspMfglibRxHandler.mjs"); if (existsSync(handlerFile)) { try { - const importedScript = await import(pathToFileURL(handlerFile).toString()) + const importedScript = await import(pathToFileURL(handlerFile).toString()); - if (typeof importedScript.default !== 'function') { - throw new TypeError(`Not a function.`) + if (typeof importedScript.default !== "function") { + throw new TypeError("Not a function."); } - this.customHandler = importedScript.default + this.customHandler = importedScript.default; - logger.info(`Loaded custom handler.`) + logger.info("Loaded custom handler."); } catch (error) { - logger.error(`Failed to load custom handler. ${error}`) + logger.error(`Failed to load custom handler. ${error}`); } } // XXX: this is currently not restored, but not a problem since only possible menu is exit - const ezspMfglibRxHandlerOriginal = this.ezsp.ezspMfglibRxHandler + const ezspMfglibRxHandlerOriginal = this.ezsp.ezspMfglibRxHandler; this.ezsp.ezspMfglibRxHandler = (linkQuality: number, rssi: number, packetContents: Buffer): void => { if (this.customHandler) { - this.customHandler(this, logger, linkQuality, rssi, packetContents) + this.customHandler(this, logger, linkQuality, rssi, packetContents); } switch (sniffDestination) { case SniffDestination.WIRESHARK: { try { - const wsZEPFrame = createWiresharkZEPFrame(channel, deviceId, linkQuality, rssi, this.sequence, packetContents) - this.sequence += 1 + const wsZEPFrame = createWiresharkZEPFrame(channel, deviceId, linkQuality, rssi, this.sequence, packetContents); + this.sequence += 1; if (this.sequence > 0xffffffff) { // wrap if necessary... - this.sequence = 0 + this.sequence = 0; } if (this.udpSocket) { - this.udpSocket.send(wsZEPFrame, this.zepUDPPort, this.wiresharkIPAddress) + this.udpSocket.send(wsZEPFrame, this.zepUDPPort, this.wiresharkIPAddress); } } catch (error) { - logger.debug(error) + logger.debug(error); } - break + break; } case SniffDestination.PCAP_FILE: { if (this.pcapFileStream) { // fix static CRC used in EZSP >= v8 - packetContents.set(computeCRC16CITTKermit(packetContents.subarray(0, -2)), packetContents.length - 2) + packetContents.set(computeCRC16CITTKermit(packetContents.subarray(0, -2)), packetContents.length - 2); - const packet = createPcapPacketRecordMs(packetContents) + const packet = createPcapPacketRecordMs(packetContents); - this.pcapFileStream.write(packet) + this.pcapFileStream.write(packet); } - break + break; } case SniffDestination.LOG_FILE: { - ezspMfglibRxHandlerOriginal(linkQuality, rssi, packetContents) + ezspMfglibRxHandlerOriginal(linkQuality, rssi, packetContents); - break + break; } } - } + }; - logger.info(`Sniffing started.`) + logger.info("Sniffing started."); - return false + return false; } private async navigateMenu(): Promise { const answer = await select<-1 | SniffMenu>({ choices: [ - { name: 'Start sniffing', value: SniffMenu.START_SNIFFING, disabled: this.sniffing }, - { name: 'Exit', value: -1 }, + { name: "Start sniffing", value: SniffMenu.START_SNIFFING, disabled: this.sniffing }, + { name: "Exit", value: -1 }, ], - message: 'Menu', - }) + message: "Menu", + }); switch (answer) { case SniffMenu.START_SNIFFING: { - return await this.menuStartSniffing() + return await this.menuStartSniffing(); } } - return true + return true; } } diff --git a/src/commands/stack/index.ts b/src/commands/stack/index.ts index 0577d74..c4cdc07 100644 --- a/src/commands/stack/index.ts +++ b/src/commands/stack/index.ts @@ -3,20 +3,20 @@ import type { EmberNetworkParameters, EmberZigbeeNetwork, SecManContext, -} from 'zigbee-herdsman/dist/adapter/ember/types.js' -import type { PanId } from 'zigbee-herdsman/dist/zspec/tstypes.js' +} from "zigbee-herdsman/dist/adapter/ember/types.js"; +import type { PanId } from "zigbee-herdsman/dist/zspec/tstypes.js"; -import type { ConfigValue, LinkKeyBackupData } from '../../utils/types.js' +import type { ConfigValue, LinkKeyBackupData } from "../../utils/types.js"; -import { randomBytes } from 'node:crypto' -import { readFileSync, writeFileSync } from 'node:fs' +import { randomBytes } from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; -import { checkbox, confirm, input, select } from '@inquirer/prompts' -import { Command } from '@oclif/core' -import { Presets, SingleBar } from 'cli-progress' +import { checkbox, confirm, input, select } from "@inquirer/prompts"; +import { Command } from "@oclif/core"; +import { Presets, SingleBar } from "cli-progress"; -import { ZSpec } from 'zigbee-herdsman' -import { EmberTokensManager } from 'zigbee-herdsman/dist/adapter/ember/adapter/tokensManager.js' +import { ZSpec } from "zigbee-herdsman"; +import { EmberTokensManager } from "zigbee-herdsman/dist/adapter/ember/adapter/tokensManager.js"; import { EmberExtendedSecurityBitmask, EmberInitialSecurityBitmask, @@ -24,15 +24,15 @@ import { EmberLibraryId, EmberNodeType, EzspNetworkScanType, - SecManKeyType, SLStatus, -} from 'zigbee-herdsman/dist/adapter/ember/enums.js' -import { EMBER_AES_HASH_BLOCK_SIZE, EMBER_ENCRYPTION_KEY_SIZE } from 'zigbee-herdsman/dist/adapter/ember/ezsp/consts.js' -import { EzspConfigId, EzspDecisionBitmask, EzspDecisionId, EzspMfgTokenId, EzspPolicyId } from 'zigbee-herdsman/dist/adapter/ember/ezsp/enums.js' -import { Ezsp } from 'zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js' -import { initSecurityManagerContext } from 'zigbee-herdsman/dist/adapter/ember/utils/initters.js' -import { toUnifiedBackup } from 'zigbee-herdsman/dist/utils/backup.js' -import { eui64LEBufferToHex } from 'zigbee-herdsman/dist/zspec/utils.js' + SecManKeyType, +} from "zigbee-herdsman/dist/adapter/ember/enums.js"; +import { EMBER_AES_HASH_BLOCK_SIZE, EMBER_ENCRYPTION_KEY_SIZE } from "zigbee-herdsman/dist/adapter/ember/ezsp/consts.js"; +import { EzspConfigId, EzspDecisionBitmask, EzspDecisionId, EzspMfgTokenId, EzspPolicyId } from "zigbee-herdsman/dist/adapter/ember/ezsp/enums.js"; +import type { Ezsp } from "zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js"; +import { initSecurityManagerContext } from "zigbee-herdsman/dist/adapter/ember/utils/initters.js"; +import { toUnifiedBackup } from "zigbee-herdsman/dist/utils/backup.js"; +import { eui64LEBufferToHex } from "zigbee-herdsman/dist/zspec/utils.js"; import { DEFAULT_CONFIGURATION_YAML_PATH, @@ -40,8 +40,8 @@ import { DEFAULT_STACK_CONFIG_PATH, DEFAULT_TOKENS_BACKUP_PATH, logger, -} from '../../index.js' -import { CREATOR_STACK_RESTORED_EUI64, TOUCHLINK_CHANNELS } from '../../utils/consts.js' +} from "../../index.js"; +import { CREATOR_STACK_RESTORED_EUI64, TOUCHLINK_CHANNELS } from "../../utils/consts.js"; import { emberFullVersion, emberNetworkInit, @@ -50,12 +50,12 @@ import { getKeyStructBitmask, getLibraryStatus, waitForStackStatus, -} from '../../utils/ember.js' -import { NVM3ObjectKey } from '../../utils/enums.js' -import { getPortConf } from '../../utils/port.js' -import { browseToFile, getBackupFromFile, toHex } from '../../utils/utils.js' +} from "../../utils/ember.js"; +import { NVM3ObjectKey } from "../../utils/enums.js"; +import { getPortConf } from "../../utils/port.js"; +import { browseToFile, getBackupFromFile, toHex } from "../../utils/utils.js"; -const enum StackMenu { +enum StackMenu { STACK_INFO = 0, STACK_CONFIG = 1, @@ -77,93 +77,93 @@ const enum StackMenu { REPAIRS = 99, } -const BULLET_FULL = '\u2022' -const BULLET_EMPTY = '\u2219' +const BULLET_FULL = "\u2022"; +const BULLET_EMPTY = "\u2219"; export default class Stack extends Command { - static override args = {} - static override description = 'Interact with the EmberZNet stack in the adapter.' - static override examples = ['<%= config.bin %> <%= command.id %>'] - static override flags = {} + static override args = {}; + static override description = "Interact with the EmberZNet stack in the adapter."; + static override examples = ["<%= config.bin %> <%= command.id %>"]; + static override flags = {}; public async run(): Promise { // const {flags} = await this.parse(Stack) - const portConf = await getPortConf() - logger.debug(`Using port conf: ${JSON.stringify(portConf)}`) + const portConf = await getPortConf(); + logger.debug(`Using port conf: ${JSON.stringify(portConf)}`); - let ezsp = await emberStart(portConf) - let exit: boolean = false + let ezsp = await emberStart(portConf); + let exit = false; while (!exit) { - exit = await this.navigateMenu(ezsp) + exit = await this.navigateMenu(ezsp); if (exit) { const restart = await confirm({ default: true, - message: 'Restart? (If no, exit)', - }) + message: "Restart? (If no, exit)", + }); if (restart) { - await emberStop(ezsp) - ezsp = await emberStart(portConf) - exit = false + await emberStop(ezsp); + ezsp = await emberStart(portConf); + exit = false; } } } - await emberStop(ezsp) + await emberStop(ezsp); - return this.exit(0) + return this.exit(0); } private async menuNetworkBackup(ezsp: Ezsp): Promise { - const saveFile = await browseToFile('Network backup save file', DEFAULT_NETWORK_BACKUP_PATH, true) - const initStatus = await emberNetworkInit(ezsp) + const saveFile = await browseToFile("Network backup save file", DEFAULT_NETWORK_BACKUP_PATH, true); + const initStatus = await emberNetworkInit(ezsp); if (initStatus === SLStatus.NOT_JOINED) { - logger.error(`No network present.`) - return true + logger.error("No network present."); + return true; } if (initStatus !== SLStatus.OK) { - logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`) - return true + logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`); + return true; } - await waitForStackStatus(ezsp, SLStatus.NETWORK_UP) + await waitForStackStatus(ezsp, SLStatus.NETWORK_UP); - const [netStatus, , netParams] = await ezsp.ezspGetNetworkParameters() + const [netStatus, , netParams] = await ezsp.ezspGetNetworkParameters(); if (netStatus !== SLStatus.OK) { - logger.error(`Failed to get network parameters with status=${SLStatus[netStatus]}.`) - return true + logger.error(`Failed to get network parameters with status=${SLStatus[netStatus]}.`); + return true; } - const eui64 = await ezsp.ezspGetEui64() - const [netKeyStatus, netKeyInfo] = await ezsp.ezspGetNetworkKeyInfo() + const eui64 = await ezsp.ezspGetEui64(); + const [netKeyStatus, netKeyInfo] = await ezsp.ezspGetNetworkKeyInfo(); if (netKeyStatus !== SLStatus.OK) { - logger.error(`Failed to get network keys info with status=${SLStatus[netKeyStatus]}.`) - return true + logger.error(`Failed to get network keys info with status=${SLStatus[netKeyStatus]}.`); + return true; } if (!netKeyInfo.networkKeySet) { - logger.error(`No network key set.`) - return true + logger.error("No network key set."); + return true; } - const [confStatus, keyTableSize] = await ezsp.ezspGetConfigurationValue(EzspConfigId.KEY_TABLE_SIZE) + const [confStatus, keyTableSize] = await ezsp.ezspGetConfigurationValue(EzspConfigId.KEY_TABLE_SIZE); if (confStatus !== SLStatus.OK) { - logger.error(`Failed to retrieve key table size from NCP with status=${SLStatus[confStatus]}.`) - return true + logger.error(`Failed to retrieve key table size from NCP with status=${SLStatus[confStatus]}.`); + return true; } - const keyList: LinkKeyBackupData[] = [] + const keyList: LinkKeyBackupData[] = []; for (let i = 0; i < keyTableSize; i++) { - const [status, context, plaintextKey, apsKeyMeta] = await ezsp.ezspExportLinkKeyByIndex(i) - logger.debug(`Export link key at index ${i}, status=${SLStatus[status]}.`) + const [status, context, plaintextKey, apsKeyMeta] = await ezsp.ezspExportLinkKeyByIndex(i); + logger.debug(`Export link key at index ${i}, status=${SLStatus[status]}.`); // only include key if we could retrieve one at index and hash it properly if (status === SLStatus.OK) { @@ -174,7 +174,7 @@ export default class Stack extends Command { { result: Buffer.alloc(EMBER_AES_HASH_BLOCK_SIZE), length: 0x00000000 }, true, plaintextKey.contents, - ) + ); if (hashStatus === SLStatus.OK) { keyList.push({ @@ -182,40 +182,40 @@ export default class Stack extends Command { key: { contents: returnContext.result }, outgoingFrameCounter: apsKeyMeta.outgoingFrameCounter, incomingFrameCounter: apsKeyMeta.incomingFrameCounter, - }) + }); } else { // this should never happen? - logger.error(`Failed to hash link key at index ${i} with status=${SLStatus[hashStatus]}. Omitting from backup.`) + logger.error(`Failed to hash link key at index ${i} with status=${SLStatus[hashStatus]}. Omitting from backup.`); } } } - logger.info(`Retrieved ${keyList.length} link keys.`) + logger.info(`Retrieved ${keyList.length} link keys.`); - let context: SecManContext = initSecurityManagerContext() - context.coreKeyType = SecManKeyType.TC_LINK - const [tclkStatus, tcLinkKey] = await ezsp.ezspExportKey(context) + let context: SecManContext = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.TC_LINK; + const [tclkStatus, tcLinkKey] = await ezsp.ezspExportKey(context); if (tclkStatus !== SLStatus.OK) { - logger.error(`Failed to export TC Link Key with status=${SLStatus[tclkStatus]}.`) - return true + logger.error(`Failed to export TC Link Key with status=${SLStatus[tclkStatus]}.`); + return true; } - context = initSecurityManagerContext() // make sure it's back to zeroes - context.coreKeyType = SecManKeyType.NETWORK - context.keyIndex = 0 - const [nkStatus, networkKey] = await ezsp.ezspExportKey(context) + context = initSecurityManagerContext(); // make sure it's back to zeroes + context.coreKeyType = SecManKeyType.NETWORK; + context.keyIndex = 0; + const [nkStatus, networkKey] = await ezsp.ezspExportKey(context); if (nkStatus !== SLStatus.OK) { - logger.error(`Failed to export Network Key with status=${SLStatus[nkStatus]}.`) - return true + logger.error(`Failed to export Network Key with status=${SLStatus[nkStatus]}.`); + return true; } const backup = { - coordinatorIeeeAddress: Buffer.from(eui64.slice(2) /* take out 0x */, 'hex').reverse(), + coordinatorIeeeAddress: Buffer.from(eui64.slice(2) /* take out 0x */, "hex").reverse(), devices: keyList.map((key) => ({ networkAddress: ZSpec.NULL_NODE_ID, // not used for restore, no reason to make NCP calls for nothing - ieeeAddress: Buffer.from(key.deviceEui64.slice(2) /* take out 0x */, 'hex').reverse(), + ieeeAddress: Buffer.from(key.deviceEui64.slice(2) /* take out 0x */, "hex").reverse(), isDirectChild: false, // not used linkKey: { key: key.key.contents, @@ -242,226 +242,226 @@ export default class Stack extends Command { }, networkUpdateId: netParams.nwkUpdateId, securityLevel: 5, // Z3.0 - } - const unifiedBackup = await toUnifiedBackup(backup) + }; + const unifiedBackup = await toUnifiedBackup(backup); - writeFileSync(saveFile, JSON.stringify(unifiedBackup, null, 2), 'utf8') + writeFileSync(saveFile, JSON.stringify(unifiedBackup, null, 2), "utf8"); - logger.info(`Network backup written to '${saveFile}'.`) + logger.info(`Network backup written to '${saveFile}'.`); - return true + return true; } private async menuNetworkInfo(ezsp: Ezsp): Promise { - const initStatus = await emberNetworkInit(ezsp) + const initStatus = await emberNetworkInit(ezsp); if (initStatus === SLStatus.NOT_JOINED) { - logger.error(`No network present.`) - return true + logger.error("No network present."); + return true; } if (initStatus !== SLStatus.OK) { - logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`) - return true + logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`); + return true; } - await waitForStackStatus(ezsp, SLStatus.NETWORK_UP) + await waitForStackStatus(ezsp, SLStatus.NETWORK_UP); - const [npStatus, nodeType, netParams] = await ezsp.ezspGetNetworkParameters() + const [npStatus, nodeType, netParams] = await ezsp.ezspGetNetworkParameters(); if (npStatus !== SLStatus.OK) { - logger.error(`Failed to get network parameters with status=${SLStatus[npStatus]}.`) - return true + logger.error(`Failed to get network parameters with status=${SLStatus[npStatus]}.`); + return true; } - const eui64 = await ezsp.ezspGetEui64() + const eui64 = await ezsp.ezspGetEui64(); - const [netKeyStatus, netKeyInfo] = await ezsp.ezspGetNetworkKeyInfo() + const [netKeyStatus, netKeyInfo] = await ezsp.ezspGetNetworkKeyInfo(); if (netKeyStatus !== SLStatus.OK) { - throw new Error(`[BACKUP] Failed to get network keys info with status=${SLStatus[netKeyStatus]}.`) + throw new Error(`[BACKUP] Failed to get network keys info with status=${SLStatus[netKeyStatus]}.`); } - const context = initSecurityManagerContext() - context.coreKeyType = SecManKeyType.TC_LINK + const context = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.TC_LINK; - const [tcKeyStatus, tcKeyInfo] = await ezsp.ezspGetApsKeyInfo(context) + const [tcKeyStatus, tcKeyInfo] = await ezsp.ezspGetApsKeyInfo(context); if (tcKeyStatus !== SLStatus.OK) { - throw new Error(`[BACKUP] Failed to get TC APS key info with status=${SLStatus[tcKeyStatus]}.`) - } - - logger.info(`Node EUI64=${eui64} type=${EmberNodeType[nodeType]}.`) - logger.info(`Network parameters:`) - logger.info(` - PAN ID: ${netParams.panId} (${toHex(netParams.panId)})`) - logger.info(` - Extended PAN ID: ${netParams.extendedPanId}`) - logger.info(` - Radio Channel: ${netParams.radioChannel}`) - logger.info(` - Radio Power: ${netParams.radioTxPower} dBm`) - logger.info(` - Preferred Channels: ${ZSpec.Utils.uint32MaskToChannels(netParams.channels).join(',')}`) - logger.info(`Network key info:`) - logger.info(` - Set? ${netKeyInfo.networkKeySet ? 'yes' : 'no'}`) - logger.info(` - Sequence Number: ${netKeyInfo.networkKeySequenceNumber}`) - logger.info(` - Frame Counter: ${netKeyInfo.networkKeyFrameCounter}`) - logger.info(` - Alt Set? ${netKeyInfo.alternateNetworkKeySet ? 'yes' : 'no'}`) - logger.info(` - Alt Sequence Number: ${netKeyInfo.altNetworkKeySequenceNumber}`) - logger.info(`Trust Center link key info:`) - logger.info(` - Properties: ${getKeyStructBitmask(tcKeyInfo.bitmask)}`) - logger.info(` - Incoming Frame Counter: ${tcKeyInfo.incomingFrameCounter}`) - logger.info(` - Outgoing Frame Counter: ${tcKeyInfo.outgoingFrameCounter}`) - - return true + throw new Error(`[BACKUP] Failed to get TC APS key info with status=${SLStatus[tcKeyStatus]}.`); + } + + logger.info(`Node EUI64=${eui64} type=${EmberNodeType[nodeType]}.`); + logger.info("Network parameters:"); + logger.info(` - PAN ID: ${netParams.panId} (${toHex(netParams.panId)})`); + logger.info(` - Extended PAN ID: ${netParams.extendedPanId}`); + logger.info(` - Radio Channel: ${netParams.radioChannel}`); + logger.info(` - Radio Power: ${netParams.radioTxPower} dBm`); + logger.info(` - Preferred Channels: ${ZSpec.Utils.uint32MaskToChannels(netParams.channels).join(",")}`); + logger.info("Network key info:"); + logger.info(` - Set? ${netKeyInfo.networkKeySet ? "yes" : "no"}`); + logger.info(` - Sequence Number: ${netKeyInfo.networkKeySequenceNumber}`); + logger.info(` - Frame Counter: ${netKeyInfo.networkKeyFrameCounter}`); + logger.info(` - Alt Set? ${netKeyInfo.alternateNetworkKeySet ? "yes" : "no"}`); + logger.info(` - Alt Sequence Number: ${netKeyInfo.altNetworkKeySequenceNumber}`); + logger.info("Trust Center link key info:"); + logger.info(` - Properties: ${getKeyStructBitmask(tcKeyInfo.bitmask)}`); + logger.info(` - Incoming Frame Counter: ${tcKeyInfo.incomingFrameCounter}`); + logger.info(` - Outgoing Frame Counter: ${tcKeyInfo.outgoingFrameCounter}`); + + return true; } private async menuNetworkLeave(ezsp: Ezsp): Promise { const confirmed = await confirm({ default: false, - message: 'Confirm leave network? (Cannot be undone without a backup.)', - }) + message: "Confirm leave network? (Cannot be undone without a backup.)", + }); if (!confirmed) { - logger.info(`Network leave cancelled.`) - return false + logger.info("Network leave cancelled."); + return false; } - const initStatus = await emberNetworkInit(ezsp) + const initStatus = await emberNetworkInit(ezsp); if (initStatus === SLStatus.NOT_JOINED) { - logger.info(`No network present.`) - return true + logger.info("No network present."); + return true; } if (initStatus !== SLStatus.OK) { - logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`) - return true + logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`); + return true; } - await waitForStackStatus(ezsp, SLStatus.NETWORK_UP) + await waitForStackStatus(ezsp, SLStatus.NETWORK_UP); - const leaveStatus = await ezsp.ezspLeaveNetwork() + const leaveStatus = await ezsp.ezspLeaveNetwork(); if (leaveStatus !== SLStatus.OK) { - logger.error(`Failed to leave network with status=${SLStatus[leaveStatus]}.`) - return true + logger.error(`Failed to leave network with status=${SLStatus[leaveStatus]}.`); + return true; } - await waitForStackStatus(ezsp, SLStatus.NETWORK_DOWN) + await waitForStackStatus(ezsp, SLStatus.NETWORK_DOWN); - logger.info(`Left network.`) + logger.info("Left network."); - return true + return true; } private async menuNetworkRestore(ezsp: Ezsp): Promise { - const backupFile = await browseToFile('Network backup file location', DEFAULT_NETWORK_BACKUP_PATH) - const backup = getBackupFromFile(backupFile) + const backupFile = await browseToFile("Network backup file location", DEFAULT_NETWORK_BACKUP_PATH); + const backup = getBackupFromFile(backupFile); if (backup === undefined) { // error logged in getBackupFromFile - return false + return false; } if (!backup.ezsp) { - const confirmed = await confirm({ message: `Backup file is not for EmberZNet stack. Restore anyway?`, default: false }) + const confirmed = await confirm({ message: "Backup file is not for EmberZNet stack. Restore anyway?", default: false }); if (!confirmed) { - logger.info(`Restore cancelled.`) - return false + logger.info("Restore cancelled."); + return false; } } if (!backup.ezsp?.hashed_tclk) { - logger.debug(`Backup file does not contain the Trust Center Link Key. Generating random one.`) + logger.debug("Backup file does not contain the Trust Center Link Key. Generating random one."); // don't care about version here, so just overwrite the whole `ezsp` object - backup.ezsp = { hashed_tclk: randomBytes(EMBER_ENCRYPTION_KEY_SIZE) } + backup.ezsp = { hashed_tclk: randomBytes(EMBER_ENCRYPTION_KEY_SIZE) }; } const radioTxPower = Number.parseInt( await input({ - default: '5', - message: 'Radio transmit power [-128-127]', + default: "5", + message: "Radio transmit power [-128-127]", validate(value) { if (/\./.test(value)) { - return false + return false; } - const v = Number.parseInt(value, 10) + const v = Number.parseInt(value, 10); - return v >= -128 && v <= 127 + return v >= -128 && v <= 127; }, }), 10, - ) - let status = await emberNetworkInit(ezsp) - const noNetwork = status === SLStatus.NOT_JOINED + ); + let status = await emberNetworkInit(ezsp); + const noNetwork = status === SLStatus.NOT_JOINED; if (!noNetwork && status !== SLStatus.OK) { - logger.error(`Failed network init request with status=${SLStatus[status]}.`) - return true + logger.error(`Failed network init request with status=${SLStatus[status]}.`); + return true; } if (!noNetwork) { const overwrite = await confirm({ default: false, - message: 'A network is present in the adapter. Leave and continue restoring?', - }) + message: "A network is present in the adapter. Leave and continue restoring?", + }); if (!overwrite) { - logger.info(`Restore cancelled.`) - return true + logger.info("Restore cancelled."); + return true; } - status = await ezsp.ezspLeaveNetwork() + status = await ezsp.ezspLeaveNetwork(); if (status !== SLStatus.OK) { - logger.error(`Failed to leave network with status=${SLStatus[status]}.`) - return true + logger.error(`Failed to leave network with status=${SLStatus[status]}.`); + return true; } - await waitForStackStatus(ezsp, SLStatus.NETWORK_DOWN) + await waitForStackStatus(ezsp, SLStatus.NETWORK_DOWN); } // before forming const keyList: LinkKeyBackupData[] = backup.devices.map((device) => { - const octets = [...device.ieeeAddress.reverse()] + const octets = [...device.ieeeAddress.reverse()]; return { - deviceEui64: `0x${octets.map((octet) => octet.toString(16).padStart(2, '0')).join('')}`, + deviceEui64: `0x${octets.map((octet) => octet.toString(16).padStart(2, "0")).join("")}`, // won't export if linkKey not present, so should always be valid here key: { contents: device.linkKey!.key }, outgoingFrameCounter: device.linkKey!.txCounter, incomingFrameCounter: device.linkKey!.rxCounter, - } - }) + }; + }); if (keyList.length > 0) { - const [confStatus, keyTableSize] = await ezsp.ezspGetConfigurationValue(EzspConfigId.KEY_TABLE_SIZE) + const [confStatus, keyTableSize] = await ezsp.ezspGetConfigurationValue(EzspConfigId.KEY_TABLE_SIZE); if (confStatus !== SLStatus.OK) { - logger.error(`Failed to retrieve key table size from NCP with status=${SLStatus[confStatus]}.`) - return true + logger.error(`Failed to retrieve key table size from NCP with status=${SLStatus[confStatus]}.`); + return true; } if (keyList.length > keyTableSize) { - logger.error(`Current key table of ${keyTableSize} is too small to import backup of ${keyList.length}!`) - return true + logger.error(`Current key table of ${keyTableSize} is too small to import backup of ${keyList.length}!`); + return true; } - let status: SLStatus + let status: SLStatus; for (let i = 0; i < keyTableSize; i++) { // erase any key index not present in backup but available on the NCP status = i >= keyList.length ? await ezsp.ezspEraseKeyTableEntry(i) - : await ezsp.ezspImportLinkKey(i, keyList[i].deviceEui64, keyList[i].key) + : await ezsp.ezspImportLinkKey(i, keyList[i].deviceEui64, keyList[i].key); if (status !== SLStatus.OK) { - logger.error(`Failed to ${i >= keyList.length ? 'erase' : 'set'} key table entry at index ${i} with status=${SLStatus[status]}`) + logger.error(`Failed to ${i >= keyList.length ? "erase" : "set"} key table entry at index ${i} with status=${SLStatus[status]}`); } } - logger.info(`Imported ${keyList.length} keys.`) + logger.info(`Imported ${keyList.length} keys.`); } // status = await ezsp.ezspSetNWKFrameCounter(backup.networkKeyInfo.frameCounter) @@ -490,22 +490,22 @@ export default class Stack extends Command { networkKeySequenceNumber: backup.networkKeyInfo.sequenceNumber, preconfiguredKey: { contents: backup.ezsp!.hashed_tclk! }, // presence validated above preconfiguredTrustCenterEui64: ZSpec.BLANK_EUI64, - } + }; - status = await ezsp.ezspSetInitialSecurityState(state) + status = await ezsp.ezspSetInitialSecurityState(state); if (status !== SLStatus.OK) { - logger.error(`Failed to set initial security state with status=${SLStatus[status]}.`) - return true + logger.error(`Failed to set initial security state with status=${SLStatus[status]}.`); + return true; } const extended: EmberExtendedSecurityBitmask = - EmberExtendedSecurityBitmask.JOINER_GLOBAL_LINK_KEY | EmberExtendedSecurityBitmask.NWK_LEAVE_REQUEST_NOT_ALLOWED - status = await ezsp.ezspSetExtendedSecurityBitmask(extended) + EmberExtendedSecurityBitmask.JOINER_GLOBAL_LINK_KEY | EmberExtendedSecurityBitmask.NWK_LEAVE_REQUEST_NOT_ALLOWED; + status = await ezsp.ezspSetExtendedSecurityBitmask(extended); if (status !== SLStatus.OK) { - logger.error(`Failed to set extended security bitmask to ${extended} with status=${SLStatus[status]}.`) - return true + logger.error(`Failed to set extended security bitmask to ${extended} with status=${SLStatus[status]}.`); + return true; } const netParams: EmberNetworkParameters = { @@ -517,29 +517,29 @@ export default class Stack extends Command { panId: backup.networkOptions.panId, radioChannel: backup.logicalChannel, radioTxPower, - } + }; - logger.info(`Forming new network with: ${JSON.stringify(netParams)}`) + logger.info(`Forming new network with: ${JSON.stringify(netParams)}`); - status = await ezsp.ezspFormNetwork(netParams) + status = await ezsp.ezspFormNetwork(netParams); if (status !== SLStatus.OK) { - logger.error(`Failed form network request with status=${SLStatus[status]}.`) - return true + logger.error(`Failed form network request with status=${SLStatus[status]}.`); + return true; } - await waitForStackStatus(ezsp, SLStatus.NETWORK_UP) + await waitForStackStatus(ezsp, SLStatus.NETWORK_UP); - const stStatus = await ezsp.ezspStartWritingStackTokens() + const stStatus = await ezsp.ezspStartWritingStackTokens(); - logger.debug(`Start writing stack tokens status=${SLStatus[stStatus]}.`) - logger.info(`New network formed!`) + logger.debug(`Start writing stack tokens status=${SLStatus[stStatus]}.`); + logger.info("New network formed!"); - const [netStatus, , parameters] = await ezsp.ezspGetNetworkParameters() + const [netStatus, , parameters] = await ezsp.ezspGetNetworkParameters(); if (netStatus !== SLStatus.OK) { - logger.error(`Failed to get network parameters with status=${SLStatus[netStatus]}.`) - return true + logger.error(`Failed to get network parameters with status=${SLStatus[netStatus]}.`); + return true; } if ( @@ -547,717 +547,717 @@ export default class Stack extends Command { Buffer.from(parameters.extendedPanId).equals(backup.networkOptions.extendedPanId) && parameters.radioChannel === backup.logicalChannel ) { - logger.info(`Restored network backup.`) + logger.info("Restored network backup."); } else { - logger.error(`Failed to restore network backup.`) + logger.error("Failed to restore network backup."); } - return true // cleaner to exit after this + return true; // cleaner to exit after this } private async menuNetworkScan(ezsp: Ezsp): Promise { const radioTxPower = Number.parseInt( await input({ - default: '5', - message: 'Radio transmit power [-128-127]', + default: "5", + message: "Radio transmit power [-128-127]", validate(value) { if (/\./.test(value)) { - return false + return false; } - const v = Number.parseInt(value, 10) + const v = Number.parseInt(value, 10); - return v >= -128 && v <= 127 + return v >= -128 && v <= 127; }, }), 10, - ) + ); - const status = await ezsp.ezspSetRadioPower(radioTxPower) + const status = await ezsp.ezspSetRadioPower(radioTxPower); if (status !== SLStatus.OK) { - logger.error(`Failed to set transmit power to ${radioTxPower} status=${SLStatus[status]}.`) - return true + logger.error(`Failed to set transmit power to ${radioTxPower} status=${SLStatus[status]}.`); + return true; } const scanType = await select({ choices: [ - { name: 'Scan each channel for its RSSI value', value: EzspNetworkScanType.ENERGY_SCAN }, - { name: 'Scan each channel for existing networks', value: EzspNetworkScanType.ACTIVE_SCAN }, + { name: "Scan each channel for its RSSI value", value: EzspNetworkScanType.ENERGY_SCAN }, + { name: "Scan each channel for existing networks", value: EzspNetworkScanType.ACTIVE_SCAN }, ], - message: 'Type of scan', - }) + message: "Type of scan", + }); // WiFi beacon frames at standard interval: 102.4msec const duration = await select({ choices: [ - { name: '3948 msec', value: 8 }, - { name: '1981 msec', value: 7 }, - { name: '998 msec', value: 6 }, - { name: '507 msec', value: 5 }, - { name: '261 msec', value: 4 }, - { name: '138 msec', value: 3 }, - { name: '77 msec', value: 2 }, - { name: '46 msec', value: 1 }, - { name: '31 msec', value: 0 }, + { name: "3948 msec", value: 8 }, + { name: "1981 msec", value: 7 }, + { name: "998 msec", value: 6 }, + { name: "507 msec", value: 5 }, + { name: "261 msec", value: 4 }, + { name: "138 msec", value: 3 }, + { name: "77 msec", value: 2 }, + { name: "46 msec", value: 1 }, + { name: "31 msec", value: 0 }, ], default: 6, - message: 'Duration of scan per channel', - }) - const progressBar = new SingleBar({ clearOnComplete: true, format: '{bar} {percentage}% | ETA: {eta}s' }, Presets.shades_classic) + message: "Duration of scan per channel", + }); + const progressBar = new SingleBar({ clearOnComplete: true, format: "{bar} {percentage}% | ETA: {eta}s" }, Presets.shades_classic); // a symbol is 16 microseconds, a scan period is 960 symbols - const totalTime = (((2 ** duration + 1) * (16 * 960)) / 1000) * ZSpec.ALL_802_15_4_CHANNELS.length - let scanCompleted: (value: PromiseLike | void) => void - const reportedValues: string[] = [] + const totalTime = (((2 ** duration + 1) * (16 * 960)) / 1000) * ZSpec.ALL_802_15_4_CHANNELS.length; + let scanCompleted: (value: PromiseLike | void) => void; + const reportedValues: string[] = []; // NOTE: expanding zigbee-herdsman - const ezspEnergyScanResultHandlerOriginal = ezsp.ezspEnergyScanResultHandler - const ezspNetworkFoundHandlerOriginal = ezsp.ezspNetworkFoundHandler - const ezspScanCompleteHandlerOriginal = ezsp.ezspScanCompleteHandler + const ezspEnergyScanResultHandlerOriginal = ezsp.ezspEnergyScanResultHandler; + const ezspNetworkFoundHandlerOriginal = ezsp.ezspNetworkFoundHandler; + const ezspScanCompleteHandlerOriginal = ezsp.ezspScanCompleteHandler; ezsp.ezspEnergyScanResultHandler = (channel: number, maxRssiValue: number): void => { - logger.debug(`ezspEnergyScanResultHandler: ${JSON.stringify({ channel, maxRssiValue })}`) - const full = 90 + maxRssiValue - const empty = 90 - full + logger.debug(`ezspEnergyScanResultHandler: ${JSON.stringify({ channel, maxRssiValue })}`); + const full = 90 + maxRssiValue; + const empty = 90 - full; if (full < 1 || empty < 1) { - reportedValues.push(`Channel ${channel}: ERROR`) + reportedValues.push(`Channel ${channel}: ERROR`); } else { - reportedValues.push(`Channel ${channel}: ${BULLET_FULL.repeat(full)}${BULLET_EMPTY.repeat(empty)} [${maxRssiValue} dBm]`) + reportedValues.push(`Channel ${channel}: ${BULLET_FULL.repeat(full)}${BULLET_EMPTY.repeat(empty)} [${maxRssiValue} dBm]`); } - } + }; ezsp.ezspNetworkFoundHandler = (networkFound: EmberZigbeeNetwork, lastHopLqi: number, lastHopRssi: number): void => { - logger.debug(`ezspNetworkFoundHandler: ${JSON.stringify({ networkFound, lastHopLqi, lastHopRssi })}`) + logger.debug(`ezspNetworkFoundHandler: ${JSON.stringify({ networkFound, lastHopLqi, lastHopRssi })}`); reportedValues.push( - `Found network:`, + "Found network:", ` - PAN ID: ${networkFound.panId}`, ` - Ext PAN ID: ${networkFound.extendedPanId}`, ` - Channel: ${networkFound.channel}`, - ` - Allowing join: ${networkFound.allowingJoin ? 'yes' : 'no'}`, + ` - Allowing join: ${networkFound.allowingJoin ? "yes" : "no"}`, ` - Node RSSI: ${lastHopRssi} dBm | LQI: ${lastHopLqi}`, - ) - } + ); + }; ezsp.ezspScanCompleteHandler = (channel: number, status: SLStatus): void => { - logger.debug(`ezspScanCompleteHandler: ${JSON.stringify({ channel, status })}`) - progressBar.stop() - clearInterval(progressInterval) + logger.debug(`ezspScanCompleteHandler: ${JSON.stringify({ channel, status })}`); + progressBar.stop(); + clearInterval(progressInterval); if (status === SLStatus.OK) { if (scanCompleted) { - scanCompleted() + scanCompleted(); } } else { - logger.error(`Failed to scan ${channel} with status=${SLStatus[status]}.`) + logger.error(`Failed to scan ${channel} with status=${SLStatus[status]}.`); } - } + }; - const startScanStatus = await ezsp.ezspStartScan(scanType, ZSpec.ALL_802_15_4_CHANNELS_MASK, duration) + const startScanStatus = await ezsp.ezspStartScan(scanType, ZSpec.ALL_802_15_4_CHANNELS_MASK, duration); if (startScanStatus !== SLStatus.OK) { - logger.error(`Failed start scan request with status=${SLStatus[startScanStatus]}.`) + logger.error(`Failed start scan request with status=${SLStatus[startScanStatus]}.`); // restore zigbee-herdsman default - ezsp.ezspEnergyScanResultHandler = ezspEnergyScanResultHandlerOriginal - ezsp.ezspNetworkFoundHandler = ezspNetworkFoundHandlerOriginal - ezsp.ezspScanCompleteHandler = ezspScanCompleteHandlerOriginal - return true + ezsp.ezspEnergyScanResultHandler = ezspEnergyScanResultHandlerOriginal; + ezsp.ezspNetworkFoundHandler = ezspNetworkFoundHandlerOriginal; + ezsp.ezspScanCompleteHandler = ezspScanCompleteHandlerOriginal; + return true; } - progressBar.start(totalTime, 0) + progressBar.start(totalTime, 0); const progressInterval = setInterval(() => { - progressBar.increment(500) - }, 500) + progressBar.increment(500); + }, 500); await new Promise((resolve) => { - scanCompleted = resolve - }) + scanCompleted = resolve; + }); for (const line of reportedValues) { - logger.info(line) + logger.info(line); } // restore zigbee-herdsman default - ezsp.ezspEnergyScanResultHandler = ezspEnergyScanResultHandlerOriginal - ezsp.ezspNetworkFoundHandler = ezspNetworkFoundHandlerOriginal - ezsp.ezspScanCompleteHandler = ezspScanCompleteHandlerOriginal - return false + ezsp.ezspEnergyScanResultHandler = ezspEnergyScanResultHandlerOriginal; + ezsp.ezspNetworkFoundHandler = ezspNetworkFoundHandlerOriginal; + ezsp.ezspScanCompleteHandler = ezspScanCompleteHandlerOriginal; + return false; } private async menuRepairs(ezsp: Ezsp): Promise { - const enum RepairId { + enum RepairId { EUI64_MISMATCH = 0, } const repairId = await select<-1 | RepairId>({ choices: [ - { name: 'Check for EUI64 mismatch', value: RepairId.EUI64_MISMATCH }, - { name: 'Go Back', value: -1 }, + { name: "Check for EUI64 mismatch", value: RepairId.EUI64_MISMATCH }, + { name: "Go Back", value: -1 }, ], - message: 'Repair', - }) + message: "Repair", + }); switch (repairId) { case RepairId.EUI64_MISMATCH: { - const initStatus = await emberNetworkInit(ezsp) + const initStatus = await emberNetworkInit(ezsp); if (initStatus === SLStatus.NOT_JOINED) { - logger.info(`No network present.`) - return true + logger.info("No network present."); + return true; } if (initStatus !== SLStatus.OK) { - logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`) - return true + logger.error(`Failed network init request with status=${SLStatus[initStatus]}.`); + return true; } - const [status, securityState] = await ezsp.ezspGetCurrentSecurityState() + const [status, securityState] = await ezsp.ezspGetCurrentSecurityState(); if (status !== SLStatus.OK) { - logger.error(`Failed get current security state request with status=${SLStatus[status]}.`) - return true + logger.error(`Failed get current security state request with status=${SLStatus[status]}.`); + return true; } - const eui64 = await ezsp.ezspGetEui64() + const eui64 = await ezsp.ezspGetEui64(); - logger.info(`Node EUI64 ${eui64} / Trust Center EUI64 ${securityState.trustCenterLongAddress}.`) + logger.info(`Node EUI64 ${eui64} / Trust Center EUI64 ${securityState.trustCenterLongAddress}.`); if (securityState.trustCenterLongAddress === eui64) { - logger.info(`EUI64 match. No fix required.`) - return true + logger.info("EUI64 match. No fix required."); + return true; } - logger.warning(`Fixing EUI64 mismatch...`) + logger.warning("Fixing EUI64 mismatch..."); - const [gtkStatus, tokenData] = await ezsp.ezspGetTokenData(NVM3ObjectKey.STACK_TRUST_CENTER, 0) + const [gtkStatus, tokenData] = await ezsp.ezspGetTokenData(NVM3ObjectKey.STACK_TRUST_CENTER, 0); if (gtkStatus !== SLStatus.OK) { - logger.error(`Failed get token data request with status=${SLStatus[gtkStatus]}.`) - return true + logger.error(`Failed get token data request with status=${SLStatus[gtkStatus]}.`); + return true; } - const tokenEUI64 = tokenData.data.subarray(2, 10) - const tcEUI64 = Buffer.from(securityState.trustCenterLongAddress.slice(2 /* 0x */), 'hex').reverse() + const tokenEUI64 = tokenData.data.subarray(2, 10); + const tcEUI64 = Buffer.from(securityState.trustCenterLongAddress.slice(2 /* 0x */), "hex").reverse(); if (tokenEUI64.equals(tcEUI64)) { - tokenData.data.set(Buffer.from(eui64.slice(2 /* 0x */), 'hex').reverse(), 2 /* skip uint16_t at start */) + tokenData.data.set(Buffer.from(eui64.slice(2 /* 0x */), "hex").reverse(), 2 /* skip uint16_t at start */); - const stkStatus = await ezsp.ezspSetTokenData(NVM3ObjectKey.STACK_TRUST_CENTER, 0, tokenData) + const stkStatus = await ezsp.ezspSetTokenData(NVM3ObjectKey.STACK_TRUST_CENTER, 0, tokenData); if (stkStatus !== SLStatus.OK) { - logger.error(`Failed set token data request with status=${SLStatus[stkStatus]}.`) - return true + logger.error(`Failed set token data request with status=${SLStatus[stkStatus]}.`); + return true; } } else { - logger.error(`Failed to fix EUI64 mismatch. NVM3 Trust Center token doesn't match current security state.`) - return true + logger.error(`Failed to fix EUI64 mismatch. NVM3 Trust Center token doesn't match current security state.`); + return true; } - break + break; } case -1: { - return false + return false; } } - return true + return true; } private async menuSecurityInfo(ezsp: Ezsp): Promise { { - const context: SecManContext = initSecurityManagerContext() - context.coreKeyType = SecManKeyType.TC_LINK - const [status, key] = await ezsp.ezspExportKey(context) + const context: SecManContext = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.TC_LINK; + const [status, key] = await ezsp.ezspExportKey(context); if (status === SLStatus.OK) { - logger.info(`Trust Center Link Key: ${key.contents.toString('hex')}`) + logger.info(`Trust Center Link Key: ${key.contents.toString("hex")}`); } else if (status !== SLStatus.NOT_FOUND) { - logger.error(`Failed to export Trust Center Link Key with status=${SLStatus[status]}.`) + logger.error(`Failed to export Trust Center Link Key with status=${SLStatus[status]}.`); } } { - const context: SecManContext = initSecurityManagerContext() - context.coreKeyType = SecManKeyType.APP_LINK - const [status, key] = await ezsp.ezspExportKey(context) + const context: SecManContext = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.APP_LINK; + const [status, key] = await ezsp.ezspExportKey(context); if (status === SLStatus.OK) { - logger.info(`App Link Key: ${key.contents.toString('hex')}`) + logger.info(`App Link Key: ${key.contents.toString("hex")}`); } else if (status !== SLStatus.NOT_FOUND) { - logger.error(`Failed to export App Link Key with status=${SLStatus[status]}.`) + logger.error(`Failed to export App Link Key with status=${SLStatus[status]}.`); } } { - const context: SecManContext = initSecurityManagerContext() - context.coreKeyType = SecManKeyType.NETWORK - context.keyIndex = 0 - const [status, key] = await ezsp.ezspExportKey(context) + const context: SecManContext = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.NETWORK; + context.keyIndex = 0; + const [status, key] = await ezsp.ezspExportKey(context); if (status === SLStatus.OK) { - logger.info(`Network Key: ${key.contents.toString('hex')}`) + logger.info(`Network Key: ${key.contents.toString("hex")}`); } else if (status !== SLStatus.NOT_FOUND) { - logger.error(`Failed to export Network Key with status=${SLStatus[status]}.`) + logger.error(`Failed to export Network Key with status=${SLStatus[status]}.`); } } { - const context: SecManContext = initSecurityManagerContext() - context.coreKeyType = SecManKeyType.ZLL_ENCRYPTION_KEY - const [status, key] = await ezsp.ezspExportKey(context) + const context: SecManContext = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.ZLL_ENCRYPTION_KEY; + const [status, key] = await ezsp.ezspExportKey(context); if (status === SLStatus.OK) { - logger.info(`ZLL Encryption Key: ${key.contents.toString('hex')}`) + logger.info(`ZLL Encryption Key: ${key.contents.toString("hex")}`); } else if (status !== SLStatus.NOT_FOUND) { - logger.error(`Failed to export ZLL Encryption Key with status=${SLStatus[status]}.`) + logger.error(`Failed to export ZLL Encryption Key with status=${SLStatus[status]}.`); } } { - const context: SecManContext = initSecurityManagerContext() - context.coreKeyType = SecManKeyType.ZLL_PRECONFIGURED_KEY - const [status, key] = await ezsp.ezspExportKey(context) + const context: SecManContext = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.ZLL_PRECONFIGURED_KEY; + const [status, key] = await ezsp.ezspExportKey(context); if (status === SLStatus.OK) { - logger.info(`ZLL Preconfigured Key: ${key.contents.toString('hex')}`) + logger.info(`ZLL Preconfigured Key: ${key.contents.toString("hex")}`); } else if (status !== SLStatus.NOT_FOUND) { - logger.error(`Failed to export ZLL Preconfigured Key with status=${SLStatus[status]}.`) + logger.error(`Failed to export ZLL Preconfigured Key with status=${SLStatus[status]}.`); } } { - const context: SecManContext = initSecurityManagerContext() - context.coreKeyType = SecManKeyType.GREEN_POWER_PROXY_TABLE_KEY - const [status, key] = await ezsp.ezspExportKey(context) + const context: SecManContext = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.GREEN_POWER_PROXY_TABLE_KEY; + const [status, key] = await ezsp.ezspExportKey(context); if (status === SLStatus.OK) { - logger.info(`Green Power Proxy Table Key: ${key.contents.toString('hex')}`) + logger.info(`Green Power Proxy Table Key: ${key.contents.toString("hex")}`); } else if (status !== SLStatus.NOT_FOUND) { - logger.error(`Failed to export Green Power Proxy Table Key with status=${SLStatus[status]}.`) + logger.error(`Failed to export Green Power Proxy Table Key with status=${SLStatus[status]}.`); } } { - const context: SecManContext = initSecurityManagerContext() - context.coreKeyType = SecManKeyType.GREEN_POWER_SINK_TABLE_KEY - const [status, key] = await ezsp.ezspExportKey(context) + const context: SecManContext = initSecurityManagerContext(); + context.coreKeyType = SecManKeyType.GREEN_POWER_SINK_TABLE_KEY; + const [status, key] = await ezsp.ezspExportKey(context); if (status === SLStatus.OK) { - logger.info(`Green Power Sink Table Key: ${key.contents.toString('hex')}`) + logger.info(`Green Power Sink Table Key: ${key.contents.toString("hex")}`); } else if (status !== SLStatus.NOT_FOUND) { - logger.error(`Failed to export Green Power Sink Table Key with status=${SLStatus[status]}.`) + logger.error(`Failed to export Green Power Sink Table Key with status=${SLStatus[status]}.`); } } - return false + return false; } private async menuStackConfig(ezsp: Ezsp): Promise { - let saveFile: string | undefined = undefined + let saveFile: string | undefined = undefined; - if (await confirm({ default: false, message: 'Save to file? (Only print if not)' })) { - saveFile = await browseToFile('Config save location (JSON)', DEFAULT_STACK_CONFIG_PATH, true) + if (await confirm({ default: false, message: "Save to file? (Only print if not)" })) { + saveFile = await browseToFile("Config save location (JSON)", DEFAULT_STACK_CONFIG_PATH, true); } - const stackConfig: ConfigValue = {} + const stackConfig: ConfigValue = {}; for (const key of Object.keys(EzspConfigId)) { - const configId = EzspConfigId[key as keyof typeof EzspConfigId] + const configId = EzspConfigId[key as keyof typeof EzspConfigId]; - if (typeof configId !== 'number') { - continue + if (typeof configId !== "number") { + continue; } - const [status, value] = await ezsp.ezspGetConfigurationValue(configId) + const [status, value] = await ezsp.ezspGetConfigurationValue(configId); - stackConfig[`CONFIG.${key}`] = status === SLStatus.OK ? `${value}` : SLStatus[status] + stackConfig[`CONFIG.${key}`] = status === SLStatus.OK ? `${value}` : SLStatus[status]; } { // needs special handling due to bitmask, excluded from below for-loop - const [status, value] = await ezsp.ezspGetPolicy(EzspPolicyId.TRUST_CENTER_POLICY) - const tcDecisions = [] + const [status, value] = await ezsp.ezspGetPolicy(EzspPolicyId.TRUST_CENTER_POLICY); + const tcDecisions = []; for (const key of Object.keys(EzspDecisionBitmask)) { - const bitmask = EzspDecisionBitmask[key as keyof typeof EzspDecisionBitmask] + const bitmask = EzspDecisionBitmask[key as keyof typeof EzspDecisionBitmask]; - if (typeof bitmask !== 'number') { - continue + if (typeof bitmask !== "number") { + continue; } if ((value & bitmask) !== 0) { - tcDecisions.push(key) + tcDecisions.push(key); } } - stackConfig[`POLICY.TRUST_CENTER_POLICY`] = status === SLStatus.OK ? tcDecisions.join(',') : SLStatus[status] + stackConfig["POLICY.TRUST_CENTER_POLICY"] = status === SLStatus.OK ? tcDecisions.join(",") : SLStatus[status]; } for (const key of Object.keys(EzspPolicyId)) { - const policyId = EzspPolicyId[key as keyof typeof EzspPolicyId] + const policyId = EzspPolicyId[key as keyof typeof EzspPolicyId]; - if (typeof policyId !== 'number' || policyId === EzspPolicyId.TRUST_CENTER_POLICY) { - continue + if (typeof policyId !== "number" || policyId === EzspPolicyId.TRUST_CENTER_POLICY) { + continue; } - const [status, value] = await ezsp.ezspGetPolicy(policyId) + const [status, value] = await ezsp.ezspGetPolicy(policyId); - stackConfig[`POLICY.${key}`] = status === SLStatus.OK ? EzspDecisionId[value] : SLStatus[status] + stackConfig[`POLICY.${key}`] = status === SLStatus.OK ? EzspDecisionId[value] : SLStatus[status]; } { // needs special handling due to zero-conflict with `FIRST`, excluded from below for-loop - const status = await ezsp.ezspGetLibraryStatus(EmberLibraryId.ZIGBEE_PRO) - stackConfig[`LIBRARY.ZIGBEE_PRO`] = getLibraryStatus(EmberLibraryId.ZIGBEE_PRO, status) + const status = await ezsp.ezspGetLibraryStatus(EmberLibraryId.ZIGBEE_PRO); + stackConfig["LIBRARY.ZIGBEE_PRO"] = getLibraryStatus(EmberLibraryId.ZIGBEE_PRO, status); } for (let i = EmberLibraryId.FIRST + 1; i < EmberLibraryId.NUMBER_OF_LIBRARIES; i++) { - const status = await ezsp.ezspGetLibraryStatus(i) - stackConfig[`LIBRARY.${EmberLibraryId[i]}`] = getLibraryStatus(i, status) + const status = await ezsp.ezspGetLibraryStatus(i); + stackConfig[`LIBRARY.${EmberLibraryId[i]}`] = getLibraryStatus(i, status); } for (const key of Object.keys(EzspMfgTokenId)) { - const tokenId = EzspMfgTokenId[key as keyof typeof EzspMfgTokenId] + const tokenId = EzspMfgTokenId[key as keyof typeof EzspMfgTokenId]; - if (typeof tokenId !== 'number') { - continue + if (typeof tokenId !== "number") { + continue; } - const [, tokenData] = await ezsp.ezspGetMfgToken(tokenId) + const [, tokenData] = await ezsp.ezspGetMfgToken(tokenId); - stackConfig[`MFG_TOKEN.${key}`] = `${tokenData.join(',')}` + stackConfig[`MFG_TOKEN.${key}`] = `${tokenData.join(",")}`; } for (const key of Object.keys(stackConfig)) { - logger.info(`${key} = ${stackConfig[key]}.`) + logger.info(`${key} = ${stackConfig[key]}.`); } if (saveFile !== undefined) { - writeFileSync(saveFile, JSON.stringify(stackConfig, null, 2), 'utf8') - logger.info(`Stack config written to '${saveFile}'.`) + writeFileSync(saveFile, JSON.stringify(stackConfig, null, 2), "utf8"); + logger.info(`Stack config written to '${saveFile}'.`); } - return false + return false; } private async menuStackInfo(): Promise { - logger.info(`EmberZNet: ${emberFullVersion.revision}. EZSP: ${emberFullVersion.ezsp}`) - return false + logger.info(`EmberZNet: ${emberFullVersion.revision}. EZSP: ${emberFullVersion.ezsp}`); + return false; } private async menuTokensBackup(ezsp: Ezsp): Promise { - const saveFile = await browseToFile('Tokens backup save file', DEFAULT_TOKENS_BACKUP_PATH, true) - const eui64 = await ezsp.ezspGetEui64() - const tokensBuf = await EmberTokensManager.saveTokens(ezsp, Buffer.from(eui64.slice(2 /* 0x */), 'hex').reverse()) + const saveFile = await browseToFile("Tokens backup save file", DEFAULT_TOKENS_BACKUP_PATH, true); + const eui64 = await ezsp.ezspGetEui64(); + const tokensBuf = await EmberTokensManager.saveTokens(ezsp, Buffer.from(eui64.slice(2 /* 0x */), "hex").reverse()); if (tokensBuf) { - writeFileSync(saveFile, tokensBuf.toString('hex'), 'utf8') + writeFileSync(saveFile, tokensBuf.toString("hex"), "utf8"); - logger.info(`Tokens backup written to '${saveFile}'.`) + logger.info(`Tokens backup written to '${saveFile}'.`); } else { - logger.error(`Failed to backup tokens.`) + logger.error("Failed to backup tokens."); } - return false + return false; } private async menuTokensReset(ezsp: Ezsp): Promise { const confirmed = await confirm({ default: false, - message: 'Confirm tokens reset? (Cannot be undone without a backup.)', - }) + message: "Confirm tokens reset? (Cannot be undone without a backup.)", + }); if (!confirmed) { - logger.info(`Tokens reset cancelled.`) - return false + logger.info("Tokens reset cancelled."); + return false; } const options = await checkbox({ choices: [ - { checked: false, name: 'Exclude network and APS outgoing frame counter tokens?', value: 'excludeOutgoingFC' }, - { checked: false, name: 'Exclude stack boot counter token?', value: 'excludeBootCounter' }, + { checked: false, name: "Exclude network and APS outgoing frame counter tokens?", value: "excludeOutgoingFC" }, + { checked: false, name: "Exclude stack boot counter token?", value: "excludeBootCounter" }, ], - message: 'Reset options', - }) + message: "Reset options", + }); - await ezsp.ezspTokenFactoryReset(options.includes('excludeOutgoingFC'), options.includes('excludeBootCounter')) + await ezsp.ezspTokenFactoryReset(options.includes("excludeOutgoingFC"), options.includes("excludeBootCounter")); - return true + return true; } private async menuTokensRestore(ezsp: Ezsp): Promise { - const backupFile = await browseToFile('Tokens backup file location', DEFAULT_TOKENS_BACKUP_PATH) - let tokensBuf = Buffer.from(readFileSync(backupFile, 'utf8'), 'hex') + const backupFile = await browseToFile("Tokens backup file location", DEFAULT_TOKENS_BACKUP_PATH); + let tokensBuf = Buffer.from(readFileSync(backupFile, "utf8"), "hex"); { // check for binding table corrupting NVM3 if size is too large (32 tested as "safe") - let bindingTableMod: undefined | { arraySizeOffset: number; clipStartOffset: number; clipEndOffset: number } - let readOffset: number = 0 - const inTokenCount = tokensBuf.readUInt8(readOffset++) + let bindingTableMod: undefined | { arraySizeOffset: number; clipStartOffset: number; clipEndOffset: number }; + let readOffset = 0; + const inTokenCount = tokensBuf.readUInt8(readOffset++); for (let i = 0; i < inTokenCount; i++) { - const nvm3Key = tokensBuf.readUInt32LE(readOffset) // 4 bytes Token Key/Creator - readOffset += 4 - const size = tokensBuf.readUInt8(readOffset++) // 1 byte token size - const arraySize = tokensBuf.readUInt8(readOffset++) // 1 byte array size. + const nvm3Key = tokensBuf.readUInt32LE(readOffset); // 4 bytes Token Key/Creator + readOffset += 4; + const size = tokensBuf.readUInt8(readOffset++); // 1 byte token size + const arraySize = tokensBuf.readUInt8(readOffset++); // 1 byte array size. if (nvm3Key === NVM3ObjectKey.STACK_BINDING_TABLE && arraySize > 32) { - logger.warning(`Binding table is too large, which is known to corrupt NVM3, keeping only first 32 entries.`) + logger.warning("Binding table is too large, which is known to corrupt NVM3, keeping only first 32 entries."); bindingTableMod = { arraySizeOffset: readOffset - 1, clipStartOffset: readOffset + 32 * size, clipEndOffset: readOffset + arraySize * size, - } + }; } - readOffset += arraySize * size + readOffset += arraySize * size; } if (bindingTableMod) { - tokensBuf[bindingTableMod.arraySizeOffset] = 32 + tokensBuf[bindingTableMod.arraySizeOffset] = 32; - const tokensBufStart = tokensBuf.subarray(0, bindingTableMod.clipStartOffset) - const tokensBufEnd = tokensBuf.subarray(bindingTableMod.clipEndOffset) - tokensBuf = Buffer.concat([tokensBufStart, tokensBufEnd]) - const saveFile = `${backupFile}-fixed.nvm3` + const tokensBufStart = tokensBuf.subarray(0, bindingTableMod.clipStartOffset); + const tokensBufEnd = tokensBuf.subarray(bindingTableMod.clipEndOffset); + tokensBuf = Buffer.concat([tokensBufStart, tokensBufEnd]); + const saveFile = `${backupFile}-fixed.nvm3`; - writeFileSync(saveFile, tokensBuf.toString('hex'), 'utf8') + writeFileSync(saveFile, tokensBuf.toString("hex"), "utf8"); - logger.info(`Fixed tokens backup written to '${saveFile}'.`) + logger.info(`Fixed tokens backup written to '${saveFile}'.`); } } - const status = await EmberTokensManager.restoreTokens(ezsp, tokensBuf) + const status = await EmberTokensManager.restoreTokens(ezsp, tokensBuf); if (status === SLStatus.OK) { - logger.info(`Restored tokens.`) + logger.info("Restored tokens."); } else { - logger.error(`Failed to restore tokens.`) + logger.error("Failed to restore tokens."); } - return true + return true; } private async menuTokensWriteEUI64(ezsp: Ezsp): Promise { - let tokenKey: number | undefined + let tokenKey: number | undefined; - const currentEUI64 = await ezsp.ezspGetEui64() + const currentEUI64 = await ezsp.ezspGetEui64(); - logger.info(`Current EUI64: ${currentEUI64} (${Buffer.from(currentEUI64.slice(2), 'hex').reverse().toString('hex')}).`) + logger.info(`Current EUI64: ${currentEUI64} (${Buffer.from(currentEUI64.slice(2), "hex").reverse().toString("hex")}).`); for (const key of [NVM3ObjectKey.STACK_RESTORED_EUI64, CREATOR_STACK_RESTORED_EUI64]) { - const [status, tokenData] = await ezsp.ezspGetTokenData(key, 0) + const [status, tokenData] = await ezsp.ezspGetTokenData(key, 0); if (status === SLStatus.OK) { - logger.info(`Current restored EUI64 token (${key}): ${eui64LEBufferToHex(tokenData.data)} (${tokenData.data.toString('hex')}).`) + logger.info(`Current restored EUI64 token (${key}): ${eui64LEBufferToHex(tokenData.data)} (${tokenData.data.toString("hex")}).`); - tokenKey = key - break + tokenKey = key; + break; } } if (tokenKey === undefined) { - logger.error(`Unable to write EUI64, operation not supported by firmware.`) + logger.error("Unable to write EUI64, operation not supported by firmware."); - return false + return false; } - const enum Source { + enum Source { FILE = 0, INPUT = 1, } const source = await select<-1 | Source>({ choices: [ - { name: 'From coordinator backup file', value: Source.FILE }, + { name: "From coordinator backup file", value: Source.FILE }, { name: `From manual input (format: ${ZSpec.BLANK_EUI64})`, value: Source.INPUT }, - { name: 'Go Back', value: -1 }, + { name: "Go Back", value: -1 }, ], - message: 'Source for the EUI64', - }) - let eui64Hex: string | undefined - let eui64: Buffer | undefined + message: "Source for the EUI64", + }); + let eui64Hex: string | undefined; + let eui64: Buffer | undefined; switch (source) { case Source.FILE: { - const backupFile = await browseToFile('File location', DEFAULT_NETWORK_BACKUP_PATH) - const backup = getBackupFromFile(backupFile) + const backupFile = await browseToFile("File location", DEFAULT_NETWORK_BACKUP_PATH); + const backup = getBackupFromFile(backupFile); if (backup === undefined) { // error logged in getBackupFromFile - return false + return false; } - eui64 = backup.coordinatorIeeeAddress - eui64Hex = `0x${Buffer.from(eui64).reverse().toString('hex')}` + eui64 = backup.coordinatorIeeeAddress; + eui64Hex = `0x${Buffer.from(eui64).reverse().toString("hex")}`; - break + break; } case Source.INPUT: { eui64Hex = await input({ - message: 'EUI64', + message: "EUI64", default: ZSpec.BLANK_EUI64, validate(value) { - return /^0x[0-9a-f]{16}$/i.test(value) + return /^0x[0-9a-f]{16}$/i.test(value); }, - }) - eui64 = Buffer.from(eui64Hex.slice(2 /* 0x */), 'hex').reverse() + }); + eui64 = Buffer.from(eui64Hex.slice(2 /* 0x */), "hex").reverse(); - break + break; } case -1: { - return false + return false; } } if (!eui64) { - logger.error(`Invalid EUI64, cannot procede.`) + logger.error("Invalid EUI64, cannot procede."); - return false + return false; } - logger.info(`Writing EUI64 ${eui64Hex} (${eui64.toString('hex')}).`) + logger.info(`Writing EUI64 ${eui64Hex} (${eui64.toString("hex")}).`); - const status = await ezsp.ezspSetTokenData(tokenKey, 0, { data: eui64, size: eui64.length }) + const status = await ezsp.ezspSetTokenData(tokenKey, 0, { data: eui64, size: eui64.length }); if (status !== SLStatus.OK) { - logger.error(`Failed to write EUI64 with status=${SLStatus[status]}.`) + logger.error(`Failed to write EUI64 with status=${SLStatus[status]}.`); } - return true + return true; } private async menuZigbee2MQTTOnboard(ezsp: Ezsp): Promise { - let status = await emberNetworkInit(ezsp) - const notJoined = status === SLStatus.NOT_JOINED + let status = await emberNetworkInit(ezsp); + const notJoined = status === SLStatus.NOT_JOINED; if (!notJoined && status !== SLStatus.OK) { - logger.error(`Failed network init request with status=${SLStatus[status]}.`) - return true + logger.error(`Failed network init request with status=${SLStatus[status]}.`); + return true; } if (!notJoined) { - const overwrite = await confirm({ default: false, message: 'A network is present in the adapter. Leave and continue onboard?' }) + const overwrite = await confirm({ default: false, message: "A network is present in the adapter. Leave and continue onboard?" }); if (!overwrite) { - logger.info(`Onboard cancelled.`) - return false + logger.info("Onboard cancelled."); + return false; } - const status = await ezsp.ezspLeaveNetwork() + const status = await ezsp.ezspLeaveNetwork(); if (status !== SLStatus.OK) { - logger.error(`Failed to leave network with status=${SLStatus[status]}.`) - return true + logger.error(`Failed to leave network with status=${SLStatus[status]}.`); + return true; } - await waitForStackStatus(ezsp, SLStatus.NETWORK_DOWN) + await waitForStackStatus(ezsp, SLStatus.NETWORK_DOWN); } // set desired tx power before scan const radioTxPower = Number.parseInt( await input({ - default: '5', - message: 'Radio transmit power [-128-127]', + default: "5", + message: "Radio transmit power [-128-127]", validate(value) { if (/\./.test(value)) { - return false + return false; } - const v = Number.parseInt(value, 10) + const v = Number.parseInt(value, 10); - return v >= -128 && v <= 127 + return v >= -128 && v <= 127; }, }), 10, - ) + ); - status = await ezsp.ezspSetRadioPower(radioTxPower) + status = await ezsp.ezspSetRadioPower(radioTxPower); if (status !== SLStatus.OK) { - logger.error(`Failed to set transmit power to ${radioTxPower} status=${SLStatus[status]}.`) - return true + logger.error(`Failed to set transmit power to ${radioTxPower} status=${SLStatus[status]}.`); + return true; } // WiFi beacon frames at standard interval: 102.4msec const duration = await select({ choices: [ - { name: '3948 msec', value: 8 }, - { name: '1981 msec', value: 7 }, - { name: '998 msec', value: 6 }, - { name: '507 msec', value: 5 }, - { name: '261 msec', value: 4 }, - { name: '138 msec', value: 3 }, + { name: "3948 msec", value: 8 }, + { name: "1981 msec", value: 7 }, + { name: "998 msec", value: 6 }, + { name: "507 msec", value: 5 }, + { name: "261 msec", value: 4 }, + { name: "138 msec", value: 3 }, // { name: '77 msec', value: 2 }, // { name: '46 msec', value: 1 }, // { name: '31 msec', value: 0 }, ], default: 6, - message: 'Duration of scan per channel', - }) + message: "Duration of scan per channel", + }); const channels = await checkbox({ choices: ZSpec.ALL_802_15_4_CHANNELS.map((c) => ({ name: c.toString(), value: c, checked: TOUCHLINK_CHANNELS.includes(c) })), - message: 'Channels to consider', + message: "Channels to consider", required: true, - }) + }); - const progressBar = new SingleBar({ clearOnComplete: true, format: '{bar} {percentage}% | ETA: {eta}s' }, Presets.shades_classic) + const progressBar = new SingleBar({ clearOnComplete: true, format: "{bar} {percentage}% | ETA: {eta}s" }, Presets.shades_classic); // a symbol is 16 microseconds, a scan period is 960 symbols - const totalTime = (((2 ** duration + 1) * (16 * 960)) / 1000) * channels.length - let scanCompleted: (value: [panId: number, channel: number] | PromiseLike<[panId: number, channel: number]>) => void + const totalTime = (((2 ** duration + 1) * (16 * 960)) / 1000) * channels.length; + let scanCompleted: (value: [panId: number, channel: number] | PromiseLike<[panId: number, channel: number]>) => void; // NOTE: expanding zigbee-herdsman - const ezspUnusedPanIdFoundHandlerOriginal = ezsp.ezspUnusedPanIdFoundHandler + const ezspUnusedPanIdFoundHandlerOriginal = ezsp.ezspUnusedPanIdFoundHandler; ezsp.ezspUnusedPanIdFoundHandler = (panId: PanId, channel: number): void => { - logger.debug(`ezspUnusedPanIdFoundHandler: ${JSON.stringify({ panId, channel })}`) - progressBar.stop() - clearInterval(progressInterval) + logger.debug(`ezspUnusedPanIdFoundHandler: ${JSON.stringify({ panId, channel })}`); + progressBar.stop(); + clearInterval(progressInterval); if (scanCompleted) { - scanCompleted([panId, channel]) + scanCompleted([panId, channel]); } - } + }; - const scanStatus = await ezsp.ezspFindUnusedPanId(ZSpec.Utils.channelsToUInt32Mask(channels), duration) + const scanStatus = await ezsp.ezspFindUnusedPanId(ZSpec.Utils.channelsToUInt32Mask(channels), duration); if (scanStatus !== SLStatus.OK) { - logger.error(`Failed find unused PAN ID request with status=${SLStatus[scanStatus]}.`) + logger.error(`Failed find unused PAN ID request with status=${SLStatus[scanStatus]}.`); // restore zigbee-herdsman default - ezsp.ezspUnusedPanIdFoundHandler = ezspUnusedPanIdFoundHandlerOriginal - return true + ezsp.ezspUnusedPanIdFoundHandler = ezspUnusedPanIdFoundHandlerOriginal; + return true; } - progressBar.start(totalTime, 0) + progressBar.start(totalTime, 0); const progressInterval = setInterval(() => { - progressBar.increment(500) - }, 500) + progressBar.increment(500); + }, 500); const result = await new Promise<[panId: PanId, channel: number]>((resolve) => { - scanCompleted = resolve - }) + scanCompleted = resolve; + }); // restore zigbee-herdsman default - ezsp.ezspUnusedPanIdFoundHandler = ezspUnusedPanIdFoundHandlerOriginal + ezsp.ezspUnusedPanIdFoundHandler = ezspUnusedPanIdFoundHandlerOriginal; // just in case if (!result) { - logger.error(`Found no suitable PAN ID and channel.`) - return true + logger.error("Found no suitable PAN ID and channel."); + return true; } - const [foundPanId, foundChannel] = result + const [foundPanId, foundChannel] = result; const confirmForm = await confirm({ message: `Found suitable PAN ID=${foundPanId}, channel=${foundChannel}. Continue with these parameters?`, default: true, - }) + }); if (!confirmForm) { - logger.info(`Onboard cancelled.`) - return true + logger.info("Onboard cancelled."); + return true; } const state: EmberInitialSecurityState = { @@ -1271,26 +1271,26 @@ export default class Stack extends Command { networkKey: { contents: randomBytes(ZSpec.DEFAULT_ENCRYPTION_KEY_SIZE) }, networkKeySequenceNumber: 0, preconfiguredTrustCenterEui64: ZSpec.BLANK_EUI64, - } + }; - status = await ezsp.ezspSetInitialSecurityState(state) + status = await ezsp.ezspSetInitialSecurityState(state); if (status !== SLStatus.OK) { - throw new Error(`Failed to set initial security state with status=${SLStatus[status]}.`) + throw new Error(`Failed to set initial security state with status=${SLStatus[status]}.`); } const extended: EmberExtendedSecurityBitmask = - EmberExtendedSecurityBitmask.JOINER_GLOBAL_LINK_KEY | EmberExtendedSecurityBitmask.NWK_LEAVE_REQUEST_NOT_ALLOWED - status = await ezsp.ezspSetExtendedSecurityBitmask(extended) + EmberExtendedSecurityBitmask.JOINER_GLOBAL_LINK_KEY | EmberExtendedSecurityBitmask.NWK_LEAVE_REQUEST_NOT_ALLOWED; + status = await ezsp.ezspSetExtendedSecurityBitmask(extended); if (status !== SLStatus.OK) { - throw new Error(`Failed to set extended security bitmask to ${extended} with status=${SLStatus[status]}.`) + throw new Error(`Failed to set extended security bitmask to ${extended} with status=${SLStatus[status]}.`); } - status = await ezsp.ezspClearKeyTable() + status = await ezsp.ezspClearKeyTable(); if (status !== SLStatus.OK) { - logger.error(`Failed to clear key table with status=${SLStatus[status]}.`) + logger.error(`Failed to clear key table with status=${SLStatus[status]}.`); } const netParams: EmberNetworkParameters = { @@ -1302,47 +1302,47 @@ export default class Stack extends Command { nwkManagerId: ZSpec.COORDINATOR_ADDRESS, nwkUpdateId: 0, channels: ZSpec.ALL_802_15_4_CHANNELS_MASK, - } + }; - logger.info(`Forming new network with: ${JSON.stringify(netParams)}`) + logger.info(`Forming new network with: ${JSON.stringify(netParams)}`); - status = await ezsp.ezspFormNetwork(netParams) + status = await ezsp.ezspFormNetwork(netParams); if (status !== SLStatus.OK) { - throw new Error(`Failed form network request with status=${SLStatus[status]}.`) + throw new Error(`Failed form network request with status=${SLStatus[status]}.`); } - await waitForStackStatus(ezsp, SLStatus.NETWORK_UP) + await waitForStackStatus(ezsp, SLStatus.NETWORK_UP); - status = await ezsp.ezspStartWritingStackTokens() + status = await ezsp.ezspStartWritingStackTokens(); - logger.debug(`Start writing stack tokens status=${SLStatus[status]}.`) - logger.info(`New network formed!`) + logger.debug(`Start writing stack tokens status=${SLStatus[status]}.`); + logger.info("New network formed!"); // grab the actual parameters (should anything have gone wrong, this will hard fail) - const [npStatus, , actualNetParams] = await ezsp.ezspGetNetworkParameters() + const [npStatus, , actualNetParams] = await ezsp.ezspGetNetworkParameters(); if (npStatus !== SLStatus.OK) { - throw new Error(`Failed to get network parameters with status=${SLStatus[npStatus]}.`) + throw new Error(`Failed to get network parameters with status=${SLStatus[npStatus]}.`); } // write partial `configuration.yaml` - const saveFile = await browseToFile('Configuration save file', DEFAULT_CONFIGURATION_YAML_PATH, true) + const saveFile = await browseToFile("Configuration save file", DEFAULT_CONFIGURATION_YAML_PATH, true); const extensions = await checkbox({ choices: [ - { name: 'Frontend', value: 'frontend', checked: true }, - { name: 'Home Assistant', value: 'homeassistant', checked: false }, + { name: "Frontend", value: "frontend", checked: true }, + { name: "Home Assistant", value: "homeassistant", checked: false }, ], - message: 'Extensions to enable', - }) - const enableExtFrontend = extensions.includes('frontend') - const enableExtHomeAssistant = extensions.includes('homeassistant') + message: "Extensions to enable", + }); + const enableExtFrontend = extensions.includes("frontend"); + const enableExtHomeAssistant = extensions.includes("homeassistant"); const mqttServer = await input({ - message: 'Address of the MQTT server', - default: enableExtHomeAssistant ? 'mqtt://core-mosquitto:1883' : 'mqtt://localhost:1883', - }) + message: "Address of the MQTT server", + default: enableExtHomeAssistant ? "mqtt://core-mosquitto:1883" : "mqtt://localhost:1883", + }); // @ts-expect-error private, avoids passing portConf around just for this - const { baudRate, path, rtscts } = ezsp.ash.portOptions + const { baudRate, path, rtscts } = ezsp.ash.portOptions; const yaml = ` mqtt: @@ -1364,95 +1364,95 @@ frontend: enabled: ${enableExtFrontend} homeassistant: enabled: ${enableExtHomeAssistant} -` +`; - writeFileSync(saveFile, yaml, 'utf8') + writeFileSync(saveFile, yaml, "utf8"); - logger.info(`Zigbee2MQTT starter configuration written to '${saveFile}'. Adjust it as necessary (port, etc.).`) + logger.info(`Zigbee2MQTT starter configuration written to '${saveFile}'. Adjust it as necessary (port, etc.).`); - return true + return true; } private async navigateMenu(ezsp: Ezsp): Promise { const answer = await select<-1 | StackMenu>({ choices: [ - { name: 'Get stack info', value: StackMenu.STACK_INFO }, - { name: 'Get stack config (firmware defaults)', value: StackMenu.STACK_CONFIG }, - { name: 'Get network info', value: StackMenu.NETWORK_INFO }, - { name: 'Scan network', value: StackMenu.NETWORK_SCAN }, - { name: 'Backup network', value: StackMenu.NETWORK_BACKUP }, - { name: 'Restore network', value: StackMenu.NETWORK_RESTORE }, - { name: 'Leave network', value: StackMenu.NETWORK_LEAVE }, - { name: 'Backup NVM3 tokens', value: StackMenu.TOKENS_BACKUP }, - { name: 'Restore NVM3 tokens', value: StackMenu.TOKENS_RESTORE }, - { name: 'Reset NVM3 tokens', value: StackMenu.TOKENS_RESET }, - { name: 'Write EUI64 NVM3 token', value: StackMenu.TOKENS_WRITE_EUI64 }, - { name: 'Get security info', value: StackMenu.SECURITY_INFO }, - { name: 'Zigbee2MQTT Onboard (auto configuration)', value: StackMenu.ZIGBEE2MQTT_ONBOARD }, - { name: 'Repairs', value: StackMenu.REPAIRS }, - { name: 'Exit', value: -1 }, + { name: "Get stack info", value: StackMenu.STACK_INFO }, + { name: "Get stack config (firmware defaults)", value: StackMenu.STACK_CONFIG }, + { name: "Get network info", value: StackMenu.NETWORK_INFO }, + { name: "Scan network", value: StackMenu.NETWORK_SCAN }, + { name: "Backup network", value: StackMenu.NETWORK_BACKUP }, + { name: "Restore network", value: StackMenu.NETWORK_RESTORE }, + { name: "Leave network", value: StackMenu.NETWORK_LEAVE }, + { name: "Backup NVM3 tokens", value: StackMenu.TOKENS_BACKUP }, + { name: "Restore NVM3 tokens", value: StackMenu.TOKENS_RESTORE }, + { name: "Reset NVM3 tokens", value: StackMenu.TOKENS_RESET }, + { name: "Write EUI64 NVM3 token", value: StackMenu.TOKENS_WRITE_EUI64 }, + { name: "Get security info", value: StackMenu.SECURITY_INFO }, + { name: "Zigbee2MQTT Onboard (auto configuration)", value: StackMenu.ZIGBEE2MQTT_ONBOARD }, + { name: "Repairs", value: StackMenu.REPAIRS }, + { name: "Exit", value: -1 }, ], - message: 'Menu', - }) + message: "Menu", + }); switch (answer) { case StackMenu.STACK_INFO: { - return await this.menuStackInfo() + return await this.menuStackInfo(); } case StackMenu.STACK_CONFIG: { - return await this.menuStackConfig(ezsp) + return await this.menuStackConfig(ezsp); } case StackMenu.NETWORK_INFO: { - return await this.menuNetworkInfo(ezsp) + return await this.menuNetworkInfo(ezsp); } case StackMenu.NETWORK_SCAN: { - return await this.menuNetworkScan(ezsp) + return await this.menuNetworkScan(ezsp); } case StackMenu.NETWORK_BACKUP: { - return await this.menuNetworkBackup(ezsp) + return await this.menuNetworkBackup(ezsp); } case StackMenu.NETWORK_RESTORE: { - return await this.menuNetworkRestore(ezsp) + return await this.menuNetworkRestore(ezsp); } case StackMenu.NETWORK_LEAVE: { - return await this.menuNetworkLeave(ezsp) + return await this.menuNetworkLeave(ezsp); } case StackMenu.TOKENS_BACKUP: { - return await this.menuTokensBackup(ezsp) + return await this.menuTokensBackup(ezsp); } case StackMenu.TOKENS_RESTORE: { - return await this.menuTokensRestore(ezsp) + return await this.menuTokensRestore(ezsp); } case StackMenu.TOKENS_RESET: { - return await this.menuTokensReset(ezsp) + return await this.menuTokensReset(ezsp); } case StackMenu.TOKENS_WRITE_EUI64: { - return await this.menuTokensWriteEUI64(ezsp) + return await this.menuTokensWriteEUI64(ezsp); } case StackMenu.SECURITY_INFO: { - return await this.menuSecurityInfo(ezsp) + return await this.menuSecurityInfo(ezsp); } case StackMenu.ZIGBEE2MQTT_ONBOARD: { - return await this.menuZigbee2MQTTOnboard(ezsp) + return await this.menuZigbee2MQTTOnboard(ezsp); } case StackMenu.REPAIRS: { - return await this.menuRepairs(ezsp) + return await this.menuRepairs(ezsp); } } - return true // exit + return true; // exit } } diff --git a/src/commands/utils/index.ts b/src/commands/utils/index.ts index 6859ff5..5fc7b30 100644 --- a/src/commands/utils/index.ts +++ b/src/commands/utils/index.ts @@ -1,119 +1,119 @@ -import { readdirSync, readFileSync, rmSync } from 'node:fs' -import { join } from 'node:path' +import { readFileSync, readdirSync, rmSync } from "node:fs"; +import { join } from "node:path"; -import { select } from '@inquirer/prompts' -import { Command } from '@oclif/core' +import { select } from "@inquirer/prompts"; +import { Command } from "@oclif/core"; -import { DEFAULT_TOKENS_BACKUP_PATH, logger, LOGS_FOLDER } from '../../index.js' -import { parseTokenData } from '../../utils/ember.js' -import { NVM3ObjectKey } from '../../utils/enums.js' -import { browseToFile } from '../../utils/utils.js' +import { DEFAULT_TOKENS_BACKUP_PATH, LOGS_FOLDER, logger } from "../../index.js"; +import { parseTokenData } from "../../utils/ember.js"; +import { NVM3ObjectKey } from "../../utils/enums.js"; +import { browseToFile } from "../../utils/utils.js"; -const enum UtilsMenu { +enum UtilsMenu { PARSE_TOKENS_BACKUP_FILE = 10, PURGE_LOG_FILES = 90, } export default class Utils extends Command { - static override args = {} - static override description = 'Execute various utility commands.' - static override examples = ['<%= config.bin %> <%= command.id %>'] - static override flags = {} + static override args = {}; + static override description = "Execute various utility commands."; + static override examples = ["<%= config.bin %> <%= command.id %>"]; + static override flags = {}; public async run(): Promise { // const {args, flags} = await this.parse(Utils) - let exit: boolean = false + let exit = false; while (!exit) { - exit = await this.navigateMenu() + exit = await this.navigateMenu(); } - return this.exit(0) + return this.exit(0); } private async menuParseTokensBackupFile(): Promise { - const backupFile = await browseToFile('Tokens backup file location', DEFAULT_TOKENS_BACKUP_PATH) - const tokensBuf = Buffer.from(readFileSync(backupFile, 'utf8'), 'hex') + const backupFile = await browseToFile("Tokens backup file location", DEFAULT_TOKENS_BACKUP_PATH); + const tokensBuf = Buffer.from(readFileSync(backupFile, "utf8"), "hex"); if (tokensBuf.length === 0) { - logger.error(`Tokens file invalid or empty.`) + logger.error("Tokens file invalid or empty."); - return true + return true; } - let readOffset: number = 0 - const inTokenCount = tokensBuf.readUInt8(readOffset++) + let readOffset = 0; + const inTokenCount = tokensBuf.readUInt8(readOffset++); for (let i = 0; i < inTokenCount; i++) { - const nvm3Key = tokensBuf.readUInt32LE(readOffset) // 4 bytes Token Key/Creator - readOffset += 4 - const size = tokensBuf.readUInt8(readOffset++) // 1 byte token size - const arraySize = tokensBuf.readUInt8(readOffset++) // 1 byte array size. + const nvm3Key = tokensBuf.readUInt32LE(readOffset); // 4 bytes Token Key/Creator + readOffset += 4; + const size = tokensBuf.readUInt8(readOffset++); // 1 byte token size + const arraySize = tokensBuf.readUInt8(readOffset++); // 1 byte array size. for (let arrayIndex = 0; arrayIndex < arraySize; arrayIndex++) { - const parsedTokenData = parseTokenData(nvm3Key, tokensBuf.subarray(readOffset, readOffset + size)) + const parsedTokenData = parseTokenData(nvm3Key, tokensBuf.subarray(readOffset, readOffset + size)); - logger.info(`Token nvm3Key=${NVM3ObjectKey[nvm3Key]} size=${size} token=[${parsedTokenData}]`) + logger.info(`Token nvm3Key=${NVM3ObjectKey[nvm3Key]} size=${size} token=[${parsedTokenData}]`); - readOffset += size + readOffset += size; } } - return false + return false; } private async menuPurgeLogFiles(): Promise { const olderThan = await select({ choices: [ - { name: 'Older than 30 days', value: 3600000 * 24 * 30 }, - { name: 'Older than 7 days', value: 3600000 * 24 * 7 }, - { name: 'Older than 1 day', value: 3600000 * 24 }, - { name: 'Older than 1 hour', value: 3600000 }, - { name: 'All', value: -1 }, + { name: "Older than 30 days", value: 3600000 * 24 * 30 }, + { name: "Older than 7 days", value: 3600000 * 24 * 7 }, + { name: "Older than 1 day", value: 3600000 * 24 }, + { name: "Older than 1 hour", value: 3600000 }, + { name: "All", value: -1 }, ], - message: 'Timeframe', - }) + message: "Timeframe", + }); - let count = 0 + let count = 0; // -1 == never process last (currently used) for (const file of readdirSync(LOGS_FOLDER).slice(0, -1)) { - const match = file.match(/^ember-zli-(\d+)\.log$/) + const match = file.match(/^ember-zli-(\d+)\.log$/); if (match) { - if (olderThan === -1 || parseInt(match[1], 10) < Date.now() - olderThan) { - rmSync(join(LOGS_FOLDER, file), { force: true }) + if (olderThan === -1 || Number.parseInt(match[1], 10) < Date.now() - olderThan) { + rmSync(join(LOGS_FOLDER, file), { force: true }); - count++ + count++; } } } - logger.info(`Purged ${count} log files.`) + logger.info(`Purged ${count} log files.`); - return false + return false; } private async navigateMenu(): Promise { const answer = await select<-1 | UtilsMenu>({ choices: [ - { name: 'Parse NVM3 tokens backup file', value: UtilsMenu.PARSE_TOKENS_BACKUP_FILE }, - { name: 'Purge log files', value: UtilsMenu.PURGE_LOG_FILES }, - { name: 'Exit', value: -1 }, + { name: "Parse NVM3 tokens backup file", value: UtilsMenu.PARSE_TOKENS_BACKUP_FILE }, + { name: "Purge log files", value: UtilsMenu.PURGE_LOG_FILES }, + { name: "Exit", value: -1 }, ], - message: 'Menu', - }) + message: "Menu", + }); switch (answer) { case UtilsMenu.PARSE_TOKENS_BACKUP_FILE: { - return await this.menuParseTokensBackupFile() + return await this.menuParseTokensBackupFile(); } case UtilsMenu.PURGE_LOG_FILES: { - return await this.menuPurgeLogFiles() + return await this.menuPurgeLogFiles(); } } - return true // exit + return true; // exit } } diff --git a/src/index.ts b/src/index.ts index 8d17427..aa4f158 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,76 +1,76 @@ -import { existsSync, mkdirSync } from 'node:fs' -import { homedir } from 'node:os' -import { join } from 'node:path' +import { existsSync, mkdirSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; -import { config, createLogger, format, transports } from 'winston' +import { config, createLogger, format, transports } from "winston"; -import { setLogger as zhSetLogger } from 'zigbee-herdsman' +import { setLogger as zhSetLogger } from "zigbee-herdsman"; -export const DATA_FOLDER = join(homedir(), 'ember-zli') -export const LOGS_FOLDER = join(DATA_FOLDER, 'logs') +export const DATA_FOLDER = join(homedir(), "ember-zli"); +export const LOGS_FOLDER = join(DATA_FOLDER, "logs"); -export const CONF_PORT_PATH = join(DATA_FOLDER, 'conf_port.json') -export const CONF_NETWORK_PATH = join(DATA_FOLDER, 'conf_network.json') -export const CONF_STACK = join(DATA_FOLDER, 'conf_stack.json') +export const CONF_PORT_PATH = join(DATA_FOLDER, "conf_port.json"); +export const CONF_NETWORK_PATH = join(DATA_FOLDER, "conf_network.json"); +export const CONF_STACK = join(DATA_FOLDER, "conf_stack.json"); -export const DEFAULT_STACK_CONFIG_PATH = join(DATA_FOLDER, 'stack_config.json') -export const DEFAULT_NETWORK_BACKUP_PATH = join(DATA_FOLDER, 'coordinator_backup.json') -export const DEFAULT_TOKENS_BACKUP_PATH = join(DATA_FOLDER, 'tokens_backup.nvm3') -export const DEFAULT_ROUTER_TOKENS_BACKUP_PATH = join(DATA_FOLDER, 'router_tokens_backup.nvm3') -export const DEFAULT_CONFIGURATION_YAML_PATH = join(DATA_FOLDER, 'configuration.yaml') -export const DEFAULT_FIRMWARE_GBL_PATH = join(DATA_FOLDER, 'firmware.gbl') -export const DEFAULT_ROUTER_SCRIPT_MJS_PATH = join(DATA_FOLDER, 'router_script.mjs') +export const DEFAULT_STACK_CONFIG_PATH = join(DATA_FOLDER, "stack_config.json"); +export const DEFAULT_NETWORK_BACKUP_PATH = join(DATA_FOLDER, "coordinator_backup.json"); +export const DEFAULT_TOKENS_BACKUP_PATH = join(DATA_FOLDER, "tokens_backup.nvm3"); +export const DEFAULT_ROUTER_TOKENS_BACKUP_PATH = join(DATA_FOLDER, "router_tokens_backup.nvm3"); +export const DEFAULT_CONFIGURATION_YAML_PATH = join(DATA_FOLDER, "configuration.yaml"); +export const DEFAULT_FIRMWARE_GBL_PATH = join(DATA_FOLDER, "firmware.gbl"); +export const DEFAULT_ROUTER_SCRIPT_MJS_PATH = join(DATA_FOLDER, "router_script.mjs"); -export const DEFAULT_PCAP_PATH = join(DATA_FOLDER, 'sniff.pcap') +export const DEFAULT_PCAP_PATH = join(DATA_FOLDER, "sniff.pcap"); if (!existsSync(DATA_FOLDER)) { - mkdirSync(DATA_FOLDER) + mkdirSync(DATA_FOLDER); } if (!existsSync(LOGS_FOLDER)) { - mkdirSync(LOGS_FOLDER) + mkdirSync(LOGS_FOLDER); } export const logger = createLogger({ format: format.combine( format.errors({ stack: true }), format.timestamp({ - format: 'YYYY-MM-DD hh:mm:ss.SSS', + format: "YYYY-MM-DD hh:mm:ss.SSS", }), - format.printf((info) => `[${info.timestamp}] ${info.level}: \t${info.namespace ?? 'cli'}: ${info.message}`), + format.printf((info) => `[${info.timestamp}] ${info.level}: \t${info.namespace ?? "cli"}: ${info.message}`), ), levels: config.syslog.levels, transports: [ new transports.Console({ - format: format.colorize({ all: true, colors: { debug: 'blue', error: 'red', info: 'green', warning: 'yellow' } }), - level: 'info', + format: format.colorize({ all: true, colors: { debug: "blue", error: "red", info: "green", warning: "yellow" } }), + level: "info", }), new transports.File({ filename: join(LOGS_FOLDER, `ember-zli-${Date.now()}.log`), - level: 'debug', + level: "debug", }), ], -}) +}); const getZHMessage = (messageOrLambda: string | (() => string)): string => { - return messageOrLambda instanceof Function ? messageOrLambda() : messageOrLambda -} + return messageOrLambda instanceof Function ? messageOrLambda() : messageOrLambda; +}; zhSetLogger({ debug(message, namespace) { - logger.debug(getZHMessage(message), { namespace }) + logger.debug(getZHMessage(message), { namespace }); }, error(message, namespace) { - logger.error(getZHMessage(message), { namespace }) + logger.error(getZHMessage(message), { namespace }); }, info(message, namespace) { - logger.info(getZHMessage(message), { namespace }) + logger.info(getZHMessage(message), { namespace }); }, warning(message, namespace) { - logger.warning(getZHMessage(message), { namespace }) + logger.warning(getZHMessage(message), { namespace }); }, -}) +}); -logger.info(`Data folder: ${DATA_FOLDER}.`) +logger.info(`Data folder: ${DATA_FOLDER}.`); -export { run } from '@oclif/core' +export { run } from "@oclif/core"; diff --git a/src/utils/bootloader.ts b/src/utils/bootloader.ts index d8dce59..cdd2d14 100644 --- a/src/utils/bootloader.ts +++ b/src/utils/bootloader.ts @@ -1,21 +1,21 @@ -import type { AdapterModel, FirmwareFileMetadata, PortConf } from './types.js' +import type { AdapterModel, FirmwareFileMetadata, PortConf } from "./types.js"; -import EventEmitter from 'node:events' -import { crc32 } from 'zlib' +import EventEmitter from "node:events"; +import { crc32 } from "node:zlib"; -import { confirm } from '@inquirer/prompts' +import { confirm } from "@inquirer/prompts"; -import { SLStatus } from 'zigbee-herdsman/dist/adapter/ember/enums.js' +import { SLStatus } from "zigbee-herdsman/dist/adapter/ember/enums.js"; -import { logger } from '../index.js' -import { TCP_REGEX } from './consts.js' -import { Cpc, CpcEvent } from './cpc.js' -import { emberStart, emberStop } from './ember.js' -import { CpcSystemStatus, FirmwareValidation } from './enums.js' -import { Transport, TransportEvent } from './transport.js' -import { XEvent, XExitStatus, XModemCRC } from './xmodem.js' +import { logger } from "../index.js"; +import { TCP_REGEX } from "./consts.js"; +import { Cpc, CpcEvent } from "./cpc.js"; +import { emberStart, emberStop } from "./ember.js"; +import { CpcSystemStatus, FirmwareValidation } from "./enums.js"; +import { Transport, TransportEvent } from "./transport.js"; +import { XEvent, type XExitStatus, XModemCRC } from "./xmodem.js"; -const NS = { namespace: 'gecko' } +const NS = { namespace: "gecko" }; export enum BootloaderState { /** Not connected to bootloader (i.e. not any of below) */ @@ -44,81 +44,81 @@ export enum BootloaderMenu { CLEAR_NVM3 = 0xff, } -const CARRIAGE_RETURN = 0x0d -const NEWLINE = 0x0a +const CARRIAGE_RETURN = 0x0d; +const NEWLINE = 0x0a; -const BOOTLOADER_KNOCK = Buffer.from([NEWLINE]) -const BOOTLOADER_MENU_UPLOAD_GBL = Buffer.from([BootloaderMenu.UPLOAD_GBL]) -const BOOTLOADER_MENU_RUN = Buffer.from([BootloaderMenu.RUN]) -const BOOTLOADER_MENU_INFO = Buffer.from([BootloaderMenu.INFO]) +const BOOTLOADER_KNOCK = Buffer.from([NEWLINE]); +const BOOTLOADER_MENU_UPLOAD_GBL = Buffer.from([BootloaderMenu.UPLOAD_GBL]); +const BOOTLOADER_MENU_RUN = Buffer.from([BootloaderMenu.RUN]); +const BOOTLOADER_MENU_INFO = Buffer.from([BootloaderMenu.INFO]); -const BOOTLOADER_PROMPT = Buffer.from('BL >', 'ascii') -const BOOTLOADER_VERSION = Buffer.from('Bootloader v', 'ascii') -const BOOTLOADER_BEGIN_UPLOAD = Buffer.from('begin upload', 'ascii') -const BOOTLOADER_UPLOAD_COMPLETE = Buffer.from('Serial upload complete', 'ascii') -const BOOTLOADER_UPLOAD_ABORTED = Buffer.from('Serial upload aborted', 'ascii') +const BOOTLOADER_PROMPT = Buffer.from("BL >", "ascii"); +const BOOTLOADER_VERSION = Buffer.from("Bootloader v", "ascii"); +const BOOTLOADER_BEGIN_UPLOAD = Buffer.from("begin upload", "ascii"); +const BOOTLOADER_UPLOAD_COMPLETE = Buffer.from("Serial upload complete", "ascii"); +const BOOTLOADER_UPLOAD_ABORTED = Buffer.from("Serial upload aborted", "ascii"); /** * End of RSTACK frame * - `1ac102092a107e` * - CANCEL, RSTACK, version, RESET_BOOTLOADER, CRC, CRC, FLAG) */ -const BOOTLOADER_FIRMWARE_RAN = Buffer.from('~', 'ascii') +const BOOTLOADER_FIRMWARE_RAN = Buffer.from("~", "ascii"); -const BOOTLOADER_KNOCK_TIMEOUT = 1500 -const BOOTLOADER_UPLOAD_TIMEOUT = 1800000 -const BOOTLOADER_UPLOAD_EXIT_TIMEOUT = 1500 -const BOOTLOADER_CMD_EXEC_TIMEOUT = 500 -const BOOTLOADER_RUN_TIMEOUT = 2000 +const BOOTLOADER_KNOCK_TIMEOUT = 1500; +const BOOTLOADER_UPLOAD_TIMEOUT = 1800000; +const BOOTLOADER_UPLOAD_EXIT_TIMEOUT = 1500; +const BOOTLOADER_CMD_EXEC_TIMEOUT = 500; +const BOOTLOADER_RUN_TIMEOUT = 2000; -const GBL_START_TAG = Buffer.from([0xeb, 0x17, 0xa6, 0x03]) +const GBL_START_TAG = Buffer.from([0xeb, 0x17, 0xa6, 0x03]); /** Contains length+CRC32 and possibly padding after this. */ -const GBL_END_TAG = Buffer.from([0xfc, 0x04, 0x04, 0xfc]) -const GBL_METADATA_TAG = Buffer.from([0xf6, 0x08, 0x08, 0xf6]) -const VALID_FIRMWARE_CRC32 = 558161692 +const GBL_END_TAG = Buffer.from([0xfc, 0x04, 0x04, 0xfc]); +const GBL_METADATA_TAG = Buffer.from([0xf6, 0x08, 0x08, 0xf6]); +const VALID_FIRMWARE_CRC32 = 558161692; -const SUPPORTED_VERSIONS_REGEX = /(7\.4\.\d\.\d)|(8\.[0-1]\.\d\.\d)/ -const FORCE_RESET_SUPPORT_ADAPTERS: ReadonlyArray = ['Sonoff ZBDongle-E', 'ROUTER - Sonoff ZBDongle-E'] -const ALWAYS_FORCE_RESET_ADAPTERS: ReadonlyArray<(typeof FORCE_RESET_SUPPORT_ADAPTERS)[number]> = ['ROUTER - Sonoff ZBDongle-E'] +const SUPPORTED_VERSIONS_REGEX = /(7\.4\.\d\.\d)|(8\.[0-1]\.\d\.\d)/; +const FORCE_RESET_SUPPORT_ADAPTERS: ReadonlyArray = ["Sonoff ZBDongle-E", "ROUTER - Sonoff ZBDongle-E"]; +const ALWAYS_FORCE_RESET_ADAPTERS: ReadonlyArray<(typeof FORCE_RESET_SUPPORT_ADAPTERS)[number]> = ["ROUTER - Sonoff ZBDongle-E"]; export enum BootloaderEvent { - FAILED = 'failed', - CLOSED = 'closed', - UPLOAD_START = 'uploadStart', - UPLOAD_STOP = 'uploadStop', - UPLOAD_PROGRESS = 'uploadProgress', + FAILED = "failed", + CLOSED = "closed", + UPLOAD_START = "uploadStart", + UPLOAD_STOP = "uploadStop", + UPLOAD_PROGRESS = "uploadProgress", } interface GeckoBootloaderEventMap { - [BootloaderEvent.CLOSED]: [] - [BootloaderEvent.FAILED]: [] - [BootloaderEvent.UPLOAD_PROGRESS]: [percent: number] - [BootloaderEvent.UPLOAD_START]: [] - [BootloaderEvent.UPLOAD_STOP]: [status: XExitStatus] + [BootloaderEvent.CLOSED]: []; + [BootloaderEvent.FAILED]: []; + [BootloaderEvent.UPLOAD_PROGRESS]: [percent: number]; + [BootloaderEvent.UPLOAD_START]: []; + [BootloaderEvent.UPLOAD_STOP]: [status: XExitStatus]; } export class GeckoBootloader extends EventEmitter { - public readonly adapterModel?: AdapterModel - public readonly portConf: PortConf - public readonly transport: Transport - public readonly xmodem: XModemCRC - private state: BootloaderState + public readonly adapterModel?: AdapterModel; + public readonly portConf: PortConf; + public readonly transport: Transport; + public readonly xmodem: XModemCRC; + private state: BootloaderState; private waiter: | { /** Expected to return true if properly resolved, false if timed out and timeout not considered hard-fail */ - resolve: (value: PromiseLike | boolean) => void - state: BootloaderState - timeout: NodeJS.Timeout + resolve: (value: PromiseLike | boolean) => void; + state: BootloaderState; + timeout: NodeJS.Timeout; } - | undefined + | undefined; constructor(portConf: PortConf, adapterModel?: AdapterModel) { - super() + super(); - this.state = BootloaderState.NOT_CONNECTED - this.waiter = undefined - this.portConf = portConf - this.adapterModel = adapterModel + this.state = BootloaderState.NOT_CONNECTED; + this.waiter = undefined; + this.portConf = portConf; + this.adapterModel = adapterModel; // override config to default for serial gecko bootloader this.transport = new Transport({ ...this.portConf, @@ -126,121 +126,121 @@ export class GeckoBootloader extends EventEmitter { rtscts: false, xon: false, xoff: false, - }) - this.xmodem = new XModemCRC() + }); + this.xmodem = new XModemCRC(); - this.transport.on(TransportEvent.FAILED, this.onTransportFailed.bind(this)) - this.transport.on(TransportEvent.DATA, this.onTransportData.bind(this)) + this.transport.on(TransportEvent.FAILED, this.onTransportFailed.bind(this)); + this.transport.on(TransportEvent.DATA, this.onTransportData.bind(this)); - this.xmodem.on(XEvent.START, this.onXModemStart.bind(this)) - this.xmodem.on(XEvent.STOP, this.onXModemStop.bind(this)) - this.xmodem.on(XEvent.DATA, this.onXModemData.bind(this)) + this.xmodem.on(XEvent.START, this.onXModemStart.bind(this)); + this.xmodem.on(XEvent.STOP, this.onXModemStop.bind(this)); + this.xmodem.on(XEvent.DATA, this.onXModemData.bind(this)); } public async connect(): Promise { if (this.state !== BootloaderState.NOT_CONNECTED) { - logger.debug(`Already connected to bootloader. Skipping connect attempt.`, NS) - return + logger.debug("Already connected to bootloader. Skipping connect attempt.", NS); + return; } - logger.info(`Connecting to bootloader...`, NS) + logger.info("Connecting to bootloader...", NS); // check if already in bootloader, don't fail if not successful - await this.knock(false) + await this.knock(false); // @ts-expect-error changed by received serial data if (this.state !== BootloaderState.IDLE) { // not already in bootloader, so launch it, then knock again const isRCP = await confirm({ default: false, - message: 'Is currently installed firmware RCP (multiprotocol)?', - }) + message: "Is currently installed firmware RCP (multiprotocol)?", + }); if (isRCP) { - await this.cpcLaunch() + await this.cpcLaunch(); } else { - await this.ezspLaunch() + await this.ezspLaunch(); } // this time will fail if not successful since exhausted all possible ways - await this.knock(true) + await this.knock(true); // @ts-expect-error changed by received serial data if (this.state !== BootloaderState.IDLE) { - logger.error(`Failed to enter bootloader menu.`, NS) - this.emit(BootloaderEvent.FAILED) - return + logger.error("Failed to enter bootloader menu.", NS); + this.emit(BootloaderEvent.FAILED); + return; } } - logger.info(`Connected to bootloader.`, NS) + logger.info("Connected to bootloader.", NS); } public async navigate(menu: BootloaderMenu, firmware?: Buffer): Promise { - this.waiter = undefined - this.state = BootloaderState.IDLE + this.waiter = undefined; + this.state = BootloaderState.IDLE; switch (menu) { case BootloaderMenu.UPLOAD_GBL: { if (firmware === undefined) { - logger.error(`Navigating to upload GBL requires a valid firmware.`, NS) - await this.transport.close(false) // don't emit closed since we're returning true which will close anyway + logger.error("Navigating to upload GBL requires a valid firmware.", NS); + await this.transport.close(false); // don't emit closed since we're returning true which will close anyway - return true + return true; } - return await this.menuUploadGBL(firmware) + return await this.menuUploadGBL(firmware); } case BootloaderMenu.RUN: { - return await this.menuRun() + return await this.menuRun(); } case BootloaderMenu.INFO: { - return await this.menuGetInfo() + return await this.menuGetInfo(); } case BootloaderMenu.CLEAR_APP: { if (firmware === undefined) { - logger.error(`Navigating to clear APP requires a valid firmware.`, NS) - await this.transport.close(false) // don't emit closed since we're returning true which will close anyway + logger.error("Navigating to clear APP requires a valid firmware.", NS); + await this.transport.close(false); // don't emit closed since we're returning true which will close anyway - return true + return true; } const confirmed = await confirm({ default: false, message: - 'Confirm APP clearing? (Cannot be undone; will erase the entire firmware (including NVM3). You MUST flash a new one afterwards.)', - }) + "Confirm APP clearing? (Cannot be undone; will erase the entire firmware (including NVM3). You MUST flash a new one afterwards.)", + }); if (!confirmed) { - logger.warning(`Cancelled APP clearing.`, NS) - return false + logger.warning("Cancelled APP clearing.", NS); + return false; } - return await this.menuUploadGBL(firmware) + return await this.menuUploadGBL(firmware); } case BootloaderMenu.CLEAR_NVM3: { if (firmware === undefined) { - logger.error(`Navigating to clear NVM3 requires a valid firmware.`, NS) - await this.transport.close(false) // don't emit closed since we're returning true which will close anyway + logger.error("Navigating to clear NVM3 requires a valid firmware.", NS); + await this.transport.close(false); // don't emit closed since we're returning true which will close anyway - return true + return true; } const confirmed = await confirm({ default: false, - message: 'Confirm NVM3 clearing? (Cannot be undone; will reset the adapter to factory defaults.)', - }) + message: "Confirm NVM3 clearing? (Cannot be undone; will reset the adapter to factory defaults.)", + }); if (!confirmed) { - logger.warning(`Cancelled NVM3 clearing.`, NS) - return false + logger.warning("Cancelled NVM3 clearing.", NS); + return false; } - return await this.menuUploadGBL(firmware) + return await this.menuUploadGBL(firmware); } } } @@ -248,460 +248,462 @@ export class GeckoBootloader extends EventEmitter { public async forceReset(exit: boolean): Promise { switch (this.adapterModel) { // TODO: support per adapter - case 'Sonoff ZBDongle-E': - case 'ROUTER - Sonoff ZBDongle-E': { - await this.transport.serialSet({ dtr: false, rts: true }) - await this.transport.serialSet({ dtr: true, rts: false }, 100) + case "Sonoff ZBDongle-E": + case "ROUTER - Sonoff ZBDongle-E": { + await this.transport.serialSet({ dtr: false, rts: true }); + await this.transport.serialSet({ dtr: true, rts: false }, 100); if (exit) { - await this.transport.serialSet({ dtr: false }, 500) + await this.transport.serialSet({ dtr: false }, 500); } - break + break; } default: { - logger.debug(`Reset by pattern unavailable for ${this.adapterModel}.`, NS) + logger.debug(`Reset by pattern unavailable for ${this.adapterModel}.`, NS); } } } public async validateFirmware(firmware: Buffer | undefined): Promise { if (!firmware) { - logger.error(`Cannot proceed without a firmware file.`, NS) - return FirmwareValidation.INVALID + logger.error("Cannot proceed without a firmware file.", NS); + return FirmwareValidation.INVALID; } if (firmware.indexOf(GBL_START_TAG) !== 0) { - logger.error(`Firmware file invalid. GBL start tag not found.`, NS) - return FirmwareValidation.INVALID + logger.error("Firmware file invalid. GBL start tag not found.", NS); + return FirmwareValidation.INVALID; } - const endTagStart = firmware.lastIndexOf(GBL_END_TAG) + const endTagStart = firmware.lastIndexOf(GBL_END_TAG); if (endTagStart === -1) { - logger.error(`Firmware file invalid. GBL end tag not found.`, NS) - return FirmwareValidation.INVALID + logger.error("Firmware file invalid. GBL end tag not found.", NS); + return FirmwareValidation.INVALID; } - const computedCRC32 = crc32(firmware.subarray(0, endTagStart + 12), 0) // tag+length+crc32 (4+4+4) + const computedCRC32 = crc32(firmware.subarray(0, endTagStart + 12), 0); // tag+length+crc32 (4+4+4) if (computedCRC32 !== VALID_FIRMWARE_CRC32) { - logger.error(`Firmware file invalid. Failed CRC validation (got ${computedCRC32}, expected ${VALID_FIRMWARE_CRC32}).`, NS) - return FirmwareValidation.INVALID + logger.error(`Firmware file invalid. Failed CRC validation (got ${computedCRC32}, expected ${VALID_FIRMWARE_CRC32}).`, NS); + return FirmwareValidation.INVALID; } - const metaTagStart = firmware.lastIndexOf(GBL_METADATA_TAG) + const metaTagStart = firmware.lastIndexOf(GBL_METADATA_TAG); if (metaTagStart === -1) { const proceed = await confirm({ default: false, - message: `Firmware file does not contain metadata. Cannot validate it. Proceed with this firmware?`, - }) + message: "Firmware file does not contain metadata. Cannot validate it. Proceed with this firmware?", + }); if (!proceed) { - logger.warning(`Cancelling firmware update.`, NS) - return FirmwareValidation.CANCELLED + logger.warning("Cancelling firmware update.", NS); + return FirmwareValidation.CANCELLED; } - return FirmwareValidation.VALID + return FirmwareValidation.VALID; } - const metaTagLength = firmware.readUInt32LE(metaTagStart + GBL_METADATA_TAG.length) - const metaStart = metaTagStart + GBL_METADATA_TAG.length + 4 - const metaEnd = metaStart + metaTagLength - const metaBuf = firmware.subarray(metaStart, metaEnd) + const metaTagLength = firmware.readUInt32LE(metaTagStart + GBL_METADATA_TAG.length); + const metaStart = metaTagStart + GBL_METADATA_TAG.length + 4; + const metaEnd = metaStart + metaTagLength; + const metaBuf = firmware.subarray(metaStart, metaEnd); logger.debug( - `Metadata: tagStart=${metaTagStart}, tagLength=${metaTagLength}, start=${metaStart}, end=${metaEnd}, data=${metaBuf.toString('hex')}`, + `Metadata: tagStart=${metaTagStart}, tagLength=${metaTagLength}, start=${metaStart}, end=${metaEnd}, data=${metaBuf.toString("hex")}`, NS, - ) + ); try { - const recdMetadata: FirmwareFileMetadata = JSON.parse(metaBuf.toString('utf8')) + const recdMetadata: FirmwareFileMetadata = JSON.parse(metaBuf.toString("utf8")); - logger.info(`Firmware file metadata: ${JSON.stringify(recdMetadata)}`, NS) + logger.info(`Firmware file metadata: ${JSON.stringify(recdMetadata)}`, NS); // checks irrelevant for router firmware - if (!recdMetadata.fw_type.includes('router')) { + if (!recdMetadata.fw_type.includes("router")) { if (!TCP_REGEX.test(this.portConf.path) && recdMetadata.baudrate !== this.portConf.baudRate) { logger.warning( `Firmware file baudrate ${recdMetadata.baudrate} differs from your current port configuration of ${this.portConf.baudRate}.`, NS, - ) + ); } if (!recdMetadata.ezsp_version || !SUPPORTED_VERSIONS_REGEX.test(recdMetadata.ezsp_version)) { - logger.warning(`Firmware file version is not recognized as currently supported by Zigbee2MQTT ember driver.`, NS) + logger.warning("Firmware file version is not recognized as currently supported by Zigbee2MQTT ember driver.", NS); } } const proceed = await confirm({ default: false, message: `Version: ${recdMetadata.ezsp_version}, Baudrate: ${recdMetadata.baudrate}. Proceed with this firmware?`, - }) + }); if (!proceed) { - logger.warning(`Cancelling firmware update.`, NS) - return FirmwareValidation.CANCELLED + logger.warning("Cancelling firmware update.", NS); + return FirmwareValidation.CANCELLED; } } catch (error) { - logger.error(`Failed to validate firmware file: ${error}.`, NS) - return FirmwareValidation.INVALID + logger.error(`Failed to validate firmware file: ${error}.`, NS); + return FirmwareValidation.INVALID; } - return FirmwareValidation.VALID + return FirmwareValidation.VALID; } private async cpcLaunch(): Promise { - logger.debug(`Launching bootloader from CPC...`, NS) + logger.debug("Launching bootloader from CPC...", NS); - const cpc = new Cpc(this.portConf) + const cpc = new Cpc(this.portConf); - await cpc.start() + await cpc.start(); - cpc.on(CpcEvent.FAILED, this.onTransportFailed.bind(this)) + cpc.on(CpcEvent.FAILED, this.onTransportFailed.bind(this)); try { - const status = await cpc.cpcLaunchStandaloneBootloader() + const status = await cpc.cpcLaunchStandaloneBootloader(); if (status !== CpcSystemStatus.OK) { - throw new Error(CpcSystemStatus[status]) + throw new Error(CpcSystemStatus[status]); } } catch (error) { - logger.error(`Unable to launch bootloader from CPC: ${error}`, NS) - this.emit(BootloaderEvent.FAILED) - return + logger.error(`Unable to launch bootloader from CPC: ${error}`, NS); + this.emit(BootloaderEvent.FAILED); + return; } - await cpc.stop() + await cpc.stop(); } private async ezspLaunch(): Promise { - logger.debug(`Launching bootloader from EZSP...`, NS) + logger.debug("Launching bootloader from EZSP...", NS); - const ezsp = await emberStart(this.portConf) + const ezsp = await emberStart(this.portConf); try { - const status = await ezsp.ezspLaunchStandaloneBootloader(true) + const status = await ezsp.ezspLaunchStandaloneBootloader(true); if (status !== SLStatus.OK) { - throw new Error(SLStatus[status]) + throw new Error(SLStatus[status]); } } catch (error) { - logger.error(`Unable to launch bootloader from EZSP: ${error}`, NS) - this.emit(BootloaderEvent.FAILED) - return + logger.error(`Unable to launch bootloader from EZSP: ${error}`, NS); + this.emit(BootloaderEvent.FAILED); + return; } // free serial - await emberStop(ezsp) + await emberStop(ezsp); } private async knock(fail: boolean): Promise { - logger.info(fail ? `Entering bootloader...` : `Trying to enter bootloader...`, NS) + logger.info(fail ? "Entering bootloader..." : "Trying to enter bootloader...", NS); try { - await this.transport.initPort() + await this.transport.initPort(); // try force reset if supported (only on initial non-fail knock) if (!fail && this.adapterModel && FORCE_RESET_SUPPORT_ADAPTERS.includes(this.adapterModel)) { // XXX: always force reset Sonoff ZBDongle-E Router to prevent issues with EZSP 6.10.3 (can be removed once versions updated and no longer used) const forceReset = ALWAYS_FORCE_RESET_ADAPTERS.includes(this.adapterModel) || - (await confirm({ message: 'Force reset into bootloader?', default: true })) + (await confirm({ message: "Force reset into bootloader?", default: true })); if (forceReset) { - logger.debug(`Entering bootloader via force reset...`, NS) + logger.debug("Entering bootloader via force reset...", NS); - await this.forceReset(false) + await this.forceReset(false); if (this.state === BootloaderState.IDLE) { // nothing else to do if already got the bl prompt - return + return; } } } } catch (error) { - logger.error(`Failed to open port: ${error}.`, NS) + logger.error(`Failed to open port: ${error}.`, NS); - await this.transport.close(false, false) // force failed below - this.emit(BootloaderEvent.FAILED) + await this.transport.close(false, false); // force failed below + this.emit(BootloaderEvent.FAILED); - return + return; } - let res: boolean = false + let res = false; for (let i = 1; i < 3; i++) { - await this.transport.write(BOOTLOADER_KNOCK) + await this.transport.write(BOOTLOADER_KNOCK); - res = await this.waitForState(BootloaderState.IDLE, BOOTLOADER_KNOCK_TIMEOUT, fail && i == 2) + res = await this.waitForState(BootloaderState.IDLE, BOOTLOADER_KNOCK_TIMEOUT, fail && i === 2); if (res) { - break - } else if (i == 1 && this.transport.isSerial) { + break; + } + + if (i === 1 && this.transport.isSerial) { // if failed first attempt, try second time with RTS/CTS enabled - await this.transport.serialSet({ rts: true, cts: true }) + await this.transport.serialSet({ rts: true, cts: true }); } } if (!res) { - await this.transport.close(fail) // emit closed based on if we want to fail on unsuccessful knock + await this.transport.close(fail); // emit closed based on if we want to fail on unsuccessful knock if (fail) { - logger.error(`Unable to enter bootloader.`, NS) + logger.error("Unable to enter bootloader.", NS); } else { - logger.info(`Unable to enter bootloader.`, NS) + logger.info("Unable to enter bootloader.", NS); } } } private async menuGetInfo(): Promise { - logger.debug(`Entering 'Info' menu...`, NS) + logger.debug(`Entering 'Info' menu...`, NS); - this.state = BootloaderState.GETTING_INFO + this.state = BootloaderState.GETTING_INFO; - await this.transport.write(BOOTLOADER_MENU_INFO) + await this.transport.write(BOOTLOADER_MENU_INFO); - await this.waitForState(BootloaderState.GOT_INFO, BOOTLOADER_CMD_EXEC_TIMEOUT) + await this.waitForState(BootloaderState.GOT_INFO, BOOTLOADER_CMD_EXEC_TIMEOUT); - return false + return false; } private async menuRun(): Promise { - logger.debug(`Entering 'Run' menu...`, NS) + logger.debug(`Entering 'Run' menu...`, NS); - this.state = BootloaderState.RUNNING + this.state = BootloaderState.RUNNING; - await this.transport.write(BOOTLOADER_MENU_RUN) + await this.transport.write(BOOTLOADER_MENU_RUN); // this is expected to fail (signals the firmware ran and bootloader was exited) - const res = await this.waitForState(BootloaderState.IDLE, BOOTLOADER_RUN_TIMEOUT, false, true) + const res = await this.waitForState(BootloaderState.IDLE, BOOTLOADER_RUN_TIMEOUT, false, true); if (res) { // got menu back, failed to run - logger.warning(`Failed to exit bootloader and run firmware.`, NS) + logger.warning("Failed to exit bootloader and run firmware.", NS); if (this.adapterModel && FORCE_RESET_SUPPORT_ADAPTERS.includes(this.adapterModel)) { - logger.warning(`Trying force reset...`, NS) + logger.warning("Trying force reset...", NS); - await this.forceReset(true) + await this.forceReset(true); } else { - logger.warning(`You may need to unplug/replug your adapter to run the firmware.`, NS) + logger.warning("You may need to unplug/replug your adapter to run the firmware.", NS); } } else { // @ts-expect-error changed by received serial data - if (this.state == BootloaderState.NOT_CONNECTED) { - logger.info(`Firmware ran, bootloader exited.`, NS) + if (this.state === BootloaderState.NOT_CONNECTED) { + logger.info("Firmware ran, bootloader exited.", NS); } else { - logger.info(`Bootloader considered exited.`, NS) + logger.info("Bootloader considered exited.", NS); } } - return true + return true; } private async menuUploadGBL(firmware: Buffer): Promise { - logger.debug(`Entering 'Upload GBL' menu...`, NS) + logger.debug(`Entering 'Upload GBL' menu...`, NS); - this.xmodem.init(firmware) + this.xmodem.init(firmware); - this.state = BootloaderState.BEGIN_UPLOAD + this.state = BootloaderState.BEGIN_UPLOAD; - await this.transport.write(BOOTLOADER_MENU_UPLOAD_GBL) // start upload - await this.waitForState(BootloaderState.UPLOADING, BOOTLOADER_UPLOAD_EXIT_TIMEOUT) - await this.waitForState(BootloaderState.UPLOADED, BOOTLOADER_UPLOAD_TIMEOUT) + await this.transport.write(BOOTLOADER_MENU_UPLOAD_GBL); // start upload + await this.waitForState(BootloaderState.UPLOADING, BOOTLOADER_UPLOAD_EXIT_TIMEOUT); + await this.waitForState(BootloaderState.UPLOADED, BOOTLOADER_UPLOAD_TIMEOUT); - const res = await this.waitForState(BootloaderState.IDLE, BOOTLOADER_UPLOAD_EXIT_TIMEOUT, false) + const res = await this.waitForState(BootloaderState.IDLE, BOOTLOADER_UPLOAD_EXIT_TIMEOUT, false); if (!res) { // force back to menu if not automatically back to it already - await this.transport.write(BOOTLOADER_KNOCK) - await this.waitForState(BootloaderState.IDLE, BOOTLOADER_UPLOAD_EXIT_TIMEOUT) + await this.transport.write(BOOTLOADER_KNOCK); + await this.waitForState(BootloaderState.IDLE, BOOTLOADER_UPLOAD_EXIT_TIMEOUT); } - return false + return false; } private async onTransportData(received: Buffer): Promise { - logger.debug(`Received transport data: ${received.toString('hex')} while in state ${BootloaderState[this.state]}.`, NS) + logger.debug(`Received transport data: ${received.toString("hex")} while in state ${BootloaderState[this.state]}.`, NS); switch (this.state) { case BootloaderState.NOT_CONNECTED: { if (received.includes(BOOTLOADER_PROMPT)) { - this.resolveState(BootloaderState.IDLE) + this.resolveState(BootloaderState.IDLE); } - break + break; } case BootloaderState.IDLE: { - break + break; } case BootloaderState.BEGIN_UPLOAD: { if (received.includes(BOOTLOADER_BEGIN_UPLOAD)) { - this.resolveState(BootloaderState.UPLOADING) + this.resolveState(BootloaderState.UPLOADING); } - break + break; } case BootloaderState.UPLOADING: { // just hand over to xmodem - return this.xmodem.process(received) + return this.xmodem.process(received); } case BootloaderState.UPLOADED: { if (received.includes(BOOTLOADER_UPLOAD_ABORTED)) { - logger.error(`Firmware upload aborted.`, NS) + logger.error("Firmware upload aborted.", NS); } else if (received.includes(BOOTLOADER_UPLOAD_COMPLETE)) { - logger.info(`Firmware upload completed.`, NS) + logger.info("Firmware upload completed.", NS); } // always check if got back prompt already (can be in same tx as above) if (received.includes(BOOTLOADER_PROMPT)) { - this.resolveState(BootloaderState.IDLE) + this.resolveState(BootloaderState.IDLE); } - break + break; } case BootloaderState.RUNNING: { - const blv = received.indexOf(BOOTLOADER_VERSION) + const blv = received.indexOf(BOOTLOADER_VERSION); if (blv !== -1) { - const [blInfo] = this.readBootloaderInfo(received, blv) + const [blInfo] = this.readBootloaderInfo(received, blv); - logger.info(`Received bootloader info while trying to exit: ${blInfo}.`, NS) + logger.info(`Received bootloader info while trying to exit: ${blInfo}.`, NS); } else if (received.includes(BOOTLOADER_PROMPT)) { - this.resolveState(BootloaderState.IDLE) + this.resolveState(BootloaderState.IDLE); } else if (received.includes(BOOTLOADER_FIRMWARE_RAN)) { - this.resolveState(BootloaderState.NOT_CONNECTED) + this.resolveState(BootloaderState.NOT_CONNECTED); } else { - logger.debug(received.toString('ascii'), NS) + logger.debug(received.toString("ascii"), NS); } - break + break; } case BootloaderState.GETTING_INFO: { - const blv = received.indexOf(BOOTLOADER_VERSION) + const blv = received.indexOf(BOOTLOADER_VERSION); if (blv !== -1) { - this.resolveState(BootloaderState.GOT_INFO) + this.resolveState(BootloaderState.GOT_INFO); - const [blInfo] = this.readBootloaderInfo(received, blv) + const [blInfo] = this.readBootloaderInfo(received, blv); - logger.info(`${blInfo}.`, NS) + logger.info(`${blInfo}.`, NS); } - break + break; } case BootloaderState.GOT_INFO: { if (received.includes(BOOTLOADER_PROMPT)) { - this.resolveState(BootloaderState.IDLE) + this.resolveState(BootloaderState.IDLE); } - break + break; } } } private onTransportFailed(): void { - this.state = BootloaderState.NOT_CONNECTED - this.emit(BootloaderEvent.FAILED) + this.state = BootloaderState.NOT_CONNECTED; + this.emit(BootloaderEvent.FAILED); } private async onXModemData(data: Buffer, progressPc: number): Promise { - this.emit(BootloaderEvent.UPLOAD_PROGRESS, progressPc) - await this.transport.write(data) + this.emit(BootloaderEvent.UPLOAD_PROGRESS, progressPc); + await this.transport.write(data); } private async onXModemStart(): Promise { - this.emit(BootloaderEvent.UPLOAD_START) + this.emit(BootloaderEvent.UPLOAD_START); } private async onXModemStop(status: XExitStatus): Promise { - this.resolveState(BootloaderState.UPLOADED) - this.emit(BootloaderEvent.UPLOAD_STOP, status) + this.resolveState(BootloaderState.UPLOADED); + this.emit(BootloaderEvent.UPLOAD_STOP, status); } private readBootloaderInfo(buffer: Buffer, blvIndex: number): [info: string, hasExtras: boolean] { // cleanup start - let startIndex = 0 + let startIndex = 0; if (buffer[0] === CARRIAGE_RETURN) { - startIndex = buffer[1] === NEWLINE ? 2 : 1 + startIndex = buffer[1] === NEWLINE ? 2 : 1; } else if (buffer[0] === NEWLINE) { - startIndex = 1 + startIndex = 1; } - const infoBuf = buffer.subarray(startIndex, buffer.indexOf(NEWLINE, blvIndex) + 1) + const infoBuf = buffer.subarray(startIndex, buffer.indexOf(NEWLINE, blvIndex) + 1); if (infoBuf.length === 0) { - return ['', false] + return ["", false]; } - logger.debug(`Reading info from: ${infoBuf.toString('hex')}.`, NS) - let hasNewline: boolean = true - let newlineStart: number = 0 - const lines: string[] = [] + logger.debug(`Reading info from: ${infoBuf.toString("hex")}.`, NS); + let hasNewline = true; + let newlineStart = 0; + const lines: string[] = []; while (hasNewline) { - const newlineEnd = infoBuf.indexOf(NEWLINE, newlineStart) + const newlineEnd = infoBuf.indexOf(NEWLINE, newlineStart); if (newlineEnd === -1) { - hasNewline = false + hasNewline = false; } else { - const newline: Buffer = infoBuf.subarray(newlineStart, newlineEnd - (infoBuf[newlineEnd - 1] === CARRIAGE_RETURN ? 1 : 0)) - newlineStart = newlineEnd + 1 + const newline: Buffer = infoBuf.subarray(newlineStart, newlineEnd - (infoBuf[newlineEnd - 1] === CARRIAGE_RETURN ? 1 : 0)); + newlineStart = newlineEnd + 1; if (newline.length > 2) { - lines.push(newline.toString('ascii')) + lines.push(newline.toString("ascii")); } } } - return [lines.join('. '), lines.length > 1] // regular only has the bootloader version line, if more, means extra + return [lines.join(". "), lines.length > 1]; // regular only has the bootloader version line, if more, means extra } private resolveState(state: BootloaderState): void { if (this.waiter?.state === state) { - clearTimeout(this.waiter.timeout) - this.waiter.resolve(true) + clearTimeout(this.waiter.timeout); + this.waiter.resolve(true); - this.waiter = undefined + this.waiter = undefined; } - logger.debug(`New bootloader state: ${BootloaderState[state]}.`, NS) + logger.debug(`New bootloader state: ${BootloaderState[state]}.`, NS); // always set even if no waiter - this.state = state + this.state = state; } - private waitForState(state: BootloaderState, timeout: number = 5000, fail: boolean = true, expectingFail: boolean = false): Promise { + private waitForState(state: BootloaderState, timeout = 5000, fail = true, expectingFail = false): Promise { return new Promise((resolve) => { this.waiter = { resolve, state, timeout: setTimeout(() => { - const msg = `Timed out waiting for ${BootloaderState[state]} after ${timeout}ms.` + const msg = `Timed out waiting for ${BootloaderState[state]} after ${timeout}ms.`; if (fail) { - logger.error(msg, NS) - this.emit(BootloaderEvent.FAILED) - return + logger.error(msg, NS); + this.emit(BootloaderEvent.FAILED); + return; } if (!expectingFail) { - logger.debug(msg, NS) + logger.debug(msg, NS); } - resolve(false) - this.waiter = undefined + resolve(false); + this.waiter = undefined; }, timeout), - } - }) + }; + }); } } diff --git a/src/utils/consts.ts b/src/utils/consts.ts index 9261677..e14e6e1 100644 --- a/src/utils/consts.ts +++ b/src/utils/consts.ts @@ -1,89 +1,89 @@ -import type { AdapterModel } from './types.js' +import type { AdapterModel } from "./types.js"; -import { EmberApsOption } from 'zigbee-herdsman/dist/adapter/ember/enums.js' +import { EmberApsOption } from "zigbee-herdsman/dist/adapter/ember/enums.js"; -export const PRE_DEFINED_FIRMWARE_LINKS_URL = `https://github.com/Nerivec/ember-zli/raw/refs/heads/main/firmware-links.json` +export const PRE_DEFINED_FIRMWARE_LINKS_URL = "https://github.com/Nerivec/ember-zli/raw/refs/heads/main/firmware-links.json"; export const ADAPTER_MODELS: ReadonlyArray = [ - 'Aeotec Zi-Stick (ZGA008)', - 'EasyIOT ZB-GW04 v1.1', - 'EasyIOT ZB-GW04 v1.2', - 'Nabu Casa SkyConnect', - 'Nabu Casa Yellow', - 'SMLight SLZB06-M', - 'SMLight SLZB07', - 'SMLight SLZB07mg24', - 'Sonoff ZBDongle-E', - 'SparkFun MGM240p', - 'TubeZB MGM24', - 'TubeZB MGM24PB', - 'ROUTER - Aeotec Zi-Stick (ZGA008)', - 'ROUTER - EasyIOT ZB-GW04 v1.1', - 'ROUTER - EasyIOT ZB-GW04 v1.2', - 'ROUTER - Nabu Casa SkyConnect', - 'ROUTER - Nabu Casa Yellow', - 'ROUTER - SMLight SLZB06-M', - 'ROUTER - SMLight SLZB07', - 'ROUTER - SMLight SLZB07mg24', - 'ROUTER - Sonoff ZBDongle-E', - 'ROUTER - SparkFun MGM240p', - 'ROUTER - TubeZB MGM24', - 'ROUTER - TubeZB MGM24PB', -] -export const TCP_REGEX = /^tcp:\/\/[\w.-]+:\d+$/ -export const BAUDRATES = [115200, 230400, 460800, 921600] + "Aeotec Zi-Stick (ZGA008)", + "EasyIOT ZB-GW04 v1.1", + "EasyIOT ZB-GW04 v1.2", + "Nabu Casa SkyConnect", + "Nabu Casa Yellow", + "SMLight SLZB06-M", + "SMLight SLZB07", + "SMLight SLZB07mg24", + "Sonoff ZBDongle-E", + "SparkFun MGM240p", + "TubeZB MGM24", + "TubeZB MGM24PB", + "ROUTER - Aeotec Zi-Stick (ZGA008)", + "ROUTER - EasyIOT ZB-GW04 v1.1", + "ROUTER - EasyIOT ZB-GW04 v1.2", + "ROUTER - Nabu Casa SkyConnect", + "ROUTER - Nabu Casa Yellow", + "ROUTER - SMLight SLZB06-M", + "ROUTER - SMLight SLZB07", + "ROUTER - SMLight SLZB07mg24", + "ROUTER - Sonoff ZBDongle-E", + "ROUTER - SparkFun MGM240p", + "ROUTER - TubeZB MGM24", + "ROUTER - TubeZB MGM24PB", +]; +export const TCP_REGEX = /^tcp:\/\/[\w.-]+:\d+$/; +export const BAUDRATES = [115200, 230400, 460800, 921600]; /** Read/write max bytes count at stream level */ -export const CONFIG_HIGHWATER_MARK = 256 +export const CONFIG_HIGHWATER_MARK = 256; /** Default behavior is to disable app key requests */ -export const ALLOW_APP_KEY_REQUESTS = false +export const ALLOW_APP_KEY_REQUESTS = false; -export const DEFAULT_APS_OPTIONS = EmberApsOption.RETRY | EmberApsOption.ENABLE_ROUTE_DISCOVERY | EmberApsOption.ENABLE_ADDRESS_DISCOVERY +export const DEFAULT_APS_OPTIONS = EmberApsOption.RETRY | EmberApsOption.ENABLE_ROUTE_DISCOVERY | EmberApsOption.ENABLE_ADDRESS_DISCOVERY; -export const APPLICATION_ZDO_SEQUENCE_MASK = 0x7f -export const DEFAULT_ZDO_REQUEST_RADIUS = 0xff +export const APPLICATION_ZDO_SEQUENCE_MASK = 0x7f; +export const DEFAULT_ZDO_REQUEST_RADIUS = 0xff; -export const TOUCHLINK_CHANNELS = [11, 15, 20, 25] +export const TOUCHLINK_CHANNELS = [11, 15, 20, 25]; -export const CPC_PAYLOAD_LENGTH_MAX = 16 -export const CPC_SYSTEM_COMMAND_HEADER_SIZE = 4 +export const CPC_PAYLOAD_LENGTH_MAX = 16; +export const CPC_SYSTEM_COMMAND_HEADER_SIZE = 4; -export const CPC_HDLC_FLAG_POS = 0 -export const CPC_HDLC_ADDRESS_POS = 1 -export const CPC_HDLC_LENGTH_POS = 2 -export const CPC_HDLC_CONTROL_POS = 4 -export const CPC_HDLC_HCS_POS = 5 +export const CPC_HDLC_FLAG_POS = 0; +export const CPC_HDLC_ADDRESS_POS = 1; +export const CPC_HDLC_LENGTH_POS = 2; +export const CPC_HDLC_CONTROL_POS = 4; +export const CPC_HDLC_HCS_POS = 5; -export const CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT = 6 -export const CPC_HDLC_CONTROL_P_F_SHIFT = 3 -export const CPC_HDLC_CONTROL_SEQ_SHIFT = 4 -export const CPC_HDLC_CONTROL_SUPERVISORY_FNCT_ID_SHIFT = 4 -export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_SHIFT = 0 +export const CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT = 6; +export const CPC_HDLC_CONTROL_P_F_SHIFT = 3; +export const CPC_HDLC_CONTROL_SEQ_SHIFT = 4; +export const CPC_HDLC_CONTROL_SUPERVISORY_FNCT_ID_SHIFT = 4; +export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_SHIFT = 0; -export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_MASK = 0x37 +export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_MASK = 0x37; -export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_INFORMATION = 0x00 -export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_POLL_FINAL = 0x04 -export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_RESET_SEQ = 0x31 -export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_ACKNOWLEDGE = 0x0e +export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_INFORMATION = 0x00; +export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_POLL_FINAL = 0x04; +export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_RESET_SEQ = 0x31; +export const CPC_HDLC_CONTROL_UNNUMBERED_TYPE_ACKNOWLEDGE = 0x0e; -export const CPC_HDLC_FLAG_VAL = 0x14 -export const CPC_HDLC_HEADER_SIZE = 5 -export const CPC_HDLC_HEADER_RAW_SIZE = 7 -export const CPC_HDLC_HCS_SIZE = CPC_HDLC_HEADER_RAW_SIZE - CPC_HDLC_HEADER_SIZE -export const CPC_HDLC_FCS_SIZE = 2 +export const CPC_HDLC_FLAG_VAL = 0x14; +export const CPC_HDLC_HEADER_SIZE = 5; +export const CPC_HDLC_HEADER_RAW_SIZE = 7; +export const CPC_HDLC_HCS_SIZE = CPC_HDLC_HEADER_RAW_SIZE - CPC_HDLC_HEADER_SIZE; +export const CPC_HDLC_FCS_SIZE = 2; -export const CPC_HDLC_FRAME_TYPE_UNNUMBERED = 3 +export const CPC_HDLC_FRAME_TYPE_UNNUMBERED = 3; -export const CPC_DEFAULT_COMMAND_TIMEOUT = 1000 +export const CPC_DEFAULT_COMMAND_TIMEOUT = 1000; /** At the next reboot bootloader is executed */ -export const CPC_SYSTEM_REBOOT_MODE_BOOTLOADER = 1 +export const CPC_SYSTEM_REBOOT_MODE_BOOTLOADER = 1; -export const CPC_PROPERTY_ID_SECONDARY_CPC_VERSION = 0x03 -export const CPC_PROPERTY_ID_BOOTLOADER_REBOOT_MODE = 0x202 +export const CPC_PROPERTY_ID_SECONDARY_CPC_VERSION = 0x03; +export const CPC_PROPERTY_ID_BOOTLOADER_REBOOT_MODE = 0x202; -export const CPC_FLAG_UNNUMBERED_POLL_FINAL = 0x01 << 2 +export const CPC_FLAG_UNNUMBERED_POLL_FINAL = 0x01 << 2; -export const CPC_SERVICE_ENDPOINT_ID_SYSTEM = 0 +export const CPC_SERVICE_ENDPOINT_ID_SYSTEM = 0; -export const CREATOR_STACK_RESTORED_EUI64 = 0xe12a +export const CREATOR_STACK_RESTORED_EUI64 = 0xe12a; diff --git a/src/utils/cpc.ts b/src/utils/cpc.ts index f29da6f..2576acc 100644 --- a/src/utils/cpc.ts +++ b/src/utils/cpc.ts @@ -1,10 +1,10 @@ -import type { CpcSystemCommand, FirmwareVersionShort, PortConf } from './types.js' +import type { CpcSystemCommand, FirmwareVersionShort, PortConf } from "./types.js"; -import EventEmitter from 'node:events' +import EventEmitter from "node:events"; -import { EzspBuffalo } from 'zigbee-herdsman/dist/adapter/ember/ezsp/buffalo.js' +import { EzspBuffalo } from "zigbee-herdsman/dist/adapter/ember/ezsp/buffalo.js"; -import { logger } from '../index.js' +import { logger } from "../index.js"; import { CPC_DEFAULT_COMMAND_TIMEOUT, CPC_FLAG_UNNUMBERED_POLL_FINAL, @@ -26,219 +26,219 @@ import { CPC_SERVICE_ENDPOINT_ID_SYSTEM, CPC_SYSTEM_COMMAND_HEADER_SIZE, CPC_SYSTEM_REBOOT_MODE_BOOTLOADER, -} from './consts.js' -import { CpcSystemCommandId, CpcSystemStatus } from './enums.js' -import { Transport, TransportEvent } from './transport.js' -import { computeCRC16 } from './utils.js' +} from "./consts.js"; +import { CpcSystemCommandId, CpcSystemStatus } from "./enums.js"; +import { Transport, TransportEvent } from "./transport.js"; +import { computeCRC16 } from "./utils.js"; -const NS = { namespace: 'cpc' } +const NS = { namespace: "cpc" }; export enum CpcEvent { - FAILED = 'failed', + FAILED = "failed", } interface CpcEventMap { - [CpcEvent.FAILED]: [] + [CpcEvent.FAILED]: []; } export class Cpc extends EventEmitter { - public readonly transport: Transport - private buffalo: EzspBuffalo - private sequence: number + public readonly transport: Transport; + private buffalo: EzspBuffalo; + private sequence: number; private waiter: | { /** Expected to return true if properly resolved, false if timed out and timeout not considered hard-fail */ - resolve: (value: CpcSystemCommand | PromiseLike) => void - sequence: number - timeout: NodeJS.Timeout + resolve: (value: CpcSystemCommand | PromiseLike) => void; + sequence: number; + timeout: NodeJS.Timeout; } - | undefined + | undefined; constructor(portConf: PortConf) { - super() + super(); - this.sequence = 0 - this.waiter = undefined - this.transport = new Transport(portConf) - this.buffalo = new EzspBuffalo(Buffer.alloc(CPC_PAYLOAD_LENGTH_MAX), 0) + this.sequence = 0; + this.waiter = undefined; + this.transport = new Transport(portConf); + this.buffalo = new EzspBuffalo(Buffer.alloc(CPC_PAYLOAD_LENGTH_MAX), 0); - this.transport.on(TransportEvent.FAILED, this.onTransportFailed.bind(this)) - this.transport.on(TransportEvent.DATA, this.onTransportData.bind(this)) + this.transport.on(TransportEvent.FAILED, this.onTransportFailed.bind(this)); + this.transport.on(TransportEvent.DATA, this.onTransportData.bind(this)); } public async cpcGetVersion(): Promise { - this.buffalo.setPosition(0) - this.buffalo.writeUInt32(CPC_PROPERTY_ID_SECONDARY_CPC_VERSION) + this.buffalo.setPosition(0); + this.buffalo.writeUInt32(CPC_PROPERTY_ID_SECONDARY_CPC_VERSION); // req: 14 00 0a00 c4 55d3 02 01 0400 03000000 baaa // rsp: 14 00 1600 c4 57e5 06 01 1000 03000000 04000000 05000000 00000000 6d3c - const result = await this.sendSystemUFrame(CpcSystemCommandId.PROP_VALUE_GET) + const result = await this.sendSystemUFrame(CpcSystemCommandId.PROP_VALUE_GET); if (!result) { - throw new Error(`Invalid result from PROP_VALUE_GET(SECONDARY_CPC_VERSION) response`) + throw new Error("Invalid result from PROP_VALUE_GET(SECONDARY_CPC_VERSION) response"); } // const propertyId = result.payload.readUInt32LE(0) - const major = result.payload.readUInt32LE(4) - const minor = result.payload.readUInt32LE(8) - const patch = result.payload.readUInt32LE(12) + const major = result.payload.readUInt32LE(4); + const minor = result.payload.readUInt32LE(8); + const patch = result.payload.readUInt32LE(12); - return `${major}.${minor}.${patch}` + return `${major}.${minor}.${patch}`; } public async cpcLaunchStandaloneBootloader(): Promise { - this.buffalo.setPosition(0) - this.buffalo.writeUInt32(CPC_PROPERTY_ID_BOOTLOADER_REBOOT_MODE) - this.buffalo.writeUInt32(CPC_SYSTEM_REBOOT_MODE_BOOTLOADER) + this.buffalo.setPosition(0); + this.buffalo.writeUInt32(CPC_PROPERTY_ID_BOOTLOADER_REBOOT_MODE); + this.buffalo.writeUInt32(CPC_SYSTEM_REBOOT_MODE_BOOTLOADER); // req: 14 00 0e00 c4 950f 02 01 0800 02020000 01000000 190d // rsp: 14 00 0e00 c4 950f 06 01 0800 02020000 01000000 cd00 - const result = await this.sendSystemUFrame(CpcSystemCommandId.PROP_VALUE_SET) + const result = await this.sendSystemUFrame(CpcSystemCommandId.PROP_VALUE_SET); if (!result) { - throw new Error(`Invalid result from PROP_VALUE_SET(BOOTLOADER_REBOOT_MODE) response.`) + throw new Error("Invalid result from PROP_VALUE_SET(BOOTLOADER_REBOOT_MODE) response."); } - const status: CpcSystemStatus = result.payload[0] + const status: CpcSystemStatus = result.payload[0]; // as of 4.5.0, this is actually returning UNIMPLEMENTED if (status !== CpcSystemStatus.OK && status !== CpcSystemStatus.UNIMPLEMENTED) { - return status + return status; } - this.buffalo.setPosition(0) + this.buffalo.setPosition(0); // don't want to parse anything coming in after RESET is sent - this.transport.removeAllListeners(TransportEvent.DATA) + this.transport.removeAllListeners(TransportEvent.DATA); // req: 14 00 0300 90 b557 06 c660 - await this.sendSystemUFrame(CpcSystemCommandId.RESET, true) + await this.sendSystemUFrame(CpcSystemCommandId.RESET, true); await new Promise((resolve) => { - setTimeout(resolve, 500) - }) + setTimeout(resolve, 500); + }); - return CpcSystemStatus.OK + return CpcSystemStatus.OK; } public receiveSystemUFrame(data: Buffer): void { if (data.length < CPC_HDLC_HEADER_RAW_SIZE) { - throw new Error(`Received invalid System UFrame length=${data.length} [${data.toString('hex')}].`) + throw new Error(`Received invalid System UFrame length=${data.length} [${data.toString("hex")}].`); } - const flag = data.readUInt8(CPC_HDLC_FLAG_POS) + const flag = data.readUInt8(CPC_HDLC_FLAG_POS); if (flag !== CPC_HDLC_FLAG_VAL) { - throw new Error(`Received invalid System UFrame flag=${CPC_HDLC_FLAG_VAL}.`) + throw new Error(`Received invalid System UFrame flag=${CPC_HDLC_FLAG_VAL}.`); } // const address = data.readUInt8(CPC_HDLC_ADDRESS_POS) - const frameLength = data.readUInt16LE(CPC_HDLC_LENGTH_POS) - const expectedFrameLength = data.length - CPC_HDLC_HEADER_RAW_SIZE + const frameLength = data.readUInt16LE(CPC_HDLC_LENGTH_POS); + const expectedFrameLength = data.length - CPC_HDLC_HEADER_RAW_SIZE; if (expectedFrameLength !== frameLength) { - throw new Error(`Received invalid System UFrame length=${data.length} expected=${expectedFrameLength}.`) + throw new Error(`Received invalid System UFrame length=${data.length} expected=${expectedFrameLength}.`); } - const control = data.readUInt8(CPC_HDLC_CONTROL_POS) - const frameType = control >> CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT + const control = data.readUInt8(CPC_HDLC_CONTROL_POS); + const frameType = control >> CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT; if (frameType !== CPC_HDLC_FRAME_TYPE_UNNUMBERED) { - throw new Error(`Unsupported frame type ${frameType}.`) + throw new Error(`Unsupported frame type ${frameType}.`); } // const unnumberedType = (control >> CPC_HDLC_CONTROL_UNNUMBERED_TYPE_SHIFT) & CPC_HDLC_CONTROL_UNNUMBERED_TYPE_MASK - const headerChecksum = data.readUInt16LE(CPC_HDLC_HEADER_SIZE) - const expectedHeaderChecksum = computeCRC16(data.subarray(0, CPC_HDLC_HEADER_SIZE)).readUInt16BE() + const headerChecksum = data.readUInt16LE(CPC_HDLC_HEADER_SIZE); + const expectedHeaderChecksum = computeCRC16(data.subarray(0, CPC_HDLC_HEADER_SIZE)).readUInt16BE(); if (headerChecksum !== expectedHeaderChecksum) { - throw new Error(`Received invalid System UFrame headerChecksum=${headerChecksum} expected=${expectedHeaderChecksum}.`) + throw new Error(`Received invalid System UFrame headerChecksum=${headerChecksum} expected=${expectedHeaderChecksum}.`); } - let i = CPC_HDLC_HEADER_RAW_SIZE - const commandId = data.readUInt8(i++) - const seq = data.readUInt8(i++) - const length = data.readUInt8(i) - i += 2 - const payload = data.subarray(i, -CPC_HDLC_FCS_SIZE) - const frameChecksum = data.readUInt16LE(i + payload.length) - const expectedFrameChecksum = computeCRC16(data.subarray(CPC_HDLC_HEADER_RAW_SIZE, -CPC_HDLC_FCS_SIZE)).readUInt16BE() + let i = CPC_HDLC_HEADER_RAW_SIZE; + const commandId = data.readUInt8(i++); + const seq = data.readUInt8(i++); + const length = data.readUInt8(i); + i += 2; + const payload = data.subarray(i, -CPC_HDLC_FCS_SIZE); + const frameChecksum = data.readUInt16LE(i + payload.length); + const expectedFrameChecksum = computeCRC16(data.subarray(CPC_HDLC_HEADER_RAW_SIZE, -CPC_HDLC_FCS_SIZE)).readUInt16BE(); if (frameChecksum !== expectedFrameChecksum) { - throw new Error(`Received invalid System UFrame frameChecksum=${frameChecksum} expected=${expectedFrameChecksum}.`) + throw new Error(`Received invalid System UFrame frameChecksum=${frameChecksum} expected=${expectedFrameChecksum}.`); } - const command: CpcSystemCommand = { commandId, seq, length, payload } + const command: CpcSystemCommand = { commandId, seq, length, payload }; - logger.debug(`Received System UFrame: ${JSON.stringify(command)}.`) + logger.debug(`Received System UFrame: ${JSON.stringify(command)}.`); - this.resolveSequence(command) + this.resolveSequence(command); } - public async sendSystemUFrame(commandId: CpcSystemCommandId, noResponse: boolean = false): Promise { - const payload = this.buffalo.getWritten() - this.sequence = (this.sequence + 1) & 0xff + public async sendSystemUFrame(commandId: CpcSystemCommandId, noResponse = false): Promise { + const payload = this.buffalo.getWritten(); + this.sequence = (this.sequence + 1) & 0xff; - const header = Buffer.alloc(CPC_HDLC_HEADER_SIZE) - header.writeUInt8(CPC_HDLC_FLAG_VAL, CPC_HDLC_FLAG_POS) - header.writeUInt8(CPC_SERVICE_ENDPOINT_ID_SYSTEM, CPC_HDLC_ADDRESS_POS) - header.writeUInt16LE(CPC_SYSTEM_COMMAND_HEADER_SIZE + payload.length + CPC_HDLC_FCS_SIZE, CPC_HDLC_LENGTH_POS) + const header = Buffer.alloc(CPC_HDLC_HEADER_SIZE); + header.writeUInt8(CPC_HDLC_FLAG_VAL, CPC_HDLC_FLAG_POS); + header.writeUInt8(CPC_SERVICE_ENDPOINT_ID_SYSTEM, CPC_HDLC_ADDRESS_POS); + header.writeUInt16LE(CPC_SYSTEM_COMMAND_HEADER_SIZE + payload.length + CPC_HDLC_FCS_SIZE, CPC_HDLC_LENGTH_POS); header.writeUInt8( (CPC_HDLC_FRAME_TYPE_UNNUMBERED << CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT) | (CPC_FLAG_UNNUMBERED_POLL_FINAL << CPC_HDLC_CONTROL_UNNUMBERED_TYPE_SHIFT), CPC_HDLC_CONTROL_POS, - ) + ); - const buffer = Buffer.alloc(CPC_HDLC_HEADER_RAW_SIZE + CPC_SYSTEM_COMMAND_HEADER_SIZE + payload.length + CPC_HDLC_FCS_SIZE) + const buffer = Buffer.alloc(CPC_HDLC_HEADER_RAW_SIZE + CPC_SYSTEM_COMMAND_HEADER_SIZE + payload.length + CPC_HDLC_FCS_SIZE); - buffer.set(header, 0) + buffer.set(header, 0); - const headerChecksum = computeCRC16(header).readUInt16BE() + const headerChecksum = computeCRC16(header).readUInt16BE(); - buffer.writeUInt16LE(headerChecksum, CPC_HDLC_HCS_POS) + buffer.writeUInt16LE(headerChecksum, CPC_HDLC_HCS_POS); - let i = CPC_HDLC_HEADER_RAW_SIZE - buffer.writeUInt8(commandId, i++) - buffer.writeUInt8(this.sequence, i++) - buffer.writeUInt16LE(payload.length, i) - i += 2 - buffer.set(payload, i) - i += payload.length + let i = CPC_HDLC_HEADER_RAW_SIZE; + buffer.writeUInt8(commandId, i++); + buffer.writeUInt8(this.sequence, i++); + buffer.writeUInt16LE(payload.length, i); + i += 2; + buffer.set(payload, i); + i += payload.length; - const frameChecksum = computeCRC16(buffer.subarray(CPC_HDLC_HEADER_RAW_SIZE, i)).readUInt16BE() + const frameChecksum = computeCRC16(buffer.subarray(CPC_HDLC_HEADER_RAW_SIZE, i)).readUInt16BE(); - buffer.writeUInt16LE(frameChecksum, i) + buffer.writeUInt16LE(frameChecksum, i); - await this.transport.write(buffer) + await this.transport.write(buffer); if (noResponse) { - return undefined + return undefined; } - return await this.waitForSequence(this.sequence, CPC_DEFAULT_COMMAND_TIMEOUT) + return await this.waitForSequence(this.sequence, CPC_DEFAULT_COMMAND_TIMEOUT); } public async start(): Promise { - return await this.transport.initPort() + return await this.transport.initPort(); } public async stop(): Promise { - await this.transport.close(false) + await this.transport.close(false); } private async onTransportData(received: Buffer): Promise { - logger.debug(`Received transport data: ${received.toString('hex')}.`, NS) + logger.debug(`Received transport data: ${received.toString("hex")}.`, NS); - this.receiveSystemUFrame(received) + this.receiveSystemUFrame(received); } private onTransportFailed(): void { - this.emit(CpcEvent.FAILED) + this.emit(CpcEvent.FAILED); } private resolveSequence(command: CpcSystemCommand): void { if (this.waiter?.sequence === command.seq) { - clearTimeout(this.waiter.timeout) - this.waiter.resolve(command) + clearTimeout(this.waiter.timeout); + this.waiter.resolve(command); - this.waiter = undefined + this.waiter = undefined; } } @@ -248,13 +248,13 @@ export class Cpc extends EventEmitter { resolve, sequence, timeout: setTimeout(() => { - const msg = `Timed out waiting for sequence(${sequence}) after ${timeout}ms.` - this.waiter = undefined + const msg = `Timed out waiting for sequence(${sequence}) after ${timeout}ms.`; + this.waiter = undefined; - logger.error(msg, NS) - this.emit(CpcEvent.FAILED) + logger.error(msg, NS); + this.emit(CpcEvent.FAILED); }, timeout), - } - }) + }; + }); } } diff --git a/src/utils/ember.ts b/src/utils/ember.ts index dbe96b8..504042e 100644 --- a/src/utils/ember.ts +++ b/src/utils/ember.ts @@ -1,16 +1,16 @@ -import type { EmberMulticastId, EmberMulticastTableEntry, EmberNetworkInitStruct } from 'zigbee-herdsman/dist/adapter/ember/types.js' +import type { EmberMulticastId, EmberMulticastTableEntry, EmberNetworkInitStruct } from "zigbee-herdsman/dist/adapter/ember/types.js"; -import type { EmberFullVersion, PortConf } from './types.js' +import type { EmberFullVersion, PortConf } from "./types.js"; -import { Zcl, ZSpec } from 'zigbee-herdsman' -import { DEFAULT_STACK_CONFIG } from 'zigbee-herdsman/dist/adapter/ember/adapter/emberAdapter.js' -import { FIXED_ENDPOINTS } from 'zigbee-herdsman/dist/adapter/ember/adapter/endpoints.js' +import { ZSpec, type Zcl } from "zigbee-herdsman"; +import type { DEFAULT_STACK_CONFIG } from "zigbee-herdsman/dist/adapter/ember/adapter/emberAdapter.js"; +import { FIXED_ENDPOINTS } from "zigbee-herdsman/dist/adapter/ember/adapter/endpoints.js"; import { EMBER_HIGH_RAM_CONCENTRATOR, EMBER_LOW_RAM_CONCENTRATOR, SECURITY_LEVEL_Z3, STACK_PROFILE_ZIGBEE_PRO, -} from 'zigbee-herdsman/dist/adapter/ember/consts.js' +} from "zigbee-herdsman/dist/adapter/ember/consts.js"; import { EmberKeyStructBitmask, EmberLibraryId, @@ -21,134 +21,134 @@ import { EzspStatus, IEEE802154CcaMode, SLStatus, -} from 'zigbee-herdsman/dist/adapter/ember/enums.js' -import { EZSP_MIN_PROTOCOL_VERSION, EZSP_PROTOCOL_VERSION, EZSP_STACK_TYPE_MESH } from 'zigbee-herdsman/dist/adapter/ember/ezsp/consts.js' -import { EzspConfigId, EzspDecisionId, EzspPolicyId, EzspValueId } from 'zigbee-herdsman/dist/adapter/ember/ezsp/enums.js' -import { Ezsp } from 'zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js' -import { lowHighBytes } from 'zigbee-herdsman/dist/adapter/ember/utils/math.js' +} from "zigbee-herdsman/dist/adapter/ember/enums.js"; +import { EZSP_MIN_PROTOCOL_VERSION, EZSP_PROTOCOL_VERSION, EZSP_STACK_TYPE_MESH } from "zigbee-herdsman/dist/adapter/ember/ezsp/consts.js"; +import { EzspConfigId, EzspDecisionId, EzspPolicyId, EzspValueId } from "zigbee-herdsman/dist/adapter/ember/ezsp/enums.js"; +import { Ezsp } from "zigbee-herdsman/dist/adapter/ember/ezsp/ezsp.js"; +import { lowHighBytes } from "zigbee-herdsman/dist/adapter/ember/utils/math.js"; -import { logger } from '../index.js' -import { NVM3ObjectKey } from './enums.js' -import { ROUTER_FIXED_ENDPOINTS } from './router-endpoints.js' +import { logger } from "../index.js"; +import { NVM3ObjectKey } from "./enums.js"; +import { ROUTER_FIXED_ENDPOINTS } from "./router-endpoints.js"; -const NS = { namespace: 'ember' } +const NS = { namespace: "ember" }; export let emberFullVersion: EmberFullVersion = { ezsp: -1, - revision: 'unknown', + revision: "unknown", build: -1, major: -1, minor: -1, patch: -1, special: -1, type: EmberVersionType.PRE_RELEASE, -} +}; -export const waitForStackStatus = async (ezsp: Ezsp, status: SLStatus, timeout: number = 10000): Promise => +export const waitForStackStatus = async (ezsp: Ezsp, status: SLStatus, timeout = 10000): Promise => await new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { - ezsp.removeListener('stackStatus', onStackStatus) - return reject(new Error(`Timed out waiting for stack status '${SLStatus[status]}'.`)) - }, timeout) + ezsp.removeListener("stackStatus", onStackStatus); + return reject(new Error(`Timed out waiting for stack status '${SLStatus[status]}'.`)); + }, timeout); const onStackStatus = (receivedStatus: SLStatus): void => { - logger.debug(`Received stack status ${receivedStatus} while waiting for ${status}.`, NS) + logger.debug(`Received stack status ${receivedStatus} while waiting for ${status}.`, NS); if (status === receivedStatus) { - clearTimeout(timeoutHandle) - ezsp.removeListener('stackStatus', onStackStatus) - resolve() + clearTimeout(timeoutHandle); + ezsp.removeListener("stackStatus", onStackStatus); + resolve(); } - } + }; - ezsp.on('stackStatus', onStackStatus) - }) + ezsp.on("stackStatus", onStackStatus); + }); export const emberStart = async (portConf: PortConf): Promise => { - const ezsp = new Ezsp({ adapter: 'ember', ...portConf }) + const ezsp = new Ezsp({ adapter: "ember", ...portConf }); // NOTE: something deep in this call can throw too - const startResult = await ezsp.start() + const startResult = await ezsp.start(); if (startResult !== 0) { - throw new Error(`Failed to start EZSP layer with status=${EzspStatus[startResult]}.`) + throw new Error(`Failed to start EZSP layer with status=${EzspStatus[startResult]}.`); } // call before any other command, else fails - emberFullVersion = await emberVersion(ezsp) + emberFullVersion = await emberVersion(ezsp); - return ezsp -} + return ezsp; +}; export const emberStop = async (ezsp: Ezsp): Promise => { // workaround to remove ASH COUNTERS logged on stop // @ts-expect-error workaround (overriding private) - ezsp.ash.logCounters = (): void => {} + ezsp.ash.logCounters = (): void => {}; - await ezsp.stop() -} + await ezsp.stop(); +}; export const emberVersion = async (ezsp: Ezsp): Promise => { // send the Host version number to the NCP. // The NCP returns the EZSP version that the NCP is running along with the stackType and stackVersion - let [ncpEzspProtocolVer, ncpStackType, ncpStackVer] = await ezsp.ezspVersion(EZSP_PROTOCOL_VERSION) + let [ncpEzspProtocolVer, ncpStackType, ncpStackVer] = await ezsp.ezspVersion(EZSP_PROTOCOL_VERSION); // verify that the stack type is what is expected if (ncpStackType !== EZSP_STACK_TYPE_MESH) { - throw new Error(`Stack type ${ncpStackType} is not expected!`) + throw new Error(`Stack type ${ncpStackType} is not expected!`); } if (ncpEzspProtocolVer === EZSP_PROTOCOL_VERSION) { - logger.debug(`NCP EZSP protocol version (${ncpEzspProtocolVer}) matches Host.`, NS) + logger.debug(`NCP EZSP protocol version (${ncpEzspProtocolVer}) matches Host.`, NS); } else if (ncpEzspProtocolVer < EZSP_PROTOCOL_VERSION && ncpEzspProtocolVer >= EZSP_MIN_PROTOCOL_VERSION) { - ;[ncpEzspProtocolVer, ncpStackType, ncpStackVer] = await ezsp.ezspVersion(ncpEzspProtocolVer) + [ncpEzspProtocolVer, ncpStackType, ncpStackVer] = await ezsp.ezspVersion(ncpEzspProtocolVer); - logger.info(`NCP EZSP protocol version (${ncpEzspProtocolVer}) lower than Host. Switched.`, NS) + logger.info(`NCP EZSP protocol version (${ncpEzspProtocolVer}) lower than Host. Switched.`, NS); } else { throw new Error( `NCP EZSP protocol version (${ncpEzspProtocolVer}) is not supported by Host [${EZSP_MIN_PROTOCOL_VERSION}-${EZSP_PROTOCOL_VERSION}].`, - ) + ); } - ezsp.setProtocolVersion(ncpEzspProtocolVer) - logger.debug(`NCP info: EZSPVersion=${ncpEzspProtocolVer} StackType=${ncpStackType} StackVersion=${ncpStackVer}`, NS) + ezsp.setProtocolVersion(ncpEzspProtocolVer); + logger.debug(`NCP info: EZSPVersion=${ncpEzspProtocolVer} StackType=${ncpStackType} StackVersion=${ncpStackVer}`, NS); - const [status, versionStruct] = await ezsp.ezspGetVersionStruct() + const [status, versionStruct] = await ezsp.ezspGetVersionStruct(); if (status !== SLStatus.OK) { // Should never happen with support of only EZSP v13+ - throw new Error(`NCP has old-style version number. Not supported.`) + throw new Error("NCP has old-style version number. Not supported."); } const version: EmberFullVersion = { ezsp: ncpEzspProtocolVer, revision: `${versionStruct.major}.${versionStruct.minor}.${versionStruct.patch} [${EmberVersionType[versionStruct.type]}]`, ...versionStruct, - } + }; if (versionStruct.type !== EmberVersionType.GA) { - logger.warning(`NCP is running a non-GA version (${EmberVersionType[versionStruct.type]}).`, NS) + logger.warning(`NCP is running a non-GA version (${EmberVersionType[versionStruct.type]}).`, NS); } - logger.info(`NCP version: ${JSON.stringify(version)}`, NS) + logger.info(`NCP version: ${JSON.stringify(version)}`, NS); - return version -} + return version; +}; -export const emberNetworkInit = async (ezsp: Ezsp, wasConfigured: boolean = false): Promise => { +export const emberNetworkInit = async (ezsp: Ezsp, wasConfigured = false): Promise => { if (!wasConfigured) { // minimum required for proper network init - const status = await ezsp.ezspSetConfigurationValue(EzspConfigId.STACK_PROFILE, STACK_PROFILE_ZIGBEE_PRO) + const status = await ezsp.ezspSetConfigurationValue(EzspConfigId.STACK_PROFILE, STACK_PROFILE_ZIGBEE_PRO); if (status !== SLStatus.OK) { - throw new Error(`Failed to set stack profile with status=${SLStatus[status]}.`) + throw new Error(`Failed to set stack profile with status=${SLStatus[status]}.`); } } const networkInitStruct: EmberNetworkInitStruct = { bitmask: EmberNetworkInitBitmask.PARENT_INFO_IN_TOKEN | EmberNetworkInitBitmask.END_DEVICE_REJOIN_ON_REBOOT, - } + }; - return await ezsp.ezspNetworkInit(networkInitStruct) -} + return await ezsp.ezspNetworkInit(networkInitStruct); +}; export const emberNetworkConfig = async ( ezsp: Ezsp, @@ -156,46 +156,46 @@ export const emberNetworkConfig = async ( manufacturerCode: Zcl.ManufacturerCode, ): Promise => { /** The address cache needs to be initialized and used with the source routing code for the trust center to operate properly. */ - await ezsp.ezspSetConfigurationValue(EzspConfigId.TRUST_CENTER_ADDRESS_CACHE_SIZE, 2) + await ezsp.ezspSetConfigurationValue(EzspConfigId.TRUST_CENTER_ADDRESS_CACHE_SIZE, 2); /** MAC indirect timeout should be 7.68 secs (STACK_PROFILE_ZIGBEE_PRO) */ - await ezsp.ezspSetConfigurationValue(EzspConfigId.INDIRECT_TRANSMISSION_TIMEOUT, 7680) + await ezsp.ezspSetConfigurationValue(EzspConfigId.INDIRECT_TRANSMISSION_TIMEOUT, 7680); /** Max hops should be 2 * nwkMaxDepth, where nwkMaxDepth is 15 (STACK_PROFILE_ZIGBEE_PRO) */ - await ezsp.ezspSetConfigurationValue(EzspConfigId.MAX_HOPS, 30) - await ezsp.ezspSetConfigurationValue(EzspConfigId.SUPPORTED_NETWORKS, 1) + await ezsp.ezspSetConfigurationValue(EzspConfigId.MAX_HOPS, 30); + await ezsp.ezspSetConfigurationValue(EzspConfigId.SUPPORTED_NETWORKS, 1); // allow other devices to modify the binding table - await ezsp.ezspSetPolicy(EzspPolicyId.BINDING_MODIFICATION_POLICY, EzspDecisionId.CHECK_BINDING_MODIFICATIONS_ARE_VALID_ENDPOINT_CLUSTERS) + await ezsp.ezspSetPolicy(EzspPolicyId.BINDING_MODIFICATION_POLICY, EzspDecisionId.CHECK_BINDING_MODIFICATIONS_ARE_VALID_ENDPOINT_CLUSTERS); // return message tag only in ezspMessageSentHandler() - await ezsp.ezspSetPolicy(EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY, EzspDecisionId.MESSAGE_TAG_ONLY_IN_CALLBACK) - await ezsp.ezspSetValue(EzspValueId.TRANSIENT_DEVICE_TIMEOUT, 2, lowHighBytes(stackConf.TRANSIENT_DEVICE_TIMEOUT)) - await ezsp.ezspSetManufacturerCode(manufacturerCode) + await ezsp.ezspSetPolicy(EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY, EzspDecisionId.MESSAGE_TAG_ONLY_IN_CALLBACK); + await ezsp.ezspSetValue(EzspValueId.TRANSIENT_DEVICE_TIMEOUT, 2, lowHighBytes(stackConf.TRANSIENT_DEVICE_TIMEOUT)); + await ezsp.ezspSetManufacturerCode(manufacturerCode); // network security init - await ezsp.ezspSetConfigurationValue(EzspConfigId.STACK_PROFILE, STACK_PROFILE_ZIGBEE_PRO) - await ezsp.ezspSetConfigurationValue(EzspConfigId.SECURITY_LEVEL, SECURITY_LEVEL_Z3) + await ezsp.ezspSetConfigurationValue(EzspConfigId.STACK_PROFILE, STACK_PROFILE_ZIGBEE_PRO); + await ezsp.ezspSetConfigurationValue(EzspConfigId.SECURITY_LEVEL, SECURITY_LEVEL_Z3); // common configs - await ezsp.ezspSetConfigurationValue(EzspConfigId.MAX_END_DEVICE_CHILDREN, stackConf.MAX_END_DEVICE_CHILDREN) - await ezsp.ezspSetConfigurationValue(EzspConfigId.END_DEVICE_POLL_TIMEOUT, stackConf.END_DEVICE_POLL_TIMEOUT) - await ezsp.ezspSetConfigurationValue(EzspConfigId.TRANSIENT_KEY_TIMEOUT_S, stackConf.TRANSIENT_KEY_TIMEOUT_S) + await ezsp.ezspSetConfigurationValue(EzspConfigId.MAX_END_DEVICE_CHILDREN, stackConf.MAX_END_DEVICE_CHILDREN); + await ezsp.ezspSetConfigurationValue(EzspConfigId.END_DEVICE_POLL_TIMEOUT, stackConf.END_DEVICE_POLL_TIMEOUT); + await ezsp.ezspSetConfigurationValue(EzspConfigId.TRANSIENT_KEY_TIMEOUT_S, stackConf.TRANSIENT_KEY_TIMEOUT_S); // XXX: temp-fix: forces a side-effect in the firmware that prevents broadcast issues in environments with unusual interferences - await ezsp.ezspSetValue(EzspValueId.CCA_THRESHOLD, 1, [0]) + await ezsp.ezspSetValue(EzspValueId.CCA_THRESHOLD, 1, [0]); if (stackConf.CCA_MODE) { // validated in `loadStackConfig` - await ezsp.ezspSetRadioIeee802154CcaMode(IEEE802154CcaMode[stackConf.CCA_MODE]) + await ezsp.ezspSetRadioIeee802154CcaMode(IEEE802154CcaMode[stackConf.CCA_MODE]); } -} +}; -export const emberRegisterFixedEndpoints = async (ezsp: Ezsp, multicastTable: EmberMulticastId[], router: boolean = false): Promise => { +export const emberRegisterFixedEndpoints = async (ezsp: Ezsp, multicastTable: EmberMulticastId[], router = false): Promise => { for (const ep of router ? ROUTER_FIXED_ENDPOINTS : FIXED_ENDPOINTS) { if (ep.networkIndex !== 0x00) { - logger.debug(`Multi-network not currently supported. Skipping endpoint ${JSON.stringify(ep)}.`, NS) - continue + logger.debug(`Multi-network not currently supported. Skipping endpoint ${JSON.stringify(ep)}.`, NS); + continue; } - const [epStatus] = await ezsp.ezspGetEndpointFlags(ep.endpoint) + const [epStatus] = await ezsp.ezspGetEndpointFlags(ep.endpoint); // endpoint already registered if (epStatus === SLStatus.OK) { - logger.debug(`Endpoint '${ep.endpoint}' already registered.`, NS) + logger.debug(`Endpoint '${ep.endpoint}' already registered.`, NS); } else { // check to see if ezspAddEndpoint needs to be called // if ezspInit is called without NCP reset, ezspAddEndpoint is not necessary and will return an error @@ -206,12 +206,12 @@ export const emberRegisterFixedEndpoints = async (ezsp: Ezsp, multicastTable: Em ep.deviceVersion, [...ep.inClusterList], // copy [...ep.outClusterList], // copy - ) + ); if (status === SLStatus.OK) { - logger.debug(`Registered endpoint '${ep.endpoint}'.`, NS) + logger.debug(`Registered endpoint '${ep.endpoint}'.`, NS); } else { - throw new Error(`Failed to register endpoint '${ep.endpoint}' with status=${SLStatus[status]}.`) + throw new Error(`Failed to register endpoint '${ep.endpoint}' with status=${SLStatus[status]}.`); } } @@ -220,90 +220,90 @@ export const emberRegisterFixedEndpoints = async (ezsp: Ezsp, multicastTable: Em multicastId, endpoint: ep.endpoint, networkIndex: ep.networkIndex, - } + }; - const status = await ezsp.ezspSetMulticastTableEntry(multicastTable.length, multicastEntry) + const status = await ezsp.ezspSetMulticastTableEntry(multicastTable.length, multicastEntry); if (status !== SLStatus.OK) { - throw new Error(`Failed to register group '${multicastId}' in multicast table with status=${SLStatus[status]}.`) + throw new Error(`Failed to register group '${multicastId}' in multicast table with status=${SLStatus[status]}.`); } - logger.debug(`Registered multicast table entry (${multicastTable.length}): ${JSON.stringify(multicastEntry)}.`, NS) - multicastTable.push(multicastEntry.multicastId) + logger.debug(`Registered multicast table entry (${multicastTable.length}): ${JSON.stringify(multicastEntry)}.`, NS); + multicastTable.push(multicastEntry.multicastId); } } -} +}; export const emberSetConcentrator = async (ezsp: Ezsp, stackConf: typeof DEFAULT_STACK_CONFIG): Promise => { const status = await ezsp.ezspSetConcentrator( true, - stackConf.CONCENTRATOR_RAM_TYPE === 'low' ? EMBER_LOW_RAM_CONCENTRATOR : EMBER_HIGH_RAM_CONCENTRATOR, + stackConf.CONCENTRATOR_RAM_TYPE === "low" ? EMBER_LOW_RAM_CONCENTRATOR : EMBER_HIGH_RAM_CONCENTRATOR, stackConf.CONCENTRATOR_MIN_TIME, stackConf.CONCENTRATOR_MAX_TIME, stackConf.CONCENTRATOR_ROUTE_ERROR_THRESHOLD, stackConf.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, stackConf.CONCENTRATOR_MAX_HOPS, - ) + ); if (status !== SLStatus.OK) { - throw new Error(`[CONCENTRATOR] Failed to set concentrator with status=${SLStatus[status]}.`) + throw new Error(`[CONCENTRATOR] Failed to set concentrator with status=${SLStatus[status]}.`); } - const remainTilMTORR = await ezsp.ezspSetSourceRouteDiscoveryMode(EmberSourceRouteDiscoveryMode.RESCHEDULE) + const remainTilMTORR = await ezsp.ezspSetSourceRouteDiscoveryMode(EmberSourceRouteDiscoveryMode.RESCHEDULE); - logger.info(`[CONCENTRATOR] Started source route discovery. ${remainTilMTORR}ms until next broadcast.`, NS) -} + logger.info(`[CONCENTRATOR] Started source route discovery. ${remainTilMTORR}ms until next broadcast.`, NS); +}; // -- Utils export const getLibraryStatus = (id: EmberLibraryId, status: EmberLibraryStatus): string => { if (status === EmberLibraryStatus.LIBRARY_ERROR) { - return 'ERROR' + return "ERROR"; } - let statusStr: string = 'NOT_PRESENT' - const present = Boolean(status & EmberLibraryStatus.LIBRARY_PRESENT_MASK) + let statusStr = "NOT_PRESENT"; + const present = Boolean(status & EmberLibraryStatus.LIBRARY_PRESENT_MASK); if (present) { - statusStr = 'PRESENT' + statusStr = "PRESENT"; if (id === EmberLibraryId.ZIGBEE_PRO) { - statusStr += status & EmberLibraryStatus.ZIGBEE_PRO_LIBRARY_HAVE_ROUTER_CAPABILITY ? ' / ROUTER_CAPABILITY' : ' / END_DEVICE_ONLY' + statusStr += status & EmberLibraryStatus.ZIGBEE_PRO_LIBRARY_HAVE_ROUTER_CAPABILITY ? " / ROUTER_CAPABILITY" : " / END_DEVICE_ONLY"; if (status & EmberLibraryStatus.ZIGBEE_PRO_LIBRARY_ZLL_SUPPORT) { - statusStr += ' / ZLL_SUPPORT' + statusStr += " / ZLL_SUPPORT"; } } if (id === EmberLibraryId.SECURITY_CORE) { - statusStr += status & EmberLibraryStatus.SECURITY_LIBRARY_HAVE_ROUTER_SUPPORT ? ' / ROUTER_SUPPORT' : ' / END_DEVICE_ONLY' + statusStr += status & EmberLibraryStatus.SECURITY_LIBRARY_HAVE_ROUTER_SUPPORT ? " / ROUTER_SUPPORT" : " / END_DEVICE_ONLY"; } if (id === EmberLibraryId.PACKET_VALIDATE) { - statusStr += status & EmberLibraryStatus.PACKET_VALIDATE_LIBRARY_ENABLED ? ' / ENABLED' : ' / DISABLED' + statusStr += status & EmberLibraryStatus.PACKET_VALIDATE_LIBRARY_ENABLED ? " / ENABLED" : " / DISABLED"; } } - return statusStr -} + return statusStr; +}; export const getKeyStructBitmask = (bitmask: EmberKeyStructBitmask): string => { - const bitmaskValues: string[] = [] + const bitmaskValues: string[] = []; for (const key in EmberKeyStructBitmask) { - const val = EmberKeyStructBitmask[key as keyof typeof EmberKeyStructBitmask] + const val = EmberKeyStructBitmask[key as keyof typeof EmberKeyStructBitmask]; - if (typeof val !== 'number') { - continue + if (typeof val !== "number") { + continue; } if (bitmask & val) { - bitmaskValues.push(key) + bitmaskValues.push(key); } } - return bitmaskValues.join('|') -} + return bitmaskValues.join("|"); +}; export const parseTokenData = (nvm3Key: NVM3ObjectKey, data: Buffer): string => { switch (nvm3Key) { @@ -314,16 +314,16 @@ export const parseTokenData = (nvm3Key: NVM3ObjectKey, data: Buffer): string => case NVM3ObjectKey.STACK_APS_FRAME_COUNTER: case NVM3ObjectKey.STACK_GP_INCOMING_FC: case NVM3ObjectKey.STACK_GP_INCOMING_FC_IN_SINK: { - return `${data.readUIntLE(0, data.length)}` + return `${data.readUIntLE(0, data.length)}`; } case NVM3ObjectKey.STACK_MIN_RECEIVED_RSSI: { - return `${data.readIntLE(0, data.length)}` + return `${data.readIntLE(0, data.length)}`; } case NVM3ObjectKey.STACK_CHILD_TABLE: { // TODO - return `EUI64: ${data.subarray(0, 8).toString('hex')} | ${data.subarray(8).toString('hex')}` + return `EUI64: ${data.subarray(0, 8).toString("hex")} | ${data.subarray(8).toString("hex")}`; } // TODO: @@ -334,31 +334,31 @@ export const parseTokenData = (nvm3Key: NVM3ObjectKey, data: Buffer): string => case NVM3ObjectKey.STACK_TRUST_CENTER: { // TODO - return `${data.subarray(0, 2).toString('hex')} | EUI64: ${data.subarray(2, 10).toString('hex')} | Link Key: ${data.subarray(10).toString('hex')}` + return `${data.subarray(0, 2).toString("hex")} | EUI64: ${data.subarray(2, 10).toString("hex")} | Link Key: ${data.subarray(10).toString("hex")}`; } case NVM3ObjectKey.STACK_KEYS: case NVM3ObjectKey.STACK_ALTERNATE_KEY: { // TODO - return `Network Key: ${data.subarray(0, -1).toString('hex')} | Sequence Number: ${data.readUInt8(16)}` + return `Network Key: ${data.subarray(0, -1).toString("hex")} | Sequence Number: ${data.readUInt8(16)}`; } case NVM3ObjectKey.STACK_NODE_DATA: { // TODO // [4-5] === network join status? return ( - `PAN ID: ${data.subarray(0, 2).toString('hex')} | Radio TX Power ${data.readUInt8(2)} | Radio Channel ${data.readUInt8(3)} ` + - `| ${data.subarray(4, 8).toString('hex')} | Ext PAN ID: ${data.subarray(8, 16).toString('hex')}` - ) + `PAN ID: ${data.subarray(0, 2).toString("hex")} | Radio TX Power ${data.readUInt8(2)} | Radio Channel ${data.readUInt8(3)} ` + + `| ${data.subarray(4, 8).toString("hex")} | Ext PAN ID: ${data.subarray(8, 16).toString("hex")}` + ); } case NVM3ObjectKey.STACK_NETWORK_MANAGEMENT: { // TODO - return `Channels: ${ZSpec.Utils.uint32MaskToChannels(data.readUInt32LE(0))} | ${data.subarray(4).toString('hex')}` + return `Channels: ${ZSpec.Utils.uint32MaskToChannels(data.readUInt32LE(0))} | ${data.subarray(4).toString("hex")}`; } default: { - return data.toString('hex') + return data.toString("hex"); } } -} +}; diff --git a/src/utils/port.ts b/src/utils/port.ts index 9f8b1e6..6764b01 100644 --- a/src/utils/port.ts +++ b/src/utils/port.ts @@ -1,192 +1,192 @@ -import type { BaudRate, PortConf, PortType, SelectChoices } from './types.js' +import type { BaudRate, PortConf, PortType, SelectChoices } from "./types.js"; -import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { confirm, input, select } from '@inquirer/prompts' -import { Bonjour } from 'bonjour-service' +import { confirm, input, select } from "@inquirer/prompts"; +import { Bonjour } from "bonjour-service"; -import { SerialPort } from 'zigbee-herdsman/dist/adapter/serialPort.js' +import { SerialPort } from "zigbee-herdsman/dist/adapter/serialPort.js"; -import { CONF_PORT_PATH, logger } from '../index.js' -import { BAUDRATES, TCP_REGEX } from './consts.js' +import { CONF_PORT_PATH, logger } from "../index.js"; +import { BAUDRATES, TCP_REGEX } from "./consts.js"; async function findmDNSAdapters(): Promise> { - logger.info(`Starting mDNS discovery...`) + logger.info("Starting mDNS discovery..."); - const bonjour = new Bonjour() - const adapters: SelectChoices = [{ name: 'Not in this list', value: undefined }] + const bonjour = new Bonjour(); + const adapters: SelectChoices = [{ name: "Not in this list", value: undefined }]; const browser = bonjour.find(null, (service) => { - if (service.txt && service.txt.radio_type === 'ezsp') { - logger.debug(`Found matching service: ${JSON.stringify(service)}`) + if (service.txt && service.txt.radio_type === "ezsp") { + logger.debug(`Found matching service: ${JSON.stringify(service)}`); - const path = `tcp://${service.addresses?.[0] ?? service.host}:${service.port}` + const path = `tcp://${service.addresses?.[0] ?? service.host}:${service.port}`; - adapters.push({ name: `${service.name ?? service.txt.name ?? 'Unknown'} (${path})`, value: path }) + adapters.push({ name: `${service.name ?? service.txt.name ?? "Unknown"} (${path})`, value: path }); } - }) + }); - browser.start() + browser.start(); return await new Promise((resolve) => { setTimeout(() => { - browser.stop() - bonjour.destroy() - resolve(adapters) - }, 2000) - }) + browser.stop(); + bonjour.destroy(); + resolve(adapters); + }, 2000); + }); } export const getPortConfFile = async (): Promise => { if (!existsSync(CONF_PORT_PATH)) { - return undefined + return undefined; } - const file = readFileSync(CONF_PORT_PATH, 'utf8') - const conf: PortConf = JSON.parse(file) + const file = readFileSync(CONF_PORT_PATH, "utf8"); + const conf: PortConf = JSON.parse(file); if (!conf.path) { - logger.error(`Cached config does not include a valid path value.`) - return undefined + logger.error("Cached config does not include a valid path value."); + return undefined; } if (!TCP_REGEX.test(conf.path)) { // serial-only validation if (!conf.baudRate || !BAUDRATES.includes(conf.baudRate)) { - logger.error(`Cached config does not include a valid baudrate value.`) - return undefined + logger.error("Cached config does not include a valid baudrate value."); + return undefined; } - const portList = await SerialPort.list() + const portList = await SerialPort.list(); if (portList.length === 0) { - logger.error('Cached config is using serial, no serial device currently connected.') - return undefined + logger.error("Cached config is using serial, no serial device currently connected."); + return undefined; } if (!portList.some((p) => p.path === conf.path)) { - logger.error(`Cached config path does not match a currently connected serial device.`) - return undefined + logger.error("Cached config path does not match a currently connected serial device."); + return undefined; } if (conf.rtscts !== true && conf.rtscts !== false) { - logger.error(`Cached config does not include a valid rtscts value.`) - return undefined + logger.error("Cached config does not include a valid rtscts value."); + return undefined; } if (conf.xon !== true && conf.xon !== false) { - conf.xon = !conf.rtscts - logger.debug(`Cached config does not include a valid xon value. Derived from rtscts (will be ${conf.xon}).`) + conf.xon = !conf.rtscts; + logger.debug(`Cached config does not include a valid xon value. Derived from rtscts (will be ${conf.xon}).`); } if (conf.xoff !== true && conf.xoff !== false) { - conf.xoff = !conf.rtscts - logger.debug(`Cached config does not include a valid xoff value. Derived from rtscts (will be ${conf.xoff}).`) + conf.xoff = !conf.rtscts; + logger.debug(`Cached config does not include a valid xoff value. Derived from rtscts (will be ${conf.xoff}).`); } } - return conf -} + return conf; +}; export const getPortConf = async (): Promise => { - const portConfFile = await getPortConfFile() + const portConfFile = await getPortConfFile(); if (portConfFile !== undefined) { - const isTcp = TCP_REGEX.test(portConfFile.path) + const isTcp = TCP_REGEX.test(portConfFile.path); const usePortConfFile = await confirm({ default: true, - message: `Path: ${portConfFile.path}${isTcp ? '' : `, Baudrate: ${portConfFile.baudRate}, RTS/CTS: ${portConfFile.rtscts}`}. Use this config?`, - }) + message: `Path: ${portConfFile.path}${isTcp ? "" : `, Baudrate: ${portConfFile.baudRate}, RTS/CTS: ${portConfFile.rtscts}`}. Use this config?`, + }); if (usePortConfFile) { - return portConfFile + return portConfFile; } } const type = await select({ choices: [ - { name: 'Serial', value: 'serial' }, - { name: 'TCP', value: 'tcp' }, + { name: "Serial", value: "serial" }, + { name: "TCP", value: "tcp" }, ], - message: 'Adapter connection type', - }) + message: "Adapter connection type", + }); - let baudRate = BAUDRATES[0] - let path = null - let rtscts = false + let baudRate = BAUDRATES[0]; + let path = null; + let rtscts = false; switch (type) { - case 'serial': { - const baudrateChoices = [] + case "serial": { + const baudrateChoices = []; for (const v of BAUDRATES) { - baudrateChoices.push({ name: v.toString(), value: v }) + baudrateChoices.push({ name: v.toString(), value: v }); } baudRate = await select({ choices: baudrateChoices, - message: 'Adapter firmware baudrate', - }) + message: "Adapter firmware baudrate", + }); - const portList = await SerialPort.list() + const portList = await SerialPort.list(); if (portList.length === 0) { - throw new Error('No serial device found.') + throw new Error("No serial device found."); } path = await select({ choices: portList.map((p) => ({ // @ts-expect-error friendlyName windows only - name: `${p.manufacturer} ${p.friendlyName ?? ''} ${p.pnpId} (${p.path})`, + name: `${p.manufacturer} ${p.friendlyName ?? ""} ${p.pnpId} (${p.path})`, value: p.path, })), - message: 'Serial port', - }) + message: "Serial port", + }); const fcChoices = [ - { name: 'Software Flow Control (rtscts=false)', value: false }, - { name: 'Hardware Flow Control (rtscts=true)', value: true }, - ] + { name: "Software Flow Control (rtscts=false)", value: false }, + { name: "Hardware Flow Control (rtscts=true)", value: true }, + ]; rtscts = await select({ choices: fcChoices, - message: 'Flow control', - }) + message: "Flow control", + }); - break + break; } - case 'tcp': { - const discover = await confirm({ message: 'Try to discover adapter?', default: true }) + case "tcp": { + const discover = await confirm({ message: "Try to discover adapter?", default: true }); if (discover) { - const choices = await findmDNSAdapters() + const choices = await findmDNSAdapters(); - path = await select({ message: 'Select adapter', choices }) + path = await select({ message: "Select adapter", choices }); } if (!discover || !path) { path = await input({ message: `TCP path ('tcp://:')`, validate(value) { - return TCP_REGEX.test(value) + return TCP_REGEX.test(value); }, - }) + }); } - break + break; } } if (!path) { - throw new Error('Invalid port path.') + throw new Error("Invalid port path."); } - const conf = { baudRate, path, rtscts, xon: !rtscts, xoff: !rtscts } + const conf = { baudRate, path, rtscts, xon: !rtscts, xoff: !rtscts }; try { - writeFileSync(CONF_PORT_PATH, JSON.stringify(conf, null, 2), 'utf8') + writeFileSync(CONF_PORT_PATH, JSON.stringify(conf, null, 2), "utf8"); } catch { - logger.error(`Could not write port conf to ${CONF_PORT_PATH}.`) + logger.error(`Could not write port conf to ${CONF_PORT_PATH}.`); } - return conf -} + return conf; +}; diff --git a/src/utils/router-endpoints.ts b/src/utils/router-endpoints.ts index f20c5d6..1feb1d0 100644 --- a/src/utils/router-endpoints.ts +++ b/src/utils/router-endpoints.ts @@ -1,26 +1,26 @@ -import type { EmberMulticastId } from 'zigbee-herdsman/dist/adapter/ember/types.js' -import type { ClusterId, ProfileId } from 'zigbee-herdsman/dist/zspec/tstypes.js' +import type { EmberMulticastId } from "zigbee-herdsman/dist/adapter/ember/types.js"; +import type { ClusterId, ProfileId } from "zigbee-herdsman/dist/zspec/tstypes.js"; -import { Zcl, ZSpec } from 'zigbee-herdsman' +import { ZSpec, Zcl } from "zigbee-herdsman"; type FixedEndpointInfo = { /** Actual Zigbee endpoint number. uint8_t */ - endpoint: number + endpoint: number; /** Profile ID of the device on this endpoint. */ - profileId: ProfileId + profileId: ProfileId; /** Device ID of the device on this endpoint. uint16_t */ - deviceId: number + deviceId: number; /** Version of the device. uint8_t */ - deviceVersion: number + deviceVersion: number; /** List of server clusters. */ - inClusterList: readonly ClusterId[] + inClusterList: readonly ClusterId[]; /** List of client clusters. */ - outClusterList: readonly ClusterId[] + outClusterList: readonly ClusterId[]; /** Network index for this endpoint. uint8_t */ - networkIndex: number + networkIndex: number; /** Multicast group IDs to register in the multicast table */ - multicastIds: readonly EmberMulticastId[] -} + multicastIds: readonly EmberMulticastId[]; +}; /** * List of endpoints to register. @@ -52,4 +52,4 @@ export const ROUTER_FIXED_ENDPOINTS: readonly FixedEndpointInfo[] = [ networkIndex: 0x00, multicastIds: [0x0b84], }, -] +]; diff --git a/src/utils/transport.ts b/src/utils/transport.ts index d9bc022..4f74ea4 100644 --- a/src/utils/transport.ts +++ b/src/utils/transport.ts @@ -1,210 +1,210 @@ -import type { PortConf } from './types.js' +import type { PortConf } from "./types.js"; -import EventEmitter from 'node:events' -import { Socket } from 'node:net' -import { Readable } from 'node:stream' +import EventEmitter from "node:events"; +import { Socket } from "node:net"; +import { Readable } from "node:stream"; -import { SerialPort } from 'zigbee-herdsman/dist/adapter/serialPort.js' +import { SerialPort } from "zigbee-herdsman/dist/adapter/serialPort.js"; -import { logger } from '../index.js' -import { CONFIG_HIGHWATER_MARK, TCP_REGEX } from './consts.js' +import { logger } from "../index.js"; +import { CONFIG_HIGHWATER_MARK, TCP_REGEX } from "./consts.js"; -const NS = { namespace: 'transport' } +const NS = { namespace: "transport" }; type SetOptions = { - brk?: boolean - cts?: boolean - dsr?: boolean - dtr?: boolean - rts?: boolean -} + brk?: boolean; + cts?: boolean; + dsr?: boolean; + dtr?: boolean; + rts?: boolean; +}; class TransportWriter extends Readable { public writeBytes(bytesToWrite: Buffer): void { - this.emit('data', bytesToWrite) + this.emit("data", bytesToWrite); } public _read(): void {} } export enum TransportEvent { - CLOSED = 'closed', - DATA = 'data', - FAILED = 'failed', + CLOSED = "closed", + DATA = "data", + FAILED = "failed", } interface SerialEventMap { - [TransportEvent.CLOSED]: [] - [TransportEvent.DATA]: [data: Buffer] - [TransportEvent.FAILED]: [] + [TransportEvent.CLOSED]: []; + [TransportEvent.DATA]: [data: Buffer]; + [TransportEvent.FAILED]: []; } /** * Serial or Socket based transport based on passed conf. */ export class Transport extends EventEmitter { - public connected: boolean - public readonly portConf: PortConf - public portWriter: TransportWriter | undefined - private portSerial: SerialPort | undefined - private portSocket: Socket | undefined + public connected: boolean; + public readonly portConf: PortConf; + public portWriter: TransportWriter | undefined; + private portSerial: SerialPort | undefined; + private portSocket: Socket | undefined; constructor(portConf: PortConf) { - super() + super(); - this.connected = false - this.portConf = portConf + this.connected = false; + this.portConf = portConf; } get isSerial(): boolean { - return Boolean(this.portSerial) + return Boolean(this.portSerial); } - public async close(emitClosed: boolean, emitFailed: boolean = true): Promise { + public async close(emitClosed: boolean, emitFailed = true): Promise { if (this.portSerial?.isOpen) { - logger.info(`Closing serial connection...`, NS) + logger.info("Closing serial connection...", NS); try { - await this.portSerial.asyncFlushAndClose() + await this.portSerial.asyncFlushAndClose(); } catch (error) { - logger.error(`Failed to close port: ${error}.`, NS) - this.portSerial.removeAllListeners() + logger.error(`Failed to close port: ${error}.`, NS); + this.portSerial.removeAllListeners(); if (emitFailed) { - this.emit(TransportEvent.FAILED) + this.emit(TransportEvent.FAILED); } - return + return; } - this.portSerial.removeAllListeners() + this.portSerial.removeAllListeners(); } else if (this.portSocket !== undefined && !this.portSocket.closed) { - logger.info(`Closing socket connection...`, NS) - this.portSocket.destroy() - this.portSocket.removeAllListeners() + logger.info("Closing socket connection...", NS); + this.portSocket.destroy(); + this.portSocket.removeAllListeners(); } if (emitClosed) { - this.emit(TransportEvent.CLOSED) + this.emit(TransportEvent.CLOSED); } } public async initPort(): Promise { // will do nothing if nothing's open - await this.close(false) + await this.close(false); if (TCP_REGEX.test(this.portConf.path)) { - const info = new URL(this.portConf.path) - logger.debug(`Opening TCP socket with ${info.hostname}:${info.port}`, NS) + const info = new URL(this.portConf.path); + logger.debug(`Opening TCP socket with ${info.hostname}:${info.port}`, NS); - this.portSocket = new Socket() + this.portSocket = new Socket(); - this.portSocket.setNoDelay(true) - this.portSocket.setKeepAlive(true, 15000) + this.portSocket.setNoDelay(true); + this.portSocket.setKeepAlive(true, 15000); - this.portWriter = new TransportWriter({ highWaterMark: CONFIG_HIGHWATER_MARK }) + this.portWriter = new TransportWriter({ highWaterMark: CONFIG_HIGHWATER_MARK }); - this.portWriter.pipe(this.portSocket) - this.portSocket.on('data', this.emitData.bind(this)) + this.portWriter.pipe(this.portSocket); + this.portSocket.on("data", this.emitData.bind(this)); return await new Promise((resolve, reject): void => { const openError = async (err: Error): Promise => { - reject(err) - } + reject(err); + }; if (this.portSocket === undefined) { - reject(new Error(`Invalid socket`)) - return + reject(new Error("Invalid socket")); + return; } - this.portSocket.on('connect', () => { - logger.debug(`Socket connected`, NS) - }) - this.portSocket.on('ready', (): void => { - logger.info(`Socket ready`, NS) - this.portSocket!.removeListener('error', openError) - this.portSocket!.once('close', this.onPortClose.bind(this)) - this.portSocket!.on('error', this.onPortError.bind(this)) - - this.connected = true - - resolve() - }) - this.portSocket.once('error', openError) - this.portSocket.connect(Number.parseInt(info.port, 10), info.hostname) - }) + this.portSocket.on("connect", () => { + logger.debug("Socket connected", NS); + }); + this.portSocket.on("ready", (): void => { + logger.info("Socket ready", NS); + this.portSocket!.removeListener("error", openError); + this.portSocket!.once("close", this.onPortClose.bind(this)); + this.portSocket!.on("error", this.onPortError.bind(this)); + + this.connected = true; + + resolve(); + }); + this.portSocket.once("error", openError); + this.portSocket.connect(Number.parseInt(info.port, 10), info.hostname); + }); } const serialOpts = { autoOpen: false, baudRate: this.portConf.baudRate, dataBits: 8 as const, - parity: 'none' as const, + parity: "none" as const, path: this.portConf.path, rtscts: this.portConf.rtscts, stopBits: 1 as const, xoff: this.portConf.xoff, xon: this.portConf.xon, - } + }; - logger.debug(`Opening serial port with ${JSON.stringify(serialOpts)}`, NS) + logger.debug(`Opening serial port with ${JSON.stringify(serialOpts)}`, NS); - this.portSerial = new SerialPort(serialOpts) - this.portWriter = new TransportWriter({ highWaterMark: CONFIG_HIGHWATER_MARK }) + this.portSerial = new SerialPort(serialOpts); + this.portWriter = new TransportWriter({ highWaterMark: CONFIG_HIGHWATER_MARK }); - this.portWriter.pipe(this.portSerial) - this.portSerial.on('data', this.emitData.bind(this)) + this.portWriter.pipe(this.portSerial); + this.portSerial.on("data", this.emitData.bind(this)); - await this.portSerial.asyncOpen() - logger.info(`Serial port opened`, NS) + await this.portSerial.asyncOpen(); + logger.info("Serial port opened", NS); - this.portSerial.once('close', this.onPortClose.bind(this)) - this.portSerial.on('error', this.onPortError.bind(this)) + this.portSerial.once("close", this.onPortClose.bind(this)); + this.portSerial.on("error", this.onPortError.bind(this)); - this.connected = true + this.connected = true; } public async serialSet(options: SetOptions, afterDelayMS?: number): Promise { await new Promise((resolve, reject) => { - const fn = (): void => this.portSerial?.set(options, (error) => (error ? reject(error) : resolve())) + const fn = (): void => this.portSerial?.set(options, (error) => (error ? reject(error) : resolve())); if (afterDelayMS) { - setTimeout(fn, afterDelayMS) + setTimeout(fn, afterDelayMS); } else { - fn() + fn(); } - }) + }); } public async write(buffer: Buffer): Promise { if (this.portWriter === undefined) { - logger.error(`No port available to write.`, NS) - this.emit(TransportEvent.FAILED) + logger.error("No port available to write.", NS); + this.emit(TransportEvent.FAILED); } else { - logger.debug(`Sending transport data: ${buffer.toString('hex')}.`, NS) - this.portWriter.writeBytes(buffer) + logger.debug(`Sending transport data: ${buffer.toString("hex")}.`, NS); + this.portWriter.writeBytes(buffer); } } private emitData(data: Buffer): void { - this.emit(TransportEvent.DATA, data) + this.emit(TransportEvent.DATA, data); } private onPortClose(error: Error): void { - logger.info(`Transport closed.`, NS) + logger.info("Transport closed.", NS); if (error && this.connected) { - logger.info(`Transport close ${error}`, NS) - this.emit(TransportEvent.FAILED) + logger.info(`Transport close ${error}`, NS); + this.emit(TransportEvent.FAILED); } else { - this.emit(TransportEvent.CLOSED) + this.emit(TransportEvent.CLOSED); } } private onPortError(error: Error): void { - this.connected = false + this.connected = false; - logger.info(`Transport ${error}`, NS) - this.emit(TransportEvent.FAILED) + logger.info(`Transport ${error}`, NS); + this.emit(TransportEvent.FAILED); } } diff --git a/src/utils/types.ts b/src/utils/types.ts index 64f380f..bbff623 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,84 +1,84 @@ -import type { checkbox, select } from '@inquirer/prompts' -import type { EmberKeyData, EmberVersion } from 'zigbee-herdsman/dist/adapter/ember/types.js' -import type { EUI64 } from 'zigbee-herdsman/dist/zspec/tstypes.js' +import type { checkbox, select } from "@inquirer/prompts"; +import type { EmberKeyData, EmberVersion } from "zigbee-herdsman/dist/adapter/ember/types.js"; +import type { EUI64 } from "zigbee-herdsman/dist/zspec/tstypes.js"; -import { BAUDRATES } from './consts.js' -import { CpcSystemCommandId } from './enums.js' +import type { BAUDRATES } from "./consts.js"; +import type { CpcSystemCommandId } from "./enums.js"; // https://github.com/microsoft/TypeScript/issues/24509 export type Mutable = { - -readonly [P in keyof T]: T[P] extends ReadonlyArray ? Mutable[] : Mutable -} + -readonly [P in keyof T]: T[P] extends ReadonlyArray ? Mutable[] : Mutable; +}; // types from inquirer/prompts are not exported -export type CheckboxChoices = Mutable>[0]['choices']> -export type SelectChoices = Mutable>[0]['choices']> +export type CheckboxChoices = Mutable>[0]["choices"]>; +export type SelectChoices = Mutable>[0]["choices"]>; export type AdapterModel = - | 'Aeotec Zi-Stick (ZGA008)' - | 'EasyIOT ZB-GW04 v1.1' - | 'EasyIOT ZB-GW04 v1.2' - | 'Nabu Casa SkyConnect' - | 'Nabu Casa Yellow' - | 'SMLight SLZB06-M' - | 'SMLight SLZB07' - | 'SMLight SLZB07mg24' - | 'Sonoff ZBDongle-E' - | 'SparkFun MGM240p' - | 'TubeZB MGM24' - | 'TubeZB MGM24PB' - | 'ROUTER - Aeotec Zi-Stick (ZGA008)' - | 'ROUTER - EasyIOT ZB-GW04 v1.1' - | 'ROUTER - EasyIOT ZB-GW04 v1.2' - | 'ROUTER - Nabu Casa SkyConnect' - | 'ROUTER - Nabu Casa Yellow' - | 'ROUTER - SMLight SLZB06-M' - | 'ROUTER - SMLight SLZB07' - | 'ROUTER - SMLight SLZB07mg24' - | 'ROUTER - Sonoff ZBDongle-E' - | 'ROUTER - SparkFun MGM240p' - | 'ROUTER - TubeZB MGM24' - | 'ROUTER - TubeZB MGM24PB' + | "Aeotec Zi-Stick (ZGA008)" + | "EasyIOT ZB-GW04 v1.1" + | "EasyIOT ZB-GW04 v1.2" + | "Nabu Casa SkyConnect" + | "Nabu Casa Yellow" + | "SMLight SLZB06-M" + | "SMLight SLZB07" + | "SMLight SLZB07mg24" + | "Sonoff ZBDongle-E" + | "SparkFun MGM240p" + | "TubeZB MGM24" + | "TubeZB MGM24PB" + | "ROUTER - Aeotec Zi-Stick (ZGA008)" + | "ROUTER - EasyIOT ZB-GW04 v1.1" + | "ROUTER - EasyIOT ZB-GW04 v1.2" + | "ROUTER - Nabu Casa SkyConnect" + | "ROUTER - Nabu Casa Yellow" + | "ROUTER - SMLight SLZB06-M" + | "ROUTER - SMLight SLZB07" + | "ROUTER - SMLight SLZB07mg24" + | "ROUTER - Sonoff ZBDongle-E" + | "ROUTER - SparkFun MGM240p" + | "ROUTER - TubeZB MGM24" + | "ROUTER - TubeZB MGM24PB"; -export type PortType = 'serial' | 'tcp' -export type BaudRate = (typeof BAUDRATES)[number] +export type PortType = "serial" | "tcp"; +export type BaudRate = (typeof BAUDRATES)[number]; export type PortConf = { - baudRate: number - path: string - rtscts: boolean - xon: boolean - xoff: boolean -} + baudRate: number; + path: string; + rtscts: boolean; + xon: boolean; + xoff: boolean; +}; -export type EmberFullVersion = { ezsp: number; revision: string } & EmberVersion -export type ConfigValue = { [key: string]: string } +export type EmberFullVersion = { ezsp: number; revision: string } & EmberVersion; +export type ConfigValue = { [key: string]: string }; -export type FirmwareVariant = 'official' | 'latest' | 'experimental' | 'nvm3_32768_clear' | 'nvm3_40960_clear' | 'app_clear' -export type FirmwareVersion = `${number}.${number}.${number}.${number}` -export type FirmwareVersionShort = `${number}.${number}.${number}` -export type FirmwareFilename = `${string}.gbl` -export type FirmwareURL = `https://${string}/${FirmwareFilename}` +export type FirmwareVariant = "official" | "latest" | "experimental" | "nvm3_32768_clear" | "nvm3_40960_clear" | "app_clear"; +export type FirmwareVersion = `${number}.${number}.${number}.${number}`; +export type FirmwareVersionShort = `${number}.${number}.${number}`; +export type FirmwareFilename = `${string}.gbl`; +export type FirmwareURL = `https://${string}/${FirmwareFilename}`; export type FirmwareFileMetadata = { - metadata_version: number // 1 - sdk_version: FirmwareVersionShort // '5.0.1' - fw_type: 'ncp-uart-hw' | 'ncp-uart-sw' | 'rcp-uart-802154' | 'rcp-uart-802154-blehci' - baudrate: number // 115200 - ezsp_version?: FirmwareVersion // '8.0.1.0' - ot_version?: FirmwareVersion // '2.5.1.0' - ble_version?: FirmwareVersionShort // '8.1.0' - cpc_version?: FirmwareVersion // '5.0.1' -} + metadata_version: number; // 1 + sdk_version: FirmwareVersionShort; // '5.0.1' + fw_type: "ncp-uart-hw" | "ncp-uart-sw" | "rcp-uart-802154" | "rcp-uart-802154-blehci"; + baudrate: number; // 115200 + ezsp_version?: FirmwareVersion; // '8.0.1.0' + ot_version?: FirmwareVersion; // '2.5.1.0' + ble_version?: FirmwareVersionShort; // '8.1.0' + cpc_version?: FirmwareVersion; // '5.0.1' +}; -export type FirmwareLinks = Record>> +export type FirmwareLinks = Record>>; export type TokensInfo = { - nvm3Key: string // keyof typeof NVM3ObjectKey - size: number - arraySize: number - data: string[] -}[] + nvm3Key: string; // keyof typeof NVM3ObjectKey + size: number; + arraySize: number; + data: string[]; +}[]; /** * Use for a link key backup. @@ -87,57 +87,57 @@ export type TokensInfo = { * This key may be hashed and not the actual link key currently in use. */ export type LinkKeyBackupData = { - deviceEui64: EUI64 - key: EmberKeyData - outgoingFrameCounter: number - incomingFrameCounter: number -} + deviceEui64: EUI64; + key: EmberKeyData; + outgoingFrameCounter: number; + incomingFrameCounter: number; +}; export type CpcSystemCommand = { /** Identifier of the command. uint8_t */ - commandId: CpcSystemCommandId + commandId: CpcSystemCommandId; /** Command sequence number. uint8_t */ - seq: number + seq: number; /** Length of the payload in bytes. uint16_t */ - length: number + length: number; /** Command payload. uint8_t[PAYLOAD_LENGTH_MAX] */ - payload: Buffer -} + payload: Buffer; +}; export type GithubReleaseAssetJson = { - url: string - id: number - node_id: string - name: string - label: null - uploader: Record - content_type: string - state: string - size: number - download_count: number - created_at: string - updated_at: string - browser_download_url: string -} + url: string; + id: number; + node_id: string; + name: string; + label: null; + uploader: Record; + content_type: string; + state: string; + size: number; + download_count: number; + created_at: string; + updated_at: string; + browser_download_url: string; +}; export type GithubReleaseJson = { - url: string - assets_url: string - upload_url: string - html_url: string - id: number - author: Record - node_id: string - tag_name: string - target_commitish: string - name: string - draft: false - prerelease: false - created_at: string - published_at: string - assets: GithubReleaseAssetJson[] - tarball_url: string - zipball_url: string - body: string - reactions: Record -} + url: string; + assets_url: string; + upload_url: string; + html_url: string; + id: number; + author: Record; + node_id: string; + tag_name: string; + target_commitish: string; + name: string; + draft: false; + prerelease: false; + created_at: string; + published_at: string; + assets: GithubReleaseAssetJson[]; + tarball_url: string; + zipball_url: string; + body: string; + reactions: Record; +}; diff --git a/src/utils/update-firmware-links.ts b/src/utils/update-firmware-links.ts index c63b3ac..9f6dfe2 100644 --- a/src/utils/update-firmware-links.ts +++ b/src/utils/update-firmware-links.ts @@ -1,294 +1,298 @@ -import type { AdapterModel, FirmwareVariant, GithubReleaseJson } from './types.js' +import type { AdapterModel, FirmwareVariant, GithubReleaseJson } from "./types.js"; -import { writeFileSync } from 'fs' -import path from 'path' +import { writeFileSync } from "node:fs"; +import path from "node:path"; -import { fetchJson } from './utils.js' +import { fetchJson } from "./utils.js"; -const GITHUB_REPOS_API = `https://api.github.com/repos/` -const GITHUB_RELEASES_ENDPOINT = `/releases` +const GITHUB_REPOS_API = "https://api.github.com/repos/"; +const GITHUB_RELEASES_ENDPOINT = "/releases"; -const NABUCASA_REPO = `NabuCasa/silabs-firmware-builder` -const DARKXST_REPO = `darkxst/silabs-firmware-builder` -const NERIVEC_REPO = `Nerivec/silabs-firmware-builder` -const NERIVEC_RECOVERY_REPO = `Nerivec/silabs-firmware-recovery` -// const TUBE0013_REPO = `tube0013/silabs-firmware-builder` +const NABUCASA_REPO = "NabuCasa/silabs-firmware-builder"; +const DARKXST_REPO = "darkxst/silabs-firmware-builder"; +const NERIVEC_REPO = "Nerivec/silabs-firmware-builder"; +const NERIVEC_RECOVERY_REPO = "Nerivec/silabs-firmware-recovery"; +// const TUBE0013_REPO = "tube0013/silabs-firmware-builder" -// const FIRMWARE_BOOTLOADER = `bootloader` -const FIRMWARE_ZIGBEE_NCP = `zigbee_ncp` -const FIRMWARE_ZIGBEE_ROUTER = `zigbee_router` +// const FIRMWARE_BOOTLOADER = "bootloader" +const FIRMWARE_ZIGBEE_NCP = "zigbee_ncp"; +const FIRMWARE_ZIGBEE_ROUTER = "zigbee_router"; -const NABUCASA_RELEASE = await getLatestGithubRelease(NABUCASA_REPO) -const DARKXST_RELEASE = await getLatestGithubRelease(DARKXST_REPO) -const NERIVEC_RELEASE = await getLatestGithubRelease(NERIVEC_REPO) -const NERIVEC_RECOVERY_RELEASE = await getLatestGithubRelease(NERIVEC_RECOVERY_REPO) +const NABUCASA_RELEASE = await getLatestGithubRelease(NABUCASA_REPO); +const DARKXST_RELEASE = await getLatestGithubRelease(DARKXST_REPO); +const NERIVEC_RELEASE = await getLatestGithubRelease(NERIVEC_REPO); +const NERIVEC_RECOVERY_RELEASE = await getLatestGithubRelease(NERIVEC_RECOVERY_REPO); // const TUBE0013_REPO = await getLatestGithubRelease(TUBE0013_REPO) async function getLatestGithubRelease(repo: string): Promise { - const response = await fetchJson(GITHUB_REPOS_API + path.posix.join(repo, GITHUB_RELEASES_ENDPOINT)) + const response = await fetchJson(GITHUB_REPOS_API + path.posix.join(repo, GITHUB_RELEASES_ENDPOINT)); - return response[0] + return response[0]; } function findFirmware(release: GithubReleaseJson, model: string, include: string | string[]): string | undefined { - const includeArr = Array.isArray(include) ? include : [include] - const firmware = release.assets.find((asset) => asset.name.startsWith(model) && includeArr.every((i) => asset.name.includes(i))) + const includeArr = Array.isArray(include) ? include : [include]; + const firmware = release.assets.find((asset) => asset.name.startsWith(model) && includeArr.every((i) => asset.name.includes(i))); - return firmware?.browser_download_url + return firmware?.browser_download_url; } const firmwareLinks: Record> = { latest: { //-- FIRMWARE_ZIGBEE_NCP - 'Aeotec Zi-Stick (ZGA008)': findFirmware(DARKXST_RELEASE, 'zga008', FIRMWARE_ZIGBEE_NCP), + "Aeotec Zi-Stick (ZGA008)": findFirmware(DARKXST_RELEASE, "zga008", FIRMWARE_ZIGBEE_NCP), - 'EasyIOT ZB-GW04 v1.1': findFirmware(DARKXST_RELEASE, 'zb-gw04-1v1', FIRMWARE_ZIGBEE_NCP), - 'EasyIOT ZB-GW04 v1.2': findFirmware(DARKXST_RELEASE, 'zb-gw04-1v2', FIRMWARE_ZIGBEE_NCP), + "EasyIOT ZB-GW04 v1.1": findFirmware(DARKXST_RELEASE, "zb-gw04-1v1", FIRMWARE_ZIGBEE_NCP), + "EasyIOT ZB-GW04 v1.2": findFirmware(DARKXST_RELEASE, "zb-gw04-1v2", FIRMWARE_ZIGBEE_NCP), - 'Nabu Casa SkyConnect': findFirmware(NABUCASA_RELEASE, 'skyconnect', FIRMWARE_ZIGBEE_NCP), - 'Nabu Casa Yellow': findFirmware(NABUCASA_RELEASE, 'yellow', FIRMWARE_ZIGBEE_NCP), + "Nabu Casa SkyConnect": findFirmware(NABUCASA_RELEASE, "skyconnect", FIRMWARE_ZIGBEE_NCP), + "Nabu Casa Yellow": findFirmware(NABUCASA_RELEASE, "yellow", FIRMWARE_ZIGBEE_NCP), - 'SMLight SLZB06-M': findFirmware(DARKXST_RELEASE, 'slzb06m', FIRMWARE_ZIGBEE_NCP), + "SMLight SLZB06-M": findFirmware(DARKXST_RELEASE, "slzb06m", FIRMWARE_ZIGBEE_NCP), // avoid matching on mg24 variant with `_` - 'SMLight SLZB07': findFirmware(DARKXST_RELEASE, 'slzb07_', FIRMWARE_ZIGBEE_NCP), - 'SMLight SLZB07mg24': findFirmware(DARKXST_RELEASE, 'slzb07Mg24', FIRMWARE_ZIGBEE_NCP), + "SMLight SLZB07": findFirmware(DARKXST_RELEASE, "slzb07_", FIRMWARE_ZIGBEE_NCP), + "SMLight SLZB07mg24": findFirmware(DARKXST_RELEASE, "slzb07Mg24", FIRMWARE_ZIGBEE_NCP), - 'Sonoff ZBDongle-E': findFirmware(DARKXST_RELEASE, 'zbdonglee', FIRMWARE_ZIGBEE_NCP), + "Sonoff ZBDongle-E": findFirmware(DARKXST_RELEASE, "zbdonglee", FIRMWARE_ZIGBEE_NCP), - 'SparkFun MGM240p': findFirmware(DARKXST_RELEASE, 'mgm240p', FIRMWARE_ZIGBEE_NCP), + "SparkFun MGM240p": findFirmware(DARKXST_RELEASE, "mgm240p", FIRMWARE_ZIGBEE_NCP), // avoid matching on PB variant with `-` - '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`, // findFirmware(TUBE0013_RELEASE, 'mgm24-', FIRMWARE_ZIGBEE_NCP), - 'TubeZB MGM24PB': undefined, // findFirmware(TUBE0013_RELEASE, 'mgm24pb-', FIRMWARE_ZIGBEE_NCP), + "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", // findFirmware(TUBE0013_RELEASE, 'mgm24-', FIRMWARE_ZIGBEE_NCP), + "TubeZB MGM24PB": undefined, // findFirmware(TUBE0013_RELEASE, 'mgm24pb-', FIRMWARE_ZIGBEE_NCP), //-- FIRMWARE_ZIGBEE_ROUTER - 'ROUTER - Aeotec Zi-Stick (ZGA008)': undefined, // findFirmware(DARKXST_RELEASE, 'zga008', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - Aeotec Zi-Stick (ZGA008)": undefined, // findFirmware(DARKXST_RELEASE, 'zga008', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - EasyIOT ZB-GW04 v1.1': undefined, // findFirmware(DARKXST_RELEASE, 'zb-gw04-1v1', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - EasyIOT ZB-GW04 v1.2': undefined, // findFirmware(DARKXST_RELEASE, 'zb-gw04-1v2', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - EasyIOT ZB-GW04 v1.1": undefined, // findFirmware(DARKXST_RELEASE, 'zb-gw04-1v1', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - EasyIOT ZB-GW04 v1.2": undefined, // findFirmware(DARKXST_RELEASE, 'zb-gw04-1v2', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - Nabu Casa SkyConnect': undefined, // findFirmware(NABUCASA_RELEASE, 'skyconnect', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - Nabu Casa Yellow': undefined, // findFirmware(NABUCASA_RELEASE, 'yellow', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - Nabu Casa SkyConnect": undefined, // findFirmware(NABUCASA_RELEASE, 'skyconnect', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - Nabu Casa Yellow": undefined, // findFirmware(NABUCASA_RELEASE, 'yellow', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - SMLight SLZB06-M': undefined, // findFirmware(DARKXST_RELEASE, 'slzb06m', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - SMLight SLZB06-M": undefined, // findFirmware(DARKXST_RELEASE, 'slzb06m', FIRMWARE_ZIGBEE_ROUTER), // avoid matching on mg24 variant with `_` - 'ROUTER - SMLight SLZB07': undefined, // findFirmware(DARKXST_RELEASE, 'slzb07_', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - SMLight SLZB07mg24': undefined, // findFirmware(DARKXST_RELEASE, 'slzb07Mg24', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - SMLight SLZB07": undefined, // findFirmware(DARKXST_RELEASE, 'slzb07_', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - SMLight SLZB07mg24": undefined, // findFirmware(DARKXST_RELEASE, 'slzb07Mg24', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - Sonoff ZBDongle-E': undefined, // findFirmware(DARKXST_RELEASE, 'zbdonglee', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - Sonoff ZBDongle-E": undefined, // findFirmware(DARKXST_RELEASE, 'zbdonglee', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - SparkFun MGM240p': undefined, // findFirmware(DARKXST_RELEASE, 'mgm240p', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - SparkFun MGM240p": undefined, // findFirmware(DARKXST_RELEASE, 'mgm240p', FIRMWARE_ZIGBEE_ROUTER), // avoid matching on variants with `-` - 'ROUTER - TubeZB MGM24': undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24-', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - TubeZB MGM24PB': undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24PB-', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - TubeZB MGM24": undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24-', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - TubeZB MGM24PB": undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24PB-', FIRMWARE_ZIGBEE_ROUTER), }, official: { //-- FIRMWARE_ZIGBEE_NCP - 'Aeotec Zi-Stick (ZGA008)': undefined, + "Aeotec Zi-Stick (ZGA008)": undefined, - 'EasyIOT ZB-GW04 v1.1': undefined, - 'EasyIOT ZB-GW04 v1.2': undefined, + "EasyIOT ZB-GW04 v1.1": undefined, + "EasyIOT ZB-GW04 v1.2": undefined, - 'Nabu Casa SkyConnect': findFirmware(NABUCASA_RELEASE, 'skyconnect', FIRMWARE_ZIGBEE_NCP), - 'Nabu Casa Yellow': findFirmware(NABUCASA_RELEASE, 'yellow', FIRMWARE_ZIGBEE_NCP), + "Nabu Casa SkyConnect": findFirmware(NABUCASA_RELEASE, "skyconnect", FIRMWARE_ZIGBEE_NCP), + "Nabu Casa Yellow": findFirmware(NABUCASA_RELEASE, "yellow", FIRMWARE_ZIGBEE_NCP), - 'SMLight SLZB06-M': findFirmware(DARKXST_RELEASE, 'slzb06m', FIRMWARE_ZIGBEE_NCP), + "SMLight SLZB06-M": findFirmware(DARKXST_RELEASE, "slzb06m", FIRMWARE_ZIGBEE_NCP), // avoid matching on mg24 variant with `_` - 'SMLight SLZB07': findFirmware(DARKXST_RELEASE, 'slzb07_', FIRMWARE_ZIGBEE_NCP), - 'SMLight SLZB07mg24': findFirmware(DARKXST_RELEASE, 'slzb07Mg24', FIRMWARE_ZIGBEE_NCP), + "SMLight SLZB07": findFirmware(DARKXST_RELEASE, "slzb07_", FIRMWARE_ZIGBEE_NCP), + "SMLight SLZB07mg24": findFirmware(DARKXST_RELEASE, "slzb07Mg24", FIRMWARE_ZIGBEE_NCP), - 'Sonoff ZBDongle-E': `https://github.com/itead/Sonoff_Zigbee_Dongle_Firmware/raw/refs/heads/master/Dongle-E/NCP_7.4.3/ncp-uart-sw_EZNet7.4.3_V1.0.0.gbl`, + "Sonoff ZBDongle-E": + "https://github.com/itead/Sonoff_Zigbee_Dongle_Firmware/raw/refs/heads/master/Dongle-E/NCP_7.4.3/ncp-uart-sw_EZNet7.4.3_V1.0.0.gbl", - 'SparkFun MGM240p': undefined, + "SparkFun MGM240p": undefined, // avoid matching on PB variant with `-` - '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`, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24-', FIRMWARE_ZIGBEE_NCP), - 'TubeZB MGM24PB': undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24pb-', FIRMWARE_ZIGBEE_NCP), + "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", // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24-', FIRMWARE_ZIGBEE_NCP), + "TubeZB MGM24PB": undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24pb-', FIRMWARE_ZIGBEE_NCP), //-- FIRMWARE_ZIGBEE_ROUTER - 'ROUTER - Aeotec Zi-Stick (ZGA008)': undefined, + "ROUTER - Aeotec Zi-Stick (ZGA008)": undefined, - 'ROUTER - EasyIOT ZB-GW04 v1.1': undefined, - 'ROUTER - EasyIOT ZB-GW04 v1.2': undefined, + "ROUTER - EasyIOT ZB-GW04 v1.1": undefined, + "ROUTER - EasyIOT ZB-GW04 v1.2": undefined, - 'ROUTER - Nabu Casa SkyConnect': undefined, // findFirmware(NABUCASA_RELEASE, 'skyconnect', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - Nabu Casa Yellow': undefined, // findFirmware(NABUCASA_RELEASE, 'yellow', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - Nabu Casa SkyConnect": undefined, // findFirmware(NABUCASA_RELEASE, 'skyconnect', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - Nabu Casa Yellow": undefined, // findFirmware(NABUCASA_RELEASE, 'yellow', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - SMLight SLZB06-M': undefined, // findFirmware(DARKXST_RELEASE, 'slzb06m', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - SMLight SLZB06-M": undefined, // findFirmware(DARKXST_RELEASE, 'slzb06m', FIRMWARE_ZIGBEE_ROUTER), // avoid matching on mg24 variant with `_` - 'ROUTER - SMLight SLZB07': undefined, // findFirmware(DARKXST_RELEASE, 'slzb07_', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - SMLight SLZB07mg24': undefined, // findFirmware(DARKXST_RELEASE, 'slzb07Mg24', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - SMLight SLZB07": undefined, // findFirmware(DARKXST_RELEASE, 'slzb07_', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - SMLight SLZB07mg24": undefined, // findFirmware(DARKXST_RELEASE, 'slzb07Mg24', FIRMWARE_ZIGBEE_ROUTER), - '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`, + "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", - 'ROUTER - SparkFun MGM240p': undefined, + "ROUTER - SparkFun MGM240p": undefined, // avoid matching on variants with `-` - 'ROUTER - TubeZB MGM24': undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24-', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - TubeZB MGM24PB': undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24PB-', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - TubeZB MGM24": undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24-', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - TubeZB MGM24PB": undefined, // findFirmware(TUBE0013_RELEASE, 'tubeszb-mgm24PB-', FIRMWARE_ZIGBEE_ROUTER), }, experimental: { //-- FIRMWARE_ZIGBEE_NCP - 'Aeotec Zi-Stick (ZGA008)': findFirmware(NERIVEC_RELEASE, 'aeotec_zga008', FIRMWARE_ZIGBEE_NCP), + "Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RELEASE, "aeotec_zga008", FIRMWARE_ZIGBEE_NCP), - 'EasyIOT ZB-GW04 v1.1': findFirmware(NERIVEC_RELEASE, 'easyiot_zb-gw04-1v1', FIRMWARE_ZIGBEE_NCP), - 'EasyIOT ZB-GW04 v1.2': findFirmware(NERIVEC_RELEASE, 'easyiot_zb-gw04-1v2', FIRMWARE_ZIGBEE_NCP), + "EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RELEASE, "easyiot_zb-gw04-1v1", FIRMWARE_ZIGBEE_NCP), + "EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RELEASE, "easyiot_zb-gw04-1v2", FIRMWARE_ZIGBEE_NCP), - 'Nabu Casa SkyConnect': findFirmware(NERIVEC_RELEASE, 'nabucasa_skyconnect', FIRMWARE_ZIGBEE_NCP), - 'Nabu Casa Yellow': findFirmware(NERIVEC_RELEASE, 'nabucasa_yellow', FIRMWARE_ZIGBEE_NCP), + "Nabu Casa SkyConnect": findFirmware(NERIVEC_RELEASE, "nabucasa_skyconnect", FIRMWARE_ZIGBEE_NCP), + "Nabu Casa Yellow": findFirmware(NERIVEC_RELEASE, "nabucasa_yellow", FIRMWARE_ZIGBEE_NCP), - 'SMLight SLZB06-M': findFirmware(NERIVEC_RELEASE, 'smlight_slzb06m', FIRMWARE_ZIGBEE_NCP), + "SMLight SLZB06-M": findFirmware(NERIVEC_RELEASE, "smlight_slzb06m", FIRMWARE_ZIGBEE_NCP), // avoid matching on mg24 variant with `_` - 'SMLight SLZB07': findFirmware(NERIVEC_RELEASE, 'smlight_slzb07_', FIRMWARE_ZIGBEE_NCP), - 'SMLight SLZB07mg24': findFirmware(NERIVEC_RELEASE, 'smlight_slzb07Mg24', FIRMWARE_ZIGBEE_NCP), + "SMLight SLZB07": findFirmware(NERIVEC_RELEASE, "smlight_slzb07_", FIRMWARE_ZIGBEE_NCP), + "SMLight SLZB07mg24": findFirmware(NERIVEC_RELEASE, "smlight_slzb07Mg24", FIRMWARE_ZIGBEE_NCP), - 'Sonoff ZBDongle-E': findFirmware(NERIVEC_RELEASE, 'sonoff_zbdonglee', FIRMWARE_ZIGBEE_NCP), + "Sonoff ZBDongle-E": findFirmware(NERIVEC_RELEASE, "sonoff_zbdonglee", FIRMWARE_ZIGBEE_NCP), - 'SparkFun MGM240p': findFirmware(NERIVEC_RELEASE, 'sparkfun_mgm240p', FIRMWARE_ZIGBEE_NCP), + "SparkFun MGM240p": findFirmware(NERIVEC_RELEASE, "sparkfun_mgm240p", FIRMWARE_ZIGBEE_NCP), // avoid matching on variants with `-` - 'TubeZB MGM24': findFirmware(NERIVEC_RELEASE, 'tubeszb-mgm24-', FIRMWARE_ZIGBEE_NCP), - 'TubeZB MGM24PB': findFirmware(NERIVEC_RELEASE, 'tubeszb-mgm24PB-', FIRMWARE_ZIGBEE_NCP), + "TubeZB MGM24": findFirmware(NERIVEC_RELEASE, "tubeszb-mgm24-", FIRMWARE_ZIGBEE_NCP), + "TubeZB MGM24PB": findFirmware(NERIVEC_RELEASE, "tubeszb-mgm24PB-", FIRMWARE_ZIGBEE_NCP), //-- FIRMWARE_ZIGBEE_ROUTER - 'ROUTER - Aeotec Zi-Stick (ZGA008)': findFirmware(NERIVEC_RELEASE, 'aeotec_zga008', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RELEASE, "aeotec_zga008", FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - EasyIOT ZB-GW04 v1.1': findFirmware(NERIVEC_RELEASE, 'easyiot_zb-gw04-1v1', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - EasyIOT ZB-GW04 v1.2': findFirmware(NERIVEC_RELEASE, 'easyiot_zb-gw04-1v2', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RELEASE, "easyiot_zb-gw04-1v1", FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RELEASE, "easyiot_zb-gw04-1v2", FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - Nabu Casa SkyConnect': findFirmware(NERIVEC_RELEASE, 'nabucasa_skyconnect', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - Nabu Casa Yellow': findFirmware(NERIVEC_RELEASE, 'nabucasa_yellow', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - Nabu Casa SkyConnect": findFirmware(NERIVEC_RELEASE, "nabucasa_skyconnect", FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - Nabu Casa Yellow": findFirmware(NERIVEC_RELEASE, "nabucasa_yellow", FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - SMLight SLZB06-M': findFirmware(NERIVEC_RELEASE, 'smlight_slzb06m', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - SMLight SLZB06-M": findFirmware(NERIVEC_RELEASE, "smlight_slzb06m", FIRMWARE_ZIGBEE_ROUTER), // avoid matching on mg24 variant with `_` - 'ROUTER - SMLight SLZB07': findFirmware(NERIVEC_RELEASE, 'smlight_slzb07_', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - SMLight SLZB07mg24': findFirmware(NERIVEC_RELEASE, 'smlight_slzb07Mg24', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - SMLight SLZB07": findFirmware(NERIVEC_RELEASE, "smlight_slzb07_", FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - SMLight SLZB07mg24": findFirmware(NERIVEC_RELEASE, "smlight_slzb07Mg24", FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - Sonoff ZBDongle-E': findFirmware(NERIVEC_RELEASE, 'sonoff_zbdonglee', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - Sonoff ZBDongle-E": findFirmware(NERIVEC_RELEASE, "sonoff_zbdonglee", FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - SparkFun MGM240p': findFirmware(NERIVEC_RELEASE, 'sparkfun_mgm240p', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - SparkFun MGM240p": findFirmware(NERIVEC_RELEASE, "sparkfun_mgm240p", FIRMWARE_ZIGBEE_ROUTER), // avoid matching on variants with `-` - 'ROUTER - TubeZB MGM24': findFirmware(NERIVEC_RELEASE, 'tubeszb-mgm24-', FIRMWARE_ZIGBEE_ROUTER), - 'ROUTER - TubeZB MGM24PB': findFirmware(NERIVEC_RELEASE, 'tubeszb-mgm24PB-', FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - TubeZB MGM24": findFirmware(NERIVEC_RELEASE, "tubeszb-mgm24-", FIRMWARE_ZIGBEE_ROUTER), + "ROUTER - TubeZB MGM24PB": findFirmware(NERIVEC_RELEASE, "tubeszb-mgm24PB-", FIRMWARE_ZIGBEE_ROUTER), }, nvm3_32768_clear: { - 'Aeotec Zi-Stick (ZGA008)': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F1024IM32', ['nvm3_clear', '32768.gbl']), + "Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", ["nvm3_clear", "32768.gbl"]), - 'EasyIOT ZB-GW04 v1.1': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '32768.gbl']), - 'EasyIOT ZB-GW04 v1.2': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '32768.gbl']), + "EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), + "EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), - 'Nabu Casa SkyConnect': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F512IM32', ['nvm3_clear', '32768.gbl']), - 'Nabu Casa Yellow': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM210PA32JIA', ['nvm3_clear', '32768.gbl']), + "Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", ["nvm3_clear", "32768.gbl"]), + "Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", ["nvm3_clear", "32768.gbl"]), - 'SMLight SLZB06-M': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '32768.gbl']), - 'SMLight SLZB07': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '32768.gbl']), - 'SMLight SLZB07mg24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG24A020F1024IM40', ['nvm3_clear', '32768.gbl']), + "SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), + "SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), + "SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "32768.gbl"]), - 'Sonoff ZBDongle-E': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '32768.gbl']), + "Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), - 'SparkFun MGM240p': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNA', ['nvm3_clear', '32768.gbl']), + "SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", ["nvm3_clear", "32768.gbl"]), - 'TubeZB MGM24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PA32VNN', ['nvm3_clear', '32768.gbl']), - 'TubeZB MGM24PB': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNN', ['nvm3_clear', '32768.gbl']), + "TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", ["nvm3_clear", "32768.gbl"]), + "TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", ["nvm3_clear", "32768.gbl"]), - 'ROUTER - Aeotec Zi-Stick (ZGA008)': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F1024IM32', ['nvm3_clear', '32768.gbl']), + "ROUTER - Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", ["nvm3_clear", "32768.gbl"]), - 'ROUTER - EasyIOT ZB-GW04 v1.1': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '32768.gbl']), - 'ROUTER - EasyIOT ZB-GW04 v1.2': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '32768.gbl']), + "ROUTER - EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), + "ROUTER - EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), - 'ROUTER - Nabu Casa SkyConnect': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F512IM32', ['nvm3_clear', '32768.gbl']), - 'ROUTER - Nabu Casa Yellow': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM210PA32JIA', ['nvm3_clear', '32768.gbl']), + "ROUTER - Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", ["nvm3_clear", "32768.gbl"]), + "ROUTER - Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", ["nvm3_clear", "32768.gbl"]), - 'ROUTER - SMLight SLZB06-M': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '32768.gbl']), - 'ROUTER - SMLight SLZB07': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '32768.gbl']), - 'ROUTER - SMLight SLZB07mg24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG24A020F1024IM40', ['nvm3_clear', '32768.gbl']), + "ROUTER - SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), + "ROUTER - SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), + "ROUTER - SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "32768.gbl"]), - 'ROUTER - Sonoff ZBDongle-E': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '32768.gbl']), + "ROUTER - Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "32768.gbl"]), - 'ROUTER - SparkFun MGM240p': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNA', ['nvm3_clear', '32768.gbl']), + "ROUTER - SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", ["nvm3_clear", "32768.gbl"]), - 'ROUTER - TubeZB MGM24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PA32VNN', ['nvm3_clear', '32768.gbl']), - 'ROUTER - TubeZB MGM24PB': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNN', ['nvm3_clear', '32768.gbl']), + "ROUTER - TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", ["nvm3_clear", "32768.gbl"]), + "ROUTER - TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", ["nvm3_clear", "32768.gbl"]), }, nvm3_40960_clear: { - 'Aeotec Zi-Stick (ZGA008)': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F1024IM32', ['nvm3_clear', '40960.gbl']), + "Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", ["nvm3_clear", "40960.gbl"]), - 'EasyIOT ZB-GW04 v1.1': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '40960.gbl']), - 'EasyIOT ZB-GW04 v1.2': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '40960.gbl']), + "EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), + "EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), - 'Nabu Casa SkyConnect': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F512IM32', ['nvm3_clear', '40960.gbl']), - 'Nabu Casa Yellow': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM210PA32JIA', ['nvm3_clear', '40960.gbl']), + "Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", ["nvm3_clear", "40960.gbl"]), + "Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", ["nvm3_clear", "40960.gbl"]), - 'SMLight SLZB06-M': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '40960.gbl']), - 'SMLight SLZB07': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '40960.gbl']), - 'SMLight SLZB07mg24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG24A020F1024IM40', ['nvm3_clear', '40960.gbl']), + "SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), + "SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), + "SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "40960.gbl"]), - 'Sonoff ZBDongle-E': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '40960.gbl']), + "Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), - 'SparkFun MGM240p': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNA', ['nvm3_clear', '40960.gbl']), + "SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", ["nvm3_clear", "40960.gbl"]), - 'TubeZB MGM24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PA32VNN', ['nvm3_clear', '40960.gbl']), - 'TubeZB MGM24PB': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNN', ['nvm3_clear', '40960.gbl']), + "TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", ["nvm3_clear", "40960.gbl"]), + "TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", ["nvm3_clear", "40960.gbl"]), - 'ROUTER - Aeotec Zi-Stick (ZGA008)': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F1024IM32', ['nvm3_clear', '40960.gbl']), + "ROUTER - Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", ["nvm3_clear", "40960.gbl"]), - 'ROUTER - EasyIOT ZB-GW04 v1.1': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '40960.gbl']), - 'ROUTER - EasyIOT ZB-GW04 v1.2': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '40960.gbl']), + "ROUTER - EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), + "ROUTER - EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), - 'ROUTER - Nabu Casa SkyConnect': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F512IM32', ['nvm3_clear', '40960.gbl']), - 'ROUTER - Nabu Casa Yellow': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM210PA32JIA', ['nvm3_clear', '40960.gbl']), + "ROUTER - Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", ["nvm3_clear", "40960.gbl"]), + "ROUTER - Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", ["nvm3_clear", "40960.gbl"]), - 'ROUTER - SMLight SLZB06-M': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '40960.gbl']), - 'ROUTER - SMLight SLZB07': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '40960.gbl']), - 'ROUTER - SMLight SLZB07mg24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG24A020F1024IM40', ['nvm3_clear', '40960.gbl']), + "ROUTER - SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), + "ROUTER - SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), + "ROUTER - SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", ["nvm3_clear", "40960.gbl"]), - 'ROUTER - Sonoff ZBDongle-E': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', ['nvm3_clear', '40960.gbl']), + "ROUTER - Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", ["nvm3_clear", "40960.gbl"]), - 'ROUTER - SparkFun MGM240p': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNA', ['nvm3_clear', '40960.gbl']), + "ROUTER - SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", ["nvm3_clear", "40960.gbl"]), - 'ROUTER - TubeZB MGM24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PA32VNN', ['nvm3_clear', '40960.gbl']), - 'ROUTER - TubeZB MGM24PB': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNN', ['nvm3_clear', '40960.gbl']), + "ROUTER - TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", ["nvm3_clear", "40960.gbl"]), + "ROUTER - TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", ["nvm3_clear", "40960.gbl"]), }, app_clear: { - 'Aeotec Zi-Stick (ZGA008)': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F1024IM32', 'app_clear'), + "Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", "app_clear"), - 'EasyIOT ZB-GW04 v1.1': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', 'app_clear'), - 'EasyIOT ZB-GW04 v1.2': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', 'app_clear'), + "EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), + "EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), - 'Nabu Casa SkyConnect': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F512IM32', 'app_clear'), - 'Nabu Casa Yellow': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM210PA32JIA', 'app_clear'), + "Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", "app_clear"), + "Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", "app_clear"), - 'SMLight SLZB06-M': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', 'app_clear'), - 'SMLight SLZB07': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', 'app_clear'), - 'SMLight SLZB07mg24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG24A020F1024IM40', 'app_clear'), + "SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), + "SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), + "SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", "app_clear"), - 'Sonoff ZBDongle-E': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', 'app_clear'), + "Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), - 'SparkFun MGM240p': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNA', 'app_clear'), + "SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", "app_clear"), - 'TubeZB MGM24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PA32VNN', 'app_clear'), - 'TubeZB MGM24PB': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNN', 'app_clear'), + "TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", "app_clear"), + "TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", "app_clear"), - 'ROUTER - Aeotec Zi-Stick (ZGA008)': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F1024IM32', 'app_clear'), + "ROUTER - Aeotec Zi-Stick (ZGA008)": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F1024IM32", "app_clear"), - 'ROUTER - EasyIOT ZB-GW04 v1.1': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', 'app_clear'), - 'ROUTER - EasyIOT ZB-GW04 v1.2': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', 'app_clear'), + "ROUTER - EasyIOT ZB-GW04 v1.1": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), + "ROUTER - EasyIOT ZB-GW04 v1.2": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), - 'ROUTER - Nabu Casa SkyConnect': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F512IM32', 'app_clear'), - 'ROUTER - Nabu Casa Yellow': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM210PA32JIA', 'app_clear'), + "ROUTER - Nabu Casa SkyConnect": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F512IM32", "app_clear"), + "ROUTER - Nabu Casa Yellow": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM210PA32JIA", "app_clear"), - 'ROUTER - SMLight SLZB06-M': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', 'app_clear'), - 'ROUTER - SMLight SLZB07': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', 'app_clear'), - 'ROUTER - SMLight SLZB07mg24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG24A020F1024IM40', 'app_clear'), + "ROUTER - SMLight SLZB06-M": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), + "ROUTER - SMLight SLZB07": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), + "ROUTER - SMLight SLZB07mg24": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG24A020F1024IM40", "app_clear"), - 'ROUTER - Sonoff ZBDongle-E': findFirmware(NERIVEC_RECOVERY_RELEASE, 'EFR32MG21A020F768IM32', 'app_clear'), + "ROUTER - Sonoff ZBDongle-E": findFirmware(NERIVEC_RECOVERY_RELEASE, "EFR32MG21A020F768IM32", "app_clear"), - 'ROUTER - SparkFun MGM240p': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNA', 'app_clear'), + "ROUTER - SparkFun MGM240p": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNA", "app_clear"), - 'ROUTER - TubeZB MGM24': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PA32VNN', 'app_clear'), - 'ROUTER - TubeZB MGM24PB': findFirmware(NERIVEC_RECOVERY_RELEASE, 'MGM240PB32VNN', 'app_clear'), + "ROUTER - TubeZB MGM24": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PA32VNN", "app_clear"), + "ROUTER - TubeZB MGM24PB": findFirmware(NERIVEC_RECOVERY_RELEASE, "MGM240PB32VNN", "app_clear"), }, -} +}; -writeFileSync('firmware-links.json', JSON.stringify(firmwareLinks, undefined, 4), 'utf8') +writeFileSync("firmware-links.json", JSON.stringify(firmwareLinks, undefined, 4), "utf8"); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 5aa7f09..ce78949 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,224 +1,224 @@ -import type { SelectChoices } from './types.js' +import type { SelectChoices } from "./types.js"; -import { existsSync, readdirSync, readFileSync, renameSync, statSync } from 'node:fs' -import { dirname, extname, join } from 'node:path' +import { existsSync, readFileSync, readdirSync, renameSync, statSync } from "node:fs"; +import { dirname, extname, join } from "node:path"; -import { input, select } from '@inquirer/prompts' +import { input, select } from "@inquirer/prompts"; -import { DEFAULT_STACK_CONFIG } from 'zigbee-herdsman/dist/adapter/ember/adapter/emberAdapter.js' -import { IEEE802154CcaMode } from 'zigbee-herdsman/dist/adapter/ember/enums.js' -import { halCommonCrc16, highByte, lowByte } from 'zigbee-herdsman/dist/adapter/ember/utils/math.js' -import { UnifiedBackupStorage } from 'zigbee-herdsman/dist/models/backup-storage-unified.js' -import { Backup } from 'zigbee-herdsman/dist/models/backup.js' -import { fromUnifiedBackup } from 'zigbee-herdsman/dist/utils/backup.js' +import { DEFAULT_STACK_CONFIG } from "zigbee-herdsman/dist/adapter/ember/adapter/emberAdapter.js"; +import { IEEE802154CcaMode } from "zigbee-herdsman/dist/adapter/ember/enums.js"; +import { halCommonCrc16, highByte, lowByte } from "zigbee-herdsman/dist/adapter/ember/utils/math.js"; +import type { UnifiedBackupStorage } from "zigbee-herdsman/dist/models/backup-storage-unified.js"; +import type { Backup } from "zigbee-herdsman/dist/models/backup.js"; +import { fromUnifiedBackup } from "zigbee-herdsman/dist/utils/backup.js"; -import { CONF_STACK, DATA_FOLDER, logger } from '../index.js' +import { CONF_STACK, DATA_FOLDER, logger } from "../index.js"; // @from zigbee2mqtt-frontend export const toHex = (input: number, padding = 4): string => { - const padStr = '0'.repeat(padding) - return '0x' + (padStr + input.toString(16)).slice(-1 * padding).toUpperCase() -} + const padStr = "0".repeat(padding); + return `0x${(padStr + input.toString(16)).slice(-1 * padding).toUpperCase()}`; +}; export const loadStackConfig = (): typeof DEFAULT_STACK_CONFIG => { try { - const customConfig = JSON.parse(readFileSync(CONF_STACK, 'utf8')) + const customConfig = JSON.parse(readFileSync(CONF_STACK, "utf8")); // set any undefined config to default - const config = { ...DEFAULT_STACK_CONFIG, ...customConfig } + const config = { ...DEFAULT_STACK_CONFIG, ...customConfig }; - const inRange = (value: number, min: number, max: number): boolean => !(value == undefined || value < min || value > max) + const inRange = (value: number, min: number, max: number): boolean => !(value == null || value < min || value > max); - if (!['high', 'low'].includes(config.CONCENTRATOR_RAM_TYPE)) { - config.CONCENTRATOR_RAM_TYPE = DEFAULT_STACK_CONFIG.CONCENTRATOR_RAM_TYPE - logger.error(`[CONF STACK] Invalid CONCENTRATOR_RAM_TYPE, using default.`) + if (!["high", "low"].includes(config.CONCENTRATOR_RAM_TYPE)) { + config.CONCENTRATOR_RAM_TYPE = DEFAULT_STACK_CONFIG.CONCENTRATOR_RAM_TYPE; + logger.error("[CONF STACK] Invalid CONCENTRATOR_RAM_TYPE, using default."); } if (!inRange(config.CONCENTRATOR_MIN_TIME, 1, 60) || config.CONCENTRATOR_MIN_TIME >= config.CONCENTRATOR_MAX_TIME) { - config.CONCENTRATOR_MIN_TIME = DEFAULT_STACK_CONFIG.CONCENTRATOR_MIN_TIME - logger.error(`[CONF STACK] Invalid CONCENTRATOR_MIN_TIME, using default.`) + config.CONCENTRATOR_MIN_TIME = DEFAULT_STACK_CONFIG.CONCENTRATOR_MIN_TIME; + logger.error("[CONF STACK] Invalid CONCENTRATOR_MIN_TIME, using default."); } if (!inRange(config.CONCENTRATOR_MAX_TIME, 30, 300) || config.CONCENTRATOR_MAX_TIME <= config.CONCENTRATOR_MIN_TIME) { - config.CONCENTRATOR_MAX_TIME = DEFAULT_STACK_CONFIG.CONCENTRATOR_MAX_TIME - logger.error(`[CONF STACK] Invalid CONCENTRATOR_MAX_TIME, using default.`) + config.CONCENTRATOR_MAX_TIME = DEFAULT_STACK_CONFIG.CONCENTRATOR_MAX_TIME; + logger.error("[CONF STACK] Invalid CONCENTRATOR_MAX_TIME, using default."); } if (!inRange(config.CONCENTRATOR_ROUTE_ERROR_THRESHOLD, 1, 100)) { - config.CONCENTRATOR_ROUTE_ERROR_THRESHOLD = DEFAULT_STACK_CONFIG.CONCENTRATOR_ROUTE_ERROR_THRESHOLD - logger.error(`[CONF STACK] Invalid CONCENTRATOR_ROUTE_ERROR_THRESHOLD, using default.`) + config.CONCENTRATOR_ROUTE_ERROR_THRESHOLD = DEFAULT_STACK_CONFIG.CONCENTRATOR_ROUTE_ERROR_THRESHOLD; + logger.error("[CONF STACK] Invalid CONCENTRATOR_ROUTE_ERROR_THRESHOLD, using default."); } if (!inRange(config.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, 1, 100)) { - config.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD = DEFAULT_STACK_CONFIG.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD - logger.error(`[CONF STACK] Invalid CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, using default.`) + config.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD = DEFAULT_STACK_CONFIG.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD; + logger.error("[CONF STACK] Invalid CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, using default."); } if (!inRange(config.CONCENTRATOR_MAX_HOPS, 0, 30)) { - config.CONCENTRATOR_MAX_HOPS = DEFAULT_STACK_CONFIG.CONCENTRATOR_MAX_HOPS - logger.error(`[CONF STACK] Invalid CONCENTRATOR_MAX_HOPS, using default.`) + config.CONCENTRATOR_MAX_HOPS = DEFAULT_STACK_CONFIG.CONCENTRATOR_MAX_HOPS; + logger.error("[CONF STACK] Invalid CONCENTRATOR_MAX_HOPS, using default."); } if (!inRange(config.MAX_END_DEVICE_CHILDREN, 6, 64)) { - config.MAX_END_DEVICE_CHILDREN = DEFAULT_STACK_CONFIG.MAX_END_DEVICE_CHILDREN - logger.error(`[CONF STACK] Invalid MAX_END_DEVICE_CHILDREN, using default.`) + config.MAX_END_DEVICE_CHILDREN = DEFAULT_STACK_CONFIG.MAX_END_DEVICE_CHILDREN; + logger.error("[CONF STACK] Invalid MAX_END_DEVICE_CHILDREN, using default."); } if (!inRange(config.TRANSIENT_DEVICE_TIMEOUT, 0, 65535)) { - config.TRANSIENT_DEVICE_TIMEOUT = DEFAULT_STACK_CONFIG.TRANSIENT_DEVICE_TIMEOUT - logger.error(`[CONF STACK] Invalid TRANSIENT_DEVICE_TIMEOUT, using default.`) + config.TRANSIENT_DEVICE_TIMEOUT = DEFAULT_STACK_CONFIG.TRANSIENT_DEVICE_TIMEOUT; + logger.error("[CONF STACK] Invalid TRANSIENT_DEVICE_TIMEOUT, using default."); } if (!inRange(config.END_DEVICE_POLL_TIMEOUT, 0, 14)) { - config.END_DEVICE_POLL_TIMEOUT = DEFAULT_STACK_CONFIG.END_DEVICE_POLL_TIMEOUT - logger.error(`[CONF STACK] Invalid END_DEVICE_POLL_TIMEOUT, using default.`) + config.END_DEVICE_POLL_TIMEOUT = DEFAULT_STACK_CONFIG.END_DEVICE_POLL_TIMEOUT; + logger.error("[CONF STACK] Invalid END_DEVICE_POLL_TIMEOUT, using default."); } if (!inRange(config.TRANSIENT_KEY_TIMEOUT_S, 0, 65535)) { - config.TRANSIENT_KEY_TIMEOUT_S = DEFAULT_STACK_CONFIG.TRANSIENT_KEY_TIMEOUT_S - logger.error(`[CONF STACK] Invalid TRANSIENT_KEY_TIMEOUT_S, using default.`) + config.TRANSIENT_KEY_TIMEOUT_S = DEFAULT_STACK_CONFIG.TRANSIENT_KEY_TIMEOUT_S; + logger.error("[CONF STACK] Invalid TRANSIENT_KEY_TIMEOUT_S, using default."); } - config.CCA_MODE = config.CCA_MODE ?? undefined // always default to undefined + config.CCA_MODE = config.CCA_MODE ?? undefined; // always default to undefined if (config.CCA_MODE && IEEE802154CcaMode[config.CCA_MODE] === undefined) { - config.CCA_MODE = undefined - logger.error(`[STACK CONFIG] Invalid CCA_MODE, ignoring.`) + config.CCA_MODE = undefined; + logger.error("[STACK CONFIG] Invalid CCA_MODE, ignoring."); } - logger.info(`Using stack config ${JSON.stringify(config)}.`) + logger.info(`Using stack config ${JSON.stringify(config)}.`); - return config + return config; } catch { /* empty */ } - logger.info(`Using default stack config.`) + logger.info("Using default stack config."); - return DEFAULT_STACK_CONFIG -} + return DEFAULT_STACK_CONFIG; +}; -export const browseToFile = async (message: string, defaultValue: string, toWrite: boolean = false): Promise => { +export const browseToFile = async (message: string, defaultValue: string, toWrite = false): Promise => { const pathOpt = await select({ choices: [ { name: `Use default (${defaultValue})`, value: 0 }, - { name: `Enter path manually`, value: 1 }, + { name: "Enter path manually", value: 1 }, { name: `Select in data folder (${DATA_FOLDER})`, value: 2 }, ], message, - }) - let filepath: string = defaultValue + }); + let filepath: string = defaultValue; switch (pathOpt) { case 1: { filepath = await input({ - message: 'Enter path to file', + message: "Enter path to file", validate(value) { - return existsSync(dirname(value)) && extname(value) === extname(defaultValue) + return existsSync(dirname(value)) && extname(value) === extname(defaultValue); }, - }) + }); - break + break; } case 2: { - const files = readdirSync(DATA_FOLDER) - const fileChoices: SelectChoices = [{ name: `Go back`, value: '-1' }] + const files = readdirSync(DATA_FOLDER); + const fileChoices: SelectChoices = [{ name: "Go back", value: "-1" }]; for (const file of files) { if (extname(file) === extname(defaultValue)) { - const { size, mtime, birthtime } = statSync(join(DATA_FOLDER, file)) + const { size, mtime, birthtime } = statSync(join(DATA_FOLDER, file)); fileChoices.push({ name: file, value: file, description: `Size: ${size} bytes | Created: ${birthtime.toISOString()} | Last Modified: ${mtime.toISOString()}`, - }) + }); } } - let chosenFile = '-1' + let chosenFile = "-1"; if (fileChoices.length === 1) { - logger.error(`Found no file in '${DATA_FOLDER}'.`) + logger.error(`Found no file in '${DATA_FOLDER}'.`); } else { - chosenFile = await select({ choices: fileChoices, message }) + chosenFile = await select({ choices: fileChoices, message }); } - filepath = chosenFile === '-1' ? await browseToFile(message, defaultValue, toWrite) : join(DATA_FOLDER, chosenFile) + filepath = chosenFile === "-1" ? await browseToFile(message, defaultValue, toWrite) : join(DATA_FOLDER, chosenFile); - break + break; } } if (toWrite && existsSync(filepath)) { const rename = await select({ choices: [ - { name: `Overwrite`, value: 0 }, - { name: `Rename`, value: 1 }, + { name: "Overwrite", value: 0 }, + { name: "Rename", value: 1 }, ], - message: 'File already exists', - }) + message: "File already exists", + }); if (rename === 1) { - const renamed = `${filepath}-${Date.now()}.old` + const renamed = `${filepath}-${Date.now()}.old`; - logger.info(`Renaming existing file to '${renamed}'.`) - renameSync(filepath, renamed) + logger.info(`Renaming existing file to '${renamed}'.`); + renameSync(filepath, renamed); } } - return filepath -} + return filepath; +}; export const getBackupFromFile = (backupFile: string): Backup | undefined => { try { - const data: UnifiedBackupStorage = JSON.parse(readFileSync(backupFile, 'utf8')) + const data: UnifiedBackupStorage = JSON.parse(readFileSync(backupFile, "utf8")); - if (data.metadata.format === 'zigpy/open-coordinator-backup') { + if (data.metadata.format === "zigpy/open-coordinator-backup") { if (data.metadata.version !== 1) { - logger.error(`Unsupported open coordinator backup version (version=${data.metadata.version}).`) - return undefined + logger.error(`Unsupported open coordinator backup version (version=${data.metadata.version}).`); + return undefined; } - return fromUnifiedBackup(data) + return fromUnifiedBackup(data); } - logger.error(`Unknown backup format.`) + logger.error("Unknown backup format."); } catch (error) { - logger.error(`Not valid backup found. ${error}`) + logger.error(`Not valid backup found. ${error}`); } - return undefined -} + return undefined; +}; -export const computeCRC16 = (data: Buffer, init: number = 0): Buffer => { - let crc = init +export const computeCRC16 = (data: Buffer, init = 0): Buffer => { + let crc = init; for (const byte of data) { - crc = halCommonCrc16(byte, crc) + crc = halCommonCrc16(byte, crc); } - return Buffer.from([highByte(crc), lowByte(crc)]) -} + return Buffer.from([highByte(crc), lowByte(crc)]); +}; -export const computeCRC16CITTKermit = (data: Buffer, init: number = 0): Buffer => { - let crc = init +export const computeCRC16CITTKermit = (data: Buffer, init = 0): Buffer => { + let crc = init; for (const byte of data) { - let t = crc ^ byte - t = (t ^ (t << 4)) & 0xff - crc = (crc >> 8) ^ (t << 8) ^ (t >> 4) ^ (t << 3) + let t = crc ^ byte; + t = (t ^ (t << 4)) & 0xff; + crc = (crc >> 8) ^ (t << 8) ^ (t >> 4) ^ (t << 3); } - return Buffer.from([lowByte(crc), highByte(crc)]) -} + return Buffer.from([lowByte(crc), highByte(crc)]); +}; export async function fetchJson(pageUrl: string): Promise { - const response = await fetch(pageUrl) + const response = await fetch(pageUrl); if (!response.ok || !response.body) { - throw new Error(`Invalid response from ${pageUrl} status=${response.status}.`) + throw new Error(`Invalid response from ${pageUrl} status=${response.status}.`); } - return (await response.json()) as T + return (await response.json()) as T; } diff --git a/src/utils/wireshark.ts b/src/utils/wireshark.ts index ff8ef94..5a29280 100644 --- a/src/utils/wireshark.ts +++ b/src/utils/wireshark.ts @@ -1,4 +1,4 @@ -import { EZSP_MAX_FRAME_LENGTH } from 'zigbee-herdsman/dist/adapter/ember/ezsp/consts.js' +import { EZSP_MAX_FRAME_LENGTH } from "zigbee-herdsman/dist/adapter/ember/ezsp/consts.js"; /** * @see https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-zep.c @@ -24,28 +24,28 @@ import { EZSP_MAX_FRAME_LENGTH } from 'zigbee-herdsman/dist/adapter/ember/ezsp/c * |2 bytes |1 byte |1 byte| 4 bytes | *------------------------------------------------------------ */ -const ZEP_PREAMBLE = 'EX' -const ZEP_PROTOCOL_VERSION = 2 -const ZEP_PROTOCOL_TYPE = 1 +const ZEP_PREAMBLE = "EX"; +const ZEP_PROTOCOL_VERSION = 2; +const ZEP_PROTOCOL_TYPE = 1; /** Baseline NTP time if bit-0=0 -> 7-Feb-2036 @ 06:28:16 UTC */ -const NTP_MSB_0_BASE_TIME = 2085978496000n +const NTP_MSB_0_BASE_TIME = 2085978496000n; /** Baseline NTP time if bit-0=1 -> 1-Jan-1900 @ 01:00:00 UTC */ -const NTP_MSB_1_BASE_TIME = -2208988800000n +const NTP_MSB_1_BASE_TIME = -2208988800000n; const getZepTimestamp = (): bigint => { - const now = BigInt(Date.now()) - const useBase1 = now < NTP_MSB_0_BASE_TIME // time < Feb-2036 + const now = BigInt(Date.now()); + const useBase1 = now < NTP_MSB_0_BASE_TIME; // time < Feb-2036 // MSB_1_BASE_TIME: dates <= Feb-2036, MSB_0_BASE_TIME: if base0 needed for dates >= Feb-2036 - const baseTime = now - (useBase1 ? NTP_MSB_1_BASE_TIME : NTP_MSB_0_BASE_TIME) - let seconds = baseTime / 1000n - const fraction = ((baseTime % 1000n) * 0x100000000n) / 1000n + const baseTime = now - (useBase1 ? NTP_MSB_1_BASE_TIME : NTP_MSB_0_BASE_TIME); + let seconds = baseTime / 1000n; + const fraction = ((baseTime % 1000n) * 0x100000000n) / 1000n; if (useBase1) { - seconds |= 0x80000000n // set high-order bit if MSB_1_BASE_TIME 1900 used + seconds |= 0x80000000n; // set high-order bit if MSB_1_BASE_TIME 1900 used } - return BigInt.asIntN(64, (seconds << 32n) | fraction) -} + return BigInt.asIntN(64, (seconds << 32n) | fraction); +}; export const createWiresharkZEPFrame = ( channelId: number, @@ -54,10 +54,10 @@ export const createWiresharkZEPFrame = ( rssi: number, sequence: number, data: Buffer, - lqiMode: boolean = false, + lqiMode = false, ): Buffer => { - const buffer = Buffer.alloc(167) - let offset = 0 + const buffer = Buffer.alloc(167); + let offset = 0; // The IEEE 802.15.4 packet encapsulated in the ZEP frame must have the "TI CC24xx" format // See figure 21 on page 24 of the CC2420 datasheet: https://www.ti.com/lit/ds/symlink/cc2420.pdf @@ -66,63 +66,63 @@ export const createWiresharkZEPFrame = ( // * Second byte: // - the most significant bit is set to 1 if the CRC of the frame is correct // - the 7 least significant bits contain the LQI value as a unsigned 7 bits integer (range 0 to 127) - data[data.length - 2] = rssi - data[data.length - 1] = 0x80 | ((lqi >> 1) & 0x7f) + data[data.length - 2] = rssi; + data[data.length - 1] = 0x80 | ((lqi >> 1) & 0x7f); // Protocol ID String | Character string | 2.0.3 to 4.2.5 - buffer.write(ZEP_PREAMBLE, offset) - offset += 2 + buffer.write(ZEP_PREAMBLE, offset); + offset += 2; // Protocol Version | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 - buffer.writeUInt8(ZEP_PROTOCOL_VERSION, offset++) + buffer.writeUInt8(ZEP_PROTOCOL_VERSION, offset++); // Type | Unsigned integer (8 bits) | 1.2.0 to 1.8.15, 1.12.0 to 4.2.5 - buffer.writeUInt8(ZEP_PROTOCOL_TYPE, offset++) + buffer.writeUInt8(ZEP_PROTOCOL_TYPE, offset++); // Channel ID | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 - buffer.writeUInt8(channelId, offset++) + buffer.writeUInt8(channelId, offset++); // Device ID | Unsigned integer (16 bits) | 1.2.0 to 4.2.5 - buffer.writeUint16BE(deviceId, offset) - offset += 2 + buffer.writeUint16BE(deviceId, offset); + offset += 2; // LQI/CRC Mode | Boolean | 1.2.0 to 4.2.5 - buffer.writeUInt8(lqiMode ? 1 : 0, offset++) + buffer.writeUInt8(lqiMode ? 1 : 0, offset++); // Link Quality Indication | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 - buffer.writeUInt8(lqi, offset++) + buffer.writeUInt8(lqi, offset++); // Timestamp | Date and time | 1.2.0 to 4.2.5 - buffer.writeBigInt64BE(getZepTimestamp(), offset) - offset += 8 + buffer.writeBigInt64BE(getZepTimestamp(), offset); + offset += 8; // Sequence Number | Unsigned integer (32 bits) | 1.2.0 to 4.2.5 - buffer.writeUint32BE(sequence, offset) - offset += 4 + buffer.writeUint32BE(sequence, offset); + offset += 4; // Reserved Fields | Byte sequence | 2.0.0 to 4.2.5 - offset += 10 + offset += 10; // Length | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 - buffer.writeUInt8(data.length, offset++) + buffer.writeUInt8(data.length, offset++); - buffer.set(data, offset) - offset += data.length + buffer.set(data, offset); + offset += data.length; - return buffer.subarray(0, offset) // increased to "beyond last" above -} + return buffer.subarray(0, offset); // increased to "beyond last" above +}; /** * @see https://datatracker.ietf.org/doc/id/draft-gharris-opsawg-pcap-00.html */ /** seconds + microseconds */ -export const PCAP_MAGIC_NUMBER_MS = 0xa1b2c3d4 +export const PCAP_MAGIC_NUMBER_MS = 0xa1b2c3d4; /** seconds + nanoseconds */ -export const PCAP_MAGIC_NUMBER_NS = 0xa1b23c4d -const PCAP_VERSION_MAJOR = 2 -const PCAP_VERSION_MINOR = 4 +export const PCAP_MAGIC_NUMBER_NS = 0xa1b23c4d; +const PCAP_VERSION_MAJOR = 2; +const PCAP_VERSION_MINOR = 4; /** IEEE 802.15.4 Low-Rate Wireless Networks, with each packet having the FCS at the end of the frame. */ -const PCAP_LINKTYPE_IEEE802_15_4_WITH_FCS = 195 +const PCAP_LINKTYPE_IEEE802_15_4_WITH_FCS = 195; export const createPcapFileHeader = (magicNumber: number = PCAP_MAGIC_NUMBER_MS): Buffer => { - const fileHeader = Buffer.alloc(24) + const fileHeader = Buffer.alloc(24); /** * An unsigned magic number, whose value is either the hexadecimal number 0xA1B2C3D4 or the hexadecimal number 0xA1B23C4D. @@ -132,7 +132,7 @@ export const createPcapFileHeader = (magicNumber: number = PCAP_MAGIC_NUMBER_MS) * and to heuristically identify pcap files. * 32 bits * */ - fileHeader.writeUInt32LE(magicNumber, 0) + fileHeader.writeUInt32LE(magicNumber, 0); /** * An unsigned value, giving the number of the current major version of the format. * The value for the current version of the format is 2. @@ -141,7 +141,7 @@ export const createPcapFileHeader = (magicNumber: number = PCAP_MAGIC_NUMBER_MS) * and code that reads the old format could not read the new format. * 16 bits */ - fileHeader.writeUInt16LE(PCAP_VERSION_MAJOR, 4) + fileHeader.writeUInt16LE(PCAP_VERSION_MAJOR, 4); /** * An unsigned value, giving the number of the current minor version of the format. * The value is for the current version of the format is 4. @@ -149,21 +149,21 @@ export const createPcapFileHeader = (magicNumber: number = PCAP_MAGIC_NUMBER_MS) * without checking the version number but code that reads the old format could not read all files in the new format. * 16 bits */ - fileHeader.writeUInt16LE(PCAP_VERSION_MINOR, 6) + fileHeader.writeUInt16LE(PCAP_VERSION_MINOR, 6); /** * Not used - SHOULD be filled with 0 by pcap file writers, and MUST be ignored by pcap file readers. * This value was documented by some older implementations as "gmt to local correction". * Some older pcap file writers stored non-zero values in this field. * 32 bits */ - fileHeader.writeUInt32LE(0, 8) + fileHeader.writeUInt32LE(0, 8); /** * Not used - SHOULD be filled with 0 by pcap file writers, and MUST be ignored by pcap file readers. * This value was documented by some older implementations as "accuracy of timestamps". * Some older pcap file writers stored non-zero values in this field. * 32 bits */ - fileHeader.writeUInt32LE(0, 12) + fileHeader.writeUInt32LE(0, 12); /** * An unsigned value indicating the maximum number of octets captured from each packet. * The portion of each packet that exceeds this value will not be stored in the file. @@ -171,37 +171,37 @@ export const createPcapFileHeader = (magicNumber: number = PCAP_MAGIC_NUMBER_MS) * to the largest packet length in the file. * 32 bits */ - fileHeader.writeUInt32LE(EZSP_MAX_FRAME_LENGTH, 16) + fileHeader.writeUInt32LE(EZSP_MAX_FRAME_LENGTH, 16); /** * An unsigned value that defines, in the lower 28 bits, the link layer type of packets in the file. * 32 bits */ - fileHeader.writeUInt32LE(PCAP_LINKTYPE_IEEE802_15_4_WITH_FCS, 20) + fileHeader.writeUInt32LE(PCAP_LINKTYPE_IEEE802_15_4_WITH_FCS, 20); - return fileHeader -} + return fileHeader; +}; export const createPcapPacketRecordMs = (packetData: Buffer): Buffer => { - const packetHeader = Buffer.alloc(16) - const timestamp = (new Date().getTime() * 1000) / 1000000 - const timestampSec = Math.trunc(timestamp) + const packetHeader = Buffer.alloc(16); + const timestamp = (new Date().getTime() * 1000) / 1000000; + const timestampSec = Math.trunc(timestamp); /** 32-bit unsigned integer that represents the number of seconds that have elapsed since 1970-01-01 00:00:00 UTC */ - packetHeader.writeUInt32LE(timestampSec, 0) + packetHeader.writeUInt32LE(timestampSec, 0); /** Number of microseconds or nanoseconds that have elapsed since that seconds. */ - packetHeader.writeUInt32LE(Math.trunc((timestamp - timestampSec) * 1000000.0), 4) + packetHeader.writeUInt32LE(Math.trunc((timestamp - timestampSec) * 1000000.0), 4); /** * Unsigned value that indicates the number of octets captured from the packet (i.e. the length of the Packet Data field). * It will be the minimum value among the Original Packet Length and the snapshot length for the interface (SnapLen, defined in Figure 1). * 32 bits */ - packetHeader.writeUInt32LE(packetData.length, 8) + packetHeader.writeUInt32LE(packetData.length, 8); /** * Unsigned value that indicates the actual length of the packet when it was transmitted on the network. * It can be different from the Captured Packet Length if the packet has been truncated by the capture process. * 32 bits */ - packetHeader.writeUInt32LE(packetData.length, 12) + packetHeader.writeUInt32LE(packetData.length, 12); - return Buffer.concat([packetHeader, packetData]) -} + return Buffer.concat([packetHeader, packetData]); +}; diff --git a/src/utils/xmodem.ts b/src/utils/xmodem.ts index b6fd389..3b9c21d 100644 --- a/src/utils/xmodem.ts +++ b/src/utils/xmodem.ts @@ -1,17 +1,17 @@ -import EventEmitter from 'node:events' +import EventEmitter from "node:events"; -import { logger } from '../index.js' -import { computeCRC16 } from './utils.js' +import { logger } from "../index.js"; +import { computeCRC16 } from "./utils.js"; -const NS = { namespace: 'xmodemcrc' } -const FILLER = 0xff +const NS = { namespace: "xmodemcrc" }; +const FILLER = 0xff; /** First block number. */ -const XMODEM_START_BLOCK = 1 +const XMODEM_START_BLOCK = 1; /** Bytes in each block (header and checksum not included) */ -const BLOCK_SIZE = 128 +const BLOCK_SIZE = 128; /** Maximum retries to send block before giving up */ -const MAX_RETRIES = 10 +const MAX_RETRIES = 10; export enum XSignal { /** Start of Header */ @@ -40,158 +40,162 @@ export enum XExitStatus { export enum XEvent { /** C byte received */ - START = 'start', - STOP = 'stop', + START = "start", + STOP = "stop", /** Data to write */ - DATA = 'data', + DATA = "data", } interface XModemCRCEventMap { - [XEvent.DATA]: [buffer: Buffer, progressPc: number] - [XEvent.START]: [] - [XEvent.STOP]: [status: XExitStatus] + [XEvent.DATA]: [buffer: Buffer, progressPc: number]; + [XEvent.START]: []; + [XEvent.STOP]: [status: XExitStatus]; } export class XModemCRC extends EventEmitter { - private blockNum: number = XMODEM_START_BLOCK - private blocks: Buffer[] = [] - private retries: number = MAX_RETRIES - private sentEOF: boolean = false - private waitForBlock: number = XMODEM_START_BLOCK + private blockNum: number = XMODEM_START_BLOCK; + private blocks: Buffer[] = []; + private retries: number = MAX_RETRIES; + private sentEOF = false; + private waitForBlock: number = XMODEM_START_BLOCK; public init(buffer: Buffer): void { - this.blockNum = XMODEM_START_BLOCK - this.blocks = [Buffer.from([])] // filler for start block offset - this.retries = MAX_RETRIES - this.sentEOF = false - this.waitForBlock = XMODEM_START_BLOCK - let currentBlock = Buffer.alloc(BLOCK_SIZE) + this.blockNum = XMODEM_START_BLOCK; + this.blocks = [Buffer.from([])]; // filler for start block offset + this.retries = MAX_RETRIES; + this.sentEOF = false; + this.waitForBlock = XMODEM_START_BLOCK; + let currentBlock = Buffer.alloc(BLOCK_SIZE); while (buffer.length > 0) { for (let i = 0; i < BLOCK_SIZE; i++) { - currentBlock[i] = buffer[i] === undefined ? FILLER : buffer[i] + currentBlock[i] = buffer[i] === undefined ? FILLER : buffer[i]; } - buffer = buffer.subarray(BLOCK_SIZE) - this.blocks.push(currentBlock) - currentBlock = Buffer.alloc(BLOCK_SIZE) + buffer = buffer.subarray(BLOCK_SIZE); + this.blocks.push(currentBlock); + currentBlock = Buffer.alloc(BLOCK_SIZE); } - const blocksCount = this.blocks.length - XMODEM_START_BLOCK + const blocksCount = this.blocks.length - XMODEM_START_BLOCK; - logger.debug(`Outgoing blocks count=${blocksCount}, size=${blocksCount * BLOCK_SIZE}.`, NS) + logger.debug(`Outgoing blocks count=${blocksCount}, size=${blocksCount * BLOCK_SIZE}.`, NS); } public process(recdData: Buffer): void { if (this.waitForBlock !== this.blockNum) { logger.warning( - `Received out of sequence data: ${recdData.toString('hex')} (blockNum=${this.blockNum}, expected=${this.waitForBlock}).`, + `Received out of sequence data: ${recdData.toString("hex")} (blockNum=${this.blockNum}, expected=${this.waitForBlock}).`, NS, - ) - this.retries-- + ); + this.retries--; if (this.retries === 0) { - logger.error(`Maximum retries ${MAX_RETRIES} reached. Giving up.`, NS) - this.emit(XEvent.STOP, XExitStatus.FAIL) + logger.error(`Maximum retries ${MAX_RETRIES} reached. Giving up.`, NS); + this.emit(XEvent.STOP, XExitStatus.FAIL); } - return + return; } - logger.debug(`Current block ${this.blockNum}. Received data: ${recdData.toString('hex')}.`, NS) + logger.debug(`Current block ${this.blockNum}. Received data: ${recdData.toString("hex")}.`, NS); switch (recdData[0]) { case XSignal.CRC: { if (this.blockNum === XMODEM_START_BLOCK) { - logger.debug(`Received C byte, starting transfer...`, NS) + logger.debug("Received C byte, starting transfer...", NS); if (this.blocks.length > this.blockNum) { - this.emit(XEvent.START) - this.emitBlock(this.blockNum, this.blocks[this.blockNum]) + this.emit(XEvent.START); + this.emitBlock(this.blockNum, this.blocks[this.blockNum]); - this.blockNum++ + this.blockNum++; } } - break + break; } case XSignal.ACK: { if (this.blockNum > XMODEM_START_BLOCK) { - this.retries = MAX_RETRIES + this.retries = MAX_RETRIES; - logger.debug('ACK received.', NS) + logger.debug("ACK received.", NS); if (this.blocks.length > this.blockNum) { - this.emitBlock(this.blockNum, this.blocks[this.blockNum]) + this.emitBlock(this.blockNum, this.blocks[this.blockNum]); - this.blockNum++ + this.blockNum++; } else if (this.blocks.length === this.blockNum) { if (this.sentEOF === false) { - this.sentEOF = true + this.sentEOF = true; - logger.debug(`Sending End of Transmission.`, NS) - this.emit(XEvent.DATA, Buffer.from([XSignal.EOT]), 100) + logger.debug("Sending End of Transmission.", NS); + this.emit(XEvent.DATA, Buffer.from([XSignal.EOT]), 100); } else { - logger.debug('Done.', NS) - this.emit(XEvent.STOP, XExitStatus.SUCCESS) + logger.debug("Done.", NS); + this.emit(XEvent.STOP, XExitStatus.SUCCESS); } } } - break + break; } case XSignal.NAK: { if (this.blockNum > XMODEM_START_BLOCK) { - this.retries-- + this.retries--; - logger.debug('NAK received.', NS) + logger.debug("NAK received.", NS); if (this.retries === 0) { - logger.error(`Maximum retries ${MAX_RETRIES} reached. Giving up.`, NS) - this.emit(XEvent.STOP, XExitStatus.FAIL) + logger.error(`Maximum retries ${MAX_RETRIES} reached. Giving up.`, NS); + this.emit(XEvent.STOP, XExitStatus.FAIL); } else if (this.blockNum === this.blocks.length && this.sentEOF) { - logger.warning('Received NAK, resending EOT.', NS) - this.emit(XEvent.DATA, Buffer.from([XSignal.EOT]), 0) + logger.warning("Received NAK, resending EOT.", NS); + this.emit(XEvent.DATA, Buffer.from([XSignal.EOT]), 0); } else { - logger.warning('Packet corrupted, resending previous block.', NS) + logger.warning("Packet corrupted, resending previous block.", NS); - this.blockNum-- + this.blockNum--; if (this.blocks.length > this.blockNum) { - this.emitBlock(this.blockNum, this.blocks[this.blockNum]) + this.emitBlock(this.blockNum, this.blocks[this.blockNum]); - this.blockNum++ + this.blockNum++; } } } - break + break; } case XSignal.CAN: { - logger.error(`Received cancel.`, NS) - this.emit(XEvent.STOP, XExitStatus.CANCEL) + logger.error("Received cancel.", NS); + this.emit(XEvent.STOP, XExitStatus.CANCEL); - break + break; } default: { - logger.debug(`Unrecognized data received for block ${this.blockNum}. Ignoring.`, NS) + logger.debug(`Unrecognized data received for block ${this.blockNum}. Ignoring.`, NS); - break + break; } } } private emitBlock(blockNum: number, blockData: Buffer): void { - const progressPc = Math.round((blockNum / (this.blocks.length - XMODEM_START_BLOCK)) * 100) - this.waitForBlock = blockNum + 1 - blockNum &= 0xff // starts at 1, goes to 255, then wraps back to 0 (XModem spec) + const progressPc = Math.round((blockNum / (this.blocks.length - XMODEM_START_BLOCK)) * 100); + this.waitForBlock = blockNum + 1; + blockNum &= 0xff; // starts at 1, goes to 255, then wraps back to 0 (XModem spec) - logger.debug(`Sending block ${blockNum}.`, NS) + logger.debug(`Sending block ${blockNum}.`, NS); - this.emit(XEvent.DATA, Buffer.concat([Buffer.from([XSignal.SOH, blockNum, 0xff - blockNum]), blockData, computeCRC16(blockData)]), progressPc) + this.emit( + XEvent.DATA, + Buffer.concat([Buffer.from([XSignal.SOH, blockNum, 0xff - blockNum]), blockData, computeCRC16(blockData)]), + progressPc, + ); } } diff --git a/test/commands/bootloader/index.test.ts b/test/commands/bootloader/index.test.ts deleted file mode 100644 index c29dc08..0000000 --- a/test/commands/bootloader/index.test.ts +++ /dev/null @@ -1 +0,0 @@ -describe('bootloader', () => {}) diff --git a/test/commands/monitor/index.test.ts b/test/commands/monitor/index.test.ts deleted file mode 100644 index 54a396b..0000000 --- a/test/commands/monitor/index.test.ts +++ /dev/null @@ -1 +0,0 @@ -describe('monitor', () => {}) diff --git a/test/commands/router/index.test.ts b/test/commands/router/index.test.ts deleted file mode 100644 index 2ba0530..0000000 --- a/test/commands/router/index.test.ts +++ /dev/null @@ -1 +0,0 @@ -describe('router', () => {}) diff --git a/test/commands/sniff/index.test.ts b/test/commands/sniff/index.test.ts deleted file mode 100644 index 1d00141..0000000 --- a/test/commands/sniff/index.test.ts +++ /dev/null @@ -1 +0,0 @@ -describe('sniff', () => {}) diff --git a/test/commands/stack/index.test.ts b/test/commands/stack/index.test.ts deleted file mode 100644 index 22224fb..0000000 --- a/test/commands/stack/index.test.ts +++ /dev/null @@ -1 +0,0 @@ -describe('stack', () => {}) diff --git a/test/commands/utils/index.test.ts b/test/commands/utils/index.test.ts deleted file mode 100644 index a709fda..0000000 --- a/test/commands/utils/index.test.ts +++ /dev/null @@ -1 +0,0 @@ -describe('utils', () => {}) diff --git a/test/tsconfig.json b/test/tsconfig.json deleted file mode 100644 index 82e3635..0000000 --- a/test/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../tsconfig", - "include": ["./**/*"], - "compilerOptions": { - "noEmit": true - }, - "references": [ - {"path": ".."} - ] -} diff --git a/tsconfig.json b/tsconfig.json index 58fc151..a0dfe76 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,20 @@ { - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "module": "nodenext", - "moduleResolution": "nodenext", - "target": "ESNext", - "lib": ["ESNext"], - "esModuleInterop": true, - "declaration": true, - "strict": true, - "noUnusedLocals": true, - "composite": true, - "checkJs": true - }, - "include": ["src/**/*"], - "ts-node": { - "esm": true - } + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "module": "nodenext", + "moduleResolution": "nodenext", + "target": "ESNext", + "lib": ["ESNext"], + "esModuleInterop": true, + "declaration": true, + "strict": true, + "noUnusedLocals": true, + "composite": true, + "checkJs": true + }, + "include": ["src/**/*"], + "ts-node": { + "esm": true + } }