diff --git a/Cargo.toml b/Cargo.toml index b167f53..cd5bad4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,9 +19,12 @@ pio = ["esp-idf-sys/pio"] anyhow = {version = "1.0.57", features = ["backtrace"]} embedded-graphics = "0.7.1" embedded-hal = "1.0.0-alpha.8" -embedded-hal-0-2 = { package = "embedded-hal", version = "0.2.7", features = ["unproven"] } +embedded-hal-0-2 = {package = "embedded-hal", version = "0.2.7", features = ["unproven"]} +env_logger = "0.9.0" esp-idf-hal = "0.37.3" esp-idf-sys = {version = "0.31.5", features = ["binstart"]} +log = "0.4.17" +retry = "1.3.1" ssd1306 = "0.7.0" [build-dependencies] diff --git a/src/beep.rs b/src/beep.rs index adc3bb3..74e2161 100644 --- a/src/beep.rs +++ b/src/beep.rs @@ -59,7 +59,7 @@ pub mod ringtone { pub type Type = [bool; 16]; - pub const POWER_DOWN: Type = [ + pub const ADAPTER_DOWN: Type = [ true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, ]; @@ -68,4 +68,8 @@ pub mod ringtone { false, false, ]; pub const SILENCE: Type = [false; 16]; + pub const SHUTDOWN: Type = [ + true, false, true, true, true, true, false, true, true, true, true, true, false, false, + false, false, + ]; } diff --git a/src/dc_out_controller.rs b/src/dc_out_controller.rs index 7a52bb2..a74e3b8 100644 --- a/src/dc_out_controller.rs +++ b/src/dc_out_controller.rs @@ -1,24 +1,109 @@ +use anyhow::{Context, Ok, Result}; +use retry; +use std::{ + sync::{ + mpsc::{self}, + Arc, Mutex, + }, + thread, + time::{self, SystemTime}, +}; + use embedded_hal::digital::blocking::OutputPin; +use log::{info, trace, warn}; -pub struct DcOutController { - pub state: bool, - pin: T, -} - -impl DcOutController +pub struct DcOutController

where - T: OutputPin, + P: OutputPin + Send, { - pub fn new(pin: T) -> DcOutController { - return DcOutController { state: false, pin }; + pub state: bool, + pin: Arc>, + shutdown_tx: Option>, +} + +impl

DcOutController

+where + P: OutputPin + Send, + P: 'static, + P: std::marker::Sync, +{ + pub fn new(pin: P) -> Self { + return Self { + state: false, + pin: Arc::new(Mutex::new(pin)), + shutdown_tx: None, + }; } - pub fn on(&mut self) -> Result<(), T::Error> { - self.state = true; - return self.pin.set_low(); + pub fn start(&mut self) -> Result<()> { + let mut pin = retry::retry(retry::delay::Fixed::from_millis(100).take(10), || { + self.pin.lock() + }) + .map_err(|_| anyhow::anyhow!("Failed to lock pin"))?; + + retry::retry(retry::delay::Fixed::from_millis(100).take(10), || { + pin.set_low() + }) + .map_err(|_| anyhow::anyhow!("Failed to lock pin"))?; + Ok(()) } - pub fn off(&mut self) -> Result<(), T::Error> { - self.state = false; - return self.pin.set_high(); + + pub fn shutdown(&mut self) { + if self.shutdown_tx.is_some() { + trace!("Shutdown task already running, skipping..."); + return; + } + + info!("Shutdown task started..."); + let (tx, rx) = mpsc::channel(); + self.shutdown_tx = Some(tx); + + let pin = Arc::clone(&self.pin); + + thread::spawn(move || { + // Wait for computer shutdown finished + let target_at = SystemTime::now() + time::Duration::from_secs(10); + loop { + if rx.try_recv().is_ok_and(|state| !*state) { + break; + } + + if target_at > SystemTime::now() { + thread::sleep(time::Duration::from_millis(1000)); + continue; + } + + info!("Shutdown timeout, force shutdown..."); + retry::retry(retry::delay::Fixed::from_millis(1000).take(10), || { + return pin + .lock() + .map_err(|_| anyhow::anyhow!("Can not lock pin"))? + .set_high() + .map_err(|_| anyhow::anyhow!("Can not shutdown")); + }).expect("Failed to shutdown"); + info!("Shutdown finished"); + break; + } + }); + } + + pub fn stop_shutdown(&mut self) -> Result<()> { + if let Some(tx) = self.shutdown_tx.as_mut() { + info!("Shutdown task stopped..."); + if retry::retry(retry::delay::Fixed::from_millis(100).take(5), || { + tx.send(false) + }) + .map_err(|_| anyhow::anyhow!("Failed to stop shutdown")) + .is_err() { + warn!("Message to shutdown task was not sent"); + } else { + info!("Shutdown task stopped"); + } + self.shutdown_tx = None; + }; + + self.start()?; + info!("Power restored"); + Ok(()) } } diff --git a/src/main.rs b/src/main.rs index e0aa80b..c259209 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ +#![feature(is_some_with)] use esp_idf_sys as _; -use std::{thread, time::Duration, sync::mpsc}; +use std::{thread, time::Duration, sync::mpsc, env}; mod beep; mod blink; @@ -7,10 +8,13 @@ mod dc_out_controller; mod manager; mod screen; fn main() { + env::set_var("RUST_LOG", env!("RUST_LOG")); + env_logger::init(); // Temporary. Will disappear once ESP-IDF 4.4 is released, but for now it is necessary to call this function once, // or else some patches to the runtime implemented by esp-idf-sys might not link properly. esp_idf_sys::link_patches(); + let peripherals = esp_idf_hal::peripherals::Peripherals::take().unwrap(); let gpio5 = peripherals.pins.gpio5; @@ -66,5 +70,6 @@ fn main() { ).expect("Failed to create manager"); loop { manager.handling_once().expect("Failed to handle once"); + thread::sleep(Duration::from_millis(100)); } } diff --git a/src/manager.rs b/src/manager.rs index 14547fb..bf602e3 100644 --- a/src/manager.rs +++ b/src/manager.rs @@ -1,5 +1,3 @@ -use std::{sync::mpsc, thread, time::Duration}; - use embedded_hal::digital::blocking::OutputPin; use embedded_hal_0_2::adc::OneShot; use esp_idf_hal::{ @@ -7,6 +5,12 @@ use esp_idf_hal::{ gpio::{Gpio1, Gpio2}, }; use esp_idf_sys::EspError; +use log::info; +use std::{ + sync::mpsc, + thread, + time::{Duration, SystemTime}, +}; use crate::{beep::ringtone, dc_out_controller::DcOutController, screen::Screen}; @@ -15,7 +19,7 @@ type BatteryGpio = Gpio2>; pub struct Manager where - C: OutputPin, + C: OutputPin + Send, { dc_out_controller: DcOutController, screen: Screen, @@ -23,11 +27,14 @@ where adapter_pin: AdapterGpio, battery_pin: BatteryGpio, tx: mpsc::Sender, + adapter_downed_at: Option, } impl Manager where - C: OutputPin, + C: OutputPin + Send, + C: 'static, + C: std::marker::Sync, { pub fn new( dc_out_controller: DcOutController, @@ -48,6 +55,7 @@ where adapter_pin, battery_pin, tx, + adapter_downed_at: None, }); } @@ -71,21 +79,34 @@ where battery /= 10.0_f32; if adapter < 1000.0 { - self.dc_out_controller.off().expect("Can not turn off Out"); - if battery < 1000.0 { + if self.adapter_downed_at.is_none() { + info!("Recording adapter downed at: {:?}", SystemTime::now()); + self.adapter_downed_at = Some(SystemTime::now()); + } + if battery < 500.0 { self.tx .send(ringtone::BATTERY_LOW) .expect("Can not send message"); + } else if battery < 1500.0 + && self + .adapter_downed_at + .is_some_and(|at| at.elapsed().is_ok_and(|dur| dur > &Duration::from_secs(10))) + { + self.dc_out_controller.shutdown(); + self.tx + .send(ringtone::SHUTDOWN) + .expect("Can not send message"); } else { self.tx - .send(ringtone::POWER_DOWN) + .send(ringtone::ADAPTER_DOWN) .expect("Can not send message"); } } else { + self.dc_out_controller.stop_shutdown().expect("Can not stop shutdown"); + self.adapter_downed_at = None; self.tx .send(ringtone::SILENCE) .expect("Can not send message"); - self.dc_out_controller.on().expect("Can not turn on Out"); } self.screen .draw_voltage(adapter, battery) diff --git a/src/screen.rs b/src/screen.rs index ca78bbf..6871d90 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -58,9 +58,6 @@ impl Screen { .init() .unwrap(); - - println!("LED rendering done"); - let mut instance = Screen { display }; instance.draw_boot()?; @@ -95,8 +92,6 @@ impl Screen { ) .draw(&mut self.display).expect("Failed to draw text"); - println!("LED rendering done"); - self.display .flush() .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?; @@ -126,8 +121,6 @@ impl Screen { ) .draw(&mut self.display).expect("Failed to draw text"); - println!("LED rendering done"); - self.display .flush() .map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?;