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.
This commit is contained in:
Djeeberjr 2025-05-13 17:19:45 +02:00
parent 31f65261df
commit 64a50d434b
6 changed files with 271 additions and 117 deletions

View File

@ -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"

View File

@ -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<Output = Result<(), Error>> + std::marker::Send;
fn beep_nak(&mut self) -> impl Future<Output = Result<(), Error>> + std::marker::Send;
}
pub struct GPIOBuzzer {
pwm: Pwm,
@ -13,14 +19,14 @@ impl GPIOBuzzer {
/// * "channel" - PWM channel for buzzer PWM0 = GPIO 12 / PWM1 = GPIO 13
pub fn new(channel: Channel) -> Result<Self, Error> {
// 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(())
}
}

View File

@ -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<Output = Result<(), HotspotError>> + 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<bool, HotspotError> {
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<Self, HotspotError> {
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<bool, HotspotError> {
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(())
}
}

View File

@ -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<Output = Result<(), Error>> + std::marker::Send;
fn turn_red_on_1s(
&mut self,
) -> impl std::future::Future<Output = Result<(), Error>> + std::marker::Send;
}
pub struct SpiLed {
controller: Ws2812<Spi>,
}
const STATUS_DURATION: Duration = Duration::from_secs(1); // 1s sleep for all status led signals
impl Led {
impl SpiLed {
pub fn new() -> Result<Self, Error> {
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(())
}
}

View File

@ -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<Mutex<GPIOBuzzer>>, status_led: &Arc<Mutex<Led>>) {
async fn feedback_success<T: Buzzer, I: StatusLed>(
gpio_buzzer: &Arc<Mutex<T>>,
status_led: &Arc<Mutex<I>>,
) {
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<Mutex<GPIOBuzzer>>, status_led: &Arc
}
/// Signal the user failure via buzzer and led
async fn feedback_failure(gpio_buzzer: &Arc<Mutex<GPIOBuzzer>>, status_led: &Arc<Mutex<Led>>) {
async fn feedback_failure<T: Buzzer, I: StatusLed>(
gpio_buzzer: &Arc<Mutex<T>>,
status_led: &Arc<Mutex<I>>,
) {
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<Mutex<GPIOBuzzer>>, status_led: &Arc
});
}
/// Create a buzzer
/// Respects the `mock_pi` flag.
fn create_buzzer() -> Result<Arc<Mutex<impl Buzzer>>, 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<Arc<Mutex<impl StatusLed>>, 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<impl Hotspot, HotspotError> {
#[cfg(feature = "mock_pi")]
{
Ok(MockHotspot {})
}
#[cfg(not(feature = "mock_pi"))]
{
NMHotspot::new_from_env()
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
setup_logger();
@ -109,9 +162,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
debug!("created store sucessfully");
let store: Arc<Mutex<IDStore>> = Arc::new(Mutex::new(raw_store));
let gpio_buzzer: Arc<Mutex<GPIOBuzzer>> =
Arc::new(Mutex::new(GPIOBuzzer::new(PWM_CHANNEL_BUZZER)?));
let status_led: Arc<Mutex<Led>> = 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<TallyID> = env::var("HOTSPOT_IDS")
.map(|ids| ids.split(";").map(|id| TallyID(id.to_owned())).collect())
@ -130,7 +183,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
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 ?

45
src/mock.rs Normal file
View File

@ -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(())
}
}