use core::fmt;
#[derive(Clone)]
pub struct OtaConfig {
pub server_url: heapless::String<128>,
pub current_version: heapless::String<16>,
pub wifi_ssid: heapless::String<32>,
pub wifi_password: heapless::String<64>,
pub check_interval_secs: u32,
pub auto_update: bool,
}
impl Default for OtaConfig {
fn default() -> Self {
Self {
server_url: heapless::String::new(),
current_version: heapless::String::try_from("0.2.1").unwrap_or_default(),
wifi_ssid: heapless::String::new(),
wifi_password: heapless::String::new(),
check_interval_secs: 3600, auto_update: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OtaState {
Idle,
Checking,
UpdateAvailable,
Downloading,
Verifying,
Applying,
Complete,
Failed,
}
impl fmt::Display for OtaState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OtaState::Idle => write!(f, "Idle"),
OtaState::Checking => write!(f, "Checking"),
OtaState::UpdateAvailable => write!(f, "Update Available"),
OtaState::Downloading => write!(f, "Downloading"),
OtaState::Verifying => write!(f, "Verifying"),
OtaState::Applying => write!(f, "Applying"),
OtaState::Complete => write!(f, "Complete"),
OtaState::Failed => write!(f, "Failed"),
}
}
}
#[derive(Clone)]
pub struct UpdateInfo {
pub version: heapless::String<16>,
pub size: u32,
pub checksum: heapless::String<64>,
pub notes: heapless::String<256>,
pub download_url: heapless::String<256>,
}
#[derive(Debug, Clone, Copy)]
pub enum OtaError {
WifiError,
HttpError,
InvalidResponse,
ChecksumMismatch,
InsufficientSpace,
FlashError,
VerificationFailed,
NoUpdate,
AlreadyUpToDate,
}
impl fmt::Display for OtaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OtaError::WifiError => write!(f, "WiFi connection failed"),
OtaError::HttpError => write!(f, "HTTP request failed"),
OtaError::InvalidResponse => write!(f, "Invalid server response"),
OtaError::ChecksumMismatch => write!(f, "Checksum verification failed"),
OtaError::InsufficientSpace => write!(f, "Not enough storage space"),
OtaError::FlashError => write!(f, "Flash write error"),
OtaError::VerificationFailed => write!(f, "Update verification failed"),
OtaError::NoUpdate => write!(f, "No update available"),
OtaError::AlreadyUpToDate => write!(f, "Already up to date"),
}
}
}
pub type ProgressCallback = fn(downloaded: u32, total: u32);
pub struct OtaManager {
config: OtaConfig,
state: OtaState,
progress: u32,
last_error: Option<OtaError>,
update_info: Option<UpdateInfo>,
}
impl OtaManager {
pub fn new(config: OtaConfig) -> Self {
Self {
config,
state: OtaState::Idle,
progress: 0,
last_error: None,
update_info: None,
}
}
pub fn state(&self) -> OtaState {
self.state
}
pub fn progress(&self) -> u32 {
self.progress
}
pub fn last_error(&self) -> Option<OtaError> {
self.last_error
}
pub fn update_info(&self) -> Option<&UpdateInfo> {
self.update_info.as_ref()
}
pub fn check_for_update(&mut self) -> Result<bool, OtaError> {
self.state = OtaState::Checking;
self.last_error = None;
let server_version = "0.2.2";
if self.is_newer_version(server_version) {
self.update_info = Some(UpdateInfo {
version: heapless::String::try_from(server_version).unwrap_or_default(),
size: 512 * 1024, checksum: heapless::String::try_from(
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
).unwrap_or_default(),
notes: heapless::String::try_from("Performance improvements and bug fixes").unwrap_or_default(),
download_url: heapless::String::try_from(
"https://github.com/ruvnet/ruvector/releases/latest/download/ruvllm-esp32"
).unwrap_or_default(),
});
self.state = OtaState::UpdateAvailable;
Ok(true)
} else {
self.state = OtaState::Idle;
self.last_error = Some(OtaError::AlreadyUpToDate);
Ok(false)
}
}
fn is_newer_version(&self, server_version: &str) -> bool {
let current = self.parse_version(self.config.current_version.as_str());
let server = self.parse_version(server_version);
server > current
}
fn parse_version(&self, version: &str) -> (u32, u32, u32) {
let mut parts = version.split('.');
let major = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
let minor = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
let patch = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
(major, minor, patch)
}
pub fn download_update(&mut self, _progress_cb: Option<ProgressCallback>) -> Result<(), OtaError> {
if self.state != OtaState::UpdateAvailable {
return Err(OtaError::NoUpdate);
}
self.state = OtaState::Downloading;
self.progress = 0;
let total_size = self.update_info.as_ref().map(|i| i.size).unwrap_or(0);
for i in 0..=100 {
self.progress = i;
if let Some(cb) = _progress_cb {
cb(i * total_size / 100, total_size);
}
}
self.state = OtaState::Verifying;
Ok(())
}
pub fn verify_update(&mut self) -> Result<(), OtaError> {
if self.state != OtaState::Verifying {
return Err(OtaError::VerificationFailed);
}
self.state = OtaState::Complete;
Ok(())
}
pub fn apply_update(&mut self) -> Result<(), OtaError> {
if self.state != OtaState::Complete {
return Err(OtaError::VerificationFailed);
}
self.state = OtaState::Applying;
Ok(())
}
pub fn rollback(&mut self) -> Result<(), OtaError> {
self.state = OtaState::Idle;
Ok(())
}
pub fn status_string(&self) -> &'static str {
match self.state {
OtaState::Idle => "Ready",
OtaState::Checking => "Checking for updates...",
OtaState::UpdateAvailable => "Update available!",
OtaState::Downloading => "Downloading update...",
OtaState::Verifying => "Verifying firmware...",
OtaState::Applying => "Applying update...",
OtaState::Complete => "Update complete! Reboot to apply.",
OtaState::Failed => "Update failed",
}
}
}
pub fn handle_ota_command(manager: &mut OtaManager, command: &str) -> heapless::String<256> {
let mut response = heapless::String::new();
let parts: heapless::Vec<&str, 4> = command.split_whitespace().collect();
let cmd = parts.first().copied().unwrap_or("");
match cmd {
"status" => {
let _ = core::fmt::write(
&mut response,
format_args!("OTA Status: {} ({}%)", manager.status_string(), manager.progress())
);
}
"check" => {
match manager.check_for_update() {
Ok(true) => {
if let Some(info) = manager.update_info() {
let _ = core::fmt::write(
&mut response,
format_args!("Update available: v{} ({}KB)", info.version, info.size / 1024)
);
}
}
Ok(false) => {
let _ = response.push_str("Already up to date");
}
Err(e) => {
let _ = core::fmt::write(&mut response, format_args!("Check failed: {}", e));
}
}
}
"download" => {
match manager.download_update(None) {
Ok(()) => {
let _ = response.push_str("Download complete");
}
Err(e) => {
let _ = core::fmt::write(&mut response, format_args!("Download failed: {}", e));
}
}
}
"apply" => {
let _ = manager.verify_update();
match manager.apply_update() {
Ok(()) => {
let _ = response.push_str("Rebooting to apply update...");
}
Err(e) => {
let _ = core::fmt::write(&mut response, format_args!("Apply failed: {}", e));
}
}
}
"rollback" => {
match manager.rollback() {
Ok(()) => {
let _ = response.push_str("Rolling back to previous firmware...");
}
Err(e) => {
let _ = core::fmt::write(&mut response, format_args!("Rollback failed: {}", e));
}
}
}
_ => {
let _ = response.push_str("OTA commands: status, check, download, apply, rollback");
}
}
response
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_comparison() {
let config = OtaConfig {
current_version: heapless::String::try_from("0.2.1").unwrap(),
..Default::default()
};
let manager = OtaManager::new(config);
assert!(manager.is_newer_version("0.2.2"));
assert!(manager.is_newer_version("0.3.0"));
assert!(manager.is_newer_version("1.0.0"));
assert!(!manager.is_newer_version("0.2.1"));
assert!(!manager.is_newer_version("0.2.0"));
assert!(!manager.is_newer_version("0.1.0"));
}
#[test]
fn test_state_transitions() {
let config = OtaConfig::default();
let mut manager = OtaManager::new(config);
assert_eq!(manager.state(), OtaState::Idle);
let _ = manager.check_for_update();
assert!(matches!(manager.state(), OtaState::UpdateAvailable | OtaState::Idle));
}
}