12.6. ערוצים בעלי שם

מזהה הערוץ בכותרת של כל מנה (packet) מאפשר עד 32 זרמים עצמאיים לחלוק את אותה תעבורה (transport) פיזית. שכבת הערוצים הופכת את המזהים המספריים הללו לנקודות קצה בעלות שם, גלויות לאפליקציה, שקוד המארח יכול להתייחס אליהן באמצעות מחרוזת.

One transport wire on the left fanning out into four labelled channels on the cam side -- stdin, stdout, stream, and a user-registered status channel -- each showing as an independent box.

12.6.1. ארבעת הערוצים המובנים

המצלמה רושמת ארבעה ערוצים בעת האתחול, לפני שכל קוד אפליקציה רץ:

  • stdin – בייטים של סקריפט שהמארח דוחף אל המצלמה לצורך הרצה. ה-IDE משתמש בערוץ זה כדי לשלוח את הסקריפט הנערך; exec() ב-SDK של המארח היא הקריאה המקבילה מתוך תוכנית Python.

  • stdout – בייטים מקריאות print() של המצלמה ומ-tracebacks של חריגות שלא נתפסו. קונסולת ה-serial של ה-IDE קוראת ערוץ זה.

  • stream – ערוץ התצוגה המקדימה החיה. ה-IDE מושך ממנו פריימים בפורמט JPEG; כל סקריפט מארח יכול לעשות זאת בעזרת read_frame().

  • profile – אירועי profiler, נוכח רק כאשר המצלמה נבנתה עם profiling מופעל. רוב גרסאות ה-release משמיטות אותו.

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

12.6.2. רישום ערוץ

סקריפט בצד המצלמה רושם ערוץ חדש על ידי קריאה ל-protocol.register() עם שם ואובייקט backend של Python:

import json
import protocol
import time

trigger_count = 0

class StatusChannel:
    def size(self):
        # Refresh the snapshot on every host query.
        self._buf = json.dumps({
            'uptime_s': time.ticks_ms() // 1000,
            'triggers': trigger_count,
        }).encode()
        return len(self._buf)

    def read(self, offset, size):
        return self._buf[offset:offset + size]

protocol.register(name='status', backend=StatusChannel())

המתודות של אובייקט ה-backend מחליטות מה הערוץ יכול לעשות. backend עם size ו-read בלבד הוא ערוץ נתונים לקריאה בלבד; הוסף write והוא הופך לדו-כיווני; הוסף poll והמארח יכול לשאול אם נתונים חדשים מוכנים לפני ששילם עבור קריאה. דגימת הנתונים בתוך size היא הדפוס הפשוט ביותר כאשר המטען קטן מספיק כדי להיכנס בקטע אחד – החוצץ נוצר לפי דרישה, אף פעם לא נשמר במטמון, אף פעם לא נתון לתחרות (race). מטענים גדולים יותר – פריימים של תמונות, עקבות חיישן – זקוקים לדפוס נעילה (latching) המחזיק את החוצץ עד שהמארח מסיים את הקריאה מרובת-הקטעים שלו, נושא המכוסה בערוץ הפריימים.

מעט ניהול רישום מתרחש באופן אוטומטי:

  • הספרייה מקצה את מזהה הערוץ הפנוי הבא (בין 0 ל-31).

  • דגלי היכולות נגזרים מהמתודות הנוכחות: CHANNEL_FLAG_READ אם read מוגדרת, CHANNEL_FLAG_WRITE אם write מוגדרת, CHANNEL_FLAG_LOCK אם lock / unlock מוגדרות.

  • מנת אירוע CHANNEL_REGISTERED נשלחת לכל מארח מחובר כך שרשימת הערוצים שלו מתעדכנת.

ערך ההחזרה הוא ידית (handle) מסוג protocol.ProtocolChannel שהאפליקציה יכולה להחזיק. המתודה send_event() של הידית היא ה-hook בצד המצלמה שנועד לומר למארח ”משהו קרה בערוץ זה בלי לשנות את הנתונים הניתנים לקריאה“ – טריגר הופעל, כפתור נלחץ, אבן-דרך של מספר דגימות נחצתה.

12.6.3. קריאת ערוצים מהמארח

ה-SDK של המארח מסופק כחבילה openmv ב-PyPI (pip install openmv), בנוי על pyserial עבור התעבורה (transport). מחלקת openmv.camera.Camera שלו חושפת את הערוצים בעלי השם של המצלמה דרך מתודות ברמה גבוהה:

from openmv.camera import Camera

with Camera('/dev/ttyACM0', baudrate=921600) as cam:
    cam.update_channels()
    if cam.has_channel('status'):
        size = cam.channel_size('status')
        data = cam.channel_read('status', size)

אזהרה

החבילה openmv דורשת CPython 3.12 או חדש יותר. מפרשנים מוקדמים יותר חסרים תכונות שה-SDK תלוי בהן; התקן בנייה של 3.12+ לפני pip install openmv.

מספר דברים שכדאי לשים לב אליהם בהגדרה:

  • מחרוזת ה-serial-port – /dev/ttyACM0 כאן – היא בסגנון COM3 ב-Windows, /dev/cu.usbmodemXXXX ב-macOS, ו-/dev/ttyACM* ב-Linux. המספר בפועל תלוי באיזה פורט המצלמה נספרה.

  • קצב הבָּאוּד (baud rate) הוא הערך הקסום של הפרוטוקול 921600, שמחסנית ה-USB-CDC של המצלמה מזהה כ“לקוח זה מדבר את הפרוטוקול, לא את ה-REPL“. כל קצב אחר נסוג לקו serial רגיל.

  • מנהל ההקשר with Camera(...) as cam: פותח את התעבורה (transport), מריץ PROTO_SYNC, מחליף יכולות, וביציאה סוגר את הפורט בצורה נקייה. הקריאה המפורשת update_channels() לאחר הכניסה מרעננת את רשימת הערוצים המקומית עם כל ערוץ שהאפליקציה רשמה לאחר האתחול.

channel_size() ו-channel_read() הן מתודות סוס-העבודה; channel_write() מעבירה חוצץ הלוך-ושוב אל המצלמה אם ל-backend יש מתודת write; has_channel() היא הדרך הבטוחה לבדוק ששם רשום לפני השימוש בו. שם הערוץ נבדק פעם אחת אל מול מזהה הערוץ שהמצלמה הקצתה במהלך register, ומשמש בכל מנה מאותו רגע והלאה.

כל זוג channel_size() / channel_read() עולה שתי נסיעות הלוך-ושוב: מנה אחת כדי לבקש את הגודל, אחת כדי לבקש את הבייטים. מעל USB-CDC שתיהן מסתיימות בכמילישנייה אחת בסך הכל; מעל UART אותו חילוף אורך זמן רב יותר ביחס לקצב הבָּאוּד (baud rate) של קו ה-serial. קוד אפליקציה הקורא בלולאה צמודה צריך לקרוא ל-channel_size() רק כאשר הגודל באמת יכול להשתנות – עבור נתונים בגודל קבוע, ניתן לשמור במטמון את הגודל מהקריאה הראשונה.

12.6.4. עצמאות בין ערוצים

שלושה דברים שכדאי לדעת על האופן שבו ערוצים מקיימים אינטראקציה:

  • בקרת זרימה עצמאית. לכל ערוץ יש מצב קריאה ממתין משלו, נתונים משלו, ופונקציות ה-callback size / read / write משלו. קריאה ממושכת על ערוץ ה-stream אינה חוסמת קריאות על ערוץ ה-config של האפליקציה.

  • סדרתי בכל ערוץ. בתוך ערוץ יחיד, מנות נמסרות לפי הסדר. שכבת המהימנות מבטיחה זאת אפילו כאשר מעורבות שידורים חוזרים.

  • תעבורה משותפת, תקציב שידור חוזר משותף. כל הערוצים חולקים את הקישור הפיזי האחד, ולכן מבול של תעבורה על ערוץ אחד מאט את האחרים על ידי השתלטות על החוט. מנגנון ה-CHANNEL_LOCK מאפשר לערוץ אחד לשמור את החוט לצורך קריאה אטומית מרובת מנות; ה-backend מצטרף על ידי מימוש פונקציות ה-callback lock / unlock.

ערוץ הוא שטח הפנים המינימלי שעליו תוכנית מארח ותוכנית מצלמה מסכימות לשתף פעולה. השם, הכיווניות (קריאה או כתיבה או שתיהן), מתודות ה-callback בצד המצלמה, וקריאות המתודה התואמות בצד המארח הן החוזה כולו.