注意:
本文以简单易理解易实现为主,仅实现最基本的交互通信功能,性能和稳定性暂无考虑。
需要材料:
硬件:stm32及下载线、esp8266-01s(wifi模块)
软件:emqx、keil
可选:wireshark,python
开始:
- 配置stm32工程
首先,我们需要一个stm32的基础工程,为了调试需要,我们需要两个usart串口分别与电脑和wifi模块进行通信。
打开stm32cube 需要配置的有RCC、SYS、USART、时钟、project manager几部分。
配置好usart1和2之后,引脚如图,将wifi模块按照tx-rx;rx-tx;EN、3v3接3.3v;GND接GND;其余悬空接入单片机。
至此,stm32基础工程和硬件配置完毕。
2、配置emqx
在官网下载windows版本的emqx,下载后解压,在解压的bin目录下打开cmd,输入
emqx console即可。
至此emqx配置完毕,可以在 http://127.0.0.1:18083通过控制台查看mqtt。
控制台账号为admin 密码为public
3、初始化wifi模块-AT指令
至此,基本条件已经配置完毕,可以开始代码部分的编写了。
详细的AT指令可以在乐鑫官网查看AT 命令集 — ESP-AT 用户指南 文档 (espressif.com)
但我相信你绝对不愿意去看也不一定能理解官网的AT指令集,这里我按照必要的配置流程梳理一下我们会用到的AT指令。
AT指令 | 作用 |
+++ | 关闭透传 |
AT+RESTORE\r\n | 恢复出厂设置 |
AT\r\n | AT查询 |
ATE0\r\n | 关闭回显 |
AT+CWMODE_CUR=1\r\n | 设置透传模式 |
AT+CWJAP_CUR="****","*********"\r\n | 连接wifi,后跟wifi的用户名和密码 |
AT+CIPSTART="TCP","xxx.xxx.x.x",1883\r\n | 配置连接的设备和端口号,注意这里的端口号是数字而不是字符串 |
AT+CIPMODE=1\r\n | 设置为AP模式 |
AT+CIPSEND\r\n | 开始发送数据 |
这些做完之后,wifi模块就配置好了,可以开始发送数据了。但是我们这个时候还不知道如何去发送数据,这个时候我们可以去查看mqtt的协议然后自己组包,但是学习mqtt协议也是很大的工作量;我们也可以使用mqtt的库,通过调用接口的方式来实现我们想要的功能,但我找不到库(xs);还有最后一种方法,我们可以通过空中数据来了解mqtt交互的时候传递了哪些数据。
我们可以使用python来实现一个简单的mqtt通信,这个也可以用来测试mqtt服务器是否配置完成。代码如下:
from paho.mqtt import client as mqtt_client
import random,time
url = "127.0.0.1"
port = 1883
topic = "my/test"
client_id = f'python-mqtt-{random.randint(0,100)}'
def mqtt_connect():
def on_connect(client,userdata,flags,rc):
if rc == 0:
print("connect success")
else:
print("connect failed ",rc)
client = mqtt_client.Client(client_id)
client.on_connect = on_connect
client.connect(url,port)
return client
def publish(client):
msg_count = 0
while True:
time.sleep(1)
msg = f"message:{msg_count}"
result = client.publish(topic,msg)
if result[0] == 0:
print("send msg{",msg,"}success")
else:
print("send msg{",msg,"}failed")
msg_count+=1
def run():
client = mqtt_connect()
client.loop_start()
publish(client)
if __name__ == "__main__":
run()
from paho.mqtt import client as mqtt_client
import random,time
url = "127.0.0.1"
port = 1883
topic = "my_test"
client_id = f'python-mqtt-{random.randint(0,100)}'
def commect_mqtt():
def on_connect(client,data,flags,rc):
if rc == 0:
print("connect success")
else:
print("connect failed ",rc)
client = mqtt_client.Client(client_id)
client.on_connect = on_connect
client.connect(url,port)
return client
def subscribe(client):
def on_message(client,userdata,msg):
print("receive",msg.payload.decode(),"from{",msg.topic,"}topic")
client.subscribe(topic)
client.on_message = on_message
return
def run():
client = commect_mqtt()
subscribe(client)
client.loop_forever()
if __name__ == "__main__":
run()
上述分别为发布和订阅mqtt服务的python例程,那么我们要怎样模拟一次数据交互呢?我们可以先使用服务器中的虚拟客户端。
然后按照上图方式创建一个虚拟客户端,
先运行第二个python历程,向my_test发送报文,报文内容为123。这样就可以在python显示框中看到我们发送过来的123了。
再运行第一个,也就是发布的python例程,可以在服务器端看到发布的报文。
已经有了通信过程,接下来就需要抓包了。下载并打开wireshark,
在如图所示过滤器下进行抓包,然后重新运行我们的第一个python例程,并在服务器端点击发送,之后就可以点击红色按键暂停抓包了,我们来看看我们抓到的数据:
connect command:连接请求
cnnect ack:连接回应
这张就是连接请求的报文。
再来运行第二个python例程,同样方式进行抓包:
有一个connect command,这个就是连接请求,服务器会返回一个connect ack。
向后翻,有一个public message,这个就是发布消息。
点选进去后,点击红框这一行,下面对应的蓝色框内即为发布的数据,从上到下以此为连接、订阅、发布消息。
有了数据包之后,我们就可以尝试通过串口连接wifi模块直接进行连接发布尝试了。
4、mqtt包简介迈向物联网第一步——MQTT理论知识详解_哔哩哔哩_bilibili
虽然但是,我们还是先来大概认识一下mqtt数据包吧,通过wireshark抓的数据包我们可能看的出来这些报文的含义,再次稍微总结一下。
连接:10 1a 00 04 4d 51 54 54 04 02 00 3c 00 0e 70 79 74 68 6f 6e 2d 6d 71 74 74 2d 38 35
共28位,这个数据长度并非固定,以这29位为例进行举例,其中标红的均可不修改。
位数 | 报文 | 含义 | ||
1 | 10 | 连接请求 | 固定报头 | |
2 | 1a | 十进制为26 | 后续字节长度 | 固定报头 |
3-8 | 00 04 4d 51 54 54 | ASCII为00 04 MQTT | 协议名 | 可变报头 |
9 | 04 | 协议级别 | 可变报头 | |
10 | 02 | 连接标志 | 可变报头 | |
11-12 | 00 3c | 十进制是60 | 保活时间 | 可变报头 |
13-14 | 00 0e | 十进制为14 | 设备名长度 | 负载 |
15-28 | 70 79 74 68 6f 6e 2d 6d 71 74 74 2d 38 35 | ASCII为 python-mqtt-85 | 设备名 | 负载 |
发布:30 0c 00 07 6d 79 5f 74 65 73 74 31 32 33
位数 | 报文 | 含义 | ||
1 | 30 | 发布请求 | 固定报头 | |
2 | 0c | 十进制为12 | 后续字节长度 | 固定报头 |
3-4 | 00 07 | 十进制为7 | 主题的长度 | 可变报头 |
5-11 | 6d 79 5f 74 65 73 74 | ASCII为my/test | 主题名 | 可变报头 |
12-14 | 31 32 33 | ASCII为123 | 发送的内容 | 负载 |
订阅:82 0c 00 01 00 07 6d 79 5f 74 65 73 74 00
位数 | 报文 | 含义 | ||
1 | 82 | 订阅请求 | 固定报头 | |
2 | 0c | 十进制为12 | 后续字节长度 | 固定报头 |
3-4 | 00 01 | msgid | 可变报头 | |
5-6 | 00 07 | 十进制为7 | 主题长度 | 可变报头 |
7-13 | 6d 79 5f 74 65 73 74 | ASCII为my/test | 主题名 | 负载 |
14 | 00 | 质量要求Qos | 负载 |
心跳:c0 00 emmm,这个就先记住就好。
5、了解完报文之后,终于可以开始代码的编写了。
配置一些宏定义,上半部分为AT指令,下半部分为接收消息的buff。
#define START "+++" //退出透传模式
#define RESTORE "AT+RESTORE\r\n" //恢复出厂设置
#define AT "AT\r\n" //
#define ATE0 "ATE0\r\n" //关闭回显
#define CWMODE "AT+CWMODE_CUR=1\r\n" //STA模式
#define CWJAP "AT+CWJAP_CUR=\"****\",\"*********\"\r\n" //连接WIFI
#define CIPSTART "AT+CIPSTART=\"TCP\",\"xxx.xxx.xx.xx\",1883\r\n" //连接服务器
#define CIPMODE "AT+CIPMODE=1\r\n" //开启透传模式
#define CIPSEND "AT+CIPSEND\r\n" //开启发送模式
#define DATA_LEN (50)
#define LOST_LEN (200)
#define LOG_LEN (50)
#define TIMEOUT (1000)
我们先定义一个log函数,用来在串口一打印调试信息,我们将串口一连接在电脑上,可以在电脑上观察log来调试代码,这一段记得加头文件 #include "stdarg.h"
void log_printf(const char *format, ...)
{
char buff[LOG_LEN] = {0};
va_list pArgs ;
va_start (pArgs, format) ;
vsnprintf (buff, LOG_LEN, format, pArgs);
va_end (pArgs) ;
HAL_UART_Transmit(&huart1, (uint8_t *)buff, strlen(buff), HAL_MAX_DELAY);
return;
}
通过AT指令初始化wifi模块
bool with_ok(uint8_t *buff, uint16_t len)
{
if (len == 0) {
log_printf("with_ok input len error\r\n");
}
int i = 0;
for(i = 0; i < len - 1; i++) {
if((*(buff+i) == 'O') && ((*(buff+i+1)) == 'K')) {
return true;
}
}
return false;
}
void esp_init(void)
{
log_printf("esp_init begin\r\n");
uint8_t recv[DATA_LEN] = {0};
uint8_t lost[LOST_LEN] = {0};
uint8_t retry = 0;
for(retry = 3; retry; retry--) {
memset(recv, 0, DATA_LEN);
HAL_UART_Transmit(&huart2, (uint8_t *)START, sizeof(START)-1, HAL_MAX_DELAY); //start -1 is \0 need to retry
HAL_UART_Receive(&huart2, recv, DATA_LEN, TIMEOUT);
if ((recv[0] != '+') || (recv[1] != '+') || (recv[2] != '+')) {
if (retry) {
continue;
} else {
log_printf("esp_init START error\r\n");
return;
}
} else {
break;
}
}
log_printf("esp_init START success\r\n");
memset(recv, 0, DATA_LEN);
HAL_UART_Transmit(&huart2, (uint8_t *)RESTORE, sizeof(RESTORE)-1, HAL_MAX_DELAY); //RESTORE -1 is \0 need lost buff
HAL_UART_Receive(&huart2, recv, DATA_LEN, TIMEOUT);
if (!with_ok(recv, DATA_LEN)) {
log_printf("esp_init RESTORE error\r\n");
return;
} else {
log_printf("esp_init RESTORE success\r\n");
}
memset(recv, 0, DATA_LEN);
while(HAL_UART_Receive(&huart2, lost, LOST_LEN, TIMEOUT) == HAL_OK);
HAL_UART_Transmit(&huart2, (uint8_t *)AT, sizeof(AT)-1, HAL_MAX_DELAY); //AT -1 is \0
HAL_UART_Receive(&huart2, recv, DATA_LEN, TIMEOUT);
if (!with_ok(recv, DATA_LEN)) {
log_printf("esp_init AT error\r\n");
return;
} else {
log_printf("esp_init AT success\r\n");
}
memset(recv, 0, DATA_LEN);
HAL_UART_Transmit(&huart2, (uint8_t *)ATE0, sizeof(ATE0)-1, HAL_MAX_DELAY); //ATE0 -1 is \0
HAL_UART_Receive(&huart2, recv, DATA_LEN, TIMEOUT);
if (!with_ok(recv, DATA_LEN)) {
log_printf("esp_init ATE0 error\r\n");
return;
} else {
log_printf("esp_init ATE0 success\r\n");
}
memset(recv, 0, DATA_LEN);
HAL_UART_Transmit(&huart2, (uint8_t *)CWMODE, sizeof(CWMODE)-1, HAL_MAX_DELAY); //CWMODE -1 is \0
HAL_UART_Receive(&huart2, recv, DATA_LEN, TIMEOUT);
if (!with_ok(recv, DATA_LEN)) {
log_printf("esp_init CWMODE error\r\n");
return;
} else {
log_printf("esp_init CWMODE success\r\n");
}
memset(recv, 0, DATA_LEN);
HAL_UART_Transmit(&huart2, (uint8_t *)CWJAP, sizeof(CWJAP)-1, HAL_MAX_DELAY); //CWJAP -1 is \0 need more timeout
HAL_UART_Receive(&huart2, recv, DATA_LEN, 8 * TIMEOUT);
if (!with_ok(recv, DATA_LEN)) {
log_printf("esp_init CWJAP error\r\n");
return;
} else {
log_printf("esp_init CWJAP success\r\n");
}
memset(recv, 0, DATA_LEN);
HAL_UART_Transmit(&huart2, (uint8_t *)CIPSTART, sizeof(CIPSTART)-1, HAL_MAX_DELAY); //CIPSTART -1 is \0
HAL_UART_Receive(&huart2, recv, DATA_LEN, TIMEOUT);
if (!with_ok(recv, DATA_LEN)) {
log_printf("esp_init CIPSTART error\r\n");
return;
} else {
log_printf("esp_init CIPSTART success\r\n");
}
memset(recv, 0, DATA_LEN);
HAL_UART_Transmit(&huart2, (uint8_t *)CIPMODE, sizeof(CIPMODE)-1, HAL_MAX_DELAY); //CIPMODE -1 is \0
HAL_UART_Receive(&huart2, recv, DATA_LEN, TIMEOUT);
if (!with_ok(recv, DATA_LEN)) {
log_printf("esp_init CIPMODE error\r\n");
return;
} else {
log_printf("esp_init CIPMODE success\r\n");
}
memset(recv, 0, DATA_LEN);
HAL_UART_Transmit(&huart2, (uint8_t *)CIPSEND, sizeof(CIPSEND)-1, HAL_MAX_DELAY); //CIPSEND -1 is \0
HAL_UART_Receive(&huart2, recv, DATA_LEN, TIMEOUT);
if (!with_ok(recv, DATA_LEN)) {
log_printf("esp_init CIPSEND error\r\n");
return;
} else {
log_printf("esp_init CIPSEND start\r\n");
}
return;
}
多次尝试发送+++,等待串口准备就绪,发送恢复出厂设置, 在这之后需要丢弃多余的串口信息,发送AT、ATE0、CWMODE、CWJAP ,此时会比较慢,继续发送CIPSTART 、CIPMODE、CIPSEND,完成wifi模块初始化。
接下来连接mqtt、发送心跳、发送消息。
void mqtt_init(void)
{
uint8_t load[] = {0x10, 0x1a, 0x00, 0x04, 0x4d, 0x51, 0x54, 0x54, 0x04, 0x02, 0x00, 0x3c, 0x00, 0x0e, 0x70, 0x79, 0x74, 0x68, 0x6f, 0x6e, 0x2d, 0x6d, 0x71, 0x74, 0x74, 0x2d, 0x31, 0x39};
uint8_t recv[4] = {0};
int8_t retry = 5;
while(retry--) {
HAL_UART_Transmit(&huart2, load, sizeof(load), HAL_MAX_DELAY);
HAL_UART_Receive(&huart2, recv, 4, TIMEOUT);
if((recv[0] == 0x20) && (recv[1] == 0x02) && (recv[2] == 0x00) && (recv[3] == 0x00) ) {
break;
}
}
if(retry > 0) {
log_printf("mqtt load success\r\n");
mqtt_send_alive();
} else {
log_printf("mqtt load failed!\r\n");
}
return;
}
void mqtt_send_alive(void)
{
uint8_t alive[] = {0xc0, 0x00};
uint8_t recv[2] = {0};
int8_t retry = 10;
while(retry--) {
HAL_UART_Transmit(&huart2, alive, sizeof(alive), HAL_MAX_DELAY);
HAL_UART_Receive(&huart2, recv, 4, TIMEOUT);
if((recv[0] == 0xD0) && (recv[1] == 0x00)) {
break;
}
}
if (retry < 0) {
log_printf("mqtt disconnected\r\n");
}
}
void mqtt_send_data(uint8_t *key, uint8_t *data, uint16_t len)
{
uint8_t send[] = {0x30, 0x0e, 0x00, 0x09, 0x6d, 0x73, 0x7a, 0x79, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x31, 0x32, 0x33};
HAL_UART_Transmit(&huart2, send, sizeof(send), HAL_MAX_DELAY);
}
这里发送数据我是直接写死了,仅作调试使用,后续根据需要自由组包。
最后,我们在初始化的时候初始化wifi模块和mqtt,在循环里面发送心跳和数据进行测试,在服务器上观察是否接收到消息。
esp_init();
mqtt_init();
mqtt_send_alive();
mqtt_send_data(NULL,NULL,0);
至此,就算搞定了。
最后,本文仅为自己学习理解过程的呈现,若有疑误欢迎批评指正,若有建议欢迎互相交流。