diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..dc6d9d5 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# These are supported funding model platforms + +github: Nerivec +patreon: Nerivec +buy_me_a_coffee: Nerivec \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..32a4c3e --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,51 @@ +name: CI + +on: + push: + pull_request: + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: 'package.json' + + - uses: biomejs/setup-biome@v2 + with: + version: latest + + - run: npm ci + + - run: npm run build:prod + + - run: biome ci + + tests: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + node: [20, 22] + runs-on: ${{ matrix.os }} + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - run: npm ci + + - run: npm run build:prod + + # coverage disabled in early stages + # - run: npm run test:cov + - run: npm run test + + # get some "in-workflow" reference numbers for future comparison + # TODO: send results to PR as needed + - run: npm run bench diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..d79ba07 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["biomejs.biome", "vitest.explorer"] +} diff --git a/README.md b/README.md index 78b20a6..4082546 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,101 @@ -# zigbee-on-host -ZigBee stack designed to run on a host and communicate with a radio co-processor (RCP) +# ZigBee on Host + +Open Source ZigBee stack designed to run on a host and communicate with a radio co-processor (RCP). + +Current implementation aims for compatibility with OpenThread RCP firmware. That base provides compatibility with any chip manufacturer that supports it (Silabs, TI, etc.) with the only requirements being proper implementation of the STREAM_RAW mechanism of the Spinel protocol (which allows to send raw 802.15.4 frames, including... ZigBee!) and hardware MAC ACKing (much faster). + +_This library can also serve as a base for pentesting ZigBee networks thanks to the ability to easily craft various payloads at any layer of the specification and send them through the raw stream using any network parameters._ + +> [!IMPORTANT] +> Work in progress! Expect breaking changes without backwards compatibility for a while! + +## Development + +### Guidelines + +Some quick guidelines to keep the codebase maintainable: + +- No external production dependencies +- Mark `TODO` / `XXX` / `@deprecated` in code as needed for quick access +- Performance in mind (with the goal to eventually bring the appropriate layers to a lower language as needed) + - No expensive calls (stringify, etc.) + - Bail as early as possible (no unnecessary parsing, holding waiters, etc.) + - Ability to no-op expensive "optional" features + - And the usuals... +- Keep MAC/ZigBee property naming mostly in line with Wireshark for easier debugging +- Keep in line with the ZigBee 3.0 specification, but allow optimization due to the host-driven nature and removal of unnecessary features that won't impact compatibility +- Focus on "Centralized Trust Center" implementation (at least at first) + +### Current status + +> [~] Partial feature, [?] Uncertain feature + +- [x] Encoding/decoding of Spinel & HDLC protocols +- [x] Encoding/decoding of MAC frames +- [x] Encoding/decoding of ZigBee NWK frames + - [ ] lacking reference sniffs for multicast (group) +- [x] Encoding/decoding of ZigBee NWK GP frames + - [ ] lacking reference sniffs, needs full re-check + - [ ] FULLENCR & auth tag checking codepaths +- [x] Encoding/decoding of ZigBee NWK APS frames +- [x] Network forming +- [ ] Network state saving (de facto backups) + - [ ] Deal with frame counters (avoiding too many writes, but preventing mismatch issues) + - [ ] Runtime changing of network parameters (ZDO channel, PAN ID...) +- [~] Joining/Rejoining + - [x] APS TC link key update mechanism (global) + - [x] Direct child router + - [x] Direct child end device + - [ ] Nested device +- [x] Indirect transmission mechanism + - _Crude implementation_ + - [ ] Deal with devices lying on `rxOnWhenIdle` property (bad firmware, resulting in transmission type mismatch) +- [ ] Routing + - [ ] Source routing + - [?] Regular routing +- [ ] Coordinator binding +- [ ] InterPAN / Touchlink +- [ ] LQI reporting in messages +- [ ] Install codes +- [?] APS APP link keys +- [ ] R23 (need reference sniffs...) +- [ ] Security +- [ ] Metrics/Statistics +- [ ] Big cleanup of unused / never will use! +- [ ] Loads of testing! +- [ ] Optimize firmware building for this usage + +And likely more, and of course a bunch of `TODO`s in the code! + +### Testing + + +#### CLI + +Install dev dependencies and build: + +```bash +npm install +npm run build +``` + +Configure parameters in `dist/dev/conf.json` then start CLI (next start will use `zoh.save` file, if not removed): + +```bash +npm run dev:cli +``` + +_Currently, the CLI is output-only._ + +> [!TIP] +> Running `npm run build:prod` omits the `src/dev` directory. + +> [!TIP] +> If having issues with building, try removing the `*.tsbuildinfo` incremental compilation files. + +> [!TIP] +> For testing purposes, you can create a network with a regular NCP, then take it over with the RCP by copying all network settings. This allows to bypass the join steps as needed. + +#### Zigbee2MQTT + +Stay tuned... diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..f4814f0 --- /dev/null +++ b/biome.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": ["dist", "coverage", "node_modules"] + }, + "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" + }, + "correctness": { + "noUnusedImports": "error", + "noUnusedVariables": { + "level": "warn", + "fix": "none" + } + }, + "performance": { + "noBarrelFile": "error", + "noReExportAll": "error" + }, + "suspicious": { + "noConstEnum": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..783818b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2743 @@ +{ + "name": "zigbee-on-host", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zigbee-on-host", + "version": "0.1.0", + "license": "GPL-3.0-or-later", + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/node": "^22.10.7", + "@vitest/coverage-v8": "^3.0.2", + "serialport": "^12.0.0", + "typescript": "^5.7.3", + "vitest": "^3.0.2" + }, + "engines": { + "node": ">=20.15.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", + "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", + "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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, + "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": ">=14.21.3" + } + }, + "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 OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "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 OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "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 OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "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 OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "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 OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "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 OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "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 OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/@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/@rollup/rollup-android-arm-eabi": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz", + "integrity": "sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.31.0.tgz", + "integrity": "sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.31.0.tgz", + "integrity": "sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.31.0.tgz", + "integrity": "sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.31.0.tgz", + "integrity": "sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.31.0.tgz", + "integrity": "sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.31.0.tgz", + "integrity": "sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.31.0.tgz", + "integrity": "sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.31.0.tgz", + "integrity": "sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.31.0.tgz", + "integrity": "sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.31.0.tgz", + "integrity": "sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.31.0.tgz", + "integrity": "sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.31.0.tgz", + "integrity": "sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.31.0.tgz", + "integrity": "sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz", + "integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.31.0.tgz", + "integrity": "sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.31.0.tgz", + "integrity": "sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.31.0.tgz", + "integrity": "sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.31.0.tgz", + "integrity": "sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@serialport/binding-mock": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", + "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@serialport/bindings-cpp": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-12.0.1.tgz", + "integrity": "sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "11.0.0", + "debug": "4.3.4", + "node-addon-api": "7.0.0", + "node-gyp-build": "4.6.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-11.0.0.tgz", + "integrity": "sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-11.0.0.tgz", + "integrity": "sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@serialport/parser-delimiter": "11.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@serialport/bindings-interface": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", + "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/@serialport/parser-byte-length": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-12.0.0.tgz", + "integrity": "sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-cctalk": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-12.0.0.tgz", + "integrity": "sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-delimiter": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", + "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-inter-byte-timeout": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-12.0.0.tgz", + "integrity": "sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-packet-length": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-12.0.0.tgz", + "integrity": "sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@serialport/parser-readline": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz", + "integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@serialport/parser-delimiter": "12.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-ready": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-12.0.0.tgz", + "integrity": "sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-regex": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-12.0.0.tgz", + "integrity": "sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-slip-encoder": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-12.0.0.tgz", + "integrity": "sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-spacepacket": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-12.0.0.tgz", + "integrity": "sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-12.0.0.tgz", + "integrity": "sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "debug": "4.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@serialport/stream/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "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/node": { + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.2.tgz", + "integrity": "sha512-U+hZYb0FtgNDb6B3E9piAHzXXIuxuBw2cd6Lvepc9sYYY4KjgiwCBmo3Sird9ZRu3ggLpLBTfw1ZRr77ipiSfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "debug": "^4.4.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.0.2", + "vitest": "3.0.2" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.2.tgz", + "integrity": "sha512-dKSHLBcoZI+3pmP5hiZ7I5grNru2HRtEW8Z5Zp4IXog8QYcxhlox7JUPyIIFWfN53+3HW3KPLIl6nSzUGgKSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.2", + "@vitest/utils": "3.0.2", + "chai": "^5.1.2", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.2.tgz", + "integrity": "sha512-Hr09FoBf0jlwwSyzIF4Xw31OntpO3XtZjkccpcBf8FeVW3tpiyKlkeUzxS/txzHqpUCNIX157NaTySxedyZLvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.2.tgz", + "integrity": "sha512-yBohcBw/T/p0/JRgYD+IYcjCmuHzjC3WLAKsVE4/LwiubzZkE8N49/xIQ/KGQwDRA8PaviF8IRO8JMWMngdVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.2.tgz", + "integrity": "sha512-GHEsWoncrGxWuW8s405fVoDfSLk6RF2LCXp6XhevbtDjdDme1WV/eNmUueDfpY1IX3MJaCRelVCEXsT9cArfEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.0.2", + "pathe": "^2.0.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.2.tgz", + "integrity": "sha512-h9s67yD4+g+JoYG0zPCo/cLTabpDqzqNdzMawmNPzDStTiwxwkyYM1v5lWE8gmGv3SVJ2DcxA2NpQJZJv9ym3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.2.tgz", + "integrity": "sha512-8mI2iUn+PJFMT44e3ISA1R+K6ALVs47W6eriDTfXe6lFqlflID05MB4+rIFhmDSLBj8iBsZkzBYlgSkinxLzSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.2.tgz", + "integrity": "sha512-Qu01ZYZlgHvDP02JnMBRpX43nRaZtNpIzw3C1clDXmn8eakgX6iQVGzTQ/NjkIr64WD8ioqOjkaYRVvHQI5qiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.2", + "loupe": "^3.1.2", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "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/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "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/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/loupe": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", + "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "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/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-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/pathe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", + "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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/postcss": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz", + "integrity": "sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.31.0", + "@rollup/rollup-android-arm64": "4.31.0", + "@rollup/rollup-darwin-arm64": "4.31.0", + "@rollup/rollup-darwin-x64": "4.31.0", + "@rollup/rollup-freebsd-arm64": "4.31.0", + "@rollup/rollup-freebsd-x64": "4.31.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.31.0", + "@rollup/rollup-linux-arm-musleabihf": "4.31.0", + "@rollup/rollup-linux-arm64-gnu": "4.31.0", + "@rollup/rollup-linux-arm64-musl": "4.31.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.31.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.31.0", + "@rollup/rollup-linux-riscv64-gnu": "4.31.0", + "@rollup/rollup-linux-s390x-gnu": "4.31.0", + "@rollup/rollup-linux-x64-gnu": "4.31.0", + "@rollup/rollup-linux-x64-musl": "4.31.0", + "@rollup/rollup-win32-arm64-msvc": "4.31.0", + "@rollup/rollup-win32-ia32-msvc": "4.31.0", + "@rollup/rollup-win32-x64-msvc": "4.31.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialport": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-12.0.0.tgz", + "integrity": "sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "12.0.1", + "@serialport/parser-byte-length": "12.0.0", + "@serialport/parser-cctalk": "12.0.0", + "@serialport/parser-delimiter": "12.0.0", + "@serialport/parser-inter-byte-timeout": "12.0.0", + "@serialport/parser-packet-length": "12.0.0", + "@serialport/parser-readline": "12.0.0", + "@serialport/parser-ready": "12.0.0", + "@serialport/parser-regex": "12.0.0", + "@serialport/parser-slip-encoder": "12.0.0", + "@serialport/parser-spacepacket": "12.0.0", + "@serialport/stream": "12.0.0", + "debug": "4.3.4" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/serialport/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/serialport/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "dev": true, + "license": "MIT" + }, + "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/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/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "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/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/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.10.tgz", + "integrity": "sha512-MEszunEcMo6pFsfXN1GhCFQqnE25tWRH0MA4f0Q7uanACi4y1Us+ZGpTMnITwCTnYzB2b9cpmnelTlxgTBmaBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.4.49", + "rollup": "^4.23.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.2.tgz", + "integrity": "sha512-hsEQerBAHvVAbv40m3TFQe/lTEbOp7yDpyqMJqr2Tnd+W58+DEYOt+fluQgekOePcsNBmR77lpVAnIU2Xu4SvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.1", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.2.tgz", + "integrity": "sha512-5bzaHakQ0hmVVKLhfh/jXf6oETDBtgPo8tQCHYB+wftNgFJ+Hah67IsWc8ivx4vFL025Ow8UiuTf4W57z4izvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.2", + "@vitest/mocker": "3.0.2", + "@vitest/pretty-format": "^3.0.2", + "@vitest/runner": "3.0.2", + "@vitest/snapshot": "3.0.2", + "@vitest/spy": "3.0.2", + "@vitest/utils": "3.0.2", + "chai": "^5.1.2", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.1", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.2", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.2", + "@vitest/ui": "3.0.2", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "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/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/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/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/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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7a786e8 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "zigbee-on-host", + "version": "0.1.0", + "description": "ZigBee stack designed to run on a host and communicate with a radio co-processor (RCP)", + "engines": { + "node": ">=20.17.0" + }, + "types": "./dist/drivers/ot-rcp-driver.d.ts", + "main": "./dist/drivers/ot-rcp-driver.js", + "scripts": { + "build": "tsc", + "build:prod": "tsc --project tsconfig.prod.json", + "test": "vitest run --config ./test/vitest.config.mts", + "test:cov": "vitest run --config ./test/vitest.config.mts --coverage", + "bench": "vitest bench --run --config ./test/vitest.config.mts", + "check": "npx @biomejs/biome check --write .", + "clean": "rm -rf dist *.tsbuildinfo", + "dev:cli": "node dist/dev/cli.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Nerivec/zigbee-on-host.git" + }, + "keywords": ["zigbee", "host", "stack", "rcp"], + "author": "Nerivec", + "license": "GPL-3.0-or-later", + "bugs": { + "url": "https://github.com/Nerivec/zigbee-on-host/issues" + }, + "homepage": "https://github.com/Nerivec/zigbee-on-host#readme", + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/node": "^22.10.7", + "@vitest/coverage-v8": "^3.0.2", + "serialport": "^12.0.0", + "typescript": "^5.7.3", + "vitest": "^3.0.2" + } +} diff --git a/src/dev/cli.ts b/src/dev/cli.ts new file mode 100644 index 0000000..1cbb541 --- /dev/null +++ b/src/dev/cli.ts @@ -0,0 +1,35 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { MinimalAdapter } from "./minimal-adapter.js"; + +if (require.main === module) { + // biome-ignore lint/suspicious/noExplicitAny: dev + const conf = JSON.parse(readFileSync(join(__dirname, "conf.json"), "utf8")) as Record; + + console.log("Starting with conf:", JSON.stringify(conf)); + + const adapter = new MinimalAdapter( + conf.adapter, + conf.streamRaw, + // NOTE: this information is overwritten on `start` if a save exists + { + eui64: Buffer.from(conf.network.eui64).readBigUInt64LE(0), + panId: conf.network.panId, + extendedPANId: Buffer.from(conf.network.extendedPANId).readBigUInt64LE(0), + channel: conf.network.channel, + nwkUpdateId: conf.network.nwkUpdateId, + txPower: conf.network.txPower, + networkKey: Buffer.from(conf.network.networkKey), + networkKeyFrameCounter: conf.network.networkKeyFrameCounter, + networkKeySequenceNumber: conf.network.networkKeySequenceNumber, + tcKey: Buffer.from(conf.network.tcKey), + tcKeyFrameCounter: conf.network.tcKeyFrameCounter, + }, + conf.sendMACToZEP, + ); + + process.on("SIGINT", async () => await adapter.stop()); + process.on("SIGTERM", async () => await adapter.stop()); + + void adapter.start(); +} diff --git a/src/dev/conf.json b/src/dev/conf.json new file mode 100644 index 0000000..6a4ef23 --- /dev/null +++ b/src/dev/conf.json @@ -0,0 +1,33 @@ +{ + "adapter": { + "path": "COM4", + "baudRate": 460800, + "rtscts": true + }, + "streamRaw": { + "txChannel": 25, + "ccaBackoffAttempts": 1, + "ccaRetries": 4, + "enableCSMACA": true, + "headerUpdated": true, + "reTx": false, + "securityProcessed": true, + "txDelay": 0, + "txDelayBaseTime": 0, + "rxChannelAfterTxDone": 25 + }, + "network": { + "tcKey": [90, 105, 103, 66, 101, 101, 65, 108, 108, 105, 97, 110, 99, 101, 48, 57], + "tcKeyFrameCounter": 0, + "networkKey": [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13], + "networkKeyFrameCounter": 0, + "networkKeySequenceNumber": 0, + "panId": 26979, + "extendedPANId": [17, 221, 34, 221, 51, 221, 68, 221], + "channel": 25, + "eui64": [99, 108, 105, 99, 108, 105, 99, 108], + "nwkUpdateId": 0, + "txPower": 19 + }, + "sendMACToZEP": false +} diff --git a/src/dev/minimal-adapter.ts b/src/dev/minimal-adapter.ts new file mode 100644 index 0000000..8ddaf55 --- /dev/null +++ b/src/dev/minimal-adapter.ts @@ -0,0 +1,280 @@ +import { type Socket as DgramSocket, createSocket } from "node:dgram"; +import { Socket } from "node:net"; +import { SerialPort } from "serialport"; +import type { StreamRawConfig } from "src/spinel/spinel.js"; +import type { ZigbeeAPSHeader, ZigbeeAPSPayload } from "src/zigbee/zigbee-aps.js"; +import { type NetworkParameters, OTRCPDriver } from "../drivers/ot-rcp-driver.js"; +import { logger } from "../utils/logger.js"; +import { DEFAULT_WIRESHARK_IP, DEFAULT_ZEP_UDP_PORT, createWiresharkZEPFrame } from "./wireshark.js"; + +const NS = "minimal-adapter"; + +export function isTcpPath(path: string): boolean { + // tcp path must be: tcp://: + return /^(?:tcp:\/\/)[\w.-]+[:][\d]+$/gm.test(path); +} + +/** + * Example: + * ```ts + * { + * path: 'COM4', + * baudRate: 460800, + * rtscts: true, + * } + * ``` + */ +type PortOptions = { + path: string; + //---- serial only + baudRate?: number; + rtscts?: boolean; +}; + +/** + * Minimal adapter using the OT RCP Driver that can be started via `cli.ts` and outputs to both the console, and Wireshark (MAC frames). + */ +export class MinimalAdapter { + public readonly driver: OTRCPDriver; + private readonly portOptions: PortOptions; + private serialPort?: SerialPort; + private socketPort?: Socket; + /** True when serial/socket is currently closing */ + private closing: boolean; + + private wiresharkSeqNum: number; + private wiresharkPort: number; + private wiresharkAddress: string; + private readonly wiresharkSocket: DgramSocket; + + constructor(portOptions: PortOptions, streamRawConfig: StreamRawConfig, netParams: NetworkParameters, sendMACToZEP: boolean) { + this.wiresharkSeqNum = 0; // start at 1 + this.wiresharkSocket = createSocket("udp4"); + this.wiresharkPort = process.env.WIRESHARK_ZEP_PORT ? Number.parseInt(process.env.WIRESHARK_ZEP_PORT) : DEFAULT_ZEP_UDP_PORT; + this.wiresharkAddress = process.env.WIRESHARK_ADDRESS ? process.env.WIRESHARK_ADDRESS : DEFAULT_WIRESHARK_IP; + this.wiresharkSocket.bind(this.wiresharkPort); + + this.driver = new OTRCPDriver(streamRawConfig, netParams, ".", sendMACToZEP); + + this.portOptions = portOptions; + this.closing = false; + + if (sendMACToZEP) { + this.driver.on("MAC_FRAME", (payload, rssi) => { + const wsZEPFrame = createWiresharkZEPFrame(this.driver.netParams.channel, 1, 0, rssi ?? 0, this.nextWiresharkSeqNum(), payload); + + this.wiresharkSocket.send(wsZEPFrame, this.wiresharkPort, this.wiresharkAddress); + }); + } + + // noop logger as needed + // setLogger({ debug: () => {}, info: () => {}, warning: () => {}, error: () => {}}); + } + + /** + * Check if port is valid, open, and not closing. + */ + get portOpen(): boolean { + if (this.closing) { + return false; + } + + if (isTcpPath(this.portOptions.path!)) { + return this.socketPort ? !this.socketPort.closed : false; + } + + return this.serialPort ? this.serialPort.isOpen : false; + } + + private nextWiresharkSeqNum(): number { + this.wiresharkSeqNum = (this.wiresharkSeqNum + 1) & 0xffffffff; + + return this.wiresharkSeqNum + 1; + } + + /** + * Init the serial or socket port and hook parser/writer. + */ + public async initPort(): Promise { + await this.closePort(); // will do nothing if nothing's open + + if (isTcpPath(this.portOptions.path!)) { + const pathUrl = new URL(this.portOptions.path!); + const hostname = pathUrl.hostname; + const port = Number.parseInt(pathUrl.port, 10); + + logger.debug(`Opening TCP socket with ${hostname}:${port}`, NS); + + this.socketPort = new Socket(); + + this.socketPort.setNoDelay(true); + this.socketPort.setKeepAlive(true, 15000); + this.driver.writer.pipe(this.socketPort); + this.socketPort.pipe(this.driver.parser); + this.driver.parser.on("data", this.driver.onFrame.bind(this.driver)); + + return await new Promise((resolve, reject): void => { + const openError = async (err: Error): Promise => { + await this.stop(); + + reject(err); + }; + + this.socketPort!.on("connect", () => { + logger.debug("Socket connected", NS); + }); + this.socketPort!.on("ready", (): void => { + logger.info("Socket ready", NS); + this.socketPort!.removeListener("error", openError); + this.socketPort!.once("close", this.onPortClose.bind(this)); + this.socketPort!.on("error", this.onPortError.bind(this)); + + resolve(); + }); + this.socketPort!.once("error", openError); + + this.socketPort!.connect(port, hostname); + }); + } + + const serialOpts = { + path: this.portOptions.path!, + baudRate: typeof this.portOptions.baudRate === "number" ? this.portOptions.baudRate : 115200, + rtscts: typeof this.portOptions.rtscts === "boolean" ? this.portOptions.rtscts : false, + autoOpen: false, + parity: "none" as const, + stopBits: 1 as const, + xon: false, + xoff: false, + }; + + // enable software flow control if RTS/CTS not enabled in config + if (!serialOpts.rtscts) { + logger.info("RTS/CTS config is off, enabling software flow control.", NS); + serialOpts.xon = true; + serialOpts.xoff = true; + } + + logger.debug(() => `Opening serial port with [path=${serialOpts.path} baudRate=${serialOpts.baudRate} rtscts=${serialOpts.rtscts}]`, NS); + this.serialPort = new SerialPort(serialOpts); + + this.driver.writer.pipe(this.serialPort); + this.serialPort.pipe(this.driver.parser); + this.driver.parser.on("data", this.driver.onFrame.bind(this.driver)); + + try { + await new Promise((resolve, reject): void => { + this.serialPort!.open((err) => (err ? reject(err) : resolve())); + }); + + logger.info("Serial port opened", NS); + + this.serialPort.once("close", this.onPortClose.bind(this)); + this.serialPort.on("error", this.onPortError.bind(this)); + } catch (error) { + await this.stop(); + + throw error; + } + } + + /** + * Handle port closing + * @param err A boolean for Socket, an Error for serialport + */ + private async onPortClose(error: boolean | Error): Promise { + if (error) { + logger.error("Port closed unexpectedly.", NS); + } else { + logger.info("Port closed.", NS); + } + } + + /** + * Handle port error + * @param error + */ + private async onPortError(error: Error): Promise { + logger.error(`Port ${error}`, NS); + + throw new Error("Port error"); + } + + public async start(): Promise { + await this.initPort(); + + if (!this.portOpen) { + throw new Error("Invalid call to start"); + } + + if (this.serialPort) { + // try clearing read/write buffers + try { + await new Promise((resolve, reject): void => { + this.serialPort!.flush((err) => (err ? reject(err) : resolve())); + }); + } catch (err) { + logger.error(`Error while flushing serial port before start: ${err}`, NS); + } + } + + await this.driver.start(); + await this.driver.formNetwork(); + // allow joins on start for 254 seconds + this.driver.allowJoins(0xfe, true); + + this.driver.on("FRAME", this.onFrame.bind(this)); + this.driver.on("DEVICE_JOINED", this.onDeviceJoined.bind(this)); + this.driver.on("DEVICE_REJOINED", this.onDeviceRejoined.bind(this)); + this.driver.on("DEVICE_LEFT", this.onDeviceLeft.bind(this)); + } + + public async stop(): Promise { + this.closing = true; + + await this.driver.stop(); + this.wiresharkSocket.close(); + await this.closePort(); + } + + public async closePort(): Promise { + if (this.serialPort?.isOpen) { + try { + await new Promise((resolve, reject): void => { + this.serialPort!.flush((err) => (err ? reject(err) : resolve())); + }); + + await new Promise((resolve, reject): void => { + this.serialPort!.close((err) => (err ? reject(err) : resolve())); + }); + } catch (err) { + logger.error(`Failed to close serial port ${err}.`, NS); + } + + this.serialPort.removeAllListeners(); + + this.serialPort = undefined; + } else if (this.socketPort != null && !this.socketPort.closed) { + this.socketPort.destroy(); + this.socketPort.removeAllListeners(); + + this.socketPort = undefined; + } + } + + private onFrame(_sender16: number | undefined, _sender64: bigint | undefined, _apsHeader: ZigbeeAPSHeader, _apsPayload: ZigbeeAPSPayload): void { + // as needed for testing + } + + private onDeviceJoined(_source16: number, _source64: bigint | undefined): void { + // as needed for testing + } + + private onDeviceRejoined(_source16: number, _source64: bigint | undefined): void { + // as needed for testing + } + + private onDeviceLeft(_source16: number, _source64: bigint): void { + // as needed for testing + } +} diff --git a/src/dev/wireshark.ts b/src/dev/wireshark.ts new file mode 100644 index 0000000..9787917 --- /dev/null +++ b/src/dev/wireshark.ts @@ -0,0 +1,115 @@ +export const DEFAULT_WIRESHARK_IP = "127.0.0.1"; +export const DEFAULT_ZEP_UDP_PORT = 17754; + +/** + * @see https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-zep.c + * @see https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-ieee802154.c + * @see https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-zbee-nwk.c + *------------------------------------------------------------ + * + * ZEP Packets must be received in the following format: + * |UDP Header| ZEP Header |IEEE 802.15.4 Packet| + * | 8 bytes | 16/32 bytes | <= 127 bytes | + *------------------------------------------------------------ + * + * ZEP v1 Header will have the following format: + * |Preamble|Version|Channel ID|Device ID|CRC/LQI Mode|LQI Val|Reserved|Length| + * |2 bytes |1 byte | 1 byte | 2 bytes | 1 byte |1 byte |7 bytes |1 byte| + * + * ZEP v2 Header will have the following format (if type=1/Data): + * |Preamble|Version| Type |Channel ID|Device ID|CRC/LQI Mode|LQI Val|NTP Timestamp|Sequence#|Reserved|Length| + * |2 bytes |1 byte |1 byte| 1 byte | 2 bytes | 1 byte |1 byte | 8 bytes | 4 bytes |10 bytes|1 byte| + * + * ZEP v2 Header will have the following format (if type=2/Ack): + * |Preamble|Version| Type |Sequence#| + * |2 bytes |1 byte |1 byte| 4 bytes | + *------------------------------------------------------------ + */ +const ZEP_PREAMBLE_NUM = 0x5845; // '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; +/** Baseline NTP time if bit-0=1 -> 1-Jan-1900 @ 01:00:00 UTC */ +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 + // 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; + + if (useBase1) { + seconds |= 0x80000000n; // set high-order bit if MSB_1_BASE_TIME 1900 used + } + + return BigInt.asIntN(64, (seconds << 32n) | fraction); +}; + +export const createWiresharkZEPFrame = ( + channelId: number, + deviceId: number, + lqi: number, + rssi: number, + sequence: number, + data: Buffer, + lqiMode = false, +): Buffer => { + const payload = Buffer.alloc(32 + data.byteLength); + 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 + // So, two bytes must be added at the end: + // * First byte: RSSI value as a signed 8 bits integer (range -128 to 127) + // * 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); + + // Protocol ID String | Character string | 2.0.3 to 4.2.5 + payload.writeUInt16LE(ZEP_PREAMBLE_NUM, offset); + offset += 2; + // Protocol Version | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 + payload.writeUInt8(ZEP_PROTOCOL_VERSION, offset); + offset += 1; + // Type | Unsigned integer (8 bits) | 1.2.0 to 1.8.15, 1.12.0 to 4.2.5 + payload.writeUInt8(ZEP_PROTOCOL_TYPE, offset); + offset += 1; + // Channel ID | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 + payload.writeUInt8(channelId, offset); + offset += 1; + // Device ID | Unsigned integer (16 bits) | 1.2.0 to 4.2.5 + payload.writeUint16BE(deviceId, offset); + offset += 2; + + // LQI/CRC Mode | Boolean | 1.2.0 to 4.2.5 + payload.writeUInt8(lqiMode ? 1 : 0, offset); + offset += 1; + // Link Quality Indication | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 + payload.writeUInt8(lqi, offset); + offset += 1; + + // Timestamp | Date and time | 1.2.0 to 4.2.5 + payload.writeBigInt64BE(getZepTimestamp(), offset); + offset += 8; + + // Sequence Number | Unsigned integer (32 bits) | 1.2.0 to 4.2.5 + payload.writeUInt32BE(sequence, offset); + offset += 4; + + // Reserved Fields | Byte sequence | 2.0.0 to 4.2.5 + offset += 10; + + // Length | Unsigned integer (8 bits) | 1.2.0 to 4.2.5 + payload.writeUInt8(data.byteLength, offset); + offset += 1; + + payload.set(data, offset); + offset += data.length; + + return payload; +}; diff --git a/src/drivers/endpoints.ts b/src/drivers/endpoints.ts new file mode 100644 index 0000000..2865251 --- /dev/null +++ b/src/drivers/endpoints.ts @@ -0,0 +1,194 @@ +import { ZigbeeMACConsts } from "../zigbee/mac.js"; +import { ZigbeeConsts } from "../zigbee/zigbee.js"; + +const MAC_CAPABILITIES = + (1 & 0x01) | // alternatePANCoordinator + ((1 << 1) & 0x02) | // deviceType + ((1 << 2) & 0x04) | // powerSource + ((1 << 3) & 0x08) | // rxOnWhenIdle + ((0 << 4) & 0x10) | // reserved1 + ((0 << 5) & 0x20) | // reserved2 + ((0 << 6) & 0x40) | // securityCapability + ((1 << 7) & 0x80); // allocateAddress +const MANUFACTURER_CODE = 0xffff; +const SERVER_MASK = + (1 & 0x01) | // primaryTrustCenter + ((0 << 1) & 0x02) | // backupTrustCenter + ((0 << 2) & 0x04) | // deprecated1 + ((0 << 3) & 0x08) | // deprecated2 + ((0 << 4) & 0x10) | // deprecated3 + ((0 << 5) & 0x20) | // deprecated4 + ((1 << 6) & 0x40) | // networkManager + ((0 << 7) & 0x80) | // reserved1 + ((0 << 8) & 0x100) | // reserved2 + ((22 << 9) & 0xfe00); // stackComplianceRevision TODO: update to 23 once properly supported + +const EP_HA = 1; +const EP_HA_PROFILE_ID = 0x0104; +const EP_HA_DEVICE_ID = 0x65; +const EP_HA_INPUT_CLUSTERS = [ + 0x0000, // Basic + 0x0003, // Identify + 0x0006, // On/off + 0x0008, // Level Control + 0x000a, // Time + 0x0019, // Over the Air Bootloading + // Cluster.genPowerProfile.ID,// 0x001A, // Power Profile XXX: missing ZCL cluster def in Z2M? + 0x0300, // Color Control +]; +const EP_HA_OUTPUT_CLUSTERS = [ + 0x0000, // Basic + 0x0003, // Identify + 0x0004, // Groups + 0x0005, // Scenes + 0x0006, // On/off + 0x0008, // Level Control + 0x0020, // Poll Control + 0x0300, // Color Control + 0x0400, // Illuminance Measurement + 0x0402, // Temperature Measurement + 0x0405, // Relative Humidity Measurement + 0x0406, // Occupancy Sensing + 0x0500, // IAS Zone + 0x0702, // Simple Metering + 0x0b01, // Meter Identification + 0x0b03, // Appliance Statistics + 0x0b04, // Electrical Measurement + 0x1000, // touchlink +]; +const EP_GP = 242; +const EP_GP_PROFILE_ID = 0xa1e0; +const EP_GP_DEVICE_ID = 0x66; +const EP_GP_INPUT_CLUSTERS = [ + 0x0021, // Green Power +]; +const EP_GP_OUTPUT_CLUSTERS = [ + 0x0021, // Green Power +]; + +const ACTIVE_ENDPOINTS_RESPONSE = [0x00, 0x00, 0x00, 0x00, 2, EP_HA, EP_GP]; + +export function encodeCoordinatorDescriptors(eui64: bigint): [address: Buffer, node: Buffer, power: Buffer, simple: Buffer, activeEndpoints: Buffer] { + // works for both NETWORK & IEEE response + const address = Buffer.alloc(12); + let offset = 2; // skip seqNum (set on use), status + + address.writeBigUInt64LE(eui64, offset); + offset += 8; + address.writeUInt16LE(ZigbeeConsts.COORDINATOR_ADDRESS, offset); + offset += 2; + + const node = Buffer.alloc(17); + offset = 4; // skip seqNum (set on use), status, nwkAddress + + node.writeUInt8( + (0 & 0x07) | // logicalType + ((0 << 5) & 0x20), // fragmentationSupported + offset, + ); + offset += 1; + node.writeUInt8( + (0 & 0x07) | // apsFlags + ((8 << 3) & 0xf8), // frequencyBand + offset, + ); + offset += 1; + node.writeUInt8(MAC_CAPABILITIES, offset); + offset += 1; + node.writeUInt16LE(MANUFACTURER_CODE, offset); + offset += 2; + node.writeUInt8(0xff, offset); + offset += 1; + node.writeUInt16LE(ZigbeeMACConsts.FRAME_MAX_SIZE, offset); + offset += 2; + node.writeUInt16LE(SERVER_MASK, offset); + offset += 2; + node.writeUInt16LE(ZigbeeMACConsts.FRAME_MAX_SIZE, offset); + offset += 2; + // skip deprecated + offset += 1; + + const power = Buffer.alloc(6); + offset = 4; // skip seqNum (set on use), status, nwkAddress + + power.writeUInt8( + (0 & 0xf) | // currentPowerMode + ((0 & 0xf) << 4), // availPowerSources + offset, + ); + offset += 1; + power.writeUInt8( + (0 & 0xf) | // currentPowerSource + ((0b1100 & 0xf) << 4), // currentPowerSourceLevel + offset, + ); + offset += 1; + + const simple = Buffer.alloc( + 21 + + (EP_HA_INPUT_CLUSTERS.length + EP_HA_OUTPUT_CLUSTERS.length + EP_GP_INPUT_CLUSTERS.length + EP_GP_OUTPUT_CLUSTERS.length) * + 2 /* uint16_t */, + ); + offset = 4; // skip seqNum (set on use), status, nwkAddress + + simple.writeUInt8( + 16 + + (EP_HA_INPUT_CLUSTERS.length + EP_HA_OUTPUT_CLUSTERS.length + EP_GP_INPUT_CLUSTERS.length + EP_GP_OUTPUT_CLUSTERS.length) * + 2 /* uint16_t */, + offset, + ); + offset += 1; + // HA endpoint + simple.writeUInt8(EP_HA, offset); + offset += 1; + simple.writeUInt16LE(EP_HA_PROFILE_ID, offset); + offset += 2; + simple.writeUInt16LE(EP_HA_DEVICE_ID, offset); + offset += 2; + simple.writeUInt8(1, offset); + offset += 1; + simple.writeUInt8(EP_HA_INPUT_CLUSTERS.length, offset); + offset += 1; + + for (const haInCluster of EP_HA_INPUT_CLUSTERS) { + simple.writeUInt16LE(haInCluster, offset); + offset += 2; + } + + simple.writeUInt8(EP_HA_OUTPUT_CLUSTERS.length, offset); + offset += 1; + + for (const haOutCluster of EP_HA_OUTPUT_CLUSTERS) { + simple.writeUInt16LE(haOutCluster, offset); + offset += 2; + } + + // GP endpoint + simple.writeUInt8(EP_GP, offset); + offset += 1; + simple.writeUInt16LE(EP_GP_PROFILE_ID, offset); + offset += 2; + simple.writeUInt16LE(EP_GP_DEVICE_ID, offset); + offset += 2; + simple.writeUInt8(1, offset); + offset += 1; + simple.writeUInt8(EP_GP_INPUT_CLUSTERS.length, offset); + offset += 1; + + for (const gpInCluster of EP_GP_INPUT_CLUSTERS) { + simple.writeUInt16LE(gpInCluster, offset); + offset += 2; + } + + simple.writeUInt8(EP_GP_OUTPUT_CLUSTERS.length, offset); + offset += 1; + + for (const gpOutCluster of EP_GP_OUTPUT_CLUSTERS) { + simple.writeUInt16LE(gpOutCluster, offset); + offset += 2; + } + + const activeEndpoints = Buffer.from(ACTIVE_ENDPOINTS_RESPONSE); + + return [address, node, power, simple, activeEndpoints]; +} diff --git a/src/drivers/ot-rcp-driver.ts b/src/drivers/ot-rcp-driver.ts new file mode 100644 index 0000000..7f79ccb --- /dev/null +++ b/src/drivers/ot-rcp-driver.ts @@ -0,0 +1,4359 @@ +import EventEmitter from "node:events"; + +import { existsSync, mkdirSync } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { SpinelCommandId } from "../spinel/commands.js"; +import { HDLC_TX_CHUNK_SIZE, type HdlcFrame, HdlcReservedByte, decodeHdlcFrame } from "../spinel/hdlc.js"; +import { SpinelPropertyId } from "../spinel/properties.js"; +import { + SPINEL_HEADER_FLG_SPINEL, + type SpinelFrame, + SpinelResetReason, + type SpinelStreamRawMetadata, + type StreamRawConfig, + decodeSpinelFrame, + encodeSpinelFrame, + getPackedUInt, + readPropertyStreamRaw, + readPropertyU, + readPropertyi, + readPropertyii, + writePropertyC, + writePropertyE, + writePropertyId, + writePropertyS, + writePropertyStreamRaw, + writePropertyb, + writePropertyc, +} from "../spinel/spinel.js"; +import { SpinelStatus } from "../spinel/statuses.js"; +import { logger } from "../utils/logger.js"; +import { + MACAssociationStatus, + MACCommandId, + MACFrameAddressMode, + MACFrameType, + MACFrameVersion, + type MACHeader, + ZigbeeMACConsts, + decodeMACFrameControl, + decodeMACHeader, + decodeMACPayload, + encodeMACFrame, + encodeMACFrameZigbee, + encodeMACZigbeeBeacon, +} from "../zigbee/mac.js"; +import { + ZigbeeAPSCommandId, + ZigbeeAPSConsts, + ZigbeeAPSDeliveryMode, + ZigbeeAPSFrameType, + type ZigbeeAPSHeader, + type ZigbeeAPSPayload, + decodeZigbeeAPSFrameControl, + decodeZigbeeAPSHeader, + decodeZigbeeAPSPayload, + encodeZigbeeAPSFrame, + encodeZigbeeAPSHeader, +} from "../zigbee/zigbee-aps.js"; +import { + ZigbeeNWKCommandId, + ZigbeeNWKConsts, + ZigbeeNWKFrameType, + type ZigbeeNWKHeader, + type ZigbeeNWKLinkStatus, + type ZigbeeNWKManyToOne, + type ZigbeeNWKMulticastControl, + ZigbeeNWKMulticastMode, + ZigbeeNWKRouteDiscovery, + ZigbeeNWKStatus, + decodeZigbeeNWKFrameControl, + decodeZigbeeNWKHeader, + decodeZigbeeNWKPayload, + encodeZigbeeNWKFrame, +} from "../zigbee/zigbee-nwk.js"; +import { + ZigbeeNWKGPFrameType, + type ZigbeeNWKGPHeader, + decodeZigbeeNWKGPFrameControl, + decodeZigbeeNWKGPHeader, + decodeZigbeeNWKGPPayload, +} from "../zigbee/zigbee-nwkgp.js"; +import { + ZigbeeConsts, + ZigbeeKeyType, + type ZigbeeSecurityHeader, + ZigbeeSecurityLevel, + encodeZigbeeSecurityHeader, + makeKeyedHash, + makeKeyedHashByType, + registerDefaultHashedKeys, +} from "../zigbee/zigbee.js"; +import { encodeCoordinatorDescriptors } from "./endpoints.js"; +import { OTRCPParser } from "./ot-rcp-parser.js"; +import { OTRCPWriter } from "./ot-rcp-writer.js"; + +const NS = "ot-rcp-driver"; + +interface AdapterDriverEventMap { + MAC_FRAME: [payload: Buffer, rssi?: number]; + FATAL_ERROR: [message: string]; + FRAME: [sender16: number | undefined, sender64: bigint | undefined, apsHeader: ZigbeeAPSHeader, apsPayload: ZigbeeAPSPayload, rssi: number]; + DEVICE_JOINED: [source16: number, source64: bigint]; + DEVICE_REJOINED: [source16: number, source64: bigint]; + DEVICE_LEFT: [source16: number, source64: bigint]; +} + +export enum InstallCodePolicy { + /** Do not support Install Codes */ + NOT_SUPPORTED = 0x00, + /** Support but do not require use of Install Codes or preset passphrases */ + NOT_REQUIRED = 0x01, + /** Require the use of Install Codes by joining devices or preset Passphrases */ + REQUIRED = 0x02, +} + +export enum TrustCenterKeyRequestPolicy { + DISALLOWED = 0x00, + /** Any device MAY request */ + ALLOWED = 0x01, + /** Only devices in the apsDeviceKeyPairSet with a KeyAttribute value of PROVISIONAL_KEY MAY request. */ + ONLY_PROVISIONAL = 0x02, +} + +export enum ApplicationKeyRequestPolicy { + DISALLOWED = 0x00, + /** Any device MAY request an application link key with any device (except the Trust Center) */ + ALLOWED = 0x01, + /** Only those devices listed in applicationKeyRequestList MAY request and receive application link keys. */ + ONLY_APPROVED = 0x02, +} + +export enum NetworkKeyUpdateMethod { + /** Broadcast using only network encryption */ + BROADCAST = 0x00, + /** Unicast using network encryption and APS encryption with a device’s link key. */ + UNICAST = 0x01, +} + +export type NetworkParameters = { + eui64: bigint; + panId: number; + extendedPANId: bigint; + channel: number; + nwkUpdateId: number; + txPower: number; + // TODO: implement ~30-day automatic key rotation? + networkKey: Buffer; + // TODO: wrap-to-zero mechanism / APS SWITCH_KEY + networkKeyFrameCounter: number; + networkKeySequenceNumber: number; + tcKey: Buffer; + tcKeyFrameCounter: number; +}; + +/** + * see 05-3474-23 #4.7.3 + */ +export type TrustCenterPolicies = { + /** + * This boolean indicates whether the Trust Center is currently allowing devices to join the network. + * A value of TRUE means that the Trust Center is allowing devices that have never been sent the network key or a trust center link key, to join the network. + */ + allowJoins: boolean; + /** This enumeration indicates if the Trust Center requires install codes to be used with joining devices. */ + installCode: InstallCodePolicy; + /** + * This value indicates if the trust center allows rejoins using well known or default keys. + * A setting of FALSE means rejoins are only allowed with trust center link keys where the KeyAttributes of the apsDeviceKeyPairSet entry indicates VERIFIED_KEY. + */ + allowRejoinsWithWellKnownKey: boolean; + /** This value controls whether devices are allowed to request a Trust Center Link Key after they have joined the network. */ + allowTCKeyRequest: TrustCenterKeyRequestPolicy; + /** This policy indicates whether a node on the network that transmits a ZDO Mgmt_Permit_Join with a significance set to 1 is allowed to effect the local Trust Center’s policies. */ + allowRemoteTCPolicyChange: boolean; + /** This value determines how the Trust Center SHALL handle attempts to request an application link key with a partner node. */ + allowAppKeyRequest: ApplicationKeyRequestPolicy; + /** + * This is a list of IEEE pairs of devices, which are allowed to establish application link keys between one another. + * The first IEEE address is the initiator, the second is the responder. + * If the responder address is set to 0xFFFFFFFFFFFFFFFF, then the initiator is allowed to request an application link key with any device. + * If the responder’s address is not 0xFFFFFFFFFFFFFFFF, then it MAY also initiate an application link key request. + * This list is only valid if allowAppKeyRequest is set to 0x02. + */ + appKeyRequestList?: [responder64: bigint, initiator64: bigint][]; + /** + * TODO: should do at least once a year to prevent deadlock at 0xffffffff + * alt: update when counter reaches 0x40000000 + * The period, in minutes, of how often the network key is updated by the Trust Center. + * A period of 0 means the Trust Center will not periodically update the network key (it MAY still update key at other times). + * uint32_t + */ + networkKeyUpdatePeriod: number; + /** This value describes the method the Trust Center uses to update the network key. */ + networkKeyUpdateMethod: NetworkKeyUpdateMethod; + /** + * This Boolean indicates whether the Trust Center is currently allowing Zigbee Direct Virtual Devices (ZVDs) to join the network. + * A value of TRUE means that the Trust Center is allowing such devices. + */ + allowVirtualDevices: boolean; +}; + +/** + * see 05-3474-23 #3.6.1.7 + * + * SHALL contain information on every device on the current Zigbee network within transmission range, up to some implementation-dependent limit. + * The neighbor does not store information about potential networks and candidate parents to join or rejoin. + * The Discovery table SHALL be used for this. + */ +export type NeighborTableEntry = { + // address16: number; // mapped + /** 64-bit IEEE address that is unique to every device. */ + address64: bigint; + /** + * The type of neighbor device: + * - 0x00 = Zigbee coordinator + * - 0x01 = Zigbee router + * - 0x02 = Zigbee end device + * + * This field SHALL be present in every neighbor table entry. + */ + deviceType: number; + rxOnWhenIdle: boolean; // TODO: in `capabilities`? + capabilities: number; // TODO: MACCapabilityFlags or parse only "when-needed" and keep as number? + /** The end device’s configuration. See section 3.4.11.3.2. The default value SHALL be 0. uint16_t */ + endDeviceConfig: number; + /** + * This field indicates the current time remaining, in seconds, for the end device. + * 0x00000000 – 0x00F00000 + */ + timeoutCounter?: number; + /** + * This field indicates the timeout, in seconds, for the end device child. + * The default value for end device entries is calculated by using the nwkEndDeviceTimeoutDefault value and indexing into Table 3-54, then converting the value to seconds. + * End Devices MAY negotiate a longer or shorter time using the NWK Command End Device Timeout Request. + * 0x00000000 – 0x0001FA40 + */ + deviceTimeout?: number; + /** + * The relationship between the neighbor and the current device: + * - 0x00 = neighbor is the parent + * - 0x01 = neighbor is a child + * - 0x02 = neighbor is a sibling + * - 0x03 = none of the above + * - 0x04 = previous child + * - 0x05 = unauthenticated child + * - 0x06 = unauthorized child with relay allowed + * - 0x07 = neighbor is a lost child + * - 0x08 = neighbor is a child with address conflict + * - 0x09 = neighbor is a backbone mesh sibling + * + * This field SHALL be present in every neighbor table entry. + */ + relationship: number; + /** + * A value indicating if previous transmissions to the device were successful or not. + * Higher values indicate more failures. + * uint8_t + * + * This field SHALL be present in every neighbor table entry. + */ + transmitFailure: number; + /** + * The estimated link quality for RF transmissions from this device. + * See section 3.6.4.1 for a discussion of how this is calculated. + * uint8_t + * + * This field SHALL be present in every neighbor table entry. + */ + lqa: number; + /** + * The cost of an outgoing link as measured by the neighbor. + * A value of 0 indicates no outgoing cost is available. + * uint8_t + * + * This field is mandatory. + */ + outgoingCost: number; + /** + * The number of nwkLinkStatusPeriod intervals since a link status command was received. + * uint8_t + * + * This field is mandatory. + */ + age: number; + /** + * The time, in symbols, at which the last beacon frame was received from the neighbor. + * This value is equal to the timestamp taken when the beacon frame was received, as described in IEEE Std 802.15.4-2020 [B1]. + * 0x000000 – 0xffffff + * + * This field is optional. + */ + incomingBeaconTimestamp?: number; + /** + * The transmission time difference, in symbols, between the neighbor’s beacon and its parent’s beacon. + * This difference MAY be subtracted from the corresponding incoming beacon timestamp to calculate the beacon transmission time of the neighbor’s parent. + * 0x000000 – 0xffffff + * + * This field is optional. + */ + beaconTransmissionTimeOffset?: number; + /** This value indicates at least one keepalive has been received from the end device since the router has rebooted. */ + keepaliveReceived: boolean; + /** This is an index into the MAC Interface Table indicating what interface the neighbor or child is bound to. 0-31 */ + macInterfaceIndex: number; + /** The number of bytes transmitted via MAC unicast to the neighbor. This is an optional field. uint32_t */ + macUnicastBytesTransmitted?: number; + /** The number of bytes received via MAC unicast from this neighbor. This is an optional field. uint32_t */ + macUnicastBytesReceived?: number; + /** + * The number of nwkLinkStatusPeriod intervals, which elapsed since this router neighbor was added to the neighbor table. + * This value is only maintained on routers and the coordinator and is only valid for entries with a relationship of ‘parent’, ‘sibling’ or ‘backbone mesh sibling’. + * This is a saturating up-counter, which does not roll-over. + * uint16_t + */ + routerAge: number; + /** + * An indicator for how well this router neighbor is connected to other routers in its vicinity. + * Higher numbers indicate better connectivity. + * This metric takes the number of mesh links and their incoming and outgoing costs into account. + * This value is only maintained on routers and the coordinator and is only valid for entries with a relationship of ‘parent’, ‘sibling’ or ‘backbone mesh sibling’. + * 0x00-0xb6 + */ + routerConnectivity: number; + /** + * An indicator for how different the sibling router’s set of neighbors is compared to the local router’s set of neighbors. + * Higher numbers indicate a higher degree of diversity. + * This value is only maintained on routers and the coordinator and is only valid for entries with a relationship of ‘parent’, ‘sibling’ or ‘backbone mesh sibling’. + */ + routerNeighborSetDiversity: number; + /** + * A saturating counter, which is preloaded with nwkRouterAgeLimit when this neighbor table entry is created; + * incremented whenever this neighbor is used as a next hop for a data packet; and decremented unconditionally once every nwkLinkStatusPeriod. + * This value is only maintained on routers and the coordinator and is only valid for entries with a relationship of ‘parent’, ‘sibling’ or ‘backbone mesh sibling’. + * uint8_t + */ + routerOutboundActivity: number; + /** + * A saturating counter, which is preloaded with nwkRouterAgeLimit when this neighbor table entry is created; + * incremented whenever the local device is used by this neighbor as a next hop for a data packet; and decremented unconditionally once every nwkLinkStatus-Period. + * This value is only maintained on routers and the coordinator and is only valid for entries with a relationship of ‘parent’, ‘sibling’ or ‘backbone mesh sibling’. + * uint8_t + */ + routerInboundActivity: number; + /** + * If the local device is joined to the network this is a countdown timer indicating how long an “unauthorized child” neighbor is allowed to be kept in the neighbor table. + * If the timer reaches zero the entry SHALL be deleted. + * If the local device is an unauthorized child and not fully joined to the network, this is a timer indicating how long it will maintain its parent before giving up the join or rejoin. + * If the timer reaches zero then the device SHALL leave the network. + * uint8_t + */ + securityTimer: number; +}; + +/** + * TODO + */ +export type DeviceTableEntry = { + // address64: bigint; // mapped + address16: number; + rxOnWhenIdle: boolean; + authorized: boolean; +}; + +/** + * TODO + */ +export type RouteRecordTableEntry = { + /** The destination network address for this route record. uint16_t */ + // networkAddress: number; // mapped + /** The count of relay nodes from concentrator to the destination. uint16_t */ + // relayCount: number; // determined from path.length + /** The set of network addresses that represent the route in order from the concentrator to the destination. */ + path: number[]; +}; + +/** + * see 05-3474-23 #??? + * TODO + */ +export type SourceRouteTableEntry = { + relayIndex: number; + relayAddresses: number[]; + pathCost: number; +}; + +/** + * see 05-3474-23 Table 4-2 + * TODO + * This set contains the network keying material, which SHOULD be accessible to commissioning applications. + */ +export type NWKSecurityMaterialSet = undefined; + +/** + * see 05-3474-23 Table 2-24 + * TODO + * The binding table for this device. Binding provides a separation of concerns in the sense that applications MAY operate without having to manage recipient address information for the frames they emit. This information can be input at commissioning time without the main application on the device even being aware of it. + */ +export type APSBindingTable = { + destination: number; +}; + +/** + * see 05-3474-23 Table 4-35 + * A set of key-pair descriptors containing link keys shared with other devices. + */ +export type APSDeviceKeyPairSet = { + /** + * A set of feature flags pertaining to this security material or denoting the peer’s support for specific APS security features: + * - Bit #0: Frame Counter Synchronization Support When set to ‘1' the peer device supports APS frame counter synchronization; else, when set to '0’, + * the peer device does not support APS frame counter synchronization. + * - Bits #1..#7 are reserved and SHALL be set to '0' by implementations of the current Revision of this specification and ignored when processing. + * + * 0x00-0x01, default: 0x00 + */ + featuresCapabilities: number; + /** Identifies the address of the entity with which this key-pair is shared. */ + deviceAddress: bigint; + /** + * This indicates attributes about the key. + * - 0x00 = PROVISIONAL_KEY + * - 0x01 = UNVERIFIED_KEY + * - 0x02 = VERIFIED_KEY + */ + keyAttributes: number; + /** The actual value of the link key. */ + linkKey: Buffer; // not backed up in R23 + /** Outgoing frame counter for use with this link key. uint32_t */ + outgoingFrameCounter: number; + /** Incoming frame counter value corresponding to DeviceAddress. uint32_t */ + incomingFrameCounter: number; + /** + * The type of link key in use. This will determine the security policies associated with sending and receiving APS messages. + * - 0x00 = Unique Link Key + * - 0x01 = Global Link Key + * + * Default: 0x00 + */ + apsLinkKeyType: number; + /** + * - 0x00 = NO_AUTHENTICATION + * - 0x01 = INSTALL_CODE_KEY + * - 0x02 = ANONYMOUS_KEY_NEGOTIATION + * - 0x03 = KEY_NEGOTIATION_WITH_AUTHENTICATION + * + * Default: 0x00 + */ + initialJoinAuthentication: number; + /** The value of the selected TLV sent to the device. 0x00-0x08, default: 0x00 (`APS Request Key` method) */ + keyNegotiationMethod: number; + /** + * - 0x00 = NO_KEY_NEGOTIATION + * - 0x01 = START_KEY_NEGOTIATION + * - 0x02 = COMPLETE_KEY_NEGOTIATION + * + * default: 0x00 + */ + keyNegotiationState: number; // not backed up + /** + * A value that is used by both sides during dynamic key negotiation. + * An unset value means this key-pair entry was not dynamically negotiated. + * Any other value indicates the entry was dynamically negotiated. + */ + passphrase?: Buffer; // if supported + /** + * The timeout, in seconds, for the specified key. + * When this timeout expires, the key SHALL be marked EXPIRED_KEY in the KeyAttributes and the LinkKey value SHALL not be used for encryption of messages. + * A value of 0xFFFF for the Timeout mean the key never expires. + * + * default: 0xffff + */ + timeout: number; // not backed up + /** + * This indicates whether the particular KeyPair passphrase MAY be updated for the device. + * A passphrase update is normally only allowed shortly after joining. + * See section 4.7.2.1. + * + * default: true + */ + passphraseUpdateAllowed: boolean; // not backed up + /** + * Indicates whether the incoming frame counter value has been verified through a challenge response. + * + * default: false + */ + verifiedFrameCounter: boolean; + /** + * This indicates what Link Key update method was used after the device joined the network. + * - 0x00 = Not Updated + * - 0x01 = Key Request Method + * - 0x02 = Unauthenticated Key Negotiation + * - 0x03 = Authenticated Key Negotiation + * - 0x04 = Application Defined Certificate Based Mutual Authentication + */ + postJoinKeyUpdateMethod: number; + /** + * The key used to indicate a Trust Center Swap-out has occurred. + * This key SHALL always be set to a hash of the LinkKey element. + * If the LinkKey is updated, then this value MUST be updated as well. + * See section 4.7.4.1.2.4. + * If the entry in the apsDeviceKeyPairSet is an application link key (where local device and the partner are not Trust Centers), + * implementations MAY elide this element for that entry. + */ + trustCenterSwapOutLinkKey?: Buffer; + /** + * If set to TRUE, the device identified by DeviceAddress is a Zigbee Direct Virtual Device (ZVD). + * A Trust Center SHALL NOT send network keys to this device. + * + * default: false + */ + isVirtualDevice: boolean; +}; + +/** + * 05-3474-23 #2.5.5 + */ +export type ConfigurationAttributes = { + /** + * NOTE: Pre-encoded as "sendable" ZDO response (see endpoints.ts for more details): + */ + address: Buffer; + /** + * 05-3474-23 #2.3.2.3 + * The :Config_Node_Descriptor is either created when the application is first loaded or initialized with a commissioning tool prior to when the device begins operations in the network. + * It is used for service discovery to describe node features to external inquiring devices. + * + * NOTE: Pre-encoded as "sendable" ZDO response (see endpoints.ts for more details): + * - Byte 1: sequence number + * - Byte 2: status + * - Byte 3-4: 0x0000 (coordinator nwk addr) + */ + nodeDescriptor: Buffer; + /** + * 05-3474-23 #2.3.2.4 + * The :Config_Power_Descriptor is either created when the application is first loaded or initialized with a commissioning tool prior to when the device begins operations in the network. + * It is used for service discovery to describe node power features to external inquiring devices. + * + * NOTE: Pre-encoded as "sendable" ZDO response (see endpoints.ts for more details): + * - Byte 1: sequence number + * - Byte 2: status + * - Byte 3-4: 0x0000 (coordinator nwk addr) + */ + powerDescriptor: Buffer; + /** + * 05-3474-23 #2.3.2.5 + * The :Config_Simple_Descriptors are created when the application is first loaded and are treated as “read-only.” + * The Simple Descriptor are used for service discovery to describe interfacing features to external inquiring devices. + * + * NOTE: Pre-encoded as "sendable" ZDO response (see endpoints.ts for more details): + * - Byte 1: sequence number + * - Byte 2: status + * - Byte 3-4: 0x0000 (coordinator nwk addr) + */ + simpleDescriptors: Buffer; + /** + * NOTE: Pre-encoded as "sendable" ZDO response (see endpoints.ts for more details): + */ + activeEndpoints: Buffer; + /** + * The :Config_NWK_Scan_Attempts is employed within ZDO to call the NLME-NETWORK-AND-PARENTDISCOVERY.request primitive the indicated number of times (for routers and end devices). + * Integer value representing the number of scan attempts to make before the NWK layer decides which Zigbee coordinator or router to associate with (see section 2.5.4.5.1). + * This attribute has default value of 5 and valid values between 1 and 255. + */ + // nwkScanAttempts: number; + /** + * The Config_NWK_Time_btwn_Scans is employed within ZDO to provide a time duration between the NLMENETWORK-AND-PARENT-DISCOVERY.request attempts. + * Integer value representing the time duration (in OctetDurations) between each NWK discovery attempt described by :Config_NWK_Scan_Attempts (see section). + * This attribute has a default value of 0xc35 OctetDurations (100 milliseconds on 2.4GHz) and valid values between 1 and 0x1f3fe1 OctetDurations (65535 milliseconds on 2.4GHz). + */ + // nwkTimeBetweenScans: number; + /** + * The :Config_Max_Bind is a maximum number of supported Binding Table entries for this device. + */ + // maxBind: number; // optional + /** + * The default value for :Config_Permit_Join_Duration is 0x00, however, this value can be established differently according to the needs of the profile. + * Permit Join Duration value set by the NLME-PERMIT-JOINING. request primitive (see Chapter 3). + */ + // permitJoinDuration: number; // optional + /** + * This attribute is used only on the Trust Center and is used to set the level of security on the network. + * Security level of the network (see Chapter 3). + */ + // nwkSecurityLevel: number; // optional + /** + * This attribute is used only on the Trust Center and is used to determine if network layer security SHALL be applied to all frames in the network. + * If all network frames SHOULD be secured (see Chapter 3). + */ + // nwkSecureAllFrames: number; // optional + /** + * 05-3474-23 Table 2-134 + * The value for this configuration attribute is established in the Stack Profile. + */ + // nwkBroadcastDeliveryTime: number; // optional + /** + * 05-3474-23 Table 2-134 + * The value for this configuration attribute is established in the Stack Profile. + * This attribute is mandatory for the Zigbee coordinator and Zigbee routers and not used for Zigbee End Devices. + */ + // nwkTransactionPersistenceTime: number; // optional + // nwkIndirectPollRate: number; // ZED-only + /** + * The value for this configuration attribute is established by the stack profile in use on the device. + * Note that for some stack profiles, the maximum associations MAY have a dimension which provides for separate maximums for router associations and end device associations. + * Sets the maximum allowed associations, either of routers, end devices, or both, to a parent router or coordinator. + */ + // maxAssoc: number; // optional + /** + * 05-3474-23 #3.2.2.16 + * :Config_NWK_Join_Direct_Addrs permits the Zigbee Coordinator or Router to be pre-configured with a list of addresses to be direct joined. + * Consists of the following fields: + * - DeviceAddress - 64-bit IEEE address for the device to be direct joined. + * - CapabilityInformation - Operating capabilities of the device to be direct joined. + * - Link Key - If security is enabled, link key for use in the key-pair descriptor for this new device (see Table 4-36). + */ + // nwkJoinDirectAddrs: {device64: bigint; capabilities: number; linkKey: Buffer}[]; // optional + // parentLinkRetryThreshold: number; // ZED-only + // rejoinInterval: number; // ZED-only + // maxRejoinInterval: number; // ZED-only +}; + +/** + * R23 changes the "recommended" way to backup by introducing hash-based keys restoration. + * Devices pre-R23 require backing up the actual keys. + */ +export type Backup = { + nwkPANId: bigint; + nwkExtendedPANId: bigint; + nwkIEEEAddress: bigint; + nwkChannel: number; + nwkActiveKeySeqNum: number; + nwkSecurityMaterialSet: NWKSecurityMaterialSet; + apsBindingTable: Map; + apsDeviceKeyPairSet: Map>; + trustCenterPolicies: TrustCenterPolicies; +}; + +// const SPINEL_FRAME_MAX_SIZE = 1300; +// const SPINEL_FRAME_MAX_COMMAND_HEADER_SIZE = 4; +// const SPINEL_FRAME_MAX_COMMAND_PAYLOAD_SIZE = SPINEL_FRAME_MAX_SIZE - SPINEL_FRAME_MAX_COMMAND_HEADER_SIZE; +// const SPINEL_ENCRYPTER_EXTRA_DATA_SIZE = 0; +// const SPINEL_FRAME_BUFFER_SIZE = SPINEL_FRAME_MAX_SIZE + SPINEL_ENCRYPTER_EXTRA_DATA_SIZE; + +const CONFIG_COMMAND_TIMEOUT = 10000; +const CONFIG_TID_MASK = 0x0e; +const CONFIG_HIGHWATER_MARK = HDLC_TX_CHUNK_SIZE * 4; +/** The number of OctetDurations until a route discovery expires. */ +// const CONFIG_NWK_ROUTE_DISCOVERY_TIME = 0x4c4b4; // 0x2710 msec on 2.4GHz +/** The maximum depth of the network (number of hops) used for various calculations of network timing and limitations. */ +const CONFIG_NWK_MAX_DEPTH = 15; +const CONFIG_NWK_MAX_HOPS = CONFIG_NWK_MAX_DEPTH * 2; +/** The number of network layer retries on unicast messages that are attempted before reporting the result to the higher layer. */ +// const CONFIG_NWK_UNICAST_RETRIES = 3; +/** The delay between network layer retries. (ms) */ +// const CONFIG_NWK_UNICAST_RETRY_DELAY = 50; +/** The total delivery time for a broadcast transmission to be delivered to all RxOnWhenIdle=TRUE devices in the network. (sec) */ +// const CONFIG_NWK_BCAST_DELIVERY_TIME = 9; +/** The time between link status command frames (msec) */ +// const CONFIG_NWK_LINK_STATUS_PERIOD = 15000; +/** Avoid synchronization with other nodes by randomizing `CONFIG_NWK_LINK_STATUS_PERIOD` with this (msec) */ +// const CONFIG_NWK_LINK_STATUS_JITTER = 1000; +/** The number of missed link status command frames before resetting the link costs to zero. */ +// const CONFIG_NWK_ROUTER_AGE_LIMIT = 3; +/** This is an index into Table 3-54. It indicates the default timeout in minutes for any end device that does not negotiate a different timeout value. */ +// const CONFIG_NWK_END_DEVICE_TIMEOUT_DEFAULT = 8; + +export class OTRCPDriver extends EventEmitter { + public readonly writer: OTRCPWriter; + public readonly parser: OTRCPParser; + public readonly streamRawConfig: StreamRawConfig; + public readonly savePath: string; + private emitMACFrames: boolean; + + /** + * Transaction ID used in Spinel frame + * + * NOTE: 0 is used for "no response expected/needed" (e.g. unsolicited update commands from NCP to host) + */ + private spinelTID: number; + /** Sequence number used in outgoing MAC frames */ + private macSeqNum: number; + /** Sequence number used in outgoing NWK frames */ + private nwkSeqNum: number; + /** Counter used in outgoing APS frames */ + private apsCounter: number; + /** Sequence number used in outgoing ZDO frames */ + private zdoSeqNum: number; + /** Whether source routing is currently enabled */ + private sourceRouting: boolean; + /** + * 8-bit sequence number for route requests. Incremented by 1 every time the NWK layer on a particular device issues a route request. + */ + private routeRequestId: number; + + /** If defined, indicates we're waiting for the property with the specific payload to come in */ + private resetWaiter: { timer: NodeJS.Timeout; resolve: (frame: SpinelFrame) => void } | undefined; + /** TID currently being awaited */ + private tidWaiters: Map< + number, + { + timer: NodeJS.Timeout; + resolve: (frame: SpinelFrame) => void; + reject: (error: Error) => void; + } + >; + + private networkUp: boolean; + + public readonly indirectTransmissions: Map Promise; timestamp: number }[]>; + + //---- Trust Center (see 05-3474-R #4.7.1) + + private readonly trustCenterPolicies: TrustCenterPolicies; + private macAssociationPermit: boolean; + private permitJoinTimeout: NodeJS.Timeout | undefined; + + //---- NWK + + public readonly netParams: NetworkParameters; + /** pre-computed hash of default TC link key for VERIFY_KEY. set by `loadState` */ + private tcVerifyKeyHash!: Buffer; + /** mapping by network16 */ + public readonly neighborTable: Map; + /** Master table of all known devices on the network. mapping by network64 */ + public readonly deviceTable: Map; + /** Lookup synced with deviceTable, maps network address to IEEE address */ + public readonly address16ToAddress64: Map; + /** mapping by network16 */ + public readonly sourceRouteTable: Map; + // TODO: possibility of a route/sourceRoute blacklist? + /** TOOD */ + public readonly routeRecordTable: Map; + + //---- APS + + /** mapping by network16 */ + public readonly apsDeviceKeyPairSet: Map; + /** mapping by network16 */ + public readonly apsBindingTable: Map; + + //---- Attribute + + /** Several attributes are set by `loadState` */ + public readonly configAttributes: ConfigurationAttributes; + + constructor(streamRawConfig: StreamRawConfig, netParams: NetworkParameters, saveDir: string, emitMACFrames = false) { + super(); + + if (!existsSync(saveDir)) { + mkdirSync(saveDir); + } + + this.savePath = join(saveDir, "zoh.save"); + this.emitMACFrames = emitMACFrames; + this.streamRawConfig = streamRawConfig; + this.writer = new OTRCPWriter({ highWaterMark: CONFIG_HIGHWATER_MARK }); + this.parser = new OTRCPParser({ readableHighWaterMark: CONFIG_HIGHWATER_MARK }); + + this.spinelTID = -1; // start at 0 but effectively 1 returned by first nextTID() call + this.resetWaiter = undefined; + this.tidWaiters = new Map(); + + this.macSeqNum = 0; // start at 1 + this.nwkSeqNum = 0; // start at 1 + this.apsCounter = 0; // start at 1 + this.zdoSeqNum = 0; // start at 1 + this.sourceRouting = false; // TODO: true + this.routeRequestId = 0; // start at 1 + + this.networkUp = false; + this.indirectTransmissions = new Map(); + + //---- Trust Center + this.trustCenterPolicies = { + allowJoins: false, + installCode: InstallCodePolicy.NOT_REQUIRED, + allowRejoinsWithWellKnownKey: true, + allowTCKeyRequest: TrustCenterKeyRequestPolicy.ALLOWED, + networkKeyUpdatePeriod: 0, // disable + networkKeyUpdateMethod: NetworkKeyUpdateMethod.BROADCAST, + allowAppKeyRequest: ApplicationKeyRequestPolicy.DISALLOWED, + // appKeyRequestList: undefined, + allowRemoteTCPolicyChange: false, + allowVirtualDevices: false, + }; + this.macAssociationPermit = false; + + //---- NWK + this.netParams = netParams; + this.tcVerifyKeyHash = Buffer.alloc(0); // set by `loadState` + + this.neighborTable = new Map(); + this.deviceTable = new Map(); + this.address16ToAddress64 = new Map(); + this.sourceRouteTable = new Map(); + this.routeRecordTable = new Map(); + + //---- APS + this.apsDeviceKeyPairSet = new Map(); + this.apsBindingTable = new Map(); + + //---- Attributes + this.configAttributes = { + address: Buffer.alloc(0), // set by `loadState` + nodeDescriptor: Buffer.alloc(0), // set by `loadState` + powerDescriptor: Buffer.alloc(0), // set by `loadState` + simpleDescriptors: Buffer.alloc(0), // set by `loadState` + activeEndpoints: Buffer.alloc(0), // set by `loadState` + }; + } + + // #region TIDs/counters + + /** + * @returns increased TID offsetted by +1. [1-14] range for the "actually-used" value (0 is reserved) + */ + private nextSpinelTID(): number { + this.spinelTID = (this.spinelTID + 1) % CONFIG_TID_MASK; + + return this.spinelTID + 1; + } + + private nextMACSeqNum(): number { + this.macSeqNum = (this.macSeqNum + 1) & 0xff; + + return this.macSeqNum; + } + + private nextNWKSeqNum(): number { + this.nwkSeqNum = (this.nwkSeqNum + 1) & 0xff; + + return this.nwkSeqNum; + } + + private nextAPSCounter(): number { + this.apsCounter = (this.apsCounter + 1) & 0xff; + + return this.apsCounter; + } + + private nextZDOSeqNum(): number { + this.zdoSeqNum = (this.zdoSeqNum + 1) & 0xff; + + return this.zdoSeqNum; + } + + private nextTCKeyFrameCounter(): number { + this.netParams.tcKeyFrameCounter = (this.netParams.tcKeyFrameCounter + 1) & 0xffffffff; + + return this.netParams.tcKeyFrameCounter; + } + + private nextNWKKeyFrameCounter(): number { + this.netParams.networkKeyFrameCounter = (this.netParams.networkKeyFrameCounter + 1) & 0xffffffff; + + return this.netParams.networkKeyFrameCounter; + } + + private nextRouteRequestId(): number { + this.routeRequestId = (this.routeRequestId + 1) & 0xff; + + return this.routeRequestId; + } + + private decrementRadius(radius: number): number { + // XXX: init at 29 when passed CONFIG_NWK_MAX_HOPS? + return radius - 1 || 1; + } + + // #endregion + + /** + * @see https://datatracker.ietf.org/doc/html/draft-rquattle-spinel-unified#appendix-C.1 + */ + public async start(): Promise { + logger.info("======== Driver starting ========", NS); + await this.loadState(); + + // flush + this.writer.writeBuffer(Buffer.from([HdlcReservedByte.FLAG])); + + // check the protocol version to see if it is supported + let response = await this.getProperty(SpinelPropertyId.PROTOCOL_VERSION); + const [major, minor] = readPropertyii(SpinelPropertyId.PROTOCOL_VERSION, response.payload); + + logger.info(`Protocol version: ${major}.${minor}`, NS); + + // check the NCP version to see if a firmware update may be necessary + response = await this.getProperty(SpinelPropertyId.NCP_VERSION); + // recommended format: STACK-NAME/STACK-VERSION[BUILD_INFO][; OTHER_INFO]; BUILD_DATE_AND_TIME + const ncpVersion = readPropertyU(SpinelPropertyId.NCP_VERSION, response.payload); + + logger.info(`NCP version: ${ncpVersion}`, NS); + + // check interface type to make sure that it is what we expect + response = await this.getProperty(SpinelPropertyId.INTERFACE_TYPE); + const interfaceType = readPropertyi(SpinelPropertyId.INTERFACE_TYPE, response.payload); + + logger.info(`Interface type: ${interfaceType}`, NS); + + response = await this.getProperty(SpinelPropertyId.RCP_API_VERSION); + const rcpAPIVersion = readPropertyi(SpinelPropertyId.RCP_API_VERSION, response.payload); + + logger.info(`RCP API version: ${rcpAPIVersion}`, NS); + + response = await this.getProperty(SpinelPropertyId.RCP_MIN_HOST_API_VERSION); + const rcpMinHostAPIVersion = readPropertyi(SpinelPropertyId.RCP_MIN_HOST_API_VERSION, response.payload); + + logger.info(`RCP min host API version: ${rcpMinHostAPIVersion}`, NS); + + await this.sendCommand(SpinelCommandId.RESET, Buffer.from([SpinelResetReason.STACK]), false); + + await new Promise((resolve, reject) => { + this.resetWaiter = { + timer: setTimeout(() => reject(new Error(`Timeout after ${CONFIG_COMMAND_TIMEOUT * 3}`)), CONFIG_COMMAND_TIMEOUT * 3), + resolve, + }; + }); + + logger.info("======== Driver started ========", NS); + } + + public async stop(): Promise { + logger.info("======== Driver stopping ========", NS); + + this.disallowJoins(); + + // pre-emptive + this.networkUp = false; + + for (const [, waiter] of this.tidWaiters) { + clearTimeout(waiter.timer); + // waiter.reject(new Error("Driver stopping")); + } + + this.tidWaiters.clear(); + + // TODO: proper spinel/radio shutdown? + await this.setProperty(writePropertyb(SpinelPropertyId.MAC_RAW_STREAM_ENABLED, false)); + await this.setProperty(writePropertyb(SpinelPropertyId.PHY_ENABLED, false)); + + await this.saveState(); + + logger.info("======== Driver stopped ========", NS); + } + + // #region HDLC/Spinel + + public async onFrame(buffer: Buffer): Promise { + const hdlcFrame = decodeHdlcFrame(buffer); + logger.debug(() => `<--- HDLC[length=${hdlcFrame.length}]`, NS); + const spinelFrame = decodeSpinelFrame(hdlcFrame); + + if (spinelFrame.header.flg !== SPINEL_HEADER_FLG_SPINEL) { + // non-Spinel frame (likely BLE HCI) + return; + } + + logger.debug(() => `<--- SPINEL[tid=${spinelFrame.header.tid} cmdId=${spinelFrame.commandId} length=${spinelFrame.payload.byteLength}]`, NS); + + // resolve waiter if any (never for tid===0 since unsolicited frames) + const waiter = spinelFrame.header.tid > 0 ? this.tidWaiters.get(spinelFrame.header.tid) : undefined; + let status: SpinelStatus = SpinelStatus.OK; + + if (waiter) { + clearTimeout(waiter.timer); + } + + if (spinelFrame.commandId === SpinelCommandId.PROP_VALUE_IS) { + const [propId, pOffset] = getPackedUInt(spinelFrame.payload, 0); + + switch (propId) { + case SpinelPropertyId.STREAM_RAW: { + const [macData, metadata] = readPropertyStreamRaw(spinelFrame.payload, pOffset); + + await this.onStreamRawFrame(macData, metadata); + break; + } + + case SpinelPropertyId.LAST_STATUS: { + [status] = getPackedUInt(spinelFrame.payload, pOffset); + + logger.debug(() => `<--- SPINEL LAST_STATUS[${SpinelStatus[status]}]`, NS); + + // TODO: getting RESET_POWER_ON after RESET instead of RESET_SOFTWARE?? + if (this.resetWaiter && (status === SpinelStatus.RESET_SOFTWARE || status === SpinelStatus.RESET_POWER_ON)) { + clearTimeout(this.resetWaiter.timer); + this.resetWaiter.resolve(spinelFrame); + + this.resetWaiter = undefined; + } + + break; + } + } + } + + if (waiter) { + if (status === SpinelStatus.OK) { + waiter.resolve(spinelFrame); + } else { + waiter.reject(new Error(`Failed with status=${SpinelStatus[status]}`)); + } + } + + this.tidWaiters.delete(spinelFrame.header.tid); + } + + /** + * Logic optimizes code paths to try to avoid more parsing when frames will eventually get ignored by detecting as early as possible. + */ + public async onStreamRawFrame(payload: Buffer, metadata: SpinelStreamRawMetadata | undefined): Promise { + // discard MAC frames before network is started + if (!this.networkUp) { + return; + } + + if (this.emitMACFrames) { + setImmediate(() => { + this.emit("MAC_FRAME", payload, metadata?.rssi); + }); + } + + try { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(payload, 0); + + // TODO: process BEACON for PAN ID conflict detection? + if (macFCF.frameType !== MACFrameType.CMD && macFCF.frameType !== MACFrameType.DATA) { + logger.debug(() => "<-~- MAC Ignoring frame with type not CMD/DATA", NS); + return; + } + + const [macHeader, macHOutOffset] = decodeMACHeader(payload, macFCFOutOffset, macFCF); + + if (metadata) { + logger.debug( + () => `<--- SPINEL STREAM_RAW METADATA[rssi=${metadata.rssi} noiseFloor=${metadata.noiseFloor} flags=${metadata.flags}]`, + NS, + ); + } + + const macPayload = decodeMACPayload(payload, macHOutOffset, macFCF, macHeader); + + if (macFCF.frameType === MACFrameType.CMD) { + await this.processMACCommandFrame(macPayload, macHeader); + + // done + return; + } + + if (macHeader.destinationPANId !== ZigbeeMACConsts.BCAST_PAN && macHeader.destinationPANId !== this.netParams.panId) { + logger.debug(() => `<-~- MAC Ignoring frame with mismatching PAN Id: ${macHeader.destinationPANId}`, NS); + return; + } + + if ( + macFCF.destAddrMode === MACFrameAddressMode.SHORT && + macHeader.destination16! !== ZigbeeMACConsts.BCAST_ADDR && + macHeader.destination16! !== ZigbeeConsts.COORDINATOR_ADDRESS + ) { + logger.debug(`<-~- MAC Ignoring frame intended for device ${macHeader.destination16}`, NS); + return; + } + + if (macPayload.byteLength > 0) { + const protocolVersion = (macPayload.readUInt8(0) & ZigbeeNWKConsts.FCF_VERSION) >> 2; + + if (protocolVersion === ZigbeeNWKConsts.VERSION_GREEN_POWER) { + if ( + (macFCF.destAddrMode === MACFrameAddressMode.SHORT && macHeader.destination16 === ZigbeeMACConsts.BCAST_ADDR) || + macFCF.destAddrMode === MACFrameAddressMode.EXT + ) { + const [nwkGPFCF, nwkGPFCFOutOffset] = decodeZigbeeNWKGPFrameControl(macPayload, 0); + const [nwkGPHeader, nwkGPHOutOffset] = decodeZigbeeNWKGPHeader(macPayload, nwkGPFCFOutOffset, nwkGPFCF); + + if (nwkGPHeader.sourceId === undefined) { + logger.debug("<-~- NWKGP Ignoring frame without sourceId", NS); + return; + } + + if (nwkGPHeader.frameControl.frameType === ZigbeeNWKGPFrameType.DATA) { + const nwkGPPayload = decodeZigbeeNWKGPPayload( + macPayload, + nwkGPHOutOffset, + this.netParams.networkKey, + macHeader.source64, + nwkGPFCF, + nwkGPHeader, + ); + + this.processZigbeeNWKGPCommandFrame(nwkGPPayload, macHeader, nwkGPHeader, metadata?.rssi ?? 0); + } else { + logger.debug(`<-~- NWKGP Ignoring frame with type ${nwkGPHeader.frameControl.frameType}`, NS); + return; + } + } else { + logger.debug(`<-x- NWKGP Invalid frame addressing ${macFCF.destAddrMode} (${macHeader.destination16})`, NS); + return; + } + } else { + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload( + macPayload, + nwkHOutOffset, + undefined, // use pre-hashed this.netParams.networkKey, + /* nwkHeader.frameControl.extendedSource ? nwkHeader.source64 : this.address16ToAddress64.get(nwkHeader.source16!) */ + nwkHeader.source64 ?? this.address16ToAddress64.get(nwkHeader.source16!), + nwkFCF, + nwkHeader, + ); + + if (nwkFCF.frameType === ZigbeeNWKFrameType.DATA) { + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + + if (apsHeader.frameControl.ackRequest) { + await this.onZigbeeAPSACKRequest(macHeader, nwkHeader, apsHeader); + } + + const apsPayload = decodeZigbeeAPSPayload( + nwkPayload, + apsHOutOffset, + undefined, // use pre-hashed this.netParams.tcKey, + /* nwkHeader.frameControl.extendedSource ? nwkHeader.source64 : this.address16ToAddress64.get(nwkHeader.source16!) */ + nwkHeader.source64 ?? this.address16ToAddress64.get(nwkHeader.source16!), + apsFCF, + apsHeader, + ); + + await this.onZigbeeAPSFrame(apsPayload, macHeader, nwkHeader, apsHeader, metadata?.rssi ?? 0); + } else if (nwkFCF.frameType === ZigbeeNWKFrameType.CMD) { + await this.processZigbeeNWKCommandFrame(nwkPayload, macHeader, nwkHeader); + } else if (nwkFCF.frameType === ZigbeeNWKFrameType.INTERPAN) { + throw new Error("INTERPAN not supported"); + } + } + } + } catch (error) { + // TODO log or throw depending on error + logger.error((error as Error).stack!, NS); + } + } + + public sendFrame(hdlcFrame: HdlcFrame): void { + // only send what is recorded as "data" (by length) + this.writer.writeBuffer(hdlcFrame.data.subarray(0, hdlcFrame.length)); + } + + public async sendCommand(commandId: SpinelCommandId, buffer: Buffer, waitForResponse: false): Promise; + public async sendCommand(commandId: SpinelCommandId, buffer: Buffer, waitForResponse: true, timeout: number): Promise; + public async sendCommand(commandId: SpinelCommandId, buffer: Buffer, waitForResponse = true, timeout = 10000): Promise { + const tid = this.nextSpinelTID(); + logger.debug(() => `---> SPINEL[tid=${tid} cmdId=${commandId} length=${buffer.byteLength} wait=${waitForResponse} timeout=${timeout}]`, NS); + const spinelFrame = { + header: { + tid, + nli: 0, + flg: SPINEL_HEADER_FLG_SPINEL, + }, + commandId, + payload: buffer, + }; + const hdlcFrame = encodeSpinelFrame(spinelFrame); + + this.sendFrame(hdlcFrame); + + if (waitForResponse) { + return await this.waitForTID(spinelFrame.header.tid, timeout); + } + } + + public async waitForTID(tid: number, timeout: number): Promise { + return await new Promise((resolve, reject) => { + // TODO reject if tid already present? (shouldn't happen as long as concurrency is fine...) + this.tidWaiters.set(tid, { + timer: setTimeout(() => reject(new Error(`-x-> SPINEL[tid=${tid}] Timeout after ${timeout}ms`)), timeout), + resolve, + reject, + }); + }); + } + + public async getProperty(propertyId: SpinelPropertyId): ReturnType { + const [data] = writePropertyId(propertyId, 0); + + return await this.sendCommand(SpinelCommandId.PROP_VALUE_GET, data, true, CONFIG_COMMAND_TIMEOUT); + } + + public async setProperty(payload: Buffer): Promise<[respPropertyId: SpinelPropertyId, data: Buffer]> { + const response = await this.sendCommand(SpinelCommandId.PROP_VALUE_SET, payload, true, CONFIG_COMMAND_TIMEOUT); + const [respPropertyId, outOffset] = getPackedUInt(response.payload, 0); + const data = response.payload.subarray(outOffset); + + return [respPropertyId, data]; + } + + // #endregion + + // #region MAC Layer + + public async sendMACFrame(seqNum: number, payload: Buffer, dest16: number | undefined, dest64: bigint | undefined): Promise { + const func = async (): Promise => { + try { + logger.debug(() => `===> MAC[seqNum=${seqNum} dest16=${dest16} dest64=${dest64}]`, NS); + + if (this.emitMACFrames) { + setImmediate(() => { + this.emit("MAC_FRAME", payload); + }); + } + + // status is checked in `onFrame` and rejects if not OK, so, avoid parsing twice + await this.setProperty(writePropertyStreamRaw(payload, this.streamRawConfig)); + + logger.debug(() => `<=== MAC[seqNum=${seqNum} dest16=${dest16} dest64=${dest64}]`, NS); + } catch (error) { + logger.error(`=x=> MAC[seqNum=${seqNum} dest16=${dest16} dest64=${dest64}] ${(error as Error).message}`, NS); + } + }; + + // TODO: optimize (not needed for non-neighbor of coordinator, etc.) + if (dest16 !== undefined || dest64 !== undefined) { + if (dest64 === undefined && dest16 !== undefined) { + dest64 = this.address16ToAddress64.get(dest16); + } + + if (dest64 === undefined) { + // if can't determine radio state, just send the packet + await func(); + } else { + const addrTX = this.indirectTransmissions.get(dest64); + + if (addrTX) { + addrTX.push({ + func, + timestamp: Date.now(), + }); + + logger.debug( + () => `=|=> MAC[seqNum=${seqNum} dest16=${dest16} dest64=${dest64}] set for indirect transmission (count: ${addrTX.length})`, + NS, + ); + } else { + // RX on when idle + await func(); + } + } + } else { + // no dest info, just send the packet + await func(); + } + } + + public async sendMACCommand( + cmdId: MACCommandId, + dest16: number | undefined, + dest64: bigint | undefined, + extSource: boolean, + payload: Buffer, + ): Promise { + const macSeqNum = this.nextMACSeqNum(); + + logger.debug(() => `===> MAC CMD[seqNum=${macSeqNum} cmdId=${cmdId} dest16=${dest16} dest64=${dest64} extSource=${extSource}]`, NS); + + const macFrame = encodeMACFrame( + { + frameControl: { + frameType: MACFrameType.CMD, + securityEnabled: false, + framePending: false, + ackRequest: dest16 !== ZigbeeMACConsts.BCAST_ADDR, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: dest64 !== undefined ? MACFrameAddressMode.EXT : MACFrameAddressMode.SHORT, + frameVersion: MACFrameVersion.v2003, + sourceAddrMode: extSource ? MACFrameAddressMode.EXT : MACFrameAddressMode.SHORT, + }, + sequenceNumber: macSeqNum, + destinationPANId: this.netParams.panId, + destination16: dest16, + destination64: dest64, + // sourcePANId: undefined, // panIdCompression=true + source16: ZigbeeConsts.COORDINATOR_ADDRESS, + source64: this.netParams.eui64, + commandId: cmdId, + fcs: 0, + }, + payload, + ); + + await this.sendMACFrame(macSeqNum, macFrame, dest16, dest64); + } + + public async processMACCommandFrame(data: Buffer, macHeader: MACHeader): Promise { + let offset = 0; + + logger.debug(() => `<=== MAC CMD[cmdId=${macHeader.commandId} macSource=${macHeader.source16}:${macHeader.source64}]`, NS); + + switch (macHeader.commandId!) { + case MACCommandId.ASSOC_REQ: { + offset = await this.processMACAssocReq(data, offset, macHeader); + break; + } + case MACCommandId.ASSOC_RSP: { + offset = this.processMACAssocRsp(data, offset, macHeader); + break; + } + case MACCommandId.BEACON_REQ: { + offset = await this.processMACBeaconReq(data, offset, macHeader); + break; + } + case MACCommandId.DATA_RQ: { + offset = await this.processMACDataReq(data, offset, macHeader); + break; + } + // TODO: other cases? + // DISASSOC_NOTIFY + // PANID_CONFLICT + // ORPHAN_NOTIFY + // COORD_REALIGN + // GTS_REQ + default: { + logger.error(`<=x= MAC CMD Unsupported ${macHeader.commandId}.`, NS); + return; + } + } + + // excess data in packet + // if (offset < data.byteLength) { + // logger.debug(() => `<=== MAC CMD contained more data: ${data.toString('hex')}`, NS); + // } + } + + public async processMACAssocReq(data: Buffer, offset: number, macHeader: MACHeader): Promise { + const capabilities = data.readUInt8(offset); + offset += 1; + + logger.debug(() => `<=== MAC ASSOC_REQ[capabilities=${capabilities}]`, NS); + + const [status, newNetwork16] = await this.associate(undefined, macHeader.source64!, 0x00 /* initial join */, capabilities); + + await this.sendMACAssocRsp(macHeader.source64!, newNetwork16, status); + + if (status === MACAssociationStatus.SUCCESS) { + // TODO: proper routed `macDest16` + await this.sendZigbeeAPSTransportKeyNWK( + newNetwork16, + newNetwork16, + this.netParams.networkKey, + this.netParams.networkKeySequenceNumber, + macHeader.source64!, + ); + } + + return offset; + } + + public processMACAssocRsp(data: Buffer, offset: number, _macHeader: MACHeader): number { + const address = data.readUInt16LE(offset); + offset += 2; + const status = data.readUInt8(offset); + offset += 1; + + logger.debug(() => `<=== MAC ASSOC_RSP[address=${address} status=${MACAssociationStatus[status]}]`, NS); + + return offset; + } + + public async sendMACAssocRsp(dest64: bigint, newNetwork16: number, status: MACAssociationStatus | number): Promise { + logger.debug(() => `===> MAC ASSOC_RSP[dest64=${dest64} newNetwork16=${newNetwork16} status=${status}]`, NS); + + const finalPayload = Buffer.alloc(3); + let offset = 0; + finalPayload.writeUInt16LE(newNetwork16, offset); + offset += 2; + finalPayload.writeUInt8(status, offset); + offset += 1; + + await this.sendMACCommand( + MACCommandId.ASSOC_RSP, + undefined, // dest16 + dest64, // dest64 + true, // sourceExt + finalPayload, + ); + } + + public async processMACBeaconReq(_data: Buffer, offset: number, _macHeader: MACHeader): Promise { + logger.debug(() => "<=== MAC BEACON_REQ[]", NS); + + const macSeqNum = this.nextMACSeqNum(); + + logger.debug(() => `===> MAC BEACON[seqNum=${macSeqNum}]`, NS); + + const macFrame = encodeMACFrame( + { + frameControl: { + frameType: MACFrameType.BEACON, + securityEnabled: false, + framePending: false, + ackRequest: false, + panIdCompression: false, + seqNumSuppress: false, + iePresent: false, + destAddrMode: MACFrameAddressMode.NONE, + frameVersion: MACFrameVersion.v2003, + sourceAddrMode: MACFrameAddressMode.SHORT, + }, + sequenceNumber: macSeqNum, + sourcePANId: this.netParams.panId, + source16: ZigbeeConsts.COORDINATOR_ADDRESS, + superframeSpec: { + beaconOrder: 0x0f, // value from spec + superframeOrder: 0x0f, // value from spec + finalCAPSlot: 0x0f, // XXX: value from sniff, matches above... + batteryExtension: false, + panCoordinator: true, + associationPermit: this.macAssociationPermit, + }, + gtsInfo: { permit: false }, + pendAddr: {}, + fcs: 0, + }, + encodeMACZigbeeBeacon({ + protocolId: ZigbeeMACConsts.ZIGBEE_BEACON_PROTOCOL_ID, + profile: 0x2, // ZigBee PRO + version: ZigbeeNWKConsts.VERSION_2007, + routerCapacity: true, + deviceDepth: 0, // coordinator + endDeviceCapacity: true, + extendedPANId: this.netParams.extendedPANId, + txOffset: 0xffffff, // TODO: value from sniffed frames + updateId: 0, + }), + ); + + await this.sendMACFrame(macSeqNum, macFrame, undefined, undefined); + + return offset; + } + + public async processMACDataReq(_data: Buffer, offset: number, macHeader: MACHeader): Promise { + logger.debug(() => "<=== MAC DATA_RQ[]", NS); + + let addr = macHeader.source64; + + if (addr === undefined && macHeader.source16 !== undefined) { + addr = this.address16ToAddress64.get(macHeader.source16); + } + + if (addr !== undefined) { + const addrTXs = this.indirectTransmissions.get(addr); + + if (addrTXs !== undefined) { + let tx = addrTXs.shift(); + + // deal with expired tx by looking for first that isn't + do { + if (tx !== undefined && tx.timestamp + ZigbeeConsts.MAC_INDIRECT_TRANSMISSION_TIMEOUT > Date.now()) { + await tx.func(); + break; + } + + tx = addrTXs.shift(); + } while (tx !== undefined); + } + } + + return offset; + } + + // #endregion + + // #region Zigbee NWK layer + + /** + * 05-3474-23 #3.6.1.10 + */ + public assignNetworkAddress(): number { + let newNetworkAddress = 0xffff; + let unique = false; + + do { + // maximum exclusive, minimum inclusive + newNetworkAddress = Math.floor(Math.random() * (ZigbeeConsts.BCAST_MIN - 0x0001) + 0x0001); + unique = this.address16ToAddress64.get(newNetworkAddress) === undefined; + } while (!unique); + + return newNetworkAddress; + } + + /** + * @param cmdId + * @param finalPayload expected to contain the full payload (including cmdId) + * @param macDest16 + * @param nwkSource16 + * @param nwkDest16 + * @param nwkDest64 + * @param nwkRadius + */ + public async sendZigbeeNWKCommand( + cmdId: ZigbeeNWKCommandId, + finalPayload: Buffer, + macDest16: number, + nwkSecurity: boolean, + nwkSource16: number, + nwkDest16: number, + nwkDest64: bigint | undefined, + nwkRadius: number, + ): Promise { + let relayIndex: number | undefined; + let relayAddresses: number[] | undefined; + let nwkSecurityHeader: ZigbeeSecurityHeader | undefined; + + if (this.sourceRouting) { + // TODO + relayIndex = 0; + relayAddresses = []; + } + + if (nwkSecurity) { + nwkSecurityHeader = { + control: { + level: ZigbeeSecurityLevel.NONE, + keyId: ZigbeeKeyType.NWK, + nonce: true, + }, + frameCounter: this.nextNWKKeyFrameCounter(), + source64: this.netParams.eui64, + keySeqNum: this.netParams.networkKeySequenceNumber, + micLen: 4, + }; + } + + const nwkSeqNum = this.nextNWKSeqNum(); + const macSeqNum = this.nextMACSeqNum(); + + logger.debug( + () => + `===> NWK CMD[seqNum=(${nwkSeqNum}/${macSeqNum}) cmdId=${cmdId} macDest16=${macDest16} nwkSource16=${nwkSource16} nwkDest16=${nwkDest16} nwkDest64=${nwkDest64} nwkRadius=${nwkRadius}]`, + NS, + ); + + const source64 = nwkSource16 === ZigbeeConsts.COORDINATOR_ADDRESS ? this.netParams.eui64 : this.address16ToAddress64.get(nwkSource16); + const nwkFrame = encodeZigbeeNWKFrame( + { + frameControl: { + frameType: ZigbeeNWKFrameType.CMD, + protocolVersion: ZigbeeNWKConsts.VERSION_2007, + discoverRoute: ZigbeeNWKRouteDiscovery.SUPPRESS, + multicast: false, + security: nwkSecurity, + sourceRoute: this.sourceRouting, + extendedDestination: nwkDest64 !== undefined, + extendedSource: source64 !== undefined, + endDeviceInitiator: false, + }, + destination16: nwkDest16, + destination64: nwkDest64, + source16: nwkSource16, + source64, + radius: this.decrementRadius(nwkRadius), + seqNum: nwkSeqNum, + relayIndex, + relayAddresses, + }, + finalPayload, + nwkSecurityHeader, + undefined, // use pre-hashed this.netParams.networkKey, + ); + const macFrame = encodeMACFrameZigbee( + { + frameControl: { + frameType: MACFrameType.DATA, + securityEnabled: false, + framePending: false, + ackRequest: macDest16 !== ZigbeeMACConsts.BCAST_ADDR, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: MACFrameAddressMode.SHORT, + frameVersion: MACFrameVersion.v2003, + sourceAddrMode: MACFrameAddressMode.SHORT, + }, + sequenceNumber: macSeqNum, + destinationPANId: this.netParams.panId, + destination16: macDest16, + // sourcePANId: undefined, // panIdCompression=true + source16: ZigbeeConsts.COORDINATOR_ADDRESS, + fcs: 0, + }, + nwkFrame, + ); + + await this.sendMACFrame(macSeqNum, macFrame, macDest16, undefined); + } + + public async processZigbeeNWKCommandFrame(data: Buffer, macHeader: MACHeader, nwkHeader: ZigbeeNWKHeader): Promise { + let offset = 0; + const cmdId = data.readUInt8(offset); + offset += 1; + + logger.debug( + () => + `<=== NWK CMD[cmdId=${cmdId} macSource=${macHeader.source16}:${macHeader.source64} nwkSource=${nwkHeader.source16}:${nwkHeader.source64}]`, + NS, + ); + + switch (cmdId) { + case ZigbeeNWKCommandId.ROUTE_REQ: { + offset = await this.processZigbeeNWKRouteReq(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.ROUTE_REPLY: { + offset = this.processZigbeeNWKRouteReply(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.NWK_STATUS: { + offset = this.processZigbeeNWKStatus(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.LEAVE: { + offset = this.processZigbeeNWKLeave(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.ROUTE_RECORD: { + offset = this.processZigbeeNWKRouteRecord(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.REJOIN_REQ: { + offset = await this.processZigbeeNWKRejoinReq(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.REJOIN_RESP: { + offset = this.processZigbeeNWKRejoinResp(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.LINK_STATUS: { + offset = this.processZigbeeNWKLinkStatus(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.NWK_REPORT: { + offset = this.processZigbeeNWKReport(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.NWK_UPDATE: { + offset = this.processZigbeeNWKUpdate(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.ED_TIMEOUT_REQUEST: { + offset = await this.processZigbeeNWKEdTimeoutRequest(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.ED_TIMEOUT_RESPONSE: { + offset = this.processZigbeeNWKEdTimeoutResponse(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.LINK_PWR_DELTA: { + offset = this.processZigbeeNWKLinkPwrDelta(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.COMMISSIONING_REQUEST: { + offset = await this.processZigbeeNWKCommissioningRequest(data, offset, macHeader, nwkHeader); + break; + } + case ZigbeeNWKCommandId.COMMISSIONING_RESPONSE: { + offset = this.processZigbeeNWKCommissioningResponse(data, offset, macHeader, nwkHeader); + break; + } + default: { + logger.error(`<=x= NWK CMD Unsupported ${cmdId}.`, NS); + return; + } + } + + // excess data in packet + // if (offset < data.byteLength) { + // logger.debug(() => `<=== NWK CMD contained more data: ${data.toString('hex')}`, NS); + // } + } + + /** + * 05-3474-R #3.4.1 + */ + public async processZigbeeNWKRouteReq(data: Buffer, offset: number, macHeader: MACHeader, nwkHeader: ZigbeeNWKHeader): Promise { + const options = data.readUInt8(offset); + offset += 1; + const manyToOne = (options & ZigbeeNWKConsts.CMD_ROUTE_OPTION_MANY_MASK) >> 3; // ZigbeeNWKManyToOne + const id = data.readUInt8(offset); + offset += 1; + const destination16 = data.readUInt16LE(offset); + offset += 2; + const pathCost = data.readUInt8(offset); + offset += 1; + let destination64: bigint | undefined; + + if (options & ZigbeeNWKConsts.CMD_ROUTE_OPTION_DEST_EXT) { + destination64 = data.readBigUInt64LE(offset); + offset += 8; + } + + logger.debug( + () => `<=== NWK ROUTE_REQ[id=${id} dst=${destination16} dst64=${destination64} pathCost=${pathCost} manyToOne=${manyToOne}]`, + NS, + ); + + await this.sendZigbeeNWKRouteReply( + macHeader.destination16!, + nwkHeader.radius!, + id, + nwkHeader.source16!, + destination16, + nwkHeader.source64 ?? this.address16ToAddress64.get(nwkHeader.source16!), + destination64, + ); + + return offset; + } + + /** + * 05-3474-R #3.4.1 + * + * @param manyToOne + * @param destination16 intended destination of the route request command frame + * @param destination64 SHOULD always be added if it is known + */ + public async sendZigbeeNWKRouteReq(manyToOne: ZigbeeNWKManyToOne, destination16: number, destination64?: bigint): Promise { + logger.debug(() => `===> NWK ROUTE_REQ[manyToOne=${manyToOne} destination16=${destination16} destination64=${destination64}]`, NS); + const hasDestination64 = destination64 !== undefined; + const options = + (((manyToOne ? 1 : 0) << 3) & ZigbeeNWKConsts.CMD_ROUTE_OPTION_MANY_MASK) | + (((hasDestination64 ? 1 : 0) << 5) & ZigbeeNWKConsts.CMD_ROUTE_OPTION_DEST_EXT); + const finalPayload = Buffer.alloc(1 + 1 + 1 + 2 + (hasDestination64 ? 8 : 0)); + let offset = 0; + finalPayload.writeUInt8(ZigbeeNWKCommandId.ROUTE_REQ, offset); + offset += 1; + finalPayload.writeUInt8(options, offset); + offset += 1; + finalPayload.writeUInt8(this.nextRouteRequestId(), offset); + offset += 1; + finalPayload.writeUInt16LE(destination16, offset); + offset += 2; + finalPayload.writeUInt8(1, offset); // pathCost TODO: proper init value? + offset += 1; + + if (hasDestination64) { + finalPayload.writeBigUInt64LE(destination64!, offset); + offset += 8; + } + + await this.sendZigbeeNWKCommand( + ZigbeeNWKCommandId.ROUTE_REQ, + finalPayload, + ZigbeeMACConsts.BCAST_ADDR, // macDest16 + true, // nwkSecurity + ZigbeeConsts.COORDINATOR_ADDRESS, // nwkSource16 + ZigbeeConsts.BCAST_DEFAULT, // nwkDest16 + undefined, // nwkDest64 + CONFIG_NWK_MAX_HOPS, // nwkRadius + ); + } + + /** + * 05-3474-R #3.4.2 + */ + public processZigbeeNWKRouteReply(data: Buffer, offset: number, _macHeader: MACHeader, _nwkHeader: ZigbeeNWKHeader): number { + const options = data.readUInt8(offset); + offset += 1; + const id = data.readUInt8(offset); + offset += 1; + const originator16 = data.readUInt16LE(offset); + offset += 2; + const responder16 = data.readUInt16LE(offset); + offset += 2; + const pathCost = data.readUInt8(offset); + offset += 1; + let originator64: bigint | undefined; + let responder64: bigint | undefined; + + if (options & ZigbeeNWKConsts.CMD_ROUTE_OPTION_ORIG_EXT) { + originator64 = data.readBigUInt64LE(offset); + offset += 8; + } + + if (options & ZigbeeNWKConsts.CMD_ROUTE_OPTION_RESP_EXT) { + responder64 = data.readBigUInt64LE(offset); + offset += 8; + } + + // TODO + // const [tlvs, tlvsOutOffset] = decodeZigbeeNWKTLVs(data, offset); + + logger.debug( + () => + `<=== NWK ROUTE_REPLY[id=${id} orig=${originator16} orig64=${originator64} resp=${responder16} resp64=${responder64} pathCost=${pathCost}]`, + NS, + ); + // TODO + + return offset; + } + + /** + * 05-3474-R #3.4.2, #3.6.4.5.2 + * + * @param requestDest1stHop16 SHALL be set to the network address of the first hop in the path back to the originator of the corresponding route request command frame + * @param requestRadius + * @param requestId 8-bit sequence number of the route request to which this frame is a reply + * @param originator16 SHALL contain the 16-bit network address of the originator of the route request command frame to which this frame is a reply + * @param responder16 SHALL always be the same as the value in the destination address field of the corresponding route request command frame + * @param originator64 SHALL be 8 octets in length and SHALL contain the 64-bit address of the originator of the route request command frame to which this frame is a reply. + * This field SHALL only be present if the originator IEEE address sub-field of the command options field has a value of 1. + * @param responder64 SHALL be 8 octets in length and SHALL contain the 64-bit address of the destination of the route request command frame to which this frame is a reply. + * This field SHALL only be present if the responder IEEE address sub-field of the command options field has a value of 1. + */ + public async sendZigbeeNWKRouteReply( + requestDest1stHop16: number, + requestRadius: number, + requestId: number, + originator16: number, + responder16: number, + originator64?: bigint, + responder64?: bigint, + ): Promise { + logger.debug( + () => + `===> NWK ROUTE_REPLY[requestDest1stHop16=${requestDest1stHop16} requestRadius=${requestRadius} requestId=${requestId} originator16=${originator16} responder16=${responder16} originator64=${originator64} responder64=${responder64}]`, + NS, + ); + const hasOriginator64 = originator64 !== undefined; + const hasResponder64 = responder64 !== undefined; + const options = + (((hasOriginator64 ? 1 : 0) << 4) & ZigbeeNWKConsts.CMD_ROUTE_OPTION_ORIG_EXT) | + (((hasResponder64 ? 1 : 0) << 5) & ZigbeeNWKConsts.CMD_ROUTE_OPTION_RESP_EXT); + const finalPayload = Buffer.alloc(1 + 1 + 1 + 2 + 2 + 1 + (hasOriginator64 ? 8 : 0) + (hasResponder64 ? 8 : 0)); + let offset = 0; + finalPayload.writeUInt8(ZigbeeNWKCommandId.ROUTE_REPLY, offset); + offset += 1; + finalPayload.writeUInt8(options, offset); + offset += 1; + finalPayload.writeUInt8(requestId, offset); + offset += 1; + finalPayload.writeUInt16LE(originator16, offset); + offset += 2; + finalPayload.writeUInt16LE(responder16, offset); + offset += 2; + finalPayload.writeUInt8(1, offset); // pathCost TODO: init to 0 or 1? + offset += 1; + + if (hasOriginator64) { + finalPayload.writeBigUInt64LE(originator64!, offset); + offset += 8; + } + + if (hasResponder64) { + finalPayload.writeBigUInt64LE(responder64!, offset); + offset += 8; + } + + // TODO + // const [tlvs, tlvsOutOffset] = encodeZigbeeNWKTLVs(); + + await this.sendZigbeeNWKCommand( + ZigbeeNWKCommandId.ROUTE_REPLY, + finalPayload, + requestDest1stHop16, // macDest16 + true, // nwkSecurity + ZigbeeConsts.COORDINATOR_ADDRESS, // nwkSource16 + requestDest1stHop16, // nwkDest16 + this.address16ToAddress64.get(requestDest1stHop16), // nwkDest64 SHALL contain the 64-bit IEEE address of the first hop in the path back to the originator of the corresponding route request + requestRadius, // nwkRadius + ); + } + + /** + * 05-3474-R #3.4.3 + */ + public processZigbeeNWKStatus(data: Buffer, offset: number, _macHeader: MACHeader, _nwkHeader: ZigbeeNWKHeader): number { + const status = data.readUInt8(offset); + offset += 1; + let destination: number | undefined; + + if (status === ZigbeeNWKStatus.LINK_FAILURE || status === ZigbeeNWKStatus.ADDRESS_CONFLICT) { + destination = data.readUInt16LE(offset); + offset += 2; + } + + // TODO + // const [tlvs, tlvsOutOffset] = decodeZigbeeNWKTLVs(data, offset); + + logger.debug(() => `<=== NWK NWK_STATUS[status=${ZigbeeNWKStatus[status]} dst=${destination}]`, NS); + // TODO + // network address update notification from here? + + return offset; + } + + /** + * 05-3474-R #3.4.3 + * + * @param requestDest1stHop16 + * @param requestSource16 + * @param status + * @param destination Destination address (only if status is LINK_FAILURE or ADDRESS_CONFLICT) + * - in case of a routing failure, it SHALL contain the destination address from the data frame that encountered the failure + * - in case of an address conflict, it SHALL contain the offending network address. + */ + public async sendZigbeeNWKStatus( + requestDest1stHop16: number, + requestSource16: number, + status: ZigbeeNWKStatus, + destination?: number, + ): Promise { + logger.debug( + () => + `===> NWK NWK_STATUS[requestDest1stHop16=${requestDest1stHop16} requestSource16=${requestSource16} status=${status} destination=${destination}]`, + NS, + ); + let finalPayload: Buffer; + + if (status === ZigbeeNWKStatus.LINK_FAILURE || status === ZigbeeNWKStatus.ADDRESS_CONFLICT) { + finalPayload = Buffer.from([ZigbeeNWKCommandId.NWK_STATUS, status, destination! & 0xff, (destination! >> 8) & 0xff]); + } else { + finalPayload = Buffer.from([ZigbeeNWKCommandId.NWK_STATUS, status]); + } + + // TODO + // const [tlvs, tlvsOutOffset] = encodeZigbeeNWKTLVs(); + + await this.sendZigbeeNWKCommand( + ZigbeeNWKCommandId.NWK_STATUS, + finalPayload, + requestDest1stHop16, // macDest16 + true, // nwkSecurity + ZigbeeConsts.COORDINATOR_ADDRESS, // nwkSource16 + requestSource16, // nwkDest16 + requestDest1stHop16 === ZigbeeMACConsts.BCAST_ADDR ? undefined : this.address16ToAddress64.get(requestDest1stHop16), // nwkDest64 + CONFIG_NWK_MAX_HOPS, // nwkRadius + ); + } + + /** + * 05-3474-R #3.4.4 + */ + public processZigbeeNWKLeave(data: Buffer, offset: number, _macHeader: MACHeader, nwkHeader: ZigbeeNWKHeader): number { + const options = data.readUInt8(offset); + offset += 1; + const removeChildren = Boolean(options & ZigbeeNWKConsts.CMD_LEAVE_OPTION_REMOVE_CHILDREN); + const request = Boolean(options & ZigbeeNWKConsts.CMD_LEAVE_OPTION_REQUEST); + const rejoin = Boolean(options & ZigbeeNWKConsts.CMD_LEAVE_OPTION_REJOIN); + + logger.debug(() => `<=== NWK LEAVE[removeChildren=${removeChildren} request=${request} rejoin=${rejoin}]`, NS); + + if (!rejoin && !request) { + this.disassociate(nwkHeader.source16, nwkHeader.source64); + } + + return offset; + } + + /** + * 05-3474-R #3.4.3 + * + * NOTE: `request` option always true + * NOTE: `removeChildren` option should not be used (mesh disruption) + * + * @param neighbor16 + * @param destination16 + * @param rejoin if true, the device that is leaving from its current parent will rejoin the network + */ + public async sendZigbeeNWKLeave(neighbor16: number, destination16: number, rejoin: boolean): Promise { + logger.debug(() => `===> NWK LEAVE[neighbor16=${neighbor16} destination16=${destination16} rejoin=${rejoin}]`, NS); + + const options = + (0 & ZigbeeNWKConsts.CMD_LEAVE_OPTION_REMOVE_CHILDREN) | + ((1 << 6) & ZigbeeNWKConsts.CMD_LEAVE_OPTION_REQUEST) | + (((rejoin ? 1 : 0) << 5) & ZigbeeNWKConsts.CMD_LEAVE_OPTION_REJOIN); + const finalPayload = Buffer.from([ZigbeeNWKCommandId.LEAVE, options]); + + await this.sendZigbeeNWKCommand( + ZigbeeNWKCommandId.LEAVE, + finalPayload, + destination16 < ZigbeeConsts.BCAST_MIN ? neighbor16 : ZigbeeMACConsts.BCAST_ADDR, // macDest16 + true, // nwkSecurity + ZigbeeConsts.COORDINATOR_ADDRESS, // nwkSource16 + destination16, // nwkDest16 + this.address16ToAddress64.get(destination16), // nwkDest64 + 1, // nwkRadius + ); + } + + /** + * 05-3474-R #3.4.5 + */ + public processZigbeeNWKRouteRecord(data: Buffer, offset: number, _macHeader: MACHeader, _nwkHeader: ZigbeeNWKHeader): number { + const relayCount = data.readUInt8(offset); + offset += 1; + const relays: number[] = []; + + for (let i = 0; i < relayCount; i++) { + const relay = data.readUInt16LE(offset); + offset += 2; + + relays.push(relay); + } + + logger.debug(() => `<=== NWK ROUTE_RECORD[relays=${relays}]`, NS); + // TODO + + return offset; + } + + // NOTE: sendZigbeeNWKRouteRecord not for coordinator + + /** + * 05-3474-R #3.4.6 + * Optional + */ + public async processZigbeeNWKRejoinReq(data: Buffer, offset: number, _macHeader: MACHeader, nwkHeader: ZigbeeNWKHeader): Promise { + const capabilities = data.readUInt8(offset); + offset += 1; + + logger.debug(() => `<=== NWK REJOIN_REQ[capabilities=${capabilities}]`, NS); + + // XXX: if !header.frameControl.security => Trust Center Rejoin + // => Unsecured Packets at the network layer claiming to be from existing neighbors (coordinators, routers or end devices) must not rewrite legitimate data in the nwkNeighborTable. + // send NWK key again in that case? + const [status, newNetwork16] = await this.associate(nwkHeader.source16!, nwkHeader.source64, 0x01 /* rejoin */, capabilities); + + await this.sendZigbeeNWKRejoinResp(nwkHeader.source16!, newNetwork16, status); + + // NOTE: a device does not have to verify its trust center link key with the APSME-VERIFY-KEY services after a rejoin. + + return offset; + } + + // NOTE: sendZigbeeNWKRejoinReq not for coordinator + + /** + * 05-3474-R #3.4.7 + * Optional + */ + public processZigbeeNWKRejoinResp(data: Buffer, offset: number, _macHeader: MACHeader, _nwkHeader: ZigbeeNWKHeader): number { + const newAddress = data.readUInt16LE(offset); + offset += 2; + const status = data.readUInt8(offset); + offset += 1; + + if (status !== MACAssociationStatus.SUCCESS) { + logger.error(`<=x= NWK REJOIN_RESP[newAddress=${newAddress} status=${MACAssociationStatus[status]}]`, NS); + } else { + logger.debug(() => `<=== NWK REJOIN_RESP[newAddress=${newAddress}]`, NS); + } + + return offset; + } + + /** + * 05-3474-R #3.4.7 + * Optional + * + * @param requestSource16 new network address assigned to the rejoining device + */ + public async sendZigbeeNWKRejoinResp(requestSource16: number, newNetwork16: number, status: MACAssociationStatus | number): Promise { + logger.debug(() => `===> NWK REJOIN_RESP[requestSource16=${requestSource16} newNetwork16=${newNetwork16} status=${status}]`, NS); + + const finalPayload = Buffer.from([ZigbeeNWKCommandId.REJOIN_RESP, newNetwork16 & 0xff, (newNetwork16 >> 8) & 0xff, status]); + + await this.sendZigbeeNWKCommand( + ZigbeeNWKCommandId.REJOIN_RESP, + finalPayload, + requestSource16, // macDest16 + true, // nwkSecurity TODO: ?? + ZigbeeConsts.COORDINATOR_ADDRESS, // nwkSource16 + requestSource16, // nwkDest16 + undefined, // nwkDest64 + CONFIG_NWK_MAX_HOPS, // nwkRadius + ); + + setImmediate(() => { + this.emit("DEVICE_REJOINED", newNetwork16, this.address16ToAddress64.get(newNetwork16)!); + }); + } + + /** + * 05-3474-R #3.4.8 + */ + public processZigbeeNWKLinkStatus(data: Buffer, offset: number, _macHeader: MACHeader, _nwkHeader: ZigbeeNWKHeader): number { + // Bit: 0 – 4 5 6 7 + // Entry count First frame Last frame Reserved + const options = data.readUInt8(offset); + offset += 1; + const firstFrame = Boolean((options & ZigbeeNWKConsts.CMD_LINK_OPTION_FIRST_FRAME) >> 5); + const lastFrame = Boolean((options & ZigbeeNWKConsts.CMD_LINK_OPTION_LAST_FRAME) >> 6); + const linkCount = options & ZigbeeNWKConsts.CMD_LINK_OPTION_COUNT_MASK; + const links: ZigbeeNWKLinkStatus[] = []; + + for (let i = 0; i < linkCount; i++) { + const address = data.readUInt16LE(offset); + offset += 2; + const costByte = data.readUInt8(offset); + offset += 1; + + links.push({ + address, + incomingCost: costByte & ZigbeeNWKConsts.CMD_LINK_INCOMING_COST_MASK, + outgoingCost: (costByte & ZigbeeNWKConsts.CMD_LINK_OUTGOING_COST_MASK) >> 4, + }); + } + + logger.debug(() => { + let linksStr = ""; + + for (const link of links) { + linksStr += `{${link.address}|in:${link.incomingCost}|out:${link.outgoingCost}}`; + } + + return `<=== NWK LINK_STATUS[firstFrame=${firstFrame} lastFrame=${lastFrame} links=${linksStr}]`; + }, NS); + // TODO + + return offset; + } + + /** + * 05-3474-R #3.4.8 + * + * @param links set of link status entries derived from the neighbor table (SHALL be specific to the interface to be transmitted on) + * Links are expected sorted in ascending order by network address. + * - incoming cost contains device's estimate of the link cost for the neighbor + * - outgoing cost contains value of outgoing cost from neighbor table + */ + public async sendZigbeeNWKLinkStatus(links: ZigbeeNWKLinkStatus[]): Promise { + logger.debug(() => `===> NWK LINK_STATUS[links=${links}]`, NS); + // TODO: check repeat logic + const linkSize = links.length * 3; + const maxLinksPayloadSize = ZigbeeNWKConsts.PAYLOAD_MIN_SIZE - 2; // 84 (- cmdId[1] - options[1]) + const maxLinksPerFrame = Math.floor(maxLinksPayloadSize / 3); // 27 + const frameCount = Math.ceil((linkSize + 3) / maxLinksPayloadSize); // (+ repeated link[3]) + let linksOffset = 0; + + for (let i = 0; i < frameCount; i++) { + const linkCount = links.length - i * maxLinksPerFrame; + const frameSize = 2 + Math.min(linkCount * 3, maxLinksPayloadSize); + const options = + (((i === 0 ? 1 : 0) << 5) & ZigbeeNWKConsts.CMD_LINK_OPTION_FIRST_FRAME) | + (((i === frameCount - 1 ? 1 : 0) << 6) & ZigbeeNWKConsts.CMD_LINK_OPTION_LAST_FRAME) | + (linkCount & ZigbeeNWKConsts.CMD_LINK_OPTION_COUNT_MASK); + const finalPayload = Buffer.alloc(frameSize); + let finalPayloadOffset = 0; + finalPayload.writeUInt8(ZigbeeNWKCommandId.LINK_STATUS, finalPayloadOffset); + finalPayloadOffset += 1; + finalPayload.writeUInt8(options, finalPayloadOffset); + finalPayloadOffset += 1; + + for (let j = 0; j < linkCount; j++) { + const link = links[linksOffset]; + finalPayload.writeUInt16LE(link.address, finalPayloadOffset); + finalPayloadOffset += 2; + finalPayload.writeUInt8( + (link.incomingCost & ZigbeeNWKConsts.CMD_LINK_INCOMING_COST_MASK) | + ((link.outgoingCost << 4) & ZigbeeNWKConsts.CMD_LINK_OUTGOING_COST_MASK), + finalPayloadOffset, + ); + finalPayloadOffset += 1; + + // last in previous frame is repeated first in next frame + if (j < linkCount - 1) { + linksOffset++; + } + } + + await this.sendZigbeeNWKCommand( + ZigbeeNWKCommandId.LINK_STATUS, + finalPayload, + ZigbeeMACConsts.BCAST_ADDR, // macDest16 + true, // nwkSecurity + ZigbeeConsts.COORDINATOR_ADDRESS, // nwkSource16 + ZigbeeConsts.BCAST_DEFAULT, // nwkDest16 + undefined, // nwkDest64 + 1, // nwkRadius + ); + } + } + + /** + * 05-3474-R #3.4.9 + * deprecated in R23, should no longer be sent by R23 devices + */ + public processZigbeeNWKReport(data: Buffer, offset: number, _macHeader: MACHeader, _nwkHeader: ZigbeeNWKHeader): number { + const options = data.readUInt8(offset); + offset += 1; + const reportCount = options & ZigbeeNWKConsts.CMD_NWK_REPORT_COUNT_MASK; + const reportType = options & ZigbeeNWKConsts.CMD_NWK_REPORT_ID_MASK; + const extendedPANId = data.readBigUInt64LE(offset); + offset += 8; + let conflictPANIds: number[] | undefined; + + if (reportType === ZigbeeNWKConsts.CMD_NWK_REPORT_ID_PAN_CONFLICT) { + conflictPANIds = []; + + for (let i = 0; i < reportCount; i++) { + const panId = data.readUInt16LE(offset); + offset += 2; + + conflictPANIds.push(panId); + } + } + + logger.debug(() => `<=== NWK NWK_REPORT[extendedPANId=${extendedPANId} reportType=${reportType} conflictPANIds=${conflictPANIds}]`, NS); + + return offset; + } + + // NOTE: sendZigbeeNWKReport deprecated in R23 + + /** + * 05-3474-R #3.4.10 + */ + public processZigbeeNWKUpdate(data: Buffer, offset: number, _macHeader: MACHeader, _nwkHeader: ZigbeeNWKHeader): number { + const options = data.readUInt8(offset); + offset += 1; + const updateCount = options & ZigbeeNWKConsts.CMD_NWK_UPDATE_COUNT_MASK; + const updateType = options & ZigbeeNWKConsts.CMD_NWK_UPDATE_ID_MASK; + const extendedPANId = data.readBigUInt64LE(offset); + offset += 8; + const updateId = data.readUInt8(offset); + offset += 1; + let panIds: number[] | undefined; + + if (updateType === ZigbeeNWKConsts.CMD_NWK_UPDATE_ID_PAN_UPDATE) { + panIds = []; + + for (let i = 0; i < updateCount; i++) { + const panId = data.readUInt16LE(offset); + offset += 2; + + panIds.push(panId); + } + } + + logger.debug(() => `<=== NWK NWK_UPDATE[extendedPANId=${extendedPANId} updateId=${updateId} updateType=${updateType} panIds=${panIds}]`, NS); + // TODO + + return offset; + } + + // NOTE: sendZigbeeNWKUpdate PAN ID change not supported + + /** + * 05-3474-R #3.4.11 + */ + public async processZigbeeNWKEdTimeoutRequest(data: Buffer, offset: number, _macHeader: MACHeader, nwkHeader: ZigbeeNWKHeader): Promise { + // 0 => 10 seconds + // 1 => 2 minutes + // 2 => 4 minutes + // 3 => 8 minutes + // 4 => 16 minutes + // 5 => 32 minutes + // 6 => 64 minutes + // 7 => 128 minutes + // 8 => 256 minutes + // 9 => 512 minutes + // 10 => 1024 minutes + // 11 => 2048 minutes + // 12 => 4096 minutes + // 13 => 8192 minutes + // 14 => 16384 minutes + const requestedTimeout = data.readUInt8(offset); + offset += 1; + // not currently used (all reserved) + const configuration = data.readUInt8(offset); + offset += 1; + + logger.debug(() => `<=== NWK ED_TIMEOUT_REQUEST[requestedTimeout=${requestedTimeout} configuration=${configuration}]`, NS); + + await this.sendZigbeeNWKEdTimeoutResponse(nwkHeader.source16!, requestedTimeout); + + return offset; + } + + // NOTE: sendZigbeeNWKEdTimeoutRequest not for coordinator + + /** + * 05-3474-R #3.4.12 + */ + public processZigbeeNWKEdTimeoutResponse(data: Buffer, offset: number, _macHeader: MACHeader, _nwkHeader: ZigbeeNWKHeader): number { + // SUCCESS 0x00 The End Device Timeout Request message was accepted by the parent. + // INCORRECT_VALUE 0x01 The received timeout value in the End Device Timeout Request command was outside the allowed range. + // UNSUPPORTED_FEATURE 0x02 The requested feature is not supported by the parent router. + const status = data.readUInt8(offset); + offset += 1; + // Bit 0 MAC Data Poll Keepalive Supported + // Bit 1 End Device Timeout Request Keepalive Supported + // Bit 2 Power Negotiation Support + const parentInfo = data.readUInt8(offset); + offset += 1; + + logger.debug(() => `<=== NWK ED_TIMEOUT_RESPONSE[status=${status} parentInfo=${parentInfo}]`, NS); + // TODO + + return offset; + } + + /** + * 05-3474-R #3.4.12 + * + * @param requestDest16 + * @param requestedTimeout Requested timeout enumeration [0-14] (mapping to actual timeout) @see processZigbeeNWKEdTimeoutRequest + */ + public async sendZigbeeNWKEdTimeoutResponse(requestDest16: number, requestedTimeout: number): Promise { + logger.debug(() => `===> NWK ED_TIMEOUT_RESPONSE[requestDest16=${requestDest16} requestedTimeout=${requestedTimeout}]`, NS); + + // sanity check + const status = requestedTimeout >= 0 && requestedTimeout <= 14 ? 0x00 : 0x01; + const parentInfo = 0b00000111; // TODO: ? + const finalPayload = Buffer.from([ZigbeeNWKCommandId.ED_TIMEOUT_RESPONSE, status, parentInfo]); + + await this.sendZigbeeNWKCommand( + ZigbeeNWKCommandId.ED_TIMEOUT_RESPONSE, + finalPayload, + requestDest16, // macDest16 + true, // nwkSecurity + ZigbeeConsts.COORDINATOR_ADDRESS, // nwkSource16 + requestDest16, // nwkDest16 + this.address16ToAddress64.get(requestDest16), // nwkDest64 TODO: can `undefined`? + 1, // nwkRadius + ); + } + + /** + * 05-3474-R #3.4.13 + */ + public processZigbeeNWKLinkPwrDelta(data: Buffer, offset: number, _macHeader: MACHeader, _nwkHeader: ZigbeeNWKHeader): number { + const options = data.readUInt8(offset); + offset += 1; + // 0 Notification An unsolicited notification. These frames are typically sent periodically from an RxOn device. If the device is a FFD, it is broadcast to all RxOn devices (0xfffd), and includes power information for all neighboring RxOn devices. If the device is an RFD with RxOn, it is sent unicast to its Parent, and includes only power information for the Parent device. + // 1 Request Typically used by sleepy RFD devices that do not receive the periodic Notifications from their Parent. The sleepy RFD will wake up periodically to send this frame to its Parent, including only the Parent’s power information in its payload. Upon receipt, the Parent sends a Response (Type = 2) as an indirect transmission, with only the RFD’s power information in its payload. After macResponseWaitTime, the RFD polls its Parent for the Response, before going back to sleep. Request commands are sent as unicast. Note: any device MAY send a Request to solicit a Response from another device. These commands SHALL be sent as unicast and contain only the power information for the destination device. If this command is received as a broadcast, it SHALL be discarded with no action. + // 2 Response This command is sent in response to a Request. Response commands are sent as unicast to the sender of the Request. The response includes only the power information for the requesting device. + // 3 Reserved + const type = options & ZigbeeNWKConsts.CMD_NWK_LINK_PWR_DELTA_TYPE_MASK; + const count = data.readUInt8(offset); + offset += 1; + const deltas: { device: number; delta: number }[] = []; + + for (let i = 0; i < count; i++) { + const device = data.readUInt16LE(offset); + offset += 2; + const delta = data.readUInt8(offset); + offset += 1; + + deltas.push({ device, delta }); + } + + logger.debug(() => `<=== NWK LINK_PWR_DELTA[type=${type} deltas=${deltas}]`, NS); + // TODO + + return offset; + } + + // NOTE: sendZigbeeNWKLinkPwrDelta not supported + + /** + * 05-3474-23 #3.4.14 + * Optional + */ + public async processZigbeeNWKCommissioningRequest( + data: Buffer, + offset: number, + macHeader: MACHeader, + nwkHeader: ZigbeeNWKHeader, + ): Promise { + // 0x00 Initial Join + // 0x01 Rejoin + const assocType = data.readUInt8(offset); + offset += 1; + const capabilities = data.readUInt8(offset); + offset += 1; + + // TODO + // const [tlvs, tlvsOutOffset] = decodeZigbeeNWKTLVs(data, offset); + + logger.debug(() => `<=== NWK COMMISSIONING_REQUEST[assocType=${assocType} capabilities=${capabilities}]`, NS); + + // NOTE: send Remove Device CMD to TC deny the join (or let timeout): `sendZigbeeAPSRemoveDevice` + + const [status, newNetwork16] = await this.associate( + nwkHeader.source16!, + nwkHeader.source64, + assocType, + capabilities, + nwkHeader.frameControl.security /* deny if true */, + ); + + await this.sendZigbeeNWKCommissioningResponse(nwkHeader.source16!, newNetwork16, status); + + if (status === MACAssociationStatus.SUCCESS) { + // TODO also for rejoin in case of nwk key change? + await this.sendZigbeeAPSTransportKeyNWK( + macHeader.source16!, + nwkHeader.source16!, + this.netParams.networkKey, + this.netParams.networkKeySequenceNumber, + this.address16ToAddress64.get(newNetwork16)!, // valid from `associate` + ); + } + + return offset; + } + + // NOTE: sendZigbeeNWKCommissioningRequest not for coordinator + + /** + * 05-3474-23 #3.4.15 + * Optional + */ + public processZigbeeNWKCommissioningResponse(data: Buffer, offset: number, _macHeader: MACHeader, _nwkHeader: ZigbeeNWKHeader): number { + const newAddress = data.readUInt16LE(offset); + offset += 2; + // `ZigbeeNWKConsts.ASSOC_STATUS_ADDR_CONFLICT`, or MACAssociationStatus + const status = data.readUInt8(offset); + offset += 1; + + if (status !== MACAssociationStatus.SUCCESS) { + logger.error( + `<=x= NWK COMMISSIONING_RESPONSE[newAddress=${newAddress} status=${MACAssociationStatus[status] ?? "NWK_ADDR_CONFLICT"}]`, + NS, + ); + } else { + logger.debug(() => `<=== NWK COMMISSIONING_RESPONSE[newAddress=${newAddress}]`, NS); + } + + // TODO + + return offset; + } + + /** + * 05-3474-23 #3.4.15 + * Optional + * + * @param requestSource16 + * @param newNetwork16 the new 16-bit network address assigned, may be same as `requestDest16` + */ + public async sendZigbeeNWKCommissioningResponse( + requestSource16: number, + newNetwork16: number, + status: MACAssociationStatus | number, + ): Promise { + logger.debug(() => `===> NWK COMMISSIONING_RESPONSE[requestSource16=${requestSource16} newNetwork16=${newNetwork16} status=${status}]`, NS); + + const finalPayload = Buffer.from([ZigbeeNWKCommandId.COMMISSIONING_RESPONSE, newNetwork16 & 0xff, (newNetwork16 >> 8) & 0xff, status]); + + await this.sendZigbeeNWKCommand( + ZigbeeNWKCommandId.COMMISSIONING_RESPONSE, + finalPayload, + requestSource16, // macDest16 + false, // nwkSecurity + ZigbeeConsts.COORDINATOR_ADDRESS, // nwkSource16 + requestSource16, // nwkDest16 + this.address16ToAddress64.get(requestSource16), // nwkDest64 + CONFIG_NWK_MAX_HOPS, // nwkRadius + ); + } + + // #endregion + + // #region Zigbee NWK GP layer + + public processZigbeeNWKGPCommandFrame(data: Buffer, macHeader: MACHeader, nwkHeader: ZigbeeNWKGPHeader, rssi: number): void { + if (nwkHeader.sourceId === undefined) { + return; + } + + let offset = 0; + const cmdId = data.readUInt8(offset); + offset += 1; + + logger.debug(() => `<=== NWKGP CMD[cmdId=${cmdId} macSource=${macHeader.source16}:${macHeader.source64} sourceId=${nwkHeader.sourceId}]`, NS); + + const apsHeader: ZigbeeAPSHeader = { + frameControl: { + frameType: ZigbeeAPSFrameType.CMD, + deliveryMode: macHeader.source16! < ZigbeeConsts.BCAST_MIN ? ZigbeeAPSDeliveryMode.UNICAST : ZigbeeAPSDeliveryMode.BCAST, // TODO: correct?? + ackFormat: false, + security: false, + ackRequest: false, + extendedHeader: false, + }, + profileId: ZigbeeConsts.GP_PROFILE_ID, + clusterId: ZigbeeConsts.GP_CLUSTER_ID, + sourceEndpoint: ZigbeeConsts.GP_ENDPOINT, + destEndpoint: ZigbeeConsts.GP_ENDPOINT, + group: ZigbeeConsts.GP_GROUP_ID, + }; + // transform into a ZCL frame + // TODO: correct?? + const gpdHeader = Buffer.alloc(15); + gpdHeader.writeUInt8(0b00000001, 0); // frameControl: FrameType.SPECIFIC + Direction.CLIENT_TO_SERVER + disableDefaultResponse=false + gpdHeader.writeUInt8(macHeader.sequenceNumber ?? 0, 1); + gpdHeader.writeUInt8(cmdId === 0xe0 ? ZigbeeConsts.GP_COMMISSIONING_NOTIFICATION : ZigbeeConsts.GP_NOTIFICATION, 2); + gpdHeader.writeUInt16LE(0, 3); // options, only srcID present + gpdHeader.writeUInt32LE(nwkHeader.sourceId, 5); + // omitted: gpdIEEEAddr (ieeeAddr) + // omitted: gpdEndpoint (uint8) + gpdHeader.writeUInt32LE(nwkHeader.securityFrameCounter ?? 0, 9); + gpdHeader.writeUInt8(cmdId, 13); + gpdHeader.writeUInt8(data.byteLength - offset, 14); + + const payload = Buffer.alloc(gpdHeader.byteLength + data.byteLength - offset); + payload.set(gpdHeader, 0); + payload.set(data.subarray(offset), gpdHeader.byteLength); + + setImmediate(() => { + this.emit("FRAME", nwkHeader.sourceId! & 0xffff, undefined, apsHeader, payload, rssi); + }); + } + + // #endregion + + // #region Zigbee APS layer + + /** + * 05-3474-R #4.4.11 + * + * @param cmdId + * @param finalPayload expected to contain the full payload (including cmdId) + * @param macDest16 + * @param nwkDest16 + * @param nwkDest64 + * @param nwkRadius + * @param apsDeliveryMode + */ + public async sendZigbeeAPSCommand( + cmdId: ZigbeeAPSCommandId, + finalPayload: Buffer, + macDest16: number, + nwkDiscoverRoute: ZigbeeNWKRouteDiscovery, + nwkSecurity: boolean, + nwkDest16: number | undefined, + nwkDest64: bigint | undefined, + apsDeliveryMode: ZigbeeAPSDeliveryMode.UNICAST | ZigbeeAPSDeliveryMode.BCAST, + apsSecurityHeader: ZigbeeSecurityHeader | undefined, + disableACKRequest = false, + ): Promise { + let nwkSecurityHeader: ZigbeeSecurityHeader | undefined; + let relayIndex: number | undefined; + let relayAddresses: number[] | undefined; + + if (nwkSecurity) { + nwkSecurityHeader = { + control: { + level: ZigbeeSecurityLevel.NONE, + keyId: ZigbeeKeyType.NWK, + nonce: true, + }, + frameCounter: this.nextNWKKeyFrameCounter(), + source64: this.netParams.eui64, + keySeqNum: this.netParams.networkKeySequenceNumber, + micLen: 4, + }; + } + + if (this.sourceRouting) { + // TODO + relayIndex = 0; + relayAddresses = []; + } + + const apsCounter = this.nextAPSCounter(); + const nwkSeqNum = this.nextNWKSeqNum(); + const macSeqNum = this.nextMACSeqNum(); + + logger.debug( + () => + `===> APS CMD[seqNum=(${apsCounter}/${nwkSeqNum}/${macSeqNum}) cmdId=${cmdId} macDest=${macDest16} nwkDest=${nwkDest16}:${nwkDest64} nwkDiscRoute=${nwkDiscoverRoute} nwkSec=${nwkSecurity} apsDelivery=${apsDeliveryMode} apsSec=${apsSecurityHeader !== undefined}]`, + NS, + ); + + const apsFrame = encodeZigbeeAPSFrame( + { + frameControl: { + frameType: ZigbeeAPSFrameType.CMD, + deliveryMode: apsDeliveryMode, + ackFormat: false, + security: apsSecurityHeader !== undefined, + // XXX: spec says all should request ACK except TUNNEL, but vectors show not a lot of stacks respect that, what's best? + ackRequest: cmdId !== ZigbeeAPSCommandId.TUNNEL && !disableACKRequest, + extendedHeader: false, + }, + counter: apsCounter, + }, + finalPayload, + apsSecurityHeader, + undefined, // use pre-hashed this.netParams.tcKey, + ); + const nwkFrame = encodeZigbeeNWKFrame( + { + frameControl: { + frameType: ZigbeeNWKFrameType.DATA, + protocolVersion: ZigbeeNWKConsts.VERSION_2007, + discoverRoute: nwkDiscoverRoute, + multicast: false, + security: nwkSecurity, + sourceRoute: this.sourceRouting, + extendedDestination: nwkDest64 !== undefined, + extendedSource: false, + endDeviceInitiator: false, + }, + destination16: nwkDest16, + destination64: nwkDest64, + source16: ZigbeeConsts.COORDINATOR_ADDRESS, + radius: this.decrementRadius(CONFIG_NWK_MAX_HOPS), + seqNum: nwkSeqNum, + relayIndex, + relayAddresses, + }, + apsFrame, + nwkSecurityHeader, + undefined, // use pre-hashed this.netParams.networkKey, + ); + const macFrame = encodeMACFrameZigbee( + { + frameControl: { + frameType: MACFrameType.DATA, + securityEnabled: false, + framePending: false, + ackRequest: macDest16 !== ZigbeeMACConsts.BCAST_ADDR, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: MACFrameAddressMode.SHORT, + frameVersion: MACFrameVersion.v2003, + sourceAddrMode: MACFrameAddressMode.SHORT, + }, + sequenceNumber: macSeqNum, + destinationPANId: this.netParams.panId, + destination16: macDest16, + // sourcePANId: undefined, // panIdCompression=true + source16: ZigbeeConsts.COORDINATOR_ADDRESS, + fcs: 0, + }, + nwkFrame, + ); + + await this.sendMACFrame(macSeqNum, macFrame, macDest16, undefined); + } + + public async sendZigbeeAPSData( + finalPayload: Buffer, + macDest16: number, + nwkDiscoverRoute: ZigbeeNWKRouteDiscovery, + nwkDest16: number | undefined, + nwkDest64: bigint | undefined, + apsDeliveryMode: ZigbeeAPSDeliveryMode, + clusterId: number, + profileId: number, + destEndpoint: number | undefined, + sourceEndpoint: number | undefined, + group: number | undefined, + ): Promise { + const apsCounter = this.nextAPSCounter(); + const nwkSeqNum = this.nextNWKSeqNum(); + const macSeqNum = this.nextMACSeqNum(); + + logger.debug( + () => + `===> APS DATA[seqNum=(${apsCounter}/${nwkSeqNum}/${macSeqNum}) macDest16=${macDest16} nwkDiscoverRoute=${nwkDiscoverRoute} nwkDest16=${nwkDest16} nwkDest64=${nwkDest64} apsDeliveryMode=${apsDeliveryMode}]`, + NS, + ); + + let multicastControl: ZigbeeNWKMulticastControl | undefined; + + if (apsDeliveryMode === ZigbeeAPSDeliveryMode.GROUP) { + // TODO + multicastControl = { + mode: ZigbeeNWKMulticastMode.MEMBER, + radius: CONFIG_NWK_MAX_HOPS, + maxRadius: CONFIG_NWK_MAX_HOPS, + }; + } + + const apsFrame = encodeZigbeeAPSFrame( + { + frameControl: { + frameType: ZigbeeAPSFrameType.DATA, + deliveryMode: apsDeliveryMode, + ackFormat: false, + security: false, // TODO link key support + ackRequest: true, + extendedHeader: false, + }, + destEndpoint, + group, + clusterId, + profileId, + sourceEndpoint, + counter: apsCounter, + }, + finalPayload, + // undefined, + // undefined, + ); + const nwkFrame = encodeZigbeeNWKFrame( + { + frameControl: { + frameType: ZigbeeNWKFrameType.DATA, + protocolVersion: ZigbeeNWKConsts.VERSION_2007, + discoverRoute: nwkDiscoverRoute, + multicast: multicastControl !== undefined, + security: true, + sourceRoute: this.sourceRouting, // TODO + extendedDestination: nwkDest64 !== undefined, + extendedSource: false, + endDeviceInitiator: false, + }, + destination16: nwkDest16, + destination64: nwkDest64, + source16: ZigbeeConsts.COORDINATOR_ADDRESS, + radius: this.decrementRadius(CONFIG_NWK_MAX_HOPS), + seqNum: nwkSeqNum, + // relayIndex, + // relayAddresses, + multicastControl, + }, + apsFrame, + { + control: { + level: ZigbeeSecurityLevel.NONE, + keyId: ZigbeeKeyType.NWK, + nonce: true, + }, + frameCounter: this.nextNWKKeyFrameCounter(), + source64: this.netParams.eui64, + keySeqNum: this.netParams.networkKeySequenceNumber, + micLen: 4, + }, + undefined, // use pre-hashed this.netParams.networkKey, + ); + const macFrame = encodeMACFrameZigbee( + { + frameControl: { + frameType: MACFrameType.DATA, + securityEnabled: false, + framePending: false, + ackRequest: macDest16 !== ZigbeeMACConsts.BCAST_ADDR, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: MACFrameAddressMode.SHORT, + frameVersion: MACFrameVersion.v2003, + sourceAddrMode: MACFrameAddressMode.SHORT, + }, + sequenceNumber: macSeqNum, + destinationPANId: this.netParams.panId, + destination16: macDest16, + // sourcePANId: undefined, // panIdCompression=true + source16: ZigbeeConsts.COORDINATOR_ADDRESS, + fcs: 0, + }, + nwkFrame, + ); + + await this.sendMACFrame(macSeqNum, macFrame, macDest16, undefined); + + return apsCounter; + } + + public async onZigbeeAPSACKRequest(macHeader: MACHeader, nwkHeader: ZigbeeNWKHeader, apsHeader: ZigbeeAPSHeader): Promise { + logger.debug( + () => + `===> APS ACK[dest=${nwkHeader.source16} seqNum=${nwkHeader.seqNum} destEp=${apsHeader.sourceEndpoint} clusterId=${apsHeader.clusterId}]`, + NS, + ); + + const ackAPSFrame = encodeZigbeeAPSFrame( + { + frameControl: { + frameType: ZigbeeAPSFrameType.ACK, + deliveryMode: ZigbeeAPSDeliveryMode.UNICAST, + ackFormat: false, + security: false, + ackRequest: false, + extendedHeader: false, + }, + destEndpoint: apsHeader.sourceEndpoint, + clusterId: apsHeader.clusterId, + profileId: apsHeader.profileId, + sourceEndpoint: apsHeader.destEndpoint, + counter: apsHeader.counter, + }, + Buffer.alloc(0), // TODO optimize + // undefined, + // undefined, + ); + const ackNWKFrame = encodeZigbeeNWKFrame( + { + frameControl: { + frameType: ZigbeeNWKFrameType.DATA, + protocolVersion: ZigbeeNWKConsts.VERSION_2007, + discoverRoute: ZigbeeNWKRouteDiscovery.SUPPRESS, + multicast: false, + security: true, + sourceRoute: false, // TODO + extendedDestination: false, + extendedSource: false, + endDeviceInitiator: false, + }, + destination16: nwkHeader.source16, + source16: nwkHeader.destination16, + radius: this.decrementRadius(nwkHeader.radius ?? CONFIG_NWK_MAX_HOPS), + seqNum: nwkHeader.seqNum, + // relayIndex, + // relayAddresses, + }, + ackAPSFrame, + { + control: { + level: ZigbeeSecurityLevel.NONE, + keyId: ZigbeeKeyType.NWK, + nonce: true, + }, + frameCounter: this.nextNWKKeyFrameCounter(), + source64: this.netParams.eui64, + keySeqNum: this.netParams.networkKeySequenceNumber, + micLen: 4, + }, + undefined, // use pre-hashed this.netParams.networkKey, + ); + const ackMACFrame = encodeMACFrameZigbee( + { + frameControl: { + frameType: MACFrameType.DATA, + securityEnabled: false, + framePending: false, + ackRequest: true, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: MACFrameAddressMode.SHORT, + frameVersion: MACFrameVersion.v2003, + sourceAddrMode: MACFrameAddressMode.SHORT, + }, + sequenceNumber: macHeader.sequenceNumber, + destinationPANId: macHeader.destinationPANId, + destination16: macHeader.source16, + // sourcePANId: undefined, // panIdCompression=true + source16: macHeader.destination16, + fcs: 0, + }, + ackNWKFrame, + ); + + await this.sendMACFrame(macHeader.sequenceNumber!, ackMACFrame, macHeader.source16, undefined); + } + + public async onZigbeeAPSFrame( + data: Buffer, + macHeader: MACHeader, + nwkHeader: ZigbeeNWKHeader, + apsHeader: ZigbeeAPSHeader, + rssi: number, + ): Promise { + switch (apsHeader.frameControl.frameType) { + case ZigbeeAPSFrameType.ACK: { + // ACKs should never contain a payload + // TODO: ? + break; + } + case ZigbeeAPSFrameType.DATA: + case ZigbeeAPSFrameType.INTERPAN: { + if (data.byteLength < 1) { + return; + } + + let processed = false; + + if (apsHeader.profileId === ZigbeeConsts.ZDO_PROFILE_ID) { + processed = await this.filterZDO(data, apsHeader.clusterId!, macHeader.source16!, nwkHeader.source16, nwkHeader.source64); + } + + if (!processed) { + if (nwkHeader.source16 === undefined && nwkHeader.source64 === undefined) { + logger.debug("<=~= APS Ignoring frame with no sender info", NS); + return; + } + + logger.debug( + () => + `<=== APS DATA[src=${nwkHeader.source16}:${nwkHeader.source64} seqNum=${nwkHeader.seqNum} profileId=${apsHeader.profileId} clusterId=${apsHeader.clusterId} srcEp=${apsHeader.sourceEndpoint} dstEp=${apsHeader.destEndpoint}]`, + NS, + ); + + setImmediate(() => { + // TODO: always lookup source64 if undef? + this.emit("FRAME", nwkHeader.source16, nwkHeader.source64, apsHeader, data, rssi); + }); + } + + break; + } + case ZigbeeAPSFrameType.CMD: { + await this.processZigbeeAPSCommandFrame(data, macHeader, nwkHeader, apsHeader); + break; + } + default: { + throw new Error(`Illegal frame type ${apsHeader.frameControl.frameType}`); + } + } + } + + public async processZigbeeAPSCommandFrame( + data: Buffer, + macHeader: MACHeader, + nwkHeader: ZigbeeNWKHeader, + apsHeader: ZigbeeAPSHeader, + ): Promise { + let offset = 0; + const cmdId = data.readUInt8(offset); + offset += 1; + + logger.debug( + () => + `<=== APS CMD[cmdId=${cmdId} macSource=${macHeader.source16}:${macHeader.source64} nwkSource=${nwkHeader.source16}:${nwkHeader.source64}]`, + NS, + ); + + switch (cmdId) { + case ZigbeeAPSCommandId.TRANSPORT_KEY: { + offset = this.processZigbeeAPSTransportKey(data, offset, macHeader, nwkHeader, apsHeader); + break; + } + case ZigbeeAPSCommandId.UPDATE_DEVICE: { + offset = this.processZigbeeAPSUpdateDevice(data, offset, macHeader, nwkHeader, apsHeader); + break; + } + case ZigbeeAPSCommandId.REMOVE_DEVICE: { + offset = this.processZigbeeAPSRemoveDevice(data, offset, macHeader, nwkHeader, apsHeader); + break; + } + case ZigbeeAPSCommandId.REQUEST_KEY: { + offset = await this.processZigbeeAPSRequestKey(data, offset, macHeader, nwkHeader, apsHeader); + break; + } + case ZigbeeAPSCommandId.SWITCH_KEY: { + offset = this.processZigbeeAPSSwitchKey(data, offset, macHeader, nwkHeader, apsHeader); + break; + } + case ZigbeeAPSCommandId.TUNNEL: { + offset = this.processZigbeeAPSTunnel(data, offset, macHeader, nwkHeader, apsHeader); + break; + } + case ZigbeeAPSCommandId.VERIFY_KEY: { + offset = await this.processZigbeeAPSVerifyKey(data, offset, macHeader, nwkHeader, apsHeader); + break; + } + case ZigbeeAPSCommandId.CONFIRM_KEY: { + offset = this.processZigbeeAPSConfirmKey(data, offset, macHeader, nwkHeader, apsHeader); + break; + } + case ZigbeeAPSCommandId.RELAY_MESSAGE_DOWNSTREAM: { + offset = this.processZigbeeAPSRelayMessageDownstream(data, offset, macHeader, nwkHeader, apsHeader); + break; + } + case ZigbeeAPSCommandId.RELAY_MESSAGE_UPSTREAM: { + offset = this.processZigbeeAPSRelayMessageUpstream(data, offset, macHeader, nwkHeader, apsHeader); + break; + } + } + + // excess data in packet + // if (offset < data.byteLength) { + // logger.debug(() => `<=== APS CMD contained more data: ${data.toString('hex')}`, NS); + // } + } + + /** + * 05-3474-R #4.4.11.1 + */ + public processZigbeeAPSTransportKey( + data: Buffer, + offset: number, + _macHeader: MACHeader, + _nwkHeader: ZigbeeNWKHeader, + _apsHeader: ZigbeeAPSHeader, + ): number { + const keyType = data.readUInt8(offset); + offset += 1; + const key = data.subarray(offset, offset + ZigbeeAPSConsts.CMD_KEY_LENGTH); + offset += ZigbeeAPSConsts.CMD_KEY_LENGTH; + + switch (keyType) { + case ZigbeeAPSConsts.CMD_KEY_STANDARD_NWK: + case ZigbeeAPSConsts.CMD_KEY_HIGH_SEC_NWK: { + const seqNum = data.readUInt8(offset); + offset += 1; + const destination = data.readBigUInt64LE(offset); + offset += 8; + const source = data.readBigUInt64LE(offset); + offset += 8; + + logger.debug( + () => `<=== APS TRANSPORT_KEY[keyType=${keyType} key=${key} seqNum=${seqNum} destination=${destination} source=${source}]`, + NS, + ); + + break; + } + case ZigbeeAPSConsts.CMD_KEY_TC_MASTER: + case ZigbeeAPSConsts.CMD_KEY_TC_LINK: { + const destination = data.readBigUInt64LE(offset); + offset += 8; + const source = data.readBigUInt64LE(offset); + offset += 8; + + // TODO + // const [tlvs, tlvsOutOffset] = decodeZigbeeAPSTLVs(data, offset); + + logger.debug(() => `<=== APS TRANSPORT_KEY[keyType=${keyType} key=${key} destination=${destination} source=${source}]`, NS); + break; + } + case ZigbeeAPSConsts.CMD_KEY_APP_MASTER: + case ZigbeeAPSConsts.CMD_KEY_APP_LINK: { + const partner = data.readBigUInt64LE(offset); + offset += 8; + const initiatorFlag = data.readUInt8(offset); + offset += 1; + + // TODO + // const [tlvs, tlvsOutOffset] = decodeZigbeeAPSTLVs(data, offset); + + logger.debug(() => `<=== APS TRANSPORT_KEY[keyType=${keyType} key=${key} partner=${partner} initiatorFlag=${initiatorFlag}]`, NS); + break; + } + } + + return offset; + } + + /** + * 05-3474-R #4.4.11.1 + * + * @param macDest16 + * @param nwkDest16 + * @param key SHALL contain the link key that SHOULD be used for APS encryption + * @param destination64 SHALL contain the address of the device which SHOULD use this link key + */ + public async sendZigbeeAPSTransportKeyTC(macDest16: number, nwkDest16: number, key: Buffer, destination64: bigint): Promise { + // TODO: tunneling support `, tunnelDest?: bigint` + // If the TunnelCommand parameter is TRUE, an APS Tunnel Command SHALL be constructed as described in section 4.6.3.7. + // It SHALL then be sent to the device specified by the TunnelAddress parameter by issuing an NLDE-DATA.request primitive. + logger.debug(() => `===> APS TRANSPORT_KEY_TC[key=${key.toString("hex")} destination64=${destination64}]`, NS); + + const finalPayload = Buffer.alloc(18 + ZigbeeAPSConsts.CMD_KEY_LENGTH); + let offset = 0; + finalPayload.writeUInt8(ZigbeeAPSCommandId.TRANSPORT_KEY, offset); + offset += 1; + finalPayload.writeUInt8(ZigbeeAPSConsts.CMD_KEY_TC_LINK, offset); + offset += 1; + finalPayload.set(key, offset); + offset += ZigbeeAPSConsts.CMD_KEY_LENGTH; + finalPayload.writeBigUInt64LE(destination64, offset); + offset += 8; + finalPayload.writeBigUInt64LE(this.netParams.eui64, offset); + offset += 8; + + // TODO + // const [tlvs, tlvsOutOffset] = encodeZigbeeAPSTLVs(); + + // encryption NWK=true, APS=true + await this.sendZigbeeAPSCommand( + ZigbeeAPSCommandId.TRANSPORT_KEY, + finalPayload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + true, // nwkSecurity + nwkDest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + { + control: { + level: ZigbeeSecurityLevel.NONE, + keyId: ZigbeeKeyType.LOAD, + nonce: true, + }, + frameCounter: this.nextTCKeyFrameCounter(), + source64: this.netParams.eui64, + // keySeqNum: undefined, only for keyId NWK + micLen: 4, + }, // apsSecurityHeader + ); + } + + /** + * 05-3474-R #4.4.11.1 #4.4.11.1.3.2 + * + * @param macDest16 + * @param nwkDest16 + * @param key SHALL contain a network key + * @param seqNum SHALL contain the sequence number associated with this network key + * @param destination64 SHALL contain the address of the device which SHOULD use this network key + * If the network key is sent to a broadcast address, the destination address subfield SHALL be set to the all-zero string and SHALL be ignored upon reception. + */ + public async sendZigbeeAPSTransportKeyNWK( + macDest16: number, + nwkDest16: number, + key: Buffer, + seqNum: number, + destination64: bigint, + ): Promise { + // TODO: tunneling support `, tunnelDest?: bigint` + logger.debug(() => `===> APS TRANSPORT_KEY_NWK[key=${key.toString("hex")} seqNum=${seqNum} destination64=${destination64}]`, NS); + + const finalPayload = Buffer.alloc(19 + ZigbeeAPSConsts.CMD_KEY_LENGTH); + let offset = 0; + finalPayload.writeUInt8(ZigbeeAPSCommandId.TRANSPORT_KEY, offset); + offset += 1; + finalPayload.writeUInt8(ZigbeeAPSConsts.CMD_KEY_STANDARD_NWK, offset); + offset += 1; + finalPayload.set(key, offset); + offset += ZigbeeAPSConsts.CMD_KEY_LENGTH; + finalPayload.writeUInt8(seqNum, offset); + offset += 1; + finalPayload.writeBigUInt64LE(destination64, offset); + offset += 8; + finalPayload.writeBigUInt64LE(this.netParams.eui64, offset); // 0xFFFFFFFFFFFFFFFF in distributed network (no TC) + offset += 8; + + // see 05-3474-23 #4.4.1.5 + // Conversely, a device receiving an APS transport key command MAY choose whether or not APS encryption is required. + // This is most often done during initial joining. + // For example, during joining a device that has no preconfigured link key would only accept unencrypted transport key messages, + // while a device with a preconfigured link key would only accept a transport key APS encrypted with its preconfigured key. + + // encryption NWK=true, APS=false + // await this.sendZigbeeAPSCommand( + // ZigbeeAPSCommandId.TRANSPORT_KEY, + // finalPayload, + // macDest16, // macDest16 + // ZigbeeNWKRouteDiscovery.SUPPRESS, + // true, // nwkSecurity + // nwkDest16, // nwkDest16 + // undefined, // nwkDest64 + // ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + // undefined, // apsSecurityHeader + // ); + + // encryption NWK=false, APS=true + await this.sendZigbeeAPSCommand( + ZigbeeAPSCommandId.TRANSPORT_KEY, + finalPayload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + false, // nwkSecurity + nwkDest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + { + control: { + level: ZigbeeSecurityLevel.NONE, + keyId: ZigbeeKeyType.TRANSPORT, + nonce: true, + }, + frameCounter: this.nextTCKeyFrameCounter(), + source64: this.netParams.eui64, + // keySeqNum: undefined, only for keyId NWK + micLen: 4, + }, // apsSecurityHeader + true, // disableACKRequest TODO: follows sniffed but not spec? + ); + } + + /** + * 05-3474-R #4.4.11.1 #4.4.11.1.3.3 + * + * @param macDest16 + * @param nwkDest16 + * @param key SHALL contain a link key that is shared with the device identified in the partner address sub-field + * @param partner SHALL contain the address of the other device that was sent this link key + * @param initiatorFlag SHALL be set to 1 if the device receiving this packet requested this key. Otherwise, this sub-field SHALL be set to 0. + */ + public async sendZigbeeAPSTransportKeyAPP( + macDest16: number, + nwkDest16: number, + key: Buffer, + partner: bigint, + initiatorFlag: boolean, + ): Promise { + // TODO: tunneling support `, tunnelDest?: bigint` + logger.debug(() => `===> APS TRANSPORT_KEY_APP[key=${key.toString("hex")} partner=${partner} initiatorFlag=${initiatorFlag}]`, NS); + + const finalPayload = Buffer.alloc(11 + ZigbeeAPSConsts.CMD_KEY_LENGTH); + let offset = 0; + finalPayload.writeUInt8(ZigbeeAPSCommandId.TRANSPORT_KEY, offset); + offset += 1; + finalPayload.writeUInt8(ZigbeeAPSConsts.CMD_KEY_APP_LINK, offset); + offset += 1; + finalPayload.set(key, offset); + offset += ZigbeeAPSConsts.CMD_KEY_LENGTH; + finalPayload.writeBigUInt64LE(partner, offset); + offset += 8; + finalPayload.writeUInt8(initiatorFlag ? 1 : 0, offset); + offset += 1; + + // TODO + // const [tlvs, tlvsOutOffset] = encodeZigbeeAPSTLVs(); + + await this.sendZigbeeAPSCommand( + ZigbeeAPSCommandId.TRANSPORT_KEY, + finalPayload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + true, // nwkSecurity + nwkDest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + { + control: { + level: ZigbeeSecurityLevel.NONE, + keyId: ZigbeeKeyType.LOAD, + nonce: true, + }, + frameCounter: this.nextTCKeyFrameCounter(), + source64: this.netParams.eui64, + // keySeqNum: undefined, only for keyId NWK + micLen: 4, + }, // apsSecurityHeader + ); + } + + /** + * 05-3474-R #4.4.11.2 + */ + public processZigbeeAPSUpdateDevice( + data: Buffer, + offset: number, + _macHeader: MACHeader, + _nwkHeader: ZigbeeNWKHeader, + _apsHeader: ZigbeeAPSHeader, + ): number { + const device64 = data.readBigUInt64LE(offset); + offset += 8; + // ZigBee 2006 and later + const device16 = data.readUInt16LE(offset); + offset += 2; + const status = data.readUInt8(offset); + offset += 1; + + // TODO + // const [tlvs, tlvsOutOffset] = decodeZigbeeAPSTLVs(data, offset); + + logger.debug(() => `<=== APS UPDATE_DEVICE[device64=${device64} device16=${device16} status=${status}]`, NS); + + return offset; + } + + /** + * 05-3474-R #4.4.11.2 + * + * @param macDest16 + * @param nwkDest16 device that SHALL be sent the update information + * @param device64 device whose status is being updated + * @param device16 device whose status is being updated + * @param status Indicates the updated status of the device given by the device64 parameter: + * - 0x00 = Standard Device Secured Rejoin + * - 0x01 = Standard Device Unsecured Join + * - 0x02 = Device Left + * - 0x03 = Standard Device Trust Center Rejoin + * - 0x04 – 0x07 = Reserved + * @param tlvs as relayed during Network Commissioning + */ + public async sendZigbeeAPSUpdateDevice( + macDest16: number, + nwkDest16: number, + device64: bigint, + device16: number, + status: number, + // tlvs: unknown[], + ): Promise { + logger.debug(() => `===> APS UPDATE_DEVICE[device64=${device64} device16=${device16} status=${status}]`, NS); + + const finalPayload = Buffer.alloc(12 /* + TLVs */); + let offset = 0; + finalPayload.writeUInt8(ZigbeeAPSCommandId.UPDATE_DEVICE, offset); + offset += 1; + finalPayload.writeBigUInt64LE(device64, offset); + offset += 8; + finalPayload.writeUInt16LE(device16, offset); + offset += 2; + finalPayload.writeUInt8(status, offset); + offset += 1; + + // TODO TLVs + + await this.sendZigbeeAPSCommand( + ZigbeeAPSCommandId.UPDATE_DEVICE, + finalPayload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + true, // nwkSecurity + nwkDest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + undefined, // apsSecurityHeader + ); + } + + /** + * 05-3474-R #4.4.11.3 + */ + public processZigbeeAPSRemoveDevice( + data: Buffer, + offset: number, + _macHeader: MACHeader, + _nwkHeader: ZigbeeNWKHeader, + _apsHeader: ZigbeeAPSHeader, + ): number { + const target = data.readBigUInt64LE(offset); + offset += 8; + + logger.debug(() => `<=== APS REMOVE_DEVICE[target=${target}]`, NS); + + return offset; + } + + /** + * 05-3474-R #4.4.11.3 + * + * @param macDest16 + * @param nwkDest16 parent + * @param target64 + */ + public async sendZigbeeAPSRemoveDevice(macDest16: number, nwkDest16: number, target64: bigint): Promise { + logger.debug(() => `===> APS REMOVE_DEVICE[target64=${target64}]`, NS); + + const finalPayload = Buffer.alloc(9); + let offset = 0; + finalPayload.writeUInt8(ZigbeeAPSCommandId.REMOVE_DEVICE, offset); + offset += 1; + finalPayload.writeBigUInt64LE(target64, offset); + offset += 8; + + await this.sendZigbeeAPSCommand( + ZigbeeAPSCommandId.REMOVE_DEVICE, + finalPayload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + true, // nwkSecurity + nwkDest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + undefined, // apsSecurityHeader + ); + } + + /** + * 05-3474-R #4.4.11.4 #4.4.5.2.3 + */ + public async processZigbeeAPSRequestKey( + data: Buffer, + offset: number, + macHeader: MACHeader, + nwkHeader: ZigbeeNWKHeader, + apsHeader: ZigbeeAPSHeader, + ): Promise { + // ZigbeeAPSConsts.CMD_KEY_APP_MASTER || ZigbeeAPSConsts.CMD_KEY_TC_LINK + const keyType = data.readUInt8(offset); + offset += 1; + + // If the APS Command Request Key message is not APS encrypted, the device SHALL drop the message and no further processing SHALL be done. + if (!apsHeader.frameControl.security) { + return offset; + } + + const device64 = this.address16ToAddress64.get(nwkHeader.source16!); + + // don't send to unknown device + if (device64 !== undefined) { + // TODO: + // const deviceKeyPair = this.apsDeviceKeyPairSet.get(nwkHeader.source16!); + // if (!deviceKeyPair || deviceKeyPair.keyNegotiationMethod === 0x00 /* `APS Request Key` method */) { + + if (keyType === ZigbeeAPSConsts.CMD_KEY_APP_MASTER) { + const partner = data.readBigUInt64LE(offset); + offset += 8; + + logger.debug(() => `<=== APS REQUEST_KEY[keyType=${keyType} partner=${partner}]`, NS); + + if (this.trustCenterPolicies.allowAppKeyRequest === ApplicationKeyRequestPolicy.ALLOWED) { + await this.sendZigbeeAPSTransportKeyAPP( + macHeader.source16!, + nwkHeader.source16!, + this.getOrGenerateAPPLinkKey(nwkHeader.source16!, partner), + partner, + true, + ); + } + // TODO ApplicationKeyRequestPolicy.ONLY_APPROVED + } else if (keyType === ZigbeeAPSConsts.CMD_KEY_TC_LINK) { + logger.debug(() => `<=== APS REQUEST_KEY[keyType=${keyType}]`, NS); + + if (this.trustCenterPolicies.allowTCKeyRequest === TrustCenterKeyRequestPolicy.ALLOWED) { + await this.sendZigbeeAPSTransportKeyTC(macHeader.source16!, nwkHeader.source16!, this.netParams.tcKey, device64); + } + // TODO TrustCenterKeyRequestPolicy.ONLY_PROVISIONAL + // this.apsDeviceKeyPairSet => find deviceAddress === this.deviceTable.get(nwkHeader.source).address64 => check provisional or drop msg + } + } else { + logger.warning(`Received key request from unknown device source=${nwkHeader.source16}`, NS); + } + + return offset; + } + + /** + * 05-3474-R #4.4.11.4 + * + * @param macDest16 + * @param nwkDest16 + * @param keyType SHALL be set to the key being requested + * - 0x02: App link key + * - 0x04: TC link key + * @param partner64 When the RequestKeyType field is 2 (that is, an application key), + * the partner address field SHALL contain the extended 64-bit address of the partner device that SHALL be sent the key. + * Both the partner device and the device originating the request-key command will be sent the key. + */ + public async sendZigbeeAPSRequestKey(macDest16: number, nwkDest16: number, keyType: 0x02, partner64: bigint): Promise; + public async sendZigbeeAPSRequestKey(macDest16: number, nwkDest16: number, keyType: 0x04): Promise; + public async sendZigbeeAPSRequestKey(macDest16: number, nwkDest16: number, keyType: 0x02 | 0x04, partner64?: bigint): Promise { + logger.debug(() => `===> APS REQUEST_KEY[keyType=${keyType} partner64=${partner64}]`, NS); + + const hasPartner64 = keyType === ZigbeeAPSConsts.CMD_KEY_APP_MASTER; + const finalPayload = Buffer.alloc(2 + (hasPartner64 ? 8 : 0)); + let offset = 0; + finalPayload.writeUInt8(ZigbeeAPSCommandId.REQUEST_KEY, offset); + offset += 1; + finalPayload.writeUInt8(keyType, offset); + offset += 1; + + if (hasPartner64) { + finalPayload.writeBigUInt64LE(partner64!, offset); + offset += 8; + } + + await this.sendZigbeeAPSCommand( + ZigbeeAPSCommandId.REQUEST_KEY, + finalPayload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + true, // nwkSecurity + nwkDest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + undefined, // apsSecurityHeader + ); + } + + /** + * 05-3474-R #4.4.11.5 + */ + public processZigbeeAPSSwitchKey( + data: Buffer, + offset: number, + _macHeader: MACHeader, + _nwkHeader: ZigbeeNWKHeader, + _apsHeader: ZigbeeAPSHeader, + ): number { + const seqNum = data.readUInt8(offset); + offset += 1; + + logger.debug(() => `<=== APS SWITCH_KEY[seqNum=${seqNum}]`, NS); + + return offset; + } + + /** + * 05-3474-R #4.4.11.5 + * + * @param macDest16 + * @param nwkDest16 + * @param seqNum SHALL contain the sequence number identifying the network key to be made active. + */ + public async sendZigbeeAPSSwitchKey(macDest16: number, nwkDest16: number, seqNum: number): Promise { + logger.debug(() => `===> APS SWITCH_KEY[seqNum=${seqNum}]`, NS); + + const finalPayload = Buffer.from([ZigbeeAPSCommandId.SWITCH_KEY, seqNum]); + + await this.sendZigbeeAPSCommand( + ZigbeeAPSCommandId.SWITCH_KEY, + finalPayload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + true, // nwkSecurity + nwkDest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + undefined, // apsSecurityHeader + ); + } + + /** + * 05-3474-R #4.4.11.6 + */ + public processZigbeeAPSTunnel( + data: Buffer, + offset: number, + _macHeader: MACHeader, + _nwkHeader: ZigbeeNWKHeader, + _apsHeader: ZigbeeAPSHeader, + ): number { + const destination = data.readBigUInt64LE(offset); + offset += 8; + const tunneledAPSFrame = data.subarray(offset); + offset += tunneledAPSFrame.byteLength; + + logger.debug(() => `<=== APS TUNNEL[destination=${destination} tunneledAPSFrame=${tunneledAPSFrame}]`, NS); + + return offset; + } + + /** + * 05-3474-R #4.4.11.6 + * + * @param macDest16 + * @param nwkDest16 + * @param destination64 SHALL be the 64-bit extended address of the device that is to receive the tunneled command + * @param tAPSHeader + * @param tApsCmdFrame SHALL be the APS command frame to be sent to the destination + * @param tAPSMIC + */ + public async sendZigbeeAPSTunnel( + macDest16: number, + nwkDest16: number, + destination64: bigint, + tApsCmdFrame: Buffer, + tAPSMIC: Buffer, + ): Promise { + logger.debug(() => `===> APS TUNNEL[destination64=${destination64}]`, NS); + + const finalPayload = Buffer.alloc(28 + tApsCmdFrame.byteLength); + let offset = 0; + finalPayload.writeUInt8(ZigbeeAPSCommandId.TUNNEL, offset); + offset += 1; + finalPayload.writeBigUInt64LE(destination64, offset); + offset += 8; + // TODO ?? + offset = encodeZigbeeAPSHeader(finalPayload, offset, { + frameControl: { + frameType: ZigbeeAPSFrameType.DATA, + deliveryMode: ZigbeeAPSDeliveryMode.UNICAST, + ackFormat: false, + security: false, + ackRequest: false, + extendedHeader: false, + }, + counter: 0, // TODO: ?? + }); + // SHALL indicate that a link key was used and SHALL include the extended nonce field. + offset = encodeZigbeeSecurityHeader(finalPayload, offset, { + control: { + level: ZigbeeSecurityLevel.NONE, + keyId: ZigbeeKeyType.LINK, + nonce: true, + }, + frameCounter: 0, // TODO: ?? + source64: this.netParams.eui64, + micLen: 4, + }); + finalPayload.set(tApsCmdFrame, offset); + offset += tApsCmdFrame.byteLength; + finalPayload.set(tAPSMIC, offset); + offset += 4; + + await this.sendZigbeeAPSCommand( + ZigbeeAPSCommandId.TUNNEL, + finalPayload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + true, // nwkSecurity + nwkDest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + undefined, // apsSecurityHeader + ); + } + + /** + * 05-3474-R #4.4.11.7 + */ + public async processZigbeeAPSVerifyKey( + data: Buffer, + offset: number, + macHeader: MACHeader, + nwkHeader: ZigbeeNWKHeader, + _apsHeader: ZigbeeAPSHeader, + ): Promise { + const keyType = data.readUInt8(offset); + offset += 1; + const source = data.readBigUInt64LE(offset); + offset += 8; + const keyHash = data.subarray(offset, offset + ZigbeeAPSConsts.CMD_KEY_LENGTH); + offset += ZigbeeAPSConsts.CMD_KEY_LENGTH; + + if (macHeader.source16 !== ZigbeeMACConsts.BCAST_ADDR) { + logger.debug(() => `<=== APS VERIFY_KEY[keyType=${keyType} source=${source} keyHash=${keyHash.toString("hex")}]`, NS); + + if (keyType === ZigbeeAPSConsts.CMD_KEY_TC_LINK) { + // TODO: not valid if operating in distributed network + const status = this.tcVerifyKeyHash.equals(keyHash) ? 0x00 /* SUCCESS */ : 0xad; /* SECURITY_FAILURE */ + + await this.sendZigbeeAPSConfirmKey(macHeader.source16!, nwkHeader.source16!, status, keyType, source); + } else if (keyType === ZigbeeAPSConsts.CMD_KEY_APP_MASTER) { + // this is illegal for TC + await this.sendZigbeeAPSConfirmKey(macHeader.source16!, nwkHeader.source16!, 0xa3 /* ILLEGAL_REQUEST */, keyType, source); + } else { + await this.sendZigbeeAPSConfirmKey(macHeader.source16!, nwkHeader.source16!, 0xaa /* NOT_SUPPORTED */, keyType, source); + } + } + + return offset; + } + + /** + * 05-3474-R #4.4.11.7 + * + * @param macDest16 + * @param nwkDest16 + * @param keyType type of key being verified + * @param source64 SHALL be the 64-bit extended address of the partner device that the destination shares the link key with + * @param hash outcome of executing the specialized keyed hash function specified in section B.1.4 using a key with the 1-octet string ‘0x03’ as the input string + * The resulting value SHALL NOT be used as a key for encryption or decryption + */ + public async sendZigbeeAPSVerifyKey(macDest16: number, nwkDest16: number, keyType: number, source64: bigint, hash: Buffer): Promise { + logger.debug(() => `===> APS VERIFY_KEY[keyType=${keyType} source64=${source64} hash=${hash.toString("hex")}]`, NS); + + const finalPayload = Buffer.alloc(26); + let offset = 0; + finalPayload.writeUInt8(ZigbeeAPSCommandId.VERIFY_KEY, offset); + offset += 1; + finalPayload.writeUInt8(keyType, offset); + offset += 1; + finalPayload.writeBigUInt64LE(source64, offset); + offset += 8; + finalPayload.set(hash, offset); + offset += hash.byteLength; // 16 + + await this.sendZigbeeAPSCommand( + ZigbeeAPSCommandId.VERIFY_KEY, + finalPayload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + true, // nwkSecurity + nwkDest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + undefined, // apsSecurityHeader + ); + } + + /** + * 05-3474-R #4.4.11.8 + */ + public processZigbeeAPSConfirmKey( + data: Buffer, + offset: number, + _macHeader: MACHeader, + _nwkHeader: ZigbeeNWKHeader, + _apsHeader: ZigbeeAPSHeader, + ): number { + const status = data.readUInt8(offset); + offset += 8; + const keyType = data.readUInt8(offset); + offset += 1; + const destination = data.readBigUInt64LE(offset); + offset += 8; + + logger.debug(() => `<=== APS CONFIRM_KEY[status=${status} keyType=${keyType} destination=${destination}]`, NS); + + return offset; + } + + /** + * 05-3474-R #4.4.11.8 + * + * @param macDest16 + * @param nwkDest16 + * @param status 1-byte status code indicating the result of the operation. See Table 2.27 + * @param keyType the type of key being verified + * @param destination64 SHALL be the 64-bit extended address of the source device of the Verify-Key message + */ + public async sendZigbeeAPSConfirmKey( + macDest16: number, + nwkDest16: number, + status: number, + keyType: number, + destination64: bigint, + ): Promise { + logger.debug(() => `===> APS CONFIRM_KEY[status=${status} keyType=${keyType} destination64=${destination64}]`, NS); + + const finalPayload = Buffer.alloc(11); + let offset = 0; + finalPayload.writeUInt8(ZigbeeAPSCommandId.CONFIRM_KEY, offset); + offset += 1; + finalPayload.writeUInt8(status, offset); + offset += 1; + finalPayload.writeUInt8(keyType, offset); + offset += 1; + finalPayload.writeBigUInt64LE(destination64, offset); + offset += 8; + + await this.sendZigbeeAPSCommand( + ZigbeeAPSCommandId.CONFIRM_KEY, + finalPayload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + true, // nwkSecurity + nwkDest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + { + control: { + level: ZigbeeSecurityLevel.NONE, + keyId: ZigbeeKeyType.LINK, // XXX: TRANSPORT? + nonce: true, + }, + frameCounter: this.nextTCKeyFrameCounter(), + source64: this.netParams.eui64, + // keySeqNum: undefined, only for keyId NWK + micLen: 4, + }, // apsSecurityHeader + ); + + const device = this.deviceTable.get(destination64); + + // TODO: proper place? + if (device !== undefined && device.authorized === false) { + device.authorized = true; + } + } + + /** + * 05-3474-R #4.4.11.9 + */ + public processZigbeeAPSRelayMessageDownstream( + data: Buffer, + offset: number, + _macHeader: MACHeader, + _nwkHeader: ZigbeeNWKHeader, + _apsHeader: ZigbeeAPSHeader, + ): number { + // this includes only TLVs + + // This contains the EUI64 of the unauthorized neighbor that is the intended destination of the relayed message. + const destination64 = data.readBigUInt64LE(offset); + offset += 8; + // This contains the single APS message, or message fragment, to be relayed from the Trust Center to the Joining device. + // The message SHALL start with the APS Header of the intended recipient. + // const message = ??; + + logger.debug(() => `<=== APS RELAY_MESSAGE_DOWNSTREAM[destination64=${destination64}]`, NS); + + return offset; + } + + // TODO: send RELAY_MESSAGE_DOWNSTREAM + + /** + * 05-3474-R #4.4.11.10 + */ + public processZigbeeAPSRelayMessageUpstream( + data: Buffer, + offset: number, + _macHeader: MACHeader, + _nwkHeader: ZigbeeNWKHeader, + _apsHeader: ZigbeeAPSHeader, + ): number { + // this includes only TLVs + + // This contains the EUI64 of the unauthorized neighbor that is the source of the relayed message. + const source64 = data.readBigUInt64LE(offset); + offset += 8; + // This contains the single APS message, or message fragment, to be relayed from the joining device to the Trust Center. + // The message SHALL start with the APS Header of the intended recipient. + // const message = ??; + + logger.debug(() => `<=== APS RELAY_MESSAGE_UPSTREAM[source64=${source64}]`, NS); + + return offset; + } + + // TODO: send RELAY_MESSAGE_UPSTREAM + + // #endregion + + // #region Network Management + + //---- 05-3474-23 #2.5.4.6 + // Network Discovery, Get, and Set attributes (both requests and confirms) are mandatory + // Zigbee Coordinator: + // - The NWK Formation request and confirm, the NWK Leave request, NWK Leave indication, NWK Leave confirm, NWK Join indication, + // NWK Permit Joining request, NWK Permit Joining confirm, NWK Route Discovery request, and NWK Route Discovery confirm SHALL be supported. + // - The NWK Direct Join request and NWK Direct Join confirm MAY be supported. + // - The NWK Join request and the NWK Join confirm SHALL NOT be supported. + // NWK Sync request, indication and confirm plus NWK reset request and confirm plus NWK route discovery request and confirm SHALL be optional + // reception of the NWK Network Status indication SHALL be supported, but no action is required + + public getOrGenerateAPPLinkKey(_device16: number, _partner64: bigint): Buffer { + // TODO: whole mechanism + return this.netParams.tcKey; + } + + public async formNetwork(): Promise { + logger.info("======== Network starting ========", NS); + + // TODO: sanity checks? + await this.setProperty(writePropertyb(SpinelPropertyId.PHY_ENABLED, true)); + await this.setProperty(writePropertyC(SpinelPropertyId.PHY_CHAN, this.netParams.channel)); + + // -128 == disable + // TODO: ? + // await this.spinel.setProperty(writePropertyc(SpinelPropertyId.PHY_CCA_THRESHOLD, 10)); + + await this.setProperty(writePropertyc(SpinelPropertyId.PHY_TX_POWER, this.netParams.txPower)); + + await this.setProperty(writePropertyE(SpinelPropertyId.MAC_15_4_LADDR, this.netParams.eui64)); + await this.setProperty(writePropertyS(SpinelPropertyId.MAC_15_4_SADDR, ZigbeeConsts.COORDINATOR_ADDRESS)); + await this.setProperty(writePropertyS(SpinelPropertyId.MAC_15_4_PANID, this.netParams.panId)); + + await this.setProperty(writePropertyb(SpinelPropertyId.MAC_RX_ON_WHEN_IDLE_MODE, true)); + await this.setProperty(writePropertyb(SpinelPropertyId.MAC_RAW_STREAM_ENABLED, true)); + + logger.info("======== Network started ========", NS); + + this.networkUp = true; + } + + /** + * @param duration The length of time in seconds during which the trust center will allow joins. + * The value 0x00 and 0xff indicate that permission is disabled or enabled, respectively, without a specified time limit. + * 0xff is clamped to 0xfe for security reasons + * @param macAssociationPermit If true, also allow association on coordinator itself. If false, only change TC policies + */ + public allowJoins(duration: number, macAssociationPermit: boolean): void { + if (duration > 0) { + clearTimeout(this.permitJoinTimeout); + this.trustCenterPolicies.allowJoins = true; + this.trustCenterPolicies.allowRejoinsWithWellKnownKey = true; + this.macAssociationPermit = macAssociationPermit; + + this.permitJoinTimeout = setTimeout(this.disallowJoins.bind(this), Math.min(duration, 0xfe) * 1000); + } else { + this.disallowJoins(); + } + } + + /** + * Revert allowing joins (keeps `allowRejoinsWithWellKnownKey=true`). + */ + public disallowJoins(): void { + clearTimeout(this.permitJoinTimeout); + + this.trustCenterPolicies.allowJoins = false; + this.trustCenterPolicies.allowRejoinsWithWellKnownKey = true; + this.macAssociationPermit = false; + } + + /** + * @param source16 + * @param source64 Assumed valid if assocType === 0x00 + * @param assocType + * @param capabilities + * @param denyOverride + * @returns + */ + private async associate( + source16: number | undefined, + source64: bigint | undefined, + assocType: number, + capabilities: number, + denyOverride?: boolean, + ): Promise<[status: MACAssociationStatus | number, newNetwork16: number]> { + // 0xffff when not successful and should not be retried + let newNetwork16 = source16; + let status: MACAssociationStatus | number = MACAssociationStatus.SUCCESS; + + if (denyOverride) { + newNetwork16 = 0xffff; + status = MACAssociationStatus.PAN_ACCESS_DENIED; + } else { + if (assocType === 0x00) { + // initial join + if (this.trustCenterPolicies.allowJoins) { + if (source16 === undefined) { + // MAC join (no source16) + newNetwork16 = this.assignNetworkAddress(); + + if (newNetwork16 === 0xffff) { + status = MACAssociationStatus.PAN_FULL; + } + } else if (this.address16ToAddress64.get(source16) !== undefined) { + // join with already taken source16 + newNetwork16 = this.assignNetworkAddress(); + + if (newNetwork16 === 0xffff) { + status = MACAssociationStatus.PAN_FULL; + } else if (newNetwork16 !== source16) { + status = ZigbeeNWKConsts.ASSOC_STATUS_ADDR_CONFLICT; + } + } + } else { + newNetwork16 = 0xffff; + status = MACAssociationStatus.PAN_ACCESS_DENIED; + } + } else if (assocType === 0x01) { + // rejoin + // if rejoin, network address will be stored + // if (this.trustCenterPolicies.allowRejoinsWithWellKnownKey) { + // } + // TODO: handle rejoin from device that previously left and was removed from known devices (could conflict on 16) + } + } + + // something went wrong above + if (newNetwork16 === undefined) { + newNetwork16 = 0xffff; + status = MACAssociationStatus.PAN_ACCESS_DENIED; + } + + logger.debug( + () => + `DEVICE_JOINING[source16=${source16} newNetwork16=${newNetwork16} assocType=${assocType} capabilities=${capabilities}] replying with status=${status}`, + NS, + ); + + if (status === MACAssociationStatus.SUCCESS) { + if (assocType === 0x00) { + const rxOnWhenIdle = Boolean((capabilities & 0x08) >> 3); + + this.deviceTable.set(source64!, { + address16: newNetwork16, + rxOnWhenIdle, + // on initial join success, device is considered joined but unauthorized after MAC Assoc / NWK Commissioning response is sent + authorized: false, + // TODO + }); + this.address16ToAddress64.set(newNetwork16, source64!); + + if (!rxOnWhenIdle) { + this.indirectTransmissions.set(source64!, []); + } + } else if (assocType === 0x01) { + // TODO + } + } + + return [status, newNetwork16]; + } + + private disassociate(source16: number | undefined, source64: bigint | undefined): void { + if (source64 === undefined && source16 !== undefined) { + source64 = this.address16ToAddress64.get(source16); + } else if (source16 === undefined && source64 !== undefined) { + source16 = this.deviceTable.get(source64)?.address16; + } + + // sanity check + if (source16 !== undefined && source64 !== undefined) { + this.deviceTable.delete(source64); + this.address16ToAddress64.delete(source16); + this.indirectTransmissions.delete(source64); + + logger.debug(() => `DEVICE_LEFT[source16=${source16} source64=${source64}]`, NS); + + setImmediate(() => { + this.emit("DEVICE_LEFT", source16, source64); + }); + } + } + + // TODO: interference detection (& optionally auto channel changing) + + /** + * Check if ZDO message is aimed at coordinator, and if it should be emitted. + * @param data + * @param clusterId + * @param macDest16 + * @param nwkDest16 + * @param nwkDest64 + * @returns True if a request was sent and no further processing is needed + */ + private async filterZDO( + data: Buffer, + clusterId: number, + macDest16: number, + nwkDest16: number | undefined, + nwkDest64: bigint | undefined, + ): Promise { + let finalPayload: Buffer; + + switch (clusterId) { + case ZigbeeConsts.NETWORK_ADDRESS_REQUEST: { + if (data.readBigUInt64LE(1 /* skip seq num */) !== this.netParams.eui64) { + // target of ZDO req is not coordinator, but is request, ignore it + return true; + } + + // TODO: handle reportKids & index, this payload is only for 0, 0 + finalPayload = Buffer.from(this.configAttributes.address); // copy + break; + } + case ZigbeeConsts.IEEE_ADDRESS_REQUEST: { + if (data.readUInt16LE(1 /* skip seq num */) !== ZigbeeConsts.COORDINATOR_ADDRESS) { + // target of ZDO req is not coordinator, but is request, ignore it + return true; + } + + // TODO: handle reportKids & index, this payload is only for 0, 0 + finalPayload = Buffer.from(this.configAttributes.address); // copy + break; + } + case ZigbeeConsts.NODE_DESCRIPTOR_REQUEST: { + if (data.readUInt16LE(1 /* skip seq num */) !== ZigbeeConsts.COORDINATOR_ADDRESS) { + // target of ZDO req is not coordinator (nwk addr of interest), but is request, ignore it + return true; + } + + finalPayload = Buffer.from(this.configAttributes.nodeDescriptor); // copy + break; + } + case ZigbeeConsts.POWER_DESCRIPTOR_REQUEST: { + if (data.readUInt16LE(1 /* skip seq num */) !== ZigbeeConsts.COORDINATOR_ADDRESS) { + // target of ZDO req is not coordinator (nwk addr of interest), but is request, ignore it + return true; + } + + finalPayload = Buffer.from(this.configAttributes.powerDescriptor); // copy + break; + } + case ZigbeeConsts.SIMPLE_DESCRIPTOR_REQUEST: { + if (data.readUInt16LE(1 /* skip seq num */) !== ZigbeeConsts.COORDINATOR_ADDRESS) { + // target of ZDO req is not coordinator (nwk addr of interest), but is request, ignore it + return true; + } + + finalPayload = Buffer.from(this.configAttributes.simpleDescriptors); // copy + break; + } + case ZigbeeConsts.ACTIVE_ENDPOINTS_REQUEST: { + if (data.readUInt16LE(1 /* skip seq num */) !== ZigbeeConsts.COORDINATOR_ADDRESS) { + // target of ZDO req is not coordinator (nwk addr of interest), but is request, ignore it + return true; + } + + finalPayload = Buffer.from(this.configAttributes.activeEndpoints); // copy + break; + } + case ZigbeeConsts.END_DEVICE_ANNOUNCE: { + let offset = 1; // skip seq num + const address16 = data.readUInt16LE(offset); + offset += 2; + const address64 = data.readBigUInt64LE(offset); + offset += 8; + const capabilities = data.readUInt8(offset); + offset += 1; + + const device = this.deviceTable.get(address64); + + if (device) { + // just in case + device.rxOnWhenIdle = Boolean((capabilities & 0x08) >> 3); + + // TODO: ideally, this shouldn't trigger (prevents early interview process from app) until AFTER authorized=true + setImmediate(() => { + // if device is authorized, it means it completed the TC link key update, so, a rejoin + this.emit(device.authorized ? "DEVICE_REJOINED" : "DEVICE_JOINED", address16, address64); + }); + + return false; + } + + // unknown device, should have been added by `associate`, something's not right, ignore it + return true; + } + default: { + // REQUEST type shouldn't continue + return (clusterId & 0x8000) === 0; + } + } + + // increment and set the ZDO sequence number in outgoing payload + finalPayload[0] = this.nextZDOSeqNum(); + + logger.debug( + () => + `===> COORD_ZDO[seqNum=${finalPayload[0]} clusterId=${clusterId} macDest16=${macDest16} nwkDest16=${nwkDest16} nwkDest64=${nwkDest64}]`, + NS, + ); + + await this.sendZigbeeAPSData( + finalPayload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + nwkDest16, // nwkDest16 + nwkDest64, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + clusterId | 0x8000, // clusterId + ZigbeeConsts.ZDO_PROFILE_ID, // profileId + ZigbeeConsts.ZDO_ENDPOINT, // destEndpoint + ZigbeeConsts.ZDO_ENDPOINT, // sourceEndpoint + undefined, // group + ); + + return true; + } + + // #endregion + + // #region State Management + + /** + * Format is + * - network data: 1024 bytes + * - device count: 2 bytes + * - device data: 512 bytes * (device count) + */ + public async saveState(): Promise { + const state = Buffer.alloc(1024 + 2 + this.deviceTable.size * 512); + let offset = 0; + + state.writeBigUInt64LE(this.netParams.eui64, offset); + offset += 8; + state.writeUInt16LE(this.netParams.panId, offset); + offset += 2; + state.writeBigUInt64LE(this.netParams.extendedPANId, offset); + offset += 8; + state.writeUInt8(this.netParams.channel, offset); + offset += 1; + state.writeUInt8(this.netParams.nwkUpdateId, offset); + offset += 1; + state.writeInt8(this.netParams.txPower, offset); + offset += 1; + state.set(this.netParams.networkKey, offset); + offset += ZigbeeConsts.SEC_KEYSIZE; + state.writeUInt32LE(this.netParams.networkKeyFrameCounter + 1024, offset); // TODO: deal with jumping + offset += 4; + state.writeUInt8(this.netParams.networkKeySequenceNumber, offset); + offset += 1; + state.set(this.netParams.tcKey, offset); + offset += ZigbeeConsts.SEC_KEYSIZE; + state.writeUInt32LE(this.netParams.tcKeyFrameCounter + 1024, offset); // TODO: deal with jumping + offset += 4; + + // reserved + offset += 1024 - offset; // currently: 962 + + state.writeUInt16LE(this.deviceTable.size, offset); + offset += 2; + + for (const [device64, device] of this.deviceTable) { + state.writeBigUInt64LE(device64, offset); + offset += 8; + state.writeUInt16LE(device.address16, offset); + offset += 2; + state.writeUInt8(device.rxOnWhenIdle ? 1 : 0, offset); + offset += 1; + state.writeUInt8(device.authorized ? 1 : 0, offset); + offset += 1; + + // reserved + offset += 512 - 12; // currently: 500 + } + + await writeFile(this.savePath, state); + } + + public async loadState(): Promise { + try { + const state = await readFile(this.savePath); + let offset = 0; + + this.netParams.eui64 = state.readBigUInt64LE(offset); + offset += 8; + this.netParams.panId = state.readUInt16LE(offset); + offset += 2; + this.netParams.extendedPANId = state.readBigUInt64LE(offset); + offset += 8; + this.netParams.channel = state.readUInt8(offset); + offset += 1; + this.netParams.nwkUpdateId = state.readUInt8(offset); + offset += 1; + this.netParams.txPower = state.readInt8(offset); + offset += 1; + this.netParams.networkKey = state.subarray(offset, offset + ZigbeeConsts.SEC_KEYSIZE); + offset += ZigbeeConsts.SEC_KEYSIZE; + this.netParams.networkKeyFrameCounter = state.readUInt32LE(offset); + offset += 4; + this.netParams.networkKeySequenceNumber = state.readUInt8(offset); + offset += 1; + this.netParams.tcKey = state.subarray(offset, offset + ZigbeeConsts.SEC_KEYSIZE); + offset += ZigbeeConsts.SEC_KEYSIZE; + this.netParams.tcKeyFrameCounter = state.readUInt32LE(offset); + offset += 4; + + // reserved + offset += 1024 - offset; // currently: 962 + + const deviceCount = state.readUInt16LE(offset); + offset += 2; + + for (let i = 0; i < deviceCount; i++) { + const address64 = state.readBigUInt64LE(offset); + offset += 8; + const address16 = state.readUInt16LE(offset); + offset += 2; + const rxOnWhenIdle = Boolean(state.readUInt8(offset)); + offset += 1; + const authorized = Boolean(state.readUInt8(offset)); + offset += 1; + + // reserved + offset += 512 - 12; // currently: 500 + + this.deviceTable.set(address64, { + address16, + rxOnWhenIdle, + authorized, + }); + this.address16ToAddress64.set(address16, address64); + + if (!rxOnWhenIdle) { + this.indirectTransmissions.set(address64, []); + } + } + } catch { + // `this.savePath` does not exist, using constructor-given network params, do initial save + await this.saveState(); + } + + // pre-compure hashes for default keys for faster processing + registerDefaultHashedKeys( + makeKeyedHashByType(ZigbeeKeyType.LINK, this.netParams.tcKey), + makeKeyedHashByType(ZigbeeKeyType.NWK, this.netParams.networkKey), + makeKeyedHashByType(ZigbeeKeyType.TRANSPORT, this.netParams.tcKey), + makeKeyedHashByType(ZigbeeKeyType.LOAD, this.netParams.tcKey), + ); + + this.tcVerifyKeyHash = makeKeyedHash(this.netParams.tcKey, 0x03 /* input byte per spec for VERIFY_KEY */); + + const [address, nodeDescriptor, powerDescriptor, simpleDescriptors, activeEndpoints] = encodeCoordinatorDescriptors(this.netParams.eui64); + + this.configAttributes.address = address; + this.configAttributes.nodeDescriptor = nodeDescriptor; + this.configAttributes.powerDescriptor = powerDescriptor; + this.configAttributes.simpleDescriptors = simpleDescriptors; + this.configAttributes.activeEndpoints = activeEndpoints; + } + + // #endregion + + // #region Wrappers + + public async sendZDO(payload: Buffer, nwkDest16: number, nwkDest64: bigint | undefined, clusterId: number): Promise { + // increment and set the ZDO sequence number in outgoing payload + payload[0] = this.nextZDOSeqNum(); + const macDest16 = nwkDest16 < ZigbeeConsts.BCAST_MIN ? nwkDest16 : ZigbeeMACConsts.BCAST_ADDR; // TODO: from routing table + + logger.debug(() => `===> ZDO[seqNum=${payload[0]} clusterId=${clusterId} macDest16=${macDest16} nwkDest=${nwkDest16}:${nwkDest64}]`, NS); + + return await this.sendZigbeeAPSData( + payload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + nwkDest16, // nwkDest16 + nwkDest64, // nwkDest64 + macDest16 === ZigbeeMACConsts.BCAST_ADDR ? ZigbeeAPSDeliveryMode.BCAST : ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + clusterId, // clusterId + ZigbeeConsts.ZDO_PROFILE_ID, // profileId + ZigbeeConsts.ZDO_ENDPOINT, // destEndpoint + ZigbeeConsts.ZDO_ENDPOINT, // sourceEndpoint + undefined, // group + ); + } + + public async sendUnicast( + payload: Buffer, + profileId: number, + clusterId: number, + dest16: number, + dest64: bigint | undefined, + destEp: number, + sourceEp: number, + ): Promise { + const macDest16 = dest16; // TODO: from routing + + return await this.sendZigbeeAPSData( + payload, + macDest16, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + dest16, // nwkDest16 + dest64, // nwkDest64 + ZigbeeAPSDeliveryMode.UNICAST, // apsDeliveryMode + clusterId, // clusterId + profileId, // profileId + destEp, // destEndpoint + sourceEp, // sourceEndpoint + undefined, // group + ); + } + + public async sendMulticast( + payload: Buffer, + profileId: number, + clusterId: number, + dest16: number, + destEp: number, + sourceEp: number, + ): Promise { + return await this.sendZigbeeAPSData( + payload, + ZigbeeMACConsts.BCAST_ADDR, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + dest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.GROUP, // apsDeliveryMode + clusterId, // clusterId + profileId, // profileId + destEp, // destEndpoint + sourceEp, // sourceEndpoint + undefined, // group + ); + } + + public async sendBroadcast( + payload: Buffer, + profileId: number, + clusterId: number, + dest16: number, + destEp: number, + sourceEp: number, + ): Promise { + return await this.sendZigbeeAPSData( + payload, + ZigbeeMACConsts.BCAST_ADDR, // macDest16 + ZigbeeNWKRouteDiscovery.SUPPRESS, // nwkDiscoverRoute + dest16, // nwkDest16 + undefined, // nwkDest64 + ZigbeeAPSDeliveryMode.BCAST, // apsDeliveryMode + clusterId, // clusterId + profileId, // profileId + destEp, // destEndpoint + sourceEp, // sourceEndpoint + undefined, // group + ); + } + + // #endregion +} diff --git a/src/drivers/ot-rcp-parser.ts b/src/drivers/ot-rcp-parser.ts new file mode 100644 index 0000000..b363231 --- /dev/null +++ b/src/drivers/ot-rcp-parser.ts @@ -0,0 +1,63 @@ +import { Transform, type TransformCallback, type TransformOptions } from "node:stream"; + +import { HdlcReservedByte } from "../spinel/hdlc.js"; +import { logger } from "../utils/logger.js"; + +const NS = "ot-rcp-driver:parser"; + +export class OTRCPParser extends Transform { + private buffer: Buffer; + + public constructor(opts?: TransformOptions) { + super(opts); + + this.buffer = Buffer.alloc(0); + } + + override _transform(chunk: Buffer, _encoding: BufferEncoding, cb: TransformCallback): void { + let data = Buffer.concat([this.buffer, chunk]); + + if (data[0] !== HdlcReservedByte.FLAG) { + // discard data before FLAG + data = data.subarray(data.indexOf(HdlcReservedByte.FLAG)); + } + + let position: number = data.indexOf(HdlcReservedByte.FLAG, 1); + + while (position !== -1) { + const endPosition = position + 1; + + // ignore repeated successive flags + if (position > 1) { + const frame = data.subarray(0, endPosition); + + logger.debug(() => `<<< FRAME[${frame.toString("hex")}]`, NS); + + this.push(frame); + + // remove the frame from internal buffer (set below) + data = data.subarray(endPosition); + } else { + data = data.subarray(position); + } + + position = data.indexOf(HdlcReservedByte.FLAG, 1); + } + + this.buffer = data; + + cb(); + } + + /* v8 ignore start */ + override _flush(cb: TransformCallback): void { + if (this.buffer.byteLength > 0) { + this.push(this.buffer); + + this.buffer = Buffer.alloc(0); + } + + cb(); + } + /* v8 ignore stop */ +} diff --git a/src/drivers/ot-rcp-writer.ts b/src/drivers/ot-rcp-writer.ts new file mode 100644 index 0000000..b9d37e2 --- /dev/null +++ b/src/drivers/ot-rcp-writer.ts @@ -0,0 +1,17 @@ +import { Readable } from "node:stream"; + +import { logger } from "../utils/logger.js"; + +const NS = "ot-rcp-driver:writer"; + +export class OTRCPWriter extends Readable { + public writeBuffer(buffer: Buffer): void { + logger.debug(() => `>>> FRAME[${buffer.toString("hex")}]`, NS); + + // this.push(buffer); + this.emit("data", buffer); // XXX: this is faster + } + + /* v8 ignore next */ + public override _read(): void {} +} diff --git a/src/spinel/commands.ts b/src/spinel/commands.ts new file mode 100644 index 0000000..d7f9629 --- /dev/null +++ b/src/spinel/commands.ts @@ -0,0 +1,304 @@ +export const enum SpinelCommandId { + /** + * No-Operation command (Host -> NCP) + * + * Encoding: Empty + * + * Induces the NCP to send a success status back to the host. This is + * primarily used for liveliness checks. The command payload for this + * command SHOULD be empty. + * + * There is no error condition for this command. + */ + NOOP = 0, + + /** + * Reset NCP command (Host -> NCP) + * + * Encoding: Empty or `C` + * + * Causes the NCP to perform a software reset. Due to the nature of + * this command, the TID is ignored. The host should instead wait + * for a `CMD_PROP_VALUE_IS` command from the NCP indicating + * `PROP_LAST_STATUS` has been set to `STATUS_RESET_SOFTWARE`. + * + * The optional command payload specifies the reset type, can be + * `SPINEL_RESET_PLATFORM`, `SPINEL_RESET_STACK`, or + * `SPINEL_RESET_BOOTLOADER`. + * + * Defaults to stack reset if unspecified. + * + * If an error occurs, the value of `PROP_LAST_STATUS` will be emitted + * instead with the value set to the generated status code for the error. + */ + RESET = 1, + + /** + * Get property value command (Host -> NCP) + * + * Encoding: `i` + * `i` : Property Id + * + * Causes the NCP to emit a `CMD_PROP_VALUE_IS` command for the + * given property identifier. + * + * The payload for this command is the property identifier encoded + * in the packed unsigned integer format `i`. + * + * If an error occurs, the value of `PROP_LAST_STATUS` will be emitted + * instead with the value set to the generated status code for the error. + */ + PROP_VALUE_GET = 2, + + /** + * Set property value command (Host -> NCP) + * + * Encoding: `iD` + * `i` : Property Id + * `D` : Value (encoding depends on the property) + * + * Instructs the NCP to set the given property to the specific given + * value, replacing any previous value. + * + * The payload for this command is the property identifier encoded in the + * packed unsigned integer format, followed by the property value. The + * exact format of the property value is defined by the property. + * + * On success a `CMD_PROP_VALUE_IS` command is emitted either for the + * given property identifier with the set value, or for `PROP_LAST_STATUS` + * with value `LAST_STATUS_OK`. + * + * If an error occurs, the value of `PROP_LAST_STATUS` will be emitted + * with the value set to the generated status code for the error. + */ + PROP_VALUE_SET = 3, + + /** + * Insert value into property command (Host -> NCP) + * + * Encoding: `iD` + * `i` : Property Id + * `D` : Value (encoding depends on the property) + * + * Instructs the NCP to insert the given value into a list-oriented + * property without removing other items in the list. The resulting order + * of items in the list is defined by the individual property being + * operated on. + * + * The payload for this command is the property identifier encoded in the + * packed unsigned integer format, followed by the value to be inserted. + * The exact format of the value is defined by the property. + * + * If the type signature of the property consists of a single structure + * enclosed by an array `A(t(...))`, then the contents of value MUST + * contain the contents of the structure (`...`) rather than the + * serialization of the whole item (`t(...)`). Specifically, the length + * of the structure MUST NOT be prepended to value. This helps to + * eliminate redundant data. + * + * On success, either a `CMD_PROP_VALUE_INSERTED` command is emitted for + * the given property, or a `CMD_PROP_VALUE_IS` command is emitted of + * property `PROP_LAST_STATUS` with value `LAST_STATUS_OK`. + * + * If an error occurs, the value of `PROP_LAST_STATUS` will be emitted + * with the value set to the generated status code for the error. + */ + PROP_VALUE_INSERT = 4, + + /** + * Remove value from property command (Host -> NCP) + * + * Encoding: `iD` + * `i` : Property Id + * `D` : Value (encoding depends on the property) + + * Instructs the NCP to remove the given value from a list-oriented property, + * without affecting other items in the list. The resulting order of items + * in the list is defined by the individual property being operated on. + * + * Note that this command operates by value, not by index! + * + * The payload for this command is the property identifier encoded in the + * packed unsigned integer format, followed by the value to be removed. The + * exact format of the value is defined by the property. + * + * If the type signature of the property consists of a single structure + * enclosed by an array `A(t(...))`, then the contents of value MUST contain + * the contents of the structure (`...`) rather than the serialization of the + * whole item (`t(...)`). Specifically, the length of the structure MUST NOT + * be prepended to `VALUE`. This helps to eliminate redundant data. + * + * On success, either a `CMD_PROP_VALUE_REMOVED` command is emitted for the + * given property, or a `CMD_PROP_VALUE_IS` command is emitted of property + * `PROP_LAST_STATUS` with value `LAST_STATUS_OK`. + * + * If an error occurs, the value of `PROP_LAST_STATUS` will be emitted + * with the value set to the generated status code for the error. + */ + PROP_VALUE_REMOVE = 5, + + /** + * Property value notification command (NCP -> Host) + * + * Encoding: `iD` + * `i` : Property Id + * `D` : Value (encoding depends on the property) + * + * This command can be sent by the NCP in response to a previous command + * from the host, or it can be sent by the NCP in an unsolicited fashion + * to notify the host of various state changes asynchronously. + * + * The payload for this command is the property identifier encoded in the + * packed unsigned integer format, followed by the current value of the + * given property. + */ + PROP_VALUE_IS = 6, + + /** + * Property value insertion notification command (NCP -> Host) + * + * Encoding:`iD` + * `i` : Property Id + * `D` : Value (encoding depends on the property) + * + * This command can be sent by the NCP in response to the + * `CMD_PROP_VALUE_INSERT` command, or it can be sent by the NCP in an + * unsolicited fashion to notify the host of various state changes + * asynchronously. + * + * The payload for this command is the property identifier encoded in the + * packed unsigned integer format, followed by the value that was inserted + * into the given property. + * + * If the type signature of the property specified by property id consists + * of a single structure enclosed by an array (`A(t(...))`), then the + * contents of value MUST contain the contents of the structure (`...`) + * rather than the serialization of the whole item (`t(...)`). Specifically, + * the length of the structure MUST NOT be prepended to `VALUE`. This + * helps to eliminate redundant data. + * + * The resulting order of items in the list is defined by the given + * property. + */ + PROP_VALUE_INSERTED = 7, + + /** + * Property value removal notification command (NCP -> Host) + * + * Encoding: `iD` + * `i` : Property Id + * `D` : Value (encoding depends on the property) + * + * This command can be sent by the NCP in response to the + * `CMD_PROP_VALUE_REMOVE` command, or it can be sent by the NCP in an + * unsolicited fashion to notify the host of various state changes + * asynchronously. + * + * Note that this command operates by value, not by index! + * + * The payload for this command is the property identifier encoded in the + * packed unsigned integer format described in followed by the value that + * was removed from the given property. + * + * If the type signature of the property specified by property id consists + * of a single structure enclosed by an array (`A(t(...))`), then the + * contents of value MUST contain the contents of the structure (`...`) + * rather than the serialization of the whole item (`t(...)`). Specifically, + * the length of the structure MUST NOT be prepended to `VALUE`. This + * helps to eliminate redundant data. + * + * The resulting order of items in the list is defined by the given + * property. + */ + PROP_VALUE_REMOVED = 8, + + NET_SAVE = 9, // Deprecated + + /** + * Clear saved network settings command (Host -> NCP) + * + * Encoding: Empty + * + * Erases all network credentials and state from non-volatile memory. + * + * This operation affects non-volatile memory only. The current network + * information stored in volatile memory is unaffected. + * + * The response to this command is always a `CMD_PROP_VALUE_IS` for + * `PROP_LAST_STATUS`, indicating the result of the operation. + */ + NET_CLEAR = 10, + + NET_RECALL = 11, // Deprecated + + /** + * Host buffer offload is an optional NCP capability that, when + * present, allows the NCP to store data buffers on the host processor + * that can be recalled at a later time. + * + * The presence of this feature can be detected by the host by + * checking for the presence of the `CAP_HBO` + * capability in `PROP_CAPS`. + * + * This feature is not currently supported on OpenThread. + */ + + HBO_OFFLOAD = 12, + HBO_RECLAIM = 13, + HBO_DROP = 14, + HBO_OFFLOADED = 15, + HBO_RECLAIMED = 16, + HBO_DROPPED = 17, + + /** + * Peek command (Host -> NCP) + * + * Encoding: `LU` + * `L` : The address to peek + * `U` : Number of bytes to read + * + * This command allows the NCP to fetch values from the RAM of the NCP + * for debugging purposes. Upon success, `CMD_PEEK_RET` is sent from the + * NCP to the host. Upon failure, `PROP_LAST_STATUS` is emitted with + * the appropriate error indication. + * + * The NCP MAY prevent certain regions of memory from being accessed. + * + * This command requires the capability `CAP_PEEK_POKE` to be present. + */ + PEEK = 18, + + /** + * Peek return command (NCP -> Host) + * + * Encoding: `LUD` + * `L` : The address peeked + * `U` : Number of bytes read + * `D` : Memory content + * + * This command contains the contents of memory that was requested by + * a previous call to `CMD_PEEK`. + * + * This command requires the capability `CAP_PEEK_POKE` to be present. + */ + PEEK_RET = 19, + + /** + * Poke command (Host -> NCP) + * + * Encoding: `LUD` + * `L` : The address to be poked + * `U` : Number of bytes to write + * `D` : Content to write + * + * This command writes the bytes to the specified memory address + * for debugging purposes. + * + * This command requires the capability `CAP_PEEK_POKE` to be present. + */ + POKE = 20, + + PROP_VALUE_MULTI_GET = 21, + PROP_VALUE_MULTI_SET = 22, + PROP_VALUES_ARE = 23, +} diff --git a/src/spinel/hdlc.ts b/src/spinel/hdlc.ts new file mode 100644 index 0000000..71d279e --- /dev/null +++ b/src/spinel/hdlc.ts @@ -0,0 +1,157 @@ +export const enum HdlcReservedByte { + XON = 0x11, + XOFF = 0x13, + FLAG = 0x7e, + ESCAPE = 0x7d, + FLAG_SPECIAL = 0xf8, +} + +/** Initial FCS value */ +const HDLC_INIT_FCS = 0xffff; +/** Good FCS value. */ +export const HDLC_GOOD_FCS = 0xf0b8; +/** FCS size (number of bytes) */ +const HDLC_FCS_SIZE = 2; +const HDLC_ESCAPE_XOR = 0x20; + +export const HDLC_TX_CHUNK_SIZE = 2048; + +export type HdlcFrame = { + data: Buffer; + /** For decoded frames, this stops before FCS+FLAG */ + length: number; + /** Final value should match HDLC_GOOD_FCS */ + fcs: number; +}; + +export function hdlcByteNeedsEscape(aByte: number): boolean { + return ( + aByte === HdlcReservedByte.XON || + aByte === HdlcReservedByte.XOFF || + aByte === HdlcReservedByte.ESCAPE || + aByte === HdlcReservedByte.FLAG || + aByte === HdlcReservedByte.FLAG_SPECIAL + ); +} + +const HDLC_FCS_TABLE = [ + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, + 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, + 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, + 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, + 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, + 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, + 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, + 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, + 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, + 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, + 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, + 0x3de3, 0x2c6a, 0x1ef1, 0x0f78, +]; + +export function updateFcs(aFcs: number, aByte: number): number { + return ((aFcs >> 8) ^ HDLC_FCS_TABLE[(aFcs ^ aByte) & 0xff]) & 0xffff; +} + +export function decodeHdlcFrame(buffer: Buffer): HdlcFrame { + // sanity check + if (buffer.byteLength > HDLC_TX_CHUNK_SIZE) { + throw new Error("HDLC frame too long"); + } + + const hdlcFrame: HdlcFrame = { + // data can only be smaller than incoming buffer (removed flags/escapes) + data: Buffer.alloc(buffer.byteLength), + length: 0, + fcs: HDLC_INIT_FCS, + }; + + let lastWasEscape = false; + + for (let i = 0; i < buffer.byteLength; i++) { + const aByte = buffer[i]; + + if (aByte === HdlcReservedByte.FLAG) { + if (i > 0) { + if (hdlcFrame.length >= HDLC_FCS_SIZE && hdlcFrame.fcs === HDLC_GOOD_FCS) { + // walk back the FCS writes by ignoring them from data length + hdlcFrame.length -= 2; + } else { + throw new Error("HDLC parsing error"); + } + } + } else if (aByte === HdlcReservedByte.ESCAPE) { + lastWasEscape = true; + } else { + if (lastWasEscape) { + const newByte = aByte ^ HDLC_ESCAPE_XOR; + hdlcFrame.fcs = updateFcs(hdlcFrame.fcs, newByte); + + hdlcFrame.data[hdlcFrame.length] = newByte; + hdlcFrame.length += 1; + + lastWasEscape = false; + } else { + hdlcFrame.fcs = updateFcs(hdlcFrame.fcs, aByte); + + hdlcFrame.data[hdlcFrame.length] = aByte; + hdlcFrame.length += 1; + } + } + } + + return hdlcFrame; +} + +/** + * @returns The new offset after encoded byte is added + */ +export function encodeByte(hdlcFrame: HdlcFrame, aByte: number, dataOffset: number): number { + if (hdlcByteNeedsEscape(aByte)) { + hdlcFrame.data[dataOffset] = HdlcReservedByte.ESCAPE; + dataOffset += 1; + hdlcFrame.data[dataOffset] = aByte ^ HDLC_ESCAPE_XOR; + dataOffset += 1; + } else { + hdlcFrame.data[dataOffset] = aByte; + dataOffset += 1; + } + + hdlcFrame.fcs = updateFcs(hdlcFrame.fcs, aByte); + + return dataOffset; +} + +export function encodeHdlcFrame(buffer: Buffer): HdlcFrame { + // sanity check + if (buffer.byteLength > HDLC_TX_CHUNK_SIZE) { + throw new Error("HDLC frame would be too long"); + } + + const hdlcFrame: HdlcFrame = { + // alloc to max possible size (as if each byte needs escaping) + data: Buffer.alloc(Math.min(buffer.byteLength * 2 + 6, HDLC_TX_CHUNK_SIZE)), + length: 0, + fcs: HDLC_INIT_FCS, + }; + hdlcFrame.data[hdlcFrame.length] = HdlcReservedByte.FLAG; + hdlcFrame.length += 1; + + for (const aByte of buffer) { + hdlcFrame.length = encodeByte(hdlcFrame, aByte, hdlcFrame.length); + } + + let fcs = hdlcFrame.fcs; + fcs ^= HDLC_INIT_FCS; + + hdlcFrame.length = encodeByte(hdlcFrame, fcs & 0xff, hdlcFrame.length); + hdlcFrame.length = encodeByte(hdlcFrame, (fcs >> 8) & 0xff, hdlcFrame.length); + + hdlcFrame.data[hdlcFrame.length] = HdlcReservedByte.FLAG; + hdlcFrame.length += 1; + + return hdlcFrame; +} diff --git a/src/spinel/properties.ts b/src/spinel/properties.ts new file mode 100644 index 0000000..80a593f --- /dev/null +++ b/src/spinel/properties.ts @@ -0,0 +1,3499 @@ +export const enum SpinelPropertyId { + /// Last Operation Status + /** Format: `i` - Read-only + * + * Describes the status of the last operation. Encoded as a packed + * unsigned integer (see `SPINEL_STATUS_*` for list of values). + * + * This property is emitted often to indicate the result status of + * pretty much any Host-to-NCP operation. + * + * It is emitted automatically at NCP startup with a value indicating + * the reset reason. It is also emitted asynchronously on an error ( + * e.g., NCP running out of buffer). + */ + LAST_STATUS = 0, + + /// Protocol Version + /** Format: `ii` - Read-only + * + * Describes the protocol version information. This property contains + * two fields, each encoded as a packed unsigned integer: + * `i`: Major Version Number + * `i`: Minor Version Number + * + * The version number is defined by `SPINEL_PROTOCOL_VERSION_THREAD_MAJOR` + * and `SPINEL_PROTOCOL_VERSION_THREAD_MINOR`. + * + * This specification describes major version 4, minor version 3. + */ + PROTOCOL_VERSION = 1, + + /// NCP Version + /** Format: `U` - Read-only + * + * Contains a string which describes the firmware currently running on + * the NCP. Encoded as a zero-terminated UTF-8 string. + */ + NCP_VERSION = 2, + + /// NCP Network Protocol Type + /** Format: 'i' - Read-only + * + * This value identifies what the network protocol for this NCP. + * The valid protocol type values are defined by enumeration + * `SPINEL_PROTOCOL_TYPE_*`: + * + * `SPINEL_PROTOCOL_TYPE_BOOTLOADER` = 0 + * `SPINEL_PROTOCOL_TYPE_ZIGBEE_IP` = 2, + * `SPINEL_PROTOCOL_TYPE_THREAD` = 3, + * + * OpenThread NCP supports only `SPINEL_PROTOCOL_TYPE_THREAD` + */ + INTERFACE_TYPE = 3, + + /// NCP Vendor ID + /** Format: 'i` - Read-only + * + * Vendor ID. Zero for unknown. + */ + VENDOR_ID = 4, + + /// NCP Capability List + /** Format: 'A(i)` - Read-only + * + * Describes the supported capabilities of this NCP. Encoded as a list of + * packed unsigned integers. + * + * The capability values are specified by SPINEL_CAP_* enumeration. + */ + CAPS = 5, + + /// NCP Interface Count + /** Format: 'C` - Read-only + * + * Provides number of interfaces. + */ + INTERFACE_COUNT = 6, + + POWER_STATE = 7, ///< PowerState [C] (deprecated, use `MCU_POWER_STATE` instead). + + /// NCP Hardware Address + /** Format: 'E` - Read-only + * + * The static EUI64 address of the device, used as a serial number. + */ + HWADDR = 8, + + LOCK = 9, ///< PropLock [b] (not supported) + HBO_MEM_MAX = 10, ///< Max offload mem [S] (not supported) + HBO_BLOCK_MAX = 11, ///< Max offload block [S] (not supported) + + /// Host Power State + /** Format: 'C` + * + * Describes the current power state of the host. This property is used + * by the host to inform the NCP when it has changed power states. The + * NCP can then use this state to determine which properties need + * asynchronous updates. Enumeration `spinel_host_power_state_t` defines + * the valid values (`SPINEL_HOST_POWER_STATE_*`): + * + * `HOST_POWER_STATE_OFFLINE`: Host is physically powered off and + * cannot be woken by the NCP. All asynchronous commands are + * squelched. + * + * `HOST_POWER_STATE_DEEP_SLEEP`: The host is in a low power state + * where it can be woken by the NCP but will potentially require more + * than two seconds to become fully responsive. The NCP MUST + * avoid sending unnecessary property updates, such as child table + * updates or non-critical messages on the debug stream. If the NCP + * needs to wake the host for traffic, the NCP MUST first take + * action to wake the host. Once the NCP signals to the host that it + * should wake up, the NCP MUST wait for some activity from the + * host (indicating that it is fully awake) before sending frames. + * + * `HOST_POWER_STATE_RESERVED`: This value MUST NOT be set by the host. If + * received by the NCP, the NCP SHOULD consider this as a synonym + * of `HOST_POWER_STATE_DEEP_SLEEP`. + * + * `HOST_POWER_STATE_LOW_POWER`: The host is in a low power state + * where it can be immediately woken by the NCP. The NCP SHOULD + * avoid sending unnecessary property updates, such as child table + * updates or non-critical messages on the debug stream. + * + * `HOST_POWER_STATE_ONLINE`: The host is awake and responsive. No + * special filtering is performed by the NCP on asynchronous updates. + * + * All other values are RESERVED. They MUST NOT be set by the + * host. If received by the NCP, the NCP SHOULD consider the value as + * a synonym of `HOST_POWER_STATE_LOW_POWER`. + * + * After setting this power state, any further commands from the host to + * the NCP will cause `HOST_POWER_STATE` to automatically revert to + * `HOST_POWER_STATE_ONLINE`. + * + * When the host is entering a low-power state, it should wait for the + * response from the NCP acknowledging the command (with `CMD_VALUE_IS`). + * Once that acknowledgment is received the host may enter the low-power + * state. + * + * If the NCP has the `CAP_UNSOL_UPDATE_FILTER` capability, any unsolicited + * property updates masked by `PROP_UNSOL_UPDATE_FILTER` should be honored + * while the host indicates it is in a low-power state. After resuming to the + * `HOST_POWER_STATE_ONLINE` state, the value of `PROP_UNSOL_UPDATE_FILTER` + * MUST be unchanged from the value assigned prior to the host indicating + * it was entering a low-power state. + */ + HOST_POWER_STATE = 12, + + /// NCP's MCU Power State + /** Format: 'C` + * Required capability: CAP_MCU_POWER_SAVE + * + * This property specifies the desired power state of NCP's micro-controller + * (MCU) when the underlying platform's operating system enters idle mode (i.e., + * all active tasks/events are processed and the MCU can potentially enter a + * energy-saving power state). + * + * The power state primarily determines how the host should interact with the NCP + * and whether the host needs an external trigger (a "poke") to NCP before it can + * communicate with the NCP or not. After a reset, the MCU power state MUST be + * SPINEL_MCU_POWER_STATE_ON. + * + * Enumeration `spinel_mcu_power_state_t` defines the valid values + * (`SPINEL_MCU_POWER_STATE_*` constants): + * + * `SPINEL_MCU_POWER_STATE_ON`: NCP's MCU stays on and active all the time. + * When the NCP's desired power state is set to this value, host can send + * messages to NCP without requiring any "poke" or external triggers. MCU is + * expected to stay on and active. Note that the `ON` power state only + * determines the MCU's power mode and is not related to radio's state. + * + * `SPINEL_MCU_POWER_STATE_LOW_POWER`: NCP's MCU can enter low-power + * (energy-saving) state. When the NCP's desired power state is set to + * `LOW_POWER`, host is expected to "poke" the NCP (e.g., an external trigger + * like an interrupt) before it can communicate with the NCP (send a message + * to the NCP). The "poke" mechanism is determined by the platform code (based + * on NCP's interface to the host). + * While power state is set to `LOW_POWER`, NCP can still (at any time) send + * messages to host. Note that receiving a message from the NCP does NOT + * indicate that the NCP's power state has changed, i.e., host is expected to + * continue to "poke" NCP when it wants to talk to the NCP until the power + * state is explicitly changed (by setting this property to `ON`). + * Note that the `LOW_POWER` power state only determines the MCU's power mode + * and is not related to radio's state. + * + * `SPINEL_MCU_POWER_STATE_OFF`: NCP is fully powered off. + * An NCP hardware reset (via a RESET pin) is required to bring the NCP back + * to `SPINEL_MCU_POWER_STATE_ON`. RAM is not retained after reset. + */ + MCU_POWER_STATE = 13, + + /// GPIO Configuration + /** Format: `A(CCU)` + * Type: Read-Only (Optionally Read-write using `CMD_PROP_VALUE_INSERT`) + * + * An array of structures which contain the following fields: + * + * * `C`: GPIO Number + * * `C`: GPIO Configuration Flags + * * `U`: Human-readable GPIO name + * + * GPIOs which do not have a corresponding entry are not supported. + * + * The configuration parameter contains the configuration flags for the + * GPIO: + * + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * |DIR|PUP|PDN|TRIGGER| RESERVED | + * +---+---+---+---+---+---+---+---+ + * |O/D| + * +---+ + * + * * `DIR`: Pin direction. Clear (0) for input, set (1) for output. + * * `PUP`: Pull-up enabled flag. + * * `PDN`/`O/D`: Flag meaning depends on pin direction: + * * Input: Pull-down enabled. + * * Output: Output is an open-drain. + * * `TRIGGER`: Enumeration describing how pin changes generate + * asynchronous notification commands (TBD) from the NCP to the host. + * * 0: Feature disabled for this pin + * * 1: Trigger on falling edge + * * 2: Trigger on rising edge + * * 3: Trigger on level change + * * `RESERVED`: Bits reserved for future use. Always cleared to zero + * and ignored when read. + * + * As an optional feature, the configuration of individual pins may be + * modified using the `CMD_PROP_VALUE_INSERT` command. Only the GPIO + * number and flags fields MUST be present, the GPIO name (if present) + * would be ignored. This command can only be used to modify the + * configuration of GPIOs which are already exposed---it cannot be used + * by the host to add additional GPIOs. + */ + GPIO_CONFIG = 0x1000 + 0, + + /// GPIO State Bitmask + /** Format: `D` + * Type: Read-Write + * + * Contains a bit field identifying the state of the GPIOs. The length of + * the data associated with these properties depends on the number of + * GPIOs. If you have 10 GPIOs, you'd have two bytes. GPIOs are numbered + * from most significant bit to least significant bit, so 0x80 is GPIO 0, + * 0x40 is GPIO 1, etc. + * + * For GPIOs configured as inputs: + * + * * `CMD_PROP_VALUE_GET`: The value of the associated bit describes the + * logic level read from the pin. + * * `CMD_PROP_VALUE_SET`: The value of the associated bit is ignored + * for these pins. + * + * For GPIOs configured as outputs: + * + * * `CMD_PROP_VALUE_GET`: The value of the associated bit is + * implementation specific. + * * `CMD_PROP_VALUE_SET`: The value of the associated bit determines + * the new logic level of the output. If this pin is configured as an + * open-drain, setting the associated bit to 1 will cause the pin to + * enter a Hi-Z state. + * + * For GPIOs which are not specified in `PROP_GPIO_CONFIG`: + * + * * `CMD_PROP_VALUE_GET`: The value of the associated bit is + * implementation specific. + * * `CMD_PROP_VALUE_SET`: The value of the associated bit MUST be + * ignored by the NCP. + * + * When writing, unspecified bits are assumed to be zero. + */ + GPIO_STATE = 0x1000 + 2, + + /// GPIO State Set-Only Bitmask + /** Format: `D` + * Type: Write-Only + * + * Allows for the state of various output GPIOs to be set without affecting + * other GPIO states. Contains a bit field identifying the output GPIOs that + * should have their state set to 1. + * + * When writing, unspecified bits are assumed to be zero. The value of + * any bits for GPIOs which are not specified in `PROP_GPIO_CONFIG` MUST + * be ignored. + */ + GPIO_STATE_SET = 0x1000 + 3, + + /// GPIO State Clear-Only Bitmask + /** Format: `D` + * Type: Write-Only + * + * Allows for the state of various output GPIOs to be cleared without affecting + * other GPIO states. Contains a bit field identifying the output GPIOs that + * should have their state cleared to 0. + * + * When writing, unspecified bits are assumed to be zero. The value of + * any bits for GPIOs which are not specified in `PROP_GPIO_CONFIG` MUST + * be ignored. + */ + GPIO_STATE_CLEAR = 0x1000 + 4, + + /// 32-bit random number from TRNG, ready-to-use. + TRNG_32 = 0x1000 + 5, + + /// 16 random bytes from TRNG, ready-to-use. + TRNG_128 = 0x1000 + 6, + + /// Raw samples from TRNG entropy source representing 32 bits of entropy. + TRNG_RAW_32 = 0x1000 + 7, + + /// NCP Unsolicited update filter + /** Format: `A(I)` + * Type: Read-Write (optional Insert-Remove) + * Required capability: `CAP_UNSOL_UPDATE_FILTER` + * + * Contains a list of properties which are excluded from generating + * unsolicited value updates. This property is empty after reset. + * In other words, the host may opt-out of unsolicited property updates + * for a specific property by adding that property id to this list. + * Hosts SHOULD NOT add properties to this list which are not + * present in `PROP_UNSOL_UPDATE_LIST`. If such properties are added, + * the NCP ignores the unsupported properties. + */ + UNSOL_UPDATE_FILTER = 0x1000 + 8, + + /// List of properties capable of generating unsolicited value update. + /** Format: `A(I)` + * Type: Read-Only + * Required capability: `CAP_UNSOL_UPDATE_FILTER` + * + * Contains a list of properties which are capable of generating + * unsolicited value updates. This list can be used when populating + * `PROP_UNSOL_UPDATE_FILTER` to disable all unsolicited property + * updates. + * + * This property is intended to effectively behave as a constant + * for a given NCP firmware. + */ + UNSOL_UPDATE_LIST = 0x1000 + 9, + + /** Format: `b` */ + PHY_ENABLED = 0x20 + 0, + /** Format: `C` */ + PHY_CHAN = 0x20 + 1, + /** Format: `A(C)` */ + PHY_CHAN_SUPPORTED = 0x20 + 2, + /** kHz Format: `L` */ + PHY_FREQ = 0x20 + 3, + /** dBm Format: `c` */ + PHY_CCA_THRESHOLD = 0x20 + 4, + /** Format: `c` */ + PHY_TX_POWER = 0x20 + 5, + /** dBm Format: `c` */ + PHY_RSSI = 0x20 + 6, + /** dBm Format: `c` */ + PHY_RX_SENSITIVITY = 0x20 + 7, + /** Format: `b` */ + PHY_PCAP_ENABLED = 0x20 + 8, + /** Format: `A(C)` */ + PHY_CHAN_PREFERRED = 0x20 + 9, + /** dBm Format: `c` */ + PHY_FEM_LNA_GAIN = 0x20 + 10, + + /// Signal the max power for a channel + /** Format: `Cc` + * + * First byte is the channel then the max transmit power, write-only. + */ + PHY_CHAN_MAX_POWER = 0x20 + 11, + /// Region code + /** Format: `S` + * + * The ascii representation of the ISO 3166 alpha-2 code. + */ + PHY_REGION_CODE = 0x20 + 12, + + /// Calibrated Power Table + /** Format: `A(Csd)` - Insert/Set + * + * The `Insert` command on the property inserts a calibration power entry to the calibrated power table. + * The `Set` command on the property with empty payload clears the calibrated power table. + * + * Structure Parameters: + * `C`: Channel. + * `s`: Actual power in 0.01 dBm. + * `d`: Raw power setting. + */ + PHY_CALIBRATED_POWER = 0x20 + 13, + + /// Target power for a channel + /** Format: `t(Cs)` - Write only + * + * Structure Parameters: + * `C`: Channel. + * `s`: Target power in 0.01 dBm. + */ + PHY_CHAN_TARGET_POWER = 0x20 + 14, + + /// Signal Jamming Detection Enable + /** Format: `b` + * + * Indicates if jamming detection is enabled or disabled. Set to true + * to enable jamming detection. + */ + JAM_DETECT_ENABLE = 0x1200 + 0, + + /// Signal Jamming Detected Indicator + /** Format: `b` (Read-Only) + * + * Set to true if radio jamming is detected. Set to false otherwise. + * + * When jamming detection is enabled, changes to the value of this + * property are emitted asynchronously via `CMD_PROP_VALUE_IS`. + */ + JAM_DETECTED = 0x1200 + 1, + + /// Jamming detection RSSI threshold + /** Format: `c` + * Units: dBm + * + * This parameter describes the threshold RSSI level (measured in + * dBm) above which the jamming detection will consider the + * channel blocked. + */ + JAM_DETECT_RSSI_THRESHOLD = 0x1200 + 2, + + /// Jamming detection window size + /** Format: `C` + * Units: Seconds (1-63) + * + * This parameter describes the window period for signal jamming + * detection. + */ + JAM_DETECT_WINDOW = 0x1200 + 3, + + /// Jamming detection busy period + /** Format: `C` + * Units: Seconds (1-63) + * + * This parameter describes the number of aggregate seconds within + * the detection window where the RSSI must be above + * `PROP_JAM_DETECT_RSSI_THRESHOLD` to trigger detection. + * + * The behavior of the jamming detection feature when `PROP_JAM_DETECT_BUSY` + * is larger than `PROP_JAM_DETECT_WINDOW` is undefined. + */ + JAM_DETECT_BUSY = 0x1200 + 4, + + /// Jamming detection history bitmap (for debugging) + /** Format: `X` (read-only) + * + * This value provides information about current state of jamming detection + * module for monitoring/debugging purpose. It returns a 64-bit value where + * each bit corresponds to one second interval starting with bit 0 for the + * most recent interval and bit 63 for the oldest intervals (63 sec earlier). + * The bit is set to 1 if the jamming detection module observed/detected + * high signal level during the corresponding one second interval. + */ + JAM_DETECT_HISTORY_BITMAP = 0x1200 + 5, + + /// Channel monitoring sample interval + /** Format: `L` (read-only) + * Units: Milliseconds + * + * Required capability: SPINEL_CAP_CHANNEL_MONITOR + * + * If channel monitoring is enabled and active, every sample interval, a + * zero-duration Energy Scan is performed, collecting a single RSSI sample + * per channel. The RSSI samples are compared with a pre-specified RSSI + * threshold. + */ + CHANNEL_MONITOR_SAMPLE_INTERVAL = 0x1200 + 6, + + /// Channel monitoring RSSI threshold + /** Format: `c` (read-only) + * Units: dBm + * + * Required capability: SPINEL_CAP_CHANNEL_MONITOR + * + * This value specifies the threshold used by channel monitoring module. + * Channel monitoring maintains the average rate of RSSI samples that + * are above the threshold within (approximately) a pre-specified number + * of samples (sample window). + */ + CHANNEL_MONITOR_RSSI_THRESHOLD = 0x1200 + 7, + + /// Channel monitoring sample window + /** Format: `L` (read-only) + * Units: Number of samples + * + * Required capability: SPINEL_CAP_CHANNEL_MONITOR + * + * The averaging sample window length (in units of number of channel + * samples) used by channel monitoring module. Channel monitoring will + * sample all channels every sample interval. It maintains the average rate + * of RSSI samples that are above the RSSI threshold within (approximately) + * the sample window. + */ + CHANNEL_MONITOR_SAMPLE_WINDOW = 0x1200 + 8, + + /// Channel monitoring sample count + /** Format: `L` (read-only) + * Units: Number of samples + * + * Required capability: SPINEL_CAP_CHANNEL_MONITOR + * + * Total number of RSSI samples (per channel) taken by the channel + * monitoring module since its start (since Thread network interface + * was enabled). + */ + CHANNEL_MONITOR_SAMPLE_COUNT = 0x1200 + 9, + + /// Channel monitoring channel occupancy + /** Format: `A(t(CU))` (read-only) + * + * Required capability: SPINEL_CAP_CHANNEL_MONITOR + * + * Data per item is: + * + * `C`: Channel + * `U`: Channel occupancy indicator + * + * The channel occupancy value represents the average rate/percentage of + * RSSI samples that were above RSSI threshold ("bad" RSSI samples) within + * (approximately) sample window latest RSSI samples. + * + * Max value of `0xffff` indicates all RSSI samples were above RSSI + * threshold (i.e. 100% of samples were "bad"). + */ + CHANNEL_MONITOR_CHANNEL_OCCUPANCY = 0x1200 + 10, + + /// Radio caps + /** Format: `i` (read-only) + * + * Data per item is: + * + * `i`: Radio Capabilities. + */ + RADIO_CAPS = 0x1200 + 11, + + /// All coex metrics related counters. + /** Format: t(LLLLLLLL)t(LLLLLLLLL)bL (Read-only) + * + * Required capability: SPINEL_CAP_RADIO_COEX + * + * The contents include two structures and two common variables, first structure corresponds to + * all transmit related coex counters, second structure provides the receive related counters. + * + * The transmit structure includes: + * 'L': NumTxRequest (The number of tx requests). + * 'L': NumTxGrantImmediate (The number of tx requests while grant was active). + * 'L': NumTxGrantWait (The number of tx requests while grant was inactive). + * 'L': NumTxGrantWaitActivated (The number of tx requests while grant was inactive that were + * ultimately granted). + * 'L': NumTxGrantWaitTimeout (The number of tx requests while grant was inactive that timed out). + * 'L': NumTxGrantDeactivatedDuringRequest (The number of tx requests that were in progress when grant was + * deactivated). + * 'L': NumTxDelayedGrant (The number of tx requests that were not granted within 50us). + * 'L': AvgTxRequestToGrantTime (The average time in usec from tx request to grant). + * + * The receive structure includes: + * 'L': NumRxRequest (The number of rx requests). + * 'L': NumRxGrantImmediate (The number of rx requests while grant was active). + * 'L': NumRxGrantWait (The number of rx requests while grant was inactive). + * 'L': NumRxGrantWaitActivated (The number of rx requests while grant was inactive that were + * ultimately granted). + * 'L': NumRxGrantWaitTimeout (The number of rx requests while grant was inactive that timed out). + * 'L': NumRxGrantDeactivatedDuringRequest (The number of rx requests that were in progress when grant was + * deactivated). + * 'L': NumRxDelayedGrant (The number of rx requests that were not granted within 50us). + * 'L': AvgRxRequestToGrantTime (The average time in usec from rx request to grant). + * 'L': NumRxGrantNone (The number of rx requests that completed without receiving grant). + * + * Two common variables: + * 'b': Stopped (Stats collection stopped due to saturation). + * 'L': NumGrantGlitch (The number of of grant glitches). + */ + RADIO_COEX_METRICS = 0x1200 + 12, + + /// Radio Coex Enable + /** Format: `b` + * + * Required capability: SPINEL_CAP_RADIO_COEX + * + * Indicates if radio coex is enabled or disabled. Set to true to enable radio coex. + */ + RADIO_COEX_ENABLE = 0x1200 + 13, + + /// MAC Scan State + /** Format: `C` + * + * Possible values are from enumeration `spinel_scan_state_t`. + * + * SCAN_STATE_IDLE + * SCAN_STATE_BEACON + * SCAN_STATE_ENERGY + * SCAN_STATE_DISCOVER + * + * Set to `SCAN_STATE_BEACON` to start an active scan. + * Beacons will be emitted from `PROP_MAC_SCAN_BEACON`. + * + * Set to `SCAN_STATE_ENERGY` to start an energy scan. + * Channel energy result will be reported by emissions + * of `PROP_MAC_ENERGY_SCAN_RESULT` (per channel). + * + * Set to `SCAN_STATE_DISCOVER` to start a Thread MLE discovery + * scan operation. Discovery scan result will be emitted from + * `PROP_MAC_SCAN_BEACON`. + * + * Value switches to `SCAN_STATE_IDLE` when scan is complete. + */ + MAC_SCAN_STATE = 0x30 + 0, + + /// MAC Scan Channel Mask + /** Format: `A(C)` + * + * List of channels to scan. + */ + MAC_SCAN_MASK = 0x30 + 1, + + /// MAC Scan Channel Period + /** Format: `S` + * Unit: milliseconds per channel + */ + MAC_SCAN_PERIOD = 0x30 + 2, + + /// MAC Scan Beacon + /** Format `Cct(ESSc)t(iCUdd)` - Asynchronous event only + * + * Scan beacons have two embedded structures which contain + * information about the MAC layer and the NET layer. Their + * format depends on the MAC and NET layer currently in use. + * The format below is for an 802.15.4 MAC with Thread: + * + * `C`: Channel + * `c`: RSSI of the beacon + * `t`: MAC layer properties (802.15.4 layer) + * `E`: Long address + * `S`: Short address + * `S`: PAN-ID + * `c`: LQI + * NET layer properties + * `i`: Protocol Number (SPINEL_PROTOCOL_TYPE_* values) + * `C`: Flags (SPINEL_BEACON_THREAD_FLAG_* values) + * `U`: Network Name + * `d`: XPANID + * `d`: Steering data + * + * Extra parameters may be added to each of the structures + * in the future, so care should be taken to read the length + * that prepends each structure. + */ + MAC_SCAN_BEACON = 0x30 + 3, + + /// MAC Long Address + /** Format: `E` + * + * The 802.15.4 long address of this node. + */ + MAC_15_4_LADDR = 0x30 + 4, + + /// MAC Short Address + /** Format: `S` + * + * The 802.15.4 short address of this node. + */ + MAC_15_4_SADDR = 0x30 + 5, + + /// MAC PAN ID + /** Format: `S` + * + * The 802.15.4 PANID this node is associated with. + */ + MAC_15_4_PANID = 0x30 + 6, + + /// MAC Stream Raw Enabled + /** Format: `b` + * + * Set to true to enable raw MAC frames to be emitted from + * `PROP_STREAM_RAW`. + */ + MAC_RAW_STREAM_ENABLED = 0x30 + 7, + + /// MAC Promiscuous Mode + /** Format: `C` + * + * Possible values are from enumeration + * `SPINEL_MAC_PROMISCUOUS_MODE_*`: + * + * `SPINEL_MAC_PROMISCUOUS_MODE_OFF` + * Normal MAC filtering is in place. + * + * `SPINEL_MAC_PROMISCUOUS_MODE_NETWORK` + * All MAC packets matching network are passed up + * the stack. + * + * `SPINEL_MAC_PROMISCUOUS_MODE_FULL` + * All decoded MAC packets are passed up the stack. + */ + MAC_PROMISCUOUS_MODE = 0x30 + 8, + + /// MAC Energy Scan Result + /** Format: `Cc` - Asynchronous event only + * + * This property is emitted during energy scan operation + * per scanned channel with following format: + * + * `C`: Channel + * `c`: RSSI (in dBm) + */ + MAC_ENERGY_SCAN_RESULT = 0x30 + 9, + + /// MAC Data Poll Period + /** Format: `L` + * Unit: millisecond + * The (user-specified) data poll (802.15.4 MAC Data Request) period + * in milliseconds. Value zero means there is no user-specified + * poll period, and the network stack determines the maximum period + * based on the MLE Child Timeout. + * + * If the value is non-zero, it specifies the maximum period between + * data poll transmissions. Note that the network stack may send data + * request transmissions more frequently when expecting a control-message + * (e.g., when waiting for an MLE Child ID Response). + */ + MAC_DATA_POLL_PERIOD = 0x30 + 10, + + /// MAC RxOnWhenIdle mode + /** Format: `b` + * + * Set to true to enable RxOnWhenIdle or false to disable it. + * When True, the radio is expected to stay in receive state during + * idle periods. When False, the radio is expected to switch to sleep + * state during idle periods. + */ + MAC_RX_ON_WHEN_IDLE_MODE = 0x30 + 11, + + /// MAC Alternate Short Address + /** Format: `S` + * + * The 802.15.4 alternate short address. + */ + MAC_15_4_ALT_SADDR = 0x30 + 12, + + /// MAC Receive At + /** Format: `XLC` + * + * Schedule a radio reception window at a specific time and duration. + * + * `X`: The receive window start time. + * `L`: The receive window duration. + * `C`: The receive channel. + */ + MAC_RX_AT = 0x30 + 13, + + /// MAC Allowlist + /** Format: `A(t(Ec))` + * Required capability: `CAP_MAC_ALLOWLIST` + * + * Structure Parameters: + * + * `E`: EUI64 address of node + * `c`: Optional RSSI-override value. The value 127 indicates + * that the RSSI-override feature is not enabled for this + * address. If this value is omitted when setting or + * inserting, it is assumed to be 127. This parameter is + * ignored when removing. + */ + MAC_ALLOWLIST = 0x1300 + 0, + + /// MAC Allowlist Enabled Flag + /** Format: `b` + * Required capability: `CAP_MAC_ALLOWLIST` + */ + MAC_ALLOWLIST_ENABLED = 0x1300 + 1, + + /// MAC Extended Address + /** Format: `E` + * + * Specified by Thread. Randomly-chosen, but non-volatile EUI-64. + */ + MAC_EXTENDED_ADDR = 0x1300 + 2, + + /// MAC Source Match Enabled Flag + /** Format: `b` + * Required Capability: SPINEL_CAP_MAC_RAW or SPINEL_CAP_CONFIG_RADIO + * + * Set to true to enable radio source matching or false to disable it. + * The source match functionality is used by radios when generating + * ACKs. The short and extended address lists are used for setting + * the Frame Pending bit in the ACKs. + */ + MAC_SRC_MATCH_ENABLED = 0x1300 + 3, + + /// MAC Source Match Short Address List + /** Format: `A(S)` + * Required Capability: SPINEL_CAP_MAC_RAW or SPINEL_CAP_CONFIG_RADIO + */ + MAC_SRC_MATCH_SHORT_ADDRESSES = 0x1300 + 4, + + /// MAC Source Match Extended Address List + /** Format: `A(E)` + * Required Capability: SPINEL_CAP_MAC_RAW or SPINEL_CAP_CONFIG_RADIO + */ + MAC_SRC_MATCH_EXTENDED_ADDRESSES = 0x1300 + 5, + + /// MAC Denylist + /** Format: `A(t(E))` + * Required capability: `CAP_MAC_ALLOWLIST` + * + * Structure Parameters: + * + * `E`: EUI64 address of node + */ + MAC_DENYLIST = 0x1300 + 6, + + /// MAC Denylist Enabled Flag + /** Format: `b` + * Required capability: `CAP_MAC_ALLOWLIST` + */ + MAC_DENYLIST_ENABLED = 0x1300 + 7, + + /// MAC Received Signal Strength Filter + /** Format: `A(t(Ec))` + * Required capability: `CAP_MAC_ALLOWLIST` + * + * Structure Parameters: + * + * * `E`: Optional EUI64 address of node. Set default RSS if not included. + * * `c`: Fixed RSS. 127 means not set. + */ + MAC_FIXED_RSS = 0x1300 + 8, + + /// The CCA failure rate + /** Format: `S` + * + * This property provides the current CCA (Clear Channel Assessment) failure rate. + * + * Maximum value `0xffff` corresponding to 100% failure rate. + */ + MAC_CCA_FAILURE_RATE = 0x1300 + 9, + + /// MAC Max direct retry number + /** Format: `C` + * + * The maximum (user-specified) number of direct frame transmission retries. + */ + MAC_MAX_RETRY_NUMBER_DIRECT = 0x1300 + 10, + + /// MAC Max indirect retry number + /** Format: `C` + * Required capability: `SPINEL_CAP_CONFIG_FTD` + * + * The maximum (user-specified) number of indirect frame transmission retries. + */ + MAC_MAX_RETRY_NUMBER_INDIRECT = 0x1300 + 11, + + /// Network Is Saved (Is Commissioned) + /** Format: `b` - Read only + * + * Returns true if there is a network state stored/saved. + */ + NET_SAVED = 0x40 + 0, + + /// Network Interface Status + /** Format `b` - Read-write + * + * Network interface up/down status. Write true to bring + * interface up and false to bring interface down. + */ + NET_IF_UP = 0x40 + 1, + + /// Thread Stack Operational Status + /** Format `b` - Read-write + * + * Thread stack operational status. Write true to start + * Thread stack and false to stop it. + */ + NET_STACK_UP = 0x40 + 2, + + /// Thread Device Role + /** Format `C` - Read-write + * + * Possible values are from enumeration `spinel_net_role_t` + * + * SPINEL_NET_ROLE_DETACHED = 0, + * SPINEL_NET_ROLE_CHILD = 1, + * SPINEL_NET_ROLE_ROUTER = 2, + * SPINEL_NET_ROLE_LEADER = 3, + * SPINEL_NET_ROLE_DISABLED = 4, + */ + NET_ROLE = 0x40 + 3, + + /// Thread Network Name + /** Format `U` - Read-write + */ + NET_NETWORK_NAME = 0x40 + 4, + + /// Thread Network Extended PAN ID + /** Format `D` - Read-write + */ + NET_XPANID = 0x40 + 5, + + /// Thread Network Key + /** Format `D` - Read-write + */ + NET_NETWORK_KEY = 0x40 + 6, + + /// Thread Network Key Sequence Counter + /** Format `L` - Read-write + */ + NET_KEY_SEQUENCE_COUNTER = 0x40 + 7, + + /// Thread Network Partition Id + /** Format `L` - Read-write + * + * The partition ID of the partition that this node is a + * member of. + */ + NET_PARTITION_ID = 0x40 + 8, + + /// Require Join Existing + /** Format: `b` + * Default Value: `false` + * + * This flag is typically used for nodes that are associating with an + * existing network for the first time. If this is set to `true` before + * `PROP_NET_STACK_UP` is set to `true`, the + * creation of a new partition at association is prevented. If the node + * cannot associate with an existing partition, `PROP_LAST_STATUS` will + * emit a status that indicates why the association failed and + * `PROP_NET_STACK_UP` will automatically revert to `false`. + * + * Once associated with an existing partition, this flag automatically + * reverts to `false`. + * + * The behavior of this property being set to `true` when + * `PROP_NET_STACK_UP` is already set to `true` is undefined. + */ + NET_REQUIRE_JOIN_EXISTING = 0x40 + 9, + + /// Thread Network Key Switch Guard Time + /** Format `L` - Read-write + */ + NET_KEY_SWITCH_GUARDTIME = 0x40 + 10, + + /// Thread Network PSKc + /** Format `D` - Read-write + */ + NET_PSKC = 0x40 + 11, + + /// Instruct NCP to leave the current network gracefully + /** Format Empty - Write only + */ + NET_LEAVE_GRACEFULLY = 0x40 + 12, + + /// Thread Leader IPv6 Address + /** Format `6` - Read only + */ + THREAD_LEADER_ADDR = 0x50 + 0, + + /// Thread Parent Info + /** Format: `ESLccCCCCC` - Read only + * + * `E`: Extended address + * `S`: RLOC16 + * `L`: Age (seconds since last heard from) + * `c`: Average RSS (in dBm) + * `c`: Last RSSI (in dBm) + * `C`: Link Quality In + * `C`: Link Quality Out + * `C`: Version + * `C`: CSL clock accuracy + * `C`: CSL uncertainty + */ + THREAD_PARENT = 0x50 + 1, + + /// Thread Child Table + /** Format: [A(t(ESLLCCcCc)] - Read only + * + * Data per item is: + * + * `E`: Extended address + * `S`: RLOC16 + * `L`: Timeout (in seconds) + * `L`: Age (in seconds) + * `L`: Network Data version + * `C`: Link Quality In + * `c`: Average RSS (in dBm) + * `C`: Mode (bit-flags) + * `c`: Last RSSI (in dBm) + */ + THREAD_CHILD_TABLE = 0x50 + 2, + + /// Thread Leader Router Id + /** Format `C` - Read only + * + * The router-id of the current leader. + */ + THREAD_LEADER_RID = 0x50 + 3, + + /// Thread Leader Weight + /** Format `C` - Read only + * + * The leader weight of the current leader. + */ + THREAD_LEADER_WEIGHT = 0x50 + 4, + + /// Thread Local Leader Weight + /** Format `C` - Read only + * + * The leader weight of this node. + */ + THREAD_LOCAL_LEADER_WEIGHT = 0x50 + 5, + + /// Thread Local Network Data + /** Format `D` - Read only + */ + THREAD_NETWORK_DATA = 0x50 + 6, + + /// Thread Local Network Data Version + /** Format `C` - Read only + */ + THREAD_NETWORK_DATA_VERSION = 0x50 + 7, + + /// Thread Local Stable Network Data + /** Format `D` - Read only + */ + THREAD_STABLE_NETWORK_DATA = 0x50 + 8, + + /// Thread Local Stable Network Data Version + /** Format `C` - Read only + */ + THREAD_STABLE_NETWORK_DATA_VERSION = 0x50 + 9, + + /// On-Mesh Prefixes + /** Format: `A(t(6CbCbSC))` + * + * Data per item is: + * + * `6`: IPv6 Prefix + * `C`: Prefix length in bits + * `b`: Stable flag + * `C`: TLV flags (SPINEL_NET_FLAG_* definition) + * `b`: "Is defined locally" flag. Set if this network was locally + * defined. Assumed to be true for set, insert and replace. Clear if + * the on mesh network was defined by another node. + * This field is ignored for INSERT and REMOVE commands. + * `S`: The RLOC16 of the device that registered this on-mesh prefix entry. + * This value is not used and ignored when adding an on-mesh prefix. + * This field is ignored for INSERT and REMOVE commands. + * `C`: TLV flags extended (additional field for Thread 1.2 features). + */ + THREAD_ON_MESH_NETS = 0x50 + 10, + + /// Off-mesh routes + /** Format: [A(t(6CbCbb))] + * + * Data per item is: + * + * `6`: Route Prefix + * `C`: Prefix length in bits + * `b`: Stable flag + * `C`: Route flags (SPINEL_ROUTE_FLAG_* and SPINEL_ROUTE_PREFERENCE_* definitions) + * `b`: "Is defined locally" flag. Set if this route info was locally + * defined as part of local network data. Assumed to be true for set, + * insert and replace. Clear if the route is part of partition's network + * data. + * `b`: "Next hop is this device" flag. Set if the next hop for the + * route is this device itself (i.e., route was added by this device) + * This value is ignored when adding an external route. For any added + * route the next hop is this device. + * `S`: The RLOC16 of the device that registered this route entry. + * This value is not used and ignored when adding a route. + */ + THREAD_OFF_MESH_ROUTES = 0x50 + 11, + + /// Thread Assisting Ports + /** Format `A(S)` + * + * Array of port numbers. + */ + THREAD_ASSISTING_PORTS = 0x50 + 12, + + /// Thread Allow Local Network Data Change + /** Format `b` - Read-write + * + * Set to true before changing local net data. Set to false when finished. + * This allows changes to be aggregated into a single event. + */ + THREAD_ALLOW_LOCAL_NET_DATA_CHANGE = 0x50 + 13, + + /// Thread Mode + /** Format: `C` + * + * This property contains the value of the mode + * TLV for this node. The meaning of the bits in this + * bit-field are defined by section 4.5.2 of the Thread + * specification. + * + * The values `SPINEL_THREAD_MODE_*` defines the bit-fields + */ + THREAD_MODE = 0x50 + 14, + + /// Thread Child Timeout + /** Format: `L` + * Unit: Seconds + * + * Used when operating in the Child role. + */ + THREAD_CHILD_TIMEOUT = 0x1500 + 0, + + /// Thread RLOC16 + /** Format: `S` + */ + THREAD_RLOC16 = 0x1500 + 1, + + /// Thread Router Upgrade Threshold + /** Format: `C` + */ + THREAD_ROUTER_UPGRADE_THRESHOLD = 0x1500 + 2, + + /// Thread Context Reuse Delay + /** Format: `L` + */ + THREAD_CONTEXT_REUSE_DELAY = 0x1500 + 3, + + /// Thread Network ID Timeout + /** Format: `C` + */ + THREAD_NETWORK_ID_TIMEOUT = 0x1500 + 4, + + /// List of active thread router ids + /** Format: `A(C)` + * + * Note that some implementations may not support CMD_GET_VALUE + * router ids, but may support CMD_REMOVE_VALUE when the node is + * a leader. + */ + THREAD_ACTIVE_ROUTER_IDS = 0x1500 + 5, + + /// Forward IPv6 packets that use RLOC16 addresses to HOST. + /** Format: `b` + * + * Allow host to directly observe all IPv6 packets received by the NCP, + * including ones sent to the RLOC16 address. + * + * Default is false. + */ + THREAD_RLOC16_DEBUG_PASSTHRU = 0x1500 + 6, + + /// Router Role Enabled + /** Format `b` + * + * Allows host to indicate whether or not the router role is enabled. + * If current role is a router, setting this property to `false` starts + * a re-attach process as an end-device. + */ + THREAD_ROUTER_ROLE_ENABLED = 0x1500 + 7, + + /// Thread Router Downgrade Threshold + /** Format: `C` + */ + THREAD_ROUTER_DOWNGRADE_THRESHOLD = 0x1500 + 8, + + /// Thread Router Selection Jitter + /** Format: `C` + */ + THREAD_ROUTER_SELECTION_JITTER = 0x1500 + 9, + + /// Thread Preferred Router Id + /** Format: `C` - Write only + * + * Specifies the preferred Router Id. Upon becoming a router/leader the node + * attempts to use this Router Id. If the preferred Router Id is not set or + * if it can not be used, a randomly generated router id is picked. This + * property can be set only when the device role is either detached or + * disabled. + */ + THREAD_PREFERRED_ROUTER_ID = 0x1500 + 10, + + /// Thread Neighbor Table + /** Format: `A(t(ESLCcCbLLc))` - Read only + * + * Data per item is: + * + * `E`: Extended address + * `S`: RLOC16 + * `L`: Age (in seconds) + * `C`: Link Quality In + * `c`: Average RSS (in dBm) + * `C`: Mode (bit-flags) + * `b`: `true` if neighbor is a child, `false` otherwise. + * `L`: Link Frame Counter + * `L`: MLE Frame Counter + * `c`: The last RSSI (in dBm) + */ + THREAD_NEIGHBOR_TABLE = 0x1500 + 11, + + /// Thread Max Child Count + /** Format: `C` + * + * Specifies the maximum number of children currently allowed. + * This parameter can only be set when Thread protocol operation + * has been stopped. + */ + THREAD_CHILD_COUNT_MAX = 0x1500 + 12, + + /// Leader Network Data + /** Format: `D` - Read only + */ + THREAD_LEADER_NETWORK_DATA = 0x1500 + 13, + + /// Stable Leader Network Data + /** Format: `D` - Read only + */ + THREAD_STABLE_LEADER_NETWORK_DATA = 0x1500 + 14, + + /// Thread Joiner Data + /** Format `A(T(ULE))` + * PSKd, joiner timeout, eui64 (optional) + * + * This property is being deprecated by MESHCOP_COMMISSIONER_JOINERS. + */ + THREAD_JOINERS = 0x1500 + 15, + + /// Thread Commissioner Enable + /** Format `b` + * + * Default value is `false`. + * + * This property is being deprecated by MESHCOP_COMMISSIONER_STATE. + */ + THREAD_COMMISSIONER_ENABLED = 0x1500 + 16, + + /// Thread TMF proxy enable + /** Format `b` + * Required capability: `SPINEL_CAP_THREAD_TMF_PROXY` + * + * This property is deprecated. + */ + THREAD_TMF_PROXY_ENABLED = 0x1500 + 17, + + /// Thread TMF proxy stream + /** Format `dSS` + * Required capability: `SPINEL_CAP_THREAD_TMF_PROXY` + * + * This property is deprecated. Please see `THREAD_UDP_FORWARD_STREAM`. + */ + THREAD_TMF_PROXY_STREAM = 0x1500 + 18, + + /// Thread "joiner" flag used during discovery scan operation + /** Format `b` + * + * This property defines the Joiner Flag value in the Discovery Request TLV. + * + * Default value is `false`. + */ + THREAD_DISCOVERY_SCAN_JOINER_FLAG = 0x1500 + 19, + + /// Enable EUI64 filtering for discovery scan operation. + /** Format `b` + * + * Default value is `false` + */ + THREAD_DISCOVERY_SCAN_ENABLE_FILTERING = 0x1500 + 20, + + /// PANID used for Discovery scan operation (used for PANID filtering). + /** Format: `S` + * + * Default value is 0xffff (Broadcast PAN) to disable PANID filtering + */ + THREAD_DISCOVERY_SCAN_PANID = 0x1500 + 21, + + /// Thread (out of band) steering data for MLE Discovery Response. + /** Format `E` - Write only + * + * Required capability: SPINEL_CAP_OOB_STEERING_DATA. + * + * Writing to this property allows to set/update the MLE + * Discovery Response steering data out of band. + * + * - All zeros to clear the steering data (indicating that + * there is no steering data). + * - All 0xFFs to set steering data/bloom filter to + * accept/allow all. + * - A specific EUI64 which is then added to current steering + * data/bloom filter. + */ + THREAD_STEERING_DATA = 0x1500 + 22, + + /// Thread Router Table. + /** Format: `A(t(ESCCCCCCb)` - Read only + * + * Data per item is: + * + * `E`: IEEE 802.15.4 Extended Address + * `S`: RLOC16 + * `C`: Router ID + * `C`: Next hop to router + * `C`: Path cost to router + * `C`: Link Quality In + * `C`: Link Quality Out + * `C`: Age (seconds since last heard) + * `b`: Link established with Router ID or not. + */ + THREAD_ROUTER_TABLE = 0x1500 + 23, + + /// Thread Active Operational Dataset + /** Format: `A(t(iD))` - Read-Write + * + * This property provides access to current Thread Active Operational Dataset. A Thread device maintains the + * Operational Dataset that it has stored locally and the one currently in use by the partition to which it is + * attached. This property corresponds to the locally stored Dataset on the device. + * + * Operational Dataset consists of a set of supported properties (e.g., channel, network key, network name, PAN id, + * etc). Note that not all supported properties may be present (have a value) in a Dataset. + * + * The Dataset value is encoded as an array of structs containing pairs of property key (as `i`) followed by the + * property value (as `D`). The property value must follow the format associated with the corresponding property. + * + * On write, any unknown/unsupported property keys must be ignored. + * + * The following properties can be included in a Dataset list: + * + * DATASET_ACTIVE_TIMESTAMP + * PHY_CHAN + * PHY_CHAN_SUPPORTED (Channel Mask Page 0) + * NET_NETWORK_KEY + * NET_NETWORK_NAME + * NET_XPANID + * MAC_15_4_PANID + * IPV6_ML_PREFIX + * NET_PSKC + * DATASET_SECURITY_POLICY + */ + THREAD_ACTIVE_DATASET = 0x1500 + 24, + + /// Thread Pending Operational Dataset + /** Format: `A(t(iD))` - Read-Write + * + * This property provide access to current locally stored Pending Operational Dataset. + * + * The formatting of this property follows the same rules as in THREAD_ACTIVE_DATASET. + * + * In addition supported properties in THREAD_ACTIVE_DATASET, the following properties can also + * be included in the Pending Dataset: + * + * DATASET_PENDING_TIMESTAMP + * DATASET_DELAY_TIMER + */ + THREAD_PENDING_DATASET = 0x1500 + 25, + + /// Send MGMT_SET Thread Active Operational Dataset + /** Format: `A(t(iD))` - Write only + * + * The formatting of this property follows the same rules as in THREAD_ACTIVE_DATASET. + * + * This is write-only property. When written, it triggers a MGMT_ACTIVE_SET meshcop command to be sent to leader + * with the given Dataset. The spinel frame response should be a `LAST_STATUS` with the status of the transmission + * of MGMT_ACTIVE_SET command. + * + * In addition to supported properties in THREAD_ACTIVE_DATASET, the following property can be + * included in the Dataset (to allow for custom raw TLVs): + * + * DATASET_RAW_TLVS + */ + THREAD_MGMT_SET_ACTIVE_DATASET = 0x1500 + 26, + + /// Send MGMT_SET Thread Pending Operational Dataset + /** Format: `A(t(iD))` - Write only + * + * This property is similar to THREAD_PENDING_DATASET and follows the same format and rules. + * + * In addition to supported properties in THREAD_PENDING_DATASET, the following property can be + * included the Dataset (to allow for custom raw TLVs to be provided). + * + * DATASET_RAW_TLVS + */ + THREAD_MGMT_SET_PENDING_DATASET = 0x1500 + 27, + + /// Operational Dataset Active Timestamp + /** Format: `X` - No direct read or write + * + * It can only be included in one of the Dataset related properties below: + * + * THREAD_ACTIVE_DATASET + * THREAD_PENDING_DATASET + * THREAD_MGMT_SET_ACTIVE_DATASET + * THREAD_MGMT_SET_PENDING_DATASET + * THREAD_MGMT_GET_ACTIVE_DATASET + * THREAD_MGMT_GET_PENDING_DATASET + */ + DATASET_ACTIVE_TIMESTAMP = 0x1500 + 28, + + /// Operational Dataset Pending Timestamp + /** Format: `X` - No direct read or write + * + * It can only be included in one of the Pending Dataset properties: + * + * THREAD_PENDING_DATASET + * THREAD_MGMT_SET_PENDING_DATASET + * THREAD_MGMT_GET_PENDING_DATASET + */ + DATASET_PENDING_TIMESTAMP = 0x1500 + 29, + + /// Operational Dataset Delay Timer + /** Format: `L` - No direct read or write + * + * Delay timer (in ms) specifies the time renaming until Thread devices overwrite the value in the Active + * Operational Dataset with the corresponding values in the Pending Operational Dataset. + * + * It can only be included in one of the Pending Dataset properties: + * + * THREAD_PENDING_DATASET + * THREAD_MGMT_SET_PENDING_DATASET + * THREAD_MGMT_GET_PENDING_DATASET + */ + DATASET_DELAY_TIMER = 0x1500 + 30, + + /// Operational Dataset Security Policy + /** Format: `SD` - No direct read or write + * + * It can only be included in one of the Dataset related properties below: + * + * THREAD_ACTIVE_DATASET + * THREAD_PENDING_DATASET + * THREAD_MGMT_SET_ACTIVE_DATASET + * THREAD_MGMT_SET_PENDING_DATASET + * THREAD_MGMT_GET_ACTIVE_DATASET + * THREAD_MGMT_GET_PENDING_DATASET + * + * Content is + * `S` : Key Rotation Time (in units of hour) + * `C` : Security Policy Flags (as specified in Thread 1.1 Section 8.10.1.15) + * `C` : Optional Security Policy Flags extension (as specified in Thread 1.2 Section 8.10.1.15). + * 0xf8 is used if this field is missing. + */ + DATASET_SECURITY_POLICY = 0x1500 + 31, + + /// Operational Dataset Additional Raw TLVs + /** Format: `D` - No direct read or write + * + * This property defines extra raw TLVs that can be added to an Operational DataSet. + * + * It can only be included in one of the following Dataset properties: + * + * THREAD_MGMT_SET_ACTIVE_DATASET + * THREAD_MGMT_SET_PENDING_DATASET + * THREAD_MGMT_GET_ACTIVE_DATASET + * THREAD_MGMT_GET_PENDING_DATASET + */ + DATASET_RAW_TLVS = 0x1500 + 32, + + /// Child table addresses + /** Format: `A(t(ESA(6)))` - Read only + * + * This property provides the list of all addresses associated with every child + * including any registered IPv6 addresses. + * + * Data per item is: + * + * `E`: Extended address of the child + * `S`: RLOC16 of the child + * `A(6)`: List of IPv6 addresses registered by the child (if any) + */ + THREAD_CHILD_TABLE_ADDRESSES = 0x1500 + 33, + + /// Neighbor Table Frame and Message Error Rates + /** Format: `A(t(ESSScc))` + * Required capability: `CAP_ERROR_RATE_TRACKING` + * + * This property provides link quality related info including + * frame and (IPv6) message error rates for all neighbors. + * + * With regards to message error rate, note that a larger (IPv6) + * message can be fragmented and sent as multiple MAC frames. The + * message transmission is considered a failure, if any of its + * fragments fail after all MAC retry attempts. + * + * Data per item is: + * + * `E`: Extended address of the neighbor + * `S`: RLOC16 of the neighbor + * `S`: Frame error rate (0 -> 0%, 0xffff -> 100%) + * `S`: Message error rate (0 -> 0%, 0xffff -> 100%) + * `c`: Average RSSI (in dBm) + * `c`: Last RSSI (in dBm) + */ + THREAD_NEIGHBOR_TABLE_ERROR_RATES = 0x1500 + 34, + + /// EID (Endpoint Identifier) IPv6 Address Cache Table + /** Format `A(t(6SCCt(bL6)t(bSS))) + * + * This property provides Thread EID address cache table. + * + * Data per item is: + * + * `6` : Target IPv6 address + * `S` : RLOC16 of target + * `C` : Age (order of use, 0 indicates most recently used entry) + * `C` : Entry state (values are defined by enumeration `SPINEL_ADDRESS_CACHE_ENTRY_STATE_*`). + * + * `t` : Info when state is `SPINEL_ADDRESS_CACHE_ENTRY_STATE_CACHED` + * `b` : Indicates whether last transaction time and ML-EID are valid. + * `L` : Last transaction time + * `6` : Mesh-local EID + * + * `t` : Info when state is other than `SPINEL_ADDRESS_CACHE_ENTRY_STATE_CACHED` + * `b` : Indicates whether the entry can be evicted. + * `S` : Timeout in seconds + * `S` : Retry delay (applicable if in query-retry state). + */ + THREAD_ADDRESS_CACHE_TABLE = 0x1500 + 35, + + /// Thread UDP forward stream + /** Format `dS6S` + * Required capability: `SPINEL_CAP_THREAD_UDP_FORWARD` + * + * This property helps exchange UDP packets with host. + * + * `d`: UDP payload + * `S`: Remote UDP port + * `6`: Remote IPv6 address + * `S`: Local UDP port + */ + THREAD_UDP_FORWARD_STREAM = 0x1500 + 36, + + /// Send MGMT_GET Thread Active Operational Dataset + /** Format: `A(t(iD))` - Write only + * + * The formatting of this property follows the same rules as in THREAD_MGMT_SET_ACTIVE_DATASET. This + * property further allows the sender to not include a value associated with properties in formatting of `t(iD)`, + * i.e., it should accept either a `t(iD)` or a `t(i)` encoding (in both cases indicating that the associated + * Dataset property should be requested as part of MGMT_GET command). + * + * This is write-only property. When written, it triggers a MGMT_ACTIVE_GET meshcop command to be sent to leader + * requesting the Dataset related properties from the format. The spinel frame response should be a `LAST_STATUS` + * with the status of the transmission of MGMT_ACTIVE_GET command. + * + * In addition to supported properties in THREAD_MGMT_SET_ACTIVE_DATASET, the following property can be + * optionally included in the Dataset: + * + * DATASET_DEST_ADDRESS + */ + THREAD_MGMT_GET_ACTIVE_DATASET = 0x1500 + 37, + + /// Send MGMT_GET Thread Pending Operational Dataset + /** Format: `A(t(iD))` - Write only + * + * The formatting of this property follows the same rules as in THREAD_MGMT_GET_ACTIVE_DATASET. + * + * This is write-only property. When written, it triggers a MGMT_PENDING_GET meshcop command to be sent to leader + * with the given Dataset. The spinel frame response should be a `LAST_STATUS` with the status of the transmission + * of MGMT_PENDING_GET command. + */ + THREAD_MGMT_GET_PENDING_DATASET = 0x1500 + 38, + + /// Operational Dataset (MGMT_GET) Destination IPv6 Address + /** Format: `6` - No direct read or write + * + * This property specifies the IPv6 destination when sending MGMT_GET command for either Active or Pending Dataset + * if not provided, Leader ALOC address is used as default. + * + * It can only be included in one of the MGMT_GET Dataset properties: + * + * THREAD_MGMT_GET_ACTIVE_DATASET + * THREAD_MGMT_GET_PENDING_DATASET + */ + DATASET_DEST_ADDRESS = 0x1500 + 39, + + /// Thread New Operational Dataset + /** Format: `A(t(iD))` - Read only - FTD build only + * + * This property allows host to request NCP to create and return a new Operation Dataset to use when forming a new + * network. + * + * Operational Dataset consists of a set of supported properties (e.g., channel, network key, network name, PAN id, + * etc). Note that not all supported properties may be present (have a value) in a Dataset. + * + * The Dataset value is encoded as an array of structs containing pairs of property key (as `i`) followed by the + * property value (as `D`). The property value must follow the format associated with the corresponding property. + * + * The following properties can be included in a Dataset list: + * + * DATASET_ACTIVE_TIMESTAMP + * PHY_CHAN + * PHY_CHAN_SUPPORTED (Channel Mask Page 0) + * NET_NETWORK_KEY + * NET_NETWORK_NAME + * NET_XPANID + * MAC_15_4_PANID + * IPV6_ML_PREFIX + * NET_PSKC + * DATASET_SECURITY_POLICY + */ + THREAD_NEW_DATASET = 0x1500 + 40, + + /// MAC CSL Period + /** Format: `L` + * Required capability: `SPINEL_CAP_THREAD_CSL_RECEIVER` + * + * The CSL period in microseconds. Value of 0 indicates that CSL should be disabled. + * + * The CSL period MUST be a multiple of 160 (which is 802.15 "ten symbols time"). + */ + THREAD_CSL_PERIOD = 0x1500 + 41, + + /// MAC CSL Timeout + /** Format: `L` + * Required capability: `SPINEL_CAP_THREAD_CSL_RECEIVER` + * + * The CSL timeout in seconds. + */ + THREAD_CSL_TIMEOUT = 0x1500 + 42, + + /// MAC CSL Channel + /** Format: `C` + * Required capability: `SPINEL_CAP_THREAD_CSL_RECEIVER` + * + * The CSL channel as described in chapter 4.6.5.1.2 of the Thread v1.2.0 Specification. + * Value of 0 means that CSL reception (if enabled) occurs on the Thread Network channel. + * Value from range [11,26] is an alternative channel on which a CSL reception occurs. + */ + THREAD_CSL_CHANNEL = 0x1500 + 43, + + /// Thread Domain Name + /** Format `U` - Read-write + * Required capability: `SPINEL_CAP_NET_THREAD_1_2` + * + * This property is available since Thread 1.2.0. + * Write to this property succeeds only when Thread protocols are disabled. + */ + THREAD_DOMAIN_NAME = 0x1500 + 44, + + /// Link metrics query + /** Format: `6CC` - Write-Only + * + * Required capability: `SPINEL_CAP_THREAD_LINK_METRICS` + * + * `6` : IPv6 destination address + * `C` : Series id (0 for Single Probe) + * `C` : List of requested metric ids encoded as bit fields in single byte + * + * +---------------+----+ + * | Metric | Id | + * +---------------+----+ + * | Received PDUs | 0 | + * | LQI | 1 | + * | Link margin | 2 | + * | RSSI | 3 | + * +---------------+----+ + * + * If the query succeeds, the NCP will send a result to the Host using + * @ref THREAD_LINK_METRICS_QUERY_RESULT. + */ + THREAD_LINK_METRICS_QUERY = 0x1500 + 45, + + /// Link metrics query result + /** Format: `6Ct(A(t(CD)))` - Unsolicited notifications only + * + * Required capability: `SPINEL_CAP_THREAD_LINK_METRICS` + * + * `6` : IPv6 destination address + * `C` : Status + * `t(A(t(CD)))` : Array of structs encoded as following: + * `C` : Metric id + * `D` : Metric value + * + * +---------------+----+----------------+ + * | Metric | Id | Value format | + * +---------------+----+----------------+ + * | Received PDUs | 0 | `L` (uint32_t) | + * | LQI | 1 | `C` (uint8_t) | + * | Link margin | 2 | `C` (uint8_t) | + * | RSSI | 3 | `c` (int8_t) | + * +---------------+----+----------------+ + */ + THREAD_LINK_METRICS_QUERY_RESULT = 0x1500 + 46, + + /// Link metrics probe + /** Format `6CC` - Write only + * Required capability: `SPINEL_CAP_THREAD_LINK_METRICS` + * + * Send a MLE Link Probe message to the peer. + * + * `6` : IPv6 destination address + * `C` : The Series ID for which this Probe message targets at + * `C` : The length of the Probe message, valid range: [0, 64] + */ + THREAD_LINK_METRICS_PROBE = 0x1500 + 47, + + /// Link metrics Enhanced-ACK Based Probing management + /** Format: 6Cd - Write only + * + * Required capability: `SPINEL_CAP_THREAD_LINK_METRICS` + * + * `6` : IPv6 destination address + * `C` : Indicate whether to register or clear the probing. `0` - clear, `1` - register + * `C` : List of requested metric ids encoded as bit fields in single byte + * + * +---------------+----+ + * | Metric | Id | + * +---------------+----+ + * | LQI | 1 | + * | Link margin | 2 | + * | RSSI | 3 | + * +---------------+----+ + * + * Result of configuration is reported asynchronously to the Host using the + * @ref THREAD_LINK_METRICS_MGMT_RESPONSE. + * + * Whenever Enh-ACK IE report is received it is passed to the Host using the + * @ref THREAD_LINK_METRICS_MGMT_ENH_ACK_IE property. + */ + THREAD_LINK_METRICS_MGMT_ENH_ACK = 0x1500 + 48, + + /// Link metrics Enhanced-ACK Based Probing IE report + /** Format: SEA(t(CD)) - Unsolicited notifications only + * + * Required capability: `SPINEL_CAP_THREAD_LINK_METRICS` + * + * `S` : Short address of the Probing Subject + * `E` : Extended address of the Probing Subject + * `t(A(t(CD)))` : Struct that contains array of structs encoded as following: + * `C` : Metric id + * `D` : Metric value + * + * +---------------+----+----------------+ + * | Metric | Id | Value format | + * +---------------+----+----------------+ + * | LQI | 1 | `C` (uint8_t) | + * | Link margin | 2 | `C` (uint8_t) | + * | RSSI | 3 | `c` (int8_t) | + * +---------------+----+----------------+ + */ + THREAD_LINK_METRICS_MGMT_ENH_ACK_IE = 0x1500 + 49, + + /// Link metrics Forward Tracking Series management + /** Format: 6CCC - Write only + * + * Required capability: `SPINEL_CAP_THREAD_LINK_METRICS` + * + * `6` : IPv6 destination address + * `C` : Series id + * `C` : Tracked frame types encoded as bit fields in single byte, if equal to zero, + * accounting is stopped and a series is removed + * `C` : Requested metric ids encoded as bit fields in single byte + * + * +------------------+----+ + * | Frame type | Id | + * +------------------+----+ + * | MLE Link Probe | 0 | + * | MAC Data | 1 | + * | MAC Data Request | 2 | + * | MAC ACK | 3 | + * +------------------+----+ + * + * +---------------+----+ + * | Metric | Id | + * +---------------+----+ + * | Received PDUs | 0 | + * | LQI | 1 | + * | Link margin | 2 | + * | RSSI | 3 | + * +---------------+----+ + * + * Result of configuration is reported asynchronously to the Host using the + * @ref THREAD_LINK_METRICS_MGMT_RESPONSE. + */ + THREAD_LINK_METRICS_MGMT_FORWARD = 0x1500 + 50, + + /// Link metrics management response + /** Format: 6C - Unsolicited notifications only + * + * Required capability: `SPINEL_CAP_THREAD_LINK_METRICS` + * + * `6` : IPv6 source address + * `C` : Received status + */ + THREAD_LINK_METRICS_MGMT_RESPONSE = 0x1500 + 51, + + /// Multicast Listeners Register Request + /** Format `t(A(6))A(t(CD))` - Write-only + * Required capability: `SPINEL_CAP_NET_THREAD_1_2` + * + * `t(A(6))`: Array of IPv6 multicast addresses + * `A(t(CD))`: Array of structs holding optional parameters as follows + * `C`: Parameter id + * `D`: Parameter value + * + * +----------------------------------------------------------------+ + * | Id: SPINEL_THREAD_MLR_PARAMID_TIMEOUT | + * | Type: `L` | + * | Description: Timeout in seconds. If this optional parameter is | + * | omitted, the default value of the BBR will be used. | + * | Special values: | + * | 0 causes given addresses to be removed | + * | 0xFFFFFFFF is permanent and persistent registration | + * +----------------------------------------------------------------+ + * + * Write to this property initiates update of Multicast Listeners Table on the primary BBR. + * If the write succeeded, the result of network operation will be notified later by the + * THREAD_MLR_RESPONSE property. If the write fails, no MLR.req is issued and + * notification through the THREAD_MLR_RESPONSE property will not occur. + */ + THREAD_MLR_REQUEST = 0x1500 + 52, + + /// Multicast Listeners Register Response + /** Format `CCt(A(6))` - Unsolicited notifications only + * Required capability: `SPINEL_CAP_NET_THREAD_1_2` + * + * `C`: Status + * `C`: MlrStatus (The Multicast Listener Registration Status) + * `A(6)`: Array of IPv6 addresses that failed to be updated on the primary BBR + * + * This property is notified asynchronously when the NCP receives MLR.rsp following + * previous write to the THREAD_MLR_REQUEST property. + */ + THREAD_MLR_RESPONSE = 0x1500 + 53, + + /// Interface Identifier specified for Thread Domain Unicast Address. + /** Format: `A(C)` - Read-write + * + * `A(C)`: Interface Identifier (8 bytes). + * + * Required capability: SPINEL_CAP_DUA + * + * If write to this property is performed without specified parameter + * the Interface Identifier of the Thread Domain Unicast Address will be cleared. + * If the DUA Interface Identifier is cleared on the NCP device, + * the get spinel property command will be returned successfully without specified parameter. + */ + THREAD_DUA_ID = 0x1500 + 54, + + /// Thread 1.2 Primary Backbone Router information in the Thread Network. + /** Format: `SSLC` - Read-Only + * + * Required capability: `SPINEL_CAP_NET_THREAD_1_2` + * + * `S`: Server. + * `S`: Reregistration Delay (in seconds). + * `L`: Multicast Listener Registration Timeout (in seconds). + * `C`: Sequence Number. + */ + THREAD_BACKBONE_ROUTER_PRIMARY = 0x1500 + 55, + + /// Thread 1.2 Backbone Router local state. + /** Format: `C` - Read-Write + * + * Required capability: `SPINEL_CAP_THREAD_BACKBONE_ROUTER` + * + * The valid values are specified by SPINEL_THREAD_BACKBONE_ROUTER_STATE_ enumeration. + * Backbone functionality will be disabled if SPINEL_THREAD_BACKBONE_ROUTER_STATE_DISABLED + * is written to this property, enabled otherwise. + */ + THREAD_BACKBONE_ROUTER_LOCAL_STATE = 0x1500 + 56, + + /// Local Thread 1.2 Backbone Router configuration. + /** Format: SLC - Read-Write + * + * Required capability: `SPINEL_CAP_THREAD_BACKBONE_ROUTER` + * + * `S`: Reregistration Delay (in seconds). + * `L`: Multicast Listener Registration Timeout (in seconds). + * `C`: Sequence Number. + */ + THREAD_BACKBONE_ROUTER_LOCAL_CONFIG = 0x1500 + 57, + + /// Register local Thread 1.2 Backbone Router configuration. + /** Format: Empty (Write only). + * + * Required capability: `SPINEL_CAP_THREAD_BACKBONE_ROUTER` + * + * Writing to this property (with any value) will register local Backbone Router configuration. + */ + THREAD_BACKBONE_ROUTER_LOCAL_REGISTER = 0x1500 + 58, + + /// Thread 1.2 Backbone Router registration jitter. + /** Format: `C` - Read-Write + * + * Required capability: `SPINEL_CAP_THREAD_BACKBONE_ROUTER` + * + * `C`: Backbone Router registration jitter. + */ + THREAD_BACKBONE_ROUTER_LOCAL_REGISTRATION_JITTER = 0x1500 + 59, + + /// Thread Active Operational Dataset in raw TLVs format. + /** Format: `D` - Read-Write + * + * This property provides access to the current Thread Active Operational Dataset. A Thread device maintains the + * Operational Dataset that it has stored locally and the one currently in use by the partition to which it is + * attached. This property corresponds to the locally stored Dataset on the device. + * + * On write, any unknown/unsupported TLVs must be ignored. + */ + THREAD_ACTIVE_DATASET_TLVS = 0x1500 + 60, + + /// Thread Pending Operational Dataset in raw TLVs format. + /** Format: `D` - Read-Write + * + * This property provides access to the current locally stored Pending Operational Dataset. + * + * The formatting of this property follows the same rules as in THREAD_ACTIVE_DATASET_TLVS. + * + * On write, any unknown/unsupported TLVs must be ignored. + */ + THREAD_PENDING_DATASET_TLVS = 0x1500 + 61, + + /// Send MGMT_SET Thread Pending Operational Dataset (in TLV format). + /** Format: `D` - Write only + * + * This is write-only property. When written, it triggers a MGMT_PENDING_SET meshcop command to be sent to leader + * with the given Dataset. + * + * When setting this property, the spinel frame response will be: + * 1. A `LAST_STATUS` with the status of the transmission of MGMT_PENDING_SET command if it fails. + * 2. A `THREAD_MGMT_SET_PENDING_DATASET_TLVS` with no content. + * + * On response reception or timeout, another notification will be sent to the host: + * A `THREAD_MGMT_SET_PENDING_DATASET_TLVS` with a spinel_status_t indicating + * the result of MGMT_SET_PENDING. + * + * On write, any unknown/unsupported TLVs must be ignored. + */ + THREAD_MGMT_SET_PENDING_DATASET_TLVS = 0x1500 + 62, + + /// Wake-up Channel + /** Format: `C` + * + * The Wake-up sample channel. Channel value should be `0` (Set Wake-up Channel unspecified, + * which means the device will use the PAN channel) or within the range [1, 10] (if 915-MHz + * supported) and [11, 26] (if 2.4 GHz supported). + */ + THREAD_WAKEUP_CHANNEL = 0x1500 + 63, + + /// Link-Local IPv6 Address + /** Format: `6` - Read only + */ + IPV6_LL_ADDR = 0x60 + 0, ///< [6] + + /// Mesh Local IPv6 Address + /** Format: `6` - Read only + */ + IPV6_ML_ADDR = 0x60 + 1, + + /// Mesh Local Prefix + /** Format: `6C` - Read-write + * + * Provides Mesh Local Prefix + * + * `6`: Mesh local prefix + * `C` : Prefix length (64 bit for Thread). + */ + IPV6_ML_PREFIX = 0x60 + 2, + + /// IPv6 (Unicast) Address Table + /** Format: `A(t(6CLLC))` + * + * This property provides all unicast addresses. + * + * Array of structures containing: + * + * `6`: IPv6 Address + * `C`: Network Prefix Length (in bits) + * `L`: Preferred Lifetime + * `L`: Valid Lifetime + */ + IPV6_ADDRESS_TABLE = 0x60 + 3, + + /// IPv6 Route Table - Deprecated + IPV6_ROUTE_TABLE = 0x60 + 4, + + /// IPv6 ICMP Ping Offload + /** Format: `b` + * + * Allow the NCP to directly respond to ICMP ping requests. If this is + * turned on, ping request ICMP packets will not be passed to the host. + * + * Default value is `false`. + */ + IPV6_ICMP_PING_OFFLOAD = 0x60 + 5, + + /// IPv6 Multicast Address Table + /** Format: `A(t(6))` + * + * This property provides all multicast addresses. + */ + IPV6_MULTICAST_ADDRESS_TABLE = 0x60 + 6, + + /// IPv6 ICMP Ping Offload + /** Format: `C` + * + * Allow the NCP to directly respond to ICMP ping requests. If this is + * turned on, ping request ICMP packets will not be passed to the host. + * + * This property allows enabling responses sent to unicast only, multicast + * only, or both. The valid value are defined by enumeration + * `spinel_ipv6_icmp_ping_offload_mode_t`. + * + * SPINEL_IPV6_ICMP_PING_OFFLOAD_DISABLED = 0 + * SPINEL_IPV6_ICMP_PING_OFFLOAD_UNICAST_ONLY = 1 + * SPINEL_IPV6_ICMP_PING_OFFLOAD_MULTICAST_ONLY = 2 + * SPINEL_IPV6_ICMP_PING_OFFLOAD_ALL = 3 + * SPINEL_IPV6_ICMP_PING_OFFLOAD_RLOC_ALOC_ONLY = 4 + * + * Default value is `NET_IPV6_ICMP_PING_OFFLOAD_DISABLED`. + */ + IPV6_ICMP_PING_OFFLOAD_MODE = 0x60 + 7, ///< [b] + + /// Debug Stream + /** Format: `U` (stream, read only) + * + * This property is a streaming property, meaning that you cannot explicitly + * fetch the value of this property. The stream provides human-readable debugging + * output which may be displayed in the host logs. + * + * The location of newline characters is not assumed by the host: it is + * the NCP's responsibility to insert newline characters where needed, + * just like with any other text stream. + * + * To receive the debugging stream, you wait for `CMD_PROP_VALUE_IS` + * commands for this property from the NCP. + */ + STREAM_DEBUG = 0x70 + 0, + + /// Raw Stream + /** Format: `dD` (stream, read only) + * Required Capability: SPINEL_CAP_MAC_RAW or SPINEL_CAP_CONFIG_RADIO + * + * This stream provides the capability of sending and receiving raw 15.4 frames + * to and from the radio. The exact format of the frame metadata and data is + * dependent on the MAC and PHY being used. + * + * This property is a streaming property, meaning that you cannot explicitly + * fetch the value of this property. To receive traffic, you wait for + * `CMD_PROP_VALUE_IS` commands with this property id from the NCP. + * + * The general format of this property is: + * + * `d` : frame data + * `D` : frame meta data + * + * The frame meta data is optional. Frame metadata MAY be empty or partially + * specified. Partially specified metadata MUST be accepted. Default values + * are used for all unspecified fields. + * + * The frame metadata field consists of the following fields: + * + * `c` : Received Signal Strength (RSSI) in dBm - default is -128 + * `c` : Noise floor in dBm - default is -128 + * `S` : Flags (see below). + * `d` : PHY-specific data/struct + * `d` : Vendor-specific data/struct + * + * Flags fields are defined by the following enumeration bitfields: + * + * SPINEL_MD_FLAG_TX = 0x0001 : Packet was transmitted, not received. + * SPINEL_MD_FLAG_BAD_FCS = 0x0004 : Packet was received with bad FCS + * SPINEL_MD_FLAG_DUPE = 0x0008 : Packet seems to be a duplicate + * SPINEL_MD_FLAG_RESERVED = 0xFFF2 : Flags reserved for future use. + * + * The format of PHY-specific data for a Thread device contains the following + * optional fields: + + * `C` : 802.15.4 channel + * `C` : IEEE 802.15.4 LQI + * `X` : The timestamp in microseconds + * + * Frames written to this stream with `CMD_PROP_VALUE_SET` will be sent out + * over the radio. This allows the caller to use the radio directly. + * + * The frame meta data for the `CMD_PROP_VALUE_SET` contains the following + * fields. Default values are used for all unspecified fields. + * + * `C` : Channel (for frame tx) - MUST be included. + * `C` : Maximum number of backoffs attempts before declaring CCA failure + * (use Thread stack default if not specified) + * `C` : Maximum number of retries allowed after a transmission failure + * (use Thread stack default if not specified) + * `b` : Set to true to enable CSMA-CA for this packet, false otherwise. + * (default true). + * `b` : Set to true to indicate if header is updated - related to + * `mIsHeaderUpdated` in `otRadioFrame` (default false). + * `b` : Set to true to indicate it is a retransmission - related to + * `mIsARetx` in `otRadioFrame` (default false). + * `b` : Set to true to indicate security was processed on tx frame + * `mIsSecurityProcessed` in `otRadioFrame` (default false). + * `L` : TX delay interval used for CSL - related to `mTxDelay` in + * `otRadioFrame` (default zero). + * `L` : TX delay based time used for CSL - related to `mTxDelayBaseTime` + * in `otRadioFrame` (default zero). + * `C` : RX channel after TX done (default assumed to be same as + * channel in metadata) + */ + STREAM_RAW = 0x70 + 1, + + /// (IPv6) Network Stream + /** Format: `dD` (stream, read only) + * + * This stream provides the capability of sending and receiving (IPv6) + * data packets to and from the currently attached network. The packets + * are sent or received securely (encryption and authentication). + * + * This property is a streaming property, meaning that you cannot explicitly + * fetch the value of this property. To receive traffic, you wait for + * `CMD_PROP_VALUE_IS` commands with this property id from the NCP. + * + * To send network packets, you call `CMD_PROP_VALUE_SET` on this property with + * the value of the packet. + * + * The general format of this property is: + * + * `d` : packet data + * `D` : packet meta data + * + * The packet metadata is optional. Packet meta data MAY be empty or partially + * specified. Partially specified metadata MUST be accepted. Default values + * are used for all unspecified fields. + * + * For OpenThread the meta data is currently empty. + */ + STREAM_NET = 0x70 + 2, + + /// (IPv6) Network Stream Insecure + /** Format: `dD` (stream, read only) + * + * This stream provides the capability of sending and receiving unencrypted + * and unauthenticated data packets to and from nearby devices for the + * purposes of device commissioning. + * + * This property is a streaming property, meaning that you cannot explicitly + * fetch the value of this property. To receive traffic, you wait for + * `CMD_PROP_VALUE_IS` commands with this property id from the NCP. + * + * To send network packets, you call `CMD_PROP_VALUE_SET` on this property with + * the value of the packet. + * + * The general format of this property is: + * + * `d` : packet data + * `D` : packet meta data + * + * The packet metadata is optional. Packet meta data MAY be empty or partially + * specified. Partially specified metadata MUST be accepted. Default values + * are used for all unspecified fields. + * + * For OpenThread the meta data is currently empty. + */ + STREAM_NET_INSECURE = 0x70 + 3, + + /// Log Stream + /** Format: `UD` (stream, read only) + * + * This property is a read-only streaming property which provides + * formatted log string from NCP. This property provides asynchronous + * `CMD_PROP_VALUE_IS` updates with a new log string and includes + * optional meta data. + * + * `U`: The log string + * `D`: Log metadata (optional). + * + * Any data after the log string is considered metadata and is OPTIONAL. + * Presence of `SPINEL_CAP_OPENTHREAD_LOG_METADATA` capability + * indicates that OpenThread log metadata format is used as defined + * below: + * + * `C`: Log level (as per definition in enumeration + * `SPINEL_NCP_LOG_LEVEL_`) + * `i`: OpenThread Log region (as per definition in enumeration + * `SPINEL_NCP_LOG_REGION_). + * `X`: Log timestamp = + + */ + STREAM_LOG = 0x70 + 4, + + // Thread Joiner State + /** Format `C` - Read Only + * + * Required capability: SPINEL_CAP_THREAD_JOINER + * + * The valid values are specified by `spinel_meshcop_joiner_state_t` (`SPINEL_MESHCOP_JOINER_STATE_`) + * enumeration. + */ + MESHCOP_JOINER_STATE = 0x80 + 0, ///<[C] + + /// Thread Joiner Commissioning command and the parameters + /** Format `b` or `bU(UUUUU)` (fields in parenthesis are optional) - Write Only + * + * This property starts or stops Joiner's commissioning process + * + * Required capability: SPINEL_CAP_THREAD_JOINER + * + * Writing to this property starts/stops the Joiner commissioning process. + * The immediate `VALUE_IS` response indicates success/failure of the starting/stopping + * the Joiner commissioning process. + * + * After a successful start operation, the join process outcome is reported through an + * asynchronous `VALUE_IS(LAST_STATUS)` update with one of the following error status values: + * + * - SPINEL_STATUS_JOIN_SUCCESS the join process succeeded. + * - SPINEL_STATUS_JOIN_SECURITY the join process failed due to security credentials. + * - SPINEL_STATUS_JOIN_NO_PEERS no joinable network was discovered. + * - SPINEL_STATUS_JOIN_RSP_TIMEOUT if a response timed out. + * - SPINEL_STATUS_JOIN_FAILURE join failure. + * + * Frame format: + * + * `b` : Start or stop commissioning process (true to start). + * + * Only if the start commissioning. + * + * `U` : Joiner's PSKd. + * + * The next fields are all optional. If not provided, OpenThread default values would be used. + * + * `U` : Provisioning URL (use empty string if not required). + * `U` : Vendor Name. If not specified or empty string, use OpenThread default (PACKAGE_NAME). + * `U` : Vendor Model. If not specified or empty string, use OpenThread default (OPENTHREAD_CONFIG_PLATFORM_INFO). + * `U` : Vendor Sw Version. If not specified or empty string, use OpenThread default (PACKAGE_VERSION). + * `U` : Vendor Data String. Will not be appended if not specified. + */ + MESHCOP_JOINER_COMMISSIONING = 0x80 + 1, + + // Thread Commissioner State + /** Format `C` + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + * + * The valid values are specified by SPINEL_MESHCOP_COMMISSIONER_STATE_ enumeration. + */ + MESHCOP_COMMISSIONER_STATE = 0x80 + 2, + + // Thread Commissioner Joiners + /** Format `A(t(t(E|CX)UL))` - get, insert or remove. + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + * + * Data per array entry is: + * + * `t()` | `t(E)` | `t(CX)` : Joiner info struct (formatting varies). + * + * - `t()` or empty struct indicates any joiner. + * - `t(E)` specifies the Joiner EUI-64. + * - `t(CX) specifies Joiner Discerner, `C` is Discerner length (in bits), and `X` is Discerner value. + * + * The struct is followed by: + * + * `L` : Timeout after which to remove Joiner (when written should be in seconds, when read is in milliseconds) + * `U` : PSKd + * + * For CMD_PROP_VALUE_REMOVE the timeout and PSKd are optional. + */ + MESHCOP_COMMISSIONER_JOINERS = 0x80 + 3, + + // Thread Commissioner Provisioning URL + /** Format `U` + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + */ + MESHCOP_COMMISSIONER_PROVISIONING_URL = 0x80 + 4, + + // Thread Commissioner Session ID + /** Format `S` - Read only + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + */ + MESHCOP_COMMISSIONER_SESSION_ID = 0x80 + 5, + + /// Thread Joiner Discerner + /** Format `CX` - Read-write + * + * Required capability: SPINEL_CAP_THREAD_JOINER + * + * This property represents a Joiner Discerner. + * + * The Joiner Discerner is used to calculate the Joiner ID used during commissioning/joining process. + * + * By default (when a discerner is not provided or cleared), Joiner ID is derived as first 64 bits of the result + * of computing SHA-256 over factory-assigned IEEE EUI-64. Note that this is the main behavior expected by Thread + * specification. + * + * Format: + * + * 'C' : The Joiner Discerner bit length (number of bits). + * `X` : The Joiner Discerner value (64-bit unsigned) - Only present/applicable when length is non-zero. + * + * When writing to this property, the length can be set to zero to clear any previously set Joiner Discerner value. + * + * When reading this property if there is no currently set Joiner Discerner, zero is returned as the length (with + * no value field). + */ + MESHCOP_JOINER_DISCERNER = 0x80 + 6, + + // Thread Commissioner Announce Begin + /** Format `LCS6` - Write only + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + * + * Writing to this property sends an Announce Begin message with the specified parameters. Response is a + * `LAST_STATUS` update with status of operation. + * + * `L` : Channel mask + * `C` : Number of messages per channel + * `S` : The time between two successive MLE Announce transmissions (milliseconds) + * `6` : IPv6 destination + */ + MESHCOP_COMMISSIONER_ANNOUNCE_BEGIN = 0x1800 + 0, + + // Thread Commissioner Energy Scan Query + /** Format `LCSS6` - Write only + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + * + * Writing to this property sends an Energy Scan Query message with the specified parameters. Response is a + * `LAST_STATUS` with status of operation. The energy scan results are emitted asynchronously through + * `MESHCOP_COMMISSIONER_ENERGY_SCAN_RESULT` updates. + * + * Format is: + * + * `L` : Channel mask + * `C` : The number of energy measurements per channel + * `S` : The time between energy measurements (milliseconds) + * `S` : The scan duration for each energy measurement (milliseconds) + * `6` : IPv6 destination. + */ + MESHCOP_COMMISSIONER_ENERGY_SCAN = 0x1800 + 1, + + // Thread Commissioner Energy Scan Result + /** Format `Ld` - Asynchronous event only + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + * + * This property provides asynchronous `CMD_PROP_VALUE_INSERTED` updates to report energy scan results for a + * previously sent Energy Scan Query message (please see `MESHCOP_COMMISSIONER_ENERGY_SCAN`). + * + * Format is: + * + * `L` : Channel mask + * `d` : Energy measurement data (note that `d` encoding includes the length) + */ + MESHCOP_COMMISSIONER_ENERGY_SCAN_RESULT = 0x1800 + 2, + + // Thread Commissioner PAN ID Query + /** Format `SL6` - Write only + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + * + * Writing to this property sends a PAN ID Query message with the specified parameters. Response is a + * `LAST_STATUS` with status of operation. The PAN ID Conflict results are emitted asynchronously through + * `MESHCOP_COMMISSIONER_PAN_ID_CONFLICT_RESULT` updates. + * + * Format is: + * + * `S` : PAN ID to query + * `L` : Channel mask + * `6` : IPv6 destination + */ + MESHCOP_COMMISSIONER_PAN_ID_QUERY = 0x1800 + 3, + + // Thread Commissioner PAN ID Conflict Result + /** Format `SL` - Asynchronous event only + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + * + * This property provides asynchronous `CMD_PROP_VALUE_INSERTED` updates to report PAN ID conflict results for a + * previously sent PAN ID Query message (please see `MESHCOP_COMMISSIONER_PAN_ID_QUERY`). + * + * Format is: + * + * `S` : The PAN ID + * `L` : Channel mask + */ + MESHCOP_COMMISSIONER_PAN_ID_CONFLICT_RESULT = 0x1800 + 4, + + // Thread Commissioner Send MGMT_COMMISSIONER_GET + /** Format `d` - Write only + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + * + * Writing to this property sends a MGMT_COMMISSIONER_GET message with the specified parameters. Response is a + * `LAST_STATUS` with status of operation. + * + * Format is: + * + * `d` : List of TLV types to get + */ + MESHCOP_COMMISSIONER_MGMT_GET = 0x1800 + 5, + + // Thread Commissioner Send MGMT_COMMISSIONER_SET + /** Format `d` - Write only + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + * + * Writing to this property sends a MGMT_COMMISSIONER_SET message with the specified parameters. Response is a + * `LAST_STATUS` with status of operation. + * + * Format is: + * + * `d` : TLV encoded data + */ + MESHCOP_COMMISSIONER_MGMT_SET = 0x1800 + 6, + + // Thread Commissioner Generate PSKc + /** Format: `UUd` - Write only + * + * Required capability: SPINEL_CAP_THREAD_COMMISSIONER + * + * Writing to this property allows user to generate PSKc from a given commissioning pass-phrase, network name, + * extended PAN Id. + * + * Written value format is: + * + * `U` : The commissioning pass-phrase. + * `U` : Network Name. + * `d` : Extended PAN ID. + * + * The response on success would be a `VALUE_IS` command with the PSKc with format below: + * + * `D` : The PSKc + * + * On a failure a `LAST_STATUS` is emitted with the error status. + */ + MESHCOP_COMMISSIONER_GENERATE_PSKC = 0x1800 + 7, + + /// Channel Manager - Channel Change New Channel + /** Format: `C` (read-write) + * + * Required capability: SPINEL_CAP_CHANNEL_MANAGER + * + * Setting this property triggers the Channel Manager to start + * a channel change process. The network switches to the given + * channel after the specified delay (see `CHANNEL_MANAGER_DELAY`). + * + * A subsequent write to this property will cancel an ongoing + * (previously requested) channel change. + */ + CHANNEL_MANAGER_NEW_CHANNEL = 0x1900 + 0, + + /// Channel Manager - Channel Change Delay + /** Format 'S' + * Units: seconds + * + * Required capability: SPINEL_CAP_CHANNEL_MANAGER + * + * This property specifies the delay (in seconds) to be used for + * a channel change request. + * + * The delay should preferably be longer than maximum data poll + * interval used by all sleepy-end-devices within the Thread + * network. + */ + CHANNEL_MANAGER_DELAY = 0x1900 + 1, + + /// Channel Manager Supported Channels + /** Format 'A(C)' + * + * Required capability: SPINEL_CAP_CHANNEL_MANAGER + * + * This property specifies the list of supported channels. + */ + CHANNEL_MANAGER_SUPPORTED_CHANNELS = 0x1900 + 2, + + /// Channel Manager Favored Channels + /** Format 'A(C)' + * + * Required capability: SPINEL_CAP_CHANNEL_MANAGER + * + * This property specifies the list of favored channels (when `ChannelManager` is asked to select channel) + */ + CHANNEL_MANAGER_FAVORED_CHANNELS = 0x1900 + 3, + + /// Channel Manager Channel Select Trigger + /** Format 'b' + * + * Required capability: SPINEL_CAP_CHANNEL_MANAGER + * + * Writing to this property triggers a request on `ChannelManager` to select a new channel. + * + * Once a Channel Select is triggered, the Channel Manager will perform the following 3 steps: + * + * 1) `ChannelManager` decides if the channel change would be helpful. This check can be skipped if in the input + * boolean to this property is set to `true` (skipping the quality check). + * This step uses the collected link quality metrics on the device such as CCA failure rate, frame and message + * error rates per neighbor, etc. to determine if the current channel quality is at the level that justifies + * a channel change. + * + * 2) If first step passes, then `ChannelManager` selects a potentially better channel. It uses the collected + * channel quality data by `ChannelMonitor` module. The supported and favored channels are used at this step. + * + * 3) If the newly selected channel is different from the current channel, `ChannelManager` requests/starts the + * channel change process. + * + * Reading this property always yields `false`. + */ + CHANNEL_MANAGER_CHANNEL_SELECT = 0x1900 + 4, + + /// Channel Manager Auto Channel Selection Enabled + /** Format 'b' + * + * Required capability: SPINEL_CAP_CHANNEL_MANAGER + * + * This property indicates if auto-channel-selection functionality is enabled/disabled on `ChannelManager`. + * + * When enabled, `ChannelManager` will periodically checks and attempts to select a new channel. The period interval + * is specified by `CHANNEL_MANAGER_AUTO_SELECT_INTERVAL`. + */ + CHANNEL_MANAGER_AUTO_SELECT_ENABLED = 0x1900 + 5, + + /// Channel Manager Auto Channel Selection Interval + /** Format 'L' + * units: seconds + * + * Required capability: SPINEL_CAP_CHANNEL_MANAGER + * + * This property specifies the auto-channel-selection check interval (in seconds). + */ + CHANNEL_MANAGER_AUTO_SELECT_INTERVAL = 0x1900 + 6, + + /// Thread network time. + /** Format: `Xc` - Read only + * + * Data per item is: + * + * `X`: The Thread network time, in microseconds. + * `c`: Time synchronization status. + */ + THREAD_NETWORK_TIME = 0x1900 + 7, + + /// Thread time synchronization period + /** Format: `S` - Read-Write + * + * Data per item is: + * + * `S`: Time synchronization period, in seconds. + */ + TIME_SYNC_PERIOD = 0x1900 + 8, + + /// Thread Time synchronization XTAL accuracy threshold for Router + /** Format: `S` - Read-Write + * + * Data per item is: + * + * `S`: The XTAL accuracy threshold for Router, in PPM. + */ + TIME_SYNC_XTAL_THRESHOLD = 0x1900 + 9, + + /// Child Supervision Interval + /** Format: `S` - Read-Write + * Units: Seconds + * + * Required capability: `SPINEL_CAP_CHILD_SUPERVISION` + * + * The child supervision interval (in seconds). Zero indicates that child supervision is disabled. + * + * When enabled, Child supervision feature ensures that at least one message is sent to every sleepy child within + * the given supervision interval. If there is no other message, a supervision message (a data message with empty + * payload) is enqueued and sent to the child. + * + * This property is available for FTD build only. + */ + CHILD_SUPERVISION_INTERVAL = 0x1900 + 10, + + /// Child Supervision Check Timeout + /** Format: `S` - Read-Write + * Units: Seconds + * + * Required capability: `SPINEL_CAP_CHILD_SUPERVISION` + * + * The child supervision check timeout interval (in seconds). Zero indicates supervision check on the child is + * disabled. + * + * Supervision check is only applicable on a sleepy child. When enabled, if the child does not hear from its parent + * within the specified check timeout, it initiates a re-attach process by starting an MLE Child Update + * Request/Response exchange with the parent. + * + * This property is available for FTD and MTD builds. + */ + CHILD_SUPERVISION_CHECK_TIMEOUT = 0x1900 + 11, + + // RCP (NCP in radio only mode) version + /** Format `U` - Read only + * + * Required capability: SPINEL_CAP_POSIX + * + * This property gives the version string of RCP (NCP in radio mode) which is being controlled by a POSIX + * application. It is available only in "POSIX" platform (i.e., `OPENTHREAD_PLATFORM_POSIX` is enabled). + */ + RCP_VERSION = 0x1900 + 12, + + /// Thread Parent Response info + /** Format: `ESccCCCb` - Asynchronous event only + * + * `E`: Extended address + * `S`: RLOC16 + * `c`: Instant RSSI + * 'c': Parent Priority + * `C`: Link Quality3 + * `C`: Link Quality2 + * `C`: Link Quality1 + * 'b': Is the node receiving parent response frame attached + * + * This property sends Parent Response frame information to the Host. + * This property is available for FTD build only. + */ + PARENT_RESPONSE_INFO = 0x1900 + 13, + + /// SLAAC enabled + /** Format `b` - Read-Write + * Required capability: `SPINEL_CAP_SLAAC` + * + * This property allows the host to enable/disable SLAAC module on NCP at run-time. When SLAAC module is enabled, + * SLAAC addresses (based on on-mesh prefixes in Network Data) are added to the interface. When SLAAC module is + * disabled any previously added SLAAC address is removed. + */ + SLAAC_ENABLED = 0x1900 + 14, + + // Supported Radio Links (by device) + /** + * Format `A(i)` - Read only + * + * This property returns list of supported radio links by the device itself. Enumeration `SPINEL_RADIO_LINK_{TYPE}` + * values indicate different radio link types. + */ + SUPPORTED_RADIO_LINKS = 0x1900 + 15, + + /// Neighbor Table Multi Radio Link Info + /** Format: `A(t(ESA(t(iC))))` - Read only + * Required capability: `SPINEL_CAP_MULTI_RADIO`. + * + * Each item represents info about a neighbor: + * + * `E`: Neighbor's Extended Address + * `S`: Neighbor's RLOC16 + * + * This is then followed by an array of radio link info structures indicating which radio links are supported by + * the neighbor: + * + * `i` : Radio link type (enumeration `SPINEL_RADIO_LINK_{TYPE}`). + * `C` : Preference value associated with radio link. + */ + NEIGHBOR_TABLE_MULTI_RADIO_INFO = 0x1900 + 16, + + /// SRP Client Start + /** Format: `b(6Sb)` - Write only + * Required capability: `SPINEL_CAP_SRP_CLIENT`. + * + * Writing to this property allows user to start or stop the SRP client operation with a given SRP server. + * + * Written value format is: + * + * `b` : TRUE to start the client, FALSE to stop the client. + * + * When used to start the SRP client, the following fields should also be included: + * + * `6` : SRP server IPv6 address. + * `U` : SRP server port number. + * `b` : Boolean to indicate whether or not to emit SRP client events (using `SRP_CLIENT_EVENT`). + */ + SRP_CLIENT_START = 0x1900 + 17, + + /// SRP Client Lease Interval + /** Format: `L` - Read/Write + * Required capability: `SPINEL_CAP_SRP_CLIENT`. + * + * The lease interval used in SRP update requests (in seconds). + */ + SRP_CLIENT_LEASE_INTERVAL = 0x1900 + 18, + + /// SRP Client Key Lease Interval + /** Format: `L` - Read/Write + * Required capability: `SPINEL_CAP_SRP_CLIENT`. + * + * The key lease interval used in SRP update requests (in seconds). + */ + SRP_CLIENT_KEY_LEASE_INTERVAL = 0x1900 + 19, + + /// SRP Client Host Info + /** Format: `UCt(A(6))` - Read only + * Required capability: `SPINEL_CAP_SRP_CLIENT`. + * + * Format is: + * + * `U` : The host name. + * `C` : The host state (values from `spinel_srp_client_item_state_t`). + * `t(A(6))` : Structure containing array of host IPv6 addresses. + */ + SRP_CLIENT_HOST_INFO = 0x1900 + 20, + + /// SRP Client Host Name (label). + /** Format: `U` - Read/Write + * Required capability: `SPINEL_CAP_SRP_CLIENT`. + */ + SRP_CLIENT_HOST_NAME = 0x1900 + 21, + + /// SRP Client Host Addresses + /** Format: `A(6)` - Read/Write + * Required capability: `SPINEL_CAP_SRP_CLIENT`. + */ + SRP_CLIENT_HOST_ADDRESSES = 0x1900 + 22, + + /// SRP Client Services + /** Format: `A(t(UUSSSd))` - Read/Insert/Remove + * Required capability: `SPINEL_CAP_SRP_CLIENT`. + * + * This property provides a list/array of services. + * + * Data per item for `SPINEL_CMD_PROP_VALUE_GET` and/or `SPINEL_CMD_PROP_VALUE_INSERT` operation is as follows: + * + * `U` : The service name labels (e.g., "_chip._udp", not the full domain name. + * `U` : The service instance name label (not the full name). + * `S` : The service port number. + * `S` : The service priority. + * `S` : The service weight. + * + * For `SPINEL_CMD_PROP_VALUE_REMOVE` command, the following format is used: + * + * `U` : The service name labels (e.g., "_chip._udp", not the full domain name. + * `U` : The service instance name label (not the full name). + * `b` : Indicates whether to clear the service entry (optional). + * + * The last boolean (`b`) field is optional. When included it indicates on `true` to clear the service (clear it + * on client immediately with no interaction to server) and on `false` to remove the service (inform server and + * wait for the service entry to be removed on server). If it is not included, the value is `false`. + */ + SRP_CLIENT_SERVICES = 0x1900 + 23, + + /// SRP Client Host And Services Remove + /** Format: `bb` : Write only + * Required capability: `SPINEL_CAP_SRP_CLIENT`. + * + * Writing to this property with starts the remove process of the host info and all services. + * Please see `otSrpClientRemoveHostAndServices()` for more details. + * + * Format is: + * + * `b` : A boolean indicating whether or not the host key lease should also be cleared. + * `b` : A boolean indicating whether or not to send update to server when host info is not registered. + */ + SRP_CLIENT_HOST_SERVICES_REMOVE = 0x1900 + 24, + + /// SRP Client Host And Services Clear + /** Format: Empty : Write only + * Required capability: `SPINEL_CAP_SRP_CLIENT`. + * + * Writing to this property clears all host info and all the services. + * Please see `otSrpClientClearHostAndServices()` for more details. + */ + SRP_CLIENT_HOST_SERVICES_CLEAR = 0x1900 + 25, + + /// SRP Client Event + /** Format: t() : Asynchronous event only + * Required capability: `SPINEL_CAP_SRP_CLIENT`. + * + * This property is asynchronously emitted when there is an event from SRP client notifying some state changes or + * errors. + * + * The general format of this property is as follows: + * + * `S` : Error code (see `spinel_srp_client_error_t` enumeration). + * `d` : Host info data. + * `d` : Active services. + * `d` : Removed services. + * + * The host info data contains: + * + * `U` : The host name. + * `C` : The host state (values from `spinel_srp_client_item_state_t`). + * `t(A(6))` : Structure containing array of host IPv6 addresses. + * + * The active or removed services data is an array of services `A(t(UUSSSd))` with each service format: + * + * `U` : The service name labels (e.g., "_chip._udp", not the full domain name. + * `U` : The service instance name label (not the full name). + * `S` : The service port number. + * `S` : The service priority. + * `S` : The service weight. + * `d` : The encoded TXT-DATA. + */ + SRP_CLIENT_EVENT = 0x1900 + 26, + + /// SRP Client Service Key Inclusion Enabled + /** Format `b` : Read-Write + * Required capability: `SPINEL_CAP_SRP_CLIENT` & `SPINEL_CAP_REFERENCE_DEVICE`. + * + * This boolean property indicates whether the "service key record inclusion" mode is enabled or not. + * + * When enabled, SRP client will include KEY record in Service Description Instructions in the SRP update messages + * that it sends. + * + * KEY record is optional in Service Description Instruction (it is required and always included in the Host + * Description Instruction). The default behavior of SRP client is to not include it. This function is intended to + * override the default behavior for testing only. + */ + SRP_CLIENT_SERVICE_KEY_ENABLED = 0x1900 + 27, + + /// Server Allow Local Network Data Change + /** Format `b` - Read-write + * + * Required capability: SPINEL_CAP_THREAD_SERVICE + * + * Set to true before changing local server net data. Set to false when finished. + * This allows changes to be aggregated into a single event. + */ + SERVER_ALLOW_LOCAL_DATA_CHANGE = 0xa0 + 0, + + // Server Services + /** Format: `A(t(LdbdS))` + * + * This property provides all services registered on the device + * + * Required capability: SPINEL_CAP_THREAD_SERVICE + * + * Array of structures containing: + * + * `L`: Enterprise Number + * `d`: Service Data + * `b`: Stable + * `d`: Server Data + * `S`: RLOC + */ + SERVER_SERVICES = 0xa0 + 1, + + // Server Leader Services + /** Format: `A(t(CLdbdS))` + * + * This property provides all services registered on the leader + * + * Array of structures containing: + * + * `C`: Service ID + * `L`: Enterprise Number + * `d`: Service Data + * `b`: Stable + * `d`: Server Data + * `S`: RLOC + */ + SERVER_LEADER_SERVICES = 0xa0 + 2, + + /// RCP API Version number + /** Format: `i` (read-only) + * + * Required capability: SPINEL_CAP_RADIO and SPINEL_CAP_RCP_API_VERSION. + * + * This property gives the RCP API Version number. + * + * Please see "Spinel definition compatibility guideline" section. + */ + RCP_API_VERSION = 0xb0 + 0, + + /// Min host RCP API Version number + /** Format: `i` (read-only) + * + * Required capability: SPINEL_CAP_RADIO and SPINEL_CAP_RCP_MIN_HOST_API_VERSION. + * + * This property gives the minimum host RCP API Version number. + * + * Please see "Spinel definition compatibility guideline" section. + */ + RCP_MIN_HOST_API_VERSION = 0xb0 + 1, + + /// Crash Dump + /** Format: Empty : Write only + * + * Required capability: SPINEL_CAP_RADIO and SPINEL_CAP_RCP_LOG_CRASH_DUMP. + * + * Writing to this property instructs the RCP to log a crash dump if available. + */ + RCP_LOG_CRASH_DUMP = 0xb0 + 2, + + /// UART Bitrate + /** Format: `L` + * + * If the NCP is using a UART to communicate with the host, + * this property allows the host to change the bitrate + * of the serial connection. The value encoding is `L`, + * which is a little-endian 32-bit unsigned integer. + * The host should not assume that all possible numeric values + * are supported. + * + * If implemented by the NCP, this property should be persistent + * across software resets and forgotten upon hardware resets. + * + * This property is only implemented when a UART is being + * used for Spinel. This property is optional. + * + * When changing the bitrate, all frames will be received + * at the previous bitrate until the response frame to this command + * is received. Once a successful response frame is received by + * the host, all further frames will be transmitted at the new + * bitrate. + */ + UART_BITRATE = 0x100 + 0, + + /// UART Software Flow Control + /** Format: `b` + * + * If the NCP is using a UART to communicate with the host, + * this property allows the host to determine if software flow + * control (XON/XOFF style) should be used and (optionally) to + * turn it on or off. + * + * This property is only implemented when a UART is being + * used for Spinel. This property is optional. + */ + UART_XON_XOFF = 0x100 + 1, + + // For direct access to the 802.15.4 PID. + // Individual registers are fetched using + // `15_4_PIB__BEGIN+[PIB_IDENTIFIER]` + // Only supported if SPINEL_CAP_15_4_PIB is set. + // + // For brevity, the entire 802.15.4 PIB space is + // not defined here, but a few choice attributes + // are defined for illustration and convenience. + IEEE_802_15_4_PIB_PHY_CHANNELS_SUPPORTED = 0x400 + 0x01, ///< [A(L)] + IEEE_802_15_4_PIB_MAC_PROMISCUOUS_MODE = 0x400 + 0x51, ///< [b] + IEEE_802_15_4_PIB_MAC_SECURITY_ENABLED = 0x400 + 0x5d, ///< [b] + + /// Counter reset + /** Format: Empty (Write only). + * + * Writing to this property (with any value) will reset all MAC, MLE, IP, and NCP counters to zero. + */ + CNTR_RESET = 0x500 + 0, + + /// The total number of transmissions. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_TOTAL = 0x500 + 1, + + /// The number of transmissions with ack request. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_ACK_REQ = 0x500 + 2, + + /// The number of transmissions that were acked. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_ACKED = 0x500 + 3, + + /// The number of transmissions without ack request. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_NO_ACK_REQ = 0x500 + 4, + + /// The number of transmitted data. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_DATA = 0x500 + 5, + + /// The number of transmitted data poll. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_DATA_POLL = 0x500 + 6, + + /// The number of transmitted beacon. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_BEACON = 0x500 + 7, + + /// The number of transmitted beacon request. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_BEACON_REQ = 0x500 + 8, + + /// The number of transmitted other types of frames. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_OTHER = 0x500 + 9, + + /// The number of retransmission times. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_RETRY = 0x500 + 10, + + /// The number of CCA failure times. + /** Format: `L` (Read-only) */ + CNTR_TX_ERR_CCA = 0x500 + 11, + + /// The number of unicast packets transmitted. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_UNICAST = 0x500 + 12, + + /// The number of broadcast packets transmitted. + /** Format: `L` (Read-only) */ + CNTR_TX_PKT_BROADCAST = 0x500 + 13, + + /// The number of frame transmission failures due to abort error. + /** Format: `L` (Read-only) */ + CNTR_TX_ERR_ABORT = 0x500 + 14, + + /// The total number of received packets. + /** Format: `L` (Read-only) */ + CNTR_RX_PKT_TOTAL = 0x500 + 100, + + /// The number of received data. + /** Format: `L` (Read-only) */ + CNTR_RX_PKT_DATA = 0x500 + 101, + + /// The number of received data poll. + /** Format: `L` (Read-only) */ + CNTR_RX_PKT_DATA_POLL = 0x500 + 102, + + /// The number of received beacon. + /** Format: `L` (Read-only) */ + CNTR_RX_PKT_BEACON = 0x500 + 103, + + /// The number of received beacon request. + /** Format: `L` (Read-only) */ + CNTR_RX_PKT_BEACON_REQ = 0x500 + 104, + + /// The number of received other types of frames. + /** Format: `L` (Read-only) */ + CNTR_RX_PKT_OTHER = 0x500 + 105, + + /// The number of received packets filtered by allowlist. + /** Format: `L` (Read-only) */ + CNTR_RX_PKT_FILT_WL = 0x500 + 106, + + /// The number of received packets filtered by destination check. + /** Format: `L` (Read-only) */ + CNTR_RX_PKT_FILT_DA = 0x500 + 107, + + /// The number of received packets that are empty. + /** Format: `L` (Read-only) */ + CNTR_RX_ERR_EMPTY = 0x500 + 108, + + /// The number of received packets from an unknown neighbor. + /** Format: `L` (Read-only) */ + CNTR_RX_ERR_UKWN_NBR = 0x500 + 109, + + /// The number of received packets whose source address is invalid. + /** Format: `L` (Read-only) */ + CNTR_RX_ERR_NVLD_SADDR = 0x500 + 110, + + /// The number of received packets with a security error. + /** Format: `L` (Read-only) */ + CNTR_RX_ERR_SECURITY = 0x500 + 111, + + /// The number of received packets with a checksum error. + /** Format: `L` (Read-only) */ + CNTR_RX_ERR_BAD_FCS = 0x500 + 112, + + /// The number of received packets with other errors. + /** Format: `L` (Read-only) */ + CNTR_RX_ERR_OTHER = 0x500 + 113, + + /// The number of received duplicated. + /** Format: `L` (Read-only) */ + CNTR_RX_PKT_DUP = 0x500 + 114, + + /// The number of unicast packets received. + /** Format: `L` (Read-only) */ + CNTR_RX_PKT_UNICAST = 0x500 + 115, + + /// The number of broadcast packets received. + /** Format: `L` (Read-only) */ + CNTR_RX_PKT_BROADCAST = 0x500 + 116, + + /// The total number of secure transmitted IP messages. + /** Format: `L` (Read-only) */ + CNTR_TX_IP_SEC_TOTAL = 0x500 + 200, + + /// The total number of insecure transmitted IP messages. + /** Format: `L` (Read-only) */ + CNTR_TX_IP_INSEC_TOTAL = 0x500 + 201, + + /// The number of dropped (not transmitted) IP messages. + /** Format: `L` (Read-only) */ + CNTR_TX_IP_DROPPED = 0x500 + 202, + + /// The total number of secure received IP message. + /** Format: `L` (Read-only) */ + CNTR_RX_IP_SEC_TOTAL = 0x500 + 203, + + /// The total number of insecure received IP message. + /** Format: `L` (Read-only) */ + CNTR_RX_IP_INSEC_TOTAL = 0x500 + 204, + + /// The number of dropped received IP messages. + /** Format: `L` (Read-only) */ + CNTR_RX_IP_DROPPED = 0x500 + 205, + + /// The number of transmitted spinel frames. + /** Format: `L` (Read-only) */ + CNTR_TX_SPINEL_TOTAL = 0x500 + 300, + + /// The number of received spinel frames. + /** Format: `L` (Read-only) */ + CNTR_RX_SPINEL_TOTAL = 0x500 + 301, + + /// The number of received spinel frames with error. + /** Format: `L` (Read-only) */ + CNTR_RX_SPINEL_ERR = 0x500 + 302, + + /// Number of out of order received spinel frames (tid increase by more than 1). + /** Format: `L` (Read-only) */ + CNTR_RX_SPINEL_OUT_OF_ORDER_TID = 0x500 + 303, + + /// The number of successful Tx IP packets + /** Format: `L` (Read-only) */ + CNTR_IP_TX_SUCCESS = 0x500 + 304, + + /// The number of successful Rx IP packets + /** Format: `L` (Read-only) */ + CNTR_IP_RX_SUCCESS = 0x500 + 305, + + /// The number of failed Tx IP packets + /** Format: `L` (Read-only) */ + CNTR_IP_TX_FAILURE = 0x500 + 306, + + /// The number of failed Rx IP packets + /** Format: `L` (Read-only) */ + CNTR_IP_RX_FAILURE = 0x500 + 307, + + /// The message buffer counter info + /** Format: `SSSSSSSSSSSSSSSS` (Read-only) + * `S`, (TotalBuffers) The number of buffers in the pool. + * `S`, (FreeBuffers) The number of free message buffers. + * `S`, (6loSendMessages) The number of messages in the 6lo send queue. + * `S`, (6loSendBuffers) The number of buffers in the 6lo send queue. + * `S`, (6loReassemblyMessages) The number of messages in the 6LoWPAN reassembly queue. + * `S`, (6loReassemblyBuffers) The number of buffers in the 6LoWPAN reassembly queue. + * `S`, (Ip6Messages) The number of messages in the IPv6 send queue. + * `S`, (Ip6Buffers) The number of buffers in the IPv6 send queue. + * `S`, (MplMessages) The number of messages in the MPL send queue. + * `S`, (MplBuffers) The number of buffers in the MPL send queue. + * `S`, (MleMessages) The number of messages in the MLE send queue. + * `S`, (MleBuffers) The number of buffers in the MLE send queue. + * `S`, (ArpMessages) The number of messages in the ARP send queue. + * `S`, (ArpBuffers) The number of buffers in the ARP send queue. + * `S`, (CoapMessages) The number of messages in the CoAP send queue. + * `S`, (CoapBuffers) The number of buffers in the CoAP send queue. + */ + MSG_BUFFER_COUNTERS = 0x500 + 400, + + /// All MAC related counters. + /** Format: t(A(L))t(A(L)) + * + * The contents include two structs, first one corresponds to + * all transmit related MAC counters, second one provides the + * receive related counters. + * + * The transmit structure includes: + * + * 'L': TxTotal (The total number of transmissions). + * 'L': TxUnicast (The total number of unicast transmissions). + * 'L': TxBroadcast (The total number of broadcast transmissions). + * 'L': TxAckRequested (The number of transmissions with ack request). + * 'L': TxAcked (The number of transmissions that were acked). + * 'L': TxNoAckRequested (The number of transmissions without ack request). + * 'L': TxData (The number of transmitted data). + * 'L': TxDataPoll (The number of transmitted data poll). + * 'L': TxBeacon (The number of transmitted beacon). + * 'L': TxBeaconRequest (The number of transmitted beacon request). + * 'L': TxOther (The number of transmitted other types of frames). + * 'L': TxRetry (The number of retransmission times). + * 'L': TxErrCca (The number of CCA failure times). + * 'L': TxErrAbort (The number of frame transmission failures due to abort error). + * 'L': TxErrBusyChannel (The number of frames that were dropped due to a busy channel). + * 'L': TxDirectMaxRetryExpiry (The number of expired retransmission retries for direct message). + * 'L': TxIndirectMaxRetryExpiry (The number of expired retransmission retries for indirect message). + * + * The receive structure includes: + * + * 'L': RxTotal (The total number of received packets). + * 'L': RxUnicast (The total number of unicast packets received). + * 'L': RxBroadcast (The total number of broadcast packets received). + * 'L': RxData (The number of received data). + * 'L': RxDataPoll (The number of received data poll). + * 'L': RxBeacon (The number of received beacon). + * 'L': RxBeaconRequest (The number of received beacon request). + * 'L': RxOther (The number of received other types of frames). + * 'L': RxAddressFiltered (The number of received packets filtered by address filter + * (allowlist or denylist)). + * 'L': RxDestAddrFiltered (The number of received packets filtered by destination check). + * 'L': RxDuplicated (The number of received duplicated packets). + * 'L': RxErrNoFrame (The number of received packets with no or malformed content). + * 'L': RxErrUnknownNeighbor (The number of received packets from unknown neighbor). + * 'L': RxErrInvalidSrcAddr (The number of received packets whose source address is invalid). + * 'L': RxErrSec (The number of received packets with security error). + * 'L': RxErrFcs (The number of received packets with FCS error). + * 'L': RxErrOther (The number of received packets with other error). + * + * Writing to this property with any value would reset all MAC counters to zero. + */ + CNTR_ALL_MAC_COUNTERS = 0x500 + 401, + + /// Thread MLE counters. + /** Format: `SSSSSSSSS` + * + * 'S': DisabledRole (The number of times device entered OT_DEVICE_ROLE_DISABLED role). + * 'S': DetachedRole (The number of times device entered OT_DEVICE_ROLE_DETACHED role). + * 'S': ChildRole (The number of times device entered OT_DEVICE_ROLE_CHILD role). + * 'S': RouterRole (The number of times device entered OT_DEVICE_ROLE_ROUTER role). + * 'S': LeaderRole (The number of times device entered OT_DEVICE_ROLE_LEADER role). + * 'S': AttachAttempts (The number of attach attempts while device was detached). + * 'S': PartitionIdChanges (The number of changes to partition ID). + * 'S': BetterPartitionAttachAttempts (The number of attempts to attach to a better partition). + * 'S': ParentChanges (The number of times device changed its parents). + * + * Writing to this property with any value would reset all MLE counters to zero. + */ + CNTR_MLE_COUNTERS = 0x500 + 402, + + /// Thread IPv6 counters. + /** Format: `t(LL)t(LL)` + * + * The contents include two structs, first one corresponds to + * all transmit related MAC counters, second one provides the + * receive related counters. + * + * The transmit structure includes: + * 'L': TxSuccess (The number of IPv6 packets successfully transmitted). + * 'L': TxFailure (The number of IPv6 packets failed to transmit). + * + * The receive structure includes: + * 'L': RxSuccess (The number of IPv6 packets successfully received). + * 'L': RxFailure (The number of IPv6 packets failed to receive). + * + * Writing to this property with any value would reset all IPv6 counters to zero. + */ + CNTR_ALL_IP_COUNTERS = 0x500 + 403, + + /// MAC retry histogram. + /** Format: t(A(L))t(A(L)) + * + * Required capability: SPINEL_CAP_MAC_RETRY_HISTOGRAM + * + * The contents include two structs, first one is histogram which corresponds to retransmissions number of direct + * messages, second one provides the histogram of retransmissions for indirect messages. + * + * The first structure includes: + * 'L': DirectRetry[0] (The number of packets after 0 retry). + * 'L': DirectRetry[1] (The number of packets after 1 retry). + * ... + * 'L': DirectRetry[n] (The number of packets after n retry). + * + * The size of the array is OPENTHREAD_CONFIG_MAC_RETRY_SUCCESS_HISTOGRAM_MAX_SIZE_COUNT_DIRECT. + * + * The second structure includes: + * 'L': IndirectRetry[0] (The number of packets after 0 retry). + * 'L': IndirectRetry[1] (The number of packets after 1 retry). + * ... + * 'L': IndirectRetry[m] (The number of packets after m retry). + * + * The size of the array is OPENTHREAD_CONFIG_MAC_RETRY_SUCCESS_HISTOGRAM_MAX_SIZE_COUNT_INDIRECT. + * + * Writing to this property with any value would reset MAC retry histogram. + */ + CNTR_MAC_RETRY_HISTOGRAM = 0x500 + 404, + + /// MAC Key + /** Format: `CCddd`. + * + * `C`: MAC key ID mode + * `C`: MAC key ID + * `d`: previous MAC key material data + * `d`: current MAC key material data + * `d`: next MAC key material data + * + * The Spinel property is used to set/get MAC key materials to and from RCP. + */ + RCP_MAC_KEY = 0x800 + 0, + + /// MAC Frame Counter + /** Format: `L` for read and `Lb` or `L` for write + * + * `L`: MAC frame counter + * 'b': Optional boolean used only during write. If not provided, `false` is assumed. + * If `true` counter is set only if the new value is larger than current value. + * If `false` the new value is set as frame counter independent of the current value. + * + * The Spinel property is used to set MAC frame counter to RCP. + */ + RCP_MAC_FRAME_COUNTER = 0x800 + 1, + + /// Timestamps when Spinel frame is received and transmitted + /** Format: `X`. + * + * `X`: Spinel frame transmit timestamp + * + * The Spinel property is used to get timestamp from RCP to calculate host and RCP timer difference. + */ + RCP_TIMESTAMP = 0x800 + 2, + + /// Configure Enhanced ACK probing + /** Format: `SEC` (Write-only). + * + * `S`: Short address + * `E`: Extended address + * `C`: List of requested metric ids encoded as bit fields in single byte + * + * +---------------+----+ + * | Metric | Id | + * +---------------+----+ + * | Received PDUs | 0 | + * | LQI | 1 | + * | Link margin | 2 | + * | RSSI | 3 | + * +---------------+----+ + * + * Enable/disable or update Enhanced-ACK Based Probing in radio for a specific Initiator. + */ + RCP_ENH_ACK_PROBING = 0x800 + 3, + + /// CSL Accuracy + /** Format: `C` + * Required capability: `SPINEL_CAP_NET_THREAD_1_2` + * + * The current CSL rx/tx scheduling drift, in units of ± ppm. + */ + RCP_CSL_ACCURACY = 0x800 + 4, + + /// CSL Uncertainty + /** Format: `C` + * Required capability: `SPINEL_CAP_NET_THREAD_1_2` + * + * The current uncertainty, in units of 10 us, of the clock used for scheduling CSL operations. + */ + RCP_CSL_UNCERTAINTY = 0x800 + 5, + + /// Multipan interface selection. + /** Format: `C` + * Type: Read-Write + * + * `C`: b[0-1] - Interface id. + * b[7] - 1: Complete pending radio operation, 0: immediate(force) switch. + * + * This feature gets or sets the radio interface to be used in multipan configuration + * + * Default value: 0 + */ + MULTIPAN_ACTIVE_INTERFACE = 0x900 + 0, + + /// Infrastructure interface state. + /** Format: `LbA(6)` + * Type: Write + * + * `L`: The infrastructure interface index. + * `b`: If the infrastructure interface is running. + * `A(6)`: The IPv6 addresses of the infrastructure interface. + * + * If the InfraIf hasn't been set up on NCP or the InfraIf changes, NCP will re-initialize + * the border routing module. NCP will compare the infrastructure interface index and decide + * whether to re-initialize the border routing module. Otherwise, NCP will simply update the + * InfraIf state and addresses. + */ + INFRA_IF_STATE = 0x910 + 1, + + /// Received ICMPv6 packet on the infrastructure interface. + /** Format: `L6d` + * Type: Write-only + * + * `L`: The infrastructure interface index. + * `6`: The IP6 source address of the ICMPv6 packet. + * `d`: The data of the ICMPv6 packet. The host MUST ensure the hoplimit is 255. + */ + INFRA_IF_RECV_ICMP6 = 0x910 + 2, + + /// ICMP6 message sent by NCP and needs to be sent on the infrastructure interface. + /** Format: `L6d` + * Type: Unsolicited notifications only + * + * `L`: The infrastructure interface index. + * `6`: The IP6 destination address of the message to send. + * `d`: The data of the message to send. + */ + INFRA_IF_SEND_ICMP6 = 0x910 + 3, + + /// SRP server state. + /** Format `b` + * Type: Read-Write + * + * `b`: Whether to enable or disable the SRP server. + */ + SRP_SERVER_ENABLED = 0x920 + 1, + + /// SRP server auto enable mode. + /** Format `b` + * Type: Read-Write + * + * `b`: A boolean that indicates the SRP server auto enable mode. + */ + SRP_SERVER_AUTO_ENABLE_MODE = 0x920 + 2, + + /// Dnssd State + /** Format `C`: Write-only + * + * `C`: The dnssd state. + */ + DNSSD_STATE = 0x930 + 1, + + /// Dnssd Request Result + /** Format `CLD`: Write + * + * `C` : The result of the request. A unsigned int8 corresponds to otError. + * `L` : The Dnssd Request ID. + * `D` : The context of the request. (A pointer to the callback for the request) + * + * Host uses this property to notify the NCP of the result of NCP's DNS-SD request. + */ + DNSSD_REQUEST_RESULT = 0x930 + 2, + + /// DNS-SD Host + /** Format `USA(6)LD`: Inserted/Removed + * + * `U` : The host name. + * `S` : The count of IPv6 addresses. + * `A(6)` : The IPv6 addresses of the host. + * `L` : The Dnssd Request ID. + * `D` : The context of the request. (A pointer to the callback for the request) + * + * NCP uses this property to register/unregister a DNS-SD host. + */ + DNSSD_HOST = 0x930 + 3, + + /// DNS-SD Service + /** + * Format `UUUt(A(U))dSSSSLD`: Inserted/Removed + * + * `U` : The host name (does not include domain name). + * `U` : The service instance name label (not the full name). + * `U` : The service type (e.g., "_mt._udp", does not include domain name). + * `t(A(U))` : Array of sub-type labels (can be empty array if no label). + * `d` : Encoded TXT data bytes. + * `S` : The service port number. + * `S` : The service priority. + * `S` : The service weight. + * `L` : The service TTL in seconds. + * `L` : The Dnssd Request ID. + * `D` : The context of the request. (A pointer to the callback for the request) + * + * NCP uses this property to register/unregister a DNS-SD service. + */ + DNSSD_SERVICE = 0x930 + 4, + + /// DNS-SD Key Record + /** + * Format `Ut(U)dSSLD`: Inserted/Removed + * + * `U` : A host or a service instance name (does not include domain name). + * `t(U)` : The service type if key is for a service (does not include domain name). + * `d` : Byte array containing the key record data. + * `S` : The resource record class. + * `L` : The TTL in seconds. + * `L` : The Dnssd Request ID. + * `D` : The context of the request. (A pointer to the callback for the request) + * + * NCP uses this property to register/unregister a DNS-SD key record. + */ + DNSSD_KEY_RECORD = 0x930 + 5, + + NEST_STREAM_MFG = 0x3bc0 + 0, + + /// The legacy network ULA prefix (8 bytes). + /** Format: 'D' + * + * This property is deprecated. + */ + NEST_LEGACY_ULA_PREFIX = 0x3bc0 + 1, + + /// The EUI64 of last node joined using legacy protocol (if none, all zero EUI64 is returned). + /** Format: 'E' + * + * This property is deprecated. + */ + NEST_LEGACY_LAST_NODE_JOINED = 0x3bc0 + 2, + + /// Testing platform assert + /** Format: 'b' (read-only) + * + * Reading this property will cause an assert on the NCP. This is intended for testing the assert functionality of + * underlying platform/NCP. Assert should ideally cause the NCP to reset, but if this is not supported a `false` + * boolean is returned in response. + */ + DEBUG_TEST_ASSERT = 0x4000 + 0, + + /// The NCP log level. + /** Format: `C` */ + DEBUG_NCP_LOG_LEVEL = 0x4000 + 1, + + /// Testing platform watchdog + /** Format: Empty (read-only) + * + * Reading this property will causes NCP to start a `while(true) ;` loop and thus triggering a watchdog. + * + * This is intended for testing the watchdog functionality on the underlying platform/NCP. + */ + DEBUG_TEST_WATCHDOG = 0x4000 + 2, + + /// The NCP timestamp base + /** Format: X (write-only) + * + * This property controls the time base value that is used for logs timestamp field calculation. + */ + DEBUG_LOG_TIMESTAMP_BASE = 0x4000 + 3, + + /// TREL Radio Link - test mode enable + /** Format `b` (read-write) + * + * This property is intended for testing TREL (Thread Radio Encapsulation Link) radio type only (during simulation). + * It allows the TREL interface to be temporarily disabled and (re)enabled. While disabled all traffic through + * TREL interface is dropped silently (to emulate a radio/interface down scenario). + * + * This property is only available when the TREL radio link type is supported. + */ + DEBUG_TREL_TEST_MODE_ENABLE = 0x4000 + 4, +} diff --git a/src/spinel/spinel.ts b/src/spinel/spinel.ts new file mode 100644 index 0000000..0289060 --- /dev/null +++ b/src/spinel/spinel.ts @@ -0,0 +1,621 @@ +import assert from "node:assert"; + +import { type HdlcFrame, encodeHdlcFrame } from "./hdlc.js"; +import { SpinelPropertyId } from "./properties.js"; + +/** + * Spinel data types: + * + * +----------+----------------------+---------------------------------+ + * | Char | Name | Description | + * +----------+----------------------+---------------------------------+ + * | "." | DATATYPE_VOID | Empty data type. Used | + * | | | internally. | + * | "b" | DATATYPE_BOOL | Boolean value. Encoded in | + * | | | 8-bits as either 0x00 or 0x01. | + * | | | All other values are illegal. | + * | "C" | DATATYPE_UINT8 | Unsigned 8-bit integer. | + * | "c" | DATATYPE_INT8 | Signed 8-bit integer. | + * | "S" | DATATYPE_UINT16 | Unsigned 16-bit integer. | + * | "s" | DATATYPE_INT16 | Signed 16-bit integer. | + * | "L" | DATATYPE_UINT32 | Unsigned 32-bit integer. | + * | "l" | DATATYPE_INT32 | Signed 32-bit integer. | + * | "i" | DATATYPE_UINT_PACKED | Packed Unsigned Integer. See | + * | | | Section 3.2. | + * | "6" | DATATYPE_IPv6ADDR | IPv6 Address. (Big-endian) | + * | "E" | DATATYPE_EUI64 | EUI-64 Address. (Big-endian) | + * | "e" | DATATYPE_EUI48 | EUI-48 Address. (Big-endian) | + * | "D" | DATATYPE_DATA | Arbitrary data. See Section | + * | | | 3.3. | + * | "d" | DATATYPE_DATA_WLEN | Arbitrary data with prepended | + * | | | length. See Section 3.3. | + * | "U" | DATATYPE_UTF8 | Zero-terminated UTF8-encoded | + * | | | string. | + * | "t(...)" | DATATYPE_STRUCT | Structured datatype with | + * | | | prepended length. See Section | + * | | | 3.4. | + * | "A(...)" | DATATYPE_ARRAY | Array of datatypes. Compound | + * | | | type. See Section 3.5. | + * +----------+----------------------+---------------------------------+ + */ + +/** + * 0 1 2 3 4 5 6 7 + * +---+---+---+---+---+---+---+---+ + * | FLG | NLI | TID | + * +---+---+---+---+---+---+---+---+ + */ +type SpinelFrameHeader = { + /** + * The least significant bits of the header represent the Transaction + * Identifier(TID). The TID is used for correlating responses to the + * commands which generated them. + * + * When a command is sent from the host, any reply to that command sent + * by the NCP will use the same value for the TID. When the host + * receives a frame that matches the TID of the command it sent, it can + * easily recognize that frame as the actual response to that command. + * + * The TID value of zero (0) is used for commands to which a correlated + * response is not expected or needed, such as for unsolicited update + * commands sent to the host from the NCP. + */ + tid: number; + /** + * The Network Link Identifier (NLI) is a number between 0 and 3, which + * is associated by the OS with one of up to four IPv6 zone indices + * corresponding to conceptual IPv6 interfaces on the NCP. This allows + * the protocol to support IPv6 nodes connecting simultaneously to more + * than one IPv6 network link using a single NCP instance. The first + * Network Link Identifier (0) MUST refer to a distinguished conceptual + * interface provided by the NCP for its IPv6 link type. The other + * three Network Link Identifiers (1, 2 and 3) MAY be dissociated from + * any conceptual interface. + */ + nli: number; + /** + * The flag field of the header byte ("FLG") is always set to the value + * two (or "10" in binary). Any frame received with these bits set to + * any other value else MUST NOT be considered a Spinel frame. + * + * This convention allows Spinel to be line compatible with BTLE HCI. + * By defining the first two bit in this way we can disambiguate between + * Spinel frames and HCI frames (which always start with either "0x01" + * or "0x04") without any additional framing overhead. + */ + flg: number; +}; + +/** + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | HEADER | COMMAND ID | PAYLOAD ... + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ +export type SpinelFrame = { + header: SpinelFrameHeader; + /** + * The command identifier is a 21-bit unsigned integer encoded in up to + * three bytes using the packed unsigned integer format described in + * Section 3.2. This encoding allows for up to 2,097,152 individual + * commands, with the first 127 commands represented as a single byte. + * Command identifiers larger than 2,097,151 are explicitly forbidden. + * + * +-----------------------+----------------------------+ + * | CID Range | Description | + * +-----------------------+----------------------------+ + * | 0 - 63 | Reserved for core commands | + * | 64 - 15,359 | _UNALLOCATED_ | + * | 15,360 - 16,383 | Vendor-specific | + * | 16,384 - 1,999,999 | _UNALLOCATED_ | + * | 2,000,000 - 2,097,151 | Experimental use only | + * +-----------------------+----------------------------+ + */ + commandId: number; + /** + * Depending on the semantics of the command in question, a payload MAY + * be included in the frame. The exact composition and length of the + * payload is defined by the command identifier. + */ + payload: Buffer; +}; + +export const enum SpinelResetReason { + PLATFORM = 1, + STACK = 2, + BOOTLOADER = 3, +} + +const SPINEL_HEADER_TID_MASK = 0x0f; +const SPINEL_HEADER_NLI_MASK = 0x30; +const SPINEL_HEADER_NLI_SHIFT = 4; +const SPINEL_HEADER_FLG_MASK = 0xc0; +const SPINEL_HEADER_FLG_SHIFT = 6; +/** @see SpinelFrameHeader.flg */ +export const SPINEL_HEADER_FLG_SPINEL = 2; + +export const SPINEL_RCP_API_VERSION = 11; + +/** + * Decode HDLC frame into Spinel frame + */ +export function decodeSpinelFrame(hdlcFrame: HdlcFrame): SpinelFrame { + const header = hdlcFrame.data[0]; + const tid = header & SPINEL_HEADER_TID_MASK; + const nli = (header & SPINEL_HEADER_NLI_MASK) >> SPINEL_HEADER_NLI_SHIFT; + const flg = (header & SPINEL_HEADER_FLG_MASK) >> SPINEL_HEADER_FLG_SHIFT; + const [commandId, outOffset] = getPackedUInt(hdlcFrame.data, 1); + const payload = hdlcFrame.data.subarray(outOffset, hdlcFrame.length); + + return { + header: { tid, nli, flg }, + commandId, + payload, + }; +} + +/** + * Encode Spinel frame into HDLC frame + */ +export function encodeSpinelFrame(frame: SpinelFrame): HdlcFrame { + const cmdIdSize = getPackedUIntSize(frame.commandId); + const buffer = Buffer.alloc(frame.payload.byteLength + 1 + cmdIdSize); + const headerByte = + (frame.header.tid & SPINEL_HEADER_TID_MASK) | + ((frame.header.nli << SPINEL_HEADER_NLI_SHIFT) & SPINEL_HEADER_NLI_MASK) | + ((frame.header.flg << SPINEL_HEADER_FLG_SHIFT) & SPINEL_HEADER_FLG_MASK); + buffer[0] = headerByte; + const outOffset = setPackedUInt(buffer, 1, frame.commandId, cmdIdSize); + buffer.set(frame.payload, outOffset); + + return encodeHdlcFrame(buffer); +} + +const SPINEL_PACKED_UINT_MASK = 0x80; +const SPINEL_PACKED_UINT_MSO_MASK = 0x7f; + +export function getPackedUIntSize(value: number): number { + if (value < 1 << 7) { + return 1; + } + + if (value < 1 << 14) { + return 2; + } + + if (value < 1 << 21) { + return 3; + } + + if (value < 1 << 28) { + return 4; + } + + return 5; +} + +export function setPackedUInt(data: Buffer, offset: number, value: number, size?: number): number { + if (!size) { + size = getPackedUIntSize(value); + } + + for (let i = 0; i !== size - 1; i++) { + data[offset] = (value & SPINEL_PACKED_UINT_MSO_MASK) | SPINEL_PACKED_UINT_MASK; + offset += 1; + + value >>= 7; + } + + data[offset] = value & SPINEL_PACKED_UINT_MSO_MASK; + offset += 1; + + return offset; +} + +export function getPackedUInt(data: Buffer, offset: number): [value: number, outOffset: number] { + let value = 0; + let i = 0; + + do { + if (i >= 40) { + throw new Error(`Invalid Packet UInt, got ${i}, expected < 40`); + } + + value |= (data[offset] & SPINEL_PACKED_UINT_MSO_MASK) << i; + i += 7; + offset += 1; + } while ((data[offset - 1] & SPINEL_PACKED_UINT_MASK) === SPINEL_PACKED_UINT_MASK); + + return [value, offset]; +} + +/** Create output array of given (size + property size) and set the property ID at index 0 */ +export function writePropertyId(propertyId: SpinelPropertyId, size: number): [Buffer, outOffset: number] { + const propIdSize = getPackedUIntSize(propertyId); + const buf = Buffer.alloc(propIdSize + size); + const offset = setPackedUInt(buf, 0, propertyId, propIdSize); + + return [buf, offset]; +} + +/** Write as boolean */ +export function writePropertyb(propertyId: SpinelPropertyId, value: boolean): Buffer { + const [buf, offset] = writePropertyId(propertyId, 1); + buf[offset] = value ? 1 : 0; + + return buf; +} + +/** Read as boolean */ +export function readPropertyb(propertyId: SpinelPropertyId, data: Buffer, offset = 0): boolean { + const [propId, pOutOffset] = getPackedUInt(data, offset); + assert(propId === propertyId); + + return !!data[pOutOffset]; +} + +/** Write as uint8 */ +export function writePropertyC(propertyId: SpinelPropertyId, value: number): Buffer { + const [buf, offset] = writePropertyId(propertyId, 1); + buf[offset] = value as number; + + return buf; +} + +/** Read as uint8 */ +export function readPropertyC(propertyId: SpinelPropertyId, data: Buffer, offset = 0): number { + const [propId, pOutOffset] = getPackedUInt(data, offset); + assert(propId === propertyId); + + return data[pOutOffset]; +} + +/** Write as int8 */ +export function writePropertyc(propertyId: SpinelPropertyId, value: number): Buffer { + const [buf, offset] = writePropertyId(propertyId, 1); + + buf.writeInt8(value as number, offset); + + return buf; +} + +/** Write as uint16 */ +export function writePropertyS(propertyId: SpinelPropertyId, value: number): Buffer { + const [buf, offset] = writePropertyId(propertyId, 2); + + buf.writeUInt16LE(value as number, offset); + + return buf; +} + +/** Read as uint16 */ +export function readPropertyS(propertyId: SpinelPropertyId, data: Buffer, offset = 0): number { + const [propId, pOutOffset] = getPackedUInt(data, offset); + assert(propId === propertyId); + + return data.readUInt16LE(pOutOffset); +} + +/** Write as int16 */ +export function writePropertys(propertyId: SpinelPropertyId, value: number): Buffer { + const [buf, offset] = writePropertyId(propertyId, 2); + + buf.writeInt16LE(value as number, offset); + + return buf; +} + +/** Write as uint32 */ +export function writePropertyL(propertyId: SpinelPropertyId, value: number): Buffer { + const [buf, offset] = writePropertyId(propertyId, 4); + + buf.writeUInt32LE(value as number, offset); + + return buf; +} + +/** Write as int32 */ +export function writePropertyl(propertyId: SpinelPropertyId, value: number): Buffer { + const [buf, offset] = writePropertyId(propertyId, 4); + + buf.writeInt32LE(value as number, offset); + + return buf; +} + +/** Write as packed uint */ +export function writePropertyi(propertyId: SpinelPropertyId, value: number): Buffer { + const valueSize = getPackedUIntSize(value as number); + const [buf, offset] = writePropertyId(propertyId, valueSize); + + setPackedUInt(buf, offset, value as number, valueSize); + + return buf; +} + +/** Read as packed uint */ +export function readPropertyi(propertyId: SpinelPropertyId, data: Buffer, offset = 0): number { + const [propId, pOutOffset] = getPackedUInt(data, offset); + assert(propId === propertyId); + + const [i] = getPackedUInt(data, pOutOffset); + + return i; +} + +/** Read as packed uint x2 */ +export function readPropertyii(propertyId: SpinelPropertyId, data: Buffer, offset = 0): [i1: number, i2: number] { + const [propId, pOutOffset] = getPackedUInt(data, offset); + assert(propId === propertyId); + + const [major, maOutOffset] = getPackedUInt(data, pOutOffset); + const [minor] = getPackedUInt(data, maOutOffset); + + return [major, minor]; +} + +/** Read as list of packed uint */ +export function readPropertyAi(propertyId: SpinelPropertyId, data: Buffer, offset = 0): number[] { + const [propId, pOutOffset] = getPackedUInt(data, offset); + assert(propId === propertyId); + + const caps: number[] = []; + + for (let i = pOutOffset; i < data.byteLength; ) { + const [cap, cOutOffset] = getPackedUInt(data, i); + + caps.push(cap); + + i += cOutOffset; + } + + return caps; +} + +/** Write as UTF8 string */ +export function writePropertyU(propertyId: SpinelPropertyId, value: string): Buffer { + const [buf, offset] = writePropertyId(propertyId, value.length); + + buf.write(value, offset, "utf8"); + + return buf; +} + +/** Read as UTF8 string */ +export function readPropertyU(propertyId: SpinelPropertyId, data: Buffer, offset = 0): string { + const [propId, pOutOffset] = getPackedUInt(data, offset); + assert(propId === propertyId); + + return data.toString("utf8", pOutOffset); +} + +/** Write as bigint */ +export function writePropertyE(propertyId: SpinelPropertyId, value: bigint): Buffer { + const [buf, offset] = writePropertyId(propertyId, 8); + + buf.writeBigUInt64BE(value, offset); + + return buf; +} + +/** Read as bigint */ +export function readPropertyE(propertyId: SpinelPropertyId, data: Buffer, offset = 0): bigint { + const [propId, pOutOffset] = getPackedUInt(data, offset); + assert(propId === propertyId); + + return data.readBigUInt64BE(pOutOffset); +} + +/** Write as Buffer of specific length */ +export function writePropertyd(propertyId: SpinelPropertyId, value: Buffer): Buffer { + const [buf, offset] = writePropertyId(propertyId, 2 + value.byteLength); + + buf.writeUInt16LE(value.byteLength, offset); + buf.set(value, offset); + + return buf; +} + +/** Read as Buffer of specific length */ +export function readPropertyd(propertyId: SpinelPropertyId, data: Buffer, offset = 0): Buffer { + const [propId, pOutOffset] = getPackedUInt(data, offset); + assert(propId === propertyId); + + const length = data.readUInt16LE(pOutOffset); + const lOutOffset = pOutOffset + 2; + + return data.subarray(lOutOffset, lOutOffset + length); +} + +/** Write as Buffer of remaining length */ +export function writePropertyD(propertyId: SpinelPropertyId, value: Buffer): Buffer { + const [buf, offset] = writePropertyId(propertyId, value.byteLength); + + buf.set(value, offset); + + return buf; +} + +/** Read as Buffer of remaining length */ +export function readPropertyD(propertyId: SpinelPropertyId, data: Buffer, offset = 0): Buffer { + const [propId, pOutOffset] = getPackedUInt(data, offset); + assert(propId === propertyId); + + return data.subarray(pOutOffset); +} + +/** @see SpinelPropertyId.STREAM_RAW */ +export type StreamRawConfig = { + /** `C` : Channel (for frame tx) - MUST be included. */ + txChannel: number; + /** `C` : Maximum number of backoffs attempts before declaring CCA failure (use Thread stack default if not specified) */ + ccaBackoffAttempts: number; + /** `C` : Maximum number of retries allowed after a transmission failure (use Thread stack default if not specified) */ + ccaRetries: number; + /** `b` : Set to true to enable CSMA-CA for this packet, false otherwise. (default true). */ + enableCSMACA: boolean; + /** `b` : Set to true to indicate if header is updated - related to `mIsHeaderUpdated` in `otRadioFrame` (default false). */ + headerUpdated: boolean; + /** `b` : Set to true to indicate it is a retransmission - related to `mIsARetx` in `otRadioFrame` (default false). */ + reTx: boolean; + /** `b` : Set to true to indicate security was processed on tx frame `mIsSecurityProcessed` in `otRadioFrame` (default false). */ + securityProcessed: boolean; + /** `L` : TX delay interval used for CSL - related to `mTxDelay` in `otRadioFrame` (default zero). */ + txDelay: number; + /** `L` : TX delay based time used for CSL - related to `mTxDelayBaseTime` in `otRadioFrame` (default zero). */ + txDelayBaseTime: number; + /** `C` : RX channel after TX done (default assumed to be same as channel in metadata) */ + rxChannelAfterTxDone: number; +}; + +/** @see https://datatracker.ietf.org/doc/html/draft-rquattle-spinel-unified#section-5.6.2 */ +export function writePropertyStreamRaw(data: Buffer, config: StreamRawConfig): Buffer { + const [buf, pOutOffset] = writePropertyId(SpinelPropertyId.STREAM_RAW, data.byteLength + 18); + let offset = pOutOffset; + + buf.writeUInt16LE(data.byteLength, offset); + offset += 2; + + buf.set(data, offset); + offset += data.byteLength; + + buf.writeUInt8(config.txChannel, offset); + offset += 1; + + buf.writeUInt8(config.ccaBackoffAttempts, offset); + offset += 1; + + buf.writeUInt8(config.ccaRetries, offset); + offset += 1; + + buf.writeUInt8(config.enableCSMACA ? 1 : 0, offset); + offset += 1; + + buf.writeUInt8(config.headerUpdated ? 1 : 0, offset); + offset += 1; + + buf.writeUInt8(config.reTx ? 1 : 0, offset); + offset += 1; + + buf.writeUInt8(config.securityProcessed ? 1 : 0, offset); + offset += 1; + + buf.writeUInt32LE(config.txDelay, offset); + offset += 4; + + buf.writeUInt32LE(config.txDelayBaseTime, offset); + offset += 4; + + buf.writeUInt8(config.rxChannelAfterTxDone, offset); + offset += 1; + + return buf; +} + +/** + * @see https://datatracker.ietf.org/doc/html/draft-rquattle-spinel-unified#section-5.6.2.1 + * +----------+-----------------------+------------+-----+---------+ + * | Field | Description | Type | Len | Default | + * +----------+-----------------------+------------+-----+---------+ + * | MD_POWER | (dBm) RSSI/TX-Power | "c" int8 | 1 | -128 | + * | MD_NOISE | (dBm) Noise floor | "c" int8 | 1 | -128 | + * | MD_FLAG | Flags (defined below) | "S" uint16 | 2 | | + * | MD_PHY | PHY-specific data | "d" data | >=2 | | + * | MD_VEND | Vendor-specific data | "d" data | >=2 | | + * +----------+-----------------------+------------+-----+---------+ + * + * The bit values in "MD_FLAG" are defined as follows: + * +---------+--------+------------------+-----------------------------+ + * | Bit | Mask | Name | Description if set | + * +---------+--------+------------------+-----------------------------+ + * | 15 | 0x0001 | MD_FLAG_TX | Packet was transmitted, not | + * | | | | received. | + * | 13 | 0x0004 | MD_FLAG_BAD_FCS | Packet was received with | + * | | | | bad FCS | + * | 12 | 0x0008 | MD_FLAG_DUPE | Packet seems to be a | + * | | | | duplicate | + * | 0-11, | 0xFFF2 | MD_FLAG_RESERVED | Flags reserved for future | + * | 14 | | | use. | + * +---------+--------+------------------+-----------------------------+ + */ +export type SpinelStreamRawMetadata = { + rssi: number; + noiseFloor: number; + flags: number; + // XXX: unreliable? + // phyData: unknown; + // vendorData: unknown; + // macData: unknown; +}; + +/** + * @see https://datatracker.ietf.org/doc/html/draft-rquattle-spinel-unified#section-5.6.2.1 + * + * Assumes payload comes from `spinel.payload` and offset is right after `SpinelPropertyId.STREAM_RAW`, per below + * + * Packed-Encoding: "dD" + * + * +---------+----------------+------------+----------------+ + * | Octets: | 2 | n | n | + * +---------+----------------+------------+----------------+ + * | Fields: | FRAME_DATA_LEN | FRAME_DATA | FRAME_METADATA | + * +---------+----------------+------------+----------------+ + * + * from pyspinel (https://github.com/openthread/pyspinel/blob/main/sniffer.py#L283): + * metadata format (totally 19 bytes or 26 bytes): + * 0. RSSI(int8) + * 1. Noise Floor(int8) + * 2. Flags(uint16) + * 3. PHY-specific data struct contains: + * 3.0 Channel(uint8) + * 3.1 LQI(uint8) + * 3.2 Timestamp in microseconds(uint64) + * 4. Vendor data struct contains: + * 4.0 Receive error(uint8) + * 5. (optional) MAC data struct contains: + * 5.0 ACK key ID(uint8) + * 5.1 ACK frame counter(uint32) + */ +export function readPropertyStreamRaw(payload: Buffer, offset: number): [macData: Buffer, metadata: SpinelStreamRawMetadata | undefined] { + const frameDataLen = payload.readUInt16LE(offset); + offset += 2; + let metaOffset = offset + frameDataLen; + let metadata: SpinelStreamRawMetadata | undefined; + + if (payload.byteLength > metaOffset) { + const rssi = payload.readInt8(metaOffset); + metaOffset += 1; + const noiseFloor = payload.readInt8(metaOffset); + metaOffset += 1; + const flags = payload.readUInt16LE(metaOffset); + metaOffset += 2; + // silabs PHY: channel: ok, lqi: 0xff or 0x00 (not working?), timestamp: seems ok + // silabs VEN: error: 0x00... always? + // silabs MAC: ackKeyId: 0?, ackFramceCounter: 0? + + // const phyDataLen = payload.readUInt16LE(metaOffset); + // metaOffset += 2; + // console.log('phy', payload.subarray(metaOffset, metaOffset + phyDataLen).toString('hex')); + // metaOffset += phyDataLen; + // const vendorDataLen = payload.readUInt16LE(metaOffset); + // metaOffset += 2; + // console.log('ven', payload.subarray(metaOffset, metaOffset + vendorDataLen).toString('hex')); + // metaOffset += vendorDataLen; + // const macDataLen = payload.readUInt16LE(metaOffset); + // metaOffset += 2; + // console.log('mac', payload.subarray(metaOffset, metaOffset + macDataLen).toString('hex')); + // metaOffset += macDataLen; + + metadata = { + rssi, + noiseFloor, + flags, + // phyData: undefined, + // vendorData: undefined, + // macData: undefined, + }; + } + + return [payload.subarray(offset, offset + frameDataLen), metadata]; +} diff --git a/src/spinel/statuses.ts b/src/spinel/statuses.ts new file mode 100644 index 0000000..82c7544 --- /dev/null +++ b/src/spinel/statuses.ts @@ -0,0 +1,118 @@ +export enum SpinelStatus { + /** Operation has completed successfully. */ + OK = 0, + /** Operation has failed for some undefined reason. */ + FAILURE = 1, + /** Given operation has not been implemented. */ + UNIMPLEMENTED = 2, + /** An argument to the operation is invalid. */ + INVALID_ARGUMENT = 3, + /** This operation is invalid for the current device state. */ + INVALID_STATE = 4, + /** This command is not recognized. */ + INVALID_COMMAND = 5, + /** This interface is not supported. */ + INVALID_INTERFACE = 6, + /** An internal runtime error has occurred. */ + INTERNAL_ERROR = 7, + /** A security/authentication error has occurred. */ + SECURITY_ERROR = 8, + /** A error has occurred while parsing the command. */ + PARSE_ERROR = 9, + /** This operation is in progress. */ + IN_PROGRESS = 10, + /** Operation prevented due to memory pressure. */ + NOMEM = 11, + /** The device is currently performing a mutually exclusive operation */ + BUSY = 12, + /** The given property is not recognized. */ + PROP_NOT_FOUND = 13, + /** A/The packet was dropped. */ + DROPPED = 14, + /** The result of the operation is empty. */ + EMPTY = 15, + /** The command was too large to fit in the internal buffer. */ + CMD_TOO_BIG = 16, + /** The packet was not acknowledged. */ + NO_ACK = 17, + /** The packet was not sent due to a CCA failure. */ + CCA_FAILURE = 18, + /** The operation is already in progress. */ + ALREADY = 19, + /** The given item could not be found. */ + ITEM_NOT_FOUND = 20, + /** The given command cannot be performed on this property. */ + INVALID_COMMAND_FOR_PROP = 21, + /** The neighbor is unknown. */ + UNKNOWN_NEIGHBOR = 22, + /** The target is not capable of handling requested operation. */ + NOT_CAPABLE = 23, + /** No response received from remote node */ + RESPONSE_TIMEOUT = 24, + /** Radio interface switch completed successfully (SPINEL_PROP_MULTIPAN_ACTIVE_INTERFACE) */ + SWITCHOVER_DONE = 25, + /** Radio interface switch failed (SPINEL_PROP_MULTIPAN_ACTIVE_INTERFACE) */ + SWITCHOVER_FAILED = 26, + + /** + * Generic failure to associate with other peers. + * + * This status error should not be used by implementers if + * enough information is available to determine that one of the + * later join failure status codes would be more accurate. + * + * \sa SPINEL_PROP_NET_REQUIRE_JOIN_EXISTING + * \sa SPINEL_PROP_MESHCOP_JOINER_COMMISSIONING + */ + JOIN_FAILURE = 104, + + /** + * The node found other peers but was unable to decode their packets. + * + * Typically this error code indicates that the network + * key has been set incorrectly. + * + * \sa SPINEL_PROP_NET_REQUIRE_JOIN_EXISTING + * \sa SPINEL_PROP_MESHCOP_JOINER_COMMISSIONING + */ + JOIN_SECURITY = 105, + + /** + * The node was unable to find any other peers on the network. + * + * \sa SPINEL_PROP_NET_REQUIRE_JOIN_EXISTING + * \sa SPINEL_PROP_MESHCOP_JOINER_COMMISSIONING + */ + JOIN_NO_PEERS = 106, + + /** + * The only potential peer nodes found are incompatible. + * + * \sa SPINEL_PROP_NET_REQUIRE_JOIN_EXISTING + */ + JOIN_INCOMPATIBLE = 107, + + /** + * No response in expecting time. + * + * \sa SPINEL_PROP_MESHCOP_JOINER_COMMISSIONING + */ + JOIN_RSP_TIMEOUT = 108, + + /** + * The node succeeds in commissioning and get the network credentials. + * + * \sa SPINEL_PROP_MESHCOP_JOINER_COMMISSIONING + */ + JOIN_SUCCESS = 109, + + RESET_POWER_ON = 112, + RESET_EXTERNAL = 113, + RESET_SOFTWARE = 114, + RESET_FAULT = 115, + RESET_CRASH = 116, + RESET_ASSERT = 117, + RESET_OTHER = 118, + RESET_UNKNOWN = 119, + RESET_WATCHDOG = 120, +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..65a0af7 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,20 @@ +export interface Logger { + debug: (messageOrLambda: string | (() => string), namespace: string) => void; + info: (messageOrLambda: string | (() => string), namespace: string) => void; + warning: (messageOrLambda: string | (() => string), namespace: string) => void; + error: (messageOrLambda: string, namespace: string) => void; +} + +export let logger: Logger = { + debug: (messageOrLambda, namespace) => + console.debug(`[${new Date().toISOString()}] ${namespace}: ${typeof messageOrLambda === "function" ? messageOrLambda() : messageOrLambda}`), + info: (messageOrLambda, namespace) => + console.info(`[${new Date().toISOString()}] ${namespace}: ${typeof messageOrLambda === "function" ? messageOrLambda() : messageOrLambda}`), + warning: (messageOrLambda, namespace) => + console.warn(`[${new Date().toISOString()}] ${namespace}: ${typeof messageOrLambda === "function" ? messageOrLambda() : messageOrLambda}`), + error: (message, namespace) => console.error(`[${new Date().toISOString()}] ${namespace}: ${message}`), +}; + +export function setLogger(l: Logger): void { + logger = l; +} diff --git a/src/zigbee/mac.ts b/src/zigbee/mac.ts new file mode 100644 index 0000000..14d57ea --- /dev/null +++ b/src/zigbee/mac.ts @@ -0,0 +1,1405 @@ +/** + * const enum with sole purpose of avoiding "magic numbers" in code for well-known values + */ +export const enum ZigbeeMACConsts { + //---- Special IEEE802.15.4 Addresses + NO_ADDR16 = 0xfffe, + BCAST_ADDR = 0xffff, + BCAST_PAN = 0xffff, + + HEADER_SIZE = 11, // 9 + 2 FCS + FRAME_MAX_SIZE = 127, + /** + * IEEE 802.15.4-2020: + * - aMaxMACPayloadSize(118) + * - aMaxMACSafePayloadSize(102) + */ + PAYLOAD_MAX_SIZE = 116, // zigbee-payload-calculator (r19) + PAYLOAD_MAX_SAFE_SIZE = 102, + ACK_FRAME_SIZE = 11, + + //---- Bit-masks for the FCF + /** Frame Type Mask */ + FCF_TYPE_MASK = 0x0007, + FCF_SEC_EN = 0x0008, + FCF_FRAME_PND = 0x0010, + FCF_ACK_REQ = 0x0020, + /** known as Intra PAN prior to IEEE 802.15.4-2006 */ + FCF_PAN_ID_COMPRESSION = 0x0040, + FCF_SEQNO_SUPPRESSION = 0x0100, + FCF_IE_PRESENT = 0x0200, + /** destination addressing mask */ + FCF_DADDR_MASK = 0x0c00, + FCF_VERSION = 0x3000, + /** source addressing mask */ + FCF_SADDR_MASK = 0xc000, + + /* Auxiliary Security Header */ + AUX_SEC_LEVEL_MASK = 0x07, + AUX_KEY_ID_MODE_MASK = 0x18, + AUX_KEY_ID_MODE_SHIFT = 3, + /** 802.15.4-2015 */ + AUX_FRAME_COUNTER_SUPPRESSION_MASK = 0x20, + /** 802.15.4-2015 */ + AUX_ASN_IN_NONCE_MASK = 0x40, + /* Note: 802.15.4-2015 specifies bits 6-7 as reserved, but 6 is used for ASN */ + // MAC_AUX_CTRL_RESERVED_MASK = 0x80, + + SUPERFRAME_BEACON_ORDER_MASK = 0x000f, + SUPERFRAME_ORDER_MASK = 0x00f0, + SUPERFRAME_CAP_MASK = 0x0f00, + SUPERFRAME_BATT_EXTENSION_MASK = 0x1000, + SUPERFRAME_COORD_MASK = 0x4000, + SUPERFRAME_ASSOC_PERMIT_MASK = 0x8000, + SUPERFRAME_ORDER_SHIFT = 4, + SUPERFRAME_CAP_SHIFT = 8, + SUPERFRAME_BATT_EXTENSION_SHIFT = 12, + SUPERFRAME_COORD_SHIFT = 14, + SUPERFRAME_ASSOC_PERMIT_SHIFT = 15, + + GTS_COUNT_MASK = 0x07, + GTS_PERMIT_MASK = 0x80, + GTS_SLOT_MASK = 0x0f, + GTS_LENGTH_MASK = 0xf0, + GTS_LENGTH_SHIFT = 4, + + PENDADDR_SHORT_MASK = 0x07, + PENDADDR_LONG_MASK = 0x70, + PENDADDR_LONG_SHIFT = 4, + + HEADER_IE_TYPE_MASK = 0x8000, + HEADER_IE_ID_MASK = 0x7f80, + HEADER_IE_LENGTH_MASK = 0x007f, + HEADER_IE_HT1 = 0x7e, + HEADER_IE_HT2 = 0x7f, + + /** currently assumed always 2 */ + FCS_LEN = 2, + + //---- ZigBee-specific + ZIGBEE_PAYLOAD_IE_OUI = 0x4a191b, + + ZIGBEE_BEACON_PROTOCOL_ID = 0x00, + ZIGBEE_BEACON_STACK_PROFILE_MASK = 0x000f, + ZIGBEE_BEACON_PROTOCOL_VERSION_MASK = 0x00f0, + ZIGBEE_BEACON_PROTOCOL_VERSION_SHIFT = 4, + ZIGBEE_BEACON_ROUTER_CAPACITY_MASK = 0x0400, + ZIGBEE_BEACON_ROUTER_CAPACITY_SHIFT = 10, + ZIGBEE_BEACON_NETWORK_DEPTH_MASK = 0x7800, + ZIGBEE_BEACON_NETWORK_DEPTH_SHIFT = 11, + ZIGBEE_BEACON_END_DEVICE_CAPACITY_MASK = 0x8000, + ZIGBEE_BEACON_END_DEVICE_CAPACITY_SHIFT = 15, + ZIGBEE_BEACON_LENGTH = 15, + + ZIGBEE_BEACON_TX_OFFSET_MASK = 0xffffff, + ZIGBEE_BEACON_UPDATE_ID_MASK = 0xff, + ZIGBEE_BEACON_UPDATE_ID_SHIFT = 24, +} + +/** Frame Type Definitions */ +export const enum MACFrameType { + /** Beacon Frame */ + BEACON = 0, + /** Data Frame */ + DATA = 1, + /** Acknowlegement Frame */ + ACK = 2, + /** MAC Command Frame */ + CMD = 3, + /** reserved */ + RESERVED = 4, + /** Multipurpose */ + MULTIPURPOSE = 5, + /** Fragment or Frak */ + FRAGMENT = 6, + /** Extended */ + EXTENDED = 7, +} + +/** Frame version definitions. */ +export const enum MACFrameVersion { + /** conforming to the 802.15.4-2003 standard */ + v2003 = 0, + /** conforming to the 802.15.4-2006 standard */ + v2006 = 1, + /** conforming to the 802.15.4-2015 standard */ + v2015 = 2, + RESERVED = 3, +} + +/** Address Mode Definitions */ +export const enum MACFrameAddressMode { + /** PAN identifier and address field are not present. */ + NONE = 0, + RESERVED = 1, + /** Address field contains a 16 bit short address. */ + SHORT = 2, + /** Address field contains a 64 bit extended address. */ + EXT = 3, +} + +/** Definitions for Association Response Command */ +export enum MACAssociationStatus { + SUCCESS = 0x00, + PAN_FULL = 0x01, + PAN_ACCESS_DENIED = 0x02, +} + +/** Command Frame Identifier Types Definitions */ +export const enum MACCommandId { + ASSOC_REQ = 0x01, + ASSOC_RSP = 0x02, + DISASSOC_NOTIFY = 0x03, + DATA_RQ = 0x04, + PANID_CONFLICT = 0x05, + ORPHAN_NOTIFY = 0x06, + BEACON_REQ = 0x07, + COORD_REALIGN = 0x08, + GTS_REQ = 0x09, + TRLE_MGMT_REQ = 0x0a, + TRLE_MGMT_RSP = 0x0b, + /* 0x0c-0x12 reserved in IEEE802.15.4-2015 */ + DSME_ASSOC_REQ = 0x13, + DSME_ASSOC_RSP = 0x14, + DSME_GTS_REQ = 0x15, + DSME_GTS_RSP = 0x16, + DSME_GTS_NOTIFY = 0x17, + DSME_INFO_REQ = 0x18, + DSME_INFO_RSP = 0x19, + DSME_BEACON_ALLOC_NOTIFY = 0x1a, + DSME_BEACON_COLL_NOTIFY = 0x1b, + DSME_LINK_REPORT = 0x1c, + /* 0x1d-0x1f reserved in IEEE802.15.4-2015 */ + RIT_DATA_REQ = 0x20, + DBS_REQ = 0x21, + DBS_RSP = 0x22, + RIT_DATA_RSP = 0x23, + VENDOR_SPECIFIC = 0x24, + /* 0x25-0xff reserved in IEEE802.15.4-2015 */ +} + +export const enum MACSecurityLevel { + NONE = 0x00, + MIC_32 = 0x01, + MIC_64 = 0x02, + MIC_128 = 0x03, + ENC = 0x04, + ENC_MIC_32 = 0x05, + ENC_MIC_64 = 0x06, + ENC_MIC_128 = 0x07, +} + +export const enum MACSecurityKeyIdMode { + IMPLICIT = 0x00, + INDEX = 0x01, + EXPLICIT_4 = 0x02, + EXPLICIT_8 = 0x03, +} + +/** + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + */ +export type MACFrameControl = { + frameType: MACFrameType; + /** + * - 0 if the frame is not cryptographically protected by the MAC sublayer + * - 1 the frame shall be protected using the keys stored in the MAC PIB for the security relationship indicated by the current frame + */ + securityEnabled: boolean; + /** shall be set to 1 if the device sending the frame has additional data to send to the recipient following the current transfer */ + framePending: boolean; + /** specifies whether an acknowledgment is required from the recipient device on receipt of a data or MAC command frame */ + ackRequest: boolean; + panIdCompression: boolean; + // reserved: number; + seqNumSuppress: boolean; + /** information elements present */ + iePresent: boolean; + destAddrMode: MACFrameAddressMode; + frameVersion: MACFrameVersion; + sourceAddrMode: MACFrameAddressMode; +}; + +export type MACAuxSecHeader = { + securityLevel?: number; + keyIdMode?: number; + frameCounterSuppression: boolean; + asn?: number; + frameCounter?: number; + keySourceAddr32?: number; + keySourceAddr64?: bigint; + keyIndex?: number; +}; + +export type MACSuperframeSpec = { + beaconOrder: number; + superframeOrder: number; + finalCAPSlot: number; + batteryExtension: boolean; + panCoordinator: boolean; + associationPermit: boolean; +}; + +export type MACGtsInfo = { + permit: boolean; + directionByte?: number; + directions?: number[]; + addresses?: number[]; + timeLengths?: number[]; + slots?: number[]; +}; + +export type MACPendAddr = { + addr16List?: number[]; + addr64List?: bigint[]; +}; + +export type MACHeaderIE = { + ies: { + id: number; + length: number; + }[]; + payloadIEPresent: boolean; +}; + +export type MACHeader = { + /** uint16_t */ + frameControl: MACFrameControl; + /** uint8_t */ + sequenceNumber?: number; + /** uint16_t */ + destinationPANId?: number; + /** uint16_t */ + destination16?: number; + /** uint64_t */ + destination64?: bigint; + /** uint16_t */ + sourcePANId?: number; + /** uint16_t */ + source16?: number; + /** uint64_t */ + source64?: bigint; + /** [1-14 bytes] */ + auxSecHeader?: MACAuxSecHeader; + /** uint16_t */ + superframeSpec?: MACSuperframeSpec; + /** [1-.. bytes] */ + gtsInfo?: MACGtsInfo; + /** [1-.. bytes] */ + pendAddr?: MACPendAddr; + /** uint8_t */ + commandId?: number; + /** [0-.. bytes] */ + headerIE?: MACHeaderIE; + /** uint32_t */ + frameCounter?: number; + /** uint8_t */ + keySeqCounter?: number; + /** uint16_t */ + fcs: number; +}; + +/** + * if the security enabled subfield is set to 1 in the frame control field, the frame payload is protected as defined by the security suite selected for that relationship. + */ +export type MACPayload = Buffer; + +/* Compute the MIC length. */ +export function getMICLength(securityLevel: number): number { + return (0x2 << (securityLevel & 0x3)) & ~0x3; +} + +export function decodeMACFrameControl(data: Buffer, offset: number): [MACFrameControl, offset: number] { + const fcf = data.readUInt16LE(offset); + offset += 2; + const frameType = fcf & ZigbeeMACConsts.FCF_TYPE_MASK; + + if (frameType === MACFrameType.MULTIPURPOSE) { + throw new Error(`Unsupported MAC frame type MULTIPURPOSE (${frameType})`); + } + + return [ + { + frameType, + securityEnabled: Boolean((fcf & ZigbeeMACConsts.FCF_SEC_EN) >> 3), + framePending: Boolean((fcf & ZigbeeMACConsts.FCF_FRAME_PND) >> 4), + ackRequest: Boolean((fcf & ZigbeeMACConsts.FCF_ACK_REQ) >> 5), + panIdCompression: Boolean((fcf & ZigbeeMACConsts.FCF_PAN_ID_COMPRESSION) >> 6), + /* bit 7 reserved */ + seqNumSuppress: Boolean((fcf & ZigbeeMACConsts.FCF_SEQNO_SUPPRESSION) >> 8), + iePresent: Boolean((fcf & ZigbeeMACConsts.FCF_IE_PRESENT) >> 9), + destAddrMode: (fcf & ZigbeeMACConsts.FCF_DADDR_MASK) >> 10, + frameVersion: (fcf & ZigbeeMACConsts.FCF_VERSION) >> 12, + sourceAddrMode: (fcf & ZigbeeMACConsts.FCF_SADDR_MASK) >> 14, + }, + offset, + ]; +} + +function encodeMACFrameControl(data: Buffer, offset: number, fcf: MACFrameControl): number { + if (fcf.frameType === MACFrameType.MULTIPURPOSE) { + throw new Error(`Unsupported MAC frame type MULTIPURPOSE (${fcf.frameType})`); + } + + data.writeUInt16LE( + (fcf.frameType & ZigbeeMACConsts.FCF_TYPE_MASK) | + (((fcf.securityEnabled ? 1 : 0) << 3) & ZigbeeMACConsts.FCF_SEC_EN) | + (((fcf.framePending ? 1 : 0) << 4) & ZigbeeMACConsts.FCF_FRAME_PND) | + (((fcf.ackRequest ? 1 : 0) << 5) & ZigbeeMACConsts.FCF_ACK_REQ) | + (((fcf.panIdCompression ? 1 : 0) << 6) & ZigbeeMACConsts.FCF_PAN_ID_COMPRESSION) | + /* bit 7 reserved */ + (((fcf.seqNumSuppress ? 1 : 0) << 8) & ZigbeeMACConsts.FCF_SEQNO_SUPPRESSION) | + (((fcf.iePresent ? 1 : 0) << 9) & ZigbeeMACConsts.FCF_IE_PRESENT) | + ((fcf.destAddrMode << 10) & ZigbeeMACConsts.FCF_DADDR_MASK) | + ((fcf.frameVersion << 12) & ZigbeeMACConsts.FCF_VERSION) | + ((fcf.sourceAddrMode << 14) & ZigbeeMACConsts.FCF_SADDR_MASK), + offset, + ); + offset += 2; + + return offset; +} + +function decodeMACAuxSecHeader(data: Buffer, offset: number, frameControl: MACFrameControl): [MACAuxSecHeader, offset: number] { + let frameCounterSuppression = false; + let asn: number | undefined; + let frameCounter: number | undefined; + let keySourceAddr32: number | undefined; + let keySourceAddr64: bigint | undefined; + let keyIndex: number | undefined; + + const securityControl = data.readUInt8(offset); + offset += 1; + const securityLevel = securityControl & ZigbeeMACConsts.AUX_SEC_LEVEL_MASK; + const keyIdMode = (securityControl & ZigbeeMACConsts.AUX_KEY_ID_MODE_MASK) >> ZigbeeMACConsts.AUX_KEY_ID_MODE_SHIFT; + + if (frameControl.frameVersion === MACFrameVersion.v2015) { + frameCounterSuppression = Boolean(securityControl & ZigbeeMACConsts.AUX_FRAME_COUNTER_SUPPRESSION_MASK); + // TODO: correct?? + asn = (securityControl & ZigbeeMACConsts.AUX_ASN_IN_NONCE_MASK) >> 6; + } + + if (!frameCounterSuppression) { + frameCounter = data.readUInt32LE(offset); + offset += 4; + } + + if (keyIdMode !== MACSecurityKeyIdMode.IMPLICIT) { + if (keyIdMode === MACSecurityKeyIdMode.EXPLICIT_4) { + keySourceAddr32 = data.readUInt32LE(offset); + offset += 4; + } else if (keyIdMode === MACSecurityKeyIdMode.EXPLICIT_8) { + keySourceAddr64 = data.readBigUInt64LE(offset); + offset += 8; + } + + keyIndex = data.readUInt8(offset); + offset += 1; + } + + return [ + { + securityLevel, + keyIdMode, + frameCounterSuppression, + asn, + frameCounter, + keySourceAddr32, + keySourceAddr64, + keyIndex, + }, + offset, + ]; +} + +// function encodeMACAuxSecHeader(data: Buffer, offset: number): number {} + +function decodeMACSuperframeSpec(data: Buffer, offset: number): [MACSuperframeSpec, offset: number] { + const spec = data.readUInt16LE(offset); + offset += 2; + const beaconOrder = spec & ZigbeeMACConsts.SUPERFRAME_BEACON_ORDER_MASK; + const superframeOrder = (spec & ZigbeeMACConsts.SUPERFRAME_ORDER_MASK) >> ZigbeeMACConsts.SUPERFRAME_ORDER_SHIFT; + const finalCAPSlot = (spec & ZigbeeMACConsts.SUPERFRAME_CAP_MASK) >> ZigbeeMACConsts.SUPERFRAME_CAP_SHIFT; + const batteryExtension = Boolean((spec & ZigbeeMACConsts.SUPERFRAME_BATT_EXTENSION_MASK) >> ZigbeeMACConsts.SUPERFRAME_BATT_EXTENSION_SHIFT); + const panCoordinator = Boolean((spec & ZigbeeMACConsts.SUPERFRAME_COORD_MASK) >> ZigbeeMACConsts.SUPERFRAME_COORD_SHIFT); + const associationPermit = Boolean((spec & ZigbeeMACConsts.SUPERFRAME_ASSOC_PERMIT_MASK) >> ZigbeeMACConsts.SUPERFRAME_ASSOC_PERMIT_SHIFT); + + return [ + { + beaconOrder, + superframeOrder, + finalCAPSlot, + batteryExtension, + panCoordinator, + associationPermit, + }, + offset, + ]; +} + +function encodeMACSuperframeSpec(data: Buffer, offset: number, header: MACHeader): number { + const spec = header.superframeSpec!; + data.writeUInt16LE( + (spec.beaconOrder & ZigbeeMACConsts.SUPERFRAME_BEACON_ORDER_MASK) | + ((spec.superframeOrder << ZigbeeMACConsts.SUPERFRAME_ORDER_SHIFT) & ZigbeeMACConsts.SUPERFRAME_ORDER_MASK) | + ((spec.finalCAPSlot << ZigbeeMACConsts.SUPERFRAME_CAP_SHIFT) & ZigbeeMACConsts.SUPERFRAME_CAP_MASK) | + (((spec.batteryExtension ? 1 : 0) << ZigbeeMACConsts.SUPERFRAME_BATT_EXTENSION_SHIFT) & ZigbeeMACConsts.SUPERFRAME_BATT_EXTENSION_MASK) | + (((spec.panCoordinator ? 1 : 0) << ZigbeeMACConsts.SUPERFRAME_COORD_SHIFT) & ZigbeeMACConsts.SUPERFRAME_COORD_MASK) | + (((spec.associationPermit ? 1 : 0) << ZigbeeMACConsts.SUPERFRAME_ASSOC_PERMIT_SHIFT) & ZigbeeMACConsts.SUPERFRAME_ASSOC_PERMIT_MASK), + offset, + ); + offset += 2; + + return offset; +} + +function decodeMACGtsInfo(data: Buffer, offset: number): [MACGtsInfo, offset: number] { + let directionByte: number | undefined; + let directions: number[] | undefined; + let addresses: number[] | undefined; + let timeLengths: number[] | undefined; + let slots: number[] | undefined; + + const spec = data.readUInt8(offset); + offset += 1; + const count = spec & ZigbeeMACConsts.GTS_COUNT_MASK; + const permit = Boolean(spec & ZigbeeMACConsts.GTS_PERMIT_MASK); + + if (count > 0) { + directionByte = data.readUInt8(offset); + offset += 1; + directions = []; + addresses = []; + timeLengths = []; + slots = []; + + for (let i = 0; i < count; i++) { + directions.push(directionByte & (0x01 << i)); + const addr = data.readUInt16LE(offset); + offset += 2; + const slotByte = data.readUInt8(offset); + offset += 1; + const timeLength = (slotByte & ZigbeeMACConsts.GTS_LENGTH_MASK) >> ZigbeeMACConsts.GTS_LENGTH_SHIFT; + const slot = slotByte & ZigbeeMACConsts.GTS_SLOT_MASK; + + addresses.push(addr); + timeLengths.push(timeLength); + slots.push(slot); + } + } + + return [ + { + permit, + directionByte, + directions, + addresses, + timeLengths, + slots, + }, + offset, + ]; +} + +function encodeMACGtsInfo(data: Buffer, offset: number, header: MACHeader): number { + const info = header.gtsInfo!; + const count = info.directions ? info.directions.length : 0; + data.writeUInt8((count & ZigbeeMACConsts.GTS_COUNT_MASK) | ((info.permit ? 1 : 0) & ZigbeeMACConsts.GTS_PERMIT_MASK), offset); + offset += 1; + + if (count > 0) { + // assert(info.directionByte !== undefined); + data.writeUInt8(info.directionByte!, offset); + offset += 1; + + for (let i = 0; i < count; i++) { + data.writeUInt16LE(info.addresses![i], offset); + offset += 2; + const timeLength = info.timeLengths![i]; + const slot = info.slots![i]; + data.writeUInt8( + ((timeLength << ZigbeeMACConsts.GTS_LENGTH_SHIFT) & ZigbeeMACConsts.GTS_LENGTH_MASK) | (slot & ZigbeeMACConsts.GTS_SLOT_MASK), + offset, + ); + offset += 1; + } + } + + return offset; +} + +function decodeMACPendAddr(data: Buffer, offset: number): [MACPendAddr, offset: number] { + const spec = data.readUInt8(offset); + offset += 1; + const num16 = spec & ZigbeeMACConsts.PENDADDR_SHORT_MASK; + const num64 = (spec & ZigbeeMACConsts.PENDADDR_LONG_MASK) >> ZigbeeMACConsts.PENDADDR_LONG_SHIFT; + let addr16List: number[] | undefined; + let addr64List: bigint[] | undefined; + + if (num16 > 0) { + addr16List = []; + + for (let i = 0; i < num16; i++) { + addr16List.push(data.readUInt16LE(offset)); + + offset += 2; + } + } + + if (num64 > 0) { + addr64List = []; + + for (let i = 0; i < num64; i++) { + addr64List.push(data.readBigUInt64LE(offset)); + + offset += 8; + } + } + + return [ + { + addr16List, + addr64List, + }, + offset, + ]; +} + +function encodeMACPendAddr(data: Buffer, offset: number, header: MACHeader): number { + const pendAddr = header.pendAddr!; + const num16 = pendAddr.addr16List ? pendAddr.addr16List.length : 0; + const num64 = pendAddr.addr64List ? pendAddr.addr64List.length : 0; + data.writeUInt8( + (num16 & ZigbeeMACConsts.PENDADDR_SHORT_MASK) | ((num64 << ZigbeeMACConsts.PENDADDR_LONG_SHIFT) & ZigbeeMACConsts.PENDADDR_LONG_MASK), + offset, + ); + offset += 1; + + for (let i = 0; i < num16; i++) { + data.writeUInt16LE(pendAddr.addr16List![i], offset); + + offset += 2; + } + + for (let i = 0; i < num64; i++) { + data.writeBigUInt64LE(pendAddr.addr64List![i], offset); + + offset += 8; + } + + return offset; +} + +export const enum MacZigbeePayloadIESubId { + REJOIN = 0x00, + TX_POWER = 0x01, + EB_PAYLOAD = 0x02, + // 0x003-0x3ff Reserved +} + +function decodeMACHeaderIEs(data: Buffer, offset: number, auxSecHeader: MACAuxSecHeader | undefined): [MACHeaderIE, offset: number] { + let remaining = data.byteLength - offset - getMICLength(auxSecHeader?.securityLevel ?? 0); + let payloadIEPresent = false; + const ies: MACHeaderIE["ies"] = []; + + do { + const header = data.readUInt16LE(offset); + offset += 2; + const id = (header & ZigbeeMACConsts.HEADER_IE_ID_MASK) >> 7; + const length = header & ZigbeeMACConsts.HEADER_IE_LENGTH_MASK; + + ies.push({ id, length }); + + offset += 2 + length; + remaining -= 2 + length; + + if (id === ZigbeeMACConsts.HEADER_IE_HT1 || id === ZigbeeMACConsts.HEADER_IE_HT2) { + payloadIEPresent = id === ZigbeeMACConsts.HEADER_IE_HT1; + + break; + } + } while (remaining > 0); + + return [ + { + ies, + payloadIEPresent, + }, + offset, + ]; +} + +// function encodeMACHeaderIEs(data: Buffer, offset: number): number {} + +// export type MACHeaderPayloadIE = { + +// } + +// /** +// * TODO: proper support for all IE stuff +// * +// * The Zigbee Payload IE is a Vendor Specific Payload IE (Group ID = 0x2) using the Zigbee OUI value of 0x4A191B. +// * - Bits: 0-5 6-15 Octets: Variable +// * - Length Sub-ID Content +// * +// * REJOIN: +// * - Octets: 8 2 +// * - Network Extended PAN ID Sender Short Address +// * +// * TX_POWER: +// * - Octets: 1 +// * - TX Power (in dBm - used to send the frame) +// * +// * EB_PAYLOAD: +// * - Octets: 15 2 2 +// * - Beacon Payload Superframe Specification Sender Short Address +// */ +// function decodeMACHeaderPayloadIEs(data: Buffer, offset: number, headerIE: MACHeaderIE): [MACHeaderPayloadIE[], offset: number] { +// return [[], offset]; +// } + +export function decodeMACHeader(data: Buffer, offset: number, frameControl: MACFrameControl): [MACHeader, offset: number] { + let sequenceNumber: number | undefined; + let destinationPANId: number | undefined; + let sourcePANId: number | undefined; + + if (!frameControl.seqNumSuppress) { + sequenceNumber = data.readUInt8(offset); + offset += 1; + } + + if (frameControl.destAddrMode === MACFrameAddressMode.RESERVED) { + throw new Error(`Invalid MAC frame: destination address mode ${frameControl.destAddrMode}`); + } + + if (frameControl.sourceAddrMode === MACFrameAddressMode.RESERVED) { + throw new Error(`Invalid MAC frame: source address mode ${frameControl.sourceAddrMode}`); + } + + let destPANPresent = false; + let sourcePANPresent = false; + + if (frameControl.frameType === MACFrameType.MULTIPURPOSE) { + throw new Error("Unsupported MAC frame: MULTIPURPOSE"); + } + + if (frameControl.frameVersion === MACFrameVersion.v2003 || frameControl.frameVersion === MACFrameVersion.v2006) { + if (frameControl.destAddrMode !== MACFrameAddressMode.NONE && frameControl.sourceAddrMode !== MACFrameAddressMode.NONE) { + // addressing information is present + if (frameControl.panIdCompression) { + // PAN IDs are identical + destPANPresent = true; + sourcePANPresent = false; + } else { + // PAN IDs are different, both shall be included in the frame + destPANPresent = true; + sourcePANPresent = true; + } + } else { + if (frameControl.panIdCompression) { + throw new Error("Invalid MAC frame: unexpected PAN ID compression"); + } + + // only either the destination or the source addressing information is present + if (frameControl.destAddrMode !== MACFrameAddressMode.NONE && frameControl.sourceAddrMode === MACFrameAddressMode.NONE) { + destPANPresent = true; + sourcePANPresent = false; + } else if (frameControl.destAddrMode === MACFrameAddressMode.NONE && frameControl.sourceAddrMode !== MACFrameAddressMode.NONE) { + destPANPresent = false; + sourcePANPresent = true; + } else if (frameControl.destAddrMode === MACFrameAddressMode.NONE && frameControl.sourceAddrMode === MACFrameAddressMode.NONE) { + destPANPresent = false; + sourcePANPresent = false; + } else { + throw new Error("Invalid MAC frame: invalid addressing"); + } + } + } else if (frameControl.frameVersion === MACFrameVersion.v2015) { + if ( + frameControl.frameType === MACFrameType.BEACON || + frameControl.frameType === MACFrameType.DATA || + frameControl.frameType === MACFrameType.ACK || + frameControl.frameType === MACFrameType.CMD + ) { + if ( + frameControl.destAddrMode === MACFrameAddressMode.NONE && + frameControl.sourceAddrMode === MACFrameAddressMode.NONE && + !frameControl.panIdCompression + ) { + destPANPresent = false; + sourcePANPresent = false; + } else if ( + frameControl.destAddrMode === MACFrameAddressMode.NONE && + frameControl.sourceAddrMode === MACFrameAddressMode.NONE && + frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else if ( + frameControl.destAddrMode !== MACFrameAddressMode.NONE && + frameControl.sourceAddrMode === MACFrameAddressMode.NONE && + !frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else if ( + frameControl.destAddrMode !== MACFrameAddressMode.NONE && + frameControl.sourceAddrMode === MACFrameAddressMode.NONE && + frameControl.panIdCompression + ) { + destPANPresent = false; + sourcePANPresent = false; + } else if ( + frameControl.destAddrMode === MACFrameAddressMode.NONE && + frameControl.sourceAddrMode !== MACFrameAddressMode.NONE && + !frameControl.panIdCompression + ) { + destPANPresent = false; + sourcePANPresent = true; + } else if ( + frameControl.destAddrMode === MACFrameAddressMode.NONE && + frameControl.sourceAddrMode !== MACFrameAddressMode.NONE && + frameControl.panIdCompression + ) { + destPANPresent = false; + sourcePANPresent = false; + } else if ( + frameControl.destAddrMode === MACFrameAddressMode.EXT && + frameControl.sourceAddrMode === MACFrameAddressMode.EXT && + !frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else if ( + frameControl.destAddrMode === MACFrameAddressMode.EXT && + frameControl.sourceAddrMode === MACFrameAddressMode.EXT && + frameControl.panIdCompression + ) { + destPANPresent = false; + sourcePANPresent = false; + } else if ( + frameControl.destAddrMode === MACFrameAddressMode.SHORT && + frameControl.sourceAddrMode === MACFrameAddressMode.SHORT && + !frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = true; + } else if ( + frameControl.destAddrMode === MACFrameAddressMode.SHORT && + frameControl.sourceAddrMode === MACFrameAddressMode.EXT && + !frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = true; + } else if ( + frameControl.destAddrMode === MACFrameAddressMode.EXT && + frameControl.sourceAddrMode === MACFrameAddressMode.SHORT && + !frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = true; + } else if ( + frameControl.destAddrMode === MACFrameAddressMode.SHORT && + frameControl.sourceAddrMode === MACFrameAddressMode.EXT && + frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else if ( + frameControl.destAddrMode === MACFrameAddressMode.EXT && + frameControl.sourceAddrMode === MACFrameAddressMode.SHORT && + frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else if ( + frameControl.destAddrMode === MACFrameAddressMode.SHORT && + frameControl.sourceAddrMode === MACFrameAddressMode.SHORT && + frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else { + throw new Error("Invalid MAC frame: unexpected PAN ID compression"); + } + } else { + // PAN ID Compression is not used + destPANPresent = false; + sourcePANPresent = false; + } + } else { + throw new Error("Invalid MAC frame: invalid version"); + } + + let destination16: number | undefined; + let destination64: bigint | undefined; + let source16: number | undefined; + let source64: bigint | undefined; + + if (destPANPresent) { + destinationPANId = data.readUInt16LE(offset); + offset += 2; + } + + if (frameControl.destAddrMode === MACFrameAddressMode.SHORT) { + destination16 = data.readUInt16LE(offset); + offset += 2; + } else if (frameControl.destAddrMode === MACFrameAddressMode.EXT) { + destination64 = data.readBigUInt64LE(offset); + offset += 8; + } + + if (sourcePANPresent) { + sourcePANId = data.readUInt16LE(offset); + offset += 2; + } else { + sourcePANId = destPANPresent ? destinationPANId : ZigbeeMACConsts.BCAST_PAN; + } + + if (frameControl.sourceAddrMode === MACFrameAddressMode.SHORT) { + source16 = data.readUInt16LE(offset); + offset += 2; + } else if (frameControl.sourceAddrMode === MACFrameAddressMode.EXT) { + source64 = data.readBigUInt64LE(offset); + offset += 8; + } + + let auxSecHeader: MACAuxSecHeader | undefined; + + if ( + frameControl.securityEnabled && + /*(frameControl.frameType === MACFrameType.MULTIPURPOSE || */ frameControl.frameVersion === MACFrameVersion.v2003 + ) { + [auxSecHeader, offset] = decodeMACAuxSecHeader(data, offset, frameControl); + } + + let superframeSpec: MACSuperframeSpec | undefined; + let gtsInfo: MACGtsInfo | undefined; + let pendAddr: MACPendAddr | undefined; + let commandId: number | undefined; + let headerIE: MACHeaderIE | undefined; + + if ( + /*frameControl.frameType !== MACFrameType.MULTIPURPOSE && */ + frameControl.frameVersion === MACFrameVersion.v2003 || + frameControl.frameVersion === MACFrameVersion.v2006 + ) { + if (frameControl.frameType === MACFrameType.BEACON) { + [superframeSpec, offset] = decodeMACSuperframeSpec(data, offset); + [gtsInfo, offset] = decodeMACGtsInfo(data, offset); + [pendAddr, offset] = decodeMACPendAddr(data, offset); + } else if (frameControl.frameType === MACFrameType.CMD) { + commandId = data.readUInt8(offset); + offset += 1; + } + } else { + if (frameControl.iePresent) { + [headerIE, offset] = decodeMACHeaderIEs(data, offset, auxSecHeader); + // TODO: headerIE.payloadIEPresent === true, Zigbee OUI? + } + } + + let frameCounter: number | undefined; + let keySeqCounter: number | undefined; + + if ( + frameControl.securityEnabled && + /*frameControl.frameType !== MACFrameType.MULTIPURPOSE && */ + frameControl.frameVersion === MACFrameVersion.v2003 + ) { + // auxSecHeader?.securityLevel = ???; + const isEncrypted = auxSecHeader!.securityLevel! & 0x04; + + if (isEncrypted) { + frameCounter = data.readUInt32LE(offset); + offset += 4; + keySeqCounter = data.readUInt8(offset); + offset += 1; + } + } + + if (offset >= data.byteLength) { + throw new Error("Invalid MAC frame: no payload"); + } + + return [ + { + frameControl, + sequenceNumber, + destinationPANId, + destination16, + destination64, + sourcePANId, + source16, + source64, + auxSecHeader, + superframeSpec, + gtsInfo, + pendAddr, + commandId, + headerIE, + frameCounter, + keySeqCounter, + fcs: 0, // set after decoded payload + }, + offset, + ]; +} + +function encodeMACHeader(data: Buffer, offset: number, header: MACHeader, zigbee: boolean): number { + offset = encodeMACFrameControl(data, offset, header.frameControl); + + if (zigbee) { + data.writeUInt8(header.sequenceNumber!, offset); + offset += 1; + + data.writeUInt16LE(header.destinationPANId!, offset); + offset += 2; + + data.writeUInt16LE(header.destination16!, offset); + offset += 2; + + if (header.sourcePANId !== undefined) { + data.writeUInt16LE(header.sourcePANId, offset); + offset += 2; + } + + // NWK GP can be NONE + if (header.frameControl.sourceAddrMode === MACFrameAddressMode.SHORT) { + data.writeUInt16LE(header.source16!, offset); + offset += 2; + } + } else { + if (!header.frameControl.seqNumSuppress) { + data.writeUInt8(header.sequenceNumber!, offset); + offset += 1; + } + + if (header.frameControl.destAddrMode === MACFrameAddressMode.RESERVED) { + throw new Error(`Invalid MAC frame: destination address mode ${header.frameControl.destAddrMode}`); + } + + if (header.frameControl.sourceAddrMode === MACFrameAddressMode.RESERVED) { + throw new Error(`Invalid MAC frame: source address mode ${header.frameControl.sourceAddrMode}`); + } + + let destPANPresent = false; + let sourcePANPresent = false; + + if (header.frameControl.frameType === MACFrameType.MULTIPURPOSE) { + throw new Error("Unsupported MAC frame: MULTIPURPOSE"); + } + + if (header.frameControl.frameVersion === MACFrameVersion.v2003 || header.frameControl.frameVersion === MACFrameVersion.v2006) { + if (header.frameControl.destAddrMode !== MACFrameAddressMode.NONE && header.frameControl.sourceAddrMode !== MACFrameAddressMode.NONE) { + // addressing information is present + if (header.frameControl.panIdCompression) { + // PAN IDs are identical + destPANPresent = true; + sourcePANPresent = false; + } else { + // PAN IDs are different, both shall be included in the frame + destPANPresent = true; + sourcePANPresent = true; + } + } else { + if (header.frameControl.panIdCompression) { + throw new Error("Invalid MAC frame: unexpected PAN ID compression"); + } + + // only either the destination or the source addressing information is present + if ( + header.frameControl.destAddrMode !== MACFrameAddressMode.NONE && + header.frameControl.sourceAddrMode === MACFrameAddressMode.NONE + ) { + destPANPresent = true; + sourcePANPresent = false; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.NONE && + header.frameControl.sourceAddrMode !== MACFrameAddressMode.NONE + ) { + destPANPresent = false; + sourcePANPresent = true; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.NONE && + header.frameControl.sourceAddrMode === MACFrameAddressMode.NONE + ) { + destPANPresent = false; + sourcePANPresent = false; + } else { + throw new Error("Invalid MAC frame: invalid addressing"); + } + } + } else if (header.frameControl.frameVersion === MACFrameVersion.v2015) { + if ( + header.frameControl.frameType === MACFrameType.BEACON || + header.frameControl.frameType === MACFrameType.DATA || + header.frameControl.frameType === MACFrameType.ACK || + header.frameControl.frameType === MACFrameType.CMD + ) { + if ( + header.frameControl.destAddrMode === MACFrameAddressMode.NONE && + header.frameControl.sourceAddrMode === MACFrameAddressMode.NONE && + !header.frameControl.panIdCompression + ) { + destPANPresent = false; + sourcePANPresent = false; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.NONE && + header.frameControl.sourceAddrMode === MACFrameAddressMode.NONE && + header.frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else if ( + header.frameControl.destAddrMode !== MACFrameAddressMode.NONE && + header.frameControl.sourceAddrMode === MACFrameAddressMode.NONE && + !header.frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else if ( + header.frameControl.destAddrMode !== MACFrameAddressMode.NONE && + header.frameControl.sourceAddrMode === MACFrameAddressMode.NONE && + header.frameControl.panIdCompression + ) { + destPANPresent = false; + sourcePANPresent = false; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.NONE && + header.frameControl.sourceAddrMode !== MACFrameAddressMode.NONE && + !header.frameControl.panIdCompression + ) { + destPANPresent = false; + sourcePANPresent = true; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.NONE && + header.frameControl.sourceAddrMode !== MACFrameAddressMode.NONE && + header.frameControl.panIdCompression + ) { + destPANPresent = false; + sourcePANPresent = false; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.EXT && + header.frameControl.sourceAddrMode === MACFrameAddressMode.EXT && + !header.frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.EXT && + header.frameControl.sourceAddrMode === MACFrameAddressMode.EXT && + header.frameControl.panIdCompression + ) { + destPANPresent = false; + sourcePANPresent = false; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.SHORT && + header.frameControl.sourceAddrMode === MACFrameAddressMode.SHORT && + !header.frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = true; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.SHORT && + header.frameControl.sourceAddrMode === MACFrameAddressMode.EXT && + !header.frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = true; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.EXT && + header.frameControl.sourceAddrMode === MACFrameAddressMode.SHORT && + !header.frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = true; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.SHORT && + header.frameControl.sourceAddrMode === MACFrameAddressMode.EXT && + header.frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.EXT && + header.frameControl.sourceAddrMode === MACFrameAddressMode.SHORT && + header.frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else if ( + header.frameControl.destAddrMode === MACFrameAddressMode.SHORT && + header.frameControl.sourceAddrMode === MACFrameAddressMode.SHORT && + header.frameControl.panIdCompression + ) { + destPANPresent = true; + sourcePANPresent = false; + } else { + throw new Error("Invalid MAC frame: unexpected PAN ID compression"); + } + } else { + // PAN ID Compression is not used + destPANPresent = false; + sourcePANPresent = false; + } + } else { + throw new Error("Invalid MAC frame: invalid version"); + } + + if (destPANPresent) { + data.writeUInt16LE(header.destinationPANId!, offset); + offset += 2; + } + + if (header.frameControl.destAddrMode === MACFrameAddressMode.SHORT) { + data.writeUInt16LE(header.destination16!, offset); + offset += 2; + } else if (header.frameControl.destAddrMode === MACFrameAddressMode.EXT) { + data.writeBigUInt64LE(header.destination64!, offset); + offset += 8; + } + + if (sourcePANPresent) { + data.writeUInt16LE(header.sourcePANId!, offset); + offset += 2; + } + + if (header.frameControl.sourceAddrMode === MACFrameAddressMode.SHORT) { + data.writeUInt16LE(header.source16!, offset); + offset += 2; + } else if (header.frameControl.sourceAddrMode === MACFrameAddressMode.EXT) { + data.writeBigUInt64LE(header.source64!, offset); + offset += 8; + } + + let auxSecHeader: MACAuxSecHeader | undefined; + + if ( + header.frameControl.securityEnabled && + /*(header.frameControl.frameType === MACFrameType.MULTIPURPOSE || */ header.frameControl.frameVersion === MACFrameVersion.v2003 + ) { + throw new Error("Unsupported: securityEnabled"); + // [auxSecHeader, offset] = encodeMACAuxSecHeader(data, offset, header.frameControl); + } + + if ( + /*header.frameControl.frameType !== MACFrameType.MULTIPURPOSE && */ + header.frameControl.frameVersion === MACFrameVersion.v2003 || + header.frameControl.frameVersion === MACFrameVersion.v2006 + ) { + if (header.frameControl.frameType === MACFrameType.BEACON) { + offset = encodeMACSuperframeSpec(data, offset, header); + offset = encodeMACGtsInfo(data, offset, header); + offset = encodeMACPendAddr(data, offset, header); + } else if (header.frameControl.frameType === MACFrameType.CMD) { + data.writeUInt8(header.commandId!, offset); + offset += 1; + } + } else { + if (header.frameControl.iePresent) { + throw new Error("Unsupported iePresent"); + // offset = encodeMACHeaderIEs(data, offset, auxSecHeader); + } + } + + if ( + header.frameControl.securityEnabled && + /*header.frameControl.frameType !== MACFrameType.MULTIPURPOSE && */ + header.frameControl.frameVersion === MACFrameVersion.v2003 + ) { + // auxSecHeader?.securityLevel = ???; + const isEncrypted = auxSecHeader!.securityLevel! & 0x04; + + if (isEncrypted) { + data.writeUInt32LE(header.frameCounter!, offset); + offset += 4; + data.writeUInt8(header.keySeqCounter!, offset); + offset += 1; + } + } + } + + return offset; +} + +function crc16CCITT(data: Buffer): number { + let fcs = 0x0000; + + for (const aByte of data) { + let q = (fcs ^ aByte) & 0x0f; + fcs = (fcs >> 4) ^ (q * 0x1081); + q = (fcs ^ (aByte >> 4)) & 0x0f; + fcs = (fcs >> 4) ^ (q * 0x1081); + } + + return fcs; +} + +function decryptPayload(data: Buffer, offset: number, frameControl: MACFrameControl): [Buffer, offset: number] { + if (frameControl.securityEnabled) { + // XXX: not needed for ZigBee + throw new Error("Unsupported MAC: security enabled"); + } + + const endOffset = data.byteLength - ZigbeeMACConsts.FCS_LEN; + + return [data.subarray(offset, endOffset), endOffset]; +} + +// function encryptPayload(data: Buffer, offset: number): number {} + +export function decodeMACPayload(data: Buffer, offset: number, frameControl: MACFrameControl, header: MACHeader): MACPayload { + const [payload, pOutOffset] = decryptPayload(data, offset, frameControl); + + if (pOutOffset >= data.byteLength) { + throw new Error("Invalid MAC frame: no FCS"); + } + + header.fcs = data.readUInt16LE(pOutOffset); + + return payload; +} + +export function encodeMACFrame(header: MACHeader, payload: Buffer): Buffer { + let offset = 0; + const data = Buffer.alloc(ZigbeeMACConsts.PAYLOAD_MAX_SAFE_SIZE); + + offset = encodeMACHeader(data, offset, header, false); + + data.set(payload, offset); + offset += payload.byteLength; + + data.writeUInt16LE(crc16CCITT(data.subarray(0, offset)), offset); + offset += 2; + + return data.subarray(0, offset); +} + +// #region ZigBee-specific + +/** Subset of @see MACHeader */ +export type MACHeaderZigbee = { + /** uint16_t */ + frameControl: MACFrameControl; + /** uint8_t */ + sequenceNumber?: number; + /** uint16_t */ + destinationPANId?: number; + /** uint16_t */ + destination16?: number; + /** uint16_t */ + sourcePANId?: number; + /** uint16_t */ + source16?: number; + /** uint16_t */ + fcs: number; +}; + +/** Encode MAC frame with hotpath for Zigbee NWK/APS payload */ +export function encodeMACFrameZigbee(header: MACHeaderZigbee, payload: Buffer): Buffer { + let offset = 0; + const data = Buffer.alloc(ZigbeeMACConsts.PAYLOAD_MAX_SAFE_SIZE); // TODO: optimize with max ZigBee header length + + // always transmit with v2003 (0) frame version @see D.6 Frame Version Value of 05-3474-23 + header.frameControl.frameVersion = MACFrameVersion.v2003; + + offset = encodeMACHeader(data, offset, header, true); // zigbee hotpath + + data.set(payload, offset); + offset += payload.byteLength; + + data.writeUInt16LE(crc16CCITT(data.subarray(0, offset)), offset); + offset += 2; + + return data.subarray(0, offset); +} + +export type MACZigbeeBeacon = { + protocolId: number; + profile: number; + version: number; + /** Whether the device can accept join requests from routing capable devices */ + routerCapacity: boolean; + /** The tree depth of the device, 0 indicates the network coordinator */ + deviceDepth: number; + /** Whether the device can accept join requests from ZigBee end devices */ + endDeviceCapacity: boolean; + extendedPANId: bigint; + /** The time difference between a device and its parent's beacon. */ + txOffset: number; + updateId: number; +}; + +export function decodeMACZigbeeBeacon(data: Buffer, offset: number): MACZigbeeBeacon { + const protocolId = data.readUInt8(offset); + offset += 1; + const beacon = data.readUInt16LE(offset); + offset += 2; + const profile = beacon & ZigbeeMACConsts.ZIGBEE_BEACON_STACK_PROFILE_MASK; + const version = (beacon & ZigbeeMACConsts.ZIGBEE_BEACON_PROTOCOL_VERSION_MASK) >> ZigbeeMACConsts.ZIGBEE_BEACON_PROTOCOL_VERSION_SHIFT; + const routerCapacity = Boolean( + (beacon & ZigbeeMACConsts.ZIGBEE_BEACON_ROUTER_CAPACITY_MASK) >> ZigbeeMACConsts.ZIGBEE_BEACON_ROUTER_CAPACITY_SHIFT, + ); + const deviceDepth = (beacon & ZigbeeMACConsts.ZIGBEE_BEACON_NETWORK_DEPTH_MASK) >> ZigbeeMACConsts.ZIGBEE_BEACON_NETWORK_DEPTH_SHIFT; + const endDeviceCapacity = Boolean( + (beacon & ZigbeeMACConsts.ZIGBEE_BEACON_END_DEVICE_CAPACITY_MASK) >> ZigbeeMACConsts.ZIGBEE_BEACON_END_DEVICE_CAPACITY_SHIFT, + ); + const extendedPANId = data.readBigUInt64LE(offset); + offset += 8; + const endBytes = data.readUInt32LE(offset); + const txOffset = endBytes & ZigbeeMACConsts.ZIGBEE_BEACON_TX_OFFSET_MASK; + const updateId = (endBytes & ZigbeeMACConsts.ZIGBEE_BEACON_UPDATE_ID_MASK) >> ZigbeeMACConsts.ZIGBEE_BEACON_UPDATE_ID_SHIFT; + + return { + protocolId, + profile, + version, + routerCapacity, + deviceDepth, + endDeviceCapacity, + extendedPANId, + txOffset, + updateId, + }; +} + +export function encodeMACZigbeeBeacon(beacon: MACZigbeeBeacon): Buffer { + const payload = Buffer.alloc(ZigbeeMACConsts.ZIGBEE_BEACON_LENGTH); + let offset = 0; + payload.writeUInt8(0, offset); // protocol ID always 0 on Zigbee beacons + offset += 1; + payload.writeUInt16LE( + (beacon.profile & ZigbeeMACConsts.ZIGBEE_BEACON_STACK_PROFILE_MASK) | + ((beacon.version << ZigbeeMACConsts.ZIGBEE_BEACON_PROTOCOL_VERSION_SHIFT) & ZigbeeMACConsts.ZIGBEE_BEACON_PROTOCOL_VERSION_MASK) | + (((beacon.routerCapacity ? 1 : 0) << ZigbeeMACConsts.ZIGBEE_BEACON_ROUTER_CAPACITY_SHIFT) & + ZigbeeMACConsts.ZIGBEE_BEACON_ROUTER_CAPACITY_MASK) | + ((beacon.deviceDepth << ZigbeeMACConsts.ZIGBEE_BEACON_NETWORK_DEPTH_SHIFT) & ZigbeeMACConsts.ZIGBEE_BEACON_NETWORK_DEPTH_MASK) | + (((beacon.endDeviceCapacity ? 1 : 0) << ZigbeeMACConsts.ZIGBEE_BEACON_END_DEVICE_CAPACITY_SHIFT) & + ZigbeeMACConsts.ZIGBEE_BEACON_END_DEVICE_CAPACITY_MASK), + offset, + ); + offset += 2; + payload.writeBigUInt64LE(beacon.extendedPANId, offset); + offset += 8; + payload.writeUInt32LE( + (beacon.txOffset & ZigbeeMACConsts.ZIGBEE_BEACON_TX_OFFSET_MASK) | + ((beacon.updateId << ZigbeeMACConsts.ZIGBEE_BEACON_UPDATE_ID_SHIFT) & ZigbeeMACConsts.ZIGBEE_BEACON_UPDATE_ID_MASK), + offset, + ); + + return payload; +} + +// #endregion diff --git a/src/zigbee/zigbee-aps.ts b/src/zigbee/zigbee-aps.ts new file mode 100644 index 0000000..746be8f --- /dev/null +++ b/src/zigbee/zigbee-aps.ts @@ -0,0 +1,431 @@ +import { type ZigbeeSecurityHeader, decryptZigbeePayload, encryptZigbeePayload } from "./zigbee.js"; + +/** + * const enum with sole purpose of avoiding "magic numbers" in code for well-known values + */ +export const enum ZigbeeAPSConsts { + HEADER_MIN_SIZE = 8, + HEADER_MAX_SIZE = 21, + FRAME_MAX_SIZE = 108, + PAYLOAD_MIN_SIZE = 65, + /** no NWK security */ + PAYLOAD_MAX_SIZE = 100, + + CMD_KEY_TC_MASTER = 0x00, + CMD_KEY_STANDARD_NWK = 0x01, + CMD_KEY_APP_MASTER = 0x02, + CMD_KEY_APP_LINK = 0x03, + CMD_KEY_TC_LINK = 0x04, + CMD_KEY_HIGH_SEC_NWK = 0x05, + CMD_KEY_LENGTH = 16, + + CMD_REQ_NWK_KEY = 0x01, + CMD_REQ_APP_KEY = 0x02, + + CMD_UPDATE_STANDARD_SEC_REJOIN = 0x00, + CMD_UPDATE_STANDARD_UNSEC_JOIN = 0x01, + CMD_UPDATE_LEAVE = 0x02, + CMD_UPDATE_STANDARD_UNSEC_REJOIN = 0x03, + CMD_UPDATE_HIGH_SEC_REJOIN = 0x04, + CMD_UPDATE_HIGH_UNSEC_JOIN = 0x05, + CMD_UPDATE_HIGH_UNSEC_REJOIN = 0x07, + + FCF_FRAME_TYPE = 0x03, + FCF_DELIVERY_MODE = 0x0c, + /** ZigBee 2004 and earlier. */ + // FCF_INDIRECT_MODE = 0x10, + /** ZigBee 2007 and later. */ + FCF_ACK_FORMAT = 0x10, + FCF_SECURITY = 0x20, + FCF_ACK_REQ = 0x40, + FCF_EXT_HEADER = 0x80, + + EXT_FCF_FRAGMENT = 0x03, +} + +export const enum ZigbeeAPSFrameType { + DATA = 0x00, + CMD = 0x01, + ACK = 0x02, + INTERPAN = 0x03, +} + +export const enum ZigbeeAPSDeliveryMode { + UNICAST = 0x00, + /** removed in Zigbee 2006 and later */ + // INDIRECT = 0x01, + BCAST = 0x02, + /** ZigBee 2006 and later */ + GROUP = 0x03, +} + +export const enum ZigbeeAPSFragmentation { + NONE = 0x00, + FIRST = 0x01, + MIDDLE = 0x02, +} + +export const enum ZigbeeAPSCommandId { + TRANSPORT_KEY = 0x05, + UPDATE_DEVICE = 0x06, + REMOVE_DEVICE = 0x07, + REQUEST_KEY = 0x08, + SWITCH_KEY = 0x09, + TUNNEL = 0x0e, + VERIFY_KEY = 0x0f, + CONFIRM_KEY = 0x10, + RELAY_MESSAGE_DOWNSTREAM = 0x11, + RELAY_MESSAGE_UPSTREAM = 0x12, +} + +/** + * Frame Control Field: Ack (0x02) + * .... ..10 = Frame Type: Ack (0x2) + * .... 00.. = Delivery Mode: Unicast (0x0) + * ...0 .... = Acknowledgement Format: False + * ..0. .... = Security: False + * .0.. .... = Acknowledgement Request: False + * 0... .... = Extended Header: False + */ +export type ZigbeeAPSFrameControl = { + frameType: ZigbeeAPSFrameType; + deliveryMode: ZigbeeAPSDeliveryMode; + // indirectMode: ZigbeeAPSIndirectMode; + ackFormat: boolean; + security: boolean; + ackRequest: boolean; + extendedHeader: boolean; +}; + +export type ZigbeeAPSHeader = { + /** uint8_t */ + frameControl: ZigbeeAPSFrameControl; + /** uint8_t */ + destEndpoint?: number; + /** uint16_t */ + group?: number; + /** uint16_t */ + clusterId?: number; + /** uint16_t */ + profileId?: number; + /** uint8_t */ + sourceEndpoint?: number; + /** uint8_t */ + counter?: number; + /** uint8_t */ + fragmentation?: ZigbeeAPSFragmentation; + /** uint8_t */ + fragBlockNumber?: number; + /** uint8_t */ + fragACKBitfield?: number; + securityHeader?: ZigbeeSecurityHeader; +}; + +export type ZigbeeAPSPayload = Buffer; + +export function decodeZigbeeAPSFrameControl(data: Buffer, offset: number): [ZigbeeAPSFrameControl, offset: number] { + const fcf = data.readUInt8(offset); + offset += 1; + + return [ + { + frameType: fcf & ZigbeeAPSConsts.FCF_FRAME_TYPE, + deliveryMode: (fcf & ZigbeeAPSConsts.FCF_DELIVERY_MODE) >> 2, + // indirectMode = (fcf & ZigbeeAPSConsts.FCF_INDIRECT_MODE) >> 4, + ackFormat: Boolean((fcf & ZigbeeAPSConsts.FCF_ACK_FORMAT) >> 4), + security: Boolean((fcf & ZigbeeAPSConsts.FCF_SECURITY) >> 5), + ackRequest: Boolean((fcf & ZigbeeAPSConsts.FCF_ACK_REQ) >> 6), + extendedHeader: Boolean((fcf & ZigbeeAPSConsts.FCF_EXT_HEADER) >> 7), + }, + offset, + ]; +} + +function encodeZigbeeAPSFrameControl(data: Buffer, offset: number, fcf: ZigbeeAPSFrameControl): number { + data.writeUInt8( + (fcf.frameType & ZigbeeAPSConsts.FCF_FRAME_TYPE) | + ((fcf.deliveryMode << 2) & ZigbeeAPSConsts.FCF_DELIVERY_MODE) | + // ((fcf.indirectMode << 4) & ZigbeeAPSConsts.FCF_INDIRECT_MODE) | + (((fcf.ackFormat ? 1 : 0) << 4) & ZigbeeAPSConsts.FCF_ACK_FORMAT) | + (((fcf.security ? 1 : 0) << 5) & ZigbeeAPSConsts.FCF_SECURITY) | + (((fcf.ackRequest ? 1 : 0) << 6) & ZigbeeAPSConsts.FCF_ACK_REQ) | + (((fcf.extendedHeader ? 1 : 0) << 7) & ZigbeeAPSConsts.FCF_EXT_HEADER), + offset, + ); + offset += 1; + + return offset; +} + +export function decodeZigbeeAPSHeader(data: Buffer, offset: number, frameControl: ZigbeeAPSFrameControl): [ZigbeeAPSHeader, offset: number] { + let hasEndpointAddressing = true; + let destPresent = false; + let sourcePresent = false; + let destEndpoint: number | undefined; + let group: number | undefined; + let clusterId: number | undefined; + let profileId: number | undefined; + let sourceEndpoint: number | undefined; + + switch (frameControl.frameType) { + case ZigbeeAPSFrameType.DATA: { + break; + } + case ZigbeeAPSFrameType.ACK: { + if (frameControl.ackFormat) { + hasEndpointAddressing = false; + } + break; + } + case ZigbeeAPSFrameType.INTERPAN: { + destPresent = false; + sourcePresent = false; + break; + } + case ZigbeeAPSFrameType.CMD: { + hasEndpointAddressing = false; + break; + } + } + + if (hasEndpointAddressing) { + if (frameControl.frameType !== ZigbeeAPSFrameType.INTERPAN) { + if (frameControl.deliveryMode === ZigbeeAPSDeliveryMode.UNICAST || frameControl.deliveryMode === ZigbeeAPSDeliveryMode.BCAST) { + destPresent = true; + sourcePresent = true; + } else if (frameControl.deliveryMode === ZigbeeAPSDeliveryMode.GROUP) { + destPresent = false; + sourcePresent = true; + } else { + throw new Error(`Invalid APS delivery mode ${frameControl.deliveryMode}`); + } + + if (destPresent) { + destEndpoint = data.readUInt8(offset); + offset += 1; + } + } + + if (frameControl.deliveryMode === ZigbeeAPSDeliveryMode.GROUP) { + group = data.readUInt16LE(offset); + offset += 2; + } + + clusterId = data.readUInt16LE(offset); + offset += 2; + + profileId = data.readUInt16LE(offset); + offset += 2; + + if (sourcePresent) { + sourceEndpoint = data.readUInt8(offset); + offset += 1; + } + } + + let counter: number | undefined; + + if (frameControl.frameType !== ZigbeeAPSFrameType.INTERPAN) { + counter = data.readUInt8(offset); + offset += 1; + } + + let fragmentation = undefined; + let fragBlockNumber: number | undefined; + let fragACKBitfield: number | undefined; + + if (frameControl.extendedHeader) { + const fcf = data.readUInt8(offset); + offset += 1; + fragmentation = fcf & ZigbeeAPSConsts.EXT_FCF_FRAGMENT; + + if (fragmentation !== ZigbeeAPSFragmentation.NONE) { + fragBlockNumber = data.readUInt8(offset); + offset += 1; + } + + if (fragmentation !== ZigbeeAPSFragmentation.NONE && frameControl.frameType === ZigbeeAPSFrameType.ACK) { + fragACKBitfield = data.readUInt8(offset); + offset += 1; + } + } + + if (fragmentation !== undefined && fragmentation !== ZigbeeAPSFragmentation.NONE) { + // TODO + throw new Error("APS fragmentation not supported"); + } + + return [ + { + frameControl, + destEndpoint: destEndpoint, + group, + clusterId, + profileId, + sourceEndpoint: sourceEndpoint, + counter, + fragmentation, + fragBlockNumber, + fragACKBitfield, + securityHeader: undefined, // set later, or not + }, + offset, + ]; +} + +export function encodeZigbeeAPSHeader(data: Buffer, offset: number, header: ZigbeeAPSHeader): number { + offset = encodeZigbeeAPSFrameControl(data, offset, header.frameControl); + let hasEndpointAddressing = true; + let destPresent = false; + let sourcePresent = false; + + switch (header.frameControl.frameType) { + case ZigbeeAPSFrameType.DATA: { + break; + } + case ZigbeeAPSFrameType.ACK: { + if (header.frameControl.ackFormat) { + hasEndpointAddressing = false; + } + break; + } + case ZigbeeAPSFrameType.INTERPAN: { + destPresent = false; + sourcePresent = false; + break; + } + case ZigbeeAPSFrameType.CMD: { + hasEndpointAddressing = false; + break; + } + } + + if (hasEndpointAddressing) { + if (header.frameControl.frameType !== ZigbeeAPSFrameType.INTERPAN) { + if ( + header.frameControl.deliveryMode === ZigbeeAPSDeliveryMode.UNICAST || + header.frameControl.deliveryMode === ZigbeeAPSDeliveryMode.BCAST + ) { + destPresent = true; + sourcePresent = true; + } else if (header.frameControl.deliveryMode === ZigbeeAPSDeliveryMode.GROUP) { + destPresent = false; + sourcePresent = true; + } else { + throw new Error(`Invalid APS delivery mode ${header.frameControl.deliveryMode}`); + } + + if (destPresent) { + data.writeUInt8(header.destEndpoint!, offset); + offset += 1; + } + } + + if (header.frameControl.deliveryMode === ZigbeeAPSDeliveryMode.GROUP) { + data.writeUInt16LE(header.group!, offset); + offset += 2; + } + + data.writeUInt16LE(header.clusterId!, offset); + offset += 2; + + data.writeUInt16LE(header.profileId!, offset); + offset += 2; + + if (sourcePresent) { + data.writeUInt8(header.sourceEndpoint!, offset); + offset += 1; + } + } + + if (header.frameControl.frameType !== ZigbeeAPSFrameType.INTERPAN) { + data.writeUInt8(header.counter!, offset); + offset += 1; + } + + if (header.frameControl.extendedHeader) { + const fcf = header.fragmentation! & ZigbeeAPSConsts.EXT_FCF_FRAGMENT; + + data.writeUInt8(fcf, offset); + offset += 1; + + if (header.fragmentation! !== ZigbeeAPSFragmentation.NONE) { + data.writeUInt8(header.fragBlockNumber!, offset); + offset += 1; + } + + if (header.fragmentation! !== ZigbeeAPSFragmentation.NONE && header.frameControl.frameType === ZigbeeAPSFrameType.ACK) { + data.writeUInt8(header.fragACKBitfield!, offset); + offset += 1; + } + } + + return offset; +} + +/** + * @param data + * @param offset + * @param decryptKey If undefined, use default pre-hashed + * @param nwkSource64 + * @param frameControl + * @param header + */ +export function decodeZigbeeAPSPayload( + data: Buffer, + offset: number, + decryptKey: Buffer | undefined, + nwkSource64: bigint | undefined, + frameControl: ZigbeeAPSFrameControl, + header: ZigbeeAPSHeader, +): ZigbeeAPSPayload { + if (frameControl.security) { + const [payload, securityHeader, dOutOffset] = decryptZigbeePayload(data, offset, decryptKey, nwkSource64); + offset = dOutOffset; + header.securityHeader = securityHeader; + + return payload; + } + + return data.subarray(offset); +} + +/** + * @param header + * @param payload + * @param securityHeader + * @param encryptKey If undefined, and security=true, use default pre-hashed + */ +export function encodeZigbeeAPSFrame( + header: ZigbeeAPSHeader, + payload: ZigbeeAPSPayload, + securityHeader?: ZigbeeSecurityHeader, + encryptKey?: Buffer, +): Buffer { + let offset = 0; + const data = Buffer.alloc(ZigbeeAPSConsts.FRAME_MAX_SIZE); + + offset = encodeZigbeeAPSHeader(data, offset, header); + + if (header.frameControl.security) { + // the octet string `a` SHALL be the string ApsHeader || Auxiliary-Header and the octet string `m` SHALL be the string Payload + const [cryptedPayload, authTag, eOutOffset] = encryptZigbeePayload(data, offset, payload, securityHeader!, encryptKey); + offset = eOutOffset; + + data.set(cryptedPayload, offset); + offset += cryptedPayload.byteLength; + + data.set(authTag, offset); + offset += authTag.byteLength; + + return data.subarray(0, offset); + } + + data.set(payload, offset); + offset += payload.byteLength; + + // TODO: auth tag? + // the octet string `a` SHALL be the string ApsHeader || AuxiliaryHeader || Payload and the octet string `m` SHALL be a string of length zero + + return data.subarray(0, offset); +} diff --git a/src/zigbee/zigbee-nwk.ts b/src/zigbee/zigbee-nwk.ts new file mode 100644 index 0000000..765aaaa --- /dev/null +++ b/src/zigbee/zigbee-nwk.ts @@ -0,0 +1,537 @@ +import { type ZigbeeSecurityHeader, decryptZigbeePayload, encryptZigbeePayload } from "./zigbee.js"; + +/** + * const enum with sole purpose of avoiding "magic numbers" in code for well-known values + */ +export const enum ZigbeeNWKConsts { + FRAME_MAX_SIZE = 116, + /** no security */ + HEADER_MIN_SIZE = 8, + HEADER_MAX_SIZE = 30, + PAYLOAD_MIN_SIZE = 86, + PAYLOAD_MAX_SIZE = 108, + + //---- ZigBee version numbers. + /** Re: 053474r06ZB_TSC-ZigBeeSpecification.pdf */ + // VERSION_2004 = 1, + /** Re: 053474r17ZB_TSC-ZigBeeSpecification.pdf */ + VERSION_2007 = 2, + VERSION_GREEN_POWER = 3, + + //---- ZigBee NWK Route Options Flags + /** ZigBee 2004 only. */ + // ROUTE_OPTION_REPAIR = 0x80, + /** ZigBee 2006 and later */ + ROUTE_OPTION_MCAST = 0x40, + /** ZigBee 2007 and later (route request only). */ + ROUTE_OPTION_DEST_EXT = 0x20, + /** ZigBee 2007 and later (route request only). */ + ROUTE_OPTION_MANY_MASK = 0x18, + /** ZigBee 2007 and layer (route reply only). */ + ROUTE_OPTION_RESP_EXT = 0x20, + /** ZigBee 2007 and later (route reply only). */ + ROUTE_OPTION_ORIG_EXT = 0x10, + /* Many-to-One modes, ZigBee 2007 and later (route request only). */ + ROUTE_OPTION_MANY_NONE = 0x00, + /* Many-to-One modes, ZigBee 2007 and later (route request only). */ + ROUTE_OPTION_MANY_REC = 0x01, + /* Many-to-One modes, ZigBee 2007 and later (route request only). */ + ROUTE_OPTION_MANY_NOREC = 0x02, + + //---- ZigBee NWK Route Options Flags + // CMD_ROUTE_OPTION_REPAIR = 0x80, /* ZigBee 2004 only. */ + // CMD_ROUTE_OPTION_MCAST = 0x40, /* ZigBee 2006 and later, @deprecated */ + CMD_ROUTE_OPTION_DEST_EXT = 0x20 /* ZigBee 2007 and later (route request only). */, + CMD_ROUTE_OPTION_MANY_MASK = 0x18 /* ZigBee 2007 and later (route request only). */, + CMD_ROUTE_OPTION_RESP_EXT = 0x20 /* ZigBee 2007 and layer (route reply only). */, + CMD_ROUTE_OPTION_ORIG_EXT = 0x10 /* ZigBee 2007 and later (route reply only). */, + + //---- Many-to-One modes, ZigBee 2007 and later (route request only) + CMD_ROUTE_OPTION_MANY_NONE = 0x00, + CMD_ROUTE_OPTION_MANY_REC = 0x01, + CMD_ROUTE_OPTION_MANY_NOREC = 0x02, + + //---- ZigBee NWK Leave Options Flags + CMD_LEAVE_OPTION_REMOVE_CHILDREN = 0x80, + CMD_LEAVE_OPTION_REQUEST = 0x40, + CMD_LEAVE_OPTION_REJOIN = 0x20, + + //---- ZigBee NWK Link Status Options + CMD_LINK_OPTION_LAST_FRAME = 0x40, + CMD_LINK_OPTION_FIRST_FRAME = 0x20, + CMD_LINK_OPTION_COUNT_MASK = 0x1f, + + //---- ZigBee NWK Link Status cost fields + CMD_LINK_INCOMING_COST_MASK = 0x07, + CMD_LINK_OUTGOING_COST_MASK = 0x70, + + //---- ZigBee NWK Report Options + CMD_NWK_REPORT_COUNT_MASK = 0x1f, + CMD_NWK_REPORT_ID_MASK = 0xe0, + CMD_NWK_REPORT_ID_PAN_CONFLICT = 0x00, + + //---- ZigBee NWK Update Options + CMD_NWK_UPDATE_COUNT_MASK = 0x1f, + CMD_NWK_UPDATE_ID_MASK = 0xe0, + CMD_NWK_UPDATE_ID_PAN_UPDATE = 0x00, + + //---- ZigBee NWK Values of the Parent Information Bitmask (Table 3.47) + CMD_ED_TIMEO_RSP_PRNT_INFO_MAC_DATA_POLL_KEEPAL_SUPP = 0x01, + CMD_ED_TIMEO_RSP_PRNT_INFO_ED_TIMOU_REQ_KEEPAL_SUPP = 0x02, + CMD_ED_TIMEO_RSP_PRNT_INFO_PWR_NEG_SUPP = 0x04, + + //---- ZigBee NWK Link Power Delta Options + CMD_NWK_LINK_PWR_DELTA_TYPE_MASK = 0x03, + + //---- MAC Association Status extension + ASSOC_STATUS_ADDR_CONFLICT = 0xf0, + + //---- ZigBee NWK FCF fields + FCF_FRAME_TYPE = 0x0003, + FCF_VERSION = 0x003c, + FCF_DISCOVER_ROUTE = 0x00c0, + /** ZigBee 2006 and Later */ + FCF_MULTICAST = 0x0100, + FCF_SECURITY = 0x0200, + /** ZigBee 2006 and Later */ + FCF_SOURCE_ROUTE = 0x0400, + /** ZigBee 2006 and Later */ + FCF_EXT_DEST = 0x0800, + /** ZigBee 2006 and Later */ + FCF_EXT_SOURCE = 0x1000, + /** ZigBee PRO r21 */ + FCF_END_DEVICE_INITIATOR = 0x2000, + + //---- ZigBee NWK Multicast Control fields - ZigBee 2006 and later + MCAST_MODE = 0x03, + MCAST_RADIUS = 0x1c, + MCAST_MAX_RADIUS = 0xe0, +} + +/** ZigBee NWK FCF Frame Types */ +export const enum ZigbeeNWKFrameType { + DATA = 0x00, + CMD = 0x01, + INTERPAN = 0x03, +} + +/** ZigBee NWK Discovery Modes. */ +export const enum ZigbeeNWKRouteDiscovery { + SUPPRESS = 0x0000, + ENABLE = 0x0001, + FORCE = 0x0003, +} + +export const enum ZigbeeNWKMulticastMode { + NONMEMBER = 0x00, + MEMBER = 0x01, +} + +export const enum ZigbeeNWKRelayType { + NO_RELAY = 0, + RELAY_UPSTREAM = 1, + RELAY_DOWNSTREAM = 2, +} + +/** ZigBee NWK Command Types */ +export const enum ZigbeeNWKCommandId { + /* Route Request Command. */ + ROUTE_REQ = 0x01, + /* Route Reply Command. */ + ROUTE_REPLY = 0x02, + /* Network Status Command. */ + NWK_STATUS = 0x03, + /* Leave Command. ZigBee 2006 and Later */ + LEAVE = 0x04, + /* Route Record Command. ZigBee 2006 and later */ + ROUTE_RECORD = 0x05, + /* Rejoin Request Command. ZigBee 2006 and later */ + REJOIN_REQ = 0x06, + /* Rejoin Response Command. ZigBee 2006 and later */ + REJOIN_RESP = 0x07, + /* Link Status Command. ZigBee 2007 and later */ + LINK_STATUS = 0x08, + /* Network Report Command. ZigBee 2007 and later */ + NWK_REPORT = 0x09, + /* Network Update Command. ZigBee 2007 and later */ + NWK_UPDATE = 0x0a, + /* Network End Device Timeout Request Command. r21 */ + ED_TIMEOUT_REQUEST = 0x0b, + /* Network End Device Timeout Response Command. r21 */ + ED_TIMEOUT_RESPONSE = 0x0c, + /* Link Power Delta Command. r22 */ + LINK_PWR_DELTA = 0x0d, + /* Network Commissioning Request Command. r23 */ + COMMISSIONING_REQUEST = 0x0e, + /* Network Commissioning Response Command. r23 */ + COMMISSIONING_RESPONSE = 0x0f, +} + +/** Network Status Code Definitions. */ +export enum ZigbeeNWKStatus { + /** @deprecated in R23, should no longer be sent, but still processed (same as @see LINK_FAILURE ) */ + LEGACY_NO_ROUTE_AVAILABLE = 0x00, + /** @deprecated in R23, should no longer be sent, but still processed (same as @see LINK_FAILURE ) */ + LEGACY_LINK_FAILURE = 0x01, + /** This link code indicates a failure to route across a link. */ + LINK_FAILURE = 0x02, + // LOW_BATTERY = 0x03, // deprecated + // NO_ROUTING = 0x04, // deprecated + // NO_INDIRECT = 0x05, // deprecated + // INDIRECT_EXPIRE = 0x06, // deprecated + // DEVICE_UNAVAIL = 0x07, // deprecated + // ADDR_UNAVAIL = 0x08, // deprecated + /** + * The failure occurred as a result of a failure in the RF link to the device’s parent. + * This status is only used locally on a device to indicate loss of communication with the parent. + */ + PARENT_LINK_FAILURE = 0x09, + // VALIDATE_ROUTE = 0x0a, // deprecated + /** Source routing has failed, probably indicating a link failure in one of the source route’s links. */ + SOURCE_ROUTE_FAILURE = 0x0b, + /** A route established as a result of a many-to-one route request has failed. */ + MANY_TO_ONE_ROUTE_FAILURE = 0x0c, + /** The address in the destination address field has been determined to be in use by two or more devices. */ + ADDRESS_CONFLICT = 0x0d, + // VERIFY_ADDRESS = 0x0e, // deprecated + /** The operational network PAN identifier of the device has been updated. */ + PANID_UPDATE = 0x0f, + /** The network address of the local device has been updated. */ + NETWORK_ADDRESS_UPDATE = 0x10, + // BAD_FRAME_COUNTER = 0x11, // XXX: not in spec + // BAD_KEY_SEQNO = 0x12, // XXX: not in spec + /** The NWK command ID is not known to the device. */ + UNKNOWN_COMMAND = 0x13, + /** Notification to the local application that a PAN ID Conflict Report has been received by the local Network Manager. */ + PANID_CONFLICT_REPORT = 0x14, + // RESERVED = 0x15-0xff, +} + +export const enum ZigbeeNWKManyToOne { + /** The route request is not a many-to-one route request. */ + DISABLED = 0, + /** The route request is a many-to-one route request and the sender supports a route record table. */ + WITH_SOURCE_ROUTING = 1, + /** The route request is a many-to-one route request and the sender does not support a route record table. */ + WITHOUT_SOURCE_ROUTING = 2, + // RESERVED = 3, +} + +export const enum ZigbeeNWKRouteStatus { + ACTIVE = 0x0, + DISCOVERY_UNDERWAY = 0x1, + DISCOVERY_FAILED = 0x2, + INACTIVE = 0x3, + // RESERVED = 0x4-0x7, +} + +export type ZigbeeNWKLinkStatus = { + /** uint16_t */ + address: number; + /** LB uint8_t */ + incomingCost: number; + /** HB uint8_t */ + outgoingCost: number; +}; + +/** + * Frame Control Field: 0x0248, Frame Type: Data, Discover Route: Enable, Security Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 01.. .... = Discover Route: Enable (0x1) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + */ +export type ZigbeeNWKFrameControl = { + frameType: ZigbeeNWKFrameType; + protocolVersion: number; + discoverRoute: ZigbeeNWKRouteDiscovery; + /** ZigBee 2006 and Later */ + multicast: boolean; + security: boolean; + /** ZigBee 2006 and Later */ + sourceRoute: boolean; + /** ZigBee 2006 and Later */ + extendedDestination: boolean; + /** ZigBee 2006 and Later */ + extendedSource: boolean; + /** ZigBee PRO r21 */ + endDeviceInitiator: boolean; +}; + +/** + * Multicast Control + * ZigBee 2006 and later + */ +export type ZigbeeNWKMulticastControl = { + mode: ZigbeeNWKMulticastMode; + radius: number; + maxRadius: number; +}; + +export type ZigbeeNWKHeader = { + frameControl: ZigbeeNWKFrameControl; + destination16?: number; + source16?: number; + radius?: number; + seqNum?: number; + destination64?: bigint; + source64?: bigint; + multicastControl?: ZigbeeNWKMulticastControl; + relayIndex?: number; + relayAddresses?: number[]; + securityHeader?: ZigbeeSecurityHeader; +}; + +/** + * if the security subfield is set to 1 in the frame control field, the frame payload is protected as defined by the security suite selected for that relationship. + * + * Octets: variable + */ +export type ZigbeeNWKPayload = Buffer; + +export function decodeZigbeeNWKFrameControl(data: Buffer, offset: number): [ZigbeeNWKFrameControl, offset: number] { + const fcf = data.readUInt16LE(offset); + offset += 2; + + return [ + { + frameType: fcf & ZigbeeNWKConsts.FCF_FRAME_TYPE, + protocolVersion: (fcf & ZigbeeNWKConsts.FCF_VERSION) >> 2, + discoverRoute: (fcf & ZigbeeNWKConsts.FCF_DISCOVER_ROUTE) >> 6, + multicast: Boolean((fcf & ZigbeeNWKConsts.FCF_MULTICAST) >> 8), + security: Boolean((fcf & ZigbeeNWKConsts.FCF_SECURITY) >> 9), + sourceRoute: Boolean((fcf & ZigbeeNWKConsts.FCF_SOURCE_ROUTE) >> 10), + extendedDestination: Boolean((fcf & ZigbeeNWKConsts.FCF_EXT_DEST) >> 11), + extendedSource: Boolean((fcf & ZigbeeNWKConsts.FCF_EXT_SOURCE) >> 12), + endDeviceInitiator: Boolean((fcf & ZigbeeNWKConsts.FCF_END_DEVICE_INITIATOR) >> 13), + }, + offset, + ]; +} + +function encodeZigbeeNWKFrameControl(view: Buffer, offset: number, fcf: ZigbeeNWKFrameControl): number { + view.writeUInt16LE( + (fcf.frameType & ZigbeeNWKConsts.FCF_FRAME_TYPE) | + ((fcf.protocolVersion << 2) & ZigbeeNWKConsts.FCF_VERSION) | + ((fcf.discoverRoute << 6) & ZigbeeNWKConsts.FCF_DISCOVER_ROUTE) | + (((fcf.multicast ? 1 : 0) << 8) & ZigbeeNWKConsts.FCF_MULTICAST) | + (((fcf.security ? 1 : 0) << 9) & ZigbeeNWKConsts.FCF_SECURITY) | + (((fcf.sourceRoute ? 1 : 0) << 10) & ZigbeeNWKConsts.FCF_SOURCE_ROUTE) | + (((fcf.extendedDestination ? 1 : 0) << 11) & ZigbeeNWKConsts.FCF_EXT_DEST) | + (((fcf.extendedSource ? 1 : 0) << 12) & ZigbeeNWKConsts.FCF_EXT_SOURCE) | + (((fcf.endDeviceInitiator ? 1 : 0) << 13) & ZigbeeNWKConsts.FCF_END_DEVICE_INITIATOR), + offset, + ); + offset += 2; + + return offset; +} + +function decodeZigbeeNWKMulticastControl(data: Buffer, offset: number): [ZigbeeNWKMulticastControl, offset: number] { + const ctrl = data.readUInt8(offset); + offset += 1; + + return [ + { + mode: ctrl & ZigbeeNWKConsts.MCAST_MODE, + radius: (ctrl & ZigbeeNWKConsts.MCAST_RADIUS) >> 2, + maxRadius: (ctrl & ZigbeeNWKConsts.MCAST_MAX_RADIUS) >> 5, + }, + offset, + ]; +} + +function encodeZigbeeNWKMulticastControl(data: Buffer, offset: number, multicastControl: ZigbeeNWKMulticastControl): number { + data.writeUInt8( + (multicastControl.mode & ZigbeeNWKConsts.MCAST_MODE) | + ((multicastControl.radius << 2) & ZigbeeNWKConsts.MCAST_RADIUS) | + ((multicastControl.maxRadius << 5) & ZigbeeNWKConsts.MCAST_MAX_RADIUS), + offset, + ); + offset += 1; + + return offset; +} + +export function decodeZigbeeNWKHeader(data: Buffer, offset: number, frameControl: ZigbeeNWKFrameControl): [ZigbeeNWKHeader, offset: number] { + let destination: number | undefined; + let source: number | undefined; + let radius: number | undefined; + let seqNum: number | undefined; + let destination64: bigint | undefined; + let source64: bigint | undefined; + let multicastControl: ZigbeeNWKMulticastControl | undefined; + let relayIndex: number | undefined; + let relayAddresses: number[] | undefined; + + if (frameControl.frameType !== ZigbeeNWKFrameType.INTERPAN) { + destination = data.readUInt16LE(offset); + offset += 2; + source = data.readUInt16LE(offset); + offset += 2; + radius = data.readUInt8(offset); + offset += 1; + seqNum = data.readUInt8(offset); + offset += 1; + + if (frameControl.extendedDestination) { + destination64 = data.readBigUInt64LE(offset); + offset += 8; + } + + if (frameControl.extendedSource) { + source64 = data.readBigUInt64LE(offset); + offset += 8; + } + + if (frameControl.multicast) { + [multicastControl, offset] = decodeZigbeeNWKMulticastControl(data, offset); + } + + if (frameControl.sourceRoute) { + const relayCount = data.readUInt8(offset); + offset += 1; + relayIndex = data.readUInt8(offset); + offset += 1; + relayAddresses = []; + + for (let i = 0; i < relayCount; i++) { + relayAddresses.push(data.readUInt16LE(offset)); + offset += 2; + } + } + } + + if (offset >= data.byteLength) { + throw new Error("Invalid NWK frame: no payload"); + } + + return [ + { + frameControl, + destination16: destination, + source16: source, + radius, + seqNum, + destination64, + source64, + multicastControl, + relayIndex, + relayAddresses, + securityHeader: undefined, // set later, or not + }, + offset, + ]; +} + +function encodeZigbeeNWKHeader(data: Buffer, offset: number, header: ZigbeeNWKHeader): number { + offset = encodeZigbeeNWKFrameControl(data, offset, header.frameControl); + + if (header.frameControl.frameType !== ZigbeeNWKFrameType.INTERPAN) { + data.writeUInt16LE(header.destination16!, offset); + offset += 2; + data.writeUInt16LE(header.source16!, offset); + offset += 2; + data.writeUInt8(header.radius!, offset); + offset += 1; + data.writeUInt8(header.seqNum!, offset); + offset += 1; + + if (header.frameControl.extendedDestination) { + data.writeBigUInt64LE(header.destination64!, offset); + offset += 8; + } + + if (header.frameControl.extendedSource) { + data.writeBigUInt64LE(header.source64!, offset); + offset += 8; + } + + if (header.frameControl.multicast) { + offset = encodeZigbeeNWKMulticastControl(data, offset, header.multicastControl!); + } + + if (header.frameControl.sourceRoute) { + data.writeUInt8(header.relayAddresses!.length, offset); + offset += 1; + data.writeUInt8(header.relayIndex!, offset); + offset += 1; + + for (const relayAddress of header.relayAddresses!) { + data.writeUInt16LE(relayAddress, offset); + offset += 2; + } + } + } + + return offset; +} + +/** + * + * @param data + * @param offset + * @param decryptKey If undefined, use default pre-hashed + * @param macSource64 + * @param frameControl + * @param header + */ +export function decodeZigbeeNWKPayload( + data: Buffer, + offset: number, + decryptKey: Buffer | undefined, + macSource64: bigint | undefined, + frameControl: ZigbeeNWKFrameControl, + header: ZigbeeNWKHeader, +): ZigbeeNWKPayload { + if (frameControl.security) { + const [payload, securityHeader, dOutOffset] = decryptZigbeePayload(data, offset, decryptKey, macSource64); + offset = dOutOffset; + header.securityHeader = securityHeader; + + return payload; + } + + return data.subarray(offset); +} + +/** + * @param header + * @param payload + * @param securityHeader + * @param encryptKey If undefined, and security=true, use default pre-hashed + */ +export function encodeZigbeeNWKFrame( + header: ZigbeeNWKHeader, + payload: ZigbeeNWKPayload, + securityHeader?: ZigbeeSecurityHeader, + encryptKey?: Buffer, +): Buffer { + let offset = 0; + const data = Buffer.alloc(ZigbeeNWKConsts.FRAME_MAX_SIZE); + + offset = encodeZigbeeNWKHeader(data, offset, header); + + if (header.frameControl.security) { + const [cryptedPayload, authTag, eOutOffset] = encryptZigbeePayload(data, offset, payload, securityHeader!, encryptKey); + offset = eOutOffset; + + data.set(cryptedPayload, offset); + offset += cryptedPayload.byteLength; + + data.set(authTag, offset); + offset += authTag.byteLength; + + return data.subarray(0, offset); + } + + data.set(payload, offset); + offset += payload.byteLength; + + return data.subarray(0, offset); +} diff --git a/src/zigbee/zigbee-nwkgp.ts b/src/zigbee/zigbee-nwkgp.ts new file mode 100644 index 0000000..21dc7c6 --- /dev/null +++ b/src/zigbee/zigbee-nwkgp.ts @@ -0,0 +1,459 @@ +import { ZigbeeConsts, aes128CcmStar } from "./zigbee.js"; + +/** + * const enum with sole purpose of avoiding "magic numbers" in code for well-known values + */ +export const enum ZigbeeNWKGPConsts { + // TODO: get actual values, these are just copied from NWK + FRAME_MAX_SIZE = 116, + /** no security */ + HEADER_MIN_SIZE = 8, + HEADER_MAX_SIZE = 30, + PAYLOAD_MIN_SIZE = 86, + PAYLOAD_MAX_SIZE = 108, + + //---- ZigBee NWK GP FCF fields + FCF_AUTO_COMMISSIONING = 0x40, + FCF_CONTROL_EXTENSION = 0x80, + FCF_FRAME_TYPE = 0x03, + FCF_VERSION = 0x3c, + + //---- Extended NWK Frame Control field + FCF_EXT_APP_ID = 0x07, // 0 - 2 b. + FCF_EXT_SECURITY_LEVEL = 0x18, // 3 - 4 b. + FCF_EXT_SECURITY_KEY = 0x20, // 5 b. + FCF_EXT_RX_AFTER_TX = 0x40, // 6 b. + FCF_EXT_DIRECTION = 0x80, // 7 b. +} + +/** ZigBee NWK GP FCF frame types. */ +export const enum ZigbeeNWKGPFrameType { + DATA = 0x00, + MAINTENANCE = 0x01, +} + +/** Definitions for application IDs. */ +export const enum ZigbeeNWKGPAppId { + DEFAULT = 0x00, + LPED = 0x01, + ZGP = 0x02, +} + +/** Definitions for GP directions. */ +export const enum ZigbeeNWKGPDirection { + // DIRECTION_DEFAULT = 0x00, + DIRECTION_FROM_ZGPD = 0x00, + DIRECTION_FROM_ZGPP = 0x01, +} + +/** Security level values. */ +export const enum ZigbeeNWKGPSecurityLevel { + NO = 0x00, + ONELSB = 0x01, + FULL = 0x02, + FULLENCR = 0x03, +} + +/** GP Security key types. */ +export const enum ZigbeeNWKGPSecurityKeyType { + NO_KEY = 0x00, + ZB_NWK_KEY = 0x01, + GPD_GROUP_KEY = 0x02, + NWK_KEY_DERIVED_GPD_KEY_GROUP_KEY = 0x03, + PRECONFIGURED_INDIVIDUAL_GPD_KEY = 0x04, + DERIVED_INDIVIDUAL_GPD_KEY = 0x07, +} + +export const enum ZigbeeNWKGPCommandId { + IDENTIFY = 0x00, + RECALL_SCENE0 = 0x10, + RECALL_SCENE1 = 0x11, + RECALL_SCENE2 = 0x12, + RECALL_SCENE3 = 0x13, + RECALL_SCENE4 = 0x14, + RECALL_SCENE5 = 0x15, + RECALL_SCENE6 = 0x16, + RECALL_SCENE7 = 0x17, + STORE_SCENE0 = 0x18, + STORE_SCENE1 = 0x19, + STORE_SCENE2 = 0x1a, + STORE_SCENE3 = 0x1b, + STORE_SCENE4 = 0x1c, + STORE_SCENE5 = 0x1d, + STORE_SCENE6 = 0x1e, + STORE_SCENE7 = 0x1f, + OFF = 0x20, + ON = 0x21, + TOGGLE = 0x22, + RELEASE = 0x23, + MOVE_UP = 0x30, + MOVE_DOWN = 0x31, + STEP_UP = 0x32, + STEP_DOWN = 0x33, + LEVEL_CONTROL_STOP = 0x34, + MOVE_UP_WITH_ON_OFF = 0x35, + MOVE_DOWN_WITH_ON_OFF = 0x36, + STEP_UP_WITH_ON_OFF = 0x37, + STEP_DOWN_WITH_ON_OFF = 0x38, + MOVE_HUE_STOP = 0x40, + MOVE_HUE_UP = 0x41, + MOVE_HUE_DOWN = 0x42, + STEP_HUE_UP = 0x43, + STEP_HUW_DOWN = 0x44, + MOVE_SATURATION_STOP = 0x45, + MOVE_SATURATION_UP = 0x46, + MOVE_SATURATION_DOWN = 0x47, + STEP_SATURATION_UP = 0x48, + STEP_SATURATION_DOWN = 0x49, + MOVE_COLOR = 0x4a, + STEP_COLOR = 0x4b, + LOCK_DOOR = 0x50, + UNLOCK_DOOR = 0x51, + PRESS11 = 0x60, + RELEASE11 = 0x61, + PRESS12 = 0x62, + RELEASE12 = 0x63, + PRESS22 = 0x64, + RELEASE22 = 0x65, + SHORT_PRESS11 = 0x66, + SHORT_PRESS12 = 0x67, + SHORT_PRESS22 = 0x68, + ATTRIBUTE_REPORTING = 0xa0, + MANUFACTURE_SPECIFIC_ATTR_REPORTING = 0xa1, + MULTI_CLUSTER_REPORTING = 0xa2, + MANUFACTURER_SPECIFIC_MCLUSTER_REPORTING = 0xa3, + REQUEST_ATTRIBUTES = 0xa4, + READ_ATTRIBUTES_RESPONSE = 0xa5, + ANY_SENSOR_COMMAND_A0_A3 = 0xaf, + COMMISSIONING = 0xe0, + DECOMMISSIONING = 0xe1, + SUCCESS = 0xe2, + CHANNEL_REQUEST = 0xe3, + COMMISSIONING_REPLY = 0xf0, + WRITE_ATTRIBUTES = 0xf1, + READ_ATTRIBUTES = 0xf2, + CHANNEL_CONFIGURATION = 0xf3, +} + +/** + * Frame Control Field: 0x8c, Frame Type: Data, NWK Frame Extension Data + * .... ..00 = Frame Type: Data (0x0) + * ..00 11.. = Protocol Version: 3 + * .0.. .... = Auto Commissioning: False + * 1... .... = NWK Frame Extension: True + */ +export type ZigbeeNWKGPFrameControl = { + frameType: number; + protocolVersion: number; + autoCommissioning: boolean; + nwkFrameControlExtension: boolean; +}; + +/** + * Extended NWK Frame Control Field: 0x30, Application ID: Unknown, Security Level: Full frame counter and full MIC only, Security Key, Direction: From ZGPD + * .... .000 = Application ID: Unknown (0x0) + * ...1 0... = Security Level: Full frame counter and full MIC only (0x2) + * ..1. .... = Security Key: True + * .0.. .... = Rx After Tx: False + * 0... .... = Direction: From ZGPD (0x0) + */ +export type ZigbeeNWKGPFrameControlExt = { + appId: ZigbeeNWKGPAppId; + securityLevel: ZigbeeNWKGPSecurityLevel; + securityKey: boolean; + rxAfterTx: boolean; + direction: ZigbeeNWKGPDirection; +}; + +export type ZigbeeNWKGPHeader = { + frameControl: ZigbeeNWKGPFrameControl; + frameControlExt?: ZigbeeNWKGPFrameControlExt; + sourceId?: number; + endpoint?: number; + micSize: 0 | 2 | 4; + securityFrameCounter?: number; + payloadLength: number; + mic?: number; +}; + +export type ZigbeeNWKGPPayload = Buffer; + +export function decodeZigbeeNWKGPFrameControl(data: Buffer, offset: number): [ZigbeeNWKGPFrameControl, offset: number] { + const fcf = data.readUInt8(offset); + offset += 1; + + return [ + { + frameType: fcf & ZigbeeNWKGPConsts.FCF_FRAME_TYPE, + protocolVersion: (fcf & ZigbeeNWKGPConsts.FCF_VERSION) >> 2, + autoCommissioning: Boolean((fcf & ZigbeeNWKGPConsts.FCF_AUTO_COMMISSIONING) >> 6), + nwkFrameControlExtension: Boolean((fcf & ZigbeeNWKGPConsts.FCF_CONTROL_EXTENSION) >> 7), + }, + offset, + ]; +} + +function encodeZigbeeNWKGPFrameControl(data: Buffer, offset: number, fcf: ZigbeeNWKGPFrameControl): number { + data.writeUInt8( + (fcf.frameType & ZigbeeNWKGPConsts.FCF_FRAME_TYPE) | + ((fcf.protocolVersion << 2) & ZigbeeNWKGPConsts.FCF_VERSION) | + (((fcf.autoCommissioning ? 1 : 0) << 6) & ZigbeeNWKGPConsts.FCF_AUTO_COMMISSIONING) | + (((fcf.nwkFrameControlExtension ? 1 : 0) << 7) & ZigbeeNWKGPConsts.FCF_CONTROL_EXTENSION), + offset, + ); + offset += 1; + + return offset; +} + +function decodeZigbeeNWKGPFrameControlExt(data: Buffer, offset: number): [ZigbeeNWKGPFrameControlExt, offset: number] { + const fcf = data.readUInt8(offset); + offset += 1; + + return [ + { + appId: fcf & ZigbeeNWKGPConsts.FCF_EXT_APP_ID, + securityLevel: (fcf & ZigbeeNWKGPConsts.FCF_EXT_SECURITY_LEVEL) >> 3, + securityKey: Boolean((fcf & ZigbeeNWKGPConsts.FCF_EXT_SECURITY_KEY) >> 5), + rxAfterTx: Boolean((fcf & ZigbeeNWKGPConsts.FCF_EXT_RX_AFTER_TX) >> 6), + direction: (fcf & ZigbeeNWKGPConsts.FCF_EXT_DIRECTION) >> 7, + }, + offset, + ]; +} + +function encodeZigbeeNWKGPFrameControlExt(data: Buffer, offset: number, fcExt: ZigbeeNWKGPFrameControlExt): number { + data.writeUInt8( + (fcExt.appId & ZigbeeNWKGPConsts.FCF_EXT_APP_ID) | + ((fcExt.securityLevel << 3) & ZigbeeNWKGPConsts.FCF_EXT_SECURITY_LEVEL) | + (((fcExt.securityKey ? 1 : 0) << 5) & ZigbeeNWKGPConsts.FCF_EXT_SECURITY_KEY) | + (((fcExt.rxAfterTx ? 1 : 0) << 6) & ZigbeeNWKGPConsts.FCF_EXT_RX_AFTER_TX) | + ((fcExt.direction << 7) & ZigbeeNWKGPConsts.FCF_EXT_DIRECTION), + offset, + ); + offset += 1; + + return offset; +} + +export function decodeZigbeeNWKGPHeader(data: Buffer, offset: number, frameControl: ZigbeeNWKGPFrameControl): [ZigbeeNWKGPHeader, offset: number] { + let frameControlExt: ZigbeeNWKGPFrameControlExt | undefined; + + if (frameControl.nwkFrameControlExtension) { + [frameControlExt, offset] = decodeZigbeeNWKGPFrameControlExt(data, offset); + } + + let sourceId: number | undefined; + let endpoint: number | undefined; + let micSize: ZigbeeNWKGPHeader["micSize"] = 0; + let securityFrameCounter: number | undefined; + let mic: number | undefined; + + if ( + (frameControl.frameType === ZigbeeNWKGPFrameType.DATA && !frameControl.nwkFrameControlExtension) || + (frameControl.frameType === ZigbeeNWKGPFrameType.DATA && + frameControl.nwkFrameControlExtension && + frameControlExt!.appId === ZigbeeNWKGPAppId.DEFAULT) || + (frameControl.frameType === ZigbeeNWKGPFrameType.MAINTENANCE && + frameControl.nwkFrameControlExtension && + frameControlExt!.appId === ZigbeeNWKGPAppId.DEFAULT && + data.readUInt8(offset) !== ZigbeeNWKGPCommandId.CHANNEL_CONFIGURATION) + ) { + sourceId = data.readUInt32LE(offset); + offset += 4; + } + + if (frameControl.nwkFrameControlExtension && frameControlExt!.appId === ZigbeeNWKGPAppId.ZGP) { + endpoint = data.readUInt8(offset); + offset += 1; + } + + if ( + frameControl.nwkFrameControlExtension && + (frameControlExt!.appId === ZigbeeNWKGPAppId.DEFAULT || + frameControlExt!.appId === ZigbeeNWKGPAppId.ZGP || + frameControlExt!.appId === ZigbeeNWKGPAppId.LPED) + ) { + if (frameControlExt!.securityLevel === ZigbeeNWKGPSecurityLevel.ONELSB && frameControlExt!.appId !== ZigbeeNWKGPAppId.LPED) { + micSize = 2; + } else if ( + frameControlExt!.securityLevel === ZigbeeNWKGPSecurityLevel.FULL || + frameControlExt!.securityLevel === ZigbeeNWKGPSecurityLevel.FULLENCR + ) { + micSize = 4; + securityFrameCounter = data.readUInt32LE(offset); + offset += 4; + } + } + + //-- here `offset` is "start of payload" + + const payloadLength = data.byteLength - offset - micSize; + + if (payloadLength <= 0) { + throw new Error("Zigbee NWK GP frame without payload"); + } + + if (micSize === 2) { + mic = data.readUInt16LE(offset + payloadLength); // at end + } else if (micSize === 4) { + mic = data.readUInt32LE(offset + payloadLength); // at end + } + + return [ + { + frameControl, + frameControlExt, + sourceId, + endpoint, + micSize, + securityFrameCounter, + payloadLength, + mic, + }, + offset, + ]; +} + +function encodeZigbeeNWKGPHeader(data: Buffer, offset: number, header: ZigbeeNWKGPHeader): number { + offset = encodeZigbeeNWKGPFrameControl(data, offset, header.frameControl); + + if (header.frameControl.nwkFrameControlExtension) { + offset = encodeZigbeeNWKGPFrameControlExt(data, offset, header.frameControlExt!); + } + + if ( + (header.frameControl.frameType === ZigbeeNWKGPFrameType.DATA && !header.frameControl.nwkFrameControlExtension) || + (header.frameControl.frameType === ZigbeeNWKGPFrameType.DATA && + header.frameControl.nwkFrameControlExtension && + header.frameControlExt!.appId === ZigbeeNWKGPAppId.DEFAULT) || + (header.frameControl.frameType === ZigbeeNWKGPFrameType.MAINTENANCE && + header.frameControl.nwkFrameControlExtension && + header.frameControlExt!.appId === ZigbeeNWKGPAppId.DEFAULT && + data.readUInt8(offset) !== ZigbeeNWKGPCommandId.CHANNEL_CONFIGURATION) + ) { + data.writeUInt32LE(header.sourceId!, offset); + offset += 4; + } + + if (header.frameControl.nwkFrameControlExtension && header.frameControlExt!.appId === ZigbeeNWKGPAppId.ZGP) { + data.writeUInt8(header.endpoint!, offset); + offset += 1; + } + + if ( + header.frameControl.nwkFrameControlExtension && + (header.frameControlExt!.appId === ZigbeeNWKGPAppId.DEFAULT || + header.frameControlExt!.appId === ZigbeeNWKGPAppId.ZGP || + header.frameControlExt!.appId === ZigbeeNWKGPAppId.LPED) + ) { + if ( + header.frameControlExt!.securityLevel === ZigbeeNWKGPSecurityLevel.FULL || + header.frameControlExt!.securityLevel === ZigbeeNWKGPSecurityLevel.FULLENCR + ) { + data.writeUInt32LE(header.securityFrameCounter!, offset); + offset += 4; + } + } + + //-- here `offset` is "start of payload" + + return offset; +} + +function makeGPNonce(header: ZigbeeNWKGPHeader, macSource64: bigint | undefined): Buffer { + const nonce = Buffer.alloc(ZigbeeConsts.SEC_NONCE_LEN); + let offset = 0; + + if (header.frameControlExt!.appId === ZigbeeNWKGPAppId.DEFAULT) { + if (header.frameControlExt!.direction === ZigbeeNWKGPDirection.DIRECTION_FROM_ZGPD) { + nonce.writeUInt32LE(header.sourceId!, offset); + offset += 4; + } + + nonce.writeUInt32LE(header.sourceId!, offset); + offset += 4; + } else if (header.frameControlExt!.appId === ZigbeeNWKGPAppId.ZGP) { + nonce.writeBigUInt64LE(macSource64!, offset); + offset += 8; + } + + nonce.writeUInt32LE(header.securityFrameCounter!, offset); + offset += 4; + + if (header.frameControlExt!.appId === ZigbeeNWKGPAppId.ZGP && header.frameControlExt!.direction === ZigbeeNWKGPDirection.DIRECTION_FROM_ZGPD) { + // Security level = 0b101, Key Identifier = 0x00, Extended nonce = 0b0, Reserved = 0b00 + nonce.writeUInt8(0xc5, offset); + offset += 1; + } else { + // Security level = 0b101, Key Identifier = 0x00, Extended nonce = 0b0, Reserved = 0b11 + nonce.writeUInt8(0x05, offset); + offset += 1; + } + + return nonce; +} + +export function decodeZigbeeNWKGPPayload( + data: Buffer, + offset: number, + decryptKey: Buffer, + macSource64: bigint | undefined, + _frameControl: ZigbeeNWKGPFrameControl, + header: ZigbeeNWKGPHeader, +): ZigbeeNWKGPPayload { + const cryptedPayload = data.subarray(offset, offset + header.payloadLength); // no MIC + let decryptedPayload: ZigbeeNWKGPPayload | undefined; + + if (header.frameControlExt?.securityLevel === ZigbeeNWKGPSecurityLevel.FULLENCR) { + const nonce = makeGPNonce(header, macSource64); + [, decryptedPayload] = aes128CcmStar(header.micSize, decryptKey, nonce, cryptedPayload); + + // TODO mic/authTag? + } else { + decryptedPayload = cryptedPayload; + + // TODO mic/authTag? + } + + if (!decryptedPayload) { + throw new Error("Unable to decrypt Zigbee NWK GP payload"); + } + + return decryptedPayload; +} + +export function encodeZigbeeNWKGPFrame( + header: ZigbeeNWKGPHeader, + payload: ZigbeeNWKGPPayload, + decryptKey: Buffer, + macSource64: bigint | undefined, +): Buffer { + let offset = 0; + const data = Buffer.alloc(ZigbeeNWKGPConsts.FRAME_MAX_SIZE); + + offset = encodeZigbeeNWKGPHeader(data, offset, header); + + if (header.frameControlExt?.securityLevel === ZigbeeNWKGPSecurityLevel.FULLENCR) { + const nonce = makeGPNonce(header, macSource64); + const [, encryptedPayload] = aes128CcmStar(header.micSize, decryptKey, nonce, payload); + + // TODO mic/authTag? + + data.set(encryptedPayload, offset); + offset += encryptedPayload.byteLength; + } else { + data.set(payload, offset); + offset += payload.byteLength; + } + + if (header.micSize === 2) { + data.writeUInt16LE(header.mic!, offset); // at end + } else if (header.micSize === 4) { + data.writeUInt32LE(header.mic!, offset); // at end + } + + offset += header.micSize; + + return data.subarray(0, offset); +} diff --git a/src/zigbee/zigbee.ts b/src/zigbee/zigbee.ts new file mode 100644 index 0000000..bc71bc4 --- /dev/null +++ b/src/zigbee/zigbee.ts @@ -0,0 +1,510 @@ +import { createCipheriv } from "node:crypto"; + +/** + * const enum with sole purpose of avoiding "magic numbers" in code for well-known values + */ +export const enum ZigbeeConsts { + COORDINATOR_ADDRESS = 0x0000, + /** min reserved address for broacasts */ + BCAST_MIN = 0xfff8, + /** Low power routers only */ + BCAST_LOW_POWER_ROUTERS = 0xfffb, + /** All routers and coordinator */ + BCAST_DEFAULT = 0xfffc, + /** macRxOnWhenIdle = TRUE (all non-sleepy devices) */ + BCAST_RX_ON_WHEN_IDLE = 0xfffd, + /** All devices in PAN (including sleepy end devices) */ + BCAST_SLEEPY = 0xffff, + /** The maximum amount of time that the MAC will hold a message for indirect transmission to a child. (7.68sec for ZigBee Pro) */ + MAC_INDIRECT_TRANSMISSION_TIMEOUT = 7680, + + //---- HA + HA_ENDPOINT = 0x01, + HA_PROFILE_ID = 0x0104, + + //---- ZDO + ZDO_ENDPOINT = 0x00, + ZDO_PROFILE_ID = 0x0000, + NETWORK_ADDRESS_REQUEST = 0x0000, + IEEE_ADDRESS_REQUEST = 0x0001, + NODE_DESCRIPTOR_REQUEST = 0x0002, + POWER_DESCRIPTOR_REQUEST = 0x0003, + SIMPLE_DESCRIPTOR_REQUEST = 0x0004, + ACTIVE_ENDPOINTS_REQUEST = 0x0005, + END_DEVICE_ANNOUNCE = 0x0013, + + //---- Green Power + GP_ENDPOINT = 0xf2, + GP_PROFILE_ID = 0xa1e0, + GP_GROUP_ID = 0x0b84, + GP_CLUSTER_ID = 0x0021, + GP_COMMISSIONING_NOTIFICATION = 4, + GP_NOTIFICATION = 0, + + //---- Touchlink + TOUCHLINK_PROFILE_ID = 0xc05e, + + //---- ZigBee Security Constants + SEC_L = 2, + SEC_BLOCKSIZE = 16, + SEC_NONCE_LEN = 16 - 2 - 1, + SEC_KEYSIZE = 16, + + SEC_CONTROL_VERIFIED_FC = 0x40, + + //---- CCM* Flags + /** 3-bit encoding of (L-1) */ + SEC_CCM_FLAG_L = 0x01, + + SEC_IPAD = 0x36, + SEC_OPAD = 0x5c, + + //---- Bit masks for the Security Control Field + SEC_CONTROL_LEVEL = 0x07, + SEC_CONTROL_KEY = 0x18, + SEC_CONTROL_NONCE = 0x20, +} + +/* ZigBee security levels. */ +export const enum ZigbeeSecurityLevel { + NONE = 0x00, + MIC32 = 0x01, + MIC64 = 0x02, + MIC128 = 0x03, + ENC = 0x04, + /** ZigBee 3.0 */ + ENC_MIC32 = 0x05, + ENC_MIC64 = 0x06, + ENC_MIC128 = 0x07, +} + +/* ZigBee Key Types */ +export const enum ZigbeeKeyType { + LINK = 0x00, + NWK = 0x01, + TRANSPORT = 0x02, + LOAD = 0x03, +} + +export type ZigbeeSecurityControl = { + level: ZigbeeSecurityLevel; + keyId: ZigbeeKeyType; + nonce: boolean; +}; + +export type ZigbeeSecurityHeader = { + /** uint8_t (same as above) */ + control: ZigbeeSecurityControl; + /** uint32_t */ + frameCounter: number; + /** uint64_t */ + source64?: bigint; + /** uint8_t */ + keySeqNum?: number; + /** (utility, not part of the spec) */ + micLen?: 0 | 4 | 8 | 16; + /** uint32_t */ + // mic?: number; +}; + +function aes128MmoHashUpdate(result: Buffer, data: Buffer, dataSize: number): void { + while (dataSize >= ZigbeeConsts.SEC_BLOCKSIZE) { + const cipher = createCipheriv("aes-128-ecb", result, null); + const block = data.subarray(0, ZigbeeConsts.SEC_BLOCKSIZE); + const u = cipher.update(block); + const f = cipher.final(); + const encryptedBlock = Buffer.alloc(u.byteLength + f.byteLength); + + encryptedBlock.set(u, 0); + encryptedBlock.set(f, u.byteLength); + + // XOR encrypted and plaintext + for (let i = 0; i < ZigbeeConsts.SEC_BLOCKSIZE; i++) { + result[i] = encryptedBlock[i] ^ block[i]; + } + + data = data.subarray(ZigbeeConsts.SEC_BLOCKSIZE); + dataSize -= ZigbeeConsts.SEC_BLOCKSIZE; + } +} + +/** + * See B.1.3 Cryptographic Hash Function + * + * AES-128-MMO (Matyas-Meyer-Oseas) hashing (using node 'crypto' built-in with 'aes-128-ecb') + * + * Used for Install Codes - see Document 13-0402-13 - 10.1 + */ +export function aes128MmoHash(data: Buffer): Buffer { + const hashResult = Buffer.alloc(ZigbeeConsts.SEC_BLOCKSIZE); + let remainingLength = data.byteLength; + let position = 0; + + for (position; remainingLength >= ZigbeeConsts.SEC_BLOCKSIZE; ) { + const chunk = data.subarray(position, position + ZigbeeConsts.SEC_BLOCKSIZE); + + aes128MmoHashUpdate(hashResult, chunk, chunk.byteLength); + + position += ZigbeeConsts.SEC_BLOCKSIZE; + remainingLength -= ZigbeeConsts.SEC_BLOCKSIZE; + } + + const temp = Buffer.alloc(ZigbeeConsts.SEC_BLOCKSIZE); + + temp.set(data.subarray(position, position + remainingLength), 0); + + // per the spec, concatenate a 1 bit followed by all zero bits + temp[remainingLength] = 0x80; + + // if appending the bit string will push us beyond the 16-byte boundary, hash that block and append another 16-byte block + if (ZigbeeConsts.SEC_BLOCKSIZE - remainingLength < 3) { + aes128MmoHashUpdate(hashResult, temp, ZigbeeConsts.SEC_BLOCKSIZE); + temp.fill(0); + } + + temp[ZigbeeConsts.SEC_BLOCKSIZE - 2] = (data.byteLength >> 5) & 0xff; + temp[ZigbeeConsts.SEC_BLOCKSIZE - 1] = (data.byteLength << 3) & 0xff; + + aes128MmoHashUpdate(hashResult, temp, ZigbeeConsts.SEC_BLOCKSIZE); + + return hashResult.subarray(0, ZigbeeConsts.SEC_BLOCKSIZE); +} + +/** + * See A CCM* MODE OF OPERATION + * + * Used for Zigbee NWK layer encryption/decryption + */ +export function aes128CcmStar(M: 0 | 2 | 4 | 8 | 16, key: Buffer, nonce: Buffer, data: Buffer): [authTag: Buffer, ciphertext: Buffer] { + const payloadLengthNoM = data.byteLength - M; + const blockCount = 1 + Math.ceil(payloadLengthNoM / ZigbeeConsts.SEC_BLOCKSIZE); + const plaintext = Buffer.alloc(blockCount * ZigbeeConsts.SEC_BLOCKSIZE); + + plaintext.set(data.subarray(-M), 0); + plaintext.set(data.subarray(0, -M), ZigbeeConsts.SEC_BLOCKSIZE); + + const cipher = createCipheriv("aes-128-ecb", key, null); + const buffer = Buffer.alloc(blockCount * ZigbeeConsts.SEC_BLOCKSIZE); + const counter = Buffer.alloc(ZigbeeConsts.SEC_BLOCKSIZE); + counter[0] = ZigbeeConsts.SEC_CCM_FLAG_L; + + counter.set(nonce, 1); + + for (let blockNum = 0; blockNum < blockCount; blockNum++) { + // big endian of size ZigbeeConsts.SEC_L + counter[counter.byteLength - 2] = (blockNum >> 8) & 0xff; + counter[counter.byteLength - 1] = blockNum & 0xff; + const plaintextBlock = plaintext.subarray(ZigbeeConsts.SEC_BLOCKSIZE * blockNum, ZigbeeConsts.SEC_BLOCKSIZE * (blockNum + 1)); + const cipherU = cipher.update(counter); + + // XOR cipher and plaintext + for (let i = 0; i < cipherU.byteLength; i++) { + cipherU[i] ^= plaintextBlock[i]; + } + + buffer.set(cipherU, ZigbeeConsts.SEC_BLOCKSIZE * blockNum); + } + + cipher.final(); + const authTag = buffer.subarray(0, M); + const ciphertext = buffer.subarray(ZigbeeConsts.SEC_BLOCKSIZE, ZigbeeConsts.SEC_BLOCKSIZE + payloadLengthNoM); + + return [authTag, ciphertext]; +} + +/** + * aes-128-cbc with iv as 0-filled block size + * + * Used for Zigbee NWK layer encryption/decryption + */ +export function computeAuthTag(authData: Buffer, M: number, key: Buffer, nonce: Buffer, data: Buffer): Buffer { + const startPaddedSize = Math.ceil( + (1 + nonce.byteLength + ZigbeeConsts.SEC_L + ZigbeeConsts.SEC_L + authData.byteLength) / ZigbeeConsts.SEC_BLOCKSIZE, + ); + const endPaddedSize = Math.ceil(data.byteLength / ZigbeeConsts.SEC_BLOCKSIZE); + const prependAuthData = Buffer.alloc(startPaddedSize * ZigbeeConsts.SEC_BLOCKSIZE + endPaddedSize * ZigbeeConsts.SEC_BLOCKSIZE); + let offset = 0; + prependAuthData[offset] = ((((M - 2) / 2) & 0x7) << 3) | (authData.byteLength > 0 ? 0x40 : 0x00) | ZigbeeConsts.SEC_CCM_FLAG_L; + offset += 1; + + prependAuthData.set(nonce, offset); + offset += nonce.byteLength; + + // big endian of size ZigbeeConsts.SEC_L + prependAuthData[offset] = (data.byteLength >> 8) & 0xff; + prependAuthData[offset + 1] = data.byteLength & 0xff; + offset += 2; + + const prepend = authData.byteLength; + // big endian of size ZigbeeConsts.SEC_L + prependAuthData[offset] = (prepend >> 8) & 0xff; + prependAuthData[offset + 1] = prepend & 0xff; + offset += 2; + + prependAuthData.set(authData, offset); + offset += authData.byteLength; + + const dataOffset = Math.ceil(offset / ZigbeeConsts.SEC_BLOCKSIZE) * ZigbeeConsts.SEC_BLOCKSIZE; + prependAuthData.set(data, dataOffset); + + const cipher = createCipheriv("aes-128-cbc", key, Buffer.alloc(ZigbeeConsts.SEC_BLOCKSIZE, 0)); + const cipherU = cipher.update(prependAuthData); + + cipher.final(); + + const authTag = cipherU.subarray(-ZigbeeConsts.SEC_BLOCKSIZE, -ZigbeeConsts.SEC_BLOCKSIZE + M); + + return authTag; +} + +export function combineSecurityControl(control: ZigbeeSecurityControl, levelOverride?: number): number { + return ( + ((levelOverride !== undefined ? levelOverride : control.level) & ZigbeeConsts.SEC_CONTROL_LEVEL) | + ((control.keyId << 3) & ZigbeeConsts.SEC_CONTROL_KEY) | + (((control.nonce ? 1 : 0) << 5) & ZigbeeConsts.SEC_CONTROL_NONCE) + ); +} + +export function makeNonce(header: ZigbeeSecurityHeader, source64: bigint, levelOverride?: number): Buffer { + const nonce = Buffer.alloc(ZigbeeConsts.SEC_NONCE_LEN); + + // TODO: write source64 as all 0/F if undefined? + nonce.writeBigUInt64LE(source64, 0); + nonce.writeUInt32LE(header.frameCounter, 8); + nonce.writeUInt8(combineSecurityControl(header.control, levelOverride), 12); + + return nonce; +} + +/** + * In order: + * ZigbeeKeyType.LINK, ZigbeeKeyType.NWK, ZigbeeKeyType.TRANSPORT, ZigbeeKeyType.LOAD + */ +const defaultHashedKeys: [Buffer, Buffer, Buffer, Buffer] = [Buffer.alloc(0), Buffer.alloc(0), Buffer.alloc(0), Buffer.alloc(0)]; + +/** + * Pre-hashing default keys makes decryptions ~5x faster + */ +export function registerDefaultHashedKeys(link: Buffer, nwk: Buffer, transport: Buffer, load: Buffer): void { + defaultHashedKeys[0] = link; + defaultHashedKeys[1] = nwk; + defaultHashedKeys[2] = transport; + defaultHashedKeys[3] = load; +} + +/** + * See B.1.4 Keyed Hash Function for Message Authentication + * + * @param key ZigBee Security Key (must be ZigbeeConsts.SEC_KEYSIZE) in length. + * @param inputByte Input byte + */ +export function makeKeyedHash(key: Buffer, inputByte: number): Buffer { + const hashOut = Buffer.alloc(ZigbeeConsts.SEC_BLOCKSIZE + 1); + const hashIn = Buffer.alloc(2 * ZigbeeConsts.SEC_BLOCKSIZE); + + for (let i = 0; i < ZigbeeConsts.SEC_KEYSIZE; i++) { + // copy the key into hashIn and XOR with opad to form: (Key XOR opad) + hashIn[i] = key[i] ^ ZigbeeConsts.SEC_OPAD; + // copy the Key into hashOut and XOR with ipad to form: (Key XOR ipad) + hashOut[i] = key[i] ^ ZigbeeConsts.SEC_IPAD; + } + + // append the input byte to form: (Key XOR ipad) || text. + hashOut[ZigbeeConsts.SEC_BLOCKSIZE] = inputByte; + // hash the contents of hashOut and append the contents to hashIn to form: (Key XOR opad) || H((Key XOR ipad) || text) + hashIn.set(aes128MmoHash(hashOut), ZigbeeConsts.SEC_BLOCKSIZE); + // hash the contents of hashIn to get the final result + hashOut.set(aes128MmoHash(hashIn), 0); + + return hashOut.subarray(0, ZigbeeConsts.SEC_BLOCKSIZE); +} + +/** Hash key if needed, else return `key` as is */ +export function makeKeyedHashByType(keyId: ZigbeeKeyType, key: Buffer): Buffer { + switch (keyId) { + case ZigbeeKeyType.NWK: + case ZigbeeKeyType.LINK: { + // NWK: decrypt with the PAN's current network key + // LINK: decrypt with the unhashed link key assigned by the trust center to this source/destination pair + return key; + } + case ZigbeeKeyType.TRANSPORT: { + // decrypt with a Transport key, a hashed link key that protects network keys sent from the trust center + return makeKeyedHash(key, 0x00); + } + case ZigbeeKeyType.LOAD: { + // decrypt with a Load key, a hashed link key that protects link keys sent from the trust center + return makeKeyedHash(key, 0x02); + } + default: { + throw new Error(`Unsupported key ID ${keyId}`); + } + } +} + +export function decodeZigbeeSecurityHeader(data: Buffer, offset: number, source64?: bigint): [ZigbeeSecurityHeader, offset: number] { + const control = data.readUInt8(offset); + offset += 1; + const level = ZigbeeSecurityLevel.ENC_MIC32; // overrides control & ZigbeeConsts.SEC_CONTROL_LEVEL; + const keyId = (control & ZigbeeConsts.SEC_CONTROL_KEY) >> 3; + const nonce = Boolean((control & ZigbeeConsts.SEC_CONTROL_NONCE) >> 5); + + const frameCounter = data.readUInt32LE(offset); + offset += 4; + + if (nonce) { + source64 = data.readBigUInt64LE(offset); + offset += 8; + } + + let keySeqNum: number | undefined; + + if (keyId === ZigbeeKeyType.NWK) { + keySeqNum = data.readUInt8(offset); + offset += 1; + } + + const micLen = 4; + // NOTE: Security level for Zigbee 3.0 === 5 + // let micLen: number; + + // switch (level) { + // case ZigbeeSecurityLevel.ENC: + // case ZigbeeSecurityLevel.NONE: + // default: + // micLen = 0; + // break; + + // case ZigbeeSecurityLevel.ENC_MIC32: + // case ZigbeeSecurityLevel.MIC32: + // micLen = 4; + // break; + + // case ZigbeeSecurityLevel.ENC_MIC64: + // case ZigbeeSecurityLevel.MIC64: + // micLen = 8; + // break; + + // case ZigbeeSecurityLevel.ENC_MIC128: + // case ZigbeeSecurityLevel.MIC128: + // micLen = 16; + // break; + // } + + return [ + { + control: { + level, + keyId, + nonce, + }, + frameCounter, + source64, + keySeqNum, + micLen, + }, + offset, + ]; +} + +export function encodeZigbeeSecurityHeader(data: Buffer, offset: number, header: ZigbeeSecurityHeader): number { + data.writeUInt8(combineSecurityControl(header.control), offset); + offset += 1; + + data.writeUInt32LE(header.frameCounter, offset); + offset += 4; + + if (header.control.nonce) { + data.writeBigUInt64LE(header.source64!, offset); + offset += 8; + } + + if (header.control.keyId === ZigbeeKeyType.NWK) { + data.writeUInt8(header.keySeqNum!, offset); + offset += 1; + } + + return offset; +} + +export function decryptZigbeePayload( + data: Buffer, + offset: number, + key?: Buffer, + source64?: bigint, +): [Buffer, header: ZigbeeSecurityHeader, offset: number] { + const controlOffset = offset; + const [header, hOutOffset] = decodeZigbeeSecurityHeader(data, offset, source64); + + let authTag: Buffer | undefined; + let decryptedPayload: Buffer | undefined; + + if (header.source64 !== undefined) { + const hashedKey = key ? makeKeyedHashByType(header.control.keyId, key) : defaultHashedKeys[header.control.keyId]; + const nonce = makeNonce(header, header.source64); + const encryptedData = data.subarray(hOutOffset); // payload + auth tag + + [authTag, decryptedPayload] = aes128CcmStar(header.micLen!, hashedKey, nonce, encryptedData); + + // take until end of securityHeader for auth tag computation + const adjustedAuthData = data.subarray(0, hOutOffset); + // patch the security level to ZigBee 3.0 + const origControl = adjustedAuthData[controlOffset]; + adjustedAuthData[controlOffset] &= ~ZigbeeConsts.SEC_CONTROL_LEVEL; + adjustedAuthData[controlOffset] |= ZigbeeConsts.SEC_CONTROL_LEVEL & ZigbeeSecurityLevel.ENC_MIC32; + + const computedAuthTag = computeAuthTag(adjustedAuthData, header.micLen!, hashedKey, nonce, decryptedPayload); + // restore security level + adjustedAuthData[controlOffset] = origControl; + + if (!computedAuthTag.equals(authTag)) { + throw new Error("Auth tag mismatch while decrypting Zigbee payload"); + } + } + + if (!decryptedPayload) { + throw new Error("Unable to decrypt Zigbee payload"); + } + + return [decryptedPayload, header, hOutOffset]; +} + +export function encryptZigbeePayload( + data: Buffer, + offset: number, + payload: Buffer, + header: ZigbeeSecurityHeader, + key?: Buffer, +): [Buffer, authTag: Buffer, offset: number] { + const controlOffset = offset; + offset = encodeZigbeeSecurityHeader(data, offset, header); + + let authTag: Buffer | undefined; + let encryptedPayload: Buffer | undefined; + + if (header.source64 !== undefined) { + const hashedKey = key ? makeKeyedHashByType(header.control.keyId, key) : defaultHashedKeys[header.control.keyId]; + const nonce = makeNonce(header, header.source64, ZigbeeSecurityLevel.ENC_MIC32); + const adjustedAuthData = data.subarray(0, offset); + // patch the security level to ZigBee 3.0 + const origControl = adjustedAuthData[controlOffset]; + adjustedAuthData[controlOffset] &= ~ZigbeeConsts.SEC_CONTROL_LEVEL; + adjustedAuthData[controlOffset] |= ZigbeeConsts.SEC_CONTROL_LEVEL & ZigbeeSecurityLevel.ENC_MIC32; + + const decryptedData = Buffer.alloc(payload.byteLength + header.micLen!); // payload + auth tag + decryptedData.set(payload, 0); + // take nwkHeader + securityHeader for auth tag computation + const computedAuthTag = computeAuthTag(adjustedAuthData, header.micLen!, hashedKey, nonce, payload); + decryptedData.set(computedAuthTag, payload.byteLength); + + // restore security level + adjustedAuthData[controlOffset] = origControl; + [authTag, encryptedPayload] = aes128CcmStar(header.micLen!, hashedKey, nonce, decryptedData); + } + + if (!encryptedPayload || !authTag) { + throw new Error("Unable to encrypt Zigbee payload"); + } + + return [encryptedPayload, authTag, offset]; +} diff --git a/test/data.ts b/test/data.ts new file mode 100644 index 0000000..1ec2174 --- /dev/null +++ b/test/data.ts @@ -0,0 +1,1486 @@ +import { ZigbeeKeyType, makeKeyedHash, makeKeyedHashByType } from "../src/zigbee/zigbee"; + +export const A_CHANNEL = 15; +export const A_PAN_ID = 0xcd12; +export const A_EXTENDED_PAN_ID = Buffer.from([0xff, 0xee, 0xdd, 0xcc, 0x44, 0x33, 0x22, 0x11]); +export const A_EUI64 = Buffer.from([0xef, 0xac, 0x23, 0x45, 0xbb, 0xff, 0x86, 0x99]); + +//---- Frames below are from sniffs of actual ZigBee networks (with various coordinators, and dates varying from 2022 to now) with details from Wireshark +//---- NOTE: FCS is using TI format in most cases which is not valid against proper IEEE 802.15.4 CRC (i.e. do not compare last two bytes) + +// #region NETDEF + +/** ZigBeeAlliance09 */ +export const NETDEF_TC_KEY = Buffer.from([0x5a, 0x69, 0x67, 0x42, 0x65, 0x65, 0x41, 0x6c, 0x6c, 0x69, 0x61, 0x6e, 0x63, 0x65, 0x30, 0x39]); +/** Default Zigbee2MQTT PAN ID */ +export const NETDEF_PAN_ID = 0x1a62; +/** Default Zigbee2MQTT extended PAN ID */ +export const NETDEF_EXTENDED_PAN_ID = Buffer.from([0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd]); +/** Default Zigbee2MQTT network key */ +export const NETDEF_NETWORK_KEY = Buffer.from([0x01, 0x03, 0x05, 0x07, 0x09, 0x0b, 0x0d, 0x0f, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0d]); + +/** + * IEEE 802.15.4 Data, Src: 0x96ba, Dst: 0x0000 + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 191 + * Destination PAN: 0x1a62 + * Destination: 0x0000 + * Source: 0x96ba + * [Extended Source: SiliconLabor_ff:fe:a4:b9:73 (80:4b:50:ff:fe:a4:b9:73)] + * [Origin: 1] + * TI CC24xx-format metadata: FCS OK + * FCS Valid: True + * RSSI: -85 dB + * LQI Correlation Value: 52 + * + * ZigBee Network Layer Data, Dst: 0x0000, Src: 0x96ba + * Frame Control Field: 0x0248, Frame Type: Data, Discover Route: Enable, Security Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 01.. .... = Discover Route: Enable (0x1) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0x0000 + * Source: 0x96ba + * Radius: 30 + * Sequence Number: 151 + * [Extended Source: SiliconLabor_ff:fe:a4:b9:73 (80:4b:50:ff:fe:a4:b9:73)] + * [Origin: 1] + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 45318893 + * Extended Source: SiliconLabor_ff:fe:a4:b9:73 (80:4b:50:ff:fe:a4:b9:73) + * Key Sequence Number: 0 + * Message Integrity Code: 74295ed5 + * + * ZigBee Application Support Layer Ack, Dst Endpt: 1, Src Endpt: 1 + * Frame Control Field: Ack (0x02) + * .... ..10 = Frame Type: Ack (0x2) + * .... 00.. = Delivery Mode: Unicast (0x0) + * ...0 .... = Acknowledgement Format: False + * ..0. .... = Security: False + * .0.. .... = Acknowledgement Request: False + * 0... .... = Extended Header: False + * Destination Endpoint: 1 + * Cluster: Unknown (0xef00) + * Profile: Home Automation (0x0104) + * Source Endpoint: 1 + * Counter: 51 + */ +export const NETDEF_ACK_FRAME_TO_COORD = Buffer.from([ + 0x61, 0x88, 0xbf, 0x62, 0x1a, 0x0, 0x0, 0xba, 0x96, 0x48, 0x2, 0x0, 0x0, 0xba, 0x96, 0x1e, 0x97, 0x28, 0xed, 0x82, 0xb3, 0x2, 0x73, 0xb9, 0xa4, + 0xfe, 0xff, 0x50, 0x4b, 0x80, 0x0, 0x24, 0x90, 0x91, 0xd5, 0x9c, 0xff, 0x6, 0xda, 0x74, 0x29, 0x5e, 0xd5, 0xab, 0xb4, +]); + +/** + * IEEE 802.15.4 Data, Src: 0x0000, Dst: 0x87c6 + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 73 + * Destination PAN: 0x1a62 + * Destination: 0x87c6 + * Source: 0x0000 + * [Extended Source: SiliconLabor_ff:fe:77:be:10 (e0:79:8d:ff:fe:77:be:10)] + * [Origin: 3] + * TI CC24xx-format metadata: FCS OK + * FCS Valid: True + * RSSI: -53 dB + * LQI Correlation Value: 116 + * + * ZigBee Network Layer Data, Dst: 0x96ba, Src: 0x0000 + * Frame Control Field: 0x0248, Frame Type: Data, Discover Route: Enable, Security Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 01.. .... = Discover Route: Enable (0x1) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0x96ba + * Source: 0x0000 + * Radius: 30 + * Sequence Number: 203 + * [Extended Source: SiliconLabor_ff:fe:77:be:10 (e0:79:8d:ff:fe:77:be:10)] + * [Origin: 3] + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 99044312 + * Extended Source: SiliconLabor_ff:fe:77:be:10 (e0:79:8d:ff:fe:77:be:10) + * Key Sequence Number: 0 + * Message Integrity Code: 55e1234c + * + * ZigBee Application Support Layer Ack, Dst Endpt: 1, Src Endpt: 1 + * Frame Control Field: Ack (0x02) + * .... ..10 = Frame Type: Ack (0x2) + * .... 00.. = Delivery Mode: Unicast (0x0) + * ...0 .... = Acknowledgement Format: False + * ..0. .... = Security: False + * .0.. .... = Acknowledgement Request: False + * 0... .... = Extended Header: False + * Destination Endpoint: 1 + * Cluster: Unknown (0xef00) + * Profile: Home Automation (0x0104) + * Source Endpoint: 1 + * Counter: 77 + **/ +export const NETDEF_ACK_FRAME_FROM_COORD = Buffer.from([ + 0x61, 0x88, 0x49, 0x62, 0x1a, 0xc6, 0x87, 0x0, 0x0, 0x48, 0x2, 0xba, 0x96, 0x0, 0x0, 0x1e, 0xcb, 0x28, 0xd8, 0x4b, 0xe7, 0x5, 0x10, 0xbe, 0x77, + 0xfe, 0xff, 0x8d, 0x79, 0xe0, 0x0, 0x1b, 0xf0, 0x72, 0xc2, 0xbe, 0xf1, 0xb, 0xd9, 0x55, 0xe1, 0x23, 0x4c, 0xcb, 0xf4, +]); + +/** + * IEEE 802.15.4 Data, Src: 0xf0a2, Dst: Broadcast + * Frame Control Field: 0x8841, Frame Type: Data, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..0. .... = Acknowledge Request: False + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 92 + * Destination PAN: 0x1a62 + * Destination: 0xffff + * Source: 0xf0a2 + * [Extended Source: TexasInstrum_00:24:c3:4d:a0 (00:12:4b:00:24:c3:4d:a0)] + * [Origin: 19] + * TI CC24xx-format metadata: FCS OK + * FCS Valid: True + * RSSI: -74 dB + * LQI Correlation Value: 74 + * + * ZigBee Network Layer Command, Dst: Broadcast, Src: 0xf0a2 + * Frame Control Field: 0x1209, Frame Type: Command, Discover Route: Suppress, Security, Extended Source Command + * .... .... .... ..01 = Frame Type: Command (0x1) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 00.. .... = Discover Route: Suppress (0x0) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...1 .... .... .... = Extended Source: True + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0xfffc + * Source: 0xf0a2 + * Radius: 1 + * Sequence Number: 223 + * Extended Source: TexasInstrum_00:24:c3:4d:a0 (00:12:4b:00:24:c3:4d:a0) + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 5505754 + * Extended Source: TexasInstrum_00:24:c3:4d:a0 (00:12:4b:00:24:c3:4d:a0) + * Key Sequence Number: 0 + * Message Integrity Code: b74632de + * Command Frame: Link Status + * Command Identifier: Link Status (0x08) + * .1.. .... = Last Frame: True + * ..1. .... = First Frame: True + * ...1 0001 = Link Status Count: 17 + * Link 1 + * Address: 0x0000 + * .... .001 = Incoming Cost: 1 + * .001 .... = Outgoing Cost: 1 + * Link 2 + * Address: 0x0b7c + * .... .111 = Incoming Cost: 7 + * .111 .... = Outgoing Cost: 7 + * Link 3 + * Address: 0x16ca + * .... .001 = Incoming Cost: 1 + * .001 .... = Outgoing Cost: 1 + * Link 4 + * Address: 0x2020 + * .... .001 = Incoming Cost: 1 + * .000 .... = Outgoing Cost: 0 + * Link 5 + * Address: 0x2303 + * .... .111 = Incoming Cost: 7 + * .111 .... = Outgoing Cost: 7 + * Link 6 + * Address: 0x5e74 + * .... .001 = Incoming Cost: 1 + * .001 .... = Outgoing Cost: 1 + * Link 7 + * Address: 0x65b1 + * .... .001 = Incoming Cost: 1 + * .001 .... = Outgoing Cost: 1 + * Link 8 + * Address: 0x67b4 + * .... .001 = Incoming Cost: 1 + * .001 .... = Outgoing Cost: 1 + * Link 9 + * Address: 0x7326 + * .... .111 = Incoming Cost: 7 + * .111 .... = Outgoing Cost: 7 + * Link 10 + * Address: 0x87c6 + * .... .001 = Incoming Cost: 1 + * .011 .... = Outgoing Cost: 3 + * Link 11 + * Address: 0x8c4f + * .... .111 = Incoming Cost: 7 + * .111 .... = Outgoing Cost: 7 + * Link 12 + * Address: 0x96ba + * .... .001 = Incoming Cost: 1 + * .001 .... = Outgoing Cost: 1 + * Link 13 + * Address: 0xaa38 + * .... .001 = Incoming Cost: 1 + * .001 .... = Outgoing Cost: 1 + * Link 14 + * Address: 0xc8cd + * .... .001 = Incoming Cost: 1 + * .001 .... = Outgoing Cost: 1 + * Link 15 + * Address: 0xd054 + * .... .001 = Incoming Cost: 1 + * .001 .... = Outgoing Cost: 1 + * Link 16 + * Address: 0xf1f0 + * .... .001 = Incoming Cost: 1 + * .001 .... = Outgoing Cost: 1 + * Link 17 + * Address: 0xfd3d + * .... .001 = Incoming Cost: 1 + * .001 .... = Outgoing Cost: 1 + */ +export const NETDEF_LINK_STATUS_FROM_DEV = Buffer.from([ + 0x41, 0x88, 0x5c, 0x62, 0x1a, 0xff, 0xff, 0xa2, 0xf0, 0x9, 0x12, 0xfc, 0xff, 0xa2, 0xf0, 0x1, 0xdf, 0xa0, 0x4d, 0xc3, 0x24, 0x0, 0x4b, 0x12, 0x0, + 0x28, 0xda, 0x2, 0x54, 0x0, 0xa0, 0x4d, 0xc3, 0x24, 0x0, 0x4b, 0x12, 0x0, 0x0, 0x57, 0xf8, 0x6e, 0x1e, 0x50, 0xc8, 0xe9, 0xc6, 0x0, 0x7c, 0xd2, + 0x3a, 0xcd, 0x3c, 0x4c, 0x3f, 0xf4, 0xc4, 0xb7, 0xfa, 0xf8, 0xe, 0x46, 0xb8, 0x54, 0x45, 0xbb, 0x4c, 0x60, 0x91, 0x10, 0xd9, 0xf7, 0x4c, 0xed, + 0x18, 0x24, 0xa0, 0x68, 0xf6, 0xb, 0xe6, 0xa6, 0x1d, 0x33, 0xe, 0x98, 0xc5, 0xb3, 0xc2, 0xab, 0x72, 0xe7, 0xb7, 0x46, 0x32, 0xde, 0xb6, 0xca, +]); + +/** + * IEEE 802.15.4 Data, Src: 0xaa38, Dst: 0x0000 + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 230 + * Destination PAN: 0x1a62 + * Destination: 0x0000 + * Source: 0xaa38 + * [Extended Source: SiliconLabor_ff:fe:d0:4a:58 (70:ac:08:ff:fe:d0:4a:58)] + * [Origin: 22] + * TI CC24xx-format metadata: FCS OK + * FCS Valid: True + * RSSI: -77 dB + * LQI Correlation Value: 68 + * + * ZigBee Network Layer Data, Dst: 0x0000, Src: 0xaa38 + * Frame Control Field: 0x0248, Frame Type: Data, Discover Route: Enable, Security Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 01.. .... = Discover Route: Enable (0x1) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0x0000 + * Source: 0xaa38 + * Radius: 30 + * Sequence Number: 128 + * [Extended Source: SiliconLabor_ff:fe:d0:4a:58 (70:ac:08:ff:fe:d0:4a:58)] + * [Origin: 22] + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 43659054 + * Extended Source: SiliconLabor_ff:fe:d0:4a:58 (70:ac:08:ff:fe:d0:4a:58) + * Key Sequence Number: 0 + * Message Integrity Code: 88ef5e6d + * + * ZigBee Application Support Layer Data, Dst Endpt: 1, Src Endpt: 1 + * Frame Control Field: Data (0x00) + * .... ..00 = Frame Type: Data (0x0) + * .... 00.. = Delivery Mode: Unicast (0x0) + * ..0. .... = Security: False + * .0.. .... = Acknowledgement Request: False + * 0... .... = Extended Header: False + * Destination Endpoint: 1 + * Cluster: Unknown (0xef00) + * Profile: Home Automation (0x0104) + * Source Endpoint: 1 + * Counter: 63 + * + * ZigBee Cluster Library Frame + * Frame Control Field: Cluster-specific (0x09) + * .... ..01 = Frame Type: Cluster-specific (0x1) + * .... .0.. = Manufacturer Specific: False + * .... 1... = Direction: Server to Client + * ...0 .... = Disable Default Response: False + * Sequence Number: 80 + * Command: Unknown (0x25) + */ +export const NETDEF_ZCL_FRAME_CMD_TO_COORD = Buffer.from([ + 0x61, 0x88, 0xe6, 0x62, 0x1a, 0x0, 0x0, 0x38, 0xaa, 0x48, 0x2, 0x0, 0x0, 0x38, 0xaa, 0x1e, 0x80, 0x28, 0x2e, 0x2f, 0x9a, 0x2, 0x58, 0x4a, 0xd0, + 0xfe, 0xff, 0x8, 0xac, 0x70, 0x0, 0x51, 0x52, 0x87, 0x1, 0x52, 0x10, 0xa7, 0x4f, 0xb7, 0x34, 0xf1, 0xd9, 0xc8, 0x88, 0xef, 0x5e, 0x6d, 0xb3, 0xc4, +]); + +/** + * IEEE 802.15.4 Data, Src: 0xaa38, Dst: 0x0000 + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 231 + * Destination PAN: 0x1a62 + * Destination: 0x0000 + * Source: 0xaa38 + * [Extended Source: SiliconLabor_ff:fe:d0:4a:58 (70:ac:08:ff:fe:d0:4a:58)] + * [Origin: 22] + * TI CC24xx-format metadata: FCS OK + * FCS Valid: True + * RSSI: -77 dB + * LQI Correlation Value: 68 + * + * ZigBee Network Layer Data, Dst: 0x0000, Src: 0xaa38 + * Frame Control Field: 0x0248, Frame Type: Data, Discover Route: Enable, Security Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 01.. .... = Discover Route: Enable (0x1) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0x0000 + * Source: 0xaa38 + * Radius: 30 + * Sequence Number: 130 + * [Extended Source: SiliconLabor_ff:fe:d0:4a:58 (70:ac:08:ff:fe:d0:4a:58)] + * [Origin: 22] + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 43659055 + * Extended Source: SiliconLabor_ff:fe:d0:4a:58 (70:ac:08:ff:fe:d0:4a:58) + * Key Sequence Number: 0 + * Message Integrity Code: 3674143b + * [Key: 01030507090b0d0f00020406080a0c0d] + * [Key Label: Z2MDefault] + * + * ZigBee Application Support Layer Data, Dst Endpt: 1, Src Endpt: 1 + * Frame Control Field: Data (0x40) + * .... ..00 = Frame Type: Data (0x0) + * .... 00.. = Delivery Mode: Unicast (0x0) + * ..0. .... = Security: False + * .1.. .... = Acknowledgement Request: True + * 0... .... = Extended Header: False + * Destination Endpoint: 1 + * Cluster: Unknown (0xef00) + * Profile: Home Automation (0x0104) + * Source Endpoint: 1 + * Counter: 64 + * + * ZigBee Cluster Library Frame, Command: Default Response, Seq: 50 + * Frame Control Field: Profile-wide (0x08) + * .... ..00 = Frame Type: Profile-wide (0x0) + * .... .0.. = Manufacturer Specific: False + * .... 1... = Direction: Server to Client + * ...0 .... = Disable Default Response: False + * Sequence Number: 50 + * Command: Default Response (0x0b) + * Response to Command: 0x25 + * Status: Success (0x00) + */ +export const NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD = Buffer.from([ + 0x61, 0x88, 0xe7, 0x62, 0x1a, 0x0, 0x0, 0x38, 0xaa, 0x48, 0x2, 0x0, 0x0, 0x38, 0xaa, 0x1e, 0x82, 0x28, 0x2f, 0x2f, 0x9a, 0x2, 0x58, 0x4a, 0xd0, + 0xfe, 0xff, 0x8, 0xac, 0x70, 0x0, 0x47, 0x3b, 0x70, 0x51, 0x48, 0x1a, 0xfd, 0xd1, 0x6a, 0x37, 0xaf, 0x59, 0xe9, 0x36, 0x74, 0x14, 0x3b, 0xb3, + 0xc4, +]); + +/** + * IEEE 802.15.4 Data, Src: 0xf1f0, Dst: 0x0000 + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 155 + * Destination PAN: 0x1a62 + * Destination: 0x0000 + * Source: 0xf1f0 + * [Extended Source: TexasInstrum_00:24:c0:41:13 (00:12:4b:00:24:c0:41:13)] + * [Origin: 112] + * TI CC24xx-format metadata: FCS OK + * FCS Valid: True + * RSSI: -97 dB + * LQI Correlation Value: 28 + * + * ZigBee Network Layer Command, Dst: 0x0000, Src: 0xac3a + * Frame Control Field: 0x1209, Frame Type: Command, Discover Route: Suppress, Security, Extended Source Command + * .... .... .... ..01 = Frame Type: Command (0x1) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 00.. .... = Discover Route: Suppress (0x0) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...1 .... .... .... = Extended Source: True + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0x0000 + * Source: 0xac3a + * Radius: 30 + * Sequence Number: 207 + * Extended Source: TexasInstrum_00:25:49:f4:42 (00:12:4b:00:25:49:f4:42) + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 6240313 + * Extended Source: TexasInstrum_00:24:c0:41:13 (00:12:4b:00:24:c0:41:13) + * Key Sequence Number: 0 + * Message Integrity Code: f406c868 + * Command Frame: Route Record + * Command Identifier: Route Record (0x05) + * Relay Count: 1 + * Relay Device 1: 0xf1f0 + */ +export const NETDEF_ROUTE_RECORD_TO_COORD = Buffer.from([ + 0x61, 0x88, 0x9b, 0x62, 0x1a, 0x0, 0x0, 0xf0, 0xf1, 0x9, 0x12, 0x0, 0x0, 0x3a, 0xac, 0x1e, 0xcf, 0x42, 0xf4, 0x49, 0x25, 0x0, 0x4b, 0x12, 0x0, + 0x28, 0x39, 0x38, 0x5f, 0x0, 0x13, 0x41, 0xc0, 0x24, 0x0, 0x4b, 0x12, 0x0, 0x0, 0xbd, 0x81, 0xd8, 0xd1, 0xf4, 0x6, 0xc8, 0x68, 0x9f, 0x9c, +]); + +/** + * IEEE 802.15.4 Data, Src: 0x0000, Dst: Broadcast + * Frame Control Field: 0x8841, Frame Type: Data, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..0. .... = Acknowledge Request: False + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 93 + * Destination PAN: 0x1a62 + * Destination: 0xffff + * Source: 0x0000 + * [Extended Source: SiliconLabor_ff:fe:77:be:10 (e0:79:8d:ff:fe:77:be:10)] + * [Origin: 3] + * TI CC24xx-format metadata: FCS OK + * FCS Valid: True + * RSSI: -53 dB + * LQI Correlation Value: 116 + * + * ZigBee Network Layer Command, Dst: Broadcast, Src: 0x0000 + * Frame Control Field: 0x1209, Frame Type: Command, Discover Route: Suppress, Security, Extended Source Command + * .... .... .... ..01 = Frame Type: Command (0x1) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 00.. .... = Discover Route: Suppress (0x0) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...1 .... .... .... = Extended Source: True + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0xfffc + * Source: 0x0000 + * Radius: 30 + * Sequence Number: 237 + * Extended Source: SiliconLabor_ff:fe:77:be:10 (e0:79:8d:ff:fe:77:be:10) + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 99044332 + * Extended Source: SiliconLabor_ff:fe:77:be:10 (e0:79:8d:ff:fe:77:be:10) + * Key Sequence Number: 0 + * Message Integrity Code: 05f16ea7 + * Command Frame: Route Request + * Command Identifier: Route Request (0x01) + * Command Options: 0x08, Many-to-One Discovery: With Source Routing + * Route ID: 45 + * Destination: 0xfffc + * Path Cost: 0 + */ +export const NETDEF_MTORR_FRAME_FROM_COORD = Buffer.from([ + 0x41, 0x88, 0x5d, 0x62, 0x1a, 0xff, 0xff, 0x0, 0x0, 0x9, 0x12, 0xfc, 0xff, 0x0, 0x0, 0x1e, 0xed, 0x10, 0xbe, 0x77, 0xfe, 0xff, 0x8d, 0x79, 0xe0, + 0x28, 0xec, 0x4b, 0xe7, 0x5, 0x10, 0xbe, 0x77, 0xfe, 0xff, 0x8d, 0x79, 0xe0, 0x0, 0x1e, 0x53, 0x91, 0xe2, 0x77, 0x31, 0x5, 0xf1, 0x6e, 0xa7, 0xcb, + 0xf4, +]); + +/** + * IEEE 802.15.4 Data, Dst: Broadcast + * Frame Control Field: 0x0801, Frame Type: Data, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: None + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..0. .... = Acknowledge Request: False + * .... .... .0.. .... = PAN ID Compression: False + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 00.. .... .... .... = Source Addressing Mode: None (0x0) + * Sequence Number: 185 + * Destination PAN: 0xffff + * Destination: 0xffff + * + * ZGP stub NWK header Data, GPD Src ID: 0x01719697 + * Frame Control Field: 0x8c, Frame Type: Data, NWK Frame Extension Data + * .... ..00 = Frame Type: Data (0x0) + * ..00 11.. = Protocol Version: 3 + * .0.. .... = Auto Commissioning: False + * 1... .... = NWK Frame Extension: True + * Extended NWK Frame Control Field: 0x30, Application ID: Unknown, Security Level: Full frame counter and full MIC only, Security Key, Direction: From ZGPD + * .... .000 = Application ID: Unknown (0x0) + * ...1 0... = Security Level: Full frame counter and full MIC only (0x2) + * ..1. .... = Security Key: True + * .0.. .... = Rx After Tx: False + * 0... .... = Direction: From ZGPD (0x0) + * Src ID: Unknown (0x01719697) + * Security Frame Counter: 185 + * Command Frame: Recall Scene 0 + * ZGPD Command ID: Recall Scene 0 (0x10) + * Security MIC: 0xd1fdebfe + */ +export const NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0 = Buffer.from([ + // crafted end FCS + 0x01, 0x08, 0xb9, 0xff, 0xff, 0xff, 0xff, 0x8c, 0x30, 0x97, 0x96, 0x71, 0x01, 0xb9, 0x00, 0x00, 0x00, 0x10, 0xfe, 0xeb, 0xfd, 0xd1, 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Data, Dst: Broadcast + * Frame Control Field: 0x0801, Frame Type: Data, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: None + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..0. .... = Acknowledge Request: False + * .... .... .0.. .... = PAN ID Compression: False + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 00.. .... .... .... = Source Addressing Mode: None (0x0) + * Sequence Number: 70 + * Destination PAN: 0xffff + * Destination: 0xffff + * TI CC24xx-format metadata: FCS OK + * FCS Valid: True + * RSSI: -24 dB + * LQI Correlation Value: 127 + * + * ZGP stub NWK header Data, GPD Src ID: 0x0155f47a + * Frame Control Field: 0x0c, Frame Type: Data Data + * .... ..00 = Frame Type: Data (0x0) + * ..00 11.. = Protocol Version: 3 + * .0.. .... = Auto Commissioning: False + * 0... .... = NWK Frame Extension: False + * Src ID: Unknown (0x0155f47a) + * Command Frame: Commissioning + * ZGPD Command ID: Commissioning (0xe0) + * ZGPD Device ID: Generic: GP On/Off Switch (0x02) + * Options Field: 0x85, MAC Sequence number capability, Application information present, Extended Option Field + * .... ...1 = MAC Sequence number capability: True + * .... ..0. = RxOnCapability: False + * .... .1.. = Application information present: True + * ...0 .... = PANId request: False + * ..0. .... = GP Security Key Request: False + * .0.. .... = Fixed Location: False + * 1... .... = Extended Option Field: True + * Extended Options Field: 0xf2, Key Type: Individual, out of the box GPD key, GPD Key Present, GPD Key Encryption, GPD Outgoing present + * .... ..10 = Security Level Capabilities: 0x2 + * ...1 00.. = Key Type: Individual, out of the box GPD key (0x4) + * ..1. .... = GPD Key Present: True + * .1.. .... = GPD Key Encryption: True + * 1... .... = GPD Outgoing present: True + * Security Key: c925821df46f458cf0e637aac3bab6aa + * GPD Key MIC: 0x111a8345 + * GPD Outgoing Counter: 0x00002346 + * Application information Field: 0x04, GP commands list present + * .... ...0 = Manufacturer ID present: False + * .... ..0. = Manufacturer Model ID present: False + * .... .1.. = GP commands list present: True + * .... 0... = Cluster reports present: False + * Number of GPD commands: 22 + * GPD CommandID list + * ZGPD Command ID: Recall Scene 0 (0x10) + * ZGPD Command ID: Recall Scene 1 (0x11) + * ZGPD Command ID: Toggle (0x22) + * ZGPD Command ID: Release (0x23) + * ZGPD Command ID: Store Scene 0 (0x18) + * ZGPD Command ID: Store Scene 1 (0x19) + * ZGPD Command ID: Recall Scene 4 (0x14) + * ZGPD Command ID: Recall Scene 5 (0x15) + * ZGPD Command ID: Recall Scene 2 (0x12) + * ZGPD Command ID: Recall Scene 3 (0x13) + * ZGPD Command ID: Press 2 of 2 (0x64) + * ZGPD Command ID: Release 2 of 2 (0x65) + * ZGPD Command ID: Press 1 of 2 (0x62) + * ZGPD Command ID: Release 1 of 2 (0x63) + * ZGPD Command ID: Store Scene 6 (0x1e) + * ZGPD Command ID: Store Scene 7 (0x1f) + * ZGPD Command ID: Store Scene 4 (0x1c) + * ZGPD Command ID: Store Scene 5 (0x1d) + * ZGPD Command ID: Store Scene 2 (0x1a) + * ZGPD Command ID: Store Scene 3 (0x1b) + * ZGPD Command ID: Recall Scene 6 (0x16) + * ZGPD Command ID: Recall Scene 7 (0x17) + * + */ +export const NETDEF_ZGP_COMMISSIONING = Buffer.from([ + // crafted end FCS + 0x1, 0x8, 0x46, 0xff, 0xff, 0xff, 0xff, 0xc, 0x7a, 0xf4, 0x55, 0x1, 0xe0, 0x2, 0x85, 0xf2, 0xc9, 0x25, 0x82, 0x1d, 0xf4, 0x6f, 0x45, 0x8c, 0xf0, + 0xe6, 0x37, 0xaa, 0xc3, 0xba, 0xb6, 0xaa, 0x45, 0x83, 0x1a, 0x11, 0x46, 0x23, 0x0, 0x0, 0x4, 0x16, 0x10, 0x11, 0x22, 0x23, 0x18, 0x19, 0x14, 0x15, + 0x12, 0x13, 0x64, 0x65, 0x62, 0x63, 0x1e, 0x1f, 0x1c, 0x1d, 0x1a, 0x1b, 0x16, 0x17, 0xff, 0xff, +]); + +// #endregion + +// #region NET1 + +// #endregion + +// #region NET2 + +//---- Represents a succession of frames from a device leaving a network then doing an initial join, until confirm key + +// export const NET2_TC_KEY = Buffer.from([0x5a, 0x69, 0x67, 0x42, 0x65, 0x65, 0x41, 0x6c, 0x6c, 0x69, 0x61, 0x6e, 0x63, 0x65, 0x30, 0x39]); +export const NET2_PAN_ID = 0x1a64; +export const NET2_EXTENDED_PAN_ID = Buffer.from([0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd]); +// export const NET2_NETWORK_KEY = Buffer.from([0x01, 0x03, 0x05, 0x07, 0x09, 0x0b, 0x0d, 0x0f, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0d]); +export const NET2_COORD_EUI64 = Buffer.from([0xf9, 0x99, 0x5, 0xfe, 0xff, 0x50, 0x4b, 0x80]); +export const NET2_COORD_EUI64_BIGINT = 9244571720516737529n; + +export const NET2_NETWORK_KEY_HASHED = makeKeyedHashByType(ZigbeeKeyType.NWK, NETDEF_NETWORK_KEY); //Buffer.from([1,3,5,7,9,11,13,15,0,2,4,6,8,10,12,13]); +export const NET2_TC_TRANSPORT_HASHED = makeKeyedHashByType(ZigbeeKeyType.TRANSPORT, NETDEF_TC_KEY); //Buffer.from([75,171,15,23,62,20,52,162,213,114,225,193,239,71,135,130]); +export const NET2_TC_LOAD_HASHED = makeKeyedHashByType(ZigbeeKeyType.LOAD, NETDEF_TC_KEY); //Buffer.from([197,164,112,53,195,50,204,191,37,21,113,216,186,222,209,136]); +export const NET2_TC_VERIFY_HASHED = makeKeyedHash(NETDEF_TC_KEY, 0x03); //Buffer.from([26,177,40,223,22,57,161,36,106,171,167,42,106,85,145,36]); + +/** + * IEEE 802.15.4 Data, Src: 0xa18f, Dst: Broadcast + * Frame Control Field: 0x8841, Frame Type: Data, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..0. .... = Acknowledge Request: False + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 237 + * Destination PAN: 0x1a64 + * Destination: 0xffff + * Source: 0xa18f + * [Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df)] + * [Origin: 3] + * + * ZigBee Network Layer Command, Dst: Broadcast, Src: 0xa18f + * Frame Control Field: 0x1209, Frame Type: Command, Discover Route: Suppress, Security, Extended Source Command + * .... .... .... ..01 = Frame Type: Command (0x1) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 00.. .... = Discover Route: Suppress (0x0) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...1 .... .... .... = Extended Source: True + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0xfffd + * Source: 0xa18f + * Radius: 1 + * Sequence Number: 195 + * Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 33483 + * Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Key Sequence Number: 0 + * Message Integrity Code: 508ebdc6 + * [Key: 01030507090b0d0f00020406080a0c0d] + * [Key Origin: 21] + * Command Frame: Leave + * Command Identifier: Leave (0x04) + * ..0. .... = Rejoin: False + * .0.. .... = Request: False + * 0... .... = Remove Children: False + */ +export const NET2_DEVICE_LEAVE_BROADCAST = Buffer.from([ + // crafted end FCS + 0x41, 0x88, 0xed, 0x64, 0x1a, 0xff, 0xff, 0x8f, 0xa1, 0x9, 0x12, 0xfd, 0xff, 0x8f, 0xa1, 0x1, 0xc3, 0xdf, 0xf, 0x28, 0x9b, 0x6d, 0x38, 0xc1, 0xa4, + 0x28, 0xcb, 0x82, 0x0, 0x0, 0xdf, 0xf, 0x28, 0x9b, 0x6d, 0x38, 0xc1, 0xa4, 0x0, 0x51, 0xcb, 0x50, 0x8e, 0xbd, 0xc6, 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Command, Dst: Broadcast + * Frame Control Field: 0x0803, Frame Type: Command, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: None + * .... .... .... .011 = Frame Type: Command (0x3) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..0. .... = Acknowledge Request: False + * .... .... .0.. .... = PAN ID Compression: False + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 00.. .... .... .... = Source Addressing Mode: None (0x0) + * Sequence Number: 100 + * Destination PAN: 0xffff + * Destination: 0xffff + * Command Identifier: Beacon Request (0x07) + */ +export const NET2_BEACON_REQ_FROM_DEVICE = Buffer.from([ + // crafted end FCS + 0x3, 0x8, 0x64, 0xff, 0xff, 0xff, 0xff, 0x7, 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Beacon, Src: 0x0000 + * Frame Control Field: 0x8000, Frame Type: Beacon, Destination Addressing Mode: None, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .000 = Frame Type: Beacon (0x0) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..0. .... = Acknowledge Request: False + * .... .... .0.. .... = PAN ID Compression: False + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 00.. .... .... = Destination Addressing Mode: None (0x0) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 186 + * Source PAN: 0x1a64 + * Source: 0x0000 + * Superframe Specification: PAN Coordinator, Association Permit + * .... .... .... 1111 = Beacon Interval: 15 + * .... .... 1111 .... = Superframe Interval: 15 + * .... 1111 .... .... = Final CAP Slot: 15 + * ...0 .... .... .... = Battery Extension: False + * .1.. .... .... .... = PAN Coordinator: True + * 1... .... .... .... = Association Permit: True + * GTS + * GTS Descriptor Count: 0 + * GTS Permit: False + * Pending Addresses: 0 Short and 0 Long + * + * ZigBee Beacon, ZigBee PRO, EPID: dd:dd:dd:dd:dd:dd:dd:dd + * Protocol ID: 0 + * Beacon: Stack Profile: ZigBee PRO, Router Capacity, End Device Capacity + * .... .... .... 0010 = Stack Profile: ZigBee PRO (0x2) + * .... .... 0010 .... = Protocol Version: 2 + * .... .1.. .... .... = Router Capacity: True + * .000 0... .... .... = Device Depth: 0 + * 1... .... .... .... = End Device Capacity: True + * Extended PAN ID: dd:dd:dd:dd:dd:dd:dd:dd (dd:dd:dd:dd:dd:dd:dd:dd) + * Tx Offset: 16777215 + * Update ID: 0 + */ +export const NET2_BEACON_RESP_FROM_COORD = Buffer.from([ + // crafted end FCS + 0x0, 0x80, 0xba, 0x64, 0x1a, 0x0, 0x0, 0xff, 0xcf, 0x0, 0x0, 0x0, 0x22, 0x84, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xff, 0xff, 0xff, + 0x0, 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Command, Src: TelinkSemico_6d:9b:28:0f:df, Dst: 0x0000 + * Frame Control Field: 0xc823, Frame Type: Command, Acknowledge Request, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Long/64-bit + * .... .... .... .011 = Frame Type: Command (0x3) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .0.. .... = PAN ID Compression: False + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 11.. .... .... .... = Source Addressing Mode: Long/64-bit (0x3) + * Sequence Number: 116 + * Destination PAN: 0x1a64 + * Destination: 0x0000 + * Source PAN: 0xffff + * Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Command Identifier: Association Request (0x01) + * Association Request + * .... ...0 = Alternate PAN Coordinator: False + * .... ..1. = Device Type: FFD + * .... .1.. = Power Source: AC/Mains Power + * .... 1... = Receive On When Idle: True + * .0.. .... = Security Capability: False + * 1... .... = Allocate Address: True + */ +export const NET2_ASSOC_REQ_FROM_DEVICE = Buffer.from([ + // crafted end FCS + 0x23, 0xc8, 0x74, 0x64, 0x1a, 0x0, 0x0, 0xff, 0xff, 0xdf, 0xf, 0x28, 0x9b, 0x6d, 0x38, 0xc1, 0xa4, 0x1, 0x8e, 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Command, Src: SiliconLabor_ff:fe:05:99:f9, Dst: TelinkSemico_6d:9b:28:0f:df + * Frame Control Field: 0xcc63, Frame Type: Command, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Long/64-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Long/64-bit + * .... .... .... .011 = Frame Type: Command (0x3) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 11.. .... .... = Destination Addressing Mode: Long/64-bit (0x3) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 11.. .... .... .... = Source Addressing Mode: Long/64-bit (0x3) + * Sequence Number: 187 + * Destination PAN: 0x1a64 + * Destination: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9) + * Command Identifier: Association Response (0x02) + * Association Response + * Short Address: 0xa18f + * Association Status: 0x00 (Association Successful) + */ +export const NET2_ASSOC_RESP_FROM_COORD = Buffer.from([ + // crafted end FCS + 0x63, 0xcc, 0xbb, 0x64, 0x1a, 0xdf, 0xf, 0x28, 0x9b, 0x6d, 0x38, 0xc1, 0xa4, 0xf9, 0x99, 0x5, 0xfe, 0xff, 0x50, 0x4b, 0x80, 0x2, 0x8f, 0xa1, 0x0, + 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Data, Src: 0x0000, Dst: 0xa18f + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 189 + * Destination PAN: 0x1a64 + * Destination: 0xa18f + * Source: 0x0000 + * [Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9)] + * [Origin: 1] + * + * ZigBee Network Layer Data, Dst: 0xa18f, Src: 0x0000 + * Frame Control Field: 0x0008, Frame Type: Data, Discover Route: Suppress Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 00.. .... = Discover Route: Suppress (0x0) + * .... ...0 .... .... = Multicast: False + * .... ..0. .... .... = Security: False + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0xa18f + * Source: 0x0000 + * Radius: 30 + * Sequence Number: 161 + * [Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9)] + * [Origin: 1] + * + * ZigBee Application Support Layer Command + * Frame Control Field: Command (0x21) + * .... ..01 = Frame Type: Command (0x1) + * .... 00.. = Delivery Mode: Unicast (0x0) + * ..1. .... = Security: True + * .0.. .... = Acknowledgement Request: False + * 0... .... = Extended Header: False + * Counter: 106 + * ZigBee Security Header + * Security Control Field: 0x30, Key Id: Key-Transport Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...1 0... = Key Id: Key-Transport Key (0x2) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 86022 + * Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9) + * Message Integrity Code: e8a75aff + * [Key: 5a6967426565416c6c69616e63653039] + * [Key Origin: 80] + * Command Frame: Transport Key + * Command Identifier: Transport Key (0x05) + * Key Type: Standard Network Key (0x01) + * Key: 01030507090b0d0f00020406080a0c0d + * Sequence Number: 0 + * Extended Destination: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9) + */ +export const NET2_TRANSPORT_KEY_NWK_FROM_COORD = Buffer.from([ + // crafted end FCS + 0x61, 0x88, 0xbd, 0x64, 0x1a, 0x8f, 0xa1, 0x0, 0x0, 0x8, 0x0, 0x8f, 0xa1, 0x0, 0x0, 0x1e, 0xa1, 0x21, 0x6a, 0x30, 0x6, 0x50, 0x1, 0x0, 0xf9, 0x99, + 0x5, 0xfe, 0xff, 0x50, 0x4b, 0x80, 0xde, 0x47, 0x3c, 0x64, 0xb5, 0x69, 0xca, 0xc6, 0x2c, 0x72, 0xac, 0x2f, 0xfd, 0x68, 0x2f, 0x57, 0x59, 0xb, + 0xaa, 0x2b, 0x6f, 0x1e, 0x3, 0x6, 0xf8, 0x24, 0xa5, 0xa9, 0x3, 0x58, 0xb2, 0x6c, 0x8e, 0x68, 0xe6, 0xe8, 0xa7, 0x5a, 0xff, 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Data, Src: 0xa18f, Dst: Broadcast + * Frame Control Field: 0x8841, Frame Type: Data, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..0. .... = Acknowledge Request: False + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 118 + * Destination PAN: 0x1a64 + * Destination: 0xffff + * Source: 0xa18f + * [Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df)] + * [Origin: 3] + * + * ZigBee Network Layer Data, Dst: Broadcast, Src: 0xa18f + * Frame Control Field: 0x0208, Frame Type: Data, Discover Route: Suppress, Security Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 00.. .... = Discover Route: Suppress (0x0) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0xfffd + * Source: 0xa18f + * Radius: 30 + * Sequence Number: 27 + * [Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df)] + * [Origin: 3] + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 33484 + * Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Key Sequence Number: 0 + * Message Integrity Code: 337383aa + * [Key: 01030507090b0d0f00020406080a0c0d] + * [Key Origin: 144] + * + * ZigBee Application Support Layer Data, Dst Endpt: 0, Src Endpt: 0 + * Frame Control Field: Data (0x08) + * .... ..00 = Frame Type: Data (0x0) + * .... 10.. = Delivery Mode: Broadcast (0x2) + * ..0. .... = Security: False + * .0.. .... = Acknowledgement Request: False + * 0... .... = Extended Header: False + * Destination Endpoint: 0 + * Device Announcement (Cluster ID: 0x0013) + * Profile: ZigBee Device Profile (0x0000) + * Source Endpoint: 0 + * Counter: 123 + * + * ZigBee Device Profile, Device Announcement, Nwk Addr: 0xa18f, Ext Addr: TelinkSemico_6d:9b:28:0f:df + * Sequence Number: 0 + * Nwk Addr of Interest: 0xa18f + * Extended Address: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Capability Information: 0x8e + * .... ...0 = Alternate Coordinator: False + * .... ..1. = Full-Function Device: True + * .... .1.. = AC Power: True + * .... 1... = Rx On When Idle: True + * .0.. .... = Security Capability: False + * 1... .... = Allocate Short Address: True + */ +export const NET2_DEVICE_ANNOUNCE_BCAST = Buffer.from([ + // crafted end FCS + 0x41, 0x88, 0x76, 0x64, 0x1a, 0xff, 0xff, 0x8f, 0xa1, 0x8, 0x2, 0xfd, 0xff, 0x8f, 0xa1, 0x1e, 0x1b, 0x28, 0xcc, 0x82, 0x0, 0x0, 0xdf, 0xf, 0x28, + 0x9b, 0x6d, 0x38, 0xc1, 0xa4, 0x0, 0x64, 0xf9, 0xf0, 0xb0, 0xbb, 0xdc, 0x55, 0xe0, 0x24, 0x82, 0x91, 0x7e, 0x90, 0x38, 0x55, 0xba, 0xba, 0x56, + 0xd5, 0x79, 0x33, 0x73, 0x83, 0xaa, 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Data, Src: 0xa18f, Dst: 0x0000 + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 128 + * Destination PAN: 0x1a64 + * Destination: 0x0000 + * Source: 0xa18f + * [Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df)] + * [Origin: 3] + * + * ZigBee Network Layer Data, Dst: 0x0000, Src: 0xa18f + * Frame Control Field: 0x0248, Frame Type: Data, Discover Route: Enable, Security Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 01.. .... = Discover Route: Enable (0x1) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0x0000 + * Source: 0xa18f + * Radius: 30 + * Sequence Number: 37 + * [Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df)] + * [Origin: 3] + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 33494 + * Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Key Sequence Number: 0 + * Message Integrity Code: 6dcba80f + * [Key: 01030507090b0d0f00020406080a0c0d] + * [Key Origin: 144] + * + * ZigBee Application Support Layer Data, Dst Endpt: 0, Src Endpt: 0 + * Frame Control Field: Data (0x40) + * .... ..00 = Frame Type: Data (0x0) + * .... 00.. = Delivery Mode: Unicast (0x0) + * ..0. .... = Security: False + * .1.. .... = Acknowledgement Request: True + * 0... .... = Extended Header: False + * Destination Endpoint: 0 + * Node Descriptor Request (Cluster ID: 0x0002) + * Profile: ZigBee Device Profile (0x0000) + * Source Endpoint: 0 + * Counter: 130 + * + * ZigBee Device Profile, Node Descriptor Request, Nwk Addr: 0x0000 + * Sequence Number: 1 + * Nwk Addr of Interest: 0x0000 + */ +export const NET2_NODE_DESC_REQ_FROM_DEVICE = Buffer.from([ + // crafted end FCS + 0x61, 0x88, 0x80, 0x64, 0x1a, 0x0, 0x0, 0x8f, 0xa1, 0x48, 0x2, 0x0, 0x0, 0x8f, 0xa1, 0x1e, 0x25, 0x28, 0xd6, 0x82, 0x0, 0x0, 0xdf, 0xf, 0x28, + 0x9b, 0x6d, 0x38, 0xc1, 0xa4, 0x0, 0x5b, 0x29, 0xff, 0xc3, 0x73, 0xcc, 0xdb, 0x31, 0x8c, 0x92, 0x1e, 0x6d, 0xcb, 0xa8, 0xf, 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Data, Src: 0xa18f, Dst: 0x0000 + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 130 + * Destination PAN: 0x1a64 + * Destination: 0x0000 + * Source: 0xa18f + * [Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df)] + * [Origin: 3] + * + * ZigBee Network Layer Data, Dst: 0x0000, Src: 0xa18f + * Frame Control Field: 0x0248, Frame Type: Data, Discover Route: Enable, Security Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 01.. .... = Discover Route: Enable (0x1) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0x0000 + * Source: 0xa18f + * Radius: 30 + * Sequence Number: 39 + * [Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df)] + * [Origin: 3] + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 33497 + * Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Key Sequence Number: 0 + * Message Integrity Code: 61efed10 + * [Key: 01030507090b0d0f00020406080a0c0d] + * [Key Origin: 144] + * + * ZigBee Application Support Layer Command + * Frame Control Field: Command (0x21) + * .... ..01 = Frame Type: Command (0x1) + * .... 00.. = Delivery Mode: Unicast (0x0) + * ..1. .... = Security: True + * .0.. .... = Acknowledgement Request: False + * 0... .... = Extended Header: False + * Counter: 131 + * ZigBee Security Header + * Security Control Field: 0x20, Key Id: Link Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 0... = Key Id: Link Key (0x0) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 33496 + * Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Message Integrity Code: 7aaf0c60 + * [Key: 5a6967426565416c6c69616e63653039] + * [Key Origin: 80] + * Command Frame: Request Key + * Command Identifier: Request Key (0x08) + * Key Type: Trust Center Link Key (0x04) + * + */ +export const NET2_REQUEST_KEY_TC_FROM_DEVICE = Buffer.from([ + // crafted end FCS + 0x61, 0x88, 0x82, 0x64, 0x1a, 0x0, 0x0, 0x8f, 0xa1, 0x48, 0x2, 0x0, 0x0, 0x8f, 0xa1, 0x1e, 0x27, 0x28, 0xd9, 0x82, 0x0, 0x0, 0xdf, 0xf, 0x28, + 0x9b, 0x6d, 0x38, 0xc1, 0xa4, 0x0, 0x1b, 0x3, 0x94, 0x92, 0xf4, 0xe4, 0xec, 0x13, 0xa5, 0xa3, 0x5b, 0x8, 0x78, 0xaf, 0x46, 0x8e, 0x70, 0xa8, 0xe9, + 0x7d, 0xfe, 0x61, 0xef, 0xed, 0x10, 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Data, Src: 0x0000, Dst: 0xa18f + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 207 + * Destination PAN: 0x1a64 + * Destination: 0xa18f + * Source: 0x0000 + * [Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9)] + * [Origin: 1] + * + * ZigBee Network Layer Data, Dst: 0xa18f, Src: 0x0000 + * Frame Control Field: 0x0208, Frame Type: Data, Discover Route: Suppress, Security Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 00.. .... = Discover Route: Suppress (0x0) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0xa18f + * Source: 0x0000 + * Radius: 30 + * Sequence Number: 185 + * [Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9)] + * [Origin: 1] + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 422014 + * Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9) + * Key Sequence Number: 0 + * Message Integrity Code: c1559100 + * [Key: 01030507090b0d0f00020406080a0c0d] + * [Key Origin: 144] + * + * ZigBee Application Support Layer Command + * Frame Control Field: Command (0x21) + * .... ..01 = Frame Type: Command (0x1) + * .... 00.. = Delivery Mode: Unicast (0x0) + * ..1. .... = Security: True + * .0.. .... = Acknowledgement Request: False + * 0... .... = Extended Header: False + * Counter: 114 + * ZigBee Security Header + * Security Control Field: 0x38, Key Id: Key-Load Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...1 1... = Key Id: Key-Load Key (0x3) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 86023 + * Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9) + * Message Integrity Code: 6b7ce3d3 + * [Key: 5a6967426565416c6c69616e63653039] + * [Key Origin: 80] + * Command Frame: Transport Key + * Command Identifier: Transport Key (0x05) + * Key Type: Trust Center Link Key (0x04) + * Key: 5a6967426565416c6c69616e63653039 + * Extended Destination: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9) + */ +export const NET2_TRANSPORT_KEY_TC_FROM_COORD = Buffer.from([ + // crafted end FCS + 0x61, 0x88, 0xcf, 0x64, 0x1a, 0x8f, 0xa1, 0x0, 0x0, 0x8, 0x2, 0x8f, 0xa1, 0x0, 0x0, 0x1e, 0xb9, 0x28, 0x7e, 0x70, 0x6, 0x0, 0xf9, 0x99, 0x5, 0xfe, + 0xff, 0x50, 0x4b, 0x80, 0x0, 0xa3, 0x9, 0x3b, 0x4f, 0x4, 0x92, 0xe2, 0xa1, 0x47, 0xb8, 0x8, 0xb8, 0xdd, 0x97, 0x49, 0xa8, 0xc9, 0xe0, 0xb6, 0xf2, + 0x57, 0x2f, 0x2e, 0x7e, 0xaf, 0xa3, 0x7f, 0x1d, 0x65, 0x92, 0xee, 0x33, 0x71, 0x33, 0x8f, 0xd7, 0x2a, 0x75, 0x12, 0xf6, 0x92, 0x54, 0x61, 0x2c, + 0xd0, 0xd6, 0x2c, 0x2b, 0x50, 0xe, 0x90, 0xc9, 0xdc, 0xc1, 0x55, 0x91, 0x0, 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Data, Src: 0xa18f, Dst: 0x0000 + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 131 + * Destination PAN: 0x1a64 + * Destination: 0x0000 + * Source: 0xa18f + * [Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df)] + * [Origin: 3] + * + * ZigBee Network Layer Data, Dst: 0x0000, Src: 0xa18f + * Frame Control Field: 0x0248, Frame Type: Data, Discover Route: Enable, Security Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 01.. .... = Discover Route: Enable (0x1) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0x0000 + * Source: 0xa18f + * Radius: 30 + * Sequence Number: 40 + * [Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df)] + * [Origin: 3] + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 33498 + * Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Key Sequence Number: 0 + * Message Integrity Code: 8290b7ec + * [Key: 01030507090b0d0f00020406080a0c0d] + * [Key Origin: 144] + * + * ZigBee Application Support Layer Command + * Frame Control Field: Command (0x01) + * .... ..01 = Frame Type: Command (0x1) + * .... 00.. = Delivery Mode: Unicast (0x0) + * ..0. .... = Security: False + * .0.. .... = Acknowledgement Request: False + * 0... .... = Extended Header: False + * Counter: 132 + * Command Frame: Verify Key + * Command Identifier: Verify Key (0x0f) + * Key Type: Trust Center Link Key (0x04) + * Extended Source: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * Key Hash: 1ab128df1639a1246aaba72a6a559124 + * + */ +export const NET2_VERIFY_KEY_TC_FROM_DEVICE = Buffer.from([ + // crafted end FCS + 0x61, 0x88, 0x83, 0x64, 0x1a, 0x0, 0x0, 0x8f, 0xa1, 0x48, 0x2, 0x0, 0x0, 0x8f, 0xa1, 0x1e, 0x28, 0x28, 0xda, 0x82, 0x0, 0x0, 0xdf, 0xf, 0x28, + 0x9b, 0x6d, 0x38, 0xc1, 0xa4, 0x0, 0x99, 0xcd, 0xde, 0xf, 0xd, 0xb6, 0x79, 0x4, 0x6e, 0x6e, 0xab, 0xc0, 0xf5, 0xba, 0x56, 0xd5, 0xd1, 0xbf, 0x8f, + 0x61, 0xe3, 0xd6, 0x57, 0x7d, 0x9a, 0x88, 0x79, 0xcb, 0x82, 0x90, 0xb7, 0xec, 0xff, 0xff, +]); + +/** + * IEEE 802.15.4 Data, Src: 0x0000, Dst: 0xa18f + * Frame Control Field: 0x8861, Frame Type: Data, Acknowledge Request, PAN ID Compression, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: Short/16-bit + * .... .... .... .001 = Frame Type: Data (0x1) + * .... .... .... 0... = Security Enabled: False + * .... .... ...0 .... = Frame Pending: False + * .... .... ..1. .... = Acknowledge Request: True + * .... .... .1.. .... = PAN ID Compression: True + * .... .... 0... .... = Reserved: False + * .... ...0 .... .... = Sequence Number Suppression: False + * .... ..0. .... .... = Information Elements Present: False + * .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2) + * ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0) + * 10.. .... .... .... = Source Addressing Mode: Short/16-bit (0x2) + * Sequence Number: 208 + * Destination PAN: 0x1a64 + * Destination: 0xa18f + * Source: 0x0000 + * [Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9)] + * [Origin: 1] + * + * ZigBee Network Layer Data, Dst: 0xa18f, Src: 0x0000 + * Frame Control Field: 0x0208, Frame Type: Data, Discover Route: Suppress, Security Data + * .... .... .... ..00 = Frame Type: Data (0x0) + * .... .... ..00 10.. = Protocol Version: 2 + * .... .... 00.. .... = Discover Route: Suppress (0x0) + * .... ...0 .... .... = Multicast: False + * .... ..1. .... .... = Security: True + * .... .0.. .... .... = Source Route: False + * .... 0... .... .... = Destination: False + * ...0 .... .... .... = Extended Source: False + * ..0. .... .... .... = End Device Initiator: False + * Destination: 0xa18f + * Source: 0x0000 + * Radius: 30 + * Sequence Number: 186 + * [Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9)] + * [Origin: 1] + * ZigBee Security Header + * Security Control Field: 0x28, Key Id: Network Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 1... = Key Id: Network Key (0x1) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 422015 + * Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9) + * Key Sequence Number: 0 + * Message Integrity Code: e466e305 + * [Key: 01030507090b0d0f00020406080a0c0d] + * [Key Origin: 144] + * + * ZigBee Application Support Layer Command + * Frame Control Field: Command (0x61) + * .... ..01 = Frame Type: Command (0x1) + * .... 00.. = Delivery Mode: Unicast (0x0) + * ..1. .... = Security: True + * .1.. .... = Acknowledgement Request: True + * 0... .... = Extended Header: False + * Counter: 115 + * ZigBee Security Header + * Security Control Field: 0x20, Key Id: Link Key, Extended Nonce + * .... .000 = Security Level: 0x0 + * ...0 0... = Key Id: Link Key (0x0) + * ..1. .... = Extended Nonce: True + * .0.. .... = Require Verified Frame Counter: 0x0 + * Frame Counter: 86024 + * Extended Source: SiliconLabor_ff:fe:05:99:f9 (80:4b:50:ff:fe:05:99:f9) + * Message Integrity Code: a6bdadce + * [Key: 5a6967426565416c6c69616e63653039] + * [Key Origin: 203] + * Command Frame: Confirm Key, SUCCESS + * Command Identifier: Confirm Key (0x10) + * Status: SUCCESS (0x00) + * Key Type: Trust Center Link Key (0x04) + * Extended Destination: TelinkSemico_6d:9b:28:0f:df (a4:c1:38:6d:9b:28:0f:df) + * + */ +export const NET2_CONFIRM_KEY_TC_SUCCESS = Buffer.from([ + // crafted end FCS + 0x61, 0x88, 0xd0, 0x64, 0x1a, 0x8f, 0xa1, 0x0, 0x0, 0x8, 0x2, 0x8f, 0xa1, 0x0, 0x0, 0x1e, 0xba, 0x28, 0x7f, 0x70, 0x6, 0x0, 0xf9, 0x99, 0x5, 0xfe, + 0xff, 0x50, 0x4b, 0x80, 0x0, 0x5a, 0xe3, 0x32, 0xc5, 0x90, 0x61, 0x6c, 0x71, 0xb6, 0xb2, 0x3c, 0xb9, 0x3f, 0xf, 0x4, 0xf5, 0x73, 0x20, 0xdf, 0xe1, + 0xe9, 0x88, 0xb6, 0x75, 0xb5, 0x59, 0x70, 0x53, 0xcc, 0xa8, 0xe4, 0x66, 0xe3, 0x5, 0xff, 0xff, +]); + +// #endregion diff --git a/test/ot-rcp-driver.test.ts b/test/ot-rcp-driver.test.ts new file mode 100644 index 0000000..ae1e0c9 --- /dev/null +++ b/test/ot-rcp-driver.test.ts @@ -0,0 +1,751 @@ +import { type Socket, createSocket } from "node:dgram"; +import { rmSync } from "node:fs"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { DEFAULT_WIRESHARK_IP, DEFAULT_ZEP_UDP_PORT, createWiresharkZEPFrame } from "../src/dev/wireshark"; +import { OTRCPDriver } from "../src/drivers/ot-rcp-driver"; +import { SpinelCommandId } from "../src/spinel/commands"; +import { SpinelPropertyId } from "../src/spinel/properties"; +import { SPINEL_HEADER_FLG_SPINEL, encodeSpinelFrame } from "../src/spinel/spinel"; +import { SpinelStatus } from "../src/spinel/statuses"; +import { MACAssociationStatus, decodeMACFrameControl, decodeMACHeader } from "../src/zigbee/mac"; +import { ZigbeeConsts } from "../src/zigbee/zigbee"; +import { + A_CHANNEL, + A_EUI64, + NET2_ASSOC_REQ_FROM_DEVICE, + NET2_BEACON_REQ_FROM_DEVICE, + NET2_COORD_EUI64_BIGINT, + NET2_DEVICE_ANNOUNCE_BCAST, + NET2_EXTENDED_PAN_ID, + NET2_NODE_DESC_REQ_FROM_DEVICE, + NET2_PAN_ID, + NET2_REQUEST_KEY_TC_FROM_DEVICE, + NET2_TRANSPORT_KEY_NWK_FROM_COORD, + NET2_VERIFY_KEY_TC_FROM_DEVICE, + NETDEF_ACK_FRAME_FROM_COORD, + NETDEF_ACK_FRAME_TO_COORD, + NETDEF_EXTENDED_PAN_ID, + NETDEF_LINK_STATUS_FROM_DEV, + NETDEF_MTORR_FRAME_FROM_COORD, + NETDEF_NETWORK_KEY, + NETDEF_PAN_ID, + NETDEF_ROUTE_RECORD_TO_COORD, + NETDEF_TC_KEY, + NETDEF_ZCL_FRAME_CMD_TO_COORD, + NETDEF_ZGP_COMMISSIONING, +} from "./data"; + +const SAVE_DIR = "temp"; + +describe("OT RCP Driver", () => { + let driver: OTRCPDriver; + let wiresharkSeqNum: number; + let wiresharkSocket: Socket | undefined; + + const nextWiresharkSeqNum = (): number => { + wiresharkSeqNum = (wiresharkSeqNum + 1) & 0xffffffff; + + return wiresharkSeqNum + 1; + }; + + const setupWireshark = (): void => { + wiresharkSeqNum = 0; // start at 1 + wiresharkSocket = createSocket("udp4"); + wiresharkSocket.bind(DEFAULT_ZEP_UDP_PORT); + + driver.on("MAC_FRAME", (payload, rssi) => { + const wsZEPFrame = createWiresharkZEPFrame(driver.netParams.channel, 1, 0, rssi ?? 0, nextWiresharkSeqNum(), payload); + + wiresharkSocket?.send(wsZEPFrame, DEFAULT_ZEP_UDP_PORT, DEFAULT_WIRESHARK_IP); + }); + }; + + const endWireshark = async (): Promise => { + if (wiresharkSocket) { + await new Promise((resolve) => wiresharkSocket?.close(() => resolve())); + } + }; + + const makeSpinelLastStatus = (tid: number, status: SpinelStatus = SpinelStatus.OK): Buffer => { + const respSpinelFrame = { + header: { + tid, + nli: 0, + flg: SPINEL_HEADER_FLG_SPINEL, + }, + commandId: SpinelCommandId.PROP_VALUE_IS, + payload: Buffer.from([SpinelPropertyId.LAST_STATUS, status]), + }; + const encRespHdlcFrame = encodeSpinelFrame(respSpinelFrame); + + return Buffer.from(encRespHdlcFrame.data.subarray(0, encRespHdlcFrame.length)); + }; + + const makeSpinelStreamRaw = (tid: number, macFrame: Buffer): Buffer => { + const spinelFrame = { + header: { + tid, + nli: 0, + flg: SPINEL_HEADER_FLG_SPINEL, + }, + commandId: SpinelCommandId.PROP_VALUE_IS, + payload: Buffer.from([SpinelPropertyId.STREAM_RAW, macFrame.byteLength & 0xff, (macFrame.byteLength >> 8) & 0xff, ...macFrame]), + }; + const encHdlcFrame = encodeSpinelFrame(spinelFrame); + + return Buffer.from(encHdlcFrame.data.subarray(0, encHdlcFrame.length)); + }; + + beforeAll(() => { + vi.useFakeTimers(); + }); + + afterAll(() => { + vi.useRealTimers(); + + rmSync(SAVE_DIR, { recursive: true, force: true }); + }); + + beforeEach(() => { + if (driver) { + rmSync(driver.savePath, { recursive: true, force: true }); + } + }); + + afterEach(async () => { + await endWireshark(); + }); + + describe("State management", () => { + beforeEach(async () => { + driver = new OTRCPDriver( + { + txChannel: A_CHANNEL, + ccaBackoffAttempts: 1, + ccaRetries: 4, + enableCSMACA: true, + headerUpdated: true, + reTx: false, + securityProcessed: true, + txDelay: 0, + txDelayBaseTime: 0, + rxChannelAfterTxDone: A_CHANNEL, + }, + { + eui64: Buffer.from(A_EUI64).readBigUInt64LE(0), + panId: NETDEF_PAN_ID, + extendedPANId: Buffer.from(NETDEF_EXTENDED_PAN_ID).readBigUInt64LE(0), + channel: A_CHANNEL, + nwkUpdateId: 0, + txPower: 10, + networkKey: Buffer.from(NETDEF_NETWORK_KEY), + networkKeyFrameCounter: 0, + networkKeySequenceNumber: 0, + tcKey: Buffer.from(NETDEF_TC_KEY), + tcKeyFrameCounter: 0, + }, + SAVE_DIR, + // true, // emitMACFrames + ); + // await driver.loadState(); + driver.parser.on("data", driver.onFrame.bind(driver)); + + // @ts-expect-error mock override + driver.networkUp = true; + }); + + afterEach(() => { + driver.deviceTable.clear(); + driver.address16ToAddress64.clear(); + }); + + it("handles loading with given network params - first start", async () => { + const saveStateSpy = vi.spyOn(driver, "saveState"); + + await driver.loadState(); + + expect(saveStateSpy).toHaveBeenCalledTimes(1); + + // reset manually + driver.netParams.eui64 = 0n; + driver.netParams.panId = 0x0; + driver.netParams.extendedPANId = 0n; + driver.netParams.channel = 11; + driver.netParams.nwkUpdateId = 0; + driver.netParams.txPower = 11; + driver.netParams.networkKey = Buffer.alloc(16); + driver.netParams.networkKeyFrameCounter = 0; + driver.netParams.networkKeySequenceNumber = 0; + driver.netParams.tcKey = Buffer.alloc(16); + driver.netParams.tcKeyFrameCounter = 0; + driver.deviceTable.clear(); + driver.address16ToAddress64.clear(); + driver.indirectTransmissions.clear(); + + await driver.loadState(); + + expect(driver.netParams.eui64).toStrictEqual(Buffer.from(A_EUI64).readBigUInt64LE(0)); + expect(driver.netParams.panId).toStrictEqual(NETDEF_PAN_ID); + expect(driver.netParams.extendedPANId).toStrictEqual(Buffer.from(NETDEF_EXTENDED_PAN_ID).readBigUInt64LE(0)); + expect(driver.netParams.channel).toStrictEqual(A_CHANNEL); + expect(driver.netParams.nwkUpdateId).toStrictEqual(0); + expect(driver.netParams.txPower).toStrictEqual(10); + expect(driver.netParams.networkKey).toStrictEqual(Buffer.from(NETDEF_NETWORK_KEY)); + expect(driver.netParams.networkKeyFrameCounter).toStrictEqual(1024); + expect(driver.netParams.networkKeySequenceNumber).toStrictEqual(0); + expect(driver.netParams.tcKey).toStrictEqual(Buffer.from(NETDEF_TC_KEY)); + expect(driver.netParams.tcKeyFrameCounter).toStrictEqual(1024); + expect(driver.deviceTable.size).toStrictEqual(0); + expect(driver.address16ToAddress64.size).toStrictEqual(0); + expect(driver.indirectTransmissions.size).toStrictEqual(0); + }); + + it("saves & loads back", async () => { + driver.netParams.eui64 = 1n; + driver.netParams.panId = 0x4356; + driver.netParams.extendedPANId = 893489346n; + driver.netParams.channel = 25; + driver.netParams.nwkUpdateId = 1; + driver.netParams.txPower = 15; + driver.netParams.networkKey = Buffer.from([ + 0x11, 0x29, 0x22, 0x18, 0x13, 0x27, 0x24, 0x16, 0x12, 0x34, 0x56, 0x78, 0x90, 0x98, 0x76, 0x54, + ]); + driver.netParams.networkKeyFrameCounter = 235568765; + driver.netParams.networkKeySequenceNumber = 1; + driver.netParams.tcKey = Buffer.from([0x51, 0x69, 0x62, 0x58, 0x53, 0x67, 0x64, 0x56, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]); + driver.netParams.tcKeyFrameCounter = 896723; + driver.deviceTable.set(1234n, { address16: 1, rxOnWhenIdle: true, authorized: true }); + driver.deviceTable.set(12656887476334n, { address16: 3457, rxOnWhenIdle: true, authorized: true }); + driver.deviceTable.set(12328965645634n, { address16: 9674, rxOnWhenIdle: false, authorized: true }); + driver.deviceTable.set(234367481234n, { address16: 54748, rxOnWhenIdle: true, authorized: false }); + + await driver.saveState(); + + // reset manually + driver.netParams.eui64 = 0n; + driver.netParams.panId = 0x0; + driver.netParams.extendedPANId = 0n; + driver.netParams.channel = 11; + driver.netParams.nwkUpdateId = 0; + driver.netParams.txPower = 11; + driver.netParams.networkKey = Buffer.alloc(16); + driver.netParams.networkKeyFrameCounter = 0; + driver.netParams.networkKeySequenceNumber = 0; + driver.netParams.tcKey = Buffer.alloc(16); + driver.netParams.tcKeyFrameCounter = 0; + driver.deviceTable.clear(); + driver.address16ToAddress64.clear(); + driver.indirectTransmissions.clear(); + + await driver.loadState(); + + expect(driver.netParams.eui64).toStrictEqual(1n); + expect(driver.netParams.panId).toStrictEqual(0x4356); + expect(driver.netParams.extendedPANId).toStrictEqual(893489346n); + expect(driver.netParams.channel).toStrictEqual(25); + expect(driver.netParams.nwkUpdateId).toStrictEqual(1); + expect(driver.netParams.txPower).toStrictEqual(15); + expect(driver.netParams.networkKey).toStrictEqual( + Buffer.from([0x11, 0x29, 0x22, 0x18, 0x13, 0x27, 0x24, 0x16, 0x12, 0x34, 0x56, 0x78, 0x90, 0x98, 0x76, 0x54]), + ); + expect(driver.netParams.networkKeyFrameCounter).toStrictEqual(235568765 + 1024); + expect(driver.netParams.networkKeySequenceNumber).toStrictEqual(1); + expect(driver.netParams.tcKey).toStrictEqual( + Buffer.from([0x51, 0x69, 0x62, 0x58, 0x53, 0x67, 0x64, 0x56, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]), + ); + expect(driver.netParams.tcKeyFrameCounter).toStrictEqual(896723 + 1024); + expect(driver.deviceTable.size).toStrictEqual(4); + expect(driver.deviceTable.get(1234n)).toStrictEqual({ address16: 1, rxOnWhenIdle: true, authorized: true }); + expect(driver.deviceTable.get(12656887476334n)).toStrictEqual({ address16: 3457, rxOnWhenIdle: true, authorized: true }); + expect(driver.deviceTable.get(12328965645634n)).toStrictEqual({ address16: 9674, rxOnWhenIdle: false, authorized: true }); + expect(driver.deviceTable.get(234367481234n)).toStrictEqual({ address16: 54748, rxOnWhenIdle: true, authorized: false }); + expect(driver.address16ToAddress64.size).toStrictEqual(4); + expect(driver.address16ToAddress64.get(1)).toStrictEqual(1234n); + expect(driver.address16ToAddress64.get(3457)).toStrictEqual(12656887476334n); + expect(driver.address16ToAddress64.get(9674)).toStrictEqual(12328965645634n); + expect(driver.address16ToAddress64.get(54748)).toStrictEqual(234367481234n); + expect(driver.indirectTransmissions.size).toStrictEqual(1); + expect(driver.indirectTransmissions.get(12328965645634n)).toStrictEqual([]); + }); + }); + + describe("NETDEF", () => { + beforeEach(async () => { + driver = new OTRCPDriver( + { + txChannel: A_CHANNEL, + ccaBackoffAttempts: 1, + ccaRetries: 4, + enableCSMACA: true, + headerUpdated: true, + reTx: false, + securityProcessed: true, + txDelay: 0, + txDelayBaseTime: 0, + rxChannelAfterTxDone: A_CHANNEL, + }, + { + eui64: Buffer.from(A_EUI64).readBigUInt64LE(0), + panId: NETDEF_PAN_ID, + extendedPANId: Buffer.from(NETDEF_EXTENDED_PAN_ID).readBigUInt64LE(0), + channel: A_CHANNEL, + nwkUpdateId: 0, + txPower: 10, + networkKey: Buffer.from(NETDEF_NETWORK_KEY), + networkKeyFrameCounter: 0, + networkKeySequenceNumber: 0, + tcKey: Buffer.from(NETDEF_TC_KEY), + tcKeyFrameCounter: 0, + }, + SAVE_DIR, + // true, // emitMACFrames + ); + await driver.loadState(); + driver.parser.on("data", driver.onFrame.bind(driver)); + + // @ts-expect-error mock override + driver.networkUp = true; + }); + + afterEach(() => { + driver.deviceTable.clear(); + driver.address16ToAddress64.clear(); + }); + + it("ignores bogus data before start of HDLC frame", async () => { + const parserEmit = vi.spyOn(driver.parser, "emit"); + const frame = makeSpinelLastStatus(1); + + driver.parser._transform(Buffer.concat([Buffer.from([0x12, 0x32]), frame]), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(parserEmit).toHaveBeenNthCalledWith(1, "data", frame); + }); + + it("ignores bogus data after end of HDLC frame", async () => { + const parserEmit = vi.spyOn(driver.parser, "emit"); + const frame = makeSpinelLastStatus(1); + + driver.parser._transform(Buffer.concat([frame, Buffer.from([0x12, 0x32])]), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(parserEmit).toHaveBeenNthCalledWith(1, "data", frame); + }); + + it("ignores bogus data before start and after end of HDLC frame", async () => { + const parserEmit = vi.spyOn(driver.parser, "emit"); + const frame = makeSpinelLastStatus(1); + + driver.parser._transform(Buffer.concat([Buffer.from([0x12, 0x32]), frame, Buffer.from([0x12, 0x32])]), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(parserEmit).toHaveBeenNthCalledWith(1, "data", frame); + }); + + it("skips duplicate FLAGs of HDLC frame", async () => { + const parserEmit = vi.spyOn(driver.parser, "emit"); + const frame = makeSpinelLastStatus(1); + + driver.parser._transform(Buffer.concat([Buffer.from([0x7e, 0x7e]), frame, Buffer.from([0x7e, 0x7e])]), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(parserEmit).toHaveBeenNthCalledWith(1, "data", frame); + }); + + it("handles multiple HDLC frames in same transform call", async () => { + const parserEmit = vi.spyOn(driver.parser, "emit"); + const frame = makeSpinelLastStatus(1); + const frame2 = makeSpinelLastStatus(2); + + driver.parser._transform(Buffer.concat([frame, frame2]), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(parserEmit).toHaveBeenNthCalledWith(1, "data", frame); + expect(parserEmit).toHaveBeenNthCalledWith(2, "data", frame2); + }); + + it("sends frame NETDEF_ACK_FRAME_FROM_COORD and receives LAST_STATUS response", async () => { + const waitForTIDSpy = vi.spyOn(driver, "waitForTID"); + const sendFrameSpy = vi.spyOn(driver, "sendFrame"); + + const p = driver.sendMACFrame(1, NETDEF_ACK_FRAME_FROM_COORD, undefined, undefined); // bypass indirect transmissions + await vi.advanceTimersByTimeAsync(10); + driver.parser._transform(makeSpinelLastStatus(1), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + await expect(p).resolves.toStrictEqual(undefined); + expect(waitForTIDSpy).toHaveBeenCalledWith(1, 10000); + expect(sendFrameSpy).toHaveBeenCalledTimes(1); + }); + + it("sends frame NETDEF_MTORR_FRAME_FROM_COORD and receives LAST_STATUS response", async () => { + const waitForTIDSpy = vi.spyOn(driver, "waitForTID"); + const sendFrameSpy = vi.spyOn(driver, "sendFrame"); + + const p = driver.sendMACFrame(1, NETDEF_MTORR_FRAME_FROM_COORD, undefined, undefined); // bypass indirect transmissions + await vi.advanceTimersByTimeAsync(10); + driver.parser._transform(makeSpinelLastStatus(1), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + await expect(p).resolves.toStrictEqual(undefined); + expect(waitForTIDSpy).toHaveBeenCalledWith(1, 10000); + expect(sendFrameSpy).toHaveBeenCalledTimes(1); + }); + + it("receives frame NETDEF_ACK_FRAME_TO_COORD", async () => { + const onStreamRawFrameSpy = vi.spyOn(driver, "onStreamRawFrame"); + const onZigbeeAPSACKRequestSpy = vi.spyOn(driver, "onZigbeeAPSACKRequest"); + const onZigbeeAPSFrameSpy = vi.spyOn(driver, "onZigbeeAPSFrame"); + const processZigbeeAPSCommandFrameSpy = vi.spyOn(driver, "processZigbeeAPSCommandFrame"); + + driver.parser._transform(makeSpinelStreamRaw(1, NETDEF_ACK_FRAME_TO_COORD), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(onStreamRawFrameSpy).toHaveBeenCalledTimes(1); + expect(onZigbeeAPSACKRequestSpy).toHaveBeenCalledTimes(0); + expect(onZigbeeAPSFrameSpy).toHaveBeenCalledTimes(1); + expect(processZigbeeAPSCommandFrameSpy).toHaveBeenCalledTimes(0); + }); + + it("receives frame NETDEF_LINK_STATUS_FROM_DEV", async () => { + const onStreamRawFrameSpy = vi.spyOn(driver, "onStreamRawFrame"); + const onZigbeeAPSACKRequestSpy = vi.spyOn(driver, "onZigbeeAPSACKRequest"); + const onZigbeeAPSFrameSpy = vi.spyOn(driver, "onZigbeeAPSFrame"); + const processZigbeeNWKLinkStatusSpy = vi.spyOn(driver, "processZigbeeNWKLinkStatus"); + + driver.parser._transform(makeSpinelStreamRaw(1, NETDEF_LINK_STATUS_FROM_DEV), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(onStreamRawFrameSpy).toHaveBeenCalledTimes(1); + expect(onZigbeeAPSACKRequestSpy).toHaveBeenCalledTimes(0); + expect(onZigbeeAPSFrameSpy).toHaveBeenCalledTimes(0); + expect(processZigbeeNWKLinkStatusSpy).toHaveBeenCalledTimes(1); + }); + + it("receives frame NETDEF_ZCL_FRAME_CMD_TO_COORD", async () => { + const emitSpy = vi.spyOn(driver, "emit"); + const onStreamRawFrameSpy = vi.spyOn(driver, "onStreamRawFrame"); + const onZigbeeAPSACKRequestSpy = vi.spyOn(driver, "onZigbeeAPSACKRequest"); + const onZigbeeAPSFrameSpy = vi.spyOn(driver, "onZigbeeAPSFrame"); + const processZigbeeAPSCommandFrameSpy = vi.spyOn(driver, "processZigbeeAPSCommandFrame"); + + driver.parser._transform(makeSpinelStreamRaw(1, NETDEF_ZCL_FRAME_CMD_TO_COORD), "utf8", () => {}); + await vi.runOnlyPendingTimersAsync(); + + expect(onStreamRawFrameSpy).toHaveBeenCalledTimes(1); + expect(onZigbeeAPSACKRequestSpy).toHaveBeenCalledTimes(0); + expect(onZigbeeAPSFrameSpy).toHaveBeenCalledTimes(1); + expect(processZigbeeAPSCommandFrameSpy).toHaveBeenCalledTimes(0); + expect(emitSpy).toHaveBeenCalledWith( + "FRAME", + 0xaa38, + undefined, + { + frameControl: { + frameType: 0x0, + deliveryMode: 0x0, + ackFormat: false, + security: false, + ackRequest: false, + extendedHeader: false, + }, + destEndpoint: 1, + group: undefined, + clusterId: 0xef00, + profileId: 0x0104, + sourceEndpoint: 1, + counter: 63, + fragmentation: undefined, + fragBlockNumber: undefined, + fragACKBitfield: undefined, + securityHeader: undefined, + }, + Buffer.from([0x09, 0x50, 0x25, 0xaf, 0x00]), + 0, // rssi + ); + }); + + it("receives frame NETDEF_ROUTE_RECORD_TO_COORD", async () => { + const onStreamRawFrameSpy = vi.spyOn(driver, "onStreamRawFrame"); + const onZigbeeAPSACKRequestSpy = vi.spyOn(driver, "onZigbeeAPSACKRequest"); + const onZigbeeAPSFrameSpy = vi.spyOn(driver, "onZigbeeAPSFrame"); + const processZigbeeNWKRouteRecSpy = vi.spyOn(driver, "processZigbeeNWKRouteRecord"); + + driver.parser._transform(makeSpinelStreamRaw(1, NETDEF_ROUTE_RECORD_TO_COORD), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(onStreamRawFrameSpy).toHaveBeenCalledTimes(1); + expect(onZigbeeAPSACKRequestSpy).toHaveBeenCalledTimes(0); + expect(onZigbeeAPSFrameSpy).toHaveBeenCalledTimes(0); + expect(processZigbeeNWKRouteRecSpy).toHaveBeenCalledTimes(1); + }); + + it("receives frame NETDEF_MTORR_FRAME_FROM_COORD", async () => { + const onStreamRawFrameSpy = vi.spyOn(driver, "onStreamRawFrame"); + const onZigbeeAPSACKRequestSpy = vi.spyOn(driver, "onZigbeeAPSACKRequest"); + const onZigbeeAPSFrameSpy = vi.spyOn(driver, "onZigbeeAPSFrame"); + const processZigbeeNWKRouteReqSpy = vi.spyOn(driver, "processZigbeeNWKRouteReq"); + const sendZigbeeNWKRouteReplySpy = vi.spyOn(driver, "sendZigbeeNWKRouteReply"); + + driver.parser._transform(makeSpinelStreamRaw(1, NETDEF_MTORR_FRAME_FROM_COORD), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + driver.parser._transform(makeSpinelLastStatus(1), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(onStreamRawFrameSpy).toHaveBeenCalledTimes(1); + expect(onZigbeeAPSACKRequestSpy).toHaveBeenCalledTimes(0); + expect(onZigbeeAPSFrameSpy).toHaveBeenCalledTimes(0); + expect(processZigbeeNWKRouteReqSpy).toHaveBeenCalledTimes(1); + expect(sendZigbeeNWKRouteReplySpy).toHaveBeenCalledTimes(1); + }); + + it("receives frame NETDEF_ZGP_COMMISSIONING", async () => { + const emitSpy = vi.spyOn(driver, "emit"); + const onStreamRawFrameSpy = vi.spyOn(driver, "onStreamRawFrame"); + const onZigbeeAPSACKRequestSpy = vi.spyOn(driver, "onZigbeeAPSACKRequest"); + const onZigbeeAPSFrameSpy = vi.spyOn(driver, "onZigbeeAPSFrame"); + const processZigbeeNWKGPCommandFrameSpy = vi.spyOn(driver, "processZigbeeNWKGPCommandFrame"); + + driver.parser._transform(makeSpinelStreamRaw(1, NETDEF_ZGP_COMMISSIONING), "utf8", () => {}); + await vi.runOnlyPendingTimersAsync(); + + expect(onStreamRawFrameSpy).toHaveBeenCalledTimes(1); + expect(onZigbeeAPSACKRequestSpy).toHaveBeenCalledTimes(0); + expect(onZigbeeAPSFrameSpy).toHaveBeenCalledTimes(0); + expect(processZigbeeNWKGPCommandFrameSpy).toHaveBeenCalledTimes(1); + expect(emitSpy).toHaveBeenCalledWith( + "FRAME", + 0x0155f47a & 0xffff, + undefined, + { + frameControl: { + frameType: 0x1, + deliveryMode: 0x2, + ackFormat: false, + security: false, + ackRequest: false, + extendedHeader: false, + }, + group: ZigbeeConsts.GP_GROUP_ID, + profileId: ZigbeeConsts.GP_PROFILE_ID, + clusterId: ZigbeeConsts.GP_CLUSTER_ID, + destEndpoint: ZigbeeConsts.GP_ENDPOINT, + sourceEndpoint: ZigbeeConsts.GP_ENDPOINT, + }, + Buffer.from([ + 1, 70, 4, 0, 0, 122, 244, 85, 1, 0, 0, 0, 0, 0xe0, 51, 0x2, 0x85, 0xf2, 0xc9, 0x25, 0x82, 0x1d, 0xf4, 0x6f, 0x45, 0x8c, 0xf0, + 0xe6, 0x37, 0xaa, 0xc3, 0xba, 0xb6, 0xaa, 0x45, 0x83, 0x1a, 0x11, 0x46, 0x23, 0x0, 0x0, 0x4, 0x16, 0x10, 0x11, 0x22, 0x23, 0x18, + 0x19, 0x14, 0x15, 0x12, 0x13, 0x64, 0x65, 0x62, 0x63, 0x1e, 0x1f, 0x1c, 0x1d, 0x1a, 0x1b, 0x16, 0x17, + ]), + 0, // rssi + ); + }); + }); + + describe("NET2", () => { + beforeEach(async () => { + driver = new OTRCPDriver( + { + txChannel: A_CHANNEL, + ccaBackoffAttempts: 1, + ccaRetries: 4, + enableCSMACA: true, + headerUpdated: true, + reTx: false, + securityProcessed: true, + txDelay: 0, + txDelayBaseTime: 0, + rxChannelAfterTxDone: A_CHANNEL, + }, + { + eui64: NET2_COORD_EUI64_BIGINT, + panId: NET2_PAN_ID, + extendedPANId: Buffer.from(NET2_EXTENDED_PAN_ID).readBigUInt64LE(0), + channel: A_CHANNEL, + nwkUpdateId: 0, + txPower: 5, + networkKey: Buffer.from(NETDEF_NETWORK_KEY), + networkKeyFrameCounter: 0, + networkKeySequenceNumber: 0, + tcKey: Buffer.from(NETDEF_TC_KEY), + tcKeyFrameCounter: 0, + }, + SAVE_DIR, + // true, // emitMACFrames + ); + await driver.loadState(); + driver.parser.on("data", driver.onFrame.bind(driver)); + + // @ts-expect-error mock override + driver.networkUp = true; + }); + + afterEach(() => { + driver.deviceTable.clear(); + driver.address16ToAddress64.clear(); + }); + + it("receives frame NET2_TRANSPORT_KEY_NWK_FROM_COORD - not for coordinator", async () => { + // encrypted only APS + const onStreamRawFrameSpy = vi.spyOn(driver, "onStreamRawFrame"); + const onZigbeeAPSACKRequestSpy = vi.spyOn(driver, "onZigbeeAPSACKRequest"); + const onZigbeeAPSFrameSpy = vi.spyOn(driver, "onZigbeeAPSFrame"); + const processZigbeeAPSTransportKeySpy = vi.spyOn(driver, "processZigbeeAPSTransportKey"); + + driver.parser._transform(makeSpinelStreamRaw(1, NET2_TRANSPORT_KEY_NWK_FROM_COORD), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(onStreamRawFrameSpy).toHaveBeenCalledTimes(1); + expect(onZigbeeAPSACKRequestSpy).toHaveBeenCalledTimes(0); + expect(onZigbeeAPSFrameSpy).toHaveBeenCalledTimes(0); + expect(processZigbeeAPSTransportKeySpy).toHaveBeenCalledTimes(0); + }); + + it("receives frame NET2_REQUEST_KEY_TC_FROM_DEVICE", async () => { + // encrypted at NWK+APS + const source64 = BigInt("0xa4c1386d9b280fdf"); + driver.deviceTable.set(source64, { + address16: 0xa18f, + rxOnWhenIdle: true, + authorized: false, + }); + driver.address16ToAddress64.set(0xa18f, source64); + + const onStreamRawFrameSpy = vi.spyOn(driver, "onStreamRawFrame"); + const onZigbeeAPSACKRequestSpy = vi.spyOn(driver, "onZigbeeAPSACKRequest"); + const onZigbeeAPSFrameSpy = vi.spyOn(driver, "onZigbeeAPSFrame"); + const processZigbeeAPSRequestKeySpy = vi.spyOn(driver, "processZigbeeAPSRequestKey"); + const sendZigbeeAPSTransportKeyTCSpy = vi.spyOn(driver, "sendZigbeeAPSTransportKeyTC"); + + driver.parser._transform(makeSpinelStreamRaw(1, NET2_REQUEST_KEY_TC_FROM_DEVICE), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + driver.parser._transform(makeSpinelLastStatus(1), "utf8", () => {}); + + expect(onStreamRawFrameSpy).toHaveBeenCalledTimes(1); + expect(onZigbeeAPSACKRequestSpy).toHaveBeenCalledTimes(0); + expect(onZigbeeAPSFrameSpy).toHaveBeenCalledTimes(1); + expect(processZigbeeAPSRequestKeySpy).toHaveBeenCalledTimes(1); + expect(sendZigbeeAPSTransportKeyTCSpy).toHaveBeenCalledTimes(1); + }); + + it("tries to join while not allowed", async () => { + // Expected flow: + // - NET2_BEACON_REQ_FROM_DEVICE + // - NET2_BEACON_RESP_FROM_COORD + // - NET2_ASSOC_REQ_FROM_DEVICE + // - NET2_ASSOC_RESP_FROM_COORD + const sendMACFrameSpy = vi.spyOn(driver, "sendMACFrame"); + const sendMACAssocRspSpy = vi.spyOn(driver, "sendMACAssocRsp"); + + driver.parser._transform(makeSpinelStreamRaw(100, NET2_BEACON_REQ_FROM_DEVICE), "utf8", () => {}); + driver.parser._transform(makeSpinelLastStatus(1), "utf8", () => {}); + + expect(sendMACFrameSpy).toHaveBeenCalledTimes(1); + const beaconRespFrame = sendMACFrameSpy.mock.calls[0][1]; + const [decBeaconRespFCF, decBeaconRespFCFOffset] = decodeMACFrameControl(beaconRespFrame, 0); + const [decBeaconRespHeader] = decodeMACHeader(beaconRespFrame, decBeaconRespFCFOffset, decBeaconRespFCF); + + expect(decBeaconRespHeader.superframeSpec?.associationPermit).toStrictEqual(false); + + driver.parser._transform(makeSpinelStreamRaw(101, NET2_ASSOC_REQ_FROM_DEVICE), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + // ASSOC_RSP => OK + driver.parser._transform(makeSpinelLastStatus(2), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(sendMACAssocRspSpy).toHaveBeenCalledTimes(1); + expect(sendMACAssocRspSpy).toHaveBeenCalledWith(11871832136131022815n, 0xffff, MACAssociationStatus.PAN_ACCESS_DENIED); + }); + + it("performs a join & authorize - ROUTER", async () => { + setupWireshark(); + // Expected flow (APS acks requested from device are skipped for brevity): + // - NET2_BEACON_REQ_FROM_DEVICE + // - NET2_BEACON_RESP_FROM_COORD + // - NET2_ASSOC_REQ_FROM_DEVICE + // - NET2_ASSOC_RESP_FROM_COORD + // - NET2_TRANSPORT_KEY_NWK_FROM_COORD + // - NET2_DEVICE_ANNOUNCE_BCAST + // - NET2_NODE_DESC_REQ_FROM_DEVICE + // - NET2_REQUEST_KEY_TC_FROM_DEVICE + // - NET2_TRANSPORT_KEY_TC_FROM_COORD + // - NET2_VERIFY_KEY_TC_FROM_DEVICE + // - NET2_CONFIRM_KEY_TC_SUCCESS + driver.allowJoins(0xfe, true); + + const emitSpy = vi.spyOn(driver, "emit"); + const sendMACFrameSpy = vi.spyOn(driver, "sendMACFrame"); + const sendMACAssocRspSpy = vi.spyOn(driver, "sendMACAssocRsp"); + const sendZigbeeAPSTransportKeyNWKSpy = vi.spyOn(driver, "sendZigbeeAPSTransportKeyNWK"); + vi.spyOn(driver, "assignNetworkAddress").mockReturnValueOnce(0xa18f); // force nwk16 matching vectors + + driver.parser._transform(makeSpinelStreamRaw(100, NET2_BEACON_REQ_FROM_DEVICE), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + // BEACON_RSP => OK + driver.parser._transform(makeSpinelLastStatus(1), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(sendMACFrameSpy).toHaveBeenCalledTimes(1); + const beaconRespFrame = sendMACFrameSpy.mock.calls[0][1]; + const [decBeaconRespFCF, decBeaconRespFCFOffset] = decodeMACFrameControl(beaconRespFrame, 0); + const [decBeaconRespHeader] = decodeMACHeader(beaconRespFrame, decBeaconRespFCFOffset, decBeaconRespFCF); + + expect(decBeaconRespHeader.superframeSpec?.associationPermit).toStrictEqual(true); + + driver.parser._transform(makeSpinelStreamRaw(101, NET2_ASSOC_REQ_FROM_DEVICE), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + // ASSOC_RSP => OK + driver.parser._transform(makeSpinelLastStatus(2), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + // TRANSPORT_KEY NWK => OK + driver.parser._transform(makeSpinelLastStatus(3), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(sendMACAssocRspSpy).toHaveBeenCalledTimes(1); + expect(sendMACAssocRspSpy).toHaveBeenCalledWith(11871832136131022815n, 0xa18f, MACAssociationStatus.SUCCESS); + expect(sendZigbeeAPSTransportKeyNWKSpy).toHaveBeenCalledTimes(1); + expect(driver.deviceTable.get(11871832136131022815n)).toStrictEqual({ + address16: 0xa18f, + rxOnWhenIdle: true, + authorized: false, + }); + + driver.parser._transform(makeSpinelStreamRaw(102, NET2_DEVICE_ANNOUNCE_BCAST), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(emitSpy).toHaveBeenCalledWith("DEVICE_JOINED", 0xa18f, 11871832136131022815n); + expect(emitSpy).toHaveBeenCalledWith("FRAME", 0xa18f, undefined, expect.any(Object), expect.any(Buffer), 0 /* rssi */); + + driver.parser._transform(makeSpinelStreamRaw(103, NET2_NODE_DESC_REQ_FROM_DEVICE), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + // node desc APS ACK => OK + driver.parser._transform(makeSpinelLastStatus(4), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + // node desc RESP => OK + driver.parser._transform(makeSpinelLastStatus(5), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + driver.parser._transform(makeSpinelStreamRaw(104, NET2_REQUEST_KEY_TC_FROM_DEVICE), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + // TRANSPORT_KEY TC => OK + driver.parser._transform(makeSpinelLastStatus(6), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + driver.parser._transform(makeSpinelStreamRaw(105, NET2_VERIFY_KEY_TC_FROM_DEVICE), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + // CONFIRM_KEY => OK + driver.parser._transform(makeSpinelLastStatus(7), "utf8", () => {}); + await vi.advanceTimersByTimeAsync(10); + + expect(driver.deviceTable.get(11871832136131022815n)).toStrictEqual({ + address16: 0xa18f, + rxOnWhenIdle: true, + authorized: true, + }); + }); + + // it("performs a join & authorize - END DEVICE", async () => { + // // TODO: with DATA req (indirect transmission) + // }); + }); +}); diff --git a/test/spinel.test.ts b/test/spinel.test.ts new file mode 100644 index 0000000..b65473d --- /dev/null +++ b/test/spinel.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it, vi } from "vitest"; +import { SpinelCommandId } from "../src/spinel/commands.js"; +import * as Hdlc from "../src/spinel/hdlc.js"; +import { SpinelPropertyId } from "../src/spinel/properties.js"; +import { + type SpinelFrame, + decodeSpinelFrame, + encodeSpinelFrame, + getPackedUInt, + readPropertyE, + readPropertyS, + readPropertyStreamRaw, + setPackedUInt, + writePropertyE, + writePropertyS, +} from "../src/spinel/spinel.js"; +import { SpinelStatus } from "../src/spinel/statuses.js"; + +describe("Spinel & HDLC", () => { + const encodeHdlcFrameSpy = vi.spyOn(Hdlc, "encodeHdlcFrame"); + + /** see https://datatracker.ietf.org/doc/html/draft-rquattle-spinel-unified#appendix-B.1 */ + const PACKED_UINT21_TEST_VECTORS: [number, string][] = [ + [0, "00"], + [1, "01"], + [127, "7f"], + [128, "8001"], + [129, "8101"], + [1337, "b90a"], + [16383, "ff7f"], + [16384, "808001"], + [16385, "818001"], + [2097151, "ffff7f"], + ]; + + for (const [dec, hex] of PACKED_UINT21_TEST_VECTORS) { + it(`writes Packed Unsigned Integer: ${dec} => ${hex}`, () => { + const buffer = Buffer.from("0ba124f50000000000", "hex"); + let offset = 4; + offset = setPackedUInt(buffer, offset, dec); + + expect(offset).toStrictEqual(4 + hex.length / 2); + expect(buffer.toString("hex").startsWith(`0ba124f5${hex}`)).toStrictEqual(true); + }); + + it(`reads Packed Unsigned Integer: ${hex} => ${dec}`, () => { + const buffer = Buffer.from(`0ba124f5${hex}4f2f6b7caa`, "hex"); + let offset = 4; + + const [val, newOffset] = getPackedUInt(buffer, offset); + offset = newOffset; + + expect(offset).toStrictEqual(4 + hex.length / 2); + expect(val).toStrictEqual(dec); + }); + } + + it("writePropertyE & readPropertyE", () => { + const buf = writePropertyE(SpinelPropertyId.MAC_15_4_LADDR, 123n); + + expect(buf).toStrictEqual(Buffer.from([52, 0, 0, 0, 0, 0, 0, 0, 123])); + + const val = readPropertyE(SpinelPropertyId.MAC_15_4_LADDR, buf); + + expect(val).toStrictEqual(123n); + }); + + it("writePropertyS & readPropertyS", () => { + const buf = writePropertyS(SpinelPropertyId.MAC_15_4_PANID, 43993); + + expect(buf).toStrictEqual(Buffer.from([54, 217, 171])); + + const val = readPropertyS(SpinelPropertyId.MAC_15_4_PANID, buf); + + expect(val).toStrictEqual(43993); + }); + + /** see https://datatracker.ietf.org/doc/html/draft-rquattle-spinel-unified#appendix-B.2 */ + const RESET_COMMAND_TEST_VECTOR_FRAME: SpinelFrame = { + header: { tid: 0, nli: 0, flg: 2 }, + commandId: SpinelCommandId.RESET, + payload: Buffer.alloc(0), + }; + const RESET_COMMAND_TEST_VECTOR_HEX = "8001"; + /** verified with code from https://github.com/openthread/openthread/tree/main/src/lib/hdlc */ + const RESET_COMMAND_TEST_VECTOR_HDLC_HEX = "7e800102927e"; + + it("writes Reset Command to HDLC", () => { + const encFrame = encodeSpinelFrame(RESET_COMMAND_TEST_VECTOR_FRAME); + + expect(encFrame).toBeDefined(); + expect(encodeHdlcFrameSpy).toHaveBeenCalledTimes(1); + expect(encodeHdlcFrameSpy.mock.calls[0][0].toString("hex")).toStrictEqual(RESET_COMMAND_TEST_VECTOR_HEX); + expect(encFrame.length).toStrictEqual(6); + expect(encFrame.data.subarray(0, encFrame.length)).toStrictEqual(Buffer.from(RESET_COMMAND_TEST_VECTOR_HDLC_HEX, "hex")); + expect(encFrame.fcs).toStrictEqual(Hdlc.HDLC_GOOD_FCS); + }); + + /** see https://datatracker.ietf.org/doc/html/draft-rquattle-spinel-unified#appendix-B.3 */ + const RESET_NOTIFICATION_TEST_VECTOR_FRAME: SpinelFrame = { + header: { tid: 0, nli: 0, flg: 2 }, + commandId: SpinelCommandId.PROP_VALUE_IS, + // these are technically packed uint21 but we know values are uint8, we can cheat + payload: Buffer.from([SpinelPropertyId.LAST_STATUS, SpinelStatus.RESET_SOFTWARE]), + }; + const RESET_NOTIFICATION_TEST_VECTOR_HEX = "80060072"; + /** verified with code from https://github.com/openthread/openthread/tree/main/src/lib/hdlc */ + const RESET_NOTIFICATION_TEST_VECTOR_HDLC_HEX = "7e80060072fc577e"; + + it("reads Reset Notification from HDLC", () => { + const decHdlcFrame = Hdlc.decodeHdlcFrame(Buffer.from(RESET_NOTIFICATION_TEST_VECTOR_HDLC_HEX, "hex")); + + expect(decHdlcFrame.length).toStrictEqual(RESET_NOTIFICATION_TEST_VECTOR_HEX.length / 2); + expect(decHdlcFrame.data.subarray(0, decHdlcFrame.length)).toStrictEqual(Buffer.from(RESET_NOTIFICATION_TEST_VECTOR_HEX, "hex")); + expect(decHdlcFrame.fcs).toStrictEqual(Hdlc.HDLC_GOOD_FCS); + + const decFrame = decodeSpinelFrame(decHdlcFrame); + + expect(decFrame).toStrictEqual(RESET_NOTIFICATION_TEST_VECTOR_FRAME); + }); + + it("reads Spinel STREAM_RAW metadata", () => { + const payload = Buffer.from([ + 0x7e, 0x80, 0x06, 0x71, 0x0a, 0x00, 0x03, 0x08, 0xd0, 0xff, 0xff, 0xff, 0xff, 0x07, 0xff, 0xcc, 0xd7, 0x80, 0x00, 0x00, 0x0a, 0x00, 0x19, + 0xff, 0xc3, 0x0c, 0xc7, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0xe9, 0x7e, + ]); + const decHdlcFrame = Hdlc.decodeHdlcFrame(payload); + const decFrame = decodeSpinelFrame(decHdlcFrame); + const [macData, metadata] = readPropertyStreamRaw(decFrame.payload, 1); + + expect(macData).toStrictEqual(Buffer.from([0x03, 0x08, 0xd0, 0xff, 0xff, 0xff, 0xff, 0x07, 0xff, 0xcc])); + expect(metadata).toStrictEqual({ rssi: -41, noiseFloor: -128, flags: 0 }); + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..cb13697 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig", + "include": ["./**/*", "vitest.config.mts"], + "compilerOptions": { + "rootDir": "..", + "noEmit": true + }, + "references": [{ "path": ".." }] +} diff --git a/test/vitest.config.mts b/test/vitest.config.mts new file mode 100644 index 0000000..7b2b3a4 --- /dev/null +++ b/test/vitest.config.mts @@ -0,0 +1,27 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + onConsoleLog() { + return false; + }, + coverage: { + enabled: false, + provider: "v8", + include: ["src/**"], + exclude: ["src/dev/**"], + extension: [".ts"], + // exclude: [], + clean: true, + cleanOnRerun: true, + reportsDirectory: "coverage", + reporter: ["text", "html"], + reportOnFailure: false, + thresholds: { + 100: true, + }, + }, + // https://github.com/vitest-dev/vitest/issues/7285 + reporters: [["default", { summary: false }]], + }, +}); diff --git a/test/wireshark.test.ts b/test/wireshark.test.ts new file mode 100644 index 0000000..e8e1588 --- /dev/null +++ b/test/wireshark.test.ts @@ -0,0 +1,29 @@ +import { createSocket } from "node:dgram"; +import { describe, it } from "vitest"; +import { DEFAULT_WIRESHARK_IP, DEFAULT_ZEP_UDP_PORT, createWiresharkZEPFrame } from "../src/dev/wireshark"; + +/** + * Util for quick triggering of "send frame to wireshark", not an actual test. + */ +describe.skip("Send to Wireshark", () => { + let wiresharkSeqNum = 0; + + const nextWiresharkSeqNum = (): number => { + wiresharkSeqNum = (wiresharkSeqNum + 1) & 0xffffffff; + + return wiresharkSeqNum + 1; + }; + + it("send", () => { + const wiresharkSocket = createSocket("udp4"); + wiresharkSocket.bind(DEFAULT_ZEP_UDP_PORT); + + const buf = Buffer.from([]); + const wsZEPFrame = createWiresharkZEPFrame(15, 1, 0, 0, nextWiresharkSeqNum(), buf); + + console.log(wsZEPFrame.toString("hex")); + wiresharkSocket.send(wsZEPFrame, DEFAULT_ZEP_UDP_PORT, DEFAULT_WIRESHARK_IP, () => { + wiresharkSocket.close(); + }); + }); +}); diff --git a/test/zigbee.bench.ts b/test/zigbee.bench.ts new file mode 100644 index 0000000..80cc1ce --- /dev/null +++ b/test/zigbee.bench.ts @@ -0,0 +1,417 @@ +import { + decodeMACFrameControl, + decodeMACHeader, + decodeMACPayload, + decodeMACZigbeeBeacon, + encodeMACFrame, + encodeMACFrameZigbee, +} from "src/zigbee/mac"; +import { decodeZigbeeAPSFrameControl, decodeZigbeeAPSHeader, decodeZigbeeAPSPayload, encodeZigbeeAPSFrame } from "src/zigbee/zigbee-aps"; +import { decodeZigbeeNWKFrameControl, decodeZigbeeNWKHeader, decodeZigbeeNWKPayload, encodeZigbeeNWKFrame } from "src/zigbee/zigbee-nwk"; +import { decodeZigbeeNWKGPFrameControl, decodeZigbeeNWKGPHeader, decodeZigbeeNWKGPPayload, encodeZigbeeNWKGPFrame } from "src/zigbee/zigbee-nwkgp"; +import { bench, describe } from "vitest"; +import { ZigbeeKeyType, makeKeyedHashByType, registerDefaultHashedKeys } from "../src/zigbee/zigbee"; +import { + NET2_ASSOC_REQ_FROM_DEVICE, + NET2_ASSOC_RESP_FROM_COORD, + NET2_BEACON_REQ_FROM_DEVICE, + NET2_BEACON_RESP_FROM_COORD, + NET2_COORD_EUI64_BIGINT, + NET2_DEVICE_LEAVE_BROADCAST, + NET2_REQUEST_KEY_TC_FROM_DEVICE, + NET2_TRANSPORT_KEY_NWK_FROM_COORD, + NETDEF_ACK_FRAME_FROM_COORD, + NETDEF_ACK_FRAME_TO_COORD, + NETDEF_LINK_STATUS_FROM_DEV, + NETDEF_MTORR_FRAME_FROM_COORD, + NETDEF_NETWORK_KEY, + NETDEF_ROUTE_RECORD_TO_COORD, + NETDEF_TC_KEY, + NETDEF_ZCL_FRAME_CMD_TO_COORD, + NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD, + NETDEF_ZGP_COMMISSIONING, + NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0, +} from "./data"; + +describe("Zigbee", () => { + registerDefaultHashedKeys( + makeKeyedHashByType(ZigbeeKeyType.LINK, NETDEF_TC_KEY), + makeKeyedHashByType(ZigbeeKeyType.NWK, NETDEF_NETWORK_KEY), + makeKeyedHashByType(ZigbeeKeyType.TRANSPORT, NETDEF_TC_KEY), + makeKeyedHashByType(ZigbeeKeyType.LOAD, NETDEF_TC_KEY), + ); + + bench( + "NETDEF_ACK_FRAME_TO_COORD", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ACK_FRAME_TO_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ACK_FRAME_TO_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ACK_FRAME_TO_COORD, macHOutOffset, macFCF, macHeader); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, undefined, apsFCF, apsHeader); + + const encMACHeader = structuredClone(macHeader); + encMACHeader.sourcePANId = undefined; + + encodeMACFrameZigbee(encMACHeader, macPayload); + + const encNWKHeader = structuredClone(nwkHeader); + + encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + const encAPSHeader = structuredClone(apsHeader); + + encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + }, + { warmupTime: 1000 }, + ); + + bench( + "NETDEF_ACK_FRAME_FROM_COORD", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ACK_FRAME_FROM_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ACK_FRAME_FROM_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ACK_FRAME_FROM_COORD, macHOutOffset, macFCF, macHeader); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, undefined, apsFCF, apsHeader); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + encodeMACFrameZigbee(encHeader, macPayload); + + const encNWKHeader = structuredClone(nwkHeader); + + encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + const encAPSHeader = structuredClone(apsHeader); + + encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + }, + { warmupTime: 1000 }, + ); + + bench( + "NETDEF_LINK_STATUS_FROM_DEV", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_LINK_STATUS_FROM_DEV, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_LINK_STATUS_FROM_DEV, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_LINK_STATUS_FROM_DEV, macHOutOffset, macFCF, macHeader); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + encodeMACFrameZigbee(encHeader, macPayload); + + const encNWKHeader = structuredClone(nwkHeader); + + encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + }, + { warmupTime: 1000 }, + ); + + bench( + "NETDEF_ZCL_FRAME_CMD_TO_COORD", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ZCL_FRAME_CMD_TO_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ZCL_FRAME_CMD_TO_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ZCL_FRAME_CMD_TO_COORD, macHOutOffset, macFCF, macHeader); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, undefined, apsFCF, apsHeader); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + encodeMACFrameZigbee(encHeader, macPayload); + + const encNWKHeader = structuredClone(nwkHeader); + + encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + const encAPSHeader = structuredClone(apsHeader); + + encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + }, + { warmupTime: 1000 }, + ); + + bench( + "NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD, macHOutOffset, macFCF, macHeader); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, undefined, apsFCF, apsHeader); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + encodeMACFrameZigbee(encHeader, macPayload); + + const encNWKHeader = structuredClone(nwkHeader); + + encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + const encAPSHeader = structuredClone(apsHeader); + + encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + }, + { warmupTime: 1000 }, + ); + + bench( + "NETDEF_ROUTE_RECORD_TO_COORD", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ROUTE_RECORD_TO_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ROUTE_RECORD_TO_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ROUTE_RECORD_TO_COORD, macHOutOffset, macFCF, macHeader); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + + // TODO Zigbee NWK cmd + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + encodeMACFrameZigbee(encHeader, macPayload); + + const encNWKHeader = structuredClone(nwkHeader); + + encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + }, + { warmupTime: 1000 }, + ); + + bench( + "NETDEF_MTORR_FRAME_FROM_COORD", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_MTORR_FRAME_FROM_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_MTORR_FRAME_FROM_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_MTORR_FRAME_FROM_COORD, macHOutOffset, macFCF, macHeader); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + + // TODO Zigbee NWK cmd + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + encodeMACFrameZigbee(encHeader, macPayload); + + const encNWKHeader = structuredClone(nwkHeader); + + encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + }, + { warmupTime: 1000 }, + ); + + bench( + "NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0, macHOutOffset, macFCF, macHeader); + + const [nwkGPFCF, nwkGPFCFOutOffset] = decodeZigbeeNWKGPFrameControl(macPayload, 0); + const [nwkGPHeader, nwkGPHOutOffset] = decodeZigbeeNWKGPHeader(macPayload, nwkGPFCFOutOffset, nwkGPFCF); + const nwkGPPayload = decodeZigbeeNWKGPPayload(macPayload, nwkGPHOutOffset, NETDEF_NETWORK_KEY, macHeader.source64, nwkGPFCF, nwkGPHeader); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + encodeMACFrameZigbee(encHeader, macPayload); + + const encNWKGPHeader = structuredClone(nwkGPHeader); + + encodeZigbeeNWKGPFrame(encNWKGPHeader, nwkGPPayload, NETDEF_NETWORK_KEY, macHeader.source64); + }, + { warmupTime: 1000 }, + ); + + bench( + "NETDEF_ZGP_COMMISSIONING", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ZGP_COMMISSIONING, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ZGP_COMMISSIONING, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ZGP_COMMISSIONING, macHOutOffset, macFCF, macHeader); + + const [nwkGPFCF, nwkGPFCFOutOffset] = decodeZigbeeNWKGPFrameControl(macPayload, 0); + const [nwkGPHeader, nwkGPHOutOffset] = decodeZigbeeNWKGPHeader(macPayload, nwkGPFCFOutOffset, nwkGPFCF); + const nwkGPPayload = decodeZigbeeNWKGPPayload(macPayload, nwkGPHOutOffset, NETDEF_NETWORK_KEY, macHeader.source64, nwkGPFCF, nwkGPHeader); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + encodeMACFrameZigbee(encHeader, macPayload); + + const encNWKGPHeader = structuredClone(nwkGPHeader); + + encodeZigbeeNWKGPFrame(encNWKGPHeader, nwkGPPayload, NETDEF_NETWORK_KEY, macHeader.source64); + }, + { warmupTime: 1000 }, + ); + + bench( + "NET2_DEVICE_LEAVE_BROADCAST", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_DEVICE_LEAVE_BROADCAST, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_DEVICE_LEAVE_BROADCAST, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_DEVICE_LEAVE_BROADCAST, macHOutOffset, macFCF, macHeader); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + encodeMACFrameZigbee(encHeader, macPayload); + + const encNWKHeader = structuredClone(nwkHeader); + + encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + }, + { warmupTime: 1000 }, + ); + + bench( + "NET2_BEACON_REQ_FROM_DEVICE", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_BEACON_REQ_FROM_DEVICE, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_BEACON_REQ_FROM_DEVICE, macFCFOutOffset, macFCF); + decodeMACPayload(NET2_BEACON_REQ_FROM_DEVICE, macHOutOffset, macFCF, macHeader); + }, + { warmupTime: 1000 }, + ); + + bench( + "NET2_BEACON_RESP_FROM_COORD", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_BEACON_RESP_FROM_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_BEACON_RESP_FROM_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_BEACON_RESP_FROM_COORD, macHOutOffset, macFCF, macHeader); + decodeMACZigbeeBeacon(macPayload, 0); + }, + { warmupTime: 1000 }, + ); + + bench( + "NET2_ASSOC_REQ_FROM_DEVICE", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_ASSOC_REQ_FROM_DEVICE, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_ASSOC_REQ_FROM_DEVICE, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_ASSOC_REQ_FROM_DEVICE, macHOutOffset, macFCF, macHeader); + + const encHeader = structuredClone(macHeader); + + encodeMACFrame(encHeader, macPayload); + }, + { warmupTime: 1000 }, + ); + + bench( + "NET2_ASSOC_RESP_FROM_COORD", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_ASSOC_RESP_FROM_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_ASSOC_RESP_FROM_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_ASSOC_RESP_FROM_COORD, macHOutOffset, macFCF, macHeader); + + const encHeader = structuredClone(macHeader); + + encodeMACFrame(encHeader, macPayload); + }, + { warmupTime: 1000 }, + ); + + bench( + "NET2_TRANSPORT_KEY_NWK_FROM_COORD", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_TRANSPORT_KEY_NWK_FROM_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_TRANSPORT_KEY_NWK_FROM_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_TRANSPORT_KEY_NWK_FROM_COORD, macHOutOffset, macFCF, macHeader); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, NET2_COORD_EUI64_BIGINT, apsFCF, apsHeader); + + const encMACHeader = structuredClone(macHeader); + encMACHeader.sourcePANId = undefined; + + encodeMACFrameZigbee(encMACHeader, macPayload); + + const encNWKHeader = structuredClone(nwkHeader); + + encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + const encAPSHeader = structuredClone(apsHeader); + + encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + }, + { warmupTime: 1000 }, + ); + + bench( + "NET2_REQUEST_KEY_TC_FROM_DEVICE", + () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_REQUEST_KEY_TC_FROM_DEVICE, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_REQUEST_KEY_TC_FROM_DEVICE, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_REQUEST_KEY_TC_FROM_DEVICE, macHOutOffset, macFCF, macHeader); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, NET2_COORD_EUI64_BIGINT, apsFCF, apsHeader); + + const encMACHeader = structuredClone(macHeader); + encMACHeader.sourcePANId = undefined; + encodeMACFrameZigbee(encMACHeader, macPayload); + + const encNWKHeader = structuredClone(nwkHeader); + encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + const encAPSHeader = structuredClone(apsHeader); + encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + }, + { warmupTime: 1000 }, + ); +}); diff --git a/test/zigbee.test.ts b/test/zigbee.test.ts new file mode 100644 index 0000000..6b10e6f --- /dev/null +++ b/test/zigbee.test.ts @@ -0,0 +1,1559 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { + type MACHeader, + type MACZigbeeBeacon, + decodeMACFrameControl, + decodeMACHeader, + decodeMACPayload, + decodeMACZigbeeBeacon, + encodeMACFrame, + encodeMACFrameZigbee, + encodeMACZigbeeBeacon, +} from "../src/zigbee/mac.js"; +import { + type ZigbeeAPSHeader, + decodeZigbeeAPSFrameControl, + decodeZigbeeAPSHeader, + decodeZigbeeAPSPayload, + encodeZigbeeAPSFrame, +} from "../src/zigbee/zigbee-aps.js"; +import { + type ZigbeeNWKHeader, + decodeZigbeeNWKFrameControl, + decodeZigbeeNWKHeader, + decodeZigbeeNWKPayload, + encodeZigbeeNWKFrame, +} from "../src/zigbee/zigbee-nwk.js"; +import { + type ZigbeeNWKGPHeader, + decodeZigbeeNWKGPFrameControl, + decodeZigbeeNWKGPHeader, + decodeZigbeeNWKGPPayload, + encodeZigbeeNWKGPFrame, +} from "../src/zigbee/zigbee-nwkgp.js"; +import { ZigbeeKeyType, makeKeyedHashByType, registerDefaultHashedKeys } from "../src/zigbee/zigbee.js"; +import { + NET2_ASSOC_REQ_FROM_DEVICE, + NET2_ASSOC_RESP_FROM_COORD, + NET2_BEACON_REQ_FROM_DEVICE, + NET2_BEACON_RESP_FROM_COORD, + NET2_COORD_EUI64_BIGINT, + NET2_DEVICE_LEAVE_BROADCAST, + NET2_REQUEST_KEY_TC_FROM_DEVICE, + NET2_TRANSPORT_KEY_NWK_FROM_COORD, + NETDEF_ACK_FRAME_FROM_COORD, + NETDEF_ACK_FRAME_TO_COORD, + NETDEF_LINK_STATUS_FROM_DEV, + NETDEF_MTORR_FRAME_FROM_COORD, + NETDEF_NETWORK_KEY, + NETDEF_ROUTE_RECORD_TO_COORD, + NETDEF_TC_KEY, + NETDEF_ZCL_FRAME_CMD_TO_COORD, + NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD, + NETDEF_ZGP_COMMISSIONING, + NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0, +} from "./data.js"; + +describe("Zigbee", () => { + beforeAll(() => { + registerDefaultHashedKeys( + makeKeyedHashByType(ZigbeeKeyType.LINK, NETDEF_TC_KEY), + makeKeyedHashByType(ZigbeeKeyType.NWK, NETDEF_NETWORK_KEY), + makeKeyedHashByType(ZigbeeKeyType.TRANSPORT, NETDEF_TC_KEY), + makeKeyedHashByType(ZigbeeKeyType.LOAD, NETDEF_TC_KEY), + ); + }); + + it("NETDEF_ACK_FRAME_TO_COORD", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ACK_FRAME_TO_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ACK_FRAME_TO_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ACK_FRAME_TO_COORD, macHOutOffset, macFCF, macHeader); + const expectedHeader: MACHeader = { + frameControl: { + frameType: 1, + securityEnabled: false, + framePending: false, + ackRequest: true, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 2, + frameVersion: 0, + sourceAddrMode: 2, + }, + sequenceNumber: 191, + destinationPANId: 0x1a62, + destination16: 0x0000, + destination64: undefined, + sourcePANId: 0x1a62, + source16: 0x96ba, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xb4ab, + }; + + expect(macHeader).toStrictEqual(expectedHeader); + expect(macPayload.byteLength).toStrictEqual(34); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + const expectedNWKHeader: ZigbeeNWKHeader = { + frameControl: { + frameType: 0, + protocolVersion: 2, + discoverRoute: 1, + multicast: false, + security: true, + sourceRoute: false, + extendedDestination: false, + extendedSource: false, + endDeviceInitiator: false, + }, + destination16: 0x0000, + source16: 0x96ba, + radius: 30, + seqNum: 151, + destination64: undefined, + source64: undefined, + multicastControl: undefined, + relayIndex: undefined, + relayAddresses: undefined, + securityHeader: { + control: { + keyId: 1, + level: 5, + nonce: true, + }, + frameCounter: 45318893, + keySeqNum: 0, + micLen: 4, + source64: 9244571720527165811n, + }, + }; + + expect(nwkHeader).toStrictEqual(expectedNWKHeader); + expect(nwkPayload).toStrictEqual(Buffer.from([0x02, 0x01, 0x00, 0xef, 0x04, 0x01, 0x01, 0x33])); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, undefined, apsFCF, apsHeader); + + const expectedAPSHeader: ZigbeeAPSHeader = { + frameControl: { + frameType: 0x2, + deliveryMode: 0x0, + ackFormat: false, + security: false, + ackRequest: false, + extendedHeader: false, + }, + destEndpoint: 1, + group: undefined, + clusterId: 0xef00, + profileId: 0x0104, + sourceEndpoint: 1, + counter: 51, + fragmentation: undefined, + fragBlockNumber: undefined, + fragACKBitfield: undefined, + securityHeader: undefined, + }; + + expect(apsHeader).toStrictEqual(expectedAPSHeader); + expect(apsPayload).toStrictEqual(Buffer.from([])); + + const encMACHeader = structuredClone(macHeader); + encMACHeader.sourcePANId = undefined; + const encMACFrame = encodeMACFrameZigbee(encMACHeader, macPayload); + + expect(encMACFrame.subarray(0, -2)).toStrictEqual(NETDEF_ACK_FRAME_TO_COORD.subarray(0, -2)); + + const encNWKHeader = structuredClone(nwkHeader); + encNWKHeader.securityHeader!.control.level = 0; + const encNWKFrame = encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + expect(encNWKFrame).toStrictEqual(macPayload); + + const encAPSHeader = structuredClone(apsHeader); + const encAPSFrame = encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + + expect(encAPSFrame).toStrictEqual(nwkPayload); + }); + + it("NETDEF_ACK_FRAME_FROM_COORD", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ACK_FRAME_FROM_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ACK_FRAME_FROM_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ACK_FRAME_FROM_COORD, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 1, + securityEnabled: false, + framePending: false, + ackRequest: true, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 2, + frameVersion: 0, + sourceAddrMode: 2, + }, + sequenceNumber: 73, + destinationPANId: 0x1a62, + destination16: 0x87c6, + destination64: undefined, + sourcePANId: 0x1a62, + source16: 0x0000, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xf4cb, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(34); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + const expectedNWKHeader: ZigbeeNWKHeader = { + frameControl: { + frameType: 0, + protocolVersion: 2, + discoverRoute: 1, + multicast: false, + security: true, + sourceRoute: false, + extendedDestination: false, + extendedSource: false, + endDeviceInitiator: false, + }, + destination16: 0x96ba, + source16: 0x0000, + radius: 30, + seqNum: 203, + destination64: undefined, + source64: undefined, + multicastControl: undefined, + relayIndex: undefined, + relayAddresses: undefined, + securityHeader: { + control: { + keyId: 1, + level: 5, + nonce: true, + }, + frameCounter: 99044312, + keySeqNum: 0, + micLen: 4, + source64: 16175115667303284240n, + }, + }; + + expect(nwkHeader).toStrictEqual(expectedNWKHeader); + expect(nwkPayload).toStrictEqual(Buffer.from([0x02, 0x01, 0x00, 0xef, 0x04, 0x01, 0x01, 0x4d])); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, undefined, apsFCF, apsHeader); + const expectedAPSHeader: ZigbeeAPSHeader = { + frameControl: { + frameType: 0x2, + deliveryMode: 0x0, + ackFormat: false, + security: false, + ackRequest: false, + extendedHeader: false, + }, + destEndpoint: 1, + group: undefined, + clusterId: 0xef00, + profileId: 0x0104, + sourceEndpoint: 1, + counter: 77, + fragmentation: undefined, + fragBlockNumber: undefined, + fragACKBitfield: undefined, + securityHeader: undefined, + }; + + expect(apsHeader).toStrictEqual(expectedAPSHeader); + expect(apsPayload).toStrictEqual(Buffer.from([])); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + const encFrame = encodeMACFrameZigbee(encHeader, macPayload); + + expect(encFrame.subarray(0, -2)).toStrictEqual(NETDEF_ACK_FRAME_FROM_COORD.subarray(0, -2)); + + const encNWKHeader = structuredClone(nwkHeader); + encNWKHeader.securityHeader!.control.level = 0; + const encNWKFrame = encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + expect(encNWKFrame).toStrictEqual(macPayload); + + const encAPSHeader = structuredClone(apsHeader); + const encAPSFrame = encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + + expect(encAPSFrame).toStrictEqual(nwkPayload); + }); + + it("NETDEF_LINK_STATUS_FROM_DEV", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_LINK_STATUS_FROM_DEV, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_LINK_STATUS_FROM_DEV, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_LINK_STATUS_FROM_DEV, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 1, + securityEnabled: false, + framePending: false, + ackRequest: false, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 2, + frameVersion: 0, + sourceAddrMode: 2, + }, + sequenceNumber: 92, + destinationPANId: 0x1a62, + destination16: 0xffff, + destination64: undefined, + sourcePANId: 0x1a62, + source16: 0xf0a2, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xcab6, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(87); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + const expectedNWKHeader: ZigbeeNWKHeader = { + frameControl: { + frameType: 1, + protocolVersion: 2, + discoverRoute: 0, + multicast: false, + security: true, + sourceRoute: false, + extendedDestination: false, + extendedSource: true, + endDeviceInitiator: false, + }, + destination16: 0xfffc, + source16: 0xf0a2, + radius: 1, + seqNum: 223, + destination64: undefined, + source64: 5149013569654176n, + multicastControl: undefined, + relayIndex: undefined, + relayAddresses: undefined, + securityHeader: { + control: { + keyId: 1, + level: 5, + nonce: true, + }, + frameCounter: 5505754, + keySeqNum: 0, + micLen: 4, + source64: 5149013569654176n, + }, + }; + + expect(nwkHeader).toStrictEqual(expectedNWKHeader); + expect(nwkPayload).toStrictEqual( + Buffer.from([ + 0x08, 0x71, 0x00, 0x00, 0x11, 0x7c, 0x0b, 0x77, 0xca, 0x16, 0x11, 0x20, 0x20, 0x01, 0x03, 0x23, 0x77, 0x74, 0x5e, 0x11, 0xb1, 0x65, + 0x11, 0xb4, 0x67, 0x11, 0x26, 0x73, 0x77, 0xc6, 0x87, 0x31, 0x4f, 0x8c, 0x77, 0xba, 0x96, 0x11, 0x38, 0xaa, 0x11, 0xcd, 0xc8, 0x11, + 0x54, 0xd0, 0x11, 0xf0, 0xf1, 0x11, 0x3d, 0xfd, 0x11, + ]), + ); + + // TODO Zigbee NWK cmd + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + const encFrame = encodeMACFrameZigbee(encHeader, macPayload); + + expect(encFrame.subarray(0, -2)).toStrictEqual(NETDEF_LINK_STATUS_FROM_DEV.subarray(0, -2)); + + const encNWKHeader = structuredClone(nwkHeader); + encNWKHeader.securityHeader!.control.level = 0; + const encNWKFrame = encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + expect(encNWKFrame).toStrictEqual(macPayload); + }); + + it("NETDEF_ZCL_FRAME_CMD_TO_COORD", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ZCL_FRAME_CMD_TO_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ZCL_FRAME_CMD_TO_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ZCL_FRAME_CMD_TO_COORD, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 1, + securityEnabled: false, + framePending: false, + ackRequest: true, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 2, + frameVersion: 0, + sourceAddrMode: 2, + }, + sequenceNumber: 230, + destinationPANId: 0x1a62, + destination16: 0x0000, + destination64: undefined, + sourcePANId: 0x1a62, + source16: 0xaa38, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xc4b3, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(39); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + const expectedNWKHeader: ZigbeeNWKHeader = { + frameControl: { + frameType: 0, + protocolVersion: 2, + discoverRoute: 1, + multicast: false, + security: true, + sourceRoute: false, + extendedDestination: false, + extendedSource: false, + endDeviceInitiator: false, + }, + destination16: 0x0000, + source16: 0xaa38, + radius: 30, + seqNum: 128, + destination64: undefined, + source64: undefined, + multicastControl: undefined, + relayIndex: undefined, + relayAddresses: undefined, + securityHeader: { + control: { + keyId: 1, + level: 5, + nonce: true, + }, + frameCounter: 43659054, + keySeqNum: 0, + micLen: 4, + source64: 8118874123826907736n, + }, + }; + + expect(nwkHeader).toStrictEqual(expectedNWKHeader); + expect(nwkPayload).toStrictEqual(Buffer.from([0x00, 0x01, 0x00, 0xef, 0x04, 0x01, 0x01, 0x3f, 0x09, 0x50, 0x25, 0xaf, 0x00])); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, undefined, apsFCF, apsHeader); + + const expectedAPSHeader: ZigbeeAPSHeader = { + frameControl: { + frameType: 0x0, + deliveryMode: 0x0, + ackFormat: false, + security: false, + ackRequest: false, + extendedHeader: false, + }, + destEndpoint: 1, + group: undefined, + clusterId: 0xef00, + profileId: 0x0104, + sourceEndpoint: 1, + counter: 63, + fragmentation: undefined, + fragBlockNumber: undefined, + fragACKBitfield: undefined, + securityHeader: undefined, + }; + + expect(apsHeader).toStrictEqual(expectedAPSHeader); + expect(apsPayload).toStrictEqual(Buffer.from([0x09, 0x50, 0x25, 0xaf, 0x00])); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + const encFrame = encodeMACFrameZigbee(encHeader, macPayload); + + expect(encFrame.subarray(0, -2)).toStrictEqual(NETDEF_ZCL_FRAME_CMD_TO_COORD.subarray(0, -2)); + + const encNWKHeader = structuredClone(nwkHeader); + encNWKHeader.securityHeader!.control.level = 0; + const encNWKFrame = encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + expect(encNWKFrame).toStrictEqual(macPayload); + + const encAPSHeader = structuredClone(apsHeader); + const encAPSFrame = encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + + expect(encAPSFrame).toStrictEqual(nwkPayload); + }); + + it("NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 1, + securityEnabled: false, + framePending: false, + ackRequest: true, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 2, + frameVersion: 0, + sourceAddrMode: 2, + }, + sequenceNumber: 231, + destinationPANId: 0x1a62, + destination16: 0x0000, + destination64: undefined, + sourcePANId: 0x1a62, + source16: 0xaa38, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xc4b3, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(39); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + const expectedNWKHeader: ZigbeeNWKHeader = { + frameControl: { + frameType: 0, + protocolVersion: 2, + discoverRoute: 1, + multicast: false, + security: true, + sourceRoute: false, + extendedDestination: false, + extendedSource: false, + endDeviceInitiator: false, + }, + destination16: 0, + source16: 0xaa38, + radius: 30, + seqNum: 130, + destination64: undefined, + source64: undefined, + multicastControl: undefined, + relayIndex: undefined, + relayAddresses: undefined, + securityHeader: { + control: { + keyId: 1, + level: 5, + nonce: true, + }, + frameCounter: 43659055, + keySeqNum: 0, + micLen: 4, + source64: 8118874123826907736n, + }, + }; + + expect(nwkHeader).toStrictEqual(expectedNWKHeader); + expect(nwkPayload).toStrictEqual(Buffer.from([0x40, 0x01, 0x00, 0xef, 0x04, 0x01, 0x01, 0x40, 0x08, 0x32, 0x0b, 0x25, 0x00])); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, undefined, apsFCF, apsHeader); + + const expectedAPSHeader: ZigbeeAPSHeader = { + frameControl: { + frameType: 0x0, + deliveryMode: 0x0, + ackFormat: false, + security: false, + ackRequest: true, + extendedHeader: false, + }, + destEndpoint: 1, + group: undefined, + clusterId: 0xef00, + profileId: 0x0104, + sourceEndpoint: 1, + counter: 64, + fragmentation: undefined, + fragBlockNumber: undefined, + fragACKBitfield: undefined, + securityHeader: undefined, + }; + + expect(apsHeader).toStrictEqual(expectedAPSHeader); + expect(apsPayload).toStrictEqual(Buffer.from([0x08, 0x32, 0x0b, 0x25, 0x00])); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + const encFrame = encodeMACFrameZigbee(encHeader, macPayload); + + expect(encFrame.subarray(0, -2)).toStrictEqual(NETDEF_ZCL_FRAME_DEF_RSP_TO_COORD.subarray(0, -2)); + + const encNWKHeader = structuredClone(nwkHeader); + encNWKHeader.securityHeader!.control.level = 0; + const encNWKFrame = encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + expect(encNWKFrame).toStrictEqual(macPayload); + + const encAPSHeader = structuredClone(apsHeader); + const encAPSFrame = encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + + expect(encAPSFrame).toStrictEqual(nwkPayload); + }); + + it("NETDEF_ROUTE_RECORD_TO_COORD", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ROUTE_RECORD_TO_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ROUTE_RECORD_TO_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ROUTE_RECORD_TO_COORD, macHOutOffset, macFCF, macHeader); + const expectedMACHeader = { + frameControl: { + frameType: 1, + securityEnabled: false, + framePending: false, + ackRequest: true, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 2, + frameVersion: 0, + sourceAddrMode: 2, + }, + sequenceNumber: 155, + destinationPANId: 0x1a62, + destination16: 0x0000, + destination64: undefined, + sourcePANId: 0x1a62, + source16: 0xf1f0, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0x9c9f, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(38); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + const expectedNWKHeader = { + frameControl: { + frameType: 1, + protocolVersion: 2, + discoverRoute: 0, + multicast: false, + security: true, + sourceRoute: false, + extendedDestination: false, + extendedSource: true, + endDeviceInitiator: false, + }, + destination16: 0x0000, + source16: 0xac3a, + radius: 30, + seqNum: 207, + destination64: undefined, + source64: 5149013578478658n, + multicastControl: undefined, + relayIndex: undefined, + relayAddresses: undefined, + securityHeader: { + control: { + keyId: 1, + level: 5, + nonce: true, + }, + frameCounter: 6240313, + keySeqNum: 0, + micLen: 4, + source64: 5149013569454355n, + }, + }; + + expect(nwkHeader).toStrictEqual(expectedNWKHeader); + expect(nwkPayload).toStrictEqual(Buffer.from([0x05, 0x01, 0xf0, 0xf1])); + + // TODO Zigbee NWK cmd + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + const encFrame = encodeMACFrameZigbee(encHeader, macPayload); + + expect(encFrame.subarray(0, -2)).toStrictEqual(NETDEF_ROUTE_RECORD_TO_COORD.subarray(0, -2)); + + const encNWKHeader = structuredClone(nwkHeader); + encNWKHeader.securityHeader!.control.level = 0; + const encNWKFrame = encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + expect(encNWKFrame).toStrictEqual(macPayload); + }); + + it("NETDEF_MTORR_FRAME_FROM_COORD", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_MTORR_FRAME_FROM_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_MTORR_FRAME_FROM_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_MTORR_FRAME_FROM_COORD, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 1, + securityEnabled: false, + framePending: false, + ackRequest: false, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 2, + frameVersion: 0, + sourceAddrMode: 2, + }, + sequenceNumber: 93, + destinationPANId: 0x1a62, + destination16: 0xffff, + destination64: undefined, + sourcePANId: 0x1a62, + source16: 0x0000, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xf4cb, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(40); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + const expectedNWKHeader: ZigbeeNWKHeader = { + frameControl: { + frameType: 1, + protocolVersion: 2, + discoverRoute: 0, + multicast: false, + security: true, + sourceRoute: false, + extendedDestination: false, + extendedSource: true, + endDeviceInitiator: false, + }, + destination16: 0xfffc, + source16: 0, + radius: 30, + seqNum: 237, + destination64: undefined, + source64: 16175115667303284240n, + multicastControl: undefined, + relayIndex: undefined, + relayAddresses: undefined, + securityHeader: { + control: { + keyId: 1, + level: 5, + nonce: true, + }, + frameCounter: 99044332, + keySeqNum: 0, + micLen: 4, + source64: 16175115667303284240n, + }, + }; + + expect(nwkHeader).toStrictEqual(expectedNWKHeader); + expect(nwkPayload).toStrictEqual(Buffer.from([0x01, 0x08, 0x2d, 0xfc, 0xff, 0x00])); + + // TODO Zigbee NWK cmd + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + const encFrame = encodeMACFrameZigbee(encHeader, macPayload); + + expect(encFrame.subarray(0, -2)).toStrictEqual(NETDEF_MTORR_FRAME_FROM_COORD.subarray(0, -2)); + + const encNWKHeader = structuredClone(nwkHeader); + encNWKHeader.securityHeader!.control.level = 0; + const encNWKFrame = encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + expect(encNWKFrame).toStrictEqual(macPayload); + }); + + it("NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 0x1, + securityEnabled: false, + framePending: false, + ackRequest: false, + panIdCompression: false, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 0x2, + frameVersion: 0, + sourceAddrMode: 0x0, + }, + sequenceNumber: 185, + destinationPANId: 0xffff, + destination16: 0xffff, + destination64: undefined, + sourcePANId: 0xffff, + source16: undefined, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xffff, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(15); + + const [nwkGPFCF, nwkGPFCFOutOffset] = decodeZigbeeNWKGPFrameControl(macPayload, 0); + const [nwkGPHeader, nwkGPHOutOffset] = decodeZigbeeNWKGPHeader(macPayload, nwkGPFCFOutOffset, nwkGPFCF); + const nwkGPPayload = decodeZigbeeNWKGPPayload(macPayload, nwkGPHOutOffset, NETDEF_NETWORK_KEY, macHeader.source64, nwkGPFCF, nwkGPHeader); + const expectedNWKGPHeader: ZigbeeNWKGPHeader = { + frameControl: { + frameType: 0x0, + protocolVersion: 3, + autoCommissioning: false, + nwkFrameControlExtension: true, + }, + frameControlExt: { + appId: 0, + securityLevel: 2, + securityKey: true, + rxAfterTx: false, + direction: 0, + }, + sourceId: 0x01719697, + endpoint: undefined, + securityFrameCounter: 185, + micSize: 4, + payloadLength: 1, + mic: 0xd1fdebfe, + }; + + expect(nwkGPHeader).toStrictEqual(expectedNWKGPHeader); + expect(nwkGPPayload).toStrictEqual(Buffer.from([0x10])); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + const encFrame = encodeMACFrameZigbee(encHeader, macPayload); + + expect(encFrame.subarray(0, -2)).toStrictEqual(NETDEF_ZGP_FRAME_BCAST_RECALL_SCENE_0.subarray(0, -2)); + + const encNWKGPHeader = structuredClone(nwkGPHeader); + + const encNWKFrame = encodeZigbeeNWKGPFrame(encNWKGPHeader, nwkGPPayload, NETDEF_NETWORK_KEY, macHeader.source64); + + expect(encNWKFrame).toStrictEqual(macPayload); + }); + + it("NETDEF_ZGP_COMMISSIONING", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NETDEF_ZGP_COMMISSIONING, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NETDEF_ZGP_COMMISSIONING, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NETDEF_ZGP_COMMISSIONING, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 0x1, + securityEnabled: false, + framePending: false, + ackRequest: false, + panIdCompression: false, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 0x2, + frameVersion: 0, + sourceAddrMode: 0x0, + }, + sequenceNumber: 70, + destinationPANId: 0xffff, + destination16: 0xffff, + destination64: undefined, + sourcePANId: 0xffff, + source16: undefined, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xffff, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(57); + + const [nwkGPFCF, nwkGPFCFOutOffset] = decodeZigbeeNWKGPFrameControl(macPayload, 0); + const [nwkGPHeader, nwkGPHOutOffset] = decodeZigbeeNWKGPHeader(macPayload, nwkGPFCFOutOffset, nwkGPFCF); + const nwkGPPayload = decodeZigbeeNWKGPPayload(macPayload, nwkGPHOutOffset, NETDEF_NETWORK_KEY, macHeader.source64, nwkGPFCF, nwkGPHeader); + const expectedNWKGPHeader: ZigbeeNWKGPHeader = { + frameControl: { + frameType: 0x0, + protocolVersion: 3, + autoCommissioning: false, + nwkFrameControlExtension: false, + }, + frameControlExt: undefined, + sourceId: 0x0155f47a, + endpoint: undefined, + securityFrameCounter: undefined, + micSize: 0, + payloadLength: 52, + mic: undefined, + }; + + expect(nwkGPHeader).toStrictEqual(expectedNWKGPHeader); + expect(nwkGPPayload).toStrictEqual( + Buffer.from([ + 0xe0, 0x2, 0x85, 0xf2, 0xc9, 0x25, 0x82, 0x1d, 0xf4, 0x6f, 0x45, 0x8c, 0xf0, 0xe6, 0x37, 0xaa, 0xc3, 0xba, 0xb6, 0xaa, 0x45, 0x83, + 0x1a, 0x11, 0x46, 0x23, 0x0, 0x0, 0x4, 0x16, 0x10, 0x11, 0x22, 0x23, 0x18, 0x19, 0x14, 0x15, 0x12, 0x13, 0x64, 0x65, 0x62, 0x63, 0x1e, + 0x1f, 0x1c, 0x1d, 0x1a, 0x1b, 0x16, 0x17, + ]), + ); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + const encFrame = encodeMACFrameZigbee(encHeader, macPayload); + + expect(encFrame.subarray(0, -2)).toStrictEqual(NETDEF_ZGP_COMMISSIONING.subarray(0, -2)); + + const encNWKGPHeader = structuredClone(nwkGPHeader); + + const encNWKFrame = encodeZigbeeNWKGPFrame(encNWKGPHeader, nwkGPPayload, NETDEF_NETWORK_KEY, macHeader.source64); + + expect(encNWKFrame).toStrictEqual(macPayload); + }); + + it("NET2_DEVICE_LEAVE_BROADCAST", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_DEVICE_LEAVE_BROADCAST, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_DEVICE_LEAVE_BROADCAST, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_DEVICE_LEAVE_BROADCAST, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 1, + securityEnabled: false, + framePending: false, + ackRequest: false, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 2, + frameVersion: 0, + sourceAddrMode: 2, + }, + sequenceNumber: 237, + destinationPANId: 0x1a64, + destination16: 0xffff, + destination64: undefined, + sourcePANId: 0x1a64, + source16: 0xa18f, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xffff, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(36); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + const expectedNWKHeader: ZigbeeNWKHeader = { + frameControl: { + frameType: 1, + protocolVersion: 2, + discoverRoute: 0, + multicast: false, + security: true, + sourceRoute: false, + extendedDestination: false, + extendedSource: true, + endDeviceInitiator: false, + }, + destination16: 0xfffd, + source16: 0xa18f, + radius: 1, + seqNum: 195, + destination64: undefined, + source64: BigInt("0xa4c1386d9b280fdf"), + multicastControl: undefined, + relayIndex: undefined, + relayAddresses: undefined, + securityHeader: { + control: { + keyId: 1, + level: 5, + nonce: true, + }, + frameCounter: 33483, + keySeqNum: 0, + micLen: 4, + source64: BigInt("0xa4c1386d9b280fdf"), + }, + }; + + expect(nwkHeader).toStrictEqual(expectedNWKHeader); + expect(nwkPayload).toStrictEqual(Buffer.from([0x04, 0x00])); + + const encHeader = structuredClone(macHeader); + encHeader.sourcePANId = undefined; + + const encFrame = encodeMACFrameZigbee(encHeader, macPayload); + + expect(encFrame.subarray(0, -2)).toStrictEqual(NET2_DEVICE_LEAVE_BROADCAST.subarray(0, -2)); + + const encNWKHeader = structuredClone(nwkHeader); + encNWKHeader.securityHeader!.control.level = 0; + const encNWKFrame = encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + expect(encNWKFrame).toStrictEqual(macPayload); + }); + + it("NET2_BEACON_REQ_FROM_DEVICE", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_BEACON_REQ_FROM_DEVICE, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_BEACON_REQ_FROM_DEVICE, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_BEACON_REQ_FROM_DEVICE, macHOutOffset, macFCF, macHeader); + + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 3, + securityEnabled: false, + framePending: false, + ackRequest: false, + panIdCompression: false, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 2, + frameVersion: 0, + sourceAddrMode: 0, + }, + sequenceNumber: 100, + destinationPANId: 65535, + destination16: 65535, + destination64: undefined, + sourcePANId: 65535, + source16: undefined, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: 7, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xffff, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(0); + }); + + it("NET2_BEACON_RESP_FROM_COORD", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_BEACON_RESP_FROM_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_BEACON_RESP_FROM_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_BEACON_RESP_FROM_COORD, macHOutOffset, macFCF, macHeader); + const beacon = decodeMACZigbeeBeacon(macPayload, 0); + + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 0, + securityEnabled: false, + framePending: false, + ackRequest: false, + panIdCompression: false, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 0, + frameVersion: 0, + sourceAddrMode: 2, + }, + sequenceNumber: 186, + destinationPANId: undefined, + destination16: undefined, + destination64: undefined, + sourcePANId: 0x1a64, + source16: 0x0000, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: { + beaconOrder: 15, + superframeOrder: 15, + finalCAPSlot: 15, + batteryExtension: false, + panCoordinator: true, + associationPermit: true, + }, + gtsInfo: { + permit: false, + directionByte: undefined, + directions: undefined, + addresses: undefined, + timeLengths: undefined, + slots: undefined, + }, + pendAddr: { addr16List: undefined, addr64List: undefined }, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xffff, + }; + const expectedBeacon: MACZigbeeBeacon = { + protocolId: 0, + profile: 2, + version: 2, + routerCapacity: true, + deviceDepth: 0, + endDeviceCapacity: true, + extendedPANId: 15987178197214944733n, + txOffset: 16777215, + updateId: 0, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(15); + expect(beacon).toStrictEqual(expectedBeacon); + expect(macPayload).toStrictEqual(encodeMACZigbeeBeacon(beacon)); + }); + + it("NET2_ASSOC_REQ_FROM_DEVICE", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_ASSOC_REQ_FROM_DEVICE, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_ASSOC_REQ_FROM_DEVICE, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_ASSOC_REQ_FROM_DEVICE, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 0x3, + securityEnabled: false, + framePending: false, + ackRequest: true, + panIdCompression: false, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 0x2, + frameVersion: 0, + sourceAddrMode: 0x3, + }, + sequenceNumber: 116, + destinationPANId: 0x1a64, + destination16: 0x0000, + destination64: undefined, + sourcePANId: 0xffff, + source16: undefined, + source64: BigInt("0xa4c1386d9b280fdf"), + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: 0x01, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xffff, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(1); + expect(macPayload).toStrictEqual(Buffer.from([0x8e])); + + const encHeader = structuredClone(macHeader); + + const encFrame = encodeMACFrame(encHeader, macPayload); + + expect(encFrame.subarray(0, -2)).toStrictEqual(NET2_ASSOC_REQ_FROM_DEVICE.subarray(0, -2)); + }); + + it("NET2_ASSOC_RESP_FROM_COORD", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_ASSOC_RESP_FROM_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_ASSOC_RESP_FROM_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_ASSOC_RESP_FROM_COORD, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 0x3, + securityEnabled: false, + framePending: false, + ackRequest: true, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 0x3, + frameVersion: 0, + sourceAddrMode: 0x3, + }, + sequenceNumber: 187, + destinationPANId: 0x1a64, + destination16: undefined, + destination64: BigInt("0xa4c1386d9b280fdf"), + sourcePANId: 0x1a64, + source16: undefined, + source64: NET2_COORD_EUI64_BIGINT, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: 0x02, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xffff, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(3); + expect(macPayload).toStrictEqual(Buffer.from([0x8f, 0xa1, 0x00])); + + const encHeader = structuredClone(macHeader); + + const encFrame = encodeMACFrame(encHeader, macPayload); + + expect(encFrame.subarray(0, -2)).toStrictEqual(NET2_ASSOC_RESP_FROM_COORD.subarray(0, -2)); + }); + + it("NET2_TRANSPORT_KEY_NWK_FROM_COORD", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_TRANSPORT_KEY_NWK_FROM_COORD, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_TRANSPORT_KEY_NWK_FROM_COORD, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_TRANSPORT_KEY_NWK_FROM_COORD, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 1, + securityEnabled: false, + framePending: false, + ackRequest: true, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 2, + frameVersion: 0, + sourceAddrMode: 2, + }, + sequenceNumber: 189, + destinationPANId: 0x1a64, + destination16: 0xa18f, + destination64: undefined, + sourcePANId: 0x1a64, + source16: 0x0000, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xffff, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(62); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + const expectedNWKHeader: ZigbeeNWKHeader = { + frameControl: { + frameType: 0, + protocolVersion: 2, + discoverRoute: 0x0, + multicast: false, + security: false, + sourceRoute: false, + extendedDestination: false, + extendedSource: false, + endDeviceInitiator: false, + }, + destination16: 0xa18f, + source16: 0x0000, + radius: 30, + seqNum: 161, + destination64: undefined, + source64: undefined, + multicastControl: undefined, + relayIndex: undefined, + relayAddresses: undefined, + securityHeader: undefined, + }; + + expect(nwkHeader).toStrictEqual(expectedNWKHeader); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, NET2_COORD_EUI64_BIGINT, apsFCF, apsHeader); + + const expectedAPSHeader: ZigbeeAPSHeader = { + frameControl: { + frameType: 0x1, + deliveryMode: 0x0, + ackFormat: false, + security: true, + ackRequest: false, + extendedHeader: false, + }, + destEndpoint: undefined, + group: undefined, + clusterId: undefined, + profileId: undefined, + sourceEndpoint: undefined, + counter: 106, + fragmentation: undefined, + fragBlockNumber: undefined, + fragACKBitfield: undefined, + securityHeader: { + control: { + level: 5, + keyId: 0x2, + nonce: true, + }, + frameCounter: 86022, + source64: NET2_COORD_EUI64_BIGINT, + keySeqNum: undefined, + micLen: 4, + }, + }; + + expect(apsHeader).toStrictEqual(expectedAPSHeader); + expect(apsPayload).toStrictEqual( + Buffer.from([ + 0x05, 0x01, 0x01, 0x03, 0x05, 0x07, 0x09, 0x0b, 0x0d, 0x0f, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0d, 0x00, 0xdf, 0x0f, 0x28, + 0x9b, 0x6d, 0x38, 0xc1, 0xa4, 0xf9, 0x99, 0x05, 0xfe, 0xff, 0x50, 0x4b, 0x80, + ]), + ); + + const encMACHeader = structuredClone(macHeader); + encMACHeader.sourcePANId = undefined; + const encMACFrame = encodeMACFrameZigbee(encMACHeader, macPayload); + + expect(encMACFrame.subarray(0, -2)).toStrictEqual(NET2_TRANSPORT_KEY_NWK_FROM_COORD.subarray(0, -2)); + + const encNWKHeader = structuredClone(nwkHeader); + const encNWKFrame = encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + expect(encNWKFrame).toStrictEqual(macPayload); + + const encAPSHeader = structuredClone(apsHeader); + encAPSHeader.securityHeader!.control.level = 0; + const encAPSFrame = encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + + expect(encAPSFrame).toStrictEqual(nwkPayload); + }); + + it("NET2_REQUEST_KEY_TC_FROM_DEVICE", () => { + const [macFCF, macFCFOutOffset] = decodeMACFrameControl(NET2_REQUEST_KEY_TC_FROM_DEVICE, 0); + const [macHeader, macHOutOffset] = decodeMACHeader(NET2_REQUEST_KEY_TC_FROM_DEVICE, macFCFOutOffset, macFCF); + const macPayload = decodeMACPayload(NET2_REQUEST_KEY_TC_FROM_DEVICE, macHOutOffset, macFCF, macHeader); + const expectedMACHeader: MACHeader = { + frameControl: { + frameType: 1, + securityEnabled: false, + framePending: false, + ackRequest: true, + panIdCompression: true, + seqNumSuppress: false, + iePresent: false, + destAddrMode: 2, + frameVersion: 0, + sourceAddrMode: 2, + }, + sequenceNumber: 130, + destinationPANId: 0x1a64, + destination16: 0x0000, + destination64: undefined, + sourcePANId: 0x1a64, + source16: 0xa18f, + source64: undefined, + auxSecHeader: undefined, + superframeSpec: undefined, + gtsInfo: undefined, + pendAddr: undefined, + commandId: undefined, + headerIE: undefined, + frameCounter: undefined, + keySeqCounter: undefined, + fcs: 0xffff, + }; + + expect(macHeader).toStrictEqual(expectedMACHeader); + expect(macPayload.byteLength).toStrictEqual(47); + + const [nwkFCF, nwkFCFOutOffset] = decodeZigbeeNWKFrameControl(macPayload, 0); + const [nwkHeader, nwkHOutOffset] = decodeZigbeeNWKHeader(macPayload, nwkFCFOutOffset, nwkFCF); + const nwkPayload = decodeZigbeeNWKPayload(macPayload, nwkHOutOffset, undefined, macHeader.source64, nwkFCF, nwkHeader); + const expectedNWKHeader: ZigbeeNWKHeader = { + frameControl: { + frameType: 0, + protocolVersion: 2, + discoverRoute: 0x1, + multicast: false, + security: true, + sourceRoute: false, + extendedDestination: false, + extendedSource: false, + endDeviceInitiator: false, + }, + destination16: 0x0000, + source16: 0xa18f, + radius: 30, + seqNum: 39, + destination64: undefined, + source64: undefined, + multicastControl: undefined, + relayIndex: undefined, + relayAddresses: undefined, + securityHeader: { + control: { + keyId: 0x01, + level: 0x05, + nonce: true, + }, + frameCounter: 33497, + keySeqNum: 0, + micLen: 4, + source64: 11871832136131022815n, + }, + }; + + expect(nwkHeader).toStrictEqual(expectedNWKHeader); + + const [apsFCF, apsFCFOutOffset] = decodeZigbeeAPSFrameControl(nwkPayload, 0); + const [apsHeader, apsHOutOffset] = decodeZigbeeAPSHeader(nwkPayload, apsFCFOutOffset, apsFCF); + const apsPayload = decodeZigbeeAPSPayload(nwkPayload, apsHOutOffset, undefined, NET2_COORD_EUI64_BIGINT, apsFCF, apsHeader); + const expectedAPSHeader: ZigbeeAPSHeader = { + frameControl: { + frameType: 0x1, + deliveryMode: 0x0, + ackFormat: false, + security: true, + ackRequest: false, + extendedHeader: false, + }, + destEndpoint: undefined, + group: undefined, + clusterId: undefined, + profileId: undefined, + sourceEndpoint: undefined, + counter: 131, + fragmentation: undefined, + fragBlockNumber: undefined, + fragACKBitfield: undefined, + securityHeader: { + control: { + level: 5, + keyId: 0x0, + nonce: true, + }, + frameCounter: 33496, + source64: 11871832136131022815n, + keySeqNum: undefined, + micLen: 4, + }, + }; + + expect(apsHeader).toStrictEqual(expectedAPSHeader); + expect(apsPayload).toStrictEqual(Buffer.from([0x08, 0x04])); + + const encMACHeader = structuredClone(macHeader); + encMACHeader.sourcePANId = undefined; + const encMACFrame = encodeMACFrameZigbee(encMACHeader, macPayload); + + expect(encMACFrame.subarray(0, -2)).toStrictEqual(NET2_REQUEST_KEY_TC_FROM_DEVICE.subarray(0, -2)); + + const encNWKHeader = structuredClone(nwkHeader); + encNWKHeader.securityHeader!.control.level = 0; + const encNWKFrame = encodeZigbeeNWKFrame(encNWKHeader, nwkPayload, encNWKHeader.securityHeader!, undefined); + + expect(encNWKFrame).toStrictEqual(macPayload); + + const encAPSHeader = structuredClone(apsHeader); + encAPSHeader.securityHeader!.control.level = 0; + const encAPSFrame = encodeZigbeeAPSFrame(encAPSHeader, apsPayload, encAPSHeader.securityHeader!, undefined); + + expect(encAPSFrame).toStrictEqual(nwkPayload); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..94e6538 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + "composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */, + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "NodeNext" /* Specify what module code is generated. */, + "rootDir": "src" /* Specify the root folder within your source files. */, + "moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */, + "baseUrl": "." /* Specify the base directory to resolve non-relative module names. */, + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true /* Create source map files for emitted JavaScript files. */, + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + "newLine": "lf" /* Set the newline character for emitting files. */, + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["./src/**/*.ts", "./src/**/*.json"], + "exclude": ["./dist", "./node_modules"] +} diff --git a/tsconfig.prod.json b/tsconfig.prod.json new file mode 100644 index 0000000..59d3346 --- /dev/null +++ b/tsconfig.prod.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig", + "exclude": ["./src/dev"] +}