10.13. رفع الإطارات المُفعَّلة إلى السحابة

عند اكتشاف الحركة تُضيء الكاميرا الآن لوحة المعلومات. هذا كافٍ للاستخدام المباشر، لكن المالك يريد أيضًا أرشيفًا دائمًا لكل إطار مُفعَّل، مُخزَّنًا في مكان خارج الكاميرا. وهذا يتطلب استدعاء HTTP صادر -- حيث تعمل الكاميرا بوصفها عميلًا.

10.13.1. الكاميرا بوصفها عميلًا

تُمثِّل وحدة requests عميل HTTP الصادر الخاص بالكاميرا. وقد صُمِّمت واجهتها لتكون نسخة مطابقة عن قصد من requests في CPython -- نفس دوال الوحدة المُسمَّاة بأفعال، ونفس الوسائط المفتاحية files= و json= و headers= و auth=. فإن سبق لك إجراء استدعاءات HTTP من CPython فأنت تعرف هذه الواجهة بالفعل:

import requests
import io

ARCHIVE_URL = 'https://api.backyard-cloud.com/frames'
ARCHIVE_TOKEN = load_archive_token()

async def archive_frame(jpeg, ts):
    try:
        r = requests.post(
            ARCHIVE_URL,
            files={'image': (
                'frame-{}.jpg'.format(ts),
                io.BytesIO(jpeg),
            )},
            headers={'Authorization': 'Bearer ' + ARCHIVE_TOKEN},
        )
    except OSError as e:
        print('upload failed:', e)
        return False
    if r.status_code >= 400:
        print('archive rejected:', r.status_code, r.reason)
        return False
    return True

تفتح requests.post() اتصال TCP، وترسل الطلب، وتُعيد كائن Response يحمل status_code و reason و headers و content و json() وبقية الخصائص المألوفة.

تبني files={...} جسمًا من نوع multipart/form-data. والقيمة عبارة عن صفّ (filename, file-like)؛ وتقرأ requests.post() الكائن الشبيه بالملف على دفعات حتى لا يضطر ملف JPEG بأكمله إلى إعادة تخزينه في سلسلة نصية أولًا. ويُغلِّف io.BytesIO بايتات JPEG الموجودة في الذاكرة بالفعل بحيث تُتيح واجهة قراءة على هيئة ملف.

إن headers={...} قاموس مباشر يُرسَل بوصفه ترويسات الطلب -- وهنا، رمز حامل (bearer token) في موضع Authorization القياسي. ويوثِّق مزوِّد الأرشفة صيغة الرمز التي يريدها؛ والمثال هنا هو الصيغة الأكثر شيوعًا.

10.13.2. ربطه بكاشف الحركة

إن دالة التعاون (coroutine) لكاشف الحركة المُقدَّمة سابقًا تعمل بالفعل على كل إطار جديد وتُفعَّل عندما يكون change > state['threshold']. أضف عملية الرفع هناك، لكن شغِّلها بوصفها مهمة في الخلفية حتى لا يتوقف الكاشف عن المراقبة أثناء جريان عملية الرفع:

async def motion_detector():
    global last_motion
    prev = None
    while True:
        await new_frame.wait()
        change = compute_change(prev, latest_jpeg)
        if change > state['threshold']:
            state['trigger_count'] += 1
            ts = int(time.time())
            last_motion = {'ts': ts,
                           'count': state['trigger_count'],
                           'change': change}
            motion_event.set()
            asyncio.create_task(archive_frame(latest_jpeg, ts))
        prev = latest_jpeg
        await asyncio.sleep_ms(50)

تجدول asyncio.create_task() دالة تعاون الرفع وتعود فورًا. ويستمر الكاشف في التقاط الإطارات؛ وتجري عملية الرفع إلى جانبه؛ ولا تتوقف الكاميرا أبدًا.

10.13.3. أنماط الفشل

تفشل شيفرة الشبكة أحيانًا. فقد تكون الكاميرا غير متصلة، وقد يكون الأرشيف معطلًا، وقد يكون الرمز الحامل قد انتهت صلاحيته. وفيما يلي الفئات الجديرة بالالتقاط:

  • OSError -- تعذّر فتح اتصال TCP أو أُغلق في منتصف النقل. مثل فشل DNS، أو غياب المسار، أو إعادة تعيين الاتصال. وتطلق requests هذا الاستثناء بالضبط.

  • status_code >= 400 -- استقبل الخادم الطلب ورفضه. مثل 401 لرمز منتهي الصلاحية، و403 لرمز مُلغى، و413 لجسم كبير للغاية، و5xx عندما يكون الأرشيف في حالة غير سليمة.

  • المهلة الصامتة -- تستخدم requests مهلة افتراضية للمقبس (بضع ثوانٍ)؛ وبعد ذلك تطلق OSError مع errno.ETIMEDOUT.

بالنسبة إلى أرشيف يهمّك فعلًا، يمكنك وضع الإطارات المرفوضة في طابور إلى /sdcard/pending/ وإعادة المحاولة في حلقة أبطأ -- وهذا يتطلب بضعة أسطر إضافية لكل حالة، فوق ما هو معروض.

10.13.4. ما لا تفعله requests

إن نسخة MicroPython صغيرة عن قصد. وفيما يلي بضعة أشياء تفعلها requests في CPython ولا تفعلها هذه النسخة:

  • تجميع الاتصالات (Connection pooling). فكل استدعاء يفتح اتصال TCP جديدًا.

  • إعادة المحاولة التلقائية عند الأخطاء العابرة. غلِّف الاستدعاء بنفسك.

  • تدفق الاستجابات. تُقرأ r.content كاملة في ذاكرة RAM؛ ولا يوجد ما يكافئ stream=True.

  • فك الضغط التلقائي للاستجابات المضغوطة بصيغة gzip. اضبط ترويسة Accept-Encoding صراحةً فقط إذا كان الخادم مُهيَّأً لذلك.

راجع requests --- عميل HTTP للاطلاع على القائمة الكاملة للدوال وما يدخل ضمن النطاق وما يخرج منه.

يعمل HTTPS مباشرةً دون إعداد -- إذ يحدده مخطط عنوان URL، ويُنشأ سياق SSL الافتراضي في الحال. ولأجل التحقق من شهادة الأرشيف مقابل حزمة CA حمّلتها على الكاميرا، راجع قسم الكاميرا بوصفها عميلًا في التحقق من خادم عام (الكاميرا كعميل).

اكتمل التطبيق بالكامل: معاينة مباشرة، وكشف الحركة، ولوحة معلومات مع تسجيل دخول، وHTTPS، وCORS/CSRF، وأرشيف سحابي.