feat: remove rss, status, and badge features
This commit is contained in:
parent
553d7d1780
commit
36720e2ba5
21 changed files with 904 additions and 1200 deletions
24
pi/README.md
24
pi/README.md
|
|
@ -113,27 +113,3 @@ All endpoints require `Authorization: Bearer <token>`.
|
|||
```json
|
||||
{"status": "open", "timestamp": 1710000000}
|
||||
```
|
||||
|
||||
**`GET /info`** — system health + GPIO config
|
||||
|
||||
```json
|
||||
{
|
||||
"uptime_secs": 3600,
|
||||
"started_at": 1710000000,
|
||||
"cpu_temp_celsius": 42.3,
|
||||
"memory_available_kb": 350000,
|
||||
"memory_total_kb": 512000,
|
||||
"disk_total_bytes": 16000000000,
|
||||
"disk_available_bytes": 12000000000,
|
||||
"load_average": [0.01, 0.05, 0.10],
|
||||
"nixos_version": "24.11.20240308.9dcb002",
|
||||
"commit": "c6e726c",
|
||||
"gpio": {
|
||||
"pin": 17,
|
||||
"active_low": true,
|
||||
"pull": "up",
|
||||
"open_level": "low",
|
||||
"current_raw_level": "low"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
|
|
@ -7,49 +7,54 @@ use axum::extract::State;
|
|||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::routing::get;
|
||||
use axum::{Json, Router};
|
||||
use noisebell_common::{validate_bearer, WebhookPayload};
|
||||
use serde::Serialize;
|
||||
use noisebell_common::{
|
||||
validate_bearer, DoorStatus, PiStatusResponse, SignalLevel, WebhookPayload,
|
||||
};
|
||||
use tokio_gpiod::{Bias, Chip, Edge, EdgeDetect, Options};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
enum LocalDoorState {
|
||||
Closed = 0,
|
||||
Open = 1,
|
||||
}
|
||||
|
||||
impl LocalDoorState {
|
||||
fn from_raw_level(raw_level: SignalLevel, active_level: SignalLevel) -> Self {
|
||||
if raw_level == active_level {
|
||||
Self::Open
|
||||
} else {
|
||||
Self::Closed
|
||||
}
|
||||
}
|
||||
|
||||
fn from_atomic(value: u8) -> Self {
|
||||
match value {
|
||||
1 => Self::Open,
|
||||
_ => Self::Closed,
|
||||
}
|
||||
}
|
||||
|
||||
const fn as_door_status(self) -> DoorStatus {
|
||||
match self {
|
||||
Self::Open => DoorStatus::Open,
|
||||
Self::Closed => DoorStatus::Closed,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
is_open: AtomicBool,
|
||||
door_state: AtomicU8,
|
||||
last_changed: AtomicU64,
|
||||
started_at: u64,
|
||||
gpio_pin: u8,
|
||||
active_low: bool,
|
||||
commit: String,
|
||||
inbound_api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StatusResponse {
|
||||
status: &'static str,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct GpioInfo {
|
||||
pin: u8,
|
||||
active_low: bool,
|
||||
pull: &'static str,
|
||||
open_level: &'static str,
|
||||
current_raw_level: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct InfoResponse {
|
||||
uptime_secs: u64,
|
||||
started_at: u64,
|
||||
cpu_temp_celsius: Option<f64>,
|
||||
memory_available_kb: Option<u64>,
|
||||
memory_total_kb: Option<u64>,
|
||||
disk_total_bytes: Option<u64>,
|
||||
disk_available_bytes: Option<u64>,
|
||||
load_average: Option<[f64; 3]>,
|
||||
nixos_version: Option<String>,
|
||||
commit: String,
|
||||
gpio: GpioInfo,
|
||||
impl AppState {
|
||||
fn current_door_state(&self) -> LocalDoorState {
|
||||
LocalDoorState::from_atomic(self.door_state.load(Ordering::Relaxed))
|
||||
}
|
||||
}
|
||||
|
||||
fn unix_timestamp() -> u64 {
|
||||
|
|
@ -59,112 +64,19 @@ fn unix_timestamp() -> u64 {
|
|||
.as_secs()
|
||||
}
|
||||
|
||||
fn status_str(is_open: bool) -> &'static str {
|
||||
if is_open {
|
||||
"open"
|
||||
} else {
|
||||
"closed"
|
||||
}
|
||||
}
|
||||
|
||||
fn read_cpu_temp() -> Option<f64> {
|
||||
std::fs::read_to_string("/sys/class/thermal/thermal_zone0/temp")
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<f64>().ok())
|
||||
.map(|m| m / 1000.0)
|
||||
}
|
||||
|
||||
fn read_meminfo_field(contents: &str, field: &str) -> Option<u64> {
|
||||
contents
|
||||
.lines()
|
||||
.find(|l| l.starts_with(field))
|
||||
.and_then(|l| l.split_whitespace().nth(1))
|
||||
.and_then(|v| v.parse().ok())
|
||||
}
|
||||
|
||||
fn read_disk_usage() -> Option<(u64, u64)> {
|
||||
let path = std::ffi::CString::new("/").ok()?;
|
||||
let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
|
||||
let ret = unsafe { libc::statvfs(path.as_ptr(), &mut stat) };
|
||||
if ret != 0 {
|
||||
return None;
|
||||
}
|
||||
let block_size = stat.f_frsize as u64;
|
||||
Some((
|
||||
stat.f_blocks * block_size,
|
||||
stat.f_bavail * block_size,
|
||||
))
|
||||
}
|
||||
|
||||
fn read_load_average() -> Option<[f64; 3]> {
|
||||
let contents = std::fs::read_to_string("/proc/loadavg").ok()?;
|
||||
let mut parts = contents.split_whitespace();
|
||||
Some([
|
||||
parts.next()?.parse().ok()?,
|
||||
parts.next()?.parse().ok()?,
|
||||
parts.next()?.parse().ok()?,
|
||||
])
|
||||
}
|
||||
|
||||
fn read_nixos_version() -> Option<String> {
|
||||
std::fs::read_to_string("/run/current-system/nixos-version")
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
|
||||
async fn get_status(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<StatusResponse>, StatusCode> {
|
||||
) -> Result<Json<PiStatusResponse>, StatusCode> {
|
||||
if !validate_bearer(&headers, &state.inbound_api_key) {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
Ok(Json(StatusResponse {
|
||||
status: status_str(state.is_open.load(Ordering::Relaxed)),
|
||||
Ok(Json(PiStatusResponse {
|
||||
status: state.current_door_state().as_door_status(),
|
||||
timestamp: state.last_changed.load(Ordering::Relaxed),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_info(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<InfoResponse>, StatusCode> {
|
||||
if !validate_bearer(&headers, &state.inbound_api_key) {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
let meminfo = std::fs::read_to_string("/proc/meminfo").ok();
|
||||
let disk = read_disk_usage();
|
||||
let is_open = state.is_open.load(Ordering::Relaxed);
|
||||
let raw_level = match (state.active_low, is_open) {
|
||||
(true, true) | (false, false) => "low",
|
||||
_ => "high",
|
||||
};
|
||||
Ok(Json(InfoResponse {
|
||||
uptime_secs: unix_timestamp() - state.started_at,
|
||||
started_at: state.started_at,
|
||||
cpu_temp_celsius: read_cpu_temp(),
|
||||
memory_available_kb: meminfo
|
||||
.as_deref()
|
||||
.and_then(|m| read_meminfo_field(m, "MemAvailable:")),
|
||||
memory_total_kb: meminfo
|
||||
.as_deref()
|
||||
.and_then(|m| read_meminfo_field(m, "MemTotal:")),
|
||||
disk_total_bytes: disk.map(|(total, _)| total),
|
||||
disk_available_bytes: disk.map(|(_, avail)| avail),
|
||||
load_average: read_load_average(),
|
||||
nixos_version: read_nixos_version(),
|
||||
commit: state.commit.clone(),
|
||||
gpio: GpioInfo {
|
||||
pin: state.gpio_pin,
|
||||
active_low: state.active_low,
|
||||
pull: if state.active_low { "up" } else { "down" },
|
||||
open_level: if state.active_low { "low" } else { "high" },
|
||||
current_raw_level: raw_level,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
|
|
@ -189,8 +101,7 @@ async fn main() -> Result<()> {
|
|||
let endpoint_url =
|
||||
std::env::var("NOISEBELL_ENDPOINT_URL").context("NOISEBELL_ENDPOINT_URL is required")?;
|
||||
|
||||
let api_key =
|
||||
std::env::var("NOISEBELL_API_KEY").context("NOISEBELL_API_KEY is required")?;
|
||||
let api_key = std::env::var("NOISEBELL_API_KEY").context("NOISEBELL_API_KEY is required")?;
|
||||
|
||||
let retry_attempts: u32 = std::env::var("NOISEBELL_RETRY_ATTEMPTS")
|
||||
.unwrap_or_else(|_| "3".into())
|
||||
|
|
@ -207,13 +118,17 @@ async fn main() -> Result<()> {
|
|||
.parse()
|
||||
.context("NOISEBELL_HTTP_TIMEOUT_SECS must be a valid u64")?;
|
||||
|
||||
let bind_address =
|
||||
std::env::var("NOISEBELL_BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0".into());
|
||||
let bind_address = std::env::var("NOISEBELL_BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0".into());
|
||||
|
||||
let active_low: bool = std::env::var("NOISEBELL_ACTIVE_LOW")
|
||||
.unwrap_or_else(|_| "true".into())
|
||||
.parse()
|
||||
.context("NOISEBELL_ACTIVE_LOW must be true or false")?;
|
||||
let active_level = if active_low {
|
||||
SignalLevel::Low
|
||||
} else {
|
||||
SignalLevel::High
|
||||
};
|
||||
|
||||
let inbound_api_key = std::env::var("NOISEBELL_INBOUND_API_KEY")
|
||||
.context("NOISEBELL_INBOUND_API_KEY is required")?;
|
||||
|
|
@ -224,7 +139,11 @@ async fn main() -> Result<()> {
|
|||
.await
|
||||
.context("failed to open gpiochip0")?;
|
||||
|
||||
let bias = if active_low { Bias::PullUp } else { Bias::PullDown };
|
||||
let bias = if active_level == SignalLevel::Low {
|
||||
Bias::PullUp
|
||||
} else {
|
||||
Bias::PullDown
|
||||
};
|
||||
|
||||
// Request the line with edge detection for monitoring
|
||||
let opts = Options::input([gpio_pin])
|
||||
|
|
@ -243,29 +162,30 @@ async fn main() -> Result<()> {
|
|||
.context("failed to read initial GPIO value")?;
|
||||
// Value is true when line is active. With Active::High (default),
|
||||
// true means the physical level is high.
|
||||
let initial_high = initial_values[0];
|
||||
let initial_open = if active_low { !initial_high } else { initial_high };
|
||||
let initial_raw_level = if initial_values[0] {
|
||||
SignalLevel::High
|
||||
} else {
|
||||
SignalLevel::Low
|
||||
};
|
||||
let initial_state = LocalDoorState::from_raw_level(initial_raw_level, active_level);
|
||||
|
||||
let now = unix_timestamp();
|
||||
let commit =
|
||||
std::env::var("NOISEBELL_COMMIT").unwrap_or_else(|_| "unknown".into());
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
is_open: AtomicBool::new(initial_open),
|
||||
door_state: AtomicU8::new(initial_state as u8),
|
||||
last_changed: AtomicU64::new(now),
|
||||
started_at: now,
|
||||
gpio_pin: gpio_pin as u8,
|
||||
active_low,
|
||||
commit,
|
||||
inbound_api_key,
|
||||
});
|
||||
|
||||
info!(initial_status = status_str(initial_open), "GPIO initialized");
|
||||
info!(
|
||||
initial_status = %initial_state.as_door_status(),
|
||||
"GPIO initialized"
|
||||
);
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<(bool, u64)>();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<(DoorStatus, u64)>();
|
||||
|
||||
// Sync initial state with the cache on startup
|
||||
let _ = tx.send((initial_open, now));
|
||||
let _ = tx.send((initial_state.as_door_status(), now));
|
||||
|
||||
// Spawn async edge detection task
|
||||
let state_for_edges = state.clone();
|
||||
|
|
@ -290,18 +210,20 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
last_event_time = std::time::Instant::now();
|
||||
|
||||
let new_open = match event.edge {
|
||||
Edge::Falling => active_low,
|
||||
Edge::Rising => !active_low,
|
||||
let new_raw_level = match event.edge {
|
||||
Edge::Falling => SignalLevel::Low,
|
||||
Edge::Rising => SignalLevel::High,
|
||||
};
|
||||
let new_state = LocalDoorState::from_raw_level(new_raw_level, active_level);
|
||||
|
||||
let was_open = state_for_edges.is_open.swap(new_open, Ordering::Relaxed);
|
||||
if was_open != new_open {
|
||||
let previous_state =
|
||||
LocalDoorState::from_atomic(state_for_edges.door_state.swap(new_state as u8, Ordering::Relaxed));
|
||||
if previous_state != new_state {
|
||||
let timestamp = unix_timestamp();
|
||||
state_for_edges
|
||||
.last_changed
|
||||
.store(timestamp, Ordering::Relaxed);
|
||||
let _ = edge_tx.send((new_open, timestamp));
|
||||
let _ = edge_tx.send((new_state.as_door_status(), timestamp));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -313,11 +235,10 @@ async fn main() -> Result<()> {
|
|||
.build()
|
||||
.expect("failed to build HTTP client");
|
||||
|
||||
while let Some((new_open, timestamp)) = rx.recv().await {
|
||||
let status = status_str(new_open);
|
||||
info!(status, timestamp, "state changed");
|
||||
while let Some((status, timestamp)) = rx.recv().await {
|
||||
info!(status = %status, timestamp, "state changed");
|
||||
|
||||
let payload = WebhookPayload { status: status.to_string(), timestamp };
|
||||
let payload = WebhookPayload { status, timestamp };
|
||||
|
||||
for attempt in 0..=retry_attempts {
|
||||
let result = client
|
||||
|
|
@ -336,9 +257,8 @@ async fn main() -> Result<()> {
|
|||
if attempt == retry_attempts {
|
||||
error!(error = %err_msg, "failed to notify endpoint after {} attempts", retry_attempts + 1);
|
||||
} else {
|
||||
let delay = Duration::from_secs(
|
||||
retry_base_delay_secs * 2u64.pow(attempt),
|
||||
);
|
||||
let delay =
|
||||
Duration::from_secs(retry_base_delay_secs * 2u64.pow(attempt));
|
||||
warn!(error = %err_msg, attempt = attempt + 1, "notify failed, retrying in {:?}", delay);
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
|
|
@ -350,7 +270,6 @@ async fn main() -> Result<()> {
|
|||
|
||||
let app = Router::new()
|
||||
.route("/", get(get_status))
|
||||
.route("/info", get(get_info))
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind((&*bind_address, port))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue