236 lines
11 KiB
Markdown
236 lines
11 KiB
Markdown
# Matter QR Code Decoder
|
|
|
|
Decode the commissioning QR code that Matter / CHIP devices ship with. A Matter
|
|
device's setup QR encodes a short `MT:` URI that carries the information a
|
|
commissioning app (e.g. Apple Home, Google Home, the Matter app) needs to pair
|
|
with the device for the first time. This tool takes that URI and shows you what
|
|
is inside.
|
|
|
|
No build, no install, no package step. Two ways to use it: a browser app and a
|
|
command-line script that share the same decode logic.
|
|
|
|
## What it decodes
|
|
|
|
Given an `MT:` URI, the payload (84 bits, see [Data format](#data-format)) is
|
|
split into these fields:
|
|
|
|
| Field | Meaning |
|
|
|---|---|
|
|
| **Version** | Encoding version (3 bits) |
|
|
| **Vendor ID** | 16-bit manufacturer identifier, shown as decimal + `0x`-hex, with a name looked up from `manufacturers.tsv` when available |
|
|
| **Product ID** | 16-bit vendor-assigned product identifier, decimal + `0x`-hex; for any vendor with a catalog the product ID is also resolved to a name from `products.tsv` (and, via `vendor-equivalents.tsv`, an equivalent vendor's catalog) when available |
|
|
| **Custom Flow** | Commissioning flow type: 0 = Standard, 1 = User Intent, 2 = Custom, 3 = Reserved |
|
|
| **Discovery** | How the device can be found: **SoftAP** / **BLE** / **On IP Network** (a bitset — a device may show more than one) |
|
|
| **Discriminator** | 12-bit discriminator |
|
|
| **Passcode** | 27-bit commissioning passcode |
|
|
| **Pairing Code** | The Matter *manual pairing code* — the PIN a commissioning app (Apple Home, Google Home, the Matter app) asks you to type. Not a raw field: derived from the passcode, discriminator and (for non-standard flows) the vendor/product IDs, then guarded by a Verhoeff check digit. 11 digits for the standard flow, 21 for user-intent/custom |
|
|
|
|
## Components
|
|
|
|
| File | What it is |
|
|
|---|---|
|
|
| [`matter-qr-decoder.html`](matter-qr-decoder.html) | The browser app. Single self-contained HTML file (Vue 3 + Bootstrap 5.3, loaded from CDNs). |
|
|
| [`matter-decode.sh`](matter-decode.sh) | A pure-bash CLI port of the same decode logic. No camera scanning. |
|
|
| [`manufacturers.tsv`](manufacturers.tsv) | Tab-separated `vendor ID → name` table (~900 rows) used to turn a numeric vendor ID into a name. Shared by both tools; optional. |
|
|
| [`gen-manufacturers.py`](gen-manufacturers.py) | Python 3 script that regenerates `manufacturers.tsv`; pulls the upstream `manufacturers.xml` over the network by default (or a URL/local file/HTML block you pass in). |
|
|
| [`products.tsv`](products.tsv) | Tab-separated `vendor ID → product ID → name` table (3 columns) used by `matter-decode.sh` to turn a product ID into a name, for any vendor that has a catalog (e.g. **LIFX**, vendor `0x1306` / `4870`). Optional. |
|
|
| [`vendor-equivalents.tsv`](vendor-equivalents.tsv) | Tab-separated `alias vendor ID → canonical vendor ID` table recording companies that are the same after a merger/acquisition (e.g. Feit `5155` = LIFX `4870`), so a product resolves under either ID. Hand-edited; optional. |
|
|
| [`gen-lifx-products.py`](gen-lifx-products.py) | Python 3 script that (re)generates LIFX's rows in `products.tsv`, merging them in from LIFX's upstream `products.json` over the network by default (or a URL/local file); preserves other vendors' rows. |
|
|
|
|
`CLAUDE.md` documents the internals for anyone (or Claude Code) working on the
|
|
code.
|
|
|
|
---
|
|
|
|
## Browser app
|
|
|
|
### Requirements
|
|
|
|
- A modern browser with network access (the page loads Vue 3, Bootstrap 5.3, and
|
|
Bootstrap Icons from CDNs).
|
|
- Camera scanning needs a **secure context** — serve it over `HTTPS` or
|
|
`localhost`, not `file://`:
|
|
|
|
```sh
|
|
python3 -m http.server 8000
|
|
# then open http://localhost:8000/matter-qr-decoder.html
|
|
```
|
|
|
|
### Use it
|
|
|
|
1. Paste an `MT:` URI into the **Input** box — decoding happens live as you type.
|
|
2. Or click **Scan QR** and point your camera at the code. Scanning stops itself
|
|
the moment it reads an `MT:` QR.
|
|
|
|
Two caveats:
|
|
|
|
- **Scan QR is Chromium-only** (Chrome / Edge). It uses the `BarcodeDetector`
|
|
API. Firefox and Safari show a "not supported" message — **pasting a URI works
|
|
everywhere**, so use that on those browsers.
|
|
- The **Data Encoding Format** and **Discovery Capabilities Bits** sections at the
|
|
bottom of the page explain the layout shown in the output.
|
|
|
|
---
|
|
|
|
## Command line
|
|
|
|
`matter-decode.sh` decodes a URI from an argument or from stdin and prints the
|
|
same fields as the web UI:
|
|
|
|
```sh
|
|
./matter-decode.sh "MT:..."
|
|
echo "MT:..." | ./matter-decode.sh
|
|
```
|
|
|
|
Example output (run the URI through the script to reproduce):
|
|
|
|
```
|
|
$ echo "MT:O083635783973181337368400343813154179" | ./matter-decode.sh
|
|
Version: 0
|
|
Vendor ID: 13012 (0x32D4)
|
|
Product ID: 50616 (0xC5B8)
|
|
Custom Flow: User Intent Commissioning Flow
|
|
Discovery: BLE
|
|
Discriminator:1723
|
|
Passcode: 8126392
|
|
Pairing Code: 549080049513012506168
|
|
```
|
|
|
|
(The vendor ID `0x32D4` isn't in `manufacturers.tsv`, so no name is printed —
|
|
that's expected, not an error.)
|
|
|
|
The script is pure bash (3.x+). It reads `manufacturers.tsv` from its own
|
|
directory to resolve the vendor name; without that file every field still decodes
|
|
and only the vendor name is omitted.
|
|
|
|
For any vendor that has a catalog in `products.tsv` (e.g. **LIFX**, vendor ID
|
|
`4870` / `0x1306`) the script additionally resolves the **product ID** to a name,
|
|
printed on the Product ID line just like the vendor name on the Vendor ID line.
|
|
When a vendor has an **equivalent** in `vendor-equivalents.tsv` (a company merged
|
|
with or acquired — e.g. Feit `5155` = LIFX `4870`), the lookup also checks the
|
|
equivalent's catalog and the Vendor ID line is annotated, so either company's
|
|
device resolves to the shared product name:
|
|
|
|
```
|
|
$ echo "MT:R5QU6X.D01M3QG5.000" | ./matter-decode.sh
|
|
Version: 1
|
|
Vendor ID: 4870 (0x1306) Lifi Labs INC. dba LIFX (a.k.a. Feit Electric Company, Inc.)
|
|
Product ID: 123 (0x007B) LIFX Mini 3.1 Color US
|
|
```
|
|
|
|
Both files are optional, like `manufacturers.tsv`: without `products.tsv` the
|
|
product name is omitted, and without `vendor-equivalents.tsv` no equivalent is
|
|
considered — but every field still decodes.
|
|
|
|
---
|
|
|
|
## Data format
|
|
|
|
The URI is `MT:` followed by a **base38** payload (charset `0-9 A-Z - .`, 38
|
|
symbols). The payload decodes to bytes, read **little-endian**, then sliced into
|
|
the 84-bit field layout **LSB-first**:
|
|
|
|
```
|
|
version(3) | vendor ID(16) | product ID(16) | custom flow(2)
|
|
| discovery caps(8) | discriminator(12) | passcode(27)
|
|
```
|
|
|
|
Base38 decoding is greedy: 5 chars → 3 bytes, 4 → 2, 2 → 1; a length that fits
|
|
none of those groupings is an error. Both the HTML app and the shell script use
|
|
this same scheme, which is why the two stay in lockstep.
|
|
|
|
## Vendor name lookup
|
|
|
|
Both tools map a numeric **Vendor ID** to a name using
|
|
`manufacturers.tsv` (`vendor ID` in the first column, name in the second). The
|
|
table is derived from upstream
|
|
`project-chip/connectedhomeip .../zcl/data-model/manufacturers.xml`
|
|
(Apache-2.0, generated 2022-08-02). **Refresh it from upstream rather than
|
|
hand-editing individual rows.**
|
|
|
|
To refresh the table, run **`gen-manufacturers.py`** (Python 3 standard library
|
|
only). By default it **pulls the upstream `manufacturers.xml` over the network**
|
|
(via `curl`) and writes the sorted two-column TSV (`decimal id \t name`) to
|
|
`manufacturers.tsv`:
|
|
|
|
```sh
|
|
python3 gen-manufacturers.py
|
|
```
|
|
|
|
The source is pinned to the same commit the HTML block was generated from, so a
|
|
fresh pull reproduces the current `manufacturers.tsv` exactly. To refresh from a
|
|
different upstream revision, pass a source — a URL (a GitHub `blob/` URL is
|
|
auto-rewritten to its `raw.githubusercontent.com` form) or a local XML file:
|
|
|
|
```sh
|
|
python3 gen-manufacturers.py https://.../manufacturers.xml
|
|
python3 gen-manufacturers.py ./manufacturers.xml
|
|
```
|
|
|
|
No network? Use the embedded block as the source instead:
|
|
|
|
```sh
|
|
python3 gen-manufacturers.py matter-qr-decoder.html
|
|
```
|
|
|
|
The same table is also embedded as a `<map>` XML block near the end of
|
|
`matter-qr-decoder.html` (marked "do not edit manually"). This script regenerates
|
|
only the TSV, so if you refresh the TSV from new upstream XML, also paste that XML
|
|
into the HTML's `<script type="text/xml">` block to keep the two copies in sync.
|
|
|
|
## Product name lookup
|
|
|
|
`matter-decode.sh` also maps a **Product ID** to a name, for any vendor that has
|
|
a catalog. The product map is `products.tsv` (three columns: `vendor ID \t
|
|
product ID \t name`); when the decoded vendor ID — or any of its equivalents, see
|
|
below — is present, the product ID is resolved to a name and printed on the
|
|
Product ID line. The table is optional — a missing file only drops the product
|
|
name, like `manufacturers.tsv` for the vendor name.
|
|
|
|
LIFX's rows (vendor `4870`) are derived from LIFX's upstream
|
|
`LIFX/products .../products.json` — **refresh them from there rather than
|
|
hand-editing.** One caveat: that file carries only a `pid` per product, with no
|
|
separate Matter/CHIP product-id field, so the lookup assumes LIFX's Matter
|
|
product ID equals their `pid`.
|
|
|
|
To refresh LIFX's rows, run **`gen-lifx-products.py`** (Python 3 standard
|
|
library only). By default it **pulls LIFX's `products.json` over the network**
|
|
(via `curl`, falling back to `urllib`) and **merges** the rows into
|
|
`products.tsv` — replacing LIFX's (vendor `4870`) rows in place and leaving any
|
|
other vendors' rows untouched:
|
|
|
|
```sh
|
|
python3 gen-lifx-products.py
|
|
```
|
|
|
|
To refresh from a different source, pass a URL or a local `.json` file:
|
|
|
|
```sh
|
|
python3 gen-lifx-products.py https://.../products.json
|
|
python3 gen-lifx-products.py ./products.json
|
|
```
|
|
|
|
Add another vendor the same way: a `gen-<vendor>-products.py` that parses that
|
|
vendor's catalog and merges its rows — keyed under its own vendor ID — into the
|
|
same `products.tsv`.
|
|
|
|
## Vendor equivalences
|
|
|
|
Some vendor IDs belong to the same company after a merger or acquisition — e.g.
|
|
Feit Electric (`5155`) acquired LIFX (`4870`). Record such equivalences in
|
|
`vendor-equivalents.tsv` (two columns: `alias vendor ID \t canonical vendor ID`;
|
|
lines starting with `#` are ignored). When a decoded vendor has an equivalent,
|
|
the product lookup also checks the equivalent's catalog and the Vendor ID line is
|
|
annotated `(a.k.a. <equivalent name>)`, so a Feit device resolves to a LIFX
|
|
product name and vice-versa.
|
|
|
|
The table is hand-edited and optional: a missing file simply means no equivalent
|
|
is considered. The canonical (col-2) id should not itself appear in col-1.
|
|
|
|
## License
|
|
|
|
The vendor mapping is derived from Matter/CHIP's `manufacturers.xml`, which is
|
|
Apache-2.0. The LIFX product mapping is derived from LIFX's `products.json`.
|
|
Vendor equivalences (`vendor-equivalents.tsv`) are maintained locally. The app
|
|
itself is a small single-file utility.
|