开发者学堂课程【基于STM32的端到端物联网全栈开发:Paho MQTT 客户端接入阿里云物联网平台(4)】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/574/detail/7940
Paho MQTT 客户端接入阿里云物联网平台(4)
Paho MQTT 客户端接入阿里云物联网平台示例操作
项目例程软件架构:
应用程序:
1.节点端业务程序
2.阿里云 MQTT 连接适配层
中间件:
1.Paho MQTT embedded C
2.mbedTLS(HMAC-SHA1)
3.网络接口抽象
底层驱动:
1.STM32L4 Cube HAL 硬件抽象层
2.传感器驱动
3.WIFI 模块驱动
//例程软件中的 Paho MQTT 协议栈向下通过 ST 提供的网络接口抽象层,来调用底层的 wifi 驱动实现网络数据的通信,向上提供 MQTT API 函数给阿里云 MQTT 连接适配层来完成应用程序的功能
//在接下来的课程中将分别介绍代码中实现网络通信的分层结构,发送和接收数据流的传递过程以及 Paho 向下和网络接口的适配以及向上和阿里云 MQTT 连接适配层的实现
各个软件层中主要对应的c文件以及他们之间的调用关系
---------------1.net_wifi 适配 Wifi 驱动
2.net_c2c 适配 C2c 驱动
3.net_eth 适配 Eth 驱动
//上边的文件通过下边的软件层通过颜色进行了对应
//节点中的业务程序主要在 main.c 业务中实现
//阿里云 MQTT 连接适配层包括 Ali_iotclient.c 文件和 Ali_iot_network_wrapper.c 文件
//Ali_iotclient.c 文件实现了阿里客户端的功能,包括构建 MQTT 连接的参数,与 MQTT 服务器建立连接,订阅发布消息等函数
//Ali_iot_network_wrapper.c 文件中实现了 MQTT 通信网络接口的封装
//MQTTClient.c 是 Paho 协议栈的文件,它提供了众多 API 给上方的 Ali_iotclient.c 中的函数调用
//net.c 是 ST 的网络接口抽象层的文件,MQTTClient.c 通过 Ali_iot_network_wrapper.c 中封装的网络接口函数向下调用 net.c 中对应的函数,再向下调用 wifi 驱动。在本次例程中只使用到了 wifi 驱动,这个软件结构的好处,是可以很方便的移植到比如2G/3G以及以太网这些网络连接方式去,只需要将 net.c 文件部分下的.c 文件进行替换就能完成,上方的文件代码均不会受到影响。
后端配置代码:
//AliIoT 路径下有三个适配文件:
Ali_iotclient.c、
Ali_iot_network_wrapper.c、mqtt_msg_handler.c 文件
// Ali_iot_network_wrapper.c 下封装了5个函数,分别是与服务器创建 tcp 连接,与服务器断开 tcp 连接以及向下对网络接口发送和接收数据的两个函数
//mqtt network 这个函数是在向 mqtt 协议栈新建一个网络接口,并且注册相关的数据收发的函数
// Ali_iotclient.c 文件中主要实现阿里客户端的一些功能,比如如何根据三元组信息来构建 mqtt 服务器的地址,如何得到 mqtt 的主题,如何得到 mqtt 连接的用户名,和 mqtt 连接的密码,还有与 mqtt 服务器创建连接向服务器发送消息和订阅主题的函数
//mqtt 连接创建的过程:
在 main 函数中,前面的初始化操作和 wifi 都已经连接之后,就会开始与阿里云 iot 平台创建 mqtt 的连接,在 main函数中会调用 connect2Aliiothub 函数,此函数是在 Ali_iotclient.c 文件中实现的,此函数首先会根据三元组的信息来构建 mqtt 服务器的地址。得到服务器的地址后先调用 mqtt_connect_network 函数来与服务器建立 tcp 的连接,此函数是在 Ali_iot_network_wrapper.c 文件中实现的。在 mqtt_connect_network 函数中会调用 net.c 文件中所提供的向上的统一的网络接口函数来实现对网络的一些操作,
首先它会先调用 net_sock_create 来创建一个 socket,在此函数中,他会根据你所使用的不同网络接口来调用对应不同的函数来创建一个 socket,socket 创建成功后,它会将对应的 socket 的操作的函数注册到 sock 结构体中。上层的应用程序来通过 sock 这个结构体来做对应的操作时,实际调用的就是它已经注册的函数。
比如要通过 sock 来进行发送数据,接收输入,那么它调用的就是 net_sock_send_tcp_wifi 和 net_sock_sen_recv_wifi 两个函数,这些函数的实现是在 net.tcp.wifi.c 文件中。mqtt_connect_network 函数首先会创建一个 sock,然后在创建 sock 的过程中,已经注册好一系列对 socket 操作的函数,接收数据,发送数据,断开连接等函数都已经注册成功,然后在对 socket 进行一些配置,最后通过 socket 与服务器建立 tcp 的连接。
#include "main.h"
#include "Ali_iot_network_wrapper.h"
int mqtt_connect_network(letwork* n, const char * host_address
,int port)
{
int mqtt_socket_send(Network *network,unsigmed char *buf
,int len,int timeout){
{
int mqtt_socket_recv(Network *network,unsigmed char *buf
,int len,int timeout)
{
int mqtt_socket_disconnect(Network *network)
{
//@sz, nev a MQTT netvork interface vith WIFI module
//must called before baidu_connect_netvork_tls/baidu_connect_network
int mqtt_network _new (Network *network)
{
network->my socket = 0;
network->mqttread = mqtt_socket_recv;
network->mqttwrite = mqtt_socket_send;
network->disconnect = mqtt_socket_disconnect;
return SUCCESS;
}
如何构建服务器地址:extern int PrepareMqttPayload (char * PayloadBuffer,int PayloadSize) ;
extern void Parameters_message_handler(MessageData * data) ;
extern void Service_message_handler(MessageData * data) ;
void calpassword(void) ;
int get_mgtt_server_addr(char* host_addr
,char* region_id,char* product_key )
{
//$( YourProductKey}.iot-as-mqtt.$ { YourRegionId}.aliyuncs.com:1883
uint32_t return_len=0;
return_len = snprintf(host_addr,NQTT_CLIENT_INFO_S12E,"4s.iot-a38-mgtt.i8.aliyuncs.com" ,product_key ,regic
n_id) ;
if (return_len >= MQTT_CLIENT_INFO_SIZE)
{
mag_info ("no enough space for host address\n") ;
return -1;
}
msg_info ("MQIT server address is :is\n",host_addr);
return 0;
}
int build_mgtt_topic(void)
{
MQTT连接过程:
//WIFI SSID/passov rd config and initializationl
// and config device certificate
ret = initPlatform() ;
if (ret! =0)
{
mag_info ("wifi initial failed! \n");
while(1) ;
// connect to Ali Iot platformret = connect2Aliiothub () :;
if (ret!=0)l
mag_info ("MQIT connection failed! \n");
while(1) ;
}
// subscribe to topicsdeviceSubscribe () ;
/ *USER CODE END 2* /
/*Infinite loop * /
/* USER CODE BEGIN WHILE*/while (1)
{
//MQTT reconnection check
if (rebuildMQTTConnection ( )==0){
doMqttYield( );
/*USER CODE END WHILE*/
int net_sock_create_tcp_wifi(net_hnd_t nethnd
,net_ockhnd_t * sockhnd,net_proto_t proto)
int rc = NET_ERR;
net_ctxt_t *ctxt = (net_ctxt_t *) nethnd;
net_sock_ctxt_t *sock = NULL;
sock = net_malloc (sizeof (net_sock_ctxt_t) );
if (sock == NULL)
mag_error ("net_sock_create allocation failed.in");
rc = NET_ERR;
else
memset (sock, 0, sizeof (net_sock _ctxt_t));sock->net = ctxt;
sock->next = ctxt->sock_list;
sock->methods.create = (net_sock_create_tcp_wifi) ;
sock->methods.open= (net_sock_open_tcp_wifi);
sock->methodg.recv= (net_sock_recv_tcp_wifi);
sock->methoda.send= (net_sock_send_tcp_wifi);
sock->methods.close= (net_sock_close_tcp_wifi) ;
sock->methods.destroy=(net_sock_destroy_tcp_wifi) ;
sock->proto= proto;
sock->blocking= NET_DEFAULT_BLOCKING;
sock->read_timeout= NET_DEFAULT_BLOCKING_READ_TIMEOUT;sock->write_timeout= NET_DEEAULT_BLOCKING_WRITE_TIMEOUT;
ctxt->sock_list= sock; /* Insert at the head of the list */
*sockhnd = (net_sockhnd_t) sock;
rc = NET_OK;
}
return rc;
网络分层及数据流:
代码中实现网络通信的分层结构,发送和接收数据流的传送过程
//这里所说的网络分层并不是指 tcp 的网络分层,而是指代码中实现网络接口调用程序的时候通过网络接口抽象层,也就是下图中的蓝色部分,将底层实际的网络接口,绿色的 WIFI 驱动部分,与上层的应用程序,黄色的部分独立开,尽量保证底层网络接口的变化不会对上层应用程序产生影响。WIFI 模块驱动也分为了三层,在 emw3080_io.c 文件中是最低层跟外设打交道的部分,包括初始化引脚,从窗口读取和发送数据。
emw3080.c 文件中是对 at 指令的实现。wifi.c 是 wifi 底层驱动和 wifi 网络层面抽象层的接口。从 tcp ip 网络分层的角度,在本例程中的代码仅仅实现了应用层的 mqtt 协议,传输层和网络层都是在 wifi 模块中实现的,从串口传给wifi 模块以及从 wifi 模块接收的都是已经封装完毕的 mqtt 数据包。传输层 tcp 和网络层封装都是由 wifi 模块完成的。
//图中的箭头是接收和发送数据传递的过程
//绿色箭头指发送和接收网络数据,例如发送温湿度信息,接收云端下发的温度阈值。紫色的箭头是 mcu 控制 wifi 模块的 at 指令以及返回值,比如连接 wifi 热点,查询固件版本等等。对于这类指令,只需要将执行的结果返回到上层的应用程序不需要将收到的具体数据向应用层传递。
//图中粉色的变量是数据在传递过程中保存的位置
//应用数据的发送的过程:比如程序要将检测到的温湿度的值从 wifi 发送到云端,应用程序首先会调用 Paho 的 mqtt publish 函数来发送温湿度信息,在 Paho 协议栈中它会将数据封装成数据包的格式并拷贝到 mqtt_write_buf 中,然后通过 mqtt_socket_send 函数向网络接口发送。
通过网络抽象层的函数 net_sock_send 和 net_sock_send_tcp_wifi 来调用 wifi 模块的驱动最后通过EMW3080_IO_Send 函数发送到串口,通过串口发送到 wifi 模块。
//对于云端下发的数据:wifi 模块通过串口将包含温湿度阈值 mqtt 数据包发送,数据首先会保存到 Uart Ring Buffer中,Paho 协议栈中会通过 mqtt 的函数不断的查询是否有新的数据收到,mqtt yarn 的函数会调用 mqtt_socket_recv再通过网络抽象层的函数来调用 WIFI 模块的驱动函数,查询对应的 Socket Buffer 中是否有未读出的数据,如果有就将数据拷贝到 MQTT_read.buf 中,如果则会通过 pullSocktData 函数将串口中 Ring Buffer 新的数据读取到 Socket Buffer 中再拷贝到 MQTT_read_buf。MQTT_read_buf 中的数据在 Paho 协议栈中进行分析,再将真正的负载数据,就是温度阈值取出并提供给上层应用程序。
//除了应用数据流的接收,还有连接wifi热点,查询固件版本的 ak 指令的发送和返回值的接收,这部分数据的传递主要在 WIFI 模块驱动这层完成,就是紫色箭头指示的部分,这部分 ak 指令的发送主要在 net_if_int 函数中触发,程序初始化 wifi 模块时调用了 net_interface_ination,然后再此函数中完成 wifi 模块的初始化,wifi 固件版本的查询,以及 wifi 热点连接的工作。
以连接 wifi 热点为例,分析紫色部分传递数据的操作:
连接 wifi 热点是通过调用 WIFI_Connect 函数完成,在这个函数中会调用 emw3080中的函数,在此函数中会进行连接 wifi 热点的 at 指令的组装,然后保存在 Atcmd_buf 中通过 runAtCmd 函数,将数据通过串口发送到 wifi 模块,同时它继续在 runAtCmd 中等待模块发回的at指令返回值,在等待返回值的过程中,通过调用 EMW3080_IO_Receive函数查询串口的 Ring Buffer 中是否收到 wifi 模块的返回值。
问:串口的 Ring Buffer 中既有需要传递到应用层的数据,又有只需要在 WIFI 模块驱动层进行数据处理的数据,程序是如何区分两类数据的呢?
答:wifi 模块在发来的每一个数据都有一定的标识,用来说明这一帧数据的类型,比如 wifi 模块发送从云端收到的温度阈值数据包时,会加上 cip 英文数据的头,从这个标识就可以区分不同的数据包,然后进行数据处理。
网络数据产生/消耗层:Paho
网络接口:
WIFI 模块驱动:
设备端向云端发送数据的过程:
int devicest,atusPub (void)
{
int ret;
MQTTMessage MQTT_mag;
ret =0;
MQTT_msg.qos = Q0S1; //QoS1 ;
MQTT_msg.dup = 0;//The DUP flag MST be set to 1 by the Client or Server vhen it attemptsto re-deliver a PUBLISH Packet
// The DuP flag MUST be set to 0 for all Qos 0 messages
MQTT_msg.retained = 1; //the Server MJsT store 757 the application Message and its Qos
MQTT_mag.payload = payload_buf;
// TODO:prepare pub payload,@sz
payload_buf [0]= GetTemperaturevalue ( ) ;
payload_buf [1]=GetHumvalue () ;
payload_buf[2]= GetTempratureThreshold( ;
MQTT_mag.payloadlen = 3;
if ( (ret=MQITPublish(zClient,temp_hum_topic, &MQTT_mag)) != 0){
mag_error ("Failed to publish data. td " ,ret) ;
mag_info(": temprature = %d,humidity = %d\n\n",GetTemperatureValue () ,cetHumValue());
}
数据接收的过程:
/**
@brief send data over the vifi connection.
*@paramBuffer: the buffer to send
*@paramLength: the Buffer's data size.
*@retval Returns EMW3080_OK on success and EM3080_ERROR othervise.
*/
EMW3080_StatusTypeDef EMNN3080_SendData(uint8_t socket,uint8_t* Buffer
,uint32_t Length,uint32_t Timeout)
EMw3080_statusTypeDef Ret = EMw3080_OK;
if(Buffer != NULL)
uint32_t tickstart;
{
/ *Construct thecommand * /
memset(Atcnd, '1o",MAX_AT_CMD_SIZE);
sprintf( (char * )AtCmd,"AT+CIPSEND=%lu,$lusc", socket,Length,'r');
/ *Tne command doesn't have a return command
until the data is actually sent. Thus ve check here whether
ve got the '>' prompt or not. */
Ret = runAtCmd (AtCmd
,strlen ((char *)AtCmd),(uint8_t*)AT_SEND_PROMPT_STRING,Timeout) ;
/ * Return Error */
if (Ret != EMw3080_oK){
return EMN3080_ERROR;
}
/ send the data */
Ret = runAtCmd (Buffer,Length,(uint8_t*)AT_OK_STRING,Timeout) ;
return Ret;
}
驱动和应用层数组定义:
1.Buffer 名称:MQTT_write_buf
默认大小:MQTT_BUFFER_SIZE(512字节)
定义所在位置:Ali_iotclient.c
注释:
2.Buffer 名称:MQTT_read_buf
默认大小:MQTT_BUFFER_SIZE(512字节)
定义所在位置:Ali_iotclient.c
注释:
3.Buffer 名称:AyCmd
默认大小:MAX_AT_SIZE(256字节)
定义所在位置:emw3080.c
注释:
4.Buffer 名称:RxBuffer
默认大小:MAX_RX_SIZE(1500字节)
定义所在位置:emw3080.c
5.Buffer 名称:WIFIRxBuffer
默认大小: RING_BUFFER_SIZE(1024字节)
定义所在位置:emw3080_io.h
6.Buffer 名称:”sock buffer”
默认大小:MAX_SOCKET_SIZE(512字节)定义所在位置:emw3080.h
WIFI_OpenClientConnection函数中分配内存
Paho MQTT客户端对下(网络连接)的适配
1. 网络接口的适配
typedef struct Network Network;
struct Network
net_sockhnd_t my_socket;
int (*mqttread) (Network*, unsigmed char* , int,int) ;
int (*mqttwrite)(Network* , unsigmed char* , int,int) ;
int (*disconnect)(Network*) ;
};
2. Timer 的适配
struct Timer {
uint32_t init_tick;
uint32_t timeout_ms;
};
typedef struct Timer Timer;
void TimerCountdownMS(Timer* timer
,unsigmed int timeout_ms);
void TimerCountdown (Timer* timer
,unsigmed int timeout);
int TimerLeftMS(Timer* timer);
char TimerIsExpired(Timer* timer);
void TimerInit(Timer* timer) ;
3. returnCode 枚举与 ST HAL 库的不兼容
Paho MQTT Client 的调用
1.MQTTClientlnit:
初始化 MQTT 客户端
2.MQTTConnect:
与服务器建立 MQTT 连接
3.MQTTSubscribe:
向服务器订阅消息主题,并注册收到消息后的回调函数
4.MQTTPublish:
向服务器发布某个主题的消息
5.MQTTYield:
根据应用调整周期调用的间隔
Paho MQTT 客户端对上(阿里云 loT)的适配
与阿里云 MQTT 服务器连接需要的参数
1.用户名/密码
2.MQTT ClientID
3.保活时间
4.Cleansession
5.MQTT 服务器域名
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
*/
unsigmed char MQITVersion;
MQTTString clientID;
unsigmed short keepAliveInterval;
unsigmed char cleansession;
unsigmed char willElag;
MQTTPacket_wil10ptions will;
MQTTString username;
MQITString password;
}MQTTPacket_connectData;
构建 MQTT 服务器域名:
MQTT 服务器域名:${YourProductKey}.iot-as-mqtt.${YourRegionld}.aliyuncs.com:1883
举例:
1.服务器域名:
a1b05UeAQ6M.iot-as-mqtt.shanghai-cn.aliyuncs.com
2.端口:1883
构建 MQTT ClientID
MQTT ClientID: clientld+"[Isecuremode=3,signmethod=hmacsha1,timestamp=132323232
clientld:客户端自己定义的ID号,可以使用 MAC 地址
3:安全模式,可以选2(TLS 直连)和3(TCP 直连)
hmacsha1:签名算法支持:
hmacmd5
hmacsha1
hmacsha256
132323232:当前的时间戳。可以通过 HAL_GetTick 获取当前时间戳。
注意:
如果 clientld为“b0f8933b9467”',签名算法选择 hmacsha1,当前时间戳为24081,
MQTTClientID 为:“b0f8933b9467|securemode=3,signmethod=hmacsha1,timestamp=24081|'
构建 MQTT 用户名:MQTT 用户名:DeviceName+“&”+ProductKey
举例:MQTT 用户名就是“smartthermometer&a1b05UeAQ6M”
构建 MQTT 密码:
MQTT 密码=sign_hmac(DeviceSecret,content)
Mbedtls:
Demo IAR 工程
Mbedtls 协议包
MQTT 主题与消息负载格式:
1.功能:设置温度阈值
MQTT 主题:${productKey}/${deviceName}/tempThresholdSet
操作权限(设备):订阅
消息方向:下行
负载格式:一个字节:温度阈值。直接二进制传输,例如:0x1E 代表30℃
2.功能:解除警报
MQTT 主题:${productKey}/${deviceName}/clearAlarm
操作权限(设备):订阅
消息方向:下行
负载格式:一个字节,固定位0x01
3.功能:高温报警
MQTT 主题:S{productKey}/${deviceName}/tempAlarm
操作权限(设备):发布
消息方向:上行
负载格式:一个字节,固定位0x01
4.功能:上报属性
MQTT 主题:S{productKey}/${deviceName}/tempHumUpload
操作权限(设备):发布
消息方向:上行
负载格式: Byte1:温度值
Byte2:湿度值
Byte3:温度阈值
MQTT 订阅消息的回调函数:
Demo 参数输入:
需要保存在 MCU 闪存中的信息:
1.WIFI 配网参数(串口输入)
2.阿里云 loT 平台三元组信息(串口输入)
3.温度报警阈值(云端下发)
Demo 参数存储:
1.MCU 用户闪存容量2M,页大小4K(双 bank 下)
2.取末尾32K 作为用户参数存储区
Sensor 数据的读取(1)
Sensor 数据的读取(2)
项目例程内存占用:
总内存占用(使用 IAR v8.32.3,最高优化等级)
1.Flash:52924字节
2.Ram:10874字节(包括4KB 堆栈)
主要模块内存占用: