12 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
A collection of Matter commissioning QR decoders (the MT: URI format used by
Matter/CHIP devices), plus a standalone bash decoder for Apple HomeKit
setup codes (the X-HM:// payload — a different on-wire format, in
homekit-decode.sh). For Matter, a user runs matter-decode.sh with an MT:
URI (as a positional argument or on stdin); it decodes the 84-bit payload and
prints the fields — version, vendor/product IDs, commissioning flow, discovery
capabilities, discriminator, passcode, and the derived manual pairing code. Key
files:
matter-decode.sh— the pure-bash CLI Matter decoder. Takes anMT:URI as an argument or on stdin and prints the decoded fields. It has no camera/QR-scanning (that was the only feature the former browser app added, and it is gone).homekit-decode.sh— a standalone pure-bash decoder for Apple HomeKit setup codes (theX-HM://payload an accessory shows on first setup — a different format from Matter'sMT:). No companion TSV (the accessory-category table is inlined). See the HomeKit setup-code decode section below.manufacturers.tsv—vendor ID → nametable (~900 rows), consumed bymatter-decode.sh; optional (a missing file only drops the vendor-name lookup).gen-manufacturers.py— Python 3 stdlib script that regeneratesmanufacturers.tsv. Default source is the upstreammanufacturers.xmlfetched over the network (viacurl; a GitHubblob/URL is auto-rewritten to itsraw.githubusercontent.comform). A positional arg overrides with another URL or a local XML file. Reproduces the committed TSV byte-for-byte.products.tsv— 3-columnvendor ID → product ID → nametable, consumed bymatter-decode.sh'sget_product_name(): when the decoded vendor ID (or an equivalent, seevendor-equivalents.tsv) is present, the product ID is resolved to a name and printed on the Product ID line. LIFX's rows are keyed under4870. Optional — a missing file only drops the product-name lookup.vendor-equivalents.tsv— 2-columnalias vendor ID → canonical vendor IDtable, hand-edited, recording companies that are the same after a merger/acquisition (e.g. Feit5155→ LIFX4870). Used bymatter-decode.shso a product resolves under either ID and the vendor name is annotated(a.k.a. ...).#-comment lines are ignored. Optional.gen-lifx-products.py— Python 3 stdlib script that regenerates LIFX's rows (vendor4870) inproducts.tsv. Default source is LIFX's upstreamproducts.jsonfetched over the network (viacurl, falling back tourllib); a positional arg overrides with another URL or a local.jsonfile. It merges LIFX's rows intoproducts.tsv(dropping LIFX's old rows, preserving every other vendor's), so re-running it is safe alongside other vendors' generators. Caveat:products.jsoncarries only apidper product (no separate Matter/CHIP product-id field), so the lookup assumes LIFX's Matter product ID equals theirpid.README.md— user-facing docs; this file documents the internals.
Commands
There is no build, test, lint, or package step — this is not a build project.
The two decoders are standalone bash scripts and the two gen-*.py are Python 3
stdlib regenerators; nothing is compiled or bundled.
- Run the Matter decoder:
./matter-decode.sh "MT:...", or pipe it:echo "MT:..." | ./matter-decode.sh. - Run the HomeKit decoder:
./homekit-decode.sh [code](with no argument it decodes the demo payload005KEOUCE). - Regenerate
manufacturers.tsv:python3 gen-manufacturers.pypulls the pinned upstreammanufacturers.xmlover the network; pass a URL or a local.xmlfile to use a different source. - Regenerate LIFX's
products.tsvrows:python3 gen-lifx-products.py(network by default; a positional URL or local.jsonoverrides). - The decoders read their companion TSVs (
manufacturers.tsv,products.tsv,vendor-equivalents.tsv) from their own directory; a missing TSV drops that extra lookup — it is never an error. - Pure bash (3.x+); the
gen-*.pyscripts need only the Python 3 standard library.
Architecture
The decode logic lives directly in the bash scripts — there is no shared library, no embedded data map, and no build output. There are two decoders, and they decode two different on-wire formats, so they are independent and should not be unified.
matter-decode.sh — the Matter MT: pipeline
main() decodes a URI given as an argument or read from stdin, in order:
- Validate the
MT:prefix, then take the base38 payload after it. - Base38-decode it to bytes. The charset is
0-9A-Z-.(38 chars). Chunking is greedy: 5 chars→3 bytes, 4→2, 2→1; a length that doesn't fit those groupings errors. Bytes are extracted little-endian (value & 0xFF, then>>= 8). - Read the 84-bit field layout LSB-first. Fixed field order/widths (see the
"Data Encoding Format" table in
README.mdfor the source of truth): version(3), vendor ID(16), product ID(16), custom flow(2), discovery caps(8), discriminator(12), passcode(27). - Display helpers translate the raw numbers: a custom-flow name, discovery
capabilities (bit flags: SoftAP / BLE / On IP Network), and decimal +
0x-hex (padded to 4) for the vendor/product IDs.
Vendor / product / equivalence lookups (matter-decode.sh only)
Three optional companion TSVs add names on top of the raw decode; each degrades silently (a missing file drops its extra, never an error):
manufacturers.tsv(vendor ID \t name) — resolves the decoded vendor ID to a name, printed on the Vendor ID line.products.tsv(vendor ID \t product ID \t name) — resolved viaget_product_name VENDOR PRODUCT, which checks the exact vendor first, then every equivalent; first match wins.vendor-equivalents.tsv(alias \t canonical,#lines ignored, canonical-form: the canonical id never appears in col 1) — resolved viaequivalent_vendor_ids VENDOR, which returns the whole equivalence class (the vendor plus every alias sharing its canonical id, sorted). The other class members' names are joined and appended to the Vendor ID line as(a.k.a. X, ...), so a Feit (5155) device readsFeit Electric Company, Inc. (a.k.a. Lifi Labs INC. dba LIFX)and a LIFX (4870) device reads the mirror image.
get_product_name must return 0 on a miss: under set -e, a non-zero status
in the pname=$(...) substitution would abort the script before anything prints.
gen-lifx-products.py regenerates LIFX's rows into products.tsv by merging
(dropping only vendor 4870's rows, preserving others). To add another vendor,
write a sibling 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;
then add that vendor's equivalences (if any) to vendor-equivalents.tsv.
Manual pairing code (matter-decode.sh only)
matter-decode.sh also prints a Pairing Code: line: the Matter manual
pairing code — the PIN a commissioning app (Apple Home, Google Home, the Matter
app) asks you to type. It is not a raw payload field; it is derived from
the passcode and discriminator and, for non-standard commissioning flows, the
vendor/product IDs, then guarded by a Verhoeff check digit. This is a port of
generateManualPairingCode() from the standalone "Matter QR Code Tool"
(chill-uk/matter-qr-app, web/app.js).
Two helpers, defined just above main():
verhoeff_check_digit DIGITS— the Verhoeff check digit over a decimal string (the three standard Verhoeff tables are inlined; digits are processed right-to-left, as the reference does).compute_pairing_code VENDOR PRODUCT FLOW DISCRIMINATOR PASSCODE— assembles the code. The 12-bit discriminator contributes its top 4 bits; the 27-bit passcode is split into its low 14 and high 13 bits; non-standard flows (user-intent1, custom2) append 5-digit vendor and product groups. Result: 11 digits for the standard flow, 21 otherwise, plus the check digit.
HomeKit setup-code decode (homekit-decode.sh)
homekit-decode.sh is a second, independent decoder — for Apple HomeKit
setup codes, the X-HM:// payload an accessory (or its packaging) shows on first
setup. That is a different on-wire format from Matter's MT:, so this is
not a port of matter-decode.sh's pipeline; it is standalone, with no
companion TSV (the accessory-category table is inlined). It sits alongside
matter-decode.sh as the repo's other CLI decoder.
Input is an optional positional argument, defaulting to the demo payload
005KEOUCE. A leading X-HM:// is stripped if present. The first 9 characters
are the fixed parameter block; anything after them is a variable-length setup
id, carried through verbatim (it is not part of the numeric block).
Unlike Matter's base38 payload (charset 0-9 A-Z - ., 38 symbols), the
HomeKit block is base36 (0-9 A-Z, 36 symbols): each char does
val = val*36 + digit, and an out-of-range character is a hard error (to
stderr, exit 1). val is then sliced LSB-first into the HomeKit layout:
pin(27) | flag(4) | categoryId(8) | reserved(4) | version(3)
flagis a 4-bit sub-flag set —1=NFC,2=IP,4=BLE,8=WAC/MFi — printed as True/False lines.categoryIdis resolved to its HAPhomekit_accessory_category_*name (e.g.5→ "Lightbulb",8→ "Switch") via an inlinedcase; an id not in the table prints "unknown" (id25is reserved by HomeKit, so it is unknown by design, and "Heater" corrects a "Header" typo in the source table). Acaserather than an associative array is used on purpose so the script runs on the stock macOS bash 3.2 that#!/usr/bin/env bashresolves to.- The decoded fields are packed back into a 9-char block (mirroring
composeSetupUri's stepwise packing) and printed as a reconstructedX-HM://…payload, so the output is a complete record the barcode can be rebuilt from.
This is the corrected bit layout — the one composeSetupUri in the
homekit-code package actually encodes — rather than a misaligned earlier one.
The header comments in homekit-decode.sh record each fix (the categoryId / flag
/ version positions, and the dropped "Thread" misread of categoryId bits).
Gotchas
matter-decode.sh(MatterMT:, base38) andhomekit-decode.sh(HomeKitX-HM://, base36) decode different on-wire formats. Don't try to unify the two decoders; when editinghomekit-decode.sh, keep the corrected bit layout (pin / flag / categoryId / reserved / version) as the source of truth.manufacturers.tsvis the only copy of the vendor map now. Regenerate it withgen-manufacturers.py: the default source is the upstreammanufacturers.xmlfetched over the network (viacurl; a GitHubblob/URL is auto-rewritten to itsraw.githubusercontent.comform), pinned to the same commit the committed TSV was generated from, so it reproduces the TSV byte-for-byte. A positional arg overrides with another URL or a local XML file.- The product-name lookup and vendor equivalences live only in
matter-decode.sh—products.tsvandvendor-equivalents.tsvare optional and degrade silently (a missing file means no name / no equivalent, never an error).vendor-equivalents.tsvis canonical-form, one hop: the canonical id must not itself appear in col 1.get_product_namemustreturn 0on a miss orset -eaborts the decode.