Контроль над браслетом в ритме BlueZ

от автора

В исследовательском проекте мне потребовался прототип медицинского браслета. Устройство должно было периодически измерять пульс, предупреждая об этом пациента, и отправлять результаты вместе с уровнем заряда батареи в облачный сервис. Таким устройством вполне мог стать и фитнес-браслет со стационарным ретранслятором вместо смартфона. Поэтому, прежде чем попытаться собрать прототип своими руками, я решил поэкспериментировать с чем-нибудь готовым. Так у меня появился новый Xiaomi mi band 1S Pulse (обзор на Geektimes) с оптическим датчиком частоты сердечного ритма.

Выбор браслета был связан с его относительно невысокой ценой, хорошим аккумулятором и тем, что измерение пульса инициируется именно со смартфона, а не запускается нажатием кнопки.

Эксперименты я начал с изучения набора сервисов и характеристик, доступных через Bluetooth 4.0 (или Bluetooth Low Energy, далее — BLE). Кое-что нетрудно было найти в сети, и эта информация мне очень помогла, но она касалась предыдущей версии, без нужного мне датчика. Поэтому я начал с BLE-сканера.

Оказалось, что подходящий и, признаться, очень удобный инструмент есть у Nordic Semiconductor. Это Master Control Panel или nRF MCP для Android 4.3+. Установив приложение на планшет и запустив “SCAN”, я без труда обнаружили mi band и записал его физический адрес – C8:0F:10:11:1B:6E:

Нажав на “OPEN TAB” и затем на “CONNECT”, получил набор сервисов:

Идентификатор измерителя пульса оказался стандартным для подобных устройств — 0x180D. Забегая вперед, скажу, что обрадовался я рано.

В качестве ретранслятора я использовал Raspberry Pi (модель B) и BLE-usb-адаптер BT400 от ASUS. Также потребовались BlueZ – настоящий швейцарский нож для работы с Bluetooth под Linux и пара дополнительных модулей для Python.

Подготовка Raspberry Pi

Для Raspberry Pi использовался образ Raspbian. После apt-get update и apt-get upgrade проверил адаптер:

pi@raspberrypi:~ $ lsusb Bus 001 Device 006: ID 0b05:17cb ASUSTek Computer, Inc.  Bus 001 Device 005: ID 046d:c077 Logitech, Inc.  Bus 001 Device 004: ID 04d9:1602 Holtek Semiconductor, Inc. 

Отлично, мой адаптер — в первой строке. Установил BlueZ. Рекомендуется скачать последний архив кода, на момент подготовки статьи это был BlueZ 5.37, распаковать и скомпилировать. Я же удовлетворился версией 5.23, которая устанавливается через apt-get. Корректность установки можно проверить, выполнив команду gatttool –help.
Gatttool — это инструмент BlueZ для работы с GATT, общим профилем атрибутов BLE устройств. В старых версиях gatttool по умолчанию не устанавливался, нужно было «прикручивать» руками, но здесь help был доступен и значит, у меня есть почти всё необходимое для работы с браслетом. Через pip установил Pexpect для работы с BlueZ из Python. Перезагрузил Raspberry и включил адаптер. Статус адаптера проверил командой hciconfig:

pi@raspberrypi:~ $ hciconfig hci0:	Type: BR/EDR  Bus: USB 	BD Address: 5C:F3:70:71:7E:F5  ACL MTU: 1021:8  SCO MTU: 64:1 	DOWN  	RX bytes:616 acl:0 sco:0 events:34 errors:0 	TX bytes:380 acl:0 sco:0 commands:34 errors:0 

Флаг DOWN показал, что адаптер выключен, включил его командой:
sudo hciconfig hci0 up

Прежде чем писать код для Raspberry, мне нужно было убедиться, что все необходимые мне сервисы (частота пульса, уровень заряда аккумулятора и виброзвонок) доступны в терминальном режиме из BlueZ.
Просканировал BLE окружение и без проблем нашел браслет:

pi@raspberrypi:~ $ sudo hcitool -i hci0 lescan LE Scan ... C8:0F:10:11:1B:6E (unknown) C8:0F:10:11:1B:6E MI1S 

Подключился к браслету командой connect, запустив gatttool в интерактивном режиме (ключ I):

pi@raspberrypi:~ $ sudo gatttool -i hci0 -b C8:0F:10:11:1B:6E -I [C8:0F:10:11:1B:6E][LE]> connect Attempting to connect to C8:0F:10:11:1B:6E Connection successful [C8:0F:10:11:1B:6E][LE]> 

Соединение в интерактивном режиме без спаривания обычно длится секунд 20. Это так называемый низкий уровень секретности, он используется по умолчанию. Список доступных сервисов выводится командой primary:

[C8:0F:10:11:1B:6E][LE]> primary attr handle: 0x0001, end grp handle: 0x0009 uuid: 00001800-0000-1000-8000-00805f9b34fb attr handle: 0x000c, end grp handle: 0x000f uuid: 00001801-0000-1000-8000-00805f9b34fb attr handle: 0x0010, end grp handle: 0x0039 uuid: 0000fee0-0000-1000-8000-00805f9b34fb attr handle: 0x003a, end grp handle: 0x0048 uuid: 0000fee1-0000-1000-8000-00805f9b34fb attr handle: 0x0049, end grp handle: 0x004e uuid: 0000180d-0000-1000-8000-00805f9b34fb attr handle: 0x004f, end grp handle: 0x0051 uuid: 00001802-0000-1000-8000-00805f9b34fb 

Определить, что за сервис можно по четырем цифрам после uuid. Получилось два общих сервиса (generic), два сервиса, заданных производителем (fee0 и fee1), HRM сервис (180d) и алертинг (1802).
Перечень характеристик браслета выводится командой char-desc в порядке возрастания указателей (handles). Нашел в списке характеристику с идентификатором ff0c:
handle: 0x002c, uuid: 0000ff0c-0000-1000-8000-00805f9b34fb,
Указатель 0x002c для уровня заряда аккумулятора был уже определен для предыдущей версии браслета. Попробовал считать данные командой char-read-hnd (прочитать данные по указателю):

Батарейка «сдалась» первой. В ответе не только уровень заряда, это первый байт в hex (смартфон накануне показывал 70%), но и полная информация о зарядке: количество циклов, дата последней зарядки, статус аккумулятора. По условиям задачи мне нужен был только уровень.

Вторым «покорился» виброзвонок. По данным из MCP я предположил, что это Immediate Alert, а Alert Level это команда, которую нужно послать на идентификатор 0x2A06:

В списке характеристик, этому идентификатору соответствует строка:
handle: 0x0051, uuid: 00002a06-0000-1000-8000-00805f9b34fb
Отправил команду на указатель 0x0051 со значением 01:

[C8:0F:10:11:1B:6E][LE]> char-write-cmd 0x0051 01 

Браслет отозвался двумя слабыми жужжаниями, значение 02 это два раза по 01, т.е. четыре сигнала, а 03 — два, но более сильных. С частотой пульса оказалось всё значительно сложнее. MCP показал следующее:

Связанные с этим сервисом характеристики:

handle: 0x004b, uuid: 00002a37-0000-1000-8000-00805f9b34fb handle: 0x004c, uuid: 00002902-0000-1000-8000-00805f9b34fb handle: 0x004d, uuid: 00002803-0000-1000-8000-00805f9b34fb handle: 0x004e, uuid: 00002a39-0000-1000-8000-00805f9b34fb 

Частота пульса передается в смартфон в режиме нотификации или push-уведомления, её нельзя считать как уровень заряда аккумулятора. Нужно разрешить нотификацию, записав в CCC (Client Characteristic Configuration) с указателем 0x004с (идентификатор у CCC всегда 2902) значение 0100 и ждать уведомления.

Ничего не вышло, значение успешно записывалось, но никаких уведомлений не поступало, браслет просто отключался через несколько секунд. Запуск gatttool в консольном режиме с ключом –listen также не дал результатов, gatttool просто «зависал» в ожидании. Загадка, одним словом.

Для прояснения ситуации пришлось использовать BLE-сниффер (на ноутбуке с Windows 8). В основе был перепрошитый BLE-usb-донгл на чипе СС2540 от Texas Instruments и программа Smart Packet Sniffer того же производителя. Всё необходимое, включая программатор, можно без труда найти в виде набора для разработчика, а программу и прошивку я свободно скачал с сайта TI.

Важно! Запускать сниффер следует, когда браслет находится в режиме презентации (advertising mode), т.е. до соединения со смартфоном. Иначе он будет невидим. Также неплохо убрать все лишние BLE-устройства подальше от сниффера, а еще лучше экранировать, это очень помогает потом разобраться в логе.

Так выглядят пакеты в сниффере в режиме презентации:

Определил, что это именно мой браслет по полю AdvA (Advertising Address). После установления связи со смартфоном, в режиме GATT-соединения, картина изменилась:

Здесь как раз видно, как значение 0100 записывается в CCC с указателем 0x004C, разрешая уведомления о частоте пульса.

Один из исследователей предыдущей версии браслета в своем блоге написал, что не все сервисы могут быть доступны анонимному устройству. Насколько я смог разобраться, в ряде случаев устройство, взаимодействующее с браслетом, должно передать в браслет корректную информацию о пользователе, которая частично хешируется при спаривании со смартфоном.

Эти данные длиной в 20 байт, как и в предыдущей версии, записываются в характеристику с указателем 0x0019 и не изменяются при каждом новом соединении. Первые четыре байта – это uid смартфона, далее, в открытом виде байты пола, возраста, роста, веса, байт разрешения перезаписи (должен быть 00) и 10 байт последовательности, похожей на хеш. Считать user info из браслета у меня не получилось.

При анализе пакетов удалось выяснить следующее:

  1. Каждый раз при соединении в браслет отправляются все разрешения уведомлений (CCC с идентификатором 2902)
  2. Далее происходит передача информации о пользователе
  3. Затем, по указателю 0x0028 записываются дата и время
  4. После этого считываются данные об уровне заряда батареи и количество пройденных за день шагов
  5. Перед тем как получить уведомление о частоте пульса по указателю 0x004E, соответствующему характеристике «точка контроля пульса» записывается последовательность 0x15 0x00 0x00 (предположу, что это сброс)
  6. Затем туда же записывается 0x15 0x02 0x01, что в моем случае соответствует левой руке
  7. После этого, через 15-20 секунд, приходит уведомление с частотой пульса в двух байтах, например 06 40. Второй байт и есть частота пульса в hex

В теории, для того чтобы получить частоту пульса, нужно было повторить все эти транзакции, возможно исключив 3-й и 4-й пункты. Как оказалось, можно обойтись и без сброса. Вручную это сделать невозможно, браслет отключился бы раньше чем я бы успел ввести все команды. Поэтому я подготовил Python-скрипт:

import sys, pexpect from time import sleep gatt=pexpect.spawn('gatttool -I') gatt.logfile = open("pylog.txt", "w") gatt.sendline('connect C8:0F:10:11:1B:6E') gatt.expect('Connection successful') # Check battery level gatt.sendline('char-read-hnd 0x002c') gatt.expect('Characteristic value.*') batt = gatt.after batt = int(batt.split()[2],16) print 'Battery level:', batt, '%' # Send alert gatt.sendline('char-write-cmd 0x0051 01') sleep(5) # Allow notification gatt.sendline('char-write-req 0x004c 0100') gatt.expect('Characteristic value.*') # Send user data gatt.sendline('char-write-req 0x0019 F8663A5F0126B45500040049676F7200000000DC') gatt.expect('Characteristic value.*') # Set control point gatt.sendline('char-write-req 0x004e 150201') # Waiting for notification try:     gatt.expect('Notification handle.*')     hrm = gatt.after     hrm = int(hrm.split()[6], 16)     print 'HRM:', hrm except:     print 'Bad control point or timeout' sys.exit(0) 

Кстати, количество пройденных шагов можно прочитать анонимно, оно доступно для чтения по указателю 0x001D. Ответ – четыре байта, читать нужно слева направо.

Скрипт выводит уровень заряда батареи, отправляет уведомление, ждет и печатает частоту пульса. Загадка решена, осталось научиться отправлять данные в облачный сервис, в качестве которого я решил использовать Thingspeak. Это бесплатный сервис с простым API и готовой визуализацией.

Настройка Thingspeak заняла не более пяти минут. Необходимо зарегистрироваться и войти в персональное пространство. Далее, создать новый канал, в настройках канала указать название, количество и метки полей. Сохранить настройки и перейти на вкладку API Keys. Там скопировать API-ключ для записи (Write API Key):

После этого — переключиться на вкладку Private View (если при настройке канала не было указано “Make Public”).

За отправку данных в Thingspeak отвечает вот такая конструкция на Python:

baseURL = 'https://api.thingspeak.com/update?api_key=%s'%YOUR_API_KEY f = urllib2.urlopen(baseURL + "&field1=%s&field2=%s" % (batt, hrm)) print f.read() f.close() 

Код полностью

import sys, pexpect from time import sleep import urllib2  sample_interval = 180 #sec sample_qty = 8 api_key = 'YOUR_API_KEY' baseURL = 'https://api.thingspeak.com/update?api_key=%s'%api_key  def getData():     try:         gatt=pexpect.spawn('gatttool -I')         # gatt.logfile = open("pylog.txt", "w") # for debug only          gatt.sendline('connect C8:0F:10:11:1B:6E')         gatt.expect('Connection successful', timeout=60)          # Get battery level         gatt.sendline('char-read-hnd 0x002c')         gatt.expect('Characteristic value.*')         batt = gatt.after         batt = int(batt.split()[2],16)          # Send alert         gatt.sendline('char-write-cmd 0x0051 01')         sleep(5)          # Allow notification         gatt.sendline('char-write-req 0x004c 0100')         gatt.expect('Characteristic value.*')          # Send user data         gatt.sendline('char-write-req 0x0019 F8663A5F0126B45500040049676F7200000000DC')         gatt.expect('Characteristic value.*')          # Set control point         gatt.sendline('char-write-req 0x004e 150201')          # Waiting for notification         gatt.expect('Notification handle.*', timeout=60)         hrm = gatt.after         hrm = int(hrm.split()[6], 16)     except:         hrm = 0         batt = 0     return (str(batt), str(hrm))  def main():     sample_count = 0     while True:         try:             batt, hrm = getData()             f = urllib2.urlopen(baseURL + "&field1=%s&field2=%s" % (batt, hrm))             print f.read()             f.close()             sample_count = sample_count + 1             if (sample_count >= sample_qty): break             sleep(sample_interval)         except:             print 'Connection error'             break  if __name__ == '__main__':     main()     sys.exit(0) 

В штатном режиме работы в консоль выводится номер отправки данных, начиная с единицы. Радиус действия ретранслятора 3-4 метра в прямой видимости, что нормально для медицинской палаты. Однако браслет на таком расстоянии легко экранируется ладонью.

Тестировал получившуюся систему в процессе тренировки на велотренажере, 20 минут. Интервал между измерениями – 3 минуты, количество измерений – 8. Браслет склонен завышать частоту пульса при неплотном контакте с кожей, для большей точности поместил датчик с тыльной стороны запястья. Результат на Thingspeak:


Как видно из графика, 8 измерений никак не повлияли на заряд аккумулятора. Думаю, эксперимент можно считать вполне успешным и полученный опыт использовать для проектирования собственного устройства или, например, поискать ОЕМ.

Полезные ссылки:

ссылка на оригинал статьи https://habrahabr.ru/post/276343/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *