11.12. บทบาทพร้อมกันและการเชื่อมต่อหลายรายการ

หน้า peripheral และ central แต่ละหน้าแสดงบทบาทเดียวที่ให้บริการการเชื่อมต่อเดียวในแต่ละครั้ง แอปพลิเคชันจริงแทบไม่เคยง่ายอย่างนั้น กล้องอาจเผยแพร่บริการ sensor ไปยังโทรศัพท์ พร้อมกับ อ่านค่าจาก heart-rate strap หรือรับการเชื่อมต่อจากโทรศัพท์ที่จับคู่พร้อมกันสองเครื่อง API ของ aioble รองรับทั้งสองรูปแบบเพราะวิทยุ multiplex อยู่เบื้องล่างและทุกการดำเนินการเป็น coroutine อยู่แล้ว -- รัน coroutine เพิ่มขึ้น และงานจะดำเนินการแบบขนานบน event loop เดียว

หน้านี้รวบรวมรูปแบบที่มักพบ

11.12.1. Client หลายรายเชื่อมต่อไปยัง peripheral เดียว

ลูป peripheral แบบง่ายบน การทำหน้าที่เป็น peripheral ให้บริการ central ที่เชื่อมต่อทีละตัว:

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

รูปแบบที่ช่วยให้รับ client มากกว่าหนึ่ง คือการเปิดตัว task ต่อการเชื่อมต่อและวนซ้ำกลับไปยัง aioble.advertise() ทันทีเพื่อให้ client ถัดไปสามารถเชื่อมต่อได้เช่นกัน:

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 ถูกแชร์ -- client ทั้งหมดเห็นบริการและ characteristic เดียวกัน -- แต่สถานะต่อการเชื่อมต่ออยู่ภายใน task การแจ้งเตือนไปยัง ทุก client ที่สมัครสมาชิกเมื่อเรียก write() ด้วย send_update=True; การ push แบบกำหนดทิศทางที่ควรไปถึง client เดียวเท่านั้นใช้ notify() / indicate() พร้อมอาร์กิวเมนต์ DeviceConnection เฉพาะ

รักษา fan-out ให้เล็ก การเชื่อมต่อที่ถือไว้แต่ละอันใช้เวลาวิทยุ RAM และ slot ในตาราง connection ของ controller และกล้องไม่ได้ออกแบบมาเป็น hub สำหรับ client หลายสิบตัว สอง-สาม central (โทรศัพท์ แท็บเล็ต companion microcontroller) อยู่ในขอบเขตที่เข้าถึงได้; การออกแบบที่ต้องการมากกว่านี้ควรอยู่บน BLE gateway ที่เหมาะสมแทนที่จะอยู่บนกล้อง

11.12.2. Peripheral และ central ในเวลาเดียวกัน

กล้องสามารถโฆษณาบริการของตัวเองไปยังโทรศัพท์ พร้อมกับ ทำหน้าที่เป็น central ไปยัง wearable ด้วย aioble ไม่มีสวิตช์ "โหมด" -- ลูปโฆษณาและลูป scan-and-connect เป็นเพียง coroutine อิสระ:

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

วิทยุแบ่งเวลาระหว่างสองบทบาท -- scan window ที่นี่ burst โฆษณาที่นั่น connection event เมื่อการเชื่อมต่อจากฝ่ายใดฝ่ายหนึ่งมีชีวิตอยู่ ปริมาณงานที่ผ่านได้ในแต่ละบทบาทลดลงเมื่อทั้งสองทำงานพร้อมกันเพราะวิทยุไม่สามารถทำสองสิ่งพร้อมกันได้ แต่สำหรับการสนทนาแบนด์วิดท์ต่ำที่ BLE ออกแบบมาสำหรับ ค่าใช้จ่ายมักจะมองไม่เห็น

สองสิ่งที่ควรคำนึงในทางปฏิบัติ:

  • ทั้งสองบทบาทต้องอยู่ใน coroutine ของตัวเอง. การเรียก aioble.scan() จากภายใน task ต่อ client ที่จัดการ central ที่เชื่อมต่อทำงานได้ แต่จะบล็อกการแจ้งเตือนของ client นั้นจนกว่าการสแกนจะเสร็จสิ้น -- รันการสแกนบน task ของตัวเองแทน

  • มีเพียงการสแกนเดียวที่ทำงานในแต่ละครั้ง. หากคุณต้องการสแกนจากสองที่แตกต่างกัน แชร์ scan iterator หรือประสานการเข้าถึง; อย่าเข้า context manager ของ aioble.scan() สองตัวแบบขนาน

11.12.3. การประสานการเชื่อมต่อหลายรายการจาก task เดียว

เมื่อการเชื่อมต่อหลายรายการต้องรวมกันเป็นการดำเนินการเชิงตรรกะเดียว -- ตัวอย่างเช่น กล้องคุยกับ sensor สองตัวพร้อมกันและรายงานผลลัพธ์หลังจากทั้งคู่ตอบสนองแล้ว -- primitives มาตรฐาน asyncio ใช้ได้โดยตรง asyncio.gather() รัน coroutine ต่อการเชื่อมต่อพร้อมกันและส่งคืนเมื่อทั้งหมดเสร็จสิ้น; asyncio.wait_for() เพิ่ม 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

รูปแบบเดียวกับที่บท asyncio (Asyncio) ใช้สำหรับเครือข่าย -- BLE coroutines เสียบเข้ากับ gather / wait_for / Event / Lock แบบเดียวกับที่ TCP ทำ

11.12.4. เมื่อบทบาทหนึ่งเสร็จสิ้นต่อรอบและอีกบทบาทไม่เสร็จ

รอบหนึ่งในกล้องที่ใช้พลังงานจากแบตเตอรี่อาจมีลักษณะดังนี้:

  • ตื่น

  • ในฐานะ central อ่านค่าใหม่จาก sensor strap ที่จับคู่

  • ในฐานะ peripheral โฆษณาให้โทรศัพท์ดาวน์โหลดการวัดของวัน

  • เมื่อทั้งคู่ไม่ได้ใช้งาน เรียก aioble.stop() และหลับ

การจัดลำดับเป็นเรื่องตรงไปตรงมาด้วยสอง task และ 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