14.3.2. المراقب (watchdog)

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

بالنسبة لمنتج مشحون، المراقب ليس اختياريًا. بدونه، يترك أي من أنماط الفشل أعلاه الكاميرا ميتة إلى أن يلاحظ أحدهم ويعيد تشغيل الطاقة. ومعه، تعود الكاميرا للعمل من تلقاء نفسها والدليل الوحيد على الفشل هو سطر واحد في السجل.

شاهد أيضا

تغطي صفحة مؤقت المراقب في فصل العتاد ما هو المراقب على مستوى العتاد وأساسيات واجهة machine.WDT. تغطي هذه الصفحة ما يتغيّر بالنسبة لنشر إنتاجي.

14.3.2.1. بدء تشغيل المراقب

machine.WDT هي الواجهة. وهي مدعومة بالعتاد: بمجرد إنشائها، يعمل المؤقت حتى عملية إعادة الضبط التالية. لا يوجد stop()، ولا deinit()، ولا مهرب بـ Ctrl-C. هذا هو المقصد.

إعداد نموذجي في أعلى main.py، مباشرةً قبل الحلقة التي يحميها:

from machine import WDT

wdt = WDT(timeout=10_000)              # milliseconds

main.py هو الموطن الصحيح للمراقب لأن ذلك هو المكان الذي تعيش فيه الحلقة. إعادة ضبط المراقب هي إعادة ضبط عتادية، لذا يُعاد تشغيل مسار الإقلاع البارد ويُعيد main.py الدخول إلى الحلقة من تلقاء نفسه -- يعمل الاسترداد دون أي توصيلات في boot.py. بدء المراقب في boot.py بدلًا من ذلك يعني أن كل إعادة ضبط ناعمة (مثل Ctrl-D من المطوّر) تسلّم التطبيق مؤقتًا عتاديًا لا سبيل له لإيقافه، وهو ما يمثّل إزعاجًا على الطاولة وفخًا في كود إعداد الإنتاج الذي يعمل قبل أن تكون الحلقة جاهزة.

اختر المهلة لتكون أطول بمقدار مرتين إلى ثلاث مرات من أسوأ زمن تكرار مُلاحَظ للحلقة الرئيسية. تذبذب معدل الإطارات، أو قراءة بطيئة من مستشعر بارد، أو تلعثم وجيز في Wi-Fi -- لا ينبغي لأيٍّ من هذه أن يُسقط المراقب. أما التعليق الحقيقي (حلقة لا نهائية، نداء I/O محجوب) فينبغي له ذلك. المهل القصيرة جدًا تحوّل المراقب إلى مصدر لإعادات ضبط زائفة؛ والمهل الطويلة جدًا تترك الكاميرا قابعة دون استجابة لدقائق قبل أن ينطلق الاسترداد.

14.3.2.2. إطعامه

يعيد wdt.feed() ضبط العد التنازلي. استدعِه مرة واحدة في كل تكرار للحلقة الرئيسية، في أعلى متن الحلقة بحيث يحدث الإطعام دون قيد أو شرط قبل أي عمل قد يعلق:

while True:
    wdt.feed()
    frame = csi0.snapshot()
    process(frame)

14.3.2.3. النجاة من الاستثناءات

يتعامل المراقب مع حالات التعليق. الاستثناءات نمط فشل مختلف. الاستثناء غير المُعالَج يطفو إلى المستوى الأعلى للبرنامج النصي، ويخرج main.py، وتسقط الكاميرا إلى REPL. عندئذٍ يُسقط المراقب بعد مهلته لأن لا شيء يطعمه من REPL، فتعيد الكاميرا الضبط، ويعمل main.py مجددًا -- فالاسترداد يعمل بالفعل، لكن الميدان يدفع مهلة كاملة زائد إعادة إقلاع عن كل تعطّل، ويذهب التتبّع العكسي إلى stdout عبر USB الذي لا يقرأه أحد، وتضيع أي حالة في الذاكرة كان التطبيق يحتفظ بها.

تغليف الحلقة الرئيسية في try / except على المستوى الأعلى يحوّل التعطّل إلى حدث مُسجَّل يتابع التطبيق العمل عبره، دون دفع كلفة إعادة الضبط:

import logging

log = logging.getLogger(__name__)

while True:
    wdt.feed()
    try:
        frame = csi0.snapshot()
        process(frame)
    except Exception:
        log.exception("frame loop iteration failed")

التقاط Exception (وليس BaseException) يُبقي KeyboardInterrupt و SystemExit يعملان، وهو ما يريده المطوّر المتصل عبر USB.

هذا النمط هو النصف البرمجي من الحيوية: يلتقط المراقب حالات التعليق، ويلتقط الغلاف حالات التعطّل، ويسجّل السجل ما التقطه أيٌّ منهما.

14.3.2.4. معرفة سبب حدوث الإقلاع

كل إعادة ضبط ناعمة وكل إعادة ضبط من المراقب تظهر في النهاية كإقلاع جديد. يسجّل مساعد تشخيصات وقت الإقلاع machine.reset_cause() في كل بدء بارد؛ وسطر reset cause هو ما يخبر الميدان ما إذا كان الاسترداد قد انطلق فعلًا أم أن الكاميرا تعيد تشغيل الطاقة بشكل طبيعي فقط.

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

14.3.2.5. نقطة بداية كاملة

ملف main.py يجمع المراقب، وإعداد التسجيل، وتشخيصات وقت الإقلاع، والغلاف معًا يبدو هكذا:

import logging
from machine import WDT

from app.logging_setup import setup_logging, log_boot_diagnostics

setup_logging('/sdcard/logs/app.log')
log_boot_diagnostics()

log = logging.getLogger(__name__)

wdt = WDT(timeout=10_000)

while True:
    wdt.feed()
    try:
        step()
    except Exception:
        log.exception("loop iteration failed")

step() هو عمل التطبيق لكل تكرار؛ أما بقية هذا الهيكل فلا يتغيّر بين المنتجات. التقوية هي مراقب واحد، وغلاف واحد، وإقلاع مُسجَّل في كل بدء بارد -- ليس كثيرًا من الكود، وهو الفرق بين كاميرا تتعافى من تلقاء نفسها وأخرى تحتاج إلى مكالمة خدمة.