Suite à une visite médicale, on m'a conseillé de suivre ma tension... J'avais bien un
tensiomètre de poignet qui traînait dans un tiroir depuis des années mais il paraît que c'est mieux d'en utiliser un avec un brassard... Petit tour dans une grande surface de trucs électroniques, je repère un
Medisana BU-546 Connect (542~546; ~50.00 euros) qui a une connexion Bluetooth. Petit tour sur Internet. Cela n'a pas l'air trop mauvais, c'est une société allemande de l'ouest qui semble avoir bonne réputation. Rien sur Linux en lien avec le bidule, ça va être l'occasion de découvrir Bluetooth et d'expérimenter. Bref, c'est irrésistible...
Le tensiomètre fontionne avec une application (gratuite) pour smartphone,
Vitadock+ ...qui demande d'activer la géolocalisation et envoie toutes les données (somme toute assez confidentielles) dans le cloud. Ça, ce n'est pas une bonne idée. En fait, c'est un peu scandaleux, c'est pire que le driver d'imprimante dont
Richard Stallman voulait les sources. Pour dispositif médical, je dois pouvoir contrôler l'utilisation des données.
D'abord, découvrir
Bluetooth sous Linux. Les outils et le b-a-ba de la communication. La première idée était de trouver un autre gadget compatible Linux et d'observer son fonctionnement. En fait, je pouvais déjà commencer à essayer de communiquer entre mon laptop et un Raspberry Pi. Mettons que je prenne le RPi comme 'device' (serveur) :
$ sudo btmgmt le on # pour activer le Low Energy
$ sudo hciconfig hci0 piscan # pour se laisser scanner (?)
$ bluetoothctl # pour le rendre découvrable et activer l'annonce (advertising)
> help
> discoverable on
> advertise on
> show
Du côté du laptop (client), je peux utiliser
gatttool(1) pour me connecter et interroger le Rpi :
$ gatttool -I
> connect 11:22:33:44:55:66
> help
> primary
> characteristics
> char-desc
> char-read-hdn 0x0001
> char-read-uuid 0x2a01
...
C'est très bien. Maintenant, il faut essayer de faire la même chose avec le tensiomètre... D'abord, connaître sa mac-address. Ce n'est évidemment marqué nulle part. Après une mesure de la tension, il active le Bluetooth et s'annonce (advertising) pendant une minute. En principe, je devrais pouvoir le détecter avec hcitool(1)
$ sudo hcitool lescan
Mais pour une raison qui m'échappe, le lescan me renvoie un tas de mac-addresses dont quelques unes seulement sont associées à un nom d'appareil (la TV des voisins, une voiture qui passe, mon smartphone si j'active le Bluetooth,...) mais rien qui ressemble à Medisana ou Transtek, la société chinoise qui fabrique le bidule. Pas plus que le
manufacturer-id du constructeur chinois (Medisana n'en a pas). En utilisant le smartphone (Samsung A40), les parasites sont absents et un BS1490 apparaît mystérieusement à la fin d'une mesure de tension. C'était donc ça!
Maclookup m'apprend que c'est une 'Localy Administred Address' (adresse aléatoire utilisée par discrétion pour éviter le repérage). Et, de fait, on retrouve TMB-190-BS dans le
manuel du tensiomètre et une recherche Internet mène à
la page du constructeur chinois. En plus, il y a moyen de trouver des
documents concernant la demande d'autorisation au
FCC américain.
Avec la
MAC-address, je peux maintenant me connecter sur le tensiomètre et lire différentes choses mais rien ne semble correspondre à une tension. Tout est quasi constant d'une mesure à l'autre. La seule chose qui varie (une horloge?) varie même à l'intérieur d'une seule session. Là, ça va être difficile... Si il faut écrire quelque chose quelque part et lire le résultat ailleurs, il va falloir capturer ue session entre le tensiomètre et l'application du smartphone. Sniffer une communication Bluetooth n'est pas aussi simple que de capturer du traffic Ethernet avec
tcpdump(8). Bluetooth fait du saut de fréquence (frequency hopping) et il est difficile de capturer toute la bande ISM 2.4 GHz. Heureusement, il existe un bidule génial de
Great Scott Gadgets dont Michael Ossmann décrit la genèse dans une
conférence au
Shmoocon de 2011 le
Ubertooth One. Radin, comme d'habitude, je commence par commander un clone chinois qui se révélera fort capricieux. Je fais des tests entre le laptop et le Raspberry Pi et cela ne fonctionne correctement que très rarement. Un forum finit par me convaincre que cela fonctionne mieux avec la version officielle. Au diable l'avarice, j'en commande un vrai (aux Pays-Bas) que je reçois deux jours plus tard. Et cela fonctionne du premier coup entre le tensiomètre et le laptop (j'utilise l'Ubertooth One sur le Raspberry Pi pour avoir une version plus récente des outils (sur le laptop, je suis toujours en Ubuntu 18.04...). J'ai juste dû faire une mise à jour avec
ubertooth-dfu(1) du dernier firmware. Quelques messages d'erreurs, mais cela fonctionne.
$ sudo ubertooth-btle -f -t 11:22:33:44:55:66 -r tensio.pcapng
Et là, surprise!
Wireshark(1) décode les packets contenant la tension.
On peut aussi utiliser tshark(1) pour produire un fichier JSON que l'on peut imprimer, traiter avec grep(1), jq(1),... :
$ tshark -r tensio.pcapng -T json > tensio.json
À noter que le fichier JSON est un peu foireux, il n'y a pas de racine unique, il faut encadrer le contenu par quelque chose comme '{ "results": [' et ']}' pour pouvoir utiliser
jq(1) et, dans jq, il faut utiliser des '"' (double quotes) parce qu'il y a des '.' dans les identifiants. Par exemple,
$ cat medisana.json |jq '.results[]._source.layers.frame."frame.protocols"' |less
$ cat medisana.json |jq '.results[]._source.layers.btatt."btatt.blood_pressure_measurement.compound_value.systolic.mmhg"' |less
Mais sinon, par exemple, dans une indication contenant une mesure, on peut lire:
"btatt": {
"btatt.opcode": "0x0000001d",
"btatt.opcode_tree": {
"btatt.opcode.authentication_signature": "0",
"btatt.opcode.command": "0",
"btatt.opcode.method": "0x0000001d"
},
"btatt.handle": "0x0000000b",
"btatt.handle_tree": {
"btatt.service_uuid16": "6160",
"btatt.uuid16": "10805"
},
"btatt.blood_pressure_measurement.flags": "0x0000001e",
"btatt.blood_pressure_measurement.flags_tree": {
"btatt.blood_pressure_measurement.flags.reserved": "0x00000000",
"btatt.blood_pressure_measurement.flags.measurement_status": "1",
"btatt.blood_pressure_measurement.flags.user_id": "1",
"btatt.blood_pressure_measurement.flags.pulse_rate": "1",
"btatt.blood_pressure_measurement.flags.timestamp": "1",
"btatt.blood_pressure_measurement.flags.unit": "0x00000000"
},
"btatt.blood_pressure_measurement.compound_value.systolic.mmhg": "120",
"btatt.blood_pressure_measurement.compound_value.diastolic.mmhg": "62",
"btatt.blood_pressure_measurement.compound_value.arterial_pressure.mmhg": "91",
"btatt.blood_pressure_measurement.compound_value.timestamp": {
"btatt.year": "2022",
"btatt.month": "10",
"btatt.day": "27",
"btatt.hours": "17",
"btatt.minutes": "52",
"btatt.seconds": "0"
},
"btatt.blood_pressure_measurement.pulse_rate": "58",
"btatt.blood_pressure_measurement.user_id": "0x00000001",
"btatt.blood_pressure_measurement.status": "0x00000000",
"btatt.blood_pressure_measurement.status_tree": {
"btatt.blood_pressure_measurement.status.reserved": "0x00000000",
"btatt.blood_pressure_measurement.status.improper_measurement_position": "0",
"btatt.blood_pressure_measurement.status.pulse_rate_range_detection": "0x00000000",
"btatt.blood_pressure_measurement.status.irregular_pulse": "0",
"btatt.blood_pressure_measurement.status.cuff_fit_too_loose": "0",
"btatt.blood_pressure_measurement.status.body_movement": "0"
}
}
Ce qui est curieux, c'est que tout ce décodage semble être fait par du code ad-hoc dans
packet-btatt.c Il n'y aurait donc pas de définitions formelles comme les
MIBs du
SNMP. Et parmi les UUID-16, on trouve 0x2A35 pour la notification d'une caractéristique concernant la pression sanguine... (voir aussi Bluetooth
Assigned numbers -
HealthDeviceProfiles.pdf)
Le tensiomètre répond donc bien à un standard :
Blood-pressure-profile-1-1-1. Ce n'est pas très compréhensible. C'est comme le bouquin '
Bluetooth Low Energy: The Developer's Handbook' de Robin Heydon. C'est intéressant, très détaillé mais pas très pratique. Il faut le lire plusieurs fois, expérimenter avec les outils Linux et Wireshark, parcourir des blogs et des forums pour finir par avoir une idée de quoi il est question. Et, il n'y a, curieusement pas de références (ou de guide pour s'orienter dans la documention étendue de
bluetooth.com)
Ainsi, avec Wireshark (en éliminant les packets sans intérêt avec le filtre '
btle.data_header.length > 0 || btle.advertising_header.pdu_type == 0x05', après une découverte du tensiomètre par l'application Vitadock+, elle active les 'indications' en écrivant 0200 dans un canal particulier. Les mesures non encore transmises sont alors envoyées une par une, avec un timestamp comme 'indication'. On peut reproduire le mécanisme avec gatttool(1)
On reçoit alors des listes de 19 caractères en hexadécimal contenant la pression systolique, diatolique, la pression, le temps de la mesure (année, mois, jour, heure, minute, seconde) et le nombre de pulsations par minute) qu'il est facile de décoder en regardant la correspondance sur Wireshark(1).
$ gatttool -b 11:22:33:44:55:66 -t random -I
[11:22:33:44:55:66][LE]> connect
Attempting to connect to 11:22:33:44:55:66
Connection successful
[11:22:33:44:55:66][LE]> char-write-req 0x000c 0200
Characteristic value was written successfully
Indication handle = 0x000b value: 1e 71 00 42 00 59 00 e6 07 0a 1c 0b 2b 00 3f 00 01 00 00
Indication handle = 0x000b value: 1e 85 00 49 00 67 00 e6 07 0a 1c 0c 1d 00 51 00 01 00 00
...
XX SS SS DD DD PP PP YY YY MM dd HH MM SS pp pp id
(SSSS=Systolic mmhg, DDDD=Diatolic, PPPP pressure, YYYY=year, MM=month, dd=day, HH=hour, MM=minute, SS=seconds, id=person)
Vers le milieu, on retrouve l'année 2022 (0x07E6), octobre (0x0A), 28 (0x1C), etc...
Reste maintenant à écrire une petite application qui fait juste ça : connect, write(0200) et accepte/affiche les indications reçues. Ce qui n'est pas spécialement trivial parce que la documentation de l'API Bluetooth sur Linux est inexistante. La stratégie la plus simple serait de partir des sources de gatttool(1) mais il y a une masse de dépendances... On trouve beaucoup de choses en Python aussi mais Python est infernal au niveau des versions et des dépendances.
Ce qui serait bien aussi, ce serait de remettre le pointeur de lecture à zéro (ou à une valeur arbitraire) parce que là, le tensiomètre n'envoie que les nouvelles mesures, effectuées
après le dernier transfert. Alors qu'il peut contenir jusqu'à 250 mesures par utilisateur. Il semblerait cependant que cela ne soit pas possible parce qu'on ne trouve pas de 'Record Access Control Point' (0x2a52) dans le caractéristiques du tensiomètre (voir ci-dessous) et que la fonctionalité est optionnelle.
Exploration avec gatttool(1)
(gatttool(1) serait à l'abandon et remplacé par
bluetoothctl(1) (tellement 'intuitif' que le manuel est virtuellement inexistant... (il faut taper 'menu gatt' pour avoir le sous-menu gatt...)).
Les 'services' du tensiomètre
$ gatttool -b 11:22:33:44:55:66 -t random -I
[11:22:33:44:55:66][LE]> connect
Attempting to connect to 11:22:33:44:55:66
Connection successful
[11:22:33:44:55:66][LE]> primary
attr handle: 0x0001, end grp handle: 0x0007 uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x0008, end grp handle: 0x0008 uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x0009, end grp handle: 0x000e uuid: 00001810-0000-1000-8000-00805f9b34fb
attr handle: 0x000f, end grp handle: 0x0012 uuid: 0000180f-0000-1000-8000-00805f9b34fb
attr handle: 0x0013, end grp handle: 0x0021 uuid: 0000180a-0000-1000-8000-00805f9b34fb
attr handle: 0x0022, end grp handle: 0xffff uuid: 00001805-0000-1000-8000-00805f9b34fb
[11:22:33:44:55:66][LE]>
La commande 'primary' liste les 'services' disponibles sur le tensiomètre.
La partie variable des UUIDs sont des UUID-16 de 'services' dont on trouve la liste dans
https://www.bluetooth.com/specifications/assigned-numbers/ :
assigned_numbers_release.pdf :
0x1800 Generic Access service
0x1801 Generic Attribute service
0x1810 Blood Pressure service
0x180f Battery service
0x180a Device Information service
0x1805 Current Time service
Ce sont des informations que l'on peut aller chercher dans le tensiomètre. Ces 'services' sont décrits dans les documents
Specifications and Tests Documents List/Services. Par exemple,
Blood Pressure Service (
blood-pressure-service-1-1-1.pdf et d'autres documents).
La manière d'accéder à ces informations est décrite dans des documents 'Profile' :
Specifications and Tests Documents List/Profiles. Par exemple,
Blood Pressure Profile (
blood-pressure-profile-1-1-1.pdf et d'autres documents).
Il faut noter que tous les bidules Bluetooth ne sont pas forcément 'conformes' à un standard accepté. Ceux qui ont passé les tests de conformance se retrouvent dans la base de données cherchable :
https://launchstudio.bluetooth.com/Listings/Search. La recherche avancée permet de trouver les bidules qui ont passé les tests avec succès. On peut, par exemple, lister ceux qui implémente le 'Blood Pressure Service' (BLS) et le 'Blood Pressure Profile' (BLP) ou, éventuellement regarder quels bidules d'une marque données ont un profil standard. Dans ce cas-ci, on arrive à une fiche concernant le BU-546 (en fait, un
TMB-1490-TS de Transtek) où on ne trouve (bizarrement?) pas de qualification 'blood pressure service/profile'...
Les 'caractéristiques' du tensiomètre
$ gatttool -b 11:22:33:44:55:66 -t random -I
[11:22:33:44:55:66][LE]> connect
Attempting to connect to 11:22:33:44:55:66
Connection successful
[11:22:33:44:55:66][LE]> char-desc
/* 0x1800 Generic Access service */
handle: 0x0001, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0002, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0003, uuid: 00002a00-0000-1000-8000-00805f9b34fb
handle: 0x0004, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0005, uuid: 00002a01-0000-1000-8000-00805f9b34fb
handle: 0x0006, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0007, uuid: 00002a04-0000-1000-8000-00805f9b34fb
/* 0x1801 Generic Attribute service */
handle: 0x0008, uuid: 00002800-0000-1000-8000-00805f9b34fb
/* 0x1810 Blood Pressure service */
handle: 0x0009, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x000a, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x000b, uuid: 00002a35-0000-1000-8000-00805f9b34fb
handle: 0x000c, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x000d, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x000e, uuid: 00002a49-0000-1000-8000-00805f9b34fb
/* 0x180f Battery service */
handle: 0x000f, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0010, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0011, uuid: 00002a19-0000-1000-8000-00805f9b34fb
handle: 0x0012, uuid: 00002902-0000-1000-8000-00805f9b34fb
/* 0x180a Device Information service */
handle: 0x0013, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0014, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0015, uuid: 00002a29-0000-1000-8000-00805f9b34fb
handle: 0x0016, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0017, uuid: 00002a24-0000-1000-8000-00805f9b34fb
handle: 0x0018, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0019, uuid: 00002a25-0000-1000-8000-00805f9b34fb
handle: 0x001a, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x001b, uuid: 00002a27-0000-1000-8000-00805f9b34fb
handle: 0x001c, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x001d, uuid: 00002a26-0000-1000-8000-00805f9b34fb
handle: 0x001e, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x001f, uuid: 00002a28-0000-1000-8000-00805f9b34fb
handle: 0x0020, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0021, uuid: 00002a23-0000-1000-8000-00805f9b34fb
/* 0x1805 Current Time service */
handle: 0x0022, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0023, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0024, uuid: 00002a2b-0000-1000-8000-00805f9b34fb
handle: 0x0025, uuid: 00002902-0000-1000-8000-00805f9b34fb
[11:22:33:44:55:66][LE]>
De nouveau, on retrouve les UUID-16 dans
assigned_numbers_release.pdf :
0x2800 Primary Service
0x2803 Characteristic
0x2902 Client Characteristic Configuration
0x2a00 Device Name
0x2a01 Appearance
0x2a04 Peripheral Preferred Connection Parameters
0x2a19 Battery Level
0x2a23 System ID
0x2a24 Model Number String
0x2a25 Serial Number String
0x2a26 Firmware Revision String
0x2a27 Hardware Revision String
0x2a28 Software Revision String
0x2a29 Manufacturer Name String
0x2a2b Current Time
0x2a35 Blood Pressure Measurement
0x2a49 Blood Pressure Feature
Bref, un peu de tout... On notera cependant qu'on écrit 0200 dans le 'Client Characteristic Configuration' (0x2902, handle 0x000c) du 'Blood Pressure service' (0x1810) pour activer les indications 'Blood Pressure Measurement' (0x2a35, handle 0x000b).