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_code、reason、headers、content、json() 以及其餘你熟悉的屬性。
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.ETIMEDOUT的OSError。
對於真正重要的封存,你會把遭拒的影格排入 /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,以及雲端封存。