feat: use async interupts for gpio monitoring
This commit is contained in:
parent
bcf986ff1f
commit
19862ecf70
4 changed files with 77 additions and 117 deletions
10
deploy.sh
10
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"
|
||||
|
|
|
|||
|
|
@ -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<Self> {
|
||||
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::<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),
|
||||
})
|
||||
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::<u64>()?;
|
||||
|
||||
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(())
|
||||
}
|
||||
74
src/gpio.rs
74
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<Self> {
|
||||
pub fn new(pin_number: u8, debounce_delay: Duration) -> Result<Self> {
|
||||
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<F>(&mut self, mut callback: F) -> Result<()>
|
||||
pub fn monitor<F>(&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,
|
||||
|
|
|
|||
18
src/main.rs
18
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(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue