initial frontend command stuff
This commit is contained in:
70
frontend/src/components/CommandList.vue
Normal file
70
frontend/src/components/CommandList.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type CommandDefinition, useAllCommands } from '@/composables/command';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
search?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selected = defineModel<CommandDefinition | null>();
|
||||||
|
|
||||||
|
const search_value = computed(() => (props.search || '').toLowerCase());
|
||||||
|
|
||||||
|
const { data: command_data } = useAllCommands();
|
||||||
|
|
||||||
|
const sorted_cmd_data = computed(() => {
|
||||||
|
const cmd_data = command_data.value;
|
||||||
|
if (cmd_data != null) {
|
||||||
|
return cmd_data
|
||||||
|
.filter((entry) =>
|
||||||
|
entry.name.toLowerCase().includes(search_value.value),
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
function onClick(cmd_entry: CommandDefinition) {
|
||||||
|
selected.value = cmd_entry;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="sorted_cmd_data.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="cmd_entry in sorted_cmd_data"
|
||||||
|
:class="`row data ${selected?.name == cmd_entry.name ? 'selected' : ''}`"
|
||||||
|
:key="cmd_entry.name"
|
||||||
|
@click="() => onClick(cmd_entry)"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ cmd_entry.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="row">
|
||||||
|
<span> No Matches Found </span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use '@/assets/variables';
|
||||||
|
div {
|
||||||
|
padding: 0.3em;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: variables.$gray-3 solid 1px;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data.selected:has(~ .data:hover),
|
||||||
|
.data:hover ~ .data.selected {
|
||||||
|
background-color: variables.$light-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data.selected,
|
||||||
|
.data:hover {
|
||||||
|
background-color: variables.$light2-background-color;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
37
frontend/src/components/CommandParameter.vue
Normal file
37
frontend/src/components/CommandParameter.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CommandParameterDefinition } from '@/composables/command.ts';
|
||||||
|
import { computed, onMounted } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
parameter: CommandParameterDefinition;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const model = defineModel<any>(); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
|
||||||
|
const is_numeric = computed(() => {
|
||||||
|
return ['Float32', 'Float64'].some((x) => x == props.parameter.data_type);
|
||||||
|
});
|
||||||
|
|
||||||
|
const is_boolean = computed(() => {
|
||||||
|
return 'Boolean' == props.parameter.data_type;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (is_numeric.value) {
|
||||||
|
model.value = 0.0;
|
||||||
|
} else if (is_boolean.value) {
|
||||||
|
model.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row">
|
||||||
|
<label> {{ parameter.name }} </label>
|
||||||
|
<input v-if="is_numeric" type="number" v-model="model" />
|
||||||
|
<input v-else-if="is_boolean" type="checkbox" v-model="model" />
|
||||||
|
<span v-else>UNKNOWN INPUT</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
67
frontend/src/components/CommandSender.vue
Normal file
67
frontend/src/components/CommandSender.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CommandDefinition } from '@/composables/command.ts';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import CommandParameter from '@/components/CommandParameter.vue';
|
||||||
|
import FlexDivider from '@/components/FlexDivider.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
command: CommandDefinition | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const parameters = ref<any>({}); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
const busy = ref(false);
|
||||||
|
const result = ref('');
|
||||||
|
|
||||||
|
async function sendCommand() {
|
||||||
|
const command = props.command;
|
||||||
|
const params = parameters.value;
|
||||||
|
if (!command) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
busy.value = true;
|
||||||
|
result.value = 'Loading...';
|
||||||
|
|
||||||
|
const response = await fetch(`/api/cmd/${command.name}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
result.value = await response.json();
|
||||||
|
} else {
|
||||||
|
result.value = await response.text();
|
||||||
|
}
|
||||||
|
busy.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row" v-if="!command">
|
||||||
|
<span> No Command Selected </span>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="row">
|
||||||
|
<span>
|
||||||
|
{{ command.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<FlexDivider></FlexDivider>
|
||||||
|
<CommandParameter
|
||||||
|
v-for="param in command.parameters"
|
||||||
|
:key="param.name"
|
||||||
|
:parameter="param"
|
||||||
|
v-model="parameters[param.name]"
|
||||||
|
></CommandParameter>
|
||||||
|
<div class="row">
|
||||||
|
<button :disabled="busy" @click.stop.prevent="sendCommand">
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="row shrink grow"></div>
|
||||||
|
<div class="row">{{ result }}</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
52
frontend/src/composables/command.ts
Normal file
52
frontend/src/composables/command.ts
Normal file
@@ -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<CommandDefinition[] | null>(null);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const error = ref<any | null>(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<string>) {
|
||||||
|
const data = ref<CommandDefinition | null>(null);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const error = ref<any | null>(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 };
|
||||||
|
}
|
||||||
@@ -34,7 +34,12 @@ export function usePanelHeirarchy(): Ref<PanelHeirarchyChildren> {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Telemetry Elements',
|
name: 'Telemetry Elements',
|
||||||
to: { name: 'list' },
|
to: { name: 'tlm' },
|
||||||
|
type: PanelHeirarchyType.LEAF,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Commands',
|
||||||
|
to: { name: 'cmd' },
|
||||||
type: PanelHeirarchyType.LEAF,
|
type: PanelHeirarchyType.LEAF,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,10 +14,15 @@ const router = createRouter({
|
|||||||
component: () => import('../views/GraphView.vue'),
|
component: () => import('../views/GraphView.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/list',
|
path: '/tlm',
|
||||||
name: 'list',
|
name: 'tlm',
|
||||||
component: () => import('../views/TelemetryListView.vue'),
|
component: () => import('../views/TelemetryListView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/cmd',
|
||||||
|
name: 'cmd',
|
||||||
|
component: () => import('../views/CommandListView.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/chart',
|
path: '/chart',
|
||||||
name: 'chart',
|
name: 'chart',
|
||||||
|
|||||||
61
frontend/src/views/CommandListView.vue
Normal file
61
frontend/src/views/CommandListView.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import TextInput from '@/components/TextInput.vue';
|
||||||
|
import { ref } from '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';
|
||||||
|
import LinearLayout from '@/components/layout/LinearLayout.vue';
|
||||||
|
import CommandList from '@/components/CommandList.vue';
|
||||||
|
import type { CommandDefinition } from '@/composables/command.ts';
|
||||||
|
import CommandSender from '@/components/CommandSender.vue';
|
||||||
|
|
||||||
|
const searchValue = ref('');
|
||||||
|
|
||||||
|
const selected = ref<CommandDefinition | null>(null);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScreenLayout :type="ScreenType.Standard" limit>
|
||||||
|
<LinearLayout
|
||||||
|
:direction="Direction.Row"
|
||||||
|
stretch
|
||||||
|
class="grow no-min-height no-basis"
|
||||||
|
>
|
||||||
|
<div class="column grow2 stretch no-min-height no-basis">
|
||||||
|
<div class="row">
|
||||||
|
<TextInput
|
||||||
|
autofocus
|
||||||
|
class="grow"
|
||||||
|
v-model="searchValue"
|
||||||
|
placeholder="Search"
|
||||||
|
></TextInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row scroll no-min-height">
|
||||||
|
<div class="column grow stretch">
|
||||||
|
<CommandList
|
||||||
|
:search="searchValue"
|
||||||
|
v-model="selected"
|
||||||
|
></CommandList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FlexDivider></FlexDivider>
|
||||||
|
<div class="column grow stretch no-basis command-sender">
|
||||||
|
<CommandSender
|
||||||
|
:command="selected"
|
||||||
|
:key="selected?.name || ''"
|
||||||
|
></CommandSender>
|
||||||
|
</div>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScreenLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '@/assets/variables';
|
||||||
|
|
||||||
|
.command-sender {
|
||||||
|
row-gap: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,7 +6,6 @@ import type { TelemetryDefinition } from '@/composables/telemetry';
|
|||||||
import TelemetryInfo from '@/components/TelemetryInfo.vue';
|
import TelemetryInfo from '@/components/TelemetryInfo.vue';
|
||||||
import FlexDivider from '@/components/FlexDivider.vue';
|
import FlexDivider from '@/components/FlexDivider.vue';
|
||||||
import ScreenLayout from '@/components/layout/ScreenLayout.vue';
|
import ScreenLayout from '@/components/layout/ScreenLayout.vue';
|
||||||
import { Direction } from '@/composables/Direction.ts';
|
|
||||||
import { ScreenType } from '@/composables/ScreenType.ts';
|
import { ScreenType } from '@/composables/ScreenType.ts';
|
||||||
|
|
||||||
const searchValue = ref('');
|
const searchValue = ref('');
|
||||||
@@ -16,7 +15,8 @@ const mousedover = ref<TelemetryDefinition | null>(null);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ScreenLayout :direction="Direction.Row" :type="ScreenType.Standard" limit>
|
<ScreenLayout :type="ScreenType.Standard" limit>
|
||||||
|
<div class="row grow stretch no-min-height no-basis">
|
||||||
<div class="column grow2 stretch no-min-height no-basis">
|
<div class="column grow2 stretch no-min-height no-basis">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -47,6 +47,7 @@ const mousedover = ref<TelemetryDefinition | null>(null);
|
|||||||
:selection="selected"
|
:selection="selected"
|
||||||
></TelemetryInfo>
|
></TelemetryInfo>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ScreenLayout>
|
</ScreenLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
36
server/src/command/definition.rs
Normal file
36
server/src/command/definition.rs
Normal file
@@ -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<CommandParameterDefinition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RegisteredCommand> 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
server/src/command/error.rs
Normal file
36
server/src/command/error.rs
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
|
mod definition;
|
||||||
|
pub mod error;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
|||||||
@@ -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::telemetry_value::Value;
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
Command, CommandDefinitionRequest, TelemetryDataType, TelemetryValue, Timestamp,
|
Command, CommandDefinitionRequest, TelemetryDataType, TelemetryValue, Timestamp,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, ensure};
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use log::error;
|
||||||
use papaya::HashMap;
|
use papaya::HashMap;
|
||||||
use std::collections::HashMap as StdHashMap;
|
use std::collections::HashMap as StdHashMap;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
|
||||||
struct RegisteredCommand {
|
#[derive(Clone)]
|
||||||
definition: CommandDefinitionRequest,
|
pub(super) struct RegisteredCommand {
|
||||||
|
pub(super) name: String,
|
||||||
|
pub(super) definition: CommandDefinitionRequest,
|
||||||
tx: Sender<Option<Command>>,
|
tx: Sender<Option<Command>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +33,24 @@ impl CommandManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_commands(&self) -> anyhow::Result<Vec<CommandDefinition>> {
|
||||||
|
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<CommandDefinition> {
|
||||||
|
self.registered_commands
|
||||||
|
.pin()
|
||||||
|
.get(name)
|
||||||
|
.map(|registration| registration.clone().into())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn register_command(
|
pub async fn register_command(
|
||||||
&self,
|
&self,
|
||||||
command: CommandDefinitionRequest,
|
command: CommandDefinitionRequest,
|
||||||
@@ -35,6 +61,7 @@ impl CommandManagementService {
|
|||||||
if let Some(previous) = registered_commands.insert(
|
if let Some(previous) = registered_commands.insert(
|
||||||
command.name.clone(),
|
command.name.clone(),
|
||||||
RegisteredCommand {
|
RegisteredCommand {
|
||||||
|
name: command.name.clone(),
|
||||||
definition: command,
|
definition: command,
|
||||||
tx,
|
tx,
|
||||||
},
|
},
|
||||||
@@ -50,7 +77,7 @@ impl CommandManagementService {
|
|||||||
&self,
|
&self,
|
||||||
name: impl Into<String>,
|
name: impl Into<String>,
|
||||||
parameters: serde_json::Map<String, serde_json::Value>,
|
parameters: serde_json::Map<String, serde_json::Value>,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), CmdError> {
|
||||||
let timestamp = Utc::now();
|
let timestamp = Utc::now();
|
||||||
let offset_from_unix_epoch =
|
let offset_from_unix_epoch =
|
||||||
timestamp - DateTime::from_timestamp(0, 0).expect("Could not get 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 name = name.into();
|
||||||
let registered_commands = self.registered_commands.pin();
|
let registered_commands = self.registered_commands.pin();
|
||||||
let Some(registration) = registered_commands.get(&name) else {
|
let Some(registration) = registered_commands.get(&name) else {
|
||||||
bail!("Command Not Found {name}");
|
return Err(CommandNotFound(name));
|
||||||
};
|
};
|
||||||
|
|
||||||
ensure!(
|
if parameters.len() != registration.definition.parameters.len() {
|
||||||
parameters.len() == registration.definition.parameters.len(),
|
return Err(IncorrectParameterCount {
|
||||||
"Command has {} parameters. {} expected",
|
expected: registration.definition.parameters.len(),
|
||||||
parameters.len(),
|
actual: parameters.len(),
|
||||||
registration.definition.parameters.len()
|
});
|
||||||
);
|
}
|
||||||
let mut result_parameters = StdHashMap::new();
|
let mut result_parameters = StdHashMap::new();
|
||||||
for parameter in ®istration.definition.parameters {
|
for parameter in ®istration.definition.parameters {
|
||||||
let Some(param_value) = parameters.get(¶meter.name) else {
|
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() {
|
let Some(param_value) = (match parameter.data_type() {
|
||||||
TelemetryDataType::Float32 => {
|
TelemetryDataType::Float32 => {
|
||||||
@@ -79,11 +106,10 @@ impl CommandManagementService {
|
|||||||
TelemetryDataType::Float64 => param_value.as_f64().map(Value::Float64),
|
TelemetryDataType::Float64 => param_value.as_f64().map(Value::Float64),
|
||||||
TelemetryDataType::Boolean => param_value.as_bool().map(Value::Boolean),
|
TelemetryDataType::Boolean => param_value.as_bool().map(Value::Boolean),
|
||||||
}) else {
|
}) else {
|
||||||
bail!(
|
return Err(WrongParameterType {
|
||||||
"Parameter {} has the wrong type. {:?} expected",
|
name: parameter.name.clone(),
|
||||||
parameter.name,
|
expected_type: parameter.data_type(),
|
||||||
parameter.data_type()
|
});
|
||||||
);
|
|
||||||
};
|
};
|
||||||
result_parameters.insert(
|
result_parameters.insert(
|
||||||
parameter.name.clone(),
|
parameter.name.clone(),
|
||||||
@@ -97,6 +123,10 @@ impl CommandManagementService {
|
|||||||
let tx = registration.tx.clone();
|
let tx = registration.tx.clone();
|
||||||
drop(registered_commands);
|
drop(registered_commands);
|
||||||
|
|
||||||
|
if tx.is_closed() {
|
||||||
|
return Err(NoCommandReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(e) = tx
|
if let Err(e) = tx
|
||||||
.send(Some(Command {
|
.send(Some(Command {
|
||||||
timestamp: Some(Timestamp {
|
timestamp: Some(Timestamp {
|
||||||
@@ -107,7 +137,8 @@ impl CommandManagementService {
|
|||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
bail!("Failed to send command {e}");
|
error!("Failed to Send Command: {e}");
|
||||||
|
return Err(FailedToSend);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::command::service::CommandManagementService;
|
use crate::command::service::CommandManagementService;
|
||||||
use crate::http::error::HttpServerResultError;
|
use crate::http::error::HttpServerResultError;
|
||||||
use actix_web::{post, web, Responder};
|
use actix_web::{get, post, web, Responder};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[post("/cmd/{name:[\\w\\d/_-]+}")]
|
#[post("/cmd/{name:[\\w\\d/_-]+}")]
|
||||||
@@ -13,5 +13,22 @@ pub(super) async fn send_command(
|
|||||||
.send_command(name.to_string(), parameters.into_inner())
|
.send_command(name.to_string(), parameters.into_inner())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(web::Json(()))
|
Ok(web::Json("Command Sent Successfully."))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/cmd")]
|
||||||
|
pub(super) async fn get_all(
|
||||||
|
command_service: web::Data<Arc<CommandManagementService>>,
|
||||||
|
) -> Result<impl Responder, HttpServerResultError> {
|
||||||
|
Ok(web::Json(command_service.get_commands()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/cmd/{name:[\\w\\d/_-]+}")]
|
||||||
|
pub(super) async fn get_one(
|
||||||
|
command_service: web::Data<Arc<CommandManagementService>>,
|
||||||
|
name: web::Path<String>,
|
||||||
|
) -> Result<impl Responder, HttpServerResultError> {
|
||||||
|
Ok(web::Json(
|
||||||
|
command_service.get_command_definition(&name.to_string()),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,7 @@ pub fn setup_api(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(panels::get_one)
|
.service(panels::get_one)
|
||||||
.service(panels::set)
|
.service(panels::set)
|
||||||
.service(panels::delete)
|
.service(panels::delete)
|
||||||
.service(cmd::send_command);
|
.service(cmd::send_command)
|
||||||
|
.service(cmd::get_all)
|
||||||
|
.service(cmd::get_one);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use actix_web::error::ResponseError;
|
|||||||
use actix_web::http::header::ContentType;
|
use actix_web::http::header::ContentType;
|
||||||
use actix_web::http::StatusCode;
|
use actix_web::http::StatusCode;
|
||||||
use actix_web::HttpResponse;
|
use actix_web::HttpResponse;
|
||||||
use anyhow::Error;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
@@ -16,31 +15,28 @@ pub enum HttpServerResultError {
|
|||||||
#[error("Timed out")]
|
#[error("Timed out")]
|
||||||
Timeout,
|
Timeout,
|
||||||
#[error("Internal Error")]
|
#[error("Internal Error")]
|
||||||
InternalError(anyhow::Error),
|
InternalError(#[from] anyhow::Error),
|
||||||
#[error("Panel Uuid Not Found: {uuid}")]
|
#[error("Panel Uuid Not Found: {uuid}")]
|
||||||
PanelUuidNotFound { uuid: String },
|
PanelUuidNotFound { uuid: String },
|
||||||
|
#[error(transparent)]
|
||||||
|
Command(#[from] crate::command::error::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for HttpServerResultError {
|
impl ResponseError for HttpServerResultError {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
match *self {
|
match self {
|
||||||
HttpServerResultError::TlmNameNotFound { .. } => StatusCode::NOT_FOUND,
|
HttpServerResultError::TlmNameNotFound { .. } => StatusCode::NOT_FOUND,
|
||||||
HttpServerResultError::TlmUuidNotFound { .. } => StatusCode::NOT_FOUND,
|
HttpServerResultError::TlmUuidNotFound { .. } => StatusCode::NOT_FOUND,
|
||||||
HttpServerResultError::InvalidDateTime { .. } => StatusCode::BAD_REQUEST,
|
HttpServerResultError::InvalidDateTime { .. } => StatusCode::BAD_REQUEST,
|
||||||
HttpServerResultError::Timeout => StatusCode::GATEWAY_TIMEOUT,
|
HttpServerResultError::Timeout => StatusCode::GATEWAY_TIMEOUT,
|
||||||
HttpServerResultError::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
HttpServerResultError::InternalError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
HttpServerResultError::PanelUuidNotFound { .. } => StatusCode::NOT_FOUND,
|
HttpServerResultError::PanelUuidNotFound { .. } => StatusCode::NOT_FOUND,
|
||||||
|
HttpServerResultError::Command(inner) => inner.status_code(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn error_response(&self) -> HttpResponse {
|
fn error_response(&self) -> HttpResponse {
|
||||||
HttpResponse::build(self.status_code())
|
HttpResponse::build(self.status_code())
|
||||||
.insert_header(ContentType::html())
|
.insert_header(ContentType::plaintext())
|
||||||
.body(self.to_string())
|
.body(self.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<anyhow::Error> for HttpServerResultError {
|
|
||||||
fn from(value: Error) -> Self {
|
|
||||||
Self::InternalError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user