13.3.1.5. الأحداث

الصفحات حتى الآن تستدعي الكاميرا: رفع برنامج نصي، قراءة إطار، الكتابة إلى قناة. كل واحدة من تلك العمليات بادئها المضيف -- المضيف يسأل والكاميرا تجيب. يعمل البروتوكول أيضًا في الاتجاه الآخر. تستطيع الكاميرا دفع أحداث إلى المضيف دون أن تُسأل، وتُسلّم SDK المضيف كل واحد منها إلى دالة رد نداء يمكن للتطبيق تجاوزها.

هذه هي الأداة الصحيحة كلما أراد التطبيق التفاعل مع شيء لاحظته الكاميرا قبل أن يسأل. بدون الأحداث، تكون الطريقة الوحيدة لمعرفة ذلك هي الاستمرار في استدعاء read_status() في حلقة.

13.3.1.5.1. دالة رد النداء الافتراضية

يشترك Camera في الأحداث داخليًا بالفعل. و_handle_event() هي دالة رد النداء التي يُشغّلها النقل كلما وصلت حزمة حدث. تتعامل الدالة الافتراضية مع ثلاثة أحداث للنظام:

  • CHANNEL_REGISTERED -- ظهرت قناة جديدة على الكاميرا بعد اتصال المضيف. يُحدّث إطار العمل ذاكرة القنوات المؤقتة لديه حتى يعثر عليها بحث has_channel() التالي.

  • CHANNEL_UNREGISTERED -- اختفت قناة.

  • SOFT_REBOOT -- أعادت الكاميرا الإقلاع من تلقاء نفسها (مراقب التشغيل، أو عطل صعب، أو machine.reset() متعمد).

كما يتتبع حدث جاهزية الإطار لقناة stream لمسار البثّ، وبدء/إيقاف البرنامج النصي لقناة stdin لتخزين stdout المؤقت. تُبقي القيمة الافتراضية events=True في المُنشئ كل هذا قيد التشغيل؛ والتطبيق الذي لا يريد أيًا منه يستطيع تمرير events=False إلى Camera فيبقى نظام الأحداث صامتًا.

13.3.1.5.2. الاشتقاق للتفاعل

للتعامل مع الأحداث الخاصة بالتطبيق التي تثيرها الكاميرا، اشتقّ صنفًا من Camera وتجاوز _handle_event(). استدعِ الأصل أولًا للحفاظ على السلوك الافتراضي، ثم وزّع الأحداث التي يهتم بها التطبيق:

from openmv import Camera

class MyCamera(Camera):
    def _handle_event(self, channel_id, event):
        super()._handle_event(channel_id, event)

        name = self.channels_by_id.get(
            channel_id, {}).get('name')
        if name == 'motion' and event == 1:
            self.on_motion()

    def on_motion(self):
        print("motion detected")

التوقيع هو (channel_id, event). تكون قيمة channel_id هي 0 لأحداث النظام وإلا فهي المعرّف الرقمي للقناة التي أثارته؛ وevent عدد صحيح يختاره البرنامج النصي على جانب الكاميرا. يمنح التعداد EventType أسماء لأحداث النظام الثلاثة؛ أما أحداث القنوات فتستخدم أي قيم تُعرّفها الخلفية على جانب الكاميرا.

تعود أحداث القنوات مُفهرسة بالمعرّف الرقمي، لا بالاسم. قاموس channels_by_id المخزّن مؤقتًا هو ما يستخدمه التجاوز أعلاه للبحث عن الاسم؛ وchannels_by_name هو مرآته، مُفهرس بالاتجاه الآخر.

13.3.1.5.3. النصف على جانب الكاميرا

يثير البرنامج النصي على جانب الكاميرا حدثًا باستدعاء send_event() على المقبض الذي تُرجعه protocol.register()

import protocol

class MotionChannel:
    def size(self):
        return 0

    def read(self, offset, size):
        return b''

    def poll(self):
        return False

ch = protocol.register(
    name='motion', backend=MotionChannel())

while True:
    if detect_motion():
        ch.send_event(1)

رقم الحدث عدد صحيح يختاره التطبيق. أي قيمة يكون تجاوز المضيف مستعدًا للتعامل معها مقبولة؛ تعامله طبقة البروتوكول كحمولة غامضة. افتراضيًا يُطلق الاستدعاء وينسى؛ مرّر wait_ack=True للحجب حتى يُقرّ المضيف، عندما تكون معرفة أن الحدث قد وصل أهم من زمن استجابة الرحلة ذهابًا وإيابًا.

القناة التي تُطلق أحداثًا فقط ولا تحمل بيانات قابلة للقراءة هي نمط صالح -- تُرجع size القيمة 0 وتُرجع read بايتات فارغة. لا تزال مكتبة البروتوكول بحاجة إلى وجود كلتا الدالتين لوسم القناة كقابلة للقراءة؛ والبرنامج النصي على جانب الكاميرا ببساطة لا يضع بيانات فيها أبدًا.

13.3.1.5.4. قيادة مسار الاستقبال أثناء الخمول

تصل الأحداث على الاتصال نفسه مثل كل شيء آخر، فأي استدعاء مضيف يرسل بايتات أو يستقبلها يمنح النقل فرصة لمعالجة الأحداث المعلّقة ضمنيًا. حلقة استطلاع تستدعي بالفعل read_status() أو read_frame() مرة في كل دورة لا تحتاج إلى أي شيء إضافي.

للبرامج التي تمضي دقائق دون إدخال/إخراج آخر، تُشغّل poll_events() مسار الاستقبال مرة واحدة دون إرسال أمر. تعود بمجرد أن يفرغ المخزن المؤقت الوارد، فحلقة محكمة حولها -- أو مؤقت قصير في حلقة أحداث واجهة المستخدم الرسومية -- هي ما يُبقي المعالِجات متفاعلة.

13.3.1.5.5. حلقة كاملة

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

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

    while True:
        cam.poll_events()

تلتقط الكاميرا وتقرر وتثير الأحداث. يجلس المضيف داخل poll_events() حتى يصل واحد، ثم تعمل on_motion. لا يعمل أي استدعاء لـ read_status() عندما لا يحدث شيء، ولا يُسحب أي إطار عبر USB عندما لا يكون لدى الكاميرا ما تُبلّغ عنه.