5.Rust嵌入式入门——OTA

2,225 阅读7分钟

5.Rust嵌入式入门——OTA

本节将学习如何利用网络来获取更新文件,并通过使用esp-idf-svc库中的OTA(Over-the-Air)框架来实现ESP32-C3设备的远程固件升级功能。

OTA

OTA(Over-the-Air)空中升级技术是指通过无线通信的方式对微控制器(MCU)的软件进行远程管理和更新的方法。这项技术使得MCU可以在部署后无需物理接触就能接收和安装软件更新,从而显著提升了设备的可维护性和灵活性。

特点

  1. 远程更新:设备可以通过无线网络接收软件更新,无需人工介入。
  2. 软件升级:可以更新固件,修正软件缺陷,添加新功能或优化现有功能。
  3. 成本节约:减少了现场维护所需的物流和人力成本。

应用场景

  • 消费电子产品:如智能家居设备中的MCU。
  • 工业自动化:用于工厂自动化设备中的MCU。
  • 物联网设备:包括传感器节点和其他嵌入式系统。

实现流程

  1. 准备更新包:开发者构建新的固件版本,并打包成适合无线传输的格式。
  2. 上传至服务器:更新包被上传到OTA服务器。
  3. 设备连接网络:MCU设备通过Wi-Fi或蓝牙等方式连接到互联网。
  4. 检查更新:设备定期检查是否有可用的更新。
  5. 下载新固件:一旦检测到新版本,设备就会从服务器下载新固件。
  6. 验证与安装:下载完成后,设备验证固件的完整性和正确性,然后安装到备用分区。
  7. 重启与验证:设备重启后从新固件分区启动,并进行最终的验证。

实现

1.配置分区文件

在项目目录下新建partitions.csv,然后将下面内容复制进去

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 2M,
app1,     app,  ota_1,   ,0x1f0000,

修改.cargo/config.toml

在runner后面指定分区表

[build]
target = "riscv32imc-esp-espidf"

[target.riscv32imc-esp-espidf]
linker = "ldproxy"
# 在后面指定分区表
runner = "espflash flash --monitor --partition-table=partitions.csv"

# ...

2.制作镜像文件

将上一节的蓝牙服务制作为进行

# 编译
cargo build --release --bin ble_server
# 制作镜像
espflash save-image --chip esp32c3 target/riscv32imc-esp-espidf/release/ble_server ble_server.bin

image-20240802105123681.png

然后可以在项目目录下看到文件ble_server.bin

3.安装插件和查看本机IP

推荐安装Live Server VsCode扩展,能快速启动一个本地文件服务。

image-20240802110016512.png

image-20240802110049290.png

然后点击Go Live就能在端口5500开启一个文件服务。

获取wifi网卡在局域网的IP地址

ipconfig

image-20240802110530609.png

访问http://192.168.88.235:5500出现下面页面表示成功。

image-20240802110232596.png

4.编写OTA服务

新建文件src/bin/ota.rs

use std::sync::{ mpsc::{ channel, Sender }, Arc, Condvar, Mutex };

use anyhow::anyhow;
use embedded_svc::http::{ client::Client, Headers };
use esp32_nimble::{ utilities::BleUuid, BLEAdvertisementData, NimbleProperties };
use esp_idf_svc::{
    http::{ client::{ Configuration, EspHttpConnection }, Method },
    ota::{ EspOta, FirmwareInfo },
};

// 配置结构体,用于读取配置文件
#[toml_cfg::toml_config]
#[derive(Debug)]
pub struct Config {
    // 默认SSID和密码
    #[default("Wokwi-GUEST")]
    wifi_ssid: &'static str,
    #[default("")]
    wifi_psk: &'static str,
}

// 常量定义,用于固件下载的chunk大小、最大和最小尺寸
const FIRMWARE_DOWNLOAD_CHUNK_SIZE: usize = 1024 * 20;
const FIRMWARE_MAX_SIZE: usize = 1024 * 1024;
const FIRMWARE_MIN_SIZE: usize = size_of::<FirmwareInfo>() + 1024;

// 主函数,程序的入口点
fn main() -> anyhow::Result<()> {
    // 初始化系统循环、外设和NVS闪存
    let (sysloop, peripherals, nvs) = rust_embedded_study::init()?;

    // 连接WiFi
    let _wifi = rust_embedded_study::wifi::connect_wifi(
        CONFIG.wifi_ssid,
        &CONFIG.wifi_psk,
        peripherals.modem,
        sysloop,
        nvs
    )?;

    // 初始化BLE设备和广告
    let device = esp32_nimble::BLEDevice::take();
    let advertising = device.get_advertising();

    // 获取BLE服务器
    let server = device.get_server();
    // 配置BLE连接时的回调函数
    server.on_connect(|server, desc| {
        log::info!("on_connect: {:#?}", desc);
        server.update_conn_params(desc.conn_handle(), 24, 48, 0, 60).unwrap();
        if server.connected_count() < (esp_idf_svc::sys::CONFIG_BT_NIMBLE_MAX_CONNECTIONS as _) {
            advertising.lock().start().unwrap();
        }
    });

    // 配置BLE断开连接时的回调函数
    server.on_disconnect(|desc, reason| {
        log::warn!("on_disconnect: {:#?}, reason: {:#?}", desc, reason)
    });

    // 创建BLE服务和OTA特性
    let services = server.create_service(BleUuid::from_uuid16(0x8849));
    let ota_characteristic = services
        .lock()
        .create_characteristic(
            BleUuid::from_uuid16(0xffa1),
            NimbleProperties::WRITE | NimbleProperties::NOTIFY
        );
    // 创建消息通道
    let (tx, rx) = channel::<(usize, usize)>();

    // 创建共享状态
    let state = Arc::new((Mutex::new(false), Condvar::new()));

    // 克隆状态,用于OTA写入时的通知
    let state_clone = state.clone();
    // 配置OTA特性的写入回调
    ota_characteristic
        .lock()
        .on_write(move |args| {
            let data = args.recv_data();
            if data[0] == 1 {
                let mut is_start = state_clone.0.lock().unwrap();
                *is_start = true;
                state_clone.1.notify_all();
            }
            if data[0] == 2 {
                unsafe {
                    esp_idf_svc::sys::esp_restart();
                }
            }
        })
        .create_2904_descriptor();

    // 创建线程进行OTA更新
    std::thread::spawn(move || {
        let (lock, condvar) = &*state;
        let mut is_start = lock.lock().unwrap();
        while !*is_start {
            is_start = condvar.wait(is_start).unwrap();
        }
        log::warn!("Start OTA");
      	// 注意将其中的URL替换成您的本地IP地址。
        firmware("http://192.168.88.235:5500/ble_server.bin", tx)
    });

    // 配置广告数据并启动广告
    advertising
        .lock()
        .set_data(
            BLEAdvertisementData::new()
                .name("ESP32_OTA")
                .add_service_uuid(BleUuid::from_uuid16(0x8849))
        )?;
    advertising.lock().start()?;
    // 打印蓝牙服务相关日志
    server.ble_gatts_show_local();

    // 接收并处理下载进度
    while let Ok((read_len, file_size)) = rx.recv() {
        let percent = (read_len * 100) / file_size;
        ota_characteristic
            .lock()
            .set_value(&[percent as u8])
            .notify();
    }

    Ok(()) 
}

// 固件下载和更新函数
fn firmware(uri: &str, tx: Sender<(usize, usize)>) -> anyhow::Result<()> {
    // 初始化HTTP客户端
    let mut client = Client::wrap(
        EspHttpConnection::new(
            &(Configuration {
                buffer_size: Some(1024 * 4),
                ..Default::default()
            })
        )?
    );
    // 准备下载请求
    let request = client.request(Method::Get, uri, &[("Accept", "application/octet-stream")])?;
    let mut response = request.submit()?;
    // 检查响应状态
    if response.status() != 200 {
        return Err(anyhow!("Firmware download failed: {}", response.status()));
    }
    // 获取文件大小并进行校验
    let file_size = response.content_len().unwrap_or(0) as usize;
    if file_size <= FIRMWARE_MIN_SIZE {
        return Err(
            anyhow!(
                "File size is {file_size}, too small to be a firmware! No need to proceed further."
            )
        );
    }
    if file_size > FIRMWARE_MAX_SIZE {
        return Err(anyhow!("File is too big ({file_size} bytes)."));
    }
    log::warn!("file size: {file_size}");
    // 初始化OTA更新
    let mut ota = EspOta::new()?;
    let mut buff = vec![0;FIRMWARE_DOWNLOAD_CHUNK_SIZE];
    let mut total_read_len = 0usize;
    // 开始OTA更新
    let mut work = ota.initiate_update()?;
    // 循环读取和写入数据
    let dl_result = loop {
        let n = response.read(&mut buff)?;
        total_read_len += n;
        tx.send((total_read_len, file_size))?;

        if n > 0 {
            if let Err(e) = work.write(&buff[..n]) {
                log::error!("Failed to write to OTA. {e}");
                break Err(anyhow!(e));
            }
        }
        if total_read_len >= file_size {
            break Ok(());
        }
    };
    // 检查下载结果并相应处理
    if dl_result.is_err() {
        return Ok(work.abort()?);
    }
    if total_read_len < file_size {
        log::error!(
            "Supposed to download {file_size} bytes, but we could only get {total_read_len}. May be network error?"
        );
        return Ok(work.abort()?);
    }
    // 完成OTA更新
    work.complete()?;
    log::info!("OTA done!");
    Ok(())
}

主要功能:

  1. 初始化系统环境

    • 初始化系统循环、外设和 NVS 闪存。
  2. 连接 WiFi

    • 根据配置文件中的 SSID 和密码连接到 WiFi 网络。
  3. 配置 BLE (Bluetooth Low Energy)

    • 设置 BLE 广告数据和服务。
    • 配置 BLE 服务器上的连接和断开连接事件处理。
    • 创建一个 OTA 特性(characteristic),用于接收 OTA 更新的启动信号和重启指令。
  4. OTA 更新逻辑

    • 创建一个线程来等待 OTA 更新的启动信号。
    • 从指定的 HTTP 服务器下载新的固件文件。
    • 验证固件文件的大小。
    • 向 OTA 特性发送下载进度通知。
    • 将固件数据写入 OTA 更新工作区。
    • 如果下载成功,则完成 OTA 更新;如果失败,则中止更新。
  5. 主循环

    • 启动 BLE 广告。
    • 监听 OTA 特性上的写入事件。
    • 在接收到启动信号后,开始 OTA 更新过程。
    • 在接收到重启指令后,重启设备。

详细说明

  • BLE OTA 特性:BLE 服务中创建了一个 OTA 特性,当该特性接收到值 1 时,会通知主线程开始 OTA 更新;当接收到值 2 时,设备会重启。

  • OTA 更新线程:在主线程中创建了一个子线程,该线程等待来自 OTA 特性的启动信号。一旦接收到信号,它会调用 firmware 函数开始下载固件。

  • 固件下载firmware 函数使用 ESP32 的 HTTP 客户端库来下载固件文件。下载过程中,它会验证固件文件的大小是否在允许范围内,并通过 BLE 特性发送进度给外部设备。

  • 进度通知:下载过程中,通过 BLE 特性向外部设备发送百分比进度。

总结

通过上述代码,我们成功实现了 ESP32C3 微控制器的 OTA 更新功能。这一实现整合了 WiFi 连接、BLE 通信和 HTTP 客户端,使固件能够远程下载和更新。这样的设计让外部设备(例如智能手机)能够通过 BLE 控制固件的下载和更新流程。

5.写入程序

擦除flash

espflash erase-flash

将程序写入到esp32c3中

cargo run --bin ota

6.使用BLE调试助手进行调试

Screenrecording_2024 -original-original

最后

🎉恭喜您成功实现了OTA功能。您所探索的代码可以在GitHub仓库yexiyue/rust-embedded-study中找到。

如果您觉得不错,请点个关注或者收藏,感谢您的支持。