11.8. وحدة aioble

تمنح مواصفة Bluetooth Core مفرداتٍ تنعكس على وحدتي MicroPython.

  • bluetooth -- الربط منخفض المستوى بمتحكم BLE. متزامن، ومُحرَّك بالأحداث عبر دالة رد نداء بنمط IRQ، ومبنيّ حول مخازن البايتات والمقابض وأوليّات GATT المجردة. يكشف البروتوكول كما هو، لا كما تريد تطبيقات Python استهلاكه.

  • aioble -- غلاف أعلى مستوى، مكتوب بـ Python فوق bluetooth، يحوّل كل عملية بعيدة إلى coroutine في asyncio وكل كائن BLE (الخدمات والخصائص والاتصالات ونتائج المسح وقنوات L2CAP) إلى صنف Python مريح للاستخدام. تصبح عمليات المسح مكرِّرات غير متزامنة؛ وتصبح الاتصالات مديري سياق غير متزامنين؛ وتصبح الإشعارات قابلة للانتظار.

11.8.1. متى تلجأ إلى الوحدة الأدنى مستوى

bluetooth ما زال الجواب الصحيح لحالتين ضيّقتين:

  • أنت تكتب نوع الشيفرة الذي بُني منه aioble نفسه -- نمط جديد يحتاج إلى تحكم على مستوى IRQ في البروتوكول.

  • أنت تعمل على هدف عتاد لا تتوفر فيه حزمة aioble، وتكون طبقة رقيقة حول المتحكم هي الخيار الوحيد.

بالنسبة لكل تطبيقات الكاميرا، aioble هو الجواب الصحيح.

11.8.2. أجزاء برنامج aioble

لكل تطبيق مبنيّ على aioble مجموعة صغيرة من الأجزاء المتحركة، بغض النظر عن الأدوار التي يؤديها.

  • حلقة أحداث asyncio طويلة العمل. كل شيء في aioble هو coroutine، لذا يُبنى التطبيق على هيئة مهمة واحدة أو أكثر على حلقة أحداث واحدة. راجع Asyncio للاطلاع على الحلقة والمهام والاستثناءات بالتفصيل.

  • راديو يعمل. يفعّل aioble راديو BLE ضمنيًا عند أول استخدام، لكن يمكن أيضًا التحكم به صراحةً عبر aioble.config() (التي تمرّر إلى bluetooth.BLE.config() بعد التأكد من تشغيل الراديو) وإيقافه عبر aioble.stop().

  • دور أو أكثر في العمل في آنٍ واحد. على جانب الطرفية: مجموعة مسجّلة من خدمات GATT (راجع aioble.register_services()) و coroutine aioble.advertise() قيد التشغيل. على جانب المركزية: مكرِّر aioble.scan() قيد التشغيل أو aioble.Device.connect() معلَّق. يجري الراديو التعدد الإرسالي للعمل؛ ويرى التطبيق كل دور كمهمة مستقلة.

11.8.3. طرفية بالحد الأدنى

أصغر برنامج aioble مفيد -- طرفية تعلن عن خاصية واحدة للقراءة فقط -- قصير:

import aioble
import asyncio
import bluetooth

SERVICE_UUID = bluetooth.UUID(0x181A)            # Environmental Sensing
TEMP_UUID = bluetooth.UUID(0x2A6E)               # Temperature

service = aioble.Service(SERVICE_UUID)
temp = aioble.Characteristic(service, TEMP_UUID, read=True)
aioble.register_services(service)

async def main():
    while True:
        conn = await aioble.advertise(
            interval_us=250000,
            name="openmv-temp",
            services=[SERVICE_UUID],
        )
        async with conn:
            await conn.disconnected()

asyncio.run(main())

ومركزية لا تفعل أكثر من الاتصال والقراءة مرة واحدة قصيرة بالمثل:

import aioble
import asyncio
import bluetooth

SERVICE_UUID = bluetooth.UUID(0x181A)
TEMP_UUID = bluetooth.UUID(0x2A6E)

async def main():
    device = None
    async with aioble.scan(duration_ms=5000, active=True) as scanner:
        async for result in scanner:
            if SERVICE_UUID in result.services():
                device = result.device
                break
    if device is None:
        return

    async with await device.connect() as conn:
        service = await conn.service(SERVICE_UUID)
        char = await service.characteristic(TEMP_UUID)
        print(await char.read())

asyncio.run(main())

كلا البرنامجين نحو خمسة عشر سطرًا ويغطيان التدفق كاملًا من "الراديو متوقف" إلى "العمل المفيد منجَز".

11.8.4. إيقاف الراديو

على كاميرا تعمل بالبطارية، راديو BLE هو أكبر استهلاك اختياري في الميزانية. هناك مفتاحان مهمّان.

الأول ضمني: يفعّل aioble الراديو عند أول استخدام، وينام الراديو بين الأحداث المجدولة (دفعات الإعلان وأحداث الاتصال ونوافذ المسح) تلقائيًا. اختيار فترات أطول في aioble.advertise() / aioble.scan() والاتفاق على فترة اتصال أطول وقت connect() يُبقي الراديو متوقفًا نسبةً أكبر من الوقت. جدول الإعلان في الإعلان والمسح هو الدليل العملي هنا.

والثاني إيقاف صريح

import aioble

await do_burst_of_ble_work()
aioble.stop()                             # radio deactivated; in-flight tasks unwound
await asyncio.sleep(60)                   # sleep with the radio off
# ... next aioble call brings the radio back up automatically

تعطّل aioble.stop() راديو BLE الأساسي وتفكّك كل ما هو قيد العمل -- تسقط الاتصالات المفتوحة، وتُلغى أجهزة المسح والإعلان، وتُغلق قنوات L2CAP. تثير الـ coroutines التي كانت تنتظر تلك العمليات استثناءاتها المعتادة (DeviceDisconnectedError وأخواتها)، وهي آلية التنظيف التي كُتبت من أجلها كتل async with المحيطة. واستدعاء أي coroutine في aioble بعد ذلك يفعّل الراديو من جديد من الإيقاف البارد.

النمط المعتاد لكاميرا مستشعر دورية تعمل بالبطارية هو:

  • الاستيقاظ وفق جدول (مؤقت، مستشعر حركة، زر).

  • تشغيل دفعة عمل BLE -- أعلن، اقبل اتصالًا، ادفع القيمة، اقطع الاتصال.

  • استدعِ aioble.stop() ونم حتى الاستيقاظ التالي.

11.8.5. ما لا يفعله aioble

يغطي aioble عن قصد GATT وGAP وL2CAP -- الطبقات التي يستخدمها التطبيق. ثلاثة أجزاء خارج النطاق:

  • أي شيء تحت طبقة الوصلة. اختيار القنوات والقفز الترددي وإقرارات الحزم وتشفير طبقة الوصلة كلها تحدث داخل منفذ BLE وسيليكون المتحكم؛ ولا يكشف aioble خطافات على ذلك المستوى.

  • Bluetooth الكلاسيكي. aioble خاص بـ BLE فقط. الوصلات الصوتية وRFCOMM وA2DP وسائر ميزات الملف الكلاسيكي ليست جزءًا من واجهة برمجة التطبيقات.

  • Bluetooth Mesh. طبقة الشبكات الشبكية من Bluetooth SIG (مكدّس منفصل فوق إعلانات BLE) غير مطبّقة على الكاميرا. يمكن للكاميرا أن تُعلن وتراقب، لكنها لا تستطيع المشاركة في أدوار التتابع / الصديق / الوكيل في شبكة شبكية.

11.8.6. الاستثناءات

تخرج أربعة أنواع استثناءات من aioble. ينطلق كلٌّ منها من داخل coroutine كان ينتظر عملية عندما يحدث خطأ ما؛ وتتفكّك كتل async with بنظافة عند انتشارها.

  • aioble.DeviceDisconnectedError -- سقطت وصلة BLE إلى النظير بينما كانت عملية GATT (read أو write أو notified أو indicated أو subscribe أو exchange_mtu ...) قيد العمل. تُثار داخل أيًّا كان الـ coroutine الذي كان ينتظر. الاستثناء الأكثر شيوعًا بفارق كبير؛ التقطه في أي شيفرة ينبغي أن تعيد الاتصال عند الفقدان.

  • aioble.GattError -- وصلت عملية GATT إلى النظير لكنها اكتملت بحالة ATT غير صفرية (رفض الكتابة مع الاستجابة، أو عدم الإقرار بالإشارة، أو عدم السماح بالقراءة ...). رمز الحالة موجود على السمة _status للاستثناء.

  • aioble.L2CAPDisconnectedError -- سقطت قناة L2CAP بينما كانت send() أو recvinto() أو flush() قيد العمل. ربما أغلق أي من الطرفين القناة، أو اختفى اتصال GAP الأساسي.

  • aioble.L2CAPConnectionError -- تُثار من l2cap_connect() عندما يرفض المستمع أو يفشل المتحكم في إعداد القناة. رمز حالة Bluetooth هو الوسيط الموضعي الأول.

العمليات التي تأخذ timeout_ms صريحة (استدعاءات connect / discovery / read / write / pair، بالإضافة إلى timeout() كغلاف) تثير إضافةً asyncio.TimeoutError من asyncio عندما تنقضي المهلة قبل اكتمال العملية.