This commit is contained in:
2024-12-30 17:22:16 -05:00
parent 10e80a0c2d
commit c7ca250b66
20 changed files with 529 additions and 472 deletions

View File

@@ -4,7 +4,9 @@ use crate::core::{
TelemetryDataType, TelemetryDefinitionRequest, TelemetryDefinitionResponse,
TelemetryInsertResponse, TelemetryItem, TelemetryValue, Uuid,
};
use crate::telemetry::{TelemetryDataItem, TelemetryDataValue, TelemetryManagementService};
use crate::telemetry::data_item::TelemetryDataItem;
use crate::telemetry::data_value::TelemetryDataValue;
use crate::telemetry::management_service::TelemetryManagementService;
use chrono::{DateTime, SecondsFormat};
use log::{error, trace};
use std::error::Error;

View File

@@ -1,232 +0,0 @@
use crate::telemetry::{TelemetryDataItem, TelemetryManagementService};
use actix_web::http::header::ContentType;
use actix_web::http::StatusCode;
use actix_web::{error, get, rt, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
use actix_ws::AggregatedMessage;
use derive_more::{Display, Error};
use log::{error, trace};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
use tokio::{pin, select};
use tokio::sync::mpsc::Sender;
use tokio::time::{sleep, Instant};
use tokio_util::sync::CancellationToken;
use tonic::codegen::tokio_stream::StreamExt;
#[derive(Debug, Display, Error)]
enum UserError {
#[display("Telemetry Not Found: {tlm}")]
TlmNotFound { tlm: String },
}
impl error::ResponseError for UserError {
fn status_code(&self) -> StatusCode {
match *self {
UserError::TlmNotFound { .. } => StatusCode::NOT_FOUND,
}
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(self.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RegisterTlmListenerRequest {
uuid: String,
minimum_separation_ms: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
enum WebsocketRequest {
RegisterTlmListener(RegisterTlmListenerRequest),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
enum WebsocketResponse {
TlmValue {
uuid: String,
value: Option<TelemetryDataItem>,
},
}
#[get("/tlm/{name:[\\w\\d/_-]+}")]
async fn get_tlm_definition(
data: web::Data<Arc<TelemetryManagementService>>,
name: web::Path<String>,
) -> Result<impl Responder, UserError> {
let string = name.to_string();
trace!("get_tlm_definition {}", string);
let Some(data) = data.get_by_name(&string) else {
return Err(UserError::TlmNotFound { tlm: string });
};
Ok(web::Json(data.definition.clone()))
}
fn handle_register_tlm_listener(data: &Arc<TelemetryManagementService>, request: RegisterTlmListenerRequest, tx: &Sender<WebsocketResponse>) {
if let Some(tlm_data) = data.get_by_uuid(&request.uuid) {
let minimum_separation = Duration::from_millis(request.minimum_separation_ms as u64);
let mut rx = tlm_data.data.subscribe();
let tx = tx.clone();
rt::spawn(async move {
let mut last_sent_at = Instant::now() - minimum_separation;
let mut last_value = None;
let sleep = sleep(Duration::from_millis(0));
pin!(sleep);
loop {
select! {
_ = tx.closed() => {
break;
}
Ok(_) = rx.changed() => {
let now = Instant::now();
let value = {
let ref_val = rx.borrow_and_update();
ref_val.clone()
};
if last_sent_at + minimum_separation > now {
last_value = value;
sleep.as_mut().reset(last_sent_at + minimum_separation);
continue;
} else {
last_value = None;
last_sent_at = now;
}
let _ = tx.send(WebsocketResponse::TlmValue {
uuid: request.uuid.clone(),
value,
}).await;
}
() = &mut sleep => {
if let Some(value) = last_value {
let _ = tx.send(WebsocketResponse::TlmValue {
uuid: request.uuid.clone(),
value: Some(value),
}).await;
}
last_value = None;
let now = Instant::now();
last_sent_at = now;
}
}
}
});
}
}
async fn handle_websocket_message(data: &Arc<TelemetryManagementService>, request: WebsocketRequest, tx: &Sender<WebsocketResponse>) {
match request {
WebsocketRequest::RegisterTlmListener(request) => handle_register_tlm_listener(data, request, tx),
};
}
async fn websocket_connect(
req: HttpRequest,
stream: web::Payload,
data: web::Data<Arc<TelemetryManagementService>>,
cancel_token: web::Data<CancellationToken>,
) -> Result<HttpResponse, actix_web::Error> {
trace!("websocket_connect");
let (res, mut session, stream) = actix_ws::handle(&req, stream)?;
let mut stream = stream
.aggregate_continuations()
// up to 1 MiB
.max_continuation_size(2_usize.pow(20));
let cancel_token = cancel_token.get_ref().clone();
rt::spawn(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel::<WebsocketResponse>(128);
loop {
select! {
_ = cancel_token.cancelled() => {
break;
},
Some(msg) = rx.recv() => {
let msg_json = match serde_json::to_string(&msg) {
Ok(msg_json) => msg_json,
Err(err) => {
error!("JSON Serialization Error Encountered {err}");
break;
},
};
if let Err(err) = session.text(msg_json).await {
error!("Tx Error Encountered {err}");
break;
}
},
Some(msg) = stream.next() => {
let result = match msg {
Ok(AggregatedMessage::Close(_)) => {
break;
},
Ok(AggregatedMessage::Text(msg)) => {
match serde_json::from_str::<WebsocketRequest>(&msg) {
Ok(request) => {
handle_websocket_message(data.get_ref(), request, &tx).await;
},
Err(err) => {
error!("JSON Deserialization Error Encountered {err}");
break;
}
}
Ok(())
},
Ok(AggregatedMessage::Ping(msg)) => {
session.pong(&msg).await
},
Err(err) => {
error!("Rx Error Encountered {err}");
break;
},
_ => {
error!("Unexpected Message");
break;
}
};
if let Err(err) = result {
error!("Tx Error Encountered {err}");
break;
}
},
else => {
break;
},
}
}
rx.close();
let _ = session.close(None).await;
});
Ok(res)
}
fn setup_api(cfg: &mut web::ServiceConfig) {
cfg.service(get_tlm_definition);
}
pub async fn setup(
cancellation_token: CancellationToken,
telemetry_definitions: Arc<TelemetryManagementService>,
) -> Result<(), Box<dyn Error>> {
let data = web::Data::new(telemetry_definitions);
let cancel_token = web::Data::new(cancellation_token);
trace!("Starting HTTP Server");
HttpServer::new(move || {
App::new()
.app_data(data.clone())
.app_data(cancel_token.clone())
.route("/ws", web::get().to(websocket_connect))
.service(web::scope("/api").configure(setup_api))
})
.bind("localhost:8080")?
.run()
.await?;
Ok(())
}

View File

@@ -0,0 +1,23 @@
use crate::http::error::HttpServerResultError;
use crate::telemetry::management_service::TelemetryManagementService;
use actix_web::{get, web, Responder};
use log::trace;
use std::sync::Arc;
#[get("/tlm/{name:[\\w\\d/_-]+}")]
async fn get_tlm_definition(
data: web::Data<Arc<TelemetryManagementService>>,
name: web::Path<String>,
) -> Result<impl Responder, HttpServerResultError> {
let string = name.to_string();
trace!("get_tlm_definition {}", string);
let Some(data) = data.get_by_name(&string) else {
return Err(HttpServerResultError::TlmNotFound { tlm: string });
};
Ok(web::Json(data.definition.clone()))
}
pub fn setup_api(cfg: &mut web::ServiceConfig) {
cfg.service(get_tlm_definition);
}

27
server/src/http/error.rs Normal file
View File

@@ -0,0 +1,27 @@
use actix_web::error::ResponseError;
use actix_web::http::header::ContentType;
use actix_web::http::StatusCode;
use actix_web::HttpResponse;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum WebsocketResponseError {}
#[derive(Error, Debug)]
pub enum HttpServerResultError {
#[error("Telemetry Not Found: {tlm}")]
TlmNotFound { tlm: String },
}
impl ResponseError for HttpServerResultError {
fn status_code(&self) -> StatusCode {
match *self {
HttpServerResultError::TlmNotFound { .. } => StatusCode::NOT_FOUND,
}
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(self.to_string())
}
}

34
server/src/http/mod.rs Normal file
View File

@@ -0,0 +1,34 @@
mod api;
mod error;
mod websocket;
use crate::http::api::setup_api;
use crate::http::websocket::setup_websocket;
use crate::telemetry::management_service::TelemetryManagementService;
use actix_web::{web, App, HttpServer};
use log::trace;
use std::error::Error;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
pub async fn setup(
cancellation_token: CancellationToken,
telemetry_definitions: Arc<TelemetryManagementService>,
) -> Result<(), Box<dyn Error>> {
let data = web::Data::new(telemetry_definitions);
let cancel_token = web::Data::new(cancellation_token);
trace!("Starting HTTP Server");
HttpServer::new(move || {
App::new()
.app_data(data.clone())
.app_data(cancel_token.clone())
.service(web::scope("/ws").configure(setup_websocket))
.service(web::scope("/api").configure(setup_api))
})
.bind("localhost:8080")?
.run()
.await?;
Ok(())
}

View File

@@ -0,0 +1,170 @@
use crate::http::websocket::request::{RegisterTlmListenerRequest, WebsocketRequest};
use crate::http::websocket::response::{TlmValueResponse, WebsocketResponse};
use crate::telemetry::management_service::TelemetryManagementService;
use actix_web::{rt, web, HttpRequest, HttpResponse};
use actix_ws::{AggregatedMessage, ProtocolError, Session};
use anyhow::anyhow;
use log::{error, trace};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::Sender;
use tokio::time::{sleep, Instant};
use tokio::{pin, select};
use tokio_util::sync::CancellationToken;
use tonic::codegen::tokio_stream::StreamExt;
pub mod request;
pub mod response;
fn handle_register_tlm_listener(
data: &Arc<TelemetryManagementService>,
request: RegisterTlmListenerRequest,
tx: &Sender<WebsocketResponse>,
) {
if let Some(tlm_data) = data.get_by_uuid(&request.uuid) {
let minimum_separation = Duration::from_millis(request.minimum_separation_ms as u64);
let mut rx = tlm_data.data.subscribe();
let tx = tx.clone();
rt::spawn(async move {
let mut last_sent_at = Instant::now() - minimum_separation;
let mut last_value = None;
let sleep = sleep(Duration::from_millis(0));
pin!(sleep);
loop {
select! {
_ = tx.closed() => {
break;
}
Ok(_) = rx.changed() => {
let now = Instant::now();
let value = {
let ref_val = rx.borrow_and_update();
ref_val.clone()
};
if last_sent_at + minimum_separation > now {
last_value = value;
sleep.as_mut().reset(last_sent_at + minimum_separation);
continue;
} else {
last_value = None;
last_sent_at = now;
}
let _ = tx.send(TlmValueResponse {
uuid: request.uuid.clone(),
value,
}.into()).await;
}
() = &mut sleep => {
if let Some(value) = last_value {
let _ = tx.send(TlmValueResponse {
uuid: request.uuid.clone(),
value: Some(value),
}.into()).await;
}
last_value = None;
let now = Instant::now();
last_sent_at = now;
}
}
}
});
}
}
async fn handle_websocket_message(
data: &Arc<TelemetryManagementService>,
request: WebsocketRequest,
tx: &Sender<WebsocketResponse>,
) {
match request {
WebsocketRequest::RegisterTlmListener(request) => {
handle_register_tlm_listener(data, request, tx)
}
};
}
async fn handle_websocket_response(
msg: WebsocketResponse,
session: &mut Session,
) -> anyhow::Result<bool> {
let msg_json = match serde_json::to_string(&msg) {
Ok(msg_json) => msg_json,
Err(err) => {
return Err(anyhow!("JSON Serialization Error Encountered {err}"));
}
};
if let Err(err) = session.text(msg_json).await {
return Err(anyhow!("Tx Error Encountered {err}"));
}
Ok(true)
}
async fn handle_websocket_incoming(
msg: Result<AggregatedMessage, ProtocolError>,
data: &Arc<TelemetryManagementService>,
session: &mut Session,
tx: &Sender<WebsocketResponse>,
) -> anyhow::Result<bool> {
match msg {
Ok(AggregatedMessage::Close(_)) => Ok(false),
Ok(AggregatedMessage::Text(msg)) => match serde_json::from_str::<WebsocketRequest>(&msg) {
Ok(request) => {
handle_websocket_message(data, request, tx).await;
Ok(true)
}
Err(err) => Err(anyhow!("JSON Deserialization Error Encountered {err}")),
},
Ok(AggregatedMessage::Ping(msg)) => match session.pong(&msg).await {
Ok(_) => Ok(true),
Err(err) => Err(anyhow!("Pong Encountered Error: {err}")),
},
Err(err) => Err(anyhow!("Rx Error Encountered {err}")),
_ => Err(anyhow!("Unexpected Message")),
}
}
pub async fn websocket_connect(
req: HttpRequest,
stream: web::Payload,
data: web::Data<Arc<TelemetryManagementService>>,
cancel_token: web::Data<CancellationToken>,
) -> Result<HttpResponse, actix_web::Error> {
trace!("websocket_connect");
let (res, mut session, stream) = actix_ws::handle(&req, stream)?;
let mut stream = stream
.aggregate_continuations()
// up to 1 MiB
.max_continuation_size(2_usize.pow(20));
let cancel_token = cancel_token.get_ref().clone();
rt::spawn(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel::<WebsocketResponse>(128);
loop {
let result = select! {
_ = cancel_token.cancelled() => Ok(false),
Some(msg) = rx.recv() => handle_websocket_response(msg, &mut session).await,
Some(msg) = stream.next() => handle_websocket_incoming(msg, data.get_ref(), &mut session, &tx).await,
else => Ok(false),
};
match result {
Ok(true) => {}
Ok(false) => break,
Err(err) => {
error!("{}", err);
break;
}
}
}
rx.close();
let _ = session.close(None).await;
});
Ok(res)
}
pub fn setup_websocket(cfg: &mut web::ServiceConfig) {
cfg.route("", web::get().to(websocket_connect));
}

View File

@@ -0,0 +1,13 @@
use derive_more::From;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterTlmListenerRequest {
pub uuid: String,
pub minimum_separation_ms: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, From)]
pub enum WebsocketRequest {
RegisterTlmListener(RegisterTlmListenerRequest),
}

View File

@@ -0,0 +1,14 @@
use crate::telemetry::data_item::TelemetryDataItem;
use derive_more::From;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TlmValueResponse {
pub uuid: String,
pub value: Option<TelemetryDataItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize, From)]
pub enum WebsocketResponse {
TlmValue(TlmValueResponse),
}

View File

@@ -7,7 +7,7 @@ pub mod core {
tonic::include_proto!("core");
}
use crate::telemetry::TelemetryManagementService;
use crate::telemetry::management_service::TelemetryManagementService;
use std::error::Error;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;

View File

@@ -1,113 +0,0 @@
use crate::core::{TelemetryDataType, TelemetryDefinitionRequest, Uuid};
use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::error::Error;
use std::fmt::Formatter;
use papaya::HashMap;
fn tlm_data_type_serialzier<S>(
tlm_data_type: &TelemetryDataType,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(tlm_data_type.as_str_name())
}
struct TlmDataTypeVisitor;
impl Visitor<'_> for TlmDataTypeVisitor {
type Value = TelemetryDataType;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("A &str")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
TelemetryDataType::from_str_name(v).ok_or(E::custom("Invalid TelemetryDataType"))
}
}
fn tlm_data_type_deserialzier<'de, D>(deserializer: D) -> Result<TelemetryDataType, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(TlmDataTypeVisitor)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryDefinition {
pub uuid: String,
pub name: String,
#[serde(serialize_with = "tlm_data_type_serialzier")]
#[serde(deserialize_with = "tlm_data_type_deserialzier")]
pub data_type: TelemetryDataType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TelemetryDataValue {
Float32(f32),
Float64(f64),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryDataItem {
pub value: TelemetryDataValue,
pub timestamp: String,
}
#[derive(Clone)]
pub struct TelemetryData {
pub definition: TelemetryDefinition,
pub data: tokio::sync::watch::Sender<Option<TelemetryDataItem>>,
}
pub struct TelemetryManagementService {
uuid_index: HashMap<String, String>,
tlm_data: HashMap<String, TelemetryData>,
}
impl TelemetryManagementService {
pub fn new() -> Self {
Self {
uuid_index: HashMap::new(),
tlm_data: HashMap::new(),
}
}
pub fn register(
&self,
telemetry_definition_request: TelemetryDefinitionRequest,
) -> Result<String, Box<dyn Error>> {
let uuid_index = self.uuid_index.pin();
let tlm_data = self.tlm_data.pin();
let uuid = uuid_index.get_or_insert_with(telemetry_definition_request.name.clone(), || Uuid::random().value).clone();
let _ = tlm_data.try_insert(uuid.clone(), TelemetryData {
definition: TelemetryDefinition {
uuid: uuid.clone(),
name: telemetry_definition_request.name.clone(),
data_type: telemetry_definition_request.data_type(),
},
data: tokio::sync::watch::channel(None).0,
});
Ok(uuid)
}
pub fn get_by_name(&self, name: &String) -> Option<TelemetryData> {
let uuid_index = self.uuid_index.pin();
let uuid = uuid_index.get(name)?;
self.get_by_uuid(uuid)
}
pub fn get_by_uuid(&self, uuid: &String) -> Option<TelemetryData> {
let tlm_data = self.tlm_data.pin();
tlm_data.get(uuid).cloned()
}
}

View File

@@ -0,0 +1,8 @@
use crate::telemetry::data_item::TelemetryDataItem;
use crate::telemetry::definition::TelemetryDefinition;
#[derive(Clone)]
pub struct TelemetryData {
pub definition: TelemetryDefinition,
pub data: tokio::sync::watch::Sender<Option<TelemetryDataItem>>,
}

View File

@@ -0,0 +1,8 @@
use crate::telemetry::data_value::TelemetryDataValue;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryDataItem {
pub value: TelemetryDataValue,
pub timestamp: String,
}

View File

@@ -0,0 +1,38 @@
use crate::core::TelemetryDataType;
use serde::de::Visitor;
use serde::{Deserializer, Serializer};
use std::fmt::Formatter;
pub fn tlm_data_type_serializer<S>(
tlm_data_type: &TelemetryDataType,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(tlm_data_type.as_str_name())
}
struct TlmDataTypeVisitor;
impl Visitor<'_> for TlmDataTypeVisitor {
type Value = TelemetryDataType;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("A &str")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
TelemetryDataType::from_str_name(v).ok_or(E::custom("Invalid TelemetryDataType"))
}
}
pub fn tlm_data_type_deserializer<'de, D>(deserializer: D) -> Result<TelemetryDataType, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(TlmDataTypeVisitor)
}

View File

@@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TelemetryDataValue {
Float32(f32),
Float64(f64),
}

View File

@@ -0,0 +1,13 @@
use crate::core::TelemetryDataType;
use crate::telemetry::data_type::tlm_data_type_deserializer;
use crate::telemetry::data_type::tlm_data_type_serializer;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryDefinition {
pub uuid: String,
pub name: String,
#[serde(serialize_with = "tlm_data_type_serializer")]
#[serde(deserialize_with = "tlm_data_type_deserializer")]
pub data_type: TelemetryDataType,
}

View File

@@ -0,0 +1,58 @@
use crate::core::{TelemetryDefinitionRequest, Uuid};
use crate::telemetry::data::TelemetryData;
use crate::telemetry::definition::TelemetryDefinition;
use papaya::HashMap;
use std::error::Error;
pub struct TelemetryManagementService {
uuid_index: HashMap<String, String>,
tlm_data: HashMap<String, TelemetryData>,
}
impl TelemetryManagementService {
pub fn new() -> Self {
Self {
uuid_index: HashMap::new(),
tlm_data: HashMap::new(),
}
}
pub fn register(
&self,
telemetry_definition_request: TelemetryDefinitionRequest,
) -> Result<String, Box<dyn Error>> {
let uuid_index = self.uuid_index.pin();
let tlm_data = self.tlm_data.pin();
let uuid = uuid_index
.get_or_insert_with(telemetry_definition_request.name.clone(), || {
Uuid::random().value
})
.clone();
let _ = tlm_data.try_insert(
uuid.clone(),
TelemetryData {
definition: TelemetryDefinition {
uuid: uuid.clone(),
name: telemetry_definition_request.name.clone(),
data_type: telemetry_definition_request.data_type(),
},
data: tokio::sync::watch::channel(None).0,
},
);
Ok(uuid)
}
pub fn get_by_name(&self, name: &String) -> Option<TelemetryData> {
let uuid_index = self.uuid_index.pin();
let uuid = uuid_index.get(name)?;
self.get_by_uuid(uuid)
}
pub fn get_by_uuid(&self, uuid: &String) -> Option<TelemetryData> {
let tlm_data = self.tlm_data.pin();
tlm_data.get(uuid).cloned()
}
}

View File

@@ -0,0 +1,6 @@
pub mod data;
pub mod data_item;
pub mod data_type;
pub mod data_value;
pub mod definition;
pub mod management_service;