Passer au contenu principal

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"chat.png

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 

chat.png

< 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.