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 位置的一个 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使用一个默认的套接字超时(几秒钟);超过这个时间它会抛出带有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、云端存档。