Одной из наиболее востребованных технологий передачи данных среди устройств интернета вещей является Bluetooth Low Energy (BLE). Однако начать работу с ним не очень просто по ряду причин, как аппаратного, так и программного характера.
Аппаратные ограничения накладывает сама спецификация Bluetooth, основной сложностью является синхронизация и временные зависимости передачи данных между устройствами. Как следствие, значительно усложняется отладка протоколов и требуются специальные средства (например, от TI или Nordic). По этим же самым практически невозможно начать работу с BLE с нуля. Решением в данном случае являются библиотеки BLE. Однако, и тут всё не так просто, например, наиболее распространённая ОС в России Windows 7 не поддерживает работу с BLE (поддержка появилась только в версиях начиная с 8, которые устанавливались на мобильные устройства, где BLE широко распространён). На Linux ситуация гораздо лучше, однако, тоже не лишена своих недостатков. BlueZ включён в ядро Linux, поддерживает множество платформ и дистрибьютивов. BlueZ первоначально был разработан компанией Qualcomm в середине нулевых и в дальнейшем доработан сообществом. В стандартном пакете BlueZ включены 2 полезные утилиты позволяющие работать с Bluettooth устройствами через консоль:
Однако, по личному опыту можем отметить, что поведение данных утилит может отличаться в зависимости от версии стека и дистрибьютива. К сожалению, периодически некоторые функцию перестают работать, как ожидается, а понять это сразу зачастую бывает сложно, так не очевиден источник проблемы — отлаживаемая программа, конечное устройство, стек или драйвера ОС. Решение в данном случае получается очень простое и красивое, однако не очевидное для большинства. Начиная с версии 5.0 BlueZ работает через D-Bus. D-Bus — система межпроцессорного взаимодействия разработки FreeDesktop. Рассмотрим работу с шиной D-Bus напрямую без использования утилит.
Кратко можно представить работу следующим образом : существует общая шина в виде дерева, представляющая те или иные объекты, именуемые сервисами, которые могут иметь дочерние объекты и функции, к которым можно обращаться. Каждый участник взаимодействия посылает сообщение другому участнику или группе (возможны широковещательные посылки). С точки зрения пользователя достаточно подключиться к шине (системной или сессионной) в нужном месте (например, к BLE-адаптеру или любому другому устройству), вызвать нужную функцию (например, сканировать доступные устройства в радиоэфире, см. пример ниже), при необходимости обновить дерево (сканирование добавляет в дерево новые дочерние узлы/ветви), а в дальнейшем работать уже с уточнёнными узлами дерева, (например характеристиками подключённого устройства). С другой стороны можно повесить функцию-обработчик на событие, который будет ждать сообщений по шине D-Bus от других устройств в соответствии с шаблоном. Это полезно для фильтрации только своих сообщений, так как через D-Bus может проходить множество данных. Данный способ передачи данных нашёл широкое примение в Linux в целом. Существует версия под Android, а под Windows находится в стадии разработки. Поэтому данный инструмент видится очень перспективным.
Рассмотрим средства разработки и взаимодействия с D-Bus. Существуют удобные графические инструменты для работы с D-Bus, например, D-Feet или QDBusViewer, которые отображает дерево в удобочитаемом виде и позволяет произвести основные операции: найти нужный сервис по маске, прочитать или записать в него, вызвать функцию с аргументом. Для первого шага — самое оно, пройтись по структуре и понять с чем же предстоит работать и каким образом.
Также стоит отметить, что для работы с D-Bus существуют библиотеки под множество языков программирования. В итоге с применением D-Bus задача взаимодействия с BLE устройствами из под Linux значительно упрощается.
Рассмотрим пример кода на Python. Сразу стоит отметить, что существует 2 похожие библиотеки для работы с D-Bus из Python. Необходимо использовать pydbus, как наиболее современный и соответствующий философии Python.
import pydbus from gi.repository import GObject from gi.repository import GLib import time import threading import sys
Функция поиска устройств BLE запускается командой StartDiscovery() из дерева.
def find_ble_devices(bus, hci, timeout=10): # Found BLE devices om = bus.get('org.bluez', '/') devices = {} try: hci.StartDiscovery() except Exception: hci.StopDiscovery() hci.StartDiscovery() time.sleep(timeout) objects = om.GetManagedObjects() hci.StopDiscovery() for path, interfaces in objects.items(): if "org.bluez.Device1" in interfaces: devices[path] = interfaces["org.bluez.Device1"] return devices
2 варианта обработчиков подписки на события (Notify) — простой и детальный:
def temp1_notify(source, value, c): print("In temp1_notify: source={}, Value={}".format(source, value)) def temp3_notify(args): object, dest, path, iface, signal, arg = args # print("In temp3_notify: path={}, iface={}, signal={}, arg={}".format(path, iface, signal, arg)) try: print("In temp3_notify: path={}, signal={}, Value={}".format(path, signal, arg[1]['Value'])) except KeyError: print("In temp3_notify: path={}, signal={}, arg={}".format(path, signal, arg[1]))
Функция сканирует дерево bluez подключённого устройства и по UUID характеристики выдаёт путь в дереве (удобно для поиска сервисов BLE).
def path_from_UUID(UUID): # main object for bluez tree bluez_tree = bus.get("org.bluez", "/" # Object path ) list_charact = [] # scan of characteristics/devices with required name for i in bluez_tree.GetManagedObjects(): # only for services (including characteristics) if "service" in i: obj = bus.get("org.bluez", i) if UUID in obj.UUID: list_charact.append(i) return (list_charact)
Основной цикл:
if __name__ == "__main__": bus = pydbus.SystemBus() hci = bus.get("org.bluez", "/org/bluez/hci0" # Object path ) devices = find_ble_devices(bus, hci, timeout=3) for i in devices.keys(): # print(i) ble_dev_addr = i break # get first key from dict # connect to device ble_dev = bus.get("org.bluez", ble_dev_addr # device path ) # connect and got D-Bus Tree of characteristics # required only first time ble_dev.Connect() ble_dev.Introspect() time.sleep(2) read_period_path = path_from_UUID("0000b006")[0] read_period = bus.get( "org.bluez", # Bus name read_period_path ) ble_dev.Connect()
Для характеристики с возможностью чтения и записи:
читаем значение характеристики, перезаписываем её и читаем снова уже новое значение.
try: read_period.ReadValue({}) print("in " + read_period.UUID + " we have :" + str(read_period.Value)) read_period.WriteValue([255], {}) read_period.ReadValue({}) print("in " + read_period.UUID + " we have :" + str(read_period.Value)) except (GLib.Error): print("GLib.Error", sys.exc_info()) except: print ("Unexpected error:", sys.exc_info()) raise
Для характеристики с возможностью чтения и оповещения:
читаем (в ручном режиме)
# Temperature from sensor 1 temp1_path = path_from_UUID("0000b001")[0] temp1 = bus.get( "org.bluez", # Bus name temp1_path ) # Temperature from sensor 2 temp2_path = path_from_UUID("0000b003")[0] temp2 = bus.get( "org.bluez", # Bus name temp2_path ) print("temp1_path :", temp1_path) print("temp2_path :", temp2_path)
и настраиваем 2 варианта подписки на события (Notify), которые генерирует D-Bus в момент, когда характеристика периферийного устрйоства прислало оповещение об изменении состояния.
temp1.PropertiesChanged.connect(temp1_notify) # subscribe to required Signal and object in second way bus.con.signal_subscribe(None, None, # sender, iface "PropertiesChanged", # signal temp2_path, # object None, 0, # arg0, flags lambda *args: temp3_notify(args) # signal_fired ) temp1.StartNotify() temp2.StartNotify()
В данной статье мы рассмотрели работу с BLE устройствами через D-Bus (BlueZ). Код получается простой , читаемый и легко адаптируемый под задачи.