10.13. トリガーされたフレームをクラウドにアップロードする¶
動きが発生すると、カメラはダッシュボードを点灯させるようになりました。ライブ用途にはこれで十分ですが、所有者はトリガーされたすべてのフレームの恒久的なアーカイブを、カメラの外部のどこかに保存したいとも考えています。これは外向きのHTTP呼び出しであり、カメラがクライアントとして動作することになります。
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接続を開いてリクエストを送信し、status_code、reason、headers、content、json() といったおなじみのプロパティを備えた Response を返します。
files={...} は multipart/form-data ボディを構築します。値は (filename, file-like) のタプルです。requests.post() はファイルライクオブジェクトをチャンク単位で読み込むため、JPEG全体を最初に文字列へ再バッファリングする必要がありません。io.BytesIO はすでにメモリ上にあるJPEGバイト列をラップし、ファイルとして読み取れるインターフェースを公開します。
headers={...} はリクエストヘッダーとして送信される単純な辞書です。ここでは、標準の 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. 障害モード¶
ネットワークコードは失敗します。カメラがオフラインかもしれませんし、アーカイブがダウンしているかもしれませんし、ベアラートークンが失効しているかもしれません。捕捉する価値のあるカテゴリは次のとおりです。
OSError-- TCP接続を開けなかったか、転送の途中で閉じられました。DNS障害、ルートなし、接続リセットなどです。requestsはまさにこの例外を送出します。status_code >= 400-- サーバーはリクエストを受信しましたが、拒否しました。失効したトークンには401、取り消されたトークンには403、ボディが大きすぎる場合は413、アーカイブが不健全な場合は5xxです。サイレントタイムアウト --
requestsはデフォルトのソケットタイムアウト(数秒)を使用します。それを超えると、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スキームがそれを駆動し、デフォルトのSSLコンテキストはその場で作成されます。カメラに読み込んだCAバンドルに対してアーカイブの証明書を検証する方法については、公開サーバーの検証(クライアントとしてのカメラ) のクライアントとしてのセクションを参照してください。
アプリは完全に出荷可能な状態になりました。ライブプレビュー、動き検出、ログイン付きダッシュボード、HTTPS、CORS/CSRF、そしてクラウドアーカイブを備えています。