前言

本文研究如何使用RT_Thread onenet组件将设备连接到中移onnet云平台,onenet组件提供多种方式连接云平台,本文使用mqtt方式连接,完成一个三色灯项目。

一、理论基础

1.onenet平台介绍

OneNET定位为PaaS服务,即在物联网应用和真实设备之间搭建高效、稳定、安全的应用平台:面向设备,适配多种网络环境和常见传输协议,提供各类硬件终端的快速接入方案和设备管理服务;面向企业应用,提供丰富的API和数据分发能力以满足各类行业应用系统的开发需求,使物联网企业可以更加专注于自身应用的开发,而不用将工作重心放在设备接入层的环境搭建上,从而缩短物联网系统的形成周期,降低企业研发、运营和运维成本。

中移OneNET是中国移动基于物联网技术和产业特点打造的开放平台和生态环境,将面向智能家居、可穿戴设备、车联网、移动健康、智能创客等多个领域开放。

以上介绍过于官方,我来从开一个开发者的视角说下哈,目前onnet是国内比较早的一批做云的平台,论坛十分活跃,各种技术分享资料层出不穷,非常适合希望了解物联网的朋友深入学习,下图是目前onenet社区的会员和帖子总体情况:

# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

二、使用实例

1.云端创建产品

接下来咱们进入正题,首先需要在云端创建一个产品,步骤如下:

创建产品
登录onenet官网,注册一个账号,地址:https://open.iot.10086.cn, 点进开发者中心,创建产品,产品配置信息如下:
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

注意;操作系统这里使用RT_Thread,没有此选项,选用linux即可

创建数据点
数据流模板->添加数据流模板
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

创建产品数据点,此处创建power和color两个数据点,power表示总开关,power为0时候,灯关闭;power不为0的时候,color数值起作用,用以选择不同模式。

创建后台显示数据面板
应用管理->独立应用->添加应用
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

注意:红色、绿色、蓝色按钮和颜色显示图片均链接color数据点,颜色显示图片仅显示左右,按钮可以下发选择不同的灯颜色。

到此为止,我们已经完成了产品的创建工作,接下来将要处理设备接入问题。

2.设备SDK移植

下载onenet组件包
打开包管理工具,进入RT-Thread online packages/IOT-internet of things目录,选择Paho MQTT、webClient、cJSON组件,如下所示:
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

选择IOT Cloud --->进入配置页面,将创建的产品信息配置进去,onenet配置信息如下:
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

注意:选择Enable OneNET automatic register device,设备可以注册到云端,而不需要每个设备都手动在平台注册,然后把设备信息再写入设备中,这样一来将为工厂产线减少工作量,解决烧录容易出现设备信息写错等问题。

下载fal、easyflash组件
这两个组件主要是用来存放设备自动注册获取的设备认证信息,下次设备启动检测是否含有设备信息,如果有可以直接进行mqtt连云,否则需要重新注册。

进入RT-Thread online packages/system packages目录,选择fal组件,如下所示:
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

进入RT-Thread online packages/tools packages目录,选择easyflash组件,如下所示:
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

移植fal、easyflash组件

在w60x路径下新建ports文件夹,然后在ports下创建两个移植组件fal和easyflash,注意fal下SConscrip指定Group名字为fal和packages/fal-latest中对应,easyflash亦然;至于移植细节,之后会另行介绍,现在着重开发项目。

修改onenet组件

a.修改函数指针:cmd_rsp_cb

函数指针cmd_rsp_cb指向一个函数,用来接收onenet下发的数据,RT_Thread中的处理方式是云端下发数据后,通过调用onenet_mqtt.cmd_rsp_cb,将数据抛给应用层的一个函数,并通过参数uint8_t *resp_data获取应用层处理后的结果,紧接着返回给onenet;我这里所作的修改是用户自行控制上发数据,因此不需要原函数中后两个参数,此外增加了char topic_name,字段,因为用户自己控制上报数据时候,需要区分数据是云端下发的相应还是设备触发的主动上报。
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

设备主动上报topic_name是$dp,需要调用:
send_mq_msg("$dp", send_msg.data_ptr, send_msg.data_size);

设备应答云端下发的topic_name通过cmd_rsp_cb对应的回调函数可以获得,回调函数中首先获取数据,然后调用:
send_mq_msg(topic_name, (uint8_t *)res_buf, rt_strlen(res_buf));

b.发送队列统一发送数据,解决高频控制或常问题

send_mq_msg()函数实际上是把数据扔进发送队列中,发送队列在一个线程中等待,检测到有内容扔进队列的时候,则从队列中取出数据发送给onenet,实现代码如下所示:

static void onenet_upload_entry(void *parameter)
{
    mq_send_msg_t send_msg = { 0x00 };
    led_blue_on();
    send_mq_msg("$dp", (uint8_t *)POST_DATA4, rt_strlen((char *)POST_DATA4));

    while (1)
    {
        rt_memset(&send_msg, 0x00, sizeof(send_msg));
        if (RT_EOK == rt_mq_recv(mq_send, &send_msg, sizeof(send_msg), RT_WAITING_FOREVER))
        {
            if (onenet_mqtt_upload_data(send_msg.topic_name, (const char *)send_msg.data_ptr))
            {
                rt_kprintf("upload has an error, stop uploading\n");
                break;
            }
            else
            {
                if (NULL==rt_strstr(send_msg.topic_name, "$dp"))  //上次上报是返回给mqtt服务器, 需要更新下数据以求同步数据到onnect后台web页面
                {
                    send_mq_msg("$dp", send_msg.data_ptr, send_msg.data_size);
                }
                rt_kprintf("buffer : %s\ntopic_name is:%s", send_msg.data_ptr, send_msg.topic_name);
                rt_free(send_msg.data_ptr);
            }
        }
        rt_thread_mdelay(50);
    }
    exit:
    rt_kprintf("upload thread exit!!!");
}

c.发送所有状态数据到onenet

RT_Thread提供的是一次性发送一个数据点给云端,我修改后也支持一次性上报所有数据,这样APP只需要每次刷新各个状态就行,不需要判断是哪个数据发生了改变。
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

函数具体实现如下:

rt_err_t onenet_mqtt_upload_data(const char *topic_name, const char *msg_str)
{
    char *send_buffer = RT_NULL;
    rt_err_t result = RT_EOK;
    size_t length = 0;

    assert(msg_str);

  send_buffer = ONENET_MALLOC(strlen(msg_str) + 4);
    if (!(send_buffer))
    {
        log_e("ONENET mqtt upload string data failed! No memory for send buffer!");
        return -RT_ENOMEM;
    }
  *(send_buffer + strlen(msg_str) + 3) = 0x00;

    strncpy(&send_buffer[3], msg_str, strlen(msg_str));
    length = strlen(msg_str);
    /* mqtt head and json length */
    send_buffer[0] = 0x03;
    send_buffer[1] = (length & 0xff00) >> 8;
    send_buffer[2] = length & 0xff;
  length += 3;

    result = onenet_mqtt_publish(topic_name, (uint8_t *)send_buffer, length);
    if (result < 0)
    {
        log_e("onenet mqtt publish digit data failed!");
        goto __exit;
    }

__exit:
    if (send_buffer)
    {
        ONENET_FREE(send_buffer);
    }

    return result;
}

3.程序分析

mqtt组件入口
main()函数中注册网络状态变化函数,然后连接网络,连接成功后,调用onenet_mqtt_init()开启mqtt服务:首先获取设备信息,如果获取不到,则进行注册流程,获取设备信息后,进行mqtt初始化设置,设置认证参数和数据接收、连接状态变化回调函数。

int onenet_mqtt_init(void)
{
    int result = 0;

    if (init_ok)
    {
        LOG_D("onenet mqtt already init!");
        return 0;
    }

    if (onenet_get_info() < 0)
    {
        result = -1;
        goto __exit;
    }

    onenet_mqtt.onenet_info = &onenet_info;
    onenet_mqtt.cmd_rsp_cb = RT_NULL;

    if (onenet_mqtt_entry() < 0)
    {
        result = -2;
        goto __exit;
    }

__exit:
    if (!result)
    {
        LOG_I("RT-Thread OneNET package(V%s) initialize success.", ONENET_SW_VERSION);
        init_ok = RT_TRUE;
    }
    else
    {
        LOG_E("RT-Thread OneNET package(V%s) initialize failed(%d).", ONENET_SW_VERSION, result);
    }

    return result;
}

数据下发入口

当有平台数据下发,设备会进入mqtt_callback()回调函数,回调函数中处理数据,然后抛给应用层数据接收处理函数onenet_cmd_rsp_cb(),可通过onenet_set_cmd_rsp_cb(onenet_cmd_rsp_cb)在应用层设置。

static void onenet_cmd_rsp_cb(char *topic_name, uint8_t *recv_data, size_t recv_size)
{
    char res_buf[128] = { 0 };
  int value = 0;

    rt_kprintf("recv data is %.*s\n", recv_size, recv_data);

  value = atoi((char *)recv_data);
  rt_kprintf("recv int data is:%d\r\n", value);
    /* match the command */
    if (value == 10) //总开关,默认为红色
    {
        /* led on */
        led_red_on();

        rt_snprintf(res_buf, sizeof(res_buf), "{\"power\":\"10\",\"color\":\"2\"}");

    }else if (value == 0)
  {
        /* led off */
        led_off();

        rt_snprintf(res_buf, sizeof(res_buf), "{\"power\":\"0\",\"color\":\"1\"}");

  }
  else if (value == 2)//红色
    {
        /* red led on */
        led_red_on();

        rt_snprintf(res_buf, sizeof(res_buf), "{\"power\":\"10\",\"color\":\"2\"}");

    }
    else if (value == 3)//绿色
    {
        /* green led on */
        led_green_on();

        rt_snprintf(res_buf, sizeof(res_buf), "{\"power\":\"10\",\"color\":\"3\"}");

    }
    else if (value == 4)//蓝色
    {
        /* blue led on */
        led_blue_on();

        rt_snprintf(res_buf, sizeof(res_buf), "{\"power\":\"10\",\"color\":\"4\"}");

    }
  send_mq_msg(topic_name, (uint8_t *)res_buf, rt_strlen(res_buf));
}

数据上报入口

当设备通过连接到onenet服务器的时候,会进入mqtt_online_callback()回调函数,数据上报入口函数就是在这里触发,创建一个发送线程,检测发送队列中的数据,一旦有数据需要发送,立刻取出队列发送给云端。

static void mqtt_online_callback(MQTTClient *c)
{
    LOG_D("Enter mqtt_online_callback!");
    onenet_upload_cycle();
}

4.配置

在applications目录下新建一个文件夹:3-cloud/1-onenet_led,然后同理需要修改aplications/SConscript脚本。

Import('RTT_ROOT')
Import('rtconfig')
from building import *

cwd = GetCurrentDir()
src  = Glob('3-cloud/1-onenet_led/*.c')
CPPPATH = [cwd]

group = DefineGroup('Applications', src, depend = [''], CPPPATH = CPPPATH)

Return('group')

三、下载运行

在ENV控制台,输入scons命令,在build/Bin目录下生成rtthread_1M.FLS,
烧录运行后,设备端按照下图所示连接电路:
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

设备Log如下所示:

 \ | /
- RT -     Thread Operating System
 / | \     4.0.0 build Jun 30 2019
 2006 - 2018 Copyright by rt-thread team
lwIP-2.0.2 initialized!
[32m[5] I/SAL_SOC: Socket Abstraction Layer initialize success.

[0m[32m[64] I/WLAN.dev: wlan init success

[0m[32m[95] I/WLAN.lwip: eth device init ok name:w0

[0m[32m[100] I/WLAN.dev: wlan init success

[0m[32m[132] I/WLAN.lwip: eth device init ok name:w1

[0m[D/FAL] (fal_flash_init:61) Flash device |                nor_flash | addr: 0x00000000 | len: 0x00100000 | blk_size: 0x00001000 |initialized finish.
[32;22m[I/FAL] ==================== FAL partition table ====================[0m
[32;22m[I/FAL] | name      | flash_dev |   offset   |    length  |[0m
[32;22m[I/FAL] -------------------------------------------------------------[0m
[32;22m[I/FAL] | app       | nor_flash | 0x00010000 | 0x00080000 |[0m
[32;22m[I/FAL] | download  | nor_flash | 0x00090000 | 0x00060000 |[0m
[32;22m[I/FAL] | fs_part   | nor_flash | 0x000f0000 | 0x0000b000 |[0m
[32;22m[I/FAL] | easyflash | nor_flash | 0x000fb000 | 0x00001000 |[0m
[32;22m[I/FAL] =============================================================[0m
[32;22m[I/FAL] RT-Thread Flash Abstraction Layer (V0.3.0) initialize success.[0m
[Flash] EasyFlash V3.3.0 is initialize success.
[Flash] You can get the latest version on https://github.com/armink/EasyFlash .
msh />[32m[4268] I/WLAN.mgnt: wifi connect success ssid:LBAGMY

[0m[D/ONENET] (onenet_mqtt_init:201) onnect mqtt init
[D/ONENET] (mqtt_connect_callback:85) Enter mqtt_connect_callback!
[36;22m[I/ONENET] RT-Thread OneNET package(V1.0.0) initialize success.[0m
[32m[5296] I/WLAN.lwip: Got IP address : 192.168.1.6

[0m[32m[5477] I/MQTT: MQTT server connect success

[0m[D/ONENET] (mqtt_online_callback:90) Enter mqtt_online_callback!
buffer : {"power":"10","color":"4"}
topic_name is:$dp[31m[30547] E/MQTT: [30547] wait Ping Response res: 0

[0m[D/ONENET] (mqtt_offline_callback:96) Enter mqtt_offline_callback!
[D/ONENET] (mqtt_connect_callback:85) Enter mqtt_connect_callback!
[32m[35662] I/MQTT: MQTT server connect success

[0m[D/ONENET] (mqtt_online_callback:90) Enter mqtt_online_callback!
buffer : {"power":"10","color":"4"}
topic_name is:$dp[D/ONENET] (mqtt_callback:60) topic $creq/a2a663b4-5b87-57ad-81d8-9e563659e540 receive a message
[D/ONENET] (mqtt_callback:62) message length is 1
recv data is 2
recv int data is:2

buffer : {"power":"10","color":"2"}
topic_name is:$crsp/a2a663b4-5b87-57ad-81d8-9e563659e540buffer : {"power":"10","color":"2"}
topic_name is:$dp[D/ONENET] (mqtt_callback:60) topic $creq/e08cd093-66ec-5e43-812c-ae7d2cd2cf5c receive a message
[D/ONENET] (mqtt_callback:62) message length is 1
recv data is 3
recv int data is:3

buffer : {"power":"10","color":"3"}
topic_name is:$crsp/e08cd093-66ec-5e43-812c-ae7d2cd2cf5cbuffer : {"power":"10","color":"3"}
topic_name is:$dp[D/ONENET] (mqtt_callback:60) topic $creq/65824a18-f17c-5286-b4b6-7f13a4d76246 receive a message
[D/ONENET] (mqtt_callback:62) message length is 1
recv data is 4
recv int data is:4

buffer : {"power":"10","color":"4"}
topic_name is:$crsp/65824a18-f17c-5286-b4b6-7f13a4d76246buffer : {"power":"10","color":"4"}
topic_name is:$dp

后台显示:

红色灯模式

# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

绿色灯模式
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

蓝色灯模式
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

四、结语

1.总结:

本节完,实际操作过程中需要注意的地方有如下几点:

(1) 组件方式调整

之前的教程都是,每篇当作一个新的项目来做,每次都重新拉取组件包,但是更新本篇内容时候,我发现有时候组件包和RT_Thread menuconfig控制工具没有很好的协调一致,比如本篇中用到的W600组件包,wm_libraries,这次配置工具并没有该选项,于是通过RT_Thread官网寻找wm_libraries组件,提示可以在包管理工具中选择,如下:
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

实际包管理工具如下图所示,并没有wm_libraries,但是有realtek的RTL8710组件,这也为之后做个铺垫,接下来会出一些RTL8710的教程,敬请期待。
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①

考虑到目前存在的问题,为了防止大家在使用的时候遇到和我类似的问题,从本篇开始,项目使用到的组件都会上传到github,大家下载后,配置SConscript选择需要运行的应用即可。

(2) 修改了RT_Thread的连接onenet连接组件包

a. 修改数据上报逻辑
原有组件包是每次回复单个数据点的数据,修改后支持一次性上报所有数据点,同时调整mqtt数据上报处理逻辑。

b. 增加队列缓冲发送机制
解决连续两次调用数据发送接口,仅有第一次发出去的问题。

(3)资料获取

如您在使用过程中有任何问题,请加QQ群进一步交流,也可以github提Issue。

QQ交流群:906015840 (备注:物联网项目交流)

关注公众后,回复w600获取资料

一叶孤沙出品:一沙一世界,一叶一菩提
# IT明星不是梦 # WIFI模块开发教程之W600连云篇1:onenet三色灯项目mqtt篇①