esp32通过OTA远程升级固件

什么是OTA?

OTA(Over-the-AirTechnology)即空中下载技术,是通过移动通信的空中接口实现对移动终端设备及SIM卡数据进行远程管理的技术。OTA升级是物联网(IOT)产品设计的一个非常重要的部分,能够实现智能设备系统漏洞修复、系统升级,通过固件和软件的升级,提供更好的服务。OTA固件升级功能不仅能够更新固件,而且还能重新配置芯片上硬件资源。同时,设备固件可通过OTA固件升级流程获得更新的补丁和更多安全算法防范病毒攻击。

ESP32支持OTA,为什么要使用OTA?

ESP32集成了2.4GHzWi-Fi和蓝牙双模,以其超高的射频性能、稳定性、通用性和可靠性,以及超低的功耗,满足不同的功耗需求,适用于各种物联网应用场景,受到低成本系统和制造商的欢迎。

此外,使用ESP32 OTA还可以实现远程控制,即通过网络来远程控制设备。例如,我们可以通过网络将新的固件发送到设备,实现设备的远程升级。

总之,使用OTA可以提高设备的维护效率,方便我们进行远程控制和升级。因此,ESP32支持OTA升级是非常有用的功能。

如何在ESP32上实现OTA?

通过http请求获取远程固件,实现升级,因此必须有一个固件的下载地址,本次示例是基于Arduino的HTTPUpdate.h, MQTT云平台使用的是巴法云的物联网平台服务。
该平台提供地址不变的最新版固件地址,且提供可追溯历史版本的固件地址,通过巴法云控制台进入指定Topic的OTA即可

PI生成的固件地址

示例源码

示例源码主要分为main.cppdapensonOTA.hpp组成,
使用到MQTT,因此添加#include <PubSubClient.h>,github下载最新发行版即可。

首先将示例代码烧录到开发板,串口打印消息记录版本号;下一步修改固件输出信息,并上传至巴法云OTA,从云端下发OTA升级指令触发升级操作。等待升级完成并串口打印比对修改过的固件输出信息,验证是否成功。

image

main 点击查看代码
/*
  Project: [巴法云mqtt灯控]
  Author: [Dapenson]
  Date: [2022-12-14]
  Description: [通过配置接入信息接入巴法云,并可通过巴法云的APP控制灯的开关和亮度,灯控使用pwm函数analogWrite()控制,输入on/off/on#亮度,]

  Revisions:
  [2022-12-14] - [添加on和off的判断,当打开on的时候,亮度默认为10%]
  [2022-12-14] - [新增OTA功能,需在wifi配置留下一个特定wifi,用于OTA升级,收到Dapenson-Update关键字后会自动升级,串口打印日志]
  ...
*/

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include "dapensonOTA.hpp"

/***************** 函数声明 *************************/

void serialEvent();
void lightControl(int brightness);
void mqttCallback(char *topic, byte *payload, unsigned int length);
void connectWIFI();
void connectMQTT();
void setup();
void loop();

/***************** 全局变量 *************************/
#define LED_PIN 2

#define MQTT_SERVER "bemfa.com"                           // 定义MQTT服务器的地址
#define MQTT_PORT 9501                                    // 定义MQTT服务器的端口
#define MQTT_CLIENT_ID "f5c5108e75xxxxxxxx6ebbf2" // 定义客户端的ID=私钥
#define MQTT_USERNAME "myusername"                        // 定义用户名=任意
#define MQTT_PASSWORD "mypassword"                        // 定义密码= 任意

#define TOPIC_SUBSCRIBE "HLlight002" // 定义订阅的主题

// 定义多组Wi-Fi配置信息
const char *ssid[] = {"Hangxxxx", "cc", "c", "Dapenson"};
const char *password[] = {"1axxxx", "mmmmmxxxx", "187xxxx", "Dapenson"};

int brightness_tar = 0; // 定义目标亮度值,初始为灭

// 定义串口输入缓冲区大小
#define INPUT_BUFFER_SIZE 64

// 定义MQTT客户端对象
WiFiClient espClient;
PubSubClient mqttClient(espClient);

/***************** 函数定义 *************************/

// 定义灯控函数
void lightControl(int brightness)
{
  // 限制频率和占空比在合理范围内 (1Hz - 10000Hz, 0% - 100%)
  // https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/peripherals/ledc.html#api
  brightness = constrain(brightness, 0, 100);
  int dutyCycle = map(brightness, 0, 100, 0, 255);
  analogWrite(LED_PIN, dutyCycle);
  Serial.printf("Set brightness to %d%%\n", brightness);
}

// 定义MQTT回调函数,用于处理从服务器接收到的消息
void mqttCallback(char *topic, byte *payload, unsigned int length)
{
  // 将接收到的消息转换为字符串
  char message[length + 1];
  memcpy(message, payload, length);
  message[length + 1] = '\0';
  Serial.printf("Message arrived [%s]%d: %s\n", topic, length, message);

  // 检测是否升级指令
  if (strstr(message, "Dapenson-Update") != NULL)
  {
    updateBin(); // 执行升级函数,完成后直接重启
  }

  brightness_tar = 0;
  if (strstr(message, "on") != NULL)
  {
    brightness_tar = 10;
    int dutyCycle = 0;
    // 如果成功解析出1个整数,则执行 if 语句中的代码。
    if (sscanf(message, "on#%d", &dutyCycle) == 1)
    {
      // 将提取到的值赋值给目标值
      brightness_tar = dutyCycle;
    }
  }

  // 调用灯控函数,设置灯的状态
  lightControl(brightness_tar);
}

// 定义连接Wi-Fi函数
void connectWIFI()
{
  // 连接Wi-Fi网络
  bool connected = false;
  for (int i = 0; i < 3; i++)
  {
    int time_connect = 0;
    WiFi.begin(ssid[i], password[i]);
    while (WiFi.status() != WL_CONNECTED)
    {
      if (time_connect > 10)
      {
        time_connect = 0;
        break;
      }
      delay(1000);
      Serial.print("Connecting to WiFi...");
      Serial.println(ssid[i]);
      time_connect++;
    }
    if (WiFi.status() == WL_CONNECTED)
    {
      connected = true;
      Serial.printf("Connected to %s , IP :%s\n", ssid[i], WiFi.localIP().toString().c_str());
      break;
    }
  }

  // 如果没有连接到Wi-Fi网络,则输出错误信息
  if (!connected)
  {
    Serial.println("Failed to connect to WiFi!");
    delay(5000);
    ESP.restart();
  }
}

// 连接MQTT服务器
void connectMQTT()
{
  if (!mqttClient.connected())
  {
    Serial.println("Connecting to MQTT server...");
    if (mqttClient.connect(MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD))
    {
      Serial.println("Connected to MQTT: " MQTT_SERVER);
      // 订阅主题
      mqttClient.subscribe(TOPIC_SUBSCRIBE);
      Serial.println("subscribed to topic: " TOPIC_SUBSCRIBE);
    }
  }
}

void setup()
{
  Serial.begin(115200);
  delay(100);
  getVersion(); // 获取版本号

  // 初始化GPIO8引脚为输出模式
  pinMode(LED_PIN, OUTPUT);

  // 将12、13引脚拉低
  pinMode(12, OUTPUT);
  pinMode(13, OUTPUT);
  digitalWrite(12, LOW);
  digitalWrite(13, LOW);

  // 调用灯控函数,设置灯的状态
  lightControl(brightness_tar);
  // 连接Wi-Fi
  connectWIFI();
  // 设置MQTT服务器的地址和端口
  mqttClient.setServer(MQTT_SERVER, MQTT_PORT);
  // 设置MQTT回调函数
  mqttClient.setCallback(mqttCallback);
}

void loop()
{
  delay(2);
  // 判断 WiFi 连接状态
  if (WiFi.status() != WL_CONNECTED)
  {
    // 如果没有连接到 WiFi,则进行连接
    connectWIFI();
  }
  if (!mqttClient.connected())
  {
    connectMQTT();
  }
  // 处理MQTT消息
  mqttClient.loop();
}

// 串口0事件
void serialEvent()
{
  // 检查串口是否有数据可读
  if (Serial.available())
  {
    // 定义串口输入缓冲区
    char inputBuffer[INPUT_BUFFER_SIZE];

    // 读取串口输入到缓冲区
    Serial.readBytesUntil('\n', inputBuffer, INPUT_BUFFER_SIZE);

    // 提取占空比
    int dutyCycle = 0;
    if (sscanf(inputBuffer, "on#%d", &dutyCycle) == 1)
    {
      // 将提取到的值赋值给目标值
      brightness_tar = dutyCycle;

      // 发布消息到主题
      mqttClient.publish(TOPIC_SUBSCRIBE, inputBuffer, sizeof(inputBuffer));
      Serial.printf("Published message: %s\n", inputBuffer);
    }
  }
}
dapensonOTA.hpp 点击查看代码
#ifndef DAPENSON_OTA_H
#define DAPENSON_OTA_H

#include <Arduino.h>

#ifdef ESP8266
#include <ESP8266WiFi.h>
#include <ESP8266httpUpdate.h>
#elif defined(ESP32)
#include <HTTPUpdate.h>
#include <WiFi.h>
#endif

/***************** 函数声明 *************************/

void update_started();
void update_finished();
void update_progress(int cur, int total);
void update_error(int err);
void updateBin();
void getVersion();
/***************** 全局变量 *************************/

String upUrl = "http://bin.bemfa.com/b/1BcZjVjNTEwOGU3NTRkNjZkZmI3YTRjYzAwY2U2ZWJiZjI=otaTest.bin"; // 固件链接,在巴法云控制台复制、粘贴到这里即可

void getVersion()
{
  Serial.println(F("====================================="));
  Serial.println(F("Version: 1.1.0"));
  Serial.println(F("Author: Dapenson"));
  Serial.println(F("Date: 2022-12-14"));
  Serial.println(F("====================================="));
}
// 当升级开始时,打印日志
void update_started()
{
  Serial.println("CALLBACK:  HTTP update process started");
}

// 当升级结束时,打印日志
void update_finished()
{
  Serial.println("CALLBACK:  HTTP update process finished");
}

// 当升级中,打印日志
void update_progress(int cur, int total)
{
  Serial.printf("CALLBACK:  HTTP update process at %d of %d bytes...\n", cur, total);
}

// 当升级失败时,打印日志
void update_error(int err)
{
  Serial.printf("CALLBACK:  HTTP update fatal error code %d\n", err);
}

/**
 * 固件升级函数
 * 在需要升级的地方,加上这个函数即可,例如setup中加的updateBin();
 * 原理:通过http请求获取远程固件,实现升级
 */
void updateBin()
{
  Serial.println("start update");
  WiFiClient UpdateClient;

#ifdef ESP8266
  ESPhttpUpdate.onStart(update_started);     // 当升级开始时
  ESPhttpUpdate.onEnd(update_finished);      // 当升级结束时
  ESPhttpUpdate.onProgress(update_progress); // 当升级中
  ESPhttpUpdate.onError(update_error);       // 当升级失败时
  t_httpUpdate_return ret = ESPhttpUpdate.update(UpdateClient, upUrl);
#elif defined(ESP32)
  httpUpdate.onStart(update_started);     // 当升级开始时
  httpUpdate.onEnd(update_finished);      // 当升级结束时
  httpUpdate.onProgress(update_progress); // 当升级中
  httpUpdate.onError(update_error);       // 当升级失败时
  t_httpUpdate_return ret = httpUpdate.update(UpdateClient, upUrl);
#endif

  switch (ret)
  {
  case HTTP_UPDATE_FAILED: // 当升级失败
    Serial.println("[update] Update failed.");
    break;
  case HTTP_UPDATE_NO_UPDATES: // 当无升级
    Serial.println("[update] Update no Update.");
    break;
  case HTTP_UPDATE_OK: // 当升级成功
    Serial.println("[update] Update ok.");
    break;
  }

  delay(10000);
  ESP.restart();
}

#endif

第一次写入时打印的信息

image

通过指令发送Dapenson-Update来触发OTA升级固件的操作

image

升级完成后会主动重启

image

至此,便完成了ESP32的OTA升级,首先是联网,然后是有一个固件下载地址,以及触发OTA的动作。
因此,还可以将固件升级地址通过mqtt消息的形式直接发送至客户端,客户端依旧可以使用关键字包含或查询的方式来校验,是否含有http或bin等特殊信息从而选择触发升级操作。

posted @ 2022-12-14 21:28  Dapenson  阅读(4744)  评论(2编辑  收藏  举报