From 19862ecf704d69fdec31144c0ef4fa540e4932fa Mon Sep 17 00:00:00 2001 From: Jet Pham <55770902+jetpham@users.noreply.github.com> Date: Sat, 7 Jun 2025 23:53:19 -0700 Subject: [PATCH] feat: use async interupts for gpio monitoring --- deploy.sh | 10 +++--- src/discord.rs | 92 +++++++++++++++++++++++--------------------------- src/gpio.rs | 74 ++++++++++------------------------------ src/main.rs | 18 ++++++---- 4 files changed, 77 insertions(+), 117 deletions(-) diff --git a/deploy.sh b/deploy.sh index 7996e93..4379b54 100755 --- a/deploy.sh +++ b/deploy.sh @@ -26,10 +26,10 @@ After=network.target [Service] Type=simple User=noisebridge -WorkingDirectory=/home/noisebridge/noisebell +WorkingDirectory=/home/noisebridge Environment=DISCORD_TOKEN=${DISCORD_TOKEN} Environment=DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID} -ExecStart=/home/noisebridge/noisebell/noisebell +ExecStart=/home/noisebridge/noisebell Restart=on-failure RestartSec=10 @@ -39,12 +39,12 @@ EOL echo "Copying to Raspberry Pi..." # Copy files -ssh noisebridge@noisebell.local "mkdir -p ~/noisebell" && scp target/aarch64-unknown-linux-gnu/release/noisebell noisebridge@noisebell.local:~/noisebell/ -scp noisebell.service noisebridge@noisebell.local:~/noisebell/ + scp target/aarch64-unknown-linux-gnu/release/noisebell noisebridge@noisebell.local:~/ +scp noisebell.service noisebridge@noisebell.local:~/ echo "Setting up service..." # Deploy service -ssh noisebridge@noisebell.local "sudo cp ~/noisebell/noisebell.service /etc/systemd/system/ && \ +ssh noisebridge@noisebell.local "sudo cp ~/noisebell.service /etc/systemd/system/ && \ sudo systemctl daemon-reload && \ sudo systemctl enable noisebell && \ sudo systemctl restart noisebell" diff --git a/src/discord.rs b/src/discord.rs index f0dd0ed..9c3506d 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -8,83 +8,77 @@ use tracing::{info, error}; const COLOR_OPEN: Color = Color::new(0x00FF00); // Green for open const COLOR_CLOSED: Color = Color::new(0xFF0000); // Red for closed -const COLOR_STARTUP: Color = Color::new(0xFFA500); // Orange for startup + +#[derive(Debug)] +pub enum SpaceEvent { + Open, + Closed, + Initializing, +} pub struct DiscordClient { client: Client, - channel_id: ChannelId, } impl DiscordClient { pub async fn new() -> Result { let token = env::var("DISCORD_TOKEN").expect("Expected DISCORD_TOKEN in environment"); - // Validate token format if let Err(e) = serenity::utils::token::validate(&token) { return Err(anyhow::anyhow!("Invalid Discord token format: {}", e)); } - let channel_id = env::var("DISCORD_CHANNEL_ID") - .expect("Expected DISCORD_CHANNEL_ID in environment") - .parse::()?; - let intents = GatewayIntents::GUILD_MESSAGES; - let client = Client::builder(&token, intents) .await .expect("Error creating Discord client"); - Ok(Self { - client, - channel_id: ChannelId::new(channel_id), - }) + Ok(Self { client }) } - pub async fn send_circuit_event(&self, event: &crate::gpio::CircuitEvent) -> Result<()> { + pub async fn handle_event(&self, event: SpaceEvent) -> Result<()> { let start = Instant::now(); - info!("Sending Discord message for circuit event: {:?}", event); + info!("Handling Discord event: {:?}", event); - let embed = CreateEmbed::new() - .title(format!("Noisebridge is {}!", event)) - .description(match event { - crate::gpio::CircuitEvent::Open => "It's time to start hacking.", - crate::gpio::CircuitEvent::Closed => "We'll see you again soon.", - }) - .color(match event { - crate::gpio::CircuitEvent::Open => COLOR_OPEN, - crate::gpio::CircuitEvent::Closed => COLOR_CLOSED, - }).thumbnail(match event { - crate::gpio::CircuitEvent::Open => "https://www.noisebridge.net/images/7/7f/Open.png", - crate::gpio::CircuitEvent::Closed => "https://www.noisebridge.net/images/c/c9/Closed.png", - }); - - if let Err(why) = self.channel_id.send_message(&self.client.http, CreateMessage::default().add_embed(embed)).await { - error!("Error sending Discord message: {:?}", why); - return Err(anyhow::anyhow!("Failed to send Discord message: {}", why)); - } + send_discord_message(&self.client, &event).await?; let duration = start.elapsed(); - info!("Discord message sent successfully in {:?}", duration); + info!("Discord event handled successfully in {:?}", duration); Ok(()) } - - pub async fn send_startup_message(&self) -> Result<()> { - let start = Instant::now(); - info!("Sending Discord startup message"); +} - let embed = CreateEmbed::new() - .title("Noisebell is starting up!") - .description("The Noisebell service is initializing and will begin monitoring the space status.") - .color(COLOR_STARTUP) - .thumbnail("https://cats.com/wp-content/uploads/2024/07/Beautiful-red-cat-stretches-and-shows-tongue.jpg"); +async fn send_discord_message(client: &Client, event: &SpaceEvent) -> Result<()> { + let (title, description, color, thumbnail) = match event { + SpaceEvent::Open => ( + "Noisebridge is Open!", + "It's time to start hacking.", + COLOR_OPEN, + "https://www.noisebridge.net/images/7/7f/Open.png" + ), + SpaceEvent::Closed => ( + "Noisebridge is Closed!", + "We'll see you again soon.", + COLOR_CLOSED, + "https://www.noisebridge.net/images/c/c9/Closed.png" + ), + SpaceEvent::Initializing => return Ok(()), // Don't send message for initialization + }; - if let Err(why) = self.channel_id.send_message(&self.client.http, CreateMessage::default().add_embed(embed)).await { - error!("Error sending Discord startup message: {:?}", why); - return Err(anyhow::anyhow!("Failed to send Discord startup message: {}", why)); - } + let channel_id = env::var("DISCORD_CHANNEL_ID") + .expect("Expected DISCORD_CHANNEL_ID in environment") + .parse::()?; - let duration = start.elapsed(); - info!("Discord startup message sent successfully in {:?}", duration); - Ok(()) + let embed = CreateEmbed::new() + .title(title) + .description(description) + .color(color) + .thumbnail(thumbnail); + + if let Err(why) = ChannelId::new(channel_id).send_message(&client.http, CreateMessage::default().add_embed(embed)).await { + error!("Error sending Discord message: {:?}", why); + return Err(anyhow::anyhow!("Failed to send Discord message: {}", why)); } + + Ok(()) } \ No newline at end of file diff --git a/src/gpio.rs b/src/gpio.rs index db2f630..0732bc9 100644 --- a/src/gpio.rs +++ b/src/gpio.rs @@ -1,9 +1,9 @@ -use std::time::{Duration, Instant}; +use std::time::Duration; use std::fmt; use serde::{Serialize, Deserialize}; use anyhow::{Result, Context}; -use rppal::gpio::{Gpio, InputPin, Level}; +use rppal::gpio::{Gpio, InputPin, Level, Trigger}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum CircuitEvent { @@ -11,14 +11,6 @@ pub enum CircuitEvent { Closed, } -#[derive(Debug, PartialEq)] -enum FsmState { - Idle, - DebouncingHigh, - High, - DebouncingLow, -} - impl fmt::Display for CircuitEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -30,14 +22,11 @@ impl fmt::Display for CircuitEvent { pub struct GpioMonitor { pin: InputPin, - poll_interval: Duration, debounce_delay: Duration, - state: FsmState, - last_potential_transition_time: Instant, } impl GpioMonitor { - pub fn new(pin_number: u8, poll_interval: Duration, debounce_delay: Duration) -> Result { + pub fn new(pin_number: u8, debounce_delay: Duration) -> Result { let gpio = Gpio::new() .context("Failed to initialize GPIO")?; let pin = gpio.get(pin_number) @@ -46,59 +35,30 @@ impl GpioMonitor { Ok(Self { pin, - poll_interval, debounce_delay, - state: FsmState::Idle, - last_potential_transition_time: Instant::now(), }) } - pub async fn monitor(&mut self, mut callback: F) -> Result<()> + pub fn monitor(&mut self, mut callback: F) -> Result<()> where F: FnMut(CircuitEvent) + Send + 'static, { - loop { - let current_switch_reading = self.get_current_state() == CircuitEvent::Closed; - let time_since_last_change = self.last_potential_transition_time.elapsed(); + self.pin.set_async_interrupt( + Trigger::Both, + Some(self.debounce_delay), + move |event| { + match event.trigger { + Trigger::RisingEdge => callback(CircuitEvent::Closed), + Trigger::FallingEdge => callback(CircuitEvent::Open), + _ => (), // Ignore other triggers + } + }, + )?; - match self.state { - FsmState::Idle => { - if current_switch_reading { - self.state = FsmState::DebouncingHigh; - self.last_potential_transition_time = Instant::now(); - } - } - - FsmState::DebouncingHigh => { - if !current_switch_reading { - self.state = FsmState::Idle; - } else if time_since_last_change >= self.debounce_delay { - self.state = FsmState::High; - callback(CircuitEvent::Closed); - } - } - - FsmState::High => { - if !current_switch_reading { - self.state = FsmState::DebouncingLow; - self.last_potential_transition_time = Instant::now(); - } - } - - FsmState::DebouncingLow => { - if current_switch_reading { - self.state = FsmState::High; - } else if time_since_last_change >= self.debounce_delay { - self.state = FsmState::Idle; - callback(CircuitEvent::Open); - } - } - } - - tokio::time::sleep(self.poll_interval).await; - } + Ok(()) } + #[allow(dead_code)] pub fn get_current_state(&self) -> CircuitEvent { match self.pin.read() { Level::Low => CircuitEvent::Open, diff --git a/src/main.rs b/src/main.rs index fb496bd..70a0bcf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,6 @@ use anyhow::Result; use tracing::{error, info}; const DEFAULT_GPIO_PIN: u8 = 17; -const DEFAULT_POLL_INTERVAL_MS: u64 = 100; const DEFAULT_DEBOUNCE_DELAY_SECS: u64 = 5; #[tokio::main] @@ -20,12 +19,11 @@ async fn main() -> Result<()> { let discord_client = discord::DiscordClient::new().await?; let discord_client = Arc::new(discord_client); - discord_client.send_startup_message().await?; + discord_client.handle_event(discord::SpaceEvent::Initializing).await?; info!("initializing gpio monitor"); let mut gpio_monitor = gpio::GpioMonitor::new( DEFAULT_GPIO_PIN, - Duration::from_millis(DEFAULT_POLL_INTERVAL_MS), Duration::from_secs(DEFAULT_DEBOUNCE_DELAY_SECS) )?; @@ -34,17 +32,25 @@ async fn main() -> Result<()> { info!("Circuit state changed to: {:?}", event); let discord_client = discord_client.clone(); tokio::spawn(async move { - if let Err(e) = discord_client.send_circuit_event(&event).await { + let space_event = match event { + gpio::CircuitEvent::Open => discord::SpaceEvent::Open, + gpio::CircuitEvent::Closed => discord::SpaceEvent::Closed, + }; + if let Err(e) = discord_client.handle_event(space_event).await { error!("Failed to send Discord message: {}", e); } }); }; - // Start monitoring - this will block until an error occurs - if let Err(e) = gpio_monitor.monitor(callback).await { + if let Err(e) = gpio_monitor.monitor(callback) { error!("GPIO monitoring error: {}", e); return Err(anyhow::anyhow!("GPIO monitoring failed")); } + info!("GPIO monitoring started. Press Ctrl+C to exit."); + + tokio::signal::ctrl_c().await?; + info!("Shutting down..."); + Ok(()) }