11.12. תפקידים מקבילים וחיבורים מרובים

עמודי ה-peripheral וה-central מציגים כל אחד תפקיד יחיד המשרת חיבור יחיד בכל רגע. יישומים אמיתיים הם לעתים רחוקות כה פשוטים. מצלמה עשויה לפרסם שירות חיישן לטלפון תוך כדי קריאת ערכים מרצועת מד-דופק, או לקבל חיבורים משני טלפונים מצומדים בו-זמנית. ה-API של aioble תומך בשני הדפוסים מכיוון שהרדיו ממַרבֵּב (multiplexes) מתחת לפני השטח וכל פעולה היא כבר coroutine – הריצו עוד coroutines, והעבודה מתרחשת במקביל על לולאת אירועים אחת.

עמוד זה אוסף את הדפוסים שצצים.

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 ומשבצת בטבלת החיבורים של הבקר, והמצלמה אינה מתוכננת לשמש כצומת עבור עשרות לקוחות. שניים או שלושה centrals (טלפון, טאבלט, מיקרו-בקר נלווה) הם בהחלט בהישג יד; תכנונים הזקוקים ליותר שייכים לשער BLE ייעודי ולא למצלמה.

11.12.2. Peripheral ו-central בו-זמנית

מצלמה יכולה לפרסם את השירות שלה לטלפון תוך גם פעולה כ-central כלפי התקן לביש. ל-aioble אין מתג ”מצב“ – לולאת הפרסום ולולאת הסריקה-והחיבור הן פשוט coroutines עצמאיים:

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 תוכנן עבורן, המחיר הוא בדרך כלל בלתי נראה.

שני דברים מעשיים שכדאי לזכור:

  • שני התפקידים צריכים להיות ב-coroutine משלהם. קריאה ל-aioble.scan() מתוך המשימה-לכל-לקוח המטפלת ב-central מחובר עובדת, אך חוסמת את ההתראות של אותו לקוח עד שהסריקה מסתיימת – במקום זאת הריצו את הסריקה במשימה משלה.

  • רק סריקה אחת רצה בכל רגע. אם עליכם לסרוק משני מקומות שונים, שתפו את איטרטור הסריקה או תאמו גישה; אל תיכנסו לשני מנהלי הקשר של aioble.scan() במקביל.

11.12.3. תיאום חיבורים מרובים ממשימה אחת

כאשר יש לשלב מספר חיבורים לפעולה לוגית אחת – לדוגמה, המצלמה מדברת עם שני חיישנים בו-זמנית ומדווחת את התוצאה רק לאחר ששניהם ענו – הפרימיטיבים הסטנדרטיים של asyncio חלים ישירות. asyncio.gather() מריצה את ה-coroutines לכל חיבור במקביל ומחזירה כאשר כולם הסתיימו; 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) משתמש בו לתקשורת רשת – coroutines של 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