From 87408da6d93a75cf56306544d191c74ae74eb3f9 Mon Sep 17 00:00:00 2001 From: Thanh Chau <1320427+thannous@users.noreply.github.com> Date: Mon, 3 Aug 2026 00:47:26 +0200 Subject: [PATCH 1/3] docs: record device-side HID observations from an independent shim The lighting protocol document was written entirely from the host side: we write to the keyboard and never see what Codex sends to it. The micro-shim of maxxspotter/codex-micro-app takes the opposite position, presenting a synthetic Micro inside the Codex process, so it observes that half of the exchange. Its framing matches ours exactly, which corroborates the evidence matrix independently. Four value-level facts are new: the ACT06-ACT12 action key names, the normalised angles of v.oai.rad, act 2 for encoder notches, and the sys.version / device.status requests Codex sends to the device. All four are marked as read from a third-party source and never reproduced here, and three of them join the open questions. Where the shim's inferred field labels contradict our hardware measurements, the document keeps the measurement. Attribution added to Sources, for that project and for the MIT-licensed Stream Deck emulator it adapts. Co-Authored-By: Claude Opus 5 --- docs/fr/research/hid-lighting-protocol.md | 40 +++++++++++++++++++++++ docs/research/hid-lighting-protocol.md | 38 +++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/docs/fr/research/hid-lighting-protocol.md b/docs/fr/research/hid-lighting-protocol.md index 02297b3..af41d7e 100644 --- a/docs/fr/research/hid-lighting-protocol.md +++ b/docs/fr/research/hid-lighting-protocol.md @@ -92,6 +92,37 @@ 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. + +Quatre faits s'y ajoutent. Tous sont **lus dans les sources de ce projet, non +vérifiés ici** ; ils décrivent des valeurs, là où ce document ne décrivait +jusqu'ici que des noms de champs. + +| Fait | Détail | +| --- | --- | +| Noms des touches d'action portés par `k` dans `v.oai.hid` | `ACT06` fast, `ACT07` approve, `ACT08` reject, `ACT09` split, `ACT10` mic, `ACT12` send. `ACT11` reste inexpliqué. Les touches Agent sont `AG00`–`AG05`, comme mesuré ici. | +| Encodage du joystick dans `v.oai.rad` | `a` est normalisé sur `[0, 1]` : droite = `0`, bas = `0,25`, gauche = `0,5`, haut = `0,75`. `d` est une distance sur `[0, 1]` ; un relâchement répète l'angle avec `d: 0`, après 80 ms dans le shim. | +| Événements de la molette | `{k: "ENC_CW" \| "ENC_CC", act: 2}` — `act` 2 signale un cran de rotation, distinct du `1`/`0` presse/relâche des touches. Le clic est `ENC_CLK`. Le shim inverse volontairement CW et CC, au motif que Codex nomme les directions vues du dessous du boîtier. | +| Codex interroge le périphérique | `sys.version`, et `device.status` attendant `{version, profile_index, layer_index, battery, is_charging}`. Le shim répond `true` à toute requête qu'il ne comprend pas, en précisant que la file RPC de Codex est sérialisée et se bloque sur un identifiant non acquitté. | + +Deux réserves. 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` sont +une piste à sonder pour le travail sur les couches prévu par la feuille de +route, pas une mesure. + ## Ce qui reste ouvert - La sémantique exacte de `sk` / `sa` (synchronisation de la couleur d'un @@ -102,6 +133,9 @@ emplacements à chaque changement. ensemble. - Si les identifiants de thread au-delà de 5 existent (autres touches) : aucun indice, non exploré. +- Les quatre faits côté périphérique ci-dessus : lus dans une source tierce, + jamais reproduits ici. `ACT11`, la vraie charge utile de `device.status` d'un + Micro et le sens physique de `ENC_CW` sont les trois à mesurer en premier. - La pérennité : le format est celui du firmware `v0.4.1` ; une mise à jour peut le faire évoluer sans prévenir. @@ -115,3 +149,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..03af64d 100644 --- a/docs/research/hid-lighting-protocol.md +++ b/docs/research/hid-lighting-protocol.md @@ -90,6 +90,36 @@ 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. + +Four facts it adds. All are **read from that project's source, not verified +here**; they describe values, where this document so far only described field +names. + +| Fact | Detail | +| --- | --- | +| Action key names carried by `v.oai.hid` `k` | `ACT06` fast, `ACT07` approve, `ACT08` reject, `ACT09` split, `ACT10` mic, `ACT12` send. `ACT11` is unaccounted for. Agent keys are `AG00`–`AG05`, as measured here. | +| Joystick encoding of `v.oai.rad` | `a` is normalised over `[0, 1]`: right = `0`, down = `0.25`, left = `0.5`, up = `0.75`. `d` is a distance over `[0, 1]`; a release repeats the angle with `d: 0`, after 80 ms in the shim. | +| Encoder events | `{k: "ENC_CW" \| "ENC_CC", act: 2}` — `act` 2 marks a rotation notch, distinct from the `1`/`0` press/release of keys. The click is `ENC_CLK`. The shim swaps CW and CC deliberately, on the grounds that Codex names the directions as seen from the underside of the case. | +| Codex queries the device | `sys.version`, and `device.status` expecting `{version, profile_index, layer_index, battery, is_charging}`. The shim answers every request it does not understand with `true`, stating that Codex's RPC queue is serialised and stalls on an unanswered id. | + +Two cautions. 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` are a lead worth probing for the layer work in the roadmap, +not a measurement. + ## What is still open - The exact semantics of `sk` / `sa` (syncing a thread's colour towards the key @@ -98,6 +128,9 @@ 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. +- The four device-side facts above: read from a third-party source, never + reproduced here. `ACT11`, the real `device.status` payload of a Micro, and the + physical direction of `ENC_CW` are the three worth measuring first. - Longevity: this is the format of firmware `v0.4.1`; an update may change it without notice. @@ -111,3 +144,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. From 8aa5bf6f50fd654f8f72f6d83c3a08a2966d34df Mon Sep 17 00:00:00 2001 From: Thanh Chau <1320427+thannous@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:22:50 +0200 Subject: [PATCH 2/3] docs: measure the device-side HID facts on firmware v0.6.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four facts borrowed from the micro-shim were recorded as third-party and unverified. Three are now measured here, by actuating every control by hand against scripts/lighting.mjs listen. The fourth, what Codex sends to the device, stays out of reach from the host side. ACT11 is not a key. One press of the wide bottom key emits ACT11 then ACT10, nested within 3 to 6 ms, three times out of three, with hold durations matching every other key in the capture. It is a single actuator on two matrix positions, which is why the shim has nothing to expose there. That also settles the count: 13 keycodes for 12 key actuators, plus the wheel press. The shim's joystick angles hold at the digit: 0.0107, 0.2388, 0.4894, 0.7614 for right, down, left, up. Two divergences appear against it — the device resets the angle to zero on release rather than repeating it, and the Agent keys carry no ag field, which the evidence matrix wrongly listed. The channel itself survives the vendor update from v0.4.1 to v0.6.1 unchanged. Dropped encoder notches join the open questions. Five slow notches produced four events while a fast burst produced five, so speed does not explain the loss and the 90 ms wheel calibration is not implicated; why an isolated slow notch disappears is unexplained. The direction of rotation in that capture rests on the operator's intent, not on an independent signal, so clockwise to ENC_CC still stands on the earlier hardware measurement rather than on this one. Co-Authored-By: Claude Opus 5 --- docs/fr/research/hid-lighting-protocol.md | 92 +++++++++++++++++------ docs/research/hid-lighting-protocol.md | 92 +++++++++++++++++------ 2 files changed, 141 insertions(+), 43 deletions(-) diff --git a/docs/fr/research/hid-lighting-protocol.md b/docs/fr/research/hid-lighting-protocol.md index af41d7e..da9c3b9 100644 --- a/docs/fr/research/hid-lighting-protocol.md +++ b/docs/fr/research/hid-lighting-protocol.md @@ -102,26 +102,60 @@ annoncer un Codex Micro synthétique, et observe donc le trafic que Codex envoie report `0x06`, canal `2`, fragments de 61 octets, même descripteur — ce qui corrobore la matrice ci-dessus de façon indépendante. -Quatre faits s'y ajoutent. Tous sont **lus dans les sources de ce projet, non -vérifiés ici** ; ils décrivent des valeurs, là où ce document ne décrivait -jusqu'ici que des noms de champs. +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 | Détail | -| --- | --- | -| Noms des touches d'action portés par `k` dans `v.oai.hid` | `ACT06` fast, `ACT07` approve, `ACT08` reject, `ACT09` split, `ACT10` mic, `ACT12` send. `ACT11` reste inexpliqué. Les touches Agent sont `AG00`–`AG05`, comme mesuré ici. | -| Encodage du joystick dans `v.oai.rad` | `a` est normalisé sur `[0, 1]` : droite = `0`, bas = `0,25`, gauche = `0,5`, haut = `0,75`. `d` est une distance sur `[0, 1]` ; un relâchement répète l'angle avec `d: 0`, après 80 ms dans le shim. | -| Événements de la molette | `{k: "ENC_CW" \| "ENC_CC", act: 2}` — `act` 2 signale un cran de rotation, distinct du `1`/`0` presse/relâche des touches. Le clic est `ENC_CLK`. Le shim inverse volontairement CW et CC, au motif que Codex nomme les directions vues du dessous du boîtier. | -| Codex interroge le périphérique | `sys.version`, et `device.status` attendant `{version, profile_index, layer_index, battery, is_charging}`. Le shim répond `true` à toute requête qu'il ne comprend pas, en précisant que la file RPC de Codex est sérialisée et se bloque sur un identifiant non acquitté. | - -Deux réserves. 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` sont -une piste à sonder pour le travail sur les couches prévu par la feuille de -route, pas une mesure. +| 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` ; l'inversion recoupe [`effort-wheel-calibration.md`](effort-wheel-calibration.md) | +| Codex interroge le périphérique | `sys.version`, et `device.status` renvoyant `{version, profile_index, layer_index, battery, is_charging}` | **non vérifié** : inatteignable côté hôte, où l'on ne voit jamais ce que Codex envoie au périphérique | + +### `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. + +### 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 @@ -133,9 +167,23 @@ route, pas une mesure. ensemble. - Si les identifiants de thread au-delà de 5 existent (autres touches) : aucun indice, non exploré. -- Les quatre faits côté périphérique ci-dessus : lus dans une source tierce, - jamais reproduits ici. `ACT11`, la vraie charge utile de `device.status` d'un - Micro et le sens physique de `ENC_CW` sont les trois à mesurer en premier. +- **Crans de molette perdus.** Dans la capture `v0.6.1`, cinq crans lents et + délibérés (espacés de 1,7 à 2,3 s) n'ont produit que quatre événements, tandis + qu'une rafale rapide en sens inverse en a produit cinq, dont deux à 99 ms + d'écart. Une capture antérieure avait perdu un cran sur quatre. La perte n'est + donc **pas** expliquée par la vitesse de rotation — c'est la moitié + rassurante ; la moitié inexpliquée, c'est qu'un cran lent et isolé se perde. + À reprendre en série comptée et instrumentée avant de se fier à une + correspondance cran par cran pour l'effort. +- Le sens de rotation dans cette capture repose sur l'intention de l'opérateur, + pas sur un signal indépendant : les deux phases de rotation devaient toutes + deux être horaires et ont émis des keycodes opposés. La lecture + `horaire → ENC_CC` tient donc toujours sur la mesure matérielle consignée dans + [`effort-wheel-calibration.md`](effort-wheel-calibration.md), avec laquelle + cette capture est cohérente sans la re-prouver par elle-même. +- La vraie charge utile de `device.status` d'un Micro, et `profile_index` / + `layer_index` en particulier : inatteignables côté hôte, il faudrait un point + d'observation côté périphérique. - La pérennité : le format est celui du firmware `v0.4.1` ; une mise à jour peut le faire évoluer sans prévenir. diff --git a/docs/research/hid-lighting-protocol.md b/docs/research/hid-lighting-protocol.md index 03af64d..e5b65db 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 @@ -100,25 +105,57 @@ 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. -Four facts it adds. All are **read from that project's source, not verified -here**; they describe values, where this document so far only described field -names. +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 | Detail | -| --- | --- | -| Action key names carried by `v.oai.hid` `k` | `ACT06` fast, `ACT07` approve, `ACT08` reject, `ACT09` split, `ACT10` mic, `ACT12` send. `ACT11` is unaccounted for. Agent keys are `AG00`–`AG05`, as measured here. | -| Joystick encoding of `v.oai.rad` | `a` is normalised over `[0, 1]`: right = `0`, down = `0.25`, left = `0.5`, up = `0.75`. `d` is a distance over `[0, 1]`; a release repeats the angle with `d: 0`, after 80 ms in the shim. | -| Encoder events | `{k: "ENC_CW" \| "ENC_CC", act: 2}` — `act` 2 marks a rotation notch, distinct from the `1`/`0` press/release of keys. The click is `ENC_CLK`. The shim swaps CW and CC deliberately, on the grounds that Codex names the directions as seen from the underside of the case. | -| Codex queries the device | `sys.version`, and `device.status` expecting `{version, profile_index, layer_index, battery, is_charging}`. The shim answers every request it does not understand with `true`, stating that Codex's RPC queue is serialised and stalls on an unanswered id. | - -Two cautions. 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` are a lead worth probing for the layer work in the roadmap, -not a measurement. +| 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`; the swap matches [`effort-wheel-calibration.md`](effort-wheel-calibration.md) | +| Codex queries the device | `sys.version`, and `device.status` returning `{version, profile_index, layer_index, battery, is_charging}` | **not verified**: unreachable from the host side, where we never see what Codex sends to the device | + +### `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. + +### 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 @@ -128,9 +165,22 @@ not a measurement. 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. -- The four device-side facts above: read from a third-party source, never - reproduced here. `ACT11`, the real `device.status` payload of a Micro, and the - physical direction of `ENC_CW` are the three worth measuring first. +- **Dropped encoder notches.** In the `v0.6.1` capture, five deliberate slow + notches (1.7 to 2.3 s apart) produced only four events, while a fast burst in + the other direction produced five, two of them 99 ms apart. An earlier capture + lost one notch out of four. Loss is therefore **not** explained by rotation + speed, which is the reassuring half; the unexplained half is why a slow, + isolated notch goes missing at all. Worth a counted, instrumented run before + trusting a notch-per-notch effort mapping. +- The direction of rotation in that capture rests on the operator's intent, not + on an independent signal: the two rotation phases were both meant to be + clockwise and emitted opposite keycodes. The `clockwise → ENC_CC` reading + therefore still stands on the hardware measurement recorded in + [`effort-wheel-calibration.md`](effort-wheel-calibration.md), which this + capture is consistent with but does not by itself re-prove. +- The real `device.status` payload of a Micro, and `profile_index` / + `layer_index` in particular: unreachable from the host side, would need a + device-side vantage point. - Longevity: this is the format of firmware `v0.4.1`; an update may change it without notice. From 9157ccfa14ba57156010639e6d2b96bdecceb07c Mon Sep 17 00:00:00 2001 From: Thanh Chau <1320427+thannous@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:32:35 +0200 Subject: [PATCH 3/3] docs: correct the device.status claim, retract the dropped-notch anomaly Two corrections to the section added in the previous commit, both from a raw capture that bypasses hid-frame.mjs and logs every 64-byte report. device.status was recorded as unreachable from the host side. It is not. The non-exclusive open broadcasts input reports to every reader, so the device's answers to Codex are visible here too, and the real payload arrives every 60.009 s carrying layer_index, which the roadmap's layer work can now read without a device-side vantage point. The earlier reasoning confused being unable to send Codex's requests with being unable to see the answers. The dropped-notch anomaly is retracted as a phenomenon. It rested on comparing event counts against detents counted by hand, a ground truth the operator judged unreliable afterwards, so no drop rate follows from those runs. What the raw capture does establish is that any such loss would not be ours: 38 reports, 30 notch events, zero unparseable lines. Settling the question needs an independent counter rather than a human one. The same capture also confirms the direction of rotation on a run whose direction was declared in advance: 30 ENC_CC and 0 ENC_CW for clockwise. Co-Authored-By: Claude Opus 5 --- docs/fr/research/hid-lighting-protocol.md | 52 ++++++++++++++--------- docs/research/hid-lighting-protocol.md | 50 ++++++++++++++-------- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/docs/fr/research/hid-lighting-protocol.md b/docs/fr/research/hid-lighting-protocol.md index da9c3b9..e54ca73 100644 --- a/docs/fr/research/hid-lighting-protocol.md +++ b/docs/fr/research/hid-lighting-protocol.md @@ -112,8 +112,8 @@ quatrième est hors d'atteinte depuis le côté hôte. | --- | --- | --- | | 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` ; l'inversion recoupe [`effort-wheel-calibration.md`](effort-wheel-calibration.md) | -| Codex interroge le périphérique | `sys.version`, et `device.status` renvoyant `{version, profile_index, layer_index, battery, is_charging}` | **non vérifié** : inatteignable côté hôte, où l'on ne voit jamais ce que Codex envoie au périphérique | +| É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 @@ -136,6 +136,27 @@ Le décompte en découle : **13 keycodes pour 12 actionneurs de touche**, plus l 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 @@ -167,23 +188,16 @@ travail sur les couches prévu par la feuille de route. ensemble. - Si les identifiants de thread au-delà de 5 existent (autres touches) : aucun indice, non exploré. -- **Crans de molette perdus.** Dans la capture `v0.6.1`, cinq crans lents et - délibérés (espacés de 1,7 à 2,3 s) n'ont produit que quatre événements, tandis - qu'une rafale rapide en sens inverse en a produit cinq, dont deux à 99 ms - d'écart. Une capture antérieure avait perdu un cran sur quatre. La perte n'est - donc **pas** expliquée par la vitesse de rotation — c'est la moitié - rassurante ; la moitié inexpliquée, c'est qu'un cran lent et isolé se perde. - À reprendre en série comptée et instrumentée avant de se fier à une - correspondance cran par cran pour l'effort. -- Le sens de rotation dans cette capture repose sur l'intention de l'opérateur, - pas sur un signal indépendant : les deux phases de rotation devaient toutes - deux être horaires et ont émis des keycodes opposés. La lecture - `horaire → ENC_CC` tient donc toujours sur la mesure matérielle consignée dans - [`effort-wheel-calibration.md`](effort-wheel-calibration.md), avec laquelle - cette capture est cohérente sans la re-prouver par elle-même. -- La vraie charge utile de `device.status` d'un Micro, et `profile_index` / - `layer_index` en particulier : inatteignables côté hôte, il faudrait un point - d'observation côté périphérique. +- **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. diff --git a/docs/research/hid-lighting-protocol.md b/docs/research/hid-lighting-protocol.md index e5b65db..c3f2948 100644 --- a/docs/research/hid-lighting-protocol.md +++ b/docs/research/hid-lighting-protocol.md @@ -115,8 +115,8 @@ side. | --- | --- | --- | | 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`; the swap matches [`effort-wheel-calibration.md`](effort-wheel-calibration.md) | -| Codex queries the device | `sys.version`, and `device.status` returning `{version, profile_index, layer_index, battery, is_charging}` | **not verified**: unreachable from the host side, where we never see what Codex sends to the device | +| 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 @@ -138,6 +138,26 @@ 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. @@ -165,22 +185,16 @@ document is the measured one. And the `device.status` payload is what the shim 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. -- **Dropped encoder notches.** In the `v0.6.1` capture, five deliberate slow - notches (1.7 to 2.3 s apart) produced only four events, while a fast burst in - the other direction produced five, two of them 99 ms apart. An earlier capture - lost one notch out of four. Loss is therefore **not** explained by rotation - speed, which is the reassuring half; the unexplained half is why a slow, - isolated notch goes missing at all. Worth a counted, instrumented run before - trusting a notch-per-notch effort mapping. -- The direction of rotation in that capture rests on the operator's intent, not - on an independent signal: the two rotation phases were both meant to be - clockwise and emitted opposite keycodes. The `clockwise → ENC_CC` reading - therefore still stands on the hardware measurement recorded in - [`effort-wheel-calibration.md`](effort-wheel-calibration.md), which this - capture is consistent with but does not by itself re-prove. -- The real `device.status` payload of a Micro, and `profile_index` / - `layer_index` in particular: unreachable from the host side, would need a - device-side vantage point. +- **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.