תכנות מיקרו-בקרים בשפת MicroPython · פרק 17 — Bluetooth Low Energy⌂📚🏠
פרק 17

Bluetooth Low Energy

Bluetooth Low Energy
3 שעות לימוד

בכיס של כל תלמיד יש טלפון, ובו משדר Bluetooth מוכן. אז למה צריך רשת ושרת כדי לקרוא את הטמפרטורה בחדר מלוח שנמצא במרחק שני מטרים? בפרק הזה נגרום ללוח להכריז על עצמו, והטלפון או לוח אחר יקראו אותו ישירות. המושגים כאן חדשים, אבל מעטים, ואחרי שמבינים אותם התוכנית קצרה.

בפרק הזה:
  • נשווה בין BLE ל-Bluetooth הקלאסי ול-WiFi, ונבחר את המתאים לפרויקט.
  • נסביר מהם התקן היקפי, התקן מרכזי ופרסום.
  • נסביר את המבנה של GATT: שירות, מאפיין ו-UUID, ונקודד ערך לבייטים.
  • נכתוב בספרייה aioble התקן היקפי שמפרסם את הטמפרטורה, והתקן מרכזי שקורא אותה.
  • נבדוק את ההתקן ההיקפי מטלפון, באפליקציה כללית.
שימו לב: אף תוכנית בפרק הזה לא הורצה בבניית הספר. המדמה אינו מייצג את הרדיו, ולכן התוכניות עוברות רק בדיקת הידור, ואין מסך פלט. כתבנו אותן לפי תיעוד הספרייה, ואיננו מציגים כאן שום תוצאה שלא ראינו.

17.1 מתי לבחור ב-BLE

Bluetooth בצריכה נמוכה (Bluetooth Low Energy), וקיצורו BLE, תוכנן להתקנים קטנים ששולחים מעט ושותקים הרבה. הוא אינו גרסה חדשה יותר של ה-Bluetooth שאנחנו מכירים באוזניות, אלא דרך עבודה אחרת. הטבלה משווה במילים ולא במספרים, כי המספרים תלויים בשבב, באנטנה ובמקום:

BLEBluetooth קלאסיWiFi
צריכת אנרגיהנמוכה, עם תקופות שתיקהגבוהה יותרבדרך כלל הגבוהה ביותר
כמות הנתוניםקטנה: קריאות ופקודותבינונית: שמעגדולה: דפים ותמונות
גישה לאינטרנטלא ישירהלא ישירהכן, דרך הנתב
מדבר עם טלפון בלי נתבכןכןצריך רשת
במיקרו-פייתוןbluetooth ו-aiobleהתיעוד אינו מזכיר אותוnetwork
שימוש אופייניחיישן שטלפון קוראאוזנייהלוח שמדבר עם שרת

לפי תיעוד המודול bluetooth, הוא תומך כיום ב-BLE. לכן Bluetooth קלאסי נמצא מחוץ למה שאפשר לתכנת כאן, גם אם השבב עצמו מסוגל לו.

שאלה 17.1: חיישן במחסן פועל על סוללה, ופעם ביום טלפון קורא אותו כשעוברים לידו. מה מתאים יותר? ואם רוצים להציג את הקריאה מבית אחר?

17.2 התקן היקפי והתקן מרכזי (Peripheral and Central)

ב-BLE שני הצדדים אינם שווים בתחילת החיבור. אחד מכריז על עצמו וממתין, והשני מחפש ובוחר:

התקן היקפי: התקן שמכריז על קיומו ומקבל חיבורים. שמו באנגלית Peripheral. בדרך כלל חיישן קטן.
התקן מרכזי: התקן שסורק פרסומים ומבקש להתחבר. שמו באנגלית Central. בדרך כלל טלפון, ויכול להיות גם לוח.
פרסום: הודעה קצרה שההתקן ההיקפי משדר שוב ושוב, בלי חיבור לאיש, ובה שמו ומה שהוא מציע. שמו באנגלית advertising. זה שונה מפרסום ההודעות ב-MQTT (publish): כאן אין מתווך, והמטרה היא שמישהו ימצא אותנו.
ההתקן ההיקפיPeripheralחיישן על לוחההתקן המרכזיCentralטלפון או לוח1. פרסום חוזר, למי שמקשיב2. בקשת חיבור3. קריאה והתראותשרת בהיקפי, ולקוח במרכזי
שלושה שלבים: ההיקפי מפרסם, המרכזי מבקש להתחבר, ואז הנתונים זורמים בחיבור.

תיעוד המודול מזכיר עוד שני תפקידים שאינם צריכים חיבור: Broadcaster רק משדר, ו-Observer רק מאזין. אותו התקן יכול למלא יותר מתפקיד אחד בו בזמן.

דוגמה 17.1
מפרסמים כל 250000 µs, הערך שבו אנחנו משתמשים בתוכנית. כמה פרסומים יש בדקה?
פתרון:
T = 250000 µs
T = 0.25 s
n = 60T
n = 600.25
n = 240

כל פרסום מעיר את הרדיו. פרק זמן ארוך יותר חוסך אנרגיה, אבל ההתקן המרכזי צריך יותר זמן כדי למצוא אותנו.

n = 240
שאלה 17.2: טלפון קורא טמפרטורה מלוח: מי ההתקן ההיקפי, ומי ההתקן המרכזי? ומי יוזם את החיבור?

17.3 מבנה GATT: שירות, מאפיין ו-UUID (Services and Characteristics)

אחרי החיבור ההתקן המרכזי רואה עץ מסודר של נתונים. המערכת שמסדרת אותו נקראת GATT. הצד שמחזיק בעץ הוא השרת, בדרך כלל ההיקפי, והצד שקורא הוא הלקוח:

שירות: קבוצת נתונים קשורים בהתקן. שמו באנגלית service. למשל "מדידת סביבה".
מאפיין: ערך אחד בתוך השירות, ולצדו מה שמותר: קריאה, כתיבה, התראה. שמו באנגלית characteristic. למשל "הטמפרטורה".
התראה: הודעה ששולח השרת כשהערך משתנה, למי שביקש זאת, כך שהלקוח אינו צריך לשאול כל פעם. שמה באנגלית notify.
מזהה UUID: מספר שמבדיל כל שירות וכל מאפיין. הוא בן 16 סיביות לסוגים שהוגדרו במפרט ה-Bluetooth, ובן 128 סיביות למה שאנחנו ממציאים בעצמנו.
הלוח: שרת GATTשירותEnvironmental Sensing — UUID 0x181AמאפייןTemperature — UUID 0x2A6Eread, notifyvalue: 29 0923.45 °Cשירות אחר...
העץ שאנחנו בונים בתוכנית הפרק. הערך במאפיין הוא בייטים ותו לא.

בוחרים ב-0x181A לשירות מדידת הסביבה וב-0x2A6E לטמפרטורה, כי שני המזהים האלה מוכרים, ולכן האפליקציה הכללית מבינה אותם. המפרט דורש שהטמפרטורה תהיה מספר שלם בן 16 סיביות עם סימן, ביחידות של מאית המעלה, כשהבייט הפחות משמעותי בא ראשון. את זה עושה הספרייה struct בפורמט "<h".

דוגמה 17.2
רוצים לשלוח את הטמפרטורה 23.45 °C. מהם שני הבייטים שנשלח?
פתרון:
N = T · 100
N = 23.45 · 100
N = 2345

בבסיס הקסדצימלי: 0x0929. הבייט הפחות משמעותי בא ראשון, ולכן שולחים 29 09.

N = 2345
שאלה 17.3: מהם שני הבייטים שנשלח לטמפרטורה −5.25 °C? השתמשו ב-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.4: למה לא כותבים תוכנית Bluetooth על Raspberry Pi 4/5 באותה ספרייה?

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 הכרחית: אחרי ניתוק החיבור ההתקן חוזר לפרסם. בלעדיה ההתקן נעשה בלתי נראה אחרי הביקור הראשון, והתוכנית עדיין רצה, ולכן אנחנו חושבים שהתקלה בטלפון.
שאלה 17.5: מה תפקידו של send_update=True? ומה רואה לקוח שקרא את הערך פעם אחת, אם נמחק אותו?

17.6 בדיקה מהטלפון (Testing from a Phone)

לפני שכותבים את ההתקן המרכזי מוודאים שההיקפי עובד, באפליקציה כללית כמו nRF Connect. השלבים כלליים, ושמות הלחצנים עשויים להשתנות בין גרסאות:

  1. מפעילים את התוכנית בלוח ואת ה-Bluetooth בטלפון.
  2. באפליקציה מתחילים סריקה SCAN ומחפשים את desk1-temp ברשימה.
  3. לוחצים CONNECT והשירותים מופיעים. התוכנית מדפיסה בלוח את השורה connected.
  4. פותחים את השירות 0x181A, ואחר כך את המאפיין 0x2A6E.
  5. מבקשים קריאה ורואים את הערך. ייתכן שיופיעו בייטים בבסיס הקסדצימלי, או טמפרטורה מפוענחת, לפי האפליקציה.
  6. מבקשים התראות, והערך משתנה מעצמו בכל מדידה.
שאלה 17.6: האפליקציה הציגה את הערך 53 07. מהי הטמפרטורה?
פעילות: בלוח אמיתי: קרבו את הטלפון והרחיקו אותו, ועקבו אחרי הערך RSSI ברשימת הסריקה. רשמו אותו בטבלה, וכתבו מה המספר הזה מראה.

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.7: התוכנית קוראת כל 5 שניות. איך גורמים לה לקבל את הערך כשהוא משתנה, בלי לשאול?

17.8 מגבלות והבדלים מתועדים (Documented Limits)

שאלה 17.8: למה התוכנית ble_sensor.py אינה מתאימה כמות שהיא לפתיחת מנעול דלת?

עיקרי הפרק:

פקודות הפרק:

פקודהמה היא עושה
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 EnergyBluetooth Low Energyالبلوتوث منخفض الطاقة
התקן היקפיPeripheralالجهاز الطرفي
התקן מרכזיCentralالجهاز المركزي
פרסוםAdvertisingالإعلان
שירותServiceالخدمة
מאפייןCharacteristicالخاصية
התראהNotificationالإشعار
צימודPairingالاقتران
התשובות הסופיות לשאלות הפרק
17.1 BLE למקרה הראשון. מבית אחר חייבים אינטרנט, כלומר WiFi, או התקן ביניים שמתחבר לשניהם.
17.2 הלוח היקפי והטלפון מרכזי. המרכזי הוא שיוזם את החיבור.
17.3 f3 fd, כי N = −525, והוא נכתב בשיטת המשלים ל-2.
17.4 כי זו ספרייה של מיקרו-פייתון, ו-Raspberry Pi 4/5 מריץ Python רגיל על לינוקס.
17.5 הוא שולח התראה למנויים. בלעדיו הערך משתנה בשרת, אבל הלקוח לא יודע על כך אלא אם קרא אותו שוב.
17.6 הופכים את סדר שני הבייטים: 0x0753, כלומר 1875, ולכן הטמפרטורה 18.75 °C.
17.7 בהתראות: המאפיין תומך ב-notify. לא בדקנו כאן את פקודות ההרשמה בצד הלקוח, ולכן פונים לתיעוד של aioble.
17.8 כי אין בה צימוד ואין אימות, ולכן כל התקן קרוב יכול להתחבר. מנעול דורש הגנה שלא למדנו כאן.

שאלות לתרגול:

רמה ראשונה — בסיסית
1.
מה ההבדל בין התקן היקפי להתקן מרכזי?
תשובות: ההיקפי מפרסם וממתין, והמרכזי סורק ומבקש להתחבר.
2.
סדרו מהגדול לקטן: מאפיין, התקן, שירות.
תשובות: התקן, שירות, מאפיין.
3.
מתי משתמשים ב-UUID בן 128 סיביות?
תשובות: לשירות או למאפיין שאנחנו ממציאים, שאין להם מזהה בן 16 סיביות במפרט.
רמה שנייה — יישום
4.
איזו טמפרטורה נותנים שני הבייטים 53 07? ומהם שני הבייטים לטמפרטורה −5.25 °C?
תשובות: 18.75 °C, ושני הבייטים f3 fd.
5.
יש לנו פרסום כל 100 ms. כמה פרסומים יש בדקה? ומה ההשפעה בהשוואה לתוכנית הפרק?
תשובות: 600 פרסומים בדקה, יותר מ-240. מוצאים את ההתקן מהר יותר, בעלות אנרגיה גבוהה יותר.
6.
שינינו את שם ההתקן בהיקפי ולא שינינו את TARGET במרכזי. מה המרכזי מדפיס?
תשובות: אחרי 5 שניות של סריקה הוא מדפיס sensor not found.
רמה שלישית — אתגר
7.
הוסיפו לשירות מאפיין שני שאנחנו ממציאים, לאחוז הקריאה של הפוטנציומטר. איך כותבים את המזהה? והאם האפליקציה הכללית תבין אותו?
תשובות: bluetooth.UUID('7b1e0d3a-4c52-4f6e-9a10-2f6d8c31a5e7'), כלומר מחרוזת בת 128 סיביות שמפיקים בכלי. האפליקציה תציג את הבייטים שלו, ולא תדע מה פירושם.
8.
פרויקט: לוח בכיתה, ומורה שרוצה את הטמפרטורה בטלפון שלו בכיתה, ובאתר מהבית. תכננו את הפתרון במשפטים.
תשובות: בכיתה מספיק BLE. לאתר צריך WiFi ו-MQTT כמו בפרק 16. הלוח יכול תיאורטית את שניהם, אבל צריך לבדוק את העבודה בו בזמן.