11.12. Паралельні ролі та множинні з’єднання¶
Сторінки peripheral та central кожна демонструє одну роль, що обслуговує одне з’єднання за раз. Реальні застосунки рідко бувають такими простими. Камера може публікувати службу датчика для телефону одночасно зчитуючи значення з нагрудного кардіодатчика, або приймати з’єднання від двох одночасно спарених телефонів. API aioble підтримує обидва шаблони, оскільки радіо мультиплексує на низькому рівні, а кожна операція вже є корутиною — запустіть більше корутин, і робота відбувається паралельно в одному циклі подій.
На цій сторінці зібрано шаблони, що трапляються найчастіше.
11.12.1. Кілька клієнтів, що підключаються до одного peripheral¶
Простий цикл peripheral на Робота у ролі периферійного пристрою обслуговує один підключений central за раз:
async def serve():
while True:
connection = await aioble.advertise(...)
async with connection:
await connection.disconnected()
Шаблон, що дозволяє приймати більше ніж одного клієнта, полягає у запуску завдання для кожного з’єднання та негайному поверненні до aioble.advertise(), щоб наступний клієнт теж міг підключитися:
async def handle_client(connection):
async with connection:
# ... per-client work: subscribe their CCCDs,
# push notifications, await writes ...
await connection.disconnected()
async def serve():
while True:
connection = await aioble.advertise(
interval_us=250000,
name="openmv-env",
services=[ENV_SERVICE],
)
asyncio.create_task(handle_client(connection))
Кожне з’єднання виконується у власному завданні. База даних GATT є спільною — всі клієнти бачать одні й ті самі служби та характеристики — але стан конкретного з’єднання живе всередині завдання. Сповіщення відправляються кожному підписаному клієнту при виклику write() з send_update=True; цільові пуш-повідомлення, призначені лише одному клієнту, використовують notify() / indicate() з конкретним аргументом DeviceConnection.
Тримайте кількість з’єднань невеликою. Кожне активне з’єднання витрачає радіочас, RAM і слот у таблиці з’єднань контролера, а камера не призначена бути хабом для десятків клієнтів. Два або три central (телефон, планшет, супровідний мікроконтролер) — цілком реально; конструкції, що потребують більшого, належать до справжнього BLE-шлюзу, а не до камери.
11.12.2. Peripheral та central одночасно¶
Камера може рекламувати власну службу для телефону, одночасно виступаючи як central для носимого пристрою. aioble не має перемикача «режиму» — цикл реклами та цикл сканування-і-підключення є просто незалежними корутинами:
async def be_peripheral():
while True:
connection = await aioble.advertise(
interval_us=250000,
name="openmv-hub",
services=[ENV_SERVICE],
)
asyncio.create_task(handle_client(connection))
async def be_central():
while True:
sensor = await find_sensor()
if sensor is None:
await asyncio.sleep(5)
continue
try:
async with await sensor.connect() as conn:
await stream_from_sensor(conn)
except aioble.DeviceDisconnectedError:
pass
async def main():
await asyncio.gather(be_peripheral(), be_central())
asyncio.run(main())
Радіо розподіляє час між двома ролями — тут вікно сканування, там спалах реклами, подія з’єднання, коли одне із з’єднань будь-якої зі сторін активне. Пропускна здатність кожної ролі падає, коли обидві активні, бо радіо буквально не може робити дві речі одночасно, але для низькосмугових розмов, для яких призначений BLE, ця вартість зазвичай непомітна.
Два практичних моменти, які варто пам’ятати:
Обидві ролі мають бути у власній корутині. Виклик
aioble.scan()всередині завдання підключеного central працює, але блокує сповіщення цього клієнта до завершення сканування — запускайте сканування у власному завданні.Одночасно виконується лише одне сканування. Якщо потрібно сканувати з двох різних місць, використовуйте спільний ітератор сканування або координуйте доступ; не входьте у два контекстних менеджери
aioble.scan()паралельно.
11.12.3. Координація кількох з’єднань з одного завдання¶
Коли кілька з’єднань потрібно об’єднати в одну логічну операцію — наприклад, камера спілкується з двома датчиками одночасно і повідомляє результат лише після відповіді обох — стандартні примітиви asyncio застосовуються безпосередньо. asyncio.gather() виконує корутини кожного з’єднання паралельно і повертається після завершення всіх; asyncio.wait_for() додає дедлайн.
async def read_pair():
async with await sensor_a.connect() as a:
async with await sensor_b.connect() as b:
value_a, value_b = await asyncio.gather(
read_value(a, A_SERVICE, A_CHAR),
read_value(b, B_SERVICE, B_CHAR),
)
return value_a, value_b
Той самий шаблон, що розділ asyncio (Asyncio) використовує для мережевої роботи — BLE-корутини підключаються до gather / wait_for / Event / Lock так само, як і TCP-корутини.
11.12.4. Коли одна роль завершує цикл, а інша — ні¶
Цикл у камері з батарейним живленням може виглядати так:
Прокинутися.
Як central, зчитати нові значення від спареного датчика.
Як peripheral, рекламувати для телефону завантаження денних вимірювань.
Коли обидва простоюють, викликати
aioble.stop()та перейти у сон.
Послідовність проста з двома завданнями та asyncio.Event
phone_done = asyncio.Event()
async def serve_phone():
connection = await aioble.advertise(
interval_us=250000,
name="openmv-hub",
services=[ENV_SERVICE],
)
async with connection:
await stream_measurements(connection)
phone_done.set()
async def read_strap():
async with await strap.connect() as conn:
await pull_fresh_values(conn)
async def cycle():
await asyncio.gather(read_strap(), serve_phone())
aioble.stop() # radio off until next wake