11.12. Các vai trò đồng thời và nhiều kết nối

Các trang peripheral và central đều hiển thị một vai trò duy nhất phục vụ một kết nối tại một thời điểm. Các ứng dụng thực tế hiếm khi đơn giản như vậy. Một camera có thể xuất bản dịch vụ cảm biến lên điện thoại trong khi đồng thời đọc giá trị từ đai đo nhịp tim, hoặc chấp nhận kết nối từ hai điện thoại được ghép nối đồng thời. API aioble hỗ trợ cả hai mẫu vì radio được ghép kênh bên dưới và mỗi thao tác đã là một coroutine -- chạy thêm coroutine là công việc xảy ra song song trên một event loop.

Trang này tập hợp các mẫu hay gặp.

11.12.1. Nhiều client kết nối đến một peripheral

Vòng lặp peripheral đơn giản trên Hoạt động như một thiết bị ngoại vi phục vụ một central đã kết nối tại một thời điểm:

async def serve():
    while True:
        connection = await aioble.advertise(...)
        async with connection:
            await connection.disconnected()

Mẫu cho phép nó chấp nhận nhiều hơn một client là khởi chạy một task mỗi kết nối và lập tức quay lại aioble.advertise() để client tiếp theo cũng có thể kết nối:

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))

Mỗi kết nối chạy trong task riêng của nó. Cơ sở dữ liệu GATT được chia sẻ -- tất cả client đều thấy cùng dịch vụ và đặc tính -- nhưng trạng thái mỗi kết nối sống trong task. Notification đến mỗi client đã đăng ký khi write() được gọi với send_update=True; các push có hướng chỉ đến một client cụ thể sử dụng notify() / indicate() với tham số DeviceConnection cụ thể.

Giữ số lượng kết nối nhỏ. Mỗi kết nối đang giữ tốn thời gian radio, RAM, và một khe trong bảng kết nối của bộ điều khiển, và camera không được thiết kế để làm hub cho hàng chục client. Hai hoặc ba central (điện thoại, máy tính bảng, vi điều khiển phụ) hoàn toàn trong tầm tay; các thiết kế cần nhiều hơn nên dùng BLE gateway thích hợp thay vì camera.

11.12.2. Peripheral và central cùng lúc

Một camera có thể quảng bá dịch vụ của mình lên điện thoại trong khi đồng thời hoạt động như một central đối với thiết bị đeo được. aioble không có công tắc "mode" -- vòng lặp quảng bá và vòng lặp quét-và-kết nối chỉ là các coroutine độc lập:

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())

Radio chia sẻ thời gian giữa hai vai trò -- một cửa sổ quét ở đây, một đợt quảng bá ở đó, một sự kiện kết nối khi một trong các kết nối ở một trong hai phía đang sống. Thông lượng trên mỗi vai trò giảm khi cả hai đang hoạt động vì radio không thể thực sự làm hai việc cùng một lúc, nhưng đối với các cuộc trò chuyện băng thông thấp mà BLE được thiết kế cho, chi phí thường không đáng kể.

Hai điều thực tế cần ghi nhớ:

  • Cả hai vai trò cần ở trong coroutine riêng của chúng. Gọi aioble.scan() từ bên trong task mỗi client xử lý một central đã kết nối vẫn hoạt động, nhưng sẽ chặn notification của client đó cho đến khi quét hoàn tất -- hãy chạy quét trong task riêng của nó.

  • Chỉ một lần quét chạy tại một thời điểm. Nếu bạn cần quét từ hai nơi khác nhau, hãy chia sẻ iterator quét hoặc phối hợp truy cập; đừng vào hai aioble.scan() context manager song song.

11.12.3. Phối hợp nhiều kết nối từ một task

Khi cần kết hợp nhiều kết nối vào một thao tác logic duy nhất -- ví dụ, camera nói chuyện với hai cảm biến cùng lúc và chỉ báo cáo kết quả sau khi cả hai đã trả lời -- các nguyên thủy asyncio chuẩn áp dụng trực tiếp. asyncio.gather() chạy các coroutine mỗi kết nối đồng thời và trả về khi tất cả hoàn thành; asyncio.wait_for() thêm deadline.

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

Mẫu tương tự mà chương asyncio (Asyncio) sử dụng cho mạng -- các coroutine BLE cắm vào gather / wait_for / Event / Lock theo cùng cách mà các coroutine TCP làm.

11.12.4. Khi một vai trò hoàn thành mỗi chu kỳ còn vai trò kia thì không

Một chu kỳ trong camera chạy bằng pin có thể trông như sau:

  • Thức dậy.

  • Với tư cách central, đọc giá trị mới từ đai cảm biến đã ghép nối.

  • Với tư cách peripheral, quảng bá để điện thoại tải xuống các đo lường trong ngày.

  • Khi cả hai đều nhàn rỗi, gọi aioble.stop() và vào chế độ ngủ.

Việc sắp xếp tuần tự rất đơn giản với hai task và một 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