12.9. זרימה דו-כיוונית

ערוצים אינם חד-כיווניים. backend שמממש write מאפשר למארח לדחוף בתים אל המצלמה, והמצלמה מגיבה. זהו הדפוס שמאחורי כל כלי אינטראקטיבי אמיתי: המפעיל מסובב כפתור בממשק הגרפי של המארח, המארח כותב את הערך החדש לערוץ תצורה, והמצלמה קוראת אותו בפעם הבאה שהיא לוכדת.

12.9.1. ערוץ תצורה

בהוספה לסקריפט הזרימה בצד המצלמה, חשוף ערוץ שני עבור איכות ה-JPEG:

class ConfigChannel:
    def __init__(self):
        self.quality = 85

    def size(self):
        return 0

    def read(self, offset, size):
        # Not used for "host writes to cam" -- but the library
        # still needs the method present.
        return b''

    def write(self, offset, data):
        # data is a bytearray view into the protocol buffer.
        # Copy out the contents before doing anything with it.
        new_q = int(bytes(data))
        if 1 <= new_q <= 100:
            self.quality = new_q
        return len(data)

config = ConfigChannel()
protocol.register(name='config', backend=config)

לולאת הלכידה קוראת מ-config.quality בכל פעם שהיא דוחסת פריים:

while True:
    img = csi0.snapshot()
    latest_jpeg = bytes(
        img.compress(quality=config.quality).bytearray()
    )
    ch.send_event(0x01)

כעת למארח יש כפתור. הגדר אותו ל-50 והפריים הבא קטן יותר (ומכוער יותר); הגדר אותו ל-95 והפריים הבא גדול יותר (וחד יותר). המצלמה ממשיכה ללכוד מבלי לאתחל מחדש; המארח אינו צריך לדחוף סקריפט חדש.

12.9.2. קריאת הכתיבה מהמארח

בצד המארח, channel_write() שולח בתים לערוץ בעל שם:

cam.channel_write('config', b'50')

ה-SDK של המארח מקודד את הבתים כחבילת CHANNEL_WRITE יחידה (או מקוטעת), שכבת הפרוטוקול מוסרת אותה למצלמה, ה-write(offset=0, data=...) של המצלמה רץ, וצד המצלמה מאשר. עד שהקריאה חוזרת המצלמה קיבלה וקיבלה את הערך החדש.

הכתיבה היא אטומית מנקודת המבט של המצלמה – ספריית הפרוטוקול מבטיחה שה-write של ה-backend רץ עד השלמתו לפני שכל פעולה אחרת על אותו ערוץ ממשיכה. קוד יישום יכול לקרוא את config.quality מתוך לולאת הלכידה מבלי לדאוג שהמארח ידרוס באמצע תמונת בזק (snapshot).

12.9.3. גודל stub וקריאה בערוץ לכתיבה-בלבד

ערוץ כתיבה טהור עדיין צריך size ו-read מוגדרים, גם אם הם stub-ים המחזירים 0 ו-b''. הספרייה משתמשת בנוכחות המתודות כדי להפיק את דגלי היכולת של הערוץ; backend שחסר לו read לא יקבל את CHANNEL_FLAG_READ מוגדר והמארח יסרב לניסיון קריאה.

עם זאת, הבתים המוחזרים מ-read בערוץ לכתיבה-בלבד שימושיים למטרה שונה: הדהוד הערך הנוכחי בחזרה כך שמארח שזה עתה התחבר יוכל לשאול את המצלמה ”מהי ההגדרה הנוכחית?“ במקום להתחיל מברירת מחדל. כדי שזה יעבוד, שני הכיוונים חייבים להסכים על סריאליזציה. ניתוח הבתים הגולמיים int(bytes(data)) בדוגמה הקודמת עובד עבור שדה מספר שלם יחיד אך לא יתרחב ברגע שיש כפתור שני להגדיר. החלפת write לניתוח JSON ושיוכו ל-read שמחזיר את ה-JSON dump התואם הופכת את הערוץ למאגר תצורה הלוך-ושוב אמיתי:

import json

class ConfigChannel:
    def __init__(self):
        self.quality = 85
        self._buf = b''
    def size(self):
        self._buf = json.dumps({'quality': self.quality}).encode()
        return len(self._buf)
    def read(self, offset, size):
        return self._buf[offset:offset + size]
    def write(self, offset, data):
        new = json.loads(bytes(data))
        if 'quality' in new:
            self.quality = int(new['quality'])
        return len(data)

כעת המארח כותב cam.channel_write('config', b'{"quality": 50}') כדי להגדיר ערך ו-cam.channel_read('config') כדי לקרוא את המצב הנוכחי בחזרה. המצלמה מסדרת JSON dump טרי בכל קריאה כך שהמארח תמיד רואה את הערכים העדכניים ביותר, והוספת כפתור נוסף (threshold, exposure, orientation) היא שורה אחת במילון ה-JSON בכל צד.

12.9.4. לולאה שלמה

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

  • המארח פותח את המצלמה, מתחיל למשוך פריימים, ומציג אותם בחלון.

  • כשהמפעיל גורר מחוון, המארח כותב את הערך החדש על config.

  • לולאת הלכידה של המצלמה קולטת את הערך בפריים הבא.

  • הפריימים החדשים זורמים דרך אותו ערוץ frame.

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

כל מה שמעבר לנקודה זו הוא קוד יישום. הוספת ערוץ שלישי להיסטוגרמה, רביעי לטלמטריה, או חמישי להדקי חיישן היא אותו מתכון של מחלקת-backend-ו-protocol.register, חוזר על עצמו. ברגע שפרויקט מצלמה מגיע לנקודה זו, הפרוטוקול מפסיק להיות הבעיה המעניינת; הלוגיקה של היישום עצמו היא זו.