use rocket::{get, post, routes, serde::json::Json, State}; use crate::storage::AllControllers; mod static_handler; pub struct ServerState { primary_name: String, data: AllControllers, tx_to_controllers: crate::controller::MultiTx, } impl ServerState { pub fn new( primary_name: &impl ToString, data: AllControllers, tx_to_controllers: crate::controller::MultiTx, ) -> Self { let primary_name = primary_name.to_string(); Self { primary_name, data, tx_to_controllers, } } } pub fn rocket(state: ServerState) -> rocket::Rocket { // serve the html from disk if running in a debug build // this allows editing the webpage without having to rebuild the executable // but in release builds, bundle the entire webapp folder into the exe let fileserver: Vec = if cfg!(debug_assertions) { rocket::fs::FileServer::from(format!( "{}/webapp", std::env::var("CARGO_MANIFEST_DIR").unwrap() )) .into() } else { static_handler::UiStatic {}.into() }; rocket::build() .attach(Cors) .manage(state) .mount("/", fileserver) .mount( "/", routes![ metrics, interfaces, all_interfaces, all_interfaces_full, primary_interface, interface, interface_full, get_control, enable_control, disable_control ], ) } #[get("/interfaces")] fn interfaces(state: &State) -> Json> { Json(state.data.controller_names().cloned().collect()) } #[get("/interfaces/primary")] async fn primary_interface( state: &State, ) -> Result, ServerError> { let s = state .data .get(&state.primary_name) .ok_or(ServerError::InvalidPrimaryName)? .read_state() .await; Ok(Json(s.as_ref().ok_or(ServerError::NoData)?.common())) } #[get("/interfaces/data")] async fn all_interfaces( state: &State, ) -> Json> { let mut data = Vec::new(); for (k, v) in state.data.all_data() { let v = v.read_state().await; if let Some(v) = v.as_ref() { data.push((k.clone(), v.common().clone())); } } Json(data) } #[get("/interfaces/data/full")] async fn all_interfaces_full( state: &State, ) -> Json> { let mut data = Vec::new(); for (k, v) in state.data.all_data() { let v = v.read_state().await; if let Some(v) = v.as_ref() { data.push((k.clone(), v.clone())); } } Json(data) } #[get("/interface/")] async fn interface( name: &str, state: &State, ) -> Result, ServerError> { let data = state .data .get(name) .ok_or(ServerError::NotFound)? .read_state() .await; Ok(Json(data.as_ref().ok_or(ServerError::NoData)?.common())) } #[get("/interface//full")] async fn interface_full( name: &str, state: &State, ) -> Result, ServerError> { let data = state .data .get(name) .ok_or(ServerError::NotFound)? .read_state() .await; Ok(Json(data.as_ref().ok_or(ServerError::NoData)?.clone())) } #[get("/metrics")] fn metrics() -> Result { Ok( prometheus::TextEncoder::new() .encode_to_string(&prometheus::default_registry().gather())?, ) } #[derive(serde::Serialize)] struct ControlState { enabled: bool, } #[get("/control")] async fn get_control() -> Json { let enabled = crate::config::access_config() .await .enable_secondary_control; Json(ControlState { enabled }) } #[post("/control/enable")] async fn enable_control() { log::warn!("enabling control"); crate::config::write_to_config() .await .enable_secondary_control = true; } #[post("/control/disable")] async fn disable_control(state: &State) { log::warn!("disabling control"); crate::config::write_to_config() .await .enable_secondary_control = false; state .tx_to_controllers .send_to_all(crate::controller::VoltageCommand::Set(-1.0)); } enum ServerError { Prometheus, NotFound, InvalidPrimaryName, NoData, } impl From for ServerError { fn from(_: prometheus::Error) -> Self { Self::Prometheus } } impl<'a> rocket::response::Responder<'a, 'a> for ServerError { fn respond_to(self, _: &'a rocket::Request<'_>) -> rocket::response::Result<'a> { Err(match self { Self::NotFound => rocket::http::Status::NotFound, Self::InvalidPrimaryName => rocket::http::Status::ServiceUnavailable, Self::NoData | Self::Prometheus => rocket::http::Status::InternalServerError, }) } } pub struct Cors; #[rocket::async_trait] impl rocket::fairing::Fairing for Cors { fn info(&self) -> rocket::fairing::Info { rocket::fairing::Info { name: "Add CORS headers to responses", kind: rocket::fairing::Kind::Response, } } async fn on_response<'r>( &self, _request: &'r rocket::Request<'_>, response: &mut rocket::Response<'r>, ) { response.set_header(rocket::http::Header::new( "Access-Control-Allow-Origin", "*", )); response.set_header(rocket::http::Header::new( "Access-Control-Allow-Methods", "POST, GET, PATCH, OPTIONS", )); response.set_header(rocket::http::Header::new( "Access-Control-Allow-Headers", "*", )); response.set_header(rocket::http::Header::new( "Access-Control-Allow-Credentials", "true", )); } }