本文将为您介绍设备如何通过调用SDK获取物联网平台获取标准UTC时间,继而解析时间。

背景说明

阿里云IoT平台提供了网络对时服务(Network Time Protocol,NTP),让设备可以基于MQTT协议从物联网平台获取标准UTC时间。

时间同步

如上图流程所示,使用C-SDK中的NTP功能,可从云平台查询到当前的UTC时间(自1970年1月1日零时至今过去的毫秒数)。

API列表

以下是完整的NTP部分API列表及简要说明 (详见components/ntp/aiot_ntp_api.h)。

接口名 说明
aiot_ntp_init 初始化NTP实例并设置默认参数。
aiot_ntp_setopt 配置NTP实例,详见ntp选项配置说明
aiot_ntp_deinit 释放NTP实例句柄的资源。
aiot_ntp_send_request 向MQTT服务器发送NTP查询请求。

API使用概述

NTP模块用于从阿里云物联网平台上获取UTC时间,API的使用流程如下:

  1. 首先参考 aiot_mqtt_api.h 的说明,保证成功建立与物联网平台的MQTT连接。
  2. 调用 aiot_ntp_init 初始化NTP会话,获取会话句柄。
  3. 调用 aiot_ntp_setopt 配置NTP会话的参数,常用配置项见 aiot_ntp_setopt 的说明。
  4. 调用 aiot_ntp_send_request 发送NTP请求。
  5. 收到的UTC时间经SDK处理后会调用由 aiot_ntp_setopt 配置的 AIOT_NTPOPT_RECV_HANDLER 回调函数demo_ntp_recv_handler(),通知用户当前的时间。
    说明demos/ntp_posix_demo.c 的demo_ntp_recv_handler()中可以看出结构体 aiot_ntp_recv_t{} 中包含当前时区下,年月日时分秒的数值,可在这里把它们解析储存起来。

下面将您详细介绍API的使用:

  • aiot_ntp_setopt

    常见的配置项如下:

    • AIOT_NTPOPT_MQTT_HANDLE:已建立连接的MQTT会话句柄。
    • AIOT_NTPOPT_TIME_ZONE:时区设置,SDK会将收到的UTC时间按配置的时区进行转换。
    • AIOT_NTPOPT_RECV_HANDLER:时间数据接收回调函数,SDK将UTC时间转换完成后,通过此回调函数输出。
  • aiot_ntp_send_request

    发送NTP请求,然后SDK会调用通过 aiot_ntp_setopt 配置的 AIOT_NTPOPT_RECV_HANDLER 回调函数,通知用户当前的时间。

例程讲解

现对照demos/ntp_posix_demo.c例程,分步骤讲解如何使用API。

时间同步步骤

这个例程适用于Linux这类支持pthread的POSIX设备,它演示了用SDK配置MQTT参数并建立连接,之后创建2个线程。

说明
  • 一个线程用于保活长连接。
  • 一个线程用于接收消息,并在有消息到达时进入默认的数据回调,在连接状态变化时进入事件回调。

接着在MQTT连接上发送NTP查询请求,如果云平台的回应报文到达,从接收线程会调用NTP消息处理的回调函数,把对时后的本地时间打印出来。

需要用户关注或修改的部分,已经用 TODO 在注释中标明。

  1. 设置设备证书

    例程使用的设备证书是公用的,所以应用于实际业务时,请替换如下的TODO部分,传入用户自己真实的设备证书。

    /* TODO: 替换为自己设备的设备证书 */
    char *product_key       = "a13FNXXXXXX";
    char *device_name       = "ntp_basic_demo";
    char *device_secret     = "ohr94yMocVYy1ldknvAkkezKkGXXXXXX";
  2. 进入程序入口,给SDK配置全局的底层依赖和日志回调

    底层依赖描述了硬件平台的资源使用方式,比如怎样获取时钟,分配内存等,日志回调是用户的函数,SDK有log输出的时候会进入这个函数。

    int main(int argc, char *argv[])
    {
        ...
        ...
        /* 配置SDK的底层依赖 */
        aiot_sysdep_set_portfile(&g_aiot_sysdep_portfile);
        /* 配置SDK的日志输出 */
        aiot_state_set_logcb(demo_state_logcb);
  3. 给MQTT会话配置参数并建立连接

    这些连接参数包括如何建立TLS连接、服务器地址、连接后的数据及事件回调函数等,然后用 aiot_mqtt_connect() 建立连接。

    int32_t demo_mqtt_start(void **handle)
    {
        ...
        ...
        /* 创建1个MQTT客户端实例并内部初始化默认参数 */
        mqtt_handle = aiot_mqtt_init();
        ...
        /* 与服务器建立MQTT连接 */
        res = aiot_mqtt_connect(mqtt_handle);

    以下代码创建1个子线程,专门用于保活设备与云平台之间的长连接。

    /* 创建一个单独的线程, 专用于执行aiot_mqtt_process, 它会自动发送心跳保活, 以及重发QoS1的未应答报文 */
        g_mqtt_process_thread_running = 1;
        res = pthread_create(&g_mqtt_process_thread, NULL, demo_mqtt_process_thread, mqtt_handle);

    以下代码创建1个子线程,专门用于接收从云平台推送下来的MQTT消息。

    /* 创建一个单独的线程用于执行aiot_mqtt_recv, 它会循环收取服务器下发的MQTT消息, 并在断线时自动重连 */
        g_mqtt_recv_thread_running = 1;
        res = pthread_create(&g_mqtt_recv_thread, NULL, demo_mqtt_recv_thread, mqtt_handle);
  4. 创建NTP会话实例并设置回应消息处理回调

    NTP服务的请求和应答是在MQTT长连接上进行的,以下代码设置了请求发送后,如果设备收到了云平台的NTP回应消息进入哪个用户回调函数。

    /* 创建1个ntp客户端实例并内部初始化默认参数 */
        ntp_handle = aiot_ntp_init();
        if (ntp_handle == NULL) {
            demo_mqtt_stop(&mqtt_handle);
            printf("aiot_ntp_init failed\n");
            return -1;
        }
    
        aiot_ntp_setopt(ntp_handle, AIOT_NTPOPT_MQTT_HANDLE, mqtt_handle);
        aiot_ntp_setopt(ntp_handle, AIOT_NTPOPT_TIME_ZONE, (int8_t *)&time_zone);
        /* TODO: NTP消息回应从云端到达设备时, 会进入此处设置的回调函数 */
        aiot_ntp_setopt(ntp_handle, AIOT_NTPOPT_RECV_HANDLER, (void *)demo_ntp_recv_handler);
        aiot_ntp_setopt(ntp_handle, AIOT_NTPOPT_EVENT_HANDLER, (void *)demo_ntp_event_handler);
  5. 向服务器发送服务请求

    使用aiot_ntp_send_request()接口,以上面设置的参数,向NTP服务器发起服务请求。

    /* 发送NTP查询请求给云平台 */
        res = aiot_ntp_send_request(ntp_handle);
        if (res < STATE_SUCCESS) {
            aiot_ntp_deinit(&ntp_handle);
            demo_mqtt_stop(&mqtt_handle);
            return -1;
        }

    以下日志就对应被发送的服务请求。

    [1581503394.999][LK-0309] pub: /ext/ntp/a13FNXXXXXX/ntp_basic_demo/request
    
    [LK-030A] > 7B 22 64 65 76 69 63 65  53 65 6E 64 54 69 6D 65 | {"deviceSendTime
    [LK-030A] > 22 3A 22 31 35 38 31 35  30 33 33 39 34 39 39 39 | ":"1581503394999
    [LK-030A] > 22 7D                                            | "}
  6. 从接收线程得到服务应答

    demo_mqtt_start() 中,已开启了1个专门的接收消息线程 demo_mqtt_recv_thread,它会永无休止的调用 aiot_mqtt_recv() 来收取消息。

    如果得到NTP的服务应答,那么会根据上面 AIOT_NTPOPT_RECV_HANDLER 选项的设置,进入用户侧的回调函数(以下只是把时间解析和打印出来)。

    /* TODO: 数据处理回调, 当SDK从网络上收到ntp消息时被调用 */
    void demo_ntp_recv_handler(void *handle, const aiot_ntp_recv_t *packet, void *userdata)
    {
        switch (packet->type) {
            /* TODO: 结构体 aiot_ntp_recv_t{} 中包含当前时区下, 年月日时分秒的数值, 可在这里把它们解析储存起来 */
            case AIOT_NTPRECV_LOCAL_TIME: {
                printf("local time: %llu, %02d/%02d/%02d-%02d:%02d:%02d:%d\n",
                       (long long unsigned int)
                       packet->data.local_time.timestamp,
                       packet->data.local_time.year, packet->data.local_time.mon, packet->data.local_time.day,
                       packet->data.local_time.hour, packet->data.local_time.min,
                       packet->data.local_time.sec, packet->data.local_time.msec);
            }
            break;
        ...
        ...

    以下日志对应的是被成功接收到的服务应答和例程的解析打印。

    [1581503395.011][LK-0309] pub: /ext/ntp/a13FNXXXXXX/ntp_basic_demo/response
    
    [LK-030A] < 7B 22 64 65 76 69 63 65  53 65 6E 64 54 69 6D 65 | {"deviceSendTime
    [LK-030A] < 22 3A 22 31 35 38 31 35  30 33 33 39 34 39 39 39 | ":"1581503394999
    [LK-030A] < 22 2C 22 73 65 72 76 65  72 53 65 6E 64 54 69 6D | ","serverSendTim
    [LK-030A] < 65 22 3A 22 31 35 38 31  35 30 33 33 39 35 30 30 | e":"158150339500
    [LK-030A] < 30 22 2C 22 73 65 72 76  65 72 52 65 63 76 54 69 | 0","serverRecvTi
    [LK-030A] < 6D 65 22 3A 22 31 35 38  31 35 30 33 33 39 35 30 | me":"15815033950
    [LK-030A] < 30 30 22 7D                                      | 00"}
    
    local time: 1581535795009, 2020/02/12-18:29:55:9

    文本 2020/02/12-18:29:55:9 就是解析打印的结果,代表:北京时间,2020年2月12日,晚上6点29分55秒9毫秒。