Контроль над браслетом в ритме 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 измерений никак не повлияли на заряд аккумулятора. Думаю, эксперимент можно считать вполне успешным и полученный опыт использовать для проектирования собственного устройства или, например, поискать ОЕМ.

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


    Share post

    Comments 31

      0
      Вариант с записью bluetooth пакетов на смартфоне (Параметры разработчика/Журнал отслеживания Bluetooth) с последующим анализом WireShark'ом почему-то не работал или показался неудобным?
        0
        Спасибо, не знал что такой путь есть. С WireShark-ом у меня не очень, пробовал мой лог из Packet Sniffer конвертировать в pcap и смотреть в WireShark-е. Показалось очень сложно. У Packet Sniffer очень простой интерфейс. И неудобный, фильтры работаю почему-то не всегда, приходится просматривать от начала до конца. Но всё очень наглядно.
        0
        Баловался схожим образом с «умной» лампочкой. Правда, я реверсил их прилагу для Android. Ну думали поковырять приложение MiBand? Могут проясниться некоторые «магические» значения. И, кстати, если не боитесь Go / NodeJS, есть обёртки над BlueZ под эти языки. Работают не через спавн процесса а через настоящий сокет к HCI-устройсву.
          0
          Разобрать MIBand не думал, на форумах пишут, что производитель её всё больше обфусцирует с каждой новой версией. Смысл, если все потом передается в открытом виде? Да, есть еще очень много интересного, чего не попробовал, PyGATT, например, или вот ребята пишут мибанду (https://bitbucket.org/OscarAcena/mibanda) — библиотеку для доступа к браслету из Python. NodeJS — очень интересно, обязательно посмотрю, спасибо.
          0
          У вас браслет одновременно работает и в телефоне и с малинкой?
          И не совсем понял фразу что малинка в роли ретранслятора.
            0
            Да, работа с малиной не портит профиль браслета в телефоне, так как используется user info телефона. Собственно, браслет «думает», что малина — это телефон. Ретранслятор? Я не подобрал правильного термина — задача малины принять данные от браслета и передать в облако.
              0
              Если не сложно, напишите про это подробнее. Или тыкните местом если это есть в статье и пропустил.
                0
                Поясните пожалуйста, что вас интересует. Использование user info из телефона? Вот:
                Один из исследователей предыдущей версии браслета в своем блоге написал, что не все сервисы могут быть доступны анонимному устройству. Насколько я смог разобраться, в ряде случаев устройство, взаимодействующее с браслетом, должно передать в браслет корректную информацию о пользователе, которая частично хешируется при спаривании со смартфоном.


                Еще вот:
                Соединение в интерактивном режиме без спаривания обычно длится секунд 20. Это так называемый низкий уровень секретности
                Браслет с малиной работает без спаривания, это security=low в gatttool по умолчанию.

                Практически вся информация с браслета доступна любому анонимному устройству, кроме измерения пульса, для этого нужно представиться телефоном и отправить на браслет данные пользователя.
                  0
                  Следующий шаг — написать приложение для Android, которое бы периодически отправляло пульс в какой-нибудь Google Fit.

                  Кстати, кое-какой код для работы с браслетом (первой версии) есть в проекте Gadgetbridge
            0
            Меня больше интересует как использовать браслет по назначению и плюс одновременно авторизовываться им на моём ноуте по силе сигнала от браслета.
              +1
              А вы точно уверены что этого хотите? Подделать MAC адрес браслета — раз плюнуть.
              0
              Тогда хочу ещё что то наподобие ssl для защищённого соединения и передачи данных. Браслет же как то скрывает от не авторизованных устройств какие то данные.
                0
                Анонимное устройство не может получить пульс, но может его подслушать. Также нельзя скачать с браслета данные пользователя (но тоже можно подслушать), остальное раздается всем кто попросит. Используется минимальный уровень безопасности, хотя, например, уже на следующем уровне — middle, данные начинают шифроваться.
                  0
                  В любом случае я хочу это для удобства, а не для защиты от целенаправленной атаки.
                    0
                    А как заставить браслет быть одновременно и спаренным с телефоном и обнаруживаемым? Что то я не нашёл ни в апи, ни в описании здесь или на сайте по ссылке.
                      0
                      Когда браслет обменивается информацией с телефоном, к нему нельзя подключиться. Так что только по очереди.
                        0
                        Комментарий удален
                    0
                    Однако, Xiaomi меня огорчают. Я понимаю, когда используют нестандартные характеристики, но когда описанную в спеках HR Control Point используют нестандартным образом, это уже дурные манеры.
                    Кстати, как я понял, по указанной команде пульс измеряется один раз, а режима постоянного измерения, как у нагрудных датчиков, которые передают информацию каждую секунду, там нет?
                      0
                      Вроде можно мерить непрерывно, используя приложение в смартфоне (не пробовал). Но нужно понимать, что измерение пульса при помощи оптического датчика отличается от режима измерения нагрудным датчиком, если он электрокардиографический. Цикл измерения браслетом это примерно 15 секунд, потом данные усредняются и отправляются в виде нотификации. Есть не мало моделей браслетов, которые начинают измерять пульс непрерывно, после нажатия кнопки на браслете.
                        +1
                        Насколько я понимаю, нагрудный датчик, как и оптический, подсчитывает удары сердца, только использует сопротивление, а не свет, и измеряет ближе к источнику, за счёт чего определение RR-интервалов точнее и энергозатраты ниже. Однако, вряд ли он определяет пульс только по RR-интервалам, тоже наверняка усредняет за какое-то время.
                        Gear S сначала несколько секунд измеряет пульс, потом обновляет каждую секунду. Но временами врёт страшно.
                      0
                      Управление виброй — это здорово, а что хотелось бы еще: описание API для управления LED.
                      Притом не просто ID цвета передавать, а полноценное RGB-888 (24бит) значение.
                      И принципиальный вопрос: можно ли с достаточной точностью в этом BLE управлять временем приёма и обработки пакетов, что я имею в виду:
                      например зажигаем LED на 150мс, затем пауза 400мс, затем зажигаем на 750мс, затем опять пауза 400мс.

                      PS: да, я действительно мечтаю сделать на браслете прием сообщений по азбуке Морзе, индикация принимаемого сообщения — по короткой вибрации, началло передачи сообщения — по резкому взмаху (как на многих браслетах и прочих смартвочах включается дисплей по взмаху bottom-top) — кстати, для этого бы надо уметь считывать и интерпретировать даные акселерометра, если их конечно можно вытащить в RAW из «народного» браслета…
                        0
                        LED в новой версии уже не цветные, а белые, во всяком случае нет ни одного штатного режима, который показал бы обратное. Подозреваю, что теперь предусмотрен только один режим — индикация значения по схеме: первый мигает — остальные выключены, первый включен — второй мигает, два включены — третий мигает, три включены. Индикация работает при спаривании со смартфоном, заряде батареи и контроле прогресса (взмахом руки).

                        Что касается акселерометра, данные можно было получить в старой версии в режиме нотификации. Сейчас не понятно по какому адресу искать. Анализ трафика показывает, что смартфон разрешает браслету четыре нотификации, одна из которых это сердечный ритм. Можно посмотреть на три оставшиеся. Если получится, проверю и сделаю update.
                          0
                          Мне первая версия браслета тоже интересна (кстати, в китайском клоне первой версии тоже монохромные LED ставили).
                          Где можно почерпнуть информацию о API первой версии браслета: возможности управления LED и телеметрия с акселерометра — с акселерометром там та еще эпопея, если передаются не последовательности значений (семплированные с периодом 100мс за последние, например, неск. секунд), то с какой периодичностью можно запрашивать эти данные вручную?
                            0
                            вот и вот Но про акселерометр там ничего толкового нет, что-то вроде:
                            0xFF0E read notify SENSOR_DATA, и всё.
                        0
                        Подскажите, а можно ли использовать надетый на руку браслет и раскиданные по помещению активные маячки для определения местоположения человека? В каждой комнате маячок с софтом определения силы сигнала для браслета. Реально ли?
                          0

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

                            0
                            А наоборот нельзя на маячках квартирных силу сигнала браслета смотреть? Тогда не нужно будет в него лезть в нутро.
                              0

                              Маячки обычно только адвертисят и ещё "тупее" браслета, разве нет?

                                0
                                Я образно употребляю слово «маячок». Здесь «маячок» = девайс с кастомной прошивкой, запитанный от сети. Вроде бы в своей прошивке можно организовать мониторинг силы сигнала до нескольких браслетов в радиусе действия?
                                  0

                                  Аа. Я подумал именно про "beacons". Тогда конечно можно. Пруф можно даже собрать из нескольких мобильных телефонов с тулзами из статьи, а триангулировать на бумажке.

                          0

                          del

                          Only users with full accounts can post comments. Log in, please.