use anyhow::Result;
use axum::{
extract::State,
http::StatusCode,
middleware,
response::Json,
routing::{get, post, put},
Router,
};
use clap::Parser;
use std::{
net::SocketAddr,
sync::{Arc, RwLock},
};
use tower_http::cors::CorsLayer;
use tracing::info;
mod api;
mod auth;
mod beacon;
use api::*;
use auth::auth_middleware;
use beacon::BeaconController;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(long, default_value = "127.0.0.1")]
host: String,
#[arg(long, default_value_t = 38861)]
port: u16,
#[arg(long)]
api_key: Option<String>,
}
#[derive(Clone)]
struct AppState {
beacon: Arc<RwLock<BeaconController>>,
api_key: Option<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let args = Args::parse();
let api_key = args.api_key;
let beacon = Arc::new(RwLock::new(BeaconController::new()?));
let state = AppState {
beacon: beacon.clone(),
api_key: api_key.clone(),
};
let beacon_for_task = beacon.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(100));
loop {
interval.tick().await;
if let Ok(mut beacon) = beacon_for_task.write() {
let _ = beacon.update_sequence();
}
}
});
let beacon_for_touch = beacon.clone();
tokio::spawn(async move {
let beacon_arc = {
let controller = beacon_for_touch.read().unwrap();
controller.get_beacon_clone()
};
if let Some(beacon_arc) = beacon_arc {
info!("Touch sensor monitoring enabled - press button to clear all outputs");
loop {
let pressed = {
let beacon = beacon_arc.lock().unwrap();
beacon.get_touch_sensor_state().unwrap_or(false)
};
if pressed {
info!("Touch sensor pressed - clearing all outputs");
if let Ok(mut controller) = beacon_for_touch.write() {
let _ = controller.clear_all();
}
loop {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let released = {
let beacon = beacon_arc.lock().unwrap();
!beacon.get_touch_sensor_state().unwrap_or(true)
};
if released {
break;
}
}
}
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
} else {
info!("No beacon device found - touch sensor monitoring disabled");
}
});
let app = create_router(state.clone());
let addr = SocketAddr::from(([0, 0, 0, 0], args.port));
let host = args.host;
let url = format!("http://{}:{}/", host, args.port);
info!("Starting PATLITE Beacon Server");
info!("API URL: {}", url);
if api_key.is_some() {
info!("API key authentication enabled");
} else {
info!("Running without authentication (API key not required)");
}
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
fn create_router(state: AppState) -> Router {
let api_routes = Router::new()
.route("/", get(get_status))
.route("/status", get(get_status))
.route("/light", put(set_light).delete(clear_light))
.route("/sequence", post(set_sequence).delete(stop_sequence))
.route("/update", post(update_status))
.route("/buzzer", put(set_buzzer).delete(stop_buzzer))
.route("/buzzer/pattern", post(set_buzzer_pattern))
.route("/clear", post(clear_all))
.route("/test", post(test_sequence))
.layer(middleware::from_fn_with_state(
state.clone(),
auth_middleware,
));
Router::new()
.merge(api_routes)
.layer(CorsLayer::permissive())
.with_state(state)
}
async fn get_status(State(state): State<AppState>) -> Result<Json<BeaconStatus>, StatusCode> {
let beacon = state.beacon.read().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(beacon.get_status()))
}
async fn set_light(
State(state): State<AppState>,
Json(payload): Json<LightSettings>,
) -> Result<Json<BeaconStatus>, StatusCode> {
let mut beacon = state.beacon.write().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let color_enum = match payload.color.to_lowercase().as_str() {
"red" => LightColor::Red,
"yellow" | "amber" => LightColor::Yellow,
"green" => LightColor::Green,
"blue" => LightColor::Blue,
"white" | "clear" => LightColor::White,
_ => return Err(StatusCode::BAD_REQUEST),
};
beacon.set_light(color_enum, payload.mode, payload.duration_ms)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(beacon.get_status()))
}
async fn clear_light(
State(state): State<AppState>,
) -> Result<Json<BeaconStatus>, StatusCode> {
let mut beacon = state.beacon.write().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
beacon.clear_light()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(beacon.get_status()))
}
async fn set_sequence(
State(state): State<AppState>,
Json(payload): Json<LightSequence>,
) -> Result<Json<BeaconStatus>, StatusCode> {
let mut beacon = state.beacon.write().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
beacon.start_sequence(payload)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(beacon.get_status()))
}
async fn stop_sequence(
State(state): State<AppState>,
) -> Result<Json<BeaconStatus>, StatusCode> {
let mut beacon = state.beacon.write().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
beacon.stop_sequence();
beacon.clear_light()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(beacon.get_status()))
}
async fn update_status(
State(state): State<AppState>,
) -> Result<Json<BeaconStatus>, StatusCode> {
let mut beacon = state.beacon.write().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
beacon.update_sequence()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(beacon.get_status()))
}
async fn set_buzzer(
State(state): State<AppState>,
Json(payload): Json<BuzzerSettings>,
) -> Result<Json<BeaconStatus>, StatusCode> {
let mut beacon = state.beacon.write().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
beacon.set_buzzer(payload.pattern, payload.volume, payload.duration_ms)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(beacon.get_status()))
}
async fn stop_buzzer(
State(state): State<AppState>,
) -> Result<Json<BeaconStatus>, StatusCode> {
let mut beacon = state.beacon.write().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
beacon.stop_buzzer()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(beacon.get_status()))
}
async fn set_buzzer_pattern(
State(state): State<AppState>,
Json(payload): Json<BuzzerPatternSettings>,
) -> Result<Json<BeaconStatus>, StatusCode> {
let mut beacon = state.beacon.write().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
beacon.set_buzzer_pattern(payload.pattern, payload.repetitions, payload.volume, payload.duration_ms)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(beacon.get_status()))
}
async fn clear_all(
State(state): State<AppState>,
) -> Result<Json<BeaconStatus>, StatusCode> {
let mut beacon = state.beacon.write().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
beacon.clear_all()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(beacon.get_status()))
}
async fn test_sequence(
State(state): State<AppState>,
) -> Result<Json<TestResult>, StatusCode> {
let results = {
let mut beacon = state.beacon.write().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut results = Vec::new();
let sequence = LightSequence {
commands: vec![
LightCommand {
color: LightColor::Red,
mode: LightMode::On,
duration_ms: 1000,
},
LightCommand {
color: LightColor::Yellow,
mode: LightMode::On,
duration_ms: 1000,
},
LightCommand {
color: LightColor::Green,
mode: LightMode::On,
duration_ms: 1000,
},
LightCommand {
color: LightColor::Blue,
mode: LightMode::On,
duration_ms: 1000,
},
LightCommand {
color: LightColor::White,
mode: LightMode::On,
duration_ms: 1000,
},
],
loop_sequence: false,
};
if let Err(e) = beacon.start_sequence(sequence) {
results.push(format!("Failed to start test sequence: {}", e));
} else {
results.push("Test sequence started".to_string());
for i in 0..5 {
beacon.update_sequence().ok();
results.push(format!("Step {} executed", i + 1));
std::thread::sleep(std::time::Duration::from_millis(1000));
}
}
beacon.clear_all().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
results
};
Ok(Json(TestResult {
success: true,
results,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
#[tokio::test]
async fn test_get_status_without_api_key() {
let state = AppState {
beacon: Arc::new(RwLock::new(BeaconController::mock())),
api_key: None,
};
let app = create_router(state);
let response = app
.oneshot(
Request::builder()
.uri("/")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_get_status_with_valid_api_key() {
let api_key = "test-key-123";
let state = AppState {
beacon: Arc::new(RwLock::new(BeaconController::mock())),
api_key: Some(api_key.to_string()),
};
let app = create_router(state);
let response = app
.oneshot(
Request::builder()
.uri(format!("/?apiKey={}", api_key))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_get_status_with_invalid_api_key() {
let state = AppState {
beacon: Arc::new(RwLock::new(BeaconController::mock())),
api_key: Some("correct-key".to_string()),
};
let app = create_router(state);
let response = app
.oneshot(
Request::builder()
.uri("/?apiKey=wrong-key")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_set_light_endpoint() {
let state = AppState {
beacon: Arc::new(RwLock::new(BeaconController::mock())),
api_key: None,
};
let app = create_router(state);
let body = serde_json::to_string(&LightSettings {
color: "red".to_string(),
mode: LightMode::On,
duration_ms: Some(1000),
})
.unwrap();
let response = app
.oneshot(
Request::builder()
.method("PUT")
.uri("/light")
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_clear_all_endpoint() {
let state = AppState {
beacon: Arc::new(RwLock::new(BeaconController::mock())),
api_key: None,
};
let app = create_router(state);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/clear")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}