11.4. פרסום וסריקה

שני התקני BLE שמעולם לא נפגשו קודם לכן צריכים למצוא זה את זה תחילה. רשתות פותרות זאת על ידי הקצאת כתובת לכל התקן מתוך מאגר משותף ובכך שהן מאפשרות לכל צד להגיע אל האחר דרך נתבים. ל-BLE אין נתבים, אין מאגר משותף, ובין רוב זוגות ההתקנים – אין כלל מערכת יחסים מוקדמת. ה-Generic Access Profile (GAP) פותר את הגילוי באמצעות תבנית של שידור-והאזנה במקום זאת. צד אחד מפרסם (advertises) – הוא משדר חבילה קצרה בשלושת ערוצי הפרסום במרווח קבוע, ומתאר מי הוא. הצד האחר סורק (scans) – הוא סורק את אותם שלושה ערוצים ומאזין לחבילות הללו.

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

11.4.1. ארבעת תפקידי GAP

A two-by-two matrix. Rows are labelled "advertises" and "does not advertise". Columns are labelled "accepts connections" and "does not accept connections". The four cells contain the role names: Peripheral, Broadcaster, Central, Observer.

ארבעת תפקידי GAP. הציר האנכי הוא האם ההתקן מפרסם; הציר האופקי הוא האם הוא מקבל (או יוזם) חיבורים.

  • peripheral מפרסם חבילות שאומרות ”אני כאן ואתה יכול להתחבר אליי“. כאשר התקן אחר פותח חיבור, ה-peripheral מפסיק לפרסם ומתחיל לשרת בקשות GATT. רצועות דופק, מדחומים, ורוב המצלמות-כחיישנים פועלים כ-peripherals.

  • central סורק אחר peripherals, בוחר אחד, ויוזם חיבור. לאחר ההתחברות הוא מדבר GATT כלקוח. טלפונים, מחשבים ניידים, ומצלמות הפועלות כאוספי נתונים הם centrals.

  • broadcaster מפרסם אך לעולם אינו מקבל חיבורים. מטען הפרסום שלו הוא הנתונים – אין למה להתחבר. iBeacons ורוב משואות הנוכחות בחנויות הם broadcasters.

  • observer סורק אחר הפרסומים הללו וקורא את המטען, שוב מבלי להתחבר אי-פעם. מצלמה שמאזינה למשואות סמוכות ופועלת על סמך מה שהיא שומעת היא observer.

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

11.4.2. מה מכילה חבילת פרסום

חבילת פרסום היא קטנה: 31 בתים של מטען, או 62 אם המפרסם מפרסם גם תגובת סריקה (scan response) שסורקים יכולים לבקש בזמן אמת. המטען הוא רשימה של שדות מוטיפסים קצרים:

  • Flags. ניתן-לחיבור או לא, בר-גילוי כללי / מוגבל.

  • שם מקומי. מחרוזת קצרה וידידותית לאדם – השם שמערכת ההפעלה בטלפון או במחשב נייד מציגה בתפריט ה-Bluetooth שלה.

  • Service UUIDs. רשימה של מזהי שירותי GATT שההתקן מארח, כך שסורק יוכל לזהות peripherals מתאימים מבלי להתחבר תחילה. רצועת דופק מפרסמת 0x180D – ה-UUID הסטנדרטי של שירות Heart-Rate – ואפליקציית דופק בטלפון יודעת מכך לבדה שכדאי להתחבר להתקן.

  • Appearance. ערך בן 16 סיביות מרשימת המספרים המוקצים של Bluetooth (חיישן, מדיה כללית, שעון כללי, …) – רמז ל-central לגבי מה להציג.

  • נתונים ספציפיים-ליצרן. בתים בפורמט חופשי עם קידומת של מזהה חברה. iBeacons משתמשים בשדה הזה כדי לשאת את ה-UUID, ה-major וה-minor שלהם; יישומים מותאמים אישית יכולים לשים כאן כל דבר שהם רוצים.

מטעני פרסום הם צפופים. מגבלת 31 הבתים הופכת את בחירת מה לכלול להחלטת תכנון אמיתית – שם ארוך וקריא-לאדם יכול להותיר במהירות ללא מקום עבור Service UUIDs. ה-API של aioble.advertise() מקבל כל אחד מאלה כארגומנט מילת-מפתח ומרכיב עבורך את הבתים, תוך גלישה אוטומטית אל תגובת הסריקה אם החבילה הראשית מתמלאת.

11.4.3. סריקה אקטיבית ופסיבית

סורק יכול לפעול פסיבי, שבו הוא מאזין לחבילות פרסום ומנתח את מה שמגיע, או אקטיבי, שבו הוא גם שולח בקשת סריקה (scan request) לכל מפרסם ומנתח את תגובת הסריקה שחוזרת.

סריקה פסיבית רואה רק את חבילת הפרסום הראשונית (עד 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 URL, UUID/major/minor של 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 לתקשר איתו, הוא מפסיק להאזין, שולח בקשת חיבור (connect request) בערוץ הפרסום שה-peripheral השתמש בו לאחרונה, ושני הצדדים נכנסים לערוצי הנתונים המקפצים של שכבת הקישור. ה-peripheral בדרך כלל מפסיק לפרסם בנקודה זו. מה שקורה לאחר מכן – פרמטרי החיבור, גילוי GATT, אורך חיי הקישור – נמצא ב-חיבורים.