Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions docs/fr/research/hid-lighting-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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.
102 changes: 102 additions & 0 deletions docs/research/hid-lighting-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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.
Loading