diff --git a/src/feedback.rs b/src/feedback.rs new file mode 100644 index 0000000..65d5b42 --- /dev/null +++ b/src/feedback.rs @@ -0,0 +1,70 @@ +use log::error; +use rppal::pwm::Channel; +use std::error::Error; +use tokio::join; + +use crate::{ + buzzer::{Buzzer, GPIOBuzzer}, + led::{SpiLed, StatusLed}, +}; + +#[cfg(feature = "mock_pi")] +use crate::mock::{MockBuzzer, MockLed}; + +const PWM_CHANNEL_BUZZER: Channel = Channel::Pwm0; //PWM0 = GPIO18/Physical pin 12 + +pub struct Feedback { + buzzer: B, + led: L, +} + +impl Feedback { + pub async fn success(&mut self) { + let (buzzer_result, led_result) = + join!(self.buzzer.beep_ack(), self.led.turn_green_on_1s()); + + buzzer_result.unwrap_or_else(|err| { + error!("Failed to buzz: {err}"); + }); + + led_result.unwrap_or_else(|err| { + error!("Failed to set LED: {err}"); + }); + } + + pub async fn failure(&mut self) { + let (buzzer_result, led_result) = join!(self.buzzer.beep_nak(), self.led.turn_red_on_1s()); + + buzzer_result.unwrap_or_else(|err| { + error!("Failed to buzz: {err}"); + }); + + led_result.unwrap_or_else(|err| { + error!("Failed to set LED: {err}"); + }); + } +} + +#[cfg(feature = "mock_pi")] +pub type FeedbackImpl = Feedback; +#[cfg(not(feature = "mock_pi"))] +pub type FeedbackImpl = Feedback; + +impl FeedbackImpl { + pub fn new() -> Result> { + #[cfg(feature = "mock_pi")] + { + Ok(Feedback { + buzzer: MockBuzzer {}, + led: MockLed {}, + }) + } + #[cfg(not(feature = "mock_pi"))] + { + Ok(Feedback { + buzzer: GPIOBuzzer::new(PWM_CHANNEL_BUZZER)?, + led: SpiLed::new()?, + }) + } + } +} diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..c09aedd --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,25 @@ +use std::env; + +use log::LevelFilter; +use simplelog::{ConfigBuilder, SimpleLogger}; + +pub fn setup_logger() { + let log_level = env::var("LOG_LEVEL") + .ok() + .and_then(|level| level.parse::().ok()) + .unwrap_or({ + if cfg!(debug_assertions) { + LevelFilter::Debug + } else { + LevelFilter::Warn + } + }); + + let config = ConfigBuilder::new() + .set_target_level(LevelFilter::Off) + .set_location_level(LevelFilter::Off) + .set_thread_level(LevelFilter::Off) + .build(); + + let _ = SimpleLogger::init(log_level, config); +} diff --git a/src/main.rs b/src/main.rs index 260866d..f449d4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,33 +1,33 @@ use activity_fairing::{ActivityNotifier, spawn_idle_watcher}; -use buzzer::{Buzzer, GPIOBuzzer}; +use feedback::{Feedback, FeedbackImpl}; use hotspot::{Hotspot, HotspotError, NMHotspot}; use id_store::IDStore; -use led::{SpiLed, StatusLed}; -use log::{LevelFilter, debug, error, info, warn}; +use log::{error, info, warn}; use pm3::run_pm3; -use rppal::pwm::Channel; -use simplelog::{ConfigBuilder, SimpleLogger}; use std::{env, error::Error, sync::Arc, time::Duration}; use tally_id::TallyID; use tokio::{ - fs, join, + fs, sync::{ Mutex, - broadcast::{self, Sender}, + broadcast::{self, Receiver, Sender}, }, + try_join, }; use webserver::start_webserver; #[cfg(feature = "mock_pi")] -use mock::{MockBuzzer, MockHotspot, MockLed}; +use mock::MockHotspot; mod activity_fairing; mod buzzer; mod color; +mod feedback; mod hotspot; mod id_mapping; mod id_store; mod led; +mod logger; mod mock; mod parser; mod pm3; @@ -35,94 +35,6 @@ mod tally_id; mod webserver; const STORE_PATH: &str = "./data.json"; -const PWM_CHANNEL_BUZZER: Channel = Channel::Pwm0; //PWM0 = GPIO18/Physical pin 12 - -fn setup_logger() { - let log_level = env::var("LOG_LEVEL") - .ok() - .and_then(|level| level.parse::().ok()) - .unwrap_or({ - if cfg!(debug_assertions) { - LevelFilter::Debug - } else { - LevelFilter::Warn - } - }); - - let config = ConfigBuilder::new() - .set_target_level(LevelFilter::Off) - .set_location_level(LevelFilter::Off) - .set_thread_level(LevelFilter::Off) - .build(); - - let _ = SimpleLogger::init(log_level, config); -} - -/// Signal the user success via buzzer and led -async fn feedback_success( - gpio_buzzer: &Arc>, - status_led: &Arc>, -) { - let mut buzzer_guard = gpio_buzzer.lock().await; - let mut led_guard = status_led.lock().await; - - let (buzz, led) = join!(buzzer_guard.beep_ack(), led_guard.turn_green_on_1s()); - - buzz.unwrap_or_else(|err| { - error!("Failed to buzz: {err}"); - }); - - led.unwrap_or_else(|err| { - error!("Failed to set LED: {err}"); - }); -} - -/// Signal the user failure via buzzer and led -async fn feedback_failure( - gpio_buzzer: &Arc>, - status_led: &Arc>, -) { - let mut buzzer_guard = gpio_buzzer.lock().await; - let mut led_guard = status_led.lock().await; - - let (buzz, led) = join!(buzzer_guard.beep_nak(), led_guard.turn_red_on_1s()); - - buzz.unwrap_or_else(|err| { - error!("Failed to buzz: {err}"); - }); - - led.unwrap_or_else(|err| { - error!("Failed to set LED: {err}"); - }); -} - -/// Create a buzzer -/// Respects the `mock_pi` flag. -fn create_buzzer() -> Result>, rppal::pwm::Error> { - #[cfg(feature = "mock_pi")] - { - Ok(Arc::new(Mutex::new(MockBuzzer {}))) - } - - #[cfg(not(feature = "mock_pi"))] - { - Ok(Arc::new(Mutex::new(GPIOBuzzer::new(PWM_CHANNEL_BUZZER)?))) - } -} - -/// Creates a status led. -/// Respects the `mock_pi` flag. -fn create_status_led() -> Result>, rppal::spi::Error> { - #[cfg(feature = "mock_pi")] - { - Ok(Arc::new(Mutex::new(MockLed {}))) - } - - #[cfg(not(feature = "mock_pi"))] - { - Ok(Arc::new(Mutex::new(SpiLed::new()?))) - } -} /// Create a struct to manage the hotspot /// Respects the `mock_pi` flag. @@ -142,7 +54,8 @@ async fn run_webserver( store: Arc>, id_channel: Sender, hotspot: Arc>, -) where +) -> Result<(), Box> +where H: Hotspot + Send + Sync + 'static, { let activity_channel = spawn_idle_watcher(Duration::from_secs(60 * 30), move || { @@ -157,46 +70,21 @@ async fn run_webserver( sender: activity_channel, }; - if let Err(e) = start_webserver(store, id_channel, notifier).await { - error!("Failed to start webserver: {e}"); + start_webserver(store, id_channel, notifier).await?; + Ok(()) +} + +async fn load_or_create_store() -> Result> { + if fs::try_exists(STORE_PATH).await? { + info!("Loading data from file"); + IDStore::new_from_json(STORE_PATH).await + } else { + info!("No data file found. Creating empty one."); + Ok(IDStore::new()) } } -#[tokio::main] -async fn main() -> Result<(), Box> { - setup_logger(); - - info!("Starting application"); - - let (tx, mut rx) = broadcast::channel::(32); - let sse_tx = tx.clone(); - - tokio::spawn(async move { - match run_pm3(tx).await { - Ok(()) => { - warn!("PM3 exited with a zero exit code"); - } - Err(e) => { - error!("Failed to run PM3: {e}"); - } - } - }); - - let raw_store = if fs::try_exists(STORE_PATH).await? { - info!("Loading data from file"); - IDStore::new_from_json(STORE_PATH).await? - } else { - info!("No data file found. Creating empty one."); - IDStore::new() - }; - - debug!("created store sucessfully"); - - let store: Arc> = Arc::new(Mutex::new(raw_store)); - let gpio_buzzer = create_buzzer()?; - let status_led = create_status_led()?; - let hotspot = Arc::new(Mutex::new(create_hotspot()?)); - +fn get_hotspot_enable_ids() -> Vec { let hotspot_ids: Vec = env::var("HOTSPOT_IDS") .map(|ids| ids.split(";").map(|id| TallyID(id.to_owned())).collect()) .unwrap_or_default(); @@ -207,40 +95,79 @@ async fn main() -> Result<(), Box> { ); } - let channel_store = store.clone(); - let channel_hotspot = hotspot.clone(); - tokio::spawn(async move { - while let Ok(tally_id_string) = rx.recv().await { - let tally_id = TallyID(tally_id_string); + hotspot_ids +} - if hotspot_ids.contains(&tally_id) { - info!("Enableing hotspot"); - channel_hotspot - .lock() - .await - .enable_hotspot() - .await - .unwrap_or_else(|err| { - error!("Hotspot: {err}"); - }); - // TODO: Should the ID be added anyway or ignored ? - } +async fn handle_ids_loop( + mut id_channel: Receiver, + hotspot_enable_ids: Vec, + id_store: Arc>, + hotspot: Arc>, + mut user_feedback: FeedbackImpl, +) -> Result<(), Box> { + while let Ok(tally_id_string) = id_channel.recv().await { + let tally_id = TallyID(tally_id_string); - if channel_store.lock().await.add_id(tally_id) { - info!("Added new id to current day"); + if hotspot_enable_ids.contains(&tally_id) { + info!("Enableing hotspot"); + hotspot + .lock() + .await + .enable_hotspot() + .await + .unwrap_or_else(|err| { + error!("Hotspot: {err}"); + }); + // TODO: Should the ID be added anyway or ignored ? + } - feedback_success(&gpio_buzzer, &status_led).await; + if id_store.lock().await.add_id(tally_id) { + info!("Added new id to current day"); - if let Err(e) = channel_store.lock().await.export_json(STORE_PATH).await { - error!("Failed to save id store to file: {e}"); - feedback_failure(&gpio_buzzer, &status_led).await; - // TODO: How to handle a failure to save ? - } + user_feedback.success().await; + + if let Err(e) = id_store.lock().await.export_json(STORE_PATH).await { + error!("Failed to save id store to file: {e}"); + user_feedback.failure().await; + // TODO: How to handle a failure to save ? } } - }); - - run_webserver(store.clone(), sse_tx, hotspot.clone()).await; + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + logger::setup_logger(); + + info!("Starting application"); + + let (tx, rx) = broadcast::channel::(32); + let sse_tx = tx.clone(); + + let store: Arc> = Arc::new(Mutex::new(load_or_create_store().await?)); + let user_feedback = Feedback::new()?; + let hotspot = Arc::new(Mutex::new(create_hotspot()?)); + let hotspot_enable_ids = get_hotspot_enable_ids(); + + let pm3_handle = run_pm3(tx); + + let loop_handle = handle_ids_loop( + rx, + hotspot_enable_ids, + store.clone(), + hotspot.clone(), + user_feedback, + ); + + let webserver_handle = run_webserver(store.clone(), sse_tx, hotspot.clone()); + + let run_result = try_join!(pm3_handle, loop_handle, webserver_handle); + + if let Err(e) = run_result { + error!("Failed to run application: {e}"); + } Ok(()) }