Files
matterdecode/CLAUDE.md
T

15 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, users either paste an MT: URI or scan a QR code; the result shows the decoded payload (version, vendor/product IDs, commissioning flow, discovery capabilities, discriminator, passcode). Key files:

  • matter-qr-decoder.html — the single-file browser app (Vue 3 + Bootstrap).
  • matter-decode.sh — a pure-bash CLI port of the same decode logic (no camera scanning; takes a URI as an argument or on stdin).
  • homekit-decode.sh — a standalone pure-bash decoder for Apple HomeKit setup codes (the X-HM:// payload an accessory shows on first setup — a different format from Matter's MT:). No browser counterpart and no companion TSV (the accessory-category table is inlined). See the HomeKit setup-code decode section below.
  • manufacturers.tsv — vendor ID → name table (~900 rows), shared by both tools and optional (a missing file only drops the vendor-name lookup).
  • gen-manufacturers.py — Python 3 stdlib script that regenerates manufacturers.tsv. Default source is the upstream manufacturers.xml fetched over the network (via curl; a GitHub blob/ URL is auto-rewritten to its raw.githubusercontent.com form). A positional arg overrides with another URL or a local XML file; passing matter-qr-decoder.html uses the embedded <map> block as an offline fallback. Reproduces the committed TSV byte-for-byte.
  • products.tsv — 3-column vendor ID → product ID → name table, consumed by matter-decode.sh's get_product_name(): when the decoded vendor ID (or an equivalent, see vendor-equivalents.tsv) is present, the product ID is resolved to a name and printed on the Product ID line. LIFX's rows are keyed under 4870. Optional — a missing file only drops the product-name lookup.
  • vendor-equivalents.tsv — 2-column alias vendor ID → canonical vendor ID table, hand-edited, recording companies that are the same after a merger/acquisition (e.g. Feit 5155 → LIFX 4870). Used by matter-decode.sh so 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 (vendor 4870) in products.tsv. Default source is LIFX's upstream products.json fetched over the network (via curl, falling back to urllib); a positional arg overrides with another URL or a local .json file. It merges LIFX's rows into products.tsv (dropping LIFX's old rows, preserving every other vendor's), so re-running it is safe alongside other vendors' generators. Caveat: products.json carries only a pid per product (no separate Matter/CHIP product-id field), so the lookup assumes LIFX's Matter product ID equals their pid.
  • README.md — user-facing docs; this file documents the internals.

Commands

There is no build, test, lint, or package step — this is not a git repo and has no package.json. The entire app is one HTML file that runs in the browser:

  • Run/develop: open matter-qr-decoder.html in a modern browser and edit it directly.
  • Camera scanning (getUserMedia) requires a secure context — serve over HTTPS or localhost (e.g. python3 -m http.server then open http://localhost:8000).
  • The "Scan QR" feature uses the BarcodeDetector API, which is Chromium-only (Chrome/Edge). Firefox/Safari fall back to a "not supported" message; paste-input works everywhere.
  • The page loads Vue 3, Bootstrap 5.3, and Bootstrap Icons from CDNs, so it needs network access.

Architecture

Everything lives in matter-qr-decoder.html (~1500 lines). Two regions matter:

  1. App logic (the <script> before #app is mounted, ~lines 255-559): a single Vue.createApp({...}) mounted to #app, with all state in setup() via ref/computed. The UI template (lines ~26-253) is plain HTML using v-model/v-if/v-on bindings, no SFC or build step.
  2. Vendor map (<script type="text/xml">, ~lines 560-1495): a <map> of <mapping code="0xXXXX" translation="Name"/> entries. This block is the bulk of the file. It is derived from upstream project-chip/connectedhomeip .../zcl/data-model/manufacturers.xml (Apache-2.0, generated 2022-08-02) and carries an "do not edit manually" note — refresh it from upstream rather than hand-editing individual rows.

Decode pipeline (the core)

decodeUri() (triggered live on input) runs, in order:

  1. Validate the MT: prefix, then take the base38 payload after it.
  2. base38Decode(str) → Uint8Array. 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 throws Invalid base38 string length. Bytes are extracted little-endian (value & 0xFF, then >>= 8).
  3. extractBits(bytes, startBit, bitCount) reads the 84-bit field layout LSB-first. Fixed field order/widths (see the in-page "Data Encoding Format" table for the source of truth): version(3), vendor ID(16), product ID(16), custom flow(2), discovery caps(8), discriminator(12), passcode(27).
  4. Display helpers translate raw numbers: getCustomFlowName, getDiscoveryCapabilities (bit flags: SoftAP / BLE / On IP Network), formatVendorId/formatProductId (decimal + 0x-hex padded to 4).

Vendor name lookup

getVendorName(id) → parseVendorMappings() lazily parses the embedded XML once (cached on vendorMap, a let inside setup()) via DOMParser, keyed by parseInt(code, 16). It is exposed reactively through the vendorName computed, so the lookup re-runs when the decoded vendor ID changes.

Product name lookup + vendor equivalences (bash only)

Unlike the vendor-name lookup, the product-name lookup and the vendor-equivalence table live only in matter-decode.sh — the browser app has neither. This is a deliberate, documented divergence from the usual "keep the two in lockstep" rule, chosen because the product catalog and the equivalence list change more often than the vendor map and are not yet worth embedding in the static HTML.

matter-decode.sh defines two helpers:

  • equivalent_vendor_ids VENDOR — resolves VENDOR to its whole equivalence class via vendor-equivalents.tsv (2 cols alias \t canonical, # lines ignored, canonical-form: the canonical id never appears in col 1). It prints the vendor itself plus every alias sharing its canonical id, sorted. A missing vendor-equivalents.tsv yields just the vendor.
  • get_product_name VENDOR PRODUCT — checks products.tsv (3 cols vendor \t product \t name) for the exact vendor first, then every equivalent; first match wins. A missing products.tsv yields no name. It must return 0 on a miss: under set -e, a non-zero status in the pname=$(...) substitution would abort the script before anything prints.

In main(), the product name (pname) is printed on the Product ID line via the same [[ -n $pname ]] && printf pattern the vendor name uses. The vendor name is annotated with its equivalents: the other class members' names (from manufacturers.tsv) are joined and appended as (a.k.a. X, ...), so a Feit (5155) device reads Feit Electric Company, Inc. (a.k.a. Lifi Labs INC. dba LIFX) and a LIFX (4870) device reads the mirror image. Both products.tsv and vendor-equivalents.tsv are optional; a missing file only drops its extra.

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. Mirroring this into the HTML app is the natural next step if it is wanted.

Manual pairing code (bash 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); the browser app in this repo has neither this nor the product/equivalence lookups, so like them it is bash-only.

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-intent 1, custom 2) append 5-digit vendor and product groups. Result: 11 digits for the standard flow, 21 otherwise, plus the check digit.

HomeKit setup-code decode (bash only)

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 the HTML app's pipeline; it is standalone, with no browser counterpart and 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)
  • flag is a 4-bit sub-flag set — 1=NFC, 2=IP, 4=BLE, 8=WAC/MFi — printed as True/False lines.
  • categoryId is resolved to its HAP homekit_accessory_category_* name (e.g. 5 → "Lightbulb", 8 → "Switch") via an inlined case; an id not in the table prints "unknown" (id 25 is reserved by HomeKit, so it is unknown by design, and "Heater" corrects a "Header" typo in the source table). A case rather than an associative array is used on purpose so the script runs on the stock macOS bash 3.2 that #!/usr/bin/env bash resolves to.
  • The decoded fields are packed back into a 9-char block (mirroring composeSetupUri's stepwise packing) and printed as a reconstructed X-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

  • The BarcodeDetector scanner polls the camera via setInterval every 200ms and auto-stops the first time it sees a QR starting with MT:; stopScanning() clears the interval and stops the media tracks — keep those paired when changing scanning.
  • parseVendorMappings short-circuits on a non-empty vendorMap; if you add a mechanism to update the map, clear/reset that cache or new codes won't be seen.
  • The relative links /tools/ and / (header/footer) imply this page is deployed as part of a larger static site (Tommie Gannert's blog/tools), but that site isn't in this repo.
  • matter-decode.sh is a bash port of the HTML app's decode pipeline, kept in lockstep with it — the base38 charset/chunking, little-endian byte extraction, and the 84-bit LSB-first field layout are identical. When you change the decode logic in the HTML app, mirror it in the script (and vice versa). The script's extras are the optional manufacturers.tsv (vendor name), products.tsv (product name), and vendor-equivalents.tsv (vendor equivalences); without any of them it still decodes everything else.
  • homekit-decode.sh is not part of that lockstep: it decodes a different on-wire format (Apple HomeKit X-HM://, base36 — not Matter's base38) and has no HTML-app counterpart. Don't try to unify the two decoders or mirror HomeKit changes into the HTML app; when editing it, keep the corrected bit layout (pin / flag / categoryId / reserved / version) as the source of truth.
  • manufacturers.tsv is regenerated with gen-manufacturers.py (Python 3 stdlib). Default source is the upstream manufacturers.xml fetched over the network (via curl; a GitHub blob/ URL is auto-rewritten to its raw.githubusercontent.com form). A positional arg overrides with another URL or a local XML file, and passing matter-qr-decoder.html uses the embedded <map> block as an offline fallback. The default fetch is pinned to the same commit the HTML block came from, so it reproduces the committed TSV byte-for-byte. Caveat: the script writes only the TSV — the embedded <script type="text/xml"> block in the HTML app has the same upstream provenance and a "do not edit manually" note, so refreshing the TSV from new upstream XML leaves the HTML copy stale. Refresh both from the same source.
  • The product-name lookup and vendor equivalences are bash-only — matter-decode.sh has them, the browser app does not, so this is one place the two are not in lockstep (deliberate, for now). products.tsv and vendor-equivalents.tsv are optional and degrade silently (a missing file means no name / no equivalent, never an error). vendor-equivalents.tsv is canonical-form, one hop: the canonical id must not itself appear in col 1. get_product_name must return 0 on a miss or set -e aborts the decode. Mirror these into the HTML app (a second embedded map plus a productName / equivalentVendorName computed) only if that is wanted.