From c92f1f6062d95d8c9c496d73cda7e17a8d9e690e Mon Sep 17 00:00:00 2001 From: Scott Garrett Date: Mon, 26 Oct 2026 20:55:59 -0400 Subject: [PATCH] Adding AGENTS.md --- AGENTS.md | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..02b8b33 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,202 @@ +# AGENTS.md + +This file provides guidance to Codex (Codex.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 an `MT:` 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 (the `X-HM://` payload an accessory shows on first setup — a + *different* format from Matter's `MT:`). 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), consumed by + `matter-decode.sh`; 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. 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 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 payload `005KEOUCE`). +- Regenerate `manufacturers.tsv`: `python3 gen-manufacturers.py` pulls the + pinned upstream `manufacturers.xml` over the network; pass a URL or a local + `.xml` file to use a different source. +- Regenerate LIFX's `products.tsv` rows: `python3 gen-lifx-products.py` (network + by default; a positional URL or local `.json` overrides). +- 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-*.py` scripts 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: + +1. Validate the `MT:` prefix, then take the base38 payload after it. +2. 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`). +3. Read the 84-bit field layout **LSB-first**. Fixed field order/widths (see the + "Data Encoding Format" table in `README.md` 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 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 via + `get_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 via + `equivalent_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 reads `Feit 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--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-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 (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) + +- `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 + +- `matter-decode.sh` (Matter `MT:`, **base38**) and `homekit-decode.sh` + (HomeKit `X-HM://`, **base36**) decode *different* on-wire formats. Don't try + to unify the two decoders; when editing `homekit-decode.sh`, keep the + corrected bit layout (`pin / flag / categoryId / reserved / version`) as the + source of truth. +- `manufacturers.tsv` is the *only* copy of the vendor map now. Regenerate it with + `gen-manufacturers.py`: the 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), 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.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.