物联网平台提供设备影子功能, 用于缓存设备上报的状态和云端下发的指令。设备上线后, 可基于MQTT通信, 主动拉取离线期间, 云端曾下发过的指令。本文将介绍如何完成设备影子功能的开发。

背景说明

设备影子示意图

如上图流程所示, 使用C-SDK中的shadow功能, 在建立了MQTT长连接之后, 设备可将对设备影子的操作上报到云端, 并接收云端下行的回应及主动更新。

  • 设备调用aiot_shadow_send向云端上报消息太过频繁时, 会被云端限流, 云端限流发生时, 设备端不会收到任何来自网络的通知, 只能人工在控制台页面查看。
  • 设备影子的JSON文件大小, JSON嵌套的层级数和JSON中的属性数量也都有一定限制。
说明
  • 设备影子是一个保存在服务器上的JSON文档, 用于存储设备上报状态(reported数据)和云上应用期望状态(desired数据)。
  • 可点击设备影子概览了解相关概念, 点击设备影子数据流了解网络通信协议。
  • 请查看云平台产品限制页面, 实时了解平台对设备影子当前限制的具体数值。

数据结构

SDK中的shadow功能将操作设备影子的消息内容都以结构体描述。

  • 所有上行消息的传递, 都以用户调用aiot_shadow_send()接口, 从设备发往云平台。
  • 所有上行消息的内容, 都以 aiot_shadow_msg_t 结构体存放, 它是aiot_shadow_send()接口的入参。
  • 所有下行消息的传递, 都以用户调用aiot_mqtt_recv()接口, 进而触发用户注册的 aiot_shadow_recv_handler_t 回调, 到达用户侧。
  • 所有下行消息的内容, 都以 aiot_shadow_recv_t 结构体存放, 它是上述回调函数的入参。

SDK中的设备影子模块不维护网络会话, 网络连接和数据收发由它所依赖MQTT模块提供。

因此, 用户在创建设备影子实例后, 应正确地为它配置MQTT实例句柄, 在进行设备影子收发前, 也应确保MQTT已经建连成功。

API列表

相关定义位于文件component/shadow/aiot_shadow_api.h中。

接口名 说明
aiot_shadow_init 初始化shadow会话实例。
aiot_shadow_setopt 设置shadow会话参数, 点击设备影子详细设置参数了解所有参数。
aiot_shadow_send 发送一条shadow消息到物联网平台, 消息类型和消息内容由msg参数决定。
aiot_shadow_deinit 销毁shadow实例, 释放资源。

API使用概述

请按照以下流程使用API。

  1. 在使用设备影子模块前, 用户应首先创建好一个MQTT实例。
  2. 调用aiot_shadow_init创建一个设备影子实例, 保存实例句柄。
  3. 调用aiot_shadow_setopt配置AIOT_SHADOWOPT_MQTT_HANDLE选项以设置MQTT句柄, 此选项为强制配置选项。
  4. 调用aiot_shadow_setopt配置AIOT_SHADOWOPT_RECV_HANDLERAIOT_SHADOWOPT_USER_DATA选项以注册数据接收回调函数和用户上下文数据指针。
  5. 在使用aiot_shadow_send发送消息前, 应先完成MQTT实例的建连。
  6. 调用aiot_shadow_send发送不同类型的消息到云平台, 在注册的回调函数中处理各种类型的云平台下行消息。

shadow模块通过调用aiot_shadow_send发送消息到云平台, 云平台下发给设备的消息通过调用aiot_shadow_setopt注册的回调函数接收。

下面为您介绍如何通过API进行收发操作。

注意 建议用户先阅读线上文档设备影子JSON详解设备影子数据流, 了解设备影子的JSON数据格式和对设备影子的常见操作。
  1. 更新设备影子中的reported值

    以下代码演示了如何用aiot_shadow_send发送更新设备影子请求, 其中message的reported参数为包含键值对的JSON对象字符串, 如例程中的"{\"LightSwitch\":1}"

    说明
    • version为更新后的设备影子版本号, 更新后的version必须大于当前的version, 否则云端将返回错误。
    • 如果version设置为-1, 表示清空设备影子数据, 设备影子会接收设备端的请求, 并将设备影子版本更新为0
    /* 发送更新设备影子reported值的请求 */
    int32_t demo_update_shadow(void *shadow_handle, char *reported_data, int64_t version)
    {
        aiot_shadow_msg_t message;
        memset(&message, 0, sizeof(aiot_shadow_msg_t));
        message.type = AIOT_SHADOWMSG_UPDATE;
        message.data.update.reported = reported_data;
        message.data.update.version = version;
        return aiot_shadow_send(shadow_handle, &message);
    }
    /* 将reported中的LigthSwitch值更新为1 */
    demo_update_shadow(shadow_handle, "{\"LightSwitch\":1}", 0);

    在成功上报请求后, 设备将收到云端下发的应答消息, 消息类型为AIOT_SHADOWRECV_GENERIC_REPLY

    status字符串为success则更新成功, 否则更新失败, 用户可以在payload中查看到错误信息。

    void demo_shadow_recv_handler(void *shadow_handle, const aiot_shadow_recv_t *recv, void *userdata)
    {
        switch (recv->type) {
            /* 云端返回的通用应答 */
            case AIOT_SHADOWRECV_GENERIC_REPLY: {
                const aiot_shadow_recv_generic_reply_t *generic_reply = &recv->data.generic_reply;
                printf("payload = \"%.*s\", status = %s, timestamp = %ld\r\n",
                       generic_reply->payload_len,
                       generic_reply->payload,
                       generic_reply->status,
                       (unsigned long)generic_reply->timestamp);
            }
            ...
        }
    }
  2. 删除设备影子中的reported值

    以下代码演示了如何用aiot_shadow_send发送删除设备影子reported值的请求, 其中message的reported参数为包含要删除属性的JSON对象字符串。

    删除reported值时, 被删除属性对应的值必须指定为null, 如例程中的"{\"LightSwitch\":\"null\"}"

    version为更新后的设备影子版本号。

    说明 若要清除所有的reported属性, 则reported参数填写"\"null\""字符串。
    /* 发送删除设备影子中特定reported值的请求 */
    int32_t demo_delete_shadow_report(void *shadow_handle, char *reported, int64_t version)
    {
        aiot_shadow_msg_t message;
        memset(&message, 0, sizeof(aiot_shadow_msg_t));
        message.type = AIOT_SHADOWMSG_DELETE_REPORTED;
        message.data.delete_reporte.reported = reported;
        message.data.delete_reporte.version = version;
        return aiot_shadow_send(shadow_handle, &message);
    }
    /* 清除reported中的LigthSwitch值 */
    demo_delete_shadow_report(shadow_handle, "{\"LightSwitch\":\"null\"}", 2);

    在成功上报请求后, 设备将收到云端下发的应答消息, 消息类型为AIOT_SHADOWRECV_GENERIC_REPLY, 若status字符串为success则删除成功, 否则删除失败。

    ...
        /* 云端返回的通用应答 */
        case AIOT_SHADOWRECV_GENERIC_REPLY: {
            const aiot_shadow_recv_generic_reply_t *generic_reply = &recv->data.generic_reply;
            printf("payload = \"%.*s\", status = %s, timestamp = %ld\r\n",
                    generic_reply->payload_len,
                    generic_reply->payload,
                    generic_reply->status,
                    (unsigned long)generic_reply->timestamp);
        }
        ...
    }
  3. 清除设备影子中的desired值

    以下代码演示了如何用aiot_shadow_send发送清除设备影子desired值的请求, 其中version字段应填写清除后的设备影子版本号。

    /* 发送清除设备影子中所有desired值的请求 */
    int32_t demo_clean_shadow_desired(void *shadow_handle, int64_t version)
    {
        aiot_shadow_msg_t message;
        memset(&message, 0, sizeof(aiot_shadow_msg_t));
        message.type = AIOT_SHADOWMSG_CLEAN_DESIRED;
        message.data.clean_desired.version = version;
        return aiot_shadow_send(shadow_handle, &message);
    }
    res = demo_clean_shadow_desired(shadow_handle, 1);

    同样在成功上报请求后, 设备将收到云端下发的应答消息, 消息类型为AIOT_SHADOWRECV_GENERIC_REPLY, 若status字符串为success则清除成功, 否则清除失败。

  4. 获取设备影子

    以下代码演示了如何用aiot_shadow_send发送获取设备影子的请求, 其中message的reported参数为包含键值对的JSON对象字符串, 如例程中的"{\"LightSwitch\":1}"

    version为更新后的设备影子版本号, 更新后的version必须大于当前的version, 否则云端将返回错误。

    说明

    如果version设置为-1, 表示清空设备影子数据, 设备影子会接收设备端的请求, 并将设备影子版本更新为0。

    /* 发送获取设备影子的请求 */
    int32_t demo_get_shadow(void *shadow_handle)
    {
        aiot_shadow_msg_t message;
        memset(&message, 0, sizeof(aiot_shadow_msg_t));
        message.type = AIOT_SHADOWMSG_GET;
        return aiot_shadow_send(shadow_handle, &message);
    }
    demo_get_shadow(shadow_handle);

    在成功上报请求后, 设备将收到云端下发的应答的设备影子, 消息类型为AIOT_SHADOWRECV_GET_REPLY

    回调函数入参的payload参数存放设备影子的JSON结构体字符串, 其中version为设备影子的版本号。

    void demo_shadow_recv_handler(void *shadow_handle, const aiot_shadow_recv_t *recv, void *userdata)
    {
        switch (recv->type) {
            /* 当设备发送AIOT_SHADOWMSG_GET消息主动获取设备影子时, 会收到此消息 */
            case AIOT_SHADOWRECV_GET_REPLY: {
                const aiot_shadow_recv_get_reply_t *get_reply = &recv->data.get_reply;
                printf("payload = \"%.*s\", version = %ld\r\n",
                       get_reply->payload_len,
                       get_reply->payload,
                       (unsigned long)get_reply->version);
            }
            ...
        }
    }
  5. 云端主动下推设备影子

    当设备在线时, 若用户应用程序调用云端APIUpdateDeviceShadow主动更新设备影子, 设备会直接收到此消息。

    void demo_shadow_recv_handler(void *shadow_handle, const aiot_shadow_recv_t *recv, void *userdata)
    {
        switch (recv->type) {
            /* 当设备在线时, 若用户应用程序调用云端API主动更新设备影子, 设备便会收到此消息 */
            case AIOT_SHADOWRECV_CONTROL: {
                const aiot_shadow_recv_control_t *control = &recv->data.control;
                printf("payload = \"%.*s\", version = %ld\r\n",
                       control->payload_len,
                       control->payload,
                       (unsigned long)control->version);
            }
            ...
        }
    }

例程讲解

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

设备影子的开发流程

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

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

接着演示了在MQTT连接上进行设备影子更新,删除,拉取等操作,取消这些代码段落的注释即可观察运行效果。

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

  1. 设置设备证书

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

    /* TODO: 替换为自己设备的设备证书 */
        char *product_key       = "a18wPXXXXXX";
        char *device_name       = "data_model_basic_demo";
        char *device_secret     = "uwMTmVAMnGGHaAkqmeDY6cHxxBXXXXXX";
  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() 建立连接。

    /* 创建1个MQTT客户端实例并内部初始化默认参数 */
        mqtt_handle = aiot_mqtt_init();
        if (mqtt_handle == NULL) {
            printf("aiot_mqtt_init failed\n");
            return -1;
        }
        snprintf(host, 100, "%s.%s", product_key, url);
        /* 配置MQTT服务器地址 */
        aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_HOST, (void *)host);
        /* 配置MQTT服务器端口 */
        aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_PORT, (void *)&port);
        /* 配置设备productKey */
        aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_PRODUCT_KEY, (void *)product_key);
        /* 配置设备deviceName */
        aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_DEVICE_NAME, (void *)device_name);
        /* 配置设备deviceSecret */
        aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_DEVICE_SECRET, (void *)device_secret);
        /* 配置网络连接的安全凭据, 上面已经创建好了 */
        aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_NETWORK_CRED, (void *)&cred);
        /* 配置MQTT事件回调函数 */
        aiot_mqtt_setopt(mqtt_handle, AIOT_MQTTOPT_EVENT_HANDLER, (void *)demo_mqtt_event_handler);
  4. 创建设备影子会话实例并建立MQTT连接

    设备影子会话创建后, 可通过其会话句柄组装和解析标准的shadow报文, 这些报文依赖MQTT模块发送和接收, 因此要关联设备影子的会话和MQTT的会话, 并建立MQTT连接。

    /* 创建设备影子实例 */
        void *shadow_handle = aiot_shadow_init();
        if (shadow_handle == NULL) {
            printf("aiot_shadow_init failed");
            return -1;
        }
        /* 配置MQTT实例句柄 */
        aiot_shadow_setopt(shadow_handle, AIOT_SHADOWOPT_MQTT_HANDLE, mqtt_handle);
        /* 配置消息接收处理回调函数 */
        aiot_shadow_setopt(shadow_handle, AIOT_SHADOWOPT_RECV_HANDLER, (void *)aiot_shadow_recv_handler);
        /* 与服务器建立MQTT连接 */
        res = aiot_mqtt_connect(mqtt_handle);
  5. 数据收发

    请查看API使用概述部分。

  6. 观察日志

    以上各种和云端的交互代码都被注释掉, 用户取消注释后, 重新编译运行例程, 即可看到对设备影子的操作过程。

    [1582892115.987][LK-0313] MQTT user calls aiot_mqtt_connect api, connect
    [1582892115.987][LK-0317] aiot_shadow_test_case_01&a18wPXXXXXX
    [1582892115.987][LK-0318] 971C83F3DEEBED04EC9FD01FAD993002ACA6FDFA14D55B96B8FBE86FFEF06B42
    [1582892115.987][LK-0319] a18wPXXXXXX.aiot_shadow_test_case_01|timestamp=2524608000000,_ss=1,_v=sdk-c-4.0.0,securemode=2,signmethod=hmacsha256,ext=1,|
    establish mbedtls connection with server(host='a18wPXXXXXX.iot-as-mqtt.cn-shanghai.aliyuncs.com', port=[1883])
    success to establish mbedtls connection, fd = 6(cost 44763 bytes in total, max used 47675 bytes)
    [1582892116.190][LK-0313] MQTT connect success in 203 ms
    AIOT_MQTTEVT_CONNECT
    [1582892116.190][LK-0309] pub: /shadow/update/a18wPXXXXXX/aiot_shadow_test_case_01
    [LK-030A] > 7B 22 6D 65 74 68 6F 64  22 3A 22 67 65 74 22 7D | {"method":"get"}
    [1582892116.241][LK-0309] pub: /shadow/get/a18wPXXXXXX/aiot_shadow_test_case_01
    [LK-030A] < 7B 22 6D 65 74 68 6F 64  22 3A 22 72 65 70 6C 79 | {"method":"reply
    [LK-030A] < 22 2C 22 70 61 79 6C 6F  61 64 22 3A 7B 22 73 74 | ","payload":{"st
    [LK-030A] < 61 74 75 73 22 3A 22 73  75 63 63 65 73 73 22 2C | atus":"success",
    [LK-030A] < 22 73 74 61 74 65 22 3A  7B 22 72 65 70 6F 72 74 | "state":{"report
    [LK-030A] < 65 64 22 3A 7B 22 4C 69  67 68 74 53 77 69 74 63 | ed":{"LightSwitc
    [LK-030A] < 68 22 3A 31 7D 7D 2C 22  6D 65 74 61 64 61 74 61 | h":1}},"metadata
    [LK-030A] < 22 3A 7B 22 72 65 70 6F  72 74 65 64 22 3A 7B 22 | ":{"reported":{"
    [LK-030A] < 4C 69 67 68 74 53 77 69  74 63 68 22 3A 7B 22 74 | LightSwitch":{"t
    [LK-030A] < 69 6D 65 73 74 61 6D 70  22 3A 31 35 38 32 38 38 | imestamp":158288
    [LK-030A] < 37 36 34 34 7D 7D 7D 7D  2C 22 74 69 6D 65 73 74 | 7644}}}},"timest
    [LK-030A] < 61 6D 70 22 3A 31 35 38  32 38 39 32 31 31 36 2C | amp":1582892116,
    [LK-030A] < 22 76 65 72 73 69 6F 6E  22 3A 30 2C 22 63 6C 69 | "version":0,"cli
    [LK-030A] < 65 6E 74 54 6F 6B 65 6E  22 3A 22 6E 75 6C 6C 22 | entToken":"null"
    [LK-030A] < 7D                                               | }
    [1582892116.241][LK-1304] SHADOW recv generic_reply message
    demo_shadow_recv_handler, type = 2, productKey = a18wPXXXXXX, deviceName = aiot_shadow_test_case_01
    payload = "{"status":"success","state":{"reported":{"LightSwitch":1}},"metadata":{"reported":{"LightSwitch":{"timestamp":1582887644}}}}", version = 0
    [1582892118.193][LK-0309] pub: /shadow/update/a18wPXXXXXX/aiot_shadow_test_case_01
    [LK-030A] > 7B 22 6D 65 74 68 6F 64  22 3A 22 75 70 64 61 74 | {"method":"updat
    [LK-030A] > 65 22 2C 22 73 74 61 74  65 22 3A 7B 22 64 65 73 | e","state":{"des
    [LK-030A] > 69 72 65 64 22 3A 22 6E  75 6C 6C 22 7D 2C 22 76 | ired":"null"},"v
    [LK-030A] > 65 72 73 69 6F 6E 22 3A  31 7D                   | ersion":1}
    [1582892118.219][LK-0309] pub: /shadow/get/a18wPXXXXXX/aiot_shadow_test_case_01
    [LK-030A] < 7B 22 6D 65 74 68 6F 64  22 3A 22 72 65 70 6C 79 | {"method":"reply
    [LK-030A] < 22 2C 22 70 61 79 6C 6F  61 64 22 3A 7B 22 73 74 | ","payload":{"st
    [LK-030A] < 61 74 75 73 22 3A 22 73  75 63 63 65 73 73 22 2C | atus":"success",
    [LK-030A] < 22 76 65 72 73 69 6F 6E  22 3A 31 7D 2C 22 63 6C | "version":1},"cl
    [LK-030A] < 69 65 6E 74 54 6F 6B 65  6E 22 3A 22 6E 75 6C 6C | ientToken":"null
    [LK-030A] < 22 2C 22 74 69 6D 65 73  74 61 6D 70 22 3A 31 35 | ","timestamp":15
    [LK-030A] < 38 32 38 39 32 31 31 38  7D                      | 82892118}
    [1582892118.219][LK-1304] SHADOW recv get_reply message
    demo_shadow_recv_handler, type = 0, productKey = a18wPXXXXXX, deviceName = aiot_shadow_test_case_01
    payload = "{"status":"success","version":1}", status = success, timestamp = 1582892118
    [1582892120.198][LK-0309] pub: /shadow/update/a18wPXXXXXX/aiot_shadow_test_case_01
    [LK-030A] > 7B 22 6D 65 74 68 6F 64  22 3A 22 64 65 6C 65 74 | {"method":"delet
    [LK-030A] > 65 22 2C 22 73 74 61 74  65 22 3A 7B 22 72 65 70 | e","state":{"rep
    [LK-030A] > 6F 72 74 65 64 22 3A 7B  22 4C 69 67 68 74 53 77 | orted":{"LightSw
    [LK-030A] > 69 74 63 68 22 3A 22 6E  75 6C 6C 22 7D 7D 2C 22 | itch":"null"}},"
    [LK-030A] > 76 65 72 73 69 6F 6E 22  3A 32 7D                | version":2}
    [1582892120.242][LK-0309] pub: /shadow/get/a18wPXXXXXX/aiot_shadow_test_case_01
    [LK-030A] < 7B 22 6D 65 74 68 6F 64  22 3A 22 72 65 70 6C 79 | {"method":"reply
    [LK-030A] < 22 2C 22 70 61 79 6C 6F  61 64 22 3A 7B 22 73 74 | ","payload":{"st
    [LK-030A] < 61 74 75 73 22 3A 22 73  75 63 63 65 73 73 22 2C | atus":"success",
    [LK-030A] < 22 76 65 72 73 69 6F 6E  22 3A 32 7D 2C 22 63 6C | "version":2},"cl
    [LK-030A] < 69 65 6E 74 54 6F 6B 65  6E 22 3A 22 6E 75 6C 6C | ientToken":"null
    [LK-030A] < 22 2C 22 74 69 6D 65 73  74 61 6D 70 22 3A 31 35 | ","timestamp":15
    [LK-030A] < 38 32 38 39 32 31 32 30  7D                      | 82892120}
    [1582892120.242][LK-1304] SHADOW recv get_reply message
    demo_shadow_recv_handler, type = 0, productKey = a18wPXXXXXX, deviceName = aiot_shadow_test_case_01
    payload = "{"status":"success","version":2}", status = success, timestamp = 1582892120
    [1582892122.202][LK-0309] pub: /shadow/update/a18wPXXXXXX/aiot_shadow_test_case_01
    [LK-030A] > 7B 22 6D 65 74 68 6F 64  22 3A 22 75 70 64 61 74 | {"method":"updat
    [LK-030A] > 65 22 2C 22 73 74 61 74  65 22 3A 7B 22 72 65 70 | e","state":{"rep
    [LK-030A] > 6F 72 74 65 64 22 3A 7B  22 4C 69 67 68 74 53 77 | orted":{"LightSw
    [LK-030A] > 69 74 63 68 22 3A 31 7D  7D 2C 22 76 65 72 73 69 | itch":1}},"versi
    [LK-030A] > 6F 6E 22 3A 30 7D                                | on":0}
    [1582892122.230][LK-0309] pub: /shadow/get/a18wPXXXXXX/aiot_shadow_test_case_01
    [LK-030A] < 7B 22 6D 65 74 68 6F 64  22 3A 22 72 65 70 6C 79 | {"method":"reply
    [LK-030A] < 22 2C 22 70 61 79 6C 6F  61 64 22 3A 7B 22 73 74 | ","payload":{"st
    [LK-030A] < 61 74 75 73 22 3A 22 73  75 63 63 65 73 73 22 2C | atus":"success",
    [LK-030A] < 22 76 65 72 73 69 6F 6E  22 3A 30 7D 2C 22 63 6C | "version":0},"cl
    [LK-030A] < 69 65 6E 74 54 6F 6B 65  6E 22 3A 22 6E 75 6C 6C | ientToken":"null
    [LK-030A] < 22 2C 22 74 69 6D 65 73  74 61 6D 70 22 3A 31 35 | ","timestamp":15
    [LK-030A] < 38 32 38 39 32 31 32 32  7D                      | 82892122}
    [1582892122.230][LK-1304] SHADOW recv get_reply message
    demo_shadow_recv_handler, type = 0, productKey = a18wPXXXXXX, deviceName = aiot_shadow_test_case_01
    payload = "{"status":"success","version":0}", status = success, timestamp = 1582892122