diff --git a/.gitignore b/.gitignore index ea8c4bf..4b14a8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +noisebell.service diff --git a/Cargo.lock b/Cargo.lock index 62fd675..282505c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1449,9 +1449,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" @@ -1742,9 +1742,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", @@ -1753,9 +1753,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", diff --git a/deploy.sh b/deploy.sh index 4e37431..77aa09c 100755 --- a/deploy.sh +++ b/deploy.sh @@ -38,7 +38,10 @@ WantedBy=multi-user.target EOL echo "Copying to Raspberry Pi..." -ssh noisebridge@noisebell.local "mkdir -p ~/noisebell" +# Stop the service if it's running +ssh noisebridge@noisebell.local "sudo systemctl stop noisebell || true" +sleep 1 +# Copy files scp target/aarch64-unknown-linux-gnu/release/noisebell noisebridge@noisebell.local:~/noisebell/ scp noisebell.service noisebridge@noisebell.local:~/noisebell/ diff --git a/src/discord.rs b/src/discord.rs index 1b08fa7..ec6e755 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -2,7 +2,7 @@ use std::env; use std::time::Instant; use anyhow::Result; -use serenity::prelude::*; +use serenity::all::{prelude::*, Color, CreateEmbed, CreateMessage}; use serenity::model::id::ChannelId; use tracing::{info, error}; @@ -40,9 +40,21 @@ impl DiscordClient { let start = Instant::now(); info!("Sending Discord message for circuit event: {:?}", event); - let message = format!("Circuit state changed: {:?}", event); - - if let Err(why) = self.channel_id.say(&self.client.http, message).await { + 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::new(0x00FF00), // Red for open + crate::gpio::CircuitEvent::Closed => Color::new(0xFF0000), // Green for closed + }).thumbnail(match event { + crate::gpio::CircuitEvent::Open => "https://www.noisebridge.net/images/7/7f/Open.png", // Image that says "Open" + crate::gpio::CircuitEvent::Closed => "https://www.noisebridge.net/images/c/c9/Closed.png", // Image that says "Closed" + }); + + 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)); } @@ -51,4 +63,44 @@ impl DiscordClient { info!("Discord message sent 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::new(0xFFA500)) // Orange for 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(()) + } + + pub async fn send_shutdown_message(&self) -> Result<()> { + let start = Instant::now(); + info!("Sending Discord shutdown message"); + + let embed = CreateEmbed::new() + .title("Noisebell is shutting down") + .description("The Noisebell service is stopping. Status updates won't go through") + .color(Color::new(0x800080)) // Purple for shutdown + .thumbnail("https://static.vecteezy.com/system/resources/thumbnails/050/619/685/large/a-laptop-computer-on-fire-on-a-desk-in-a-dark-room-video.jpg"); + + if let Err(why) = self.channel_id.send_message(&self.client.http, CreateMessage::default().add_embed(embed)).await { + error!("Error sending Discord shutdown message: {:?}", why); + return Err(anyhow::anyhow!("Failed to send Discord shutdown message: {}", why)); + } + + let duration = start.elapsed(); + info!("Discord shutdown message sent successfully in {:?}", duration); + Ok(()) + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 3c1a02e..5beb7e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod discord; use std::fs; use std::time::Duration; +use std::sync::Arc; use anyhow::Result; use tracing::{error, info}; @@ -10,6 +11,22 @@ use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; +struct ShutdownGuard { + discord_client: Arc, +} + +impl Drop for ShutdownGuard { + fn drop(&mut self) { + info!("Shutdown guard triggered"); + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async { + if let Err(e) = self.discord_client.send_shutdown_message().await { + error!("Failed to send shutdown message: {}", e); + } + }); + } +} + #[tokio::main] async fn main() -> Result<()> { info!("creating logs directory"); @@ -39,18 +56,27 @@ async fn main() -> Result<()> { info!("initializing Discord client"); let discord_client = discord::DiscordClient::new().await?; + let discord_client = Arc::new(discord_client); + + // Create shutdown guard that will send message on any exit + let _guard = ShutdownGuard { + discord_client: Arc::clone(&discord_client), + }; + + discord_client.send_startup_message().await?; const DEFAULT_GPIO_PIN: u8 = 17; info!("initializing gpio monitor"); let mut gpio_monitor = gpio::GpioMonitor::new(DEFAULT_GPIO_PIN, Duration::from_millis(100))?; - let discord_client = std::sync::Arc::new(discord_client); - let discord_client_clone = discord_client.clone(); + // Send initial state + discord_client.clone().send_circuit_event(&gpio_monitor.get_current_state()).await?; + // Set up the callback for state changes let callback = move |event: gpio::CircuitEvent| { - info!("Circuit state changed: {:?}", event); - let discord_client = discord_client_clone.clone(); + 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 { error!("Failed to send Discord message: {}", e); @@ -58,10 +84,10 @@ async fn main() -> Result<()> { }); }; - info!("starting GPIO monitor"); - + // Start monitoring - this will block until an error occurs if let Err(e) = gpio_monitor.monitor(callback).await { error!("GPIO monitoring error: {}", e); + return Err(anyhow::anyhow!("GPIO monitoring failed")); } Ok(())