12.6. القنوات المسمّاة¶
معرّف القناة في ترويسة كل حزمة يتيح لما يصل إلى 32 تدفقًا مستقلًا أن تتشارك الناقل الفيزيائي نفسه. تحوّل طبقة القنوات تلك المعرّفات الرقمية إلى نقاط نهاية مسمّاة ومرئية للتطبيق يمكن لكود المضيف الإشارة إليها بسلسلة نصية.
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.
القناة هي أصغر مساحة سطح يتفق عليها برنامج المضيف وبرنامج الكاميرا للتعاون. فالاسم، والاتجاهية (قراءة أو كتابة أو كلاهما)، ودوال رد النداء على جانب الكاميرا، واستدعاءات الدوال المطابقة على جانب المضيف، هي العقد بأكمله.