本文描述在支持TCP的广域网模组上集成SDK的方法。

说明 基于Link SDK v3.0.1编写。

Link SDK是阿里云IoT提供的用于将设备连接到阿里云IoT的设备端SDK,用于完成设备认证、数据通信等功能。将Link SDK集成到通信模组中,可以带来以下好处:

  • 设备厂商在MCU上无需关心如何连接阿里云IoT,只是通过调用模组提供的AT指令就可以连接阿里云IoT,因此对MCU的资源消耗没有增加。
  • 阿里将在认证合作伙伴页面露出通过认证的模组型号、购买链接、开发指导等文档,引导设备商以及服务提供商购买通过认证的通信模组连接阿里云IoT。
使用集成了SDK的模组开发设备的示意图如下所示:集成了SDK的模组开发设备

设备商开发设备的流程为:

  • 购买集成了阿里云Link SDK的模组。
  • 在MCU上通过模组提供的AT指令连接阿里云,以及从阿里云IoT收发数据。
  • 在阿里云IoT上部署云端服务,对设备进行管理。

对于模组商而言,在模组上需要完成的工作包括:

  • 将Link SDK正确的集成到模组上。
  • 提供相应的连接阿里云IoT物联网的AT指令供MCU调用。

下面的文档只讲解如何将SDK的MQTT功能集成到模组上。对于模组商来说,集成SDK的功能越多,MCU侧设备厂商的开发功能越少,因此建议模组商尽可能多的集成SDK的功能,比如OTA、物模型等。

在阿里云物联网平台上的操作

为了验证模组上集成的SDK是否运行正确,需要将一个测试设备连接到阿里云物联网平台。用户需要在阿里云物联网平台上创建一个产品,并创建一个该产品的设备实例以获取到设备的身份信息。

  1. 点击阿里云物联网平台登录控制台,模组商需要注册一个阿里云账号,注册阿里云账号是免费的。
  2. 登录阿里云物联网平台的控制台之后,按照创建产品描述的步骤创建一个产品。在产品页面可以获取到产品的ProductKey和ProductSecret。
  3. 参照创建设备描述的步骤添加一个测试设备,在设备页面可以获取到设备的DeviceName和DeviceSecret。

集成SDK的开发过程

模组商在模组上集成SDK时,需要进行下面几个开发过程。

集成SDK的开发过程

SDK配置与代码抽取

配置SDK

SDK包含的功能较多,下面讲解如何配置本场景中需要的功能。

运行配置命令

  • Linux系统

    进入SDK的根目录下,运行命令

    make menuconfig
                
  • Windows系统

    运行SDK根目录下的config.bat

    config.bat

上面的两种方式都会启动SDK的配置工具,界面如下所示:

界面显示1

使能需要的SDK功能

在功能配置界面,按下空格键可以选中或者失效某个功能,使用小键盘的上下键来在不同功能之间切换;如果想知道每个选项的具体含义,先用方向键将高亮光条移到那个选项上,再按键盘上的“h”按键,将出现帮助文本,说明选项是什么含义,以及打开了和关闭了意味着什么。

  • 如果编译环境有自带标准头文件<stdint.h>,请使能选项:

    PLATFORM_HAS_STDINT

  • 如果目标系统上运行有嵌入式操作系统,请使能选项:

    PLATFORM_HAS_OS

  • 由于模组支持TCP但是不支持MQTT,因此必须使能下面三项配置:
    • FEATURE_MQTT_COMM_ENABLED,使用阿里SDK提供的MQTT API与云端通信。
    • FEATURE_MQTT_DEFAULT_IMPL,使用阿里SDK中自带的MQTT Client实现,用户需要实现相关的TCP连接的创建、连接、数据收发过程。
    • FEATURE_MQTT_DIRECT,设备端指定阿里云物联网云端站点。

建议使能FEATURE_SUPPORT_TLS,让数据与物联网平台之间的数据通信是加密的。本文档中为了降低适配工作量,未使能该选项。

其它功能均无需使能。

抽取选中功能的源代码

运行SDK根目录下的extract.sh(windows下运行extract.bat),客户选中的功能所对应的代码将会被放置到文件夹output。

将SDK代码文件加入客户编译环境

客户将上一个步骤中得到的Link SDK的代码文件从output目录复制到自己的工程目录中,并修改自己的编译环境或者开发工具将这些代码文件集成到编译环境。

客户需要集成的文件包括eng目录下面的dev_sign、infra、mqtt、wrappers目录下的代码文件。

实现HAL对接函数

Link SDK被设计为可以在不同的操作系统上运行,或者甚至在不支持操作系统的MCU上运行,因此与系统相关的操作被定义成一些HAL函数,需要客户进行实现;另外,由于不同的通信模组上的OS不同,所以与通信模组上TCP相关的操作也被定义成HAL函数需要客户进行实现。

所有HAL函数位于文件为output/eng/wrappers/wrapper.c中。

系统相关HAL

必须实现函数:

** ** 函数名 说明
1 HAL_Malloc 对应标准C库中的malloc(), 按入参长度开辟一片可用内存, 并返回首地址
2 HAL_Free 对应标准C库中的free(), 将入参指针所指向的内存空间释放
3 HAL_Printf 对应标准C库中的printf(), 根据入参格式字符串将字符文本显示到终端。如果用户的调试环境有更好的调试手段,该函数无需实现
4 HAL_Snprintf 类似printf, 但输出的结果不再是显示到终端, 而是存入指定的缓冲区内存
5 HAL_UptimeMs 返回一个uint64_t类型的数值, 表达设备启动后到当前时间点过去的毫秒数
6 HAL_SleepMs 按照指定入参的数值, 睡眠相应的毫秒, 比如参数是10, 那么就会睡眠10毫秒

对以上函数若需了解更多细节, 可访问SDK官方文档页面

另外,在SDK加压后的目录(非代码抽取目录)wrappers/os下有HAL的参考实现,用户可以查看是否有自己需要的OS参考实现,若未提供则用户需要自己进行实现。

可选实现函数

如果模组没有运行OS,或者SDK的MQTT API并没有在多个线程中被调用,以下函数可以不用修改wrapper.c中相关的函数实现;在有OS场景下并且MQTT API被APP在多个线程中调用,则需要用户对接以下函数:

** ** 函数名 说明
1 HAL_MutexCreate 创建一个互斥锁, 返回值可以传递给HAL_MutexLock/Unlock
2 HAL_MutexDestroy 销毁一个互斥锁, 这个锁由入参标识
3 HAL_MutexLock 申请互斥锁, 如果当前该锁由其它线程持有, 则当前线程睡眠, 否则继续
4 HAL_MutexUnlock 释放互斥锁, 此后当前在该锁上睡眠的其它线程将取得锁并往下执行
5 HAL_SemaphoreCreate 创建一个信号量, 返回值可以传递给HAL_SemaphorePost/Wait
6 HAL_SemaphoreDestroy 销毁一个信号量, 这个信号量由入参标识
7 HAL_SemaphorePost 在指定的计数信号量上做自增操作, 解除其它线程的等待
8 HAL_SemaphoreWait 在指定的计数信号量上等待并做自减操作
9 HAL_ThreadCreate 根据配置参数创建thread

TCP相关HAL

MQTT基于TCP进行通信,模组商需要实现下面四个TCP HAL函数。

序号 函数名 说明
1 HAL_TCP_Establish 建立一个TCP连接。注意:* 入参host是一个域名,需要转换为IP地址* 返回值是tcp的socket号
2 HAL_TCP_Destroy 关闭tcp连接,入参是HAL_TCP_Establish的返回值,返回值0表示成功
3 HAL_TCP_Write 通过TCP连接发送数据。注意:* 该函数传入了一个超时时间,如果超时仍未将数据发送结束那么函数也需要返回;* 如果TCP连接已断开,需要返回一个小于0的负数
4 HAL_TCP_Read 在指定的时间内读取数据并返回,该函数的入参中指定了可接收的数据的最大长度,如果从TCP中读取到该最大长度的数据,那么可以立即返回

产品相关HAL

下面的HAL用于获取产品的身份认证信息,设备厂商需要设计如何在设备上烧写设备身份信息,并通过下面的HAL函数将其读出后提供给SDK:

序号 函数名 说明
1 HAL_GetProductKey 获取设备的ProductKey, 用于标识设备的产品型号
2 HAL_GetDeviceName 获取设备的DeviceName, 用于唯一标识单个设备
3 HAL_GetDeviceSecret 获取设备的DeviceSecret, 用于标识单个设备的密钥

对以上函数若需了解更多细节, 可直接访问SDK官方文档页面

注:这几个参数在实际产品开发时应该由设备厂商通过AT指令告知模组,模组商调试时可以将自己创建的测试设备的ProductKey、DeviceName、DeviceSecret直接通过上面这几个函数返回。

参照example实现产品功能

模组商可参考output文件夹中的 eng/examples/mqtt_example.c进行功能调试, 设备厂商可以将该文件复制到产品工程中,对其进行修改后使用。

该example将连接设备到阿里云,订阅一个指定的topic并发送数据给该topic,即设备上报的消息会被物联网平台发送给设备,下面是example的大概过程说明:

注意:需要在云端将该topic从默认的权限从”订阅”修改为”发布和订阅”,如下图所示:

从程序入口的 main() 函数看起, 首先是调用模组提供的HAL函数获取产品的身份信息:

int main(int argc, char *argv[])
{
    void                   *pclient = NULL;
    int                     res = 0;
    int                     loop_cnt = 0;
    iotx_mqtt_param_t       mqtt_params;

    HAL_GetProductKey(DEMO_PRODUCT_KEY);
    HAL_GetDeviceName(DEMO_DEVICE_NAME);
    HAL_GetDeviceSecret(DEMO_DEVICE_SECRET);

    EXAMPLE_TRACE("mqtt example");
            

注:

  • 上面的三个HAL_GetXXX函数是获取设备的证书信息,模组商可以在相应的HAL函数中填入测试设备的证书信息即可。

接下来对MQTT连接参数进行指定,客户可以根据自己的需要对参数进行修改:

/* Initialize MQTT parameter */
    memset(&mqtt_params, 0x0, sizeof(mqtt_params));
    mqtt_params.port = sign_mqtt.port;
    mqtt_params.host = sign_mqtt.hostname;
    mqtt_params.client_id = sign_mqtt.clientid;
    mqtt_params.username = sign_mqtt.username;
    mqtt_params.password = sign_mqtt.password;
    mqtt_params.request_timeout_ms = 2000;
    mqtt_params.clean_session = 0;
    mqtt_params.keepalive_interval_ms = 60000;
    mqtt_params.read_buf_size = 1024;
    mqtt_params.write_buf_size = 1024;
    mqtt_params.handle_event.h_fp = example_event_handle;
    mqtt_params.handle_event.pcontext = NULL;
    pclient = IOT_MQTT_Construct(&mqtt_params);
            

通过调用接口 IOT_MQTT_Construct() 触发SDK连接云平台, 若接口返回值非NULL, 则连云成功之后调用example_subscribe对一个指定的topic进行数据订阅:

res = example_subscribe(pclient);
            
example_subscribe的函数内容如下:

注:

  • 设备商需要根据自己的产品设计,订阅自己希望订阅的TOPIC,以及注册相应的处理函数。
  • 上面例子程序中第一个橙色圈选的代码是指定topic的格式:/$ProductKey/$DeviceName,这个topic是在物联网平台创建一个产品时默认生成的。
  • 第二个橙色圈选的代码是生成topic的内容。
  • 上图的第三个框展示了如何订阅一个指定的topic以及当通过该topic接收到数据时的处理函数。

以下段落演示MQTT的发布功能,即将业务报文上报到云平台:

 while (1) {
        if (0 == loop_cnt % 20) {
            example_publish(pclient);
        }

        IOT_MQTT_Yield(pclient, 200);

        loop_cnt += 1;
    }
            

注:

  • 上面的代码是周期性的将固定的消息发送给云端,设备商需要根据自己的产品功能,在必要的时候才上传数据给物联网平台。
  • 客户可以删除main函数中example_publish(pclient)语句,避免周期发送无效数据给到云端。
  • IOT_MQTT_Yield是让SDK去接收来自MQTT Broker的数据,其中200毫秒是等待时间,如果用户的消息数量比较大、或者实时性要求较高,可以将时间改小。
下面是example_publish函数体的内容:

上面图中第一个框是发送的消息的内容,第二个框是调用SDK提给的API将消息发送给指定的topic。

上传模组商编码和模组型号

如果模组商希望将模组送到阿里云IoT进行模组认证,那么模组商需要将模组商编码和模组型号进行上报,这样阿里云物联网平台可以统计通过指定模组商连接到平台的设备数量,也可以统计通过模组商的某个型号模组连接设备的数量。

模组商编码和模组型号请在集成SDK前联系阿里进行获取,请按如下模板发送消息,联系我们

主题:设备接入Link SDK产品-模组/芯片型号申请

当模组与阿里云物联网平台建立连接之后,请复制并调用下面的函数进行信息上报,其中参数pid是模组商编码、mid是型号编码:

#define PID_STRING_LEN_MAX          32  /* PID字符串最大长度 */
#define MID_STRING_LEN_MAX          32  /* MID字符串最大长度 */


int example_report_pid_mid(void *pclient, const char *product_key, const char *device_name, const char *pid, const char *mid)
{
    int res = 0;
    iotx_mqtt_topic_info_t topic_msg;

    const char topic_frag1[] = "/sys/";
    const char topic_frag2[] = "/thing/deviceinfo/update";
    char topic[sizeof(topic_frag1) + sizeof(topic_frag2) + IOTX_PRODUCT_KEY_LEN + IOTX_DEVICE_NAME_LEN] = {0};

    const char payload_frag1[] = "{\"id\":\"0\",\"version\":\"1.0\",\"params\":[{\"attrKey\":\"SYS_MODULE_ID\",\"attrValue\":\"";
    const char payload_frag2[] = "\",\"domain\":\"SYSTEM\"},{\"attrKey\":\"SYS_PARTNER_ID\",\"attrValue\":\"";
    const char payload_frag3[] = "\",\"domain\":\"SYSTEM\"}],\"method\": \"thing.deviceinfo.update\"}";
    char payload[sizeof(payload_frag1) + sizeof(payload_frag2) + sizeof(payload_frag3) + PID_STRING_LEN_MAX + MID_STRING_LEN_MAX] = {0};

    if (strlen(pid) > PID_STRING_LEN_MAX || strlen(mid) > MID_STRING_LEN_MAX) {
        return -1;
    }

    /* 组装MQTT topic字符串 */
    memcpy(topic, topic_frag1, strlen(topic_frag1));
    memcpy(topic + strlen(topic), product_key, strlen(product_key));
    memcpy(topic + strlen(topic), "/", 1);
    memcpy(topic + strlen(topic), device_name, strlen(device_name));
    memcpy(topic + strlen(topic), topic_frag2, strlen(topic_frag2));

    /* 组装MQTT payload字符串, payload中包含了PID, MID字符串 */
    memcpy(payload, payload_frag1, strlen(payload_frag1));
    memcpy(payload + strlen(payload), mid, strlen(mid));
    memcpy(payload + strlen(payload), payload_frag2, strlen(payload_frag2));
    memcpy(payload + strlen(payload), pid, strlen(pid));
    memcpy(payload + strlen(payload), payload_frag3, strlen(payload_frag3));

    topic_msg.qos = IOTX_MQTT_QOS0;
    topic_msg.retain = 0;
    topic_msg.dup = 0;
    topic_msg.payload = (void *)payload;
    topic_msg.payload_len = strlen(payload);

    /* 使用MQTT publish API发送PID,MID信息报文 */
    res = IOT_MQTT_Publish(pclient, topic, &topic_msg);
    if (res < 0) {
        return -1;
    }

    return 0;
}

            

功能调试

下面的信息截图以mqtt_example.c为例编写。

如何判断设备已连接到阿里云

注意:/${productKey}/${deviceName}/get这个topic默认只有“订阅”权限,请在物联网平台的控制台将其修改为“发布和订阅”,避免消息发送到云端后被云端丢弃; 把该topic修改为“发布和订阅”,主要是为了让example程序运行不出错。

下面的打印是HAL_Printf函数将信息打印到串口后运行example的输出内容,其中使用橙色圈选的信息表明设备已成功连接到阿里云物联网平台:

如何判断设备已成功发送数据到云端

登录阿里网物联网平台的商家后台,选中指定的设备,可以查看是否收到来自设备的消息,如下图所示:

注:上图中的内容只能看见消息发送到了哪个topic,消息的内容并不会显示出来。

如何判断设备已可成功接收来自云端数据

在商家后台的“下行消息分析”分析中可以看见由物联网平台发送给设备的消息:
也可在设备端查看是否已收到来自云端的数据,exmaple代码中收到云端发送的数据的打印信息如下所示:

AT指令实现

如果模组不提供开发环境给用户进行二次开发,而是外接一个MCU并且产品的业务逻辑运行在MCU上,那么模组商还需要提供AT指令给MCU调用,由于模组以前只支持TCP,所以需要提供MQTT连接配置、发起连接、断开连接、订阅、Publish等接口。

下面是推荐增加的AT指令(模组商也可以合并这些指令或者拆分指令),格式由模组商自行定义:

指令项 说明
阿里设备身份信息设置 设置设备的product_key, product_secret, device_name, device_secret
阿里云端region设置 阿里云IoT提供中国、美国、日本等多个云端站点,可以让MCU指定需要连接的阿里云IoT的站点以及端口信息
建立MQTT连接 建立到阿里云MQTT Broker的连接,该指令中可以指定MQTT clean session、keepalive间隔、Req Timeout时间
断开MQTT连接 断开与阿里云IoT的MQTT连接
订阅某个Topic 对某个Topic进行消息订阅
取消Topic订阅 取消对某个topic的消息订阅
向某个topic发送数据 向指定的topic发送数据

注:当MCU通过AT指令将设备身份信息传递到模组时,建议将设备身份信息存储到全局变量中,并在HAL_GetProductKey、HAL_GetDeviceName、HAL_GetDeviceSecret等几个HAL函数中将其进行返回,这样example程序不用进行修改。