From 46512a5ee7be723b7f65d81f9c7d2d9cc03668aa Mon Sep 17 00:00:00 2001 From: Sergio Gasquez Date: Wed, 25 Feb 2026 10:27:31 +0100 Subject: [PATCH] feat: Add an ota interval check, add rgb led status --- project/part5/Cargo.lock | 41 ++++++++++++++++++++++++++ project/part5/Cargo.toml | 2 ++ project/part5/src/button.rs | 21 ------------- project/part5/src/main.rs | 20 +++++-------- project/part5/src/ota.rs | 51 ++++++++++++++++++++------------ project/part5/src/status_led.rs | 52 +++++++++++++++++++++++++++++++++ 6 files changed, 135 insertions(+), 52 deletions(-) delete mode 100644 project/part5/src/button.rs create mode 100644 project/part5/src/status_led.rs diff --git a/project/part5/Cargo.lock b/project/part5/Cargo.lock index 89c2743..674d3b9 100644 --- a/project/part5/Cargo.lock +++ b/project/part5/Cargo.lock @@ -704,6 +704,18 @@ dependencies = [ "termcolor", ] +[[package]] +name = "esp-hal-smartled" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23eaae863a73f9bd1626687f539ff38942b442ae3974838ceeae5e932ddf0a2a" +dependencies = [ + "document-features", + "esp-hal", + "rgb", + "smart-leds-trait", +] + [[package]] name = "esp-metadata-generated" version = "0.3.0" @@ -1176,6 +1188,7 @@ dependencies = [ "esp-backtrace", "esp-bootloader-esp-idf", "esp-hal", + "esp-hal-smartled", "esp-println", "esp-radio", "esp-rtos", @@ -1186,6 +1199,7 @@ dependencies = [ "serde", "serde-json-core", "shtcx", + "smart-leds", "static_cell", ] @@ -1344,6 +1358,15 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +dependencies = [ + "bytemuck", +] + [[package]] name = "riscv" version = "0.15.0" @@ -1508,6 +1531,24 @@ dependencies = [ "embedded-hal-async", ] +[[package]] +name = "smart-leds" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66df34e571fa9993fa6f99131a374d58ca3d694b75f9baac93458fe0d6057bf0" +dependencies = [ + "smart-leds-trait", +] + +[[package]] +name = "smart-leds-trait" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f4441a131924d58da6b83a7ad765c460e64630cce504376c3a87a2558c487f" +dependencies = [ + "rgb", +] + [[package]] name = "smoltcp" version = "0.12.0" diff --git a/project/part5/Cargo.toml b/project/part5/Cargo.toml index 4a0b00d..f41b50c 100644 --- a/project/part5/Cargo.toml +++ b/project/part5/Cargo.toml @@ -35,6 +35,7 @@ esp-backtrace = { version = "0.18.1", features = [ ] } esp-bootloader-esp-idf = { version = "0.4.0", features = ["esp32c3"] } esp-hal = { version = "1.0.0", features = ["esp32c3", "unstable"] } +esp-hal-smartled = { version = "0.17.0", features = ["esp32c3"] } esp-println = { version = "0.16.1", features = ["esp32c3", "log-04"] } esp-radio = { version = "0.17.0", features = ["esp32c3", "unstable", "wifi"] } esp-rtos = { version = "0.2.0", features = ["embassy", "esp-radio", "esp32c3"] } @@ -45,6 +46,7 @@ rust-mqtt = { version = "0.3.0", default-features = false } serde = { version = "1.0.228", default-features = false, features = ["derive"] } serde-json-core = "0.6.0" shtcx = { git = "https://github.com/Fristi/shtcx-rs", branch = "feature/async-support" } +smart-leds = "0.4" static_cell = "2.1.1" [profile.dev] diff --git a/project/part5/src/button.rs b/project/part5/src/button.rs deleted file mode 100644 index 0f50867..0000000 --- a/project/part5/src/button.rs +++ /dev/null @@ -1,21 +0,0 @@ -use embassy_sync::signal::Signal; -use esp_hal::gpio::Input; -use log::debug; - -pub static BUTTON_PRESSED: Signal = - Signal::new(); - -#[embassy_executor::task] -pub async fn button_monitor( - mut button: Input<'static>, - button_pressed: &'static Signal, -) { - debug!("Button monitor: Waiting for button press..."); - - loop { - // Wait for falling edge (button press - goes from high to low due to pull-up) - button.wait_for_falling_edge().await; - log::info!("Button pressed!"); - button_pressed.signal(()); - } -} diff --git a/project/part5/src/main.rs b/project/part5/src/main.rs index 011fbca..6656b9a 100644 --- a/project/part5/src/main.rs +++ b/project/part5/src/main.rs @@ -9,9 +9,9 @@ // Host IP will be printed by xtask command, but if auto-detect fails, you can find it manually by running: // ipconfig getifaddr en0 or ip addr show eth0 // HOST_IP="" cargo r -r -// 5. Join the AP network and navigate to http:/// the wifi credentials -// Once the device stops the AP mode and starts the STA mode connected to the wifi, it will start sending sensor data to the MQTT broker and wait for the button press to trigger OTA update. -// 6. Press the button to trigger OTA update. Ctrl+R to reset the device after the firmware is downloaded. +// 5. Join the AP network and navigate to http:/// to set the wifi credentials. +// Once the device stops the AP mode and starts the STA mode connected to the wifi, it will start sending sensor data to the MQTT broker and periodically check for OTA updates. +// 6. Optional: configure the OTA interval with `OTA_CHECK_INTERVAL_SECS=` (default: 300). #![no_std] #![no_main] @@ -22,12 +22,12 @@ )] #![deny(clippy::large_stack_frames)] -mod button; mod http; mod mqtt; mod network; mod ota; mod sensor; +mod status_led; use core::net::Ipv4Addr; use core::str::FromStr; @@ -39,7 +39,6 @@ use embassy_time::{Duration as EmbassyDuration, Timer}; use esp_alloc as _; use esp_backtrace as _; use esp_hal::{ - gpio::{Input, InputConfig}, i2c::master::{Config, I2c}, interrupt::software::SoftwareInterruptControl, ram, @@ -48,13 +47,13 @@ use esp_hal::{ use esp_radio::Controller; use log::{debug, info}; -use crate::button::{BUTTON_PRESSED, button_monitor}; use crate::http::{run_captive_portal, run_dhcp, run_http_server}; use crate::mqtt::mqtt_task; use crate::network::{ NetworkStacks, WifiCredentials, connection, create_network_stacks, net_task, sta_net_task, }; use crate::ota::{FLASH_STORAGE, http_client_task}; +use crate::status_led::{LED_STATUS, LedStatus, status_led_task}; use shtcx::asynchronous::shtc3; esp_bootloader_esp_idf::esp_app_desc!(); @@ -91,10 +90,7 @@ async fn main(spawner: Spawner) -> ! { .into_async(); let sht = shtc3(i2c); - // Set up button on GPIO9 (BOOT button on ESP32-C3) - let button_pin = peripherals.GPIO9; - let config = InputConfig::default(); - let button = Input::new(button_pin, config); + LED_STATUS.signal(LedStatus::Provisioning); // Initialize WiFi radio static ESP_RADIO_CTRL_CELL: static_cell::StaticCell> = @@ -136,9 +132,9 @@ async fn main(spawner: Spawner) -> ! { spawner.spawn(run_dhcp(ap_stack, gw_ip_addr)).ok(); spawner.spawn(run_captive_portal(ap_stack, gw_ip_addr)).ok(); spawner.spawn(mqtt_task(sta_stack, sht)).ok(); - spawner.spawn(button_monitor(button, &BUTTON_PRESSED)).ok(); + spawner.spawn(http_client_task(sta_stack)).ok(); spawner - .spawn(http_client_task(sta_stack, &BUTTON_PRESSED)) + .spawn(status_led_task(peripherals.RMT, peripherals.GPIO2)) .ok(); // Wait for AP link to come up diff --git a/project/part5/src/ota.rs b/project/part5/src/ota.rs index 275ce8a..7055325 100644 --- a/project/part5/src/ota.rs +++ b/project/part5/src/ota.rs @@ -7,13 +7,27 @@ use embedded_storage::Storage; use esp_storage::FlashStorage; use log::{debug, error, info}; +use crate::status_led::{LED_STATUS, LedStatus}; + const HOST_IP: Option<&'static str> = option_env!("HOST_IP"); +const OTA_CHECK_INTERVAL_SECS: Option<&'static str> = option_env!("OTA_CHECK_INTERVAL_SECS"); pub static FLASH_STORAGE: Mutex< embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, Option>, > = Mutex::new(None); +fn ota_check_interval() -> EmbassyDuration { + const DEFAULT_SECS: u64 = 300; + + let secs = OTA_CHECK_INTERVAL_SECS + .and_then(|raw| raw.parse::().ok()) + .filter(|secs: &u64| *secs > 0) + .unwrap_or(DEFAULT_SECS); + + EmbassyDuration::from_secs(secs) +} + async fn download_and_flash_firmware( stack: Stack<'static>, host_ip_str: &str, @@ -179,7 +193,7 @@ async fn download_and_flash_firmware( } } - info!("HTTP Client: OTA update complete! Please reset the device."); + info!("HTTP Client: OTA update complete"); return Ok(()); } } @@ -192,13 +206,7 @@ async fn download_and_flash_firmware( } #[embassy_executor::task] -pub async fn http_client_task( - stack: Stack<'static>, - button_pressed: &'static embassy_sync::signal::Signal< - embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, - (), - >, -) { +pub async fn http_client_task(stack: Stack<'static>) { debug!("HTTP Client: Task started!"); // Wait for WiFi connection debug!("HTTP Client: Waiting for WiFi connection..."); @@ -206,6 +214,7 @@ pub async fn http_client_task( // Wait for network to be configured (which means WiFi is connected) stack.wait_config_up().await; debug!("HTTP Client: Network configured, WiFi is connected"); + LED_STATUS.signal(LedStatus::Idle); // Wait for network to be fully ready debug!("HTTP Client: Waiting for network to stabilize..."); @@ -215,20 +224,21 @@ pub async fn http_client_task( debug!("HTTP Client: Got IP address: {}", config.address); } - debug!("HTTP Client: Ready, waiting for button press..."); + let ota_interval = ota_check_interval(); + info!( + "HTTP Client: Periodic OTA checks enabled (every {}s)", + ota_interval.as_secs() + ); loop { - // Wait for button press signal - debug!("HTTP Client: Waiting for BUTTON_PRESSED signal..."); - button_pressed.wait().await; - debug!("HTTP Client: Button pressed signal received! Starting firmware download..."); + debug!("HTTP Client: Checking for firmware update..."); // Get host IP from environment variable let host_ip_str = match HOST_IP { Some(ip) => ip, None => { debug!("HTTP Client: HOST_IP not set, skipping OTA update"); - Timer::after(EmbassyDuration::from_millis(100)).await; + Timer::after(ota_interval).await; continue; } }; @@ -236,20 +246,23 @@ pub async fn http_client_task( Ok(ipv4) => IpAddress::Ipv4(ipv4), Err(_) => { debug!("HTTP Client: Invalid HOST_IP format: {}", host_ip_str); - Timer::after(EmbassyDuration::from_millis(100)).await; + Timer::after(ota_interval).await; continue; } }; - // Attempt firmware download - if successful, break out of loop + // Attempt firmware download - reset if successful + LED_STATUS.signal(LedStatus::Updating); + if download_and_flash_firmware(stack, host_ip_str, address, &FLASH_STORAGE) .await .is_ok() { - break; + info!("HTTP Client: Rebooting into updated firmware"); + esp_hal::system::software_reset(); } - // Small delay before waiting for next button press - Timer::after(EmbassyDuration::from_millis(100)).await; + LED_STATUS.signal(LedStatus::Idle); + Timer::after(ota_interval).await; } } diff --git a/project/part5/src/status_led.rs b/project/part5/src/status_led.rs new file mode 100644 index 0000000..55189dc --- /dev/null +++ b/project/part5/src/status_led.rs @@ -0,0 +1,52 @@ +use embassy_sync::signal::Signal; +use esp_hal::{rmt::Rmt, time::Rate}; +use esp_hal_smartled::{SmartLedsAdapterAsync, buffer_size_async}; +use smart_leds::{RGB8, SmartLedsWriteAsync, brightness, gamma}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LedStatus { + Provisioning, + Idle, + Updating, +} + +pub static LED_STATUS: Signal< + embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex, + LedStatus, +> = Signal::new(); + +#[embassy_executor::task] +pub async fn status_led_task( + rmt: esp_hal::peripherals::RMT<'static>, + gpio2: esp_hal::peripherals::GPIO2<'static>, +) { + let rmt: Rmt<'_, esp_hal::Async> = { + let frequency: Rate = Rate::from_mhz(80); + Rmt::new(rmt, frequency) + } + .expect("Failed to initialize RMT") + .into_async(); + + let rmt_channel = rmt.channel0; + let mut rmt_buffer = [esp_hal::rmt::PulseCode::default(); buffer_size_async(1)]; + let mut led = SmartLedsAdapterAsync::new(rmt_channel, gpio2, &mut rmt_buffer); + let level = 10; + + // Default state before any signal arrives. + led.write(brightness(gamma([RGB8::new(0, 255, 0)].into_iter()), level)) + .await + .unwrap(); + + loop { + let status = LED_STATUS.wait().await; + let color = match status { + LedStatus::Provisioning => RGB8::new(0, 255, 0), + LedStatus::Idle => RGB8::new(0, 0, 0), + LedStatus::Updating => RGB8::new(0, 0, 255), + }; + + led.write(brightness(gamma([color].into_iter()), level)) + .await + .unwrap(); + } +}