From 64a50d434b2bb60565d8fa2820d64891182b3086 Mon Sep 17 00:00:00 2001 From: Djeeberjr Date: Tue, 13 May 2025 17:19:45 +0200 Subject: [PATCH] implemented mocking of rpi hardware buzzer,led & hotspot got traits and a mock version of it. Based on the flag the real or mock version is used. --- Cargo.toml | 4 + src/buzzer.rs | 35 ++++----- src/hotspot.rs | 199 ++++++++++++++++++++++++++++++------------------- src/led.rs | 36 ++++++--- src/main.rs | 69 +++++++++++++++-- src/mock.rs | 45 +++++++++++ 6 files changed, 271 insertions(+), 117 deletions(-) create mode 100644 src/mock.rs diff --git a/Cargo.toml b/Cargo.toml index deaaa68..50aa4fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,10 @@ name = "fw-anwesenheit" version = "0.1.0" edition = "2024" +[features] +default = [] +mock_pi = [] # Enable mocking of the rpi hardware + [dependencies] chrono = { version = "0.4.40", features = ["serde"] } gpio = "0.4.1" diff --git a/src/buzzer.rs b/src/buzzer.rs index cc709f7..5ebe8e6 100644 --- a/src/buzzer.rs +++ b/src/buzzer.rs @@ -1,6 +1,12 @@ use rppal::pwm::{Channel, Error, Polarity, Pwm}; +use std::{future::Future, time::Duration}; use tokio::time::sleep; -use std::time::Duration; + +pub trait Buzzer { + fn beep_ack(&mut self) -> impl Future> + std::marker::Send; + + fn beep_nak(&mut self) -> impl Future> + std::marker::Send; +} pub struct GPIOBuzzer { pwm: Pwm, @@ -8,19 +14,19 @@ pub struct GPIOBuzzer { impl GPIOBuzzer { /// Create a new GPIOBuzzer instance. - /// 0.5 duty cyle + /// 0.5 duty cyle /// # Arguments /// * "channel" - PWM channel for buzzer PWM0 = GPIO 12 / PWM1 = GPIO 13 pub fn new(channel: Channel) -> Result { // Enable with dummy values; we'll set frequency/duty in the tone method - let duty_cycle:f64 = 0.5; + let duty_cycle: f64 = 0.5; let pwm = Pwm::with_frequency(channel, 1000.0, duty_cycle, Polarity::Normal, true)?; pwm.disable()?; // Start disabled Ok(GPIOBuzzer { pwm }) } - /// Play a tone using hardware PWM on supported GPIO pins. + /// Play a tone using hardware PWM on supported GPIO pins. /// /// # Arguments /// * `frequency` - Frequency in Hz. @@ -32,33 +38,24 @@ impl GPIOBuzzer { self.pwm.disable()?; Ok(()) } +} - pub async fn beep_ack(&mut self) -> Result<(), Error>{ +impl Buzzer for GPIOBuzzer { + async fn beep_ack(&mut self) -> Result<(), Error> { let sleep_ms: u64 = 10; self.modulated_tone(1200.0, 100).await?; sleep(Duration::from_millis(sleep_ms)).await; - self.modulated_tone(2000.0,50).await?; + self.modulated_tone(2000.0, 50).await?; Ok(()) } - pub async fn beep_nak(&mut self) -> Result<(), Error>{ + async fn beep_nak(&mut self) -> Result<(), Error> { let sleep_ms: u64 = 100; self.modulated_tone(600.0, 150).await?; sleep(Duration::from_millis(sleep_ms)).await; - self.modulated_tone(600.0,150).await?; - Ok(()) - } - - pub async fn beep_unnkown(&mut self) -> Result<(), Error>{ - let sleep_ms: u64 = 100; - - self.modulated_tone(750.0, 100).await?; - sleep(Duration::from_millis(sleep_ms)).await; - self.modulated_tone(1200.0,100).await?; - sleep(Duration::from_millis(sleep_ms)).await; - self.modulated_tone(2300.0,100).await?; + self.modulated_tone(600.0, 150).await?; Ok(()) } } diff --git a/src/hotspot.rs b/src/hotspot.rs index b346eff..150b34f 100644 --- a/src/hotspot.rs +++ b/src/hotspot.rs @@ -1,5 +1,6 @@ -use log::trace; +use log::{error, trace, warn}; use std::{ + env, fmt::{self}, process::Output, }; @@ -14,6 +15,7 @@ const IPV4_ADDRES: &str = "192.168.4.1/24"; pub enum HotspotError { IoError(std::io::Error), NonZeroExit(Output), + PasswordToShort, } impl fmt::Display for HotspotError { @@ -30,104 +32,145 @@ impl fmt::Display for HotspotError { "Failed to run hotspot command.\nStdout: {stdout}\nStderr: {stderr}", ) } + HotspotError::PasswordToShort => { + write!(f, "The password must be at leat 8 characters long") + } } } } impl std::error::Error for HotspotError {} -/// Create the connection in NM -/// Will fail if already exists -async fn create_hotspot() -> Result<(), HotspotError> { - let cmd = Command::new("nmcli") - .args(["device", "wifi", "hotspot"]) - .arg("con-name") - .arg(CON_NAME) - .arg("ssid") - .arg(SSID) - .arg("password") - .arg(PASSWORD) - .output() - .await - .map_err(HotspotError::IoError)?; +pub trait Hotspot { + fn enable_hotspot( + &self, + ) -> impl std::future::Future> + std::marker::Send; - trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout)); - trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr)); - - if !cmd.status.success() { - return Err(HotspotError::NonZeroExit(cmd)); - } - - let cmd = Command::new("nmcli") - .arg("connection") - .arg("modify") - .arg(CON_NAME) - .arg("ipv4.method") - .arg("shared") - .arg("ipv4.addresses") - .arg(IPV4_ADDRES) - .output() - .await - .map_err(HotspotError::IoError)?; - - if !cmd.status.success() { - return Err(HotspotError::NonZeroExit(cmd)); - } - - Ok(()) + async fn disable_hotspot(&self) -> Result<(), HotspotError>; } -/// Checks if the connection already exists -async fn exists() -> Result { - let cmd = Command::new("nmcli") - .args(["connection", "show"]) - .arg(CON_NAME) - .output() - .await - .map_err(HotspotError::IoError)?; - - trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout)); - trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr)); - - Ok(cmd.status.success()) +/// NetworkManager Hotspot +pub struct NMHotspot { + ssid: String, + con_name: String, + password: String, + ipv4: String, } -pub async fn enable_hotspot() -> Result<(), HotspotError> { - if !exists().await? { - create_hotspot().await?; +impl NMHotspot { + pub fn new_from_env() -> Result { + let ssid = env::var("HOTSPOT_SSID").unwrap_or(SSID.to_owned()); + let password = env::var("HOTSPOT_PW").unwrap_or_else(|_| { + warn!("HOTSPOT_PW not set. Using default password"); + PASSWORD.to_owned() + }); + + if password.len() < 8 { + error!("Hotspot PW is to short"); + return Err(HotspotError::PasswordToShort); + } + + Ok(NMHotspot { + ssid, + con_name: CON_NAME.to_owned(), + password, + ipv4: IPV4_ADDRES.to_owned(), + }) } - let cmd = Command::new("nmcli") - .args(["connection", "up"]) - .arg(CON_NAME) - .output() - .await - .map_err(HotspotError::IoError)?; + async fn create_hotspot(&self) -> Result<(), HotspotError> { + let cmd = Command::new("nmcli") + .args(["device", "wifi", "hotspot"]) + .arg("con-name") + .arg(&self.con_name) + .arg("ssid") + .arg(&self.ssid) + .arg("password") + .arg(&self.password) + .output() + .await + .map_err(HotspotError::IoError)?; - trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout)); - trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr)); + trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout)); + trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr)); - if !cmd.status.success() { - return Err(HotspotError::NonZeroExit(cmd)); + if !cmd.status.success() { + return Err(HotspotError::NonZeroExit(cmd)); + } + + let cmd = Command::new("nmcli") + .arg("connection") + .arg("modify") + .arg(&self.con_name) + .arg("ipv4.method") + .arg("shared") + .arg("ipv4.addresses") + .arg(&self.ipv4) + .output() + .await + .map_err(HotspotError::IoError)?; + + if !cmd.status.success() { + return Err(HotspotError::NonZeroExit(cmd)); + } + + Ok(()) } - Ok(()) + /// Checks if the connection already exists + async fn exists(&self) -> Result { + let cmd = Command::new("nmcli") + .args(["connection", "show"]) + .arg(&self.con_name) + .output() + .await + .map_err(HotspotError::IoError)?; + + trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout)); + trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr)); + + Ok(cmd.status.success()) + } } -pub async fn disable_hotspot() -> Result<(), HotspotError> { - let cmd = Command::new("nmcli") - .args(["connection", "down"]) - .arg(CON_NAME) - .output() - .await - .map_err(HotspotError::IoError)?; +impl Hotspot for NMHotspot { + async fn enable_hotspot(&self) -> Result<(), HotspotError> { + if !self.exists().await? { + self.create_hotspot().await?; + } - trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout)); - trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr)); + let cmd = Command::new("nmcli") + .args(["connection", "up"]) + .arg(&self.con_name) + .output() + .await + .map_err(HotspotError::IoError)?; - if !cmd.status.success() { - return Err(HotspotError::NonZeroExit(cmd)); + trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout)); + trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr)); + + if !cmd.status.success() { + return Err(HotspotError::NonZeroExit(cmd)); + } + + Ok(()) } - Ok(()) + async fn disable_hotspot(&self) -> Result<(), HotspotError> { + let cmd = Command::new("nmcli") + .args(["connection", "down"]) + .arg(&self.con_name) + .output() + .await + .map_err(HotspotError::IoError)?; + + trace!("nmcli (std): {}", String::from_utf8_lossy(&cmd.stdout)); + trace!("nmcli (err): {}", String::from_utf8_lossy(&cmd.stderr)); + + if !cmd.status.success() { + return Err(HotspotError::NonZeroExit(cmd)); + } + + Ok(()) + } } diff --git a/src/led.rs b/src/led.rs index c018906..302d83b 100644 --- a/src/led.rs +++ b/src/led.rs @@ -7,35 +7,47 @@ use ws2812_spi::Ws2812; use crate::color::NamedColor; -pub struct Led { +const STATUS_DURATION: Duration = Duration::from_secs(1); // 1s sleep for all status led signals + +pub trait StatusLed { + fn turn_green_on_1s( + &mut self, + ) -> impl std::future::Future> + std::marker::Send; + + fn turn_red_on_1s( + &mut self, + ) -> impl std::future::Future> + std::marker::Send; +} + +pub struct SpiLed { controller: Ws2812, } -const STATUS_DURATION: Duration = Duration::from_secs(1); // 1s sleep for all status led signals - -impl Led { +impl SpiLed { pub fn new() -> Result { let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 3_800_000, Mode::Mode0)?; let controller = Ws2812::new(spi); - Ok(Led { controller }) + Ok(SpiLed { controller }) } - pub async fn turn_green_on_1s(&mut self) -> Result<(), Error> { + fn turn_off(&mut self) -> Result<(), Error> { + self.controller.write(NamedColor::Off.into_iter())?; + Ok(()) + } +} + +impl StatusLed for SpiLed { + async fn turn_green_on_1s(&mut self) -> Result<(), Error> { self.controller.write(NamedColor::Green.into_iter())?; sleep(STATUS_DURATION).await; self.turn_off()?; Ok(()) } - pub async fn turn_red_on_1s(&mut self) -> Result<(), Error> { + async fn turn_red_on_1s(&mut self) -> Result<(), Error> { self.controller.write(NamedColor::Red.into_iter())?; sleep(STATUS_DURATION).await; self.turn_off()?; Ok(()) } - - pub fn turn_off(&mut self) -> Result<(), Error> { - self.controller.write(NamedColor::Off.into_iter())?; - Ok(()) - } } diff --git a/src/main.rs b/src/main.rs index 5a387e4..476b958 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ -use buzzer::GPIOBuzzer; +use buzzer::{Buzzer, GPIOBuzzer}; +use hotspot::{Hotspot, HotspotError, NMHotspot}; use id_store::IDStore; -use led::Led; +use led::{SpiLed, StatusLed}; use log::{LevelFilter, debug, error, info, warn}; use pm3::run_pm3; use rppal::pwm::Channel; @@ -13,11 +14,15 @@ use tokio::{ }; use webserver::start_webserver; +#[cfg(feature = "mock_pi")] +use mock::{MockBuzzer, MockHotspot, MockLed}; + mod buzzer; mod color; mod hotspot; mod id_store; mod led; +mod mock; mod parser; mod pm3; mod tally_id; @@ -48,7 +53,10 @@ fn setup_logger() { } /// Signal the user success via buzzer and led -async fn feedback_success(gpio_buzzer: &Arc>, status_led: &Arc>) { +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; @@ -64,7 +72,10 @@ async fn feedback_success(gpio_buzzer: &Arc>, status_led: &Arc } /// Signal the user failure via buzzer and led -async fn feedback_failure(gpio_buzzer: &Arc>, status_led: &Arc>) { +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; @@ -79,6 +90,48 @@ async fn feedback_failure(gpio_buzzer: &Arc>, status_led: &Arc }); } +/// 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. +fn create_hotspot() -> Result { + #[cfg(feature = "mock_pi")] + { + Ok(MockHotspot {}) + } + + #[cfg(not(feature = "mock_pi"))] + { + NMHotspot::new_from_env() + } +} + #[tokio::main] async fn main() -> Result<(), Box> { setup_logger(); @@ -109,9 +162,9 @@ async fn main() -> Result<(), Box> { debug!("created store sucessfully"); let store: Arc> = Arc::new(Mutex::new(raw_store)); - let gpio_buzzer: Arc> = - Arc::new(Mutex::new(GPIOBuzzer::new(PWM_CHANNEL_BUZZER)?)); - let status_led: Arc> = Arc::new(Mutex::new(Led::new()?)); + let gpio_buzzer = create_buzzer()?; + let status_led = create_status_led()?; + let hotspot = create_hotspot()?; let hotspot_ids: Vec = env::var("HOTSPOT_IDS") .map(|ids| ids.split(";").map(|id| TallyID(id.to_owned())).collect()) @@ -130,7 +183,7 @@ async fn main() -> Result<(), Box> { if hotspot_ids.contains(&tally_id) { info!("Enableing hotspot"); - hotspot::enable_hotspot().await.unwrap_or_else(|err| { + hotspot.enable_hotspot().await.unwrap_or_else(|err| { error!("Hotspot: {err}"); }); // TODO: Should the ID be added anyway or ignored ? diff --git a/src/mock.rs b/src/mock.rs new file mode 100644 index 0000000..3846330 --- /dev/null +++ b/src/mock.rs @@ -0,0 +1,45 @@ +use log::debug; + +use crate::{buzzer::Buzzer, hotspot::Hotspot, led::StatusLed}; + +pub struct MockBuzzer {} + +impl Buzzer for MockBuzzer { + async fn beep_ack(&mut self) -> Result<(), rppal::pwm::Error> { + debug!("Mockbuzzer: ACK"); + Ok(()) + } + + async fn beep_nak(&mut self) -> Result<(), rppal::pwm::Error> { + debug!("Mockbuzzer: NAK"); + Ok(()) + } +} + +pub struct MockLed {} + +impl StatusLed for MockLed { + async fn turn_green_on_1s(&mut self) -> Result<(), rppal::spi::Error> { + debug!("Mockled: Turn LED green for 1 sec"); + Ok(()) + } + + async fn turn_red_on_1s(&mut self) -> Result<(), rppal::spi::Error> { + debug!("Mockled: Turn LED red for 1 sec"); + Ok(()) + } +} + +pub struct MockHotspot {} + +impl Hotspot for MockHotspot { + async fn enable_hotspot(&self) -> Result<(), crate::hotspot::HotspotError> { + debug!("Mockhotspot: Enable hotspot"); + Ok(()) + } + + async fn disable_hotspot(&self) -> Result<(), crate::hotspot::HotspotError> { + debug!("Mockhotspot: Disable hotspot"); + Ok(()) + } +}