11.7. Операции GATT¶
Характеристика просто находится в базе данных GATT как именованное значение. Полезной её делает небольшой, чётко определённый набор операций, которые клиент может над ней выполнять. Каждая характеристика объявляет, какие операции она поддерживает, в виде битовой маски свойств – сервер, которому нечего предоставить, может публиковать значение только для чтения, управляющий регистр может быть только для записи, датчик, передающий обновления потоком, установит бит notify. Клиент обнаруживает битовую маску во время обнаружения и соблюдает её.
Пять операций – это read, write, write without response, notify и indicate. Они делятся на две группы – pull (запрашивает клиент) и push (отправляет сервер).
11.7.1. Pull: read и write¶
Эти две операции самые простые и выглядят в точности как вызовы функций.
Read. Клиент запрашивает текущее значение, сервер отправляет его обратно. Один цикл обмена, клиент получает те байты, которые сервер установил для этой характеристики, сервер не получает никакой информации о том, кто прочитал.
Write. Клиент отправляет новые байты, сервер сохраняет их (и при необходимости выполняет логику приложения над новым значением). Существуют две разновидности:
Write with response – сервер подтверждает, выдавая любую ошибку приложения при ненулевом статусе. Надёжно, один цикл обмена.
Write without response – сервер сохраняет байты молча; клиент вообще не получает подтверждения. Быстрее (нет цикла обмена с ожиданием подтверждения) и полезно для потоковой передачи, ценой того, что об ошибках можно узнать только через побочное обратное чтение.
В aioble API на стороне клиента скрывает этот выбор за единственным методом aioble.ClientCharacteristic.write() с ключевым аргументом response (True / False / None для автоматического выбора на основе того, что рекламирует партнёр).
11.7.2. Push: notify и indicate¶
Модель pull не подходит для данных датчиков. Нагрудный датчик частоты сердечных сокращений, который телефон должен опрашивать каждую секунду, расходовал бы заряд батареи на сотню ненужных радиособытий; датчик, который отправляет значение только при наличии нового показания, и есть смысл BLE как такового.
GATT решает это с помощью операций, инициируемых сервером. Клиент подписывается на характеристику; с этого момента каждый раз, когда сервер обновляет значение, новое значение передаётся по каналу клиенту. Две разновидности:
Notify. Отправил и забыл. Сервер ставит уведомление в очередь, канальный уровень передаёт его во время следующего события соединения, клиент получает его. На уровне GATT нет подтверждения; обычная повторная передача канального уровня обрабатывает потери на стороне радио, но приложение не видит подтверждения того, что значение было обработано.
Indicate. Сервер отправляет уведомление и ждёт подтверждения от клиента на уровне GATT перед отправкой следующего. Одна индикация за раз. Используется, когда серверу нужно знать, что клиент действительно увидел значение – характеристика критического сигнала тревоги, подтверждение конфигурации.
Pull (read) против push (notify). При уведомлениях клиент подписывается один раз, и сервер отправляет новые значения всякий раз, когда они меняются.¶
Подписка происходит путём записи в дескриптор, прикреплённый к характеристике – Client Characteristic Configuration Descriptor (CCCD, 0x2902). Запись 0x0001 включает уведомления, 0x0002 включает индикации, 0x0000 отключает обе. Метод aioble.ClientCharacteristic.subscribe() выполняет запись за вас, с ключевыми флагами notify=True и indicate=True.
После подписки клиент ожидает входящие push-сообщения с помощью notified() и indicated() – обе асинхронные сопрограммы, которые приостанавливаются до прибытия следующего push-сообщения.
11.7.3. MTU управляет размером полезной нагрузки¶
Каждая операция ограничена согласованным значением MTU, на котором остановилось соединение во время установления связи. MTU по умолчанию составляет 23 байта, что оставляет 20 байт для байтов значения характеристики после заголовка GATT. Всё, что больше этого, должно либо помещаться в большее MTU (согласованное в сторону увеличения через aioble.DeviceConnection.exchange_mtu(), до 512 байт на камере), либо быть разбито на несколько характеристик или несколько уведомлений.
Инициируемые клиентом чтения и записи значений больше MTU обрабатываются длинными процедурами GATT за кулисами (Read Long / Prepare-Write + Execute-Write); aioble выполняет их прозрачно, поэтому вызов read() / write() со слишком большим значением просто стоит больше циклов обмена. Инициируемые сервером уведомления и индикации не фрагментируются – одно push-сообщение ограничено MTU, и приложение разбивает всё, что больше, на несколько уведомлений или полностью отказывается от GATT.
Для действительно больших передач – захваченного кадра, пакета измерений, блоба прошивки – правильный ответ обычно состоит в том, чтобы полностью отказаться от GATT и вместо этого использовать канал L2CAP (см. Каналы L2CAP).
11.7.4. Две стороны кратко¶
Пять операций по-разному предоставляются на каждой стороне соединения:
На сервере (периферийное устройство, в обычной схеме):
aioble.Characteristic.read()– прочитать текущее локальное значение из базы данных GATT (серверная сторона того, «что увидел бы клиент»).aioble.Characteristic.write()– обновить локальное значение, при необходимости отправляя обновление каждому подписанному клиенту.aioble.Characteristic.notify()/indicate()– отправить push-сообщение одному конкретному клиенту.aioble.Characteristic.written()– дождаться следующей входящей записи от любого клиента.aioble.Characteristic.on_read()– функция обратного вызова, вызываемая синхронно, когда клиент читает, полезна для вычисления значения по запросу.
На клиенте (центральное устройство, в обычной схеме):
aioble.ClientCharacteristic.read()– запросить у сервера текущее значение.aioble.ClientCharacteristic.write()– отправить новое значение, с ответом или без него.aioble.ClientCharacteristic.subscribe()– включить / отключить уведомления и индикации.aioble.ClientCharacteristic.notified()/indicated()– дождаться следующего push-сообщения.