9.16. 时间与 NTP

刚上电的摄像头并不知道现在是什么时间。板载时钟从某个任意时刻开始(在大多数板子上是 1970-01-01),并从那里一直向前计数,直到有东西告诉它正确的时间为止。NTP——即网络时间协议(Network Time Protocol)——就是摄像头向网络询问真实世界挂钟时间,并根据回答设置自身时钟的方式。

9.16.1. 摄像头为什么需要知道时间

对于很多脚本来说,摄像头的时钟无关紧要——一个帧捕获循环并不关心今天是几号。但对于少数常见的场景,它却非常重要:

  • 日志或上传数据中的时间戳。 如果所有条目都显示 1970-01-01,事后就很难理解它们的含义了。

  • 定时任务。 “在 03:00 运行”需要摄像头知道 03:00 是什么时候。

9.16.2. NTP 的作用

NTP 是一项小型的公共服务:由一组服务器组成的网络,通过一次 UDP 交互来回答“现在几点了?”。摄像头向已知的 NTP 服务器发送一个简短的请求;服务器以精确的时间戳回复(对于任何常见的公共服务器,精度可达几毫秒);摄像头便据此设置自己的时钟。摄像头默认使用的服务器是 pool.ntp.org,这是一个全球负载均衡的服务器池,正是为这类客户端设计的。

9.16.3. Python API:ntptime

MicroPython 将该协议封装在一次调用中。常见的做法是先把链路建立起来,然后再向 NTP 询问时间:

import network
import ntptime
import time

wlan = network.WLAN(network.WLAN.IF_STA)
wlan.active(True)
wlan.connect("my-network", "my-password")

while not wlan.isconnected():
    time.sleep_ms(100)

ntptime.settime()                 # cam's clock is now UTC
print(time.localtime())

ntptime.settime() 返回之后,板载实时时钟和 time.localtime() 都会反映出当前的 UTC 时间。有两个旋钮可以调整默认值:

  • ntptime.host 是要查询的服务器名称。在调用 settime() 之前覆盖它(ntptime.host = "time.google.com"),即可指向另一台服务器。

  • ntptime.timeout 是放弃前等待回复的秒数;默认值很短。

9.16.4. 何时调用它

  • 在网络链路建立之后。 NTP 运行在 UDP 之上,而 UDP 又依赖于已建立的 IP 配置。要先等待 isconnected() 返回 True

  • 对长时间运行的摄像头要定期调用。 板载时钟会在数小时和数天内产生漂移。每天或每周调用一次 settime() 可以让它保持准确。

9.16.5. 时区

NTP 返回的是 UTC 时间。MicroPython 并不自带时区数据库,因此将 UTC 转换为本地时间是脚本的工作。通常的做法是为部署所在的时区使用一个固定的偏移量:

import time

offset = -5 * 3600                  # hours -> seconds, US Eastern
local = time.localtime(time.time() + offset)
print(local)

这种方法不会处理夏令时、闰秒以及历史上的时区变更。对于大多数摄像头部署场景,固定偏移量已经足够;如果某个脚本确实需要带夏令时的民用时间,请在服务器端进行转换。

9.16.6. 可能出错的地方

  • 还没有网络。 如果 ntptime.settime() 无法连接服务器,它会抛出 OSError。可能是链路尚未建立、名称解析失败、服务器不可达,或者在 ntptime.timeout 内没有收到回复。等链路稳定后再重试。

  • 强制门户(Captive portal)。 拦截 DNS 的 Wi-Fi 网络可能会用门户自己的 IP 来回答 NTP 服务器名称,而发往该地址的 NTP 请求只会返回乱七八糟的数据。摄像头会以为网络已连通,但时间设置要么失败,要么严重错误。请改用干净的网络,或硬编码一个 IP 地址。

  • 频繁访问公共池。 公共 NTP 池会对滥用的客户端进行限速。每小时一次绰绰有余;每分钟一次则会导致摄像头被封禁。

完整的 ntptime 参考请见 ntptime --- 简单的 NTP 客户端