diff --git a/frontend/src/components/CommandList.vue b/frontend/src/components/CommandList.vue new file mode 100644 index 0000000..b089ce1 --- /dev/null +++ b/frontend/src/components/CommandList.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/frontend/src/components/CommandParameter.vue b/frontend/src/components/CommandParameter.vue new file mode 100644 index 0000000..e1e5490 --- /dev/null +++ b/frontend/src/components/CommandParameter.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/frontend/src/components/CommandSender.vue b/frontend/src/components/CommandSender.vue new file mode 100644 index 0000000..37d1315 --- /dev/null +++ b/frontend/src/components/CommandSender.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/frontend/src/composables/command.ts b/frontend/src/composables/command.ts new file mode 100644 index 0000000..13f4439 --- /dev/null +++ b/frontend/src/composables/command.ts @@ -0,0 +1,52 @@ +import { ref, toValue, watchEffect } from 'vue'; +import { type MaybeRefOrGetter } from 'vue'; + +export interface CommandParameterDefinition { + name: string; + data_type: string; +} + +export interface CommandDefinition { + name: string; + parameters: CommandParameterDefinition[]; +} + +export function useAllCommands() { + const data = ref(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const error = ref(null); + + watchEffect(async () => { + try { + const res = await fetch(`/api/cmd`); + data.value = await res.json(); + error.value = null; + } catch (e) { + data.value = null; + error.value = e; + } + }); + + return { data, error }; +} + +export function ueCommand(name: MaybeRefOrGetter) { + const data = ref(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const error = ref(null); + + watchEffect(async () => { + const name_value = toValue(name); + + try { + const res = await fetch(`/api/cmd/${name_value}`); + data.value = await res.json(); + error.value = null; + } catch (e) { + data.value = null; + error.value = e; + } + }); + + return { data, error }; +} diff --git a/frontend/src/panels/panel.ts b/frontend/src/panels/panel.ts index b721f93..45479c0 100644 --- a/frontend/src/panels/panel.ts +++ b/frontend/src/panels/panel.ts @@ -34,7 +34,12 @@ export function usePanelHeirarchy(): Ref { }, { name: 'Telemetry Elements', - to: { name: 'list' }, + to: { name: 'tlm' }, + type: PanelHeirarchyType.LEAF, + }, + { + name: 'Commands', + to: { name: 'cmd' }, type: PanelHeirarchyType.LEAF, }, { diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index e3a174c..13ef3a9 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -14,10 +14,15 @@ const router = createRouter({ component: () => import('../views/GraphView.vue'), }, { - path: '/list', - name: 'list', + path: '/tlm', + name: 'tlm', component: () => import('../views/TelemetryListView.vue'), }, + { + path: '/cmd', + name: 'cmd', + component: () => import('../views/CommandListView.vue'), + }, { path: '/chart', name: 'chart', diff --git a/frontend/src/views/CommandListView.vue b/frontend/src/views/CommandListView.vue new file mode 100644 index 0000000..c5f3f46 --- /dev/null +++ b/frontend/src/views/CommandListView.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/frontend/src/views/TelemetryListView.vue b/frontend/src/views/TelemetryListView.vue index 672873d..2859bd8 100644 --- a/frontend/src/views/TelemetryListView.vue +++ b/frontend/src/views/TelemetryListView.vue @@ -6,7 +6,6 @@ import type { TelemetryDefinition } from '@/composables/telemetry'; import TelemetryInfo from '@/components/TelemetryInfo.vue'; import FlexDivider from '@/components/FlexDivider.vue'; import ScreenLayout from '@/components/layout/ScreenLayout.vue'; -import { Direction } from '@/composables/Direction.ts'; import { ScreenType } from '@/composables/ScreenType.ts'; const searchValue = ref(''); @@ -16,36 +15,38 @@ const mousedover = ref(null); diff --git a/server/src/command/definition.rs b/server/src/command/definition.rs new file mode 100644 index 0000000..d724c8a --- /dev/null +++ b/server/src/command/definition.rs @@ -0,0 +1,36 @@ +use crate::command::service::RegisteredCommand; +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(Clone, Debug, Serialize, Deserialize)] +pub struct CommandParameterDefinition { + pub name: String, + #[serde(serialize_with = "tlm_data_type_serializer")] + #[serde(deserialize_with = "tlm_data_type_deserializer")] + pub data_type: TelemetryDataType, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CommandDefinition { + pub name: String, + pub parameters: Vec, +} + +impl From for CommandDefinition { + fn from(value: RegisteredCommand) -> Self { + Self { + name: value.name, + parameters: value + .definition + .parameters + .into_iter() + .map(|param| CommandParameterDefinition { + data_type: param.data_type(), + name: param.name, + }) + .collect(), + } + } +} diff --git a/server/src/command/error.rs b/server/src/command/error.rs new file mode 100644 index 0000000..aa0bbe0 --- /dev/null +++ b/server/src/command/error.rs @@ -0,0 +1,36 @@ +use crate::core::TelemetryDataType; +use actix_web::http::StatusCode; +use actix_web::ResponseError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Command Not Found {0}")] + CommandNotFound(String), + #[error("Incorrect Number of Parameters Specified. {expected} expected. {actual} found.")] + IncorrectParameterCount { expected: usize, actual: usize }, + #[error("Missing Parameter {0}.")] + MisingParameter(String), + #[error("Incorrect Parameter Type for {name}. {expected_type:?} expected.")] + WrongParameterType { + name: String, + expected_type: TelemetryDataType, + }, + #[error("No Command Receiver")] + NoCommandReceiver, + #[error("Failed to Send")] + FailedToSend, +} + +impl ResponseError for Error { + fn status_code(&self) -> StatusCode { + match *self { + Error::CommandNotFound(_) => StatusCode::NOT_FOUND, + Error::IncorrectParameterCount { .. } => StatusCode::BAD_REQUEST, + Error::MisingParameter(_) => StatusCode::BAD_REQUEST, + Error::WrongParameterType { .. } => StatusCode::BAD_REQUEST, + Error::NoCommandReceiver => StatusCode::SERVICE_UNAVAILABLE, + Error::FailedToSend => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} diff --git a/server/src/command/mod.rs b/server/src/command/mod.rs index 1f278a4..e3c4024 100644 --- a/server/src/command/mod.rs +++ b/server/src/command/mod.rs @@ -1 +1,3 @@ +mod definition; +pub mod error; pub mod service; diff --git a/server/src/command/service.rs b/server/src/command/service.rs index b4bcd83..452451e 100644 --- a/server/src/command/service.rs +++ b/server/src/command/service.rs @@ -1,16 +1,24 @@ +use crate::command::definition::CommandDefinition; +use crate::command::error::Error as CmdError; +use crate::command::error::Error::{ + CommandNotFound, FailedToSend, IncorrectParameterCount, MisingParameter, NoCommandReceiver, + WrongParameterType, +}; use crate::core::telemetry_value::Value; use crate::core::{ Command, CommandDefinitionRequest, TelemetryDataType, TelemetryValue, Timestamp, }; -use anyhow::{bail, ensure}; use chrono::{DateTime, Utc}; +use log::error; use papaya::HashMap; use std::collections::HashMap as StdHashMap; use tokio::sync::mpsc; use tokio::sync::mpsc::{Receiver, Sender}; -struct RegisteredCommand { - definition: CommandDefinitionRequest, +#[derive(Clone)] +pub(super) struct RegisteredCommand { + pub(super) name: String, + pub(super) definition: CommandDefinitionRequest, tx: Sender>, } @@ -25,6 +33,24 @@ impl CommandManagementService { } } + pub fn get_commands(&self) -> anyhow::Result> { + let mut result = vec![]; + + let registered_commands = self.registered_commands.pin(); + for registration in registered_commands.values() { + result.push(registration.clone().into()); + } + + Ok(result) + } + + pub fn get_command_definition(&self, name: &String) -> Option { + self.registered_commands + .pin() + .get(name) + .map(|registration| registration.clone().into()) + } + pub async fn register_command( &self, command: CommandDefinitionRequest, @@ -35,6 +61,7 @@ impl CommandManagementService { if let Some(previous) = registered_commands.insert( command.name.clone(), RegisteredCommand { + name: command.name.clone(), definition: command, tx, }, @@ -50,7 +77,7 @@ impl CommandManagementService { &self, name: impl Into, parameters: serde_json::Map, - ) -> anyhow::Result<()> { + ) -> Result<(), CmdError> { let timestamp = Utc::now(); let offset_from_unix_epoch = timestamp - DateTime::from_timestamp(0, 0).expect("Could not get Unix epoch"); @@ -58,19 +85,19 @@ impl CommandManagementService { let name = name.into(); let registered_commands = self.registered_commands.pin(); let Some(registration) = registered_commands.get(&name) else { - bail!("Command Not Found {name}"); + return Err(CommandNotFound(name)); }; - ensure!( - parameters.len() == registration.definition.parameters.len(), - "Command has {} parameters. {} expected", - parameters.len(), - registration.definition.parameters.len() - ); + if parameters.len() != registration.definition.parameters.len() { + return Err(IncorrectParameterCount { + expected: registration.definition.parameters.len(), + actual: parameters.len(), + }); + } let mut result_parameters = StdHashMap::new(); for parameter in ®istration.definition.parameters { let Some(param_value) = parameters.get(¶meter.name) else { - bail!("Command Missing Parameter: {}", parameter.name); + return Err(MisingParameter(parameter.name.clone())); }; let Some(param_value) = (match parameter.data_type() { TelemetryDataType::Float32 => { @@ -79,11 +106,10 @@ impl CommandManagementService { TelemetryDataType::Float64 => param_value.as_f64().map(Value::Float64), TelemetryDataType::Boolean => param_value.as_bool().map(Value::Boolean), }) else { - bail!( - "Parameter {} has the wrong type. {:?} expected", - parameter.name, - parameter.data_type() - ); + return Err(WrongParameterType { + name: parameter.name.clone(), + expected_type: parameter.data_type(), + }); }; result_parameters.insert( parameter.name.clone(), @@ -97,6 +123,10 @@ impl CommandManagementService { let tx = registration.tx.clone(); drop(registered_commands); + if tx.is_closed() { + return Err(NoCommandReceiver); + } + if let Err(e) = tx .send(Some(Command { timestamp: Some(Timestamp { @@ -107,7 +137,8 @@ impl CommandManagementService { })) .await { - bail!("Failed to send command {e}"); + error!("Failed to Send Command: {e}"); + return Err(FailedToSend); } Ok(()) diff --git a/server/src/http/api/cmd.rs b/server/src/http/api/cmd.rs index d08c74f..39de4b0 100644 --- a/server/src/http/api/cmd.rs +++ b/server/src/http/api/cmd.rs @@ -1,6 +1,6 @@ use crate::command::service::CommandManagementService; use crate::http::error::HttpServerResultError; -use actix_web::{post, web, Responder}; +use actix_web::{get, post, web, Responder}; use std::sync::Arc; #[post("/cmd/{name:[\\w\\d/_-]+}")] @@ -13,5 +13,22 @@ pub(super) async fn send_command( .send_command(name.to_string(), parameters.into_inner()) .await?; - Ok(web::Json(())) + Ok(web::Json("Command Sent Successfully.")) +} + +#[get("/cmd")] +pub(super) async fn get_all( + command_service: web::Data>, +) -> Result { + Ok(web::Json(command_service.get_commands()?)) +} + +#[get("/cmd/{name:[\\w\\d/_-]+}")] +pub(super) async fn get_one( + command_service: web::Data>, + name: web::Path, +) -> Result { + Ok(web::Json( + command_service.get_command_definition(&name.to_string()), + )) } diff --git a/server/src/http/api/mod.rs b/server/src/http/api/mod.rs index 82f5c15..0c7875b 100644 --- a/server/src/http/api/mod.rs +++ b/server/src/http/api/mod.rs @@ -13,5 +13,7 @@ pub fn setup_api(cfg: &mut web::ServiceConfig) { .service(panels::get_one) .service(panels::set) .service(panels::delete) - .service(cmd::send_command); + .service(cmd::send_command) + .service(cmd::get_all) + .service(cmd::get_one); } diff --git a/server/src/http/error.rs b/server/src/http/error.rs index 4027309..36ae486 100644 --- a/server/src/http/error.rs +++ b/server/src/http/error.rs @@ -2,7 +2,6 @@ use actix_web::error::ResponseError; use actix_web::http::header::ContentType; use actix_web::http::StatusCode; use actix_web::HttpResponse; -use anyhow::Error; use thiserror::Error; #[derive(Error, Debug)] @@ -16,31 +15,28 @@ pub enum HttpServerResultError { #[error("Timed out")] Timeout, #[error("Internal Error")] - InternalError(anyhow::Error), + InternalError(#[from] anyhow::Error), #[error("Panel Uuid Not Found: {uuid}")] PanelUuidNotFound { uuid: String }, + #[error(transparent)] + Command(#[from] crate::command::error::Error), } impl ResponseError for HttpServerResultError { fn status_code(&self) -> StatusCode { - match *self { + match self { HttpServerResultError::TlmNameNotFound { .. } => StatusCode::NOT_FOUND, HttpServerResultError::TlmUuidNotFound { .. } => StatusCode::NOT_FOUND, HttpServerResultError::InvalidDateTime { .. } => StatusCode::BAD_REQUEST, HttpServerResultError::Timeout => StatusCode::GATEWAY_TIMEOUT, HttpServerResultError::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR, HttpServerResultError::PanelUuidNotFound { .. } => StatusCode::NOT_FOUND, + HttpServerResultError::Command(inner) => inner.status_code(), } } fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()) - .insert_header(ContentType::html()) + .insert_header(ContentType::plaintext()) .body(self.to_string()) } } - -impl From for HttpServerResultError { - fn from(value: Error) -> Self { - Self::InternalError(value) - } -}