12.6. القنوات المسمّاة

معرّف القناة في ترويسة كل حزمة يتيح لما يصل إلى 32 تدفقًا مستقلًا أن تتشارك الناقل الفيزيائي نفسه. تحوّل طبقة القنوات تلك المعرّفات الرقمية إلى نقاط نهاية مسمّاة ومرئية للتطبيق يمكن لكود المضيف الإشارة إليها بسلسلة نصية.

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 -- بايتات البرنامج النصي التي يدفعها المضيف إلى الكاميرا لتنفيذها. تستخدم OpenMV IDE هذه القناة لإرسال البرنامج النصي الجاري تحريره؛ و exec() في حزمة SDK للمضيف هو الاستدعاء المكافئ من برنامج Python.

  • stdout -- بايتات ناتجة عن استدعاءات print() في الكاميرا وتتبّعات الاستثناءات غير المُلتقطة. تقرأ وحدة التحكم التسلسلية في OpenMV IDE هذه القناة.

  • stream -- قناة المعاينة الحية. تسحب OpenMV IDE إطارات JPEG منها؛ ويمكن لأي برنامج نصي على المضيف فعل الشيء نفسه عبر read_frame().

  • profile -- أحداث المُحلِّل (profiler)، وتكون موجودة فقط عندما تُبنى الكاميرا مع تفعيل التحليل. معظم بنيات الإصدار تحذفها.

نادرًا ما يحتاج كود التطبيق إلى التعامل مع أي من القنوات المدمجة؛ فالعمل المثير للاهتمام يحدث على القنوات التي يسجّلها التطبيق بنفسه.

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())

دوال الكائن الخلفي تقرر ما يمكن للقناة فعله. الكائن الخلفي الذي يحتوي على size و read فقط هو قناة بيانات للقراءة فقط؛ أضف write فتصبح ثنائية الاتجاه؛ أضف poll فيستطيع المضيف أن يسأل إن كانت بيانات جديدة جاهزة قبل دفع تكلفة القراءة. أخذ عينة من البيانات داخل size هو أبسط نمط عندما تكون الحمولة صغيرة بما يكفي لتناسب جزءًا واحدًا -- إذ يُولَّد المخزن المؤقت عند الطلب، ولا يُخزَّن مؤقتًا أبدًا، ولا يتعرّض لتسابق أبدًا. أما الحمولات الأكبر -- إطارات الصور وآثار المستشعرات -- فتحتاج إلى نمط تثبيت يُمسك المخزن المؤقت حتى ينهي المضيف قراءته متعددة الأجزاء، وهو ما يُغطّى مع قناة الإطارات.

يحدث قدر يسير من مسك الدفاتر تلقائيًا:

  • تُسنِد المكتبة معرّف القناة الحر التالي (بين 0 و31).

  • تُشتق رايات القدرات من الدوال الموجودة: CHANNEL_FLAG_READ إذا كانت read معرّفة، و CHANNEL_FLAG_WRITE إذا كانت write معرّفة، و CHANNEL_FLAG_LOCK إذا كانت lock / unlock معرّفتين.

  • تُرسَل حزمة حدث CHANNEL_REGISTERED إلى أي مضيف متصل ليُحدِّث قائمة قنواته.

القيمة المُرجَعة هي مَقبِض protocol.ProtocolChannel يمكن للتطبيق الاحتفاظ به. ودالة المَقبِض send_event() هي الخُطّاف على جانب الكاميرا لإخبار المضيف بأن "شيئًا ما حدث على هذه القناة دون تغيير البيانات القابلة للقراءة" -- أُطلق مُشغِّل، أو ضُغط زر، أو تجاوزَ عدّاد العينات حدًا فاصلًا.

12.6.3. قراءة القنوات من المضيف

تُشحن حزمة SDK للمضيف بوصفها حزمة openmv على PyPI (pip install openmv)، مبنيّة على pyserial للناقل. ويُتيح صنفها 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.

بضعة أمور ينبغي ملاحظتها بشأن الإعداد:

  • سلسلة المنفذ التسلسلي -- /dev/ttyACM0 هنا -- تكون بنمط COM3 على Windows، و /dev/cu.usbmodemXXXX على macOS، و /dev/ttyACM* على Linux. ويعتمد الرقم الفعلي على المنفذ الذي عُدّت الكاميرا عليه.

  • معدل الباود هو القيمة السحرية للبروتوكول 921600، التي تتعرّف عليها حزمة USB-CDC في الكاميرا على أنها "هذا العميل يتحدث البروتوكول، لا REPL." أي معدل آخر يرجع إلى خط تسلسلي عادي.

  • مدير السياق with Camera(...) as cam: يفتح الناقل، ويشغّل PROTO_SYNC، ويتبادل القدرات، وعند الخروج يغلق المنفذ نظيفًا. والاستدعاء الصريح update_channels() بعد الدخول يُحدّث قائمة القنوات المحلية بأي قنوات سجّلها التطبيق بعد الإقلاع.

channel_size() و channel_read() هما الدالتان الأساسيتان؛ و channel_write() تنقل مخزنًا مؤقتًا ذهابًا وإيابًا إلى الكاميرا إذا كان للكائن الخلفي دالة write؛ و has_channel() هي الطريقة الآمنة للتحقق من أن اسمًا ما مُسجَّل قبل استخدامه. يُبحَث عن اسم القناة مرة واحدة للحصول على معرّف القناة الذي أسنده الكاميرا أثناء register، ويُستخدم في كل حزمة من حينها فصاعدًا.

كل زوج channel_size() / channel_read() يكلّف ذهابين وإيابين: حزمة لطلب الحجم، وأخرى لطلب البايتات. عبر USB-CDC ينتهي كلاهما في نحو مللي ثانية مجتمعين؛ وعبر UART يستغرق التبادل نفسه وقتًا أطول بما يتناسب مع معدل الباود للخط التسلسلي. وينبغي لكود التطبيق الذي يقرأ في حلقة ضيّقة أن يستدعي channel_size() فقط عندما يمكن للحجم أن يتغيّر فعلًا -- فبالنسبة للبيانات ذات الحجم الثابت، يمكن تخزين الحجم من الاستدعاء الأول مؤقتًا.

12.6.4. الاستقلالية بين القنوات

ثلاثة أمور تستحق المعرفة بشأن كيفية تفاعل القنوات:

  • تحكم مستقل في التدفق. لكل قناة حالة قراءة معلّقة خاصة بها، وبياناتها الخاصة، ودوال رد النداء size / read / write الخاصة بها. فالقراءة الطويلة الأمد على قناة stream لا تحجب القراءات على قناة config الخاصة بالتطبيق.

  • متسلسلة لكل قناة. داخل القناة الواحدة، تُسلَّم الحزم بالترتيب. وتضمن طبقة الموثوقية هذا حتى عند وجود إعادات الإرسال.

  • ناقل مشترك، وميزانية إعادة إرسال مشتركة. تتشارك جميع القنوات الوصلة الفيزيائية الواحدة، لذا فإن سيلًا من حركة المرور على قناة واحدة يُبطئ الأخريات باحتكاره للسلك. وتتيح آلية CHANNEL_LOCK لقناة واحدة أن تحجز السلك من أجل قراءة ذرّية متعددة الحزم؛ ويختار الكائن الخلفي المشاركة بتنفيذ دالتي رد النداء lock / unlock.

القناة هي أصغر مساحة سطح يتفق عليها برنامج المضيف وبرنامج الكاميرا للتعاون. فالاسم، والاتجاهية (قراءة أو كتابة أو كلاهما)، ودوال رد النداء على جانب الكاميرا، واستدعاءات الدوال المطابقة على جانب المضيف، هي العقد بأكمله.