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即可
示例源码
示例源码主要分为main.cpp
和dapensonOTA.hpp
组成,
使用到MQTT,因此添加#include <PubSubClient.h>
,github下载最新发行版即可。
首先将示例代码烧录到开发板,串口打印消息记录版本号;下一步修改固件输出信息,并上传至巴法云OTA,从云端下发OTA升级指令触发升级操作。等待升级完成并串口打印比对修改过的固件输出信息,验证是否成功。
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
第一次写入时打印的信息
通过指令发送Dapenson-Update
来触发OTA升级固件的操作
升级完成后会主动重启
至此,便完成了ESP32的OTA升级,首先是联网,然后是有一个固件下载地址,以及触发OTA的动作。
因此,还可以将固件升级地址通过mqtt消息的形式直接发送至客户端,客户端依旧可以使用关键字包含或查询的方式来校验,是否含有http或bin等特殊信息从而选择触发升级操作。