11.12. الأدوار المتزامنة والاتصالات المتعددة

تعرض صفحتا الطرفية والمركزية كلٌّ منهما دورًا واحدًا يخدم اتصالًا واحدًا في المرة. نادرًا ما تكون التطبيقات الواقعية بهذه البساطة. فقد تنشر الكاميرا خدمة مستشعر إلى هاتف بينما تقرأ في الوقت نفسه قيمًا من حزام قياس نبضات القلب، أو تقبل اتصالات من هاتفين مقترنين في آنٍ واحد. تدعم واجهة aioble كلا النمطين لأن الراديو يُجري التعدد الإرسالي في الطبقة السفلى وكل عملية هي أصلًا coroutine -- شغّل المزيد من coroutines، وسيجري العمل بالتوازي على حلقة أحداث واحدة.

تجمع هذه الصفحة الأنماط التي تظهر في الاستخدام.

11.12.1. اتصال عدة عملاء بطرفية واحدة

تخدم حلقة الطرفية البسيطة في العمل كطرفية مركزية واحدة متصلة في المرة:

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 وفتحة في جدول اتصالات المتحكم، والكاميرا ليست مصممة لتكون محورًا لعشرات العملاء. فمركزيتان أو ثلاث (هاتف، جهاز لوحي، متحكم دقيق مرافق) في المتناول تمامًا؛ أما التصاميم التي تحتاج إلى أكثر من ذلك فمكانها بوابة BLE مناسبة لا الكاميرا.

11.12.2. الطرفية والمركزية في الوقت نفسه

يمكن للكاميرا أن تعلن عن خدمتها الخاصة إلى هاتف بينما تعمل أيضًا كمركزية لجهاز قابل للارتداء. لا يوجد لدى 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() من داخل مهمة كل عميل التي تتعامل مع مركزية متصلة يعمل، لكنه يحجب إشعارات ذلك العميل حتى ينتهي المسح -- شغّل المسح في مهمته الخاصة بدلًا من ذلك.

  • يجري مسح واحد فقط في المرة. إذا احتجت إلى المسح من مكانين مختلفين، شارك مكرِّر المسح أو نسّق الوصول؛ لا تدخل مديرَي سياق 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 بالطريقة نفسها التي تتصل بها coroutines الخاصة بـ TCP.

11.12.4. عندما ينتهي أحد الدورين في كل دورة بينما لا ينتهي الآخر

قد تبدو الدورة في كاميرا تعمل بالبطارية كما يلي:

  • الاستيقاظ.

  • كمركزية، اقرأ قيمًا جديدة من حزام مستشعر مقترن.

  • كطرفية، أعلن لكي ينزّل هاتف قياسات اليوم.

  • عندما يكون كلاهما خاملًا، استدعِ 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