文章目录
物联网时代,我们想把周边的嵌入式设备都接入网络,依托云平台提供的各种服务,实现对嵌入式设备的远程监测和控制。前篇博文介绍了如何实现Bootloader OTA 升级? 这也是物联网设备应该实现的一个重要功能,除了远程固件或应用升级功能外,物联网设备更常用的功能是将采集到的数据上传到云服务平台,并从云服务平台接收并执行控制指令,该如何将嵌入式设备接入云服务平台并实现远程监控功能呢?
一、设备怎么接入OneNET 物联网平台?
随着物联网、云计算、大数据、人工智能等概念的流行,为物联网设备提供云服务的平台也越来越丰富,比如国外的有AWS IoT、Azure IoT、IBM Watson IoT、Google Cloud IoT 等,国内的有中国移动OneNET、阿里云IoT、百度天工IoT、小米IoT、华为云IoT 等。
物联网云服务从结构上大概可以分为“云-管-端” 三个层级,为了更高效的支撑大规模物联网设备的接入,通常还加入了“边缘计算” 层,因此也常将其分为“云-网-边-端” 四个层级。
本文选择较为简单易用的中国移动OneNET 作为设备接入的IoT 云服务平台,该平台的“云-网-边-端”整体架构如下:

本文示例在 “云-管-端” 各层的选择:
- 端-设备:接入到物联网络中的各种嵌入式设备或智能终端,要接入云平台通常需要在设备端工程代码中集成目标云平台的SDK 代码。本文选用Pandora IoT 开发板,芯片型号为STM32L475;
- 管-网络:终端设备与云平台之间的数据通道,主要是承载在通信介质上的下层通信协议,比如Ethernet、Wi-Fi、BLE、NB-IoT、Cellular 等。本文选择Wi-Fi 作为终端设备接入Internet 的通信协议;
- 云平台:提供高效计算、消息传递、数据存储分析、智能识别等服务的云平台,实际是统一管理调度的服务器集群。本文选择接入OneNET 的应用层协议是MQTT,需要使用OneNET 的设备管理、数据流展示、下发命令等功能。
我们开发的嵌入式设备要接入OneNET 云平台,大概需要怎样的接入流程呢?我们可以从中移动OneNET 开发文档中找到答案。
我们选择MQTT 作为接入OneNET 的应用层消息传递协议,OneNET 提供了新版MQTTS (在MQTT 物联网套件选项中,支持TLS 加密和认证,port 为8883)接入和旧版MQTT(在多协议接入选项中,不使用TLS 加密,port 为1883) 接入两种方式。
在前篇博文介绍如何实现Bootloader OTA 升级时谈到,我们的Pandora 开发板在添加mbedTLS 组件后编译烧录工程提示空间不足(除去Bootloader 的64KB 空间,留给Application 的只剩448KB,MbedTLS 要占用超过100KB),因此本文选择使用旧版MQTT 接入OneNET 云平台。使用旧版MQTT 协议接入OneNET 的开发流程如下:

我们开发的嵌入式设备接入OneNET 云平台,主要分为平台域配置和设备域集成SDK 两部分,OneNET 为开发者接入云平台提供了设备模拟器,方便我们调试分析。我们先在平台域创建产品和设备,然后使用设备模拟器连接平台域创建的设备,并尝试上传数据点和下发命令,待通讯正常后再在我们的开发板上集成相应 SDK 并接入OneNET。
1.1 OneNET 平台域配置
OneNET 平台域的账户注册可以参阅开发者文档,旧版MQTT 选择“前往旧版控制台” --> 选择"多协议接入" --> 选择“MQTT(旧版)” --> “添加产品”(可参阅文档创建产品),添加后的产品如下:

我们在OneNET 上添加的产品,需要记住“产品ID” 和“Master-APIKey” 等信息(access_key 用于MQTTS 更安全的鉴权方式,本文示例用不着),这是我们将设备接入OneNET 的身份认证信息之一。
接下来在OneNET 上创建设备,选择“设备列表” --> “添加设备” (可参阅文档创建设备),“鉴权信息” 推荐填写设备的唯一生产序列号,我们这里填写当前的时间戳,添加后的设备如下(手动“添加APIKey”,弹窗“APIKey” 输入的是“Device1_APIKey1”):

我们在OneNET 上添加的产品,需要记住“设备ID” 、“鉴权信息”、“APIKey” 等信息,这也是我们将设备接入OneNET 的身份认证信息之一。
到这里就完成了OneNET 平台域产品和设备的创建,接下来我们使用OneNET 提供的设备模拟器“simulate-device.exe” 尝试接入OneNET,并尝试上传数据点、响应下发命令。
1.2 OneNET 设备模拟器上传数据点与下发命令
从上面OneNET 提供的开发流程图可知,旧版MQTT 接入的IP 地址是“183.230.40.39”,端口号是“6002”(不清楚为何没采用MQTT 协议标准的1883 端口号)。设备要接入OneNET,还需要我们创建的“设备ID”、“产品ID”、“鉴权信息”(也即设备编号) 等参数,我们将以上信息配置到设备模拟器中,点击”Connect“,成功连接到我们在平台域创建的设备:

接下来如何向OneNET 上传数据点、如何响应OneNET 下发的指令呢?点击”[OneNET]上传数据点“ 界面,发现有7种数据类型,我们该选择哪种数据类型、编辑怎样的Json 数据才能正确上传呢?
我们可以查看OneNET 提供的”设备终端接入协议-MQTT“文档,在“5.2.1 数据点上报” 一节介绍了支持上报的7 种Json 数据类型格式。我们尝试上传温湿度浮点型数据,每次上传一个数据就行了,数据平台默认以时序存储接收到的数据,因此我们不需要使用分隔符或时间戳,剩下 3 种Json 数据类型。Json 格式1 每个数据流可以同时上传多个数据点,格式略复杂,Json 格式3 需要带日期时间,Json 格式2 只需要datastream_id 和value 两个字段,比较简单且满足我们的需求,因此我们选择Json 格式2(也即数据类型3),按照示例格式上传温湿度数据如下:

从OneNET 下发命令不像上传数据点这么麻烦,向OneNET 上传数据点需要按照OneNET 定义的数据类型才能被云平台解析,从OneNET 下发命令则根据自己需求定义数据结构就行了,实际上就是将命令作为字符串原样传输给设备端了,设备端接收到命令字符串根据预设行为做出响应,从OneNET 下发命令的图示如下:

我们使用OneNET 设备模拟器成功接入云平台,上传数据点和下发命令都正常,接下来就需要基于我们的开发板集成OneNET SDK,尝试接入OneNET 云平台,并上传温湿度数据,响应从OneNET 下发的指令了。
二、Paho-MQTT 的实现原理是什么?
我们在前面http_ota 示例工程 的基础上继续开发,OneNET 提供了接入其云平台的SDK,我们可以将其集成到我们的工程代码中,中间少不了一些移植操作。既然我们使用的RT-Thread 系统提供了丰富的第三方组件,我们可以先在http://packages.rt-thread.org/搜索下关键字“OneNET”,确实搜到了OneNET 组件包。既然RT-Thread 提供了OneNET 组件包,移植工作量就小多了,本文直接使用RT-Thread 提供的组件包。
从OneNET 组件包的介绍看,它依赖paho-mqtt 和cJSON 组件包,paho-mqtt 是接入OneNET 的MQTT Client,cJSON 是一个生成或解析JSON 格式数据的C 语言库(OneNET 上传数据点需要Json 数据类型)。
Paho-mqtt 是在Eclipse Paho project 项目MQTT Client embedded-c 语言实现版本的基础上移植来的,Eclipse Paho project 提供了主流编程语言的MQTT Client 实现版本,在资源比较受限的嵌入式设备中(比如STM32L475)常选用paho.mqtt.embedded-c 版本,该版本支持的功能特性如下:

由于paho.mqtt.embedded-c 常用于资源受限的嵌入式设备,很多功能并没有实现,也不支持MQTT 5.0 新特性。本文使用MQTT 3.1.1 版本协议,虽然paho-mqtt 支持TLS 加密,受限于我们设备的存储空间,就不使用TLS 加密了。我们通过menuconfig 将paho-mqtt 组件包添加进stm32l475_onenet_sample 工程中(从stm32l475_ota_sample 工程复制而来),配置界面如下:

保存配置并退出menuconfig,自动从github 仓库下载paho-mqtt 软件包到我们的工程中,在env 环境中执行“scons --target=mdk5” 命令生成Keil MDK5 project。前篇博文已经详细介绍了MQTT 协议的设计原理和报文格式,这里简单介绍下paho-mqtt 的大概实现过程,以及移植工作。
2.1 Paho-MQTT 订阅-发布实现逻辑
- Paho-MQTT Client Session 数据结构
Paho-mqtt 是工作在 TCP/IP 协议之上的,而且是基于连接的,因此需要先使用socket API 建立TCP 连接。既然MQTT 协议也需要建立并维持连接状态,paho-mqtt 也应该为client session 设计一个数据结构用来记录连接状态信息,MQTTClient 的数据结构定义如下(主要字段已添加注释):
// .\RT-Thread_Projects\projects\stm32l475_onenet_sample\packages\pahomqtt-v1.1.0\MQTTClient-RT\paho_mqtt.h
struct MQTTClient
{
/** uri 存储建立TCP或TLS连接的 ip_address:port,比如 "tcp://test.mosquitto.org:1883" 或"ssl://test.mosquitto.org:8883",
sock 存储为TCP 或TLS 连接创建的socket 套接字 */
const char *uri;
int sock;
/* MQTT CONNECT 报文中包含的字段信息,包括clientID、User name、Password、keepAliveInterval、Will data 等 */
MQTTPacket_connectData condata;
unsigned int next_packetid, command_timeout_ms;
size_t buf_size, readbuf_size;
unsigned char *buf, *readbuf;
unsigned int keepAliveInterval;
int connect_timeout;
int reconnect_interval;
int isblocking;
int isconnected;
uint32_t tick_ping;
/* 用于通知上层应用 MQTT连接状态变更的回调函数,比如MQTT Client连接、上线、下线时做出什么行为 */
void (*connect_callback)(MQTTClient *);
void (*online_callback)(MQTTClient *);
void (*offline_callback)(MQTTClient *);
/* MQTT Client 订阅主题时,同时为每个MQTT主题名或主题过滤器注册一个回调函数,
当该主题有消息到来时自动执行相应主题的回调函数处理接收到的消息 */
struct MessageHandlers
{
char *topicFilter;
void (*callback)(MQTTClient *, MessageData *);
enum QoS qos;
} messageHandlers[MAX_MESSAGE_HANDLERS]; /* Message handlers are indexed by subscription topic */
/* 设置默认的消息处理函数,当上面的主题都不匹配时,执行这个默认消息处理函数 */
void (*defaultMessageHandler)(MQTTClient *, MessageData *);
/* publish interface */
rt_mutex_t pub_mutex; /* publish data mutex for blocking */
#if defined(RT_USING_POSIX) && (defined(RT_USING_DFS_NET) || defined(SAL_USING_POSIX))
/* 使用 pipe 管道设备将应用线程要发布的消息传递给paho_mqtt 线程,
pipe 内部是一个环形缓冲区,其中pub_pipe[0] 是读取消息的端口,pub_pipe[1] 是写入消息的端口 */
struct rt_pipe_device* pipe_device;
int pub_pipe[2];
#else
/* 使用socket UDP 将应用线程要发布的消息传递给paho_mqtt 线程,应用线程将要发布的消息发送到本地网卡的pub_port 端口,
paho_mqtt 线程 则从本地网卡的pub_port 端口读取消息并发布 */
int pub_sock;
int pub_port;
#endif /* RT_USING_POSIX && (RT_USING_DFS_NET || SAL_USING_POSIX) */
#ifdef MQTT_USING_TLS
MbedTLSSession *tls_session; /* mbedtls session struct */
#endif
void *user_data; /* user-specific data */
};
typedef struct
{
/** The eyecatcher for this structure. must be MQTC. */
char struct_id[4];
/** The version number of this structure. Must be 0 */
int struct_version;
/** Version of MQTT to be used. 3 = 3.1 4 = 3.1.1 */
unsigned char MQTTVersion;
MQTTString clientID;
unsigned short keepAliveInterval;
unsigned char cleansession;
unsigned char willFlag;
MQTTPacket_willOptions will;
MQTTString username;
MQTTString password;
} MQTTPacket_connectData;
/* MQTT 消息都是基于主题的,因此将消息内容跟主题名放到一个结构体中,消息内容包含QoS、ID、retained、dup、payload 等字段 */
typedef struct MessageData
{
MQTTMessage *message;
MQTTString *topicName;
} MessageData;
typedef struct MQTTMessage
{
enum QoS qos;
unsigned char retained;
unsigned char dup;
unsigned short id;
void *payload;
size_t payloadlen;
} MQTTMessage;
MQTT 协议的主要功能可以分为连接/保活连接/断开连接、订阅/退订主题、发布消息这三个部分:MQTTClient 数据结构中前半部分成员变量主要跟连接管理有关,比如uri、sock、condata、keepAliveInterval、tick_ping 等;中间部分主要维护了订阅主题列表及其对应的消息处理函数指针,比如messageHandlers[i]、defaultMessageHandler 等;后半部分主要用来管理应用线程与paho_mqtt 线程之间的消息传递,paho-mqtt 组件包提供了两种线程间消息传递方式,一种是通过pipe 管道设备,另一种是通过socket UDP 端口。
RT-Thread 为paho-mqtt 消息处理专门创建了一个线程paho_mqtt_thread,我们发布消息一般是在另外的应用线程中,线程间消息传递通常有管道、消息队列、共享内存、socket 等,paho-mqtt 提供了pipe 管道和socket udp 两种线程间消息传递方式,将要发布的消息从应用线程传递到paho_mqtt_thread 线程。如果你熟悉 lwip 协议栈,会知道 lwip 也为网络数据包的处理专门创建了一个内核线程tcpip_thread,用户线程与tcpip_thread 线程之间的消息传递是通过邮箱和共享内存实现的,跟消息队列的实现方式类似,不过减少了消息内容的复制,性能更高些。
- Paho-MQTT 订阅-发布实现逻辑
Paho-mqtt 组件库是在用户配置完MQTT 连接参数后调用函数paho_mqtt_start 启动一个MQTT Client 的,该函数主要是创建了一个线程paho_mqtt_thread 来处理MQTT 连接会话的创建、预设主题的订阅、订阅主题消息的监听和处理、待发布消息的监听和发布、心跳保活报文的周期性发送等任务,该函数的实现代码如下(主要函数调用已添加注释,注释以TCP 连接而非TLS 连接为例):
// .\RT-Thread_Projects\projects\stm32l475_onenet_sample\packages\pahomqtt-v1.1.0\MQTTClient-RT\paho_mqtt_pipe.c
static void paho_mqtt_thread(void *param)
{
MQTTClient *c = (MQTTClient *)param;
int i, rc, len;
int rc_t = 0;
/* create publish pipe */
c->pipe_device = mqtt_pipe_init(c->pub_pipe);
if (c->pipe_device == RT_NULL)
goto _mqtt_exit;
_mqtt_start:
if (c->connect_callback)
c->connect_callback(c);
/* 解析c->uri,并调用socket API 建立TCP 或TLS 连接,比如通过调用connect 建立TCP 连接 */
rc = net_connect(c);
if (rc != 0)
{
LOG_E("Net connect error(%d).", rc);
goto _mqtt_restart;
}
/* 通过函数MQTTSerialize_connect 构造MQTT CONNECT 报文,调用函数send 将CONNECT 报文发送出去,
调用select 函数监听MQTT CONNACK 报文,并通过函数recv 读取接收到的报文,
调用函数MQTTDeserialize_connack 解析CONNACK 报文,返回值是CONNACK 中的Reason Code */
rc = MQTTConnect(c);
if (rc != 0)
{
LOG_E("MQTT connect error(%d): %s.", rc, MQTTSerialize_connack_string(rc));
goto _mqtt_restart;
}
LOG_I("MQTT server connect success.");
/* 将创建MQTTClient 是预设的订阅主题通过函数MQTTSubscribe 构造SUBSCRIBE 报文发送出去 */
for (i = 0; i < MAX_MESSAGE_HANDLERS; i++)
{
const char *topic = c->messageHandlers[i].topicFilter;
enum QoS qos = c->messageHandlers[i].qos;
if (topic == RT_NULL)
continue;
/* 通过函数MQTTSerialize_subscribe 构造MQTT SUBSCRIBE 报文,调用函数send 将SUBSCRIBE 报文发送出去,
调用select 函数监听MQTT SUBACK 报文,并通过函数recv 读取接收到的报文,
调用函数MQTTDeserialize_suback 解析SUBACK 报文,返回值是SUBACK 中的Reason Code */
rc = MQTTSubscribe(c, topic, qos);
LOG_I("Subscribe #%d %s %s!", i, topic, (rc < 0) || (rc == 0x80) ? ("fail") : ("OK"));
if (rc != 0)
{
if (rc == 0x80)
LOG_E("QoS(%d) config err!", qos);
goto _mqtt_disconnect;
}
}
if (c->online_callback)
c->online_callback(c);
c->tick_ping = rt_tick_get();
while (1)
{
int res;
rt_tick_t tick_now;
fd_set readset;
struct timeval timeout;
tick_now = rt_tick_get();
if (((tick_now - c->tick_ping) / RT_TICK_PER_SECOND) > (c->keepAliveInterval - 5))
timeout.tv_sec = 1;
else
timeout.tv_sec = c->keepAliveInterval - 10 - (tick_now - c->tick_ping) / RT_TICK_PER_SECOND;
timeout.tv_usec = 0;
/* 调用函数select 监听c->sock 和c->pub_pipe[0],其中c->sock 是本地Client 与远端Server 建立连接的socket,
c->pub_pipe[0] 是pipe 管道设备的读取端口,可以从该端口读取应用线程写入到c->pub_pipe[1] 的消息 */
FD_ZERO(&readset);
FD_SET(c->sock, &readset);
FD_SET(c->pub_pipe[0],
本文详细介绍了如何使用MQTT协议将嵌入式设备接入中国移动OneNET物联网平台,包括平台域配置、Paho-MQTT的实现原理以及远程监控的实现。首先,创建OneNET产品和设备,使用设备模拟器进行数据上传和命令响应。接着,介绍Paho-MQTT的工作逻辑,以及如何在RT-Thread系统中移植和使用。最后,实现远程监控,包括上传温湿度数据和响应LED控制命令。
最低0.47元/天 解锁文章
--- 如何使用MQTT 协议实现OneNET 远程监控?&spm=1001.2101.3001.5002&articleId=116718799&d=1&t=3&u=91a09c16bf244324a73ba49a3feb5b3f)
1万+

被折叠的 条评论
为什么被折叠?



