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, час існування посилання – описано на сторінці З’єднання.