11.11. قنوات L2CAP¶
GATT هو نموذج مفتاح/قيمة. تنقل العمليات التي يوفّرها (read وwrite وnotify وindicate) قيمة قصيرة واحدة في المرة، وأكبر حمولة منفردة يمكنها حملها هي ما يسمح به MTU المتفاوَض عليه -- بضع مئات من البايتات في أحسن الأحوال. يعمل ذلك جيدًا لقراءات المستشعرات وسجلات الأوامر وأعلام الحالة. لكنه ينهار عند الكيلوبايتات أو الميغابايتات: فتقسيم كتلة طويلة إلى مئات من عمليات الكتابة الصغيرة يكلّف رحلات ذهاب وإياب يفوقها الراديو سرعةً بكثير.
بالنسبة لتدفقات البيانات الكبيرة -- إطار ملتقط تبثّه الكاميرا إلى هاتف، أو صورة تحديث عبر الأثير، أو تصدير دفعيّ للقياسات -- يقدّم BLE مسارًا بديلًا: بروتوكول التحكم بالوصلة المنطقية والتكييف (Logical Link Control and Adaptation Protocol)، أي L2CAP. يقع L2CAP بين طبقة الوصلة وGATT ويتيح للتطبيق المطالبة بـ قناة موجَّهة بالاتصال خاصة به فوق وصلة الراديو نفسها. القناة مسار بايتات يخضع للتحكم بتدفق الائتمان وله MTU أكبر بكثير لكل حزمة وبلا تأطير GATT في الوسط.
11.11.1. متى تستخدم L2CAP¶
قنوات L2CAP هي الأداة المناسبة عندما:
يتجاوز النقل بضع مئات من البايتات.
يعلم الطرفان أن قناة L2CAP ستُستخدم (فهي غير مكشوفة في حمولة الإعلان؛ على العميل أن يعرف رقم معدِّد بروتوكول/خدمة القناة، أو PSM، عبر قناة خارجية).
يكون التطبيق مستعدًا للتخلي عن مزايا GATT: لا قابلية عنونة مدمجة عبر UUID، ولا قابلية اكتشاف من قِبل العميل عبر التطبيقات القياسية، ولا إشعارات.
الحالة الأكثر شيوعًا في التطبيقات المبنية على aioble هي نقل كتلة ثنائية بين قطعتي برمجيات تعرفان كلتاهما عرف PSM -- بروتوكول مخصص بين الكاميرا والهاتف، أو زوج من كاميرات openmv يتحدث بعضها إلى بعض، أو مسار تحديث برنامج ثابت داخلي ضمن خدمة GATT لطرفية.
بالنسبة لكل ما عدا ذلك، ابقَ على GATT. فحالة قصيرة، أو سجل تحكم، أو قراءة مستشعر -- كل تلك تنتمي إلى خاصية.
11.11.2. إنشاء قناة¶
يجري L2CAP فوق اتصال aioble.DeviceConnection قائم، لذا فإن تدفق الاكتشاف / الإعلان / الاتصال على جانب GAP هو نفسه تمامًا كما في GATT. فبمجرد أن يحمل الطرفان اتصالًا، يستمع أحدهما على PSM ويتصل به الآخر.
PSM مجرد عدد صحيح صغير. تحجز Bluetooth SIG أسفل المدى للاستخدام المعياري (0x0001-0x007F)؛ أما للقنوات الخاصة بالتطبيقات فاستخدم رقمًا من المدى الديناميكي (0x0080-0x00FF لـ PSMs الثابتة، و0x0040 فما فوق متاح عادةً للاستخدام المخصص). على الطرفين أن يتفقا على القيمة مسبقًا.
MTU على قناة L2CAP هو أكبر SDU منفرد (وحدة بيانات الخدمة، Service Data Unit) سيُسلّمه أي من الطرفين في send() واحد -- وليس MTU وصلة BLE. يجزّئ Aioble الحمولات الأكبر تلقائيًا. يحدّ مضيف BLE في الكاميرا MTU الخاص بـ L2CAP عند 1017 بايتًا؛ و512 قيمة افتراضية معقولة تترك متسعًا على الطرفين دون استنزاف ذاكرة RAM.
على جانب المستمع (مثلًا الكاميرا كطرفية):
async def serve_l2cap(connection, image_bytes):
channel = await connection.l2cap_accept(psm=0x80, mtu=512)
async with channel:
# image_bytes is a bytearray -- e.g. csi0.snapshot().bytearray()
# or a compressed JPEG buffer. send() fragments into MTU-sized
# chunks automatically and awaits flow-control credits between.
await channel.send(image_bytes)
await channel.flush()
على جانب الموصِّل (مثلًا هاتف أو مركزية):
async def open_l2cap(connection, total_bytes):
channel = await connection.l2cap_connect(psm=0x80, mtu=512)
async with channel:
image_bytes = bytearray(total_bytes)
view = memoryview(image_bytes)
received = 0
while received < total_bytes:
n = await channel.recvinto(view[received:])
if n == 0:
break
received += n
return image_bytes
تحجب l2cap_accept() حتى يتصل النظير (أو حتى تنطلق timeout_ms)؛ وتحجب l2cap_connect() حتى يقبل المستمع (أو يفشل). يعيد كلاهما aioble.L2CAPChannel -- وهو بذاته مدير سياق غير متزامن يغلق القناة عند الخروج.
11.11.3. الإرسال والاستقبال¶
العمليتان الرئيسيتان على القناة هما send() (تكتب البايتات إلى النظير) وrecvinto() (تقرأ إلى مخزن مؤقت مخصص مسبقًا). كلاهما coroutines.
تجزّئ
send()المخزن المؤقت إلى أجزاء بحجم MTU وتنتظر ائتمانات التحكم بتدفق طبقة الوصلة بينها. الإرسال الطويل هوawaitواحد من منظور التطبيق؛ لكنه داخليًا قد يصفّ العديد من الحزم ويتوقف كلما نفدت ائتمانات استقبال النظير.تملأ
recvinto()المخزن المؤقت المُمرَّر بكل ما هو متاح (حتى MTU الخاص بالقناة) وتعيد عدد البايتات. وتنتظر إذا لم يكن هناك شيء متاح.تعيد
available()القيمةTrueبصورة متزامنة إذا كانت هناك بيانات مخزَّنة جاهزة -- مفيدة للاستقصاء دون التعليق.تنتظر
flush()حتى يُرسَل أي إرسال معلَّق بالكامل إلى المتحكم.
قنوات L2CAP تشبه التدفقات بمعنى أن البايتات تصل بالترتيب ودون فقدان، لكن حدود send المنفرد محفوظة -- يخرج كل SDU من recvinto واحد. وهذا بخلاف TCP، حيث قد تتلطّخ حدود send() واحد عبر عدة استدعاءات recv().
11.11.4. التعامل مع قطع الاتصال¶
تختفي القناة عند ثلاث حالات: استدعاء أي من الطرفين لـ disconnect()، أو سقوط اتصال GAP الأساسي، أو وصول قطع اتصال على مستوى L2CAP. تثير العمليات النشطة aioble.L2CAPDisconnectedError. وكما في جانب GATT، يظهر هذا كاستثناء في الـ coroutine الذي كان ينتظر، ويخرج كتلة async with channel بنظافة.
إذا أصبحت القناة غير قابلة للوصول عبر قطع اتصال على مستوى GAP، يعود التطبيق إلى الإعلان أو المسح بالطريقة نفسها التي سيفعل بها مع قطع اتصال GATT.
11.11.5. تكلفة الذاكرة¶
تستخدم قيم MTU الأكبر والطوابير الأطول مزيدًا من ذاكرة RAM على الطرفين. فـ MTU بحجم 512 بايت زائد مخزن استقبال لكل قناة يبلغ نحو 1 كيلوبايت لكل قناة -- وليس مجانيًا على كاميرا صغيرة إذا كانت عدة قنوات مفتوحة في آنٍ واحد. التزم بقناة واحدة لكل اتصال واختر MTU يطابق حجم الرسالة المتوقع؛ الإعداد الافتراضي بقناة L2CAPChannel واحدة لكل DeviceConnection كافٍ لمعظم التطبيقات.
L2CAP هو صمام أمان BLE. GATT هو ما يلجأ إليه كل تطبيق تقريبًا أولًا، وتبقى بقية أمثلة المركزية / الطرفية في هذا القسم على GATT. واجهة برمجة التطبيقات ذات الطابع القنوي هي الجواب عندما يتجاوز التطبيق نموذج المفتاح/القيمة.