10.13. העלאת פריימים שהופעלו על ידי טריגר לענן¶
כעת, כשתנועה מפעילה את הטריגר, המצלמה מאירה את לוח הבקרה. זה מספיק לשימוש חי, אך הבעלים רוצה גם ארכיון קבוע של כל פריים שהופעל, השמור היכנשהו מחוץ למצלמה. זוהי קריאת HTTP יוצאת – המצלמה פועלת כ-לקוח.
10.13.1. המצלמה כלקוח¶
המודול requests הוא לקוח ה-HTTP היוצא של המצלמה. ממשק המשתמש שלו הוא העתק מכוון של requests של CPython – אותן פונקציות מודול הקרויות על שם הפעלים, אותם ארגומנטי מילות מפתח files=, json=, headers=, auth=. אם ביצעת קריאות HTTP מ-CPython אתה כבר מכיר את ה-API:
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={...} הוא dict ישיר שנשלח ככותרות הבקשה – כאן, אסימון bearer במיקום ה-Authorization הסטנדרטי. ספק הארכיון מתעד באיזה פורמט אסימון הוא רוצה; הדוגמה היא הצורה הנפוצה ביותר.
10.13.2. חיבור למזהה התנועה¶
הקורוטינה של מזהה התנועה שהוצגה קודם כבר פועלת על כל פריים חדש ומופעלת כש-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. מצבי כשל¶
קוד רשת נכשל. ייתכן שהמצלמה לא מחוברת, ייתכן שהארכיון מושבת, ייתכן שאסימון ה-bearer פג. הקטגוריות שכדאי לתפוס:
OSError– לא ניתן היה לפתוח את חיבור ה-TCP או שהוא נסגר באמצע ההעברה. כשל DNS, אין נתיב, איפוס חיבור.requestsמעלה בדיוק את החריגה הזו.status_code >= 400– השרת קיבל את הבקשה ודחה אותה. 401 עבור אסימון שפג תוקפו, 403 עבור אסימון שבוטל, 413 עבור גוף גדול מדי, 5xx עבור ארכיון שאינו תקין.פסק זמן שקט –
requestsמשתמש בפסק זמן ברירת מחדל של socket (כמה שניות); מעבר לכך הוא מעלהOSErrorעםerrno.ETIMEDOUT.
עבור ארכיון שבאמת חשוב, היית מציב פריימים שנדחו בתור אל /sdcard/pending/ ומנסה שוב בלולאה איטית יותר – זה עוד כמה שורות לכל מקרה, נוסף על מה שמוצג.
10.13.4. מה ש-requests לא עושה¶
פורט ה-MicroPython קטן במכוון. כמה דברים ש-requests של CPython עושה ושפורט זה אינו עושה:
איגום חיבורים. כל קריאה פותחת חיבור TCP חדש.
ניסיונות חוזרים אוטומטיים בשגיאות חולפות. עטוף את הקריאה בעצמך.
תגובות בזרימה.
r.contentנקרא במלואו ל-RAM; אין מקבילה ל-stream=True.פירוק דחיסה אוטומטי של תגובות gzipped. הגדר את הכותרת
Accept-Encodingבמפורש רק אם השרת מוגדר לכך.
ראה requests — לקוח HTTP עבור רשימת המתודות המלאה ומה נכלל בתחום ומה לא.
HTTPS עובד מהקופסה – סכמת ה-URL מניעה זאת, והקשר ה-SSL ברירת המחדל נוצר תוך כדי תנועה. לאימות תעודת הארכיון מול חבילת CA שטענת אל המצלמה, ראה את החלק as a client של אימות שרת ציבורי (המצלמה כלקוח).
האפליקציה נשלחה במלואה: תצוגה מקדימה חיה, זיהוי תנועה, לוח בקרה עם התחברות, HTTPS, CORS/CSRF, ארכיון ענן.