10.13. การอัปโหลดเฟรมที่ถูกทริกเกอร์ไปยังคลาวด์

เมื่อมีการเคลื่อนไหว กล้องจะแสดงผลบนแดชบอร์ดทันที ซึ่งเพียงพอสำหรับการใช้งานแบบเรียลไทม์ แต่เจ้าของยังต้องการเก็บถาวรทุกเฟรมที่ถูกทริกเกอร์ไว้ถาวร โดยจัดเก็บไว้นอกตัวกล้อง ซึ่งต้องใช้การเรียก HTTP แบบส่งออก — กล้องทำหน้าที่เป็น client

10.13.1. กล้องในฐานะ client

โมดูล requests คือ HTTP client สำหรับการส่งออกของกล้อง โดยมี API ที่ออกแบบให้เหมือนกับ requests ของ CPython — ฟังก์ชันในโมดูลที่ตั้งชื่อตาม HTTP verb, อาร์กิวเมนต์คีย์เวิร์ด 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={...} สร้าง body แบบ multipart/form-data โดย value คือ tuple แบบ (filename, file-like) ซึ่ง requests.post() จะอ่านข้อมูลจาก file-like ทีละส่วน เพื่อไม่ต้องบัฟเฟอร์ JPEG ทั้งหมดเป็น string ก่อน io.BytesIO ครอบข้อมูล JPEG ที่อยู่ในหน่วยความจำแล้ว เพื่อให้เปิดเป็น interface แบบ file ได้

headers={...} คือ dict ธรรมดาที่ส่งเป็น request header — ในที่นี้คือ bearer token ในตำแหน่ง Authorization มาตรฐาน ผู้ให้บริการเก็บถาวรจะระบุรูปแบบ token ที่ต้องการ ตัวอย่างนี้เป็นรูปแบบที่พบบ่อยที่สุด

10.13.2. เชื่อมต่อกับระบบตรวจจับการเคลื่อนไหว

coroutine ของระบบตรวจจับการเคลื่อนไหวที่แนะนำไว้ก่อนหน้านี้ทำงานทุกเฟรมใหม่ และทริกเกอร์เมื่อ change > state['threshold'] ให้เพิ่มการอัปโหลดตรงนั้น แต่เรียกใช้เป็น background task เพื่อไม่ให้ระบบตรวจจับหยุดเฝ้าดูระหว่างที่กำลังอัปโหลด:

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() กำหนดเวลา coroutine การอัปโหลดและกลับมาทันที ระบบตรวจจับยังคงดึงเฟรมต่อไป การอัปโหลดทำงานควบคู่กัน กล้องไม่ค้างเลย

10.13.3. รูปแบบความล้มเหลว

โค้ดเครือข่ายอาจล้มเหลวได้ กล้องอาจออฟไลน์ ระบบเก็บถาวรอาจล่ม bearer token อาจหมดอายุ ประเภทข้อผิดพลาดที่ควรจับ:

  • OSError — ไม่สามารถเปิดการเชื่อมต่อ TCP หรือถูกตัดระหว่างการถ่ายโอน เช่น DNS ล้มเหลว ไม่มีเส้นทาง หรือ connection reset โดย requests จะ raise exception นี้โดยตรง

  • status_code >= 400 — เซิร์ฟเวอร์รับคำขอแล้วแต่ปฏิเสธ เช่น 401 สำหรับ token หมดอายุ, 403 สำหรับ token ที่ถูกเพิกถอน, 413 สำหรับ body ที่ใหญ่เกินไป, 5xx สำหรับระบบเก็บถาวรที่ขัดข้อง

  • Silent timeout — requests ใช้ socket timeout ค่าเริ่มต้น (สองสามวินาที) เมื่อเกินจะ raise OSError พร้อมกับ errno.ETIMEDOUT

สำหรับระบบเก็บถาวรที่สำคัญจริงๆ คุณควรจัดคิวเฟรมที่ถูกปฏิเสธไปที่ /sdcard/pending/ และลองใหม่ในลูปที่ช้ากว่า — ซึ่งต้องเพิ่มโค้ดอีกสองสามบรรทัดต่อกรณี นอกเหนือจากที่แสดงไว้

10.13.4. สิ่งที่ requests ไม่รองรับ

MicroPython port ถูกออกแบบให้เล็กตั้งใจ สิ่งที่ requests ของ CPython ทำได้แต่ตัวนี้ไม่รองรับ:

  • Connection pooling — ทุกการเรียกจะเปิดการเชื่อมต่อ TCP ใหม่

  • การลองใหม่อัตโนมัติเมื่อเกิดข้อผิดพลาดชั่วคราว ให้ครอบการเรียกด้วยตนเอง

  • Streaming responses — r.content ถูกอ่านลง RAM ทั้งหมด ไม่มีตัวเลือก stream=True

  • การแตกไฟล์ gzip อัตโนมัติสำหรับ response ที่บีบอัด ให้ตั้งค่า header Accept-Encoding อย่างชัดเจนเฉพาะเมื่อเซิร์ฟเวอร์รองรับเท่านั้น

ดู requests --- HTTP client สำหรับรายการเมธอดทั้งหมดและขอบเขตการรองรับ

HTTPS ทำงานได้ทันที — URL scheme เป็นตัวกำหนด และ SSL context ค่าเริ่มต้นจะถูกสร้างขึ้นทันที หากต้องการตรวจสอบใบรับรองของระบบเก็บถาวรกับ CA bundle ที่โหลดไว้บนกล้อง ดูส่วน as a client ใน การตรวจสอบเซิร์ฟเวอร์สาธารณะ (กล้องทำหน้าที่เป็น client)

แอปพลิเคชันพร้อมใช้งานสมบูรณ์แล้ว: ดูตัวอย่างแบบสด ตรวจจับการเคลื่อนไหว แดชบอร์ดพร้อมระบบล็อกอิน HTTPS, CORS/CSRF และระบบเก็บถาวรบนคลาวด์