三、开发客户端ESP8266
1、自定义AT指令集
(1)实际上,Wi-Fi模块通常配有官方的AT指令集,只要刷写官方提供的固件即可(但可能功能不全),不过,AT指令本身就是基于串口通信实现的一种指令,其原理并不复杂,如果由自己实现,那么就可以自定义指令集。
(2)以下是本例程中使用的AT指令集,读者可自行设计,也可使用官方提供的固件(需要注意的是,调试时,串口监视器发送消息必须将结束符设置为"换行 和 回车 两者都是",传输指令时无需单独输入换行和回车)。
①Wi-Fi连接指令:
|---------------------------------------------------------------------------------------------------|--------------------------------------------------------|------------------------------------|
| AT指令 | 功能 | ESP8266执行动作 |
| "AT+CWJAP=\"<ssid>\",\"<password>\"\r\n" 例:AT+CWJAP="Zevalin_Computer","00114514"\r\n | 连接Wi-Fi网络 <ssid>:欲连接Wi-Fi的名 <password>:欲连接Wi-Fi的密码 | 调用ESP8266WiFi库的begin函数,连接目标Wi-Fi网络 |
②发送HTTP请求指令:
|------------------------------------------------|----------------------------------------------------------------------------|----------------------------------|
| AT指令 | 功能 | ESP8266执行动作 |
| "AT+HTTPCONNECT=\"<host>,<port>\"\r\n" | 与服务器建立连接 <host>:被连接网络服务器的网址/IP地址 <port>:被连接网络服务器的端口编号(HTTP协议的熟知端口号为80) | 调用WiFiClient库的connect函数,与服务器建立连接 |
| "AT+HTTPREQ=\"<message>\"\r\n" | 向服务器发送请求报文 <message>:报文内容需要程序员按照通信协议的格式自行构建 | 调用WiFiClient库的print函数,向服务器发送请求报文 |
| "AT+HTTPSTOP=NULL\r\n" | 与服务器的连接断开 | 调用WiFiClient库的stop函数,与服务器断开连接 |
③订阅MQTT主题:
|-----------------------------------------------------|-------------------------------------------------------------------|-------------------------------------------------------------------|
| AT指令 | 功能 | ESP8266执行动作 |
| "AT+MQTTCONNECT=\"<mqttsever>,<port>\"\r\n" | 连接MQTT服务器 <mqttsever>:欲连接的MQTT服务器地址 <port>:欲连接的MQTT服务器端口号 | 调用PubSubClient库的setServer函数设置目标MQTT服务器及端口号,然后调用connect函数连接MQTT服务端 |
| "AT+MQTTSUB=\"<topic>\"\r\n" | 订阅主题 <topic>:欲订阅的主题 | 调用PubSubClient库的subscribe函数向MQTT服务端订阅主题 |
| "AT+MQTTDISCONNECT=NULL\r\n" | 断开与MQTT服务器的连接 <mqttsever>:欲断开的MQTT服务器地址 <port>:欲断开的MQTT服务器端口号 | 调用PubSubClient库的disconnect函数断开与MQTT服务器的连接 |
(3)以下是本例程中使用的回复数据集,读者可自行设计,也可使用官方提供的固件。
①Wi-Fi连接回复:
|------------------------------------------|-----------------------|
| 回复数据 | 功能 |
| "+IPD,16:Wi-Fi Connected!\r\n" | 回复Wi-Fi网络连接成功 16:数据长度 |
| "+IPD,24:Wi-Fi Connection Failed!\r\n" | 回复Wi-Fi网络连接失败 24:数据长度 |
②接收HTTP响应回复:
|------------------------------------------|--------------------------------------------|
| 回复数据 | 功能 |
| "+IPD,16:Sever Connected!\r\n" | 回复服务器连接成功 16:数据长度 |
| "+IPD,24:Sever Connection Failed!\r\n" | 回复服务器连接失败 24:数据长度 |
| "+IPD,<数据长度>:<响应报文>\r\n" | 返回HTTP响应报文 <数据长度>:数据长度 <响应报文>:HTTP响应报文 |
③接收MQTT报文:
|---------------------------------------------------|------------------------------------------------------|
| 回复数据 | 功能 |
| "+IPD,15:MQTT Connected!\r\n" | 回复MQTT服务器连接成功 15:数据长度 |
| "+IPD,23:MQTT Connection Failed!\r\n" | 回复MQTT服务器连接失败 23:数据长度 |
| "+IPD,17:Subscribe Success!\r\n" | 回复主题订阅成功 17:数据长度 |
| "+IPD,16:Subscribe Failed!\r\n" | 回复主题订阅失败 16:数据长度 |
| "+IPD,<数据长度>:\"<主题>\",\"<消息主体>\"\r\n" | 返回MQTT报文 <数据长度>:数据长度 <主题>:MQTT主题 <消息主体>:MQTT消息 |
2、自定义AT指令集实现
(1)需要说明的是,以下代码不一定考虑到所有可能发生的场景,实际开发中可能会面临各种各样的异常情况,由于篇幅有限,部分异常情况仅做判断,不做后处理方案(可参考注释的发送内容,STM32根据"ERROR"捕捉异常情况码,做相应的后处理)。
(2)将以下代码文件编译,下载到ESP8266中。
cpp
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <PubSubClient.h>
ESP8266WiFiMulti wifiMulti;
WiFiClient httpClient, espClient; // 同一个节点与两台服务器交互,因而有两个抽象客户端
PubSubClient mqttClient(espClient);
bool isConnected = false; //HTTP连接状态
bool mqttConnected = false; //MQTT连接状态
String inputString = "";
int state = 0; // 0: 正常; 1: 收到\r; 2: 收到\r\n 完成
void setup(){
Serial.begin(9600);
Serial.println("");
}
void loop(){
while (Serial.available()) {
char c = (char)Serial.read();
inputString += c;
// 状态机检测 "\r\n"(根据AT指令结束标志获取AT指令)
if (c == '\r' && state == 0) {
state = 1; //Serial.println("state -> 1 (got \\r)"); //调试使用
}
else if (c == '\n' && state == 1) {
state = 2; //Serial.println("state -> 2 (got \\n)"); //调试使用
}
else {
// 如果出现不匹配的字符,重置状态(但保留已读数据)
state = 0; //Serial.print("Resetting state due to char: "); Serial.println(c); //调试使用
}
// 当完成序列时,处理AT指令
if (state == 2) {
// 去掉末尾的 "\r\n"
if (inputString.length() >= 2) {
inputString.remove(inputString.length() - 2);
}
AT_work(inputString); // 根据AT指令执行动作
// 清空准备下一条指令接收
inputString = "";
state = 0;
}
}
if(mqttConnected == true) {
mqttClient.loop(); //如果连接了MQTT服务器,就保持心跳
}
}
// 连接MQTT服务端的具体函数
bool connectToMqtt(String host, int port) {
// 1. 设置MQTT服务器地址和端口
mqttClient.setServer(host.c_str(), port);
// 2. 生成一个唯一的客户端ID
String clientId = "ESP8266Client-" + String(random(0xffff), HEX);
// 3. 尝试连接,使用最基本的连接方式(无用户名密码)
if (mqttClient.connect(clientId.c_str())) {
mqttConnected = true;
// 连接成功后,可以在这里设置回调函数,用于处理收到的消息
mqttClient.setCallback(callback);
return true;
}
else {
mqttConnected = false;
return false;
}
}
void callback(char* topic, byte* payload, unsigned int length) {
// 将消息主体转换为 String(假设 payload 是文本)
String message = "";
for (unsigned int i = 0; i < length; i++) {
message += (char)payload[i];
}
// 构造要输出的主体部分:"topic","message"
// 如果 topic 或 message 中包含双引号,需要转义为 \"
String dataPart = "\"" + String(topic) + "\",\"" + message + "\"";
int dataLen = dataPart.length(); // 数据部分的字节长度
// 构造完整的 AT 指令输出行
String output = "+IPD," + String(dataLen) + ":" + dataPart + "\r\n";
// 通过串口发送
Serial.print(output);
}
void AT_CWJAP(String inputString)
{
// 提取 "AT+CWJAP=" 之后的部分(包括双引号)
int pos = inputString.indexOf("AT+CWJAP=");
String params = inputString.substring(pos + 9);
// 查找第一个双引号的位置
int firstQuote = params.indexOf('"');
if (firstQuote == -1) {
//Serial.print("ERROR: missing SSID quote\r\n");
return;
}
int secondQuote = params.indexOf('"', firstQuote + 1);
if (secondQuote == -1) {
//Serial.print("ERROR: missing closing SSID quote\r\n");
return;
}
String ssid = params.substring(firstQuote + 1, secondQuote);
// 查找第二个双引号后的逗号
int comma = params.indexOf(',', secondQuote);
if (comma == -1) {
//Serial.print("ERROR: missing comma\r\n");
return;
}
// 查找密码部分的起始双引号
int pwdFirstQuote = params.indexOf('"', comma + 1);
if (pwdFirstQuote == -1) {
//Serial.print("ERROR: missing password quote\r\n");
return;
}
int pwdSecondQuote = params.indexOf('"', pwdFirstQuote + 1);
if (pwdSecondQuote == -1) {
//Serial.print("ERROR: missing closing password quote\r\n");
return;
}
String password = params.substring(pwdFirstQuote + 1, pwdSecondQuote);
/* 打印调试信息 */
//Serial.print("SSID: "); Serial.println(ssid);
//Serial.print("PWD: "); Serial.println(password);
// 传递String对象(如果addAP支持 const String&),若不支持,可转换为字符数组并复制
wifiMulti.addAP(ssid.c_str(), password.c_str());
int count = 0;
while (wifiMulti.run() != WL_CONNECTED && count < 15) {
delay(1000);
count++;
}
if (wifiMulti.run() == WL_CONNECTED) {
// 按照 AT 指令格式输出:+IPD,<len>:<data>
Serial.print("+IPD,");
Serial.print("16");
Serial.print(":");
Serial.print("Wi-Fi Connected!\r\n"); // 回复Wi-Fi连接成功
Serial.print("\r\n");
}
else {
// 按照 AT 指令格式输出:+IPD,<len>:<data>
Serial.print("+IPD,");
Serial.print("24");
Serial.print(":");
Serial.print("Wi-Fi Connection Failed!\r\n"); // 回复Wi-Fi连接失败
Serial.print("\r\n");
}
}
void AT_HTTPCONNECT(String inputString)
{
// 提取 "AT+HTTPCONNECT=" 之后的部分
int pos = inputString.indexOf("AT+HTTPCONNECT=");
if (pos == -1) return;
String params = inputString.substring(pos + 16); // "AT+HTTPCONNECT=" 长度为16
// 查找第一个双引号,提取 host
int firstQuote = params.indexOf('"');
if (firstQuote == -1) {
//Serial.print("ERROR: missing host quote\r\n");
return;
}
int secondQuote = params.indexOf('"', firstQuote + 1);
if (secondQuote == -1) {
//Serial.print("ERROR: missing closing host quote\r\n");
return;
}
String host = params.substring(firstQuote + 1, secondQuote);
// 查找逗号位置,然后提取 port(可能在双引号外)
int comma = params.indexOf(',', secondQuote);
if (comma == -1) {
//Serial.print("ERROR: missing comma\r\n");
return;
}
// 提取逗号后面的部分,去除可能的空白和引号
String portStr = params.substring(comma + 1);
portStr.trim();
// 如果端口号也被引号包围(某些格式),去除引号
if (portStr.startsWith("\"") && portStr.endsWith("\"")) {
portStr = portStr.substring(1, portStr.length() - 1);
}
int port = portStr.toInt();
if (port == 0) {
//Serial.print("ERROR: invalid port number\r\n");
return;
}
/* 打印调试信息 */
//Serial.print("Connecting to ");
//Serial.print(host);
//Serial.print(":");
//Serial.println(port);
// 尝试建立 TCP 连接
if (httpClient.connect(host.c_str(), port)) {
// 按照 AT 指令格式输出:+IPD,<len>:<data>
Serial.print("+IPD,");
Serial.print("16");
Serial.print(":");
Serial.print("Sever Connected!\r\n"); // 回复服务器连接成功
Serial.print("\r\n");
isConnected = true; // 置位连接状态
}
else {
// 按照 AT 指令格式输出:+IPD,<len>:<data>
Serial.print("+IPD,");
Serial.print("24");
Serial.print(":");
Serial.print("Sever Connection Failed!\r\n"); // 回复服务器连接失败
Serial.print("\r\n");
httpClient.stop();
isConnected = false; // 置位连接状态
}
}
void AT_HTTPREQ(String inputString)
{
// 检查是否已建立 TCP 连接
if (!isConnected || !httpClient.connected()) {
//Serial.print("ERROR: no TCP connection\r\n");
return;
}
// 提取 "AT+HTTPREQ=" 之后的部分
int pos = inputString.indexOf("AT+HTTPREQ=");
if (pos == -1) return;
String params = inputString.substring(pos + 10); // "AT+HTTPREQ=" 长度 10
// 查找双引号包裹的 message
int firstQuote = params.indexOf('"');
if (firstQuote == -1) {
//Serial.print("ERROR: missing request message\r\n");
return;
}
int lastQuote = params.lastIndexOf('"');
if (lastQuote == firstQuote) {
//Serial.print("ERROR: invalid message format\r\n");
return;
}
String request = params.substring(firstQuote + 1, lastQuote);
// 直接发送请求
//Serial.print("Sending HTTP request...\r\n");
size_t sent = httpClient.write((const uint8_t*)request.c_str(), request.length());
if (sent != request.length()) {
//Serial.print("ERROR: send incomplete\r\n");
return;
}
// 等待并读取响应,设置超时时间(5秒)
unsigned long timeout = millis() + 5000;
String response = "";
while (millis() < timeout) {
if (httpClient.available()) {
// 读取一行或全部数据,这里简单读取所有可用数据
while (httpClient.available()) {
response += (char)httpClient.read();
}
break;
}
delay(10);
}
if (response.length() > 0) {
// 按照 AT 指令格式输出:+IPD,<len>:<data>
Serial.print("+IPD,");
Serial.print(response.length());
Serial.print(":");
Serial.print(response);
Serial.print("\r\n");
}
else {
//Serial.print("ERROR: no response from server\r\n");
}
// 是否保持连接由请求头决定(Connection: close 则服务器会关闭)
// 若服务器关闭了连接,需要更新 isConnected 状态
if (!httpClient.connected()) {
httpClient.stop();
isConnected = false;
//Serial.print("Server closed connection\r\n");
}
}
void AT_HTTPSTOP(String inputString) {
// 1. 解析指令
if (!inputString.startsWith("AT+HTTPSTOP=")) {
//Serial.print("ERROR: invalid command\r\n");
return;
}
// 2. 检查连接状态
if (!isConnected && !httpClient.connected()) {
//Serial.print("ERROR: HTTP not connected\r\n");
return;
}
// 3. 关闭连接
httpClient.stop(); // 调用 stop() 关闭连接
isConnected = false; // 更新连接状态标志
// 4. 返回响应
//Serial.print("OK\r\n");
}
void AT_MQTTCONNECT(String inputString) {
// 1. 检查指令前缀
if (!inputString.startsWith("AT+MQTTCONNECT=")) {
//Serial.print("ERROR: invalid command\r\n");
return;
}
// 2. 提取参数部分 (AT+MQTTCONNECT= 的长度为 15)
String params = inputString.substring(15);
// 3. 查找并提取 host (服务器地址)
int firstQuote = params.indexOf('"');
if (firstQuote == -1) {
//Serial.print("ERROR: missing host\r\n");
return;
}
int secondQuote = params.indexOf('"', firstQuote + 1);
if (secondQuote == -1) {
//Serial.print("ERROR: missing closing host quote\r\n");
return;
}
String host = params.substring(firstQuote + 1, secondQuote);
// 4. 提取 port (端口号)
int comma = params.indexOf(',', secondQuote);
if (comma == -1) {
//Serial.print("ERROR: missing comma\r\n");
return;
}
String portStr = params.substring(comma + 1);
portStr.trim();
int port = portStr.toInt();
if (port == 0) {
//Serial.print("ERROR: invalid port number\r\n");
return;
}
/* 打印解析结果,方便调试 */
//Serial.print("Connecting to MQTT server: ");
//Serial.print(host); /Serial.print(":"); Serial.println(port);
// 进行实际的 MQTT 连接
if (connectToMqtt(host, port)) {
// 按照 AT 指令格式输出:+IPD,<len>:<data>
Serial.print("+IPD,");
Serial.print("15");
Serial.print(":");
Serial.print("MQTT Connected!");
Serial.print("\r\n");
}
else {
// 按照 AT 指令格式输出:+IPD,<len>:<data>
Serial.print("+IPD,");
Serial.print("23");
Serial.print(":");
Serial.print("MQTT Connection Failed!");
Serial.print("\r\n");
}
}
void AT_MQTTSUB(String inputString) {
// 1. 检查指令前缀
if (!inputString.startsWith("AT+MQTTSUB=")) {
//Serial.print("ERROR: invalid command\r\n");
return;
}
// 2. 检查 MQTT 是否已连接
if (!mqttConnected || !mqttClient.connected()) {
//Serial.print("ERROR: MQTT not connected\r\n");
return;
}
// 3. 提取参数部分 (AT+MQTTSUB= 的长度为 10)
String params = inputString.substring(10);
// 4. 查找双引号,提取主题
int firstQuote = params.indexOf('"');
if (firstQuote == -1) {
//Serial.print("ERROR: missing topic quote\r\n");
return;
}
int secondQuote = params.indexOf('"', firstQuote + 1);
if (secondQuote == -1) {
//Serial.print("ERROR: missing closing topic quote\r\n");
return;
}
String topic = params.substring(firstQuote + 1, secondQuote);
// 5. 检查主题是否为空
if (topic.length() == 0) {
//Serial.print("ERROR: empty topic\r\n");
return;
}
// 6. 执行订阅
if (mqttClient.subscribe(topic.c_str(), 1)) {
// 按照 AT 指令格式输出:+IPD,<len>:<data>
Serial.print("+IPD,");
Serial.print("17");
Serial.print(":");
Serial.print("Subscribe Success!");
Serial.print("\r\n");
}
else {
// 按照 AT 指令格式输出:+IPD,<len>:<data>
Serial.print("+IPD,");
Serial.print("16");
Serial.print(":");
Serial.print("Subscribe Failed!");
Serial.print("\r\n");
}
}
void AT_MQTTDISCONNECT(String inputString)
{
// 检查指令前缀(忽略后面的任何内容)
if (!inputString.startsWith("AT+MQTTDISCONNECT=")) {
//Serial.print("ERROR: invalid command\r\n");
return;
}
// 检查当前 MQTT 是否已连接
if (!mqttConnected && !mqttClient.connected()) {
//Serial.print("ERROR: MQTT not connected\r\n");
return;
}
// 断开MQTT连接
mqttClient.disconnect();
// 强制关闭底层TCP连接(disconnect通常会自动关闭,但为保险可调用)
espClient.stop();
// 更新连接状态标志
mqttConnected = false;
// 返回成功响应
//Serial.print("OK\r\n");
}
void AT_work(String inputString){
// 连接Wi-Fi网络的AT指令
int pos = inputString.indexOf("AT+CWJAP=");
if(pos != -1) // 判断是否为该AT指令
{
AT_CWJAP(inputString);
return;
}
// 与服务器建立连接的AT指令
pos = inputString.indexOf("AT+HTTPCONNECT=");
if(pos != -1) // 判断是否为该AT指令
{
AT_HTTPCONNECT(inputString);
return;
}
// 向服务器发送请求报文的AT指令
pos = inputString.indexOf("AT+HTTPREQ=");
if(pos != -1) // 判断是否为该AT指令
{
AT_HTTPREQ(inputString);
return;
}
// 与服务器的连接断开的AT指令
pos = inputString.indexOf("AT+HTTPSTOP=");
if(pos != -1) // 判断是否为该AT指令
{
AT_HTTPSTOP(inputString);
return;
}
// 连接MQTT服务器的AT指令
pos = inputString.indexOf("AT+MQTTCONNECT=");
if(pos != -1) // 判断是否为该AT指令
{
AT_MQTTCONNECT(inputString);
return;
}
// 订阅主题的AT指令
pos = inputString.indexOf("AT+MQTTSUB=");
if(pos != -1) // 判断是否为该AT指令
{
AT_MQTTSUB(inputString);
return;
}
// 断开与MQTT服务器的连接
pos = inputString.indexOf("AT+MQTTDISCONNECT=");
if(pos != -1) // 判断是否为该AT指令
{
AT_MQTTDISCONNECT(inputString);
return;
}
}
四、开发用户端STM32
1、准备工作
(1)拷贝一份STM32教程中按键控制LED的工程文件夹,并更名为"A区支持OTA实验工程"。