11.4. الإعلان والمسح¶
على جهازي BLE لم يلتقيا من قبل أن يعثر كل منهما على الآخر أولاً. تحل الشبكات ذلك بمنح كل جهاز عنواناً من مجمّع مشترك والسماح لأي طرف بالوصول إلى الآخر عبر الموجّهات. أما BLE فليس لديه موجّهات، ولا مجمّع مشترك، ولا -- بين معظم أزواج الأجهزة -- أي علاقة سابقة على الإطلاق. يحل بروتوكول الوصول العام (GAP) مشكلة الاكتشاف بنمط البثّ والإنصات بدلاً من ذلك. يقوم أحد الطرفين بـ الإعلان -- إذ يُرسل حزمة قصيرة على قنوات الإعلان الثلاث على فترات منتظمة، تصف من يكون. بينما يقوم الطرف الآخر بـ المسح -- إذ يمسح القنوات الثلاث نفسها منصتاً لتلك الحزم.
يعرّف GAP أربعة أدوار حول هذا النمط، كل منها مزيج محدد من الإعلان والإنصات.
11.4.1. أدوار GAP الأربعة¶
أدوار GAP الأربعة. المحور الرأسي هو ما إذا كان الجهاز يُعلن؛ والمحور الأفقي هو ما إذا كان يقبل الاتصالات (أو يبدؤها).¶
يُعلن جهاز peripheral حزماً تقول "أنا هنا ويمكنك الاتصال بي". وعندما يفتح جهاز آخر اتصالاً، يتوقف الـ peripheral عن الإعلان ويبدأ بخدمة طلبات GATT. تعمل أحزمة معدل ضربات القلب، ومقاييس الحرارة، ومعظم الكاميرات بوصفها مستشعرات كأجهزة peripheral.
يقوم جهاز central بالمسح بحثاً عن أجهزة peripheral، ويختار واحداً، ويبدأ اتصالاً. وبعد الاتصال يتحدث GATT كعميل. تُعد الهواتف والحواسيب المحمولة والكاميرات التي تعمل كجامعات بيانات أجهزة central.
يُعلن جهاز broadcaster لكنه لا يقبل الاتصالات أبداً. فحمولة إعلانه هي البيانات -- لا يوجد شيء للاتصال به. تُعد أجهزة iBeacon ومعظم منارات تحديد التواجد في المتاجر أجهزة broadcaster.
يقوم جهاز observer بالمسح بحثاً عن تلك الإعلانات ويقرأ الحمولة، مجدداً دون الاتصال أبداً. الكاميرا التي تنصت للمنارات القريبة وتتصرف بناءً على ما تسمعه هي جهاز observer.
يمكن لجهاز واحد أن يؤدي أكثر من دور في الوقت نفسه -- فالكاميرا يمكن أن تكون peripheral تنشر حالتها الخاصة و في الوقت نفسه central تتصل بمستشعر قريب. يُجزّئ الراديو العمل زمنياً.
11.4.2. ما الذي تحتويه حزمة الإعلان¶
حزمة الإعلان صغيرة: 31 بايت من الحمولة، أو 62 إذا نشر المُعلِن أيضاً استجابة مسح يمكن للماسحات أن تطلبها أثناء العمل. الحمولة عبارة عن قائمة من الحقول القصيرة المصنّفة:
الأعلام (Flags). قابل للاتصال أم لا، قابل للاكتشاف عام / محدود.
الاسم المحلي. سلسلة نصية قصيرة وودودة للإنسان -- الاسم الذي يعرضه نظام التشغيل على هاتف أو حاسوب محمول في قائمة Bluetooth الخاصة به.
معرّفات خدمات UUID. قائمة بمعرّفات خدمات GATT التي يستضيفها الجهاز، بحيث يمكن للماسح أن يتعرّف على أجهزة peripheral القادرة دون الاتصال أولاً. يُعلن حزام معدل ضربات القلب عن
0x180D-- معرّف UUID القياسي لخدمة Heart-Rate -- ويعرف تطبيق معدل ضربات القلب على الهاتف من ذلك وحده أن الجهاز يستحق الاتصال به.المظهر (Appearance). قيمة 16 بت من قائمة الأرقام المخصصة لـ Bluetooth (مستشعر، وسائط عامة، ساعة عامة، ...) -- تلميح للـ central حول ما يجب عرضه.
البيانات الخاصة بالمصنّع. بايتات حرة الشكل مسبوقة بمعرّف شركة. تستخدم أجهزة iBeacon هذا الحقل لحمل الـ UUID والقيمة الكبرى والقيمة الصغرى الخاصة بها؛ ويمكن للتطبيقات المخصصة وضع أي شيء تريده هنا.
حمولات الإعلان ضيّقة. يجعل حد الـ 31 بايت اختيار ما يُدرَج قراراً تصميمياً حقيقياً -- فالاسم الطويل القابل للقراءة بشريّاً يمكن أن يستهلك بسرعة المساحة المتاحة لمعرّفات UUID للخدمات. يأخذ الـ API aioble.advertise() كلاً من هذه كوسيط كلمة مفتاحية ويُجمّع لك البايتات، متجاوزاً تلقائياً إلى استجابة المسح إذا امتلأت الحزمة الرئيسية.
11.4.3. المسح النشط والسلبي¶
يمكن للماسح أن يعمل بشكل سلبي، حيث ينصت لحزم الإعلان ويُحلّل ما يصل، أو بشكل نشط، حيث يرسل أيضاً طلب مسح إلى كل مُعلِن ويُحلّل استجابة المسح التي تعود.
يرى المسح السلبي حزمة الإعلان الأولية فقط (حتى 31 بايت). أما المسح النشط فيضاعف ذلك -- إذ تكون استجابة المسح 31 بايت إضافية يمكن للـ peripheral استخدامها للحقول التي لم تتسع. كما يكلّف المسح النشط طاقة على كلا الطرفين، لأن الماسح يُرسل والمُعلِن يُرسل حزمة إضافية، لذا فهو خيار وليس وضعاً افتراضياً.
في الـ API الخاص بـ aioble، يُبدّل active=True على aioble.scan() الوضع، وتعرض كل ScanResult البيانات المدمجة adv_data بالإضافة إلى resp_data إضافة إلى مساعدات مثل result.name() وresult.services() التي تخفي التحليل على مستوى البايت.
ملاحظة
السمتان adv_data وresp_data هما حمولتا الإعلان واستجابة المسح الخام (bytes). أما المساعدات -- name() وservices() وmanufacturer() -- فتغطي الحقول القياسية الشائعة وهي الخيار الصحيح في 99% من الأحيان. لا تلجأ إلى البايتات الخام إلا عندما تحتاج إلى حقل خاص بمورّد لا تُحلّله المساعدات (روابط Eddystone، أو UUID/القيمة الكبرى/القيمة الصغرى لـ iBeacon، أو أنواع إعلان مخصصة). تخطيط البايتات هو تخطيط TLV القياسي: كل حقل هو length, type, value....
11.4.4. فترة الإعلان¶
عدد مرات بثّ الـ peripheral هو مقايضة بين الطاقة وزمن استجابة الاكتشاف. الإعلانات التي تخرج كل 20 ms يلتقطها الماسح فوراً تقريباً لكنها تُبقي الراديو مشغولاً وتستنزف البطارية؛ أما الإعلانات كل ثانية فلا تستخدم أي طاقة تقريباً لكنها تجعل مسح الماسح أبطأ في ملاحظة الجهاز.
يضبط interval_us على aioble.advertise() الفترة بالميكروثانية:
من 20,000 إلى 100,000 us (20 ms - 100 ms) -- اقتران سريع، التطبيق يتوقع استجابة سريعة، جهاز موصول بالكهرباء.
من 250,000 إلى 1,000,000 us (250 ms - 1 s) -- إعداد افتراضي معقول لجهاز peripheral يعمل بالبطارية ويريد أن يكون قابلاً للاكتشاف دون استهلاك الشحن.
أعلى من 1,000,000 us -- بثّ خلفي بطيء، منارات تُرسل تحديث موقع كل بضع ثوانٍ.
للماسح مقابضه الخاصة -- إذ يأخذ aioble.scan() كلاً من interval_us وwindow_us (عدد مرات استيقاظ الماسح لرادويه ومدة إنصاته في كل مرة). الإعدادات الافتراضية جيدة؛ والتغيير الشائع الوحيد هو جعل الاثنين متساويين من أجل مسح مستمر عندما لا تكون البطارية مصدر قلق.
11.4.5. الأنماط بلا اتصال -- broadcaster وobserver¶
تتناول الصفحتان العمل كطرفية والعمل كجهاز مركزي الشكل القابل للاتصال من الـ API -- حيث يقبل peripheral اتصالاً ويتبادل الطرفان البيانات عبر GATT. أما الشكل الآخر فهو بلا اتصال: يبثّ broadcaster الحمولة على شكل إعلان، ويمكن لأي observer ضمن النطاق قراءتها دون الاتصال أبداً. المنارات، ومستشعرات التواجد، والقياس عن بعد أحادي الاتجاه كلها تعيش هنا.
الـ broadcaster هو aioble.advertise() مع connectable=False. وتحمل البيانات الخاصة بالمصنّع الحمولة:
import aioble
import asyncio
import struct
_COMPANY_ID = const(0xFFFF) # 0xFFFF is "no specific vendor"
async def beacon():
seq = 0
while True:
seq = (seq + 1) & 0xFFFF
payload = struct.pack("<H", seq)
await aioble.advertise(
interval_us=500000,
connectable=False,
name="openmv-beacon",
manufacturer=(_COMPANY_ID, payload),
timeout_ms=1000, # one cycle, then loop
)
asyncio.run(beacon())
تُنهي الكلمة المفتاحية timeout_ms استدعاء الإعلان بعد ثانية؛ وتُعيد الحلقة الخارجية إصداره برقم التسلسل التالي بحيث يرى المنصتون بيانات جديدة. والعَلَم connectable=False هو ما يجعل الإعلان على طراز broadcaster -- فلن تستجيب الكاميرا لطلب اتصال حتى لو وصل واحد.
الـ observer هو الماسح المطابق للقراءة فقط. يُشغّل aioble.scan() إلى الأبد، ويُحلّل الإعلانات الواردة، ولا يستدعي connect() أبداً:
import aioble
import asyncio
_COMPANY_ID = const(0xFFFF)
async def watch():
async with aioble.scan(duration_ms=0, active=False) as scanner:
async for result in scanner:
for company, data in result.manufacturer(filter=_COMPANY_ID):
print(result.device.addr_hex(),
"rssi", result.rssi, "data", data)
asyncio.run(watch())
يمسح duration_ms=0 حتى يخرج مدير السياق؛ ويُبقي active=False راديو الـ observer نفسه صامتاً (بلا طلبات استجابة مسح) لأدنى استهلاك للطاقة. ويتجاهل الوسيط filter= على manufacturer() كل إعلان لا يطابق معرّف الشركة، بحيث لا تُطلَق الحلقة إلا لحركة مرور الـ broadcaster.
11.4.6. من الاكتشاف إلى الاتصال¶
بمجرد أن يختار central جهاز peripheral للتحدث إليه، يتوقف عن الإنصات، ويُرسل طلب اتصال على قناة الإعلان التي استخدمها الـ peripheral آخر مرة، وينتقل الطرفان إلى قنوات البيانات المتنقلة لطبقة الوصلة. وعادة ما يتوقف الـ peripheral عن الإعلان عند هذه النقطة. أما ما يحدث بعد ذلك -- معاملات الاتصال، واكتشاف GATT، وعمر الوصلة -- فهو في الاتصالات.