基于STM32实现OTA&BootLoader 第五章——OTA功能开发【下】

三、开发客户端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实验工程"。

相关推荐
EmbeddedCore2 小时前
低成本物联网产品放弃SSL加密的隐形成本与市场逻辑
物联网·网络协议·ssl
三万棵雪松2 小时前
【Linux 物联网网关主控系统-Web部分(二)】
linux·前端·物联网
红叶落水2 小时前
GD32H737 1Mbps 数字通信链路实现
单片机
LCG元3 小时前
STM32实战:基于STM32F103的MQTT协议通信(EMQ X Broker)
stm32·单片机·嵌入式硬件
zmj3203243 小时前
51单片机
单片机·嵌入式硬件·51单片机
zmj3203243 小时前
MCS-51单片机
单片机·嵌入式硬件·51单片机
深念Y3 小时前
从CH341A编程器、SPI Flash到Linux+STM32理解
linux·stm32·flash·bios·固件·编程器·闪存
小柯博客3 小时前
从零开始打造 OpenSTLinux 6.6 Yocto 系统 - STM32MP2(基于STM32CubeMX)(八)
c语言·git·stm32·单片机·嵌入式硬件·嵌入式·yocto
421!12 小时前
GPIO工作原理以及核心
开发语言·单片机·嵌入式硬件·学习