11.14. סיכום

עברת דרך Bluetooth Low Energy מהרדיו ועד ה-API של Python המשמש להפעלתו:

  • המוטיבציה – BLE הוא התשובה כאשר המצלמה רוצה לדבר עם משהו קרוב מבלי שום תשתית ביניהם. טלפון באותו חדר, מתקן לביש על פרק היד, משואה (beacon) על קיר. טווח קצר, אין רשת להצטרף אליה, כמעט ללא צריכת חשמל.

  • הרדיו – 2.4 GHz, 40 ערוצים: שלושה לפרסום, 37 לנתוני חיבור, מקופצים ברצף פסבדו-אקראי עם הימנעות אדפטיבית מערוצים רועשים. מנות קצרות, רדיו שישן רוב הזמן.

  • שכבת החיבור – מסגור מנות, מיעון, תזמון חיבורים, שידור חוזר, והצפנה בשכבת החיבור. אף אחד מאלה אינו מוגדר מ-Python; כולם משתקפים בפרמטרי החיבור וב-MTU.

  • Generic Access Profile (GAP) – גילוי וניהול חיבורים. ארבעה תפקידים: peripheral ו-broadcaster (מפרסמים), central ו-observer (סורקים). מטעני הפרסום נושאים את השם המקומי, מזהי UUID של שירותים, מראה (appearance), ונתונים ספציפיים ליצרן – 31 בתים בתוספת תגובת סריקה אופציונלית של 31 בתים. מרווח החיבור, השהיית ההתקן ההיקפי, וזמן הקצוב לפיקוח קובעים כיצד חיבור פתוח מרגיש.

  • Generic Attribute Profile (GATT) – עץ של שירותים, כל אחד מחזיק מאפיינים, כל אחד מחזיק אופציונלית מתארים, מזוהים על ידי מזהי UUID (16 סיביות עבור תקני Bluetooth-SIG, 128 סיביות עבור מותאמים אישית). חמש פעולות: read ו-write (משיכה, ביוזמת הלקוח), notify ו-indicate (דחיפה, ביוזמת השרת, מנויים דרך Client Characteristic Configuration Descriptor). גודל המטען מוגבל על ידי ה-MTU שנוהל במשא ומתן.

  • ה-API של Pythonaioble הופך כל דפוס BLE לקורוטינת asyncio. התקן היקפי הוא aioble.advertise() הלולא על חיבורים, עם אובייקטי Service / Characteristic הנבנים פעם אחת ומומחזים על ידי aioble.register_services(). התקן מרכזי הוא aioble.scan() כדי למצוא עמית, connect() כדי לפתוח את החיבור, service() ו-characteristic() כדי לעבור על עץ ה-GATT המרוחק, ואז read() / write() / subscribe() / notified() עבור הנתונים בפועל. ניתוקים צפים כ-aioble.DeviceDisconnectedError בתוך הקורוטינה שהמתינה.

  • ערוצי L2CAP – פתח המילוט עבור זרמי בתים בכמויות גדולות שאינם מתאימים למודל מפתח/ערך של GATT. aioble.DeviceConnection.l2cap_accept() / l2cap_connect() פותחים ערוץ לכל יישום על גבי חיבור ה-GAP, עם שליחה / קבלה בבקרת זרימת אשראי ו-MTU גדול יותר ממה ש-GATT יכול לשאת.

  • צימוד והצפנה – חיבורי BLE הם ציבוריים כברירת מחדל. aioble.DeviceConnection.pair() יוזם חילופי מפתחות המייצרים חיבור מוצפן; bond=True (ברירת המחדל) משמר את המפתחות כך שחיבורים הבאים מדלגים על לחיצת היד. ללא mitm=True ויכולת קלט/פלט שמישה, הצפנה מגנה מפני מאזינים סמויים פסיביים אך לא מפני הפניה פעילה במהלך הצימוד המקורי.

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

11.14.1. פתרון בעיות

כשלי BLE הם בעיקר אי-התאמות בין מה ששני הצדדים מצפים לו, ובוחן בצד הטלפון הוא הדרך המהירה ביותר לראות של מי הציפיות שגויות. הכלי הסטנדרטי הוא nRF Connect for Mobile (Nordic Semiconductor, חינמי ב-Android וב-iOS): הוא סורק, מתחבר, עובר על מסד הנתונים של GATT, קורא וכותב מאפיינים, ונרשם להתראות – כך שניתן לבדוק את ההתנהגות בצד המצלמה בבידוד, מבלי לכתוב אפליקציה נלווית כלל.

מצבי הכשל הנפוצים:

  • ”ההתקן שלי מופיע בסורק אך לא מתחבר.“ לרוב למנת הפרסום יש connectable=False (מצב broadcaster), או שחיבור קודם עדיין פתוח והמצלמה כבר עברה את aioble.advertise(). הוסף הוראות print סביב קריאת הפרסום כדי לוודא.

  • ”exchange_mtu(512) רצה אך ההתראות שלי עדיין מוגבלות ל-20 בתים.“ ה-MTU שנוהל במשא ומתן הוא min(local, peer) – ייתכן שהטלפון או ספריית ההתקן המרכזי לא ביקשו MTU גדול יותר בצדם, ובמקרה זה החיבור נשאר ב-23. בדוק את mtu לאחר ש-exchange_mtu() חוזרת. שים לב גם ש-exchange_mtu() עובדת רק פעם אחת בכל חיבור; קרא לה לפני הפעולה הגדולה הראשונה.

  • ”צימוד נכשל עם שגיאה כללית.“ שני אשמים רגילים: אי-התאמת יכולת קלט/פלט (בקשת mitm=True במצלמה המצהירה io=3 / ללא קלט ללא פלט – אין דרך לאשר את הקוד המספרי, ולכן מנוע הצימוד מוותר), וזמן שעון לא נכון בעליל במצלמה כאשר העמית דורש זאת. הגדר את השעון עם ntptime.settime() לפני ניסיון הצימוד הראשון.

  • ”התראות לעולם אינן מגיעות ללקוח.“ שני דברים לבדוק, לפי הסדר: (א) האם המאפיין הוכרז עם notify=True? – ביט המאפיין חייב להיות מוגדר בצד השרת; (ב) האם הלקוח קרא ל-subscribe()? – ללא כתיבת ה-Client Characteristic Configuration Descriptor (CCCD), נאמר לשרת שאף לקוח אינו רוצה התראות והוא מפיל אותן בשקט.

  • ”השם המפורסם קטוע או חסר.“ מטען הפרסום הוא 31 בתים, ושדות הדגלים + UUID של שירות + מראה לוקחים כל אחד בתים מהראש. name= ארוך בתוספת מספר מזהי UUID של שירותים גורם לגלישה. או שתקצר את השם או שתשתמש בסריקה אקטיבית כך שתגובת הסריקה (עוד 31 בתים) תישא את הגלישה. nRF Connect מציג את שני החצאים בנפרד, מה שהופך את הפיצול לברור.

  • ”חיבור L2CAP מעלה חריגה מיד.“ בדרך כלל אי-התאמת PSM – שני הצדדים חייבים להסכים על אותו מספר PSM מחוץ-לפס. L2CAPConnectionError נושאת את קוד הסטטוס של Bluetooth כארגומנט הראשון שלה; סטטוס 2 (”PSM not supported“) הוא הסימן המסגיר.

  • ”חיבורים מקושרים עדיין מפעילים לחיצת יד מלאה של צימוד בכל חיבור מחדש.“ aioble.security.load_secrets() לא נקראה בעת ההפעלה. בלעדיה, המפתחות השמורים נמצאים בזיכרון פלאש אך לעולם אינם נטענים לזיכרון, ולכן זהות העמית אינה ידועה והצימוד רץ מאפס בכל פעם.

כאשר כל השאר נכשל, מודול bluetooth ברמה הנמוכה יותר חושף פונקציית callback מסוג IRQ שמופעלת עבור כל אירוע בסיסי; הרשמה אליה לזמן קצר והדפסת האירועים היא המקבילה למעקב Wireshark עבור צד המצלמה.

11.14.2. שימוש במדריך הזה מאוחר יותר

התייחס לפרקי ה-Bluetooth כחומר עזר; חזרה לבדיקת הפריסה המדויקת של מטען הפרסום של התקן היקפי או זרימת הסריקה-וההרשמה של התקן מרכזי היא השימוש המיועד. דפי העזר aioble — BLE אסינכרוני ו-bluetooth — Bluetooth ברמה נמוכה מפרטים כל מתודה, דגל, וקבוע במקום אחד כאשר השאלה היא רק ”מה השם המדויק של הקריאה הזו“.