11.11. ערוצי L2CAP¶
GATT הוא מודל מפתח/ערך. הפעולות שהוא מציע (read, write, notify, indicate) מעבירות ערך קצר אחד בכל פעם, והמטען הבודד הגדול ביותר שהן יכולות לשאת הוא מה שה-MTU המנוהל מתיר – כמה מאות בתים לכל היותר. זה עובד היטב עבור קריאות חיישן, אוגרי פקודה ודגלי סטטוס. זה מתפרק על קילו-בתים או מגה-בתים: פיצול blob ארוך למאות כתיבות קטנות עולה בנסיעות הלוך-ושוב שהרדיו איטי בהן בהרבה.
עבור זרימות נתונים בכמות גדולה – פריים שנלכד שהמצלמה משדרת לטלפון, תמונת עדכון אלחוטי, ייצוא מקובץ של מדידות – BLE מציע נתיב חלופי: ה-Logical Link Control and Adaptation Protocol, או L2CAP. L2CAP יושב בין שכבת הקישור ל-GATT ומאפשר ליישום לתבוע ערוץ מוכוון-חיבור משלו מעל אותו קישור רדיו. הערוץ הוא נתיב בתים המבוקר בזרימת אשראי עם MTU גדול בהרבה לכל מנה וללא מסגור GATT באמצע.
11.11.1. מתי להשתמש ב-L2CAP¶
ערוצי L2CAP הם הכלי הנכון כאשר:
ההעברה גדולה מכמה מאות בתים.
שני הקצוות יודעים ששימוש בערוץ L2CAP ייעשה (הוא אינו נחשף במטען הפרסום; הלקוח חייב לדעת את מספר ה-protocol/service multiplexer, או PSM, של הערוץ מחוץ לערוץ התקשורת).
היישום מוכן לוותר על נוחיות ה-GATT: ללא יכולת מיעון מובנית לפי UUID, ללא יכולת גילוי על ידי הלקוח באמצעות אפליקציות סטנדרטיות, ללא התראות.
המקרה הנפוץ ביותר ביישומים מבוססי aioble הוא העברת blob בינארי בין שני רכיבי תוכנה שמכירים שניהם את מוסכמת ה-PSM – פרוטוקול מותאם אישית בין מצלמה לטלפון, זוג מצלמות openmv המדברות זו עם זו, נתיב פנימי לעדכון קושחה תחת שירות GATT של peripheral.
עבור כל השאר, הישארו ב-GATT. סטטוס קצר, אוגר בקרה, קריאת חיישן – כל אלו שייכים למאפיין.
11.11.2. הקמת ערוץ¶
L2CAP רץ מעל aioble.DeviceConnection קיים, כך שזרימת הגילוי / הפרסום / החיבור בצד ה-GAP זהה לחלוטין לזו של GATT. ברגע ששני הצדדים מחזיקים בחיבור, צד אחד מאזין על PSM, והצד השני מתחבר אליו.
ה-PSM הוא פשוט מספר שלם קטן. ה-Bluetooth SIG שומר את תחתית הטווח לשימוש מתוקנן (0x0001-0x007F); עבור ערוצים ספציפיים ליישום השתמשו במספר מהטווח הדינמי (0x0080-0x00FF עבור PSMs קבועים, 0x0040 ומעלה בדרך כלל פנוי לשימוש מותאם אישית). שני הצדדים חייבים להסכים על הערך מראש.
ה-MTU על ערוץ L2CAP הוא ה-SDU (Service Data Unit) הבודד הגדול ביותר שכל אחד מהצדדים ימסור ב-send() אחד – לא ה-MTU של קישור ה-BLE. Aioble מפצל מטענים גדולים יותר אוטומטית. מארח ה-BLE של המצלמה מגביל את MTU של L2CAP ל-1017 בתים; 512 הוא ברירת מחדל הגיונית שמשאירה מקום בשני הצדדים מבלי לשרוף RAM.
בצד המאזין (למשל המצלמה כ-peripheral):
async def serve_l2cap(connection, image_bytes):
channel = await connection.l2cap_accept(psm=0x80, mtu=512)
async with channel:
# image_bytes is a bytearray -- e.g. csi0.snapshot().bytearray()
# or a compressed JPEG buffer. send() fragments into MTU-sized
# chunks automatically and awaits flow-control credits between.
await channel.send(image_bytes)
await channel.flush()
בצד המתחבר (למשל טלפון או central):
async def open_l2cap(connection, total_bytes):
channel = await connection.l2cap_connect(psm=0x80, mtu=512)
async with channel:
image_bytes = bytearray(total_bytes)
view = memoryview(image_bytes)
received = 0
while received < total_bytes:
n = await channel.recvinto(view[received:])
if n == 0:
break
received += n
return image_bytes
l2cap_accept() חוסם עד שהעמית מתחבר (או ש-timeout_ms נורה); l2cap_connect() חוסם עד שהמאזין מקבל (או נכשל). שניהם מחזירים aioble.L2CAPChannel – שהוא עצמו מנהל הקשר אסינכרוני הסוגר את הערוץ ביציאה.
11.11.3. שליחה וקבלה¶
שתי הפעולות העיקריות על ערוץ הן send() (כותבת בתים לעמית) ו-recvinto() (קוראת לתוך חוצץ (buffer) שהוקצה מראש). שתיהן coroutines.
send()מפצל את החוצץ (buffer) לנתחים בגודל MTU וממתין לאשראי בקרת-זרימה ברמת הקישור ביניהם. שליחה ארוכה היאawaitאחד מנקודת מבטו של היישום; פנימית היא עשויה להעמיד בתור מנות רבות ולהשהות בכל פעם שאשראי הקבלה של העמית אוזל.recvinto()ממלא את החוצץ (buffer) שהועבר בכל מה שזמין (עד MTU של הערוץ) ומחזיר את מספר הבתים. ממתין אם אין דבר זמין.available()מחזירTrueבאופן סינכרוני אם יש נתונים מאוחסנים בחוצץ ומוכנים – שימושי לבדיקה תקופתית ללא השהיה.flush()ממתין עד שכל שליחה תלויה ועומדת שודרה במלואה לבקר.
ערוצי L2CAP הם דמויי-זרם במובן שהבתים מגיעים לפי הסדר וללא אובדן, אך הגבולות של send בודד נשמרים – כל SDU יוצא מ-recvinto בודד. זה שונה מ-TCP, שבו הגבולות של send() אחד עלולים להתמרח על פני מספר קריאות recv().
11.11.4. טיפול בניתוק¶
הערוץ נעלם בשלושה תנאים: אחד הצדדים קורא ל-disconnect(), חיבור ה-GAP הבסיסי נופל, או שמגיע ניתוק ברמת L2CAP. פעולות פעילות מעלות aioble.L2CAPDisconnectedError. כמו בצד ה-GATT, הדבר צף כחריגה ב-coroutine שהמתין, ובלוק ה-async with channel יוצא בצורה נקייה.
אם ערוץ הופך לבלתי נגיש דרך ניתוק ברמת GAP, היישום חוזר בלולאה לפרסום או לסריקה באותו אופן שהיה עושה עבור ניתוק GATT.
11.11.5. עלות זיכרון¶
MTUs גדולים יותר ותורים ארוכים יותר משתמשים ביותר RAM בשני הצדדים. MTU בן 512 בתים בתוספת חוצץ קבלה לכל ערוץ הם כ-1 KB לכל ערוץ – לא חינמי על מצלמה קטנה אם מספר ערוצים פתוחים בו-זמנית. היצמדו לערוץ אחד לכל חיבור ובחרו MTU שתואם את גודל ההודעה הצפוי; ברירת המחדל של L2CAPChannel אחד לכל DeviceConnection מספיקה לרוב היישומים.
L2CAP הוא שסתום הביטחון של BLE. GATT הוא מה שכמעט כל יישום פונה אליו ראשון, ושאר הדוגמאות של central / peripheral בחלק זה נצמדות ל-GATT. ה-API בטעם הערוצים הוא התשובה כאשר יישום גדל מעבר למודל המפתח/ערך.