Compare commits

...

2 commits

Author SHA1 Message Date
Brent Schroeter
5b58040975 move time zone selection to config.rs 2025-09-06 12:59:55 -07:00
Brent Schroeter
1970d37d51 refactor setup tasks 2025-09-06 12:59:55 -07:00
4 changed files with 146 additions and 92 deletions

View file

@ -9,6 +9,9 @@ pub(crate) const T_ON: &'static str = "17:00";
/// End of "on" cycle for switch, in 24 hour "HH:MM" format. /// End of "on" cycle for switch, in 24 hour "HH:MM" format.
pub(crate) const T_OFF: &'static str = "09:00"; pub(crate) const T_OFF: &'static str = "09:00";
/// Time zone for T_ON and T_OFF.
pub(crate) const TZ: chrono_tz::Tz = chrono_tz::US::Pacific;
/// Access point SSID. /// Access point SSID.
pub(crate) const WIFI_SSID: &'static str = "Example"; pub(crate) const WIFI_SSID: &'static str = "Example";

View file

@ -2,29 +2,38 @@ use std::default::Default;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use chrono::{NaiveTime, Utc}; use chrono::{NaiveTime, Utc};
use chrono_tz::US::Pacific;
use esp_idf_svc::{ use esp_idf_svc::{
eventloop::EspSystemEventLoop, eventloop::EspSystemEventLoop,
hal::{delay::FreeRtos, gpio::*, modem::Modem, peripheral::Peripheral, prelude::Peripherals}, hal::{
sntp::{EspSntp, SntpConf, SyncMode, SyncStatus}, delay::FreeRtos,
wifi::{AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi}, gpio::{self, Gpio7, OutputPin, PinDriver},
prelude::Peripherals,
},
sntp::{EspSntp, SntpConf, SyncMode},
wifi::EspWifi,
}; };
use log::info; use log::info;
mod config; mod config;
mod ntp;
mod wifi;
const SNTP_STATUS_POLL_INTVL_MS: u32 = 2000;
/// Delay between executions of the main control loop, in milliseconds. /// Delay between executions of the main control loop, in milliseconds.
const CONTROL_LOOP_INTVL: u32 = 60000; const CONTROL_LOOP_INTVL: u32 = 60000;
fn main() -> Result<()> { /// Struct holding resources which should remain in scope throughout execution.
// It is necessary to call this function once. Otherwise some patches to struct AppState<P: OutputPin> {
// the runtime implemented by esp-idf-sys might not link properly. Refer ntp: EspSntp<'static>,
// to https://github.com/esp-rs/esp-idf-template/issues/71. switch_driver: PinDriver<'static, P, gpio::Output>,
esp_idf_svc::sys::link_patches(); wifi: EspWifi<'static>,
}
// Bind the log crate to the ESP Logging facilities fn main() -> Result<()> {
esp_idf_svc::log::EspLogger::initialize_default(); let AppState::<_> {
ntp: _ntp,
mut switch_driver,
wifi: _wifi,
} = *setup()?;
let t_on = NaiveTime::parse_from_str(config::T_ON, "%H:%M")?; let t_on = NaiveTime::parse_from_str(config::T_ON, "%H:%M")?;
let t_off = NaiveTime::parse_from_str(config::T_OFF, "%H:%M")?; let t_off = NaiveTime::parse_from_str(config::T_OFF, "%H:%M")?;
@ -32,40 +41,14 @@ fn main() -> Result<()> {
bail!("t_on and t_off must have distinct values"); bail!("t_on and t_off must have distinct values");
} }
let peripherals = Peripherals::take()?;
let mut switch = PinDriver::output(peripherals.pins.gpio7)?;
switch.set_low()?;
let sysloop = EspSystemEventLoop::take()?;
// Dropping EspWifi shuts down the connection, so this variable must remain
// in scope.
let _wifi = init_wifi(peripherals.modem, sysloop);
// SNTP client will continue running in the background until this value is
// dropped.
let ntp = EspSntp::new(&SntpConf {
servers: [config::SNTP_SERVER],
sync_mode: SyncMode::Smooth,
..Default::default()
})?;
// get_sync_status() returns "Completed" one time and then reverts to
// "Reset" on subsequent calls.
while ntp.get_sync_status() != SyncStatus::Completed {
info!("Waiting for SNTP to sync...");
FreeRtos::delay_ms(SNTP_STATUS_POLL_INTVL_MS);
}
// ======== Main Control Loop ======== // // ======== Main Control Loop ======== //
loop { loop {
let now = Utc::now().with_timezone(&Pacific); let dt = Utc::now().with_timezone(&config::TZ);
info!("Current time: {}", now); info!("Current time: {}", dt);
let t = now.time(); let t = dt.time();
let active = match (t_on < t_off, t > t_on, t > t_off) { let switch_active = match (t_on < t_off, t > t_on, t > t_off) {
// Active period falls within single day, and t falls between t_on // Active period falls within single day, and t falls between t_on
// and t_off. // and t_off.
(true, true, false) => true, (true, true, false) => true,
@ -77,10 +60,10 @@ fn main() -> Result<()> {
_ => false, _ => false,
}; };
if active { if switch_active {
switch.set_high()?; switch_driver.set_high()?;
} else { } else {
switch.set_low()?; switch_driver.set_low()?;
} }
// TODO: enter low power mode ("light sleep" or "deep sleep") instead // TODO: enter low power mode ("light sleep" or "deep sleep") instead
@ -89,54 +72,40 @@ fn main() -> Result<()> {
} }
} }
/// Start WiFi module and connect to access point. Returns an error if either /// Runs initial application startup tasks, including connecting WiFi, syncing
/// of WiFi startup or connection fail. /// system time over SNTP, and setting the initial switch state. May block for
fn init_wifi( /// several seconds, or indefinitely if initial SNTP sync is not successful.
modem: impl Peripheral<P = Modem> + 'static, fn setup() -> Result<Box<AppState<Gpio7>>> {
sysloop: EspSystemEventLoop, // It is necessary to call this function once. Otherwise some patches to
) -> Result<Box<EspWifi<'static>>> { // the runtime implemented by esp-idf-sys might not link properly. Refer
let auth_method = AuthMethod::WPA2Personal; // to https://github.com/esp-rs/esp-idf-template/issues/71.
let mut esp_wifi = EspWifi::new(modem, sysloop.clone(), None)?; esp_idf_svc::sys::link_patches();
let mut wifi = BlockingWifi::wrap(&mut esp_wifi, sysloop)?;
wifi.set_configuration(&Configuration::Client(ClientConfiguration::default()))?;
info!("Starting wifi..."); // Bind the log crate to the ESP Logging facilities
wifi.start()?; esp_idf_svc::log::EspLogger::initialize_default();
info!("Scanning...");
let ap_infos = wifi.scan()?; let peripherals = Peripherals::take()?;
let ours = ap_infos.into_iter().find(|a| a.ssid == config::WIFI_SSID); let sysloop = EspSystemEventLoop::take()?;
let channel = if let Some(ours) = ours {
info!( let mut switch_driver = PinDriver::output(peripherals.pins.gpio7)?;
"Found configured access point {} on channel {}", switch_driver.set_low()?;
config::WIFI_SSID,
ours.channel // Dropping EspWifi shuts down the connection, so this variable must remain
); // in scope.
Some(ours.channel) let wifi = crate::wifi::init(peripherals.modem, sysloop)?;
} else {
info!( // SNTP client will continue running in the background until this value is
"Configured access point {} not found during scanning, will go with unknown channel", // dropped.
config::WIFI_SSID let ntp = EspSntp::new(&SntpConf {
); servers: [config::SNTP_SERVER],
None sync_mode: SyncMode::Smooth,
};
wifi.set_configuration(&Configuration::Client(ClientConfiguration {
ssid: config::WIFI_SSID
.try_into()
.expect("Could not parse the given SSID into WiFi config"),
password: config::WIFI_PASS
.try_into()
.expect("Could not parse the given password into WiFi config"),
channel,
auth_method,
..Default::default() ..Default::default()
}))?; })?;
crate::ntp::wait_for_sync(&ntp);
info!("Connecting wifi..."); Ok(Box::new(AppState::<_> {
wifi.connect()?; ntp,
info!("Waiting for DHCP lease..."); switch_driver,
wifi.wait_netif_up()?; wifi: *wifi,
let ip_info = wifi.wifi().sta_netif().get_ip_info()?; }))
info!("Wifi DHCP info: {:?}", ip_info);
Ok(Box::new(esp_wifi))
} }

20
src/ntp.rs Normal file
View file

@ -0,0 +1,20 @@
use esp_idf_svc::{
hal::delay::FreeRtos,
sntp::{EspSntp, SyncStatus},
};
use log::info;
/// Delay between checks of the initial NTP sync check, in milliseconds.
const SNTP_STATUS_POLL_INTVL: u32 = 2000;
/// Blocks until the SNTP client reports successful synchronization.
///
/// Note that the SNTP client's status flag is reset after it is read. Calling
/// this function twice will cause the second invocation to wait for the next
/// synchronization event.
pub(crate) fn wait_for_sync(client: &EspSntp) {
while client.get_sync_status() != SyncStatus::Completed {
info!("Waiting for SNTP to sync...");
FreeRtos::delay_ms(SNTP_STATUS_POLL_INTVL);
}
}

62
src/wifi.rs Normal file
View file

@ -0,0 +1,62 @@
use anyhow::Result;
use esp_idf_svc::{
eventloop::EspSystemEventLoop,
hal::{modem::Modem, peripheral::Peripheral},
wifi::{AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi},
};
use log::{debug, info};
use crate::config;
/// Start WiFi module and connect to access point. Returns an error if either
/// of WiFi startup or connection fail.
pub(crate) fn init(
modem: impl Peripheral<P = Modem> + 'static,
sysloop: EspSystemEventLoop,
) -> Result<Box<EspWifi<'static>>> {
let auth_method = AuthMethod::WPA2Personal;
let mut esp_wifi = EspWifi::new(modem, sysloop.clone(), None)?;
let mut wifi = BlockingWifi::wrap(&mut esp_wifi, sysloop)?;
wifi.set_configuration(&Configuration::Client(ClientConfiguration::default()))?;
info!("Starting wifi...");
wifi.start()?;
debug!("Scanning...");
let ap_infos = wifi.scan()?;
let ours = ap_infos.into_iter().find(|a| a.ssid == config::WIFI_SSID);
let channel = if let Some(ours) = ours {
debug!(
"Found configured access point {} on channel {}",
config::WIFI_SSID,
ours.channel
);
Some(ours.channel)
} else {
debug!(
"Configured access point {} not found during scanning, will go with unknown channel",
config::WIFI_SSID
);
None
};
wifi.set_configuration(&Configuration::Client(ClientConfiguration {
ssid: config::WIFI_SSID
.try_into()
.expect("Could not parse the given SSID into WiFi config"),
password: config::WIFI_PASS
.try_into()
.expect("Could not parse the given password into WiFi config"),
channel,
auth_method,
..Default::default()
}))?;
debug!("Connecting wifi...");
wifi.connect()?;
debug!("Waiting for DHCP lease...");
wifi.wait_netif_up()?;
let ip_info = wifi.wifi().sta_netif().get_ip_info()?;
info!("Wifi DHCP info: {:?}", ip_info);
Ok(Box::new(esp_wifi))
}