بلوتوث منخفض الطاقة BLE
الهاتف في جيب كل طالب، وفيه راديو بلوتوث جاهز. فلماذا نحتاج إلى شبكة وخادم لنقرأ حرارة الغرفة من لوحة تبعد مترين؟ في هذا الفصل نجعل اللوحة تعلن عن نفسها، فيقرؤها الهاتف أو لوحة أخرى مباشرة. المفاهيم هنا جديدة، لكنها قليلة، وحين نفهمها يصبح البرنامج قصيرا.
- نقارن بين BLE والبلوتوث الكلاسيكي وWiFi، ونختار المناسب لمشروع.
- نشرح الجهاز الطرفي والجهاز المركزي والإعلان.
- نشرح بنية GATT: الخدمة والخاصية وUUID، ونرمّز قيمة في بايتات.
- نكتب بمكتبة
aiobleجهازا طرفيا ينشر الحرارة، وجهازا مركزيا يقرؤها. - نفحص الجهاز الطرفي من هاتف، بتطبيق عام.
17.1 متى نختار BLE؟ – מתי לבחור ב-BLE
البلوتوث منخفض الطاقة (Bluetooth Low Energy)، واختصاره BLE، صُمّم لأجهزة صغيرة ترسل قليلا وتصمت طويلا. وهو ليس نسخة أحدث من البلوتوث الذي نعرفه في السماعات، بل طريقة عمل أخرى. والجدول يقارن بالكلام لا بالأرقام، لأن الأرقام تتعلّق بالشريحة والهوائي والمكان:
| BLE | بلوتوث كلاسيكي | WiFi | |
|---|---|---|---|
| استهلاك الطاقة | منخفض، ومع فترات صمت | أعلى | الأعلى عادة |
| كمية البيانات | قليلة: قراءات وأوامر | متوسطة: صوت | كبيرة: صفحات وصور |
| الوصول إلى الإنترنت | ليس مباشرة | ليس مباشرة | نعم، عبر الموجّه |
| يكلّم الهاتف دون موجّه | نعم | نعم | يحتاج إلى شبكة |
| في مايكروبايثون | bluetooth وaioble | لا يذكره التوثيق | network |
| استعمال نموذجي | حسّاس يقرؤه هاتف | سماعة | لوحة تكلّم خادما |
حسب توثيق وحدة bluetooth، هي تدعم حاليا BLE. ولذلك نعدّ البلوتوث الكلاسيكي خارج ما نستطيع برمجته هنا، حتى إذا كانت الشريحة نفسها قادرة عليه.
17.2 الجهاز الطرفي والجهاز المركزي – התקן היקפי והתקן מרכזי (Peripheral and Central)
في BLE لا يتساوى الطرفان عند بدء الاتصال. واحد يعلن عن نفسه وينتظر، والآخر يبحث ويختار:
وتوثيق الوحدة يذكر دورين آخرين لا يحتاجان إلى اتصال: Broadcaster يبثّ فقط، وObserver يستمع فقط. ويستطيع الجهاز نفسه أن يقوم بأكثر من دور معا.
كل إعلان يوقظ الراديو. الفترة الأطول توفّر الطاقة، لكن المركزي يحتاج إلى وقت أطول ليجدنا.
17.3 بنية GATT: الخدمة والخاصية وUUID – מבנה GATT: שירות ומאפיין (Services and Characteristics)
بعد الاتصال، يرى المركزي شجرة مرتّبة من البيانات. والنظام الذي يرتّبها اسمه GATT. الطرف الذي يملك الشجرة هو الخادم، وهو عادة الطرفي، والطرف الذي يقرأ هو العميل:
نختار 0x181A لخدمة قياس البيئة و0x2A6E للحرارة، لأن هذين المعرّفين معروفان، فيفهمهما التطبيق العام. والمواصفة تطلب أن تكون الحرارة عددا صحيحا من 16 بتّا بإشارة، بوحدة جزء من مئة من الدرجة، والبايت الأقل أهمية أولا. وهذا ما تفعله مكتبة struct بالصيغة "<h".
بالست عشري: 0x0929. والبايت الأقل أهمية يأتي أولا، فنرسل 29 09.
struct.pack("<h", n) في مايكروبايثون.17.4 اللوحات والمكتبة – לוחות וספרייה (Boards and Library)
| اللوحة | BLE في مايكروبايثون | مصدر المعلومة |
|---|---|---|
| ESP32 | موجود | صفحة تنزيل البرنامج الثابت ESP32_GENERIC |
| Pico W | موجود | صفحة تنزيل RPI_PICO_W |
| Pico 2 W | موجود | صفحة تنزيل RPI_PICO2_W |
| Raspberry Pi 4/5 | لا: تعمل عليها لينكس، لا مايكروبايثون | الفصل 22 |
وحدة bluetooth في البرنامج الثابت منخفضة المستوى، وتوثيقها نفسه ينصح بمكتبة aioble لمعظم التطبيقات. وهي من micropython-lib، وتحتاج حسب توثيقها إلى مايكروبايثون 1.17 أو أحدث. نثبّتها مرة واحدة من الحاسوب، أو من اللوحة بعد الاتصال بالشبكة بالدالة mip.install('aioble'):
mpremote mip install aioble
والمكتبة مقسّمة، فيمكن تثبيت الأجزاء المطلوبة فقط، مثل aioble-peripheral وaioble-server، إذا ضاقت الذاكرة.
aioble على asyncio، وسنشرحها في الفصل 19. يكفي هنا هذا: async def تعرّف مهمة، وawait تعني «انتظر، ودع غيري يعمل»، وasyncio.gather تشغّل مهمتين معا. هكذا يقيس البرنامج الحرارة وهو ينتظر اتصالا.17.5 جهاز طرفي ينشر الحرارة – התקן היקפי ששולח טמפרטורה (A Temperature Peripheral)
البرنامج يبني الشجرة، ثم يشغّل مهمتين: مهمة تقيس وتكتب القيمة، ومهمة تعلن وتنتظر الاتصال. ويستعمل الحسّاس DHT22 من الفصل 13.
import asyncio
import struct
import aioble
import bluetooth
import dht
from machine import Pin
import board
ENV_SERVICE = bluetooth.UUID(0x181A)
TEMP_CHAR = bluetooth.UUID(0x2A6E)
ADV_INTERVAL_US = 250_000
service = aioble.Service(ENV_SERVICE)
temp = aioble.Characteristic(service, TEMP_CHAR, read=True, notify=True)
aioble.register_services(service)
sensor = dht.DHT22(Pin(board.DHT))
async def measure_task():
while True:
sensor.measure()
data = struct.pack("<h", round(sensor.temperature() * 100))
temp.write(data, send_update=True)
await asyncio.sleep(5)
async def advertise_task():
while True:
async with await aioble.advertise(
ADV_INTERVAL_US, name="desk1-temp", services=[ENV_SERVICE]
) as conn:
print("connected:", conn.device)
await conn.disconnected(timeout_ms=None)
async def main():
await asyncio.gather(measure_task(), advertise_task())
asyncio.run(main())| الأمر | عمله |
|---|---|
| aioble.Service(uuid) | ننشئ خدمة. |
| aioble.Characteristic(s, uuid, read=True, notify=True) | خاصية داخل الخدمة، ومعها ما يُسمح به. |
| aioble.register_services(s) | نسجّل الشجرة في الراديو. |
| temp.write(data, send_update=True) | نكتب القيمة، ونرسل إشعارا إلى من طلبه. |
| aioble.advertise(us, name=, services=) | نعلن وننتظر حتى يتصل أحد، فنحصل على الاتصال. |
| conn.disconnected(timeout_ms=None) | ننتظر انتهاء الاتصال. |
while True حول advertise ضرورية: بعد انقطاع الاتصال، يعود الجهاز إلى الإعلان. من دونها يصبح الجهاز غير مرئي بعد أول زيارة، والبرنامج ما زال يعمل، فنظن أن العطل في الهاتف.send_update=True؟ وماذا يرى عميل قرأ القيمة مرة، إن حذفناها؟17.6 الفحص من الهاتف – בדיקה מהטלפון (Testing from a Phone)
قبل أن نكتب الجهاز المركزي نتأكّد أن الطرفي يعمل، بتطبيق عام مثل nRF Connect. الخطوات عامة، وأسماء الأزرار قد تختلف من إصدار إلى آخر:
- نشغّل البرنامج على اللوحة، والبلوتوث على الهاتف.
- في التطبيق نبدأ الفحص SCAN، ونبحث عن desk1-temp في القائمة.
- نضغط CONNECT، فتظهر الخدمات. ويطبع البرنامج على اللوحة سطر
connected. - نفتح الخدمة 0x181A، ثم الخاصية 0x2A6E.
- نطلب القراءة، فنرى القيمة. وقد تظهر بايتات بالست عشري، أو حرارة مفكوكة، حسب التطبيق.
- نطلب الإشعارات، فتتغيّر القيمة وحدها عند كل قياس.
17.7 جهاز مركزي يقرأ – התקן מרכזי שקורא (A Central That Reads)
لوحة ثانية تبحث عن الجهاز باسمه، وتتصل به، ثم تنزل في الشجرة: خدمة ثم خاصية ثم قراءة. وهذا ما فعلناه بأيدينا في الهاتف.
import asyncio
import struct
import aioble
import bluetooth
ENV_SERVICE = bluetooth.UUID(0x181A)
TEMP_CHAR = bluetooth.UUID(0x2A6E)
TARGET = "desk1-temp"
async def find_sensor():
async with aioble.scan(5000, active=True) as scanner:
async for result in scanner:
if result.name() == TARGET:
return result.device
return None
async def read_loop(device):
conn = await device.connect()
async with conn:
service = await conn.service(ENV_SERVICE)
char = await service.characteristic(TEMP_CHAR)
while conn.is_connected():
data = await char.read()
celsius = struct.unpack("<h", data)[0] / 100
print("temp: %.2f C" % celsius)
await asyncio.sleep(5)
async def main():
device = await find_sensor()
if device is None:
print("sensor not found")
return
try:
await read_loop(device)
except asyncio.TimeoutError:
print("timeout")
asyncio.run(main())| الأمر | عمله |
|---|---|
| aioble.scan(ms, active=True) | نفحص الإعلانات مدة محدّدة، بالفحص النشط كما في مثال المكتبة. |
| result.name() | اسم الجهاز، إذا وصلنا. |
| device.connect() | نطلب الاتصال. والفشل يظهر كـasyncio.TimeoutError. |
| conn.service(uuid) | نبحث عن خدمة في الجهاز البعيد. |
| service.characteristic(uuid) | نبحث عن خاصية داخلها. |
| char.read() | نقرأ القيمة بايتات. |
| conn.is_connected() | هل ما زلنا متصلين؟ |
data مباشرة لرأينا شيئا مثل b")\t" بدل الحرارة. وعلى الطرفين أن يتفقا على الصيغة "<h" والمقياس 100: البروتوكول لا يتحقق من ذلك، فأي اختلاف يعطي رقما خاطئا دون خطأ.17.8 حدود وفروق موثّقة – מגבלות והבדלים מתועדים (Documented Limits)
- توثيق وحدة
bluetoothيقول إنها ما زالت قيد التطوير، وقد تتغيّر واجهتها. لذلك نقرأ توثيق الإصدار الذي نستعمله. - قنوات L2CAP لا يشمل دعمها ESP32، حسب التوثيق. ولا نحتاج إليها هنا.
- الاقتران (צימוד) مدعوم على بعض المنافذ فقط. فبرامجنا هنا مكشوفة: أي هاتف قريب يتصل ويقرأ، فلا نضع فيها سرا، ولا نفتح بها بابا.
- حمولة الإعلان صغيرة، ولذلك نختار اسما قصيرا.
- لم نفحص في هذا الكتاب استهلاك الطاقة، ولا المدى، ولا العمل المتزامن مع WiFi على لوحة حقيقية. فلا نعطي أرقاما عنها، ونقيسها في مشروعنا.
ble_sensor.py كما هو لفتح قفل باب؟أهمّ أفكار الفصل:
- BLE للقليل من البيانات وطاقة منخفضة، وWiFi للشبكة والإنترنت.
- الطرفي يعلن وينتظر، والمركزي يفحص ويطلب الاتصال.
- بعد الاتصال نقرأ شجرة: جهاز، ثم خدمة، ثم خاصية، وكل منها له UUID.
- القيمة بايتات، والصيغة اتفاق بين الطرفين.
aiobleتقوم علىasyncio، ويعود الجهاز الطرفي إلى الإعلان بعد كل انقطاع.- برامج هذا الفصل لم تُشغَّل في بناء الكتاب، ولذلك نختبرها أولا على لوحة حقيقية من الهاتف.
أوامر الفصل:
| الأمر | عمله |
|---|---|
| bluetooth.UUID(0x181A) | معرّف من 16 بتّا. |
| aioble.Service(uuid) | خدمة. |
| aioble.Characteristic(s, uuid, ...) | خاصية. |
| aioble.register_services(s) | تسجيل الشجرة. |
| aioble.advertise(us, name=, services=) | إعلان وانتظار اتصال. |
| aioble.scan(ms, ...) | فحص الإعلانات. |
| device.connect() | طلب اتصال. |
| conn.service(u), s.characteristic(u) | البحث في الشجرة. |
| char.read() | قراءة. |
| temp.write(d, send_update=True) | كتابة وإشعار. |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية | English |
|---|---|---|
| البلوتوث منخفض الطاقة | Bluetooth Low Energy | Bluetooth Low Energy |
| الجهاز الطرفي | התקן היקפי | Peripheral |
| الجهاز المركزي | התקן מרכזי | Central |
| الإعلان | פרסום | Advertising |
| الخدمة | שירות | Service |
| الخاصية | מאפיין | Characteristic |
| الإشعار | התראה | Notification |
| الاقتران | צימוד | Pairing |
notify. ولم نتحقق هنا من أوامر الاشتراك في جهة العميل، فنرجع إلى توثيق aioble.أسئلة للتمرين:
TARGET في الجهاز المركزي. ماذا يطبع المركزي؟sensor not found.bluetooth.UUID('7b1e0d3a-4c52-4f6e-9a10-2f6d8c31a5e7')، أي نص من 128 بتّا نولّده بأداة. يظهر التطبيق بايتاتها، ولا يعرف معناها.