11.7. Операції GATT

Характеристика просто знаходиться в базі даних GATT як іменоване значення. Її корисність визначається невеликим, чітко визначеним набором операцій, які клієнт може виконувати над нею. Кожна характеристика оголошує підтримувані операції у вигляді бітової маски властивостей – сервер, якому нічого не потрібно надавати, може публікувати значення лише для читання, керуючий регістр може бути лише для запису, датчик, що передає оновлення, встановлює біт сповіщення. Клієнт виявляє бітову маску під час виявлення і дотримується її.

Існує п’ять операцій: читання, запис, запис без відповіді, сповіщення та індикація. Вони поділяються на дві групи – витягування (клієнт запитує) і відштовхування (сервер надсилає).

11.7.1. Витягування: читання та запис

Ці дві операції є найпростішими і виглядають точно як виклики функцій.

  • Читання. Клієнт запитує поточне значення, сервер надсилає його у відповідь. Один раунд, клієнт отримує будь-які байти, які сервер встановив для цієї характеристики, сервер не отримує нічого про те, хто читав.

  • Запис. Клієнт надсилає нові байти, сервер їх зберігає (і, за бажанням, виконує логіку застосунку для нового значення). Існує два варіанти:

    • Запис з підтвердженням – сервер підтверджує, викликаючи будь-яку помилку застосунку при ненульовому статусі. Надійно, один раунд.

    • Запис без підтвердження – сервер мовчки зберігає байти; клієнт не отримує жодного підтвердження. Швидше (немає раунду очікування підтвердження) і корисно для потокової передачі, ціною того, що про помилки можна дізнатися лише через побічний канал зворотного читання.

В aioble клієнтський API приховує вибір за єдиним методом aioble.ClientCharacteristic.write() з ключовим словом response (True / False / None для автоматичного вибору на основі того, що рекламує однорангова сторона).

11.7.2. Відштовхування: сповіщення та індикація

Модель витягування не підходить для даних датчика. Датчик серцевого ритму, який телефон повинен опитувати кожну секунду, витрачав би батарею на сотні непотрібних подій радіозв’язку; той, що надсилає значення лише при появі нового зчитування, і є суттю BLE.

GATT вирішує це за допомогою операцій, ініційованих сервером. Клієнт підписується на характеристику; з того моменту кожного разу, коли сервер оновлює значення, нове значення передається через канал клієнту. Два варіанти:

  • Сповіщення. Надіслати й забути. Сервер ставить сповіщення в чергу, канальний рівень передає його під час наступної події з’єднання, клієнт його отримує. На рівні GATT підтвердження немає; звичайна повторна передача канального рівня обробляє втрати на радіостороні, але застосунок не бачить підтвердження обробки значення.

  • Індикація. Сервер надсилає сповіщення і чекає підтвердження від клієнта на рівні GATT, перш ніж надіслати наступне. По одній індикації за раз. Використовується, коли серверу потрібно знати, що клієнт дійсно побачив значення – характеристика критичного сигналу тривоги, підтвердження конфігурації.

Two side-by-side diagrams of a server and a client. On the left, the client sends "read", the server replies with the value. Three reads in a row, each pair of arrows. On the right, the client sends a single "subscribe", then the server pushes three "notify" packets at the times it chooses, without any client request in between.

Витягування (читання) проти відштовхування (сповіщення). При сповіщеннях клієнт підписується один раз, а сервер надсилає нові значення щоразу, коли вони змінюються.

Підписка відбувається шляхом запису в дескриптор, прикріплений до характеристики – Дескриптор конфігурації клієнтської характеристики (CCCD, 0x2902). Запис 0x0001 вмикає сповіщення, 0x0002 – індикації, 0x0000 – вимикає обидва. Метод aioble.ClientCharacteristic.subscribe() виконує запис за вас з прапорцями notify=True та indicate=True.

Після підписки клієнт очікує вхідних відправок за допомогою notified() і indicated() – обидві є асинхронними сопрограмами, що призупиняються до надходження наступного повідомлення.

11.7.3. MTU визначає розмір корисного навантаження

Кожна операція обмежена узгодженим MTU, на якому зупинилося з’єднання під час підключення. MTU за замовчуванням – 23 байти, що залишає 20 байтів для байтів значення характеристики після заголовка GATT. Будь-що більше має або вміститися в більший MTU (узгоджується через aioble.DeviceConnection.exchange_mtu(), до 512 байтів на камері) або бути розбитим на кілька характеристик чи кілька сповіщень.

Ініційовані клієнтом читання та записи значень, більших за MTU, обробляються процедурами GATT long у фоновому режимі (Read Long / Prepare-Write + Execute-Write); aioble виконує їх прозоро, тому виклик read() / write() з надмірно великим значенням просто коштує більше раундів. Ініційовані сервером сповіщення та індикації не фрагментуються – одна відправка обмежена MTU, і застосунок розбиває все більше на кілька сповіщень або повністю виходить за рамки GATT.

Для справді великих передач – захоплений кадр, пакет вимірювань, двійковий файл мікропрограми – правильним рішенням зазвичай є повністю відмовитись від GATT і натомість використати канал L2CAP (див. Канали L2CAP).

11.7.4. Обидві сторони з першого погляду

П’ять операцій по-різному проявляються на кожній стороні з’єднання:

  • На сервері (периферійному пристрої, у типовому компонуванні):

    • aioble.Characteristic.read() – зчитує поточне локальне значення з бази даних GATT (серверна сторона «що побачить клієнт»).

    • aioble.Characteristic.write() – оновлює локальне значення, за бажанням передаючи оновлення кожному підписаному клієнту.

    • aioble.Characteristic.notify() / indicate() – надсилає відправку одному конкретному клієнту.

    • aioble.Characteristic.written() – очікує наступного вхідного запису від будь-якого клієнта.

    • aioble.Characteristic.on_read() – зворотний виклик, що викликається синхронно при читанні клієнтом, корисний для обчислення значення за запитом.

  • На клієнті (центральному пристрої, у типовому компонуванні):