pub mod cpu;
pub mod gpu;
pub mod server;
pub use cpu::{cpu_routes, CpuPowerSamplingPeriod};
pub use gpu::{gpu_control_routes, gpu_read_routes};
pub use server::{server_routes, CpuDiscoveryInfo, DiscoveryInfo, GpuDiscoveryInfo};
use actix_web::web::Bytes;
use actix_web::HttpResponse;
use futures::stream::{self, LocalBoxStream};
use futures::StreamExt;
use serde::Serialize;
use crate::power_streaming::PowerBroadcasts;
fn bad_request(error: String) -> HttpResponse {
HttpResponse::BadRequest().json(serde_json::json!({ "error": error }))
}
fn parse_device_ids(raw: &str, device: &str) -> Result<Vec<usize>, HttpResponse> {
let param = format!("{}_ids", device.to_lowercase());
if raw.trim().is_empty() {
return Err(bad_request(format!(
"{param} must contain at least one {device} index"
)));
}
let mut ids = Vec::new();
for part in raw.split(',') {
let trimmed = part.trim();
if trimmed.is_empty() {
return Err(bad_request(format!(
"{param} contains an empty {device} index"
)));
}
let id = trimmed
.parse()
.map_err(|_| bad_request(format!("Invalid {device} index: {trimmed}")))?;
ids.push(id);
}
ids.sort_unstable();
ids.dedup();
Ok(ids)
}
fn resolve_read_device_ids(
query: &Option<String>,
device_count: usize,
device: &str,
) -> Result<Vec<usize>, HttpResponse> {
match query {
Some(raw) => {
let parsed = parse_device_ids(raw, device)?;
for &id in &parsed {
if id >= device_count {
return Err(bad_request(format!("{device} {id} not found")));
}
}
Ok(parsed)
}
None => Ok((0..device_count).collect()),
}
}
fn resolve_stream_device_ids<T>(
query: &Option<String>,
broadcasts: &PowerBroadcasts<T>,
device: &str,
) -> Result<Vec<usize>, HttpResponse>
where
T: Clone + Default + Send + Sync + 'static,
{
let ids = match query {
Some(raw) => parse_device_ids(raw, device)?,
None => broadcasts.valid_ids(),
};
if let Err(unknown) = broadcasts.validate_ids(&ids) {
return Err(bad_request(format!(
"Unknown {device} indices: {unknown:?}. Available: {:?}",
broadcasts.valid_ids(),
)));
}
Ok(ids)
}
fn power_stream_response<T>(device_ids: Vec<usize>, broadcasts: &PowerBroadcasts<T>) -> HttpResponse
where
T: Clone + Default + Send + Sync + Serialize + 'static,
{
let mut streams: Vec<LocalBoxStream<'static, Result<Bytes, actix_web::Error>>> =
Vec::with_capacity(device_ids.len());
for device_id in device_ids {
let Some(broadcast) = broadcasts.get(device_id).cloned() else {
continue;
};
let stream = broadcast.stream();
let guard = broadcast.add_subscriber();
streams.push(
stream
.map(move |sample| {
let _ = &guard;
let json = serde_json::to_string(&sample).unwrap_or_default();
Ok::<_, actix_web::Error>(Bytes::from(format!("data: {json}\n\n")))
})
.boxed_local(),
);
}
let stream: LocalBoxStream<'static, Result<Bytes, actix_web::Error>> = if streams.is_empty() {
stream::pending().boxed_local()
} else {
stream::select_all(streams).boxed_local()
};
HttpResponse::Ok()
.insert_header(("Content-Type", "text/event-stream"))
.insert_header(("Cache-Control", "no-cache"))
.streaming(stream)
}