11.4. Рекламування та сканування#

Два BLE-пристрої, що ніколи раніше не зустрічалися, спершу повинні знайти один одного. Мережа вирішує це, видаючи кожному пристрою адресу зі спільного пулу та дозволяючи будь-якій стороні дістатись до іншої через маршрутизатори. У BLE немає маршрутизаторів, немає спільного пулу і – між більшістю пар пристроїв – жодних попередніх стосунків. Generic Access Profile (GAP) вирішує виявлення за допомогою патерну «широкомовлення і прослуховування». Одна сторона рекламує – вона передає короткий пакет на трьох рекламних каналах з регулярним інтервалом, описуючи себе. Інша сторона сканує – вона прослуховує ті самі три канали в очікуванні таких пакетів.

GAP визначає чотири ролі на основі цього патерну, кожна з яких є конкретною комбінацією рекламування та прослуховування.

11.4.1. Чотири ролі GAP#

A two-by-two matrix. Rows are labelled "advertises" and "does not advertise". Columns are labelled "accepts connections" and "does not accept connections". The four cells contain the role names: Peripheral, Broadcaster, Central, Observer.

Чотири ролі GAP. Вертикальна вісь – чи рекламує пристрій; горизонтальна – чи приймає (або ініціює) з’єднання.#

  • Peripheral рекламує пакети зі змістом «Я тут і ти можеш підключитись до мене». Коли інший пристрій відкриває з’єднання, peripheral припиняє рекламування та починає обробляти GATT-запити. Пульсометри, термометри та більшість камер-датчиків виконують роль peripheral.

  • Central сканує peripheral-пристрої, обирає один і ініціює з’єднання. Після підключення він діє як GATT-клієнт. Телефони, ноутбуки та камери, що виконують роль збирачів даних, є central.

  • Broadcaster рекламує, але ніколи не приймає з’єднання. Його рекламний пакет і є даними – немає нічого, до чого підключатися. iBeacon та більшість маяків присутності в магазинах є broadcaster.

  • Observer сканує ті рекламні пакети та зчитує їхній вміст, теж ніколи не підключаючись. Камера, що прослуховує маяки поблизу та реагує на почуте, є observer.

Один пристрій може одночасно виконувати кілька ролей – камера може бути peripheral, що публікує власний стан, і central, що підключається до сусіднього датчика. Радіо мультиплексує роботу.

11.4.2. Що містить рекламний пакет#

Рекламний пакет невеликий: 31 байт корисного навантаження, або 62, якщо рекламодавець також публікує scan response, який сканери можуть запросити на льоту. Корисне навантаження – це список коротких типізованих полів:

  • Прапори. Можливість підключення, загальний / обмежений режим виявлення.

  • Локальна назва. Коротка, зручна для читання рядок – ім’я, яке операційна система телефону або ноутбука відображає у меню Bluetooth.

  • UUID сервісів. Список ідентифікаторів GATT-сервісів, які розміщує пристрій, щоб сканер міг розпізнати відповідні peripheral-пристрої без попереднього підключення. Пульсометр рекламує 0x180D – стандартний UUID сервісу Heart-Rate – і застосунок пульсометра на телефоні вже з цього розуміє, що пристрій варто підключити.

  • Зовнішній вигляд. 16-бітне значення зі списку призначених номерів Bluetooth (датчик, загальні медіа, загальний годинник, …) – підказка central щодо того, що відображати.

  • Специфічні для виробника дані. Довільні байти з префіксом ідентифікатора компанії. iBeacon використовує це поле для UUID, major та minor; власні застосунки можуть поміщати сюди будь-що.

Рекламні пакети обмежені. Ліміт у 31 байт робить вибір того, що включати, справжнім дизайнерським рішенням – довга зручна для читання назва може легко не залишити місця для UUID сервісів. API aioble.advertise() приймає кожен з них як іменований аргумент і збирає байти за вас, автоматично переповнюючись у scan response, якщо основний пакет заповнений.

11.4.3. Активне та пасивне сканування#

Сканер може працювати в пасивному режимі, де він прослуховує рекламні пакети та розбирає те, що надходить, або в активному, де він також надсилає scan request кожному рекламодавцю та розбирає scan response, що повертається.

Пасивне сканування бачить лише початковий рекламний пакет (до 31 байта). Активне подвоює це – scan response є ще 31 байтом, який peripheral може використати для полів, що не вмістились. Активне сканування також потребує більше енергії з обох сторін, оскільки сканер передає і рекламодавець передає додатковий пакет, тому це – вибір, а не типовий режим.

В API aioble active=True у aioble.scan() перемикає режим, і кожен ScanResult надає комбіновані adv_data та resp_data, а також допоміжні методи, як result.name() та result.services(), що приховують розбір байтів.

Примітка

Атрибути adv_data та resp_data – це необроблені рекламні та scan-response корисні навантаження (bytes). Допоміжні методи – name(), services(), manufacturer() – охоплюють поширені стандартні поля і є правильним вибором у 99% випадків. Звертайтеся до необроблених байтів лише коли потрібне поле виробника, яке допоміжні методи не розбирають (URL Eddystone, UUID/major/minor iBeacon, власні типи рекламування). Розмітка байтів – стандартна TLV: кожне поле – це length, type, value....

11.4.4. Інтервал рекламування#

Частота широкомовлення peripheral – це компроміс між споживанням та затримкою виявлення. Пакети, що надсилаються кожні 20 мс, майже миттєво помічаються сканером, але навантажують радіо та розряджають акумулятор; пакети раз на секунду майже не споживають енергії, але змушують сканер довше помічати пристрій.

interval_us у aioble.advertise() встановлює інтервал у мікросекундах:

  • 20 000–100 000 мкс (20 мс – 100 мс) – швидке зв’язування, застосунок очікує швидкої відповіді, пристрій підключений до мережі.

  • 250 000–1 000 000 мкс (250 мс – 1 с) – розумний типовий інтервал для периферійного пристрою на акумуляторі, що хоче бути виявленим без зайвого споживання заряду.

  • Понад 1 000 000 мкс – повільне фонове широкомовлення, маяки, що надсилають оновлення позиції кожні кілька секунд.

Сторона сканера має власні параметри – aioble.scan() приймає interval_us та window_us (як часто сканер вмикає радіо і як довго прослуховує щоразу). Типові значення цілком підходять; єдина поширена зміна – встановити обидва однаковими для безперервного сканування, коли витрата заряду не є проблемою.

11.4.5. Безз’єднувальні патерни – broadcaster та observer#

Сторінки Робота у ролі периферійного пристрою та Робота в ролі central розглядають з’єднувальну форму API – де peripheral приймає з’єднання і обидві сторони обмінюються даними через GATT. Інша форма – безз’єднувальна: broadcaster передає корисне навантаження у вигляді рекламного пакету, і будь-який observer в зоні досяжності може прочитати його, не підключаючись. Маяки, датчики присутності та односторонній телеметрій – все це тут.

Broadcaster – це aioble.advertise() з connectable=False. Специфічні для виробника дані несуть корисне навантаження:

import aioble
import asyncio
import struct

_COMPANY_ID = const(0xFFFF)                # 0xFFFF is "no specific vendor"

async def beacon():
    seq = 0
    while True:
        seq = (seq + 1) & 0xFFFF
        payload = struct.pack("<H", seq)
        await aioble.advertise(
            interval_us=500000,
            connectable=False,
            name="openmv-beacon",
            manufacturer=(_COMPANY_ID, payload),
            timeout_ms=1000,                # one cycle, then loop
        )

asyncio.run(beacon())

Ключове слово timeout_ms завершує виклик advertise через секунду; зовнішній цикл повторно викликає його з наступним порядковим номером, щоб слухачі бачили свіжі дані. Прапор connectable=False робить рекламування стилю broadcaster – камера не відповідатиме на запит підключення, навіть якщо він надійде.

Observer – це відповідний сканер лише для читання. Він виконує aioble.scan() нескінченно, розбирає вхідні рекламні пакети та ніколи не викликає connect()

import aioble
import asyncio

_COMPANY_ID = const(0xFFFF)

async def watch():
    async with aioble.scan(duration_ms=0, active=False) as scanner:
        async for result in scanner:
            for company, data in result.manufacturer(filter=_COMPANY_ID):
                print(result.device.addr_hex(),
                      "rssi", result.rssi, "data", data)

asyncio.run(watch())

duration_ms=0 сканує до виходу з менеджера контексту; active=False тримає власне радіо observer мовчазним (без запитів scan-response) для мінімального споживання. Аргумент filter= у manufacturer() відкидає кожен рекламний пакет, що не відповідає ідентифікатору компанії, тому цикл спрацьовує лише на трафік broadcaster.

11.4.6. Від виявлення до з’єднання#

Щойно central обирає peripheral для спілкування, він припиняє прослуховування, надсилає запит на з’єднання на рекламному каналі, який peripheral використав останнього разу, і обидві сторони переходять на стрибкоподібні канали даних канального рівня. Peripheral, як правило, в цей момент припиняє рекламування. Те, що відбувається далі – параметри з’єднання, виявлення GATT, час існування посилання – описано на сторінці З’єднання.