diff --git a/docs/fr/research/hid-lighting-protocol.md b/docs/fr/research/hid-lighting-protocol.md index 02297b3..e54ca73 100644 --- a/docs/fr/research/hid-lighting-protocol.md +++ b/docs/fr/research/hid-lighting-protocol.md @@ -92,6 +92,92 @@ attendu, affiché à l'utilisateur. `~/.claude/thread-status/slots.json` et pousse les couleurs d'état des six emplacements à chaque changement. +## Observations côté périphérique, via un shim indépendant + +Tout ce qui précède a été mesuré côté hôte : c'est nous qui écrivons vers le +clavier. Un projet MIT distinct, `maxxspotter/codex-micro-app`, fait l'inverse. +Son `apps/micro-shim/` patche `node-hid` à l'intérieur du processus Codex pour y +annoncer un Codex Micro synthétique, et observe donc le trafic que Codex envoie +*vers* le périphérique. Son cadrage correspond exactement à ce document — +report `0x06`, canal `2`, fragments de 61 octets, même descripteur — ce qui +corrobore la matrice ci-dessus de façon indépendante. + +Il y ajoutait quatre faits portant sur des **valeurs**, là où ce document ne +décrivait que des noms de champs. Trois des quatre ont depuis été **mesurés +ici** sur le firmware `v0.6.1` (capture du 2 août 2026, +`scripts/lighting.mjs listen`, chaque commande actionnée à la main). Le +quatrième est hors d'atteinte depuis le côté hôte. + +| Fait | Affirmation du shim | Mesuré ici | +| --- | --- | --- | +| Keycodes d'action dans `k` de `v.oai.hid` | `ACT06` fast, `ACT07` approve, `ACT08` reject, `ACT09` split, `ACT10` mic, `ACT12` send ; `ACT11` inexpliqué | **confirmé, et `ACT11` expliqué** — voir ci-dessous | +| Encodage du joystick dans `v.oai.rad` | `a` normalisé sur `[0, 1]` : droite `0`, bas `0,25`, gauche `0,5`, haut `0,75` ; `d` distance sur `[0, 1]` | **confirmé** : `0,0107`, `0,2388`, `0,4894`, `0,7614` à `d = 1`. Une divergence au relâchement, ci-dessous | +| Événements de la molette | `{k, act: 2}` pour un cran, `ENC_CLK` pour le clic, CW/CC inversés par rapport au sens physique | **confirmé** : `act: 2` à la rotation sans événement de relâche, `ENC_CLK` en `1`/`0` ; une série déclarée horaire a donné 30 `ENC_CC` et 0 `ENC_CW` | +| Codex interroge le périphérique | `sys.version`, et `device.status` renvoyant `{version, profile_index, layer_index, battery, is_charging}` | **confirmé** : la vraie réponse du périphérique est diffusée à tous les lecteurs — voir ci-dessous | + +### `ACT11` n'est pas une touche + +Un seul appui sur la touche large du bas émet **deux keycodes**, `ACT11` puis +`ACT10`, trois fois sur trois : + +``` +23:14:14.245 ACT11 act1 +5ms ACT10 act1 … ACT10 act0 +3ms ACT11 act0 +23:14:18.502 ACT11 act1 +6ms ACT10 act1 … ACT10 act0 +4ms ACT11 act0 +23:14:21.334 ACT11 act1 +6ms ACT10 act1 … ACT10 act0 +6ms ACT11 act0 +``` + +L'ordre est invariable, l'imbrication tient en 3 à 6 ms, et les durées d'appui +(163, 203, 213 ms) sont celles de toutes les autres touches de la même capture. +C'est **un actionneur physique occupant deux positions de matrice**, pas deux +touches. D'où le trou apparent chez le shim : `ACT11` n'a pas d'actionneur +propre à exposer. + +Le décompte en découle : **13 keycodes pour 12 actionneurs de touche**, plus le +clic de molette — c'est ainsi que se composent les 13 switches annoncés par le +README, par un assemblage différent de celui des 13 keycodes. + +### Le trafic propre de Codex est lisible d'ici + +L'ouverture non exclusive diffuse les reports d'entrée à **tous** les lecteurs, +ce qui inclut les réponses du périphérique à *Codex*, pas seulement aux nôtres. +Une capture brute contournant `hid-frame.mjs` montre la vraie réponse à +`device.status`, émise toutes les 60,009 s : + +```json +{"version":"v0.6.1","profile_index":0,"layer_index":1,"battery":100,"is_charging":false} +``` + +Le jeu de champs est exactement celui qu'annonçait le shim. `layer_index` +rapporte la couche active : le travail sur les couches prévu par la feuille de +route peut donc la lire sans aucun point d'observation côté périphérique. + +Cela corrige une affirmation antérieure de ce document, qui décrivait cette +charge utile comme inatteignable côté hôte. Elle ne l'est pas : le raisonnement +confondait « nous ne pouvons pas émettre les requêtes de Codex » et « nous ne +pouvons pas en voir les réponses », or la propriété de diffusion déjà documentée +plus haut rend la seconde fausse. + +### Deux divergences avec le shim + +- **Relâchement du joystick.** Le périphérique envoie `{a: 0, d: 0}` et remet + donc l'angle à zéro, là où le shim répète le dernier angle avec `d: 0`. Un + consommateur lisant l'angle au relâchement lirait « droite » sur le vrai + matériel. +- **Pas de champ `ag`.** Les touches Agent n'émettent que `{k, act}`. Le shim + envoie un index `ag` avec ses appuis Agent, et la matrice ci-dessus liste + `{k, act, ag}` : `ag` n'a jamais été observé dans cette capture. + +### Réserve sur les étiquettes du shim + +Le shim se contente d'observer : ses étiquettes de champs sont des déductions, +pas des mesures. Il lit une entrée de thread comme `{id, color: c, enabled: e, +effect: m}`, alors que `e` est l'énumération d'effet confirmée sur matériel ici +et que `m` apparaît dans la description des zones, pas dans les entrées de +thread. En cas de désaccord, c'est ce document qui a mesuré. Et la charge utile +de `device.status` est ce que le shim *prétend* être, non ce que rapporte un +vrai Micro : `profile_index` et `layer_index` restent une piste à sonder pour le +travail sur les couches prévu par la feuille de route. + ## Ce qui reste ouvert - La sémantique exacte de `sk` / `sa` (synchronisation de la couleur d'un @@ -102,6 +188,16 @@ emplacements à chaque changement. ensemble. - Si les identifiants de thread au-delà de 5 existent (autres touches) : aucun indice, non exploré. +- **Savoir si le périphérique perd des crans : non testé.** Plusieurs captures + ont rendu moins d'événements de rotation que l'opérateur entendait produire, + mais le compte physique n'a jamais été établi indépendamment — il reposait sur + un comptage de crans à la main, que l'opérateur a jugé peu fiable après coup. + Aucun taux de perte ne peut en être tiré, et aucun ne doit en être cité. Ce + qui est acquis, c'est qu'une telle perte ne viendrait pas de nous : une + capture brute contournant `hid-frame.mjs` a journalisé 38 reports, 30 crans et + **zéro ligne illisible**, donc chaque cran parvenu à l'hôte a été parsé et le + `catch` silencieux de `#dispatch` n'a rien avalé. Trancher demanderait un + compteur indépendant, pas humain. - La pérennité : le format est celui du firmware `v0.4.1` ; une mise à jour peut le faire évoluer sans prévenir. @@ -115,3 +211,9 @@ emplacements à chaque changement. - [`thread-status-feasibility.md`](thread-status-feasibility.md) — mesures amont (roster, hooks, contention, réponses orphelines dans le log d'Input). - [`appsense-behavior.md`](appsense-behavior.md) — contention et zones. +- [`maxxspotter/codex-micro-app`](https://github.com/maxxspotter/codex-micro-app) + (MIT), `apps/micro-shim/` — les observations côté périphérique ci-dessus. Sa + couche d'interception `node-hid` est elle-même adaptée de l'émulateur + [Codex Micro Stream Deck](https://github.com/mpociot/codex-micro-stream-deck-emulator) + de Marcel Pociot, sous licence MIT. Lecture pour documentation ; aucun code de + l'un ou l'autre projet n'est réutilisé ici. diff --git a/docs/research/hid-lighting-protocol.md b/docs/research/hid-lighting-protocol.md index ebfce73..c3f2948 100644 --- a/docs/research/hid-lighting-protocol.md +++ b/docs/research/hid-lighting-protocol.md @@ -15,6 +15,11 @@ events. Measurements on macOS `26.5.2` arm64, Codex Micro firmware `v0.4.1` (read through `sys.version`), `node-hid` `3.4.0`. +**Re-verified on firmware `v0.6.1`**, 2 August 2026, after a vendor update. The +framing, the RPC channel and `v.oai.thstatus` survive two minor versions +unchanged. The notification surface was measured in full on that occasion; the +results are in the section below. + ## Legal framing, restated This document describes an **observed format**: constants, byte positions, JSON @@ -90,6 +95,88 @@ the user. `~/.claude/thread-status/slots.json` and pushes the state colours of the six slots on every change. +## Device-side observations, from an independent shim + +Everything above was measured host-side: we write to the keyboard. A separate +MIT project, `maxxspotter/codex-micro-app`, does the opposite. Its +`apps/micro-shim/` patches `node-hid` inside the Codex desktop process to +advertise a synthetic Codex Micro, so it observes the traffic Codex sends *to* +the device. Its framing matches this document exactly — report `0x06`, channel +`2`, 61-byte chunks, same descriptor — which is independent corroboration of the +matrix above. + +It added four facts about **values**, where this document previously described +only field names. Three of the four have since been **measured here** on +firmware `v0.6.1` (capture of 2 August 2026, `scripts/lighting.mjs listen`, +every control actuated by hand). The fourth cannot be reached from the host +side. + +| Fact | Shim's claim | Measured here | +| --- | --- | --- | +| Action keycodes on `v.oai.hid` `k` | `ACT06` fast, `ACT07` approve, `ACT08` reject, `ACT09` split, `ACT10` mic, `ACT12` send; `ACT11` unexplained | **confirmed, plus the explanation of `ACT11`** — see below | +| Joystick encoding of `v.oai.rad` | `a` normalised over `[0, 1]`: right `0`, down `0.25`, left `0.5`, up `0.75`; `d` a distance over `[0, 1]` | **confirmed**: `0.0107`, `0.2388`, `0.4894`, `0.7614` at `d = 1`. One divergence on release, below | +| Encoder events | `{k, act: 2}` for a rotation notch, `ENC_CLK` for the click, CW/CC swapped relative to the physical direction | **confirmed**: `act: 2` on rotation with no release event, `ENC_CLK` in `1`/`0`; a declared-clockwise run gave 30 `ENC_CC` and 0 `ENC_CW` | +| Codex queries the device | `sys.version`, and `device.status` returning `{version, profile_index, layer_index, battery, is_charging}` | **confirmed**: the device's real answer is broadcast to every reader — see below | + +### `ACT11` is not a key + +A single press of the wide bottom key emits **two keycodes**, `ACT11` then +`ACT10`, three times out of three: + +``` +23:14:14.245 ACT11 act1 +5ms ACT10 act1 … ACT10 act0 +3ms ACT11 act0 +23:14:18.502 ACT11 act1 +6ms ACT10 act1 … ACT10 act0 +4ms ACT11 act0 +23:14:21.334 ACT11 act1 +6ms ACT10 act1 … ACT10 act0 +6ms ACT11 act0 +``` + +The order is invariant, the nesting is 3 to 6 ms, and the hold durations (163, +203, 213 ms) match every other key in the same capture. This is one physical +actuator occupying **two matrix positions**, not two keys. Hence the shim's +apparent gap: `ACT11` has no actuator of its own to expose. + +Counting follows from that: **13 keycodes for 12 key actuators**, plus the wheel +press — which is how the 13 switches announced in the README are made up, by a +different composition than the 13 keycodes. + +### Codex's own traffic is readable from here + +The non-exclusive open broadcasts input reports to **every** reader, which +includes the device's answers to *Codex*, not only to us. A raw capture that +bypasses `hid-frame.mjs` shows the real `device.status` response, emitted every +60.009 s: + +```json +{"version":"v0.6.1","profile_index":0,"layer_index":1,"battery":100,"is_charging":false} +``` + +The field set is exactly the one the shim claimed. `layer_index` reports the +active layer, so the roadmap's layer work can read it without any device-side +vantage point. + +This corrects an earlier statement in this document, which described that +payload as unreachable from the host side. It is not: the reasoning confused +"we cannot send Codex's requests" with "we cannot see the answers", and the +broadcast property already documented above makes the second one false. + +### Two divergences from the shim + +- **Joystick release.** The device sends `{a: 0, d: 0}`, resetting the angle. + The shim repeats the last angle with `d: 0`. A consumer reading the angle on + release would see "right" on real hardware. +- **No `ag` field.** The Agent keys emit `{k, act}` only. The shim sends an `ag` + index alongside agent taps, and the matrix above lists `{k, act, ag}` — `ag` + was never observed in this capture. + +### Caution on the shim's labels + +The shim only observes, so its field labels are inferences rather than +measurements: it reads a thread entry as `{id, color: c, enabled: e, effect: m}`, +whereas `e` is the effect enumeration confirmed on hardware here and `m` appears +in zone descriptions, not in thread entries. Where the two disagree, this +document is the measured one. And the `device.status` payload is what the shim +*claims* to be, not what a real Micro reports — `profile_index` and +`layer_index` remain a lead worth probing for the layer work in the roadmap. + ## What is still open - The exact semantics of `sk` / `sa` (syncing a thread's colour towards the key @@ -98,6 +185,16 @@ slots on every change. describes both zones at once, so the CLI requires `--keys` and `--ambient` together. - Whether thread ids beyond 5 exist (other keys): no clue, not explored. +- **Whether the device drops rotation notches at all: untested.** Several + captures returned fewer rotation events than the operator meant to produce, + but the physical count was never independently ground-truthed — it rested on + counting detents by hand, which the operator judged unreliable afterwards. No + drop rate can be derived from those runs, and none should be quoted from them. + What *is* settled is that any such loss would not be ours: a raw capture + bypassing `hid-frame.mjs` logged 38 reports, 30 notch events and **zero + unparseable lines**, so every notch that reached the host was parsed and the + silent `catch` in `#dispatch` swallowed nothing. Settling the question needs + an independent counter, not a human one. - Longevity: this is the format of firmware `v0.4.1`; an update may change it without notice. @@ -111,3 +208,8 @@ slots on every change. - [`thread-status-feasibility.md`](thread-status-feasibility.md) — upstream measurements (roster, hooks, contention, orphan responses in Input's log). - [`appsense-behavior.md`](appsense-behavior.md) — contention and zones. +- [`maxxspotter/codex-micro-app`](https://github.com/maxxspotter/codex-micro-app) + (MIT), `apps/micro-shim/` — the device-side observations above. Its `node-hid` + interception layer is itself adapted from Marcel Pociot's MIT-licensed + [Codex Micro Stream Deck emulator](https://github.com/mpociot/codex-micro-stream-deck-emulator). + Read for documentation; no code from either project is reused here.