diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md index 455cf18..4feb469 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,25 @@ flow, discovery capabilities, discriminator, passcode). Three artifacts: another URL or a local XML file; passing `matter-qr-decoder.html` uses the embedded `` 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 @@ -74,6 +93,64 @@ Everything lives in `matter-qr-decoder.html` (~1500 lines). Two regions matter: `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--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 @@ -86,8 +163,9 @@ the lookup re-runs when the decoded vendor ID changes. - `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 only extra is a `manufacturers.tsv` - lookup for the vendor name; without that file it still decodes everything else. + 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 @@ -99,3 +177,12 @@ the lookup re-runs when the decoded vendor ID changes. `