13.3.1.4. القنوات المخصصة

القناة هي تدفق بايتات مُسمّى ثنائي الاتجاه بين برنامج نصي على جانب الكاميرا والمضيف. تُسجّل الكاميرا قناة وتوفّر دوال رد نداء تنتج البيانات أو تستهلكها؛ ويقرأ المضيف من تلك القناة ويكتب إليها بالاسم. الآلية نفسها التي تستخدمها الحزمة داخليًا لقناة stream التي تحمل الإطارات، وقناة stdout التي تحمل مخرجات البرنامج النصي، وقناة stdin التي تحمل رفع البرنامج النصي، مكشوفة للبرامج النصية للمستخدم، فأي بيانات خاصة بالتطبيق يحتاجها المضيف يمكنها أن تركب اتصال USB نفسه دون اختراع بروتوكول ثانٍ.

هذه هي أكثر ميزات الحزمة فائدة وأقلها تغطية في الوثائق القياسية، لذا تتناولها هذه الصفحة من البداية إلى النهاية.

13.3.1.4.1. النصفان

تحتاج القناة المخصصة إلى تعليمات برمجية متعاونة على الجانبين. البرنامج النصي على جانب الكاميرا يستورد protocol، ويُعرّف صنفًا بثلاث دوال (size() وread() وpoll()) بالإضافة إلى دالة write() اختيارية، ويستدعي protocol.register(name=..., backend=...) لنشر القناة تحت اسم مختار:

import protocol
import time

class TicksChannel:
    def size(self):
        return 10

    def read(self, offset, size):
        return f'{time.ticks_ms():010d}'

    def poll(self):
        return True

protocol.register(name='ticks', backend=TicksChannel())

تُرجع الدالة size() عدد البايتات المتاحة حاليًا في القناة. أما read() فهي المنتِج: بناءً على offset وsize اللذين يطلبهما المضيف، تُرجع البايتات (أو سلسلة نصية تُرمّزها طبقة البروتوكول). وتُرجع poll() القيمة True عندما يكون هناك شيء للقراءة -- تستخدم طبقة البروتوكول هذا لوضع علامة على القناة كجاهزة في read_status().

برنامج جانب المضيف يستخدم أربع دوال من openmv.Camera: has_channel() للتحقق من وجود القناة، وchannel_size() للسؤال عن مقدار البيانات المنتظرة، وchannel_read() لسحب البايتات، وchannel_write() لدفع البايتات. وتستطلع read_status() كل قناة دفعة واحدة:

from openmv import Camera

with Camera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('ticks_cam.py').read())

    while True:
        status = cam.read_status()

        if status.get('ticks'):
            data = cam.channel_read('ticks')
            print(f"ticks: {data.decode()}")

تستطلع حلقة المضيف read_status()؛ وعندما تكون قناة ticks جاهزة تستدعي channel_read() بدون size لسحب كل ما هو متاح. وتُرجع TicksChannel.poll() في الكاميرا القيمة True في كل فحص، فتكون القناة دائمًا "جاهزة" ويحصل المضيف على قيمة نبضة جديدة في كل استطلاع.

13.3.1.4.2. قناة ثنائية الاتجاه

بالنسبة لمضيف يحتاج إلى دفع البيانات إلى الخلف، يضيف الصنف على جانب الكاميرا دالة write() تقبل البايتات الواردة:

import protocol

class CommandChannel:
    def __init__(self):
        self.last_command = b''
        self.replied = False

    def size(self):
        return len(self.last_command)

    def read(self, offset, size):
        self.replied = True
        return self.last_command

    def write(self, offset, data):
        self.last_command = b'echo: ' + bytes(data)
        self.replied = False

    def poll(self):
        return not self.replied and len(self.last_command) > 0

protocol.register(name='echo', backend=CommandChannel())

يكتب المضيف إلى القناة باستخدام channel_write() ويقرأ الرد عبر نمط read_status() / channel_read() المعتاد:

with Camera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('echo_cam.py').read())

    cam.channel_write('echo', b'hello')

    while True:
        if cam.read_status().get('echo'):
            print(cam.channel_read('echo').decode())
            break

13.3.1.4.3. ما الذي يكسبه هذا للتطبيق

القنوات المخصصة هي الأداة الصحيحة كلما أراد تطبيق أن يركب اتصال USB القائم لبيانات ليست إطارات وليست مطبوعات: عدّادات قياس عن بُعد، ومقابض إعداد تُبثّ مباشرة من واجهة مستخدم على المضيف، وأوامر تحكم تُرسل في الاتجاه الآخر، ونتائج قياس حسبته الكاميرا لا يناسبه تأطير "الصورة" الذي تفترضه قناة البثّ. تتولى طبقة البروتوكول التأطير والتجزئة والإقرار وإعادة المحاولة؛ ولا يحتاج البرنامج النصي إلا إلى تنفيذ الخلفية ذات الدوال الأربع، ولا يحتاج المضيف إلا إلى معرفة اسم القناة وشكل البيانات.

علَم --channel NAME في واجهة سطر الأوامر طريقة سريعة للتحقق من قناة مخصصة من الطرفية دون كتابة برنامج على جانب المضيف: تستطلع واجهة سطر الأوامر القناة المُسمّاة وتطبع أول عشرة بايتات من كل تحديث.

حد الحجم في استدعاء channel_read() أو channel_write() المنفرد هو قيمة max_payload المتفاوض عليها في البروتوكول -- 4096 بايت افتراضيًا. تقسم دوال جانب المضيف الكتابات الأكبر تلقائيًا إلى العدد الصحيح من الحزم، فيستطيع التطبيق تمرير مخازن مؤقتة بأي حجم كان؛ والتجزئة غير مرئية.