10.13. 將觸發的影格上傳至雲端

現在當偵測到動作時,相機便會點亮儀表板。對於即時使用而言這已經足夠,但擁有者還想要一份永久封存檔,將每一個觸發的影格儲存在相機之外的某處。這需要一個對外的 HTTP 呼叫——相機此時扮演 用戶端(client)的角色。

10.13.1. 相機作為用戶端

requests 模組是相機的對外 HTTP 用戶端。它的介面刻意比照 CPython 的 requests——相同的以動詞命名的模組函式,相同的 files=json=headers=auth= 關鍵字引數。如果你曾經從 CPython 發出過 HTTP 呼叫,那你已經熟悉這套 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_codereasonheaderscontentjson() 以及其餘你熟悉的屬性。

files={...} 會建構一個 multipart/form-data 主體。其值是一個 (filename, file-like) 元組;requests.post() 會分段讀取這個類檔案物件,這樣整個 JPEG 就不必先重新緩衝成一個字串。io.BytesIO 會包裹已經位於記憶體中的 JPEG 位元組,使它們對外提供如同檔案一般的讀取介面。

headers={...} 是一個直接作為請求標頭送出的 dict——這裡是放在標準 Authorization 位置的 bearer token。封存服務供應商會說明他們需要哪種 token 格式;此範例採用的是最常見的形式。

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 token 可能已過期。值得攔截的類別有:

  • OSError——TCP 連線無法開啟,或在傳輸途中被關閉。DNS 失敗、無路由、連線重設。requests 拋出的正是這個例外。

  • status_code >= 400——伺服器收到請求但拒絕了它。401 代表 token 過期、403 代表 token 已撤銷、413 代表主體過大、5xx 代表封存服務不健康。

  • 無聲的逾時——requests 使用預設的 socket 逾時(數秒);超過之後它會拋出帶有 errno.ETIMEDOUTOSError

對於真正重要的封存,你會把遭拒的影格排入 /sdcard/pending/ 佇列,並在較慢的迴圈上重試——這在前面所示的基礎上,每種情況只多出寥寥幾行。

10.13.4. requests 不做的事

MicroPython 移植版刻意保持精簡。以下是 CPython 的 requests 會做、但這個版本不做的幾件事:

  • 連線池。每次呼叫都會開啟一個新的 TCP 連線。

  • 暫時性錯誤時自動重試。請自行包裹這個呼叫。

  • 串流回應。r.content 會被完整讀入 RAM;沒有等同於 stream=True 的功能。

  • 對 gzip 壓縮回應的自動解壓縮。只有在伺服器有相應設定時,才明確設定 Accept-Encoding 標頭。

完整的方法清單以及哪些功能在範圍內/範圍外,請參閱 requests --- HTTP 用戶端

HTTPS 開箱即用——由 URL 的協定方案(scheme)驅動,預設的 SSL context 會即時建立。若要根據你載入相機的 CA 套件來驗證封存服務的憑證,請參閱 驗證公開伺服器(相機作為用戶端) 中的 作為用戶端(as a client)一節。

這個應用程式已完整交付:即時預覽、動作偵測、附登入功能的儀表板、HTTPS、CORS/CSRF,以及雲端封存。