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.