diff --git a/apps/devtypes/.gitignore b/apps/devtypes/.gitignore new file mode 100644 index 0000000..0e1fa41 --- /dev/null +++ b/apps/devtypes/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.parcel-cache diff --git a/apps/devtypes/README.md b/apps/devtypes/README.md new file mode 100644 index 0000000..c2fed26 --- /dev/null +++ b/apps/devtypes/README.md @@ -0,0 +1,21 @@ +# Raft Device Types + +A web app for Raft device type records. You can: + +- browse and search the catalogue; +- connect to a Raft device and see its device types; +- build and push its device type file (from RaftCore's records, GitHub sources and local record files); +- download the setup as a project's `DeviceTypeSources.json`, with its lock. + +It's published on RaftCore's GitHub Pages site at `https://robdobsn.github.io/RaftCore/devtypes/app/`, built by RaftCore's `pages.yml` workflow. The design is in [devdocs/device-type-management-web-plan.md](../../devdocs/device-type-management-web-plan.md). + +```sh +npm install # in this folder (and npm install in raftjs, which the app is built from) +npm start # http://localhost:1234 +npm run build # dist/ +``` + +- **Catalogue:** the app reads it from the site it's served from (`.../devtypes/`), otherwise from RaftCore's Pages site. To use another catalogue, add `?catalogue=`, e.g. one built with `AssembleDeviceTypes.py --pages` and served locally. +- **Connections:** Bluetooth and USB serial need Chrome or Edge and a secure page (`https` or `localhost`). WiFi (WebSocket) is offered only on `http` pages and `localhost`, because an `https` page can't open `ws://` to a device. +- **Logic:** all of it is in raftjs (`RaftDeviceTypeFile`, `RaftDeviceTypeSources`, `RaftDeviceTypeCatalogue` and `RaftDeviceTypeFileManager`), checked against the same test vectors as RaftCore's Python tools. `src/model.ts` holds the app's state, and the `*Panel.tsx` files are its views. +- **Storage:** the selection and any local record files are kept in this browser's local storage. diff --git a/apps/devtypes/package-lock.json b/apps/devtypes/package-lock.json new file mode 100644 index 0000000..27a705d --- /dev/null +++ b/apps/devtypes/package-lock.json @@ -0,0 +1,3476 @@ +{ + "name": "raftjs-devtypes-app", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "raftjs-devtypes-app", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "isomorphic-ws": "^5.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@types/web-bluetooth": "^0.0.21", + "buffer": "^6.0.3", + "parcel": "^2.16.3", + "process": "^0.11.10", + "rimraf": "^6.1.3", + "typescript": "^5.3.3" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.4.tgz", + "integrity": "sha512-xt3Q4z64xM+xhg4M7NXqw1N//1O74bUFo5kz6fwMeC4qwxSuRdwkYSmjcqCEEKYkNw5e67f2hcNqBoNERAEU0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lezer/lr": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.11.tgz", + "integrity": "sha512-UwgZcl1R66H2hDzBrA7kWyYmX6s8p5TQ+e/t7CuvgCtBEPxbEPaHYHu+pJBvTvDotEJpuQkJjbXuHiw5Cixxgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.8.5.tgz", + "integrity": "sha512-KPDeVScZgA1oq0CiPBcOa3kHIqU+pTOwRFDIhxvmf8CTNvqdZQYp5cCKW0bUk69VygB2PuTiINFWbY78aR2pQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-2.8.5.tgz", + "integrity": "sha512-w/sLhN4T7MW1nB3R/U8WK5BgQLz904wh+/SmA2jD8NnF7BLLoUgflCNxOeSPOWp8geP6nP/+VjWzZVip7rZ1ug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-2.8.5.tgz", + "integrity": "sha512-c0TGMbm2M55pwTDIfkDLB6BpIsgxV4PjYck2HiOX+cy/JWiBXz32lYbarPqejKs9Flm7YVAKSILUducU9g2RVg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-2.8.5.tgz", + "integrity": "sha512-vtbZRHH5UDlL01TT5jB576Zox3+hdyogvpcbvVJlmU5PdL3c5V7cj1EODdh1CHPksRl+cws/58ugEHi8bcj4Ww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-2.8.5.tgz", + "integrity": "sha512-Xkc8IUx9aEhP0zvgeKy7IQ3ReX2N8N1L0WPcQwnZweWmOuKfwpS3GRIYqLtK5za/w3E60zhFfNdS+3pBZPytqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.8.5.tgz", + "integrity": "sha512-4wvrf5BgnR8RpogHhtpCPJMKBmvyZPhhUtEwMJbXh0ni2BucpfF07jlmyM11zRqQ2XIq6PbC2j7W7UCCcm1rRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@mischnic/json-sourcemap": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@mischnic/json-sourcemap/-/json-sourcemap-0.1.1.tgz", + "integrity": "sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/lr": "^1.0.0", + "json5": "^2.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@parcel/bundler-default": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/bundler-default/-/bundler-default-2.16.4.tgz", + "integrity": "sha512-Nb8peNvhfm1+660CLwssWh4weY+Mv6vEGS6GPKqzJmTMw50udi0eS1YuWFzvmhSiu1KsYcUD37mqQ1LuIDtWoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/graph": "3.6.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/cache": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/cache/-/cache-2.16.4.tgz", + "integrity": "sha512-+uCyeElSga2MBbmbXpIj/WVKH7TByCrKaxtHbelfKKIJpYMgEHVjO4cuc7GUfTrUAmRUS8ZGvnX7Etgq6/jQhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/fs": "2.16.4", + "@parcel/logger": "2.16.4", + "@parcel/utils": "2.16.4", + "lmdb": "2.8.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.4" + } + }, + "node_modules/@parcel/codeframe": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/codeframe/-/codeframe-2.16.4.tgz", + "integrity": "sha512-s64aMfOJoPrXhKH+Y98ahX0O8aXWvTR+uNlOaX4yFkpr4FFDnviLcGngDe/Yo4Qq2FJZ0P6dNswbJTUH9EGxkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/compressor-raw": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/compressor-raw/-/compressor-raw-2.16.4.tgz", + "integrity": "sha512-IK8IpNhw61B2HKgA1JhGhO9y+ZJFRZNTEmvhN1NdLdPqvgEXm2EunT+m6D9z7xeoeT6XnUKqM0eRckEdD0OXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/config-default": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/config-default/-/config-default-2.16.4.tgz", + "integrity": "sha512-kBxuTY/5trEVnvXk92l7LVkYjNuz3SaqWymFhPjEnc8GY4ZVdcWrWdXWTB9hUhpmRYJctFCyGvM0nN05JTiM2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/bundler-default": "2.16.4", + "@parcel/compressor-raw": "2.16.4", + "@parcel/namer-default": "2.16.4", + "@parcel/optimizer-css": "2.16.4", + "@parcel/optimizer-html": "2.16.4", + "@parcel/optimizer-image": "2.16.4", + "@parcel/optimizer-svg": "2.16.4", + "@parcel/optimizer-swc": "2.16.4", + "@parcel/packager-css": "2.16.4", + "@parcel/packager-html": "2.16.4", + "@parcel/packager-js": "2.16.4", + "@parcel/packager-raw": "2.16.4", + "@parcel/packager-svg": "2.16.4", + "@parcel/packager-wasm": "2.16.4", + "@parcel/reporter-dev-server": "2.16.4", + "@parcel/resolver-default": "2.16.4", + "@parcel/runtime-browser-hmr": "2.16.4", + "@parcel/runtime-js": "2.16.4", + "@parcel/runtime-rsc": "2.16.4", + "@parcel/runtime-service-worker": "2.16.4", + "@parcel/transformer-babel": "2.16.4", + "@parcel/transformer-css": "2.16.4", + "@parcel/transformer-html": "2.16.4", + "@parcel/transformer-image": "2.16.4", + "@parcel/transformer-js": "2.16.4", + "@parcel/transformer-json": "2.16.4", + "@parcel/transformer-node": "2.16.4", + "@parcel/transformer-postcss": "2.16.4", + "@parcel/transformer-posthtml": "2.16.4", + "@parcel/transformer-raw": "2.16.4", + "@parcel/transformer-react-refresh-wrap": "2.16.4", + "@parcel/transformer-svg": "2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.4" + } + }, + "node_modules/@parcel/core": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.16.4.tgz", + "integrity": "sha512-a0CgrW5A5kwuSu5J1RFRoMQaMs9yagvfH2jJMYVw56+/7NRI4KOtu612SG9Y1ERWfY55ZwzyFxtLWvD6LO+Anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.1", + "@parcel/cache": "2.16.4", + "@parcel/diagnostic": "2.16.4", + "@parcel/events": "2.16.4", + "@parcel/feature-flags": "2.16.4", + "@parcel/fs": "2.16.4", + "@parcel/graph": "3.6.4", + "@parcel/logger": "2.16.4", + "@parcel/package-manager": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/profiler": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4", + "@parcel/workers": "2.16.4", + "base-x": "^3.0.11", + "browserslist": "^4.24.5", + "clone": "^2.1.2", + "dotenv": "^16.5.0", + "dotenv-expand": "^11.0.7", + "json5": "^2.2.3", + "msgpackr": "^1.11.2", + "nullthrows": "^1.1.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/diagnostic": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.16.4.tgz", + "integrity": "sha512-YN5CfX7lFd6yRLxyZT4Sj3sR6t7nnve4TdXSIqapXzQwL7Bw+sj79D95wTq2rCm3mzk5SofGxFAXul2/nG6gcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/error-overlay": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/error-overlay/-/error-overlay-2.16.4.tgz", + "integrity": "sha512-e8KYKnMsfmQnqIhsUWBUZAXlDK30wkxsAGle1tZ0gOdoplaIdVq/WjGPatHLf6igLM76c3tRn2vw8jZFput0jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/events": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/events/-/events-2.16.4.tgz", + "integrity": "sha512-slWQkBRAA7o0cN0BLEd+yCckPmlVRVhBZn5Pn6ktm4EzEtrqoMzMeJOxxH8TXaRzrQDYnTcnYIHFgXWd4kkUfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/feature-flags": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/feature-flags/-/feature-flags-2.16.4.tgz", + "integrity": "sha512-nYdx53siKPLYikHHxfzgjzzgxdrjquK6DMnuSgOTyIdRG4VHdEN0+NqKijRLuVgiUFo/dtxc2h+amwqFENMw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/fs": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.16.4.tgz", + "integrity": "sha512-maCMOiVn7oJYZlqlfxgLne8n6tSktIT1k0AeyBp4UGWCXyeJUJ+nL7QYShFpKNLtMLeF0cEtgwRAknWzbcDS1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/feature-flags": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/types-internal": "2.16.4", + "@parcel/utils": "2.16.4", + "@parcel/watcher": "^2.0.7", + "@parcel/workers": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.4" + } + }, + "node_modules/@parcel/graph": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.6.4.tgz", + "integrity": "sha512-Cj9yV+/k88kFhE+D+gz0YuNRpvNOCVDskO9pFqkcQhGbsGq6kg2XpZ9V7HlYraih31xf8Vb589bZOwjKIiHixQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/feature-flags": "2.16.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/logger": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.16.4.tgz", + "integrity": "sha512-QR8QLlKo7xAy9JBpPDAh0RvluaixqPCeyY7Fvo2K7hrU3r85vBNNi06pHiPbWoDmB4x1+QoFwMaGnJOHR+/fMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/events": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/markdown-ansi": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/markdown-ansi/-/markdown-ansi-2.16.4.tgz", + "integrity": "sha512-0+oQApAVF3wMcQ6d1ZfZ0JsRzaMUYj9e4U+naj6YEsFsFGOPp+pQYKXBf1bobQeeB7cPKPT3SUHxFqced722Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/namer-default": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/namer-default/-/namer-default-2.16.4.tgz", + "integrity": "sha512-CE+0lFg881sJq575EXxj2lKUn81tsS5itpNUUErHxit195m3PExyAhoXM6ed/SXxwi+uv+T5FS/jjDLBNuUFDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/node-resolver-core": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.7.4.tgz", + "integrity": "sha512-b3VDG+um6IWW5CTod6M9hQsTX5mdIelKmam7mzxzgqg4j5hnycgTWqPMc9UxhYoUY/Q/PHfWepccNcKtvP5JiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.1", + "@parcel/diagnostic": "2.16.4", + "@parcel/fs": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4", + "nullthrows": "^1.1.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-css": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-css/-/optimizer-css-2.16.4.tgz", + "integrity": "sha512-aqdXCtmvpcXYgJFGk2DtXF34wuM2TD1fZorKMrJdKB9sSkWVRs1tq6RAXQrbi0ZPDH9wfE/9An3YdkTex7RHuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.16.4", + "browserslist": "^4.24.5", + "lightningcss": "^1.30.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-html": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-html/-/optimizer-html-2.16.4.tgz", + "integrity": "sha512-vg/R2uuSni+NYYUUV8m+5bz8p5zBv8wc/nNleoBnGuCDwn7uaUwTZ8Gt9CjZO8jjG0xCLILoc/TW+e2FF3pfgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-image": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-image/-/optimizer-image-2.16.4.tgz", + "integrity": "sha512-2RV54WnvMYr18lxSx7Zlx/DXpJwMzOiPxDnoFyvaUoYutvgHO6chtcgFgh1Bvw/PoI95vYzlTkZ8QfUOk5A0JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4", + "@parcel/workers": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.4" + } + }, + "node_modules/@parcel/optimizer-svg": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-svg/-/optimizer-svg-2.16.4.tgz", + "integrity": "sha512-22+BqIffCrVErg8y2XwhasbTaFNn75OKXZ3KTDBIfOSAZKLUKs1iHfDXETzTRN7cVcS+Q36/6EHd7N/RA8i1fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-swc": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-swc/-/optimizer-swc-2.16.4.tgz", + "integrity": "sha512-+URqwnB6u1gqaLbG1O1DDApH+UVj4WCbK9No1fdxLBxQ9a84jyli25o1kK1hYB9Nb/JMyYNnEBfvYUW6RphOxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.16.4", + "@swc/core": "^1.11.24", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/package-manager": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.16.4.tgz", + "integrity": "sha512-obWv9gZgdnkT3Kd+fBkKjhdNEY7zfOP5gVaox5i4nQstVCaVnDlMv5FwLEXwehL+WbwEcGyEGGxOHHkAFKk7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/fs": "2.16.4", + "@parcel/logger": "2.16.4", + "@parcel/node-resolver-core": "3.7.4", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4", + "@parcel/workers": "2.16.4", + "@swc/core": "^1.11.24", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.4" + } + }, + "node_modules/@parcel/packager-css": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-css/-/packager-css-2.16.4.tgz", + "integrity": "sha512-rWRtfiX+VVIOZvq64jpeNUKkvWAbnokfHQsk/js1s5jD4ViNQgPcNLiRaiIANjymqL6+dQqWvGUSW2a5FAZYfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.16.4", + "lightningcss": "^1.30.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-html": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-html/-/packager-html-2.16.4.tgz", + "integrity": "sha512-AWo5f6SSqBsg2uWOsX0gPX8hCx2iE6GYLg2Z4/cDy2mPlwDICN8/bxItEztSZFmObi+ti26eetBKRDxAUivyIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-js": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-js/-/packager-js-2.16.4.tgz", + "integrity": "sha512-L2o39f/fhta+hxto7w8OTUKdstY+te5BmHZREckbQm0KTBg93BG7jB0bfoxLSZF0d8uuAYIVXjzeHNqha+du1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4", + "globals": "^13.24.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-raw": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-raw/-/packager-raw-2.16.4.tgz", + "integrity": "sha512-A9j60G9OmbTkEeE4WRMXCiErEprHLs9NkUlC4HXCxmSrPMOVaMaMva2LdejE3A9kujZqYtYfuc8+a+jN+Nro4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-svg": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.16.4.tgz", + "integrity": "sha512-LT9l7eInFrAZJ6w3mYzAUgDq3SIzYbbQyW46Dz26M9lJQbf6uCaATUTac3BEHegW0ikDuw4OOGHK41BVqeeusg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-wasm": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/packager-wasm/-/packager-wasm-2.16.4.tgz", + "integrity": "sha512-AY96Aqu/RpmaSZK2RGkIrZWjAperDw8DAlxLAiaP1D/RPVnikZtl5BmcUt/Wz3PrzG7/q9ZVqqKkWsLmhkjXZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4" + }, + "engines": { + "node": ">=16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/plugin": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.16.4.tgz", + "integrity": "sha512-aN2VQoRGC1eB41ZCDbPR/Sp0yKOxe31oemzPx1nJzOuebK2Q6FxSrJ9Bjj9j/YCaLzDtPwelsuLOazzVpXJ6qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/types": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/profiler": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/profiler/-/profiler-2.16.4.tgz", + "integrity": "sha512-R3JhfcnoReTv2sVFHPR2xKZvs3d3IRrBl9sWmAftbIJFwT4rU70/W7IdwfaJVkD/6PzHq9mcgOh1WKL4KAxPdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/events": "2.16.4", + "@parcel/types-internal": "2.16.4", + "chrome-trace-event": "^1.0.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-cli": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.16.4.tgz", + "integrity": "sha512-DQx9TwcTZrDv828+tcwEi//xyW7OHTGzGX1+UEVxPp0mSzuOmDn0zfER8qNIqGr1i4D/FXhb5UJQDhGHV8mOpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "@parcel/types": "2.16.4", + "@parcel/utils": "2.16.4", + "chalk": "^4.1.2", + "term-size": "^2.2.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-dev-server": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/reporter-dev-server/-/reporter-dev-server-2.16.4.tgz", + "integrity": "sha512-YWvay25htQDifpDRJ0+yFh6xUxKnbfeJxYkPYyuXdxpEUhq4T0UWW0PbPCN/wFX7StgeUTXq5Poeo/+eys9m3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/codeframe": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-tracer": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/reporter-tracer/-/reporter-tracer-2.16.4.tgz", + "integrity": "sha512-JKnlXpPepak0/ZybmZn9JtyjJiDBWYrt7ZUlXQhQb0xzNcd/k+RqfwVkTKIwyFHsWtym0cwibkvsi2bWFzS7tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4", + "chrome-trace-event": "^1.0.3", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/resolver-default": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/resolver-default/-/resolver-default-2.16.4.tgz", + "integrity": "sha512-wJe9XQS0hn/t32pntQpJbls3ZL8mGVVhK9L7s7BTmZT9ufnvP2nif1psJz/nbgnP9LF6mLSk43OdMJKpoStsjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/node-resolver-core": "3.7.4", + "@parcel/plugin": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-browser-hmr": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.16.4.tgz", + "integrity": "sha512-asx7p3NjUSfibI3bC7+8+jUIGHWVk2Zuq9SjJGCGDt+auT9A4uSGljnsk1BWWPqqZ0WILubq4czSAqm0+wt4cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-js": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-js/-/runtime-js-2.16.4.tgz", + "integrity": "sha512-gUKmsjg+PULQBu2QbX0QKll9tXSqHPO8NrfxHwWb2lz5xDKDos1oV0I7BoMWbHhUHkoToXZrm654oGViujtVUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-rsc": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-rsc/-/runtime-rsc-2.16.4.tgz", + "integrity": "sha512-CHkotYE/cNiUjJmrc5FD9YhlFp1UF5wMNNJmoWaL40eBzsqcaV0sSn5V3bNapwewn3wrMYgdPgvOTHfaZaG73A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-service-worker": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/runtime-service-worker/-/runtime-service-worker-2.16.4.tgz", + "integrity": "sha512-FT0Q58bf5Re+dq5cL2XHbxqHHFZco6qtRijeVpT3TSPMRPlniMArypSytTeZzVNL7h/hxjWsNu7fRuC0yLB5hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust/-/rust-2.16.4.tgz", + "integrity": "sha512-RBMKt9rCdv6jr4vXG6LmHtxzO5TuhQvXo1kSoSIF7fURRZ81D1jzBtLxwLmfxCPsofJNqWwdhy5vIvisX+TLlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/rust-darwin-arm64": "2.16.4", + "@parcel/rust-darwin-x64": "2.16.4", + "@parcel/rust-linux-arm-gnueabihf": "2.16.4", + "@parcel/rust-linux-arm64-gnu": "2.16.4", + "@parcel/rust-linux-arm64-musl": "2.16.4", + "@parcel/rust-linux-x64-gnu": "2.16.4", + "@parcel/rust-linux-x64-musl": "2.16.4", + "@parcel/rust-win32-x64-msvc": "2.16.4" + }, + "peerDependencies": { + "napi-wasm": "^1.1.2" + }, + "peerDependenciesMeta": { + "napi-wasm": { + "optional": true + } + } + }, + "node_modules/@parcel/rust-darwin-arm64": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-darwin-arm64/-/rust-darwin-arm64-2.16.4.tgz", + "integrity": "sha512-P3Se36H9EO1fOlwXqQNQ+RsVKTGn5ztRSUGbLcT8ba6oOMmU1w7J4R810GgsCbwCuF10TJNUMkuD3Q2Sz15Q3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-darwin-x64": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-darwin-x64/-/rust-darwin-x64-2.16.4.tgz", + "integrity": "sha512-8aNKNyPIx3EthYpmVJevIdHmFsOApXAEYGi3HU69jTxLgSIfyEHDdGE9lEsMvhSrd/SSo4/euAtiV+pqK04wnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-linux-arm-gnueabihf": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm-gnueabihf/-/rust-linux-arm-gnueabihf-2.16.4.tgz", + "integrity": "sha512-QrvqiSHaWRLc0JBHgUHVvDthfWSkA6AFN+ikV1UGENv4j2r/QgvuwJiG0VHrsL6pH5dRqj0vvngHzEgguke9DA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-linux-arm64-gnu": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm64-gnu/-/rust-linux-arm64-gnu-2.16.4.tgz", + "integrity": "sha512-f3gBWQHLHRUajNZi3SMmDQiEx54RoRbXtZYQNuBQy7+NolfFcgb1ik3QhkT7xovuTF/LBmaqP3UFy0PxvR/iwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-linux-arm64-musl": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-arm64-musl/-/rust-linux-arm64-musl-2.16.4.tgz", + "integrity": "sha512-cwml18RNKsBwHyZnrZg4jpecXkWjaY/mCArocWUxkFXjjB97L56QWQM9W86f2/Y3HcFcnIGJwx1SDDKJrV6OIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-linux-x64-gnu": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-x64-gnu/-/rust-linux-x64-gnu-2.16.4.tgz", + "integrity": "sha512-0xIjQaN8hiG0F9R8coPYidHslDIrbfOS/qFy5GJNbGA3S49h61wZRBMQqa7JFW4+2T8R0J9j0SKHhLXpbLXrIg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-linux-x64-musl": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-linux-x64-musl/-/rust-linux-x64-musl-2.16.4.tgz", + "integrity": "sha512-fYn21GIecHK9RoZPKwT9NOwxwl3Gy3RYPR6zvsUi0+hpFo19Ph9EzFXN3lT8Pi5KiwQMCU4rsLb5HoWOBM1FeA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust-win32-x64-msvc": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/rust-win32-x64-msvc/-/rust-win32-x64-msvc-2.16.4.tgz", + "integrity": "sha512-TcpWC3I1mJpfP2++018lgvM7UX0P8IrzNxceBTHUKEIDMwmAYrUKAQFiaU0j1Ldqk6yP8SPZD3cvphumsYpJOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/source-map": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", + "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": "^12.18.3 || >=14" + } + }, + "node_modules/@parcel/transformer-babel": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-babel/-/transformer-babel-2.16.4.tgz", + "integrity": "sha512-CMDUOQYX7+cmeyHxHSFnoPcwvXNL7rRFE+Q06uVFzsYYiVhbwGF/1J5Bx4cW3Froumqla4YTytTsEteJEybkdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.16.4", + "browserslist": "^4.24.5", + "json5": "^2.2.3", + "nullthrows": "^1.1.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-css": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-css/-/transformer-css-2.16.4.tgz", + "integrity": "sha512-VG/+DbDci2HKe20GFRDs65ZQf5GUFfnmZAa1BhVl/MO+ijT3XC3eoVUy5cExRkq4VLcPY4ytL0g/1T2D6x7lBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.16.4", + "browserslist": "^4.24.5", + "lightningcss": "^1.30.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-html": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-html/-/transformer-html-2.16.4.tgz", + "integrity": "sha512-w6JErYTeNS+KAzUAER18NHFIFFvxiLGd4Fht1UYcb/FDjJdLAMB/FljyEs0Rto/WAhZ2D0MuSL25HQh837R62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-image": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-image/-/transformer-image-2.16.4.tgz", + "integrity": "sha512-ZzIn3KvvRqMfcect4Dy+57C9XoQXZhpVJKBdQWMp9wM1qJEgsVgGDcaSBYCs/UYSKMRMP6Wm20pKCt408RkQzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4", + "@parcel/workers": "2.16.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "peerDependencies": { + "@parcel/core": "^2.16.4" + } + }, + "node_modules/@parcel/transformer-js": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-js/-/transformer-js-2.16.4.tgz", + "integrity": "sha512-FD2fdO6URwAGBPidb3x1dDgLBt972mko0LelcSU05aC/pcKaV9mbCtINbPul1MlStzkxDelhuImcCYIyerheVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.16.4", + "@parcel/workers": "2.16.4", + "@swc/helpers": "^0.5.0", + "browserslist": "^4.24.5", + "nullthrows": "^1.1.1", + "regenerator-runtime": "^0.14.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.4" + } + }, + "node_modules/@parcel/transformer-json": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-json/-/transformer-json-2.16.4.tgz", + "integrity": "sha512-pB3ZNqgokdkBCJ+4G0BrPYcIkyM9K1HVk0GvjzcLEFDKsoAp8BGEM68FzagFM/nVq9anYTshIaoh349GK0M/bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "json5": "^2.2.3" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-node": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-node/-/transformer-node-2.16.4.tgz", + "integrity": "sha512-7t43CPGfMJk1LqFokwxHSsRi+kKC2QvDXaMtqiMShmk50LCwn81WgzuFvNhMwf6lSiBihWupGwF3Fqksg+aisg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-postcss": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-postcss/-/transformer-postcss-2.16.4.tgz", + "integrity": "sha512-jfmh9ho03H+qwz9S1b/a/oaOmgfMovtHKYDweIGMjKULKIee3AFRqo8RZIOuUMjDuqHWK8SqQmjery4syFV3Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/utils": "2.16.4", + "clone": "^2.1.2", + "nullthrows": "^1.1.1", + "postcss-value-parser": "^4.2.0", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-posthtml": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-posthtml/-/transformer-posthtml-2.16.4.tgz", + "integrity": "sha512-+GXsmGx1L25KQGQnwclgEuQe1t4QU+IoDkgN+Ikj+EnQCOWG4/ts2VpMBeqP5F18ZT4cCSRafj6317o/2lSGJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-raw": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-raw/-/transformer-raw-2.16.4.tgz", + "integrity": "sha512-7WDUPq+bW11G9jKxaQIVL+NPGolV99oq/GXhpjYip0SaGaLzRCW7gEk60cftuk0O7MsDaX5jcAJm3G/AX+LJKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-react-refresh-wrap": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.16.4.tgz", + "integrity": "sha512-MiLNZrsGQJTANKKa4lzZyUbGj/en0Hms474mMdQkCBFg6GmjfmXwaMMgtTfPA3ZwSp2+3LeObCyca/f9B2gBZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/error-overlay": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/utils": "2.16.4", + "react-refresh": "^0.16.0" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-svg": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.16.4.tgz", + "integrity": "sha512-0dm4cQr/WpfQP6N0xjFtwdLTxcONDfoLgTOMk4eNUWydHipSgmLtvUk/nOc/FWkwztRScfAObtZXOiPOd3Oy9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/plugin": "2.16.4", + "@parcel/rust": "2.16.4" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.16.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/types": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.16.4.tgz", + "integrity": "sha512-ctx4mBskZHXeDVHg4OjMwx18jfYH9BzI/7yqbDQVGvd5lyA+/oVVzYdpele2J2i2sSaJ87cA8nb57GDQ8kHAqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/types-internal": "2.16.4", + "@parcel/workers": "2.16.4" + } + }, + "node_modules/@parcel/types-internal": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/types-internal/-/types-internal-2.16.4.tgz", + "integrity": "sha512-PE6Qmt5cjzBxX+6MPLiF7r+twoC+V9Skt3zyuBQ+H1c0i9o07Bbz2NKX10nvlPukfmW6Fu/1RvTLkzBZR1bU6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/feature-flags": "2.16.4", + "@parcel/source-map": "^2.1.1", + "utility-types": "^3.11.0" + } + }, + "node_modules/@parcel/utils": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.16.4.tgz", + "integrity": "sha512-lkmxQHcHyOWZLbV8t+h2CGZIkPiBurLm/TS5wNT7+tq0qt9KbVwL7FP2K93TbXhLMGTmpI79Bf3qKniPM167Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/codeframe": "2.16.4", + "@parcel/diagnostic": "2.16.4", + "@parcel/logger": "2.16.4", + "@parcel/markdown-ansi": "2.16.4", + "@parcel/rust": "2.16.4", + "@parcel/source-map": "^2.1.1", + "chalk": "^4.1.2", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/workers": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-2.16.4.tgz", + "integrity": "sha512-dkBEWqnHXDZnRbTZouNt4uEGIslJT+V0c8OH1MPOfjISp1ucD6/u9ET8k9d/PxS9h1hL53og0SpBuuSEPLDl6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.16.4", + "@parcel/logger": "2.16.4", + "@parcel/profiler": "2.16.4", + "@parcel/types-internal": "2.16.4", + "@parcel/utils": "2.16.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.16.4" + } + }, + "node_modules/@swc/core": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.20.tgz", + "integrity": "sha512-zcdfS7k28OJIMuLhqRRyGja9PxrC2GXhFXkbQ1X15y6uZEMsTrqyej55V1DiJtlXaDOmwSLn5222YPT9ZoFxPA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.28" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.16.20", + "@swc/core-darwin-x64": "1.16.20", + "@swc/core-linux-arm-gnueabihf": "1.16.20", + "@swc/core-linux-arm64-gnu": "1.16.20", + "@swc/core-linux-arm64-musl": "1.16.20", + "@swc/core-linux-ppc64-gnu": "1.16.20", + "@swc/core-linux-s390x-gnu": "1.16.20", + "@swc/core-linux-x64-gnu": "1.16.20", + "@swc/core-linux-x64-musl": "1.16.20", + "@swc/core-win32-arm64-msvc": "1.16.20", + "@swc/core-win32-ia32-msvc": "1.16.20", + "@swc/core-win32-x64-msvc": "1.16.20" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.20.tgz", + "integrity": "sha512-i7NFSDnuE6tdBvBgmoVKKmburgcwZt/FZK2IusJaVGrjFrMUC1hrgE3RI+Kt9RhYM7vHvQj0W9j53zSxKEMbWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.20.tgz", + "integrity": "sha512-ho/LkT1Q3beSBeZGNZnm1mVbh5bKZHC0UGHtK3MswqOylbHtZoMXJBL40N8JpDj5SBxwECmdNEyXnU9HJ4F6RA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.20.tgz", + "integrity": "sha512-xm3/s3zp9WyNgB1Sl78tpH76MHgobI3TSenTY15bkSHyrdQa9W2MUSESHRPd7CHGFLfpWl/0jwwNp5+hwG0e9A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.20.tgz", + "integrity": "sha512-e6Um0U+VcTQyurOXhbratRQlrPITSRwugG00fhICbi2MGTi1vRiE4TMqGCjWpR1zPmZKXDGMw+yNFvh7AAgT2Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.20.tgz", + "integrity": "sha512-ekCO2obfK9NFwXGHfAQjjuU5mLDDC7qMaUJH/V/4iyzD0BqNhilr6McZrXLytOQ/xKtTNxLQ71wYrD3M2LW8Kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.20.tgz", + "integrity": "sha512-6/x2K3l4GNUWyaSmkywnth6ipZISzhVzOLsI66bj12/hSQYaKAyg9MKTwnuFf2B/iYA6GrjDghk36RYghRameQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.20.tgz", + "integrity": "sha512-1GTd8sqS/NpcF+Y3YDSE0bT31V61+hD7ZZWEsVyiyxk/HQt2zpk4zlJJHOASBLc+/CyUuBZ949n6QMszbIXkmQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.20.tgz", + "integrity": "sha512-XtMeYAgQa5UZhAX84TXn0sXPmhf1xHwcv0cl5hHiBXxTVbplY7WiTwIQU9uOtVm3VUqL/quyCm78/umUNZ7FmA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.20.tgz", + "integrity": "sha512-qeF3UILa+MA4zJ29CdQ8KE7YbGFPMQBxWo3Lh0Ed0ETFEJ8pCKISWo7wQG4/SmfM9HX4KT1Odoj6ixyBn6Ji2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.20.tgz", + "integrity": "sha512-ciMtHXAmE+jnrWWO14rErr6tfK34QZm8jZRSdRLPodqJlzrYDw7TqN60HC8Yim5Ua9a1r/poyfkpNft6GRXvxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.20.tgz", + "integrity": "sha512-MTbkgGooWebmjmATx1+QRECVZ+q4ICKNXCutwOrFVD5b5hdxla/yV4e6cRFrfNMowejaNpmMQg31ENWt0Yfzpg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.16.20", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.20.tgz", + "integrity": "sha512-Lf5oMaSdJBImtGhDecasxcIa7Z5LeJ4aGmBOY7TScroO+A/MxxB9PslIpgV0rtG/FcxtipN7hveBg9ZTeboaaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", + "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.32.tgz", + "integrity": "sha512-UvuxFU9P3hM29LrQdwu0cD6w6/+SFr+UlB9s/knI7+O2rK9xFxKUwSDWxGdbbqQ9/V7BJ5s0NI5nQAE4uJp3gQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.29.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.29", + "caniuse-lite": "^1.0.30001816", + "electron-to-chromium": "^1.5.459", + "node-releases": "^2.0.59", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001819", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", + "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "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/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.464", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz", + "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-port": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", + "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/lmdb": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-2.8.5.tgz", + "integrity": "sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "msgpackr": "^1.9.5", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.1.1", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "2.8.5", + "@lmdb/lmdb-darwin-x64": "2.8.5", + "@lmdb/lmdb-linux-arm": "2.8.5", + "@lmdb/lmdb-linux-arm64": "2.8.5", + "@lmdb/lmdb-linux-x64": "2.8.5", + "@lmdb/lmdb-win32-x64": "2.8.5" + } + }, + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/msgpackr": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/msgpackr-extract/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/msgpackr-extract/node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp-build-optional-packages/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", + "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ordered-binary": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.3.tgz", + "integrity": "sha512-Bc8LnA2x4ZHRQ9LjCm/NOdD3+CTQPhGNu8m7/6+ERtiB6Bb/fn7nCySwcmAC+5WCqwelpSAQgo1VX9BGpsC0tQ==", + "dev": true, + "license": "MIT" + }, + "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/parcel": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/parcel/-/parcel-2.16.4.tgz", + "integrity": "sha512-RQlrqs4ujYNJpTQi+dITqPKNhRWEqpjPd1YBcGp50Wy3FcJHpwu0/iRm7XWz2dKU/Bwp2qCcVYPIeEDYi2uOUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/config-default": "2.16.4", + "@parcel/core": "2.16.4", + "@parcel/diagnostic": "2.16.4", + "@parcel/events": "2.16.4", + "@parcel/feature-flags": "2.16.4", + "@parcel/fs": "2.16.4", + "@parcel/logger": "2.16.4", + "@parcel/package-manager": "2.16.4", + "@parcel/reporter-cli": "2.16.4", + "@parcel/reporter-dev-server": "2.16.4", + "@parcel/reporter-tracer": "2.16.4", + "@parcel/utils": "2.16.4", + "chalk": "^4.1.2", + "commander": "^12.1.0", + "get-port": "^4.2.0" + }, + "bin": { + "parcel": "lib/bin.js" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.16.0.tgz", + "integrity": "sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/apps/devtypes/package.json b/apps/devtypes/package.json new file mode 100644 index 0000000..e94e71c --- /dev/null +++ b/apps/devtypes/package.json @@ -0,0 +1,29 @@ +{ + "name": "raftjs-devtypes-app", + "version": "0.1.0", + "private": true, + "description": "Device Types: browse Raft device type records and manage a device's device type file", + "scripts": { + "start": "parcel src/index.html", + "clean": "rimraf dist .parcel-cache", + "build": "npm run clean && parcel build src/index.html --public-url ./", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "author": "Rob Dobson", + "license": "MIT", + "devDependencies": { + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@types/web-bluetooth": "^0.0.21", + "buffer": "^6.0.3", + "parcel": "^2.16.3", + "process": "^0.11.10", + "rimraf": "^6.1.3", + "typescript": "^5.3.3" + }, + "dependencies": { + "isomorphic-ws": "^5.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/apps/devtypes/src/App.tsx b/apps/devtypes/src/App.tsx new file mode 100644 index 0000000..8741f7c --- /dev/null +++ b/apps/devtypes/src/App.tsx @@ -0,0 +1,120 @@ +import React, { useEffect, useState, useSyncExternalStore } from "react"; +import { DevTypesModel } from "./model"; +import { CataloguePanel } from "./CataloguePanel"; +import { DevicePanel } from "./DevicePanel"; +import { SourcesPanel } from "./SourcesPanel"; +import { TypeFilePanel } from "./TypeFilePanel"; +import { ExportPanel } from "./ExportPanel"; + +export const model = new DevTypesModel(); +(window as unknown as { devtypesModel: DevTypesModel }).devtypesModel = model; // for scripted tests + +// Re-render when the model changes +export function useModel(): DevTypesModel { + useSyncExternalStore((cb) => model.subscribe(cb), () => model.version); + return model; +} + +const TABS = ["Catalogue", "Device", "Sources", "Type file", "Export"] as const; +type Tab = typeof TABS[number]; + +function ConnectBar() { + const m = useModel(); + const [host, setHost] = useState(""); + const secure = window.isSecureContext; + const hasBle = "bluetooth" in navigator; + const hasSerial = "serial" in navigator; + // ws:// from an https page is blocked (mixed content), so WiFi is offered only on http pages and localhost + const wsAllowed = location.protocol === "http:" || location.hostname === "localhost" || location.hostname === "127.0.0.1"; + if (m.connState === "connected") + return ( +
+ Connected ({m.connMethod === "WebBLE" ? "Bluetooth" : m.connMethod === "WebSerial" ? "serial" : "WiFi"}) + +
+ ); + return ( +
+ {m.connState === "connecting" && Connecting...} + + + {wsAllowed && ( + <> + setHost(e.target.value)} size={16} /> + + + )} +
+ ); +} + +function PushConfirm() { + const m = useModel(); + const check = m.pushState.check; + if (!m.pushState.awaitingConfirm || !check) + return null; + return ( +
+
+

Apply this type file?

+

The device checked the file: {check.added ?? 0} added, {check.replaced ?? 0} replaced, {check.unchanged ?? 0} unchanged,{" "} + {check.disabled ?? 0} disabled, {check.rejected ?? 0} rejected. Applying it restarts the device.

+
+ + + + {(check.entries ?? []).map((e) => ( + + + + + ))} + +
TypeOutcomeDecoderNotes
{e.key}{e.outcome}{e.decoder ?? (e.noDecoder ? "none" : "")}{[e.error, e.warnings].filter(Boolean).join("; ")}
+
+
+ + +
+
+
+ ); +} + +export default function App() { + const m = useModel(); + const [tab, setTab] = useState("Catalogue"); + useEffect(() => { + void model.start(); + }, []); + useEffect(() => { + if (m.connState === "connected") + setTab((t) => (t === "Catalogue" ? "Device" : t)); + }, [m.connState]); + return ( + <> +
+

Raft Device Types

+ +
+ +
+ {m.connError &&
Not connected: {m.connError}
} + {m.message &&
{m.message.text}{" "} +
} + {tab === "Catalogue" && } + {tab === "Device" && } + {tab === "Sources" && } + {tab === "Type file" && } + {tab === "Export" && } +
+ + + ); +} diff --git a/apps/devtypes/src/CataloguePanel.tsx b/apps/devtypes/src/CataloguePanel.tsx new file mode 100644 index 0000000..72b7f39 --- /dev/null +++ b/apps/devtypes/src/CataloguePanel.tsx @@ -0,0 +1,140 @@ +import React, { useMemo, useState } from "react"; +import { DevTypeJson, devTypeJsonGet, devTypeJsonToPlain, writeDevTypeJson } from "../../../src/main"; +import { useModel } from "./App"; + +// The name of the selection's source of RaftCore's records +export function bundledSourceName(m: ReturnType): string | undefined { + return Object.keys(m.selection.sources).find((n) => m.sourceKind(n) === "bundled"); +} + +export function RecordView({ record, keyName }: { record: DevTypeJson | undefined; keyName: string }) { + if (record === undefined) + return

Loading the record...

; + const plain = devTypeJsonToPlain(record) as Record; + const info = plain.devInfoJson ?? {}; + const attrs: any[] = info.resp?.a ?? []; + const actions: any[] = info.actions ?? []; + return ( + <> +
+
Key
{keyName}
+ {info.name && <>
Name
{info.name}
} + {info.manu && <>
Manufacturer
{info.manu}
} + {info.desc && <>
Description
{info.desc}
} + {plain.addresses && <>
Addresses
{plain.addresses}
} + {plain.hwTypeId && <>
Hardware type ID
{plain.hwTypeId}
} + {plain.pollInfo?.i !== undefined && <>
Poll interval
{plain.pollInfo.i / 1000} ms
} + {info.resp?.c && <>
Decoding
custom decode code (decoded on the device only with the code interpreter)
} + {plain.initCode && <>
Init code
yes (runs on the device only with the code interpreter, unless compiled in)
} +
+ {attrs.length > 0 && ( +
+ + + + {attrs.map((a, i) => ( + + + + + ))} + +
AttributeTypeUnitsRange
{a.n}{a.t} + {Array.isArray(a.r) ? a.r.join(" to ") : ""}
+
+ )} + {actions.length > 0 &&

Actions: {actions.map((a) => a.n).join(", ")}

} +
+ The record +
{writeDevTypeJson(record, { indent: 2 })}
+
+ + ); +} + +// Units are written with HTML entities (°C): allow those and nothing else +function escapeUnits(u: unknown): string { + if (typeof u !== "string") + return ""; + return u.replace(/&(?!#?[a-zA-Z0-9]+;)/g, "&").replace(//g, ">"); +} + +export function CataloguePanel() { + const m = useModel(); + const [query, setQuery] = useState(""); + const [selected, setSelected] = useState(undefined); + const entries = m.catalogue?.entries() ?? []; + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) + return entries; + return entries.filter((e) => { + const s = e.summary; + const clas = Array.isArray(s.clas) ? s.clas.join(" ") : (s.clas ?? ""); + const hay = [e.key, s.name, s.manu, s.type, clas, s.addresses, s.hwTypeId].join(" ").toLowerCase(); + // An address ("0x6a") also matches records whose range holds it + if (/^0x[0-9a-f]{1,2}$/.test(q)) { + const addr = parseInt(q, 16); + if (m.recordsForAddress(addr).some((r) => r.key === e.key)) + return true; + } + return hay.includes(q); + }); + }, [query, entries, m]); + const bundled = bundledSourceName(m); + const records = bundled ? m.sourceRecords.get(bundled)?.records : undefined; + const current = selected ?? filtered[0]?.key; + const pattern = bundled && current ? `${bundled}:${current}` : undefined; + const label = m.catalogue?.label; + + if (m.catalogueError) + return
{m.catalogueError}
; + if (!m.catalogue) + return

Loading the catalogue...

; + return ( + <> +
+ setQuery(e.target.value)} /> + + {m.device?.status?.raftCore && the device has RaftCore {m.device.status.raftCore}} +
+
+
+ {filtered.map((e) => ( +
setSelected(e.key)}> +
{e.summary.name ?? e.key}{bundled && m.includes(`${bundled}:${e.key}`) && + in the type file}
+
{e.key}{e.summary.manu ? ` · ${e.summary.manu}` : ""}{e.summary.addresses ? ` · ${e.summary.addresses}` : ""}
+
+ ))} + {filtered.length === 0 &&
No records match
} +
+
+ {current && ( +
+

{m.catalogueEntry(current)?.summary.name ?? current}

+
+ {pattern && ( + + )} + On GitHub +
+ + {records?.get(current) !== undefined && devTypeJsonGet(records.get(current), "devInfoJson", "resp") === undefined && +

This record has no data (nothing is polled).

} +
+ )} +
+
+ + ); +} diff --git a/apps/devtypes/src/DevicePanel.tsx b/apps/devtypes/src/DevicePanel.tsx new file mode 100644 index 0000000..2835811 --- /dev/null +++ b/apps/devtypes/src/DevicePanel.tsx @@ -0,0 +1,142 @@ +import React from "react"; +import { DeviceTypePolicyName } from "../../../src/main"; +import { useModel } from "./App"; +import { bundledSourceName } from "./CataloguePanel"; +import { download, hex2 } from "./util"; + +export function DevicePanel() { + const m = useModel(); + if (m.connState !== "connected") + return ( +
+

Connect to a device

+

Connect to a Raft device by Bluetooth or USB serial (Chrome or Edge) to see the devices it has found, the device + types it knows and its device type file, and to change that file.

+ {location.protocol === "https:" && +

WiFi isn't offered on this page: a page served over https can't open a connection to a device on + your network. To use WiFi, run this app on your computer (it is in raftjs, apps/devtypes).

} +
+ ); + const d = m.device; + if (!d) + return

Reading the device...

; + const s = d.status; + const bundled = bundledSourceName(m); + const table = d.table ?? []; + return ( + <> + {d.error &&
{d.error}
} +
+
+

{d.systemName ?? "Device"} {d.systemVersion}

+
+
RaftCore
{s?.raftCore ?? not reported (firmware before 2026-10-24)}
+
On-device decoding
+
{s?.decode === undefined ? not reported : + s.decode.attrs ? (s.decode.code ? "attributes and custom code" : "attributes (not custom code)") : "none"}
+
Device types
{table.length} ({table.filter((t) => t.src === "base").length} compiled in)
+
+
+ + +
+
+
+

Device type file

+ {s?.rslt === "ok" ? ( + <> +
+
File
{s.file || "none configured"}
+
State
{s.state}{s.crc && s.present ? (CRC {s.crc}) : null}
+ {s.present && <>
Entries
{s.added} added, {s.replaced} replaced, {s.disabled} disabled, {s.rejected} rejected
} + {s.firstError && <>
First error
{s.firstError}
} +
Limits
{s.maxFileBytes} bytes; {s.records}/{s.maxRecords} extended records
+
+ {s.restartRequired &&
The file has changed since startup - a restart applies it
} +
+ +
+ + ) :

Not supported by this firmware.

} +
+
+ + {d.pulled && ( +
+

The device's type file {d.pulled.path}

+

{d.pulled.text.length} bytes, CRC {d.pulled.crc}: {d.pulled.keys.join(", ") || "no entries"}

+

{d.pulled.sources ? "It says how it was made (_sources), so the selection can be loaded and changed." : + "It doesn't say how it was made (no _sources): its records can be loaded as a local source."}

+
+ + +
+
+ )} + +
+

Devices found

+ {d.devices.length === 0 && d.unidentified.length === 0 &&

None.

} + {d.devices.length > 0 && ( +
+ + + + {d.devices.map((dev) => ( + + ))} + +
DeviceIDType
{dev.displayName}{dev.deviceid}{dev.key ?? ""}
+
+ )} + {d.unidentified.map((u) => { + const candidates = m.recordsForAddress(u.addr); + return ( +
+ Not identified: {u.bus} address {hex2(u.addr)}{u.slot ? ` (slot ${u.slot})` : ""} responds, but no device type it + knows matches.{" "} + {candidates.length > 0 ? <>Records for this address in the catalogue:{" "} + {candidates.map((c) => { + const pattern = bundled ? `${bundled}:${c.key}` : undefined; + const known = table.some((t) => t.key === c.key); + return ( + + {c.key} ({c.summary.name}){known ? - the device has it : pattern && ( + <> )} + + ); + })} : The catalogue has no record for this address.} +
+ ); + })} +
+ +
+

Device types the device knows

+

Compiled-in types can be disabled by the type file (so they aren't used to identify devices).

+
+ + + + {table.map((t) => ( + + + + + + + ))} + +
TypeFromPolicy nowIn the type file
{t.key}{t.src === "base" ? "compiled" : t.src === "override" ? "type file (replaces compiled)" : "type file"}{t.policy}{t.protected ? protected : null}{t.src === "base" && !t.protected ? ( + + ) : t.protected ? a system device expects it : null}
+
+
+ + ); +} diff --git a/apps/devtypes/src/ExportPanel.tsx b/apps/devtypes/src/ExportPanel.tsx new file mode 100644 index 0000000..73b9a08 --- /dev/null +++ b/apps/devtypes/src/ExportPanel.tsx @@ -0,0 +1,42 @@ +import React, { useEffect, useState } from "react"; +import { useModel } from "./App"; +import { download } from "./util"; + +export function ExportPanel() { + const m = useModel(); + const [asBuiltin, setAsBuiltin] = useState(false); + const [lock, setLock] = useState(); + useEffect(() => { + void m.lockText().then(setLock).catch(() => setLock(undefined)); + }, [m.version]); + const sourcesText = m.sourcesFileText(asBuiltin); + const sysDir = `systypes/${m.sysType}`; + return ( + <> +
+

Put this setup into a project

+

The download is laid out as a Raft project: unpack it into the project folder. DeviceTypeSources.json selects the + records (and its lock pins git sources to their commits); local record files go where its path sources say.

+
+ + +
+
+ + + + +
+ {!asBuiltin &&

With "fsImage": true in its typeFile section, a build puts the type file in the + file system image, so flashing the board installs it.

} +
+
+

{sysDir}/DeviceTypeSources.json

+
{sourcesText}
+ {lock && <>

{sysDir}/DeviceTypeSources.lock

{lock}
} +
+ + ); +} diff --git a/apps/devtypes/src/SourcesPanel.tsx b/apps/devtypes/src/SourcesPanel.tsx new file mode 100644 index 0000000..7a9aeb7 --- /dev/null +++ b/apps/devtypes/src/SourcesPanel.tsx @@ -0,0 +1,141 @@ +import React, { useState } from "react"; +import { DeviceTypeRecordFile, DeviceTypeSourceRecordsWithFiles } from "../../../src/main"; +import { useModel } from "./App"; + +async function readFiles(list: FileList | null): Promise { + const files = Array.from(list ?? []); + return Promise.all(files.map(async (f) => ({ path: (f as File & { webkitRelativePath?: string }).webkitRelativePath || f.name, text: await f.text() }))); +} + +const NAME_RE = /^[A-Za-z0-9_.-]+$/; + +function AddGitSource() { + const m = useModel(); + const [name, setName] = useState(""); + const [url, setUrl] = useState(""); + const [ref, setRef] = useState(""); + const [path, setPath] = useState(""); + const valid = NAME_RE.test(name) && !m.selection.sources[name] && url && ref; + return ( +
+

Add a git source

+

A public GitHub repo with a folder of record files (default records) or one record file, at a tag, + branch or commit - as a git source in DeviceTypeSources.json.

+
+ setName(e.target.value)} size={10} /> + setUrl(e.target.value)} size={34} /> + setRef(e.target.value)} size={14} /> + setPath(e.target.value)} size={12} /> + +
+
+ ); +} + +function AddLocalSource() { + const m = useModel(); + const [name, setName] = useState("local"); + const [path, setPath] = useState("../../devtypes"); + const valid = NAME_RE.test(name) && !m.selection.sources[name]; + const add = async (list: FileList | null, asFolder: boolean) => { + const files = await readFiles(list); + if (files.length) + await m.addLocalSource(name, path, asFolder ? files : files, asFolder || files.length > 1); + }; + return ( +
+

Add record files from this computer

+

Record files (<Key>.json) or a catalogue file - a path source. They are kept + in this browser, and the export puts them at the path below (relative to systypes/<SysType>/).

+
+ setName(e.target.value)} size={10} title="source name" /> + setPath(e.target.value)} size={16} title="path in the project" /> + + + {!valid && choose a new name} +
+
+ ); +} + +function ImportProject() { + const m = useModel(); + const [common, setCommon] = useState(); + const [sysType, setSysType] = useState(); + const [lock, setLock] = useState(); + const doImport = async () => { + const files = []; + if (common) + files.push({ name: "systypes/Common/DeviceTypeSources.json", text: await common.text() }); + if (sysType) + files.push({ name: `systypes/${m.sysType}/DeviceTypeSources.json`, text: await sysType.text() }); + await m.importProject(files, lock ? await lock.text() : undefined); + }; + return ( +
+

Import a project's selection

+

Its DeviceTypeSources.json files (Common's and the SysType's) and lock: git sources are read at + the locked commits, so the records are those the project's build uses. A path source's records are then picked here.

+
+ + + + +
+
+ ); +} + +export function SourcesPanel() { + const m = useModel(); + return ( + <> +
+

Sources

+
+ + + + {Object.entries(m.selection.sources).map(([name, spec]) => { + const kind = m.sourceKind(name); + const loaded = m.sourceRecords.get(name) as DeviceTypeSourceRecordsWithFiles | undefined; + const error = m.sourceErrors.get(name); + return ( + + + + + + + + ); + })} + +
NameKindWhatRecords
{name}{kind} + {kind === "bundled" && <>RaftCore's records {m.catalogue ? `(${m.catalogue.label})` : ""}} + {kind === "git" && <>{String(spec.git)} at {String(spec.ref)} + {loaded?.commit &&
commit {loaded.commit.slice(0, 12)}
}} + {kind === "path" && {String(spec.path)}} + {error &&
{error}
} +
{loaded ? loaded.records.size : ""} +
+ {kind === "git" && } + {kind === "path" && } + +
+
+
+ {m.sourcesLoading &&

Reading sources...

} + {!Object.keys(m.selection.sources).some((n) => m.sourceKind(n) === "bundled") && + } +
+ + + + + ); +} diff --git a/apps/devtypes/src/SystemTypeDevTypes.ts b/apps/devtypes/src/SystemTypeDevTypes.ts new file mode 100644 index 0000000..543c2e8 --- /dev/null +++ b/apps/devtypes/src/SystemTypeDevTypes.ts @@ -0,0 +1,23 @@ +// The system type for any Raft device: only commands are sent (no device data is subscribed to) + +import { RaftEventFn, RaftSystemType, RaftSystemUtils, SystemCapabilities } from "../../../src/main"; + +export default class SystemTypeDevTypes implements RaftSystemType { + nameForDialogs = "Raft device"; + defaultWiFiHostname = "Raft"; + firmwareDestName = "ricfw"; + normalFileDestName = "fs"; + connectorOptions = { wsSuffix: "ws", bleConnItvlMs: 50 }; + BLEServiceUUIDs = ["aa76677e-9cfd-4626-a510-0d305be57c8d", "da903f65-d5c2-4f4d-a065-d1aade7af874"]; + BLECmdUUID = "aa76677e-9cfd-4626-a510-0d305be57c8e"; + BLERespUUID = "aa76677e-9cfd-4626-a510-0d305be57c8f"; + capabilities: SystemCapabilities = { tuning: { bleMaxWriteSize: 244 } }; + + setup(_systemUtils: RaftSystemUtils, _onEvent: RaftEventFn | null): void { + // nothing to set up + } + + subscribeForUpdates = null; + stateIsInvalid = null; + rxOtherMsgType = null; +} diff --git a/apps/devtypes/src/TypeFilePanel.tsx b/apps/devtypes/src/TypeFilePanel.tsx new file mode 100644 index 0000000..48e9c40 --- /dev/null +++ b/apps/devtypes/src/TypeFilePanel.tsx @@ -0,0 +1,134 @@ +import React, { useState } from "react"; +import { TypeFileBuildEntry } from "../../../src/main"; +import { useModel } from "./App"; + +const STAGE_TEXT: Record = { + status: "Reading the device's file", upload: "Uploading", check: "The device is checking the file", confirm: "Waiting for you", + apply: "Applying", restart: "The device is restarting", verify: "Checking the device loaded it", done: "Done", +}; + +function decodingNote(entry: TypeFileBuildEntry, decode: { attrs?: boolean; code?: boolean } | undefined): string { + if (!entry.noDecoder) + return entry.kind === "override" ? "the compiled decoder" : ""; + if (entry.decoder === "none") + return decode?.code ? "on the device (code interpreter)" : decode ? "in this app only - DataLogger, ROS and named values won't see it" : + "on the device only with the code interpreter"; + return decode?.attrs === false ? "in this app only - the firmware has no runtime decoding" : "on the device, from its attributes"; +} + +function SourceRecords({ name }: { name: string }) { + const m = useModel(); + const [filter, setFilter] = useState(""); + const loaded = m.sourceRecords.get(name); + if (!loaded) + return null; + const all = `${name}:*`; + const keys = Array.from(loaded.records.keys()).filter((k) => k.toLowerCase().includes(filter.toLowerCase())); + return ( +
+ {name} ({loaded.records.size} records{m.includes(all) ? ", all in the type file" : ""}) +
+ + setFilter(e.target.value)} /> +
+
+ {keys.map((k) => ( + + ))} +
+
+ ); +} + +export function TypeFilePanel() { + const m = useModel(); + const b = m.build; + const tf = m.selection.typeFile; + const decode = m.device?.status?.decode; + const connected = m.connState === "connected"; + const busy = m.pushState.stage !== undefined && !m.pushState.result; + const pushResult = m.pushState.result; + return ( + <> +
+

Selection

+ {(tf.include ?? []).length === 0 && Object.keys(tf.policy ?? {}).length === 0 && +

Nothing yet. Add records from the catalogue, the device's unidentified addresses or the sources below, and + disable compiled types on the Device tab.

} +
+ {(tf.include ?? []).map((p) => ( + {p} + ))} + {Object.entries(tf.policy ?? {}).map(([k, p]) => ( + {k}: {p} + ))} +
+ {((tf.include ?? []).length > 0 || Object.keys(tf.policy ?? {}).length > 0) && + } +
+ {Object.keys(m.selection.sources).map((name) => )} + +
+

The type file

+ {b.offline &&
No device connected: every record is shown as new. Connect to compare with the records the + device has compiled in (those identical to a compiled record are left out).
} + {b.unknownCompiled.length > 0 &&
The device's current type file replaces {b.unknownCompiled.join(", ")}, + so their compiled records can't be read: they are kept in the file.
} + {b.error &&
{b.error}
} + {b.result && ( + <> +

{b.result.bytes} of {b.maxBytes} bytes · CRC {b.result.crc} + {m.device?.pulled && m.device.pulled.crc === b.result.crc && · the device has this file}

+
+ + + + {b.result.entries.map((e) => ( + + + + + + + + ))} + +
TypeSourceWhat it doesDecodedNotes
{e.key}{b.selection?.origin.get(e.key) ?? ""}{e.kind === "new" ? "adds a type" : e.kind === "override" ? "replaces the compiled record" : + e.kind === "unchanged" ? "the same as compiled - left out" : e.policy === "disabledClaim" ? "disables it (address kept)" : "disables it"}{e.kind === "new" || e.kind === "override" ? decodingNote(e, decode) : ""}{e.initCode === "runtime" ? "init code: needs the code interpreter" : e.initCode ? "init code: compiled one used without the interpreter" : ""}
+
+ + )} +
+ + + {!connected && connect a device to push} +
+ {busy &&

{STAGE_TEXT[m.pushState.stage as string] ?? m.pushState.stage}{m.pushState.detail ? ` (${m.pushState.detail})` : ""}...

} + {pushResult && ( +
+ {pushResult.message} + {pushResult.outcome === "deviceFileChanged" && <> - read it again on the Device tab, or{" "} + } +
+ )} + {pushResult?.check?.entries && pushResult.outcome !== "applied" && ( +
+ + + + {pushResult.check.entries.map((e) => ( + + + ))} + +
TypeThe device's checkNotes
{e.key}{e.outcome}{[e.error, e.warnings].filter(Boolean).join("; ")}
+
+ )} +
+ + ); +} diff --git a/apps/devtypes/src/index.html b/apps/devtypes/src/index.html new file mode 100644 index 0000000..1933c23 --- /dev/null +++ b/apps/devtypes/src/index.html @@ -0,0 +1,15 @@ + + + + + + + Raft Device Types + + + + +
+ + + diff --git a/apps/devtypes/src/index.tsx b/apps/devtypes/src/index.tsx new file mode 100644 index 0000000..7ac17ba --- /dev/null +++ b/apps/devtypes/src/index.tsx @@ -0,0 +1,5 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; + +createRoot(document.getElementById("root") as HTMLElement).render(); diff --git a/apps/devtypes/src/model.ts b/apps/devtypes/src/model.ts new file mode 100644 index 0000000..40177ff --- /dev/null +++ b/apps/devtypes/src/model.ts @@ -0,0 +1,669 @@ +// The app's state and what it does, apart from the views: the catalogue, the sources and the selection (a +// DeviceTypeSources.json), the device and its type file, building, pushing and exporting + +import { + DevManTypeFileCheck, DevManTypeFileStatus, DevTypeJson, DevTypeJsonObject, DeviceTypeCatalogueEntry, DeviceTypeCatalogueIndex, + DeviceTypeFileManager, DeviceTypePolicyName, DeviceTypeRecordFile, DeviceTypeSelectionResult, DeviceTypeSourceRecords, + DeviceTypeSourceRecordsWithFiles, DevManTypeTableEntry, MergedDeviceTypeSources, RaftConnEvent, RaftConnector, RaftCoreCatalogue, + RaftSystemUtils, TypeFileBuildResult, TypeFilePushResult, TypeFilePushStage, TypeFileResponse, buildTypeFile, + deviceTypeLockText, deviceTypeSourceKind, devTypeJsonFromPlain, devTypeJsonGet, devTypeJsonToPlain, isDevTypeJsonObject, + mergeDeviceTypeSources, parseDevTypeJson, readGitHubSource, recordsFromFiles, resolveDeviceTypeSelection, sha256Hex, + writeDevTypeJson, RAFTCORE_PAGES_CATALOGUE_URL, +} from "../../../src/main"; +import SystemTypeDevTypes from "./SystemTypeDevTypes"; +import { joinPath, makeZip, parseAddresses, readStore, writeStore } from "./util"; + +export interface SectionSpec { include?: string[]; exclude?: string[]; policy?: Record; fsImage?: boolean } +export interface SourcesFile { sources: Record>; builtin?: SectionSpec; typeFile: SectionSpec } + +// Records the user picked for a path source (kept in the browser) +export interface LocalSourceFiles { files: DeviceTypeRecordFile[]; asFolder: boolean } + +export interface DeviceInfo { + systemName?: string; + systemVersion?: string; + status?: TypeFileResponse; + table?: DevManTypeTableEntry[]; + devices: Array<{ deviceid: string; displayName?: string; dtIdx?: number; key?: string }>; + unidentified: Array<{ bus: string; addr: number; slot: number; state: string }>; + pulled?: { text: string; crc: string; path: string; keys: string[]; sources?: unknown }; + error?: string; +} + +export interface BuildState { + merged?: MergedDeviceTypeSources; + selection?: DeviceTypeSelectionResult; + result?: TypeFileBuildResult; + error?: string; + offline: boolean; // no device: records shown as new + unknownCompiled: string[]; // keys the device's type file overrides (compiled record unknown) + maxBytes: number; +} + +export interface PushState { + stage?: TypeFilePushStage; + detail?: string; + check?: TypeFileResponse; + awaitingConfirm?: boolean; + result?: TypeFilePushResult; +} + +const DEFAULT_SELECTION: SourcesFile = { sources: { raftcore: { bundled: true } }, typeFile: { include: [] } }; +const STORE_SELECTION = "devtypes.selection"; +const STORE_LOCAL = "devtypes.localSources"; +const STORE_LOCK = "devtypes.lockedCommits"; +const STORE_SYSTYPE = "devtypes.sysType"; + +export type ConnState = "disconnected" | "connecting" | "connected"; + +function catalogueBaseUrl(): string { + const param = new URLSearchParams(location.search).get("catalogue"); + if (param) + return param.replace(/\/+$/, ""); + // Hosted beside the catalogue (RaftCore's Pages site, devtypes/app/): use it from the same origin + const m = /^(.*\/devtypes)\/app(\/|$)/.exec(location.pathname); + if (m) + return location.origin + m[1]; + return RAFTCORE_PAGES_CATALOGUE_URL; +} + +const browserFetch = (url: string, init?: { headers?: Record }) => fetch(url, init); + +export class DevTypesModel { + // State (views read these and re-render on change) + catalogueBase = catalogueBaseUrl(); + index?: DeviceTypeCatalogueIndex; + catalogue?: RaftCoreCatalogue; + catalogueError?: string; + selection: SourcesFile = readStore(STORE_SELECTION, DEFAULT_SELECTION); + localSources: Record = readStore(STORE_LOCAL, {}); + lockedCommits: Record = readStore(STORE_LOCK, {}); + sysType: string = readStore(STORE_SYSTYPE, "Main"); + sourceRecords = new Map(); + sourceErrors = new Map(); + sourcesLoading = false; + connState: ConnState = "disconnected"; + connError?: string; + connMethod?: string; + device?: DeviceInfo; + compiled = new Map(); + build: BuildState = { offline: true, unknownCompiled: [], maxBytes: 65536 }; + pushState: PushState = {}; + message?: { text: string; error?: boolean }; + + private _listeners = new Set<() => void>(); + private _connector?: RaftConnector; + private _manager?: DeviceTypeFileManager; + private _confirmResolve?: (ok: boolean) => void; + private _version = 0; + + get version() { + return this._version; + } + + subscribe(listener: () => void): () => void { + this._listeners.add(listener); + return () => this._listeners.delete(listener); + } + + private changed() { + this._version++; + this._listeners.forEach((l) => l()); + } + + notify(text: string, error = false) { + this.message = { text, error }; + this.changed(); + } + + dismissMessage() { + this.message = undefined; + this.changed(); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Catalogue + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + async start() { + try { + this.index = await RaftCoreCatalogue.index(browserFetch, this.catalogueBase); + await this.openCatalogue(RaftCoreCatalogue.chooseVersion(this.index, undefined) ?? "main"); + } catch (err) { + this.catalogueError = `The catalogue couldn't be read from ${this.catalogueBase}: ${errText(err)}`; + this.changed(); + } + await this.loadSources(); + } + + async openCatalogue(label: string) { + try { + this.catalogue = await RaftCoreCatalogue.open(browserFetch, label, this.catalogueBase); + this.catalogueError = undefined; + } catch (err) { + this.catalogueError = errText(err); + } + this.sourceRecords.forEach((_, name) => { + if (this.sourceKind(name) === "bundled") + this.sourceRecords.delete(name); + }); + this.changed(); + await this.loadSources(); + } + + catalogueEntry(key: string): DeviceTypeCatalogueEntry | undefined { + return this.catalogue?.entries().find((e) => e.key === key); + } + + // Catalogue records that can be at an I2C address + recordsForAddress(addr: number): DeviceTypeCatalogueEntry[] { + return (this.catalogue?.entries() ?? []).filter((e) => parseAddresses(e.summary.addresses).includes(addr)); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Sources and the selection + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + sourceKind(name: string) { + const spec = this.selection.sources[name]; + return spec ? deviceTypeSourceKind(devTypeJsonFromPlain(spec) as DevTypeJsonObject) : undefined; + } + + private saveSelection() { + writeStore(STORE_SELECTION, this.selection); + writeStore(STORE_LOCAL, this.localSources); + writeStore(STORE_LOCK, this.lockedCommits); + writeStore(STORE_SYSTYPE, this.sysType); + } + + setSysType(name: string) { + this.sysType = name.replace(/[^A-Za-z0-9_-]/g, "") || "Main"; + this.saveSelection(); + this.changed(); + } + + // Read every source's records that aren't read yet + async loadSources() { + this.sourcesLoading = true; + this.changed(); + for (const [name, spec] of Object.entries(this.selection.sources)) { + if (this.sourceRecords.has(name)) + continue; + this.sourceErrors.delete(name); + try { + const kind = this.sourceKind(name); + if (kind === "bundled") { + if (!this.catalogue) + throw new Error("RaftCore's catalogue isn't available"); + this.sourceRecords.set(name, await this.catalogue.records()); + } else if (kind === "git") { + const raftCore = this.device?.status?.raftCore; + this.sourceRecords.set(name, await readGitHubSource(name, devTypeJsonFromPlain(spec) as DevTypeJsonObject, + { fetch: browserFetch, raftCore }, this.lockedCommits[name])); + } else if (kind === "path") { + const local = this.localSources[name]; + if (!local) + throw new Error("pick its record files (a path source's records are only on your computer)"); + this.sourceRecords.set(name, recordsFromFiles(local.files, local.asFolder)); + } else { + throw new Error('needs "bundled", "path" or "git"'); + } + } catch (err) { + this.sourceErrors.set(name, errText(err)); + } + } + this.sourcesLoading = false; + await this.rebuild(); + } + + async addBundledSource() { + const name = this.selection.sources.raftcore ? "raftcore2" : "raftcore"; + this.selection.sources = { [name]: { bundled: true }, ...this.selection.sources }; + this.saveSelection(); + await this.loadSources(); + } + + async addGitSource(name: string, url: string, ref: string, path: string) { + const spec: Record = { git: url.trim(), ref: ref.trim() }; + if (path.trim()) + spec.path = path.trim(); + this.selection.sources[name] = spec; + delete this.lockedCommits[name]; + this.sourceRecords.delete(name); + this.saveSelection(); + await this.loadSources(); + } + + async addLocalSource(name: string, path: string, files: DeviceTypeRecordFile[], asFolder: boolean) { + this.selection.sources[name] = { path: path.trim() || `../../${name}` }; + this.localSources[name] = { files, asFolder }; + this.sourceRecords.delete(name); + this.saveSelection(); + await this.loadSources(); + } + + async setLocalFiles(name: string, files: DeviceTypeRecordFile[], asFolder: boolean) { + this.localSources[name] = { files, asFolder }; + this.sourceRecords.delete(name); + this.saveSelection(); + await this.loadSources(); + } + + // A git source: resolve its ref again (take new commits on a branch) + async updateGitSource(name: string) { + delete this.lockedCommits[name]; + this.sourceRecords.delete(name); + this.saveSelection(); + await this.loadSources(); + } + + async removeSource(name: string) { + delete this.selection.sources[name]; + delete this.localSources[name]; + delete this.lockedCommits[name]; + this.sourceRecords.delete(name); + this.sourceErrors.delete(name); + const tf = this.selection.typeFile; + tf.include = (tf.include ?? []).filter((p) => !p.startsWith(name + ":")); + tf.exclude = (tf.exclude ?? []).filter((p) => !p.startsWith(name + ":")); + this.saveSelection(); + await this.rebuild(); + } + + includes(pattern: string): boolean { + return (this.selection.typeFile.include ?? []).includes(pattern); + } + + async toggleInclude(pattern: string, on: boolean) { + const tf = this.selection.typeFile; + tf.include = (tf.include ?? []).filter((p) => p !== pattern); + if (on) + tf.include.push(pattern); + this.saveSelection(); + await this.rebuild(); + } + + policyOf(key: string): DeviceTypePolicyName | undefined { + return this.selection.typeFile.policy?.[key]; + } + + async setPolicy(key: string, policy: DeviceTypePolicyName | undefined) { + const tf = this.selection.typeFile; + tf.policy = { ...(tf.policy ?? {}) }; + if (policy) + tf.policy[key] = policy; + else + delete tf.policy[key]; + this.saveSelection(); + await this.rebuild(); + } + + async clearSelection() { + this.selection.typeFile = { include: [] }; + this.saveSelection(); + await this.rebuild(); + } + + // A project's DeviceTypeSources.json files (Common's, then the SysType's) and lock: merged into the app's selection + async importProject(files: Array<{ name: string; text: string }>, lockText?: string) { + try { + const merged = mergeDeviceTypeSources(files.map((f) => ({ name: f.name, content: f.text }))); + const sources: SourcesFile["sources"] = {}; + merged.sources.forEach((spec, name) => { sources[name] = devTypeJsonToPlain(spec) as Record; }); + const section = (s: MergedDeviceTypeSources["sections"]["builtin"]): SectionSpec => { + const out: SectionSpec = {}; + if (s.hasInclude) + out.include = [...s.include]; + if (s.exclude.length) + out.exclude = [...s.exclude]; + if (s.policy.size) + out.policy = Object.fromEntries(s.policy); + if (s.fsImage) + out.fsImage = true; + return out; + }; + this.selection = { sources, builtin: section(merged.sections.builtin), typeFile: section(merged.sections.typeFile) }; + if (!this.selection.typeFile.include) + this.selection.typeFile.include = []; + this.lockedCommits = {}; + if (lockText) { + const lock = JSON.parse(lockText) as { sources?: Record }; + for (const [name, entry] of Object.entries(lock.sources ?? {})) { + const spec = sources[name]; + if (spec && entry.commit && spec.git === entry.git && spec.ref === entry.ref) + this.lockedCommits[name] = entry.commit; + } + } + const sysTypeMatch = /systypes\/([^/]+)\/DeviceTypeSources\.json$/.exec(files[files.length - 1]?.name ?? ""); + if (sysTypeMatch && sysTypeMatch[1] !== "Common") + this.sysType = sysTypeMatch[1]; + this.sourceRecords.clear(); + this.sourceErrors.clear(); + this.saveSelection(); + this.notify(`Imported ${files.map((f) => f.name).join(" and ")}${lockText ? " with its lock" : ""}`); + await this.loadSources(); + } catch (err) { + this.notify(`Not imported: ${errText(err)}`, true); + } + } + + // The device's pulled type file into the selection: its _sources if it has them, otherwise its records as a + // local source and its "disable" entries as policies + async usePulledFile() { + const pulled = this.device?.pulled; + if (!pulled) + return; + const parsed = DeviceTypeFileManager.parseTypeFile(pulled.text); + const prov = parsed.sources !== undefined ? devTypeJsonToPlain(parsed.sources) as Record : undefined; + if (prov && prov.sources && prov.typeFile) { + const sources: SourcesFile["sources"] = {}; + for (const [name, s] of Object.entries(prov.sources)) { + if (s.git) { + sources[name] = { git: s.git, ...(s.ref ? { ref: s.ref } : {}), ...(s.path ? { path: s.path } : {}) }; + if (s.commit) + this.lockedCommits[name] = s.commit; + } else if (s.bundled) { + sources[name] = { bundled: true }; + } else { + sources[name] = { path: s.path ?? "" }; + } + } + // Path sources' records are in the file itself + const files: DeviceTypeRecordFile[] = []; + parsed.devTypes.forEach((rec, key) => files.push({ path: `${key}.json`, text: writeDevTypeJson(rec, { indent: 4 }) + "\n" })); + for (const name of Object.keys(sources)) { + if (!sources[name].git && !sources[name].bundled) + this.localSources[name] = { files, asFolder: true }; + } + this.selection = { sources, typeFile: { include: prov.typeFile.include ?? [], exclude: prov.typeFile.exclude, policy: prov.typeFile.policy } }; + this.notify("The selection the device's file was made from (its _sources) is loaded"); + } else { + const files: DeviceTypeRecordFile[] = []; + const policy: Record = {}; + parsed.devTypes.forEach((rec, key) => { + const disable = devTypeJsonGet(rec, "disable"); + if (isDevTypeJsonObject(rec) && rec.size === 1 && disable !== undefined) + policy[key] = disable === "claim" ? "disabledClaim" : "disabled"; + else + files.push({ path: `${key}.json`, text: writeDevTypeJson(rec, { indent: 4 }) + "\n" }); + }); + this.selection = { sources: { ...this.selection.sources, device: { path: "../../devtypes" } }, + typeFile: { include: files.length ? ["device:*"] : [], policy } }; + this.localSources.device = { files, asFolder: true }; + this.notify("The device's file has no _sources: its records are loaded as the local source \"device\""); + } + this.sourceRecords.clear(); + this.saveSelection(); + await this.loadSources(); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Building the type file + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + async rebuild() { + const build: BuildState = { offline: !this.device?.table, unknownCompiled: [], + maxBytes: this.device?.status?.maxFileBytes ?? 65536 }; + try { + const sources = new Map(); + for (const name of Object.keys(this.selection.sources)) { + const loaded = this.sourceRecords.get(name); + if (!loaded) + throw new Error(`source "${name}": ${this.sourceErrors.get(name) ?? "not read yet"}`); + sources.set(name, loaded); + } + build.merged = mergeDeviceTypeSources([{ name: "selection", content: JSON.stringify(this.selection) }]); + build.selection = resolveDeviceTypeSelection(build.merged, "typeFile", sources); + // The records the device has compiled in (from the device), for new / override / unchanged + const compiled = new Map(); + const table = this.device?.table; + if (table && this._manager) { + for (const key of [...build.selection.records.keys(), ...build.selection.policyOnly.keys()]) { + const entry = table.find((t) => t.key === key); + if (!entry) + continue; + if (entry.src === "base") { + if (!this.compiled.has(key)) { + const rec = await this._manager.deviceRecord(key); + if (rec !== undefined) + this.compiled.set(key, rec); + } + const rec = this.compiled.get(key); + if (rec !== undefined) + compiled.set(key, rec); + } else if (entry.src === "override") { + // Overridden by the device's type file: its compiled record can't be read, so the record is kept + compiled.set(key, parseDevTypeJson('{"_compiledRecordUnknown":true}')); + build.unknownCompiled.push(key); + } + } + } else { + // No device: policies are taken as being for compiled types + build.selection.policyOnly.forEach((_, key) => compiled.set(key, parseDevTypeJson("{}"))); + } + build.result = buildTypeFile({ records: build.selection.records, policyOnly: build.selection.policyOnly, + provenance: build.selection.provenance }, compiled, { maxBytes: build.maxBytes }); + } catch (err) { + build.error = errText(err); + } + this.build = build; + this.changed(); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // The device + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + get connector(): RaftConnector | undefined { + return this._connector; + } + + async connect(method: "WebBLE" | "WebSerial" | "WebSocket", locator = "") { + this.connState = "connecting"; + this.connError = undefined; + this.changed(); + try { + const sysType = new SystemTypeDevTypes(); + const connector = new RaftConnector(async (_utils: RaftSystemUtils) => sysType); + connector.setEventListener((type, evt) => { + if (type === "conn" && evt === RaftConnEvent.CONN_DISCONNECTED && this.connState === "connected" && !this.pushState.stage) { + this.connState = "disconnected"; + this.changed(); + } + }); + await connector.initializeChannel(method); + let ok: boolean; + if (method === "WebBLE") { + const dev = await navigator.bluetooth.requestDevice({ filters: sysType.BLEServiceUUIDs.map((uuid) => ({ services: [uuid] })) }); + ok = await connector.connect(dev as unknown as object); + } else { + ok = await connector.connect(locator); + } + if (!ok) + throw new Error("the connection failed"); + this._connector = connector; + this._manager = new DeviceTypeFileManager(connector); + this.connMethod = method; + this.connState = "connected"; + this.compiled.clear(); + this.changed(); + await this.refreshDevice(); + } catch (err) { + this.connState = "disconnected"; + this.connError = errText(err); + this.changed(); + } + } + + async disconnect() { + await this._connector?.disconnect(); + this._connector = undefined; + this._manager = undefined; + this.connState = "disconnected"; + this.device = undefined; + this.compiled.clear(); + await this.rebuild(); + } + + async refreshDevice() { + const conn = this._connector; + const manager = this._manager; + if (!conn || !manager) + return; + const info: DeviceInfo = { devices: [], unidentified: [], pulled: this.device?.pulled }; + try { + const v = await conn.sendRICRESTMsg("v", {}) as { SystemName?: string; SystemVersion?: string }; + info.systemName = v.SystemName; + info.systemVersion = v.SystemVersion; + info.status = await manager.status(true); + if (info.status?.rslt !== "ok") + info.error = "This firmware has no device type file support (devman/typefile)"; + info.table = await manager.table(); + const list = await conn.sendRICRESTMsg("devman/listdevs", {}) as { devices?: DeviceInfo["devices"] }; + info.devices = (list.devices ?? []).map((d) => ({ ...d, key: info.table?.find((t) => t.dtIdx === d.dtIdx)?.key })); + const bus = await conn.sendRICRESTMsg("devman/busstatus", {}) as { buses?: Record }> }; + for (const [busName, b] of Object.entries(bus.buses ?? {})) { + for (const t of b.identTiming ?? []) { + if (t.state !== "noMatch") + continue; + const raw = parseInt(t.addr, 16); + info.unidentified.push({ bus: busName, addr: raw & 0xff, slot: (raw >> 8) & 0x3f, state: t.state }); + } + } + } catch (err) { + info.error = errText(err); + } + this.device = info; + // The catalogue of the device's RaftCore version + const raftCore = info.status?.raftCore; + if (raftCore && this.index) { + const label = RaftCoreCatalogue.chooseVersion(this.index, raftCore); + if (label && label !== this.catalogue?.label) { + await this.openCatalogue(label); + return; + } + } + await this.rebuild(); + } + + async pull() { + if (!this._manager || !this.device) + return; + const pulled = await this._manager.pull(); + if (!pulled) { + this.notify("The device has no type file to read", true); + return; + } + try { + const parsed = DeviceTypeFileManager.parseTypeFile(pulled.text); + this.device.pulled = { ...pulled, keys: Array.from(parsed.devTypes.keys()), + sources: parsed.sources !== undefined ? devTypeJsonToPlain(parsed.sources) : undefined }; + this.changed(); + } catch (err) { + this.notify(`The device's type file isn't valid: ${errText(err)}`, true); + } + } + + async push(dryRun: boolean) { + const text = this.build.result?.text; + if (!this._manager || !text) + return; + this.pushState = { stage: "status" }; + this.changed(); + const result = await this._manager.push(text, { + expectedDeviceCrc: this.device?.pulled?.crc, + dryRun, + onProgress: (stage, detail) => { + this.pushState = { ...this.pushState, stage, detail }; + this.changed(); + }, + confirm: (check) => new Promise((resolve) => { + this._confirmResolve = resolve; + this.pushState = { ...this.pushState, check, awaitingConfirm: true }; + this.changed(); + }), + }); + this.pushState = { check: result.check ?? this.pushState.check, result }; + this.changed(); + if (result.outcome === "applied") { + this.device = { ...(this.device as DeviceInfo), pulled: undefined }; + await this.refreshDevice(); + } + } + + confirmPush(ok: boolean) { + this.pushState = { ...this.pushState, awaitingConfirm: false }; + this._confirmResolve?.(ok); + this._confirmResolve = undefined; + this.changed(); + } + + async reidentify() { + await this._connector?.sendRICRESTMsg("devman/reident?unidentified=1", {}); + setTimeout(() => void this.refreshDevice(), 1500); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Export + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + sourcesFileText(asBuiltin = false): string { + const out: Record = { sources: this.selection.sources }; + if (asBuiltin) { + out.builtin = { ...this.selection.typeFile, fsImage: undefined }; + } else { + if (this.selection.builtin && Object.keys(this.selection.builtin).length) + out.builtin = this.selection.builtin; + out.typeFile = this.selection.typeFile; + } + return JSON.stringify(out, null, 4) + "\n"; + } + + async lockText(): Promise { + const { merged, selection } = this.build; + if (!merged || !selection) + return undefined; + const hasGit = Object.keys(this.selection.sources).some((n) => this.sourceKind(n) === "git"); + return hasGit ? deviceTypeLockText(merged, selection, this.sourceRecords) : undefined; + } + + async manifestText(): Promise { + const records: Record = {}; + for (const [key, source] of this.build.selection?.origin ?? []) { + const loaded = this.sourceRecords.get(source) as DeviceTypeSourceRecordsWithFiles | undefined; + const file = loaded?.files?.get(key); + records[key] = { source, ...(file ? { file: file.path, sha256: await sha256Hex(file.text) } : {}), + ...(loaded?.commit ? { commit: loaded.commit } : {}), ...(loaded?.raftCore ? { raftCore: loaded.raftCore } : {}) }; + } + return JSON.stringify({ formatVersion: 1, section: "typeFile", records }, null, 1) + "\n"; + } + + // Everything, laid out as in a project + async exportZip(asBuiltin: boolean): Promise { + const sysDir = `systypes/${this.sysType}`; + const files: Array<{ path: string; text: string }> = [{ path: `${sysDir}/DeviceTypeSources.json`, text: this.sourcesFileText(asBuiltin) }]; + const lock = await this.lockText(); + if (lock) + files.push({ path: `${sysDir}/DeviceTypeSources.lock`, text: lock }); + for (const [name, spec] of Object.entries(this.selection.sources)) { + const local = this.localSources[name]; + if (this.sourceKind(name) !== "path" || !local) + continue; + const dir = joinPath(sysDir, String(spec.path ?? name)); + for (const file of local.files) { + if (local.asFolder) + files.push({ path: `${dir}/${file.path.slice(file.path.lastIndexOf("/") + 1)}`, text: file.text }); + else + files.push({ path: dir.endsWith(".json") ? dir : `${dir}/${file.path.slice(file.path.lastIndexOf("/") + 1)}`, text: file.text }); + } + } + if (this.build.result) + files.push({ path: "devlib.json", text: this.build.result.text }); + files.push({ path: "DeviceTypeSources.manifest.json", text: await this.manifestText() }); + return makeZip(files); + } +} + +export function errText(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} diff --git a/apps/devtypes/src/styles.css b/apps/devtypes/src/styles.css new file mode 100644 index 0000000..d2b63e0 --- /dev/null +++ b/apps/devtypes/src/styles.css @@ -0,0 +1,89 @@ +:root { + --bg: #ffffff; + --surface: #f6f8fa; + --fg: #1f2328; + --muted: #59636e; + --rule: #d1d9e0; + --accent: #0969da; + --accent-fg: #ffffff; + --ok: #1a7f37; + --warn: #9a6700; + --bad: #cf222e; + --tag: #ddf4ff; + --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} +@media (prefers-color-scheme: dark) { + :root { + --bg: #0d1117; + --surface: #161b22; + --fg: #e6edf3; + --muted: #9198a1; + --rule: #3d444d; + --accent: #4493f8; + --accent-fg: #0d1117; + --ok: #3fb950; + --warn: #d29922; + --bad: #f85149; + --tag: #0c2d6b; + } +} + +* { box-sizing: border-box; } +body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; } +a { color: var(--accent); } +code, pre, .mono { font-family: var(--mono); font-size: 0.88em; } +pre { background: var(--surface); border: 1px solid var(--rule); border-radius: 6px; padding: 10px; overflow: auto; max-height: 420px; } + +header { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; justify-content: space-between; + padding: 10px 16px; border-bottom: 1px solid var(--rule); } +header h1 { font-size: 18px; margin: 0; } +header .conn { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } +nav { display: flex; gap: 2px; padding: 0 16px; border-bottom: 1px solid var(--rule); overflow-x: auto; } +nav button { background: none; border: none; border-bottom: 2px solid transparent; color: var(--muted); padding: 10px 12px; + font: inherit; cursor: pointer; white-space: nowrap; } +nav button.active { color: var(--fg); border-bottom-color: var(--accent); } +main { padding: 16px; max-width: 1100px; margin: 0 auto; } + +button.btn { background: var(--surface); color: var(--fg); border: 1px solid var(--rule); border-radius: 6px; padding: 5px 12px; + font: inherit; cursor: pointer; } +button.btn.primary { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); } +button.btn:disabled { opacity: 0.5; cursor: default; } +button.link { background: none; border: none; color: var(--accent); padding: 0; font: inherit; cursor: pointer; } +input[type=text], input[type=search], select { background: var(--bg); color: var(--fg); border: 1px solid var(--rule); border-radius: 6px; + padding: 5px 8px; font: inherit; } +input[type=search] { width: 100%; max-width: 360px; } + +.row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 8px 0; } +.muted { color: var(--muted); } +.ok { color: var(--ok); } +.warn { color: var(--warn); } +.bad { color: var(--bad); } +.notice { border: 1px solid var(--rule); border-left: 4px solid var(--accent); border-radius: 6px; padding: 8px 12px; margin: 10px 0; + background: var(--surface); } +.notice.error { border-left-color: var(--bad); } +.notice.warn { border-left-color: var(--warn); } +.card { border: 1px solid var(--rule); border-radius: 8px; padding: 12px 14px; margin: 12px 0; } +.card h3 { margin: 0 0 8px; font-size: 16px; } +.tag { display: inline-block; background: var(--tag); border-radius: 10px; padding: 0 8px; font-size: 12px; margin: 0 4px 0 0; } +.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 12px; } + +table { border-collapse: collapse; width: 100%; } +.table-wrap { overflow-x: auto; } +th, td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--rule); vertical-align: top; } +th { color: var(--muted); font-weight: 600; font-size: 13px; } +tr.selected td { background: var(--surface); } +td.num { text-align: right; font-variant-numeric: tabular-nums; } + +.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 16px; } +@media (max-width: 800px) { .split { grid-template-columns: minmax(0, 1fr); } } +.split > div > .card:first-child { margin-top: 0; } +.list { max-height: 70vh; overflow: auto; border: 1px solid var(--rule); border-radius: 8px; } +.list .item { padding: 7px 10px; border-bottom: 1px solid var(--rule); cursor: pointer; } +.list .item:last-child { border-bottom: none; } +.list .item.active { background: var(--surface); } +.list .item .title { font-weight: 600; } +dl.kv { display: grid; grid-template-columns: max-content 1fr; gap: 3px 12px; margin: 6px 0; } +dl.kv dt { color: var(--muted); } +dl.kv dd { margin: 0; } +.modal { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; padding: 16px; } +.modal .card { background: var(--bg); max-width: 720px; width: 100%; max-height: 90vh; overflow: auto; } diff --git a/apps/devtypes/src/util.ts b/apps/devtypes/src/util.ts new file mode 100644 index 0000000..612987c --- /dev/null +++ b/apps/devtypes/src/util.ts @@ -0,0 +1,140 @@ +// Small helpers: a zip file (stored, not compressed - the files are small JSON), I2C address ranges, downloads + +const CRC32_TABLE = (() => { + const table = new Uint32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + table[n] = c >>> 0; + } + return table; +})(); + +function crc32(data: Uint8Array): number { + let crc = 0xffffffff; + for (let i = 0; i < data.length; i++) + crc = CRC32_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8); + return (crc ^ 0xffffffff) >>> 0; +} + +// A zip of text files (paths with "/" for folders) +export function makeZip(files: Array<{ path: string; text: string }>): Uint8Array { + const encoder = new TextEncoder(); + const chunks: Uint8Array[] = []; + const central: Uint8Array[] = []; + let offset = 0; + const now = new Date(); + const dosTime = (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1); + const dosDate = ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate(); + for (const file of files) { + const name = encoder.encode(file.path); + const data = encoder.encode(file.text); + const crc = crc32(data); + const local = new DataView(new ArrayBuffer(30)); + local.setUint32(0, 0x04034b50, true); + local.setUint16(4, 20, true); + local.setUint16(6, 0x0800, true); // UTF-8 names + local.setUint16(8, 0, true); // stored + local.setUint16(10, dosTime, true); + local.setUint16(12, dosDate, true); + local.setUint32(14, crc, true); + local.setUint32(18, data.length, true); + local.setUint32(22, data.length, true); + local.setUint16(26, name.length, true); + local.setUint16(28, 0, true); + chunks.push(new Uint8Array(local.buffer), name, data); + const entry = new DataView(new ArrayBuffer(46)); + entry.setUint32(0, 0x02014b50, true); + entry.setUint16(4, 20, true); + entry.setUint16(6, 20, true); + entry.setUint16(8, 0x0800, true); + entry.setUint16(10, 0, true); + entry.setUint16(12, dosTime, true); + entry.setUint16(14, dosDate, true); + entry.setUint32(16, crc, true); + entry.setUint32(20, data.length, true); + entry.setUint32(24, data.length, true); + entry.setUint16(28, name.length, true); + entry.setUint32(42, offset, true); + central.push(new Uint8Array(entry.buffer), name); + offset += 30 + name.length + data.length; + } + const centralSize = central.reduce((n, c) => n + c.length, 0); + const end = new DataView(new ArrayBuffer(22)); + end.setUint32(0, 0x06054b50, true); + end.setUint16(8, files.length, true); + end.setUint16(10, files.length, true); + end.setUint32(12, centralSize, true); + end.setUint32(16, offset, true); + const all = [...chunks, ...central, new Uint8Array(end.buffer)]; + const out = new Uint8Array(all.reduce((n, c) => n + c.length, 0)); + let pos = 0; + for (const c of all) { + out.set(c, pos); + pos += c.length; + } + return out; +} + +// The I2C addresses of a record's "addresses" ("0x18-0x1f,0x53") +export function parseAddresses(spec: string | undefined): number[] { + const out: number[] = []; + for (const part of (spec ?? "").split(",")) { + const m = /^\s*0x([0-9a-f]+)\s*(?:-\s*0x([0-9a-f]+))?\s*$/i.exec(part); + if (!m) + continue; + const from = parseInt(m[1], 16); + const to = m[2] ? parseInt(m[2], 16) : from; + for (let a = from; a <= to && a - from < 256; a++) + out.push(a); + } + return out; +} + +export function hex2(n: number): string { + return "0x" + n.toString(16).padStart(2, "0"); +} + +export function download(name: string, data: string | Uint8Array, type = "application/json") { + const blob = new Blob([data as BlobPart], { type }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = name; + document.body.appendChild(a); + a.click(); + a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); +} + +// "systypes/Main" + "../../devtypes" -> "devtypes" +export function joinPath(base: string, rel: string): string { + const parts = rel.startsWith("/") ? [] : base.split("/").filter(Boolean); + for (const p of rel.split("/")) { + if (p === "" || p === ".") + continue; + if (p === "..") + parts.pop(); + else + parts.push(p); + } + return parts.join("/"); +} + +export function readStore(key: string, fallback: T): T { + try { + const text = localStorage.getItem(key); + return text ? (JSON.parse(text) as T) : fallback; + } catch { + return fallback; + } +} + +export function writeStore(key: string, value: unknown) { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch { + // storage unavailable (private window) - the app works without it + } +} diff --git a/apps/devtypes/tsconfig.json b/apps/devtypes/tsconfig.json new file mode 100644 index 0000000..8dcdd66 --- /dev/null +++ b/apps/devtypes/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "jsx": "react", + "esModuleInterop": true, + "resolveJsonModule": true, + "module": "esnext", + "target": "es2017", + "strict": true, + "noUnusedLocals": true, + "moduleResolution": "bundler", + "moduleSuffixes": [".web", ""], + "skipLibCheck": true, + "lib": ["ES2017", "DOM", "DOM.Iterable"] + }, + "include": ["src"] +} diff --git a/devdocs/device-type-management-web-plan.md b/devdocs/device-type-management-web-plan.md new file mode 100644 index 0000000..61a3c86 --- /dev/null +++ b/devdocs/device-type-management-web-plan.md @@ -0,0 +1,523 @@ +# Device Type Management from raftjs and a Web UI: Plan + +## Status + +Decisions W1-W9 made 2026-10-24 (§9). **Phases 1-6 implemented 2026-10-24** (§11), except the on-hardware checks +(no board was attached): RaftCore firmware fields, `_sources`, `--pages` and the Pages workflow; raftjs's type file, +grammar, selection, catalogue and device workflow modules; the Device Types app (`apps/devtypes`). Not committed. + +Companion documents: + +- RaftCore `devdocs/device-type-record-management-plan.md` - one file per record, `DeviceTypeSources.json`, git + sources, runtime type files, the raft CLI (phases 1-6, done). §3.7 there says a graphical tool is for later and + needs only raftjs plus upload, check and apply wrappers. This document is that tool. +- RaftCore `devdocs/runtime-decoding.md` - on-device decoders from a record's attributes and the code interpreter, + which decide whether a record added at runtime is decoded on the device. +- Wiki: *DeviceManager Settings* (device type file, `typeTable`, `typePolicy`, `typeSettings`), *Selecting Device + Types* (`DeviceTypeSources.json`, runtime type files, `raft devtypes`), *DeviceManager REST API* (`devman/typefile`, + `typefile/check`, `typefile/apply`, `typepolicy`, `types`, `typeinfo full=1`), *Device Type Record Format*. + +--- + +## 1. Aims + +1. **An end user, not a firmware developer, can add a device type to a device** - a sensor the firmware wasn't built + with - from a browser, without a toolchain, a project checkout or the raft CLI. +2. **A browsable catalogue** of RaftCore's records and third parties', searchable by name, manufacturer, class and + I2C address, for anyone, with or without a device. +3. **Records from other sources**, described as `DeviceTypeSources.json` describes them (`bundled`, `git`, `path`), so + a project's selection can be loaded into the app, and what the app builds can be downloaded as a + `DeviceTypeSources.json` with its lock (§6a). +4. **The raftjs library does the work**, so the web UI, the dashboard example, apps that embed raftjs (React Native + included) and scripts all share one implementation of building, checking, pushing and pulling type files. +5. **The device's rules decide.** The browser gives early warnings, but what is accepted is what + `devman/typefile/check` on the device says - the same principle as the CLI (M7 of the RaftCore plan). +6. **A bridge back to the build**: what a user chose in the UI can be exported as a `typeFile` (or `builtin`) + selection for `DeviceTypeSources.json`, so a working setup becomes part of a project. + +Non-aims (for now): editing a project's files from the browser; private git sources; compiling firmware; anything +that changes the record format or the firmware's checks. Later, on the same site: an ESP32 flasher that knows the +layout of a Raft app (§8, phase 8). + +--- + +## 2. What exists (2026-10-24) + +| Piece | Where | State | +|-|-|-| +| Records, one file each | RaftCore `devtypes/records/*.json` (59 records) | Done | +| Catalogue file of every record | RaftCore release asset `DeviceTypeRecords.json` (`release-assets.yml`) | Done. Release assets aren't served with CORS headers, so a browser page can't fetch it | +| Type file builder | RaftCore `scripts/AssembleDeviceTypes.py` `build_type_file()` (new / override / unchanged / policy, `noDecoder`, `initCode`, minify, 64 KB) | Python only | +| Command grammar check | RaftCore `DeviceTypeGrammar.cpp` and `scripts/DeviceTypeGrammar.py`, shared vectors `scripts/tests/grammar_vectors.json` | C++ and Python | +| Decode code check | `PseudocodeHandler.check_code` (Python) | Python; raftjs has `PseudocodeTranspiler.ts` (tokenizer and transpiler) | +| Device endpoints | `devman/typefile`, `typefile/check`, `typefile/apply`, `typepolicy ... save=1`, `types` (paged table with `src` base / override / extended and policy), `typeinfo ... full=1` (a record in type file form), `reident`, `caps` | Done | +| raftjs wrappers | `DeviceManager.getTypeFileStatus`, `checkTypeFile`, `applyTypeFile`, `setTypePolicy`, `reIdentify`; types `DevManTypeFileStatus`, `DevManTypeFileCheck`, `DevManTypeFileEntry` | Done | +| raftjs file transfer | `RaftConnector.sendFile`, `fsGetContents` (RICREST, over BLE, Web Serial and WebSocket) | Done | +| Dashboard panel | `examples/dashboard/src/TypeFilePanel.tsx`: choose a file, upload as `devlib.new.json`, show the device's check per entry, apply | Done, but the workflow is written in the panel, not the library; no reconnect or verify after the restart | +| raft CLI | `raft devtypes push / pull / status` (HTTP), the logic in `DeviceTypesTool.py` | Done | + +So the device side is complete for this work. What is missing is (a) the type file logic in TypeScript, (b) a +catalogue a browser can fetch, (c) a few facts the device doesn't yet report, and (d) the UI. + +--- + +## 3. Shape of the solution + +``` + RaftCore repo raftjs (npm) Browser + ───────────── ──────────── ─────── + devtypes/records/*.json ──CI──► Pages: catalogue index RaftDeviceTypes module + + records per release ─fetch─► catalogue, type file ◄── Device Types app + builder, early checks (static page, + device workflow ──────► GitHub Pages) + │ │ + └── RaftConnector ── BLE / Web Serial / WS ── device + devman/typefile, typefile/check, apply, fs +``` + +Three deliverables: + +1. **A `RaftDeviceTypes` module in raftjs** (§5): no DOM, no React, usable from Node and React Native. +2. **A catalogue published to GitHub Pages by RaftCore's CI** (§6). +3. **A Device Types web app** (§7), a static page that uses both. Hosted on GitHub Pages. + +Plus small, additive firmware changes (§4). + +--- + +## 4. Firmware additions (RaftCore, additive) + +The UI needs to know which rules the device applies before it builds a file for it. Today it can learn the limits +(`devman/typefile`) and the table (`devman/types`), but not: + +| Missing | Why it matters | Proposal | +|-|-|-| +| RaftCore version | Records follow the format of a RaftCore version (e.g. `hwTypeId` replaced `deviceTypeId`, which is now rejected - M14). The UI should offer the catalogue of the device's RaftCore version, or warn when using a newer one. `v` gives the app's `SystemVersion`, not RaftCore's | `raftCore` in `devman/typefile` status (and in `v` if cheap) | +| Runtime decoding built in | Whether a new record will be decoded on the device (`RAFT_RUNTIME_DECODE`) and whether custom decode code and `initCode` run (`RAFT_RUNTIME_DECODE_CODE`). The status has `runtimeDecoders` (the setting) but not what the build has | `decode: {"attrs": true, "code": false}` in the status | +| Record format version | Lets the UI refuse records newer than the device understands without comparing RaftCore versions | `recordFormat: 1`, matching `formatVersion` in a source's `raftdevtypes.json` manifest | +| Protected types | A type a system device expects (`expectType`) can't be overridden or disabled; the UI should grey it out rather than let the check reject it (W6) | `protected: true` on its entry in `devman/types` | + +The check endpoint already reports per entry what the device will do, so these only let the UI warn earlier and pick +the right catalogue. raftjs treats them all as optional (older firmware: assume the catalogue of the newest release +and rely on the check). + +Not proposed now (W7, maybe later): an endpoint to add or remove one record. The client pulls the device's file, changes it and pushes the +whole file, with the file's CRC to detect a change made meanwhile (as `raft devtypes push` does). This keeps one +route onto the device, through the staged check and apply, and needs no new firmware. + +Possibly later: `devman/types?full=1` (records with the table page), if fetching compiled records one `typeinfo` at a +time proves slow over BLE (§5.3). + +--- + +## 5. raftjs: the `RaftDeviceTypes` module + +New files in `src/`, exported from `main.ts`. Pure logic is separated from device I/O so it can be unit tested with the +same fixtures as the Python. + +### 5.1 Catalogue model and sources + +```ts +interface DeviceTypeRecord { [field: string]: unknown } // a record as in devtypes/records/.json + +interface DeviceTypeCatalogueEntry { + key: string; + source: string; // "raftcore", "local", or a git source's name + version?: string; // RaftCore tag, or the git commit + sha256: string; // of the record file, as the manifest records it + summary: { // for search without loading every record + name?: string; manu?: string; clas?: string[]; addresses?: string; hwTypeId?: string; + hasData: boolean; customCode: boolean; initCode: boolean; + }; +} + +interface DeviceTypeSource { + name: string; + list(): Promise; + get(key: string): Promise; +} +``` + +Source implementations: + +- `RaftCorePagesSource(baseUrl, version)` - the published catalogue (§6). Default for the app; it stands for a + `{"bundled": true}` source. +- `GitSource(url, ref, path)` - a `git` source of `DeviceTypeSources.json`: a public repo with a folder of record files + or one record file (default path `records`), optional `raftdevtypes.json` manifest (`formatVersion`, + `minRaftCore`, checked as the assembler checks them). A browser can't run git, so it reads through the host's HTTP + API: for GitHub, `ref` is resolved to a commit with the API, the tree is listed once, and files are read from + `raw.githubusercontent.com` at that commit (both serve CORS headers). Unauthenticated GitHub API requests are + limited to 60 per hour per IP, so a commit is read once and cached in the browser (records at a commit never + change). Other hosts (GitLab, Gitea) are further adapters behind the same interface; a host without CORS can't be + read from a page. +- `LocalRecordsSource(name, files)` - a `path` source: records the user drops or picks (a record file, a folder of + them, a catalogue file, or a type file pulled from a device). + +`fetch` is injected, so Node tests and React Native work. How sources are combined is §6a. + +### 5.2 Type file builder (port of `build_type_file`) + +```ts +interface TypeFileSelection { + records: Record; // key -> record to put in the file + policy: Record; // compiled types to disable +} +interface TypeFileBuildEntry { + key: string; kind: "new" | "override" | "unchanged" | "policy"; + noDecoder?: boolean; decoder?: "runtime" | "none"; initCode?: "runtime" | "runtime or compiled"; + problems?: string[]; // early checks (§5.4) +} +function buildTypeFile(sel: TypeFileSelection, compiled: Record, + opts?: { maxBytes?: number }): { text: string; bytes: number; crc: string; entries: TypeFileBuildEntry[] }; +``` + +Same rules as the Python: `_notes` removed, unchanged records left out, `noDecoder` from the compact `resp` JSON, +keys sorted by UTF-8 bytes, minified, policies as `{"disable": true | "claim"}`, a type file can't enable a compiled +type, the `initCode` / init value slot rules. `crc` is CRC16-CCITT as `filehash` and `devman/typefile` report it, so +the UI can tell whether the device has the file. + +**Keeping the two in step:** the Python tests write golden outputs (input selection, compiled records, expected text +and entries) to `scripts/tests/typefile_vectors.json` in RaftCore; raftjs tests run the same vectors (copied into +`testdata/`, or read from a sibling RaftCore checkout in CI). The grammar already works this way with +`grammar_vectors.json`. + +### 5.3 What the device has compiled + +`buildTypeFile` needs the compiled records to classify new / override / unchanged. The CLI reads them from the build; +a browser has only the device: + +- `devman/types` (paged) gives every key with `src` and policy: enough to tell new from override, and to list what + can be disabled. +- `devman/typeinfo?deviceTypeKey=&full=1` gives a compiled record, fetched only for keys the user selects that the + device also has (to tell override from unchanged, and whether the response layout changed). Cached per connection, + keyed by the table's `generation`. + +The device's record may not be byte-identical to the source file (field order, whitespace in strings), so the +comparison is structural (sorted keys, as `_comparable`), and a doubtful "unchanged" errs towards including the +record. The device's check is authoritative in any case. + +### 5.4 Early checks in the browser + +Warnings before upload, so a user isn't surprised by the device's check: + +1. **Grammar** of `detectionValues`, `initValues`, `pollInfo.c`: port `DeviceTypeGrammar.py` (about 470 lines) to + `DeviceTypeGrammar.ts`, tested against `grammar_vectors.json`. +2. **Response size**: `devInfoJson.resp.b` equals what the poll reads produce (from the grammar's steps). +3. **Decode and init code** in the decode language: reuse `PseudocodeTranspiler.ts`'s tokenizer for the rules of + `PseudocodeHandler.check_code` (no calls, only declared names). +4. **Limits** from `devman/typefile`: `maxFileBytes`, extended records and bytes free, `typeFileLimits`. +5. **Decoding**: with the device's `decode` flags (§4), a clear statement per record - "decoded on the device", + "decoded in this app only (DataLogger, RaftROS and named values won't see it)". + +Option for later (W5): compile the C++ loader and grammar to WebAssembly so the browser uses exactly the firmware's +rules offline. Not needed while the device check exists. + +### 5.5 Device workflow + +Move the dashboard panel's steps into the library, and add what the CLI's `push` does: + +```ts +class DeviceTypeFileManager { + constructor(connector: RaftConnector); + status(entries?: boolean): Promise; + table(): Promise; // devman/types, all pages + compiledRecord(key: string): Promise; + pull(): Promise<{ text: string; crc: string } | undefined>; // fsGetContents of the type file + stage(text: string, onProgress?): Promise; // upload devlib.new.json, check + apply(opts?: { waitForRestart?: boolean; timeoutMs?: number }): Promise; + push(text: string, opts?: { expectedDeviceCrc?: string; dryRun?: boolean; confirm?: (check) => Promise }): Promise; +} +``` + +`push` = refuse if the device's current CRC isn't `expectedDeviceCrc` (the file changed since it was pulled - e.g. +`typepolicy save=1`), unless forced; `stage`; refuse if not `valid`; `confirm`; `apply`; wait for the connection to +drop and come back (the connector's reconnect for BLE and WebSocket; Web Serial survives a restart of most boards); +then `status`: `state` applied, same CRC, the same outcome per entry as the check. A dry run removes the staged file. + +`TypeFilePanel.tsx` is then rewritten on top of this class. + +--- + +## 6. The published catalogue (RaftCore CI) + +A GitHub Pages workflow in RaftCore, on a release and on a push to `main`: + +``` +https://robdobsn.github.io/RaftCore/devtypes/ + index.json {"formatVersion":1, "versions":["main","v1.62.0", ...], "latest":"v1.62.0"} + v1.62.0/catalogue.json entries (§5.1): key, sha256, summary + v1.62.0/records/.json each record, as in the repo + v1.62.0/DeviceTypeRecords.json all records in one file (as the release asset) + main/... the same for the current main +``` + +- Built by `AssembleDeviceTypes.py` (a new `--pages ` option, so the summary fields and SHA-256 are computed by + the same code as the manifest). +- Each release adds a version folder; earlier ones are kept (the workflow restores the previous site from the + `gh-pages` branch, or rebuilds every tag - W3). +- Records are small (49 records were 36 KB minified; the 59 now are still well under 64 KB), so the whole history + stays small. + +Same origin note: if the app is also on `robdobsn.github.io` (§7), the catalogue is same-origin and needs no CORS at +all; for apps elsewhere, GitHub Pages serves `Access-Control-Allow-Origin: *`. + +--- + +## 6a. Sources in the app, and the `DeviceTypeSources.json` it generates + +The app's working state *is* a `DeviceTypeSources.json`: named `sources` and a `typeFile` section (`include`, +`exclude`, `policy`). Everything the user does - adding a source, picking records, disabling a compiled type - edits +that, and the type file is built from it. So the app and the build speak one format, and a selection moves between +them without translation. + +**Resolving a selection in the browser** (`resolveSelection`, a port of `assemble()`'s selection rules, without the +git fetching): `"*"`, `":*"`, `":"` and bare keys; `exclude`; a `path` source replaces the same +key from another source; the same key from two other sources is an error unless `include` names one (M6); `policy` +for compiled types. Covered by vectors from the Python tests, as the builder is (§5.2). + +**Adding sources:** + +| Source kind | In the app | +|-|-| +| `{"bundled": true}` | The published catalogue (§6), at the device's RaftCore version where it reports one (§4), else a version the user picks | +| `{"git": "", "ref": "", "path": "..."}` | Paste a repo URL and a tag, branch or commit; read as in §5.1. The app shows the commit the ref resolved to, and offers newer commits on a branch (as `raft devtypes update`) | +| `{"path": "..."}` | Pick or drop record files or a folder. The records are kept in the browser, under the source's name | + +**Importing a project's selection:** load a project's `DeviceTypeSources.json` (Common's and the SysType's, merged as +the build merges them) and its `DeviceTypeSources.lock`. Git sources are read at the locked commits, so the app sees +the records the project's build sees; `path` sources can't be read from a URL, so the app asks for their folder (with +`webkitdirectory` or the File System Access API). The `typeFile` section then gives the same type file as `raft +devtypes typefile` (same CRC), which phase 5 verifies. + +**Downloads (the record of what was generated):** + +- `DeviceTypeSources.json` - the sources and `typeFile` selection, ready to put in `systypes//`. With a + choice of `typeFile` (records loaded at runtime, as now) or `builtin` (compile them in instead). +- `DeviceTypeSources.lock` - in the build's format (`formatVersion`, the commit each git source resolved to, the + SHA-256 of each record used), so a build reproduces exactly what was pushed. +- The records of `path` sources, as files under the source's folder name, since only the browser has them. +- `devlib.json` itself, and a manifest per record (source, commit, SHA-256) like + `DeviceTypeSources.manifest.json`. + +These go in one zip, laid out as in a project (`systypes//DeviceTypeSources.json`, `.lock`, +`devtypes/.json`), so it can be unpacked into a project. + +**Provenance in the type file (W9, proposed):** the device type file loader ignores top-level keys other than +`devTypes`, so the generated file can carry a small `_sources` entry - the selection, commits and record hashes. Then +pulling a file from a device tells the app (and `raft devtypes pull`) how it was made, and a device in the field can +be reproduced without the user having kept the download. Costs a few hundred bytes of the 64 KB. If adopted, both +builders (Python and TypeScript) write it, so the CRCs still agree. + +--- + +## 7. The Device Types web app + +### 7.1 Where it lives and how it is hosted + +- **Source:** in raftjs, `apps/devtypes/` (React and Parcel, like `examples/dashboard`, sharing its connection code + where sensible). It is a product, not an example, hence `apps/`. +- **Hosting (W1, decided):** on RaftCore's GitHub Pages site, next to the catalogue and with the rest of Raft's + documentation: RaftCore's Pages workflow checks out raftjs at a pinned tag, builds the app and deploys it with the + catalogue, at `https://robdobsn.github.io/RaftCore/devtypes/app/`. The app version is pinned with the catalogue. +- **Later on the same site:** an ESP32 flasher that knows a Raft app's layout (§8, phase 8). + +### 7.2 Transports from a page on HTTPS + +GitHub Pages serves over HTTPS, which decides how the app can reach a device: + +| Transport | From the hosted page | Notes | +|-|-|-| +| Web Bluetooth | Yes | Chrome, Edge, Opera (desktop and Android). Not Firefox or iOS Safari | +| Web Serial | Yes | Chrome, Edge, Opera desktop. USB cable | +| WebSocket / HTTP to the device | **No** | `ws://` and `http://` from an `https://` page are blocked as mixed content (and by Private Network Access); the device has no TLS | + +So the hosted app connects by BLE and serial. WiFi is later (W4); the options: + +- **A second host serving the app over plain `http`** (W4's preferred direction). The blocker is mixed content, not + CORS: an `http` page may open `ws://` to the device, and fetches the catalogue from Pages (CORS allowed). But Web + Bluetooth and Web Serial need a secure context (`https` or `localhost`), so that copy of the app is WiFi only - the + same build, with the transports offered chosen by `window.isSecureContext`. +- **The app run locally** (`npx` or a downloaded build on `http://localhost`): `localhost` is a secure context, so all + three transports work. Probably the simplest route for WiFi users. +- **Served by the device** (RaftWebServer WebUI): WebSocket works, but the build must be small, and with the device + as an access point there is no internet for the catalogue. +- The raft CLI remains the route for HTTP pushes from a terminal. + +### 7.3 Screens + +1. **Catalogue** (no device needed): search and filter (name, manufacturer, class, address, "has data", source); + record view - identity and addresses, attributes with units and ranges, actions, poll interval, the raw JSON, and + a link to the record's file on GitHub. Version picker (from `index.json`). +2. **Device**: connect (BLE / serial); firmware and RaftCore version; type file status (state, CRC, counts, first + error, `restartRequired`); the device's type table (key, source base / override / extended, policy); the devices + present and how each was identified (`devman/listdevs`, `typeinfo`), with an "unidentified at 0x.." list that + links to catalogue records with that address - the commonest reason to use the app. +3. **Sources**: the sources in use (RaftCore's catalogue at a version, git repos at a ref and the commit it resolved + to, local folders); add one, update a git ref, or import a project's `DeviceTypeSources.json` and lock (§6a). +4. **Compose**: start from the device's current type file (pulled; its `_sources`, if present, restores the + selection - W9), from an imported selection, or empty; add records from any source; disable compiled types; per entry new / override / unchanged / policy, decoder, + early check problems; total size against the device's limits. Changes are kept in the browser (`localStorage`) + until pushed or discarded. +5. **Push**: the device's check, entry by entry, beside the app's own classification (differences highlighted); + confirm; apply; progress through the restart and reconnect; verification. Then back to the Device screen, where + the new devices appear. +6. **Export**: the downloads of §6a - `DeviceTypeSources.json` (as `typeFile` or `builtin`), its lock, local records + and `devlib.json`, as one zip laid out as a project, or each on its own - so a developer can put the setup into a + project and build it in with `fsImage` or compile it in. Offered after every push, as the record of what the + device was given. + +### 7.4 Security and trust + +- The app is static; nothing leaves the browser except to the device and to GitHub for reads. +- Records from third-party sources are shown with their source and commit, and records with decode or init code are + marked: that code runs in the device's interpreter (bounded - a loop that doesn't end is stopped), and the decode + language check (§5.4) is applied as for git sources in a build. +- A type file can't override or disable a protected type (`expectType`); the device enforces it, and the app greys + such types out (from `protected` in `devman/types`, W6). + +--- + +## 8. Phases + +| Phase | Work | Repos | Verify | +|-|-|-|-| +| **1** | Firmware facts (§4): `raftCore`, `decode`, `recordFormat` in `devman/typefile` status, `protected` in `devman/types`; wiki | RaftCore, wiki | Host test of the fields; FeatherS3 and Axiom report them, the Axiom's fuel gauge type as protected | +| **2** | raftjs `RaftDeviceTypes`: models, `LocalRecordsSource`, `buildTypeFile`, CRC, `DeviceTypeFileManager` (status, table, compiledRecord, pull, stage, apply, push with restart and verify); `typefile_vectors.json` from the Python tests; `TypeFilePanel` moved onto it | raftjs, RaftCore (vectors) | Jest on the shared vectors (the TS build gives the Python's text and entries for every vector); dashboard on a FeatherS3 over BLE and serial: push, restart, verify; a file changed on the device is detected | +| **3** | Early checks: `DeviceTypeGrammar.ts` on `grammar_vectors.json`; resp size; decode code check | raftjs | Jest; every RaftCore record passes; the known bad vectors fail as the C++ does | +| **4** | Catalogue on Pages: `AssembleDeviceTypes.py --pages`; Pages workflow (release and `main`); `RaftCorePagesSource` | RaftCore, raftjs | Host test of `--pages`; the site serves `index.json` and a version; raftjs fetch test against a local copy | +| **5** | Other sources and the generated selection (§6a): `resolveSelection` (on vectors from the Python), `GitSource` for GitHub, `LocalRecordsSource` for folders, import of a project's `DeviceTypeSources.json` and lock, the downloads (zip); W9 `_sources` in both builders if adopted | raftjs, RaftCore (vectors; `_sources` in `AssembleDeviceTypes.py`) | Jest; a test repo on GitHub with a manifest and records, at a tag and a branch; Axiom018's selection imported in the browser gives the same type file (same CRC) as `raft devtypes typefile` | +| **6** | The app, hosted: Catalogue, Device, Sources, Compose, Push, Export over BLE and serial | raftjs (`apps/devtypes`), RaftCore (Pages workflow) | On hardware: add a record for an unidentified sensor on a FeatherS3 and the Axiom from the hosted page, from RaftCore's catalogue and from a git source; unpack the downloaded zip into a project and build it with `fsImage`, giving the same file (same CRC) | +| **7** | Later: WiFi (a plain `http` host, or the app run locally - W4); WASM checker (W5); single-record endpoints (W7); record editor with live grammar checks and a test against a device (`devman/cmdraw` reads of detection values, decoded preview with `RaftAttributeHandler`) (W8); other git hosts; `devman/types?full=1` | as needed | Per item | +| **8** | Later: an ESP32 flasher on the same site (esptool-js over Web Serial) that knows a Raft app's layout - the partition table, app, file system image (so a `fsImage` type file goes with it) and OTA slots - flashing a release's build for a SysType, or just the file system | raftjs or its own app, RaftCore (release assets per SysType) | Per board family | + +Phases 2 and 4 are independent and can run in parallel; 3 can follow 2 or be folded into 6; 5 needs 2 and 4. + +--- + +## 9. Decisions + +| # | Question | Decision | +|-|-|-| +| W1 | Where the app is hosted | **RaftCore's Pages site**, with the wiki and the rest of Raft's documentation, built from raftjs at a pinned tag, beside the catalogue (§7.1). An ESP32 flasher aware of Raft apps' layout can join it later (phase 8) (decided 2026-10-24) | +| W2 | Type file logic in TypeScript or the Python run in the browser (Pyodide) | **TypeScript port**, kept in step with the Python by shared vectors (§5.2) (decided 2026-10-24) | +| W3 | Catalogue history on Pages, and sources beyond RaftCore | **Keep every release's folder, plus `main`. Records from other sources too**, described as `DeviceTypeSources.json` does, and the generated `DeviceTypeSources.json` and lock offered as a download (§6a) (decided 2026-10-24) | +| W4 | WiFi from the app | **Later** (phase 7). Preferred direction: a second host serving the app over `http` (WiFi only there, since BLE and serial need a secure context), or the app run on `localhost` (§7.2) (decided 2026-10-24) | +| W5 | WebAssembly build of the firmware's loader and grammar | **Later**; the device's check is authoritative (decided 2026-10-24) | +| W6 | Report RaftCore version, build features and protected types from the device | **Yes**, additive fields (phase 1) (decided 2026-10-24) | +| W7 | Single-record add and remove endpoints on the device | **Later, if needed**; meanwhile pull, change and push the whole file through the staged check (§4) (decided 2026-10-24) | +| W8 | Record authoring in the app | **Later** (phase 7) (decided 2026-10-24) | +| W9 | Provenance (`_sources`) inside the generated type file | **Yes**, written by both builders (§6a). As built it holds the sources that supplied a record (git ones with their commit, RaftCore's with its version) and the selection - not per-record hashes, which would cost about 1.7 KB for 56 records; the records can be checked against the sources at those commits (decided 2026-10-24) | + +--- + +## 10. Risks and open points + +- **Structural equality of records** between the source files and `typeinfo full=1` (§5.3): if they differ in ways a + structural compare doesn't hide, records will be reported as overrides that are really unchanged. Harmless (the + device reports `unchanged` and the file is a little larger) but confusing; a test per RaftCore record on a device + settles it. +- **Catalogue version vs firmware version**: until phase 1 firmware is common, the app can't know a device's RaftCore + version and may offer records in a newer format. The device's check catches it; the app should explain the + rejection ("this record needs RaftCore 1.6x or later"). +- **Browser support**: no Firefox or iOS Safari (no Web Bluetooth or Web Serial). The catalogue works everywhere; the + device screens say why they are unavailable. +- **Restart and reconnect**: BLE reconnect after `apply` needs the user's gesture again in some browsers (Web + Bluetooth reconnects to a remembered device only where `getDevices()` is supported). The workflow must cope with a + manual reconnect step. +- **GitHub API rate limits** for `GitSource` (60 requests per hour unauthenticated): resolve a ref and list the tree + once per commit, read files from `raw.githubusercontent.com` (not rate limited the same way), and cache by commit. + A user who hits the limit could add a token, kept only in the browser (later). +- **Git sources the browser can't read**: private repos, `git@` URLs and hosts without CORS work in a build but not in + the app. On import the app says which sources it couldn't read and builds without them only if the user agrees. +- **Two implementations of the rules** (Python and TypeScript): the shared vectors are what keeps them the same; any + change to `build_type_file` or the grammar must add vectors. + +--- + +## 11. Progress (2026-10-24) + +### Phase 1: firmware facts (RaftCore) + +- `devman/typefile` status: `raftCore` (from `library.json`, as the compile definition `RAFTCORE_VERSION` set in + `CMakeLists.txt`), `recordFormat` (`DeviceTypeFileLoader::RECORD_FORMAT_VERSION`, 1) and `decode` + (`{"attrs":..,"code":..}` from `RAFT_RUNTIME_DECODE` / `RAFT_RUNTIME_DECODE_CODE`). `devman/types`: `"protected":true`. +- Checked: `DeviceManager.cpp` compiles for the ESP32-S3 (with and without `RAFTCORE_VERSION`), the CMake version + extraction gives `1.62.0`, `make -C linux_unit_tests check` passes. **Not yet checked on hardware.** +- Wiki: *DeviceManager REST API* (`typefile` fields, `types` `protected`). + +### W9 and phase 2: `_sources` and the TypeScript builder + +- `AssembleDeviceTypes.py`: `assemble()` records the provenance; `build_type_file` writes it as `_sources` (after + `devTypes`; `provenance=False` for none). The fs image and `raft devtypes typefile` files have it too. +- Shared vectors (RaftCore `scripts/tests`, copied into raftjs `testdata/`): `typefile_vectors.json` + (`make_typefile_vectors.py`: every RaftCore record as new and as unchanged, overrides and decoder kinds, policies, + init code rules, errors, and JSON that JavaScript reads and writes differently from Python) and + `selection_vectors.json` (`make_selection_vectors.py`: 25 selection cases). `test_typefile_vectors.py` checks that + the stored outputs are what the Python gives for the stored inputs, so a record change doesn't make them stale. +- raftjs (new files, exported from `main.ts`): + - `RaftDeviceTypeJson` - JSON as Python reads and writes it. Key order is kept: `JSON.parse` would move LSM6DS3's + rate map keys `"26","52"` ahead of `"12.5"`. Numbers keep their text and are written as Python's float repr + (`0.000000125` -> `1.25e-07`), and non-ASCII characters are escaped. + - `RaftDeviceTypeGrammar` - the command grammar, passing `grammar_vectors.json`. + - `RaftDeviceTypeFile` - `buildTypeFile`, `typeFileCrc`. Every vector gives the Python's text, byte for byte + (including the 63.7 KB file of every RaftCore record), and its entries. + - `RaftDeviceTypeFileManager` - status, table, `deviceRecord` (typeinfo full=1, cached per table generation), + `pull`, `upload`/`stage`/`check`/`apply`, `push` (stops if the device already has the file or its file changed + since it was read; dry run; confirm; waits for the restart; verifies CRC and per-entry outcomes). The upload goes + to `local/devlib.new.json` explicitly, since a bare name goes to the default file system, which can be the SD card. + - `RaftDeviceManager`: the new status fields and `protected` (types only). +- Not done: the dashboard's `TypeFilePanel` still has its own steps. It should move onto `DeviceTypeFileManager`, + but the dashboard is being changed by other work, so it was left alone. + +### Phase 3: early checks + +- The grammar (above). The decode language check (`PseudocodeHandler.check_code`) isn't ported: the device's check + rejects such code, and the app marks records with custom code. + +### Phase 4: the catalogue on Pages (RaftCore) + +- `AssembleDeviceTypes.py --pages --label ` and `--pages-index ` (a host test). +- `.github/workflows/pages.yml`: on a push to main (records, scripts, the page), a release, or by hand; builds every + release from v1.59.0 from its own records (`git archive`), `main` and the index, the landing page + (`scripts/pages/index.html`) and the app; deploys with `actions/deploy-pages`. Run locally (the same commands): 4 + versions. **Pages must be enabled in the repo settings (Source: GitHub Actions)** before the first deploy. + +### Phase 5: other sources and the generated selection (raftjs) + +- `RaftDeviceTypeSources`: `mergeDeviceTypeSources`, `resolveDeviceTypeSelection` and `deviceTypeProvenance`, + passing `selection_vectors.json`. +- `RaftDeviceTypeCatalogue`: `RaftCoreCatalogue` (index, version for a RaftCore version, entries, records, record + files), `readGitHubSource` (ref to commit with the GitHub API, the tree once per commit, files from + `raw.githubusercontent.com`, the manifest's `formatVersion` and `minRaftCore`, cached by commit), + `recordsFromFiles` (folder, record file or catalogue file, with the build's checks), and `deviceTypeLockText` (the + build's lock, byte for byte). + +### Phase 6: the app (raftjs `apps/devtypes`) + +- React and Parcel. The model is kept apart from the views. Tabs: + - **Catalogue**: search, including by address; record detail; add to the type file. + - **Device**: firmware facts and type file status; devices found; unidentified addresses (`busstatus` identTiming + `noMatch`) with the catalogue records for them; the type table with per-type policy; read the file back and + start from it (its `_sources`, or its records as a local source). + - **Sources**: GitHub sources, local files or folders, import of a project's selection and lock. + - **Type file**: the selection, entries with what each does and where it is decoded, check on the device, push + with confirmation. + - **Export**: a zip laid out as a project, or each file on its own. +- Checked in headless Chrome against a local copy of the site: + - the catalogue, search, adding a record and the export; + - phone width, with no horizontal overflow; + - importing a test selection (part of RaftCore's records, a git source - RaftCore itself at v1.61.0, read live + from GitHub - Axiom's records as a path source, an exclude and two policies) gave the same type file as + `AssembleDeviceTypes.py --typefile` (51,108 bytes, CRC `9796`) and the same lock; + - unpacking the app's export zip as a project and building it with the Python tool gave the same file again, with + the lock unchanged. +- **Not yet checked:** anything on a device (connect, Device tab, push, pull). Next, on a FeatherS3 or Axiom: + - connect by Bluetooth and by serial; + - push a file with a new record for a sensor that isn't identified, and see it identified after the restart; + - a rejected record is refused; read the file back and start from it; + - the restart and reconnect over Bluetooth (§10). + +### Found on the way + +- Every RaftCore record as one type file is now 63,673 bytes, just under the 64 KB default `maxFileBytes`. The + "all records at runtime" setup (RaftCore plan §3.8) will need a larger limit after a record or two more. diff --git a/src/RaftDeviceManager.ts b/src/RaftDeviceManager.ts index cd0a10b..a1aa9e7 100644 --- a/src/RaftDeviceManager.ts +++ b/src/RaftDeviceManager.ts @@ -52,7 +52,8 @@ export interface DevManListDevs { export interface DevManTypes { total?: number; generation?: number; - types?: Array<{ dtIdx: number; key: string; src: string; policy: DevManTypePolicy }>; + // protected: a type a system device expects (expectType) - it can't be overridden or disabled (firmware from 2026-10-24) + types?: Array<{ dtIdx: number; key: string; src: string; policy: DevManTypePolicy; protected?: boolean }>; } export interface DevManIdentInfo { @@ -113,6 +114,12 @@ export interface DevManTypeFileStatus { maxFileBytes?: number; identReady?: boolean; // devices are being identified (the file has been loaded, or its timeout passed) restartRequired?: boolean; + // The firmware's rules (firmware from 2026-10-24): its RaftCore version, the record format it reads, and the + // on-device decoding built in - from a record's attributes (RAFT_RUNTIME_DECODE) and of custom decode and init + // code (RAFT_RUNTIME_DECODE_CODE) + raftCore?: string; + recordFormat?: number; + decode?: { attrs?: boolean; code?: boolean }; entries?: DevManTypeFileEntry[]; // with getTypeFileStatus(true) } diff --git a/src/RaftDeviceTypeCatalogue.test.ts b/src/RaftDeviceTypeCatalogue.test.ts new file mode 100644 index 0000000..63e3f2c --- /dev/null +++ b/src/RaftDeviceTypeCatalogue.test.ts @@ -0,0 +1,125 @@ +// Reading device type sources: RaftCore's published catalogue, GitHub git sources and record files; the lock file + +import { DeviceTypeFetch, DeviceTypeSourceError, RaftCoreCatalogue, deviceTypeLockText, parseGitHubUrl, readGitHubSource, + recordsFromFiles, sha256Hex, versionLessThan } from "./RaftDeviceTypeCatalogue"; +import { DevTypeJsonObject, parseDevTypeJson, writeDevTypeJson } from "./RaftDeviceTypeJson"; +import { DeviceTypeSourceRecords, mergeDeviceTypeSources, resolveDeviceTypeSelection } from "./RaftDeviceTypeSources"; + +function fakeFetch(files: Record, log: string[] = []): DeviceTypeFetch { + return async (url: string) => { + log.push(url); + const text = files[url]; + return { ok: text !== undefined, status: text !== undefined ? 200 : 404, text: async () => text ?? "" }; + }; +} + +const SITE = "https://example.com/devtypes"; +const site = { + [`${SITE}/index.json`]: JSON.stringify({ formatVersion: 1, latest: "v1.61.0", versions: [ + { label: "main", raftCore: "1.62.0", count: 2 }, { label: "v1.61.0", raftCore: "1.61.0", count: 2 }, + { label: "v1.59.0", raftCore: "1.59.0", count: 1 }] }), + [`${SITE}/v1.61.0/catalogue.json`]: JSON.stringify({ formatVersion: 1, label: "v1.61.0", raftCore: "1.61.0", recordFormat: 1, count: 2, + records: [{ key: "LSM6DS3", file: "records/LSM6DS3.json", sha256: "x", summary: { name: "IMU", hasData: true, customCode: false, initCode: false } }, + { key: "MCP9808", file: "records/MCP9808.json", sha256: "y", summary: { name: "Temp", hasData: true, customCode: false, initCode: false } }] }), + [`${SITE}/v1.61.0/DeviceTypeRecords.json`]: '{"devTypes":{"LSM6DS3":{"addresses":"0x6a","m":{"12.5":1,"26":2}},"MCP9808":{"addresses":"0x18"}}}', + [`${SITE}/v1.61.0/records/MCP9808.json`]: '{\n "addresses": "0x18"\n}\n', +}; + +describe("RaftCore's published catalogue", () => { + test("index, version choice, entries, records and a record file", async () => { + const fetchFn = fakeFetch(site); + const index = await RaftCoreCatalogue.index(fetchFn, SITE); + expect(RaftCoreCatalogue.chooseVersion(index, "1.61.0")).toBe("v1.61.0"); + expect(RaftCoreCatalogue.chooseVersion(index, "1.60.2")).toBe("v1.59.0"); + expect(RaftCoreCatalogue.chooseVersion(index, "1.70.0")).toBe("v1.61.0"); + expect(RaftCoreCatalogue.chooseVersion(index, undefined)).toBe("v1.61.0"); + const catalogue = await RaftCoreCatalogue.open(fetchFn, "v1.61.0", SITE); + expect(catalogue.raftCore).toBe("1.61.0"); + expect(catalogue.entries().map((e) => e.key)).toEqual(["LSM6DS3", "MCP9808"]); + const records = await catalogue.records(); + expect(writeDevTypeJson(records.records.get("LSM6DS3") ?? null)).toBe('{"addresses":"0x6a","m":{"12.5":1,"26":2}}'); + expect(records.raftCore).toBe("1.61.0"); + expect((await catalogue.recordFile("MCP9808")).text).toContain("0x18"); + await expect(catalogue.recordFile("Nope")).rejects.toThrow("no record"); + await expect(RaftCoreCatalogue.open(fetchFn, "v9.9.9", SITE)).rejects.toThrow("HTTP 404"); + }); +}); + +describe("record files", () => { + test("a folder, a record file and a catalogue file", () => { + const folder = recordsFromFiles([{ path: "devtypes/Zeta.json", text: '{"addresses":"0x30"}' }, + { path: "devtypes/Alpha.json", text: '{"addresses":"0x31","deviceType":"Alpha"}' }, { path: "devtypes/notes.txt", text: "" }], true); + expect(Array.from(folder.records.keys())).toEqual(["Alpha", "Zeta"]); + expect(folder.files.get("Zeta")?.path).toBe("devtypes/Zeta.json"); + expect(Array.from(recordsFromFiles([{ path: "x/Gadget.json", text: '{"addresses":"0x40"}' }], false).records.keys())).toEqual(["Gadget"]); + expect(Array.from(recordsFromFiles([{ path: "cat.json", text: '{"devTypes":{"B":{},"A":{}}}' }], false).records.keys())).toEqual(["B", "A"]); + expect(() => recordsFromFiles([{ path: "d/Alpha.json", text: '{"deviceType":"Beta"}' }], true)).toThrow("differs from the file name"); + expect(() => recordsFromFiles([{ path: "d/Cat.json", text: '{"devTypes":{}}' }], true)).toThrow("catalogue file"); + expect(() => recordsFromFiles([{ path: "d/Bad.json", text: "{" }], true)).toThrow(DeviceTypeSourceError); + }); +}); + +describe("GitHub sources", () => { + const commit = "a".repeat(40); + const api = "https://api.github.com/repos/acme/devtypes"; + const raw = `https://raw.githubusercontent.com/acme/devtypes/${commit}`; + const gadget = '{\n "addresses": "0x40"\n}\n'; + const github = { + [`${api}/commits/v1.0.0`]: JSON.stringify({ sha: commit }), + [`${api}/git/trees/${commit}?recursive=1`]: JSON.stringify({ tree: [ + { path: "raftdevtypes.json", type: "blob" }, { path: "records", type: "tree" }, + { path: "records/Gadget.json", type: "blob" }, { path: "records/sub", type: "tree" }, { path: "records/sub/Other.json", type: "blob" }, + { path: "one/Widget.json", type: "blob" }] }), + [`${raw}/raftdevtypes.json`]: '{"name":"acme","formatVersion":1,"minRaftCore":"1.60.0"}', + [`${raw}/records/Gadget.json`]: gadget, + [`${raw}/one/Widget.json`]: '{"addresses":"0x41"}', + }; + const spec = (extra = {}) => parseDevTypeJson(JSON.stringify({ git: "https://github.com/acme/devtypes.git", ref: "v1.0.0", ...extra })) as DevTypeJsonObject; + + test("URLs", () => { + expect(parseGitHubUrl("https://github.com/acme/devtypes.git")).toEqual({ owner: "acme", repo: "devtypes" }); + expect(parseGitHubUrl("git@github.com:acme/devtypes.git")).toEqual({ owner: "acme", repo: "devtypes" }); + expect(parseGitHubUrl("https://github.com/acme/devtypes")).toEqual({ owner: "acme", repo: "devtypes" }); + expect(parseGitHubUrl("https://gitlab.com/acme/devtypes.git")).toBeUndefined(); + }); + + test("a folder at a tag, a record file, the manifest, cached by commit", async () => { + const log: string[] = []; + const fetchFn = fakeFetch(github, log); + const source = await readGitHubSource("acme", spec(), { fetch: fetchFn, raftCore: "1.62.0" }); + expect(source.commit).toBe(commit); + expect(Array.from(source.records.keys())).toEqual(["Gadget"]); + expect(source.files.get("Gadget")?.text).toBe(gadget); + const widget = await readGitHubSource("acme", spec({ path: "one/Widget.json" }), { fetch: fetchFn }, commit); + expect(Array.from(widget.records.keys())).toEqual(["Widget"]); + expect(log.filter((u) => u.includes("/git/trees/")).length).toBe(1); + await expect(readGitHubSource("acme", spec(), { fetch: fetchFn, raftCore: "1.59.0" })).rejects.toThrow("needs RaftCore 1.60.0"); + await expect(readGitHubSource("acme", spec({ path: "nope" }), { fetch: fetchFn })).rejects.toThrow("not found in the repo"); + await expect(readGitHubSource("acme", spec({ ref: "v9" }), { fetch: fetchFn })).rejects.toThrow("HTTP 404"); + await expect(readGitHubSource("acme", parseDevTypeJson('{"git":"https://gitlab.com/a/b.git","ref":"v1"}') as DevTypeJsonObject, + { fetch: fetchFn })).rejects.toThrow("only GitHub"); + }); + + test("the lock file, as a build writes it", async () => { + const source = await readGitHubSource("acme", spec(), { fetch: fakeFetch(github) }); + const merged = mergeDeviceTypeSources([{ name: "x", content: JSON.stringify({ + sources: { raftcore: { bundled: true }, acme: { git: "https://github.com/acme/devtypes.git", ref: "v1.0.0" } }, + typeFile: { include: ["acme:Gadget", "raftcore:MCP9808"] } }) }]); + const sources = new Map([["acme", source], ["raftcore", { records: new Map([["MCP9808", parseDevTypeJson("{}")]]) }]]); + const result = resolveDeviceTypeSelection(merged, "typeFile", sources); + expect(await deviceTypeLockText(merged, result, sources)).toBe( + '{\n "formatVersion": 1,\n "sources": {\n "acme": {\n "git": "https://github.com/acme/devtypes.git",\n "ref": "v1.0.0",\n' + + ' "commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\n }\n },\n "records": {\n "Gadget": {\n "source": "acme",\n' + + ' "commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",\n "sha256": "b7df4af80eff5bd2b72a31dbb8854b4ce8188891adf0f62f4f37da6c43f9cae2"\n' + + ' }\n }\n}\n'); + }); +}); + +describe("helpers", () => { + test("SHA-256 and versions", async () => { + expect(await sha256Hex("abc")).toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + expect(versionLessThan("1.59.0", "1.60.0")).toBe(true); + expect(versionLessThan("1.60.0", "1.60.0")).toBe(false); + expect(versionLessThan("v1.61.2", "1.61.10")).toBe(true); + }); +}); diff --git a/src/RaftDeviceTypeCatalogue.ts b/src/RaftDeviceTypeCatalogue.ts new file mode 100644 index 0000000..6baeb03 --- /dev/null +++ b/src/RaftDeviceTypeCatalogue.ts @@ -0,0 +1,363 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// RaftDeviceTypeCatalogue +// Part of RaftJS +// +// Reading the records of DeviceTypeSources.json sources in a browser (or Node, or React Native - fetch is passed +// in): RaftCore's records from the catalogue published on RaftCore's GitHub Pages site, git sources on GitHub (read +// through its API at the commit a ref resolves to - a browser can't run git), and record files the user picks. And +// the lock file (DeviceTypeSources.lock) of a resolved selection, as a build writes it. +// +// Rob Dobson 2026 +// (C) 2026 All rights reserved +// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +import { DevTypeJson, DevTypeJsonNumber, DevTypeJsonObject, compareCodePoints, devTypeJsonGet, isDevTypeJsonObject, parseDevTypeJson, + writeDevTypeJson } from "./RaftDeviceTypeJson"; +import { DeviceTypeSelectionResult, DeviceTypeSourceRecords, MergedDeviceTypeSources, deviceTypeSourceKind } from "./RaftDeviceTypeSources"; + +export const RAFTCORE_PAGES_CATALOGUE_URL = "https://robdobsn.github.io/RaftCore/devtypes"; +export const DEVICE_TYPE_SOURCE_MANIFEST = "raftdevtypes.json"; +export const DEVICE_TYPE_SOURCE_FORMAT_VERSION = 1; +export const DEVICE_TYPE_LOCK_FORMAT_VERSION = 1; + +export class DeviceTypeSourceError extends Error {} + +// What fetch must offer (the browser's fetch, Node's, or a test's) +export type DeviceTypeFetch = (url: string, init?: { headers?: Record }) => + Promise<{ ok: boolean; status: number; text(): Promise }>; + +// A record's file as read (its text is what the lock's SHA-256 is of) +export interface DeviceTypeRecordFile { + path: string; + text: string; +} + +// Source records with the file each came from +export interface DeviceTypeSourceRecordsWithFiles extends DeviceTypeSourceRecords { + files: Map; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Helpers +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export async function sha256Hex(text: string): Promise { + const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text) as Uint8Array); + return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join(""); +} + +function versionTuple(version: string | undefined): number[] { + return (version ?? "").match(/\d+/g)?.slice(0, 3).map(Number) ?? []; +} + +// a < b as Python compares version tuples +export function versionLessThan(a: string, b: string): boolean { + const ta = versionTuple(a); + const tb = versionTuple(b); + for (let i = 0; i < Math.max(ta.length, tb.length); i++) { + if (i >= ta.length) + return true; + if (i >= tb.length) + return false; + if (ta[i] !== tb[i]) + return ta[i] < tb[i]; + } + return false; +} + +async function fetchText(fetchFn: DeviceTypeFetch, url: string, what: string, headers?: Record): Promise { + let resp; + try { + resp = await fetchFn(url, headers ? { headers } : undefined); + } catch (err) { + throw new DeviceTypeSourceError(`${what}: couldn't fetch ${url} (${err instanceof Error ? err.message : err})`); + } + if (!resp.ok) + throw new DeviceTypeSourceError(`${what}: ${url} gave HTTP ${resp.status}`); + return resp.text(); +} + +function parse(text: string, where: string): DevTypeJson { + try { + return parseDevTypeJson(text); + } catch (err) { + throw new DeviceTypeSourceError(`${where}: not valid JSON (${err instanceof Error ? err.message : err})`); + } +} + +function recordFromFile(file: DeviceTypeRecordFile, key: string): DevTypeJson { + const record = parse(file.text, file.path); + if (!isDevTypeJsonObject(record)) + throw new DeviceTypeSourceError(`${file.path}: a record file holds one record (a JSON object)`); + if (record.has("devTypes")) + throw new DeviceTypeSourceError(`${file.path}: a catalogue file (with "devTypes") in a folder of records`); + const deviceType = record.get("deviceType"); + if (deviceType !== undefined && deviceType !== key) + throw new DeviceTypeSourceError(`${file.path}: deviceType "${deviceType}" differs from the file name "${key}"`); + return record; +} + +const baseName = (path: string) => path.slice(path.lastIndexOf("/") + 1); +const keyOf = (path: string) => baseName(path).replace(/\.json$/, ""); + +// Records from files, as a build reads a source's path: a folder of record files (.json, in key order), one +// record file, or a catalogue file ({"devTypes": {...}}) +export function recordsFromFiles(files: DeviceTypeRecordFile[], asFolder: boolean): DeviceTypeSourceRecordsWithFiles { + const records = new Map(); + const fileMap = new Map(); + if (asFolder) { + const jsonFiles = files.filter((f) => f.path.endsWith(".json")); + jsonFiles.sort((a, b) => compareCodePoints(keyOf(a.path), keyOf(b.path))); + for (const file of jsonFiles) { + const key = keyOf(file.path); + records.set(key, recordFromFile(file, key)); + fileMap.set(key, file); + } + return { records, files: fileMap }; + } + if (files.length !== 1) + throw new DeviceTypeSourceError("expected one file"); + const file = files[0]; + const content = parse(file.text, file.path); + const devTypes = devTypeJsonGet(content, "devTypes"); + if (isDevTypeJsonObject(devTypes)) { + devTypes.forEach((rec, key) => { records.set(key, rec); fileMap.set(key, file); }); + return { records, files: fileMap }; + } + const key = keyOf(file.path); + records.set(key, recordFromFile(file, key)); + fileMap.set(key, file); + return { records, files: fileMap }; +} + +// A source's raftdevtypes.json manifest: its format, and the RaftCore it needs +export function checkSourceManifest(name: string, text: string | undefined, raftCore: string | undefined) { + if (text === undefined) + return; + const manifest = parse(text, `source "${name}": ${DEVICE_TYPE_SOURCE_MANIFEST}`); + const fmt = devTypeJsonGet(manifest, "formatVersion"); + const fmtValue = fmt === undefined ? 1 : (fmt instanceof DevTypeJsonNumber ? fmt.value : fmt); + if (fmtValue !== DEVICE_TYPE_SOURCE_FORMAT_VERSION) + throw new DeviceTypeSourceError(`source "${name}": ${DEVICE_TYPE_SOURCE_MANIFEST} formatVersion ${fmtValue} isn't supported ` + + `(supports ${DEVICE_TYPE_SOURCE_FORMAT_VERSION})`); + const needed = devTypeJsonGet(manifest, "minRaftCore"); + if (typeof needed === "string" && needed && raftCore && versionLessThan(raftCore, needed)) + throw new DeviceTypeSourceError(`source "${name}" needs RaftCore ${needed} or later (the device has ${raftCore})`); +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// RaftCore's records: the catalogue on RaftCore's GitHub Pages site +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export interface DeviceTypeCatalogueSummary { + name?: string; + manu?: string; + type?: string; + clas?: string[] | string; + addresses?: string; + hwTypeId?: string; + hasData: boolean; + customCode: boolean; + initCode: boolean; +} + +export interface DeviceTypeCatalogueEntry { + key: string; + file: string; + sha256: string; + summary: DeviceTypeCatalogueSummary; +} + +export interface DeviceTypeCatalogueIndex { + formatVersion: number; + latest: string | null; + versions: Array<{ label: string; raftCore?: string; count?: number }>; +} + +export interface DeviceTypeCatalogueInfo { + formatVersion: number; + label: string; + raftCore?: string; + recordFormat?: number; + count: number; + records: DeviceTypeCatalogueEntry[]; +} + +export class RaftCoreCatalogue { + private _records: Map | undefined = undefined; + + private constructor(public readonly baseUrl: string, public readonly info: DeviceTypeCatalogueInfo, + private _fetch: DeviceTypeFetch) {} + + get label(): string { + return this.info.label; + } + + get raftCore(): string | undefined { + return this.info.raftCore; + } + + // The versions published + static async index(fetchFn: DeviceTypeFetch, baseUrl = RAFTCORE_PAGES_CATALOGUE_URL): Promise { + return JSON.parse(await fetchText(fetchFn, `${baseUrl}/index.json`, "RaftCore catalogue")) as DeviceTypeCatalogueIndex; + } + + // The version to use for a device of a RaftCore version: that release if published, else the newest release not + // newer than it, else the latest + static chooseVersion(index: DeviceTypeCatalogueIndex, raftCore: string | undefined): string | undefined { + const releases = index.versions.filter((v) => /^v?\d+\.\d+\.\d+$/.test(v.label)); + if (raftCore) { + const exact = releases.find((v) => v.label.replace(/^v/, "") === raftCore.replace(/^v/, "")); + if (exact) + return exact.label; + const older = releases.find((v) => !versionLessThan(raftCore, v.label)); + if (older) + return older.label; + } + return index.latest ?? index.versions[0]?.label; + } + + static async open(fetchFn: DeviceTypeFetch, label: string, baseUrl = RAFTCORE_PAGES_CATALOGUE_URL): Promise { + const info = JSON.parse(await fetchText(fetchFn, `${baseUrl}/${label}/catalogue.json`, "RaftCore catalogue")) as DeviceTypeCatalogueInfo; + return new RaftCoreCatalogue(baseUrl, info, fetchFn); + } + + entries(): DeviceTypeCatalogueEntry[] { + return this.info.records; + } + + // Every record (one fetch of the version's DeviceTypeRecords.json) + async records(): Promise { + if (!this._records) { + const text = await fetchText(this._fetch, `${this.baseUrl}/${this.label}/DeviceTypeRecords.json`, "RaftCore catalogue"); + const devTypes = devTypeJsonGet(parse(text, "DeviceTypeRecords.json"), "devTypes"); + if (!isDevTypeJsonObject(devTypes)) + throw new DeviceTypeSourceError("RaftCore catalogue: DeviceTypeRecords.json has no devTypes"); + this._records = devTypes; + } + return { records: this._records, raftCore: this.raftCore }; + } + + // One record's file, as it is in RaftCore + async recordFile(key: string): Promise { + const entry = this.info.records.find((r) => r.key === key); + if (!entry) + throw new DeviceTypeSourceError(`RaftCore catalogue ${this.label}: no record "${key}"`); + return { path: entry.file, text: await fetchText(this._fetch, `${this.baseUrl}/${this.label}/${entry.file}`, "RaftCore catalogue") }; + } +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Git sources on GitHub +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// owner and repo of a GitHub URL (https://github.com/o/r(.git), git@github.com:o/r.git), or undefined +export function parseGitHubUrl(url: string): { owner: string; repo: string } | undefined { + const m = /^(?:https?:\/\/(?:www\.)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(url.trim()); + return m ? { owner: m[1], repo: m[2] } : undefined; +} + +export interface GitHubSourceOptions { + fetch: DeviceTypeFetch; + apiUrl?: string; // default https://api.github.com + rawUrl?: string; // default https://raw.githubusercontent.com + token?: string; // for the API's rate limit (kept by the caller) + raftCore?: string; // the RaftCore the records are for (checked against the source's minRaftCore) +} + +// Files read at a commit never change, so they are kept for the session +const gitHubCache = new Map>(); + +// A git source's records from GitHub: ref resolved to a commit (or the locked commit), the manifest checked, the +// files at path read (a folder of record files, one record file or a catalogue file; default path "records") +export async function readGitHubSource(name: string, spec: DevTypeJsonObject, options: GitHubSourceOptions, + lockedCommit?: string): Promise { + const url = spec.get("git"); + const ref = spec.get("ref"); + if (typeof url !== "string") + throw new DeviceTypeSourceError(`source "${name}": "git" must be a URL`); + if (typeof ref !== "string" || !ref) + throw new DeviceTypeSourceError(`source "${name}": "ref" (a tag, branch or commit) is needed`); + const pathSpec = spec.get("path"); + const subpath = (typeof pathSpec === "string" ? pathSpec : "records").replace(/^\/+|\/+$/g, ""); + const repo = parseGitHubUrl(url); + if (!repo) + throw new DeviceTypeSourceError(`source "${name}": only GitHub repos can be read here (${url})`); + const api = options.apiUrl ?? "https://api.github.com"; + const raw = options.rawUrl ?? "https://raw.githubusercontent.com"; + const headers: Record = { Accept: "application/vnd.github+json" }; + if (options.token) + headers.Authorization = `Bearer ${options.token}`; + const cached = (key: string, get: () => Promise) => { + if (!gitHubCache.has(key)) + gitHubCache.set(key, get().catch((err) => { gitHubCache.delete(key); throw err; })); + return gitHubCache.get(key) as Promise; + }; + const what = `source "${name}"`; + + // The commit + let commit = lockedCommit; + if (!commit) + commit = /^[0-9a-f]{40}$/i.test(ref) ? ref.toLowerCase() : undefined; + if (!commit) { + const resp = JSON.parse(await fetchText(options.fetch, `${api}/repos/${repo.owner}/${repo.repo}/commits/${encodeURIComponent(ref)}`, + what, headers)) as { sha?: string }; + if (!resp.sha) + throw new DeviceTypeSourceError(`${what}: ${ref} not found in ${url}`); + commit = resp.sha; + } + const base = `${repo.owner}/${repo.repo}/${commit}`; + + // The tree at the commit + const treeText = await cached(`tree:${base}`, () => + fetchText(options.fetch, `${api}/repos/${repo.owner}/${repo.repo}/git/trees/${commit}?recursive=1`, what, headers)); + const tree = JSON.parse(treeText) as { tree?: Array<{ path: string; type: string }>; truncated?: boolean }; + const entries = tree.tree ?? []; + const readFile = (path: string) => cached(`file:${base}/${path}`, () => + fetchText(options.fetch, `${raw}/${base}/${path.split("/").map(encodeURIComponent).join("/")}`, what)); + + if (entries.some((e) => e.path === DEVICE_TYPE_SOURCE_MANIFEST && e.type === "blob")) + checkSourceManifest(name, await readFile(DEVICE_TYPE_SOURCE_MANIFEST), options.raftCore); + + const asFile = entries.find((e) => e.path === subpath && e.type === "blob"); + const inFolder = entries.filter((e) => e.type === "blob" && e.path.startsWith(subpath + "/") && + !e.path.slice(subpath.length + 1).includes("/") && e.path.endsWith(".json")); + if (!asFile && !entries.some((e) => e.path === subpath && e.type === "tree")) + throw new DeviceTypeSourceError(`${what}: ${subpath} not found in the repo`); + const files = await Promise.all((asFile ? [asFile] : inFolder).map(async (e) => ({ path: e.path, text: await readFile(e.path) }))); + const result = recordsFromFiles(files, !asFile); + return { ...result, commit }; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// The lock file +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// DeviceTypeSources.lock for a resolved selection, as a build writes it: the commit each git source resolved to, +// and the SHA-256 of each record from a git source +export async function deviceTypeLockText(merged: MergedDeviceTypeSources, result: DeviceTypeSelectionResult, + sourceRecords: Map): Promise { + const sources: DevTypeJsonObject = new Map(); + for (const [name, spec] of merged.sources) { + const commit = sourceRecords.get(name)?.commit; + if (deviceTypeSourceKind(spec) !== "git" || !commit) + continue; + sources.set(name, new Map([["git", spec.get("git") as DevTypeJson], ["ref", spec.get("ref") as DevTypeJson], + ["commit", commit]])); + } + const records: DevTypeJsonObject = new Map(); + for (const [key, source] of result.origin) { + if (!sources.has(source)) + continue; + const loaded = sourceRecords.get(source) as DeviceTypeSourceRecordsWithFiles; + const file = loaded.files?.get(key); + if (!file) + throw new DeviceTypeSourceError(`"${key}" from source "${source}": its file wasn't kept, so it can't be hashed`); + records.set(key, new Map([["source", source], ["commit", loaded.commit as string], + ["sha256", await sha256Hex(file.text)]])); + } + return writeDevTypeJson(new Map([["formatVersion", new DevTypeJsonNumber(String(DEVICE_TYPE_LOCK_FORMAT_VERSION))], + ["sources", sources], ["records", records]]), { indent: 1 }) + "\n"; +} diff --git a/src/RaftDeviceTypeFile.test.ts b/src/RaftDeviceTypeFile.test.ts new file mode 100644 index 0000000..61b622d --- /dev/null +++ b/src/RaftDeviceTypeFile.test.ts @@ -0,0 +1,144 @@ +// Device type file building, the grammar and the JSON it is written with, against the vectors shared with RaftCore +// (scripts/tests/typefile_vectors.json and grammar_vectors.json, copied into testdata/ - after a change there, copy +// them again; the last test fails if a RaftCore checkout next to raftjs has different ones) + +import * as fs from "fs"; +import * as path from "path"; +import { DevTypeJson, DevTypeJsonNumber, devTypeJsonFromPlain, devTypeJsonGet, devTypeJsonToPlain, isDevTypeJsonObject, + parseDevTypeJson, pythonFloatRepr, writeDevTypeJson } from "./RaftDeviceTypeJson"; +import { compileDeviceTypeCommand, deviceTypeCrc8, deviceTypeDetectionStrength, DeviceTypeGrammarField, + isDeviceTypeGrammarError } from "./RaftDeviceTypeGrammar"; +import { buildTypeFile, crc16Ccitt, DeviceTypeFileError, DeviceTypePolicyName, typeFileCrc } from "./RaftDeviceTypeFile"; + +const TESTDATA = path.join(__dirname, "..", "testdata"); +const RAFTCORE_TESTS = path.join(__dirname, "..", "..", "RaftCore", "scripts", "tests"); + +function readVectors(name: string): any[] { + return JSON.parse(fs.readFileSync(path.join(TESTDATA, name), "utf-8")).vectors; +} + +function recordsOf(catalogueText: string): Map { + const devTypes = devTypeJsonGet(parseDevTypeJson(catalogueText), "devTypes"); + if (!isDevTypeJsonObject(devTypes)) + throw new Error("no devTypes"); + return devTypes; +} + +describe("type file build vectors", () => { + const vectors = readVectors("typefile_vectors.json"); + + test("there are vectors", () => { + expect(vectors.length).toBeGreaterThan(10); + }); + + test.each(vectors.map((v) => [v.name, v]))("%s", (_name, vec) => { + const input = { + records: recordsOf(vec.records), + policyOnly: new Map(Object.entries(vec.policyOnly)), + provenance: vec.provenance ? parseDevTypeJson(vec.provenance) : undefined, + }; + const build = () => buildTypeFile(input, recordsOf(vec.compiled), { maxBytes: vec.maxBytes }); + if (vec.error !== undefined) { + expect(build).toThrow(DeviceTypeFileError); + expect(build).toThrow(vec.error); + return; + } + const result = build(); + expect(result.text).toBe(vec.text); + expect(result.crc).toBe(vec.crc); + expect(result.bytes).toBe(vec.text.length); + expect(result.entries).toEqual(vec.entries); + }); +}); + +describe("grammar vectors", () => { + const vectors = readVectors("grammar_vectors.json"); + const hex = (bytes: number[]) => bytes.map((b) => b.toString(16).padStart(2, "0")).join(""); + + test.each(vectors.map((v) => [v.name, v]))("%s", (_name, vec) => { + const result = compileDeviceTypeCommand(vec.str, vec.field as DeviceTypeGrammarField); + expect(result.issues.map((i) => i.code)).toEqual(vec.issues); + const expected = vec.steps.map((s: any) => ({ write: s.write, read: s.read, pause: s.pause, checks: s.checks ?? [], + crc8w: s.crc8w ?? false, expr: s.expr ?? false, slots: s.slots ?? [], guard: s.guard ?? "" })); + const got = result.steps.map((s) => ({ write: hex(s.write), read: s.read, pause: s.pause, + checks: s.checks.map((c) => hex(c.mask) + ":" + hex(c.value)), crc8w: s.crc8w, expr: s.expr, + slots: s.slots.map((slot) => `${slot.offset}:${slot.name}`), guard: s.guard })); + expect(got).toEqual(expected); + }); + + test("RaftCore's records have no grammar errors", () => { + const records = recordsOf(readVectors("typefile_vectors.json")[0].records); + expect(records.size).toBeGreaterThan(40); + const errors: string[] = []; + records.forEach((rec, key) => { + const fields: Array<[DeviceTypeGrammarField, DevTypeJson | undefined]> = [ + ["detection", devTypeJsonGet(rec, "detectionValues")], ["init", devTypeJsonGet(rec, "initValues")], + ["poll", devTypeJsonGet(rec, "pollInfo", "c")]]; + for (const [field, value] of fields) { + const result = compileDeviceTypeCommand(typeof value === "string" ? value : "", field); + result.issues.filter((i) => isDeviceTypeGrammarError(i.code)).forEach((i) => errors.push(`${key} ${field} ${i.code}@${i.pos}`)); + } + }); + expect(errors).toEqual([]); + }); + + test("detection strength and CRC-8", () => { + expect(deviceTypeDetectionStrength(compileDeviceTypeCommand("0x0f=0x69", "detection"))).toBe(8); + expect(deviceTypeDetectionStrength(compileDeviceTypeCommand("0x0c=0b100001100000XXXX", "detection"))).toBe(12); + expect(deviceTypeDetectionStrength(compileDeviceTypeCommand("0x86=A005", "detection"))).toBe(-1); + expect(deviceTypeCrc8([0xbe, 0xef])).toBe(0x92); + }); +}); + +describe("JSON as Python writes it", () => { + test("float repr", () => { + const cases: Array<[number, string]> = [[16, "16.0"], [1.5, "1.5"], [0.000000125, "1.25e-07"], [1e16, "1e+16"], + [1.5e16, "1.5e+16"], [1234567890123456, "1234567890123456.0"], [0.0001, "0.0001"], [0.00001, "1e-05"], + [-0, "-0.0"], [0, "0.0"], [-2.5e-300, "-2.5e-300"], [1.7976931348623157e308, "1.7976931348623157e+308"], + [182.04444444444444, "182.04444444444445"], [Infinity, "Infinity"]]; + for (const [value, text] of cases) + expect(pythonFloatRepr(value)).toBe(text); + }); + + test("key order, numbers and repeated keys kept", () => { + const value = parseDevTypeJson('{"12.5":1,"26":2.50,"a":{"x":1,"y":2,"x":3},"n":-0,"e":1E3}'); + expect(writeDevTypeJson(value)).toBe('{"12.5":1,"26":2.5,"a":{"x":3,"y":2},"n":0,"e":1000.0}'); + expect(writeDevTypeJson(value, { sortKeys: true })).toBe('{"12.5":1,"26":2.5,"a":{"x":3,"y":2},"e":1000.0,"n":0}'); + }); + + test("indented as json.dumps(indent=n)", () => { + const value = parseDevTypeJson('{"a":[1,{"b":[]}],"c":{},"d":"x"}'); + expect(writeDevTypeJson(value, { indent: 1 })).toBe('{\n "a": [\n 1,\n {\n "b": []\n }\n ],\n "c": {},\n "d": "x"\n}'); + }); + + test("strings escaped as with ensure_ascii", () => { + expect(writeDevTypeJson("°C \u007f\t\"\\/ 😀")).toBe('"\\u00b0C \\u007f\\t\\"\\\\/ \\ud83d\\ude00"'); + }); + + test("parse errors", () => { + for (const bad of ["{", '{"a":}', "[1,]", "01", '{"a":1} x', "'a'", "NaN"]) + expect(() => parseDevTypeJson(bad)).toThrow(); + }); + + test("plain values", () => { + const value = devTypeJsonFromPlain({ a: [1, 2.5, "x", null, true], b: { c: 1e21 } }); + expect(writeDevTypeJson(value)).toBe('{"a":[1,2.5,"x",null,true],"b":{"c":1e+21}}'); + expect(devTypeJsonToPlain(value)).toEqual({ a: [1, 2.5, "x", null, true], b: { c: 1e21 } }); + expect((devTypeJsonGet(value, "a") as DevTypeJson[])[0]).toBeInstanceOf(DevTypeJsonNumber); + }); +}); + +describe("CRC", () => { + test("CRC16-CCITT as the firmware", () => { + expect(crc16Ccitt(new TextEncoder().encode("123456789"))).toBe(0x29b1); + expect(typeFileCrc('{"devTypes":{}}')).toBe("bfcb"); + }); +}); + +describe("vectors are RaftCore's", () => { + // Only where a RaftCore checkout next to raftjs has them + const names = ["typefile_vectors.json", "grammar_vectors.json"].filter((n) => fs.existsSync(path.join(RAFTCORE_TESTS, n))); + (names.length > 0 ? test.each(names) : test.skip.each(["(no RaftCore checkout with the vectors)"]))("%s", (name) => { + expect(fs.readFileSync(path.join(TESTDATA, name), "utf-8")).toBe(fs.readFileSync(path.join(RAFTCORE_TESTS, name), "utf-8")); + }); +}); diff --git a/src/RaftDeviceTypeFile.ts b/src/RaftDeviceTypeFile.ts new file mode 100644 index 0000000..0345ca6 --- /dev/null +++ b/src/RaftDeviceTypeFile.ts @@ -0,0 +1,221 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// RaftDeviceTypeFile +// Part of RaftJS +// +// Building a device type file (the records a device loads from its file system at startup - RaftCore wiki, +// DeviceManager Settings, "Device type file"): a TypeScript copy of RaftCore's AssembleDeviceTypes.build_type_file, +// which gives the same text (so the device's CRC of the file agrees) and entries. Both are checked against the +// vectors in RaftCore scripts/tests/typefile_vectors.json. +// +// Each selected record is new, an override of a compiled record, or unchanged (identical to the compiled record, so +// left out); policies for compiled types become {"disable": ...} entries. A record with data but no compiled +// decoder is decoded on the device from its attributes (firmware with runtime decoding) unless it has custom decode +// code, which needs the code interpreter. The device's own check (devman/typefile/check) decides what it accepts. +// +// Rob Dobson 2026 +// (C) 2026 All rights reserved +// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +import { DevTypeJson, DevTypeJsonNumber, DevTypeJsonObject, cloneDevTypeJson, compareCodePoints, devTypeJsonGet, + isDevTypeJsonObject, writeDevTypeJson } from "./RaftDeviceTypeJson"; +import { compileDeviceTypeCommand } from "./RaftDeviceTypeGrammar"; + +export const TYPE_FILE_MAX_BYTES_DEFAULT = 65536; + +// Policies for a device type (as typePolicy, DeviceTypeSources.json policy) +export type DeviceTypePolicyName = "enabled" | "disabled" | "disabledClaim"; +const POLICY_VALUES: Record = { + enabled: undefined, + disabled: true, + disabledClaim: "claim", +}; + +export class DeviceTypeFileError extends Error {} + +export interface TypeFileBuildEntry { + key: string; + kind: "new" | "override" | "unchanged" | "policy"; + policy?: DeviceTypePolicyName; // kind "policy" + // A record with its own initCode: "runtime" (only firmware with the code interpreter runs it) or + // "runtime or compiled" (firmware without it uses the compiled record's) + initCode?: "runtime" | "runtime or compiled"; + noDecoder?: boolean; // has data but no compiled decoder + decoder?: "runtime" | "none"; // then: decoded on the device from its attributes, or not (custom code) +} + +export interface TypeFileBuildInput { + records: Map; // key -> record selected for the file, in key order + policyOnly?: Map; // policies for compiled types that aren't in the file + provenance?: DevTypeJson; // written as the file's _sources (ignored by the device) +} + +export interface TypeFileBuildResult { + text: string; + bytes: number; + crc: string; // as devman/typefile and filehash report it + entries: TypeFileBuildEntry[]; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// CRC +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// CRC16-CCITT as the firmware's devman/typefile and filehash report it (DeviceTypeFileLoader::crc16) +export function crc16Ccitt(data: ArrayLike): number { + let crc = 0xffff; + for (let i = 0; i < data.length; i++) { + crc ^= data[i] << 8; + for (let bit = 0; bit < 8; bit++) + crc = crc & 0x8000 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff; + } + return crc; +} + +// A file's CRC as the device reports it ("%04x" of the CRC16-CCITT of its UTF-8 bytes) +export function typeFileCrc(text: string | Uint8Array): string { + const bytes = typeof text === "string" ? new TextEncoder().encode(text) : text; + return crc16Ccitt(bytes).toString(16).padStart(4, "0"); +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Record comparisons (as the Python) +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +function withoutTopLevel(record: DevTypeJson, ...keys: string[]): DevTypeJson { + const copy = cloneDevTypeJson(record); + if (isDevTypeJsonObject(copy)) + keys.forEach((k) => copy.delete(k)); + return copy; +} + +function comparable(record: DevTypeJson): string { + return writeDevTypeJson(withoutTopLevel(record, "_notes", "deviceType"), { sortKeys: true }); +} + +// The response layout as the firmware compares it (DeviceTypeRecords::sameResponseLayout): resp as compact JSON +function responseLayout(record: DevTypeJson): string | null { + const resp = devTypeJsonGet(record, "devInfoJson", "resp"); + return resp === undefined ? null : writeDevTypeJson(resp); +} + +function stringMember(record: DevTypeJson | undefined, key: string): string { + const value = devTypeJsonGet(record, key); + return typeof value === "string" ? value : ""; +} + +// [names a record's initCode computes, value slot and guard names its initValues uses] +export function deviceTypeInitValueNames(record: DevTypeJson | undefined): [Set, Set] { + const initCode = devTypeJsonGet(record, "initCode"); + const code = isDevTypeJsonObject(initCode) ? stringMember(initCode, "c") : (typeof initCode === "string" ? initCode : ""); + const computed = new Set(); + const computedRe = /\binit\.([A-Za-z_][A-Za-z0-9_]*)/g; + for (let m = computedRe.exec(code); m !== null; m = computedRe.exec(code)) + computed.add(m[1]); + const slots = new Set(); + for (const step of compileDeviceTypeCommand(stringMember(record, "initValues"), "init").steps) { + step.slots.forEach((slot) => slots.add(slot.name)); + if (step.guard) + slots.add(step.guard); + } + return [computed, slots]; +} + +const sortedNames = (names: Iterable) => Array.from(names).sort(compareCodePoints); + +function hasInitCode(record: DevTypeJson): boolean { + const initCode = devTypeJsonGet(record, "initCode"); + // Python's truth value: an empty string, object or array is false + if (initCode === undefined || initCode === null || initCode === false || initCode === "") + return false; + if (isDevTypeJsonObject(initCode)) + return initCode.size > 0; + if (Array.isArray(initCode)) + return initCode.length > 0; + if (initCode instanceof DevTypeJsonNumber) + return initCode.value !== 0; + return true; +} + +function hasData(record: DevTypeJson): boolean { + const b = devTypeJsonGet(record, "devInfoJson", "resp", "b"); + return b instanceof DevTypeJsonNumber && b.value > 0; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Build +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// A device type file from the records selected for it, against the records compiled into the firmware +export function buildTypeFile(input: TypeFileBuildInput, compiled: Map, + options: { maxBytes?: number } = {}): TypeFileBuildResult { + const maxBytes = options.maxBytes ?? TYPE_FILE_MAX_BYTES_DEFAULT; + const entries: TypeFileBuildEntry[] = []; + const devTypes: DevTypeJsonObject = new Map(); + for (const [key, record] of input.records) { + const compiledRecord = compiled.get(key); + // Init values ({name} value slots, {?name} guards) need init code: the record's own initCode (run by the + // code interpreter in firmware built with RAFT_RUNTIME_DECODE_CODE), or a compiled record of the same key + // whose initCode computes them (firmware without the interpreter uses that) + const [ownComputes, slots] = deviceTypeInitValueNames(record); + const hasOwnCode = hasInitCode(record); + const compiledComputes = compiledRecord !== undefined ? deviceTypeInitValueNames(compiledRecord)[0] : new Set(); + if (hasOwnCode) { + const missing = sortedNames(Array.from(slots).filter((s) => !ownComputes.has(s))); + if (missing.length > 0) + throw new DeviceTypeFileError(`typeFile "${key}": initValues uses ${missing.join(", ")}, which its initCode doesn't compute`); + } else if (slots.size > 0) { + const missing = sortedNames(Array.from(slots).filter((s) => !compiledComputes.has(s))); + if (missing.length > 0 || compiledComputes.size === 0) + throw new DeviceTypeFileError(`typeFile "${key}": init values ({name} value slots, {?name} guards) need init code - ` + + `initCode in the record (needs firmware with RAFT_RUNTIME_DECODE_CODE) or a compiled ${key} ` + + `whose initCode computes ${missing.length > 0 ? missing.join(", ") : "them"}`); + } + const compiledCoversSlots = compiledComputes.size > 0 && Array.from(slots).every((s) => compiledComputes.has(s)); + const initCodeRuntime = hasOwnCode && !compiledCoversSlots; + const withData = hasData(record); + if (compiledRecord !== undefined && comparable(record) === comparable(compiledRecord)) { + entries.push({ key, kind: "unchanged" }); + continue; + } + let noDecoder: boolean; + let kind: TypeFileBuildEntry["kind"]; + if (compiledRecord !== undefined) { + noDecoder = withData && responseLayout(record) !== responseLayout(compiledRecord); + kind = "override"; + } else { + noDecoder = withData; + kind = "new"; + } + const entry: TypeFileBuildEntry = { key, kind }; + if (hasOwnCode) + entry.initCode = initCodeRuntime ? "runtime" : "runtime or compiled"; + if (noDecoder) { + entry.noDecoder = true; + const resp = devTypeJsonGet(record, "devInfoJson", "resp"); + entry.decoder = isDevTypeJsonObject(resp) && resp.has("c") ? "none" : "runtime"; + } + entries.push(entry); + devTypes.set(key, withoutTopLevel(record, "_notes")); + } + for (const [key, policy] of input.policyOnly ?? new Map()) { + if (!compiled.has(key)) + throw new DeviceTypeFileError(`typeFile.policy "${key}": not a selected record or a compiled one`); + const value = POLICY_VALUES[policy]; + if (value === undefined) + throw new DeviceTypeFileError(`typeFile.policy "${key}": a type file can't enable a compiled type (only disable it)`); + devTypes.set(key, new Map([["disable", value]])); + entries.push({ key, kind: "policy", policy }); + } + const content: DevTypeJsonObject = new Map(); + const sortedDevTypes: DevTypeJsonObject = new Map(); + Array.from(devTypes.keys()).sort(compareCodePoints).forEach((k) => sortedDevTypes.set(k, devTypes.get(k) as DevTypeJson)); + content.set("devTypes", sortedDevTypes); + if (input.provenance !== undefined && input.provenance !== null) + content.set("_sources", input.provenance); + const text = writeDevTypeJson(content); + if (text.length > maxBytes) + throw new DeviceTypeFileError(`the type file is ${text.length} bytes, more than the ${maxBytes} byte limit`); + return { text, bytes: text.length, crc: typeFileCrc(text), entries }; +} diff --git a/src/RaftDeviceTypeFileManager.test.ts b/src/RaftDeviceTypeFileManager.test.ts new file mode 100644 index 0000000..3a88cf6 --- /dev/null +++ b/src/RaftDeviceTypeFileManager.test.ts @@ -0,0 +1,173 @@ +// The device type file workflow (push: upload, check, confirm, apply, restart, verify; pull; the type table and +// records) against a fake device that keeps files and answers devman/typefile as the firmware does + +import { RaftFileDownloadResult } from "./RaftTypes"; +import { DeviceTypeFileConnection, DeviceTypeFileManager, TYPE_FILE_STAGED_PATH } from "./RaftDeviceTypeFileManager"; +import { typeFileCrc } from "./RaftDeviceTypeFile"; +import { writeDevTypeJson } from "./RaftDeviceTypeJson"; + +const TYPE_FILE = "/local/devlib.json"; + +class FakeDevice implements DeviceTypeFileConnection { + files = new Map(); + loadedCrc = "0000"; // of the file loaded at startup + loadedEntries: Array<{ key: string; outcome: string }> = []; + rejectKeys = new Set(); // entries the device's check rejects + restartingFor = 0; // requests that fail while restarting + commands: string[] = []; + records: Record = { MCP9808: { addresses: "0x18-0x1f", pollInfo: { c: "0x05=r2", i: 5000, s: 1 } } }; + + private checkOf(path: string) { + const text = this.files.get(path); + if (text === undefined) + return { rslt: "fail", error: "failFileNotFound" }; + const keys = Object.keys(JSON.parse(text).devTypes); + const entries = keys.map((key) => ({ key, outcome: this.rejectKeys.has(key) ? "rejected" : "added" })); + const rejected = entries.filter((e) => e.outcome === "rejected").length; + return { rslt: "ok", file: path, crc: typeFileCrc(text), valid: rejected === 0, added: keys.length - rejected, + rejected, firstError: rejected ? `${entries.find((e) => e.outcome === "rejected")?.key}: bad` : "", entries }; + } + + restart() { + const text = this.files.get(TYPE_FILE); + this.loadedCrc = text === undefined ? "0000" : typeFileCrc(text); + this.loadedEntries = text === undefined ? [] : (this.checkOf(TYPE_FILE).entries ?? []); + this.restartingFor = 2; + } + + async sendRICRESTMsg(command: string): Promise { + this.commands.push(command); + if (this.restartingFor > 0) { + this.restartingFor--; + throw new Error("no connection"); + } + const [base, query] = command.split("?"); + const params = new URLSearchParams(query ?? ""); + if (base === "devman/typefile") + return { rslt: "ok", file: TYPE_FILE, state: this.files.has(TYPE_FILE) ? "applied" : "none", crc: this.loadedCrc, + rejected: 0, entries: params.get("entries") ? this.loadedEntries : undefined }; + if (base === "devman/typefile/check") + return this.checkOf(params.get("file") ?? TYPE_FILE); + if (base === "devman/typefile/apply") { + const file = params.get("file") ?? TYPE_FILE; + const check = this.checkOf(file); + if (!check.valid) + return { ...check, rslt: "fail", error: "failCheck" }; + this.files.set(TYPE_FILE, this.files.get(file) as string); + this.files.delete(file); + this.restart(); + return { rslt: "ok", restarting: true }; + } + if (base.startsWith("filedelete/")) { + this.files.delete("/" + base.slice("filedelete/".length)); + return { rslt: "ok" }; + } + if (base === "devman/types") { + const types = [{ dtIdx: 0, key: "MCP9808", src: "base", policy: "enabled" }, + { dtIdx: 1, key: "MAX17048", src: "base", policy: "enabled", protected: true }]; + const offset = Number(params.get("offset")); + return { rslt: "ok", total: types.length, generation: 1, types: types.slice(offset, offset + 50) }; + } + if (base === "devman/typeinfo") { + const key = params.get("deviceTypeKey") as string; + return key in this.records ? { rslt: "ok", record: { [key]: this.records[key] } } : { rslt: "fail", error: "failTypeNotFound" }; + } + return { rslt: "fail", error: "failUnknownAPI" }; + } + + async sendFile(fileName: string, contents: Uint8Array): Promise { + this.files.set("/" + fileName, new TextDecoder().decode(contents)); + return true; + } + + async fsGetContents(fileName: string): Promise { + const text = this.files.get("/" + fileName); + return text === undefined ? new RaftFileDownloadResult() : new RaftFileDownloadResult(new TextEncoder().encode(text)); + } +} + +const fileText = (keys: string[]) => JSON.stringify({ devTypes: Object.fromEntries(keys.map((k) => [k, { addresses: "0x40" }])) }); + +describe("DeviceTypeFileManager", () => { + test("push: upload, check, apply, restart and verify", async () => { + const device = new FakeDevice(); + const manager = new DeviceTypeFileManager(device); + const stages: string[] = []; + const text = fileText(["Gadget", "Widget"]); + const result = await manager.push(text, { onProgress: (s) => { if (stages[stages.length - 1] !== s) stages.push(s); }, + restartTimeoutMs: 10000, restartPollMs: 50 }); + expect(result.outcome).toBe("applied"); + expect(result.crc).toBe(typeFileCrc(text)); + expect(device.files.get(TYPE_FILE)).toBe(text); + expect(device.files.has(TYPE_FILE_STAGED_PATH)).toBe(false); + expect(stages).toEqual(["status", "upload", "check", "apply", "restart", "verify", "done"]); + // Pushed again: the device has it + expect((await manager.push(text)).outcome).toBe("unchangedOnDevice"); + }, 20000); + + test("push: a rejected entry stops it; a dry run and an unconfirmed push remove the staged file", async () => { + const device = new FakeDevice(); + const manager = new DeviceTypeFileManager(device); + device.rejectKeys.add("Bad"); + const invalid = await manager.push(fileText(["Bad", "Good"])); + expect(invalid.outcome).toBe("invalid"); + expect(invalid.message).toContain("Bad: bad"); + expect(device.files.has(TYPE_FILE)).toBe(false); + const dry = await manager.push(fileText(["Good"]), { dryRun: true }); + expect(dry.outcome).toBe("dryRun"); + expect(dry.check?.entries).toEqual([{ key: "Good", outcome: "added" }]); + expect(device.files.has(TYPE_FILE_STAGED_PATH)).toBe(false); + const unconfirmed = await manager.push(fileText(["Good"]), { confirm: async () => false }); + expect(unconfirmed.outcome).toBe("notConfirmed"); + expect(device.files.size).toBe(0); + }); + + test("push: stops if the device's file changed since it was read", async () => { + const device = new FakeDevice(); + device.files.set(TYPE_FILE, fileText(["Old"])); + const manager = new DeviceTypeFileManager(device); + const pulled = await manager.pull(); + expect(pulled?.crc).toBe(typeFileCrc(fileText(["Old"]))); + device.files.set(TYPE_FILE, fileText(["Old", "Changed"])); + const result = await manager.push(fileText(["New"]), { expectedDeviceCrc: pulled?.crc }); + expect(result.outcome).toBe("deviceFileChanged"); + expect((await manager.push(fileText(["New"]), { expectedDeviceCrc: pulled?.crc, force: true, restartTimeoutMs: 10000, restartPollMs: 50 })).outcome) + .toBe("applied"); + }, 20000); + + test("push: the device not back in time", async () => { + const device = new FakeDevice(); + device.restart = function () { this.restartingFor = 1000; }; + const manager = new DeviceTypeFileManager(device); + const result = await manager.push(fileText(["Gadget"]), { restartTimeoutMs: 500, restartPollMs: 50 }); + expect(result.outcome).toBe("restartTimeout"); + }, 10000); + + test("verify: what the device loaded against the check", () => { + const check = { entries: [{ key: "A", outcome: "added" as const }] }; + expect(DeviceTypeFileManager.verify({ rslt: "ok", crc: "1234", state: "applied", entries: [{ key: "A", outcome: "added" }] }, check, "1234")) + .toBeUndefined(); + expect(DeviceTypeFileManager.verify({ rslt: "ok", crc: "1234", state: "applied", entries: [{ key: "A", outcome: "rejected" }] }, check, "1234")) + .toContain("the check said added"); + expect(DeviceTypeFileManager.verify({ rslt: "ok", crc: "9999", state: "applied" }, check, "1234")).toContain("CRC 9999"); + expect(DeviceTypeFileManager.verify({ rslt: "ok", crc: "1234", state: "applied", rejected: 1, firstError: "A: bad" }, check, "1234")) + .toContain("A: bad"); + }); + + test("table, device records and the type file's _sources", async () => { + const device = new FakeDevice(); + const manager = new DeviceTypeFileManager(device); + const table = await manager.table(); + expect(table?.map((t) => [t.key, t.protected ?? false])).toEqual([["MCP9808", false], ["MAX17048", true]]); + const record = await manager.deviceRecord("MCP9808"); + expect(writeDevTypeJson(record ?? null)).toBe('{"addresses":"0x18-0x1f","pollInfo":{"c":"0x05=r2","i":5000,"s":1}}'); + expect(await manager.deviceRecord("Nope")).toBeUndefined(); + const typeinfoCalls = device.commands.filter((c) => c.startsWith("devman/typeinfo")).length; + await manager.deviceRecord("MCP9808"); + expect(device.commands.filter((c) => c.startsWith("devman/typeinfo")).length).toBe(typeinfoCalls); + const parsed = DeviceTypeFileManager.parseTypeFile('{"devTypes":{"A":{"disable":true}},"_sources":{"formatVersion":1}}'); + expect(Array.from(parsed.devTypes.keys())).toEqual(["A"]); + expect(writeDevTypeJson(parsed.sources ?? null)).toBe('{"formatVersion":1}'); + expect(() => DeviceTypeFileManager.parseTypeFile("{}")).toThrow("no devTypes"); + }); +}); diff --git a/src/RaftDeviceTypeFileManager.ts b/src/RaftDeviceTypeFileManager.ts new file mode 100644 index 0000000..74f2375 --- /dev/null +++ b/src/RaftDeviceTypeFileManager.ts @@ -0,0 +1,264 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// RaftDeviceTypeFileManager +// Part of RaftJS +// +// A device's device type file (RaftCore wiki: DeviceManager Settings, "Device type file"; DeviceManager REST API, +// devman/typefile): its status, its type table and compiled records, pulling the file, and pushing a new one the +// safe way - upload under another name, the device's check, apply (the device restarts), then verify that the device +// loaded it (as raft devtypes push does). +// +// Rob Dobson 2026 +// (C) 2026 All rights reserved +// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +import { RaftFileDownloadResult, RaftOKFail } from "./RaftTypes"; +import { DevManTypeFileCheck, DevManTypeFileEntry, DevManTypeFileStatus, DevManTypePolicy } from "./RaftDeviceManager"; +import { DevTypeJson, devTypeJsonFromPlain, devTypeJsonGet, isDevTypeJsonObject, parseDevTypeJson } from "./RaftDeviceTypeJson"; +import { typeFileCrc } from "./RaftDeviceTypeFile"; + +// What a connection needs to offer (RaftConnector has these) +export interface DeviceTypeFileConnection { + sendRICRESTMsg(commandName: string, params: object): Promise; + sendFile(fileName: string, fileContents: Uint8Array, + progressCallback: ((sent: number, total: number, progress: number) => void) | undefined): Promise; + fsGetContents(fileName: string, fileSource: string, + progressCallback: ((received: number, total: number) => void) | undefined): Promise; +} + +// An entry of the device's type table (devman/types) +export interface DevManTypeTableEntry { + dtIdx: number; + key: string; + src: "base" | "override" | "extended" | string; + policy: DevManTypePolicy; + protected?: boolean; // a type a system device expects: can't be overridden or disabled (firmware from 2026-10-24) +} + +// The staged file: uploaded here (on the local file system, whatever the default file system) and checked before +// it is applied (apply moves or copies it to the device type file's path) +export const TYPE_FILE_STAGED_UPLOAD_NAME = "local/devlib.new.json"; +export const TYPE_FILE_STAGED_PATH = "/local/devlib.new.json"; + +export type TypeFileResponse = RaftOKFail & T & { error?: string }; + +export interface TypeFilePushOptions { + // The device's file is expected to be this one (its CRC when it was pulled or last pushed): if it has changed + // since - e.g. by devman/typepolicy save=1 - the push stops unless force is set + expectedDeviceCrc?: string; + force?: boolean; + dryRun?: boolean; // stop after the device's check (and remove the staged file) + confirm?: (check: TypeFileResponse) => Promise; // after the check, before applying + onProgress?: (stage: TypeFilePushStage, detail?: string) => void; + restartTimeoutMs?: number; // how long to wait for the device to come back (default 60 s) + restartPollMs?: number; // how often to ask meanwhile (default 1.5 s) +} + +export type TypeFilePushStage = "status" | "upload" | "check" | "confirm" | "apply" | "restart" | "verify" | "done"; + +export interface TypeFilePushResult { + outcome: "applied" | "unchangedOnDevice" | "deviceFileChanged" | "uploadFailed" | "checkFailed" | "invalid" | + "dryRun" | "notConfirmed" | "applyFailed" | "restartTimeout" | "verifyFailed"; + message: string; + crc: string; // of the file pushed + check?: TypeFileResponse; + status?: TypeFileResponse; // after the restart +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export class DeviceTypeFileManager { + // Compiled records fetched (typeinfo full=1), for the table generation they were fetched in + private _compiledCache = new Map(); + private _compiledGeneration: number | undefined = undefined; + + constructor(private _conn: DeviceTypeFileConnection) {} + + private async cmd(command: string): Promise | undefined> { + try { + return await this._conn.sendRICRESTMsg(command, {}) as TypeFileResponse; + } catch { + return undefined; + } + } + + // devman/typefile (entries: what happened to each entry at startup) + async status(entries = false): Promise | undefined> { + return this.cmd(`devman/typefile${entries ? "?entries=1" : ""}`); + } + + // The whole device type table (devman/types, every page). Clears the compiled record cache if the table changed + async table(): Promise { + const out: DevManTypeTableEntry[] = []; + for (let offset = 0; ; ) { + const page = await this.cmd<{ total?: number; generation?: number; types?: DevManTypeTableEntry[] }>( + `devman/types?offset=${offset}&count=50`); + if (page?.rslt !== "ok") + return undefined; + if (page.generation !== this._compiledGeneration) { + this._compiledCache.clear(); + this._compiledGeneration = page.generation; + } + const types = page.types ?? []; + out.push(...types); + offset += 50; + if (types.length === 0 || offset >= (page.total ?? 0)) + return out; + } + } + + // A record the device has, as devman/typeinfo full=1 gives it (in the type file format); undefined if it has none + async deviceRecord(key: string): Promise { + if (this._compiledCache.has(key)) + return this._compiledCache.get(key) ?? undefined; + const resp = await this.cmd<{ record?: Record }>( + `devman/typeinfo?deviceTypeKey=${encodeURIComponent(key)}&full=1`); + if (resp === undefined) + return undefined; + const record = resp.rslt === "ok" && resp.record ? resp.record[key] : undefined; + const value = record !== undefined ? devTypeJsonFromPlain(record) : null; + this._compiledCache.set(key, value); + return value ?? undefined; + } + + // The device's type file (undefined if it can't be read - e.g. there is none) + async pull(onProgress?: (received: number, total: number) => void): Promise<{ text: string; crc: string; path: string } | undefined> { + const status = await this.status(); + const path = status?.file; + if (!path) + return undefined; + const result = await this._conn.fsGetContents(path.replace(/^\//, ""), "fs", onProgress); + if (!result.downloadedOk || !result.fileData) + return undefined; + return { text: new TextDecoder().decode(result.fileData), crc: typeFileCrc(result.fileData), path }; + } + + // The records and policies a type file holds, and its _sources if it has one + static parseTypeFile(text: string): { devTypes: Map; sources?: DevTypeJson } { + const content = parseDevTypeJson(text); + const devTypes = devTypeJsonGet(content, "devTypes"); + if (!isDevTypeJsonObject(devTypes)) + throw new Error("not a device type file (no devTypes object)"); + return { devTypes, sources: devTypeJsonGet(content, "_sources") }; + } + + // The CRC of the device's own file as it is now (from a check of it - the status's CRC is of the file loaded at + // startup); undefined if there is no file + async deviceFileCrc(): Promise { + const check = await this.cmd("devman/typefile/check"); + return check?.rslt === "ok" ? check.crc : undefined; + } + + // Upload a file as the staged file + async upload(text: string, onProgress?: (sent: number, total: number) => void): Promise { + return this._conn.sendFile(TYPE_FILE_STAGED_UPLOAD_NAME, new TextEncoder().encode(text), + onProgress ? (s, t) => onProgress(s, t) : undefined); + } + + // Upload a file as the staged file and have the device check it (undefined if the upload failed) + async stage(text: string, onProgress?: (sent: number, total: number) => void): Promise | undefined> { + if (!(await this.upload(text, onProgress))) + return undefined; + return (await this.check(TYPE_FILE_STAGED_PATH)) ?? { rslt: "fail", error: "noResponse" }; + } + + // What a file on the device would do at the next startup, entry by entry (default: the device type file) + async check(file?: string): Promise | undefined> { + return this.cmd(`devman/typefile/check${file ? `?file=${encodeURIComponent(file)}` : ""}`); + } + + // Make the staged file the device type file and restart (refused by the device if anything in it is rejected) + async apply(file: string = TYPE_FILE_STAGED_PATH): Promise | undefined> { + return this.cmd(`devman/typefile/apply?file=${encodeURIComponent(file)}`); + } + + // Remove the staged file + async removeStaged(): Promise { + await this.cmd(`filedelete/${TYPE_FILE_STAGED_UPLOAD_NAME}`); + } + + // Wait for the device to answer with a file of this CRC loaded (after apply restarts it) + async waitForLoaded(crc: string, timeoutMs = 60000, pollMs = 1500): Promise | undefined> { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + await sleep(pollMs); + const status = await this.status(true); + if (status?.rslt === "ok" && status.crc === crc && status.state !== "loading") + return status; + } + return undefined; + } + + // The device's status after a push: the file is loaded, with what the check said of each entry + static verify(status: TypeFileResponse, check: DevManTypeFileCheck | undefined, crc: string): string | undefined { + if (status.crc !== crc) + return `the device's file has CRC ${status.crc}, not ${crc}`; + if (status.state !== "applied" && status.state !== "none") + return `the device's type file state is ${status.state}${status.firstError ? ` (${status.firstError})` : ""}`; + if ((status.rejected ?? 0) > 0) + return `${status.rejected} entr${status.rejected === 1 ? "y was" : "ies were"} rejected: ${status.firstError}`; + const outcome = (entries: DevManTypeFileEntry[] | undefined) => new Map((entries ?? []).map((e) => [e.key, e.outcome])); + const loaded = outcome(status.entries); + for (const [key, expected] of outcome(check?.entries)) { + if (status.entries && loaded.get(key) !== expected) + return `"${key}" was ${loaded.get(key) ?? "not loaded"}, the check said ${expected}`; + } + return undefined; + } + + // Push a type file: stop if the device already has it, or if its file isn't the expected one; upload, check, confirm, + // apply, wait for the restart and verify + async push(text: string, options: TypeFilePushOptions = {}): Promise { + const crc = typeFileCrc(text); + const progress = options.onProgress ?? (() => undefined); + progress("status"); + const deviceCrc = await this.deviceFileCrc(); + if (deviceCrc === crc && !options.force) + return { outcome: "unchangedOnDevice", message: "The device already has this type file", crc }; + if (options.expectedDeviceCrc !== undefined && deviceCrc !== undefined && deviceCrc !== options.expectedDeviceCrc && !options.force) + return { outcome: "deviceFileChanged", crc, + message: `The device's type file (CRC ${deviceCrc}) has changed since it was read (CRC ${options.expectedDeviceCrc})` }; + progress("upload"); + if (!(await this.upload(text, (s, t) => progress("upload", `${s} / ${t} bytes`)))) + return { outcome: "uploadFailed", message: "The upload failed", crc }; + progress("check"); + const check = (await this.check(TYPE_FILE_STAGED_PATH)) ?? { rslt: "fail" as const, error: "noResponse" }; + if (check.rslt !== "ok") + return { outcome: "checkFailed", message: `The device's check failed: ${check.error ?? "no response"}`, crc, check }; + if (check.crc !== undefined && check.crc !== crc) { + await this.removeStaged(); + return { outcome: "checkFailed", message: `The uploaded file has CRC ${check.crc}, not ${crc}`, crc, check }; + } + if (!check.valid) + return { outcome: "invalid", crc, check, + message: `The device rejects ${check.rejected} entr${check.rejected === 1 ? "y" : "ies"}: ${check.firstError}` }; + if (options.dryRun) { + await this.removeStaged(); + return { outcome: "dryRun", message: "Checked by the device; not applied", crc, check }; + } + if (options.confirm) { + progress("confirm"); + if (!(await options.confirm(check))) { + await this.removeStaged(); + return { outcome: "notConfirmed", message: "Not applied", crc, check }; + } + } + progress("apply"); + const applied = await this.apply(TYPE_FILE_STAGED_PATH); + if (applied?.rslt !== "ok") + return { outcome: "applyFailed", crc, check, + message: `Not applied: ${applied?.error ?? "no response"}${applied?.firstError ? ` - ${applied.firstError}` : ""}` }; + progress("restart"); + const status = await this.waitForLoaded(crc, options.restartTimeoutMs ?? 60000, options.restartPollMs ?? 1500); + if (!status) + return { outcome: "restartTimeout", crc, check, + message: "Applied - the device is restarting. Reconnect to it to check it loaded the file" }; + progress("verify"); + const problem = DeviceTypeFileManager.verify(status, check, crc); + if (problem) + return { outcome: "verifyFailed", message: problem, crc, check, status }; + progress("done"); + return { outcome: "applied", message: "Applied and loaded by the device", crc, check, status }; + } +} diff --git a/src/RaftDeviceTypeGrammar.ts b/src/RaftDeviceTypeGrammar.ts new file mode 100644 index 0000000..b54dc52 --- /dev/null +++ b/src/RaftDeviceTypeGrammar.ts @@ -0,0 +1,517 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// RaftDeviceTypeGrammar +// Part of RaftJS +// +// Device type record command grammar: a TypeScript copy of RaftCore's DeviceTypeGrammar.cpp (and its Python copy, +// scripts/DeviceTypeGrammar.py), checked against the same vectors (scripts/tests/grammar_vectors.json). +// +// detectionValues, initValues and pollInfo.c share one syntax, with a different meaning for the value side of a +// step in each. A string is steps separated by "&" or ";"; each step is "write[=value]". compile() gives the steps +// and the issues (errors and warnings) the firmware finds. +// +// Rob Dobson 2026 +// (C) 2026 All rights reserved +// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export type DeviceTypeGrammarField = "detection" | "init" | "poll"; + +export const DEVICE_TYPE_GRAMMAR_ERRORS = ["NotHex", "MultipleEquals", "BadCheck", "BadReadExpr", "ReadExprNotAllowed", + "BadReadLength", "BadValue", "CrcLength", "BadPause", "BadPlaceholder"] as const; +export const DEVICE_TYPE_GRAMMAR_WARNINGS = ["OddHex", "BarePause", "InitValueNotWritten", "InitBinaryIsReadCount", + "InitValueIgnored", "MixedCheckLengths", "BinaryNotWholeBytes"] as const; +export const DEVICE_TYPE_GRAMMAR_FIELD_NAMES: Record = + { detection: "detectionValues", init: "initValues", poll: "pollInfo.c" }; + +export function isDeviceTypeGrammarError(code: string): boolean { + return (DEVICE_TYPE_GRAMMAR_ERRORS as readonly string[]).includes(code); +} + +const HEX_DIGITS = "0123456789abcdefABCDEF"; +const WHITESPACE = " \t\n\v\f\r"; + +export class DeviceTypeGrammarStep { + write: number[] = []; + read = 0; + pause = 0; + hasValue = false; + checks: Array<{ mask: number[]; value: number[] }> = []; + crc8w = false; + expr = false; // read length is an r{...} expression + slots: Array<{ offset: number; name: string }> = []; // init: {name} value slots in the write data + guard = ""; // init: {?name} - the step is only done if that computed value isn't 0 +} + +export interface DeviceTypeGrammarIssue { + code: string; + pos: number; +} + +export class DeviceTypeGrammarResult { + steps: DeviceTypeGrammarStep[] = []; + issues: DeviceTypeGrammarIssue[] = []; + hasErrors(): boolean { + return this.issues.some((issue) => isDeviceTypeGrammarError(issue.code)); + } +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// C library behaviour the C++ relies on +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const isDigit = (c: string | undefined) => c !== undefined && c >= "0" && c <= "9"; +const isSpace = (c: string | undefined) => c !== undefined && c !== "" && WHITESPACE.includes(c); +const isAsciiAlpha = (c: string | undefined) => c !== undefined && /^[A-Za-z]$/.test(c); +const isAsciiAlnum = (c: string | undefined) => c !== undefined && /^[A-Za-z0-9]$/.test(c); + +// strtol/strtoul: skip whitespace, optional sign, digits (base 16 allows 0x). Returns [value, end]; end is pos if +// there are no digits +function strtol(s: string, pos = 0, base = 10): [number, number] { + let i = pos; + while (i < s.length && isSpace(s[i])) + i++; + let sign = 1; + if (i < s.length && (s[i] === "+" || s[i] === "-")) { + sign = s[i] === "-" ? -1 : 1; + i++; + } + if (base === 16 && s.slice(i, i + 2).toLowerCase() === "0x" && i + 2 < s.length && HEX_DIGITS.includes(s[i + 2])) + i += 2; + const digits = base === 10 ? "0123456789" : HEX_DIGITS; + const start = i; + while (i < s.length && digits.includes(s[i])) + i++; + if (i === start) + return [0, pos]; + return [sign * parseInt(s.slice(start, i), base), i]; +} + +function firstPauseMs(lc: string): number { + const pos = lc.indexOf("p"); + if (pos < 0) + return 0; + return Math.max(strtol(lc, pos + 1)[0], 0); +} + +function isPauseToken(s: string): boolean { + return s.length >= 2 && (s[0] === "p" || s[0] === "P") && /^[0-9]+$/.test(s.slice(1)); +} + +function hexToBytes(digits: string): number[] { + const out = new Array(Math.floor((digits.length + 1) / 2)).fill(0); + for (let i = 0; i < digits.length - 1; i += 2) + out[i / 2] = parseInt(digits.slice(i, i + 2), 16); + return out; +} + +// PollReadLenExpr::parse: the :max value, or null if the expression doesn't parse or has no max +function readLenExprMax(expr: string): number | null { + let maxValue = 0xFFFFFFFF; + let p = 0; + const end = expr.length; + if (p >= end || expr[p] !== "$") + return null; + p++; + p = strtol(expr, p)[1]; + if (p < end && expr[p] === "[") { + p++; + p = strtol(expr, p)[1]; + if (p < end && expr[p] === "]") + p++; + else + return null; + } + if (p < end && expr[p] === ".") { + p++; + if (p < end && (expr[p] === "w" || expr[p] === "W")) { + p++; + p = strtol(expr, p)[1]; + } else { + return null; + } + } + while (p < end) { + const c = expr[p]; + if (c === ":") { + p++; + if (expr.startsWith("mask", p)) { + p = strtol(expr, p + 4, 16)[1]; + } else if (expr.startsWith("max", p)) { + const [value, next] = strtol(expr, p + 3); + maxValue = value >>> 0; + p = next; + } else if (expr.startsWith("min", p)) { + p = strtol(expr, p + 3)[1]; + } else if (expr.startsWith("align", p)) { + p = strtol(expr, p + 5)[1]; + } else { + return null; + } + } else if ("*/+-".includes(c)) { + p = strtol(expr, p + 1)[1]; + } else if (c === "&" || c === "|") { + p = strtol(expr, p + 1, 16)[1]; + } else if ((c === ">" || c === "<") && p + 1 < end && expr[p + 1] === c) { + p = strtol(expr, p + 2)[1]; + } else { + return null; + } + } + return maxValue === 0xFFFFFFFF ? null : maxValue; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Compile +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +class Ctx { + constructor(public result: DeviceTypeGrammarResult, public stepPos: number) {} + issue(code: string, posInStep: number) { + this.result.issues.push({ code, pos: this.stepPos + posInStep }); + } +} + +function setPause(step: DeviceTypeGrammarStep, ms: number, ctx: Ctx, pos: number): boolean { + if (ms > 65535) { + ctx.issue("BadPause", pos); + return false; + } + step.pause = ms; + return true; +} + +function isSlotName(name: string): boolean { + return name.length > 0 && (isAsciiAlpha(name[0]) || name[0] === "_") && + Array.from(name).every((c) => isAsciiAlnum(c) || c === "_"); +} + +// [bytes, slots], or null if they aren't hex. In an init string the hex may contain {name} placeholders: each is one +// byte, written with the value the record's initCode computes +function parseWrite(name: string, ctx: Ctx, field: DeviceTypeGrammarField): [number[], Array<{ offset: number; name: string }>] | null { + const start = name.length >= 2 && name[0] === "0" && (name[1] === "x" || name[1] === "X") ? 2 : 0; + const digits = name.slice(start); + if (start === 2 && digits === "") { + ctx.issue("NotHex", 2); + return null; + } + const out: number[] = []; + const slots: Array<{ offset: number; name: string }> = []; + let hexRun = ""; + let i = 0; + while (i < digits.length) { + const c = digits[i]; + if (c === "{") { + const end = digits.indexOf("}", i + 1); + const slotName = end > 0 ? digits.slice(i + 1, end) : ""; + // A placeholder: only in init strings, a name, and at a byte boundary + if (field !== "init" || end < 0 || !isSlotName(slotName) || hexRun.length % 2 !== 0) { + ctx.issue("BadPlaceholder", start + i); + return null; + } + out.push(...hexToBytes(hexRun)); + hexRun = ""; + slots.push({ offset: out.length, name: slotName }); + out.push(0); + i = end + 1; + continue; + } + if (!HEX_DIGITS.includes(c)) { + ctx.issue("NotHex", start + i); + return null; + } + hexRun += c; + i++; + } + if (hexRun.length % 2 !== 0) + ctx.issue("OddHex", 0); + out.push(...hexToBytes(hexRun)); + return [out, slots]; +} + +type CheckKind = "read" | "hex" | "binary" | "pause" | null; + +function parseCheckValue(lc: string, ctx: Ctx, pos: number): [CheckKind, number[], number[], number] { + const rPos = lc.indexOf("r"); + if (rPos >= 0) { + const length = strtol(lc, rPos + 1)[0]; + if (length <= 0 || length > 65535) { + ctx.issue("BadReadLength", pos + rPos); + return [null, [], [], 0]; + } + const zeros = new Array(length).fill(0); + return ["read", zeros, zeros.slice(), length]; + } + let idx = 0; + while (idx < lc.length && isSpace(lc[idx])) + idx++; + if (idx < lc.length && lc[idx] === "p") { + idx++; + while (idx < lc.length && isDigit(lc[idx])) + idx++; + while (idx < lc.length && isSpace(lc[idx])) + idx++; + } + if (lc.startsWith("0x", idx)) { + const digits = lc.slice(idx + 2); + for (let i = 0; i < digits.length; i++) { + if (!HEX_DIGITS.includes(digits[i])) { + ctx.issue("BadCheck", pos + idx + 2 + i); + return [null, [], [], 0]; + } + } + if (digits.length % 2 !== 0) + ctx.issue("OddHex", pos + idx); + const value = hexToBytes(digits); + return ["hex", new Array(value.length).fill(0xff), value, value.length]; + } + if (lc.startsWith("0b", idx)) { + const bits = lc.slice(idx + 2); + for (let i = 0; i < bits.length; i++) { + if (!"01x".includes(bits[i])) { + ctx.issue("BadCheck", pos + idx + 2 + i); + return [null, [], [], 0]; + } + } + if (bits.length % 8 !== 0) + ctx.issue("BinaryNotWholeBytes", pos + idx); + const numBytes = Math.floor((bits.length + 7) / 8); + const mask = new Array(numBytes).fill(0xff); + const value = new Array(numBytes).fill(0); + for (let i = 0; i < bits.length; i++) { + const bit = 0x80 >> (i % 8); + if (bits[i] === "x") + mask[Math.floor(i / 8)] &= ~bit & 0xff; + else if (bits[i] === "1") + value[Math.floor(i / 8)] |= bit; + } + return ["binary", mask, value, numBytes]; + } + let i = 0; + while (i < lc.length) { + if (lc[i] === "p") { + while (i + 1 < lc.length && isDigit(lc[i + 1])) + i++; + i++; + continue; + } + if (!isSpace(lc[i])) { + ctx.issue("BadCheck", pos + i); + return [null, [], [], 0]; + } + i++; + } + return ["pause", [], [], 0]; +} + +function parseDetectionValue(value: string, valuePos: number, step: DeviceTypeGrammarStep, ctx: Ctx): boolean { + let lc = value.toLowerCase(); + const suffix = ":crc8w"; + if (lc.endsWith(suffix)) { + step.crc8w = true; + lc = lc.slice(0, -suffix.length); + } + if (!setPause(step, firstPauseMs(lc), ctx, valuePos)) + return false; + let start = 0; + while (start < lc.length) { + let comma = lc.indexOf(",", start); + if (comma < 0) + comma = lc.length; + const [kind, mask, val] = parseCheckValue(lc.slice(start, comma), ctx, valuePos + start); + if (kind === null) + return false; + if (step.checks.length > 0 && val.length !== step.checks[0].value.length) + ctx.issue("MixedCheckLengths", valuePos + start); + step.checks.push({ mask, value: val }); + start = comma + 1; + } + step.read = step.checks.length > 0 ? step.checks[0].value.length : 0; + if (step.crc8w && (step.read === 0 || step.read % 3 !== 0)) { + ctx.issue("CrcLength", valuePos); + return false; + } + return true; +} + +function parseInitValue(value: string, valuePos: number, step: DeviceTypeGrammarStep, ctx: Ctx): boolean { + const lc = value.toLowerCase(); + if (!setPause(step, firstPauseMs(lc), ctx, valuePos)) + return false; + if (lc === "") + return true; + if (lc[0] === "r") { + step.read = Math.max(strtol(lc, 1)[0], 0) & 0xffff; + } else if (lc.startsWith("0b")) { + step.read = Math.floor((lc.length - 2 + 7) / 8); + ctx.issue("InitBinaryIsReadCount", valuePos); + return true; + } else if (lc.startsWith("0x")) { + ctx.issue("InitValueNotWritten", valuePos); + return true; + } + let i = 0; + let valid = true; + if (i < lc.length && lc[i] === "r") { + i++; + const digitsStart = i; + while (i < lc.length && isDigit(lc[i])) + i++; + valid = i > digitsStart; + } + if (valid && i < lc.length) + valid = isPauseToken(lc.slice(i)); + if (!valid) + ctx.issue("InitValueIgnored", valuePos); + return true; +} + +function parsePollValue(value: string, valuePos: number, step: DeviceTypeGrammarStep, ctx: Ctx): boolean { + const dynStart = value.indexOf("r{"); + if (dynStart >= 0) { + const dynEnd = value.indexOf("}", dynStart + 2); + if (dynEnd < 0) { + ctx.issue("BadReadExpr", valuePos + dynStart); + return false; + } + const maxValue = readLenExprMax(value.slice(dynStart + 2, dynEnd)); + if (maxValue === null) { + ctx.issue("BadReadExpr", valuePos + dynStart); + return false; + } + step.expr = true; + step.read = maxValue & 0xffff; + return setPause(step, firstPauseMs(value.toLowerCase()), ctx, valuePos); + } + const lc = value.toLowerCase(); + const issuesBefore = ctx.result.issues.length; + const [kind, , , readLen] = parseCheckValue(lc, ctx, valuePos); + if (kind === null) { + for (let i = issuesBefore; i < ctx.result.issues.length; i++) { + if (ctx.result.issues[i].code === "BadCheck") + ctx.result.issues[i] = { code: "BadValue", pos: ctx.result.issues[i].pos }; + } + return false; + } + step.read = readLen & 0xffff; + return setPause(step, firstPauseMs(lc), ctx, valuePos); +} + +const STRIP_CHARS = " \t\r\n"; +const lstrip = (s: string) => { + let i = 0; + while (i < s.length && STRIP_CHARS.includes(s[i])) + i++; + return s.slice(i); +}; +const strip = (s: string) => { + let end = s.length; + while (end > 0 && STRIP_CHARS.includes(s[end - 1])) + end--; + return lstrip(s.slice(0, end)); +}; + +// Compile a command string +export function compileDeviceTypeCommand(commandStr: string | undefined | null, field: DeviceTypeGrammarField): DeviceTypeGrammarResult { + const result = new DeviceTypeGrammarResult(); + const s = commandStr ?? ""; + let startPos = 0; + let detectionInvalid = false; + while (startPos < s.length) { + const sep = s.slice(startPos).search(/[&;]/); + const endPos = sep >= 0 ? startPos + sep : s.length; + const stepStr = s.slice(startPos, endPos); + const ctx = new Ctx(result, startPos); + const eqPos = stepStr.indexOf("="); + let name = eqPos < 0 ? stepStr : stepStr.slice(0, eqPos); + const stripped = strip(name); + let namePos = stripped ? name.length - lstrip(name).length : 0; + name = stripped; + const step = new DeviceTypeGrammarStep(); + let stepValid = true; + // Guard {?name} at the start of an init step + if (name.startsWith("{?")) { + const end = name.indexOf("}"); + const guardName = end >= 0 ? name.slice(2, end) : ""; + if (field !== "init" || !isSlotName(guardName)) { + ctx.issue("BadPlaceholder", namePos); + stepValid = false; + } else { + step.guard = guardName; + const rest = name.slice(end + 1); + const strippedRest = lstrip(rest); + namePos += strippedRest ? end + 1 + rest.length - strippedRest.length : name.length; + name = strippedRest; + } + } + if (!stepValid) { + // bad guard (reported above) + } else if (eqPos < 0 && isPauseToken(name)) { + ctx.issue("BarePause", namePos); + stepValid = setPause(step, strtol(name, 1)[0], ctx, namePos); + } else { + const write = parseWrite(name, new Ctx(result, startPos + namePos), field); + if (write === null) { + stepValid = false; + } else { + [step.write, step.slots] = write; + if (eqPos >= 0) { + step.hasValue = true; + const value = stepStr.slice(eqPos + 1); + const valuePos = eqPos + 1; + if (value.includes("=")) { + ctx.issue("MultipleEquals", valuePos + value.indexOf("=")); + stepValid = false; + } else if (field === "detection") { + stepValid = parseDetectionValue(value, valuePos, step, ctx); + } else if (field === "init") { + if (value.includes("r{")) { + ctx.issue("ReadExprNotAllowed", valuePos + value.indexOf("r{")); + stepValid = false; + } else { + stepValid = parseInitValue(value, valuePos, step, ctx); + } + } else { + stepValid = parsePollValue(value, valuePos, step, ctx); + } + } + } + } + if (stepValid) + result.steps.push(step); + else + detectionInvalid = true; + startPos = endPos + 1; + } + if (field === "detection" && detectionInvalid) + result.steps = []; + return result; +} + +// Strength of a compiled detection string: the bits it compares (for each step, the fewest bits any of its +// alternatives compares, plus 8 for each CRC byte checked by :crc8w); -1 if it has errors (it never matches) +export function deviceTypeDetectionStrength(result: DeviceTypeGrammarResult): number { + if (result.hasErrors()) + return -1; + let strength = 0; + const popcount = (b: number) => { let n = 0; for (; b; b >>= 1) n += b & 1; return n; }; + for (const step of result.steps) { + const bits = step.checks.map((c) => c.mask.reduce((sum, b) => sum + popcount(b), 0)); + if (bits.length > 0 && Math.min(...bits) > 0) + strength += Math.min(...bits); + if (step.crc8w) + strength += 8 * Math.floor(step.read / 3); + } + return strength; +} + +// Sensirion CRC-8 (poly 0x31, init 0xff) +export function deviceTypeCrc8(data: ArrayLike): number { + let crc = 0xff; + for (let i = 0; i < data.length; i++) { + crc ^= data[i]; + for (let bit = 0; bit < 8; bit++) + crc = crc & 0x80 ? ((crc << 1) ^ 0x31) & 0xff : (crc << 1) & 0xff; + } + return crc; +} diff --git a/src/RaftDeviceTypeJson.ts b/src/RaftDeviceTypeJson.ts new file mode 100644 index 0000000..17c9f56 --- /dev/null +++ b/src/RaftDeviceTypeJson.ts @@ -0,0 +1,316 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// RaftDeviceTypeJson +// Part of RaftJS +// +// JSON for device type records as RaftCore's Python tools read and write it, so a type file built here is byte for +// byte the one AssembleDeviceTypes.py builds (and the device's CRC of it agrees): +// - objects keep their keys in the order written (JSON.parse moves integer-like keys such as "26" ahead of "12.5"), +// and a repeated key keeps its first place with its last value (as Python's OrderedDict); +// - numbers keep the text they were written with; they are written as Python writes them (an integer as an +// integer, other numbers as Python's float repr - 16.0, 1.25e-07, 1e+16); +// - strings are written with every character outside printable ASCII escaped (Python's ensure_ascii). +// +// Rob Dobson 2026 +// (C) 2026 All rights reserved +// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// A number as written in the JSON text +export class DevTypeJsonNumber { + constructor(public readonly text: string) {} + get value(): number { + return Number(this.text); + } + // Python reads a number with no fraction or exponent as an int, any other as a float + get isInteger(): boolean { + return /^-?\d+$/.test(this.text); + } +} + +export type DevTypeJson = null | boolean | string | DevTypeJsonNumber | DevTypeJson[] | DevTypeJsonObject; +export type DevTypeJsonObject = Map; + +export class DevTypeJsonError extends Error {} + +export function isDevTypeJsonObject(value: DevTypeJson | undefined): value is DevTypeJsonObject { + return value instanceof Map; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Parsing +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export function parseDevTypeJson(text: string): DevTypeJson { + let pos = 0; + const fail = (what: string): never => { + throw new DevTypeJsonError(`${what} at ${pos}`); + }; + const skipSpace = () => { + while (pos < text.length && " \t\n\r".includes(text[pos])) + pos++; + }; + const parseString = (): string => { + const start = pos; + pos++; + while (pos < text.length && text[pos] !== '"') { + if (text[pos] === "\\") + pos++; + pos++; + } + if (pos >= text.length) + fail("unterminated string"); + pos++; + try { + return JSON.parse(text.slice(start, pos)) as string; + } catch { + pos = start; + return fail("invalid string"); + } + }; + const parseValue = (): DevTypeJson => { + skipSpace(); + const c = text[pos]; + if (c === "{") { + pos++; + const obj: DevTypeJsonObject = new Map(); + skipSpace(); + if (text[pos] === "}") { + pos++; + return obj; + } + for (;;) { + skipSpace(); + if (text[pos] !== '"') + fail("expected a key"); + const key = parseString(); + skipSpace(); + if (text[pos] !== ":") + fail("expected ':'"); + pos++; + obj.set(key, parseValue()); + skipSpace(); + if (text[pos] === ",") { + pos++; + continue; + } + if (text[pos] === "}") { + pos++; + return obj; + } + fail("expected ',' or '}'"); + } + } + if (c === "[") { + pos++; + const arr: DevTypeJson[] = []; + skipSpace(); + if (text[pos] === "]") { + pos++; + return arr; + } + for (;;) { + arr.push(parseValue()); + skipSpace(); + if (text[pos] === ",") { + pos++; + continue; + } + if (text[pos] === "]") { + pos++; + return arr; + } + fail("expected ',' or ']'"); + } + } + if (c === '"') + return parseString(); + const literal = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][-+]?\d+)?)/.exec(text.slice(pos, pos + 400)); + if (!literal) + return fail("unexpected character"); + pos += literal[0].length; + if (literal[0] === "true") + return true; + if (literal[0] === "false") + return false; + if (literal[0] === "null") + return null; + return new DevTypeJsonNumber(literal[0]); + }; + const value = parseValue(); + skipSpace(); + if (pos < text.length) + fail("unexpected text after the JSON value"); + return value; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Writing (as Python's json.dumps) +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Python's repr of a float: the shortest digits that read back as the same number, in fixed notation for decimal +// exponents from -4 to 15 (with ".0" if there is no fraction), otherwise as d.ddde+XX +export function pythonFloatRepr(value: number): string { + if (Number.isNaN(value)) + return "NaN"; + if (!Number.isFinite(value)) + return value > 0 ? "Infinity" : "-Infinity"; + if (value === 0) + return Object.is(value, -0) ? "-0.0" : "0.0"; + const [mantissa, expText] = value.toExponential().split("e"); + const negative = mantissa.startsWith("-"); + const digits = mantissa.replace("-", "").replace(".", ""); + const exp = parseInt(expText, 10); + let out: string; + if (exp >= -4 && exp < 16) { + if (exp >= 0) { + const intPart = digits.slice(0, exp + 1).padEnd(exp + 1, "0"); + out = intPart + "." + (digits.slice(exp + 1) || "0"); + } else { + out = "0." + "0".repeat(-exp - 1) + digits; + } + } else { + const absExp = Math.abs(exp); + out = digits[0] + (digits.length > 1 ? "." + digits.slice(1) : "") + "e" + (exp < 0 ? "-" : "+") + + (absExp < 10 ? "0" : "") + absExp; + } + return (negative ? "-" : "") + out; +} + +function numberText(num: DevTypeJsonNumber): string { + if (num.isInteger) { + // Python's int: "-0" is 0; leading zeros aren't valid JSON + return /^-0+$/.test(num.text) ? "0" : num.text; + } + return pythonFloatRepr(num.value); +} + +// A string as Python's json.dumps writes it with ensure_ascii (the default) +export function pythonJsonString(value: string): string { + let out = '"'; + for (let i = 0; i < value.length; i++) { + const c = value.charCodeAt(i); + if (c === 0x22) out += '\\"'; + else if (c === 0x5c) out += "\\\\"; + else if (c === 0x0a) out += "\\n"; + else if (c === 0x0d) out += "\\r"; + else if (c === 0x09) out += "\\t"; + else if (c === 0x08) out += "\\b"; + else if (c === 0x0c) out += "\\f"; + else if (c < 0x20 || c > 0x7e) out += "\\u" + c.toString(16).padStart(4, "0"); + else out += value[i]; + } + return out + '"'; +} + +// Strings in code point order (UTF-8 byte order), as Python sorts them +export function compareCodePoints(a: string, b: string): number { + const ai = a[Symbol.iterator](); + const bi = b[Symbol.iterator](); + for (;;) { + const an = ai.next(); + const bn = bi.next(); + if (an.done || bn.done) + return an.done ? (bn.done ? 0 : -1) : 1; + const diff = (an.value.codePointAt(0) ?? 0) - (bn.value.codePointAt(0) ?? 0); + if (diff !== 0) + return diff; + } +} + +export interface DevTypeJsonWriteOptions { + sortKeys?: boolean; // as json.dumps(sort_keys=True) + indent?: number; // as json.dumps(indent=n) (members on lines of their own, ": " after keys) +} + +// The value as Python's json.dumps writes it: by default json.dumps(value, separators=(",", ":")) +export function writeDevTypeJson(value: DevTypeJson, options: DevTypeJsonWriteOptions = {}, level = 0): string { + if (value === null) + return "null"; + if (value === true) + return "true"; + if (value === false) + return "false"; + if (typeof value === "string") + return pythonJsonString(value); + if (value instanceof DevTypeJsonNumber) + return numberText(value); + const indent = options.indent; + const open = indent === undefined ? "" : "\n" + " ".repeat(indent * (level + 1)); + const close = indent === undefined ? "" : "\n" + " ".repeat(indent * level); + const sep = indent === undefined ? ":" : ": "; + if (Array.isArray(value)) { + if (value.length === 0) + return "[]"; + return "[" + open + value.map((v) => writeDevTypeJson(v, options, level + 1)).join("," + open) + close + "]"; + } + if (value.size === 0) + return "{}"; + const keys = Array.from(value.keys()); + if (options.sortKeys) + keys.sort(compareCodePoints); + return "{" + open + keys.map((k) => pythonJsonString(k) + sep + writeDevTypeJson(value.get(k) as DevTypeJson, options, level + 1)) + .join("," + open) + close + "}"; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Plain JavaScript values +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// A plain value (e.g. from JSON.parse - a record from a device response). Integers become Python ints +export function devTypeJsonFromPlain(value: unknown): DevTypeJson { + if (value === null || value === undefined) + return null; + if (typeof value === "boolean" || typeof value === "string") + return value; + if (typeof value === "number") { + if (Number.isInteger(value) && Math.abs(value) < 1e21) + return new DevTypeJsonNumber(value.toFixed(0)); + return new DevTypeJsonNumber(String(value)); + } + if (value instanceof DevTypeJsonNumber || value instanceof Map) + return value as DevTypeJson; + if (Array.isArray(value)) + return value.map(devTypeJsonFromPlain); + const obj: DevTypeJsonObject = new Map(); + for (const [k, v] of Object.entries(value as Record)) + obj.set(k, devTypeJsonFromPlain(v)); + return obj; +} + +// A plain value for display or for code that reads records as objects (integer-like keys lose their order) +export function devTypeJsonToPlain(value: DevTypeJson): unknown { + if (value instanceof DevTypeJsonNumber) + return value.value; + if (Array.isArray(value)) + return value.map(devTypeJsonToPlain); + if (value instanceof Map) { + const out: Record = {}; + value.forEach((v, k) => { out[k] = devTypeJsonToPlain(v); }); + return out; + } + return value; +} + +// A deep copy +export function cloneDevTypeJson(value: DevTypeJson): DevTypeJson { + if (Array.isArray(value)) + return value.map(cloneDevTypeJson); + if (value instanceof Map) { + const out: DevTypeJsonObject = new Map(); + value.forEach((v, k) => out.set(k, cloneDevTypeJson(v))); + return out; + } + return value; +} + +// The member at a path of object keys, or undefined +export function devTypeJsonGet(value: DevTypeJson | undefined, ...path: string[]): DevTypeJson | undefined { + let current = value; + for (const key of path) { + if (!(current instanceof Map)) + return undefined; + current = current.get(key); + } + return current; +} diff --git a/src/RaftDeviceTypeSources.test.ts b/src/RaftDeviceTypeSources.test.ts new file mode 100644 index 0000000..f518790 --- /dev/null +++ b/src/RaftDeviceTypeSources.test.ts @@ -0,0 +1,74 @@ +// Resolving DeviceTypeSources.json selections, against the vectors shared with RaftCore +// (scripts/tests/selection_vectors.json, copied into testdata/ - after a change there, copy it again) + +import * as fs from "fs"; +import * as path from "path"; +import { DevTypeJson, devTypeJsonGet, isDevTypeJsonObject, parseDevTypeJson, writeDevTypeJson } from "./RaftDeviceTypeJson"; +import { DeviceTypeSection, DeviceTypeSelectionError, DeviceTypeSourceRecords, mergeDeviceTypeSources, + resolveDeviceTypeSelection } from "./RaftDeviceTypeSources"; + +const TESTDATA = path.join(__dirname, "..", "testdata"); +const RAFTCORE_TESTS = path.join(__dirname, "..", "..", "RaftCore", "scripts", "tests"); +const vectorsFile = JSON.parse(fs.readFileSync(path.join(TESTDATA, "selection_vectors.json"), "utf-8")); + +function recordsOf(catalogueText: string): Map { + const devTypes = devTypeJsonGet(parseDevTypeJson(catalogueText), "devTypes"); + if (!isDevTypeJsonObject(devTypes)) + throw new Error("no devTypes"); + return devTypes; +} + +describe("selection vectors", () => { + const vectors: any[] = vectorsFile.vectors; + + test("there are vectors", () => { + expect(vectors.length).toBeGreaterThan(20); + }); + + test.each(vectors.map((v) => [v.name, v]))("%s", (_name, vec) => { + const resolve = () => { + const merged = mergeDeviceTypeSources(Object.entries(vec.files).map(([name, content]) => ({ name, content: content as string }))); + const sources = new Map(); + for (const [name, source] of Object.entries(vec.sources)) + sources.set(name, { records: recordsOf(source.records), raftCore: source.kind === "bundled" ? vectorsFile.raftCore : undefined }); + return resolveDeviceTypeSelection(merged, vec.section as DeviceTypeSection, sources); + }; + if (vec.error !== undefined) { + expect(resolve).toThrow(DeviceTypeSelectionError); + expect(resolve).toThrow(vec.error); + return; + } + const result = resolve(); + const records: Record = {}; + result.records.forEach((rec, key) => { records[key] = { source: result.origin.get(key) as string, record: writeDevTypeJson(rec) }; }); + expect(Object.keys(records)).toEqual(Object.keys(vec.records)); + expect(records).toEqual(vec.records); + expect(Object.fromEntries(result.replaced)).toEqual(vec.replaced); + expect(Object.fromEntries(result.policyOnly)).toEqual(vec.policyOnly); + expect(result.messages).toEqual(vec.messages); + expect(writeDevTypeJson(result.provenance)).toBe(vec.provenance); + }); + + test("a source whose records weren't read", () => { + const merged = mergeDeviceTypeSources([{ name: "x", content: '{"sources":{"acme":{"git":"https://example.com/a.git","ref":"v1"}}}' }]); + expect(() => resolveDeviceTypeSelection(merged, "builtin", new Map())).toThrow("haven't been read"); + }); + + test("a git source in the provenance has its commit", () => { + const merged = mergeDeviceTypeSources([{ name: "x", content: + '{"sources":{"acme":{"git":"https://example.com/a.git","ref":"v1","path":"recs"}},"typeFile":{"include":["acme:*"]}}' }]); + const result = resolveDeviceTypeSelection(merged, "typeFile", + new Map([["acme", { records: recordsOf('{"devTypes":{"Gadget":{"addresses":"0x40"}}}'), commit: "abc123" }]])); + expect(writeDevTypeJson(result.provenance)).toBe('{"formatVersion":1,"sources":{"acme":{"git":"https://example.com/a.git",' + + '"ref":"v1","path":"recs","commit":"abc123"}},"typeFile":{"include":["acme:*"]}}'); + }); +}); + +describe("vectors are RaftCore's", () => { + // Only where a RaftCore checkout next to raftjs has them + const present = fs.existsSync(path.join(RAFTCORE_TESTS, "selection_vectors.json")); + (present ? test : test.skip)("selection_vectors.json", () => { + expect(fs.readFileSync(path.join(TESTDATA, "selection_vectors.json"), "utf-8")) + .toBe(fs.readFileSync(path.join(RAFTCORE_TESTS, "selection_vectors.json"), "utf-8")); + }); +}); diff --git a/src/RaftDeviceTypeSources.ts b/src/RaftDeviceTypeSources.ts new file mode 100644 index 0000000..bf4c9e8 --- /dev/null +++ b/src/RaftDeviceTypeSources.ts @@ -0,0 +1,328 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// RaftDeviceTypeSources +// Part of RaftJS +// +// DeviceTypeSources.json (RaftCore wiki: Selecting Device Types): named sources of device type records - RaftCore's +// ("bundled"), folders ("path") and git repos ("git") - and the records a build compiles in ("builtin") or puts in a +// device type file ("typeFile"). A TypeScript copy of RaftCore's AssembleDeviceTypes read_selection and assemble +// (without fetching: the caller supplies each source's records), checked against the vectors in RaftCore +// scripts/tests/selection_vectors.json, so a selection resolves here as it does in a build. +// +// Rob Dobson 2026 +// (C) 2026 All rights reserved +// +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +import { DevTypeJson, DevTypeJsonObject, DevTypeJsonNumber, cloneDevTypeJson, compareCodePoints, isDevTypeJsonObject, + parseDevTypeJson } from "./RaftDeviceTypeJson"; +import { DeviceTypePolicyName } from "./RaftDeviceTypeFile"; + +export type DeviceTypeSection = "builtin" | "typeFile"; +export const DEVICE_TYPE_SECTIONS: DeviceTypeSection[] = ["builtin", "typeFile"]; +export const DEVICE_TYPE_PROVENANCE_FORMAT_VERSION = 1; + +const POLICY_VALUES: Record = { enabled: undefined, disabled: true, disabledClaim: "claim" }; + +export class DeviceTypeSelectionError extends Error {} + +export interface DeviceTypeSectionSelection { + include: string[]; + exclude: string[]; + policy: Map; + hasInclude: boolean; // the section has an include list (builtin's default is everything, typeFile's nothing) + fsImage: boolean; +} + +// Selection files merged (Common's first, then the SysType's) +export interface MergedDeviceTypeSources { + sources: Map; // name -> spec, e.g. {"git": , "ref": } + sections: Record; + messages: string[]; +} + +export type DeviceTypeSourceKind = "bundled" | "path" | "git"; + +export function deviceTypeSourceKind(spec: DevTypeJsonObject): DeviceTypeSourceKind | undefined { + if (spec.has("git")) + return "git"; + if (truthy(spec.get("bundled"))) + return "bundled"; + if (spec.has("path")) + return "path"; + return undefined; +} + +// A source's records, as read by the caller (from the published catalogue, a git host or local files) +export interface DeviceTypeSourceRecords { + records: Map; + commit?: string; // a git source: the commit its ref resolved to + raftCore?: string; // RaftCore's records: its version +} + +export interface DeviceTypeSelectionResult { + records: Map; // key -> record (policies applied), in key order + origin: Map; // key -> source name + replaced: Map; // key -> sources whose record a path source's replaced + policyOnly: Map; // typeFile: policies for types not in the file (compiled types) + messages: string[]; + provenance: DevTypeJson; // what the records came from (a type file's _sources) + fsImage: boolean; +} + +function truthy(value: DevTypeJson | undefined): boolean { + if (value === undefined || value === null || value === false || value === "") + return false; + if (value instanceof DevTypeJsonNumber) + return value.value !== 0; + if (Array.isArray(value)) + return value.length > 0; + if (value instanceof Map) + return value.size > 0; + return true; +} + +function basename(path: string): string { + return path.slice(path.lastIndexOf("/") + 1); +} + +const emptySection = (): DeviceTypeSectionSelection => ({ include: [], exclude: [], policy: new Map(), hasInclude: false, fsImage: false }); + +// Merge DeviceTypeSources.json files (Common's first, then the SysType's): sources are added (or replaced by name), +// include and exclude lists joined, policies merged (the later file's win). name is used in error messages +export function mergeDeviceTypeSources(files: Array<{ name: string; content: string | DevTypeJson }>): MergedDeviceTypeSources { + const merged: MergedDeviceTypeSources = { sources: new Map(), messages: [], sections: { builtin: emptySection(), typeFile: emptySection() } }; + for (const file of files) { + const fail = (message: string): never => { throw new DeviceTypeSelectionError(`${file.name}: ${message}`); }; + let content: DevTypeJson; + try { + content = typeof file.content === "string" ? parseDevTypeJson(file.content) : file.content; + } catch (err) { + return fail(`not valid JSON (${err instanceof Error ? err.message : err})`); + } + if (!isDevTypeJsonObject(content)) + fail("expected a JSON object"); + const obj = content as DevTypeJsonObject; + for (const key of obj.keys()) { + if (!["sources", "_notes", ...DEVICE_TYPE_SECTIONS].includes(key)) + fail(`unknown key "${key}"`); + } + const sources = obj.get("sources"); + if (isDevTypeJsonObject(sources)) { + for (const [name, spec] of sources) { + if (!isDevTypeJsonObject(spec)) + fail(`source "${name}" must be an object`); + if (name.includes(":") || name === "*") + fail(`source name "${name}" can't contain ':' or be '*'`); + let sourceSpec = spec as DevTypeJsonObject; + if (sourceSpec.has("builtin") && !sourceSpec.has("bundled")) { + // The older name for "bundled" + const renamed: DevTypeJsonObject = new Map(); + sourceSpec.forEach((v, k) => renamed.set(k === "builtin" ? "bundled" : k, v)); + sourceSpec = renamed; + merged.messages.push(`${basename(file.name)}: source "${name}": "builtin": true is now "bundled": true`); + } + merged.sources.set(name, sourceSpec); + } + } + for (const section of DEVICE_TYPE_SECTIONS) { + const spec = obj.get(section); + if (spec === undefined) + continue; + if (!isDevTypeJsonObject(spec)) + fail(`"${section}" must be an object`); + const sectionSpec = spec as DevTypeJsonObject; + const target = merged.sections[section]; + for (const listName of ["include", "exclude"] as const) { + let value = sectionSpec.get(listName) ?? []; + if (typeof value === "string") + value = [value]; + if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) + fail(`${section}.${listName} must be a list of strings`); + target[listName].push(...(value as string[])); + } + if (sectionSpec.has("include")) + target.hasInclude = true; + const policy = sectionSpec.get("policy"); + if (isDevTypeJsonObject(policy)) { + for (const [key, name] of policy) { + if (typeof name !== "string" || !(name in POLICY_VALUES)) + fail(`${section}.policy "${key}" must be one of ${Object.keys(POLICY_VALUES).join(", ")}`); + target.policy.set(key, name as DeviceTypePolicyName); + } + } + if (sectionSpec.has("fsImage")) { + if (section !== "typeFile") + fail("fsImage belongs in the typeFile section"); + target.fsImage = truthy(sectionSpec.get("fsImage")); + } + } + } + return merged; +} + +function parsePattern(pattern: string, sourceNames: string[]): [string | undefined, string] { + const colon = pattern.indexOf(":"); + if (colon >= 0) { + const source = pattern.slice(0, colon); + if (!sourceNames.includes(source)) + throw new DeviceTypeSelectionError(`"${pattern}": no source called "${source}"`); + return [source, pattern.slice(colon + 1)]; + } + return [undefined, pattern]; +} + +// Resolve a section of a selection into records (sorted by key), given the records of each source +export function resolveDeviceTypeSelection(merged: MergedDeviceTypeSources, section: DeviceTypeSection, + sourceRecords: Map): DeviceTypeSelectionResult { + const result: DeviceTypeSelectionResult = { records: new Map(), origin: new Map(), replaced: new Map(), policyOnly: new Map(), + messages: [...merged.messages], provenance: null, fsImage: merged.sections[section].fsImage }; + const sources = new Map; isLocal: boolean }>(); + for (const [name, spec] of merged.sources) { + const kind = deviceTypeSourceKind(spec); + if (kind === undefined) + throw new DeviceTypeSelectionError(`source "${name}": needs "bundled", "path" or "git"`); + const loaded = sourceRecords.get(name); + if (!loaded) + throw new DeviceTypeSelectionError(`source "${name}": its records haven't been read`); + sources.set(name, { records: loaded.records, isLocal: kind === "path" }); + } + const names = Array.from(sources.keys()); + const chosen = merged.sections[section]; + // builtin includes everything unless it says otherwise; a type file includes nothing unless it says otherwise + const includes = chosen.hasInclude ? chosen.include : (section === "builtin" ? ["*"] : []); + + // Candidates for each key: the sources it was selected from, and whether a source was named for it + const candidates = new Map(); + const addTo = (list: string[], name: string) => { if (!list.includes(name)) list.push(name); }; + for (const pattern of includes) { + const [source, key] = parsePattern(pattern, names); + let matched = false; + for (const name of source ? [source] : names) { + const records = (sources.get(name) as { records: Map }).records; + const keys = key === "*" ? Array.from(records.keys()) : (records.has(key) ? [key] : []); + for (const k of keys) { + if (!candidates.has(k)) + candidates.set(k, { named: [], any: [] }); + const entry = candidates.get(k) as { named: string[]; any: string[] }; + addTo(source && key !== "*" ? entry.named : entry.any, name); + matched = true; + } + } + if (!matched && key !== "*") { + const where = source ? `source "${source}"` : "any source"; + throw new DeviceTypeSelectionError(`${section}.include "${pattern}": no record "${key}" in ${where}`); + } + } + for (const pattern of chosen.exclude) { + const [source, key] = parsePattern(pattern, names); + for (const k of Array.from(candidates.keys())) { + if (key === "*" || k === key) { + const entry = candidates.get(k) as { named: string[]; any: string[] }; + if (source === undefined) { + entry.named = []; + entry.any = []; + } else { + entry.named = entry.named.filter((n) => n !== source); + entry.any = entry.any.filter((n) => n !== source); + } + if (entry.named.length === 0 && entry.any.length === 0) + candidates.delete(k); + } + } + } + + // One source for each key + for (const key of Array.from(candidates.keys()).sort(compareCodePoints)) { + const { named, any } = candidates.get(key) as { named: string[]; any: string[] }; + let source: string; + if (named.length > 1) + throw new DeviceTypeSelectionError(`"${key}" is named from more than one source: ${named.join(", ")}`); + if (named.length === 1) { + source = named[0]; + } else { + const options = names.filter((n) => any.includes(n)); + const local = options.filter((n) => (sources.get(n) as { isLocal: boolean }).isLocal); + if (local.length > 0) { + source = local[local.length - 1]; + const replaced = options.filter((n) => n !== source); + if (replaced.length > 0) { + result.messages.push(`"${key}" from ${source} replaces the record from ${replaced.join(", ")}`); + result.replaced.set(key, replaced); + } + } else if (options.length > 1) { + throw new DeviceTypeSelectionError(`"${key}" is in sources ${options.join(", ")} - name the source to use in ` + + `${section}.include (e.g. "${options[0]}:${key}")`); + } else { + source = options[0]; + } + } + result.records.set(key, cloneDevTypeJson((sources.get(source) as { records: Map }).records.get(key) as DevTypeJson)); + result.origin.set(key, source); + } + + // Policies + for (const [key, policy] of chosen.policy) { + const record = result.records.get(key); + if (record === undefined) { + // In a type file, a policy for a type that isn't in the file applies to a compiled record + if (section === "typeFile") { + result.policyOnly.set(key, policy); + continue; + } + throw new DeviceTypeSelectionError(`${section}.policy "${key}": not a selected record`); + } + if (isDevTypeJsonObject(record)) { + const value = POLICY_VALUES[policy]; + if (value === undefined) + record.delete("disable"); + else + record.set("disable", value); + } + } + result.provenance = deviceTypeProvenance(merged, section, result.origin, sourceRecords); + return result; +} + +// What a selection's records came from (a type file's _sources): the sources that supplied a record - a git source +// with the commit its ref resolved to, RaftCore's records with RaftCore's version, a path source with its path - and +// the section's selection +export function deviceTypeProvenance(merged: MergedDeviceTypeSources, section: DeviceTypeSection, origin: Map, + sourceRecords: Map): DevTypeJson { + const used = new Set(origin.values()); + const sources: DevTypeJsonObject = new Map(); + for (const [name, spec] of merged.sources) { + if (!used.has(name)) + continue; + const entry: DevTypeJsonObject = new Map(); + const kind = deviceTypeSourceKind(spec); + if (kind === "git") { + entry.set("git", spec.get("git") as DevTypeJson); + for (const field of ["ref", "path"]) { + if (spec.has(field)) + entry.set(field, spec.get(field) as DevTypeJson); + } + const commit = sourceRecords.get(name)?.commit; + if (commit) + entry.set("commit", commit); + } else if (kind === "bundled") { + entry.set("bundled", true); + const version = sourceRecords.get(name)?.raftCore; + if (version) + entry.set("raftCore", version); + } else { + entry.set("path", spec.get("path") ?? ""); + } + sources.set(name, entry); + } + const chosen = merged.sections[section]; + const chosenOut: DevTypeJsonObject = new Map(); + if (chosen.hasInclude) + chosenOut.set("include", [...chosen.include]); + if (chosen.exclude.length > 0) + chosenOut.set("exclude", [...chosen.exclude]); + if (chosen.policy.size > 0) + chosenOut.set("policy", new Map(chosen.policy)); + return new Map([["formatVersion", new DevTypeJsonNumber(String(DEVICE_TYPE_PROVENANCE_FORMAT_VERSION))], + ["sources", sources], [section, chosenOut]]); +} diff --git a/src/main.ts b/src/main.ts index da4b1cf..214b344 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,3 +42,9 @@ export * from "./RaftProtocolDefs"; export * from "./RaftDeviceStates"; export * from "./RaftDeviceInfo"; export { getHostTimezone, getHostPosixTZ, ianaToPosixTZ } from './RaftTimezone'; +export * from './RaftDeviceTypeJson'; +export * from './RaftDeviceTypeGrammar'; +export * from './RaftDeviceTypeFile'; +export * from './RaftDeviceTypeFileManager'; +export * from './RaftDeviceTypeSources'; +export * from './RaftDeviceTypeCatalogue'; diff --git a/testdata/grammar_vectors.json b/testdata/grammar_vectors.json new file mode 100644 index 0000000..3a253ca --- /dev/null +++ b/testdata/grammar_vectors.json @@ -0,0 +1,162 @@ +{ + "_doc": [ + "Shared test vectors for the device type record command grammar (DeviceTypeGrammar in C++ and", + "DeviceTypeGrammar.py in the generator). Each vector compiles 'str' as 'field' (detection, init or poll) and", + "must give exactly these steps and issues. A step: write = hex bytes written, read = bytes read, pause = ms", + "after, checks = alternatives as 'mask:value' hex (detection only; a pause-only value gives one empty check", + "':'), crc8w = each 2-byte word read followed by a CRC-8, expr = the read length is an r{...} expression.", + "issues lists issue names in order. A detection string with an error has no steps." + ], + "vectors": [ + {"name": "who am i", "field": "detection", "str": "0x0f=0x69,0x6a,0x6c", + "steps": [{"write": "0f", "read": 1, "pause": 0, "checks": ["ff:69", "ff:6a", "ff:6c"]}], "issues": []}, + {"name": "two byte hex", "field": "detection", "str": "0x07=0x81c4,0x81d4", + "steps": [{"write": "07", "read": 2, "pause": 0, "checks": ["ffff:81c4", "ffff:81d4"]}], "issues": []}, + {"name": "binary mask", "field": "detection", "str": "0x0c=0b100001100000XXXX", + "steps": [{"write": "0c", "read": 2, "pause": 0, "checks": ["fff0:8600"]}], "issues": []}, + {"name": "binary with 0x inside (C3 regression)", "field": "detection", "str": "0x86=0b1010XXXX&0x87=0x05", + "steps": [{"write": "86", "read": 1, "pause": 0, "checks": ["f0:a0"]}, + {"write": "87", "read": 1, "pause": 0, "checks": ["ff:05"]}], "issues": []}, + {"name": "write pause then read", "field": "detection", "str": "0x00FF=p25&=0bXXXXXXXX00000101", + "steps": [{"write": "00ff", "read": 0, "pause": 25, "checks": [":"]}, + {"write": "", "read": 2, "pause": 0, "checks": ["00ff:0005"]}], "issues": []}, + {"name": "leading pause", "field": "detection", "str": "=p250&0x00=0x0084", + "steps": [{"write": "", "read": 0, "pause": 250, "checks": [":"]}, + {"write": "00", "read": 2, "pause": 0, "checks": ["ffff:0084"]}], "issues": []}, + {"name": "pause before value", "field": "detection", "str": "0x01=p10 0x55", + "steps": [{"write": "01", "read": 1, "pause": 10, "checks": ["ff:55"]}], "issues": []}, + {"name": "semicolon separator", "field": "detection", "str": "0xfd=0x50;0xfe=0x5d", + "steps": [{"write": "fd", "read": 1, "pause": 0, "checks": ["ff:50"]}, + {"write": "fe", "read": 1, "pause": 0, "checks": ["ff:5d"]}], "issues": []}, + {"name": "uppercase hex and prefix", "field": "detection", "str": "0X010F=0xEBAA", + "steps": [{"write": "010f", "read": 2, "pause": 0, "checks": ["ffff:ebaa"]}], "issues": []}, + {"name": "write without 0x", "field": "detection", "str": "0f=0x69", + "steps": [{"write": "0f", "read": 1, "pause": 0, "checks": ["ff:69"]}], "issues": []}, + {"name": "read compares nothing (C10)", "field": "detection", "str": "0x89=p10&=r6", + "steps": [{"write": "89", "read": 0, "pause": 10, "checks": [":"]}, + {"write": "", "read": 6, "pause": 0, "checks": ["000000000000:000000000000"]}], "issues": []}, + {"name": "crc words", "field": "detection", "str": "0x3682=p1&=0bXXXXXXXXXXXXXXXXXXXXXXXX:crc8w", + "steps": [{"write": "3682", "read": 0, "pause": 1, "checks": [":"]}, + {"write": "", "read": 3, "pause": 0, "checks": ["000000:000000"], "crc8w": true}], "issues": []}, + {"name": "crc words exact", "field": "detection", "str": "0x365b=p1&=0x090173018ad4:crc8w", + "steps": [{"write": "365b", "read": 0, "pause": 1, "checks": [":"]}, + {"write": "", "read": 6, "pause": 0, "checks": ["ffffffffffff:090173018ad4"], "crc8w": true}], "issues": []}, + {"name": "crc on a bad length", "field": "detection", "str": "=0x0901:crc8w", + "steps": [], "issues": ["CrcLength"]}, + {"name": "empty", "field": "detection", "str": "", "steps": [], "issues": []}, + {"name": "hex check without prefix is an error", "field": "detection", "str": "0x86=A005", + "steps": [], "issues": ["BadCheck"]}, + {"name": "bad hex digit in check", "field": "detection", "str": "0x0f=0x6g", + "steps": [], "issues": ["BadCheck"]}, + {"name": "bad binary digit", "field": "detection", "str": "0x0f=0b1012xxxx", + "steps": [], "issues": ["BadCheck"]}, + {"name": "non-hex write", "field": "detection", "str": "0xzz=0x01", + "steps": [], "issues": ["NotHex"]}, + {"name": "two equals", "field": "detection", "str": "0x0f=0x69=p5", + "steps": [], "issues": ["MultipleEquals"]}, + {"name": "odd hex write kept as before", "field": "detection", "str": "0x123=0x01", + "steps": [{"write": "1200", "read": 1, "pause": 0, "checks": ["ff:01"]}], "issues": ["OddHex"]}, + {"name": "odd hex check kept as before", "field": "detection", "str": "0x0f=0x691", + "steps": [{"write": "0f", "read": 2, "pause": 0, "checks": ["ffff:6900"]}], "issues": ["OddHex"]}, + {"name": "mixed lengths", "field": "detection", "str": "0x0f=0x69,0x6a6b", + "steps": [{"write": "0f", "read": 1, "pause": 0, "checks": ["ff:69", "ffff:6a6b"]}], "issues": ["MixedCheckLengths"]}, + {"name": "binary not whole bytes", "field": "detection", "str": "0x00=0b1010", + "steps": [{"write": "00", "read": 1, "pause": 0, "checks": ["ff:a0"]}], "issues": ["BinaryNotWholeBytes"]}, + {"name": "bare pause (C4)", "field": "detection", "str": "p20&0x0f=0x69", + "steps": [{"write": "", "read": 0, "pause": 20, "checks": []}, + {"write": "0f", "read": 1, "pause": 0, "checks": ["ff:69"]}], "issues": ["BarePause"]}, + {"name": "r expr not allowed in detection", "field": "detection", "str": "0x3e=r{$0.w12:max240}", + "steps": [], "issues": ["BadReadLength"]}, + + {"name": "init writes", "field": "init", "str": "0x0a00&0x1244&0x1048", + "steps": [{"write": "0a00", "read": 0, "pause": 0}, {"write": "1244", "read": 0, "pause": 0}, + {"write": "1048", "read": 0, "pause": 0}], "issues": []}, + {"name": "init write then pause", "field": "init", "str": "0x2003=p100", + "steps": [{"write": "2003", "read": 0, "pause": 100}], "issues": []}, + {"name": "init empty value", "field": "init", "str": "0xbe0800=", + "steps": [{"write": "be0800", "read": 0, "pause": 0}], "issues": []}, + {"name": "init pause step", "field": "init", "str": "0x8002&=p20&0x8001", + "steps": [{"write": "8002", "read": 0, "pause": 0}, {"write": "", "read": 0, "pause": 20}, + {"write": "8001", "read": 0, "pause": 0}], "issues": []}, + {"name": "init read", "field": "init", "str": "0xd100=p5&=r3", + "steps": [{"write": "d100", "read": 0, "pause": 5}, {"write": "", "read": 3, "pause": 0}], "issues": []}, + {"name": "init read and pause", "field": "init", "str": "0x91=r1p5", + "steps": [{"write": "91", "read": 1, "pause": 5}], "issues": []}, + {"name": "init value not written (trap)", "field": "init", "str": "0x01=0x0220", + "steps": [{"write": "01", "read": 0, "pause": 0}], "issues": ["InitValueNotWritten"]}, + {"name": "init binary is a read count (trap)", "field": "init", "str": "0x01=0b00010000&=p250", + "steps": [{"write": "01", "read": 1, "pause": 0}, {"write": "", "read": 0, "pause": 250}], + "issues": ["InitBinaryIsReadCount"]}, + {"name": "init junk value ignored", "field": "init", "str": "0x01=abc", + "steps": [{"write": "01", "read": 0, "pause": 0}], "issues": ["InitValueIgnored"]}, + {"name": "init bare pause (C4)", "field": "init", "str": "0x8002&p20&0x8001", + "steps": [{"write": "8002", "read": 0, "pause": 0}, {"write": "", "read": 0, "pause": 20}, + {"write": "8001", "read": 0, "pause": 0}], "issues": ["BarePause"]}, + {"name": "init non-hex step dropped", "field": "init", "str": "0x0a00&0xq1&0x1048", + "steps": [{"write": "0a00", "read": 0, "pause": 0}, {"write": "1048", "read": 0, "pause": 0}], + "issues": ["NotHex"]}, + {"name": "init semicolon", "field": "init", "str": "0x2d0c=;0x310a=", + "steps": [{"write": "2d0c", "read": 0, "pause": 0}, {"write": "310a", "read": 0, "pause": 0}], "issues": []}, + + {"name": "poll reads", "field": "poll", "str": "0x08=r2&0x09=r2", + "steps": [{"write": "08", "read": 2, "pause": 0}, {"write": "09", "read": 2, "pause": 0}], "issues": []}, + {"name": "poll read then trigger", "field": "poll", "str": "=r6&0xfd=", + "steps": [{"write": "", "read": 6, "pause": 0}, {"write": "fd", "read": 0, "pause": 0}], "issues": []}, + {"name": "poll write only", "field": "poll", "str": "0x004f=r1&0x001507", + "steps": [{"write": "004f", "read": 1, "pause": 0}, {"write": "001507", "read": 0, "pause": 0}], "issues": []}, + {"name": "poll pause steps", "field": "poll", "str": "0x0202=p5&=r3", + "steps": [{"write": "0202", "read": 0, "pause": 5}, {"write": "", "read": 3, "pause": 0}], "issues": []}, + {"name": "poll binary is a byte count", "field": "poll", "str": "0x08=0bXXXXXXXXXXXXXXXX", + "steps": [{"write": "08", "read": 2, "pause": 0}], "issues": []}, + {"name": "poll hex is a byte count", "field": "poll", "str": "0x08=0x0000", + "steps": [{"write": "08", "read": 2, "pause": 0}], "issues": []}, + {"name": "poll dynamic read", "field": "poll", "str": "0x3a=r4&0x3e=r{$0.w12:mask0FFF*2:max240}", + "steps": [{"write": "3a", "read": 4, "pause": 0}, {"write": "3e", "read": 240, "pause": 0, "expr": true}], + "issues": []}, + {"name": "poll dynamic read without max (C13)", "field": "poll", "str": "0x3a=r4&0x3e=r{$0.w12}&0x10=r1", + "steps": [{"write": "3a", "read": 4, "pause": 0}, {"write": "10", "read": 1, "pause": 0}], + "issues": ["BadReadExpr"]}, + {"name": "poll unterminated expr", "field": "poll", "str": "0x3e=r{$0.w12", + "steps": [], "issues": ["BadReadExpr"]}, + {"name": "poll r0", "field": "poll", "str": "0x10=r0", + "steps": [], "issues": ["BadReadLength"]}, + {"name": "poll junk value", "field": "poll", "str": "0x10=zz&0x11=r1", + "steps": [{"write": "11", "read": 1, "pause": 0}], "issues": ["BadValue"]}, + {"name": "init r expr not allowed", "field": "init", "str": "0x3e=r{$0.w12:max240}", + "steps": [], "issues": ["ReadExprNotAllowed"]}, + {"name": "init value slot", "field": "init", "str": "0xe0b6=p10&0xeb=r4&0x5a{rh0}&0x7110", + "steps": [{"write": "e0b6", "read": 0, "pause": 10}, {"write": "eb", "read": 4, "pause": 0}, {"write": "5a00", "read": 0, "pause": 0, "slots": ["1:rh0"]}, {"write": "7110", "read": 0, "pause": 0}], "issues": []}, + {"name": "init two value slots", "field": "init", "str": "0x12{a}34{b_2}", + "steps": [{"write": "12003400", "read": 0, "pause": 0, "slots": ["1:a", "3:b_2"]}], "issues": []}, + {"name": "value slot not in detection", "field": "detection", "str": "0x5a{rh0}=0x01", + "steps": [], "issues": ["BadPlaceholder"]}, + {"name": "value slot not in poll", "field": "poll", "str": "0x5a{v}=r2&0x11=r1", + "steps": [{"write": "11", "read": 1, "pause": 0}], "issues": ["BadPlaceholder"]}, + {"name": "value slot off byte boundary", "field": "init", "str": "0x5{a}", + "steps": [], "issues": ["BadPlaceholder"]}, + {"name": "value slot unclosed", "field": "init", "str": "0x5a{a", + "steps": [], "issues": ["BadPlaceholder"]}, + {"name": "value slot unnamed", "field": "init", "str": "0x5a{}", + "steps": [], "issues": ["BadPlaceholder"]}, + {"name": "value slot name starts with digit", "field": "init", "str": "0x5a{1x}", + "steps": [], "issues": ["BadPlaceholder"]}, + {"name": "init guard", "field": "init", "str": "0x4600=p5&=r3&{?setInt}0x46000002e3=p10&0x0010000081", + "steps": [{"write": "4600", "read": 0, "pause": 5}, {"write": "", "read": 3, "pause": 0}, {"write": "46000002e3", "read": 0, "pause": 10, "guard": "setInt"}, {"write": "0010000081", "read": 0, "pause": 0}], "issues": []}, + {"name": "init guard with value slot", "field": "init", "str": "{?g}0x5a{v}", + "steps": [{"write": "5a00", "read": 0, "pause": 0, "slots": ["1:v"], "guard": "g"}], "issues": []}, + {"name": "init guard on a read", "field": "init", "str": " {?g_1} =r3", + "steps": [{"write": "", "read": 3, "pause": 0, "guard": "g_1"}], "issues": []}, + {"name": "init guard on a bare pause", "field": "init", "str": "{?g}p10", + "steps": [{"write": "", "read": 0, "pause": 10, "guard": "g"}], "issues": ["BarePause"]}, + {"name": "guard not in detection", "field": "detection", "str": "{?g}0x10=0x01", + "steps": [], "issues": ["BadPlaceholder"]}, + {"name": "guard not in poll", "field": "poll", "str": "{?g}0x10=r2&0x11=r1", + "steps": [{"write": "11", "read": 1, "pause": 0}], "issues": ["BadPlaceholder"]}, + {"name": "guard not at the start", "field": "init", "str": "0x12{?g}", + "steps": [], "issues": ["BadPlaceholder"]}, + {"name": "guard unclosed", "field": "init", "str": "{?g0x12", + "steps": [], "issues": ["BadPlaceholder"]}, + {"name": "guard unnamed", "field": "init", "str": "{?}0x12", + "steps": [], "issues": ["BadPlaceholder"]} + ] +} diff --git a/testdata/selection_vectors.json b/testdata/selection_vectors.json new file mode 100644 index 0000000..84e3a6e --- /dev/null +++ b/testdata/selection_vectors.json @@ -0,0 +1,590 @@ +{ + "_doc": "DeviceTypeSources.json selection vectors - see make_selection_vectors.py", + "raftCore": "1.62.0", + "vectors": [ + { + "name": "include defaults to everything", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + }, + "Bravo": { + "source": "raftcore", + "record": "{\"addresses\":\"0x10\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Bravo core\"}}" + }, + "Charlie": { + "source": "raftcore", + "record": "{\"addresses\":\"0x12\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"builtin\":{}}" + }, + { + "name": "include forms and exclude", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"local\": {\"path\": \"../../local\"}}, \"builtin\": {\"include\": [\"raftcore:Alpha\", \"local:*\", \"Charlie\"], \"exclude\": [\"local:Delta\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + }, + "local": { + "kind": "path", + "records": "{\"devTypes\": {\"Delta\": {\"addresses\": \"0x13\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Echo\": {\"addresses\": \"0x14\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + }, + "Charlie": { + "source": "raftcore", + "record": "{\"addresses\":\"0x12\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + }, + "Echo": { + "source": "local", + "record": "{\"addresses\":\"0x14\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"},\"local\":{\"path\":\"../../local\"}},\"builtin\":{\"include\":[\"raftcore:Alpha\",\"local:*\",\"Charlie\"],\"exclude\":[\"local:Delta\"]}}" + }, + { + "name": "exclude a wildcard and a source", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"*\"], \"exclude\": [\"raftcore:Bravo\", \"Charlie\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"builtin\":{\"include\":[\"*\"],\"exclude\":[\"raftcore:Bravo\",\"Charlie\"]}}" + }, + { + "name": "a local copy replaces", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"local\": {\"path\": \"../../local\"}}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + }, + "local": { + "kind": "path", + "records": "{\"devTypes\": {\"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo local\"}}}}" + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + }, + "Bravo": { + "source": "local", + "record": "{\"addresses\":\"0x10\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Bravo local\"}}" + }, + "Charlie": { + "source": "raftcore", + "record": "{\"addresses\":\"0x12\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": { + "Bravo": [ + "raftcore" + ] + }, + "policyOnly": {}, + "messages": [ + "\"Bravo\" from local replaces the record from raftcore" + ], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"},\"local\":{\"path\":\"../../local\"}},\"builtin\":{}}" + }, + { + "name": "the later path source wins", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"a\": {\"path\": \"../../a\"}, \"b\": {\"path\": \"../../b\"}}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + }, + "a": { + "kind": "path", + "records": "{\"devTypes\": {\"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"A\"}}}}" + }, + "b": { + "kind": "path", + "records": "{\"devTypes\": {\"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"B\"}}}}" + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + }, + "Bravo": { + "source": "b", + "record": "{\"addresses\":\"0x10\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"B\"}}" + }, + "Charlie": { + "source": "raftcore", + "record": "{\"addresses\":\"0x12\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": { + "Bravo": [ + "raftcore", + "a" + ] + }, + "policyOnly": {}, + "messages": [ + "\"Bravo\" from b replaces the record from raftcore, a" + ], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"},\"b\":{\"path\":\"../../b\"}},\"builtin\":{}}" + }, + { + "name": "a named source wins over a local copy", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"local\": {\"path\": \"../../local\"}}, \"builtin\": {\"include\": [\"raftcore:Bravo\", \"local:*\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + }, + "local": { + "kind": "path", + "records": "{\"devTypes\": {\"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo local\"}}}}" + } + }, + "section": "builtin", + "records": { + "Bravo": { + "source": "raftcore", + "record": "{\"addresses\":\"0x10\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Bravo core\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"builtin\":{\"include\":[\"raftcore:Bravo\",\"local:*\"]}}" + }, + { + "name": "error: the same key from two other sources", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"other\": {\"bundled\": true}}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + }, + "other": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "error": "\"Alpha\" is in sources raftcore, other - name the source to use in builtin.include (e.g. \"raftcore:Alpha\")" + }, + { + "name": "the same key from two other sources, one named", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"other\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"other:Alpha\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + }, + "other": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "other", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"other\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"builtin\":{\"include\":[\"other:Alpha\"]}}" + }, + { + "name": "error: named from two sources", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"other\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"other:Alpha\", \"raftcore:Alpha\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + }, + "other": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "error": "\"Alpha\" is named from more than one source: other, raftcore" + }, + { + "name": "error: no such record", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"Zulu\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "error": "builtin.include \"Zulu\": no record \"Zulu\" in any source" + }, + { + "name": "error: no such record in a source", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"raftcore:Zulu\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "error": "builtin.include \"raftcore:Zulu\": no record \"Zulu\" in source \"raftcore\"" + }, + { + "name": "error: no such source", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"acme:*\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "error": "\"acme:*\": no source called \"acme\"" + }, + { + "name": "policies applied", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"policy\": {\"Alpha\": \"disabled\", \"Bravo\": \"disabledClaim\", \"Charlie\": \"enabled\"}}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}, \"disable\": true}}}" + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"},\"disable\":true}" + }, + "Bravo": { + "source": "raftcore", + "record": "{\"addresses\":\"0x10\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"},\"disable\":\"claim\"}" + }, + "Charlie": { + "source": "raftcore", + "record": "{\"addresses\":\"0x12\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"builtin\":{\"policy\":{\"Alpha\":\"disabled\",\"Bravo\":\"disabledClaim\",\"Charlie\":\"enabled\"}}}" + }, + { + "name": "error: policy for a record not selected", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"Alpha\"], \"policy\": {\"Bravo\": \"disabled\"}}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "error": "builtin.policy \"Bravo\": not a selected record" + }, + { + "name": "error: unknown policy", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"policy\": {\"Alpha\": \"off\"}}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "error": "systypes/Common/DeviceTypeSources.json: builtin.policy \"Alpha\" must be one of enabled, disabled, disabledClaim" + }, + { + "name": "error: unknown key", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"typefile\": {}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "error": "systypes/Common/DeviceTypeSources.json: unknown key \"typefile\"" + }, + { + "name": "error: bad source name", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"a:b\": {\"bundled\": true}}}" + }, + "sources": {}, + "section": "builtin", + "error": "systypes/Common/DeviceTypeSources.json: source name \"a:b\" can't contain ':' or be '*'" + }, + { + "name": "error: include not a list of strings", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": [1]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "error": "systypes/Common/DeviceTypeSources.json: builtin.include must be a list of strings" + }, + { + "name": "error: fsImage outside typeFile", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"fsImage\": true}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "error": "systypes/Common/DeviceTypeSources.json: fsImage belongs in the typeFile section" + }, + { + "name": "include as a string", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": \"Alpha\"}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"builtin\":{\"include\":[\"Alpha\"]}}" + }, + { + "name": "the older name builtin: true", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"builtin\": true}}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + }, + "Bravo": { + "source": "raftcore", + "record": "{\"addresses\":\"0x10\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Bravo core\"}}" + }, + "Charlie": { + "source": "raftcore", + "record": "{\"addresses\":\"0x12\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [ + "DeviceTypeSources.json: source \"raftcore\": \"builtin\": true is now \"bundled\": true" + ], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"builtin\":{}}" + }, + { + "name": "a SysType's file adds to Common's", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"local\": {\"path\": \"../../local\"}}, \"builtin\": {\"include\": [\"raftcore:Alpha\"], \"policy\": {\"Alpha\": \"disabled\"}}, \"typeFile\": {\"include\": [\"local:Delta\"]}}", + "systypes/Main/DeviceTypeSources.json": "{\"sources\": {\"local\": {\"path\": \"../../local\"}}, \"builtin\": {\"include\": [\"raftcore:Bravo\"], \"policy\": {\"Alpha\": \"enabled\"}}, \"typeFile\": {\"policy\": {\"Charlie\": \"disabled\"}}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + }, + "local": { + "kind": "path", + "records": "{\"devTypes\": {\"Delta\": {\"addresses\": \"0x13\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + }, + "Bravo": { + "source": "raftcore", + "record": "{\"addresses\":\"0x10\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Bravo core\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"builtin\":{\"include\":[\"raftcore:Alpha\",\"raftcore:Bravo\"],\"policy\":{\"Alpha\":\"enabled\"}}}" + }, + { + "name": "a type file: its records and policies for compiled types", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"local\": {\"path\": \"../../local\"}}, \"typeFile\": {\"include\": [\"local:*\"], \"policy\": {\"Charlie\": \"disabledClaim\", \"Delta\": \"disabled\"}}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + }, + "local": { + "kind": "path", + "records": "{\"devTypes\": {\"Delta\": {\"addresses\": \"0x13\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Echo\": {\"addresses\": \"0x14\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "typeFile", + "records": { + "Delta": { + "source": "local", + "record": "{\"addresses\":\"0x13\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"},\"disable\":true}" + }, + "Echo": { + "source": "local", + "record": "{\"addresses\":\"0x14\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": {}, + "policyOnly": { + "Charlie": "disabledClaim" + }, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"local\":{\"path\":\"../../local\"}},\"typeFile\":{\"include\":[\"local:*\"],\"policy\":{\"Charlie\":\"disabledClaim\",\"Delta\":\"disabled\"}}}" + }, + { + "name": "a type file includes nothing unless it says so", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "typeFile", + "records": {}, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{},\"typeFile\":{}}" + }, + { + "name": "a type file's provenance: only the sources it uses", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"local\": {\"path\": \"../../local\"}}, \"typeFile\": {\"include\": [\"raftcore:Alpha\"], \"exclude\": [\"local:*\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + }, + "local": { + "kind": "path", + "records": "{\"devTypes\": {\"Delta\": {\"addresses\": \"0x13\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Echo\": {\"addresses\": \"0x14\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "typeFile", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"typeFile\":{\"include\":[\"raftcore:Alpha\"],\"exclude\":[\"local:*\"]}}" + } + ] +} diff --git a/testdata/typefile_vectors.json b/testdata/typefile_vectors.json new file mode 100644 index 0000000..787d647 --- /dev/null +++ b/testdata/typefile_vectors.json @@ -0,0 +1,881 @@ +{ + "_doc": "Device type file build vectors - see make_typefile_vectors.py", + "vectors": [ + { + "name": "every RaftCore record, new", + "records": "{\"devTypes\":{\"ADXL313\":{\n \"addresses\": \"0x1d,0x53\",\n \"detectionValues\": \"0x00=0xAD1DCB\",\n \"initValues\": \"0x310b=&0x2d08=\",\n \"pollInfo\": {\n \"c\": \"0x32=r6\",\n \"i\": 100,\n \"s\": 3\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer\",\n \"desc\": \"3-Axis Accel\",\n \"manu\": \"Analog Devices\",\n \"type\": \"ADXL313\",\n \"clas\": [\"ACC\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"ax\",\n \"t\": \"I\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"m\": \"0xfffff000\",\n \"s\": 12,\n \"d\": 10485.76,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"temperature\",\n \"at\": 2,\n \"t\": \">I\",\n \"u\": \"°C\",\n \"r\": [-40, 85],\n \"m\": \"0x000fffff\",\n \"d\": 5242.88,\n \"a\": -50,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"AMG8833\":{\n \"addresses\": \"0x69\",\n \"_notes\": \"default settings on power on: normal (not sleep) mode, 10 frames per second, no interrupt, moving average off\",\n \"detectionValues\": \"\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x80=r128\",\n \"i\": 100,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Thermal Camera\",\n \"desc\": \"\",\n \"manu\": \"Panasonic\",\n \"type\": \"AMG8833\",\n \"clas\": [\"TCAM\"],\n \"resp\": {\n \"b\": 128,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"H\",\n \"u\": \"degrees\",\n \"r\": [0, 360],\n \"f\": \"3.1f\",\n \"o\": \"float\",\n \"d\": 11.377777777777778\n }\n ]\n }\n }\n}\n,\"AdafruitGamepad\":{\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x0001=0b10000111\",\n \"initValues\": \"0x007fff=p20&0x010300010067=p1&0x010b00010067=p1&0x010500010067=p1\",\n \"pollInfo\": {\n \"c\": \"0x0915=r2&0x0916=r2&0x0104=r4\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"readEffect\", \"basis\": \"source\", \"note\": \"ATtiny seesaw: shipped firmware has no handler for 0x99 (read NACKed); library 2.0.0 discards one-byte writes, so the read answers for the previous register and, if that was GPIO bulk, clears the interrupt flags (Adafruit_seesawPeripheral 8070dfb, f7f738d)\"}},\n \"devInfoJson\": {\n \"name\": \"Adafruit Gamepad\",\n \"desc\": \"\",\n \"manu\": \"Adafruit\",\n \"type\": \"Gamepad\",\n \"clas\": [\"GAME\"],\n \"resp\": {\n \"b\": 8,\n \"a\": [\n {\n \"n\": \"x\",\n \"t\": \">H\",\n \"r\": [0, 65535],\n \"d\": -1,\n \"a\": 1023,\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"y\",\n \"t\": \">H\",\n \"r\": [0, 65535],\n \"d\": -1,\n \"a\": 1023,\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"SELECT\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x01\",\n \"m\": \"0x01\",\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"B\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x02\",\n \"m\": \"0x02\",\n \"s\": 1,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"Y\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x04\",\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"A\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x20\",\n \"m\": \"0x20\",\n \"s\": 5,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"X\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x40\",\n \"m\": \"0x40\",\n \"s\": 6,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"START\",\n \"at\": 5,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x01\",\n \"m\": \"0x01\",\n \"f\": \"b\",\n \"o\": \"bool\"\n }\n ]\n }\n }\n}\n,\"AdafruitSoilSensor\":{\n \"addresses\": \"0x36\",\n \"_notes\": \"sensor must be wrapped in heatshrink to produce sensible values. very sensitive to human proximity\",\n \"detectionValues\": \"0x0001=p25&=0x55\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x0F10=p250&=r2&0x0004=p250&=r4\",\n \"i\": 1000,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"SAMD09 seesaw: 0x99 isn't a module base, so the command is ignored and the read returns 0xFF or stale reply bytes (adafruit/seesaw 20e2892)\"}},\n \"devInfoJson\": {\n \"name\": \"Soil Moisture Sensor\",\n \"desc\": \"\",\n \"manu\": \"Adafruit\",\n \"type\": \"AdafruitSoilSensor\",\n \"clas\": [\"SOIL\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"moisture\",\n \"t\": \">H\",\n \"u\": \"\",\n \"r\": [0, 65535],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"temperature\",\n \"t\": \">i\",\n \"u\": \"°C\",\n \"r\": [-45, 175],\n \"d\": 65536,\n \"f\": \".2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"BLEBTHome\":{\n \"detectionValues\": \"\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"=r17\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"BLE Sensor (BTHome)\",\n \"desc\": \"\",\n \"manu\": \"Allterco\",\n \"type\": \"BLEBTHome\",\n \"clas\": [\"BTHM\"],\n \"resp\": {\n \"b\": 17,\n \"a\": [\n {\n \"n\": \"ID\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0,255],\n \"f\": \"d\",\n \"o\": \"uint8\"\n },\n {\n \"n\": \"MAC\",\n \"t\": \">Q\",\n \"o\": \"uint64\"\n },\n {\n \"n\": \"motion\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0,1],\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"battery\",\n \"t\": \"B\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"f\": \"d\",\n \"o\": \"uint8\"\n },\n {\n \"n\": \"temp\",\n \"t\": \">h\",\n \"u\": \"°C\",\n \"r\": [-100,100],\n \"d\": 100,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"light\",\n \"t\": \">I\",\n \"u\": \"Lux\",\n \"r\": [0,100000],\n \"d\": 100,\n \"f\": \".1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"BME280\":{\n \"_notes\": \"Bosch BME280 (temperature, pressure, humidity) at 0x76 (SDO low) or 0x77 (SDO high, Adafruit default). Detection: chip ID (0xD0) 0x60 - a BMP280 (0x56-0x58) or BME680 (0x61) doesn't match. Init: soft reset (0xE0B6), 5 ms; config (0xF5) standby 500 ms, filter off; ctrl_hum (0xF2) humidity x1 - written before ctrl_meas, which applies it; ctrl_meas (0xF4) temperature x1, pressure x1, normal mode. Poll: the calibration (0x88, 26 bytes; 0xE1, 7 bytes) and the data (0xF7, 8 bytes) - the decode code applies Bosch's floating-point compensation (datasheet 8.1). 0x76 is also a common multiplexer address (Axiom018's), where the BME280 can't be used.\",\n \"addresses\": \"0x76-0x77\",\n \"detectionValues\": \"0xd0=0x60\",\n \"initValues\": \"0xe0b6=p5&0xf580&0xf201&0xf427\",\n \"pollInfo\": {\n \"c\": \"0x88=r26&0xe1=r7&0xf7=r8\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature, Pressure & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Bosch\",\n \"type\": \"BME280\",\n \"clas\": [\"TEMP\",\"PRES\",\"RH\"],\n \"resp\": {\n \"b\": 41,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"32767){T2=T2-65536;}int T3=buf[4]|(buf[5]<<8);if(T3>32767){T3=T3-65536;}int P1=buf[6]|(buf[7]<<8);int P2=buf[8]|(buf[9]<<8);if(P2>32767){P2=P2-65536;}int P3=buf[10]|(buf[11]<<8);if(P3>32767){P3=P3-65536;}int P4=buf[12]|(buf[13]<<8);if(P4>32767){P4=P4-65536;}int P5=buf[14]|(buf[15]<<8);if(P5>32767){P5=P5-65536;}int P6=buf[16]|(buf[17]<<8);if(P6>32767){P6=P6-65536;}int P7=buf[18]|(buf[19]<<8);if(P7>32767){P7=P7-65536;}int P8=buf[20]|(buf[21]<<8);if(P8>32767){P8=P8-65536;}int P9=buf[22]|(buf[23]<<8);if(P9>32767){P9=P9-65536;}int H1=buf[25];int H2=buf[26]|(buf[27]<<8);if(H2>32767){H2=H2-65536;}int H3=buf[28];int E4=buf[29];if(E4>127){E4=E4-256;}int H4=E4*16+(buf[30]&15);int E6=buf[31];if(E6>127){E6=E6-256;}int H5=E6*16+(buf[30]>>4);int H6=buf[32];if(H6>127){H6=H6-256;}int aP=(buf[33]<<12)|(buf[34]<<4)|(buf[35]>>4);int aT=(buf[36]<<12)|(buf[37]<<4)|(buf[38]>>4);int aH=(buf[39]<<8)|buf[40];float v1=(aT/16384.0-T1/1024.0)*T2;float d=aT/131072.0-T1/8192.0;float v2=d*d*T3;float tf=v1+v2;out.temperature=tf/5120.0;v1=tf/2.0-64000.0;v2=v1*v1*P6/32768.0+v1*P5*2.0;v2=v2/4.0+P4*65536.0;v1=(P3*v1*v1/524288.0+P2*v1)/524288.0;v1=(1.0+v1/32768.0)*P1;float p=0.0;if(v1>0.0){p=1048576.0-aP;p=(p-v2/4096.0)*6250.0/v1;float w1=P9*p*p/2147483648.0;float w2=p*P8/32768.0;p=p+(w1+w2+P7)/16.0;}out.pressure=p/100.0;float h=tf-76800.0;h=(aH-(H4*64.0+H5/16384.0*h))*(H2/65536.0*(1.0+H6/67108864.0*h*(1.0+H3/67108864.0*h)));h=h*(1.0-H1*h/524288.0);if(h>100.0){h=100.0;}if(h<0.0){h=0.0;}out.humidity=h;\"\n }\n }\n }\n}\n,\"BME680\":{\n \"_notes\": \"Bosch BME680 (temperature, pressure, humidity, gas) at 0x76 (SDO low) or 0x77 (SDO high, Adafruit default). Detection: chip ID (0xD0) 0x61 and variant (0xF0) 0x00 - a BME688 (variant 0x01) has another data layout. Forced mode only, so each poll triggers a measurement (ctrl_meas 0x74 = 0x25: temperature and pressure x1, forced), waits 200 ms (TPH plus the 152 ms heater time) and reads the calibration (0x89 25 bytes, 0xE1 16 bytes, 0x00 5 bytes) and the data (0x1D 15 bytes); the decode code applies Bosch's floating-point compensation and gas-resistance calculation (range lookup tables as if-chains); gas is 0 unless gas valid and heater stable are both set. Init: soft reset, 10 ms; read the heater calibration; ctrl_hum 0x01; config 0x00 (filter off); gas_wait_0 0x66 (152 ms); res_heat_0; ctrl_gas_1 0x10 (run gas, heater profile 0). res_heat_0 is computed for each unit by initCode (Bosch's calc_res_heat): init reads par_g2/par_g1/par_g3 (0xEB-0xEE) and res_heat_val/res_heat_range (0x00-0x02), and the value for a 320 C hot plate at an assumed 25 C ambient is written to 0x5A ({heat}; the ambient temperature changes it by about 1 count over 15-35 C). 0x76 is also a common multiplexer address (Axiom018's).\",\n \"addresses\": \"0x76-0x77\",\n \"detectionValues\": \"0xd0=0x61&0xf0=0x00\",\n \"initValues\": \"0xe0b6=p10&0xeb=r4&0x00=r3&0x7201&0x7500&0x6466&0x5a{heat}&0x7110\",\n \"initCode\": {\n \"n\": \"bme680_heater\",\n \"c\": \"int g2=buf[0]|(buf[1]<<8);if(g2>32767){g2=g2-65536;}int g1=buf[2];if(g1>127){g1=g1-256;}int g3=buf[3];if(g3>127){g3=g3-256;}int rhv=buf[4];if(rhv>127){rhv=rhv-256;}int rhr=(buf[6]&48)>>4;float v1=g1/16.0+49.0;float v2=g2/32768.0*0.0005+0.00235;float v3=g3/1024.0;float v5=v1*(1.0+v2*320.0)+v3*25.0;init.heat=3.4*(v5*(4.0/(4.0+rhr))*(1.0/(1.0+rhv*0.002))-25.0);\"\n },\n \"pollInfo\": {\n \"c\": \"0x7425=p200&0x89=r25&0xe1=r16&0x00=r5&0x1d=r15\",\n \"i\": 3000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Environmental Sensor\",\n \"desc\": \"Temperature, pressure, humidity, gas\",\n \"manu\": \"Bosch\",\n \"type\": \"BME680\",\n \"clas\": [\"TEMP\",\"PRES\",\"RH\",\"GAS\"],\n \"resp\": {\n \"b\": 61,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"32767){t2=t2-65536;}int t3=buf[3];if(t3>127){t3=t3-256;}int p1=buf[5]|(buf[6]<<8);int p2=buf[7]|(buf[8]<<8);if(p2>32767){p2=p2-65536;}int p3=buf[9];if(p3>127){p3=p3-256;}int p4=buf[11]|(buf[12]<<8);if(p4>32767){p4=p4-65536;}int p5=buf[13]|(buf[14]<<8);if(p5>32767){p5=p5-65536;}int p7=buf[15];if(p7>127){p7=p7-256;}int p6=buf[16];if(p6>127){p6=p6-256;}int p8=buf[19]|(buf[20]<<8);if(p8>32767){p8=p8-65536;}int p9=buf[21]|(buf[22]<<8);if(p9>32767){p9=p9-65536;}int p10=buf[23];int h1=(buf[27]<<4)|(buf[26]&15);int h2=(buf[25]<<4)|(buf[26]>>4);int h3=buf[28];if(h3>127){h3=h3-256;}int h4=buf[29];if(h4>127){h4=h4-256;}int h5=buf[30];if(h5>127){h5=h5-256;}int h6=buf[31];int h7=buf[32];if(h7>127){h7=h7-256;}int rse=buf[45]&240;if(rse>127){rse=rse-256;}rse=rse/16;int aP=(buf[48]<<12)|(buf[49]<<4)|(buf[50]>>4);int aT=(buf[51]<<12)|(buf[52]<<4)|(buf[53]>>4);int aH=(buf[54]<<8)|buf[55];int aG=(buf[59]<<2)|(buf[60]>>6);int gr=buf[60]&15;float v1=(aT/16384.0-t1/1024.0)*t2;float d=aT/131072.0-t1/8192.0;float v2=d*d*t3*16.0;float tf=v1+v2;float tc=tf/5120.0;out.temperature=tc;v1=tf/2.0-64000.0;v2=v1*v1*p6/131072.0;v2=v2+v1*p5*2.0;v2=v2/4.0+p4*65536.0;v1=(p3*v1*v1/16384.0+p2*v1)/524288.0;v1=(1.0+v1/32768.0)*p1;float p=0.0;if(v1>0.0){p=1048576.0-aP;p=(p-v2/4096.0)*6250.0/v1;float w1=p9*p*p/2147483648.0;float w2=p*p8/32768.0;float q=p/256.0;float w3=q*q*q*p10/131072.0;p=p+(w1+w2+w3+p7*128.0)/16.0;}out.pressure=p/100.0;float e1=aH-(h1*16.0+h3/2.0*tc);float e2=e1*(h2/262144.0*(1.0+h4/16384.0*tc+h5/1048576.0*tc*tc));float hum=e2+(h6/16384.0+h7/2097152.0*tc)*e2*e2;if(hum>100.0){hum=100.0;}if(hum<0.0){hum=0.0;}out.humidity=hum;float k1=0.0;if(gr==5){k1=-1.0;}if(gr==7){k1=-0.8;}if(gr==10){k1=-0.2;}if(gr==11){k1=-0.5;}if(gr==13){k1=-1.0;}float k2=0.0;if(gr==4){k2=0.1;}if(gr==5){k2=0.7;}if(gr==7){k2=-0.8;}if(gr==8){k2=-0.1;}float g1=1340.0+5.0*rse;float g2=g1*(1.0+k1/100.0);float g3=1.0+k2/100.0;float gas=0.0;if((buf[60]&48)==48){gas=1.0/(g3*0.000000125*(1<32767){g2=g2-65536;}int g1=buf[2];if(g1>127){g1=g1-256;}int g3=buf[3];if(g3>127){g3=g3-256;}int rhv=buf[4];if(rhv>127){rhv=rhv-256;}int rhr=(buf[6]&48)>>4;float v1=g1/16.0+49.0;float v2=g2/32768.0*0.0005+0.00235;float v3=g3/1024.0;float v5=v1*(1.0+v2*320.0)+v3*25.0;init.heat=3.4*(v5*(4.0/(4.0+rhr))*(1.0/(1.0+rhv*0.002))-25.0);\"\n },\n \"pollInfo\": {\n \"c\": \"0x7425=p200&0x8a=r23&0xe1=r14&0x00=r5&0x1d=r17\",\n \"i\": 3000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Environmental Sensor\",\n \"desc\": \"Temperature, pressure, humidity, gas\",\n \"manu\": \"Bosch\",\n \"type\": \"BME690\",\n \"clas\": [\"TEMP\",\"PRES\",\"RH\",\"GAS\"],\n \"resp\": {\n \"b\": 59,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"127){t3=t3-256;}int p1=buf[10]|(buf[11]<<8);if(p1>32767){p1=p1-65536;}int p2=buf[12]|(buf[13]<<8);int p3=buf[14];if(p3>127){p3=p3-256;}int p4=buf[15];if(p4>127){p4=p4-256;}int p5=buf[4]|(buf[5]<<8);if(p5>32767){p5=p5-65536;}int p6=buf[6]|(buf[7]<<8);if(p6>32767){p6=p6-65536;}int p7=buf[8];if(p7>127){p7=p7-256;}int p8=buf[9];if(p8>127){p8=p8-256;}int p9=buf[18]|(buf[19]<<8);if(p9>32767){p9=p9-65536;}int p10=buf[20];if(p10>127){p10=p10-256;}int p11=buf[21];if(p11>127){p11=p11-256;}int h5=(buf[23]<<4)|(buf[24]>>4);if(h5>2047){h5=h5-4096;}int h1=(buf[25]<<4)|(buf[24]&15);if(h1>2047){h1=h1-4096;}int h2=buf[26];if(h2>127){h2=h2-256;}int h4=buf[27];if(h4>127){h4=h4-256;}int h3=buf[28];int h6=buf[29];int aP=(buf[44]<<16)|(buf[45]<<8)|buf[46];int aT=(buf[47]<<16)|(buf[48]<<8)|buf[49];int aH=(buf[50]<<8)|buf[51];int aG=(buf[57]<<2)|(buf[58]>>6);int gr=buf[58]&15;float cf=aT-t1*256.0;float T=cf*(t2/1073741824.0)+cf*cf*(t3/281474976710656.0);out.temperature=T;float o=p1*8.0;float tk10=p2/64.0;float tk20=p3/256.0;float tk30=p4/32768.0;float s=(p5-16384.0)/1048576.0;float tk1s=(p6-16384.0)/536870912.0;float tk2s=p7/4294967296.0;float tk3s=p8/137438953472.0;float nls=p9/281474976710656.0;float tknls=p10/281474976710656.0;float nls3=p11/36893488147419103232.0;float pf=aP*1.0;float P=o+tk10*T+tk20*T*T+tk30*T*T*T+pf*(s+tk1s*T+tk2s*T*T+tk3s*T*T*T)+pf*pf*(nls+tknls*T)+pf*pf*pf*nls3;out.pressure=P/100.0;float tc=T*5120.0-76800.0;float hoff=aH-(h1*64.0+h2/16384.0*tc);float k4=h4/67108864.0;float hs=hoff*(h5/65536.0)*(1.0+k4*tc+k4*(h3/67108864.0)*tc*tc);float H=hs*(1.0-h6/524288.0*hs);if(H<0.0){H=0.0;}if(H>100.0){H=100.0;}out.humidity=H;float gas=0.0;if((buf[58]&48)==48){gas=1000000.0*(262144>>gr)/(4096.0+3.0*(aG-512.0));}out.gas=gas;\"\n }\n }\n }\n}\n,\"CAP1188\":{\n \"_notes\": \"Microchip CAP1188 8-channel capacitive touch sensor with 8 LED drivers. Product ID 0x50, Manufacturer ID 0x5D. ADDR_COMM strap selects address in range 0x28-0x2D.\",\n \"addresses\": \"0x28-0x2d\",\n \"detectionValues\": \"0xfd=0x50&0xfe=0x5d\",\n \"initValues\": \"0x0000&0x1f36&0x2a00\",\n \"pollInfo\": {\n \"c\": \"0x02=r2&0x10=r8&0x0000\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Capacitive Touch\",\n \"desc\": \"8 channel\",\n \"manu\": \"Microchip\",\n \"type\": \"CAP1188\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 10,\n \"a\": [\n {\n \"n\": \"C1\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x01\",\n \"s\": 0,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C2\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x02\",\n \"s\": 1,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C3\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C4\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x08\",\n \"s\": 3,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C5\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x10\",\n \"s\": 4,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C6\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x20\",\n \"s\": 5,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C7\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x40\",\n \"s\": 6,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C8\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x80\",\n \"s\": 7,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"d1\",\n \"at\": 2,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d2\",\n \"at\": 3,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d3\",\n \"at\": 4,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d4\",\n \"at\": 5,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d5\",\n \"at\": 6,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d6\",\n \"at\": 7,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d7\",\n \"at\": 8,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d8\",\n \"at\": 9,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n }\n ]\n }\n }\n}\n,\"CAP1203\":{\n \"addresses\": \"0x28\",\n \"detectionValues\": \"0xfd=0b01101101\",\n \"initValues\": \"0x0000&0x1f06\",\n \"pollInfo\": {\n \"c\": \"0x02=r2&0x0000\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Capacitive Touch\",\n \"desc\": \"\",\n \"manu\": \"Sparkfun\",\n \"type\": \"CAP1203\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"A\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x01\",\n \"s\": 0,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"B\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x02\",\n \"s\": 1,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"status\",\n \"t\": \">H\",\n \"vs\": false,\n \"f\": \"04x\",\n \"o\": \"uint16\",\n \"bl\": {\"0x0001\": \"CS1\", \"0x0002\": \"CS2\", \"0x0004\": \"CS3\", \"0x0100\": \"TOUCH\", \"0x0200\": \"MTP\", \"0x0400\": \"MULT\", \"0x1000\": \"PWR\", \"0x2000\": \"ACAL_FAIL\", \"0x4000\": \"BC_OUT\"}\n }\n ]\n }\n }\n}\n,\"DPS310\":{\n \"_notes\": \"Infineon DPS310 barometric pressure and temperature sensor (Adafruit board) at 0x77 (default, SDO high) or 0x76. Also matches the Goertek SPL06-001, which has the same ID, registers and compensation. Detection: PRODUCT_ID (0x0D) = 0x10 (REV_ID 1, PROD_ID 0); a BME280/BME680/BME690 reads otherwise there (a BME690 0x04 on the bench). Init: soft reset (RESET 0x0C = 0x89), 50 ms for the coefficients to be ready; Infineon's temperature correction sequence (0x0E = 0xA5, 0x0F = 0x96, 0x62 = 0x02, 0x0E = 0, 0x0F = 0), without which some chips read temperatures about twice too high; PRS_CFG (0x06) 8 measurements/s, 16x oversampling (27.6 ms each); TMP_CFG (0x07) the external (MEMS) temperature sensor, 8/s, 1x; CFG_REG (0x09) P_SHIFT, needed above 8x pressure oversampling; MEAS_CFG (0x08) continuous pressure and temperature. TMP_CFG's sensor must match the one the coefficients were calibrated with (COEF_SRCE, 0x28 bit 7): the external one on every chip seen (0xA9 on the bench), which the init assumes. Poll: PSR_B2-B0 and TMP_B2-B0 (0x00, 6 bytes, 24-bit two's complement, big-endian) and the coefficients (0x10, 18 bytes); the decode code applies Infineon's compensation (datasheet 4.9) with the scale factors for 16x (pressure, 253952) and 1x (temperature, 524288) oversampling, so a change to PRS_CFG or TMP_CFG oversampling needs them changed too.\",\n \"addresses\": \"0x76-0x77\",\n \"detectionValues\": \"0x0d=0x10\",\n \"initValues\": \"0x0c89=p50&0x0ea5&0x0f96&0x6202&0x0e00&0x0f00&0x0634&0x07b0&0x0904&0x0807\",\n \"pollInfo\": {\n \"c\": \"0x00=r6&0x10=r18\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the lone 0x99 + 9-byte read (registers 0x99-0xA1, not in the map) read zeros; configuration, coefficients and COEF_SRCE unchanged and measuring carried on. Its addresses are in Axiom's excluded RSAO range anyway\"}},\n \"devInfoJson\": {\n \"name\": \"Pressure & Temperature Sensor\",\n \"desc\": \"\",\n \"manu\": \"Infineon\",\n \"type\": \"DPS310\",\n \"clas\": [\"PRES\",\"TEMP\"],\n \"resp\": {\n \"b\": 24,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"8388607){pr=pr-16777216;}int tr=(buf[3]<<16)|(buf[4]<<8)|buf[5];if(tr>8388607){tr=tr-16777216;}int c0=(buf[6]<<4)|(buf[7]>>4);if(c0>2047){c0=c0-4096;}int c1=((buf[7]&15)<<8)|buf[8];if(c1>2047){c1=c1-4096;}int c00=(buf[9]<<12)|(buf[10]<<4)|(buf[11]>>4);if(c00>524287){c00=c00-1048576;}int c10=((buf[11]&15)<<16)|(buf[12]<<8)|buf[13];if(c10>524287){c10=c10-1048576;}int c01=(buf[14]<<8)|buf[15];if(c01>32767){c01=c01-65536;}int c11=(buf[16]<<8)|buf[17];if(c11>32767){c11=c11-65536;}int c20=(buf[18]<<8)|buf[19];if(c20>32767){c20=c20-65536;}int c21=(buf[20]<<8)|buf[21];if(c21>32767){c21=c21-65536;}int c30=(buf[22]<<8)|buf[23];if(c30>32767){c30=c30-65536;}float ts=tr/524288.0;float ps=pr/253952.0;out.temperature=c0*0.5+c1*ts;float p=c00+ps*(c10+ps*(c20+ps*c30))+ts*c01+ts*ps*(c11+ps*c21);out.pressure=p/100.0;\"\n }\n }\n }\n}\n,\"ENS160\":{\n \"_notes\": \"ScioSense ENS160 digital metal-oxide multi-gas sensor (Adafruit and SparkFun boards) at 0x53 (default, ADDR high) or 0x52. Detection: PART_ID (0x00, 2 bytes little-endian) = 0x0160. Init: OPMODE (0x10) idle (0x01), then compensation TEMP_IN (0x13, kelvin x 64) = 25 degC and RH_IN (0x15, %RH x 512) = 50 %RH in one write, then OPMODE standard (0x02), which measures once a second. Poll (1 s): 6 bytes from DEVICE_STATUS (0x20): status, DATA_AQI (UBA air quality index 1-5, bits 2:0), DATA_TVOC (ppb) and DATA_ECO2 (ppm), both little-endian. Status: STATAS (bit 7) = an operating mode is running, STATER (bit 6) = error, VALIDITY (bits 3:2) 0 = normal, 1 = warm-up (the first 3 min after each power-on), 2 = initial start-up (the first hour of operation in the sensor's life), 3 = invalid output; NEWDAT (bit 1) = new data, NEWGPR (bit 0) = new general-purpose register data; the status labels are S (STATAS), E (STATER), D (NEWDAT) and G (NEWGPR), with the validity bits left to the validity attribute. Readings during warm-up or initial start-up are not yet reliable. The compensation actions take degC and %RH (e.g. from another sensor); the mode action switches between deep sleep, idle and standard.\",\n \"addresses\": \"0x52-0x53\",\n \"detectionValues\": \"0x00=0x6001\",\n \"initValues\": \"0x1001=p10&0x138a4a0064=&0x1002=p10\",\n \"pollInfo\": {\n \"c\": \"0x20=r6\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"readEffect\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the chip ignores bit 7 of the register address, so the lone 0x99 points at 0x19 and the 9-byte read covers 0x19-0x21, ending with DEVICE_STATUS and DATA_AQI. No configuration register changed (OPMODE, CONFIG, TEMP_IN, RH_IN) and measuring carried on; reading DATA_AQI may clear the NEWDAT flag (transient)\"}},\n \"devInfoJson\": {\n \"name\": \"Gas Sensor (VOC, eCO2)\",\n \"desc\": \"\",\n \"manu\": \"ScioSense\",\n \"type\": \"ENS160\",\n \"clas\": [\"VOC\",\"CO2\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"r\": [0,255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0x80\": \"S\", \"0x40\": \"E\", \"0x08\": \"\", \"0x04\": \"\", \"0x02\": \"D\", \"0x01\": \"G\"}\n },\n {\n \"n\": \"validity\",\n \"at\": 0,\n \"t\": \"B\",\n \"r\": [0,3],\n \"m\": \"0x0c\",\n \"s\": 2,\n \"f\": \"d\",\n \"o\": \"uint8\",\n \"vl\": {\"0\": \"normal\", \"1\": \"warm-up\", \"2\": \"start-up\", \"3\": \"invalid\"}\n },\n {\n \"n\": \"AQI\",\n \"at\": 1,\n \"t\": \"B\",\n \"u\": \"UBA\",\n \"r\": [1,5],\n \"m\": \"0x07\",\n \"f\": \"d\",\n \"o\": \"uint8\"\n },\n {\n \"n\": \"TVOC\",\n \"at\": 2,\n \"t\": \"I\",\n \"u\": \"N\",\n \"r\": [-10000,10000],\n \"m\": \"0x00ffffff\",\n \"d\": 44000,\n \"sb\": 23,\n \"f\": \".3f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0,1],\n \"vl\": {\"0\": \"no\", \"1\": \"yes\"}\n }\n ]\n }\n }\n}\n,\"LPS25\":{\n \"addresses\": \"0x5C-0x5D\",\n \"detectionValues\": \"0x0f=0b10111101\",\n \"initValues\": \"0x2104=p5&0x20C4=\",\n \"pollInfo\": {\n \"c\": \"0xA7=r6\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"lone 0x99 + 9-byte read (2026-10-23): pointer to 0x19 (reserved), no configuration register changed, pressure unchanged\"}},\n \"devInfoJson\": {\n \"name\": \"Pressure Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"LPS25\",\n \"clas\": [\"PRES\",\"TEMP\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"r\": [0,255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0x01\": \"T_DA\", \"0x02\": \"P_DA\", \"0x10\": \"T_OR\", \"0x20\": \"P_OR\"}\n },\n {\n \"n\": \"pressure\",\n \"at\": 0,\n \"t\": \"6.6 s filled the 4096-word FIFO, the count read 0 and the {$0.w12} poll read nothing forever - devdocs/lsm6ds-fifo-full-deadlock.md). The _conf.rate entries keep the watermark. Gyro 70 mdps/LSB at 2000 dps.\",\n \"addresses\": \"0x6a-0x6b\",\n \"detectionValues\": \"0x0f=0x69\",\n \"initValues\": \"0x0a00&0x1244&0x1048&0x114c&0x1640&0x0809&0x0900&0x1301&0x06fc&0x0703&0x0a26\",\n \"pollInfo\": {\n \"c\": \"0x3a=r4&0x3e=r{$0.w12:mask0FFF*2:max240}\",\n \"i\": 50,\n \"s\": 20\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer & Gyroscope\",\n \"desc\": \"6-Axis IMU\",\n \"manu\": \"ST\",\n \"type\": \"LSM6DS3\",\n \"clas\": [\"ACC\", \"GYRO\"],\n \"resp\": {\n \"b\": 244,\n \"a\": [\n {\n \"n\": \"gx\",\n \"t\": \"maxN){N=maxN;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(i6.6 s filled the 4096-word FIFO, the count read 0 and the {$0.w12} poll read nothing forever - devdocs/lsm6ds-fifo-full-deadlock.md). The _conf.rate entries keep the watermark. Gyro 70 mdps/LSB at 2000 dps.\",\n \"addresses\": \"0x6a-0x6b\",\n \"detectionValues\": \"0x0f=0x6a\",\n \"initValues\": \"0x0a00&0x1244&0x1048&0x114c&0x1640&0x0809&0x0980&0x06fc&0x0703&0x0a26\",\n \"pollInfo\": {\n \"c\": \"0x3a=r4&0x3e=r{$0.w12:mask0FFF*2:max240}\",\n \"i\": 50,\n \"s\": 20\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer & Gyroscope\",\n \"desc\": \"6-Axis IMU\",\n \"manu\": \"ST\",\n \"type\": \"LSM6DS3TRC\",\n \"clas\": [\"ACC\", \"GYRO\"],\n \"resp\": {\n \"b\": 244,\n \"a\": [\n {\n \"n\": \"gx\",\n \"t\": \"maxN){N=maxN;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(iH\",\n \"u\": \"V\",\n \"r\": [0, 5.12],\n \"d\": 12800,\n \"f\": \".3f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"charge\",\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"d\": 256,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"chargeRate\",\n \"t\": \">h\",\n \"u\": \"%/hr\",\n \"r\": [-300, 300],\n \"d\": 4.808,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"at\": 6,\n \"r\": [0, 255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0\": \"OK\", \"0x01\": \"RI\", \"0x02\": \"VH\", \"0x04\": \"VL\", \"0x08\": \"VR\", \"0x10\": \"HD\", \"0x20\": \"SC\", \"0x40\": \"\"}\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"config\",\n \"t\": \">H\",\n \"w\": \"0c\",\n \"f\": \"B\",\n \"r\": [0, 65535],\n \"d\": 38678,\n \"desc\": \"CONFIG register\"\n },\n {\n \"n\": \"reset config\",\n \"w\": \"0c9716\",\n \"desc\": \"Reset CONFIG\"\n }\n ]\n }\n}\n,\"MAX30101\":{\n \"_notes\": \"Maxim MAX30101 / MAX30105 pulse oximetry, heart-rate and particle sensor at 0x57 (fixed). The MAX30102 has the same part ID but no green LED (its green values are meaningless). Detection: PART_ID (0xFF) = 0x15 (all three). Init: reset (0x09 bit 6), 10 ms; MODE_CONFIG (0x09) multi-LED mode (7); FIFO_CONFIG (0x08) 4-sample averaging, rollover on, almost-full 15; INT status 1 read (clears it); SPO2_CONFIG (0x0A) ADC range 4096 nA, 100 samples/s, 411 us pulses (18 bits); LED1-LED3 pulse amplitudes (0x0C-0x0E, red, IR, green) 0x40 = 12.8 mA (0.2 mA per step); slots (0x11-0x12) red, IR, green, none; FIFO pointers cleared (0x04-0x06). Poll: FIFO_WR_PTR, OVF_COUNTER, FIFO_RD_PTR, then 8 samples of 9 bytes from FIFO_DATA (the register pointer stays at 0x07, so one read covers both), 18 bits per LED; then FIFO_RD_PTR again. The decode keeps the samples the read popped (RD_PTR after minus before): a read of an empty FIFO returns the last sample again without moving RD_PTR, and a sample that arrives during the read (about 8 ms) is popped too - counting from the pointers before the read dropped it, about one sample in five at 100/s (bench, 2026-10-24). The rate is the 100 samples/s divided by the on-chip averaging (1-16), each rate polled often enough to keep under 8 samples a poll. In multi-LED mode an LED is turned off by setting its current to 0, so the LED actions also choose what is measured (red only for heart rate, red and IR for SpO2, all three). Bench (Axiom018, 2026-10-24, MAX30105): with nothing in front about 1500 (red) and 2400 (IR), a fingertip about 196000 with a 68 bpm pulse of about 1700 p-p in IR.\",\n \"addresses\": \"0x57\",\n \"detectionValues\": \"0xff=0x15\",\n \"initValues\": \"0x0940=p10&0x0907&0x085f&0x00=r1&0x0a27&0x0c404040&0x112103&0x04000000\",\n \"pollInfo\": {\n \"c\": \"0x04=r75&0x06=r1\",\n \"i\": 200,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\n \"rsao\": {\n \"effect\": \"none\",\n \"basis\": \"bench\",\n \"note\": \"bench (Axiom018, 2026-10-24, MAX30105): the lone 0x99 + 9-byte read (0x99-0xA1, not in the register map) read 99 af ff ff ff ff ff 00 00; FIFO, mode, SpO2, LED, slot and interrupt-enable registers unchanged and sampling carried on\"\n }\n },\n \"devInfoJson\": {\n \"name\": \"Heart & Optical Sensor\",\n \"desc\": \"Red, IR and green reflectance: heart rate, SpO2, proximity and (MAX30105) particles\",\n \"manu\": \"Maxim\",\n \"type\": \"MAX30101\",\n \"clas\": [\n \"HRM\"\n ],\n \"resp\": {\n \"b\": 76,\n \"a\": [\n {\n \"n\": \"Red\",\n \"t\": \">I\",\n \"u\": \"Red\",\n \"r\": [\n 0,\n 262143\n ],\n \"f\": \"6d\",\n \"o\": \"uint32\"\n },\n {\n \"n\": \"IR\",\n \"t\": \">I\",\n \"u\": \"IR\",\n \"r\": [\n 0,\n 262143\n ],\n \"f\": \"6d\",\n \"o\": \"uint32\"\n },\n {\n \"n\": \"Green\",\n \"t\": \">I\",\n \"u\": \"Green\",\n \"r\": [\n 0,\n 262143\n ],\n \"f\": \"6d\",\n \"o\": \"uint32\"\n }\n ],\n \"c\": {\n \"n\": \"max30101_fifo\",\n \"c\": \"int N=(buf[75]+32-buf[2])%32;if(N>8){N=8;}int k=3;int i=0;while(iH\",\n \"u\": \"°C\",\n \"r\": [-40,125],\n \"m\": \"0x1fff\",\n \"d\": 16,\n \"sb\": 12,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MICS6814Pimoroni\":{\n \"_notes\": \"SGX MiCS-6814 gas sensor (reducing gases e.g. CO, NH3, oxidising gases e.g. NO2) on Pimoroni's breakout at 0x19: a Nuvoton MS51 running Pimoroni's IO Expander firmware reads the three sensing elements with its ADC and switches the heater (as Pimoroni's mics6814 and ioexpander Python libraries do). Detection: the IO Expander chip ID (0xFA-0xFB) 0xE26A - shared by Pimoroni's IO Expander boards; 0x19 is the MICS6814's address. Init: read the pin mode registers P0M1, P0M2, P1M1, P1M2 (0x71-0x74) and ADCCON1 (0xA1); initCode computes read-modify-write values (the same registers set the modes of the pins the firmware itself uses): P0.5-P0.7 input-only for the ADC (AIN4, AIN3, AIN2), P1.5 push-pull for the heater enable; then P1.5 low (bit-addressed P1 write 0x05: heater on) and the ADC enabled. Poll: for each of reducing (AIN2), NH3 (AIN3), oxidising (AIN4): select the channel (AINDIDS0 0xB6), start a conversion (ADCCON0 0xA8 = 0x40 | channel), 2 ms, read ADCRL/ADCRH (12 bits: low nibble of 0x82, high 8 bits in 0x83). Decode: each element's resistance R = raw * 56000 / (4095 - raw) ohms (the board's 56k divider, as the library). The heater needs some minutes to warm up before the resistances settle.\",\n \"addresses\": \"0x19\",\n \"detectionValues\": \"0xfa=0x6ae2\",\n \"initValues\": \"0x71=r1&0x72=r1&0x73=r1&0x74=r1&0xa1=r1&0x71{p0m1}&0x72{p0m2}&0x73{p1m1}&0x74{p1m2}&0x5005&0xa1{adccon1}\",\n \"initCode\": {\n \"n\": \"mics6814_pins\",\n \"c\": \"init.p0m1=buf[0]|224;init.p0m2=buf[1]&31;init.p1m1=buf[2]&223;init.p1m2=buf[3]|32;init.adccon1=buf[4]|1;\"\n },\n \"pollInfo\": {\n \"c\": \"0xb604&0xa842=p2&0x82=r2&0xb608&0xa843=p2&0x82=r2&0xb610&0xa844=p2&0x82=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"inferred\", \"note\": \"Pimoroni IO Expander firmware (not public): a one-byte write only sets the pointer; 0x99-0xA1 are PWM and ADC configuration registers with no read side effect (ioe-python register map)\"}},\n \"devInfoJson\": {\n \"name\": \"Gas Sensor\",\n \"desc\": \"Reducing, NH3 and oxidising gases\",\n \"manu\": \"Pimoroni\",\n \"type\": \"MICS6814Pimoroni\",\n \"clas\": [\"GAS\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"reducing\",\n \"t\": \"H\",\n \"u\": \"µT\",\n \"r\": [\n -3000,\n 3000\n ],\n \"d\": 10,\n \"a\": -3276.8,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"y\",\n \"t\": \">H\",\n \"u\": \"µT\",\n \"r\": [\n -3000,\n 3000\n ],\n \"d\": 10,\n \"a\": -3276.8,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"z\",\n \"t\": \">H\",\n \"u\": \"µT\",\n \"r\": [\n -3000,\n 3000\n ],\n \"d\": 10,\n \"a\": -3276.8,\n \"f\": \".1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MPRLS\":{\n \"_notes\": \"Honeywell MPRLS 0025PA (0-25 psi, transfer function A: 10-90% of 2^24 counts). No detection values, so the record claims any device at 0x18; the MCP9808 and Robotical servo/pump records, which share the address and have detection values, are tried first (detection strength order).\",\n \"addresses\": \"0x18\",\n \"detectionValues\": \"\",\n \"initValues\": \"0xAA0000\",\n \"pollInfo\": {\n \"c\": \"=r4&0xAA0000\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Pressure Sensor\",\n \"desc\": \"\",\n \"manu\": \"Honeywell\",\n \"type\": \"MPRLS\",\n \"clas\": [\"PRES\"],\n \"resp\": {\n \"b\": 4,\n \"a\": [\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"r\": [0, 255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0x01\": \"saturated\", \"0x04\": \"mem error\", \"0x20\": \"busy\", \"0x40\": \"powered\"}\n },\n {\n \"n\": \"pressure\",\n \"at\": 0,\n \"m\": \"0x00ffffff\",\n \"t\": \">I\",\n \"u\": \"hPa\",\n \"r\": [0, 1724],\n \"a\": -215.461,\n \"d\": 7786.654,\n \"f\": \"4.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MS8607PT\":{\n \"_notes\": \"TE MS8607 pressure and temperature part at 0x76 (fixed; the humidity part is MS8607RH at 0x40). Commands, not registers: reset 0x1E, convert pressure 0x40-0x4A / temperature 0x50-0x5A (by oversampling), ADC read 0x00 (3 bytes), PROM read 0xA0-0xAC (7 words: word 0 holds a CRC-4, words 1-6 the coefficients C1-C6). Detection: an ADC read, then a second one, which returns 0 whatever went before (datasheet: a repeated ADC read gives 0) - a pattern rather than an ID (a BME280's register 0x00 also reads 0), so it is tried after the records with an ID (identPriority -1). Init: reset, 5 ms. Poll: pressure conversion at OSR 8192 (0x4A, max 16.4 ms), 20 ms (the address is barred), read; temperature conversion (0x5A), 20 ms, read; C1-C6. Decode: TE's first- and second-order compensation (also right for the MS5637 and MS5837-02BA, which share the maths; not the MS5611 or MS5837-30BA). The PROM CRC-4 isn't checked (the grammar can't). 0x76 is also a common multiplexer address (Axiom018 reserved it until 2026-10-18).\",\n \"addresses\": \"0x76\",\n \"identPriority\": -1,\n \"detectionValues\": \"0x00=r3&0x00=0x000000\",\n \"initValues\": \"0x1e=p5\",\n \"pollInfo\": {\n \"c\": \"0x4a=p20&0x00=r3&0x5a=p20&0x00=r3&0xa2=r2&0xa4=r2&0xa6=r2&0xa8=r2&0xaa=r2&0xac=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Pressure & Temperature Sensor\",\n \"desc\": \"MS8607 pressure part\",\n \"manu\": \"TE\",\n \"type\": \"MS8607PT\",\n \"clas\": [\"PRES\",\"TEMP\"],\n \"resp\": {\n \"b\": 18,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"=2000.0){t2=5.0*dT*dT/274877906944.0;}t=t-t2;off=off-off2;sens=sens-sens2;float p=(D1*sens/2097152.0-off)/32768.0;out.temperature=t/100.0;out.pressure=p/100.0;\"\n }\n }\n }\n}\n,\"MS8607RH\":{\n \"_notes\": \"TE MS8607 humidity part at 0x40 (fixed; the pressure and temperature part is MS8607PT at 0x76). HTU21-style commands: reset 0xFE, measure RH without clock stretching 0xF5 (with stretching, 0xE5, holds the clock longer than the bus timeout), user register read 0xE7. Detection: the user register's fixed bits (reserved bits 5:3 = 0, bit 1 = 1 by default) - weak, so it is tried after the records with an ID (identPriority -1); 0x40 is also used by parts with no record (INA219, PCA9685). Init: reset, 15 ms. Poll: measure (0xF5), 20 ms (max 16 ms at 12 bits; the pause bars the address, as an address probe between trigger and read loses the result), read 3 bytes (RH, CRC with init 0x00 - not checked). RH = -6 + 125*raw/2^16 (status bits 1:0 masked), not temperature-compensated (the MS8607 compensation uses the temperature from the other part: RH + (20 - T) * -0.18).\",\n \"addresses\": \"0x40\",\n \"identPriority\": -1,\n \"detectionValues\": \"0xe7=0bX000XX1X\",\n \"initValues\": \"0xfe=p15\",\n \"pollInfo\": {\n \"c\": \"0xf5=p20&=r3\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Humidity Sensor\",\n \"desc\": \"MS8607 humidity part\",\n \"manu\": \"TE\",\n \"type\": \"MS8607RH\",\n \"clas\": [\"RH\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"humidity\",\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"m\": \"0xfffc\",\n \"d\": 524.288,\n \"a\": -6,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MT6701\":{\n \"addresses\": \"0x06\",\n \"detectionValues\": \"\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x03=r2\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Magnetic Encoder\",\n \"desc\": \"\",\n \"manu\": \"MagnTek\",\n \"type\": \"MT6701\",\n \"clas\": [\"ANG\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"angle\",\n \"t\": \">H\",\n \"u\": \"degrees\",\n \"r\": [0, 360],\n \"f\": \"3.1f\",\n \"o\": \"float\",\n \"d\": 182.04444444444444\n }\n ]\n }\n }\n}\n,\"MXC400xXC\":{\n \"_notes\": \"x in part code sets i2c address LSB from 0x10-0x17\",\n \"addresses\": \"0x10-0x17\",\n \"detectionValues\": \"0x0E=0x02,0x03\",\n \"initValues\": \"0x0110=p5&0x0d00=\",\n \"pollInfo\": {\n \"c\": \"0x03=r7\",\n \"i\": 100,\n \"s\": 3\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer\",\n \"desc\": \"3-Axis Accel\",\n \"manu\": \"Memsic\",\n \"type\": \"MXC400xXC\",\n \"clas\": [\"ACC\"],\n \"resp\": {\n \"b\": 7,\n \"a\": [\n {\n \"n\": \"ax\",\n \"t\": \">h\",\n \"u\": \"g\",\n \"r\": [-2.0, 2.0],\n \"d\": 16384,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"ay\",\n \"t\": \">h\",\n \"u\": \"g\",\n \"r\": [-2.0, 2.0],\n \"d\": 16384,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"az\",\n \"t\": \">h\",\n \"u\": \"g\",\n \"r\": [-2.0, 2.0],\n \"d\": 16384,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"temperature\",\n \"t\": \"b\",\n \"u\": \"°C\",\n \"r\": [-40, 85],\n \"d\": 1.7065,\n \"a\": 25,\n \"f\": \".2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"PCT2075\":{\n \"_notes\": \"NXP PCT2075, LM75-compatible temperature sensor, 0.125 C, at any of 27 addresses (A0-A2 tri-state): 0x28-0x2F, 0x35-0x37 (Adafruit default 0x37), 0x48-0x4F, 0x70-0x77 - of which 0x70-0x73 are left out, as GravityO2 (no detection) claims them first. No ID register, so detection checks the registers' fixed bits (read-only): temperature low 5 bits 0 (11-bit, left-justified), configuration bits 7-5 0, Thyst and Tos low 7 bits 0 (9-bit), Tidle bits 7-5 0, and pointer 0x07 (no register) reads 0xFFFF - a part whose low registers read 0 (e.g. a BME280, which matched all the bit patterns) or a TMP117 (offset register 0x0000) fails that. Being a pattern rather than an ID, it is tried after the records with an ID at the same address (identPriority -1) - a TMP117 or an idle CAP1188 can pass it. Init: configuration 0 (normal mode, in case it was left shut down). Poll: temperature register; signed 16-bit / 256 (the low 5 bits are 0).\",\n \"addresses\": \"0x28-0x2F,0x35-0x37,0x48-0x4F,0x74-0x77\",\n \"identPriority\": -1,\n \"detectionValues\": \"0x00=0bXXXXXXXXXXX00000&0x01=0b000XXXXX&0x02=0bXXXXXXXXX0000000&0x03=0bXXXXXXXXX0000000&0x04=0b000XXXXX&0x07=0xFFFF\",\n \"initValues\": \"0x0100=\",\n \"pollInfo\": {\n \"c\": \"0x00=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature Sensor\",\n \"desc\": \"\",\n \"manu\": \"NXP\",\n \"type\": \"PCT2075\",\n \"clas\": [\"TEMP\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">h\",\n \"u\": \"°C\",\n \"r\": [-55, 125],\n \"d\": 256,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"QwiicButton\":{\n \"_notes\": \"SparkFun Qwiic Button (ATtiny84 with SparkFun firmware) at 0x6F by default (the address can be changed in software). The LED is a single colour (red or green, depending on the board), so it has brightness and pulse settings, not a colour. Every LED and debounce setting is saved in the button's EEPROM, so it survives a power cycle (a new button: brightness 255, no pulsing, debounce 10 ms). The pulse step must not be 0: the firmware divides by it. Poll: the status register (0x03), bit 2 = pressed; its clicked and event bits stay set until written back, so they aren't decoded.\",\n \"addresses\": \"0x6f\",\n \"detectionValues\": \"0x00=0b01011101\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x03=r1\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"changes\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, button on a mux slot, 2026-10-23): the lone 0x99 + 9-byte read fails with an I2C hardware timeout and the button drops off the bus for about 1.1 s, then is found and identified again; devices on other slots and the main bus are unaffected (the mux isolates the slot, the master times out after about 1.6 ms). Source: the read handler doesn't check the register pointer, so from 0x99 the length wraps to 65415 and the handler loops inside the I2C interrupt, holding the clock (Qwiic_Button e89a82f interrupts.ino 76-91). No setting changes. Its default address 0x6F is excluded from RSAO addressing\"}},\n \"devInfoJson\": {\n \"name\": \"QWIIC Button\",\n \"desc\": \"\",\n \"manu\": \"Sparkfun\",\n \"type\": \"QwiicButton\",\n \"clas\": [\"BTN\"],\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"press\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"LED\",\n \"desc\": \"LED presets: off, on (full brightness, steady) or breathe (full brightness, 1 s pulse, 0.5 s off)\",\n \"map\": {\n \"off\": {\"w\": \"1900&1b0000\"},\n \"on\": {\"w\": \"19ff&1b0000\"},\n \"breathe\": {\"w\": \"19ff&1a01&1be803&1df401\"}\n }\n },\n {\n \"n\": \"LED brightness\",\n \"desc\": \"LED brightness, 0-255 (when pulsing, the peak of each pulse)\",\n \"t\": \"B\",\n \"w\": \"19\",\n \"r\": [0,255],\n \"d\": 255\n },\n {\n \"n\": \"LED pulse step\",\n \"desc\": \"Brightness change at each step of a pulse, 1-255 (1 is smoothest)\",\n \"t\": \"B\",\n \"w\": \"1a\",\n \"r\": [1,255],\n \"d\": 1\n },\n {\n \"n\": \"LED pulse time\",\n \"desc\": \"Length of one pulse (ramp up and down) in ms; 0 = no pulsing, the LED stays at its brightness\",\n \"t\": \">7)&255;float co2=0.0;if(co2e>0){if(co2e<255){co2=1.0+(((co2h&127)*65536)+co2l)/8388608.0;int co2k=co2e-127;while(co2k>0){co2=co2*2.0;co2k=co2k-1;}while(co2k<0){co2=co2/2.0;co2k=co2k+1;}}}if(co2h>32767){co2=0.0-co2;}out.CO2=co2;int tmph=(buf[12]<<8)|buf[13];int tmpl=(buf[15]<<8)|buf[16];int tmpe=(tmph>>7)&255;float tmp=0.0;if(tmpe>0){if(tmpe<255){tmp=1.0+(((tmph&127)*65536)+tmpl)/8388608.0;int tmpk=tmpe-127;while(tmpk>0){tmp=tmp*2.0;tmpk=tmpk-1;}while(tmpk<0){tmp=tmp/2.0;tmpk=tmpk+1;}}}if(tmph>32767){tmp=0.0-tmp;}out.temperature=tmp;int humh=(buf[18]<<8)|buf[19];int huml=(buf[21]<<8)|buf[22];int hume=(humh>>7)&255;float hum=0.0;if(hume>0){if(hume<255){hum=1.0+(((humh&127)*65536)+huml)/8388608.0;int humk=hume-127;while(humk>0){hum=hum*2.0;humk=humk-1;}while(humk<0){hum=hum/2.0;humk=humk+1;}}}if(humh>32767){hum=0.0-hum;}out.humidity=hum;next;}\"\n }\n },\n \"actions\": [\n {\n \"n\": \"_conf.rate\",\n \"r\": [0.0167, 0.333],\n \"d\": 0.333,\n \"desc\": \"Sample rate (Hz)\",\n \"map\": {\n \"0.333\": {\n \"i\": 3000000,\n \"s\": 1\n },\n \"0.2\": {\n \"i\": 5000000,\n \"s\": 1\n },\n \"0.1\": {\n \"i\": 10000000,\n \"s\": 1\n },\n \"0.0333\": {\n \"i\": 30000000,\n \"s\": 1\n },\n \"0.0167\": {\n \"i\": 60000000,\n \"s\": 1\n }\n }\n },\n {\n \"n\": \"autoCal\",\n \"t\": \">H\",\n \"w\": \"5306\",\n \"crc\": \"crc8w\",\n \"r\": [0, 1],\n \"d\": 0,\n \"desc\": \"Auto-calibration\"\n },\n {\n \"n\": \"forceCal\",\n \"t\": \">H\",\n \"w\": \"5204\",\n \"crc\": \"crc8w\",\n \"r\": [400, 2000],\n \"d\": 420,\n \"desc\": \"Force cal (ppm)\"\n },\n {\n \"n\": \"tempOffset\",\n \"t\": \">H\",\n \"w\": \"5403\",\n \"crc\": \"crc8w\",\n \"mul\": 100,\n \"r\": [0, 20],\n \"d\": 0,\n \"desc\": \"Temp offset (C)\"\n },\n {\n \"n\": \"altitude\",\n \"t\": \">H\",\n \"w\": \"5102\",\n \"crc\": \"crc8w\",\n \"r\": [0, 3000],\n \"d\": 0,\n \"desc\": \"Altitude (m)\"\n },\n {\n \"n\": \"pressure\",\n \"t\": \">H\",\n \"w\": \"0010\",\n \"crc\": \"crc8w\",\n \"r\": [0, 1400],\n \"d\": 0,\n \"desc\": \"Pressure (mbar)\"\n },\n {\n \"n\": \"softReset\",\n \"w\": \"d304\",\n \"desc\": \"Soft reset\"\n }\n ]\n }\n}\n,\"SCD40\":{\n \"_notes\": \"Sensirion SCD40 CO2/temperature/humidity. No detection values (unique address 0x62). read_measurement (0xEC05) returns three big-endian words, each followed by a CRC byte: CO2 (ppm) at 0, T at 3 (-45 + 175*raw/65535), RH at 6 (100*raw/65535). Standard decode. temperatureOffset, altitude, autoCalibration and calibrationTarget are only accepted while periodic measurement is stopped; the record keeps it running, so at present the sensor ignores them. ambientPressure (hPa) overrides the altitude and is accepted during measurement.\",\n \"addresses\": \"0x62\",\n \"detectionValues\": \"\",\n \"initValues\": \"0x21b1=\",\n \"pollInfo\": {\n \"c\": \"0xec05=r9\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"CO2 Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SCD40\",\n \"clas\": [\"CO2\", \"TEMP\", \"RH\"],\n \"resp\": {\n \"b\": 9,\n \"a\": [\n {\n \"n\": \"CO2\",\n \"t\": \">H\",\n \"u\": \"ppm\",\n \"r\": [0, 40000],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"temperature\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-45, 175],\n \"d\": 374.4857142857143,\n \"a\": -45,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 6,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"d\": 655.35,\n \"f\": \".2f\",\n \"o\": \"float\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"temperatureOffset\",\n \"t\": \">H\",\n \"w\": \"241d\",\n \"crc\": \"crc8w\",\n \"mul\": 374.4857,\n \"r\": [0, 20],\n \"d\": 4,\n \"desc\": \"Temp offset (C)\"\n },\n {\n \"n\": \"altitude\",\n \"t\": \">H\",\n \"w\": \"2427\",\n \"crc\": \"crc8w\",\n \"r\": [0, 3000],\n \"d\": 0,\n \"desc\": \"Altitude (m)\"\n },\n {\n \"n\": \"ambientPressure\",\n \"t\": \">H\",\n \"w\": \"e000\",\n \"crc\": \"crc8w\",\n \"r\": [700, 1200],\n \"d\": 1013,\n \"desc\": \"Pressure (hPa)\"\n },\n {\n \"n\": \"autoCalibration\",\n \"t\": \">H\",\n \"w\": \"2416\",\n \"crc\": \"crc8w\",\n \"r\": [0, 1],\n \"d\": 1,\n \"desc\": \"Auto-calibration\"\n },\n {\n \"n\": \"calibrationTarget\",\n \"t\": \">H\",\n \"w\": \"243a\",\n \"crc\": \"crc8w\",\n \"r\": [0, 65535],\n \"d\": 400,\n \"desc\": \"ASC target (ppm)\"\n }\n ]\n }\n}\n,\"SGP30\":{\n \"_notes\": \"Sensirion SGP30 indoor air quality (eCO2 and TVOC) at 0x58 (fixed). Detection: get_feature_set (0x202F, 10 ms) - one word with its Sensirion CRC-8 (:crc8w) whose top 4 bits are the product type, 0 for the SGP30. Init: iaq_init (0x2003, 10 ms). Poll (1 s - measure_iaq must be called once a second for the chip's own dynamic baseline compensation): measure_iaq (0x2008), wait 12 ms, read eCO2 (ppm) and TVOC (ppb), each word followed by a CRC (not checked). For the first 15 s after iaq_init it reports 400 ppm and 0 ppb. setBaseline restores the IAQ baseline: TVOC baseline, then eCO2 baseline (the reverse of the order get_iaq_baseline returns them; the record has no action to read the baseline). setHumidity: absolute humidity for compensation, 8.8 fixed point in g/m3, 0 turns it off.\",\n \"addresses\": \"0x58\",\n \"detectionValues\": \"0x202f=p10&=0b0000XXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"0x2003=p10\",\n \"pollInfo\": {\n \"c\": \"0x2008=p12&=r6\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the lone 0x99 is NACKed (half a 16-bit command); the serial number, feature set and measurements are unchanged\"}},\n \"devInfoJson\": {\n \"name\": \"Gas Sensor (VOC)\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SGP30\",\n \"clas\": [\"VOC\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"eCO2\",\n \"t\": \">H\",\n \"u\": \"ppm\",\n \"r\": [400, 60000],\n \"f\": \"5d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"VOC\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"ppb\",\n \"r\": [0, 60000],\n \"f\": \"5d\",\n \"o\": \"uint16\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"setBaseline\",\n \"t\": \">HH\",\n \"w\": \"201e\",\n \"crc\": \"crc8w\",\n \"r\": [0, 65535],\n \"d\": 0,\n \"desc\": \"Set baseline\"\n },\n {\n \"n\": \"setHumidity\",\n \"t\": \">H\",\n \"w\": \"2061\",\n \"crc\": \"crc8w\",\n \"mul\": 256,\n \"r\": [0, 255],\n \"d\": 0,\n \"desc\": \"Humidity (g/m3)\"\n }\n ]\n }\n}\n,\"SGP40\":{\n \"_notes\": \"Sensirion SGP40 VOC sensor at 0x59 (fixed). Detection: get_serial_number (0x3682, 1 ms) - three data words, each with a Sensirion CRC-8 (:crc8w). Init: start a raw measurement - measure_raw_signal (0x260F) with the default compensation 50 %RH (0x8000, CRC 0xA2) and 25 degC (0x6666, CRC 0x93); the result is ready after 30 ms. Poll (1 s, the rate Sensirion's VOC algorithm expects): read the previous measurement (SRAW_VOC word + its CRC, which isn't checked), then start the next, so no poll waits for the measurement. The value is the raw signal in ticks: the VOC index (0-500) needs Sensirion's gas index algorithm, run by the host (e.g. the app). Compensation stays at the defaults: the record can't feed in another sensor's humidity and temperature.\",\n \"addresses\": \"0x59\",\n \"detectionValues\": \"0x3682=p1&=0bXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"0x260f8000a2666693\",\n \"pollInfo\": {\n \"c\": \"=r3&0x260f8000a2666693=\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the lone 0x99 is NACKed (half a 16-bit command), so nothing is sent on; the serial number and the measurement are unchanged\"}},\n \"devInfoJson\": {\n \"name\": \"SGP40\",\n \"desc\": \"Gas Sensor\",\n \"manu\": \"Sensirion\",\n \"type\": \"SGP40\",\n \"clas\": [\"VOC\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"vocTicks\",\n \"t\": \">H\",\n \"u\": \"ticks\",\n \"r\": [0, 65535],\n \"f\": \"5d\",\n \"o\": \"uint16\"\n }\n ]\n }\n }\n}\n,\"SHT31\":{\n \"_notes\": \"Sensirion SHT3x (SHT30, SHT31, SHT35 - same protocol) at 0x44 (ADDR low) or 0x45 (ADDR high), the SHT4x addresses: 2-byte commands, where the SHT4x has 1-byte ones, so each record's detection fails on the other part. Detection: read the status register (0xF32D) and require a valid CRC - only the CRC: the datasheet's reserved status bits aren't reliably 0 (bits 5 and 6 read 1 in periodic mode, status 0x8070), and the read works in both single-shot and periodic mode. Init: break (0x3093) in case periodic measurement was left running, which would make single-shot commands fail. Poll: single-shot, high repeatability, no clock stretching (0x2400), 16 ms (max 15.5 ms), then read 6 bytes: T_MSB,T_LSB,T_CRC,RH_MSB,RH_LSB,RH_CRC. The sensor NACKs while measuring; the pause after the trigger bars the address, so a detection starting then is retried rather than failing, and the sensor is idle between polls.\",\n \"addresses\": \"0x44-0x45\",\n \"detectionValues\": \"0xf32d=p1&=0bXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"0x3093=p2\",\n \"pollInfo\": {\n \"c\": \"0x2400=p16&=r6\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SHT31\",\n \"clas\": [\"TEMP\",\"RH\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-40,125],\n \"d\": 374.48571428571427,\n \"a\": -45,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"d\": 655.35,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"SHT40\":{\n \"_notes\": \"Sensirion SHT4x (SHT40, SHT41, SHT45 - indistinguishable over I2C) at 0x44-0x46. Detection: read the serial number (0x89, 10 ms) and require valid CRCs on both words. Poll: high-precision single-shot measurement (0xFD), 10 ms, then read 6 bytes: T_MSB,T_LSB,T_CRC,RH_MSB,RH_LSB,RH_CRC. The sensor NACKs while measuring (8.3 ms); the pause after the trigger bars the address, so a detection starting then is retried rather than failing, and the sensor is idle between polls.\",\n \"addresses\": \"0x44-0x46\",\n \"detectionValues\": \"0x89=p10&=0bXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0xfd=p10&=r6\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SHT40\",\n \"clas\": [\"TEMP\",\"RH\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-45,130],\n \"d\": 374.48571428571427,\n \"a\": -45,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"d\": 524.28,\n \"a\": -6,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"SHTC3\":{\n \"_notes\": \"Sensirion SHTC3 at 0x70 (fixed - also GravityO2's address and a common multiplexer address). Detection: wake-up (0x3517, harmless when awake, needed if left asleep), then the ID register (0xEFC8): bit 11 = 1 and bits 5-0 = 000111, with a valid CRC. Init: wake-up. Poll: measure, normal mode, T first, no clock stretching (0x7866), 13 ms (max 12.1 ms; the pause bars the address), then read 6 bytes: T_MSB,T_LSB,T_CRC,RH_MSB,RH_LSB,RH_CRC. T = -45 + 175*raw/2^16, RH = 100*raw/2^16.\",\n \"addresses\": \"0x70\",\n \"detectionValues\": \"0x3517=p1&0xefc8=p1&=0bXXXX1XXXXX000111XXXXXXXX:crc8w\",\n \"initValues\": \"0x3517=p1\",\n \"pollInfo\": {\n \"c\": \"0x7866=p13&=r6\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SHTC3\",\n \"clas\": [\"TEMP\",\"RH\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-40,125],\n \"d\": 374.4914285714286,\n \"a\": -45,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"d\": 655.36,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"STCC4\":{\n \"_notes\": \"Sensirion STCC4 miniature CO2 sensor (paired with SHT41 for RH/T compensation), fixed address 0x64. Detection is empty (unique address) - the product-ID command cannot be used because the detection path issues a single write+read with no inter-command delay and the STCC4 NACKs that read - so the record claims any device at 0x64. Init: start_continuous_measurement (0x218B). Poll every 1.5 s (longer than the 1 s measurement, so the read always has new data; at 1 s the two drift and a read is NACKed now and then): read_measurement (0xEC05), 1 ms pause, read 12 bytes = CO2/temp/humidity/status words each followed by a CRC byte (skipped via 'at'). CO2 is signed (i16 in the datasheet). Status: only bit 0x0040 (testing mode) is documented; after power-up 0x0100 was seen for about 2 minutes. Actions: testingMode (pauses ASC), pressure (hPa). Forced recalibration isn't an action: it needs measurement stopped (1.2 s), then FRC, then a restart. Datasheet: https://sensirion.com/media/documents/6AED4B15/69295E41/CD_DS_STCC4_D1.pdf. Rates (_conf.rate) only set the poll interval, 1.5 s to 60 s. CO2 raw = ppm; temperature = -45 + 175*s/65535; humidity = -6 + 125*s/65535. Must never be probed by the RSAO delegated-identification hook (its framed 0x99 command stops the sensor measuring); RaftI2C DeviceIdentMgr only runs that hook when no device-type record matches - this record's empty detection always matches, so the hook is never reached at 0x64. See devdocs/stcc4-identification-notes.md testingMode: 1 enable_testing_mode (0x3FBC), 0 disable_testing_mode (0x3F3D); ASC is always on otherwise. pressure: sent as Pa/2, reset to 1013 hPa at power-up.\",\n \"addresses\": \"0x64-0x65\",\n \"detectionValues\": \"\",\n \"initValues\": \"0x218b=\",\n \"pollInfo\": {\n \"c\": \"0xec05=p1&=r12\",\n \"i\": 1500,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"CO2 Sensor\",\n \"desc\": \"CO2, Temp & Humidity\",\n \"manu\": \"Sensirion\",\n \"type\": \"STCC4\",\n \"clas\": [\"CO2\", \"TEMP\", \"RH\"],\n \"resp\": {\n \"b\": 12,\n \"a\": [\n {\n \"n\": \"CO2\",\n \"at\": 0,\n \"t\": \">h\",\n \"u\": \"ppm\",\n \"r\": [0, 32000],\n \"f\": \"d\",\n \"o\": \"int16\"\n },\n {\n \"n\": \"temperature\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-45, 130],\n \"d\": 374.48571428571427,\n \"a\": -45,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 6,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"d\": 524.28,\n \"a\": -6,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"status\",\n \"at\": 9,\n \"t\": \">H\",\n \"u\": \"\",\n \"r\": [0, 65535],\n \"f\": \"04x\",\n \"o\": \"uint16\",\n \"vs\": false,\n \"bl\": {\"0\": \"OK\", \"0x0040\": \"testing\"}\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"_conf.rate\",\n \"r\": [0.0167, 0.667],\n \"d\": 0.667,\n \"desc\": \"Sample rate (Hz)\",\n \"map\": {\n \"0.667\": {\"i\": 1500000, \"s\": 1},\n \"0.5\": {\"i\": 2000000, \"s\": 1},\n \"0.2\": {\"i\": 5000000, \"s\": 1},\n \"0.1\": {\"i\": 10000000, \"s\": 1},\n \"0.0333\": {\"i\": 30000000, \"s\": 1},\n \"0.0167\": {\"i\": 60000000, \"s\": 1}\n }\n },\n {\n \"n\": \"testingMode\",\n \"map\": {\"0\": \"3f3d\", \"1\": \"3fbc\"},\n \"desc\": \"Testing mode\"\n },\n {\n \"n\": \"pressure\",\n \"t\": \">H\",\n \"w\": \"e016\",\n \"crc\": \"crc8w\",\n \"mul\": 50,\n \"r\": [400, 1100],\n \"d\": 1013,\n \"desc\": \"Pressure (hPa)\"\n }\n ]\n }\n}\n,\"TMP117\":{\n \"_notes\": \"High-Accuracy Temperature Sensor. Detection: 0x0F (DevID) = 0x0117. Init: 0x01 (Config) = 0x0220 (1s continuous mode, 8 avg). Poll: 0x00 (Temp) = 2 bytes.\",\n \"addresses\": \"0x48-0x4B\",\n \"detectionValues\": \"0x0F=0x0117\",\n \"initValues\": \"0x010220=\",\n \"pollInfo\": {\n \"c\": \"0x00=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature Sensor\",\n \"desc\": \"\",\n \"manu\": \"Texas Instruments\",\n \"type\": \"TMP117\",\n \"clas\": [\"TEMP\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">h\",\n \"u\": \"°C\",\n \"r\": [-55, 150],\n \"d\": 128,\n \"a\": 0,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"TrillBar\":{\n \"_notes\": \"Bela Trill Bar - 1D capacitive touch, 26 channels, up to 5 simultaneous touches. CENTROID mode. Identify cmd returns device_type=1 in byte 1 of 3-byte response. Address jumper-configurable 0x20-0x27.\",\n \"addresses\": \"0x20-0x27\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000001\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r20\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Bar\",\n \"desc\": \"Capacitive Touch Bar\",\n \"manu\": \"Bela\",\n \"type\": \"TrillBar\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 20,\n \"a\": [\n { \"n\": \"loc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillCraft\":{\n \"_notes\": \"Bela Trill Craft - 1D capacitive sensor strip, 30 channels, up to 5 simultaneous touches in CENTROID mode (default DIFF). Address jumper-configurable 0x30-0x37.\",\n \"addresses\": \"0x30-0x37\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000011\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r20\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Craft\",\n \"desc\": \"Capacitive Touch Craft\",\n \"manu\": \"Bela\",\n \"type\": \"TrillCraft\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 20,\n \"a\": [\n { \"n\": \"loc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillFlex\":{\n \"_notes\": \"Bela Trill Flex - flexible 1D capacitive touch with custom electrode connection, 30 channels, up to 5 touches in CENTROID mode (default DIFF). Address jumper-configurable 0x48-0x4F. Init, as Bela's Trill library (Trill-Linux) for the Flex: CENTROID mode; prescaler 4; scan settings speed 0, 12 bits; a baseline update; noise threshold 123 (0.03 at 12 bits). Poll: up to 5 touches, positions (loc, 0 to 3712 = 29 x 128, the first to the 30th channel; 65535 for no touch) then sizes (size, 0 for no touch; the library scales by 1200 at 12 bits), as 5-element arrays; the dashboard shows them with its touch strip visualizer (vt touchstrip). The prescaler matters: on the bench (Axiom018, 2026-10-23, with the flexible strip) the chip as left after the mode command read 4095 (saturated) on every channel, so no touch was ever seen; prescaler 2 read about 2100-2750 untouched with a finger saturating, 4 about 540-710 with a finger peaking at 1200-1950, and 8 about 0. Bela's library uses DIFF mode for the Flex and finds centroids on the host.\",\n \"addresses\": \"0x48-0x4F\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000110\",\n \"initValues\": \"0x000100=p15&0x000304=p15&0x0002000c=p15&0x0006=p30&0x00047b=p15\",\n \"pollInfo\": {\n \"c\": \"0x04=r20\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Flex\",\n \"desc\": \"Capacitive Touch Flex\",\n \"manu\": \"Bela\",\n \"type\": \"TrillFlex\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 20,\n \"a\": [\n {\n \"n\": \"loc\",\n \"t\": \">H[5]\",\n \"r\": [0, 3712],\n \"f\": \"d\",\n \"o\": \"uint16\",\n \"vt\": \"touchstrip\",\n \"vo\": {\"sizeAttr\": \"size\", \"noValue\": 65535, \"posMax\": 3712, \"sizeMax\": 1200, \"numChannels\": 30}\n },\n {\n \"n\": \"size\",\n \"t\": \">H[5]\",\n \"r\": [0, 65535],\n \"f\": \"d\",\n \"o\": \"uint16\"\n }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 123, \"desc\": \"Noise threshold (counts at 12 bits; 123 is the Trill library's 0.03)\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [1, 8], \"d\": 4, \"desc\": \"Prescaler (higher reads lower; lower it for more sensitivity, raise it if channels saturate at 4095)\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillHex\":{\n \"_notes\": \"Bela Trill Hex - 2D hexagonal capacitive touch pad, 30 channels, up to 4 simultaneous touches. CENTROID response: 4 vertical loc + 4 vertical size + 4 horizontal loc + 4 horizontal size (uint16 BE). Address jumper-configurable 0x40-0x47.\",\n \"addresses\": \"0x40-0x47\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000101\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r32\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Hex\",\n \"desc\": \"Capacitive Touch Hex\",\n \"manu\": \"Bela\",\n \"type\": \"TrillHex\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 32,\n \"a\": [\n { \"n\": \"vloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillRing\":{\n \"_notes\": \"Bela Trill Ring - 1D circular capacitive touch, 28 ring channels + 2 button channels, up to 5 touches. CENTROID response: 5 loc + 5 size + 2 button values (uint16 BE). Address jumper-configurable 0x38-0x3F.\",\n \"addresses\": \"0x38-0x3F\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000100\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r24\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Ring\",\n \"desc\": \"Capacitive Touch Ring\",\n \"manu\": \"Bela\",\n \"type\": \"TrillRing\",\n \"clas\": [\"TCH\", \"BTN\"],\n \"resp\": {\n \"b\": 24,\n \"a\": [\n { \"n\": \"loc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"btn0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"btn1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillSquare\":{\n \"_notes\": \"Bela Trill Square - 2D capacitive touch, 30 channels, up to 4 simultaneous touches. CENTROID mode returns 4 vertical loc+size pairs followed by 4 horizontal loc+size pairs. Address jumper-configurable 0x28-0x2F.\",\n \"addresses\": \"0x28-0x2F\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000010\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r32\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Square\",\n \"desc\": \"Capacitive Touch Square\",\n \"manu\": \"Bela\",\n \"type\": \"TrillSquare\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 32,\n \"a\": [\n { \"n\": \"vloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"VCNL4040\":{\n \"addresses\": \"0x60\",\n \"detectionValues\": \"0x0c=0b100001100000XXXX\",\n \"initValues\": \"0x041007=&0x030e08=&0x000000=\",\n \"pollInfo\": {\n \"c\": \"0x08=r2&0x09=r2&0x0a=r2\",\n \"i\": 200,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Light Sensor (ambient & proximity)\",\n \"desc\": \"\",\n \"manu\": \"Vishay\",\n \"type\": \"VCNL4040\",\n \"clas\": [\"PROX\",\"LGHT\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"prox\",\n \"t\": \"H\",\n \"u\": \"mm\",\n \"r\": [0, 2000],\n \"f\": \"4d\",\n \"o\": \"uint16\",\n \"vft\": \"valid\"\n }\n ]\n }\n }\n}\n,\"VL53L1X\":{\n \"_notes\": \"ToF distance sensor with extended range up to 4m\",\n \"addresses\": \"0x29\",\n \"identPriority\": 1,\n \"detectionValues\": \"0x010f=0xEACC\",\n \"initValues\": \"0x002D01&0x0087&0x25012c00=p100&0x0060=p100\",\n \"pollInfo\": {\n \"c\": \"0x0089=r1&0x0096=r2\",\n \"i\": 200,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Distance Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"VL53L1X\",\n \"clas\": [\"DIST\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"x\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false\n },\n {\n \"n\": \"dist\",\n \"t\": \">H\",\n \"u\": \"mm\",\n \"r\": [0, 4000],\n \"f\": \"4d\",\n \"o\": \"uint16\",\n \"vft\": \"valid\"\n }\n ]\n }\n }\n}\n,\"VL53L4CD\":{\n \"_notes\": \"ToF distance sensor with range 0-1.3m\",\n \"addresses\": \"0x29\",\n \"identPriority\": 1,\n \"detectionValues\": \"0x010F=0xEBAA\",\n \"initValues\": \"0x002D0001011102000208000810010100000000FF000F0000000000200B000002142100&0x004e000500000000c8000038ff010008000001cc0701f10500a0008008380000&006c00000f89000000000000000107050606000002c7ff9b000000010000&0x008740=p200&0x008601&0x008780&0x000809&0x000b00&0x00240500&0x006c00000000&0x005e02f2&0x006103a1&008721\",\n \"pollInfo\": {\n \"c\": \"0x008601=&0x0089=r1&0x0096=r2\",\n \"i\": 100,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Distance Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"VL53L4CD\",\n \"clas\": [\"DIST\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"x\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false\n },\n {\n \"n\": \"dist\",\n \"t\": \">H\",\n \"u\": \"mm\",\n \"r\": [0, 1000],\n \"f\": \"3d\",\n \"o\": \"uint16\",\n \"vft\": \"valid\"\n }\n ]\n }\n }\n}\n,\"VL6180\":{\n \"_notes\": \"VL6180X ToF proximity sensor with 1x range scaling (RANGE_SCALER 0x0096/0x0097=>0x00fd), so the range is an 8-bit value in mm (0-255). Specified range 0-100mm, typically up to ~200mm with a reflective target and low ambient light\\nInit: write 0x0018=>0x01 then wait 100ms, which stops continuous ranging left running by a previous init (the sensor may not have been power cycled) or otherwise does one harmless single-shot range; AN4545 private settings https://www.st.com/resource/en/application_note/an4545-vl6180x-basic-ranging-application-note-stmicroelectronics.pdf; clear SYSTEM__FRESH_OUT_OF_RESET (0x0016=>0x00); GPIO1 interrupt output active low (0x0011=>0x10); readout averaging period 4.3ms (0x010a=>0x30); VHV recalibration every 255 ranges (0x0031=>0xff) and once now (0x002e=>0x01); range inter-measurement period 90ms (0x001b=>0x08); interrupt on new range sample only (0x0014=>0x04); clear interrupts (0x0015=>0x07); start continuous ranging (0x0018=>0x03)\\nPoll: RESULT__RANGE_STATUS, RESULT__ALS_STATUS and RESULT__INTERRUPT_STATUS_GPIO (0x004d-0x004f), RESULT__RANGE_VAL (0x0062), then clear interrupts (0x0015=>0x07)\\nerr is the range error code (0x004d bits 7:4) and valid is true only when it is 0. With no target in range err is typically 6 (early convergence estimate), 7 (max convergence time) or 11 (signal to noise) and the raw range reads 255\\nfresh is 1 when a new range sample completed since the previous poll (0x004f bit 2, new sample ready) and 0 when the poll re-read the previous sample\\nSample rate (_conf.rate): the sensor's inter-measurement period (0x001b) is set shorter than the poll interval so that every poll normally sees a new sample. Up to 10Hz the period is 90ms with the default max convergence time 49ms (0x001c=>0x31) and readout averaging 4.3ms. 20Hz uses a 40ms period, 25ms max convergence and 4.3ms averaging. 50Hz uses a 10ms period, 3ms max convergence and 1.8ms averaging (0x010a=>0x08), which reduces range on dark targets and increases noise. Ranging is stopped while the timing is changed and then restarted\\nAmbient light sensing (ALS) is not used\",\n \"identPriority\": 2,\n \"addresses\": \"0x29\",\n \"detectionValues\": \"0x0000=0b10110100\",\n \"initValues\": \"0x001801=p100&0x020701&0x020801&0x009600&0x0097fd&0x00e301&0x00e403&0x00e502&0x00e601&0x00e703&0x00f502&0x00d905&0x00dbce&0x00dc03&0x00ddf8&0x009f00&0x00a33c&0x00b700&0x00bb3c&0x00b209&0x00ca09&0x019801&0x01b017&0x01ad00&0x00ff05&0x010005&0x019905&0x01a61b&0x01ac3e&0x01a71f&0x003000&0x001600&0x001110&0x010a30&0x0031ff&0x002e01=p10&0x001c31&0x001b08&0x001404&0x001507&0x001803\",\n \"pollInfo\": {\n \"c\": \"0x004d=r3&0x0062=r1&0x001507\",\n \"i\": 200,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Distance Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"VL6180\",\n \"clas\": [\"DIST\"],\n \"resp\": {\n \"b\": 4,\n \"a\": [\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"at\": 0,\n \"u\": \"\",\n \"r\": [0,1],\n \"m\": \"0xF0\",\n \"s\": 4,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false,\n \"lut\": [\n {\n \"r\": \"0\",\n \"v\": 1\n },\n {\n \"r\": \"\",\n \"v\": 0\n }\n ]\n },\n {\n \"n\": \"err\",\n \"t\": \"B\",\n \"at\": 0,\n \"u\": \"\",\n \"r\": [0,15],\n \"m\": \"0xF0\",\n \"s\": 4,\n \"f\": \"d\",\n \"vs\": false,\n \"vl\": {\"0\": \"OK\", \"1\": \"VCSEL cont\", \"2\": \"VCSEL test\", \"3\": \"VCSEL wdog\", \"4\": \"PLL1\", \"5\": \"PLL2\", \"6\": \"early conv\", \"7\": \"max conv\", \"8\": \"no target\", \"9\": \"unused\", \"10\": \"unused\", \"11\": \"SNR\", \"12\": \"raw under\", \"13\": \"raw over\", \"14\": \"underflow\", \"15\": \"overflow\"}\n },\n {\n \"n\": \"fresh\",\n \"t\": \"B\",\n \"at\": 2,\n \"u\": \"\",\n \"r\": [0,1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false\n },\n {\n \"n\": \"dist\",\n \"t\": \"B\",\n \"at\": 3,\n \"u\": \"mm\",\n \"r\": [0,255],\n \"f\": \"3d\",\n \"o\": \"float\",\n \"vft\": \"valid\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"_conf.rate\",\n \"t\": \"B\",\n \"r\": [1,50],\n \"d\": 5,\n \"desc\": \"Sample rate (Hz)\",\n \"map\": {\n \"1\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 1000000,\n \"s\": 1\n },\n \"2\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 500000,\n \"s\": 1\n },\n \"5\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 200000,\n \"s\": 1\n },\n \"10\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 100000,\n \"s\": 1\n },\n \"20\": {\n \"w\": \"001801&001b03&001c19&010a30&001507&001803\",\n \"i\": 50000,\n \"s\": 1\n },\n \"50\": {\n \"w\": \"001801&001b00&001c03&010a08&001507&001803\",\n \"i\": 20000,\n \"s\": 1\n }\n }\n }\n ]\n }\n}\n}}", + "compiled": "{\"devTypes\":{}}", + "policyOnly": {}, + "provenance": "{\"formatVersion\": 1, \"sources\": {\"raftcore\": {\"bundled\": true, \"raftCore\": \"1.62.0\"}, \"acme\": {\"git\": \"https://github.com/acme/devtypes.git\", \"ref\": \"v1.0.0\", \"commit\": \"3f2a9c1e7b0d0000000000000000000000000000\"}}, \"typeFile\": {\"include\": [\"raftcore:*\", \"acme:Gadget\"], \"policy\": {\"TrillRing\": \"disabled\"}}}", + "maxBytes": 1000000, + "text": "{\"devTypes\":{\"ADXL313\":{\"addresses\":\"0x1d,0x53\",\"detectionValues\":\"0x00=0xAD1DCB\",\"initValues\":\"0x310b=&0x2d08=\",\"pollInfo\":{\"c\":\"0x32=r6\",\"i\":100,\"s\":3},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Accelerometer\",\"desc\":\"3-Axis Accel\",\"manu\":\"Analog Devices\",\"type\":\"ADXL313\",\"clas\":[\"ACC\"],\"resp\":{\"b\":6,\"a\":[{\"n\":\"ax\",\"t\":\"I\",\"u\":\"%\",\"r\":[0,100],\"m\":\"0xfffff000\",\"s\":12,\"d\":10485.76,\"f\":\"3.1f\",\"o\":\"float\"},{\"n\":\"temperature\",\"at\":2,\"t\":\">I\",\"u\":\"°C\",\"r\":[-40,85],\"m\":\"0x000fffff\",\"d\":5242.88,\"a\":-50,\"f\":\"3.2f\",\"o\":\"float\"}]}}},\"AMG8833\":{\"addresses\":\"0x69\",\"detectionValues\":\"\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x80=r128\",\"i\":100,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Thermal Camera\",\"desc\":\"\",\"manu\":\"Panasonic\",\"type\":\"AMG8833\",\"clas\":[\"TCAM\"],\"resp\":{\"b\":128,\"a\":[{\"n\":\"temperature\",\"t\":\"H\",\"u\":\"degrees\",\"r\":[0,360],\"f\":\"3.1f\",\"o\":\"float\",\"d\":11.377777777777778}]}}},\"AdafruitGamepad\":{\"addresses\":\"0x50\",\"detectionValues\":\"0x0001=0b10000111\",\"initValues\":\"0x007fff=p20&0x010300010067=p1&0x010b00010067=p1&0x010500010067=p1\",\"pollInfo\":{\"c\":\"0x0915=r2&0x0916=r2&0x0104=r4\",\"i\":100,\"s\":5},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"readEffect\",\"basis\":\"source\",\"note\":\"ATtiny seesaw: shipped firmware has no handler for 0x99 (read NACKed); library 2.0.0 discards one-byte writes, so the read answers for the previous register and, if that was GPIO bulk, clears the interrupt flags (Adafruit_seesawPeripheral 8070dfb, f7f738d)\"}},\"devInfoJson\":{\"name\":\"Adafruit Gamepad\",\"desc\":\"\",\"manu\":\"Adafruit\",\"type\":\"Gamepad\",\"clas\":[\"GAME\"],\"resp\":{\"b\":8,\"a\":[{\"n\":\"x\",\"t\":\">H\",\"r\":[0,65535],\"d\":-1,\"a\":1023,\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"y\",\"t\":\">H\",\"r\":[0,65535],\"d\":-1,\"a\":1023,\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"SELECT\",\"at\":7,\"t\":\"B\",\"r\":[0,1],\"x\":\"0x01\",\"m\":\"0x01\",\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"B\",\"at\":7,\"t\":\"B\",\"r\":[0,1],\"x\":\"0x02\",\"m\":\"0x02\",\"s\":1,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"Y\",\"at\":7,\"t\":\"B\",\"r\":[0,1],\"x\":\"0x04\",\"m\":\"0x04\",\"s\":2,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"A\",\"at\":7,\"t\":\"B\",\"r\":[0,1],\"x\":\"0x20\",\"m\":\"0x20\",\"s\":5,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"X\",\"at\":7,\"t\":\"B\",\"r\":[0,1],\"x\":\"0x40\",\"m\":\"0x40\",\"s\":6,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"START\",\"at\":5,\"t\":\"B\",\"r\":[0,1],\"x\":\"0x01\",\"m\":\"0x01\",\"f\":\"b\",\"o\":\"bool\"}]}}},\"AdafruitSoilSensor\":{\"addresses\":\"0x36\",\"detectionValues\":\"0x0001=p25&=0x55\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x0F10=p250&=r2&0x0004=p250&=r4\",\"i\":1000,\"s\":5},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"source\",\"note\":\"SAMD09 seesaw: 0x99 isn't a module base, so the command is ignored and the read returns 0xFF or stale reply bytes (adafruit/seesaw 20e2892)\"}},\"devInfoJson\":{\"name\":\"Soil Moisture Sensor\",\"desc\":\"\",\"manu\":\"Adafruit\",\"type\":\"AdafruitSoilSensor\",\"clas\":[\"SOIL\"],\"resp\":{\"b\":6,\"a\":[{\"n\":\"moisture\",\"t\":\">H\",\"u\":\"\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"temperature\",\"t\":\">i\",\"u\":\"°C\",\"r\":[-45,175],\"d\":65536,\"f\":\".2f\",\"o\":\"float\"}]}}},\"BLEBTHome\":{\"detectionValues\":\"\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"=r17\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"BLE Sensor (BTHome)\",\"desc\":\"\",\"manu\":\"Allterco\",\"type\":\"BLEBTHome\",\"clas\":[\"BTHM\"],\"resp\":{\"b\":17,\"a\":[{\"n\":\"ID\",\"t\":\"B\",\"u\":\"\",\"r\":[0,255],\"f\":\"d\",\"o\":\"uint8\"},{\"n\":\"MAC\",\"t\":\">Q\",\"o\":\"uint64\"},{\"n\":\"motion\",\"t\":\"B\",\"u\":\"\",\"r\":[0,1],\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"battery\",\"t\":\"B\",\"u\":\"%\",\"r\":[0,100],\"f\":\"d\",\"o\":\"uint8\"},{\"n\":\"temp\",\"t\":\">h\",\"u\":\"°C\",\"r\":[-100,100],\"d\":100,\"f\":\".1f\",\"o\":\"float\"},{\"n\":\"light\",\"t\":\">I\",\"u\":\"Lux\",\"r\":[0,100000],\"d\":100,\"f\":\".1f\",\"o\":\"float\"}]}}},\"BME280\":{\"addresses\":\"0x76-0x77\",\"detectionValues\":\"0xd0=0x60\",\"initValues\":\"0xe0b6=p5&0xf580&0xf201&0xf427\",\"pollInfo\":{\"c\":\"0x88=r26&0xe1=r7&0xf7=r8\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Temperature, Pressure & Humidity Sensor\",\"desc\":\"\",\"manu\":\"Bosch\",\"type\":\"BME280\",\"clas\":[\"TEMP\",\"PRES\",\"RH\"],\"resp\":{\"b\":41,\"a\":[{\"n\":\"temperature\",\"t\":\"32767){T2=T2-65536;}int T3=buf[4]|(buf[5]<<8);if(T3>32767){T3=T3-65536;}int P1=buf[6]|(buf[7]<<8);int P2=buf[8]|(buf[9]<<8);if(P2>32767){P2=P2-65536;}int P3=buf[10]|(buf[11]<<8);if(P3>32767){P3=P3-65536;}int P4=buf[12]|(buf[13]<<8);if(P4>32767){P4=P4-65536;}int P5=buf[14]|(buf[15]<<8);if(P5>32767){P5=P5-65536;}int P6=buf[16]|(buf[17]<<8);if(P6>32767){P6=P6-65536;}int P7=buf[18]|(buf[19]<<8);if(P7>32767){P7=P7-65536;}int P8=buf[20]|(buf[21]<<8);if(P8>32767){P8=P8-65536;}int P9=buf[22]|(buf[23]<<8);if(P9>32767){P9=P9-65536;}int H1=buf[25];int H2=buf[26]|(buf[27]<<8);if(H2>32767){H2=H2-65536;}int H3=buf[28];int E4=buf[29];if(E4>127){E4=E4-256;}int H4=E4*16+(buf[30]&15);int E6=buf[31];if(E6>127){E6=E6-256;}int H5=E6*16+(buf[30]>>4);int H6=buf[32];if(H6>127){H6=H6-256;}int aP=(buf[33]<<12)|(buf[34]<<4)|(buf[35]>>4);int aT=(buf[36]<<12)|(buf[37]<<4)|(buf[38]>>4);int aH=(buf[39]<<8)|buf[40];float v1=(aT/16384.0-T1/1024.0)*T2;float d=aT/131072.0-T1/8192.0;float v2=d*d*T3;float tf=v1+v2;out.temperature=tf/5120.0;v1=tf/2.0-64000.0;v2=v1*v1*P6/32768.0+v1*P5*2.0;v2=v2/4.0+P4*65536.0;v1=(P3*v1*v1/524288.0+P2*v1)/524288.0;v1=(1.0+v1/32768.0)*P1;float p=0.0;if(v1>0.0){p=1048576.0-aP;p=(p-v2/4096.0)*6250.0/v1;float w1=P9*p*p/2147483648.0;float w2=p*P8/32768.0;p=p+(w1+w2+P7)/16.0;}out.pressure=p/100.0;float h=tf-76800.0;h=(aH-(H4*64.0+H5/16384.0*h))*(H2/65536.0*(1.0+H6/67108864.0*h*(1.0+H3/67108864.0*h)));h=h*(1.0-H1*h/524288.0);if(h>100.0){h=100.0;}if(h<0.0){h=0.0;}out.humidity=h;\"}}}},\"BME680\":{\"addresses\":\"0x76-0x77\",\"detectionValues\":\"0xd0=0x61&0xf0=0x00\",\"initValues\":\"0xe0b6=p10&0xeb=r4&0x00=r3&0x7201&0x7500&0x6466&0x5a{heat}&0x7110\",\"initCode\":{\"n\":\"bme680_heater\",\"c\":\"int g2=buf[0]|(buf[1]<<8);if(g2>32767){g2=g2-65536;}int g1=buf[2];if(g1>127){g1=g1-256;}int g3=buf[3];if(g3>127){g3=g3-256;}int rhv=buf[4];if(rhv>127){rhv=rhv-256;}int rhr=(buf[6]&48)>>4;float v1=g1/16.0+49.0;float v2=g2/32768.0*0.0005+0.00235;float v3=g3/1024.0;float v5=v1*(1.0+v2*320.0)+v3*25.0;init.heat=3.4*(v5*(4.0/(4.0+rhr))*(1.0/(1.0+rhv*0.002))-25.0);\"},\"pollInfo\":{\"c\":\"0x7425=p200&0x89=r25&0xe1=r16&0x00=r5&0x1d=r15\",\"i\":3000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Environmental Sensor\",\"desc\":\"Temperature, pressure, humidity, gas\",\"manu\":\"Bosch\",\"type\":\"BME680\",\"clas\":[\"TEMP\",\"PRES\",\"RH\",\"GAS\"],\"resp\":{\"b\":61,\"a\":[{\"n\":\"temperature\",\"t\":\"32767){t2=t2-65536;}int t3=buf[3];if(t3>127){t3=t3-256;}int p1=buf[5]|(buf[6]<<8);int p2=buf[7]|(buf[8]<<8);if(p2>32767){p2=p2-65536;}int p3=buf[9];if(p3>127){p3=p3-256;}int p4=buf[11]|(buf[12]<<8);if(p4>32767){p4=p4-65536;}int p5=buf[13]|(buf[14]<<8);if(p5>32767){p5=p5-65536;}int p7=buf[15];if(p7>127){p7=p7-256;}int p6=buf[16];if(p6>127){p6=p6-256;}int p8=buf[19]|(buf[20]<<8);if(p8>32767){p8=p8-65536;}int p9=buf[21]|(buf[22]<<8);if(p9>32767){p9=p9-65536;}int p10=buf[23];int h1=(buf[27]<<4)|(buf[26]&15);int h2=(buf[25]<<4)|(buf[26]>>4);int h3=buf[28];if(h3>127){h3=h3-256;}int h4=buf[29];if(h4>127){h4=h4-256;}int h5=buf[30];if(h5>127){h5=h5-256;}int h6=buf[31];int h7=buf[32];if(h7>127){h7=h7-256;}int rse=buf[45]&240;if(rse>127){rse=rse-256;}rse=rse/16;int aP=(buf[48]<<12)|(buf[49]<<4)|(buf[50]>>4);int aT=(buf[51]<<12)|(buf[52]<<4)|(buf[53]>>4);int aH=(buf[54]<<8)|buf[55];int aG=(buf[59]<<2)|(buf[60]>>6);int gr=buf[60]&15;float v1=(aT/16384.0-t1/1024.0)*t2;float d=aT/131072.0-t1/8192.0;float v2=d*d*t3*16.0;float tf=v1+v2;float tc=tf/5120.0;out.temperature=tc;v1=tf/2.0-64000.0;v2=v1*v1*p6/131072.0;v2=v2+v1*p5*2.0;v2=v2/4.0+p4*65536.0;v1=(p3*v1*v1/16384.0+p2*v1)/524288.0;v1=(1.0+v1/32768.0)*p1;float p=0.0;if(v1>0.0){p=1048576.0-aP;p=(p-v2/4096.0)*6250.0/v1;float w1=p9*p*p/2147483648.0;float w2=p*p8/32768.0;float q=p/256.0;float w3=q*q*q*p10/131072.0;p=p+(w1+w2+w3+p7*128.0)/16.0;}out.pressure=p/100.0;float e1=aH-(h1*16.0+h3/2.0*tc);float e2=e1*(h2/262144.0*(1.0+h4/16384.0*tc+h5/1048576.0*tc*tc));float hum=e2+(h6/16384.0+h7/2097152.0*tc)*e2*e2;if(hum>100.0){hum=100.0;}if(hum<0.0){hum=0.0;}out.humidity=hum;float k1=0.0;if(gr==5){k1=-1.0;}if(gr==7){k1=-0.8;}if(gr==10){k1=-0.2;}if(gr==11){k1=-0.5;}if(gr==13){k1=-1.0;}float k2=0.0;if(gr==4){k2=0.1;}if(gr==5){k2=0.7;}if(gr==7){k2=-0.8;}if(gr==8){k2=-0.1;}float g1=1340.0+5.0*rse;float g2=g1*(1.0+k1/100.0);float g3=1.0+k2/100.0;float gas=0.0;if((buf[60]&48)==48){gas=1.0/(g3*0.000000125*(1<32767){g2=g2-65536;}int g1=buf[2];if(g1>127){g1=g1-256;}int g3=buf[3];if(g3>127){g3=g3-256;}int rhv=buf[4];if(rhv>127){rhv=rhv-256;}int rhr=(buf[6]&48)>>4;float v1=g1/16.0+49.0;float v2=g2/32768.0*0.0005+0.00235;float v3=g3/1024.0;float v5=v1*(1.0+v2*320.0)+v3*25.0;init.heat=3.4*(v5*(4.0/(4.0+rhr))*(1.0/(1.0+rhv*0.002))-25.0);\"},\"pollInfo\":{\"c\":\"0x7425=p200&0x8a=r23&0xe1=r14&0x00=r5&0x1d=r17\",\"i\":3000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Environmental Sensor\",\"desc\":\"Temperature, pressure, humidity, gas\",\"manu\":\"Bosch\",\"type\":\"BME690\",\"clas\":[\"TEMP\",\"PRES\",\"RH\",\"GAS\"],\"resp\":{\"b\":59,\"a\":[{\"n\":\"temperature\",\"t\":\"127){t3=t3-256;}int p1=buf[10]|(buf[11]<<8);if(p1>32767){p1=p1-65536;}int p2=buf[12]|(buf[13]<<8);int p3=buf[14];if(p3>127){p3=p3-256;}int p4=buf[15];if(p4>127){p4=p4-256;}int p5=buf[4]|(buf[5]<<8);if(p5>32767){p5=p5-65536;}int p6=buf[6]|(buf[7]<<8);if(p6>32767){p6=p6-65536;}int p7=buf[8];if(p7>127){p7=p7-256;}int p8=buf[9];if(p8>127){p8=p8-256;}int p9=buf[18]|(buf[19]<<8);if(p9>32767){p9=p9-65536;}int p10=buf[20];if(p10>127){p10=p10-256;}int p11=buf[21];if(p11>127){p11=p11-256;}int h5=(buf[23]<<4)|(buf[24]>>4);if(h5>2047){h5=h5-4096;}int h1=(buf[25]<<4)|(buf[24]&15);if(h1>2047){h1=h1-4096;}int h2=buf[26];if(h2>127){h2=h2-256;}int h4=buf[27];if(h4>127){h4=h4-256;}int h3=buf[28];int h6=buf[29];int aP=(buf[44]<<16)|(buf[45]<<8)|buf[46];int aT=(buf[47]<<16)|(buf[48]<<8)|buf[49];int aH=(buf[50]<<8)|buf[51];int aG=(buf[57]<<2)|(buf[58]>>6);int gr=buf[58]&15;float cf=aT-t1*256.0;float T=cf*(t2/1073741824.0)+cf*cf*(t3/281474976710656.0);out.temperature=T;float o=p1*8.0;float tk10=p2/64.0;float tk20=p3/256.0;float tk30=p4/32768.0;float s=(p5-16384.0)/1048576.0;float tk1s=(p6-16384.0)/536870912.0;float tk2s=p7/4294967296.0;float tk3s=p8/137438953472.0;float nls=p9/281474976710656.0;float tknls=p10/281474976710656.0;float nls3=p11/36893488147419103232.0;float pf=aP*1.0;float P=o+tk10*T+tk20*T*T+tk30*T*T*T+pf*(s+tk1s*T+tk2s*T*T+tk3s*T*T*T)+pf*pf*(nls+tknls*T)+pf*pf*pf*nls3;out.pressure=P/100.0;float tc=T*5120.0-76800.0;float hoff=aH-(h1*64.0+h2/16384.0*tc);float k4=h4/67108864.0;float hs=hoff*(h5/65536.0)*(1.0+k4*tc+k4*(h3/67108864.0)*tc*tc);float H=hs*(1.0-h6/524288.0*hs);if(H<0.0){H=0.0;}if(H>100.0){H=100.0;}out.humidity=H;float gas=0.0;if((buf[58]&48)==48){gas=1000000.0*(262144>>gr)/(4096.0+3.0*(aG-512.0));}out.gas=gas;\"}}}},\"CAP1188\":{\"addresses\":\"0x28-0x2d\",\"detectionValues\":\"0xfd=0x50&0xfe=0x5d\",\"initValues\":\"0x0000&0x1f36&0x2a00\",\"pollInfo\":{\"c\":\"0x02=r2&0x10=r8&0x0000\",\"i\":100,\"s\":5},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Capacitive Touch\",\"desc\":\"8 channel\",\"manu\":\"Microchip\",\"type\":\"CAP1188\",\"clas\":[\"TCH\"],\"resp\":{\"b\":10,\"a\":[{\"n\":\"C1\",\"at\":1,\"t\":\"B\",\"r\":[0,1],\"m\":\"0x01\",\"s\":0,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"C2\",\"at\":1,\"t\":\"B\",\"r\":[0,1],\"m\":\"0x02\",\"s\":1,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"C3\",\"at\":1,\"t\":\"B\",\"r\":[0,1],\"m\":\"0x04\",\"s\":2,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"C4\",\"at\":1,\"t\":\"B\",\"r\":[0,1],\"m\":\"0x08\",\"s\":3,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"C5\",\"at\":1,\"t\":\"B\",\"r\":[0,1],\"m\":\"0x10\",\"s\":4,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"C6\",\"at\":1,\"t\":\"B\",\"r\":[0,1],\"m\":\"0x20\",\"s\":5,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"C7\",\"at\":1,\"t\":\"B\",\"r\":[0,1],\"m\":\"0x40\",\"s\":6,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"C8\",\"at\":1,\"t\":\"B\",\"r\":[0,1],\"m\":\"0x80\",\"s\":7,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"d1\",\"at\":2,\"t\":\"b\",\"r\":[-128,127],\"f\":\"d\",\"o\":\"int8\"},{\"n\":\"d2\",\"at\":3,\"t\":\"b\",\"r\":[-128,127],\"f\":\"d\",\"o\":\"int8\"},{\"n\":\"d3\",\"at\":4,\"t\":\"b\",\"r\":[-128,127],\"f\":\"d\",\"o\":\"int8\"},{\"n\":\"d4\",\"at\":5,\"t\":\"b\",\"r\":[-128,127],\"f\":\"d\",\"o\":\"int8\"},{\"n\":\"d5\",\"at\":6,\"t\":\"b\",\"r\":[-128,127],\"f\":\"d\",\"o\":\"int8\"},{\"n\":\"d6\",\"at\":7,\"t\":\"b\",\"r\":[-128,127],\"f\":\"d\",\"o\":\"int8\"},{\"n\":\"d7\",\"at\":8,\"t\":\"b\",\"r\":[-128,127],\"f\":\"d\",\"o\":\"int8\"},{\"n\":\"d8\",\"at\":9,\"t\":\"b\",\"r\":[-128,127],\"f\":\"d\",\"o\":\"int8\"}]}}},\"CAP1203\":{\"addresses\":\"0x28\",\"detectionValues\":\"0xfd=0b01101101\",\"initValues\":\"0x0000&0x1f06\",\"pollInfo\":{\"c\":\"0x02=r2&0x0000\",\"i\":100,\"s\":5},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Capacitive Touch\",\"desc\":\"\",\"manu\":\"Sparkfun\",\"type\":\"CAP1203\",\"clas\":[\"TCH\"],\"resp\":{\"b\":2,\"a\":[{\"n\":\"A\",\"at\":1,\"t\":\"B\",\"r\":[0,1],\"m\":\"0x01\",\"s\":0,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"B\",\"at\":1,\"t\":\"B\",\"r\":[0,1],\"m\":\"0x02\",\"s\":1,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"C\",\"at\":1,\"t\":\"B\",\"r\":[0,1],\"m\":\"0x04\",\"s\":2,\"f\":\"b\",\"o\":\"bool\"},{\"n\":\"status\",\"t\":\">H\",\"vs\":false,\"f\":\"04x\",\"o\":\"uint16\",\"bl\":{\"0x0001\":\"CS1\",\"0x0002\":\"CS2\",\"0x0004\":\"CS3\",\"0x0100\":\"TOUCH\",\"0x0200\":\"MTP\",\"0x0400\":\"MULT\",\"0x1000\":\"PWR\",\"0x2000\":\"ACAL_FAIL\",\"0x4000\":\"BC_OUT\"}}]}}},\"DPS310\":{\"addresses\":\"0x76-0x77\",\"detectionValues\":\"0x0d=0x10\",\"initValues\":\"0x0c89=p50&0x0ea5&0x0f96&0x6202&0x0e00&0x0f00&0x0634&0x07b0&0x0904&0x0807\",\"pollInfo\":{\"c\":\"0x00=r6&0x10=r18\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"bench\",\"note\":\"bench (Axiom018, 2026-10-23): the lone 0x99 + 9-byte read (registers 0x99-0xA1, not in the map) read zeros; configuration, coefficients and COEF_SRCE unchanged and measuring carried on. Its addresses are in Axiom's excluded RSAO range anyway\"}},\"devInfoJson\":{\"name\":\"Pressure & Temperature Sensor\",\"desc\":\"\",\"manu\":\"Infineon\",\"type\":\"DPS310\",\"clas\":[\"PRES\",\"TEMP\"],\"resp\":{\"b\":24,\"a\":[{\"n\":\"temperature\",\"t\":\"8388607){pr=pr-16777216;}int tr=(buf[3]<<16)|(buf[4]<<8)|buf[5];if(tr>8388607){tr=tr-16777216;}int c0=(buf[6]<<4)|(buf[7]>>4);if(c0>2047){c0=c0-4096;}int c1=((buf[7]&15)<<8)|buf[8];if(c1>2047){c1=c1-4096;}int c00=(buf[9]<<12)|(buf[10]<<4)|(buf[11]>>4);if(c00>524287){c00=c00-1048576;}int c10=((buf[11]&15)<<16)|(buf[12]<<8)|buf[13];if(c10>524287){c10=c10-1048576;}int c01=(buf[14]<<8)|buf[15];if(c01>32767){c01=c01-65536;}int c11=(buf[16]<<8)|buf[17];if(c11>32767){c11=c11-65536;}int c20=(buf[18]<<8)|buf[19];if(c20>32767){c20=c20-65536;}int c21=(buf[20]<<8)|buf[21];if(c21>32767){c21=c21-65536;}int c30=(buf[22]<<8)|buf[23];if(c30>32767){c30=c30-65536;}float ts=tr/524288.0;float ps=pr/253952.0;out.temperature=c0*0.5+c1*ts;float p=c00+ps*(c10+ps*(c20+ps*c30))+ts*c01+ts*ps*(c11+ps*c21);out.pressure=p/100.0;\"}}}},\"ENS160\":{\"addresses\":\"0x52-0x53\",\"detectionValues\":\"0x00=0x6001\",\"initValues\":\"0x1001=p10&0x138a4a0064=&0x1002=p10\",\"pollInfo\":{\"c\":\"0x20=r6\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"readEffect\",\"basis\":\"bench\",\"note\":\"bench (Axiom018, 2026-10-23): the chip ignores bit 7 of the register address, so the lone 0x99 points at 0x19 and the 9-byte read covers 0x19-0x21, ending with DEVICE_STATUS and DATA_AQI. No configuration register changed (OPMODE, CONFIG, TEMP_IN, RH_IN) and measuring carried on; reading DATA_AQI may clear the NEWDAT flag (transient)\"}},\"devInfoJson\":{\"name\":\"Gas Sensor (VOC, eCO2)\",\"desc\":\"\",\"manu\":\"ScioSense\",\"type\":\"ENS160\",\"clas\":[\"VOC\",\"CO2\"],\"resp\":{\"b\":6,\"a\":[{\"n\":\"status\",\"t\":\"B\",\"r\":[0,255],\"m\":\"0xff\",\"f\":\"02x\",\"o\":\"uint8\",\"vs\":false,\"bl\":{\"0x80\":\"S\",\"0x40\":\"E\",\"0x08\":\"\",\"0x04\":\"\",\"0x02\":\"D\",\"0x01\":\"G\"}},{\"n\":\"validity\",\"at\":0,\"t\":\"B\",\"r\":[0,3],\"m\":\"0x0c\",\"s\":2,\"f\":\"d\",\"o\":\"uint8\",\"vl\":{\"0\":\"normal\",\"1\":\"warm-up\",\"2\":\"start-up\",\"3\":\"invalid\"}},{\"n\":\"AQI\",\"at\":1,\"t\":\"B\",\"u\":\"UBA\",\"r\":[1,5],\"m\":\"0x07\",\"f\":\"d\",\"o\":\"uint8\"},{\"n\":\"TVOC\",\"at\":2,\"t\":\"I\",\"u\":\"N\",\"r\":[-10000,10000],\"m\":\"0x00ffffff\",\"d\":44000,\"sb\":23,\"f\":\".3f\",\"o\":\"float\"},{\"n\":\"valid\",\"t\":\"B\",\"u\":\"\",\"r\":[0,1],\"vl\":{\"0\":\"no\",\"1\":\"yes\"}}]}}},\"LPS25\":{\"addresses\":\"0x5C-0x5D\",\"detectionValues\":\"0x0f=0b10111101\",\"initValues\":\"0x2104=p5&0x20C4=\",\"pollInfo\":{\"c\":\"0xA7=r6\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"bench\",\"note\":\"lone 0x99 + 9-byte read (2026-10-23): pointer to 0x19 (reserved), no configuration register changed, pressure unchanged\"}},\"devInfoJson\":{\"name\":\"Pressure Sensor\",\"desc\":\"\",\"manu\":\"ST\",\"type\":\"LPS25\",\"clas\":[\"PRES\",\"TEMP\"],\"resp\":{\"b\":6,\"a\":[{\"n\":\"status\",\"t\":\"B\",\"r\":[0,255],\"m\":\"0xff\",\"f\":\"02x\",\"o\":\"uint8\",\"vs\":false,\"bl\":{\"0x01\":\"T_DA\",\"0x02\":\"P_DA\",\"0x10\":\"T_OR\",\"0x20\":\"P_OR\"}},{\"n\":\"pressure\",\"at\":0,\"t\":\"maxN){N=maxN;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(imaxN){N=maxN;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(iH\",\"u\":\"V\",\"r\":[0,5.12],\"d\":12800,\"f\":\".3f\",\"o\":\"float\"},{\"n\":\"charge\",\"t\":\">H\",\"u\":\"%\",\"r\":[0,100],\"d\":256,\"f\":\".1f\",\"o\":\"float\"},{\"n\":\"chargeRate\",\"t\":\">h\",\"u\":\"%/hr\",\"r\":[-300,300],\"d\":4.808,\"f\":\".1f\",\"o\":\"float\"},{\"n\":\"status\",\"t\":\"B\",\"at\":6,\"r\":[0,255],\"m\":\"0xff\",\"f\":\"02x\",\"o\":\"uint8\",\"vs\":false,\"bl\":{\"0\":\"OK\",\"0x01\":\"RI\",\"0x02\":\"VH\",\"0x04\":\"VL\",\"0x08\":\"VR\",\"0x10\":\"HD\",\"0x20\":\"SC\",\"0x40\":\"\"}}]},\"actions\":[{\"n\":\"config\",\"t\":\">H\",\"w\":\"0c\",\"f\":\"B\",\"r\":[0,65535],\"d\":38678,\"desc\":\"CONFIG register\"},{\"n\":\"reset config\",\"w\":\"0c9716\",\"desc\":\"Reset CONFIG\"}]}},\"MAX30101\":{\"addresses\":\"0x57\",\"detectionValues\":\"0xff=0x15\",\"initValues\":\"0x0940=p10&0x0907&0x085f&0x00=r1&0x0a27&0x0c404040&0x112103&0x04000000\",\"pollInfo\":{\"c\":\"0x04=r75&0x06=r1\",\"i\":200,\"s\":5},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"bench\",\"note\":\"bench (Axiom018, 2026-10-24, MAX30105): the lone 0x99 + 9-byte read (0x99-0xA1, not in the register map) read 99 af ff ff ff ff ff 00 00; FIFO, mode, SpO2, LED, slot and interrupt-enable registers unchanged and sampling carried on\"}},\"devInfoJson\":{\"name\":\"Heart & Optical Sensor\",\"desc\":\"Red, IR and green reflectance: heart rate, SpO2, proximity and (MAX30105) particles\",\"manu\":\"Maxim\",\"type\":\"MAX30101\",\"clas\":[\"HRM\"],\"resp\":{\"b\":76,\"a\":[{\"n\":\"Red\",\"t\":\">I\",\"u\":\"Red\",\"r\":[0,262143],\"f\":\"6d\",\"o\":\"uint32\"},{\"n\":\"IR\",\"t\":\">I\",\"u\":\"IR\",\"r\":[0,262143],\"f\":\"6d\",\"o\":\"uint32\"},{\"n\":\"Green\",\"t\":\">I\",\"u\":\"Green\",\"r\":[0,262143],\"f\":\"6d\",\"o\":\"uint32\"}],\"c\":{\"n\":\"max30101_fifo\",\"c\":\"int N=(buf[75]+32-buf[2])%32;if(N>8){N=8;}int k=3;int i=0;while(iH\",\"u\":\"°C\",\"r\":[-40,125],\"m\":\"0x1fff\",\"d\":16,\"sb\":12,\"f\":\"3.2f\",\"o\":\"float\"}]}}},\"MICS6814Pimoroni\":{\"addresses\":\"0x19\",\"detectionValues\":\"0xfa=0x6ae2\",\"initValues\":\"0x71=r1&0x72=r1&0x73=r1&0x74=r1&0xa1=r1&0x71{p0m1}&0x72{p0m2}&0x73{p1m1}&0x74{p1m2}&0x5005&0xa1{adccon1}\",\"initCode\":{\"n\":\"mics6814_pins\",\"c\":\"init.p0m1=buf[0]|224;init.p0m2=buf[1]&31;init.p1m1=buf[2]&223;init.p1m2=buf[3]|32;init.adccon1=buf[4]|1;\"},\"pollInfo\":{\"c\":\"0xb604&0xa842=p2&0x82=r2&0xb608&0xa843=p2&0x82=r2&0xb610&0xa844=p2&0x82=r2\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"inferred\",\"note\":\"Pimoroni IO Expander firmware (not public): a one-byte write only sets the pointer; 0x99-0xA1 are PWM and ADC configuration registers with no read side effect (ioe-python register map)\"}},\"devInfoJson\":{\"name\":\"Gas Sensor\",\"desc\":\"Reducing, NH3 and oxidising gases\",\"manu\":\"Pimoroni\",\"type\":\"MICS6814Pimoroni\",\"clas\":[\"GAS\"],\"resp\":{\"b\":6,\"a\":[{\"n\":\"reducing\",\"t\":\"H\",\"u\":\"µT\",\"r\":[-3000,3000],\"d\":10,\"a\":-3276.8,\"f\":\".1f\",\"o\":\"float\"},{\"n\":\"y\",\"t\":\">H\",\"u\":\"µT\",\"r\":[-3000,3000],\"d\":10,\"a\":-3276.8,\"f\":\".1f\",\"o\":\"float\"},{\"n\":\"z\",\"t\":\">H\",\"u\":\"µT\",\"r\":[-3000,3000],\"d\":10,\"a\":-3276.8,\"f\":\".1f\",\"o\":\"float\"}]}}},\"MPRLS\":{\"addresses\":\"0x18\",\"detectionValues\":\"\",\"initValues\":\"0xAA0000\",\"pollInfo\":{\"c\":\"=r4&0xAA0000\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Pressure Sensor\",\"desc\":\"\",\"manu\":\"Honeywell\",\"type\":\"MPRLS\",\"clas\":[\"PRES\"],\"resp\":{\"b\":4,\"a\":[{\"n\":\"status\",\"t\":\"B\",\"r\":[0,255],\"m\":\"0xff\",\"f\":\"02x\",\"o\":\"uint8\",\"vs\":false,\"bl\":{\"0x01\":\"saturated\",\"0x04\":\"mem error\",\"0x20\":\"busy\",\"0x40\":\"powered\"}},{\"n\":\"pressure\",\"at\":0,\"m\":\"0x00ffffff\",\"t\":\">I\",\"u\":\"hPa\",\"r\":[0,1724],\"a\":-215.461,\"d\":7786.654,\"f\":\"4.2f\",\"o\":\"float\"}]}}},\"MS8607PT\":{\"addresses\":\"0x76\",\"identPriority\":-1,\"detectionValues\":\"0x00=r3&0x00=0x000000\",\"initValues\":\"0x1e=p5\",\"pollInfo\":{\"c\":\"0x4a=p20&0x00=r3&0x5a=p20&0x00=r3&0xa2=r2&0xa4=r2&0xa6=r2&0xa8=r2&0xaa=r2&0xac=r2\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Pressure & Temperature Sensor\",\"desc\":\"MS8607 pressure part\",\"manu\":\"TE\",\"type\":\"MS8607PT\",\"clas\":[\"PRES\",\"TEMP\"],\"resp\":{\"b\":18,\"a\":[{\"n\":\"temperature\",\"t\":\"=2000.0){t2=5.0*dT*dT/274877906944.0;}t=t-t2;off=off-off2;sens=sens-sens2;float p=(D1*sens/2097152.0-off)/32768.0;out.temperature=t/100.0;out.pressure=p/100.0;\"}}}},\"MS8607RH\":{\"addresses\":\"0x40\",\"identPriority\":-1,\"detectionValues\":\"0xe7=0bX000XX1X\",\"initValues\":\"0xfe=p15\",\"pollInfo\":{\"c\":\"0xf5=p20&=r3\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Humidity Sensor\",\"desc\":\"MS8607 humidity part\",\"manu\":\"TE\",\"type\":\"MS8607RH\",\"clas\":[\"RH\"],\"resp\":{\"b\":3,\"a\":[{\"n\":\"humidity\",\"t\":\">H\",\"u\":\"%\",\"r\":[0,100],\"m\":\"0xfffc\",\"d\":524.288,\"a\":-6,\"f\":\"3.1f\",\"o\":\"float\"}]}}},\"MT6701\":{\"addresses\":\"0x06\",\"detectionValues\":\"\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x03=r2\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"Magnetic Encoder\",\"desc\":\"\",\"manu\":\"MagnTek\",\"type\":\"MT6701\",\"clas\":[\"ANG\"],\"resp\":{\"b\":2,\"a\":[{\"n\":\"angle\",\"t\":\">H\",\"u\":\"degrees\",\"r\":[0,360],\"f\":\"3.1f\",\"o\":\"float\",\"d\":182.04444444444445}]}}},\"MXC400xXC\":{\"addresses\":\"0x10-0x17\",\"detectionValues\":\"0x0E=0x02,0x03\",\"initValues\":\"0x0110=p5&0x0d00=\",\"pollInfo\":{\"c\":\"0x03=r7\",\"i\":100,\"s\":3},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Accelerometer\",\"desc\":\"3-Axis Accel\",\"manu\":\"Memsic\",\"type\":\"MXC400xXC\",\"clas\":[\"ACC\"],\"resp\":{\"b\":7,\"a\":[{\"n\":\"ax\",\"t\":\">h\",\"u\":\"g\",\"r\":[-2.0,2.0],\"d\":16384,\"f\":\".2f\",\"o\":\"float\"},{\"n\":\"ay\",\"t\":\">h\",\"u\":\"g\",\"r\":[-2.0,2.0],\"d\":16384,\"f\":\".2f\",\"o\":\"float\"},{\"n\":\"az\",\"t\":\">h\",\"u\":\"g\",\"r\":[-2.0,2.0],\"d\":16384,\"f\":\".2f\",\"o\":\"float\"},{\"n\":\"temperature\",\"t\":\"b\",\"u\":\"°C\",\"r\":[-40,85],\"d\":1.7065,\"a\":25,\"f\":\".2f\",\"o\":\"float\"}]}}},\"PCT2075\":{\"addresses\":\"0x28-0x2F,0x35-0x37,0x48-0x4F,0x74-0x77\",\"identPriority\":-1,\"detectionValues\":\"0x00=0bXXXXXXXXXXX00000&0x01=0b000XXXXX&0x02=0bXXXXXXXXX0000000&0x03=0bXXXXXXXXX0000000&0x04=0b000XXXXX&0x07=0xFFFF\",\"initValues\":\"0x0100=\",\"pollInfo\":{\"c\":\"0x00=r2\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Temperature Sensor\",\"desc\":\"\",\"manu\":\"NXP\",\"type\":\"PCT2075\",\"clas\":[\"TEMP\"],\"resp\":{\"b\":2,\"a\":[{\"n\":\"temperature\",\"t\":\">h\",\"u\":\"°C\",\"r\":[-55,125],\"d\":256,\"f\":\"3.2f\",\"o\":\"float\"}]}}},\"QwiicButton\":{\"addresses\":\"0x6f\",\"detectionValues\":\"0x00=0b01011101\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x03=r1\",\"i\":100,\"s\":5},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"changes\",\"basis\":\"bench\",\"note\":\"bench (Axiom018, button on a mux slot, 2026-10-23): the lone 0x99 + 9-byte read fails with an I2C hardware timeout and the button drops off the bus for about 1.1 s, then is found and identified again; devices on other slots and the main bus are unaffected (the mux isolates the slot, the master times out after about 1.6 ms). Source: the read handler doesn't check the register pointer, so from 0x99 the length wraps to 65415 and the handler loops inside the I2C interrupt, holding the clock (Qwiic_Button e89a82f interrupts.ino 76-91). No setting changes. Its default address 0x6F is excluded from RSAO addressing\"}},\"devInfoJson\":{\"name\":\"QWIIC Button\",\"desc\":\"\",\"manu\":\"Sparkfun\",\"type\":\"QwiicButton\",\"clas\":[\"BTN\"],\"resp\":{\"b\":1,\"a\":[{\"n\":\"press\",\"t\":\"B\",\"u\":\"\",\"r\":[0,1],\"m\":\"0x04\",\"s\":2,\"f\":\"b\",\"o\":\"bool\"}]},\"actions\":[{\"n\":\"LED\",\"desc\":\"LED presets: off, on (full brightness, steady) or breathe (full brightness, 1 s pulse, 0.5 s off)\",\"map\":{\"off\":{\"w\":\"1900&1b0000\"},\"on\":{\"w\":\"19ff&1b0000\"},\"breathe\":{\"w\":\"19ff&1a01&1be803&1df401\"}}},{\"n\":\"LED brightness\",\"desc\":\"LED brightness, 0-255 (when pulsing, the peak of each pulse)\",\"t\":\"B\",\"w\":\"19\",\"r\":[0,255],\"d\":255},{\"n\":\"LED pulse step\",\"desc\":\"Brightness change at each step of a pulse, 1-255 (1 is smoothest)\",\"t\":\"B\",\"w\":\"1a\",\"r\":[1,255],\"d\":1},{\"n\":\"LED pulse time\",\"desc\":\"Length of one pulse (ramp up and down) in ms; 0 = no pulsing, the LED stays at its brightness\",\"t\":\">7)&255;float co2=0.0;if(co2e>0){if(co2e<255){co2=1.0+(((co2h&127)*65536)+co2l)/8388608.0;int co2k=co2e-127;while(co2k>0){co2=co2*2.0;co2k=co2k-1;}while(co2k<0){co2=co2/2.0;co2k=co2k+1;}}}if(co2h>32767){co2=0.0-co2;}out.CO2=co2;int tmph=(buf[12]<<8)|buf[13];int tmpl=(buf[15]<<8)|buf[16];int tmpe=(tmph>>7)&255;float tmp=0.0;if(tmpe>0){if(tmpe<255){tmp=1.0+(((tmph&127)*65536)+tmpl)/8388608.0;int tmpk=tmpe-127;while(tmpk>0){tmp=tmp*2.0;tmpk=tmpk-1;}while(tmpk<0){tmp=tmp/2.0;tmpk=tmpk+1;}}}if(tmph>32767){tmp=0.0-tmp;}out.temperature=tmp;int humh=(buf[18]<<8)|buf[19];int huml=(buf[21]<<8)|buf[22];int hume=(humh>>7)&255;float hum=0.0;if(hume>0){if(hume<255){hum=1.0+(((humh&127)*65536)+huml)/8388608.0;int humk=hume-127;while(humk>0){hum=hum*2.0;humk=humk-1;}while(humk<0){hum=hum/2.0;humk=humk+1;}}}if(humh>32767){hum=0.0-hum;}out.humidity=hum;next;}\"}},\"actions\":[{\"n\":\"_conf.rate\",\"r\":[0.0167,0.333],\"d\":0.333,\"desc\":\"Sample rate (Hz)\",\"map\":{\"0.333\":{\"i\":3000000,\"s\":1},\"0.2\":{\"i\":5000000,\"s\":1},\"0.1\":{\"i\":10000000,\"s\":1},\"0.0333\":{\"i\":30000000,\"s\":1},\"0.0167\":{\"i\":60000000,\"s\":1}}},{\"n\":\"autoCal\",\"t\":\">H\",\"w\":\"5306\",\"crc\":\"crc8w\",\"r\":[0,1],\"d\":0,\"desc\":\"Auto-calibration\"},{\"n\":\"forceCal\",\"t\":\">H\",\"w\":\"5204\",\"crc\":\"crc8w\",\"r\":[400,2000],\"d\":420,\"desc\":\"Force cal (ppm)\"},{\"n\":\"tempOffset\",\"t\":\">H\",\"w\":\"5403\",\"crc\":\"crc8w\",\"mul\":100,\"r\":[0,20],\"d\":0,\"desc\":\"Temp offset (C)\"},{\"n\":\"altitude\",\"t\":\">H\",\"w\":\"5102\",\"crc\":\"crc8w\",\"r\":[0,3000],\"d\":0,\"desc\":\"Altitude (m)\"},{\"n\":\"pressure\",\"t\":\">H\",\"w\":\"0010\",\"crc\":\"crc8w\",\"r\":[0,1400],\"d\":0,\"desc\":\"Pressure (mbar)\"},{\"n\":\"softReset\",\"w\":\"d304\",\"desc\":\"Soft reset\"}]}},\"SCD40\":{\"addresses\":\"0x62\",\"detectionValues\":\"\",\"initValues\":\"0x21b1=\",\"pollInfo\":{\"c\":\"0xec05=r9\",\"i\":5000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"CO2 Sensor\",\"desc\":\"\",\"manu\":\"Sensirion\",\"type\":\"SCD40\",\"clas\":[\"CO2\",\"TEMP\",\"RH\"],\"resp\":{\"b\":9,\"a\":[{\"n\":\"CO2\",\"t\":\">H\",\"u\":\"ppm\",\"r\":[0,40000],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"temperature\",\"at\":3,\"t\":\">H\",\"u\":\"°C\",\"r\":[-45,175],\"d\":374.4857142857143,\"a\":-45,\"f\":\".2f\",\"o\":\"float\"},{\"n\":\"humidity\",\"at\":6,\"t\":\">H\",\"u\":\"%\",\"r\":[0,100],\"d\":655.35,\"f\":\".2f\",\"o\":\"float\"}]},\"actions\":[{\"n\":\"temperatureOffset\",\"t\":\">H\",\"w\":\"241d\",\"crc\":\"crc8w\",\"mul\":374.4857,\"r\":[0,20],\"d\":4,\"desc\":\"Temp offset (C)\"},{\"n\":\"altitude\",\"t\":\">H\",\"w\":\"2427\",\"crc\":\"crc8w\",\"r\":[0,3000],\"d\":0,\"desc\":\"Altitude (m)\"},{\"n\":\"ambientPressure\",\"t\":\">H\",\"w\":\"e000\",\"crc\":\"crc8w\",\"r\":[700,1200],\"d\":1013,\"desc\":\"Pressure (hPa)\"},{\"n\":\"autoCalibration\",\"t\":\">H\",\"w\":\"2416\",\"crc\":\"crc8w\",\"r\":[0,1],\"d\":1,\"desc\":\"Auto-calibration\"},{\"n\":\"calibrationTarget\",\"t\":\">H\",\"w\":\"243a\",\"crc\":\"crc8w\",\"r\":[0,65535],\"d\":400,\"desc\":\"ASC target (ppm)\"}]}},\"SGP30\":{\"addresses\":\"0x58\",\"detectionValues\":\"0x202f=p10&=0b0000XXXXXXXXXXXXXXXXXXXX:crc8w\",\"initValues\":\"0x2003=p10\",\"pollInfo\":{\"c\":\"0x2008=p12&=r6\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"bench\",\"note\":\"bench (Axiom018, 2026-10-23): the lone 0x99 is NACKed (half a 16-bit command); the serial number, feature set and measurements are unchanged\"}},\"devInfoJson\":{\"name\":\"Gas Sensor (VOC)\",\"desc\":\"\",\"manu\":\"Sensirion\",\"type\":\"SGP30\",\"clas\":[\"VOC\"],\"resp\":{\"b\":6,\"a\":[{\"n\":\"eCO2\",\"t\":\">H\",\"u\":\"ppm\",\"r\":[400,60000],\"f\":\"5d\",\"o\":\"uint16\"},{\"n\":\"VOC\",\"at\":3,\"t\":\">H\",\"u\":\"ppb\",\"r\":[0,60000],\"f\":\"5d\",\"o\":\"uint16\"}]},\"actions\":[{\"n\":\"setBaseline\",\"t\":\">HH\",\"w\":\"201e\",\"crc\":\"crc8w\",\"r\":[0,65535],\"d\":0,\"desc\":\"Set baseline\"},{\"n\":\"setHumidity\",\"t\":\">H\",\"w\":\"2061\",\"crc\":\"crc8w\",\"mul\":256,\"r\":[0,255],\"d\":0,\"desc\":\"Humidity (g/m3)\"}]}},\"SGP40\":{\"addresses\":\"0x59\",\"detectionValues\":\"0x3682=p1&=0bXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\"initValues\":\"0x260f8000a2666693\",\"pollInfo\":{\"c\":\"=r3&0x260f8000a2666693=\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"bench\",\"note\":\"bench (Axiom018, 2026-10-23): the lone 0x99 is NACKed (half a 16-bit command), so nothing is sent on; the serial number and the measurement are unchanged\"}},\"devInfoJson\":{\"name\":\"SGP40\",\"desc\":\"Gas Sensor\",\"manu\":\"Sensirion\",\"type\":\"SGP40\",\"clas\":[\"VOC\"],\"resp\":{\"b\":3,\"a\":[{\"n\":\"vocTicks\",\"t\":\">H\",\"u\":\"ticks\",\"r\":[0,65535],\"f\":\"5d\",\"o\":\"uint16\"}]}}},\"SHT31\":{\"addresses\":\"0x44-0x45\",\"detectionValues\":\"0xf32d=p1&=0bXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\"initValues\":\"0x3093=p2\",\"pollInfo\":{\"c\":\"0x2400=p16&=r6\",\"i\":5000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Temperature & Humidity Sensor\",\"desc\":\"\",\"manu\":\"Sensirion\",\"type\":\"SHT31\",\"clas\":[\"TEMP\",\"RH\"],\"resp\":{\"b\":6,\"a\":[{\"n\":\"temperature\",\"t\":\">H\",\"u\":\"°C\",\"r\":[-40,125],\"d\":374.48571428571427,\"a\":-45,\"f\":\"3.2f\",\"o\":\"float\"},{\"n\":\"humidity\",\"at\":3,\"t\":\">H\",\"u\":\"%\",\"r\":[0,100],\"d\":655.35,\"f\":\"3.1f\",\"o\":\"float\"}]}}},\"SHT40\":{\"addresses\":\"0x44-0x46\",\"detectionValues\":\"0x89=p10&=0bXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0xfd=p10&=r6\",\"i\":5000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Temperature & Humidity Sensor\",\"desc\":\"\",\"manu\":\"Sensirion\",\"type\":\"SHT40\",\"clas\":[\"TEMP\",\"RH\"],\"resp\":{\"b\":6,\"a\":[{\"n\":\"temperature\",\"t\":\">H\",\"u\":\"°C\",\"r\":[-45,130],\"d\":374.48571428571427,\"a\":-45,\"f\":\"3.2f\",\"o\":\"float\"},{\"n\":\"humidity\",\"at\":3,\"t\":\">H\",\"u\":\"%\",\"r\":[0,100],\"d\":524.28,\"a\":-6,\"f\":\"3.1f\",\"o\":\"float\"}]}}},\"SHTC3\":{\"addresses\":\"0x70\",\"detectionValues\":\"0x3517=p1&0xefc8=p1&=0bXXXX1XXXXX000111XXXXXXXX:crc8w\",\"initValues\":\"0x3517=p1\",\"pollInfo\":{\"c\":\"0x7866=p13&=r6\",\"i\":5000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Temperature & Humidity Sensor\",\"desc\":\"\",\"manu\":\"Sensirion\",\"type\":\"SHTC3\",\"clas\":[\"TEMP\",\"RH\"],\"resp\":{\"b\":6,\"a\":[{\"n\":\"temperature\",\"t\":\">H\",\"u\":\"°C\",\"r\":[-40,125],\"d\":374.4914285714286,\"a\":-45,\"f\":\"3.2f\",\"o\":\"float\"},{\"n\":\"humidity\",\"at\":3,\"t\":\">H\",\"u\":\"%\",\"r\":[0,100],\"d\":655.36,\"f\":\"3.1f\",\"o\":\"float\"}]}}},\"STCC4\":{\"addresses\":\"0x64-0x65\",\"detectionValues\":\"\",\"initValues\":\"0x218b=\",\"pollInfo\":{\"c\":\"0xec05=p1&=r12\",\"i\":1500,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"CO2 Sensor\",\"desc\":\"CO2, Temp & Humidity\",\"manu\":\"Sensirion\",\"type\":\"STCC4\",\"clas\":[\"CO2\",\"TEMP\",\"RH\"],\"resp\":{\"b\":12,\"a\":[{\"n\":\"CO2\",\"at\":0,\"t\":\">h\",\"u\":\"ppm\",\"r\":[0,32000],\"f\":\"d\",\"o\":\"int16\"},{\"n\":\"temperature\",\"at\":3,\"t\":\">H\",\"u\":\"°C\",\"r\":[-45,130],\"d\":374.48571428571427,\"a\":-45,\"f\":\".2f\",\"o\":\"float\"},{\"n\":\"humidity\",\"at\":6,\"t\":\">H\",\"u\":\"%\",\"r\":[0,100],\"d\":524.28,\"a\":-6,\"f\":\".1f\",\"o\":\"float\"},{\"n\":\"status\",\"at\":9,\"t\":\">H\",\"u\":\"\",\"r\":[0,65535],\"f\":\"04x\",\"o\":\"uint16\",\"vs\":false,\"bl\":{\"0\":\"OK\",\"0x0040\":\"testing\"}}]},\"actions\":[{\"n\":\"_conf.rate\",\"r\":[0.0167,0.667],\"d\":0.667,\"desc\":\"Sample rate (Hz)\",\"map\":{\"0.667\":{\"i\":1500000,\"s\":1},\"0.5\":{\"i\":2000000,\"s\":1},\"0.2\":{\"i\":5000000,\"s\":1},\"0.1\":{\"i\":10000000,\"s\":1},\"0.0333\":{\"i\":30000000,\"s\":1},\"0.0167\":{\"i\":60000000,\"s\":1}}},{\"n\":\"testingMode\",\"map\":{\"0\":\"3f3d\",\"1\":\"3fbc\"},\"desc\":\"Testing mode\"},{\"n\":\"pressure\",\"t\":\">H\",\"w\":\"e016\",\"crc\":\"crc8w\",\"mul\":50,\"r\":[400,1100],\"d\":1013,\"desc\":\"Pressure (hPa)\"}]}},\"TMP117\":{\"addresses\":\"0x48-0x4B\",\"detectionValues\":\"0x0F=0x0117\",\"initValues\":\"0x010220=\",\"pollInfo\":{\"c\":\"0x00=r2\",\"i\":1000,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Temperature Sensor\",\"desc\":\"\",\"manu\":\"Texas Instruments\",\"type\":\"TMP117\",\"clas\":[\"TEMP\"],\"resp\":{\"b\":2,\"a\":[{\"n\":\"temperature\",\"t\":\">h\",\"u\":\"°C\",\"r\":[-55,150],\"d\":128,\"a\":0,\"f\":\"3.2f\",\"o\":\"float\"}]}}},\"TrillBar\":{\"addresses\":\"0x20-0x27\",\"detectionValues\":\"0x00FF=p25&=0bXXXXXXXX00000001\",\"initValues\":\"0x000100=p15&0x0006=p30\",\"pollInfo\":{\"c\":\"0x04=r20\",\"i\":50,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"source\",\"note\":\"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\"devInfoJson\":{\"name\":\"Trill Bar\",\"desc\":\"Capacitive Touch Bar\",\"manu\":\"Bela\",\"type\":\"TrillBar\",\"clas\":[\"TCH\"],\"resp\":{\"b\":20,\"a\":[{\"n\":\"loc0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc4\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size4\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"}]},\"actions\":[{\"n\":\"baseline\",\"w\":\"0006\",\"desc\":\"Update baseline\"},{\"n\":\"_conf.mode\",\"t\":\"B\",\"r\":[0,3],\"d\":0,\"desc\":\"Sensor mode\",\"map\":{\"centroid\":{\"w\":\"000100\"},\"raw\":{\"w\":\"000101\"},\"baseline\":{\"w\":\"000102\"},\"diff\":{\"w\":\"000103\"}}},{\"n\":\"_conf.noise\",\"t\":\"B\",\"w\":\"0004\",\"r\":[0,255],\"d\":0,\"desc\":\"Noise threshold\"},{\"n\":\"_conf.prescaler\",\"t\":\"B\",\"w\":\"0003\",\"r\":[0,8],\"d\":2,\"desc\":\"Prescaler\"},{\"n\":\"_conf.minsize\",\"t\":\">H\",\"w\":\"0007\",\"r\":[0,65535],\"d\":0,\"desc\":\"Minimum touch size\"}]}},\"TrillCraft\":{\"addresses\":\"0x30-0x37\",\"detectionValues\":\"0x00FF=p25&=0bXXXXXXXX00000011\",\"initValues\":\"0x000100=p15&0x0006=p30\",\"pollInfo\":{\"c\":\"0x04=r20\",\"i\":50,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"source\",\"note\":\"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\"devInfoJson\":{\"name\":\"Trill Craft\",\"desc\":\"Capacitive Touch Craft\",\"manu\":\"Bela\",\"type\":\"TrillCraft\",\"clas\":[\"TCH\"],\"resp\":{\"b\":20,\"a\":[{\"n\":\"loc0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc4\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size4\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"}]},\"actions\":[{\"n\":\"baseline\",\"w\":\"0006\",\"desc\":\"Update baseline\"},{\"n\":\"_conf.mode\",\"t\":\"B\",\"r\":[0,3],\"d\":0,\"desc\":\"Sensor mode\",\"map\":{\"centroid\":{\"w\":\"000100\"},\"raw\":{\"w\":\"000101\"},\"baseline\":{\"w\":\"000102\"},\"diff\":{\"w\":\"000103\"}}},{\"n\":\"_conf.noise\",\"t\":\"B\",\"w\":\"0004\",\"r\":[0,255],\"d\":0,\"desc\":\"Noise threshold\"},{\"n\":\"_conf.prescaler\",\"t\":\"B\",\"w\":\"0003\",\"r\":[0,8],\"d\":2,\"desc\":\"Prescaler\"},{\"n\":\"_conf.minsize\",\"t\":\">H\",\"w\":\"0007\",\"r\":[0,65535],\"d\":0,\"desc\":\"Minimum touch size\"}]}},\"TrillFlex\":{\"addresses\":\"0x48-0x4F\",\"detectionValues\":\"0x00FF=p25&=0bXXXXXXXX00000110\",\"initValues\":\"0x000100=p15&0x000304=p15&0x0002000c=p15&0x0006=p30&0x00047b=p15\",\"pollInfo\":{\"c\":\"0x04=r20\",\"i\":50,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"source\",\"note\":\"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\"devInfoJson\":{\"name\":\"Trill Flex\",\"desc\":\"Capacitive Touch Flex\",\"manu\":\"Bela\",\"type\":\"TrillFlex\",\"clas\":[\"TCH\"],\"resp\":{\"b\":20,\"a\":[{\"n\":\"loc\",\"t\":\">H[5]\",\"r\":[0,3712],\"f\":\"d\",\"o\":\"uint16\",\"vt\":\"touchstrip\",\"vo\":{\"sizeAttr\":\"size\",\"noValue\":65535,\"posMax\":3712,\"sizeMax\":1200,\"numChannels\":30}},{\"n\":\"size\",\"t\":\">H[5]\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"}]},\"actions\":[{\"n\":\"baseline\",\"w\":\"0006\",\"desc\":\"Update baseline\"},{\"n\":\"_conf.mode\",\"t\":\"B\",\"r\":[0,3],\"d\":0,\"desc\":\"Sensor mode\",\"map\":{\"centroid\":{\"w\":\"000100\"},\"raw\":{\"w\":\"000101\"},\"baseline\":{\"w\":\"000102\"},\"diff\":{\"w\":\"000103\"}}},{\"n\":\"_conf.noise\",\"t\":\"B\",\"w\":\"0004\",\"r\":[0,255],\"d\":123,\"desc\":\"Noise threshold (counts at 12 bits; 123 is the Trill library's 0.03)\"},{\"n\":\"_conf.prescaler\",\"t\":\"B\",\"w\":\"0003\",\"r\":[1,8],\"d\":4,\"desc\":\"Prescaler (higher reads lower; lower it for more sensitivity, raise it if channels saturate at 4095)\"},{\"n\":\"_conf.minsize\",\"t\":\">H\",\"w\":\"0007\",\"r\":[0,65535],\"d\":0,\"desc\":\"Minimum touch size\"}]}},\"TrillHex\":{\"addresses\":\"0x40-0x47\",\"detectionValues\":\"0x00FF=p25&=0bXXXXXXXX00000101\",\"initValues\":\"0x000100=p15&0x0006=p30\",\"pollInfo\":{\"c\":\"0x04=r32\",\"i\":50,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"source\",\"note\":\"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\"devInfoJson\":{\"name\":\"Trill Hex\",\"desc\":\"Capacitive Touch Hex\",\"manu\":\"Bela\",\"type\":\"TrillHex\",\"clas\":[\"TCH\"],\"resp\":{\"b\":32,\"a\":[{\"n\":\"vloc0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vloc1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vloc2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vloc3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vsize0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vsize1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vsize2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vsize3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hloc0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hloc1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hloc2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hloc3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hsize0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hsize1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hsize2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hsize3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"}]},\"actions\":[{\"n\":\"baseline\",\"w\":\"0006\",\"desc\":\"Update baseline\"},{\"n\":\"_conf.mode\",\"t\":\"B\",\"r\":[0,3],\"d\":0,\"desc\":\"Sensor mode\",\"map\":{\"centroid\":{\"w\":\"000100\"},\"raw\":{\"w\":\"000101\"},\"baseline\":{\"w\":\"000102\"},\"diff\":{\"w\":\"000103\"}}},{\"n\":\"_conf.noise\",\"t\":\"B\",\"w\":\"0004\",\"r\":[0,255],\"d\":0,\"desc\":\"Noise threshold\"},{\"n\":\"_conf.prescaler\",\"t\":\"B\",\"w\":\"0003\",\"r\":[0,8],\"d\":2,\"desc\":\"Prescaler\"},{\"n\":\"_conf.minsize\",\"t\":\">H\",\"w\":\"0007\",\"r\":[0,65535],\"d\":0,\"desc\":\"Minimum touch size\"}]}},\"TrillRing\":{\"addresses\":\"0x38-0x3F\",\"detectionValues\":\"0x00FF=p25&=0bXXXXXXXX00000100\",\"initValues\":\"0x000100=p15&0x0006=p30\",\"pollInfo\":{\"c\":\"0x04=r24\",\"i\":50,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"source\",\"note\":\"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\"devInfoJson\":{\"name\":\"Trill Ring\",\"desc\":\"Capacitive Touch Ring\",\"manu\":\"Bela\",\"type\":\"TrillRing\",\"clas\":[\"TCH\",\"BTN\"],\"resp\":{\"b\":24,\"a\":[{\"n\":\"loc0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"loc4\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"size4\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"btn0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"btn1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"}]},\"actions\":[{\"n\":\"baseline\",\"w\":\"0006\",\"desc\":\"Update baseline\"},{\"n\":\"_conf.mode\",\"t\":\"B\",\"r\":[0,3],\"d\":0,\"desc\":\"Sensor mode\",\"map\":{\"centroid\":{\"w\":\"000100\"},\"raw\":{\"w\":\"000101\"},\"baseline\":{\"w\":\"000102\"},\"diff\":{\"w\":\"000103\"}}},{\"n\":\"_conf.noise\",\"t\":\"B\",\"w\":\"0004\",\"r\":[0,255],\"d\":0,\"desc\":\"Noise threshold\"},{\"n\":\"_conf.prescaler\",\"t\":\"B\",\"w\":\"0003\",\"r\":[0,8],\"d\":2,\"desc\":\"Prescaler\"},{\"n\":\"_conf.minsize\",\"t\":\">H\",\"w\":\"0007\",\"r\":[0,65535],\"d\":0,\"desc\":\"Minimum touch size\"}]}},\"TrillSquare\":{\"addresses\":\"0x28-0x2F\",\"detectionValues\":\"0x00FF=p25&=0bXXXXXXXX00000010\",\"initValues\":\"0x000100=p15&0x0006=p30\",\"pollInfo\":{\"c\":\"0x04=r32\",\"i\":50,\"s\":1},\"scanPriority\":\"high\",\"hwProbeEffects\":{\"rsao\":{\"effect\":\"none\",\"basis\":\"source\",\"note\":\"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\"devInfoJson\":{\"name\":\"Trill Square\",\"desc\":\"Capacitive Touch Square\",\"manu\":\"Bela\",\"type\":\"TrillSquare\",\"clas\":[\"TCH\"],\"resp\":{\"b\":32,\"a\":[{\"n\":\"vloc0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vloc1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vloc2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vloc3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vsize0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vsize1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vsize2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"vsize3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hloc0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hloc1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hloc2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hloc3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hsize0\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hsize1\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hsize2\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"},{\"n\":\"hsize3\",\"t\":\">H\",\"r\":[0,65535],\"f\":\"d\",\"o\":\"uint16\"}]},\"actions\":[{\"n\":\"baseline\",\"w\":\"0006\",\"desc\":\"Update baseline\"},{\"n\":\"_conf.mode\",\"t\":\"B\",\"r\":[0,3],\"d\":0,\"desc\":\"Sensor mode\",\"map\":{\"centroid\":{\"w\":\"000100\"},\"raw\":{\"w\":\"000101\"},\"baseline\":{\"w\":\"000102\"},\"diff\":{\"w\":\"000103\"}}},{\"n\":\"_conf.noise\",\"t\":\"B\",\"w\":\"0004\",\"r\":[0,255],\"d\":0,\"desc\":\"Noise threshold\"},{\"n\":\"_conf.prescaler\",\"t\":\"B\",\"w\":\"0003\",\"r\":[0,8],\"d\":2,\"desc\":\"Prescaler\"},{\"n\":\"_conf.minsize\",\"t\":\">H\",\"w\":\"0007\",\"r\":[0,65535],\"d\":0,\"desc\":\"Minimum touch size\"}]}},\"VCNL4040\":{\"addresses\":\"0x60\",\"detectionValues\":\"0x0c=0b100001100000XXXX\",\"initValues\":\"0x041007=&0x030e08=&0x000000=\",\"pollInfo\":{\"c\":\"0x08=r2&0x09=r2&0x0a=r2\",\"i\":200,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Light Sensor (ambient & proximity)\",\"desc\":\"\",\"manu\":\"Vishay\",\"type\":\"VCNL4040\",\"clas\":[\"PROX\",\"LGHT\"],\"resp\":{\"b\":6,\"a\":[{\"n\":\"prox\",\"t\":\"H\",\"u\":\"mm\",\"r\":[0,2000],\"f\":\"4d\",\"o\":\"uint16\",\"vft\":\"valid\"}]}}},\"VL53L1X\":{\"addresses\":\"0x29\",\"identPriority\":1,\"detectionValues\":\"0x010f=0xEACC\",\"initValues\":\"0x002D01&0x0087&0x25012c00=p100&0x0060=p100\",\"pollInfo\":{\"c\":\"0x0089=r1&0x0096=r2\",\"i\":200,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Distance Sensor\",\"desc\":\"\",\"manu\":\"ST\",\"type\":\"VL53L1X\",\"clas\":[\"DIST\"],\"resp\":{\"b\":3,\"a\":[{\"n\":\"valid\",\"t\":\"B\",\"u\":\"\",\"r\":[0,1],\"m\":\"0x04\",\"x\":\"0x04\",\"s\":2,\"f\":\"b\",\"o\":\"bool\",\"vs\":false},{\"n\":\"dist\",\"t\":\">H\",\"u\":\"mm\",\"r\":[0,4000],\"f\":\"4d\",\"o\":\"uint16\",\"vft\":\"valid\"}]}}},\"VL53L4CD\":{\"addresses\":\"0x29\",\"identPriority\":1,\"detectionValues\":\"0x010F=0xEBAA\",\"initValues\":\"0x002D0001011102000208000810010100000000FF000F0000000000200B000002142100&0x004e000500000000c8000038ff010008000001cc0701f10500a0008008380000&006c00000f89000000000000000107050606000002c7ff9b000000010000&0x008740=p200&0x008601&0x008780&0x000809&0x000b00&0x00240500&0x006c00000000&0x005e02f2&0x006103a1&008721\",\"pollInfo\":{\"c\":\"0x008601=&0x0089=r1&0x0096=r2\",\"i\":100,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Distance Sensor\",\"desc\":\"\",\"manu\":\"ST\",\"type\":\"VL53L4CD\",\"clas\":[\"DIST\"],\"resp\":{\"b\":3,\"a\":[{\"n\":\"valid\",\"t\":\"B\",\"u\":\"\",\"r\":[0,1],\"m\":\"0x04\",\"x\":\"0x04\",\"s\":2,\"f\":\"b\",\"o\":\"bool\",\"vs\":false},{\"n\":\"dist\",\"t\":\">H\",\"u\":\"mm\",\"r\":[0,1000],\"f\":\"3d\",\"o\":\"uint16\",\"vft\":\"valid\"}]}}},\"VL6180\":{\"identPriority\":2,\"addresses\":\"0x29\",\"detectionValues\":\"0x0000=0b10110100\",\"initValues\":\"0x001801=p100&0x020701&0x020801&0x009600&0x0097fd&0x00e301&0x00e403&0x00e502&0x00e601&0x00e703&0x00f502&0x00d905&0x00dbce&0x00dc03&0x00ddf8&0x009f00&0x00a33c&0x00b700&0x00bb3c&0x00b209&0x00ca09&0x019801&0x01b017&0x01ad00&0x00ff05&0x010005&0x019905&0x01a61b&0x01ac3e&0x01a71f&0x003000&0x001600&0x001110&0x010a30&0x0031ff&0x002e01=p10&0x001c31&0x001b08&0x001404&0x001507&0x001803\",\"pollInfo\":{\"c\":\"0x004d=r3&0x0062=r1&0x001507\",\"i\":200,\"s\":1},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Distance Sensor\",\"desc\":\"\",\"manu\":\"ST\",\"type\":\"VL6180\",\"clas\":[\"DIST\"],\"resp\":{\"b\":4,\"a\":[{\"n\":\"valid\",\"t\":\"B\",\"at\":0,\"u\":\"\",\"r\":[0,1],\"m\":\"0xF0\",\"s\":4,\"f\":\"b\",\"o\":\"bool\",\"vs\":false,\"lut\":[{\"r\":\"0\",\"v\":1},{\"r\":\"\",\"v\":0}]},{\"n\":\"err\",\"t\":\"B\",\"at\":0,\"u\":\"\",\"r\":[0,15],\"m\":\"0xF0\",\"s\":4,\"f\":\"d\",\"vs\":false,\"vl\":{\"0\":\"OK\",\"1\":\"VCSEL cont\",\"2\":\"VCSEL test\",\"3\":\"VCSEL wdog\",\"4\":\"PLL1\",\"5\":\"PLL2\",\"6\":\"early conv\",\"7\":\"max conv\",\"8\":\"no target\",\"9\":\"unused\",\"10\":\"unused\",\"11\":\"SNR\",\"12\":\"raw under\",\"13\":\"raw over\",\"14\":\"underflow\",\"15\":\"overflow\"}},{\"n\":\"fresh\",\"t\":\"B\",\"at\":2,\"u\":\"\",\"r\":[0,1],\"m\":\"0x04\",\"s\":2,\"f\":\"b\",\"o\":\"bool\",\"vs\":false},{\"n\":\"dist\",\"t\":\"B\",\"at\":3,\"u\":\"mm\",\"r\":[0,255],\"f\":\"3d\",\"o\":\"float\",\"vft\":\"valid\"}]},\"actions\":[{\"n\":\"_conf.rate\",\"t\":\"B\",\"r\":[1,50],\"d\":5,\"desc\":\"Sample rate (Hz)\",\"map\":{\"1\":{\"w\":\"001801&001b08&001c31&010a30&001507&001803\",\"i\":1000000,\"s\":1},\"2\":{\"w\":\"001801&001b08&001c31&010a30&001507&001803\",\"i\":500000,\"s\":1},\"5\":{\"w\":\"001801&001b08&001c31&010a30&001507&001803\",\"i\":200000,\"s\":1},\"10\":{\"w\":\"001801&001b08&001c31&010a30&001507&001803\",\"i\":100000,\"s\":1},\"20\":{\"w\":\"001801&001b03&001c19&010a30&001507&001803\",\"i\":50000,\"s\":1},\"50\":{\"w\":\"001801&001b00&001c03&010a08&001507&001803\",\"i\":20000,\"s\":1}}}]}}},\"_sources\":{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"},\"acme\":{\"git\":\"https://github.com/acme/devtypes.git\",\"ref\":\"v1.0.0\",\"commit\":\"3f2a9c1e7b0d0000000000000000000000000000\"}},\"typeFile\":{\"include\":[\"raftcore:*\",\"acme:Gadget\"],\"policy\":{\"TrillRing\":\"disabled\"}}}}", + "crc": "935c", + "entries": [ + { + "key": "ADXL313", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "AHT20", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "AMG8833", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "AS5600", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "AdafruitGamepad", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "AdafruitSoilSensor", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "BLEBTHome", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "BME280", + "kind": "new", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "BME680", + "kind": "new", + "initCode": "runtime", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "BME690", + "kind": "new", + "initCode": "runtime", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "CAP1188", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "CAP1203", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "DPS310", + "kind": "new", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "ENS160", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "GravityO2", + "kind": "new", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "HX711", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "LPS25", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "LPS3x", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "LSM6DS3", + "kind": "new", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "LSM6DS3TRC", + "kind": "new", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "LTR-329", + "kind": "new", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "LTR-390", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "M5-Weight", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "M5Encoder", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "MAX17048", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "MAX30101", + "kind": "new", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "MCP9808", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "MICS6814Pimoroni", + "kind": "new", + "initCode": "runtime", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "MMC5603", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "MPRLS", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "MS8607PT", + "kind": "new", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "MS8607RH", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "MT6701", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "MXC400xXC", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "PCT2075", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "QwiicButton", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "QwiicLEDStick", + "kind": "new" + }, + { + "key": "SCD30", + "kind": "new", + "initCode": "runtime", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "SCD40", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "SGP30", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "SGP40", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "SHT31", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "SHT40", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "SHTC3", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "STCC4", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "TMP117", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "TrillBar", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "TrillCraft", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "TrillFlex", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "TrillHex", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "TrillRing", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "TrillSquare", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "VCNL4040", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "VEML6040", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "VEML7700", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "VL53L0X", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "VL53L1X", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "VL53L4CD", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "VL6180", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + } + ] + }, + { + "name": "every RaftCore record, all compiled: unchanged", + "records": "{\"devTypes\":{\"ADXL313\":{\n \"addresses\": \"0x1d,0x53\",\n \"detectionValues\": \"0x00=0xAD1DCB\",\n \"initValues\": \"0x310b=&0x2d08=\",\n \"pollInfo\": {\n \"c\": \"0x32=r6\",\n \"i\": 100,\n \"s\": 3\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer\",\n \"desc\": \"3-Axis Accel\",\n \"manu\": \"Analog Devices\",\n \"type\": \"ADXL313\",\n \"clas\": [\"ACC\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"ax\",\n \"t\": \"I\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"m\": \"0xfffff000\",\n \"s\": 12,\n \"d\": 10485.76,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"temperature\",\n \"at\": 2,\n \"t\": \">I\",\n \"u\": \"°C\",\n \"r\": [-40, 85],\n \"m\": \"0x000fffff\",\n \"d\": 5242.88,\n \"a\": -50,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"AMG8833\":{\n \"addresses\": \"0x69\",\n \"_notes\": \"default settings on power on: normal (not sleep) mode, 10 frames per second, no interrupt, moving average off\",\n \"detectionValues\": \"\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x80=r128\",\n \"i\": 100,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Thermal Camera\",\n \"desc\": \"\",\n \"manu\": \"Panasonic\",\n \"type\": \"AMG8833\",\n \"clas\": [\"TCAM\"],\n \"resp\": {\n \"b\": 128,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"H\",\n \"u\": \"degrees\",\n \"r\": [0, 360],\n \"f\": \"3.1f\",\n \"o\": \"float\",\n \"d\": 11.377777777777778\n }\n ]\n }\n }\n}\n,\"AdafruitGamepad\":{\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x0001=0b10000111\",\n \"initValues\": \"0x007fff=p20&0x010300010067=p1&0x010b00010067=p1&0x010500010067=p1\",\n \"pollInfo\": {\n \"c\": \"0x0915=r2&0x0916=r2&0x0104=r4\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"readEffect\", \"basis\": \"source\", \"note\": \"ATtiny seesaw: shipped firmware has no handler for 0x99 (read NACKed); library 2.0.0 discards one-byte writes, so the read answers for the previous register and, if that was GPIO bulk, clears the interrupt flags (Adafruit_seesawPeripheral 8070dfb, f7f738d)\"}},\n \"devInfoJson\": {\n \"name\": \"Adafruit Gamepad\",\n \"desc\": \"\",\n \"manu\": \"Adafruit\",\n \"type\": \"Gamepad\",\n \"clas\": [\"GAME\"],\n \"resp\": {\n \"b\": 8,\n \"a\": [\n {\n \"n\": \"x\",\n \"t\": \">H\",\n \"r\": [0, 65535],\n \"d\": -1,\n \"a\": 1023,\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"y\",\n \"t\": \">H\",\n \"r\": [0, 65535],\n \"d\": -1,\n \"a\": 1023,\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"SELECT\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x01\",\n \"m\": \"0x01\",\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"B\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x02\",\n \"m\": \"0x02\",\n \"s\": 1,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"Y\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x04\",\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"A\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x20\",\n \"m\": \"0x20\",\n \"s\": 5,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"X\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x40\",\n \"m\": \"0x40\",\n \"s\": 6,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"START\",\n \"at\": 5,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x01\",\n \"m\": \"0x01\",\n \"f\": \"b\",\n \"o\": \"bool\"\n }\n ]\n }\n }\n}\n,\"AdafruitSoilSensor\":{\n \"addresses\": \"0x36\",\n \"_notes\": \"sensor must be wrapped in heatshrink to produce sensible values. very sensitive to human proximity\",\n \"detectionValues\": \"0x0001=p25&=0x55\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x0F10=p250&=r2&0x0004=p250&=r4\",\n \"i\": 1000,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"SAMD09 seesaw: 0x99 isn't a module base, so the command is ignored and the read returns 0xFF or stale reply bytes (adafruit/seesaw 20e2892)\"}},\n \"devInfoJson\": {\n \"name\": \"Soil Moisture Sensor\",\n \"desc\": \"\",\n \"manu\": \"Adafruit\",\n \"type\": \"AdafruitSoilSensor\",\n \"clas\": [\"SOIL\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"moisture\",\n \"t\": \">H\",\n \"u\": \"\",\n \"r\": [0, 65535],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"temperature\",\n \"t\": \">i\",\n \"u\": \"°C\",\n \"r\": [-45, 175],\n \"d\": 65536,\n \"f\": \".2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"BLEBTHome\":{\n \"detectionValues\": \"\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"=r17\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"BLE Sensor (BTHome)\",\n \"desc\": \"\",\n \"manu\": \"Allterco\",\n \"type\": \"BLEBTHome\",\n \"clas\": [\"BTHM\"],\n \"resp\": {\n \"b\": 17,\n \"a\": [\n {\n \"n\": \"ID\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0,255],\n \"f\": \"d\",\n \"o\": \"uint8\"\n },\n {\n \"n\": \"MAC\",\n \"t\": \">Q\",\n \"o\": \"uint64\"\n },\n {\n \"n\": \"motion\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0,1],\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"battery\",\n \"t\": \"B\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"f\": \"d\",\n \"o\": \"uint8\"\n },\n {\n \"n\": \"temp\",\n \"t\": \">h\",\n \"u\": \"°C\",\n \"r\": [-100,100],\n \"d\": 100,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"light\",\n \"t\": \">I\",\n \"u\": \"Lux\",\n \"r\": [0,100000],\n \"d\": 100,\n \"f\": \".1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"BME280\":{\n \"_notes\": \"Bosch BME280 (temperature, pressure, humidity) at 0x76 (SDO low) or 0x77 (SDO high, Adafruit default). Detection: chip ID (0xD0) 0x60 - a BMP280 (0x56-0x58) or BME680 (0x61) doesn't match. Init: soft reset (0xE0B6), 5 ms; config (0xF5) standby 500 ms, filter off; ctrl_hum (0xF2) humidity x1 - written before ctrl_meas, which applies it; ctrl_meas (0xF4) temperature x1, pressure x1, normal mode. Poll: the calibration (0x88, 26 bytes; 0xE1, 7 bytes) and the data (0xF7, 8 bytes) - the decode code applies Bosch's floating-point compensation (datasheet 8.1). 0x76 is also a common multiplexer address (Axiom018's), where the BME280 can't be used.\",\n \"addresses\": \"0x76-0x77\",\n \"detectionValues\": \"0xd0=0x60\",\n \"initValues\": \"0xe0b6=p5&0xf580&0xf201&0xf427\",\n \"pollInfo\": {\n \"c\": \"0x88=r26&0xe1=r7&0xf7=r8\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature, Pressure & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Bosch\",\n \"type\": \"BME280\",\n \"clas\": [\"TEMP\",\"PRES\",\"RH\"],\n \"resp\": {\n \"b\": 41,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"32767){T2=T2-65536;}int T3=buf[4]|(buf[5]<<8);if(T3>32767){T3=T3-65536;}int P1=buf[6]|(buf[7]<<8);int P2=buf[8]|(buf[9]<<8);if(P2>32767){P2=P2-65536;}int P3=buf[10]|(buf[11]<<8);if(P3>32767){P3=P3-65536;}int P4=buf[12]|(buf[13]<<8);if(P4>32767){P4=P4-65536;}int P5=buf[14]|(buf[15]<<8);if(P5>32767){P5=P5-65536;}int P6=buf[16]|(buf[17]<<8);if(P6>32767){P6=P6-65536;}int P7=buf[18]|(buf[19]<<8);if(P7>32767){P7=P7-65536;}int P8=buf[20]|(buf[21]<<8);if(P8>32767){P8=P8-65536;}int P9=buf[22]|(buf[23]<<8);if(P9>32767){P9=P9-65536;}int H1=buf[25];int H2=buf[26]|(buf[27]<<8);if(H2>32767){H2=H2-65536;}int H3=buf[28];int E4=buf[29];if(E4>127){E4=E4-256;}int H4=E4*16+(buf[30]&15);int E6=buf[31];if(E6>127){E6=E6-256;}int H5=E6*16+(buf[30]>>4);int H6=buf[32];if(H6>127){H6=H6-256;}int aP=(buf[33]<<12)|(buf[34]<<4)|(buf[35]>>4);int aT=(buf[36]<<12)|(buf[37]<<4)|(buf[38]>>4);int aH=(buf[39]<<8)|buf[40];float v1=(aT/16384.0-T1/1024.0)*T2;float d=aT/131072.0-T1/8192.0;float v2=d*d*T3;float tf=v1+v2;out.temperature=tf/5120.0;v1=tf/2.0-64000.0;v2=v1*v1*P6/32768.0+v1*P5*2.0;v2=v2/4.0+P4*65536.0;v1=(P3*v1*v1/524288.0+P2*v1)/524288.0;v1=(1.0+v1/32768.0)*P1;float p=0.0;if(v1>0.0){p=1048576.0-aP;p=(p-v2/4096.0)*6250.0/v1;float w1=P9*p*p/2147483648.0;float w2=p*P8/32768.0;p=p+(w1+w2+P7)/16.0;}out.pressure=p/100.0;float h=tf-76800.0;h=(aH-(H4*64.0+H5/16384.0*h))*(H2/65536.0*(1.0+H6/67108864.0*h*(1.0+H3/67108864.0*h)));h=h*(1.0-H1*h/524288.0);if(h>100.0){h=100.0;}if(h<0.0){h=0.0;}out.humidity=h;\"\n }\n }\n }\n}\n,\"BME680\":{\n \"_notes\": \"Bosch BME680 (temperature, pressure, humidity, gas) at 0x76 (SDO low) or 0x77 (SDO high, Adafruit default). Detection: chip ID (0xD0) 0x61 and variant (0xF0) 0x00 - a BME688 (variant 0x01) has another data layout. Forced mode only, so each poll triggers a measurement (ctrl_meas 0x74 = 0x25: temperature and pressure x1, forced), waits 200 ms (TPH plus the 152 ms heater time) and reads the calibration (0x89 25 bytes, 0xE1 16 bytes, 0x00 5 bytes) and the data (0x1D 15 bytes); the decode code applies Bosch's floating-point compensation and gas-resistance calculation (range lookup tables as if-chains); gas is 0 unless gas valid and heater stable are both set. Init: soft reset, 10 ms; read the heater calibration; ctrl_hum 0x01; config 0x00 (filter off); gas_wait_0 0x66 (152 ms); res_heat_0; ctrl_gas_1 0x10 (run gas, heater profile 0). res_heat_0 is computed for each unit by initCode (Bosch's calc_res_heat): init reads par_g2/par_g1/par_g3 (0xEB-0xEE) and res_heat_val/res_heat_range (0x00-0x02), and the value for a 320 C hot plate at an assumed 25 C ambient is written to 0x5A ({heat}; the ambient temperature changes it by about 1 count over 15-35 C). 0x76 is also a common multiplexer address (Axiom018's).\",\n \"addresses\": \"0x76-0x77\",\n \"detectionValues\": \"0xd0=0x61&0xf0=0x00\",\n \"initValues\": \"0xe0b6=p10&0xeb=r4&0x00=r3&0x7201&0x7500&0x6466&0x5a{heat}&0x7110\",\n \"initCode\": {\n \"n\": \"bme680_heater\",\n \"c\": \"int g2=buf[0]|(buf[1]<<8);if(g2>32767){g2=g2-65536;}int g1=buf[2];if(g1>127){g1=g1-256;}int g3=buf[3];if(g3>127){g3=g3-256;}int rhv=buf[4];if(rhv>127){rhv=rhv-256;}int rhr=(buf[6]&48)>>4;float v1=g1/16.0+49.0;float v2=g2/32768.0*0.0005+0.00235;float v3=g3/1024.0;float v5=v1*(1.0+v2*320.0)+v3*25.0;init.heat=3.4*(v5*(4.0/(4.0+rhr))*(1.0/(1.0+rhv*0.002))-25.0);\"\n },\n \"pollInfo\": {\n \"c\": \"0x7425=p200&0x89=r25&0xe1=r16&0x00=r5&0x1d=r15\",\n \"i\": 3000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Environmental Sensor\",\n \"desc\": \"Temperature, pressure, humidity, gas\",\n \"manu\": \"Bosch\",\n \"type\": \"BME680\",\n \"clas\": [\"TEMP\",\"PRES\",\"RH\",\"GAS\"],\n \"resp\": {\n \"b\": 61,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"32767){t2=t2-65536;}int t3=buf[3];if(t3>127){t3=t3-256;}int p1=buf[5]|(buf[6]<<8);int p2=buf[7]|(buf[8]<<8);if(p2>32767){p2=p2-65536;}int p3=buf[9];if(p3>127){p3=p3-256;}int p4=buf[11]|(buf[12]<<8);if(p4>32767){p4=p4-65536;}int p5=buf[13]|(buf[14]<<8);if(p5>32767){p5=p5-65536;}int p7=buf[15];if(p7>127){p7=p7-256;}int p6=buf[16];if(p6>127){p6=p6-256;}int p8=buf[19]|(buf[20]<<8);if(p8>32767){p8=p8-65536;}int p9=buf[21]|(buf[22]<<8);if(p9>32767){p9=p9-65536;}int p10=buf[23];int h1=(buf[27]<<4)|(buf[26]&15);int h2=(buf[25]<<4)|(buf[26]>>4);int h3=buf[28];if(h3>127){h3=h3-256;}int h4=buf[29];if(h4>127){h4=h4-256;}int h5=buf[30];if(h5>127){h5=h5-256;}int h6=buf[31];int h7=buf[32];if(h7>127){h7=h7-256;}int rse=buf[45]&240;if(rse>127){rse=rse-256;}rse=rse/16;int aP=(buf[48]<<12)|(buf[49]<<4)|(buf[50]>>4);int aT=(buf[51]<<12)|(buf[52]<<4)|(buf[53]>>4);int aH=(buf[54]<<8)|buf[55];int aG=(buf[59]<<2)|(buf[60]>>6);int gr=buf[60]&15;float v1=(aT/16384.0-t1/1024.0)*t2;float d=aT/131072.0-t1/8192.0;float v2=d*d*t3*16.0;float tf=v1+v2;float tc=tf/5120.0;out.temperature=tc;v1=tf/2.0-64000.0;v2=v1*v1*p6/131072.0;v2=v2+v1*p5*2.0;v2=v2/4.0+p4*65536.0;v1=(p3*v1*v1/16384.0+p2*v1)/524288.0;v1=(1.0+v1/32768.0)*p1;float p=0.0;if(v1>0.0){p=1048576.0-aP;p=(p-v2/4096.0)*6250.0/v1;float w1=p9*p*p/2147483648.0;float w2=p*p8/32768.0;float q=p/256.0;float w3=q*q*q*p10/131072.0;p=p+(w1+w2+w3+p7*128.0)/16.0;}out.pressure=p/100.0;float e1=aH-(h1*16.0+h3/2.0*tc);float e2=e1*(h2/262144.0*(1.0+h4/16384.0*tc+h5/1048576.0*tc*tc));float hum=e2+(h6/16384.0+h7/2097152.0*tc)*e2*e2;if(hum>100.0){hum=100.0;}if(hum<0.0){hum=0.0;}out.humidity=hum;float k1=0.0;if(gr==5){k1=-1.0;}if(gr==7){k1=-0.8;}if(gr==10){k1=-0.2;}if(gr==11){k1=-0.5;}if(gr==13){k1=-1.0;}float k2=0.0;if(gr==4){k2=0.1;}if(gr==5){k2=0.7;}if(gr==7){k2=-0.8;}if(gr==8){k2=-0.1;}float g1=1340.0+5.0*rse;float g2=g1*(1.0+k1/100.0);float g3=1.0+k2/100.0;float gas=0.0;if((buf[60]&48)==48){gas=1.0/(g3*0.000000125*(1<32767){g2=g2-65536;}int g1=buf[2];if(g1>127){g1=g1-256;}int g3=buf[3];if(g3>127){g3=g3-256;}int rhv=buf[4];if(rhv>127){rhv=rhv-256;}int rhr=(buf[6]&48)>>4;float v1=g1/16.0+49.0;float v2=g2/32768.0*0.0005+0.00235;float v3=g3/1024.0;float v5=v1*(1.0+v2*320.0)+v3*25.0;init.heat=3.4*(v5*(4.0/(4.0+rhr))*(1.0/(1.0+rhv*0.002))-25.0);\"\n },\n \"pollInfo\": {\n \"c\": \"0x7425=p200&0x8a=r23&0xe1=r14&0x00=r5&0x1d=r17\",\n \"i\": 3000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Environmental Sensor\",\n \"desc\": \"Temperature, pressure, humidity, gas\",\n \"manu\": \"Bosch\",\n \"type\": \"BME690\",\n \"clas\": [\"TEMP\",\"PRES\",\"RH\",\"GAS\"],\n \"resp\": {\n \"b\": 59,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"127){t3=t3-256;}int p1=buf[10]|(buf[11]<<8);if(p1>32767){p1=p1-65536;}int p2=buf[12]|(buf[13]<<8);int p3=buf[14];if(p3>127){p3=p3-256;}int p4=buf[15];if(p4>127){p4=p4-256;}int p5=buf[4]|(buf[5]<<8);if(p5>32767){p5=p5-65536;}int p6=buf[6]|(buf[7]<<8);if(p6>32767){p6=p6-65536;}int p7=buf[8];if(p7>127){p7=p7-256;}int p8=buf[9];if(p8>127){p8=p8-256;}int p9=buf[18]|(buf[19]<<8);if(p9>32767){p9=p9-65536;}int p10=buf[20];if(p10>127){p10=p10-256;}int p11=buf[21];if(p11>127){p11=p11-256;}int h5=(buf[23]<<4)|(buf[24]>>4);if(h5>2047){h5=h5-4096;}int h1=(buf[25]<<4)|(buf[24]&15);if(h1>2047){h1=h1-4096;}int h2=buf[26];if(h2>127){h2=h2-256;}int h4=buf[27];if(h4>127){h4=h4-256;}int h3=buf[28];int h6=buf[29];int aP=(buf[44]<<16)|(buf[45]<<8)|buf[46];int aT=(buf[47]<<16)|(buf[48]<<8)|buf[49];int aH=(buf[50]<<8)|buf[51];int aG=(buf[57]<<2)|(buf[58]>>6);int gr=buf[58]&15;float cf=aT-t1*256.0;float T=cf*(t2/1073741824.0)+cf*cf*(t3/281474976710656.0);out.temperature=T;float o=p1*8.0;float tk10=p2/64.0;float tk20=p3/256.0;float tk30=p4/32768.0;float s=(p5-16384.0)/1048576.0;float tk1s=(p6-16384.0)/536870912.0;float tk2s=p7/4294967296.0;float tk3s=p8/137438953472.0;float nls=p9/281474976710656.0;float tknls=p10/281474976710656.0;float nls3=p11/36893488147419103232.0;float pf=aP*1.0;float P=o+tk10*T+tk20*T*T+tk30*T*T*T+pf*(s+tk1s*T+tk2s*T*T+tk3s*T*T*T)+pf*pf*(nls+tknls*T)+pf*pf*pf*nls3;out.pressure=P/100.0;float tc=T*5120.0-76800.0;float hoff=aH-(h1*64.0+h2/16384.0*tc);float k4=h4/67108864.0;float hs=hoff*(h5/65536.0)*(1.0+k4*tc+k4*(h3/67108864.0)*tc*tc);float H=hs*(1.0-h6/524288.0*hs);if(H<0.0){H=0.0;}if(H>100.0){H=100.0;}out.humidity=H;float gas=0.0;if((buf[58]&48)==48){gas=1000000.0*(262144>>gr)/(4096.0+3.0*(aG-512.0));}out.gas=gas;\"\n }\n }\n }\n}\n,\"CAP1188\":{\n \"_notes\": \"Microchip CAP1188 8-channel capacitive touch sensor with 8 LED drivers. Product ID 0x50, Manufacturer ID 0x5D. ADDR_COMM strap selects address in range 0x28-0x2D.\",\n \"addresses\": \"0x28-0x2d\",\n \"detectionValues\": \"0xfd=0x50&0xfe=0x5d\",\n \"initValues\": \"0x0000&0x1f36&0x2a00\",\n \"pollInfo\": {\n \"c\": \"0x02=r2&0x10=r8&0x0000\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Capacitive Touch\",\n \"desc\": \"8 channel\",\n \"manu\": \"Microchip\",\n \"type\": \"CAP1188\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 10,\n \"a\": [\n {\n \"n\": \"C1\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x01\",\n \"s\": 0,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C2\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x02\",\n \"s\": 1,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C3\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C4\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x08\",\n \"s\": 3,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C5\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x10\",\n \"s\": 4,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C6\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x20\",\n \"s\": 5,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C7\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x40\",\n \"s\": 6,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C8\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x80\",\n \"s\": 7,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"d1\",\n \"at\": 2,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d2\",\n \"at\": 3,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d3\",\n \"at\": 4,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d4\",\n \"at\": 5,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d5\",\n \"at\": 6,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d6\",\n \"at\": 7,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d7\",\n \"at\": 8,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d8\",\n \"at\": 9,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n }\n ]\n }\n }\n}\n,\"CAP1203\":{\n \"addresses\": \"0x28\",\n \"detectionValues\": \"0xfd=0b01101101\",\n \"initValues\": \"0x0000&0x1f06\",\n \"pollInfo\": {\n \"c\": \"0x02=r2&0x0000\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Capacitive Touch\",\n \"desc\": \"\",\n \"manu\": \"Sparkfun\",\n \"type\": \"CAP1203\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"A\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x01\",\n \"s\": 0,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"B\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x02\",\n \"s\": 1,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"status\",\n \"t\": \">H\",\n \"vs\": false,\n \"f\": \"04x\",\n \"o\": \"uint16\",\n \"bl\": {\"0x0001\": \"CS1\", \"0x0002\": \"CS2\", \"0x0004\": \"CS3\", \"0x0100\": \"TOUCH\", \"0x0200\": \"MTP\", \"0x0400\": \"MULT\", \"0x1000\": \"PWR\", \"0x2000\": \"ACAL_FAIL\", \"0x4000\": \"BC_OUT\"}\n }\n ]\n }\n }\n}\n,\"DPS310\":{\n \"_notes\": \"Infineon DPS310 barometric pressure and temperature sensor (Adafruit board) at 0x77 (default, SDO high) or 0x76. Also matches the Goertek SPL06-001, which has the same ID, registers and compensation. Detection: PRODUCT_ID (0x0D) = 0x10 (REV_ID 1, PROD_ID 0); a BME280/BME680/BME690 reads otherwise there (a BME690 0x04 on the bench). Init: soft reset (RESET 0x0C = 0x89), 50 ms for the coefficients to be ready; Infineon's temperature correction sequence (0x0E = 0xA5, 0x0F = 0x96, 0x62 = 0x02, 0x0E = 0, 0x0F = 0), without which some chips read temperatures about twice too high; PRS_CFG (0x06) 8 measurements/s, 16x oversampling (27.6 ms each); TMP_CFG (0x07) the external (MEMS) temperature sensor, 8/s, 1x; CFG_REG (0x09) P_SHIFT, needed above 8x pressure oversampling; MEAS_CFG (0x08) continuous pressure and temperature. TMP_CFG's sensor must match the one the coefficients were calibrated with (COEF_SRCE, 0x28 bit 7): the external one on every chip seen (0xA9 on the bench), which the init assumes. Poll: PSR_B2-B0 and TMP_B2-B0 (0x00, 6 bytes, 24-bit two's complement, big-endian) and the coefficients (0x10, 18 bytes); the decode code applies Infineon's compensation (datasheet 4.9) with the scale factors for 16x (pressure, 253952) and 1x (temperature, 524288) oversampling, so a change to PRS_CFG or TMP_CFG oversampling needs them changed too.\",\n \"addresses\": \"0x76-0x77\",\n \"detectionValues\": \"0x0d=0x10\",\n \"initValues\": \"0x0c89=p50&0x0ea5&0x0f96&0x6202&0x0e00&0x0f00&0x0634&0x07b0&0x0904&0x0807\",\n \"pollInfo\": {\n \"c\": \"0x00=r6&0x10=r18\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the lone 0x99 + 9-byte read (registers 0x99-0xA1, not in the map) read zeros; configuration, coefficients and COEF_SRCE unchanged and measuring carried on. Its addresses are in Axiom's excluded RSAO range anyway\"}},\n \"devInfoJson\": {\n \"name\": \"Pressure & Temperature Sensor\",\n \"desc\": \"\",\n \"manu\": \"Infineon\",\n \"type\": \"DPS310\",\n \"clas\": [\"PRES\",\"TEMP\"],\n \"resp\": {\n \"b\": 24,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"8388607){pr=pr-16777216;}int tr=(buf[3]<<16)|(buf[4]<<8)|buf[5];if(tr>8388607){tr=tr-16777216;}int c0=(buf[6]<<4)|(buf[7]>>4);if(c0>2047){c0=c0-4096;}int c1=((buf[7]&15)<<8)|buf[8];if(c1>2047){c1=c1-4096;}int c00=(buf[9]<<12)|(buf[10]<<4)|(buf[11]>>4);if(c00>524287){c00=c00-1048576;}int c10=((buf[11]&15)<<16)|(buf[12]<<8)|buf[13];if(c10>524287){c10=c10-1048576;}int c01=(buf[14]<<8)|buf[15];if(c01>32767){c01=c01-65536;}int c11=(buf[16]<<8)|buf[17];if(c11>32767){c11=c11-65536;}int c20=(buf[18]<<8)|buf[19];if(c20>32767){c20=c20-65536;}int c21=(buf[20]<<8)|buf[21];if(c21>32767){c21=c21-65536;}int c30=(buf[22]<<8)|buf[23];if(c30>32767){c30=c30-65536;}float ts=tr/524288.0;float ps=pr/253952.0;out.temperature=c0*0.5+c1*ts;float p=c00+ps*(c10+ps*(c20+ps*c30))+ts*c01+ts*ps*(c11+ps*c21);out.pressure=p/100.0;\"\n }\n }\n }\n}\n,\"ENS160\":{\n \"_notes\": \"ScioSense ENS160 digital metal-oxide multi-gas sensor (Adafruit and SparkFun boards) at 0x53 (default, ADDR high) or 0x52. Detection: PART_ID (0x00, 2 bytes little-endian) = 0x0160. Init: OPMODE (0x10) idle (0x01), then compensation TEMP_IN (0x13, kelvin x 64) = 25 degC and RH_IN (0x15, %RH x 512) = 50 %RH in one write, then OPMODE standard (0x02), which measures once a second. Poll (1 s): 6 bytes from DEVICE_STATUS (0x20): status, DATA_AQI (UBA air quality index 1-5, bits 2:0), DATA_TVOC (ppb) and DATA_ECO2 (ppm), both little-endian. Status: STATAS (bit 7) = an operating mode is running, STATER (bit 6) = error, VALIDITY (bits 3:2) 0 = normal, 1 = warm-up (the first 3 min after each power-on), 2 = initial start-up (the first hour of operation in the sensor's life), 3 = invalid output; NEWDAT (bit 1) = new data, NEWGPR (bit 0) = new general-purpose register data; the status labels are S (STATAS), E (STATER), D (NEWDAT) and G (NEWGPR), with the validity bits left to the validity attribute. Readings during warm-up or initial start-up are not yet reliable. The compensation actions take degC and %RH (e.g. from another sensor); the mode action switches between deep sleep, idle and standard.\",\n \"addresses\": \"0x52-0x53\",\n \"detectionValues\": \"0x00=0x6001\",\n \"initValues\": \"0x1001=p10&0x138a4a0064=&0x1002=p10\",\n \"pollInfo\": {\n \"c\": \"0x20=r6\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"readEffect\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the chip ignores bit 7 of the register address, so the lone 0x99 points at 0x19 and the 9-byte read covers 0x19-0x21, ending with DEVICE_STATUS and DATA_AQI. No configuration register changed (OPMODE, CONFIG, TEMP_IN, RH_IN) and measuring carried on; reading DATA_AQI may clear the NEWDAT flag (transient)\"}},\n \"devInfoJson\": {\n \"name\": \"Gas Sensor (VOC, eCO2)\",\n \"desc\": \"\",\n \"manu\": \"ScioSense\",\n \"type\": \"ENS160\",\n \"clas\": [\"VOC\",\"CO2\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"r\": [0,255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0x80\": \"S\", \"0x40\": \"E\", \"0x08\": \"\", \"0x04\": \"\", \"0x02\": \"D\", \"0x01\": \"G\"}\n },\n {\n \"n\": \"validity\",\n \"at\": 0,\n \"t\": \"B\",\n \"r\": [0,3],\n \"m\": \"0x0c\",\n \"s\": 2,\n \"f\": \"d\",\n \"o\": \"uint8\",\n \"vl\": {\"0\": \"normal\", \"1\": \"warm-up\", \"2\": \"start-up\", \"3\": \"invalid\"}\n },\n {\n \"n\": \"AQI\",\n \"at\": 1,\n \"t\": \"B\",\n \"u\": \"UBA\",\n \"r\": [1,5],\n \"m\": \"0x07\",\n \"f\": \"d\",\n \"o\": \"uint8\"\n },\n {\n \"n\": \"TVOC\",\n \"at\": 2,\n \"t\": \"I\",\n \"u\": \"N\",\n \"r\": [-10000,10000],\n \"m\": \"0x00ffffff\",\n \"d\": 44000,\n \"sb\": 23,\n \"f\": \".3f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0,1],\n \"vl\": {\"0\": \"no\", \"1\": \"yes\"}\n }\n ]\n }\n }\n}\n,\"LPS25\":{\n \"addresses\": \"0x5C-0x5D\",\n \"detectionValues\": \"0x0f=0b10111101\",\n \"initValues\": \"0x2104=p5&0x20C4=\",\n \"pollInfo\": {\n \"c\": \"0xA7=r6\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"lone 0x99 + 9-byte read (2026-10-23): pointer to 0x19 (reserved), no configuration register changed, pressure unchanged\"}},\n \"devInfoJson\": {\n \"name\": \"Pressure Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"LPS25\",\n \"clas\": [\"PRES\",\"TEMP\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"r\": [0,255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0x01\": \"T_DA\", \"0x02\": \"P_DA\", \"0x10\": \"T_OR\", \"0x20\": \"P_OR\"}\n },\n {\n \"n\": \"pressure\",\n \"at\": 0,\n \"t\": \"6.6 s filled the 4096-word FIFO, the count read 0 and the {$0.w12} poll read nothing forever - devdocs/lsm6ds-fifo-full-deadlock.md). The _conf.rate entries keep the watermark. Gyro 70 mdps/LSB at 2000 dps.\",\n \"addresses\": \"0x6a-0x6b\",\n \"detectionValues\": \"0x0f=0x69\",\n \"initValues\": \"0x0a00&0x1244&0x1048&0x114c&0x1640&0x0809&0x0900&0x1301&0x06fc&0x0703&0x0a26\",\n \"pollInfo\": {\n \"c\": \"0x3a=r4&0x3e=r{$0.w12:mask0FFF*2:max240}\",\n \"i\": 50,\n \"s\": 20\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer & Gyroscope\",\n \"desc\": \"6-Axis IMU\",\n \"manu\": \"ST\",\n \"type\": \"LSM6DS3\",\n \"clas\": [\"ACC\", \"GYRO\"],\n \"resp\": {\n \"b\": 244,\n \"a\": [\n {\n \"n\": \"gx\",\n \"t\": \"maxN){N=maxN;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(i6.6 s filled the 4096-word FIFO, the count read 0 and the {$0.w12} poll read nothing forever - devdocs/lsm6ds-fifo-full-deadlock.md). The _conf.rate entries keep the watermark. Gyro 70 mdps/LSB at 2000 dps.\",\n \"addresses\": \"0x6a-0x6b\",\n \"detectionValues\": \"0x0f=0x6a\",\n \"initValues\": \"0x0a00&0x1244&0x1048&0x114c&0x1640&0x0809&0x0980&0x06fc&0x0703&0x0a26\",\n \"pollInfo\": {\n \"c\": \"0x3a=r4&0x3e=r{$0.w12:mask0FFF*2:max240}\",\n \"i\": 50,\n \"s\": 20\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer & Gyroscope\",\n \"desc\": \"6-Axis IMU\",\n \"manu\": \"ST\",\n \"type\": \"LSM6DS3TRC\",\n \"clas\": [\"ACC\", \"GYRO\"],\n \"resp\": {\n \"b\": 244,\n \"a\": [\n {\n \"n\": \"gx\",\n \"t\": \"maxN){N=maxN;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(iH\",\n \"u\": \"V\",\n \"r\": [0, 5.12],\n \"d\": 12800,\n \"f\": \".3f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"charge\",\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"d\": 256,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"chargeRate\",\n \"t\": \">h\",\n \"u\": \"%/hr\",\n \"r\": [-300, 300],\n \"d\": 4.808,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"at\": 6,\n \"r\": [0, 255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0\": \"OK\", \"0x01\": \"RI\", \"0x02\": \"VH\", \"0x04\": \"VL\", \"0x08\": \"VR\", \"0x10\": \"HD\", \"0x20\": \"SC\", \"0x40\": \"\"}\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"config\",\n \"t\": \">H\",\n \"w\": \"0c\",\n \"f\": \"B\",\n \"r\": [0, 65535],\n \"d\": 38678,\n \"desc\": \"CONFIG register\"\n },\n {\n \"n\": \"reset config\",\n \"w\": \"0c9716\",\n \"desc\": \"Reset CONFIG\"\n }\n ]\n }\n}\n,\"MAX30101\":{\n \"_notes\": \"Maxim MAX30101 / MAX30105 pulse oximetry, heart-rate and particle sensor at 0x57 (fixed). The MAX30102 has the same part ID but no green LED (its green values are meaningless). Detection: PART_ID (0xFF) = 0x15 (all three). Init: reset (0x09 bit 6), 10 ms; MODE_CONFIG (0x09) multi-LED mode (7); FIFO_CONFIG (0x08) 4-sample averaging, rollover on, almost-full 15; INT status 1 read (clears it); SPO2_CONFIG (0x0A) ADC range 4096 nA, 100 samples/s, 411 us pulses (18 bits); LED1-LED3 pulse amplitudes (0x0C-0x0E, red, IR, green) 0x40 = 12.8 mA (0.2 mA per step); slots (0x11-0x12) red, IR, green, none; FIFO pointers cleared (0x04-0x06). Poll: FIFO_WR_PTR, OVF_COUNTER, FIFO_RD_PTR, then 8 samples of 9 bytes from FIFO_DATA (the register pointer stays at 0x07, so one read covers both), 18 bits per LED; then FIFO_RD_PTR again. The decode keeps the samples the read popped (RD_PTR after minus before): a read of an empty FIFO returns the last sample again without moving RD_PTR, and a sample that arrives during the read (about 8 ms) is popped too - counting from the pointers before the read dropped it, about one sample in five at 100/s (bench, 2026-10-24). The rate is the 100 samples/s divided by the on-chip averaging (1-16), each rate polled often enough to keep under 8 samples a poll. In multi-LED mode an LED is turned off by setting its current to 0, so the LED actions also choose what is measured (red only for heart rate, red and IR for SpO2, all three). Bench (Axiom018, 2026-10-24, MAX30105): with nothing in front about 1500 (red) and 2400 (IR), a fingertip about 196000 with a 68 bpm pulse of about 1700 p-p in IR.\",\n \"addresses\": \"0x57\",\n \"detectionValues\": \"0xff=0x15\",\n \"initValues\": \"0x0940=p10&0x0907&0x085f&0x00=r1&0x0a27&0x0c404040&0x112103&0x04000000\",\n \"pollInfo\": {\n \"c\": \"0x04=r75&0x06=r1\",\n \"i\": 200,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\n \"rsao\": {\n \"effect\": \"none\",\n \"basis\": \"bench\",\n \"note\": \"bench (Axiom018, 2026-10-24, MAX30105): the lone 0x99 + 9-byte read (0x99-0xA1, not in the register map) read 99 af ff ff ff ff ff 00 00; FIFO, mode, SpO2, LED, slot and interrupt-enable registers unchanged and sampling carried on\"\n }\n },\n \"devInfoJson\": {\n \"name\": \"Heart & Optical Sensor\",\n \"desc\": \"Red, IR and green reflectance: heart rate, SpO2, proximity and (MAX30105) particles\",\n \"manu\": \"Maxim\",\n \"type\": \"MAX30101\",\n \"clas\": [\n \"HRM\"\n ],\n \"resp\": {\n \"b\": 76,\n \"a\": [\n {\n \"n\": \"Red\",\n \"t\": \">I\",\n \"u\": \"Red\",\n \"r\": [\n 0,\n 262143\n ],\n \"f\": \"6d\",\n \"o\": \"uint32\"\n },\n {\n \"n\": \"IR\",\n \"t\": \">I\",\n \"u\": \"IR\",\n \"r\": [\n 0,\n 262143\n ],\n \"f\": \"6d\",\n \"o\": \"uint32\"\n },\n {\n \"n\": \"Green\",\n \"t\": \">I\",\n \"u\": \"Green\",\n \"r\": [\n 0,\n 262143\n ],\n \"f\": \"6d\",\n \"o\": \"uint32\"\n }\n ],\n \"c\": {\n \"n\": \"max30101_fifo\",\n \"c\": \"int N=(buf[75]+32-buf[2])%32;if(N>8){N=8;}int k=3;int i=0;while(iH\",\n \"u\": \"°C\",\n \"r\": [-40,125],\n \"m\": \"0x1fff\",\n \"d\": 16,\n \"sb\": 12,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MICS6814Pimoroni\":{\n \"_notes\": \"SGX MiCS-6814 gas sensor (reducing gases e.g. CO, NH3, oxidising gases e.g. NO2) on Pimoroni's breakout at 0x19: a Nuvoton MS51 running Pimoroni's IO Expander firmware reads the three sensing elements with its ADC and switches the heater (as Pimoroni's mics6814 and ioexpander Python libraries do). Detection: the IO Expander chip ID (0xFA-0xFB) 0xE26A - shared by Pimoroni's IO Expander boards; 0x19 is the MICS6814's address. Init: read the pin mode registers P0M1, P0M2, P1M1, P1M2 (0x71-0x74) and ADCCON1 (0xA1); initCode computes read-modify-write values (the same registers set the modes of the pins the firmware itself uses): P0.5-P0.7 input-only for the ADC (AIN4, AIN3, AIN2), P1.5 push-pull for the heater enable; then P1.5 low (bit-addressed P1 write 0x05: heater on) and the ADC enabled. Poll: for each of reducing (AIN2), NH3 (AIN3), oxidising (AIN4): select the channel (AINDIDS0 0xB6), start a conversion (ADCCON0 0xA8 = 0x40 | channel), 2 ms, read ADCRL/ADCRH (12 bits: low nibble of 0x82, high 8 bits in 0x83). Decode: each element's resistance R = raw * 56000 / (4095 - raw) ohms (the board's 56k divider, as the library). The heater needs some minutes to warm up before the resistances settle.\",\n \"addresses\": \"0x19\",\n \"detectionValues\": \"0xfa=0x6ae2\",\n \"initValues\": \"0x71=r1&0x72=r1&0x73=r1&0x74=r1&0xa1=r1&0x71{p0m1}&0x72{p0m2}&0x73{p1m1}&0x74{p1m2}&0x5005&0xa1{adccon1}\",\n \"initCode\": {\n \"n\": \"mics6814_pins\",\n \"c\": \"init.p0m1=buf[0]|224;init.p0m2=buf[1]&31;init.p1m1=buf[2]&223;init.p1m2=buf[3]|32;init.adccon1=buf[4]|1;\"\n },\n \"pollInfo\": {\n \"c\": \"0xb604&0xa842=p2&0x82=r2&0xb608&0xa843=p2&0x82=r2&0xb610&0xa844=p2&0x82=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"inferred\", \"note\": \"Pimoroni IO Expander firmware (not public): a one-byte write only sets the pointer; 0x99-0xA1 are PWM and ADC configuration registers with no read side effect (ioe-python register map)\"}},\n \"devInfoJson\": {\n \"name\": \"Gas Sensor\",\n \"desc\": \"Reducing, NH3 and oxidising gases\",\n \"manu\": \"Pimoroni\",\n \"type\": \"MICS6814Pimoroni\",\n \"clas\": [\"GAS\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"reducing\",\n \"t\": \"H\",\n \"u\": \"µT\",\n \"r\": [\n -3000,\n 3000\n ],\n \"d\": 10,\n \"a\": -3276.8,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"y\",\n \"t\": \">H\",\n \"u\": \"µT\",\n \"r\": [\n -3000,\n 3000\n ],\n \"d\": 10,\n \"a\": -3276.8,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"z\",\n \"t\": \">H\",\n \"u\": \"µT\",\n \"r\": [\n -3000,\n 3000\n ],\n \"d\": 10,\n \"a\": -3276.8,\n \"f\": \".1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MPRLS\":{\n \"_notes\": \"Honeywell MPRLS 0025PA (0-25 psi, transfer function A: 10-90% of 2^24 counts). No detection values, so the record claims any device at 0x18; the MCP9808 and Robotical servo/pump records, which share the address and have detection values, are tried first (detection strength order).\",\n \"addresses\": \"0x18\",\n \"detectionValues\": \"\",\n \"initValues\": \"0xAA0000\",\n \"pollInfo\": {\n \"c\": \"=r4&0xAA0000\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Pressure Sensor\",\n \"desc\": \"\",\n \"manu\": \"Honeywell\",\n \"type\": \"MPRLS\",\n \"clas\": [\"PRES\"],\n \"resp\": {\n \"b\": 4,\n \"a\": [\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"r\": [0, 255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0x01\": \"saturated\", \"0x04\": \"mem error\", \"0x20\": \"busy\", \"0x40\": \"powered\"}\n },\n {\n \"n\": \"pressure\",\n \"at\": 0,\n \"m\": \"0x00ffffff\",\n \"t\": \">I\",\n \"u\": \"hPa\",\n \"r\": [0, 1724],\n \"a\": -215.461,\n \"d\": 7786.654,\n \"f\": \"4.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MS8607PT\":{\n \"_notes\": \"TE MS8607 pressure and temperature part at 0x76 (fixed; the humidity part is MS8607RH at 0x40). Commands, not registers: reset 0x1E, convert pressure 0x40-0x4A / temperature 0x50-0x5A (by oversampling), ADC read 0x00 (3 bytes), PROM read 0xA0-0xAC (7 words: word 0 holds a CRC-4, words 1-6 the coefficients C1-C6). Detection: an ADC read, then a second one, which returns 0 whatever went before (datasheet: a repeated ADC read gives 0) - a pattern rather than an ID (a BME280's register 0x00 also reads 0), so it is tried after the records with an ID (identPriority -1). Init: reset, 5 ms. Poll: pressure conversion at OSR 8192 (0x4A, max 16.4 ms), 20 ms (the address is barred), read; temperature conversion (0x5A), 20 ms, read; C1-C6. Decode: TE's first- and second-order compensation (also right for the MS5637 and MS5837-02BA, which share the maths; not the MS5611 or MS5837-30BA). The PROM CRC-4 isn't checked (the grammar can't). 0x76 is also a common multiplexer address (Axiom018 reserved it until 2026-10-18).\",\n \"addresses\": \"0x76\",\n \"identPriority\": -1,\n \"detectionValues\": \"0x00=r3&0x00=0x000000\",\n \"initValues\": \"0x1e=p5\",\n \"pollInfo\": {\n \"c\": \"0x4a=p20&0x00=r3&0x5a=p20&0x00=r3&0xa2=r2&0xa4=r2&0xa6=r2&0xa8=r2&0xaa=r2&0xac=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Pressure & Temperature Sensor\",\n \"desc\": \"MS8607 pressure part\",\n \"manu\": \"TE\",\n \"type\": \"MS8607PT\",\n \"clas\": [\"PRES\",\"TEMP\"],\n \"resp\": {\n \"b\": 18,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"=2000.0){t2=5.0*dT*dT/274877906944.0;}t=t-t2;off=off-off2;sens=sens-sens2;float p=(D1*sens/2097152.0-off)/32768.0;out.temperature=t/100.0;out.pressure=p/100.0;\"\n }\n }\n }\n}\n,\"MS8607RH\":{\n \"_notes\": \"TE MS8607 humidity part at 0x40 (fixed; the pressure and temperature part is MS8607PT at 0x76). HTU21-style commands: reset 0xFE, measure RH without clock stretching 0xF5 (with stretching, 0xE5, holds the clock longer than the bus timeout), user register read 0xE7. Detection: the user register's fixed bits (reserved bits 5:3 = 0, bit 1 = 1 by default) - weak, so it is tried after the records with an ID (identPriority -1); 0x40 is also used by parts with no record (INA219, PCA9685). Init: reset, 15 ms. Poll: measure (0xF5), 20 ms (max 16 ms at 12 bits; the pause bars the address, as an address probe between trigger and read loses the result), read 3 bytes (RH, CRC with init 0x00 - not checked). RH = -6 + 125*raw/2^16 (status bits 1:0 masked), not temperature-compensated (the MS8607 compensation uses the temperature from the other part: RH + (20 - T) * -0.18).\",\n \"addresses\": \"0x40\",\n \"identPriority\": -1,\n \"detectionValues\": \"0xe7=0bX000XX1X\",\n \"initValues\": \"0xfe=p15\",\n \"pollInfo\": {\n \"c\": \"0xf5=p20&=r3\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Humidity Sensor\",\n \"desc\": \"MS8607 humidity part\",\n \"manu\": \"TE\",\n \"type\": \"MS8607RH\",\n \"clas\": [\"RH\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"humidity\",\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"m\": \"0xfffc\",\n \"d\": 524.288,\n \"a\": -6,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MT6701\":{\n \"addresses\": \"0x06\",\n \"detectionValues\": \"\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x03=r2\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Magnetic Encoder\",\n \"desc\": \"\",\n \"manu\": \"MagnTek\",\n \"type\": \"MT6701\",\n \"clas\": [\"ANG\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"angle\",\n \"t\": \">H\",\n \"u\": \"degrees\",\n \"r\": [0, 360],\n \"f\": \"3.1f\",\n \"o\": \"float\",\n \"d\": 182.04444444444444\n }\n ]\n }\n }\n}\n,\"MXC400xXC\":{\n \"_notes\": \"x in part code sets i2c address LSB from 0x10-0x17\",\n \"addresses\": \"0x10-0x17\",\n \"detectionValues\": \"0x0E=0x02,0x03\",\n \"initValues\": \"0x0110=p5&0x0d00=\",\n \"pollInfo\": {\n \"c\": \"0x03=r7\",\n \"i\": 100,\n \"s\": 3\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer\",\n \"desc\": \"3-Axis Accel\",\n \"manu\": \"Memsic\",\n \"type\": \"MXC400xXC\",\n \"clas\": [\"ACC\"],\n \"resp\": {\n \"b\": 7,\n \"a\": [\n {\n \"n\": \"ax\",\n \"t\": \">h\",\n \"u\": \"g\",\n \"r\": [-2.0, 2.0],\n \"d\": 16384,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"ay\",\n \"t\": \">h\",\n \"u\": \"g\",\n \"r\": [-2.0, 2.0],\n \"d\": 16384,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"az\",\n \"t\": \">h\",\n \"u\": \"g\",\n \"r\": [-2.0, 2.0],\n \"d\": 16384,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"temperature\",\n \"t\": \"b\",\n \"u\": \"°C\",\n \"r\": [-40, 85],\n \"d\": 1.7065,\n \"a\": 25,\n \"f\": \".2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"PCT2075\":{\n \"_notes\": \"NXP PCT2075, LM75-compatible temperature sensor, 0.125 C, at any of 27 addresses (A0-A2 tri-state): 0x28-0x2F, 0x35-0x37 (Adafruit default 0x37), 0x48-0x4F, 0x70-0x77 - of which 0x70-0x73 are left out, as GravityO2 (no detection) claims them first. No ID register, so detection checks the registers' fixed bits (read-only): temperature low 5 bits 0 (11-bit, left-justified), configuration bits 7-5 0, Thyst and Tos low 7 bits 0 (9-bit), Tidle bits 7-5 0, and pointer 0x07 (no register) reads 0xFFFF - a part whose low registers read 0 (e.g. a BME280, which matched all the bit patterns) or a TMP117 (offset register 0x0000) fails that. Being a pattern rather than an ID, it is tried after the records with an ID at the same address (identPriority -1) - a TMP117 or an idle CAP1188 can pass it. Init: configuration 0 (normal mode, in case it was left shut down). Poll: temperature register; signed 16-bit / 256 (the low 5 bits are 0).\",\n \"addresses\": \"0x28-0x2F,0x35-0x37,0x48-0x4F,0x74-0x77\",\n \"identPriority\": -1,\n \"detectionValues\": \"0x00=0bXXXXXXXXXXX00000&0x01=0b000XXXXX&0x02=0bXXXXXXXXX0000000&0x03=0bXXXXXXXXX0000000&0x04=0b000XXXXX&0x07=0xFFFF\",\n \"initValues\": \"0x0100=\",\n \"pollInfo\": {\n \"c\": \"0x00=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature Sensor\",\n \"desc\": \"\",\n \"manu\": \"NXP\",\n \"type\": \"PCT2075\",\n \"clas\": [\"TEMP\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">h\",\n \"u\": \"°C\",\n \"r\": [-55, 125],\n \"d\": 256,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"QwiicButton\":{\n \"_notes\": \"SparkFun Qwiic Button (ATtiny84 with SparkFun firmware) at 0x6F by default (the address can be changed in software). The LED is a single colour (red or green, depending on the board), so it has brightness and pulse settings, not a colour. Every LED and debounce setting is saved in the button's EEPROM, so it survives a power cycle (a new button: brightness 255, no pulsing, debounce 10 ms). The pulse step must not be 0: the firmware divides by it. Poll: the status register (0x03), bit 2 = pressed; its clicked and event bits stay set until written back, so they aren't decoded.\",\n \"addresses\": \"0x6f\",\n \"detectionValues\": \"0x00=0b01011101\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x03=r1\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"changes\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, button on a mux slot, 2026-10-23): the lone 0x99 + 9-byte read fails with an I2C hardware timeout and the button drops off the bus for about 1.1 s, then is found and identified again; devices on other slots and the main bus are unaffected (the mux isolates the slot, the master times out after about 1.6 ms). Source: the read handler doesn't check the register pointer, so from 0x99 the length wraps to 65415 and the handler loops inside the I2C interrupt, holding the clock (Qwiic_Button e89a82f interrupts.ino 76-91). No setting changes. Its default address 0x6F is excluded from RSAO addressing\"}},\n \"devInfoJson\": {\n \"name\": \"QWIIC Button\",\n \"desc\": \"\",\n \"manu\": \"Sparkfun\",\n \"type\": \"QwiicButton\",\n \"clas\": [\"BTN\"],\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"press\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"LED\",\n \"desc\": \"LED presets: off, on (full brightness, steady) or breathe (full brightness, 1 s pulse, 0.5 s off)\",\n \"map\": {\n \"off\": {\"w\": \"1900&1b0000\"},\n \"on\": {\"w\": \"19ff&1b0000\"},\n \"breathe\": {\"w\": \"19ff&1a01&1be803&1df401\"}\n }\n },\n {\n \"n\": \"LED brightness\",\n \"desc\": \"LED brightness, 0-255 (when pulsing, the peak of each pulse)\",\n \"t\": \"B\",\n \"w\": \"19\",\n \"r\": [0,255],\n \"d\": 255\n },\n {\n \"n\": \"LED pulse step\",\n \"desc\": \"Brightness change at each step of a pulse, 1-255 (1 is smoothest)\",\n \"t\": \"B\",\n \"w\": \"1a\",\n \"r\": [1,255],\n \"d\": 1\n },\n {\n \"n\": \"LED pulse time\",\n \"desc\": \"Length of one pulse (ramp up and down) in ms; 0 = no pulsing, the LED stays at its brightness\",\n \"t\": \">7)&255;float co2=0.0;if(co2e>0){if(co2e<255){co2=1.0+(((co2h&127)*65536)+co2l)/8388608.0;int co2k=co2e-127;while(co2k>0){co2=co2*2.0;co2k=co2k-1;}while(co2k<0){co2=co2/2.0;co2k=co2k+1;}}}if(co2h>32767){co2=0.0-co2;}out.CO2=co2;int tmph=(buf[12]<<8)|buf[13];int tmpl=(buf[15]<<8)|buf[16];int tmpe=(tmph>>7)&255;float tmp=0.0;if(tmpe>0){if(tmpe<255){tmp=1.0+(((tmph&127)*65536)+tmpl)/8388608.0;int tmpk=tmpe-127;while(tmpk>0){tmp=tmp*2.0;tmpk=tmpk-1;}while(tmpk<0){tmp=tmp/2.0;tmpk=tmpk+1;}}}if(tmph>32767){tmp=0.0-tmp;}out.temperature=tmp;int humh=(buf[18]<<8)|buf[19];int huml=(buf[21]<<8)|buf[22];int hume=(humh>>7)&255;float hum=0.0;if(hume>0){if(hume<255){hum=1.0+(((humh&127)*65536)+huml)/8388608.0;int humk=hume-127;while(humk>0){hum=hum*2.0;humk=humk-1;}while(humk<0){hum=hum/2.0;humk=humk+1;}}}if(humh>32767){hum=0.0-hum;}out.humidity=hum;next;}\"\n }\n },\n \"actions\": [\n {\n \"n\": \"_conf.rate\",\n \"r\": [0.0167, 0.333],\n \"d\": 0.333,\n \"desc\": \"Sample rate (Hz)\",\n \"map\": {\n \"0.333\": {\n \"i\": 3000000,\n \"s\": 1\n },\n \"0.2\": {\n \"i\": 5000000,\n \"s\": 1\n },\n \"0.1\": {\n \"i\": 10000000,\n \"s\": 1\n },\n \"0.0333\": {\n \"i\": 30000000,\n \"s\": 1\n },\n \"0.0167\": {\n \"i\": 60000000,\n \"s\": 1\n }\n }\n },\n {\n \"n\": \"autoCal\",\n \"t\": \">H\",\n \"w\": \"5306\",\n \"crc\": \"crc8w\",\n \"r\": [0, 1],\n \"d\": 0,\n \"desc\": \"Auto-calibration\"\n },\n {\n \"n\": \"forceCal\",\n \"t\": \">H\",\n \"w\": \"5204\",\n \"crc\": \"crc8w\",\n \"r\": [400, 2000],\n \"d\": 420,\n \"desc\": \"Force cal (ppm)\"\n },\n {\n \"n\": \"tempOffset\",\n \"t\": \">H\",\n \"w\": \"5403\",\n \"crc\": \"crc8w\",\n \"mul\": 100,\n \"r\": [0, 20],\n \"d\": 0,\n \"desc\": \"Temp offset (C)\"\n },\n {\n \"n\": \"altitude\",\n \"t\": \">H\",\n \"w\": \"5102\",\n \"crc\": \"crc8w\",\n \"r\": [0, 3000],\n \"d\": 0,\n \"desc\": \"Altitude (m)\"\n },\n {\n \"n\": \"pressure\",\n \"t\": \">H\",\n \"w\": \"0010\",\n \"crc\": \"crc8w\",\n \"r\": [0, 1400],\n \"d\": 0,\n \"desc\": \"Pressure (mbar)\"\n },\n {\n \"n\": \"softReset\",\n \"w\": \"d304\",\n \"desc\": \"Soft reset\"\n }\n ]\n }\n}\n,\"SCD40\":{\n \"_notes\": \"Sensirion SCD40 CO2/temperature/humidity. No detection values (unique address 0x62). read_measurement (0xEC05) returns three big-endian words, each followed by a CRC byte: CO2 (ppm) at 0, T at 3 (-45 + 175*raw/65535), RH at 6 (100*raw/65535). Standard decode. temperatureOffset, altitude, autoCalibration and calibrationTarget are only accepted while periodic measurement is stopped; the record keeps it running, so at present the sensor ignores them. ambientPressure (hPa) overrides the altitude and is accepted during measurement.\",\n \"addresses\": \"0x62\",\n \"detectionValues\": \"\",\n \"initValues\": \"0x21b1=\",\n \"pollInfo\": {\n \"c\": \"0xec05=r9\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"CO2 Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SCD40\",\n \"clas\": [\"CO2\", \"TEMP\", \"RH\"],\n \"resp\": {\n \"b\": 9,\n \"a\": [\n {\n \"n\": \"CO2\",\n \"t\": \">H\",\n \"u\": \"ppm\",\n \"r\": [0, 40000],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"temperature\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-45, 175],\n \"d\": 374.4857142857143,\n \"a\": -45,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 6,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"d\": 655.35,\n \"f\": \".2f\",\n \"o\": \"float\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"temperatureOffset\",\n \"t\": \">H\",\n \"w\": \"241d\",\n \"crc\": \"crc8w\",\n \"mul\": 374.4857,\n \"r\": [0, 20],\n \"d\": 4,\n \"desc\": \"Temp offset (C)\"\n },\n {\n \"n\": \"altitude\",\n \"t\": \">H\",\n \"w\": \"2427\",\n \"crc\": \"crc8w\",\n \"r\": [0, 3000],\n \"d\": 0,\n \"desc\": \"Altitude (m)\"\n },\n {\n \"n\": \"ambientPressure\",\n \"t\": \">H\",\n \"w\": \"e000\",\n \"crc\": \"crc8w\",\n \"r\": [700, 1200],\n \"d\": 1013,\n \"desc\": \"Pressure (hPa)\"\n },\n {\n \"n\": \"autoCalibration\",\n \"t\": \">H\",\n \"w\": \"2416\",\n \"crc\": \"crc8w\",\n \"r\": [0, 1],\n \"d\": 1,\n \"desc\": \"Auto-calibration\"\n },\n {\n \"n\": \"calibrationTarget\",\n \"t\": \">H\",\n \"w\": \"243a\",\n \"crc\": \"crc8w\",\n \"r\": [0, 65535],\n \"d\": 400,\n \"desc\": \"ASC target (ppm)\"\n }\n ]\n }\n}\n,\"SGP30\":{\n \"_notes\": \"Sensirion SGP30 indoor air quality (eCO2 and TVOC) at 0x58 (fixed). Detection: get_feature_set (0x202F, 10 ms) - one word with its Sensirion CRC-8 (:crc8w) whose top 4 bits are the product type, 0 for the SGP30. Init: iaq_init (0x2003, 10 ms). Poll (1 s - measure_iaq must be called once a second for the chip's own dynamic baseline compensation): measure_iaq (0x2008), wait 12 ms, read eCO2 (ppm) and TVOC (ppb), each word followed by a CRC (not checked). For the first 15 s after iaq_init it reports 400 ppm and 0 ppb. setBaseline restores the IAQ baseline: TVOC baseline, then eCO2 baseline (the reverse of the order get_iaq_baseline returns them; the record has no action to read the baseline). setHumidity: absolute humidity for compensation, 8.8 fixed point in g/m3, 0 turns it off.\",\n \"addresses\": \"0x58\",\n \"detectionValues\": \"0x202f=p10&=0b0000XXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"0x2003=p10\",\n \"pollInfo\": {\n \"c\": \"0x2008=p12&=r6\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the lone 0x99 is NACKed (half a 16-bit command); the serial number, feature set and measurements are unchanged\"}},\n \"devInfoJson\": {\n \"name\": \"Gas Sensor (VOC)\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SGP30\",\n \"clas\": [\"VOC\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"eCO2\",\n \"t\": \">H\",\n \"u\": \"ppm\",\n \"r\": [400, 60000],\n \"f\": \"5d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"VOC\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"ppb\",\n \"r\": [0, 60000],\n \"f\": \"5d\",\n \"o\": \"uint16\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"setBaseline\",\n \"t\": \">HH\",\n \"w\": \"201e\",\n \"crc\": \"crc8w\",\n \"r\": [0, 65535],\n \"d\": 0,\n \"desc\": \"Set baseline\"\n },\n {\n \"n\": \"setHumidity\",\n \"t\": \">H\",\n \"w\": \"2061\",\n \"crc\": \"crc8w\",\n \"mul\": 256,\n \"r\": [0, 255],\n \"d\": 0,\n \"desc\": \"Humidity (g/m3)\"\n }\n ]\n }\n}\n,\"SGP40\":{\n \"_notes\": \"Sensirion SGP40 VOC sensor at 0x59 (fixed). Detection: get_serial_number (0x3682, 1 ms) - three data words, each with a Sensirion CRC-8 (:crc8w). Init: start a raw measurement - measure_raw_signal (0x260F) with the default compensation 50 %RH (0x8000, CRC 0xA2) and 25 degC (0x6666, CRC 0x93); the result is ready after 30 ms. Poll (1 s, the rate Sensirion's VOC algorithm expects): read the previous measurement (SRAW_VOC word + its CRC, which isn't checked), then start the next, so no poll waits for the measurement. The value is the raw signal in ticks: the VOC index (0-500) needs Sensirion's gas index algorithm, run by the host (e.g. the app). Compensation stays at the defaults: the record can't feed in another sensor's humidity and temperature.\",\n \"addresses\": \"0x59\",\n \"detectionValues\": \"0x3682=p1&=0bXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"0x260f8000a2666693\",\n \"pollInfo\": {\n \"c\": \"=r3&0x260f8000a2666693=\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the lone 0x99 is NACKed (half a 16-bit command), so nothing is sent on; the serial number and the measurement are unchanged\"}},\n \"devInfoJson\": {\n \"name\": \"SGP40\",\n \"desc\": \"Gas Sensor\",\n \"manu\": \"Sensirion\",\n \"type\": \"SGP40\",\n \"clas\": [\"VOC\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"vocTicks\",\n \"t\": \">H\",\n \"u\": \"ticks\",\n \"r\": [0, 65535],\n \"f\": \"5d\",\n \"o\": \"uint16\"\n }\n ]\n }\n }\n}\n,\"SHT31\":{\n \"_notes\": \"Sensirion SHT3x (SHT30, SHT31, SHT35 - same protocol) at 0x44 (ADDR low) or 0x45 (ADDR high), the SHT4x addresses: 2-byte commands, where the SHT4x has 1-byte ones, so each record's detection fails on the other part. Detection: read the status register (0xF32D) and require a valid CRC - only the CRC: the datasheet's reserved status bits aren't reliably 0 (bits 5 and 6 read 1 in periodic mode, status 0x8070), and the read works in both single-shot and periodic mode. Init: break (0x3093) in case periodic measurement was left running, which would make single-shot commands fail. Poll: single-shot, high repeatability, no clock stretching (0x2400), 16 ms (max 15.5 ms), then read 6 bytes: T_MSB,T_LSB,T_CRC,RH_MSB,RH_LSB,RH_CRC. The sensor NACKs while measuring; the pause after the trigger bars the address, so a detection starting then is retried rather than failing, and the sensor is idle between polls.\",\n \"addresses\": \"0x44-0x45\",\n \"detectionValues\": \"0xf32d=p1&=0bXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"0x3093=p2\",\n \"pollInfo\": {\n \"c\": \"0x2400=p16&=r6\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SHT31\",\n \"clas\": [\"TEMP\",\"RH\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-40,125],\n \"d\": 374.48571428571427,\n \"a\": -45,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"d\": 655.35,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"SHT40\":{\n \"_notes\": \"Sensirion SHT4x (SHT40, SHT41, SHT45 - indistinguishable over I2C) at 0x44-0x46. Detection: read the serial number (0x89, 10 ms) and require valid CRCs on both words. Poll: high-precision single-shot measurement (0xFD), 10 ms, then read 6 bytes: T_MSB,T_LSB,T_CRC,RH_MSB,RH_LSB,RH_CRC. The sensor NACKs while measuring (8.3 ms); the pause after the trigger bars the address, so a detection starting then is retried rather than failing, and the sensor is idle between polls.\",\n \"addresses\": \"0x44-0x46\",\n \"detectionValues\": \"0x89=p10&=0bXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0xfd=p10&=r6\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SHT40\",\n \"clas\": [\"TEMP\",\"RH\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-45,130],\n \"d\": 374.48571428571427,\n \"a\": -45,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"d\": 524.28,\n \"a\": -6,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"SHTC3\":{\n \"_notes\": \"Sensirion SHTC3 at 0x70 (fixed - also GravityO2's address and a common multiplexer address). Detection: wake-up (0x3517, harmless when awake, needed if left asleep), then the ID register (0xEFC8): bit 11 = 1 and bits 5-0 = 000111, with a valid CRC. Init: wake-up. Poll: measure, normal mode, T first, no clock stretching (0x7866), 13 ms (max 12.1 ms; the pause bars the address), then read 6 bytes: T_MSB,T_LSB,T_CRC,RH_MSB,RH_LSB,RH_CRC. T = -45 + 175*raw/2^16, RH = 100*raw/2^16.\",\n \"addresses\": \"0x70\",\n \"detectionValues\": \"0x3517=p1&0xefc8=p1&=0bXXXX1XXXXX000111XXXXXXXX:crc8w\",\n \"initValues\": \"0x3517=p1\",\n \"pollInfo\": {\n \"c\": \"0x7866=p13&=r6\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SHTC3\",\n \"clas\": [\"TEMP\",\"RH\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-40,125],\n \"d\": 374.4914285714286,\n \"a\": -45,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"d\": 655.36,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"STCC4\":{\n \"_notes\": \"Sensirion STCC4 miniature CO2 sensor (paired with SHT41 for RH/T compensation), fixed address 0x64. Detection is empty (unique address) - the product-ID command cannot be used because the detection path issues a single write+read with no inter-command delay and the STCC4 NACKs that read - so the record claims any device at 0x64. Init: start_continuous_measurement (0x218B). Poll every 1.5 s (longer than the 1 s measurement, so the read always has new data; at 1 s the two drift and a read is NACKed now and then): read_measurement (0xEC05), 1 ms pause, read 12 bytes = CO2/temp/humidity/status words each followed by a CRC byte (skipped via 'at'). CO2 is signed (i16 in the datasheet). Status: only bit 0x0040 (testing mode) is documented; after power-up 0x0100 was seen for about 2 minutes. Actions: testingMode (pauses ASC), pressure (hPa). Forced recalibration isn't an action: it needs measurement stopped (1.2 s), then FRC, then a restart. Datasheet: https://sensirion.com/media/documents/6AED4B15/69295E41/CD_DS_STCC4_D1.pdf. Rates (_conf.rate) only set the poll interval, 1.5 s to 60 s. CO2 raw = ppm; temperature = -45 + 175*s/65535; humidity = -6 + 125*s/65535. Must never be probed by the RSAO delegated-identification hook (its framed 0x99 command stops the sensor measuring); RaftI2C DeviceIdentMgr only runs that hook when no device-type record matches - this record's empty detection always matches, so the hook is never reached at 0x64. See devdocs/stcc4-identification-notes.md testingMode: 1 enable_testing_mode (0x3FBC), 0 disable_testing_mode (0x3F3D); ASC is always on otherwise. pressure: sent as Pa/2, reset to 1013 hPa at power-up.\",\n \"addresses\": \"0x64-0x65\",\n \"detectionValues\": \"\",\n \"initValues\": \"0x218b=\",\n \"pollInfo\": {\n \"c\": \"0xec05=p1&=r12\",\n \"i\": 1500,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"CO2 Sensor\",\n \"desc\": \"CO2, Temp & Humidity\",\n \"manu\": \"Sensirion\",\n \"type\": \"STCC4\",\n \"clas\": [\"CO2\", \"TEMP\", \"RH\"],\n \"resp\": {\n \"b\": 12,\n \"a\": [\n {\n \"n\": \"CO2\",\n \"at\": 0,\n \"t\": \">h\",\n \"u\": \"ppm\",\n \"r\": [0, 32000],\n \"f\": \"d\",\n \"o\": \"int16\"\n },\n {\n \"n\": \"temperature\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-45, 130],\n \"d\": 374.48571428571427,\n \"a\": -45,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 6,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"d\": 524.28,\n \"a\": -6,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"status\",\n \"at\": 9,\n \"t\": \">H\",\n \"u\": \"\",\n \"r\": [0, 65535],\n \"f\": \"04x\",\n \"o\": \"uint16\",\n \"vs\": false,\n \"bl\": {\"0\": \"OK\", \"0x0040\": \"testing\"}\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"_conf.rate\",\n \"r\": [0.0167, 0.667],\n \"d\": 0.667,\n \"desc\": \"Sample rate (Hz)\",\n \"map\": {\n \"0.667\": {\"i\": 1500000, \"s\": 1},\n \"0.5\": {\"i\": 2000000, \"s\": 1},\n \"0.2\": {\"i\": 5000000, \"s\": 1},\n \"0.1\": {\"i\": 10000000, \"s\": 1},\n \"0.0333\": {\"i\": 30000000, \"s\": 1},\n \"0.0167\": {\"i\": 60000000, \"s\": 1}\n }\n },\n {\n \"n\": \"testingMode\",\n \"map\": {\"0\": \"3f3d\", \"1\": \"3fbc\"},\n \"desc\": \"Testing mode\"\n },\n {\n \"n\": \"pressure\",\n \"t\": \">H\",\n \"w\": \"e016\",\n \"crc\": \"crc8w\",\n \"mul\": 50,\n \"r\": [400, 1100],\n \"d\": 1013,\n \"desc\": \"Pressure (hPa)\"\n }\n ]\n }\n}\n,\"TMP117\":{\n \"_notes\": \"High-Accuracy Temperature Sensor. Detection: 0x0F (DevID) = 0x0117. Init: 0x01 (Config) = 0x0220 (1s continuous mode, 8 avg). Poll: 0x00 (Temp) = 2 bytes.\",\n \"addresses\": \"0x48-0x4B\",\n \"detectionValues\": \"0x0F=0x0117\",\n \"initValues\": \"0x010220=\",\n \"pollInfo\": {\n \"c\": \"0x00=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature Sensor\",\n \"desc\": \"\",\n \"manu\": \"Texas Instruments\",\n \"type\": \"TMP117\",\n \"clas\": [\"TEMP\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">h\",\n \"u\": \"°C\",\n \"r\": [-55, 150],\n \"d\": 128,\n \"a\": 0,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"TrillBar\":{\n \"_notes\": \"Bela Trill Bar - 1D capacitive touch, 26 channels, up to 5 simultaneous touches. CENTROID mode. Identify cmd returns device_type=1 in byte 1 of 3-byte response. Address jumper-configurable 0x20-0x27.\",\n \"addresses\": \"0x20-0x27\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000001\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r20\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Bar\",\n \"desc\": \"Capacitive Touch Bar\",\n \"manu\": \"Bela\",\n \"type\": \"TrillBar\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 20,\n \"a\": [\n { \"n\": \"loc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillCraft\":{\n \"_notes\": \"Bela Trill Craft - 1D capacitive sensor strip, 30 channels, up to 5 simultaneous touches in CENTROID mode (default DIFF). Address jumper-configurable 0x30-0x37.\",\n \"addresses\": \"0x30-0x37\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000011\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r20\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Craft\",\n \"desc\": \"Capacitive Touch Craft\",\n \"manu\": \"Bela\",\n \"type\": \"TrillCraft\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 20,\n \"a\": [\n { \"n\": \"loc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillFlex\":{\n \"_notes\": \"Bela Trill Flex - flexible 1D capacitive touch with custom electrode connection, 30 channels, up to 5 touches in CENTROID mode (default DIFF). Address jumper-configurable 0x48-0x4F. Init, as Bela's Trill library (Trill-Linux) for the Flex: CENTROID mode; prescaler 4; scan settings speed 0, 12 bits; a baseline update; noise threshold 123 (0.03 at 12 bits). Poll: up to 5 touches, positions (loc, 0 to 3712 = 29 x 128, the first to the 30th channel; 65535 for no touch) then sizes (size, 0 for no touch; the library scales by 1200 at 12 bits), as 5-element arrays; the dashboard shows them with its touch strip visualizer (vt touchstrip). The prescaler matters: on the bench (Axiom018, 2026-10-23, with the flexible strip) the chip as left after the mode command read 4095 (saturated) on every channel, so no touch was ever seen; prescaler 2 read about 2100-2750 untouched with a finger saturating, 4 about 540-710 with a finger peaking at 1200-1950, and 8 about 0. Bela's library uses DIFF mode for the Flex and finds centroids on the host.\",\n \"addresses\": \"0x48-0x4F\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000110\",\n \"initValues\": \"0x000100=p15&0x000304=p15&0x0002000c=p15&0x0006=p30&0x00047b=p15\",\n \"pollInfo\": {\n \"c\": \"0x04=r20\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Flex\",\n \"desc\": \"Capacitive Touch Flex\",\n \"manu\": \"Bela\",\n \"type\": \"TrillFlex\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 20,\n \"a\": [\n {\n \"n\": \"loc\",\n \"t\": \">H[5]\",\n \"r\": [0, 3712],\n \"f\": \"d\",\n \"o\": \"uint16\",\n \"vt\": \"touchstrip\",\n \"vo\": {\"sizeAttr\": \"size\", \"noValue\": 65535, \"posMax\": 3712, \"sizeMax\": 1200, \"numChannels\": 30}\n },\n {\n \"n\": \"size\",\n \"t\": \">H[5]\",\n \"r\": [0, 65535],\n \"f\": \"d\",\n \"o\": \"uint16\"\n }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 123, \"desc\": \"Noise threshold (counts at 12 bits; 123 is the Trill library's 0.03)\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [1, 8], \"d\": 4, \"desc\": \"Prescaler (higher reads lower; lower it for more sensitivity, raise it if channels saturate at 4095)\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillHex\":{\n \"_notes\": \"Bela Trill Hex - 2D hexagonal capacitive touch pad, 30 channels, up to 4 simultaneous touches. CENTROID response: 4 vertical loc + 4 vertical size + 4 horizontal loc + 4 horizontal size (uint16 BE). Address jumper-configurable 0x40-0x47.\",\n \"addresses\": \"0x40-0x47\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000101\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r32\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Hex\",\n \"desc\": \"Capacitive Touch Hex\",\n \"manu\": \"Bela\",\n \"type\": \"TrillHex\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 32,\n \"a\": [\n { \"n\": \"vloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillRing\":{\n \"_notes\": \"Bela Trill Ring - 1D circular capacitive touch, 28 ring channels + 2 button channels, up to 5 touches. CENTROID response: 5 loc + 5 size + 2 button values (uint16 BE). Address jumper-configurable 0x38-0x3F.\",\n \"addresses\": \"0x38-0x3F\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000100\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r24\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Ring\",\n \"desc\": \"Capacitive Touch Ring\",\n \"manu\": \"Bela\",\n \"type\": \"TrillRing\",\n \"clas\": [\"TCH\", \"BTN\"],\n \"resp\": {\n \"b\": 24,\n \"a\": [\n { \"n\": \"loc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"btn0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"btn1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillSquare\":{\n \"_notes\": \"Bela Trill Square - 2D capacitive touch, 30 channels, up to 4 simultaneous touches. CENTROID mode returns 4 vertical loc+size pairs followed by 4 horizontal loc+size pairs. Address jumper-configurable 0x28-0x2F.\",\n \"addresses\": \"0x28-0x2F\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000010\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r32\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Square\",\n \"desc\": \"Capacitive Touch Square\",\n \"manu\": \"Bela\",\n \"type\": \"TrillSquare\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 32,\n \"a\": [\n { \"n\": \"vloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"VCNL4040\":{\n \"addresses\": \"0x60\",\n \"detectionValues\": \"0x0c=0b100001100000XXXX\",\n \"initValues\": \"0x041007=&0x030e08=&0x000000=\",\n \"pollInfo\": {\n \"c\": \"0x08=r2&0x09=r2&0x0a=r2\",\n \"i\": 200,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Light Sensor (ambient & proximity)\",\n \"desc\": \"\",\n \"manu\": \"Vishay\",\n \"type\": \"VCNL4040\",\n \"clas\": [\"PROX\",\"LGHT\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"prox\",\n \"t\": \"H\",\n \"u\": \"mm\",\n \"r\": [0, 2000],\n \"f\": \"4d\",\n \"o\": \"uint16\",\n \"vft\": \"valid\"\n }\n ]\n }\n }\n}\n,\"VL53L1X\":{\n \"_notes\": \"ToF distance sensor with extended range up to 4m\",\n \"addresses\": \"0x29\",\n \"identPriority\": 1,\n \"detectionValues\": \"0x010f=0xEACC\",\n \"initValues\": \"0x002D01&0x0087&0x25012c00=p100&0x0060=p100\",\n \"pollInfo\": {\n \"c\": \"0x0089=r1&0x0096=r2\",\n \"i\": 200,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Distance Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"VL53L1X\",\n \"clas\": [\"DIST\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"x\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false\n },\n {\n \"n\": \"dist\",\n \"t\": \">H\",\n \"u\": \"mm\",\n \"r\": [0, 4000],\n \"f\": \"4d\",\n \"o\": \"uint16\",\n \"vft\": \"valid\"\n }\n ]\n }\n }\n}\n,\"VL53L4CD\":{\n \"_notes\": \"ToF distance sensor with range 0-1.3m\",\n \"addresses\": \"0x29\",\n \"identPriority\": 1,\n \"detectionValues\": \"0x010F=0xEBAA\",\n \"initValues\": \"0x002D0001011102000208000810010100000000FF000F0000000000200B000002142100&0x004e000500000000c8000038ff010008000001cc0701f10500a0008008380000&006c00000f89000000000000000107050606000002c7ff9b000000010000&0x008740=p200&0x008601&0x008780&0x000809&0x000b00&0x00240500&0x006c00000000&0x005e02f2&0x006103a1&008721\",\n \"pollInfo\": {\n \"c\": \"0x008601=&0x0089=r1&0x0096=r2\",\n \"i\": 100,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Distance Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"VL53L4CD\",\n \"clas\": [\"DIST\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"x\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false\n },\n {\n \"n\": \"dist\",\n \"t\": \">H\",\n \"u\": \"mm\",\n \"r\": [0, 1000],\n \"f\": \"3d\",\n \"o\": \"uint16\",\n \"vft\": \"valid\"\n }\n ]\n }\n }\n}\n,\"VL6180\":{\n \"_notes\": \"VL6180X ToF proximity sensor with 1x range scaling (RANGE_SCALER 0x0096/0x0097=>0x00fd), so the range is an 8-bit value in mm (0-255). Specified range 0-100mm, typically up to ~200mm with a reflective target and low ambient light\\nInit: write 0x0018=>0x01 then wait 100ms, which stops continuous ranging left running by a previous init (the sensor may not have been power cycled) or otherwise does one harmless single-shot range; AN4545 private settings https://www.st.com/resource/en/application_note/an4545-vl6180x-basic-ranging-application-note-stmicroelectronics.pdf; clear SYSTEM__FRESH_OUT_OF_RESET (0x0016=>0x00); GPIO1 interrupt output active low (0x0011=>0x10); readout averaging period 4.3ms (0x010a=>0x30); VHV recalibration every 255 ranges (0x0031=>0xff) and once now (0x002e=>0x01); range inter-measurement period 90ms (0x001b=>0x08); interrupt on new range sample only (0x0014=>0x04); clear interrupts (0x0015=>0x07); start continuous ranging (0x0018=>0x03)\\nPoll: RESULT__RANGE_STATUS, RESULT__ALS_STATUS and RESULT__INTERRUPT_STATUS_GPIO (0x004d-0x004f), RESULT__RANGE_VAL (0x0062), then clear interrupts (0x0015=>0x07)\\nerr is the range error code (0x004d bits 7:4) and valid is true only when it is 0. With no target in range err is typically 6 (early convergence estimate), 7 (max convergence time) or 11 (signal to noise) and the raw range reads 255\\nfresh is 1 when a new range sample completed since the previous poll (0x004f bit 2, new sample ready) and 0 when the poll re-read the previous sample\\nSample rate (_conf.rate): the sensor's inter-measurement period (0x001b) is set shorter than the poll interval so that every poll normally sees a new sample. Up to 10Hz the period is 90ms with the default max convergence time 49ms (0x001c=>0x31) and readout averaging 4.3ms. 20Hz uses a 40ms period, 25ms max convergence and 4.3ms averaging. 50Hz uses a 10ms period, 3ms max convergence and 1.8ms averaging (0x010a=>0x08), which reduces range on dark targets and increases noise. Ranging is stopped while the timing is changed and then restarted\\nAmbient light sensing (ALS) is not used\",\n \"identPriority\": 2,\n \"addresses\": \"0x29\",\n \"detectionValues\": \"0x0000=0b10110100\",\n \"initValues\": \"0x001801=p100&0x020701&0x020801&0x009600&0x0097fd&0x00e301&0x00e403&0x00e502&0x00e601&0x00e703&0x00f502&0x00d905&0x00dbce&0x00dc03&0x00ddf8&0x009f00&0x00a33c&0x00b700&0x00bb3c&0x00b209&0x00ca09&0x019801&0x01b017&0x01ad00&0x00ff05&0x010005&0x019905&0x01a61b&0x01ac3e&0x01a71f&0x003000&0x001600&0x001110&0x010a30&0x0031ff&0x002e01=p10&0x001c31&0x001b08&0x001404&0x001507&0x001803\",\n \"pollInfo\": {\n \"c\": \"0x004d=r3&0x0062=r1&0x001507\",\n \"i\": 200,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Distance Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"VL6180\",\n \"clas\": [\"DIST\"],\n \"resp\": {\n \"b\": 4,\n \"a\": [\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"at\": 0,\n \"u\": \"\",\n \"r\": [0,1],\n \"m\": \"0xF0\",\n \"s\": 4,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false,\n \"lut\": [\n {\n \"r\": \"0\",\n \"v\": 1\n },\n {\n \"r\": \"\",\n \"v\": 0\n }\n ]\n },\n {\n \"n\": \"err\",\n \"t\": \"B\",\n \"at\": 0,\n \"u\": \"\",\n \"r\": [0,15],\n \"m\": \"0xF0\",\n \"s\": 4,\n \"f\": \"d\",\n \"vs\": false,\n \"vl\": {\"0\": \"OK\", \"1\": \"VCSEL cont\", \"2\": \"VCSEL test\", \"3\": \"VCSEL wdog\", \"4\": \"PLL1\", \"5\": \"PLL2\", \"6\": \"early conv\", \"7\": \"max conv\", \"8\": \"no target\", \"9\": \"unused\", \"10\": \"unused\", \"11\": \"SNR\", \"12\": \"raw under\", \"13\": \"raw over\", \"14\": \"underflow\", \"15\": \"overflow\"}\n },\n {\n \"n\": \"fresh\",\n \"t\": \"B\",\n \"at\": 2,\n \"u\": \"\",\n \"r\": [0,1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false\n },\n {\n \"n\": \"dist\",\n \"t\": \"B\",\n \"at\": 3,\n \"u\": \"mm\",\n \"r\": [0,255],\n \"f\": \"3d\",\n \"o\": \"float\",\n \"vft\": \"valid\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"_conf.rate\",\n \"t\": \"B\",\n \"r\": [1,50],\n \"d\": 5,\n \"desc\": \"Sample rate (Hz)\",\n \"map\": {\n \"1\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 1000000,\n \"s\": 1\n },\n \"2\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 500000,\n \"s\": 1\n },\n \"5\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 200000,\n \"s\": 1\n },\n \"10\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 100000,\n \"s\": 1\n },\n \"20\": {\n \"w\": \"001801&001b03&001c19&010a30&001507&001803\",\n \"i\": 50000,\n \"s\": 1\n },\n \"50\": {\n \"w\": \"001801&001b00&001c03&010a08&001507&001803\",\n \"i\": 20000,\n \"s\": 1\n }\n }\n }\n ]\n }\n}\n}}", + "compiled": "{\"devTypes\":{\"ADXL313\":{\n \"addresses\": \"0x1d,0x53\",\n \"detectionValues\": \"0x00=0xAD1DCB\",\n \"initValues\": \"0x310b=&0x2d08=\",\n \"pollInfo\": {\n \"c\": \"0x32=r6\",\n \"i\": 100,\n \"s\": 3\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer\",\n \"desc\": \"3-Axis Accel\",\n \"manu\": \"Analog Devices\",\n \"type\": \"ADXL313\",\n \"clas\": [\"ACC\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"ax\",\n \"t\": \"I\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"m\": \"0xfffff000\",\n \"s\": 12,\n \"d\": 10485.76,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"temperature\",\n \"at\": 2,\n \"t\": \">I\",\n \"u\": \"°C\",\n \"r\": [-40, 85],\n \"m\": \"0x000fffff\",\n \"d\": 5242.88,\n \"a\": -50,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"AMG8833\":{\n \"addresses\": \"0x69\",\n \"_notes\": \"default settings on power on: normal (not sleep) mode, 10 frames per second, no interrupt, moving average off\",\n \"detectionValues\": \"\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x80=r128\",\n \"i\": 100,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Thermal Camera\",\n \"desc\": \"\",\n \"manu\": \"Panasonic\",\n \"type\": \"AMG8833\",\n \"clas\": [\"TCAM\"],\n \"resp\": {\n \"b\": 128,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"H\",\n \"u\": \"degrees\",\n \"r\": [0, 360],\n \"f\": \"3.1f\",\n \"o\": \"float\",\n \"d\": 11.377777777777778\n }\n ]\n }\n }\n}\n,\"AdafruitGamepad\":{\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x0001=0b10000111\",\n \"initValues\": \"0x007fff=p20&0x010300010067=p1&0x010b00010067=p1&0x010500010067=p1\",\n \"pollInfo\": {\n \"c\": \"0x0915=r2&0x0916=r2&0x0104=r4\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"readEffect\", \"basis\": \"source\", \"note\": \"ATtiny seesaw: shipped firmware has no handler for 0x99 (read NACKed); library 2.0.0 discards one-byte writes, so the read answers for the previous register and, if that was GPIO bulk, clears the interrupt flags (Adafruit_seesawPeripheral 8070dfb, f7f738d)\"}},\n \"devInfoJson\": {\n \"name\": \"Adafruit Gamepad\",\n \"desc\": \"\",\n \"manu\": \"Adafruit\",\n \"type\": \"Gamepad\",\n \"clas\": [\"GAME\"],\n \"resp\": {\n \"b\": 8,\n \"a\": [\n {\n \"n\": \"x\",\n \"t\": \">H\",\n \"r\": [0, 65535],\n \"d\": -1,\n \"a\": 1023,\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"y\",\n \"t\": \">H\",\n \"r\": [0, 65535],\n \"d\": -1,\n \"a\": 1023,\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"SELECT\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x01\",\n \"m\": \"0x01\",\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"B\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x02\",\n \"m\": \"0x02\",\n \"s\": 1,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"Y\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x04\",\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"A\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x20\",\n \"m\": \"0x20\",\n \"s\": 5,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"X\",\n \"at\": 7,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x40\",\n \"m\": \"0x40\",\n \"s\": 6,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"START\",\n \"at\": 5,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"x\": \"0x01\",\n \"m\": \"0x01\",\n \"f\": \"b\",\n \"o\": \"bool\"\n }\n ]\n }\n }\n}\n,\"AdafruitSoilSensor\":{\n \"addresses\": \"0x36\",\n \"_notes\": \"sensor must be wrapped in heatshrink to produce sensible values. very sensitive to human proximity\",\n \"detectionValues\": \"0x0001=p25&=0x55\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x0F10=p250&=r2&0x0004=p250&=r4\",\n \"i\": 1000,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"SAMD09 seesaw: 0x99 isn't a module base, so the command is ignored and the read returns 0xFF or stale reply bytes (adafruit/seesaw 20e2892)\"}},\n \"devInfoJson\": {\n \"name\": \"Soil Moisture Sensor\",\n \"desc\": \"\",\n \"manu\": \"Adafruit\",\n \"type\": \"AdafruitSoilSensor\",\n \"clas\": [\"SOIL\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"moisture\",\n \"t\": \">H\",\n \"u\": \"\",\n \"r\": [0, 65535],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"temperature\",\n \"t\": \">i\",\n \"u\": \"°C\",\n \"r\": [-45, 175],\n \"d\": 65536,\n \"f\": \".2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"BLEBTHome\":{\n \"detectionValues\": \"\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"=r17\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"BLE Sensor (BTHome)\",\n \"desc\": \"\",\n \"manu\": \"Allterco\",\n \"type\": \"BLEBTHome\",\n \"clas\": [\"BTHM\"],\n \"resp\": {\n \"b\": 17,\n \"a\": [\n {\n \"n\": \"ID\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0,255],\n \"f\": \"d\",\n \"o\": \"uint8\"\n },\n {\n \"n\": \"MAC\",\n \"t\": \">Q\",\n \"o\": \"uint64\"\n },\n {\n \"n\": \"motion\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0,1],\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"battery\",\n \"t\": \"B\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"f\": \"d\",\n \"o\": \"uint8\"\n },\n {\n \"n\": \"temp\",\n \"t\": \">h\",\n \"u\": \"°C\",\n \"r\": [-100,100],\n \"d\": 100,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"light\",\n \"t\": \">I\",\n \"u\": \"Lux\",\n \"r\": [0,100000],\n \"d\": 100,\n \"f\": \".1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"BME280\":{\n \"_notes\": \"Bosch BME280 (temperature, pressure, humidity) at 0x76 (SDO low) or 0x77 (SDO high, Adafruit default). Detection: chip ID (0xD0) 0x60 - a BMP280 (0x56-0x58) or BME680 (0x61) doesn't match. Init: soft reset (0xE0B6), 5 ms; config (0xF5) standby 500 ms, filter off; ctrl_hum (0xF2) humidity x1 - written before ctrl_meas, which applies it; ctrl_meas (0xF4) temperature x1, pressure x1, normal mode. Poll: the calibration (0x88, 26 bytes; 0xE1, 7 bytes) and the data (0xF7, 8 bytes) - the decode code applies Bosch's floating-point compensation (datasheet 8.1). 0x76 is also a common multiplexer address (Axiom018's), where the BME280 can't be used.\",\n \"addresses\": \"0x76-0x77\",\n \"detectionValues\": \"0xd0=0x60\",\n \"initValues\": \"0xe0b6=p5&0xf580&0xf201&0xf427\",\n \"pollInfo\": {\n \"c\": \"0x88=r26&0xe1=r7&0xf7=r8\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature, Pressure & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Bosch\",\n \"type\": \"BME280\",\n \"clas\": [\"TEMP\",\"PRES\",\"RH\"],\n \"resp\": {\n \"b\": 41,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"32767){T2=T2-65536;}int T3=buf[4]|(buf[5]<<8);if(T3>32767){T3=T3-65536;}int P1=buf[6]|(buf[7]<<8);int P2=buf[8]|(buf[9]<<8);if(P2>32767){P2=P2-65536;}int P3=buf[10]|(buf[11]<<8);if(P3>32767){P3=P3-65536;}int P4=buf[12]|(buf[13]<<8);if(P4>32767){P4=P4-65536;}int P5=buf[14]|(buf[15]<<8);if(P5>32767){P5=P5-65536;}int P6=buf[16]|(buf[17]<<8);if(P6>32767){P6=P6-65536;}int P7=buf[18]|(buf[19]<<8);if(P7>32767){P7=P7-65536;}int P8=buf[20]|(buf[21]<<8);if(P8>32767){P8=P8-65536;}int P9=buf[22]|(buf[23]<<8);if(P9>32767){P9=P9-65536;}int H1=buf[25];int H2=buf[26]|(buf[27]<<8);if(H2>32767){H2=H2-65536;}int H3=buf[28];int E4=buf[29];if(E4>127){E4=E4-256;}int H4=E4*16+(buf[30]&15);int E6=buf[31];if(E6>127){E6=E6-256;}int H5=E6*16+(buf[30]>>4);int H6=buf[32];if(H6>127){H6=H6-256;}int aP=(buf[33]<<12)|(buf[34]<<4)|(buf[35]>>4);int aT=(buf[36]<<12)|(buf[37]<<4)|(buf[38]>>4);int aH=(buf[39]<<8)|buf[40];float v1=(aT/16384.0-T1/1024.0)*T2;float d=aT/131072.0-T1/8192.0;float v2=d*d*T3;float tf=v1+v2;out.temperature=tf/5120.0;v1=tf/2.0-64000.0;v2=v1*v1*P6/32768.0+v1*P5*2.0;v2=v2/4.0+P4*65536.0;v1=(P3*v1*v1/524288.0+P2*v1)/524288.0;v1=(1.0+v1/32768.0)*P1;float p=0.0;if(v1>0.0){p=1048576.0-aP;p=(p-v2/4096.0)*6250.0/v1;float w1=P9*p*p/2147483648.0;float w2=p*P8/32768.0;p=p+(w1+w2+P7)/16.0;}out.pressure=p/100.0;float h=tf-76800.0;h=(aH-(H4*64.0+H5/16384.0*h))*(H2/65536.0*(1.0+H6/67108864.0*h*(1.0+H3/67108864.0*h)));h=h*(1.0-H1*h/524288.0);if(h>100.0){h=100.0;}if(h<0.0){h=0.0;}out.humidity=h;\"\n }\n }\n }\n}\n,\"BME680\":{\n \"_notes\": \"Bosch BME680 (temperature, pressure, humidity, gas) at 0x76 (SDO low) or 0x77 (SDO high, Adafruit default). Detection: chip ID (0xD0) 0x61 and variant (0xF0) 0x00 - a BME688 (variant 0x01) has another data layout. Forced mode only, so each poll triggers a measurement (ctrl_meas 0x74 = 0x25: temperature and pressure x1, forced), waits 200 ms (TPH plus the 152 ms heater time) and reads the calibration (0x89 25 bytes, 0xE1 16 bytes, 0x00 5 bytes) and the data (0x1D 15 bytes); the decode code applies Bosch's floating-point compensation and gas-resistance calculation (range lookup tables as if-chains); gas is 0 unless gas valid and heater stable are both set. Init: soft reset, 10 ms; read the heater calibration; ctrl_hum 0x01; config 0x00 (filter off); gas_wait_0 0x66 (152 ms); res_heat_0; ctrl_gas_1 0x10 (run gas, heater profile 0). res_heat_0 is computed for each unit by initCode (Bosch's calc_res_heat): init reads par_g2/par_g1/par_g3 (0xEB-0xEE) and res_heat_val/res_heat_range (0x00-0x02), and the value for a 320 C hot plate at an assumed 25 C ambient is written to 0x5A ({heat}; the ambient temperature changes it by about 1 count over 15-35 C). 0x76 is also a common multiplexer address (Axiom018's).\",\n \"addresses\": \"0x76-0x77\",\n \"detectionValues\": \"0xd0=0x61&0xf0=0x00\",\n \"initValues\": \"0xe0b6=p10&0xeb=r4&0x00=r3&0x7201&0x7500&0x6466&0x5a{heat}&0x7110\",\n \"initCode\": {\n \"n\": \"bme680_heater\",\n \"c\": \"int g2=buf[0]|(buf[1]<<8);if(g2>32767){g2=g2-65536;}int g1=buf[2];if(g1>127){g1=g1-256;}int g3=buf[3];if(g3>127){g3=g3-256;}int rhv=buf[4];if(rhv>127){rhv=rhv-256;}int rhr=(buf[6]&48)>>4;float v1=g1/16.0+49.0;float v2=g2/32768.0*0.0005+0.00235;float v3=g3/1024.0;float v5=v1*(1.0+v2*320.0)+v3*25.0;init.heat=3.4*(v5*(4.0/(4.0+rhr))*(1.0/(1.0+rhv*0.002))-25.0);\"\n },\n \"pollInfo\": {\n \"c\": \"0x7425=p200&0x89=r25&0xe1=r16&0x00=r5&0x1d=r15\",\n \"i\": 3000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Environmental Sensor\",\n \"desc\": \"Temperature, pressure, humidity, gas\",\n \"manu\": \"Bosch\",\n \"type\": \"BME680\",\n \"clas\": [\"TEMP\",\"PRES\",\"RH\",\"GAS\"],\n \"resp\": {\n \"b\": 61,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"32767){t2=t2-65536;}int t3=buf[3];if(t3>127){t3=t3-256;}int p1=buf[5]|(buf[6]<<8);int p2=buf[7]|(buf[8]<<8);if(p2>32767){p2=p2-65536;}int p3=buf[9];if(p3>127){p3=p3-256;}int p4=buf[11]|(buf[12]<<8);if(p4>32767){p4=p4-65536;}int p5=buf[13]|(buf[14]<<8);if(p5>32767){p5=p5-65536;}int p7=buf[15];if(p7>127){p7=p7-256;}int p6=buf[16];if(p6>127){p6=p6-256;}int p8=buf[19]|(buf[20]<<8);if(p8>32767){p8=p8-65536;}int p9=buf[21]|(buf[22]<<8);if(p9>32767){p9=p9-65536;}int p10=buf[23];int h1=(buf[27]<<4)|(buf[26]&15);int h2=(buf[25]<<4)|(buf[26]>>4);int h3=buf[28];if(h3>127){h3=h3-256;}int h4=buf[29];if(h4>127){h4=h4-256;}int h5=buf[30];if(h5>127){h5=h5-256;}int h6=buf[31];int h7=buf[32];if(h7>127){h7=h7-256;}int rse=buf[45]&240;if(rse>127){rse=rse-256;}rse=rse/16;int aP=(buf[48]<<12)|(buf[49]<<4)|(buf[50]>>4);int aT=(buf[51]<<12)|(buf[52]<<4)|(buf[53]>>4);int aH=(buf[54]<<8)|buf[55];int aG=(buf[59]<<2)|(buf[60]>>6);int gr=buf[60]&15;float v1=(aT/16384.0-t1/1024.0)*t2;float d=aT/131072.0-t1/8192.0;float v2=d*d*t3*16.0;float tf=v1+v2;float tc=tf/5120.0;out.temperature=tc;v1=tf/2.0-64000.0;v2=v1*v1*p6/131072.0;v2=v2+v1*p5*2.0;v2=v2/4.0+p4*65536.0;v1=(p3*v1*v1/16384.0+p2*v1)/524288.0;v1=(1.0+v1/32768.0)*p1;float p=0.0;if(v1>0.0){p=1048576.0-aP;p=(p-v2/4096.0)*6250.0/v1;float w1=p9*p*p/2147483648.0;float w2=p*p8/32768.0;float q=p/256.0;float w3=q*q*q*p10/131072.0;p=p+(w1+w2+w3+p7*128.0)/16.0;}out.pressure=p/100.0;float e1=aH-(h1*16.0+h3/2.0*tc);float e2=e1*(h2/262144.0*(1.0+h4/16384.0*tc+h5/1048576.0*tc*tc));float hum=e2+(h6/16384.0+h7/2097152.0*tc)*e2*e2;if(hum>100.0){hum=100.0;}if(hum<0.0){hum=0.0;}out.humidity=hum;float k1=0.0;if(gr==5){k1=-1.0;}if(gr==7){k1=-0.8;}if(gr==10){k1=-0.2;}if(gr==11){k1=-0.5;}if(gr==13){k1=-1.0;}float k2=0.0;if(gr==4){k2=0.1;}if(gr==5){k2=0.7;}if(gr==7){k2=-0.8;}if(gr==8){k2=-0.1;}float g1=1340.0+5.0*rse;float g2=g1*(1.0+k1/100.0);float g3=1.0+k2/100.0;float gas=0.0;if((buf[60]&48)==48){gas=1.0/(g3*0.000000125*(1<32767){g2=g2-65536;}int g1=buf[2];if(g1>127){g1=g1-256;}int g3=buf[3];if(g3>127){g3=g3-256;}int rhv=buf[4];if(rhv>127){rhv=rhv-256;}int rhr=(buf[6]&48)>>4;float v1=g1/16.0+49.0;float v2=g2/32768.0*0.0005+0.00235;float v3=g3/1024.0;float v5=v1*(1.0+v2*320.0)+v3*25.0;init.heat=3.4*(v5*(4.0/(4.0+rhr))*(1.0/(1.0+rhv*0.002))-25.0);\"\n },\n \"pollInfo\": {\n \"c\": \"0x7425=p200&0x8a=r23&0xe1=r14&0x00=r5&0x1d=r17\",\n \"i\": 3000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Environmental Sensor\",\n \"desc\": \"Temperature, pressure, humidity, gas\",\n \"manu\": \"Bosch\",\n \"type\": \"BME690\",\n \"clas\": [\"TEMP\",\"PRES\",\"RH\",\"GAS\"],\n \"resp\": {\n \"b\": 59,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"127){t3=t3-256;}int p1=buf[10]|(buf[11]<<8);if(p1>32767){p1=p1-65536;}int p2=buf[12]|(buf[13]<<8);int p3=buf[14];if(p3>127){p3=p3-256;}int p4=buf[15];if(p4>127){p4=p4-256;}int p5=buf[4]|(buf[5]<<8);if(p5>32767){p5=p5-65536;}int p6=buf[6]|(buf[7]<<8);if(p6>32767){p6=p6-65536;}int p7=buf[8];if(p7>127){p7=p7-256;}int p8=buf[9];if(p8>127){p8=p8-256;}int p9=buf[18]|(buf[19]<<8);if(p9>32767){p9=p9-65536;}int p10=buf[20];if(p10>127){p10=p10-256;}int p11=buf[21];if(p11>127){p11=p11-256;}int h5=(buf[23]<<4)|(buf[24]>>4);if(h5>2047){h5=h5-4096;}int h1=(buf[25]<<4)|(buf[24]&15);if(h1>2047){h1=h1-4096;}int h2=buf[26];if(h2>127){h2=h2-256;}int h4=buf[27];if(h4>127){h4=h4-256;}int h3=buf[28];int h6=buf[29];int aP=(buf[44]<<16)|(buf[45]<<8)|buf[46];int aT=(buf[47]<<16)|(buf[48]<<8)|buf[49];int aH=(buf[50]<<8)|buf[51];int aG=(buf[57]<<2)|(buf[58]>>6);int gr=buf[58]&15;float cf=aT-t1*256.0;float T=cf*(t2/1073741824.0)+cf*cf*(t3/281474976710656.0);out.temperature=T;float o=p1*8.0;float tk10=p2/64.0;float tk20=p3/256.0;float tk30=p4/32768.0;float s=(p5-16384.0)/1048576.0;float tk1s=(p6-16384.0)/536870912.0;float tk2s=p7/4294967296.0;float tk3s=p8/137438953472.0;float nls=p9/281474976710656.0;float tknls=p10/281474976710656.0;float nls3=p11/36893488147419103232.0;float pf=aP*1.0;float P=o+tk10*T+tk20*T*T+tk30*T*T*T+pf*(s+tk1s*T+tk2s*T*T+tk3s*T*T*T)+pf*pf*(nls+tknls*T)+pf*pf*pf*nls3;out.pressure=P/100.0;float tc=T*5120.0-76800.0;float hoff=aH-(h1*64.0+h2/16384.0*tc);float k4=h4/67108864.0;float hs=hoff*(h5/65536.0)*(1.0+k4*tc+k4*(h3/67108864.0)*tc*tc);float H=hs*(1.0-h6/524288.0*hs);if(H<0.0){H=0.0;}if(H>100.0){H=100.0;}out.humidity=H;float gas=0.0;if((buf[58]&48)==48){gas=1000000.0*(262144>>gr)/(4096.0+3.0*(aG-512.0));}out.gas=gas;\"\n }\n }\n }\n}\n,\"CAP1188\":{\n \"_notes\": \"Microchip CAP1188 8-channel capacitive touch sensor with 8 LED drivers. Product ID 0x50, Manufacturer ID 0x5D. ADDR_COMM strap selects address in range 0x28-0x2D.\",\n \"addresses\": \"0x28-0x2d\",\n \"detectionValues\": \"0xfd=0x50&0xfe=0x5d\",\n \"initValues\": \"0x0000&0x1f36&0x2a00\",\n \"pollInfo\": {\n \"c\": \"0x02=r2&0x10=r8&0x0000\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Capacitive Touch\",\n \"desc\": \"8 channel\",\n \"manu\": \"Microchip\",\n \"type\": \"CAP1188\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 10,\n \"a\": [\n {\n \"n\": \"C1\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x01\",\n \"s\": 0,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C2\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x02\",\n \"s\": 1,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C3\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C4\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x08\",\n \"s\": 3,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C5\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x10\",\n \"s\": 4,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C6\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x20\",\n \"s\": 5,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C7\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x40\",\n \"s\": 6,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C8\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x80\",\n \"s\": 7,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"d1\",\n \"at\": 2,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d2\",\n \"at\": 3,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d3\",\n \"at\": 4,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d4\",\n \"at\": 5,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d5\",\n \"at\": 6,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d6\",\n \"at\": 7,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d7\",\n \"at\": 8,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n },\n {\n \"n\": \"d8\",\n \"at\": 9,\n \"t\": \"b\",\n \"r\": [-128, 127],\n \"f\": \"d\",\n \"o\": \"int8\"\n }\n ]\n }\n }\n}\n,\"CAP1203\":{\n \"addresses\": \"0x28\",\n \"detectionValues\": \"0xfd=0b01101101\",\n \"initValues\": \"0x0000&0x1f06\",\n \"pollInfo\": {\n \"c\": \"0x02=r2&0x0000\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Capacitive Touch\",\n \"desc\": \"\",\n \"manu\": \"Sparkfun\",\n \"type\": \"CAP1203\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"A\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x01\",\n \"s\": 0,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"B\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x02\",\n \"s\": 1,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"C\",\n \"at\": 1,\n \"t\": \"B\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n },\n {\n \"n\": \"status\",\n \"t\": \">H\",\n \"vs\": false,\n \"f\": \"04x\",\n \"o\": \"uint16\",\n \"bl\": {\"0x0001\": \"CS1\", \"0x0002\": \"CS2\", \"0x0004\": \"CS3\", \"0x0100\": \"TOUCH\", \"0x0200\": \"MTP\", \"0x0400\": \"MULT\", \"0x1000\": \"PWR\", \"0x2000\": \"ACAL_FAIL\", \"0x4000\": \"BC_OUT\"}\n }\n ]\n }\n }\n}\n,\"DPS310\":{\n \"_notes\": \"Infineon DPS310 barometric pressure and temperature sensor (Adafruit board) at 0x77 (default, SDO high) or 0x76. Also matches the Goertek SPL06-001, which has the same ID, registers and compensation. Detection: PRODUCT_ID (0x0D) = 0x10 (REV_ID 1, PROD_ID 0); a BME280/BME680/BME690 reads otherwise there (a BME690 0x04 on the bench). Init: soft reset (RESET 0x0C = 0x89), 50 ms for the coefficients to be ready; Infineon's temperature correction sequence (0x0E = 0xA5, 0x0F = 0x96, 0x62 = 0x02, 0x0E = 0, 0x0F = 0), without which some chips read temperatures about twice too high; PRS_CFG (0x06) 8 measurements/s, 16x oversampling (27.6 ms each); TMP_CFG (0x07) the external (MEMS) temperature sensor, 8/s, 1x; CFG_REG (0x09) P_SHIFT, needed above 8x pressure oversampling; MEAS_CFG (0x08) continuous pressure and temperature. TMP_CFG's sensor must match the one the coefficients were calibrated with (COEF_SRCE, 0x28 bit 7): the external one on every chip seen (0xA9 on the bench), which the init assumes. Poll: PSR_B2-B0 and TMP_B2-B0 (0x00, 6 bytes, 24-bit two's complement, big-endian) and the coefficients (0x10, 18 bytes); the decode code applies Infineon's compensation (datasheet 4.9) with the scale factors for 16x (pressure, 253952) and 1x (temperature, 524288) oversampling, so a change to PRS_CFG or TMP_CFG oversampling needs them changed too.\",\n \"addresses\": \"0x76-0x77\",\n \"detectionValues\": \"0x0d=0x10\",\n \"initValues\": \"0x0c89=p50&0x0ea5&0x0f96&0x6202&0x0e00&0x0f00&0x0634&0x07b0&0x0904&0x0807\",\n \"pollInfo\": {\n \"c\": \"0x00=r6&0x10=r18\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the lone 0x99 + 9-byte read (registers 0x99-0xA1, not in the map) read zeros; configuration, coefficients and COEF_SRCE unchanged and measuring carried on. Its addresses are in Axiom's excluded RSAO range anyway\"}},\n \"devInfoJson\": {\n \"name\": \"Pressure & Temperature Sensor\",\n \"desc\": \"\",\n \"manu\": \"Infineon\",\n \"type\": \"DPS310\",\n \"clas\": [\"PRES\",\"TEMP\"],\n \"resp\": {\n \"b\": 24,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"8388607){pr=pr-16777216;}int tr=(buf[3]<<16)|(buf[4]<<8)|buf[5];if(tr>8388607){tr=tr-16777216;}int c0=(buf[6]<<4)|(buf[7]>>4);if(c0>2047){c0=c0-4096;}int c1=((buf[7]&15)<<8)|buf[8];if(c1>2047){c1=c1-4096;}int c00=(buf[9]<<12)|(buf[10]<<4)|(buf[11]>>4);if(c00>524287){c00=c00-1048576;}int c10=((buf[11]&15)<<16)|(buf[12]<<8)|buf[13];if(c10>524287){c10=c10-1048576;}int c01=(buf[14]<<8)|buf[15];if(c01>32767){c01=c01-65536;}int c11=(buf[16]<<8)|buf[17];if(c11>32767){c11=c11-65536;}int c20=(buf[18]<<8)|buf[19];if(c20>32767){c20=c20-65536;}int c21=(buf[20]<<8)|buf[21];if(c21>32767){c21=c21-65536;}int c30=(buf[22]<<8)|buf[23];if(c30>32767){c30=c30-65536;}float ts=tr/524288.0;float ps=pr/253952.0;out.temperature=c0*0.5+c1*ts;float p=c00+ps*(c10+ps*(c20+ps*c30))+ts*c01+ts*ps*(c11+ps*c21);out.pressure=p/100.0;\"\n }\n }\n }\n}\n,\"ENS160\":{\n \"_notes\": \"ScioSense ENS160 digital metal-oxide multi-gas sensor (Adafruit and SparkFun boards) at 0x53 (default, ADDR high) or 0x52. Detection: PART_ID (0x00, 2 bytes little-endian) = 0x0160. Init: OPMODE (0x10) idle (0x01), then compensation TEMP_IN (0x13, kelvin x 64) = 25 degC and RH_IN (0x15, %RH x 512) = 50 %RH in one write, then OPMODE standard (0x02), which measures once a second. Poll (1 s): 6 bytes from DEVICE_STATUS (0x20): status, DATA_AQI (UBA air quality index 1-5, bits 2:0), DATA_TVOC (ppb) and DATA_ECO2 (ppm), both little-endian. Status: STATAS (bit 7) = an operating mode is running, STATER (bit 6) = error, VALIDITY (bits 3:2) 0 = normal, 1 = warm-up (the first 3 min after each power-on), 2 = initial start-up (the first hour of operation in the sensor's life), 3 = invalid output; NEWDAT (bit 1) = new data, NEWGPR (bit 0) = new general-purpose register data; the status labels are S (STATAS), E (STATER), D (NEWDAT) and G (NEWGPR), with the validity bits left to the validity attribute. Readings during warm-up or initial start-up are not yet reliable. The compensation actions take degC and %RH (e.g. from another sensor); the mode action switches between deep sleep, idle and standard.\",\n \"addresses\": \"0x52-0x53\",\n \"detectionValues\": \"0x00=0x6001\",\n \"initValues\": \"0x1001=p10&0x138a4a0064=&0x1002=p10\",\n \"pollInfo\": {\n \"c\": \"0x20=r6\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"readEffect\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the chip ignores bit 7 of the register address, so the lone 0x99 points at 0x19 and the 9-byte read covers 0x19-0x21, ending with DEVICE_STATUS and DATA_AQI. No configuration register changed (OPMODE, CONFIG, TEMP_IN, RH_IN) and measuring carried on; reading DATA_AQI may clear the NEWDAT flag (transient)\"}},\n \"devInfoJson\": {\n \"name\": \"Gas Sensor (VOC, eCO2)\",\n \"desc\": \"\",\n \"manu\": \"ScioSense\",\n \"type\": \"ENS160\",\n \"clas\": [\"VOC\",\"CO2\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"r\": [0,255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0x80\": \"S\", \"0x40\": \"E\", \"0x08\": \"\", \"0x04\": \"\", \"0x02\": \"D\", \"0x01\": \"G\"}\n },\n {\n \"n\": \"validity\",\n \"at\": 0,\n \"t\": \"B\",\n \"r\": [0,3],\n \"m\": \"0x0c\",\n \"s\": 2,\n \"f\": \"d\",\n \"o\": \"uint8\",\n \"vl\": {\"0\": \"normal\", \"1\": \"warm-up\", \"2\": \"start-up\", \"3\": \"invalid\"}\n },\n {\n \"n\": \"AQI\",\n \"at\": 1,\n \"t\": \"B\",\n \"u\": \"UBA\",\n \"r\": [1,5],\n \"m\": \"0x07\",\n \"f\": \"d\",\n \"o\": \"uint8\"\n },\n {\n \"n\": \"TVOC\",\n \"at\": 2,\n \"t\": \"I\",\n \"u\": \"N\",\n \"r\": [-10000,10000],\n \"m\": \"0x00ffffff\",\n \"d\": 44000,\n \"sb\": 23,\n \"f\": \".3f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0,1],\n \"vl\": {\"0\": \"no\", \"1\": \"yes\"}\n }\n ]\n }\n }\n}\n,\"LPS25\":{\n \"addresses\": \"0x5C-0x5D\",\n \"detectionValues\": \"0x0f=0b10111101\",\n \"initValues\": \"0x2104=p5&0x20C4=\",\n \"pollInfo\": {\n \"c\": \"0xA7=r6\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"lone 0x99 + 9-byte read (2026-10-23): pointer to 0x19 (reserved), no configuration register changed, pressure unchanged\"}},\n \"devInfoJson\": {\n \"name\": \"Pressure Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"LPS25\",\n \"clas\": [\"PRES\",\"TEMP\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"r\": [0,255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0x01\": \"T_DA\", \"0x02\": \"P_DA\", \"0x10\": \"T_OR\", \"0x20\": \"P_OR\"}\n },\n {\n \"n\": \"pressure\",\n \"at\": 0,\n \"t\": \"6.6 s filled the 4096-word FIFO, the count read 0 and the {$0.w12} poll read nothing forever - devdocs/lsm6ds-fifo-full-deadlock.md). The _conf.rate entries keep the watermark. Gyro 70 mdps/LSB at 2000 dps.\",\n \"addresses\": \"0x6a-0x6b\",\n \"detectionValues\": \"0x0f=0x69\",\n \"initValues\": \"0x0a00&0x1244&0x1048&0x114c&0x1640&0x0809&0x0900&0x1301&0x06fc&0x0703&0x0a26\",\n \"pollInfo\": {\n \"c\": \"0x3a=r4&0x3e=r{$0.w12:mask0FFF*2:max240}\",\n \"i\": 50,\n \"s\": 20\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer & Gyroscope\",\n \"desc\": \"6-Axis IMU\",\n \"manu\": \"ST\",\n \"type\": \"LSM6DS3\",\n \"clas\": [\"ACC\", \"GYRO\"],\n \"resp\": {\n \"b\": 244,\n \"a\": [\n {\n \"n\": \"gx\",\n \"t\": \"maxN){N=maxN;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(i6.6 s filled the 4096-word FIFO, the count read 0 and the {$0.w12} poll read nothing forever - devdocs/lsm6ds-fifo-full-deadlock.md). The _conf.rate entries keep the watermark. Gyro 70 mdps/LSB at 2000 dps.\",\n \"addresses\": \"0x6a-0x6b\",\n \"detectionValues\": \"0x0f=0x6a\",\n \"initValues\": \"0x0a00&0x1244&0x1048&0x114c&0x1640&0x0809&0x0980&0x06fc&0x0703&0x0a26\",\n \"pollInfo\": {\n \"c\": \"0x3a=r4&0x3e=r{$0.w12:mask0FFF*2:max240}\",\n \"i\": 50,\n \"s\": 20\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer & Gyroscope\",\n \"desc\": \"6-Axis IMU\",\n \"manu\": \"ST\",\n \"type\": \"LSM6DS3TRC\",\n \"clas\": [\"ACC\", \"GYRO\"],\n \"resp\": {\n \"b\": 244,\n \"a\": [\n {\n \"n\": \"gx\",\n \"t\": \"maxN){N=maxN;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(iH\",\n \"u\": \"V\",\n \"r\": [0, 5.12],\n \"d\": 12800,\n \"f\": \".3f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"charge\",\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"d\": 256,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"chargeRate\",\n \"t\": \">h\",\n \"u\": \"%/hr\",\n \"r\": [-300, 300],\n \"d\": 4.808,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"at\": 6,\n \"r\": [0, 255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0\": \"OK\", \"0x01\": \"RI\", \"0x02\": \"VH\", \"0x04\": \"VL\", \"0x08\": \"VR\", \"0x10\": \"HD\", \"0x20\": \"SC\", \"0x40\": \"\"}\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"config\",\n \"t\": \">H\",\n \"w\": \"0c\",\n \"f\": \"B\",\n \"r\": [0, 65535],\n \"d\": 38678,\n \"desc\": \"CONFIG register\"\n },\n {\n \"n\": \"reset config\",\n \"w\": \"0c9716\",\n \"desc\": \"Reset CONFIG\"\n }\n ]\n }\n}\n,\"MAX30101\":{\n \"_notes\": \"Maxim MAX30101 / MAX30105 pulse oximetry, heart-rate and particle sensor at 0x57 (fixed). The MAX30102 has the same part ID but no green LED (its green values are meaningless). Detection: PART_ID (0xFF) = 0x15 (all three). Init: reset (0x09 bit 6), 10 ms; MODE_CONFIG (0x09) multi-LED mode (7); FIFO_CONFIG (0x08) 4-sample averaging, rollover on, almost-full 15; INT status 1 read (clears it); SPO2_CONFIG (0x0A) ADC range 4096 nA, 100 samples/s, 411 us pulses (18 bits); LED1-LED3 pulse amplitudes (0x0C-0x0E, red, IR, green) 0x40 = 12.8 mA (0.2 mA per step); slots (0x11-0x12) red, IR, green, none; FIFO pointers cleared (0x04-0x06). Poll: FIFO_WR_PTR, OVF_COUNTER, FIFO_RD_PTR, then 8 samples of 9 bytes from FIFO_DATA (the register pointer stays at 0x07, so one read covers both), 18 bits per LED; then FIFO_RD_PTR again. The decode keeps the samples the read popped (RD_PTR after minus before): a read of an empty FIFO returns the last sample again without moving RD_PTR, and a sample that arrives during the read (about 8 ms) is popped too - counting from the pointers before the read dropped it, about one sample in five at 100/s (bench, 2026-10-24). The rate is the 100 samples/s divided by the on-chip averaging (1-16), each rate polled often enough to keep under 8 samples a poll. In multi-LED mode an LED is turned off by setting its current to 0, so the LED actions also choose what is measured (red only for heart rate, red and IR for SpO2, all three). Bench (Axiom018, 2026-10-24, MAX30105): with nothing in front about 1500 (red) and 2400 (IR), a fingertip about 196000 with a 68 bpm pulse of about 1700 p-p in IR.\",\n \"addresses\": \"0x57\",\n \"detectionValues\": \"0xff=0x15\",\n \"initValues\": \"0x0940=p10&0x0907&0x085f&0x00=r1&0x0a27&0x0c404040&0x112103&0x04000000\",\n \"pollInfo\": {\n \"c\": \"0x04=r75&0x06=r1\",\n \"i\": 200,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\n \"rsao\": {\n \"effect\": \"none\",\n \"basis\": \"bench\",\n \"note\": \"bench (Axiom018, 2026-10-24, MAX30105): the lone 0x99 + 9-byte read (0x99-0xA1, not in the register map) read 99 af ff ff ff ff ff 00 00; FIFO, mode, SpO2, LED, slot and interrupt-enable registers unchanged and sampling carried on\"\n }\n },\n \"devInfoJson\": {\n \"name\": \"Heart & Optical Sensor\",\n \"desc\": \"Red, IR and green reflectance: heart rate, SpO2, proximity and (MAX30105) particles\",\n \"manu\": \"Maxim\",\n \"type\": \"MAX30101\",\n \"clas\": [\n \"HRM\"\n ],\n \"resp\": {\n \"b\": 76,\n \"a\": [\n {\n \"n\": \"Red\",\n \"t\": \">I\",\n \"u\": \"Red\",\n \"r\": [\n 0,\n 262143\n ],\n \"f\": \"6d\",\n \"o\": \"uint32\"\n },\n {\n \"n\": \"IR\",\n \"t\": \">I\",\n \"u\": \"IR\",\n \"r\": [\n 0,\n 262143\n ],\n \"f\": \"6d\",\n \"o\": \"uint32\"\n },\n {\n \"n\": \"Green\",\n \"t\": \">I\",\n \"u\": \"Green\",\n \"r\": [\n 0,\n 262143\n ],\n \"f\": \"6d\",\n \"o\": \"uint32\"\n }\n ],\n \"c\": {\n \"n\": \"max30101_fifo\",\n \"c\": \"int N=(buf[75]+32-buf[2])%32;if(N>8){N=8;}int k=3;int i=0;while(iH\",\n \"u\": \"°C\",\n \"r\": [-40,125],\n \"m\": \"0x1fff\",\n \"d\": 16,\n \"sb\": 12,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MICS6814Pimoroni\":{\n \"_notes\": \"SGX MiCS-6814 gas sensor (reducing gases e.g. CO, NH3, oxidising gases e.g. NO2) on Pimoroni's breakout at 0x19: a Nuvoton MS51 running Pimoroni's IO Expander firmware reads the three sensing elements with its ADC and switches the heater (as Pimoroni's mics6814 and ioexpander Python libraries do). Detection: the IO Expander chip ID (0xFA-0xFB) 0xE26A - shared by Pimoroni's IO Expander boards; 0x19 is the MICS6814's address. Init: read the pin mode registers P0M1, P0M2, P1M1, P1M2 (0x71-0x74) and ADCCON1 (0xA1); initCode computes read-modify-write values (the same registers set the modes of the pins the firmware itself uses): P0.5-P0.7 input-only for the ADC (AIN4, AIN3, AIN2), P1.5 push-pull for the heater enable; then P1.5 low (bit-addressed P1 write 0x05: heater on) and the ADC enabled. Poll: for each of reducing (AIN2), NH3 (AIN3), oxidising (AIN4): select the channel (AINDIDS0 0xB6), start a conversion (ADCCON0 0xA8 = 0x40 | channel), 2 ms, read ADCRL/ADCRH (12 bits: low nibble of 0x82, high 8 bits in 0x83). Decode: each element's resistance R = raw * 56000 / (4095 - raw) ohms (the board's 56k divider, as the library). The heater needs some minutes to warm up before the resistances settle.\",\n \"addresses\": \"0x19\",\n \"detectionValues\": \"0xfa=0x6ae2\",\n \"initValues\": \"0x71=r1&0x72=r1&0x73=r1&0x74=r1&0xa1=r1&0x71{p0m1}&0x72{p0m2}&0x73{p1m1}&0x74{p1m2}&0x5005&0xa1{adccon1}\",\n \"initCode\": {\n \"n\": \"mics6814_pins\",\n \"c\": \"init.p0m1=buf[0]|224;init.p0m2=buf[1]&31;init.p1m1=buf[2]&223;init.p1m2=buf[3]|32;init.adccon1=buf[4]|1;\"\n },\n \"pollInfo\": {\n \"c\": \"0xb604&0xa842=p2&0x82=r2&0xb608&0xa843=p2&0x82=r2&0xb610&0xa844=p2&0x82=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"inferred\", \"note\": \"Pimoroni IO Expander firmware (not public): a one-byte write only sets the pointer; 0x99-0xA1 are PWM and ADC configuration registers with no read side effect (ioe-python register map)\"}},\n \"devInfoJson\": {\n \"name\": \"Gas Sensor\",\n \"desc\": \"Reducing, NH3 and oxidising gases\",\n \"manu\": \"Pimoroni\",\n \"type\": \"MICS6814Pimoroni\",\n \"clas\": [\"GAS\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"reducing\",\n \"t\": \"H\",\n \"u\": \"µT\",\n \"r\": [\n -3000,\n 3000\n ],\n \"d\": 10,\n \"a\": -3276.8,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"y\",\n \"t\": \">H\",\n \"u\": \"µT\",\n \"r\": [\n -3000,\n 3000\n ],\n \"d\": 10,\n \"a\": -3276.8,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"z\",\n \"t\": \">H\",\n \"u\": \"µT\",\n \"r\": [\n -3000,\n 3000\n ],\n \"d\": 10,\n \"a\": -3276.8,\n \"f\": \".1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MPRLS\":{\n \"_notes\": \"Honeywell MPRLS 0025PA (0-25 psi, transfer function A: 10-90% of 2^24 counts). No detection values, so the record claims any device at 0x18; the MCP9808 and Robotical servo/pump records, which share the address and have detection values, are tried first (detection strength order).\",\n \"addresses\": \"0x18\",\n \"detectionValues\": \"\",\n \"initValues\": \"0xAA0000\",\n \"pollInfo\": {\n \"c\": \"=r4&0xAA0000\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Pressure Sensor\",\n \"desc\": \"\",\n \"manu\": \"Honeywell\",\n \"type\": \"MPRLS\",\n \"clas\": [\"PRES\"],\n \"resp\": {\n \"b\": 4,\n \"a\": [\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"r\": [0, 255],\n \"m\": \"0xff\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\"0x01\": \"saturated\", \"0x04\": \"mem error\", \"0x20\": \"busy\", \"0x40\": \"powered\"}\n },\n {\n \"n\": \"pressure\",\n \"at\": 0,\n \"m\": \"0x00ffffff\",\n \"t\": \">I\",\n \"u\": \"hPa\",\n \"r\": [0, 1724],\n \"a\": -215.461,\n \"d\": 7786.654,\n \"f\": \"4.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MS8607PT\":{\n \"_notes\": \"TE MS8607 pressure and temperature part at 0x76 (fixed; the humidity part is MS8607RH at 0x40). Commands, not registers: reset 0x1E, convert pressure 0x40-0x4A / temperature 0x50-0x5A (by oversampling), ADC read 0x00 (3 bytes), PROM read 0xA0-0xAC (7 words: word 0 holds a CRC-4, words 1-6 the coefficients C1-C6). Detection: an ADC read, then a second one, which returns 0 whatever went before (datasheet: a repeated ADC read gives 0) - a pattern rather than an ID (a BME280's register 0x00 also reads 0), so it is tried after the records with an ID (identPriority -1). Init: reset, 5 ms. Poll: pressure conversion at OSR 8192 (0x4A, max 16.4 ms), 20 ms (the address is barred), read; temperature conversion (0x5A), 20 ms, read; C1-C6. Decode: TE's first- and second-order compensation (also right for the MS5637 and MS5837-02BA, which share the maths; not the MS5611 or MS5837-30BA). The PROM CRC-4 isn't checked (the grammar can't). 0x76 is also a common multiplexer address (Axiom018 reserved it until 2026-10-18).\",\n \"addresses\": \"0x76\",\n \"identPriority\": -1,\n \"detectionValues\": \"0x00=r3&0x00=0x000000\",\n \"initValues\": \"0x1e=p5\",\n \"pollInfo\": {\n \"c\": \"0x4a=p20&0x00=r3&0x5a=p20&0x00=r3&0xa2=r2&0xa4=r2&0xa6=r2&0xa8=r2&0xaa=r2&0xac=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Pressure & Temperature Sensor\",\n \"desc\": \"MS8607 pressure part\",\n \"manu\": \"TE\",\n \"type\": \"MS8607PT\",\n \"clas\": [\"PRES\",\"TEMP\"],\n \"resp\": {\n \"b\": 18,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \"=2000.0){t2=5.0*dT*dT/274877906944.0;}t=t-t2;off=off-off2;sens=sens-sens2;float p=(D1*sens/2097152.0-off)/32768.0;out.temperature=t/100.0;out.pressure=p/100.0;\"\n }\n }\n }\n}\n,\"MS8607RH\":{\n \"_notes\": \"TE MS8607 humidity part at 0x40 (fixed; the pressure and temperature part is MS8607PT at 0x76). HTU21-style commands: reset 0xFE, measure RH without clock stretching 0xF5 (with stretching, 0xE5, holds the clock longer than the bus timeout), user register read 0xE7. Detection: the user register's fixed bits (reserved bits 5:3 = 0, bit 1 = 1 by default) - weak, so it is tried after the records with an ID (identPriority -1); 0x40 is also used by parts with no record (INA219, PCA9685). Init: reset, 15 ms. Poll: measure (0xF5), 20 ms (max 16 ms at 12 bits; the pause bars the address, as an address probe between trigger and read loses the result), read 3 bytes (RH, CRC with init 0x00 - not checked). RH = -6 + 125*raw/2^16 (status bits 1:0 masked), not temperature-compensated (the MS8607 compensation uses the temperature from the other part: RH + (20 - T) * -0.18).\",\n \"addresses\": \"0x40\",\n \"identPriority\": -1,\n \"detectionValues\": \"0xe7=0bX000XX1X\",\n \"initValues\": \"0xfe=p15\",\n \"pollInfo\": {\n \"c\": \"0xf5=p20&=r3\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Humidity Sensor\",\n \"desc\": \"MS8607 humidity part\",\n \"manu\": \"TE\",\n \"type\": \"MS8607RH\",\n \"clas\": [\"RH\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"humidity\",\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"m\": \"0xfffc\",\n \"d\": 524.288,\n \"a\": -6,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"MT6701\":{\n \"addresses\": \"0x06\",\n \"detectionValues\": \"\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x03=r2\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Magnetic Encoder\",\n \"desc\": \"\",\n \"manu\": \"MagnTek\",\n \"type\": \"MT6701\",\n \"clas\": [\"ANG\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"angle\",\n \"t\": \">H\",\n \"u\": \"degrees\",\n \"r\": [0, 360],\n \"f\": \"3.1f\",\n \"o\": \"float\",\n \"d\": 182.04444444444444\n }\n ]\n }\n }\n}\n,\"MXC400xXC\":{\n \"_notes\": \"x in part code sets i2c address LSB from 0x10-0x17\",\n \"addresses\": \"0x10-0x17\",\n \"detectionValues\": \"0x0E=0x02,0x03\",\n \"initValues\": \"0x0110=p5&0x0d00=\",\n \"pollInfo\": {\n \"c\": \"0x03=r7\",\n \"i\": 100,\n \"s\": 3\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer\",\n \"desc\": \"3-Axis Accel\",\n \"manu\": \"Memsic\",\n \"type\": \"MXC400xXC\",\n \"clas\": [\"ACC\"],\n \"resp\": {\n \"b\": 7,\n \"a\": [\n {\n \"n\": \"ax\",\n \"t\": \">h\",\n \"u\": \"g\",\n \"r\": [-2.0, 2.0],\n \"d\": 16384,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"ay\",\n \"t\": \">h\",\n \"u\": \"g\",\n \"r\": [-2.0, 2.0],\n \"d\": 16384,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"az\",\n \"t\": \">h\",\n \"u\": \"g\",\n \"r\": [-2.0, 2.0],\n \"d\": 16384,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"temperature\",\n \"t\": \"b\",\n \"u\": \"°C\",\n \"r\": [-40, 85],\n \"d\": 1.7065,\n \"a\": 25,\n \"f\": \".2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"PCT2075\":{\n \"_notes\": \"NXP PCT2075, LM75-compatible temperature sensor, 0.125 C, at any of 27 addresses (A0-A2 tri-state): 0x28-0x2F, 0x35-0x37 (Adafruit default 0x37), 0x48-0x4F, 0x70-0x77 - of which 0x70-0x73 are left out, as GravityO2 (no detection) claims them first. No ID register, so detection checks the registers' fixed bits (read-only): temperature low 5 bits 0 (11-bit, left-justified), configuration bits 7-5 0, Thyst and Tos low 7 bits 0 (9-bit), Tidle bits 7-5 0, and pointer 0x07 (no register) reads 0xFFFF - a part whose low registers read 0 (e.g. a BME280, which matched all the bit patterns) or a TMP117 (offset register 0x0000) fails that. Being a pattern rather than an ID, it is tried after the records with an ID at the same address (identPriority -1) - a TMP117 or an idle CAP1188 can pass it. Init: configuration 0 (normal mode, in case it was left shut down). Poll: temperature register; signed 16-bit / 256 (the low 5 bits are 0).\",\n \"addresses\": \"0x28-0x2F,0x35-0x37,0x48-0x4F,0x74-0x77\",\n \"identPriority\": -1,\n \"detectionValues\": \"0x00=0bXXXXXXXXXXX00000&0x01=0b000XXXXX&0x02=0bXXXXXXXXX0000000&0x03=0bXXXXXXXXX0000000&0x04=0b000XXXXX&0x07=0xFFFF\",\n \"initValues\": \"0x0100=\",\n \"pollInfo\": {\n \"c\": \"0x00=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature Sensor\",\n \"desc\": \"\",\n \"manu\": \"NXP\",\n \"type\": \"PCT2075\",\n \"clas\": [\"TEMP\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">h\",\n \"u\": \"°C\",\n \"r\": [-55, 125],\n \"d\": 256,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"QwiicButton\":{\n \"_notes\": \"SparkFun Qwiic Button (ATtiny84 with SparkFun firmware) at 0x6F by default (the address can be changed in software). The LED is a single colour (red or green, depending on the board), so it has brightness and pulse settings, not a colour. Every LED and debounce setting is saved in the button's EEPROM, so it survives a power cycle (a new button: brightness 255, no pulsing, debounce 10 ms). The pulse step must not be 0: the firmware divides by it. Poll: the status register (0x03), bit 2 = pressed; its clicked and event bits stay set until written back, so they aren't decoded.\",\n \"addresses\": \"0x6f\",\n \"detectionValues\": \"0x00=0b01011101\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x03=r1\",\n \"i\": 100,\n \"s\": 5\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"changes\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, button on a mux slot, 2026-10-23): the lone 0x99 + 9-byte read fails with an I2C hardware timeout and the button drops off the bus for about 1.1 s, then is found and identified again; devices on other slots and the main bus are unaffected (the mux isolates the slot, the master times out after about 1.6 ms). Source: the read handler doesn't check the register pointer, so from 0x99 the length wraps to 65415 and the handler loops inside the I2C interrupt, holding the clock (Qwiic_Button e89a82f interrupts.ino 76-91). No setting changes. Its default address 0x6F is excluded from RSAO addressing\"}},\n \"devInfoJson\": {\n \"name\": \"QWIIC Button\",\n \"desc\": \"\",\n \"manu\": \"Sparkfun\",\n \"type\": \"QwiicButton\",\n \"clas\": [\"BTN\"],\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"press\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"LED\",\n \"desc\": \"LED presets: off, on (full brightness, steady) or breathe (full brightness, 1 s pulse, 0.5 s off)\",\n \"map\": {\n \"off\": {\"w\": \"1900&1b0000\"},\n \"on\": {\"w\": \"19ff&1b0000\"},\n \"breathe\": {\"w\": \"19ff&1a01&1be803&1df401\"}\n }\n },\n {\n \"n\": \"LED brightness\",\n \"desc\": \"LED brightness, 0-255 (when pulsing, the peak of each pulse)\",\n \"t\": \"B\",\n \"w\": \"19\",\n \"r\": [0,255],\n \"d\": 255\n },\n {\n \"n\": \"LED pulse step\",\n \"desc\": \"Brightness change at each step of a pulse, 1-255 (1 is smoothest)\",\n \"t\": \"B\",\n \"w\": \"1a\",\n \"r\": [1,255],\n \"d\": 1\n },\n {\n \"n\": \"LED pulse time\",\n \"desc\": \"Length of one pulse (ramp up and down) in ms; 0 = no pulsing, the LED stays at its brightness\",\n \"t\": \">7)&255;float co2=0.0;if(co2e>0){if(co2e<255){co2=1.0+(((co2h&127)*65536)+co2l)/8388608.0;int co2k=co2e-127;while(co2k>0){co2=co2*2.0;co2k=co2k-1;}while(co2k<0){co2=co2/2.0;co2k=co2k+1;}}}if(co2h>32767){co2=0.0-co2;}out.CO2=co2;int tmph=(buf[12]<<8)|buf[13];int tmpl=(buf[15]<<8)|buf[16];int tmpe=(tmph>>7)&255;float tmp=0.0;if(tmpe>0){if(tmpe<255){tmp=1.0+(((tmph&127)*65536)+tmpl)/8388608.0;int tmpk=tmpe-127;while(tmpk>0){tmp=tmp*2.0;tmpk=tmpk-1;}while(tmpk<0){tmp=tmp/2.0;tmpk=tmpk+1;}}}if(tmph>32767){tmp=0.0-tmp;}out.temperature=tmp;int humh=(buf[18]<<8)|buf[19];int huml=(buf[21]<<8)|buf[22];int hume=(humh>>7)&255;float hum=0.0;if(hume>0){if(hume<255){hum=1.0+(((humh&127)*65536)+huml)/8388608.0;int humk=hume-127;while(humk>0){hum=hum*2.0;humk=humk-1;}while(humk<0){hum=hum/2.0;humk=humk+1;}}}if(humh>32767){hum=0.0-hum;}out.humidity=hum;next;}\"\n }\n },\n \"actions\": [\n {\n \"n\": \"_conf.rate\",\n \"r\": [0.0167, 0.333],\n \"d\": 0.333,\n \"desc\": \"Sample rate (Hz)\",\n \"map\": {\n \"0.333\": {\n \"i\": 3000000,\n \"s\": 1\n },\n \"0.2\": {\n \"i\": 5000000,\n \"s\": 1\n },\n \"0.1\": {\n \"i\": 10000000,\n \"s\": 1\n },\n \"0.0333\": {\n \"i\": 30000000,\n \"s\": 1\n },\n \"0.0167\": {\n \"i\": 60000000,\n \"s\": 1\n }\n }\n },\n {\n \"n\": \"autoCal\",\n \"t\": \">H\",\n \"w\": \"5306\",\n \"crc\": \"crc8w\",\n \"r\": [0, 1],\n \"d\": 0,\n \"desc\": \"Auto-calibration\"\n },\n {\n \"n\": \"forceCal\",\n \"t\": \">H\",\n \"w\": \"5204\",\n \"crc\": \"crc8w\",\n \"r\": [400, 2000],\n \"d\": 420,\n \"desc\": \"Force cal (ppm)\"\n },\n {\n \"n\": \"tempOffset\",\n \"t\": \">H\",\n \"w\": \"5403\",\n \"crc\": \"crc8w\",\n \"mul\": 100,\n \"r\": [0, 20],\n \"d\": 0,\n \"desc\": \"Temp offset (C)\"\n },\n {\n \"n\": \"altitude\",\n \"t\": \">H\",\n \"w\": \"5102\",\n \"crc\": \"crc8w\",\n \"r\": [0, 3000],\n \"d\": 0,\n \"desc\": \"Altitude (m)\"\n },\n {\n \"n\": \"pressure\",\n \"t\": \">H\",\n \"w\": \"0010\",\n \"crc\": \"crc8w\",\n \"r\": [0, 1400],\n \"d\": 0,\n \"desc\": \"Pressure (mbar)\"\n },\n {\n \"n\": \"softReset\",\n \"w\": \"d304\",\n \"desc\": \"Soft reset\"\n }\n ]\n }\n}\n,\"SCD40\":{\n \"_notes\": \"Sensirion SCD40 CO2/temperature/humidity. No detection values (unique address 0x62). read_measurement (0xEC05) returns three big-endian words, each followed by a CRC byte: CO2 (ppm) at 0, T at 3 (-45 + 175*raw/65535), RH at 6 (100*raw/65535). Standard decode. temperatureOffset, altitude, autoCalibration and calibrationTarget are only accepted while periodic measurement is stopped; the record keeps it running, so at present the sensor ignores them. ambientPressure (hPa) overrides the altitude and is accepted during measurement.\",\n \"addresses\": \"0x62\",\n \"detectionValues\": \"\",\n \"initValues\": \"0x21b1=\",\n \"pollInfo\": {\n \"c\": \"0xec05=r9\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"CO2 Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SCD40\",\n \"clas\": [\"CO2\", \"TEMP\", \"RH\"],\n \"resp\": {\n \"b\": 9,\n \"a\": [\n {\n \"n\": \"CO2\",\n \"t\": \">H\",\n \"u\": \"ppm\",\n \"r\": [0, 40000],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"temperature\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-45, 175],\n \"d\": 374.4857142857143,\n \"a\": -45,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 6,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"d\": 655.35,\n \"f\": \".2f\",\n \"o\": \"float\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"temperatureOffset\",\n \"t\": \">H\",\n \"w\": \"241d\",\n \"crc\": \"crc8w\",\n \"mul\": 374.4857,\n \"r\": [0, 20],\n \"d\": 4,\n \"desc\": \"Temp offset (C)\"\n },\n {\n \"n\": \"altitude\",\n \"t\": \">H\",\n \"w\": \"2427\",\n \"crc\": \"crc8w\",\n \"r\": [0, 3000],\n \"d\": 0,\n \"desc\": \"Altitude (m)\"\n },\n {\n \"n\": \"ambientPressure\",\n \"t\": \">H\",\n \"w\": \"e000\",\n \"crc\": \"crc8w\",\n \"r\": [700, 1200],\n \"d\": 1013,\n \"desc\": \"Pressure (hPa)\"\n },\n {\n \"n\": \"autoCalibration\",\n \"t\": \">H\",\n \"w\": \"2416\",\n \"crc\": \"crc8w\",\n \"r\": [0, 1],\n \"d\": 1,\n \"desc\": \"Auto-calibration\"\n },\n {\n \"n\": \"calibrationTarget\",\n \"t\": \">H\",\n \"w\": \"243a\",\n \"crc\": \"crc8w\",\n \"r\": [0, 65535],\n \"d\": 400,\n \"desc\": \"ASC target (ppm)\"\n }\n ]\n }\n}\n,\"SGP30\":{\n \"_notes\": \"Sensirion SGP30 indoor air quality (eCO2 and TVOC) at 0x58 (fixed). Detection: get_feature_set (0x202F, 10 ms) - one word with its Sensirion CRC-8 (:crc8w) whose top 4 bits are the product type, 0 for the SGP30. Init: iaq_init (0x2003, 10 ms). Poll (1 s - measure_iaq must be called once a second for the chip's own dynamic baseline compensation): measure_iaq (0x2008), wait 12 ms, read eCO2 (ppm) and TVOC (ppb), each word followed by a CRC (not checked). For the first 15 s after iaq_init it reports 400 ppm and 0 ppb. setBaseline restores the IAQ baseline: TVOC baseline, then eCO2 baseline (the reverse of the order get_iaq_baseline returns them; the record has no action to read the baseline). setHumidity: absolute humidity for compensation, 8.8 fixed point in g/m3, 0 turns it off.\",\n \"addresses\": \"0x58\",\n \"detectionValues\": \"0x202f=p10&=0b0000XXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"0x2003=p10\",\n \"pollInfo\": {\n \"c\": \"0x2008=p12&=r6\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the lone 0x99 is NACKed (half a 16-bit command); the serial number, feature set and measurements are unchanged\"}},\n \"devInfoJson\": {\n \"name\": \"Gas Sensor (VOC)\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SGP30\",\n \"clas\": [\"VOC\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"eCO2\",\n \"t\": \">H\",\n \"u\": \"ppm\",\n \"r\": [400, 60000],\n \"f\": \"5d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"VOC\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"ppb\",\n \"r\": [0, 60000],\n \"f\": \"5d\",\n \"o\": \"uint16\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"setBaseline\",\n \"t\": \">HH\",\n \"w\": \"201e\",\n \"crc\": \"crc8w\",\n \"r\": [0, 65535],\n \"d\": 0,\n \"desc\": \"Set baseline\"\n },\n {\n \"n\": \"setHumidity\",\n \"t\": \">H\",\n \"w\": \"2061\",\n \"crc\": \"crc8w\",\n \"mul\": 256,\n \"r\": [0, 255],\n \"d\": 0,\n \"desc\": \"Humidity (g/m3)\"\n }\n ]\n }\n}\n,\"SGP40\":{\n \"_notes\": \"Sensirion SGP40 VOC sensor at 0x59 (fixed). Detection: get_serial_number (0x3682, 1 ms) - three data words, each with a Sensirion CRC-8 (:crc8w). Init: start a raw measurement - measure_raw_signal (0x260F) with the default compensation 50 %RH (0x8000, CRC 0xA2) and 25 degC (0x6666, CRC 0x93); the result is ready after 30 ms. Poll (1 s, the rate Sensirion's VOC algorithm expects): read the previous measurement (SRAW_VOC word + its CRC, which isn't checked), then start the next, so no poll waits for the measurement. The value is the raw signal in ticks: the VOC index (0-500) needs Sensirion's gas index algorithm, run by the host (e.g. the app). Compensation stays at the defaults: the record can't feed in another sensor's humidity and temperature.\",\n \"addresses\": \"0x59\",\n \"detectionValues\": \"0x3682=p1&=0bXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"0x260f8000a2666693\",\n \"pollInfo\": {\n \"c\": \"=r3&0x260f8000a2666693=\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"bench\", \"note\": \"bench (Axiom018, 2026-10-23): the lone 0x99 is NACKed (half a 16-bit command), so nothing is sent on; the serial number and the measurement are unchanged\"}},\n \"devInfoJson\": {\n \"name\": \"SGP40\",\n \"desc\": \"Gas Sensor\",\n \"manu\": \"Sensirion\",\n \"type\": \"SGP40\",\n \"clas\": [\"VOC\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"vocTicks\",\n \"t\": \">H\",\n \"u\": \"ticks\",\n \"r\": [0, 65535],\n \"f\": \"5d\",\n \"o\": \"uint16\"\n }\n ]\n }\n }\n}\n,\"SHT31\":{\n \"_notes\": \"Sensirion SHT3x (SHT30, SHT31, SHT35 - same protocol) at 0x44 (ADDR low) or 0x45 (ADDR high), the SHT4x addresses: 2-byte commands, where the SHT4x has 1-byte ones, so each record's detection fails on the other part. Detection: read the status register (0xF32D) and require a valid CRC - only the CRC: the datasheet's reserved status bits aren't reliably 0 (bits 5 and 6 read 1 in periodic mode, status 0x8070), and the read works in both single-shot and periodic mode. Init: break (0x3093) in case periodic measurement was left running, which would make single-shot commands fail. Poll: single-shot, high repeatability, no clock stretching (0x2400), 16 ms (max 15.5 ms), then read 6 bytes: T_MSB,T_LSB,T_CRC,RH_MSB,RH_LSB,RH_CRC. The sensor NACKs while measuring; the pause after the trigger bars the address, so a detection starting then is retried rather than failing, and the sensor is idle between polls.\",\n \"addresses\": \"0x44-0x45\",\n \"detectionValues\": \"0xf32d=p1&=0bXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"0x3093=p2\",\n \"pollInfo\": {\n \"c\": \"0x2400=p16&=r6\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SHT31\",\n \"clas\": [\"TEMP\",\"RH\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-40,125],\n \"d\": 374.48571428571427,\n \"a\": -45,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"d\": 655.35,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"SHT40\":{\n \"_notes\": \"Sensirion SHT4x (SHT40, SHT41, SHT45 - indistinguishable over I2C) at 0x44-0x46. Detection: read the serial number (0x89, 10 ms) and require valid CRCs on both words. Poll: high-precision single-shot measurement (0xFD), 10 ms, then read 6 bytes: T_MSB,T_LSB,T_CRC,RH_MSB,RH_LSB,RH_CRC. The sensor NACKs while measuring (8.3 ms); the pause after the trigger bars the address, so a detection starting then is retried rather than failing, and the sensor is idle between polls.\",\n \"addresses\": \"0x44-0x46\",\n \"detectionValues\": \"0x89=p10&=0bXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:crc8w\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0xfd=p10&=r6\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SHT40\",\n \"clas\": [\"TEMP\",\"RH\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-45,130],\n \"d\": 374.48571428571427,\n \"a\": -45,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"d\": 524.28,\n \"a\": -6,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"SHTC3\":{\n \"_notes\": \"Sensirion SHTC3 at 0x70 (fixed - also GravityO2's address and a common multiplexer address). Detection: wake-up (0x3517, harmless when awake, needed if left asleep), then the ID register (0xEFC8): bit 11 = 1 and bits 5-0 = 000111, with a valid CRC. Init: wake-up. Poll: measure, normal mode, T first, no clock stretching (0x7866), 13 ms (max 12.1 ms; the pause bars the address), then read 6 bytes: T_MSB,T_LSB,T_CRC,RH_MSB,RH_LSB,RH_CRC. T = -45 + 175*raw/2^16, RH = 100*raw/2^16.\",\n \"addresses\": \"0x70\",\n \"detectionValues\": \"0x3517=p1&0xefc8=p1&=0bXXXX1XXXXX000111XXXXXXXX:crc8w\",\n \"initValues\": \"0x3517=p1\",\n \"pollInfo\": {\n \"c\": \"0x7866=p13&=r6\",\n \"i\": 5000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Sensirion\",\n \"type\": \"SHTC3\",\n \"clas\": [\"TEMP\",\"RH\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-40,125],\n \"d\": 374.4914285714286,\n \"a\": -45,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0,100],\n \"d\": 655.36,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"STCC4\":{\n \"_notes\": \"Sensirion STCC4 miniature CO2 sensor (paired with SHT41 for RH/T compensation), fixed address 0x64. Detection is empty (unique address) - the product-ID command cannot be used because the detection path issues a single write+read with no inter-command delay and the STCC4 NACKs that read - so the record claims any device at 0x64. Init: start_continuous_measurement (0x218B). Poll every 1.5 s (longer than the 1 s measurement, so the read always has new data; at 1 s the two drift and a read is NACKed now and then): read_measurement (0xEC05), 1 ms pause, read 12 bytes = CO2/temp/humidity/status words each followed by a CRC byte (skipped via 'at'). CO2 is signed (i16 in the datasheet). Status: only bit 0x0040 (testing mode) is documented; after power-up 0x0100 was seen for about 2 minutes. Actions: testingMode (pauses ASC), pressure (hPa). Forced recalibration isn't an action: it needs measurement stopped (1.2 s), then FRC, then a restart. Datasheet: https://sensirion.com/media/documents/6AED4B15/69295E41/CD_DS_STCC4_D1.pdf. Rates (_conf.rate) only set the poll interval, 1.5 s to 60 s. CO2 raw = ppm; temperature = -45 + 175*s/65535; humidity = -6 + 125*s/65535. Must never be probed by the RSAO delegated-identification hook (its framed 0x99 command stops the sensor measuring); RaftI2C DeviceIdentMgr only runs that hook when no device-type record matches - this record's empty detection always matches, so the hook is never reached at 0x64. See devdocs/stcc4-identification-notes.md testingMode: 1 enable_testing_mode (0x3FBC), 0 disable_testing_mode (0x3F3D); ASC is always on otherwise. pressure: sent as Pa/2, reset to 1013 hPa at power-up.\",\n \"addresses\": \"0x64-0x65\",\n \"detectionValues\": \"\",\n \"initValues\": \"0x218b=\",\n \"pollInfo\": {\n \"c\": \"0xec05=p1&=r12\",\n \"i\": 1500,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"CO2 Sensor\",\n \"desc\": \"CO2, Temp & Humidity\",\n \"manu\": \"Sensirion\",\n \"type\": \"STCC4\",\n \"clas\": [\"CO2\", \"TEMP\", \"RH\"],\n \"resp\": {\n \"b\": 12,\n \"a\": [\n {\n \"n\": \"CO2\",\n \"at\": 0,\n \"t\": \">h\",\n \"u\": \"ppm\",\n \"r\": [0, 32000],\n \"f\": \"d\",\n \"o\": \"int16\"\n },\n {\n \"n\": \"temperature\",\n \"at\": 3,\n \"t\": \">H\",\n \"u\": \"°C\",\n \"r\": [-45, 130],\n \"d\": 374.48571428571427,\n \"a\": -45,\n \"f\": \".2f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"humidity\",\n \"at\": 6,\n \"t\": \">H\",\n \"u\": \"%\",\n \"r\": [0, 100],\n \"d\": 524.28,\n \"a\": -6,\n \"f\": \".1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"status\",\n \"at\": 9,\n \"t\": \">H\",\n \"u\": \"\",\n \"r\": [0, 65535],\n \"f\": \"04x\",\n \"o\": \"uint16\",\n \"vs\": false,\n \"bl\": {\"0\": \"OK\", \"0x0040\": \"testing\"}\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"_conf.rate\",\n \"r\": [0.0167, 0.667],\n \"d\": 0.667,\n \"desc\": \"Sample rate (Hz)\",\n \"map\": {\n \"0.667\": {\"i\": 1500000, \"s\": 1},\n \"0.5\": {\"i\": 2000000, \"s\": 1},\n \"0.2\": {\"i\": 5000000, \"s\": 1},\n \"0.1\": {\"i\": 10000000, \"s\": 1},\n \"0.0333\": {\"i\": 30000000, \"s\": 1},\n \"0.0167\": {\"i\": 60000000, \"s\": 1}\n }\n },\n {\n \"n\": \"testingMode\",\n \"map\": {\"0\": \"3f3d\", \"1\": \"3fbc\"},\n \"desc\": \"Testing mode\"\n },\n {\n \"n\": \"pressure\",\n \"t\": \">H\",\n \"w\": \"e016\",\n \"crc\": \"crc8w\",\n \"mul\": 50,\n \"r\": [400, 1100],\n \"d\": 1013,\n \"desc\": \"Pressure (hPa)\"\n }\n ]\n }\n}\n,\"TMP117\":{\n \"_notes\": \"High-Accuracy Temperature Sensor. Detection: 0x0F (DevID) = 0x0117. Init: 0x01 (Config) = 0x0220 (1s continuous mode, 8 avg). Poll: 0x00 (Temp) = 2 bytes.\",\n \"addresses\": \"0x48-0x4B\",\n \"detectionValues\": \"0x0F=0x0117\",\n \"initValues\": \"0x010220=\",\n \"pollInfo\": {\n \"c\": \"0x00=r2\",\n \"i\": 1000,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature Sensor\",\n \"desc\": \"\",\n \"manu\": \"Texas Instruments\",\n \"type\": \"TMP117\",\n \"clas\": [\"TEMP\"],\n \"resp\": {\n \"b\": 2,\n \"a\": [\n {\n \"n\": \"temperature\",\n \"t\": \">h\",\n \"u\": \"°C\",\n \"r\": [-55, 150],\n \"d\": 128,\n \"a\": 0,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n}\n,\"TrillBar\":{\n \"_notes\": \"Bela Trill Bar - 1D capacitive touch, 26 channels, up to 5 simultaneous touches. CENTROID mode. Identify cmd returns device_type=1 in byte 1 of 3-byte response. Address jumper-configurable 0x20-0x27.\",\n \"addresses\": \"0x20-0x27\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000001\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r20\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Bar\",\n \"desc\": \"Capacitive Touch Bar\",\n \"manu\": \"Bela\",\n \"type\": \"TrillBar\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 20,\n \"a\": [\n { \"n\": \"loc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillCraft\":{\n \"_notes\": \"Bela Trill Craft - 1D capacitive sensor strip, 30 channels, up to 5 simultaneous touches in CENTROID mode (default DIFF). Address jumper-configurable 0x30-0x37.\",\n \"addresses\": \"0x30-0x37\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000011\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r20\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Craft\",\n \"desc\": \"Capacitive Touch Craft\",\n \"manu\": \"Bela\",\n \"type\": \"TrillCraft\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 20,\n \"a\": [\n { \"n\": \"loc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillFlex\":{\n \"_notes\": \"Bela Trill Flex - flexible 1D capacitive touch with custom electrode connection, 30 channels, up to 5 touches in CENTROID mode (default DIFF). Address jumper-configurable 0x48-0x4F. Init, as Bela's Trill library (Trill-Linux) for the Flex: CENTROID mode; prescaler 4; scan settings speed 0, 12 bits; a baseline update; noise threshold 123 (0.03 at 12 bits). Poll: up to 5 touches, positions (loc, 0 to 3712 = 29 x 128, the first to the 30th channel; 65535 for no touch) then sizes (size, 0 for no touch; the library scales by 1200 at 12 bits), as 5-element arrays; the dashboard shows them with its touch strip visualizer (vt touchstrip). The prescaler matters: on the bench (Axiom018, 2026-10-23, with the flexible strip) the chip as left after the mode command read 4095 (saturated) on every channel, so no touch was ever seen; prescaler 2 read about 2100-2750 untouched with a finger saturating, 4 about 540-710 with a finger peaking at 1200-1950, and 8 about 0. Bela's library uses DIFF mode for the Flex and finds centroids on the host.\",\n \"addresses\": \"0x48-0x4F\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000110\",\n \"initValues\": \"0x000100=p15&0x000304=p15&0x0002000c=p15&0x0006=p30&0x00047b=p15\",\n \"pollInfo\": {\n \"c\": \"0x04=r20\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Flex\",\n \"desc\": \"Capacitive Touch Flex\",\n \"manu\": \"Bela\",\n \"type\": \"TrillFlex\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 20,\n \"a\": [\n {\n \"n\": \"loc\",\n \"t\": \">H[5]\",\n \"r\": [0, 3712],\n \"f\": \"d\",\n \"o\": \"uint16\",\n \"vt\": \"touchstrip\",\n \"vo\": {\"sizeAttr\": \"size\", \"noValue\": 65535, \"posMax\": 3712, \"sizeMax\": 1200, \"numChannels\": 30}\n },\n {\n \"n\": \"size\",\n \"t\": \">H[5]\",\n \"r\": [0, 65535],\n \"f\": \"d\",\n \"o\": \"uint16\"\n }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 123, \"desc\": \"Noise threshold (counts at 12 bits; 123 is the Trill library's 0.03)\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [1, 8], \"d\": 4, \"desc\": \"Prescaler (higher reads lower; lower it for more sensitivity, raise it if channels saturate at 4095)\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillHex\":{\n \"_notes\": \"Bela Trill Hex - 2D hexagonal capacitive touch pad, 30 channels, up to 4 simultaneous touches. CENTROID response: 4 vertical loc + 4 vertical size + 4 horizontal loc + 4 horizontal size (uint16 BE). Address jumper-configurable 0x40-0x47.\",\n \"addresses\": \"0x40-0x47\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000101\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r32\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Hex\",\n \"desc\": \"Capacitive Touch Hex\",\n \"manu\": \"Bela\",\n \"type\": \"TrillHex\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 32,\n \"a\": [\n { \"n\": \"vloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillRing\":{\n \"_notes\": \"Bela Trill Ring - 1D circular capacitive touch, 28 ring channels + 2 button channels, up to 5 touches. CENTROID response: 5 loc + 5 size + 2 button values (uint16 BE). Address jumper-configurable 0x38-0x3F.\",\n \"addresses\": \"0x38-0x3F\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000100\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r24\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Ring\",\n \"desc\": \"Capacitive Touch Ring\",\n \"manu\": \"Bela\",\n \"type\": \"TrillRing\",\n \"clas\": [\"TCH\", \"BTN\"],\n \"resp\": {\n \"b\": 24,\n \"a\": [\n { \"n\": \"loc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"loc4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"size4\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"btn0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"btn1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"TrillSquare\":{\n \"_notes\": \"Bela Trill Square - 2D capacitive touch, 30 channels, up to 4 simultaneous touches. CENTROID mode returns 4 vertical loc+size pairs followed by 4 horizontal loc+size pairs. Address jumper-configurable 0x28-0x2F.\",\n \"addresses\": \"0x28-0x2F\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000010\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r32\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\"rsao\": {\"effect\": \"none\", \"basis\": \"source\", \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"}},\n \"devInfoJson\": {\n \"name\": \"Trill Square\",\n \"desc\": \"Capacitive Touch Square\",\n \"manu\": \"Bela\",\n \"type\": \"TrillSquare\",\n \"clas\": [\"TCH\"],\n \"resp\": {\n \"b\": 32,\n \"a\": [\n { \"n\": \"vloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"vsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hloc3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize0\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize1\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize2\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" },\n { \"n\": \"hsize3\", \"t\": \">H\", \"r\": [0, 65535], \"f\": \"d\", \"o\": \"uint16\" }\n ]\n },\n \"actions\": [\n { \"n\": \"baseline\", \"w\": \"0006\", \"desc\": \"Update baseline\" },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [0, 3],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": { \"w\": \"000100\" },\n \"raw\": { \"w\": \"000101\" },\n \"baseline\": { \"w\": \"000102\" },\n \"diff\": { \"w\": \"000103\" }\n }\n },\n { \"n\": \"_conf.noise\", \"t\": \"B\", \"w\": \"0004\", \"r\": [0, 255], \"d\": 0, \"desc\": \"Noise threshold\" },\n { \"n\": \"_conf.prescaler\", \"t\": \"B\", \"w\": \"0003\", \"r\": [0, 8], \"d\": 2, \"desc\": \"Prescaler\" },\n { \"n\": \"_conf.minsize\", \"t\": \">H\", \"w\": \"0007\", \"r\": [0, 65535], \"d\": 0, \"desc\": \"Minimum touch size\" }\n ]\n }\n}\n,\"VCNL4040\":{\n \"addresses\": \"0x60\",\n \"detectionValues\": \"0x0c=0b100001100000XXXX\",\n \"initValues\": \"0x041007=&0x030e08=&0x000000=\",\n \"pollInfo\": {\n \"c\": \"0x08=r2&0x09=r2&0x0a=r2\",\n \"i\": 200,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Light Sensor (ambient & proximity)\",\n \"desc\": \"\",\n \"manu\": \"Vishay\",\n \"type\": \"VCNL4040\",\n \"clas\": [\"PROX\",\"LGHT\"],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"prox\",\n \"t\": \"H\",\n \"u\": \"mm\",\n \"r\": [0, 2000],\n \"f\": \"4d\",\n \"o\": \"uint16\",\n \"vft\": \"valid\"\n }\n ]\n }\n }\n}\n,\"VL53L1X\":{\n \"_notes\": \"ToF distance sensor with extended range up to 4m\",\n \"addresses\": \"0x29\",\n \"identPriority\": 1,\n \"detectionValues\": \"0x010f=0xEACC\",\n \"initValues\": \"0x002D01&0x0087&0x25012c00=p100&0x0060=p100\",\n \"pollInfo\": {\n \"c\": \"0x0089=r1&0x0096=r2\",\n \"i\": 200,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Distance Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"VL53L1X\",\n \"clas\": [\"DIST\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"x\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false\n },\n {\n \"n\": \"dist\",\n \"t\": \">H\",\n \"u\": \"mm\",\n \"r\": [0, 4000],\n \"f\": \"4d\",\n \"o\": \"uint16\",\n \"vft\": \"valid\"\n }\n ]\n }\n }\n}\n,\"VL53L4CD\":{\n \"_notes\": \"ToF distance sensor with range 0-1.3m\",\n \"addresses\": \"0x29\",\n \"identPriority\": 1,\n \"detectionValues\": \"0x010F=0xEBAA\",\n \"initValues\": \"0x002D0001011102000208000810010100000000FF000F0000000000200B000002142100&0x004e000500000000c8000038ff010008000001cc0701f10500a0008008380000&006c00000f89000000000000000107050606000002c7ff9b000000010000&0x008740=p200&0x008601&0x008780&0x000809&0x000b00&0x00240500&0x006c00000000&0x005e02f2&0x006103a1&008721\",\n \"pollInfo\": {\n \"c\": \"0x008601=&0x0089=r1&0x0096=r2\",\n \"i\": 100,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Distance Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"VL53L4CD\",\n \"clas\": [\"DIST\"],\n \"resp\": {\n \"b\": 3,\n \"a\": [\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [0, 1],\n \"m\": \"0x04\",\n \"x\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false\n },\n {\n \"n\": \"dist\",\n \"t\": \">H\",\n \"u\": \"mm\",\n \"r\": [0, 1000],\n \"f\": \"3d\",\n \"o\": \"uint16\",\n \"vft\": \"valid\"\n }\n ]\n }\n }\n}\n,\"VL6180\":{\n \"_notes\": \"VL6180X ToF proximity sensor with 1x range scaling (RANGE_SCALER 0x0096/0x0097=>0x00fd), so the range is an 8-bit value in mm (0-255). Specified range 0-100mm, typically up to ~200mm with a reflective target and low ambient light\\nInit: write 0x0018=>0x01 then wait 100ms, which stops continuous ranging left running by a previous init (the sensor may not have been power cycled) or otherwise does one harmless single-shot range; AN4545 private settings https://www.st.com/resource/en/application_note/an4545-vl6180x-basic-ranging-application-note-stmicroelectronics.pdf; clear SYSTEM__FRESH_OUT_OF_RESET (0x0016=>0x00); GPIO1 interrupt output active low (0x0011=>0x10); readout averaging period 4.3ms (0x010a=>0x30); VHV recalibration every 255 ranges (0x0031=>0xff) and once now (0x002e=>0x01); range inter-measurement period 90ms (0x001b=>0x08); interrupt on new range sample only (0x0014=>0x04); clear interrupts (0x0015=>0x07); start continuous ranging (0x0018=>0x03)\\nPoll: RESULT__RANGE_STATUS, RESULT__ALS_STATUS and RESULT__INTERRUPT_STATUS_GPIO (0x004d-0x004f), RESULT__RANGE_VAL (0x0062), then clear interrupts (0x0015=>0x07)\\nerr is the range error code (0x004d bits 7:4) and valid is true only when it is 0. With no target in range err is typically 6 (early convergence estimate), 7 (max convergence time) or 11 (signal to noise) and the raw range reads 255\\nfresh is 1 when a new range sample completed since the previous poll (0x004f bit 2, new sample ready) and 0 when the poll re-read the previous sample\\nSample rate (_conf.rate): the sensor's inter-measurement period (0x001b) is set shorter than the poll interval so that every poll normally sees a new sample. Up to 10Hz the period is 90ms with the default max convergence time 49ms (0x001c=>0x31) and readout averaging 4.3ms. 20Hz uses a 40ms period, 25ms max convergence and 4.3ms averaging. 50Hz uses a 10ms period, 3ms max convergence and 1.8ms averaging (0x010a=>0x08), which reduces range on dark targets and increases noise. Ranging is stopped while the timing is changed and then restarted\\nAmbient light sensing (ALS) is not used\",\n \"identPriority\": 2,\n \"addresses\": \"0x29\",\n \"detectionValues\": \"0x0000=0b10110100\",\n \"initValues\": \"0x001801=p100&0x020701&0x020801&0x009600&0x0097fd&0x00e301&0x00e403&0x00e502&0x00e601&0x00e703&0x00f502&0x00d905&0x00dbce&0x00dc03&0x00ddf8&0x009f00&0x00a33c&0x00b700&0x00bb3c&0x00b209&0x00ca09&0x019801&0x01b017&0x01ad00&0x00ff05&0x010005&0x019905&0x01a61b&0x01ac3e&0x01a71f&0x003000&0x001600&0x001110&0x010a30&0x0031ff&0x002e01=p10&0x001c31&0x001b08&0x001404&0x001507&0x001803\",\n \"pollInfo\": {\n \"c\": \"0x004d=r3&0x0062=r1&0x001507\",\n \"i\": 200,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Distance Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"VL6180\",\n \"clas\": [\"DIST\"],\n \"resp\": {\n \"b\": 4,\n \"a\": [\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"at\": 0,\n \"u\": \"\",\n \"r\": [0,1],\n \"m\": \"0xF0\",\n \"s\": 4,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false,\n \"lut\": [\n {\n \"r\": \"0\",\n \"v\": 1\n },\n {\n \"r\": \"\",\n \"v\": 0\n }\n ]\n },\n {\n \"n\": \"err\",\n \"t\": \"B\",\n \"at\": 0,\n \"u\": \"\",\n \"r\": [0,15],\n \"m\": \"0xF0\",\n \"s\": 4,\n \"f\": \"d\",\n \"vs\": false,\n \"vl\": {\"0\": \"OK\", \"1\": \"VCSEL cont\", \"2\": \"VCSEL test\", \"3\": \"VCSEL wdog\", \"4\": \"PLL1\", \"5\": \"PLL2\", \"6\": \"early conv\", \"7\": \"max conv\", \"8\": \"no target\", \"9\": \"unused\", \"10\": \"unused\", \"11\": \"SNR\", \"12\": \"raw under\", \"13\": \"raw over\", \"14\": \"underflow\", \"15\": \"overflow\"}\n },\n {\n \"n\": \"fresh\",\n \"t\": \"B\",\n \"at\": 2,\n \"u\": \"\",\n \"r\": [0,1],\n \"m\": \"0x04\",\n \"s\": 2,\n \"f\": \"b\",\n \"o\": \"bool\",\n \"vs\": false\n },\n {\n \"n\": \"dist\",\n \"t\": \"B\",\n \"at\": 3,\n \"u\": \"mm\",\n \"r\": [0,255],\n \"f\": \"3d\",\n \"o\": \"float\",\n \"vft\": \"valid\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"_conf.rate\",\n \"t\": \"B\",\n \"r\": [1,50],\n \"d\": 5,\n \"desc\": \"Sample rate (Hz)\",\n \"map\": {\n \"1\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 1000000,\n \"s\": 1\n },\n \"2\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 500000,\n \"s\": 1\n },\n \"5\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 200000,\n \"s\": 1\n },\n \"10\": {\n \"w\": \"001801&001b08&001c31&010a30&001507&001803\",\n \"i\": 100000,\n \"s\": 1\n },\n \"20\": {\n \"w\": \"001801&001b03&001c19&010a30&001507&001803\",\n \"i\": 50000,\n \"s\": 1\n },\n \"50\": {\n \"w\": \"001801&001b00&001c03&010a08&001507&001803\",\n \"i\": 20000,\n \"s\": 1\n }\n }\n }\n ]\n }\n}\n}}", + "policyOnly": {}, + "provenance": null, + "maxBytes": 65536, + "text": "{\"devTypes\":{}}", + "crc": "bfcb", + "entries": [ + { + "key": "ADXL313", + "kind": "unchanged" + }, + { + "key": "AHT20", + "kind": "unchanged" + }, + { + "key": "AMG8833", + "kind": "unchanged" + }, + { + "key": "AS5600", + "kind": "unchanged" + }, + { + "key": "AdafruitGamepad", + "kind": "unchanged" + }, + { + "key": "AdafruitSoilSensor", + "kind": "unchanged" + }, + { + "key": "BLEBTHome", + "kind": "unchanged" + }, + { + "key": "BME280", + "kind": "unchanged" + }, + { + "key": "BME680", + "kind": "unchanged" + }, + { + "key": "BME690", + "kind": "unchanged" + }, + { + "key": "CAP1188", + "kind": "unchanged" + }, + { + "key": "CAP1203", + "kind": "unchanged" + }, + { + "key": "DPS310", + "kind": "unchanged" + }, + { + "key": "ENS160", + "kind": "unchanged" + }, + { + "key": "GravityO2", + "kind": "unchanged" + }, + { + "key": "HX711", + "kind": "unchanged" + }, + { + "key": "LPS25", + "kind": "unchanged" + }, + { + "key": "LPS3x", + "kind": "unchanged" + }, + { + "key": "LSM6DS3", + "kind": "unchanged" + }, + { + "key": "LSM6DS3TRC", + "kind": "unchanged" + }, + { + "key": "LTR-329", + "kind": "unchanged" + }, + { + "key": "LTR-390", + "kind": "unchanged" + }, + { + "key": "M5-Weight", + "kind": "unchanged" + }, + { + "key": "M5Encoder", + "kind": "unchanged" + }, + { + "key": "MAX17048", + "kind": "unchanged" + }, + { + "key": "MAX30101", + "kind": "unchanged" + }, + { + "key": "MCP9808", + "kind": "unchanged" + }, + { + "key": "MICS6814Pimoroni", + "kind": "unchanged" + }, + { + "key": "MMC5603", + "kind": "unchanged" + }, + { + "key": "MPRLS", + "kind": "unchanged" + }, + { + "key": "MS8607PT", + "kind": "unchanged" + }, + { + "key": "MS8607RH", + "kind": "unchanged" + }, + { + "key": "MT6701", + "kind": "unchanged" + }, + { + "key": "MXC400xXC", + "kind": "unchanged" + }, + { + "key": "PCT2075", + "kind": "unchanged" + }, + { + "key": "QwiicButton", + "kind": "unchanged" + }, + { + "key": "QwiicLEDStick", + "kind": "unchanged" + }, + { + "key": "SCD30", + "kind": "unchanged" + }, + { + "key": "SCD40", + "kind": "unchanged" + }, + { + "key": "SGP30", + "kind": "unchanged" + }, + { + "key": "SGP40", + "kind": "unchanged" + }, + { + "key": "SHT31", + "kind": "unchanged" + }, + { + "key": "SHT40", + "kind": "unchanged" + }, + { + "key": "SHTC3", + "kind": "unchanged" + }, + { + "key": "STCC4", + "kind": "unchanged" + }, + { + "key": "TMP117", + "kind": "unchanged" + }, + { + "key": "TrillBar", + "kind": "unchanged" + }, + { + "key": "TrillCraft", + "kind": "unchanged" + }, + { + "key": "TrillFlex", + "kind": "unchanged" + }, + { + "key": "TrillHex", + "kind": "unchanged" + }, + { + "key": "TrillRing", + "kind": "unchanged" + }, + { + "key": "TrillSquare", + "kind": "unchanged" + }, + { + "key": "VCNL4040", + "kind": "unchanged" + }, + { + "key": "VEML6040", + "kind": "unchanged" + }, + { + "key": "VEML7700", + "kind": "unchanged" + }, + { + "key": "VL53L0X", + "kind": "unchanged" + }, + { + "key": "VL53L1X", + "kind": "unchanged" + }, + { + "key": "VL53L4CD", + "kind": "unchanged" + }, + { + "key": "VL6180", + "kind": "unchanged" + } + ] + }, + { + "name": "overrides, decoder kinds and policies", + "records": "{\n \"devTypes\": {\n \"AHT20\": {\n \"_notes\": \"a note\",\n \"addresses\": \"0x38\",\n \"detectionValues\": \"=0bX00XXXXX\",\n \"initValues\": \"0xbe0800=p10\",\n \"pollInfo\": {\n \"c\": \"0xac3300=p80&=r6\",\n \"i\": 500,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Asair\",\n \"type\": \"AHT20\",\n \"clas\": [\n \"TEMP\",\n \"RH\"\n ],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"u\": \"\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\n \"0x08\": \"calibrated\",\n \"0x80\": \"busy\"\n }\n },\n {\n \"n\": \"humidity\",\n \"t\": \">I\",\n \"u\": \"%\",\n \"r\": [\n 0,\n 100\n ],\n \"m\": \"0xfffff000\",\n \"s\": 12,\n \"d\": 10485.76,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"temperature\",\n \"at\": 2,\n \"t\": \">I\",\n \"u\": \"°C\",\n \"r\": [\n -40,\n 85\n ],\n \"m\": \"0x000fffff\",\n \"d\": 5242.88,\n \"a\": -50,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n },\n \"deviceType\": \"AHT20\"\n },\n \"Code\": {\n \"addresses\": \"0x31\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Code\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ],\n \"c\": {\n \"n\": \"f\",\n \"c\": \"out.v=buf[0];\"\n }\n }\n }\n },\n \"LSM6DS3\": {\n \"_notes\": \"ST LSM6DS3 / LSM6DS33 (WHO_AM_I 0x69), FIFO-enabled 6-axis IMU (Axiom). STOP_ON_FTH on this part is CTRL4_C (0x13) bit 0, so init writes FIFO_CTRL4 = 0x00 and CTRL4_C = 0x01 (measured on Axiom018: the FIFO then stops at the 1020-word threshold). The 0x6A parts are LSM6DS3TRC. Two-phase I2C read: first reads FIFO_STATUS1-4 (word count), then reads only the required FIFO data bytes from 0x3E. FIFO_PATTERN (STATUS3/4) used to skip to next aligned group boundary before decoding. Each sample = 6 words (gx,gy,gz,ax,ay,az). FIFO depth is limited to the 1020-word watermark (FIFO_CTRL1/2) by STOP_ON_FTH, so DIFF_FIFO can never wrap to 0 (with an unlimited depth a host pause of >6.6 s filled the 4096-word FIFO, the count read 0 and the {$0.w12} poll read nothing forever - devdocs/lsm6ds-fifo-full-deadlock.md). The _conf.rate entries keep the watermark. Gyro 70 mdps/LSB at 2000 dps.\",\n \"addresses\": \"0x6a-0x6b\",\n \"detectionValues\": \"0x0f=0x69\",\n \"initValues\": \"0x0a00&0x1244&0x1048&0x114c&0x1640&0x0809&0x0900&0x1301&0x06fc&0x0703&0x0a26\",\n \"pollInfo\": {\n \"c\": \"0x3a=r4&0x3e=r{$0.w12:mask0FFF*2:max240}\",\n \"i\": 20000,\n \"s\": 20\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer & Gyroscope\",\n \"desc\": \"6-Axis IMU\",\n \"manu\": \"ST\",\n \"type\": \"LSM6DS3\",\n \"clas\": [\n \"ACC\",\n \"GYRO\"\n ],\n \"resp\": {\n \"b\": 244,\n \"a\": [\n {\n \"n\": \"gx\",\n \"t\": \"maxN){N=maxN;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(iH\",\n \"u\": \"°C\",\n \"r\": [\n -40,\n 125\n ],\n \"m\": \"0x1fff\",\n \"d\": 16,\n \"sb\": 12,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n },\n \"Std\": {\n \"addresses\": \"0x30\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Std\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n },\n \"_notes\": \"dropped\"\n },\n \"NoData\": {\n \"addresses\": \"0x32\",\n \"detectionValues\": \"0x00=0x01\"\n }\n }\n}", + "compiled": "{\n \"devTypes\": {\n \"LSM6DS3\": {\n \"_notes\": \"ST LSM6DS3 / LSM6DS33 (WHO_AM_I 0x69), FIFO-enabled 6-axis IMU (Axiom). STOP_ON_FTH on this part is CTRL4_C (0x13) bit 0, so init writes FIFO_CTRL4 = 0x00 and CTRL4_C = 0x01 (measured on Axiom018: the FIFO then stops at the 1020-word threshold). The 0x6A parts are LSM6DS3TRC. Two-phase I2C read: first reads FIFO_STATUS1-4 (word count), then reads only the required FIFO data bytes from 0x3E. FIFO_PATTERN (STATUS3/4) used to skip to next aligned group boundary before decoding. Each sample = 6 words (gx,gy,gz,ax,ay,az). FIFO depth is limited to the 1020-word watermark (FIFO_CTRL1/2) by STOP_ON_FTH, so DIFF_FIFO can never wrap to 0 (with an unlimited depth a host pause of >6.6 s filled the 4096-word FIFO, the count read 0 and the {$0.w12} poll read nothing forever - devdocs/lsm6ds-fifo-full-deadlock.md). The _conf.rate entries keep the watermark. Gyro 70 mdps/LSB at 2000 dps.\",\n \"addresses\": \"0x6a-0x6b\",\n \"detectionValues\": \"0x0f=0x69\",\n \"initValues\": \"0x0a00&0x1244&0x1048&0x114c&0x1640&0x0809&0x0900&0x1301&0x06fc&0x0703&0x0a26\",\n \"pollInfo\": {\n \"c\": \"0x3a=r4&0x3e=r{$0.w12:mask0FFF*2:max240}\",\n \"i\": 50,\n \"s\": 20\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Accelerometer & Gyroscope\",\n \"desc\": \"6-Axis IMU\",\n \"manu\": \"ST\",\n \"type\": \"LSM6DS3\",\n \"clas\": [\n \"ACC\",\n \"GYRO\"\n ],\n \"resp\": {\n \"b\": 244,\n \"a\": [\n {\n \"n\": \"gx\",\n \"t\": \"maxN){N=maxN;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(iH\",\n \"u\": \"°C\",\n \"r\": [\n -40,\n 125\n ],\n \"m\": \"0x1fff\",\n \"d\": 16,\n \"sb\": 12,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n },\n \"TrillRing\": {\n \"_notes\": \"Bela Trill Ring - 1D circular capacitive touch, 28 ring channels + 2 button channels, up to 5 touches. CENTROID response: 5 loc + 5 size + 2 button values (uint16 BE). Address jumper-configurable 0x38-0x3F.\",\n \"addresses\": \"0x38-0x3F\",\n \"detectionValues\": \"0x00FF=p25&=0bXXXXXXXX00000100\",\n \"initValues\": \"0x000100=p15&0x0006=p30\",\n \"pollInfo\": {\n \"c\": \"0x04=r24\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"hwProbeEffects\": {\n \"rsao\": {\n \"effect\": \"none\",\n \"basis\": \"source\",\n \"note\": \"Trill firmware v2: the first byte is a buffer offset and 0x99 is beyond the 64-byte buffer, so it is NACKed and ignored; reads have no side effects (BelaPlatform/Trill 537ebd6). Firmware v3 isn't public\"\n }\n },\n \"devInfoJson\": {\n \"name\": \"Trill Ring\",\n \"desc\": \"Capacitive Touch Ring\",\n \"manu\": \"Bela\",\n \"type\": \"TrillRing\",\n \"clas\": [\n \"TCH\",\n \"BTN\"\n ],\n \"resp\": {\n \"b\": 24,\n \"a\": [\n {\n \"n\": \"loc0\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"loc1\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"loc2\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"loc3\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"loc4\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"size0\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"size1\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"size2\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"size3\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"size4\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"btn0\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n },\n {\n \"n\": \"btn1\",\n \"t\": \">H\",\n \"r\": [\n 0,\n 65535\n ],\n \"f\": \"d\",\n \"o\": \"uint16\"\n }\n ]\n },\n \"actions\": [\n {\n \"n\": \"baseline\",\n \"w\": \"0006\",\n \"desc\": \"Update baseline\"\n },\n {\n \"n\": \"_conf.mode\",\n \"t\": \"B\",\n \"r\": [\n 0,\n 3\n ],\n \"d\": 0,\n \"desc\": \"Sensor mode\",\n \"map\": {\n \"centroid\": {\n \"w\": \"000100\"\n },\n \"raw\": {\n \"w\": \"000101\"\n },\n \"baseline\": {\n \"w\": \"000102\"\n },\n \"diff\": {\n \"w\": \"000103\"\n }\n }\n },\n {\n \"n\": \"_conf.noise\",\n \"t\": \"B\",\n \"w\": \"0004\",\n \"r\": [\n 0,\n 255\n ],\n \"d\": 0,\n \"desc\": \"Noise threshold\"\n },\n {\n \"n\": \"_conf.prescaler\",\n \"t\": \"B\",\n \"w\": \"0003\",\n \"r\": [\n 0,\n 8\n ],\n \"d\": 2,\n \"desc\": \"Prescaler\"\n },\n {\n \"n\": \"_conf.minsize\",\n \"t\": \">H\",\n \"w\": \"0007\",\n \"r\": [\n 0,\n 65535\n ],\n \"d\": 0,\n \"desc\": \"Minimum touch size\"\n }\n ]\n }\n },\n \"VL53L0X\": {\n \"_notes\": \"ToF distance sensor with range up to 2m. Initialization sequence adapted from the Pololu library.\",\n \"addresses\": \"0x29\",\n \"identPriority\": 3,\n \"detectionValues\": \"0xC0=0xEE&0xC1=0xAA\",\n \"initValues\": \"0x8800&0x440040&0x8001&0xff01&0x0000&0x91=r1&0x0001&0xff00&0x8000&0x0001\",\n \"pollInfo\": {\n \"c\": \"0x14=r12&0x0b01&0x0001\",\n \"i\": 50,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Distance Sensor\",\n \"desc\": \"\",\n \"manu\": \"ST\",\n \"type\": \"VL53L0X\",\n \"clas\": [\n \"DIST\"\n ],\n \"resp\": {\n \"b\": 12,\n \"a\": [\n {\n \"n\": \"valid\",\n \"t\": \"B\",\n \"u\": \"\",\n \"r\": [\n 0,\n 1\n ],\n \"f\": \"b\",\n \"o\": \"bool\",\n \"m\": \"0x78\",\n \"s\": 3,\n \"vs\": false,\n \"lut\": [\n {\n \"r\": \"11\",\n \"v\": 1\n },\n {\n \"r\": \"\",\n \"v\": 0\n }\n ]\n },\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"at\": 0,\n \"r\": [\n 0,\n 255\n ],\n \"f\": \"02x\",\n \"vs\": false\n },\n {\n \"n\": \"rangeStatus\",\n \"t\": \"B\",\n \"at\": 0,\n \"u\": \"\",\n \"r\": [\n 0,\n 15\n ],\n \"m\": \"0x78\",\n \"s\": 3,\n \"f\": \"d\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"vl\": {\n \"0\": \"OK\",\n \"1\": \"VCSEL cont\",\n \"2\": \"VCSEL wdog\",\n \"3\": \"no VHV\",\n \"4\": \"no target\",\n \"5\": \"SNR\",\n \"6\": \"phase\",\n \"7\": \"sigma\",\n \"8\": \"TCC\",\n \"9\": \"phase cons\",\n \"10\": \"min clip\",\n \"11\": \"complete\",\n \"12\": \"underflow\",\n \"13\": \"overflow\",\n \"14\": \"ignore\"\n }\n },\n {\n \"n\": \"dist\",\n \"at\": 10,\n \"t\": \">H\",\n \"u\": \"mm\",\n \"r\": [\n 0,\n 2000\n ],\n \"f\": \"4d\",\n \"o\": \"uint16\",\n \"vft\": \"valid\"\n }\n ]\n }\n }\n },\n \"AHT20\": {\n \"_notes\": \"Aosong AHT20 (also AHT21/AHT25/DHT20) at 0x38. Data format: Status(8bit) + Humidity(20bit) + Temperature(20bit) (+ CRC, not read). Conversion: humidity(%) = (H[19:0]/2^20)*100, temp(°C) = (T[19:0]/2^20)*200-50. Detection: a bare read of the status byte with bits 6:5 = 0 - weak, but it passes idle (0x1C), measuring (busy bit 7 set: the AHT20 answers rather than NACKing) and just after a soft reset (0x80, calibration bit clear), so it can't fail on a genuine part; a Trill Ring's reply starts 0xFE. Init: calibrate (0xBE0800), 10 ms. Poll: trigger (0xAC3300), 80 ms (the measurement time; a read before then gives incomplete data with a valid CRC), read 6 bytes.\",\n \"addresses\": \"0x38\",\n \"detectionValues\": \"=0bX00XXXXX\",\n \"initValues\": \"0xbe0800=p10\",\n \"pollInfo\": {\n \"c\": \"0xac3300=p80&=r6\",\n \"i\": 500,\n \"s\": 1\n },\n \"scanPriority\": \"high\",\n \"devInfoJson\": {\n \"name\": \"Temperature & Humidity Sensor\",\n \"desc\": \"\",\n \"manu\": \"Asair\",\n \"type\": \"AHT20\",\n \"clas\": [\n \"TEMP\",\n \"RH\"\n ],\n \"resp\": {\n \"b\": 6,\n \"a\": [\n {\n \"n\": \"status\",\n \"t\": \"B\",\n \"u\": \"\",\n \"f\": \"02x\",\n \"o\": \"uint8\",\n \"vs\": false,\n \"bl\": {\n \"0x08\": \"calibrated\",\n \"0x80\": \"busy\"\n }\n },\n {\n \"n\": \"humidity\",\n \"t\": \">I\",\n \"u\": \"%\",\n \"r\": [\n 0,\n 100\n ],\n \"m\": \"0xfffff000\",\n \"s\": 12,\n \"d\": 10485.76,\n \"f\": \"3.1f\",\n \"o\": \"float\"\n },\n {\n \"n\": \"temperature\",\n \"at\": 2,\n \"t\": \">I\",\n \"u\": \"°C\",\n \"r\": [\n -40,\n 85\n ],\n \"m\": \"0x000fffff\",\n \"d\": 5242.88,\n \"a\": -50,\n \"f\": \"3.2f\",\n \"o\": \"float\"\n }\n ]\n }\n }\n }\n }\n}", + "policyOnly": { + "VL53L0X": "disabled", + "TrillRing": "disabledClaim" + }, + "provenance": "{\"formatVersion\": 1, \"sources\": {\"raftcore\": {\"bundled\": true, \"raftCore\": \"1.62.0\"}, \"acme\": {\"git\": \"https://github.com/acme/devtypes.git\", \"ref\": \"v1.0.0\", \"commit\": \"3f2a9c1e7b0d0000000000000000000000000000\"}}, \"typeFile\": {\"include\": [\"raftcore:*\", \"acme:Gadget\"], \"policy\": {\"TrillRing\": \"disabled\"}}}", + "maxBytes": 65536, + "text": "{\"devTypes\":{\"Code\":{\"addresses\":\"0x31\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"Code\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}],\"c\":{\"n\":\"f\",\"c\":\"out.v=buf[0];\"}}}},\"LSM6DS3\":{\"addresses\":\"0x6a-0x6b\",\"detectionValues\":\"0x0f=0x69\",\"initValues\":\"0x0a00&0x1244&0x1048&0x114c&0x1640&0x0809&0x0900&0x1301&0x06fc&0x0703&0x0a26\",\"pollInfo\":{\"c\":\"0x3a=r4&0x3e=r{$0.w12:mask0FFF*2:max240}\",\"i\":20000,\"s\":20},\"scanPriority\":\"high\",\"devInfoJson\":{\"name\":\"Accelerometer & Gyroscope\",\"desc\":\"6-Axis IMU\",\"manu\":\"ST\",\"type\":\"LSM6DS3\",\"clas\":[\"ACC\",\"GYRO\"],\"resp\":{\"b\":244,\"a\":[{\"n\":\"gx\",\"t\":\"maxN){N=maxN;}if(N<1){N=0;}int k=4+skip*2;int i=0;while(iH\",\"u\":\"°C\",\"r\":[-40,125],\"m\":\"0x1fff\",\"d\":16,\"sb\":12,\"f\":\"3.2f\",\"o\":\"float\"}]}}},\"NoData\":{\"addresses\":\"0x32\",\"detectionValues\":\"0x00=0x01\"},\"Std\":{\"addresses\":\"0x30\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"Std\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}}},\"TrillRing\":{\"disable\":\"claim\"},\"VL53L0X\":{\"disable\":true}},\"_sources\":{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"},\"acme\":{\"git\":\"https://github.com/acme/devtypes.git\",\"ref\":\"v1.0.0\",\"commit\":\"3f2a9c1e7b0d0000000000000000000000000000\"}},\"typeFile\":{\"include\":[\"raftcore:*\",\"acme:Gadget\"],\"policy\":{\"TrillRing\":\"disabled\"}}}}", + "crc": "f962", + "entries": [ + { + "key": "AHT20", + "kind": "unchanged" + }, + { + "key": "Code", + "kind": "new", + "noDecoder": true, + "decoder": "none" + }, + { + "key": "LSM6DS3", + "kind": "override" + }, + { + "key": "MCP9808", + "kind": "override", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "Std", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "NoData", + "kind": "new" + }, + { + "key": "VL53L0X", + "kind": "policy", + "policy": "disabled" + }, + { + "key": "TrillRing", + "kind": "policy", + "policy": "disabledClaim" + } + ] + }, + { + "name": "numbers, characters and key order as Python writes them", + "records": "{\"devTypes\":{\"Lexemes\":{\n \"addresses\": \"0x48\",\n \"detectionValues\": \"0x00=0x12\",\n \"pollInfo\": {\"c\": \"0x00=r2\", \"i\": 1E3, \"s\": 1},\n \"devInfoJson\": {\n \"name\": \"Lexemes °C ✓ 😀 \\u007f\\t/ café °\",\n \"resp\": {\"b\": 2, \"a\": [{\"n\": \"v\", \"t\": \">H\", \"d\": 16.0, \"a\": -0.0, \"m\": 0.000000125, \"x\": 1.50,\n \"big\": 36893488147419103232.0, \"e\": 2.5e-5, \"s\": 1e16, \"f\": 123456789012345.0,\n \"g\": 1234567890123456.0, \"h\": 12345678901234567.0, \"neg\": -0, \"huge\": 12345678901234567890123,\n \"small\": 0.0001, \"smaller\": 0.00001, \"r\": [-40, 125.5]}]},\n \"actions\": [{\"n\": \"rate\", \"map\": {\"12.5\": 1, \"26\": 2, \"104\": 3, \"0\": 4, \"-1\": 5, \"01\": 6}}],\n \"dup\": {\"a\": 1, \"b\": 2, \"a\": 3}\n }\n}}}", + "compiled": "{\"devTypes\":{}}", + "policyOnly": {}, + "provenance": "{\"formatVersion\": 1, \"note\": \"caf\\u00e9 \\u00b0 \\ud83d\\ude00\", \"n\": 1.0}", + "maxBytes": 65536, + "text": "{\"devTypes\":{\"Lexemes\":{\"addresses\":\"0x48\",\"detectionValues\":\"0x00=0x12\",\"pollInfo\":{\"c\":\"0x00=r2\",\"i\":1000.0,\"s\":1},\"devInfoJson\":{\"name\":\"Lexemes \\u00b0C \\u2713 \\ud83d\\ude00 \\u007f\\t/ caf\\u00e9 \\u00b0\",\"resp\":{\"b\":2,\"a\":[{\"n\":\"v\",\"t\":\">H\",\"d\":16.0,\"a\":-0.0,\"m\":1.25e-07,\"x\":1.5,\"big\":3.6893488147419103e+19,\"e\":2.5e-05,\"s\":1e+16,\"f\":123456789012345.0,\"g\":1234567890123456.0,\"h\":1.2345678901234568e+16,\"neg\":0,\"huge\":12345678901234567890123,\"small\":0.0001,\"smaller\":1e-05,\"r\":[-40,125.5]}]},\"actions\":[{\"n\":\"rate\",\"map\":{\"12.5\":1,\"26\":2,\"104\":3,\"0\":4,\"-1\":5,\"01\":6}}],\"dup\":{\"a\":3,\"b\":2}}}},\"_sources\":{\"formatVersion\":1,\"note\":\"caf\\u00e9 \\u00b0 \\ud83d\\ude00\",\"n\":1.0}}", + "crc": "44eb", + "entries": [ + { + "key": "Lexemes", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + } + ] + }, + { + "name": "keys in UTF-8 byte order", + "records": "{\n \"devTypes\": {\n \"b\": {\n \"addresses\": \"0x40\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"b\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n },\n \"B\": {\n \"addresses\": \"0x40\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"B\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n },\n \"a_1\": {\n \"addresses\": \"0x40\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"a_1\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n },\n \"a\": {\n \"addresses\": \"0x40\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"a\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n },\n \"é\": {\n \"addresses\": \"0x40\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"é\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n },\n \"Z9\": {\n \"addresses\": \"0x40\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Z9\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n },\n \"😀\": {\n \"addresses\": \"0x40\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"😀\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n },\n \"○\": {\n \"addresses\": \"0x40\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"○\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n }\n }\n}", + "compiled": "{\"devTypes\":{}}", + "policyOnly": {}, + "provenance": null, + "maxBytes": 65536, + "text": "{\"devTypes\":{\"B\":{\"addresses\":\"0x40\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"B\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}}},\"Z9\":{\"addresses\":\"0x40\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"Z9\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}}},\"a\":{\"addresses\":\"0x40\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"a\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}}},\"a_1\":{\"addresses\":\"0x40\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"a_1\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}}},\"b\":{\"addresses\":\"0x40\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"b\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}}},\"\\u00e9\":{\"addresses\":\"0x40\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"\\u00e9\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}}},\"\\uffee\":{\"addresses\":\"0x40\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"\\uffee\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}}},\"\\ud83d\\ude00\":{\"addresses\":\"0x40\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"\\ud83d\\ude00\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}}}}}", + "crc": "fd50", + "entries": [ + { + "key": "b", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "B", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "a_1", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "a", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "é", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "Z9", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "😀", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + }, + { + "key": "○", + "kind": "new", + "noDecoder": true, + "decoder": "runtime" + } + ] + }, + { + "name": "init value slots: own initCode", + "records": "{\n \"devTypes\": {\n \"Own\": {\n \"addresses\": \"0x51\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"0x10{rate}&{?on}0x11=\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Test\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n },\n \"initCode\": \"init.rate = 3; init.on = 1;\"\n }\n }\n}", + "compiled": "{\"devTypes\":{}}", + "policyOnly": {}, + "provenance": null, + "maxBytes": 65536, + "text": "{\"devTypes\":{\"Own\":{\"addresses\":\"0x51\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"0x10{rate}&{?on}0x11=\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"Test\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}},\"initCode\":\"init.rate = 3; init.on = 1;\"}}}", + "crc": "db3a", + "entries": [ + { + "key": "Own", + "kind": "new", + "initCode": "runtime", + "noDecoder": true, + "decoder": "runtime" + } + ] + }, + { + "name": "init value slots: the compiled record's initCode", + "records": "{\n \"devTypes\": {\n \"Slot\": {\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"0x10{rate}\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Test\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n }\n }\n}", + "compiled": "{\n \"devTypes\": {\n \"Slot\": {\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"0x10{rate}\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Slot\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n },\n \"initCode\": {\n \"c\": \"init.rate = 4;\"\n }\n }\n }\n}", + "policyOnly": {}, + "provenance": null, + "maxBytes": 65536, + "text": "{\"devTypes\":{\"Slot\":{\"addresses\":\"0x50\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"0x10{rate}\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"Test\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}}}}}", + "crc": "f028", + "entries": [ + { + "key": "Slot", + "kind": "override" + } + ] + }, + { + "name": "init code: own code and compiled code", + "records": "{\n \"devTypes\": {\n \"Slot\": {\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"0x10{rate}\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Test\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n },\n \"initCode\": {\n \"c\": \"init.rate = 5;\"\n }\n }\n }\n}", + "compiled": "{\n \"devTypes\": {\n \"Slot\": {\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"0x10{rate}\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Slot\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n },\n \"initCode\": {\n \"c\": \"init.rate = 4;\"\n }\n }\n }\n}", + "policyOnly": {}, + "provenance": null, + "maxBytes": 65536, + "text": "{\"devTypes\":{\"Slot\":{\"addresses\":\"0x50\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"0x10{rate}\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"Test\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}},\"initCode\":{\"c\":\"init.rate = 5;\"}}}}", + "crc": "28dd", + "entries": [ + { + "key": "Slot", + "kind": "override", + "initCode": "runtime or compiled" + } + ] + }, + { + "name": "init code: own code computing more", + "records": "{\n \"devTypes\": {\n \"Slot\": {\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"0x10{rate}&{?on}0x11=\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Test\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n },\n \"initCode\": \"init.rate = 5; init.on = 1;\"\n }\n }\n}", + "compiled": "{\n \"devTypes\": {\n \"Slot\": {\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"0x10{rate}\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Slot\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n },\n \"initCode\": {\n \"c\": \"init.rate = 4;\"\n }\n }\n }\n}", + "policyOnly": {}, + "provenance": null, + "maxBytes": 65536, + "text": "{\"devTypes\":{\"Slot\":{\"addresses\":\"0x50\",\"detectionValues\":\"0x00=0x12\",\"initValues\":\"0x10{rate}&{?on}0x11=\",\"pollInfo\":{\"c\":\"0x00=r1\",\"i\":100,\"s\":1},\"devInfoJson\":{\"name\":\"Test\",\"resp\":{\"b\":1,\"a\":[{\"n\":\"v\",\"t\":\"B\",\"o\":\"uint8\"}]}},\"initCode\":\"init.rate = 5; init.on = 1;\"}}}", + "crc": "0e1e", + "entries": [ + { + "key": "Slot", + "kind": "override", + "initCode": "runtime" + } + ] + }, + { + "name": "error: over the size limit", + "records": "{\n \"devTypes\": {\n \"Std\": {\n \"addresses\": \"0x10\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Test\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n }\n }\n}", + "compiled": "{\"devTypes\":{}}", + "policyOnly": {}, + "provenance": null, + "maxBytes": 100, + "error": "the type file is 209 bytes, more than the 100 byte limit" + }, + { + "name": "error: a type file can't enable a compiled type", + "records": "{\"devTypes\":{}}", + "compiled": "{\n \"devTypes\": {\n \"Std\": {\n \"addresses\": \"0x10\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Test\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n }\n }\n}", + "policyOnly": { + "Std": "enabled" + }, + "provenance": null, + "maxBytes": 65536, + "error": "typeFile.policy \"Std\": a type file can't enable a compiled type (only disable it)" + }, + { + "name": "error: policy for a type that isn't compiled", + "records": "{\"devTypes\":{}}", + "compiled": "{\"devTypes\":{}}", + "policyOnly": { + "Zulu": "disabled" + }, + "provenance": null, + "maxBytes": 65536, + "error": "typeFile.policy \"Zulu\": not a selected record or a compiled one" + }, + { + "name": "error: slots without init code", + "records": "{\n \"devTypes\": {\n \"Slot\": {\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"0x10{rate}\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Test\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n }\n }\n}", + "compiled": "{\"devTypes\":{}}", + "policyOnly": {}, + "provenance": null, + "maxBytes": 65536, + "error": "typeFile \"Slot\": init values ({name} value slots, {?name} guards) need init code - initCode in the record (needs firmware with RAFT_RUNTIME_DECODE_CODE) or a compiled Slot whose initCode computes rate" + }, + { + "name": "error: initCode doesn't compute a slot", + "records": "{\n \"devTypes\": {\n \"Slot\": {\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"0x10{rate}{gain}\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Test\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n },\n \"initCode\": \"init.rate = 1;\"\n }\n }\n}", + "compiled": "{\"devTypes\":{}}", + "policyOnly": {}, + "provenance": null, + "maxBytes": 65536, + "error": "typeFile \"Slot\": initValues uses gain, which its initCode doesn't compute" + }, + { + "name": "error: compiled initCode doesn't compute a slot", + "records": "{\n \"devTypes\": {\n \"Slot\": {\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"0x10{gain}\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Test\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n }\n }\n }\n}", + "compiled": "{\n \"devTypes\": {\n \"Slot\": {\n \"addresses\": \"0x50\",\n \"detectionValues\": \"0x00=0x12\",\n \"initValues\": \"0x10{rate}\",\n \"pollInfo\": {\n \"c\": \"0x00=r1\",\n \"i\": 100,\n \"s\": 1\n },\n \"devInfoJson\": {\n \"name\": \"Slot\",\n \"resp\": {\n \"b\": 1,\n \"a\": [\n {\n \"n\": \"v\",\n \"t\": \"B\",\n \"o\": \"uint8\"\n }\n ]\n }\n },\n \"initCode\": {\n \"c\": \"init.rate = 4;\"\n }\n }\n }\n}", + "policyOnly": {}, + "provenance": null, + "maxBytes": 65536, + "error": "typeFile \"Slot\": init values ({name} value slots, {?name} guards) need init code - initCode in the record (needs firmware with RAFT_RUNTIME_DECODE_CODE) or a compiled Slot whose initCode computes gain" + } + ] +}