Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions project/part5/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions project/part5/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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]
Expand Down
21 changes: 0 additions & 21 deletions project/part5/src/button.rs

This file was deleted.

20 changes: 8 additions & 12 deletions project/part5/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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="<IP>" cargo r -r
// 5. Join the AP network and navigate to http://<MCU_IP>/ 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://<MCU_IP>/ 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=<seconds>` (default: 300).

#![no_std]
#![no_main]
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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!();
Expand Down Expand Up @@ -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<Controller<'static>> =
Expand Down Expand Up @@ -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
Expand Down
51 changes: 32 additions & 19 deletions project/part5/src/ota.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FlashStorage<'static>>,
> = Mutex::new(None);

fn ota_check_interval() -> EmbassyDuration {
const DEFAULT_SECS: u64 = 300;

let secs = OTA_CHECK_INTERVAL_SECS
.and_then(|raw| raw.parse::<u64>().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,
Expand Down Expand Up @@ -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(());
}
}
Expand All @@ -192,20 +206,15 @@ 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...");

// 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...");
Expand All @@ -215,41 +224,45 @@ 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;
}
};
let address = match host_ip_str.parse::<Ipv4Address>() {
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;
}
}
52 changes: 52 additions & 0 deletions project/part5/src/status_led.rs
Original file line number Diff line number Diff line change
@@ -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();
}
}