diff --git a/frontend/src/components/Axis.vue b/frontend/src/components/Axis.vue new file mode 100644 index 0000000..d843ad2 --- /dev/null +++ b/frontend/src/components/Axis.vue @@ -0,0 +1,24 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/Graph.vue b/frontend/src/components/Graph.vue new file mode 100644 index 0000000..bfbdfca --- /dev/null +++ b/frontend/src/components/Graph.vue @@ -0,0 +1,40 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/Line.vue b/frontend/src/components/Line.vue new file mode 100644 index 0000000..bf0d63d --- /dev/null +++ b/frontend/src/components/Line.vue @@ -0,0 +1,84 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/src/composables/telemetry.ts b/frontend/src/composables/telemetry.ts index 227bf6d..c4eb6f8 100644 --- a/frontend/src/composables/telemetry.ts +++ b/frontend/src/composables/telemetry.ts @@ -1,13 +1,28 @@ -import { ref } from 'vue' +import { ref, toValue, watchEffect } from 'vue' +import { type MaybeRefOrGetter } from '@vue/reactivity' -export function useTelemetry(name: string) { - const data = ref(null); - const error = ref(null); - - fetch(`/api/tlm/${name}`) - .then((res) => res.json()) - .then((json) => (data.value = json)) - .catch((err) => (error.value = err)); - - return { data, error }; +export interface TelemetryDefinition { + uuid: string; + name: string; + data_type: string; +} + +export function useTelemetry(name: MaybeRefOrGetter) { + const data = ref(null); + const error = ref(null); + + watchEffect(async () => { + const name_value = toValue(name); + + try { + const res = await fetch(`/api/tlm/${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/composables/ticker.ts b/frontend/src/composables/ticker.ts new file mode 100644 index 0000000..baf832b --- /dev/null +++ b/frontend/src/composables/ticker.ts @@ -0,0 +1,19 @@ +import { onMounted, onUnmounted, ref, shallowRef } from 'vue' + +export function useNow(update_ms: number) { + const handle = shallowRef(undefined); + const now = ref(Date.now()); + + onMounted(() => { + handle.value = setInterval(() => { + now.value = Date.now(); + }, update_ms); + }); + onUnmounted(() => { + if (handle.value) { + clearInterval(handle.value); + } + }); + + return now; +} diff --git a/frontend/src/composables/websocket.ts b/frontend/src/composables/websocket.ts index 603bec9..6245d2e 100644 --- a/frontend/src/composables/websocket.ts +++ b/frontend/src/composables/websocket.ts @@ -1,112 +1,121 @@ import { computed, onMounted, onUnmounted, ref, type Ref, shallowRef, watch } from 'vue' -class WebsocketHandle { - websocket: WebSocket | null; - should_be_connected: boolean; - connected: Ref; - on_telem_value: Mapvoid>>; - - constructor() { - this.websocket = null; - this.should_be_connected = false; - this.connected = ref(false); - this.on_telem_value = new Map(); - } - - connect() { - this.should_be_connected = true; - if (this.websocket != null) { - return; - } - - this.websocket = new WebSocket(location.protocol.replace("http", "ws") + "//" + location.host + "/ws"); - this.websocket.addEventListener("open", (event) => { - this.connected.value = true; - }); - this.websocket.addEventListener("close", (event) => { - if (this.should_be_connected) { - this.disconnect(); - setTimeout(() => { - this.connect(); - }, 1000); - } - }); - this.websocket.addEventListener("error", (event) => { - if (this.should_be_connected) { - this.disconnect(); - setTimeout(() => { - this.connect(); - }, 1000); - } - }); - this.websocket.addEventListener("message", (event) => { - const message = JSON.parse(event.data); - if (message["TlmValue"]) { - const uuid = message["TlmValue"]["uuid"]; - if (uuid) { - const listeners = this.on_telem_value.get(uuid); - if (listeners) { - listeners.forEach((listener) => { - listener(message["TlmValue"]["value"]); - }); - } - } - } - }); - } - - disconnect() { - this.should_be_connected = false; - if (this.websocket == null) { - return; - } - - this.websocket.close(); - this.websocket = null; - this.connected.value = false; - this.on_telem_value.clear(); - } - - listen_to_telemetry(telemetry: Ref) { - const value_result = ref(null); - - const uuid = computed(() => { - if (telemetry.value) { - return telemetry.value.uuid; - } - return null; - }); - - watch([uuid, this.connected], () => { - if (this.connected) { - let uuid_value = uuid.value - this.websocket?.send(JSON.stringify({ - "RegisterTlmListener": { - uuid: uuid_value - } - })); - if (!this.on_telem_value.has(uuid_value)) { - this.on_telem_value.set(uuid_value, []); - } - this.on_telem_value.get(uuid_value)?.push((value) => { - value_result.value = value; - }); - } - }); - - return value_result; - } +export interface TelemetryDataItem { + value: any; + timestamp: string; } +interface TlmValue { + uuid: string; + value: TelemetryDataItem; +} + +export class WebsocketHandle { + websocket: WebSocket | null; + should_be_connected: boolean; + connected: Ref; + on_telem_value: Mapvoid>>; + + constructor() { + this.websocket = null; + this.should_be_connected = false; + this.connected = ref(false); + this.on_telem_value = new Map(); + } + + connect() { + this.should_be_connected = true; + if (this.websocket != null) { + return; + } + + this.websocket = new WebSocket(location.protocol.replace("http", "ws") + "//" + location.host + "/ws"); + this.websocket.addEventListener("open", (event) => { + this.connected.value = true; + }); + this.websocket.addEventListener("close", (event) => { + if (this.should_be_connected) { + this.disconnect(); + setTimeout(() => { + this.connect(); + }, 1000); + } + }); + this.websocket.addEventListener("error", (event) => { + if (this.should_be_connected) { + this.disconnect(); + setTimeout(() => { + this.connect(); + }, 1000); + } + }); + this.websocket.addEventListener("message", (event) => { + const message = JSON.parse(event.data); + if (message["TlmValue"]) { + const tlm_value = message["TlmValue"] as TlmValue; + const listeners = this.on_telem_value.get(tlm_value.uuid); + if (listeners) { + listeners.forEach((listener) => { + listener(tlm_value.value); + }); + } + } + }); + } + + disconnect() { + this.should_be_connected = false; + if (this.websocket == null) { + return; + } + + this.connected.value = false; + this.websocket.close(); + this.websocket = null; + this.on_telem_value.clear(); + } + + listen_to_telemetry(telemetry: Ref) { + const value_result = ref(null); + + const uuid = computed(() => { + if (telemetry.value) { + return telemetry.value.uuid; + } + return null; + }); + + watch([uuid, this.connected], ([uuid_value, connected]) => { + if (connected && uuid_value) { + this.websocket?.send(JSON.stringify({ + "RegisterTlmListener": { + uuid: uuid_value + } + })); + if (!this.on_telem_value.has(uuid_value)) { + this.on_telem_value.set(uuid_value, []); + } + this.on_telem_value.get(uuid_value)?.push((value) => { + value_result.value = value; + }); + } + }); + + return value_result; + } +} + +export const WEBSOCKET_SYMBOL = Symbol() + export function useWebsocket() { - const handle = shallowRef(new WebsocketHandle()); + const handle = shallowRef(new WebsocketHandle()); - onMounted(() => { - handle.value.connect(); - }); - onUnmounted(() => { - handle.value.disconnect(); - }) + onMounted(() => { + handle.value.connect(); + }); + onUnmounted(() => { + handle.value.disconnect(); + }) - return handle; + return handle; } diff --git a/frontend/src/graph/axis.ts b/frontend/src/graph/axis.ts new file mode 100644 index 0000000..783d439 --- /dev/null +++ b/frontend/src/graph/axis.ts @@ -0,0 +1,7 @@ +import type { MaybeRefOrGetter } from '@vue/reactivity' + +export const AXIS_DATA = Symbol() +export interface AxisData { + min_y: MaybeRefOrGetter; + max_y: MaybeRefOrGetter; +} diff --git a/frontend/src/graph/graph.ts b/frontend/src/graph/graph.ts new file mode 100644 index 0000000..b422ec9 --- /dev/null +++ b/frontend/src/graph/graph.ts @@ -0,0 +1,10 @@ +import type { MaybeRefOrGetter } from '@vue/reactivity' + +export const GRAPH_DATA = Symbol() + +export interface GraphData { + min_x: MaybeRefOrGetter; + max_x: MaybeRefOrGetter; + width: MaybeRefOrGetter; + height: MaybeRefOrGetter; +} diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 355d647..02a8272 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -1,30 +1,25 @@ - - - {{ sin_data.name }} ({{ sin_data.data_type }}) = {{ sin_value[sin_data.data_type] }} - - - {{ sin_error }} - - - {{ cos_data.name }} ({{ cos_data.data_type }}) = {{ cos_value[cos_data.data_type] }} - - - {{ cos_error }} - - + + + + + + + + + + diff --git a/server/src/grpc.rs b/server/src/grpc.rs index 9a15217..44625de 100644 --- a/server/src/grpc.rs +++ b/server/src/grpc.rs @@ -1,6 +1,7 @@ use std::error::Error; use std::pin::Pin; use std::sync::Arc; +use chrono::{DateTime, SecondsFormat}; use log::{error, trace}; use tokio::select; use tokio::sync::mpsc; @@ -13,7 +14,7 @@ use tonic::transport::Server; use crate::core::telemetry_service_server::{TelemetryService, TelemetryServiceServer}; use crate::core::{TelemetryDataType, TelemetryDefinitionRequest, TelemetryDefinitionResponse, TelemetryInsertResponse, TelemetryItem, TelemetryValue, Uuid}; use crate::core::telemetry_value::Value; -use crate::telemetry::{TelemetryDataValue, TelemetryManagementService}; +use crate::telemetry::{TelemetryDataItem, TelemetryDataValue, TelemetryManagementService}; pub struct CoreTelemetryService { pub tlm_management: Arc, @@ -89,6 +90,10 @@ impl CoreTelemetryService { return Err(Status::failed_precondition("Value Missing")); }; + let Some(timestamp) = tlm_item.timestamp else { + return Err(Status::failed_precondition("Timestamp Missing")); + }; + let expected_type = match value { Value::Float32(_) => TelemetryDataType::Float32, Value::Float64(_) => TelemetryDataType::Float64, @@ -97,9 +102,16 @@ impl CoreTelemetryService { return Err(Status::failed_precondition("Data Type Mismatch")); }; - let _ = tlm_data.data.send_replace(Some(match value { - Value::Float32(x) => TelemetryDataValue::Float32(x), - Value::Float64(x) => TelemetryDataValue::Float64(x), + let Some(timestamp) = DateTime::from_timestamp(timestamp.secs, timestamp.nanos as u32) else { + return Err(Status::invalid_argument("Failed to construct UTC DateTime")); + }; + + let _ = tlm_data.data.send_replace(Some(TelemetryDataItem { + value: match value { + Value::Float32(x) => TelemetryDataValue::Float32(x), + Value::Float64(x) => TelemetryDataValue::Float64(x), + }, + timestamp: timestamp.to_rfc3339_opts(SecondsFormat::Millis, true) })); Ok(TelemetryInsertResponse {}) diff --git a/server/src/http.rs b/server/src/http.rs index 400e75d..2a54095 100644 --- a/server/src/http.rs +++ b/server/src/http.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use tokio::select; use tokio_util::sync::CancellationToken; use tonic::codegen::tokio_stream::StreamExt; -use crate::telemetry::{TelemetryDataValue, TelemetryManagementService}; +use crate::telemetry::{TelemetryDataItem, TelemetryDataValue, TelemetryManagementService}; #[derive(Debug, Display, Error)] enum UserError { @@ -41,7 +41,7 @@ enum WebsocketRequest { enum WebsocketResponse { TlmValue { uuid: String, - value: Option + value: Option } } diff --git a/server/src/telemetry.rs b/server/src/telemetry.rs index 0411f4a..4056473 100644 --- a/server/src/telemetry.rs +++ b/server/src/telemetry.rs @@ -2,11 +2,12 @@ use std::collections::HashMap; use std::error::Error; use std::fmt::Formatter; use std::sync::Arc; +use chrono::{DateTime, Utc}; use log::trace; use serde::de::Visitor; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tokio::sync::Mutex; -use crate::core::{TelemetryDataType, TelemetryDefinitionRequest, Uuid}; +use crate::core::{TelemetryDataType, TelemetryDefinitionRequest, TelemetryItem, Timestamp, Uuid}; fn tlm_data_type_serialzier(tlm_data_type: &TelemetryDataType, serializer: S) -> Result where S: Serializer { serializer.serialize_str(tlm_data_type.as_str_name()) @@ -48,10 +49,16 @@ pub enum TelemetryDataValue { 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>, + pub data: tokio::sync::watch::Sender>, } pub struct TelemetryManagementService {