11.14. الخلاصة

لقد تنقّلت عبر Bluetooth Low Energy من الراديو وصولًا إلى واجهة Python المستخدمة لتشغيله:

  • الدافع -- BLE هو الإجابة عندما تريد الكاميرا التحدث إلى شيء قريب دون أي بنية تحتية بينهما. هاتف في الغرفة نفسها، أو جهاز يُرتدى على المعصم، أو منارة على الحائط. مدى قصير، ولا شبكة للانضمام إليها، وطاقة شبه معدومة.

  • الراديو -- 2.4 GHz، 40 قناة: ثلاث للإعلان، و37 لبيانات الاتصال، تُقفز على تسلسل شبه عشوائي مع تجنّب تكيّفي للقنوات المزدحمة بالضوضاء. حزم قصيرة، وأجهزة راديو نائمة في الغالب.

  • طبقة الرابط -- تأطير الحزم، والعنونة، وجدولة الاتصال، وإعادة الإرسال، وتشفير طبقة الرابط. لا يُضبط أي منها من Python؛ بل يظهر كله من خلال معاملات الاتصال وقيمة MTU.

  • Generic Access Profile (GAP) -- الاكتشاف وإدارة الاتصال. أربعة أدوار: الطرفي والباثّ (يعلنان)، والمركزي والمراقب (يمسحان). تحمل حمولات الإعلان الاسم المحلي ومعرّفات UUID للخدمات والمظهر والبيانات الخاصة بالمصنّع -- 31 بايت زائد استجابة مسح اختيارية بحجم 31 بايت. وتحكم فترة الاتصال وزمن استجابة الطرفي ومهلة الإشراف الإحساس الذي يمنحه الاتصال المفتوح.

  • Generic Attribute Profile (GATT) -- شجرة من الخدمات، كل منها تحتوي على خصائص، وكل خاصية تحتوي اختياريًا على واصفات، تُعرَّف بمعرّفات UUID (16 بت لمعايير Bluetooth-SIG، و128 بت للمعايير المخصصة). خمس عمليات: القراءة والكتابة (سحب، يبدأها العميل)، والإشعار والإشارة (دفع، يبدأها الخادم، ويُشترك فيها عبر Client Characteristic Configuration Descriptor). حجم الحمولة محدود بقيمة MTU المتفاوض عليها.

  • واجهة Python -- يحوّل aioble كل نمط من أنماط BLE إلى كوروتين asyncio. الطرفي هو aioble.advertise() يدور في حلقة فوق الاتصالات، مع كائنات Service / Characteristic تُبنى مرة واحدة وتُثبَّت بواسطة aioble.register_services(). والمركزي هو aioble.scan() للعثور على نظير، وconnect() لفتح الرابط، وservice() وcharacteristic() للتنقل في شجرة GATT البعيدة، ثم read() / write() / subscribe() / notified() للبيانات الفعلية. تظهر حالات قطع الاتصال على شكل aioble.DeviceDisconnectedError داخل الكوروتين الذي كان ينتظر.

  • قنوات L2CAP -- مخرج الطوارئ لتدفقات البايتات الكبيرة التي لا تتناسب مع نموذج المفتاح/القيمة في GATT. يفتح aioble.DeviceConnection.l2cap_accept() / l2cap_connect() قناة لكل تطبيق فوق اتصال GAP، مع إرسال/استقبال متحكَّم فيه بتدفق الائتمان وقيمة MTU أكبر مما يستطيع GATT حمله.

  • الإقران والتشفير -- روابط BLE عامة افتراضيًا. يبدأ aioble.DeviceConnection.pair() تبادل مفاتيح ينتج عنه رابط مشفّر؛ ويحفظ bond=True (الإعداد الافتراضي) المفاتيح بحيث تتخطى الاتصالات اللاحقة المصافحة. وبدون mitm=True وقدرة إدخال/إخراج قابلة للاستخدام، يحمي التشفير من المتنصتين السلبيين لكنه لا يحمي من إعادة توجيه نشطة أثناء الإقران الأصلي.

هذا يكفي لكتابة تطبيقات كاميرا تنشر الحالة كطرفي، وتقرأ بيانات المستشعرات كمركزي، وتدفع قيمًا حية إلى هاتف عبر BLE، وتؤمّن الرابط بخطوة إقران وربط، و-- للحالة النادرة المتمثلة في النقل بالجملة -- تنتقل من GATT إلى قناة L2CAP.

11.14.1. استكشاف الأخطاء وإصلاحها

إخفاقات BLE هي في الغالب عدم تطابق بين ما يتوقعه الطرفان، وفاحص من جانب الهاتف هو أسرع طريقة لمعرفة توقعات أيهما خاطئة. الأداة القياسية هي nRF Connect for Mobile (Nordic Semiconductor، مجانية على Android وiOS): تمسح وتتصل وتتنقل في قاعدة بيانات GATT وتقرأ الخصائص وتكتبها وتشترك في الإشعارات -- بحيث يمكن اختبار سلوك جانب الكاميرا بمعزل، دون كتابة تطبيق مرافق على الإطلاق.

أنماط الإخفاق الشائعة:

  • "يظهر جهازي في الماسح لكنه لا يتصل." غالبًا ما تكون حزمة الإعلان بها connectable=False (وضع الباثّ)، أو أن اتصالًا سابقًا لا يزال مفتوحًا والكاميرا قد تجاوزت بالفعل aioble.advertise(). أضف عبارات الطباعة حول استدعاء الإعلان للتأكد.

  • "تم تشغيل exchange_mtu(512) لكن إشعاراتي لا تزال محدودة بـ 20 بايت." قيمة MTU المتفاوض عليها هي min(local, peer) -- قد لا يكون الهاتف أو مكتبة الجهاز المركزي قد طلب قيمة MTU أكبر من جانبه، وفي هذه الحالة يبقى الاتصال عند 23. افحص mtu بعد عودة exchange_mtu(). لاحظ أيضًا أن exchange_mtu() يعمل مرة واحدة فقط لكل اتصال؛ فاستدعِه قبل أول عملية كبيرة.

  • "يفشل الإقران بخطأ عام." هناك سببان معتادان: عدم تطابق قدرة الإدخال/الإخراج (طلب mitm=True على كاميرا تعلن io=3 / لا إدخال ولا إخراج -- إذ لا توجد طريقة لتأكيد الرمز الرقمي، فيستسلم محرك الإقران)، ووقت ساعة حائط خاطئ تمامًا على الكاميرا عندما يتطلبه النظير. اضبط الساعة باستخدام ntptime.settime() قبل أول محاولة إقران.

  • "الإشعارات لا تصل أبدًا إلى العميل." أمران للتحقق منهما، بالترتيب: (أ) هل أُعلن عن الخاصية بـ notify=True؟ -- يجب ضبط بت الخاصية على جانب الخادم؛ (ب) هل استدعى العميل subscribe()؟ -- فبدون الكتابة إلى Client Characteristic Configuration Descriptor (CCCD)، يُبلَّغ الخادم بأن لا عميل يريد الإشعارات فيُسقطها بصمت.

  • "الاسم المُعلَن مقطوع أو مفقود." حمولة الإعلان 31 بايت، وحقول الأعلام + معرّف UUID للخدمة + المظهر يأخذ كل منها بايتات من الأعلى. اسم name= طويل بالإضافة إلى عدة معرّفات UUID للخدمات يتجاوز السعة. إما أن تقصّر الاسم أو تستخدم المسح النشط حتى تحمل استجابة المسح (31 بايت أخرى) الفائض. يعرض nRF Connect كلا النصفين بشكل منفصل، مما يجعل التقسيم واضحًا.

  • "اتصال L2CAP يطرح استثناءً فورًا." عادةً عدم تطابق في PSM -- يجب أن يتفق الطرفان على رقم PSM نفسه خارج النطاق. يحمل L2CAPConnectionError رمز حالة Bluetooth كوسيطه الأول؛ والحالة 2 ("PSM not supported") هي الدليل القاطع.

  • "الاتصالات المربوطة لا تزال تطلق مصافحة إقران كاملة عند كل إعادة اتصال." لم يُستدعَ aioble.security.load_secrets() عند بدء التشغيل. وبدونه، تكون المفاتيح المحفوظة على ذاكرة فلاش لكنها لا تُحمَّل أبدًا إلى الذاكرة، فتكون هوية النظير مجهولة ويعمل الإقران من البداية في كل مرة.

وعندما يفشل كل شيء آخر، تعرض وحدة bluetooth منخفضة المستوى دالة رد نداء IRQ تُطلَق لكل حدث أساسي؛ والاشتراك فيها لفترة وجيزة وطباعة الأحداث هو مكافئ لتتبّع Wireshark بالنسبة لجانب الكاميرا.

11.14.2. استخدام هذا المرجع لاحقًا

عامل فصول Bluetooth كمادة مرجعية؛ فالعودة إليها لمعرفة التخطيط الدقيق لحمولة إعلان الطرفي أو تدفق المسح والاشتراك للمركزي هو الاستخدام المقصود. تسرد صفحتا المرجع aioble --- BLE غير المتزامن وbluetooth --- Bluetooth منخفض المستوى كل دالة وعلم وثابت في مكان واحد عندما يكون السؤال مجرد "ما الاسم الدقيق لهذا الاستدعاء".