Files
matterdecode/README.md
T

262 lines
12 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. A command-line script decodes the Matter
`MT:` payload for you.
There is also a separate, standalone command-line decoder for Apple **HomeKit**
setup codes — the `X-HM://` payload an accessory shows on first setup. That is a
different format from Matter's `MT:` (see [HomeKit setup-code decoder](#homekit-setup-code-decoder));
it is a separate, standalone decoder with no companion tables.
## 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-decode.sh`](matter-decode.sh) | A pure-bash CLI decoder for the Matter `MT:` payload. No camera scanning. |
| [`homekit-decode.sh`](homekit-decode.sh) | A standalone pure-bash decoder for Apple **HomeKit** setup codes (the `X-HM://` payload) — a different format from Matter's `MT:`, with no companion tables. |
| [`manufacturers.tsv`](manufacturers.tsv) | Tab-separated `vendor ID → name` table (~900 rows) used to turn a numeric vendor ID into a name. Used by `matter-decode.sh`; 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 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.
---
## Command line
`matter-decode.sh` decodes a URI from an argument or from stdin and prints these
fields:
```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. The shell script uses this scheme.
## Vendor name lookup
`matter-decode.sh` maps 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 committed `manufacturers.tsv` was
generated from, so a fresh pull reproduces it 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
```
## 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.
## HomeKit setup-code decoder
`homekit-decode.sh` decodes a **HomeKit** setup code — the `X-HM://` payload an
Apple accessory (or its packaging) shows on first setup. It is a *different*
on-wire format from Matter's `MT:`, a standalone tool with no
companion tables — the accessory-category names are inlined in the script.
```sh
./homekit-decode.sh # defaults to the demo payload 005KEOUCE
./homekit-decode.sh "X-HM://005KEOUCE"
```
A leading `X-HM://` is stripped if present; the first 9 characters are the fixed
parameter block and anything after them is a variable-length **setup id**, kept
verbatim (it is not part of the numeric block).
Example output:
```
$ ./homekit-decode.sh
Setup PIN: 38313374
Flag: 10
Supports NFC: False
Supports IP: True
Supports BLE: False
Supports WAC/MFi: True
Category ID: 5 (Lightbulb)
Reserved: 0
Version: 0
Setup ID: <none>
Setup Payload: X-HM://005KEOUCE
```
### How it decodes
Unlike Matter's **base38** payload (`0-9 A-Z - .`), the 9-character HomeKit block
is **base36** (`0-9 A-Z`, 36 symbols) — an out-of-range character is a hard
error. The resulting integer is sliced **least-significant-bit first** into the
HomeKit field layout:
```
pin(27) | flag(4) | categoryId(8) | reserved(4) | version(3)
```
- **Flag** is a 4-bit sub-flag set: `1` = NFC, `2` = IP, `4` = BLE, `8` = WAC/MFi
(each printed True/False).
- **Category ID** is resolved to its HomeKit
[`homekit_accessory_category`](https://developer.apple.com/documentation/homekit/hmaccessorycategory)
name (e.g. `5` → Lightbulb, `8` → Switch); an id not in the table prints
"unknown".
- The fields are packed back into a 9-character block and printed as a
reconstructed `X-HM://…` payload, so the output is a complete record the barcode
can be rebuilt from.
The script uses the **corrected** bit layout that `composeSetupUri` in the
`homekit-code` package actually encodes; an earlier version had the
`categoryId` / `flag` / `version` positions misaligned. Each fix is noted in the
script's header comments.
---
## 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
HomeKit decoder's accessory-category names follow Apple's HomeKit
`homekit_accessory_category` definitions. The decoders are small standalone bash
utilities.