189 lines
12 KiB
Markdown
189 lines
12 KiB
Markdown
# 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). 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). Three artifacts:
|
|
|
|
- `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).
|
|
- `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.
|
|
|
|
## 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.
|
|
- `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.
|