Files

12 KiB

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); it is a separate, standalone decoder with no companion tables.

What it decodes

Given an MT: URI, the payload (84 bits, see 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 A pure-bash CLI decoder for the Matter MT: payload. No camera scanning.
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 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 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 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 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 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:

./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:

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:

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:

python3 gen-lifx-products.py

To refresh from a different source, pass a URL or a local .json file:

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.

./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 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.