La conversation, pas à pas
Maintenant qu'on connaît la grammaire, on peut écouter une conversation complète. Toute la séquence est écrite dans une seule fonction, downloadSamples, dans session.cpp. Elle suit un ordre strict : si on saute une étape ou qu'on inverse deux messages, le lecteur abandonne.
Pour illustrer, j'utilise une trace : un fichier texte où chaque ligne est un transfert USB. > veut dire "le PC envoie", < veut dire "le lecteur répond", c c'est le transfert de contrôle du début. Celle-ci vient des tests (tests/fixtures/meals.trace) : elle est construite par un simulateur calqué sur un vrai Accu-Chek Guide, avec des mesures inventées.
Une trace enregistrée sur un vrai lecteur contient toutes les mesures de la personne. Ce sont des données de santé : on ne la publie pas, on ne la met pas dans un dépôt Git. Le .gitignore d'accuchek ignore d'ailleurs les fichiers *.trace en dehors des exemples de test.
Vue d'ensemble
| Étape | Sens | Message |
|---|---|---|
| 1 | PC → lecteur | "toc toc" : requête USB GET_STATUS |
| 2 | lecteur → PC | demande d'association |
| 3 | PC → lecteur | association acceptée |
| 4 | lecteur → PC | description de ses objets (configuration) |
| 5 | PC → lecteur | "configuration bien reçue" |
| 6-7 | aller-retour | identité et horloge du lecteur (MDS) |
| 8-9 | aller-retour | liste des segments de la mémoire |
| (10-11) | aller-retour | mise à l'heure, seulement si demandée et utile |
| 12... | boucle | transfert des glycémies, avec accusé de réception à chaque paquet |
| ... | boucle | transfert des marqueurs repas |
| fin | aller-retour | fin d'association, on raccroche |
1. Le "toc toc"
c 0000
On commence par une requête USB standard GET_STATUS sur le périphérique. Le lecteur répond deux octets à zéro. On ne se sert pas de cette réponse, mais c'est ce que fait le pilote Tidepool avant de commencer, et quand on reproduit un protocole qu'on ne connaît pas par cœur, on évite de retirer des étapes "pour voir". Dans le doute, je fais tout pareil.
2 et 3. La poignée de main
< E2000032800000000001002A0000...
> E300002C0003507900268000000280008000000000000000800000000008123456780000000000000000000000000000
Le lecteur ouvre la discussion avec une demande d'association (E2 00). On lui répond (E3 00) avec un message entièrement construit par buildAssociationResponse. Décortiqué, ça donne :
E3 00 réponse d'association
00 2C longueur : 44 octets
00 03 résultat : "accepté, configuration inconnue"
50 79 protocole 20601 (0x5079), la norme ISO/IEEE 11073-20601
00 26 longueur des infos de protocole
80 00 00 02 version du protocole
80 00 règles d'encodage : MDER
80 00 00 00 version de la nomenclature
00 00 00 00 unités fonctionnelles : association normale
80 00 00 00 type de système : "manager"
00 08 longueur de l'identifiant système
12 34 56 78 00 00 00 00 notre identifiant (fictif)
...
Le point important est le résultat 00 03, "accepted-unknown-config" : on accepte la connexion mais on dit au lecteur qu'on ne connaît pas encore sa configuration. Du coup, il va nous la décrire. C'est exactement ce qu'on veut : on ne suppose rien, on lui demande de se présenter.
4 et 5. Le lecteur se décrit
< E700003A00380010010100320000FFFFFFFF0D1C00284000000200220006000100010006092F00027144003D01000002000C092100020100095100020004
> E7000016001400100201000E0000000000000D1C000440000000
Le lecteur envoie un événement 0D 1C (MDC_NOTI_CONFIG) : la liste de ses objets. On n'a besoin que d'un seul d'entre eux, le PM-Store, la mémoire des mesures. parseConfigInfo le cherche par sa classe :
00 3D classe 61 : MDC_MOC_VMO_PMSTORE, c'est lui
01 00 son handle : 0x0100
00 02 il a 2 attributs
00 0C sur 12 octets
09 21 00 02 01 00 attribut 2337 (ID_HANDLE) = 0x0100
09 51 00 02 00 04 attribut 2385 (NUM_SEG) = 4 segments
On retient le handle 0x0100 : toutes les questions sur la mémoire seront adressées à cet objet. Puis on accuse réception de la configuration (02 01 + 0D 1C, résultat 00 00 = acceptée), en reprenant l'invoke-id du lecteur (00 10).
6 et 7. Qui es-tu ?
> E700000E000C001101030006000000000000
< E70000AE00AC0011020300A6...
On demande tous les attributs de l'objet MDS (handle 0), c'est-à-dire le lecteur lui-même. Remarquez l'invoke-id : 00 11, soit le 00 10 précédent plus un. La réponse contient le fabricant, le modèle, le numéro de série, le firmware et l'horloge. Je la décortique au chapitre suivant, parce qu'elle mérite qu'on s'y attarde.
8 et 9. Qu'est-ce que tu as en mémoire ?
> E7000014001200120107000C01000C0D0006000100020000
< E700012E012C00120207012601000C0D0120...
On demande au PM-Store (01 00) l'action 0C 0D (SEG_GET_INFO) pour tous ses segments. Le lecteur répond avec la description de chacun d'eux. Sur le vrai Accu-Chek Guide, on en trouve quatre, avec des noms assez parlants :
| Instance | Nom | Contenu |
|---|---|---|
| 0 | PMSegGluc |
les glycémies |
| 1 | PMSegCtrl |
les mesures de contrôle (solution de test) |
| 2 | PMSegError |
les erreurs |
| 3 | PMSegCMeal |
les marqueurs repas |
C'est une des grosses différences avec la version d'origine, qui lisait simplement le segment 0. Le code ne se fie pas aux noms ni aux numéros : segmentKind lit la "carte" de chaque segment (l'attribut PM_SEG_MAP), qui décrit le format exact de ses entrées :
- glycémies : objet numérique, partition 2, qui n'est pas une mesure de contrôle, avec une valeur sur 2 octets et un statut sur 2 octets ;
- marqueurs repas : objet "énumération", partition 128, code
MDC_CTXT_GLU_MEAL, avec un code repas sur 2 octets.
Au passage, on récupère le compteur d'entrées de chaque segment (SEG_USAGE_CNT). Ça permettra de vérifier à la fin qu'on a bien tout reçu : "le lecteur annonce 638 mesures, on en a reçu 638".
12. Le transfert des glycémies
On demande le transfert du segment des glycémies avec l'action 0C 1C (SEG_TRIG_XFER) :
> E7000010000E00130107000801000C1C00020000
< E7000012001000130207000A01000C1C000400000000
La réponse fait 22 octets. Les deux derniers sont le verdict : 00 00 veut dire "c'est parti", 00 03 veut dire "ce segment est vide" (un lecteur tout neuf, par exemple). Tout autre code est une erreur. Puis le lecteur commence à envoyer ses paquets de données :
< E700003800360020010100300100FFFFFFFF0D2100260000000000000000000280000018202609300731120000700000202609301302400000B60000
C'est le message le plus intéressant de toute la conversation. Le voici découpé :
E7 00 00 38 00 36 message de données, longueurs
00 20 invoke-id choisi par le lecteur
01 01 événement à confirmer
00 30 longueur
01 00 objet : le PM-Store
FF FF FF FF temps relatif (non utilisé)
0D 21 événement MDC_NOTI_SEGMENT_DATA : "voici des mesures"
00 26 longueur
00 00 instance du segment (0 = glycémies)
00 00 00 00 index de la première entrée de ce paquet
00 02 nombre d'entrées dans ce paquet : 2
80 00 statut : 0x80 = premier paquet (0x40 = dernier)
00 18 taille des entrées : 24 octets = 2 x 12
20 26 09 30 07 31 12 00 date BCD : 2026/09/30 07:31:12
00 70 valeur : 0x70 = 112 mg/dL
00 00 statut : 0, mesure normale
20 26 09 30 13 02 40 00 date BCD : 2026/09/30 13:02:40
00 B6 valeur : 0xB6 = 182 mg/dL
00 00 statut : 0
Chaque glycémie tient donc sur 12 octets : 8 pour la date, 2 pour la valeur en mg/dL, 2 pour le statut. Les mmol/L sont simplement calculés en divisant par 18.
Après chaque paquet, il faut renvoyer un accusé de réception, sinon le lecteur n'envoie pas la suite :
> E700001E001C0020020100160100FFFFFFFF0D21000C000000000000000000020080
On y recopie l'invoke-id du lecteur (00 20), l'instance, l'index et le nombre d'entrées du paquet, suivis du statut 00 80, qui signifie "le manager confirme".
Petite anecdote de reverse engineering : dans le code d'origine (et dans Tidepool), ces champs s'appelaient simplement u0, u1 et u2, des valeurs "qu'on renvoie telles quelles" sans trop savoir ce qu'elles sont. En les comparant à la norme, on comprend que ce sont l'instance du segment, l'index de la première entrée, le nombre d'entrées et le statut du paquet. Le code garde ces noms, mais on sait maintenant ce qu'il recopie.
La boucle continue jusqu'au paquet dont le statut a le bit 0x40 ("dernier paquet"). Détail qui m'a fait chercher un moment : pendant cette boucle, les invoke-id sont ceux du lecteur (00 20, 00 21...). Une fois le segment terminé, il faut revenir à notre numérotation, celle de la réponse au déclenchement du transfert. C'est le rôle de la variable answerInvokeId dans transferSegment.
Le transfert des marqueurs repas
Si le lecteur a un segment de marqueurs repas non vide, on recommence exactement la même danse avec son instance (ici la 3). Les entrées sont plus petites, 10 octets :
20 26 09 30 07 31 12 00 date BCD : 2026/09/30 07:31:12
72 54 code repas : 29268 = à jeun
Il n'y a pas de valeur de glycémie ici : juste une date et un code. Comment on relie ce marqueur à la bonne mesure ? Je l'explique au chapitre suivant.
Fin. On raccroche proprement
> E40000020000
< E50000020000
On envoie une demande de fin d'association (E4 00, "fin normale") et le lecteur confirme (E5 00). Transfert terminé s'affiche sur l'écran du glucomètre
Un choix que j'ai fait ici : si cette toute dernière étape échoue (lecteur débranché un poil trop tôt, par exemple), on garde quand même les mesures. Tous les paquets ont déjà été confirmés, les données sont complètes, il n'y a aucune raison de les jeter pour un "au revoir" manqué.
Et si le lecteur coupe en plein milieu ?
À n'importe quelle étape, le lecteur peut envoyer un abandon (E6 00) au lieu de la réponse attendue. La fonction receive le détecte à chaque message reçu et arrête tout avec un message précis, du genre received association abort request instead of MDS attribute answer. Pas de mesures à moitié lues : on recommencera.
