feat: use async interupts for gpio monitoring

This commit is contained in:
Jet Pham 2025-06-07 23:53:19 -07:00
parent bcf986ff1f
commit 19862ecf70
No known key found for this signature in database
4 changed files with 77 additions and 117 deletions

View file

@ -26,10 +26,10 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
User=noisebridge User=noisebridge
WorkingDirectory=/home/noisebridge/noisebell WorkingDirectory=/home/noisebridge
Environment=DISCORD_TOKEN=${DISCORD_TOKEN} Environment=DISCORD_TOKEN=${DISCORD_TOKEN}
Environment=DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID} Environment=DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID}
ExecStart=/home/noisebridge/noisebell/noisebell ExecStart=/home/noisebridge/noisebell
Restart=on-failure Restart=on-failure
RestartSec=10 RestartSec=10
@ -39,12 +39,12 @@ EOL
echo "Copying to Raspberry Pi..." echo "Copying to Raspberry Pi..."
# Copy files # Copy files
ssh noisebridge@noisebell.local "mkdir -p ~/noisebell" && scp target/aarch64-unknown-linux-gnu/release/noisebell noisebridge@noisebell.local:~/noisebell/ scp target/aarch64-unknown-linux-gnu/release/noisebell noisebridge@noisebell.local:~/
scp noisebell.service noisebridge@noisebell.local:~/noisebell/ scp noisebell.service noisebridge@noisebell.local:~/
echo "Setting up service..." echo "Setting up service..."
# Deploy 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 daemon-reload && \
sudo systemctl enable noisebell && \ sudo systemctl enable noisebell && \
sudo systemctl restart noisebell" sudo systemctl restart noisebell"

View file

@ -8,83 +8,77 @@ use tracing::{info, error};
const COLOR_OPEN: Color = Color::new(0x00FF00); // Green for open const COLOR_OPEN: Color = Color::new(0x00FF00); // Green for open
const COLOR_CLOSED: Color = Color::new(0xFF0000); // Red for closed 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 { pub struct DiscordClient {
client: Client, client: Client,
channel_id: ChannelId,
} }
impl DiscordClient { impl DiscordClient {
pub async fn new() -> Result<Self> { pub async fn new() -> Result<Self> {
let token = env::var("DISCORD_TOKEN").expect("Expected DISCORD_TOKEN in environment"); let token = env::var("DISCORD_TOKEN").expect("Expected DISCORD_TOKEN in environment");
// Validate token format
if let Err(e) = serenity::utils::token::validate(&token) { if let Err(e) = serenity::utils::token::validate(&token) {
return Err(anyhow::anyhow!("Invalid Discord token format: {}", e)); return Err(anyhow::anyhow!("Invalid Discord token format: {}", e));
} }
let intents = GatewayIntents::GUILD_MESSAGES;
let client = Client::builder(&token, intents)
.await
.expect("Error creating Discord client");
Ok(Self { client })
}
pub async fn handle_event(&self, event: SpaceEvent) -> Result<()> {
let start = Instant::now();
info!("Handling Discord event: {:?}", event);
send_discord_message(&self.client, &event).await?;
let duration = start.elapsed();
info!("Discord event handled successfully in {:?}", duration);
Ok(())
}
}
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
};
let channel_id = env::var("DISCORD_CHANNEL_ID") let channel_id = env::var("DISCORD_CHANNEL_ID")
.expect("Expected DISCORD_CHANNEL_ID in environment") .expect("Expected DISCORD_CHANNEL_ID in environment")
.parse::<u64>()?; .parse::<u64>()?;
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),
})
}
pub async fn send_circuit_event(&self, event: &crate::gpio::CircuitEvent) -> Result<()> {
let start = Instant::now();
info!("Sending Discord message for circuit event: {:?}", event);
let embed = CreateEmbed::new() let embed = CreateEmbed::new()
.title(format!("Noisebridge is {}!", event)) .title(title)
.description(match event { .description(description)
crate::gpio::CircuitEvent::Open => "It's time to start hacking.", .color(color)
crate::gpio::CircuitEvent::Closed => "We'll see you again soon.", .thumbnail(thumbnail);
})
.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 { if let Err(why) = ChannelId::new(channel_id).send_message(&client.http, CreateMessage::default().add_embed(embed)).await {
error!("Error sending Discord message: {:?}", why); error!("Error sending Discord message: {:?}", why);
return Err(anyhow::anyhow!("Failed to send Discord message: {}", why)); return Err(anyhow::anyhow!("Failed to send Discord message: {}", why));
} }
let duration = start.elapsed();
info!("Discord message sent successfully in {:?}", duration);
Ok(()) 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");
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 duration = start.elapsed();
info!("Discord startup message sent successfully in {:?}", duration);
Ok(())
}
} }

View file

@ -1,9 +1,9 @@
use std::time::{Duration, Instant}; use std::time::Duration;
use std::fmt; use std::fmt;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use anyhow::{Result, Context}; 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CircuitEvent { pub enum CircuitEvent {
@ -11,14 +11,6 @@ pub enum CircuitEvent {
Closed, Closed,
} }
#[derive(Debug, PartialEq)]
enum FsmState {
Idle,
DebouncingHigh,
High,
DebouncingLow,
}
impl fmt::Display for CircuitEvent { impl fmt::Display for CircuitEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@ -30,14 +22,11 @@ impl fmt::Display for CircuitEvent {
pub struct GpioMonitor { pub struct GpioMonitor {
pin: InputPin, pin: InputPin,
poll_interval: Duration,
debounce_delay: Duration, debounce_delay: Duration,
state: FsmState,
last_potential_transition_time: Instant,
} }
impl GpioMonitor { impl GpioMonitor {
pub fn new(pin_number: u8, poll_interval: Duration, debounce_delay: Duration) -> Result<Self> { pub fn new(pin_number: u8, debounce_delay: Duration) -> Result<Self> {
let gpio = Gpio::new() let gpio = Gpio::new()
.context("Failed to initialize GPIO")?; .context("Failed to initialize GPIO")?;
let pin = gpio.get(pin_number) let pin = gpio.get(pin_number)
@ -46,59 +35,30 @@ impl GpioMonitor {
Ok(Self { Ok(Self {
pin, pin,
poll_interval,
debounce_delay, debounce_delay,
state: FsmState::Idle,
last_potential_transition_time: Instant::now(),
}) })
} }
pub async fn monitor<F>(&mut self, mut callback: F) -> Result<()> pub fn monitor<F>(&mut self, mut callback: F) -> Result<()>
where where
F: FnMut(CircuitEvent) + Send + 'static, F: FnMut(CircuitEvent) + Send + 'static,
{ {
loop { self.pin.set_async_interrupt(
let current_switch_reading = self.get_current_state() == CircuitEvent::Closed; Trigger::Both,
let time_since_last_change = self.last_potential_transition_time.elapsed(); Some(self.debounce_delay),
move |event| {
match self.state { match event.trigger {
FsmState::Idle => { Trigger::RisingEdge => callback(CircuitEvent::Closed),
if current_switch_reading { Trigger::FallingEdge => callback(CircuitEvent::Open),
self.state = FsmState::DebouncingHigh; _ => (), // Ignore other triggers
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 { pub fn get_current_state(&self) -> CircuitEvent {
match self.pin.read() { match self.pin.read() {
Level::Low => CircuitEvent::Open, Level::Low => CircuitEvent::Open,

View file

@ -9,7 +9,6 @@ use anyhow::Result;
use tracing::{error, info}; use tracing::{error, info};
const DEFAULT_GPIO_PIN: u8 = 17; const DEFAULT_GPIO_PIN: u8 = 17;
const DEFAULT_POLL_INTERVAL_MS: u64 = 100;
const DEFAULT_DEBOUNCE_DELAY_SECS: u64 = 5; const DEFAULT_DEBOUNCE_DELAY_SECS: u64 = 5;
#[tokio::main] #[tokio::main]
@ -20,12 +19,11 @@ async fn main() -> Result<()> {
let discord_client = discord::DiscordClient::new().await?; let discord_client = discord::DiscordClient::new().await?;
let discord_client = Arc::new(discord_client); 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"); info!("initializing gpio monitor");
let mut gpio_monitor = gpio::GpioMonitor::new( let mut gpio_monitor = gpio::GpioMonitor::new(
DEFAULT_GPIO_PIN, DEFAULT_GPIO_PIN,
Duration::from_millis(DEFAULT_POLL_INTERVAL_MS),
Duration::from_secs(DEFAULT_DEBOUNCE_DELAY_SECS) Duration::from_secs(DEFAULT_DEBOUNCE_DELAY_SECS)
)?; )?;
@ -34,17 +32,25 @@ async fn main() -> Result<()> {
info!("Circuit state changed to: {:?}", event); info!("Circuit state changed to: {:?}", event);
let discord_client = discord_client.clone(); let discord_client = discord_client.clone();
tokio::spawn(async move { 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); error!("Failed to send Discord message: {}", e);
} }
}); });
}; };
// Start monitoring - this will block until an error occurs if let Err(e) = gpio_monitor.monitor(callback) {
if let Err(e) = gpio_monitor.monitor(callback).await {
error!("GPIO monitoring error: {}", e); error!("GPIO monitoring error: {}", e);
return Err(anyhow::anyhow!("GPIO monitoring failed")); 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(()) Ok(())
} }