# La langue du lecteur : ISO/IEEE 11073

Ouvrir le tuyau USB, c'est bien. Mais si on envoie n'importe quoi dedans, le lecteur ne répond rien, ou pire, il coupe la communication. Il faut parler sa langue. Et c'est là que commence vraiment le **reverse engineering**.

### D'où vient la connaissance du protocole ?[![medecin.png](https://librenard.fr/wiki/uploads/images/gallery/2026-10/scaled-1680-/medecin.png)](https://librenard.fr/wiki/uploads/images/gallery/2026-10/medecin.png)

Roche ne publie pas de documentation sur le protocole de ses lecteurs. Par contre, il existe un endroit où quelqu'un l'a déjà implémenté : le pilote Roche du [Tidepool uploader](https://github.com/tidepool-org/uploader/tree/master/lib/drivers/roche), écrit en JavaScript.

Le chemin a donc été le suivant :

1. **emogenet**, l'auteur d'origine, a lu le code JavaScript de Tidepool, récupéré les constantes et l'ordre des messages, et réécrit l'ensemble en C++ au-dessus de libusb. Il le dit lui-même en commentaire dans le code : le pilote Tidepool "ne marche que dans Chrome", et son auteur "avait sans doute accès à un manuel décrivant le protocole".
2. En lisant ces constantes, on retrouve des noms comme `MDC_ATTR_TIME_ABS` ou `MDC_MOC_VMO_PMSTORE`. En cherchant ces noms, on tombe sur la norme **ISO/IEEE 11073-20601**, celle des "Personal Health Devices" de la [Continua Health Alliance](https://en.wikipedia.org/wiki/Continua_Health_Alliance). C'est le protocole qui va avec la classe USB PHDC qu'on a vue dans `lsusb`.
3. Pour tout ce que Tidepool ne lisait pas (l'identité du lecteur, son horloge, les marqueurs repas), je suis reparti de la norme et des projets libres qui l'implémentent, comme [Antidote](https://github.com/signove/antidote), puis j'ai comparé avec les vraies réponses de notre Accu-Chek Guide, capturés octets par octet.

<p class="callout info">Pour voir passer chaque paquet, il suffit de lancer `ACCUCHEK_DBG=1 accuchek`. Le programme écrit alors sur la sortie d'erreur un "hexdump" de chaque message envoyé et reçu, avec le numéro de l'étape. C'est l'outil principal de tout ce travail : on regarde les octets, on fait une hypothèse, on vérifie sur le message suivant.</p>

### Le principe : un "agent" et un "manager"

Dans cette norme, il y a deux rôles :

- **l'agent** : l'appareil de santé, ici le lecteur. C'est lui qui a les données ;
- **le manager** : celui qui les récupère, ici notre PC.

Le lecteur ne se contente pas d'envoyer une liste de chiffres. Il se décrit comme un ensemble d'**objets**, un peu comme une petite base de données :

- l'objet **MDS** (Medical Device System, handle 0) : le lecteur lui-même, avec son fabricant, son modèle, son numéro de série, son horloge ;
- l'objet **PM-Store** (Persistent Metric Store) : la mémoire où sont rangées les mesures ;
- des **segments** à l'intérieur du PM-Store : un segment pour les glycémies, un pour les mesures de contrôle, un pour les erreurs, un pour les marqueurs repas...

Chaque objet a un numéro (son "handle") et des **attributs**. Chaque attribut a un identifiant numérique standard, c'est la "nomenclature MDC". Par exemple :

<table id="bkmrk-nom-valeur-ce-que-c%27"><thead><tr><th>Nom</th><th>Valeur</th><th>Ce que c'est</th></tr></thead><tbody><tr><td><code>MDC&#95;MOC&#95;VMO&#95;PMSTORE</code></td><td>61</td><td>la classe "PM-Store", la mémoire des mesures</td></tr><tr><td><code>MDC&#95;ATTR&#95;NUM&#95;SEG</code></td><td>2385</td><td>nombre de segments dans le PM-Store</td></tr><tr><td><code>MDC&#95;ATTR&#95;ID&#95;MODEL</code></td><td>2344</td><td>fabricant et modèle</td></tr><tr><td><code>MDC&#95;ATTR&#95;TIME&#95;ABS</code></td><td>2439</td><td>l'horloge du lecteur</td></tr><tr><td><code>MDC&#95;ATTR&#95;SEG&#95;USAGE&#95;CNT</code></td><td>2427</td><td>nombre d'entrées dans un segment</td></tr><tr><td><code>MDC&#95;CTXT&#95;GLU&#95;MEAL&#95;FASTING</code></td><td>29268</td><td>marqueur "à jeun"</td></tr></tbody></table>

Dans `protocol.h`, toutes ces constantes sont rangées dans une grande liste `MDC_LIST`. Une petite astuce de préprocesseur C (les "X-macros") permet d'en tirer à la fois une énumération et une fonction `findKeyByValue` qui retrouve le nom à partir du numéro. Bien pratique quand on lit des logs.

### La forme d'un message

Tous les messages s'appellent des **APDU**. Ils commencent tous par 2 octets de type et 2 octets de longueur :

<table id="bkmrk-type-message-qui-l%27e"><thead><tr><th>Type</th><th>Message</th><th>Qui l'envoie</th></tr></thead><tbody><tr><td><code>E2 00</code></td><td>demande d'association ("on se connecte ?")</td><td>le lecteur</td></tr><tr><td><code>E3 00</code></td><td>réponse d'association ("d'accord")</td><td>le PC</td></tr><tr><td><code>E4 00</code></td><td>demande de fin d'association ("on raccroche")</td><td>le PC</td></tr><tr><td><code>E5 00</code></td><td>réponse de fin d'association</td><td>le lecteur</td></tr><tr><td><code>E6 00</code></td><td>abandon ("je coupe tout")</td><td>l'un ou l'autre</td></tr><tr><td><code>E7 00</code></td><td>message de données (tout le reste)</td><td>les deux</td></tr></tbody></table>

Les messages de données `E7 00` ont une structure un peu plus riche :

```bash
E7 00    type : message de données
00 0E    longueur de ce qui suit
00 0C    longueur du contenu
00 11    invoke-id : numéro de la question
01 03    type d'opération : ici "GET" (lire des attributs)
00 06    longueur de l'opération
00 00    handle de l'objet visé (0 = le lecteur lui-même)
...      le reste dépend de l'opération
```

Le type d'opération (le "data choice") dit ce que fait le message :

<table id="bkmrk-code-signification-0"><thead><tr><th>Code</th><th>Signification</th></tr></thead><tbody><tr><td><code>01 01</code></td><td>le lecteur signale un événement et attend un accusé de réception</td></tr><tr><td><code>02 01</code></td><td>l'accusé de réception de cet événement</td></tr><tr><td><code>01 03</code></td><td>demande de lecture d'attributs (GET)</td></tr><tr><td><code>02 03</code></td><td>réponse à cette lecture</td></tr><tr><td><code>01 07</code></td><td>demande d'action (lister les segments, lancer un transfert, régler l'heure...)</td></tr><tr><td><code>02 07</code></td><td>réponse à cette action</td></tr></tbody></table>

### L'invoke-id, ou le numéro de ticket

L'**invoke-id**, c'est le numéro de ticket de la conversation. Chaque question porte un numéro, et la réponse reprend le même numéro. Le lecteur s'attend à ce que le numéro de la question suivante soit le précédent plus un. Si on se trompe de numéro, il considère que la conversation est cassée.

Dans le code, après chaque message reçu, `updateInvokeId()` relit ce numéro (octets 6 et 7), et les messages que l'on construit utilisent `invokeId + 1`. C'est une des choses les plus faciles à rater quand on réécrit le protocole, et le lecteur ne prévient pas : il abandonne, tout simplement.

### Tout est en "big endian"

Les nombres sur plusieurs octets sont écrits **octet de poids fort en premier** (big endian), comme on lit un nombre de gauche à droite. `00 70` vaut 0x0070, soit 112. C'est l'inverse de ce que fait un processeur x86, donc on ne peut pas juste copier les octets dans un entier : `protocol.cpp` a ses petites fonctions `be16`, `be32` (écriture) et `be16r`, `be32r` (lecture) pour ça. La norme appelle ces règles d'encodage **MDER** (Medical Device Encoding Rules).[![calendrier.png](https://librenard.fr/wiki/uploads/images/gallery/2026-10/scaled-1680-/calendrier.png)](https://librenard.fr/wiki/uploads/images/gallery/2026-10/calendrier.png)

### Les dates en BCD


Dernière curiosité, et pas des moindres : les dates. Le lecteur les stocke sur 8 octets en **BCD** (Binary Coded Decimal). Chaque octet contient deux chiffres décimaux, un par moitié d'octet. Il suffit donc de lire l'hexadécimal "comme si c'était du décimal" :

```bash
20 26 10 01 21 06 58 00
│  │  │  │  │  │  │  └── centièmes de seconde : 00
│  │  │  │  │  │  └───── secondes : 58
│  │  │  │  │  └──────── minutes : 06
│  │  │  │  └─────────── heures : 21
│  │  │  └────────────── jour : 01
│  │  └───────────────── mois : 10
│  └──────────────────── année : 26
└─────────────────────── siècle : 20

=> 2026/10/01 21:06:58
```

Le code d'origine qualifiait ça de "weird-ass encoding", et ça m'a bien fait rire ! Je dois avouer que la première fois qu'on voit `0x26` qui veut dire 26 et pas 38, ça surprend. La fonction `decodeBcd` fait la conversion, et renvoie `-1` si un des deux chiffres dépasse 9 : dans ce cas l'octet n'est pas du BCD valide, et la date est marquée invalide au lieu de produire n'importe quoi.

```cpp
int decodeBcd(uint8_t x) {
    auto hi = (x >> 4);
    auto lo = (x & 0xF);
    if(9<hi || 9<lo) {
        return -1;
    }
    return 10*hi + lo;
}
```

Avec ces quelques règles en tête (APDU, invoke-id, big endian, BCD, nomenclature MDC), on a tout ce qu'il faut pour suivre la conversation complète entre le PC et le lecteur. C'est le sujet du chapitre suivant.