initial graph

This commit is contained in:
2024-12-02 23:30:05 -08:00
parent 8e4a94f8c5
commit 3eafc20e9d
12 changed files with 367 additions and 145 deletions

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import { provide } from 'vue'
import { AXIS_DATA, type AxisData } from '@/graph/axis'
const props = defineProps<{
y_limits?: [number, number]
}>();
provide<AxisData>(AXIS_DATA, {
min_y: -1.05,
max_y: 1.05,
});
</script>
<template>
<g>
<slot></slot>
</g>
</template>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
import { computed, provide } from 'vue'
import { useNow } from '@/composables/ticker'
import { GRAPH_DATA, type GraphData } from '@/graph/graph'
const props = defineProps<{
width: number
height: number
// min_t?: number
// max_t?: number
}>();
const svg_viewbox = computed(() => {
return `0 0 ${props.width} ${props.height}`;
})
const now = useNow(33);
const window_duration = 30 * 1000; // 30 seconds
const min_x = computed(() => now.value - window_duration);
provide<GraphData>(GRAPH_DATA, {
min_x: min_x,
max_x: now,
width: () => props.width,
height: () => props.height,
});
</script>
<template>
<svg :viewBox="svg_viewbox" :width="props.width" :height="props.height">
<slot></slot>
</svg>
</template>
<style scoped lang="scss">
svg {
border: white 1px solid;
}
</style>

View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
import { useTelemetry } from '@/composables/telemetry'
import { computed, inject, shallowRef, type ShallowRef, toValue, useId, watch } from 'vue'
import { type TelemetryDataItem, WEBSOCKET_SYMBOL, type WebsocketHandle } from '@/composables/websocket'
import { GRAPH_DATA, type GraphData } from '@/graph/graph'
import { AXIS_DATA, type AxisData } from '@/graph/axis'
const props = defineProps<{
data: string
color: string
}>();
const smoothing_distance = 0.15 * 1000;
const { data, error } = useTelemetry(() => props.data);
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
let value = websocket.value.listen_to_telemetry(data);
const graph_data = inject<GraphData>(GRAPH_DATA)!;
const axis_data = inject<AxisData>(AXIS_DATA)!;
let memo = shallowRef<TelemetryDataItem[]>([]);
watch([value], ([val]) => {
const min_x = toValue(graph_data.min_x);
if (val) {
const new_memo = [val].concat(memo.value);
while (new_memo.length > 2 && Date.parse(new_memo[new_memo.length - 2].timestamp) < min_x) {
new_memo.pop();
}
memo.value = new_memo;
}
});
let points = computed(() => {
let points = "";
if (memo.value.length == 0 || data.value == null) {
return "";
}
const max_x = toValue(graph_data.max_x);
const min_x = toValue(graph_data.min_x);
const diff_x = max_x - min_x;
const max_y = toValue(axis_data.max_y);
const min_y = toValue(axis_data.min_y);
const diff_y = max_y - min_y;
const width = toValue(graph_data.width);
const height = toValue(graph_data.height);
let last_x = width;
let last_t = Math.max(max_x, Date.now());
for (let data_item of memo.value) {
const t = Date.parse(data_item.timestamp);
const v = data_item.value[data.value.data_type];
const x = width * (t - min_x) / diff_x;
const y = height * (1 - (v - min_y) / diff_y);
if (last_t - t < smoothing_distance) {
points += ` ${x},${y}`;
} else {
points += ` ${last_x},${y} ${x},${y}`;
}
last_x = x;
last_t = t;
if (last_x <= 0.0) {
break;
}
}
return points;
});
console.log(useId());
</script>
<template>
<polyline fill="none" :stroke="color" stroke-width="1" :points="points"></polyline>
</template>
<style>
</style>

View File

@@ -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) { export interface TelemetryDefinition {
const data = ref<any | null>(null); uuid: string;
name: string;
data_type: string;
}
export function useTelemetry(name: MaybeRefOrGetter<string>) {
const data = ref<TelemetryDefinition | null>(null);
const error = ref<any | null>(null); const error = ref<any | null>(null);
fetch(`/api/tlm/${name}`) watchEffect(async () => {
.then((res) => res.json()) const name_value = toValue(name);
.then((json) => (data.value = json))
.catch((err) => (error.value = err)); 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 }; return { data, error };
} }

View File

@@ -0,0 +1,19 @@
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
export function useNow(update_ms: number) {
const handle = shallowRef<number | undefined>(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;
}

View File

@@ -1,10 +1,20 @@
import { computed, onMounted, onUnmounted, ref, type Ref, shallowRef, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, type Ref, shallowRef, watch } from 'vue'
class WebsocketHandle { export interface TelemetryDataItem {
value: any;
timestamp: string;
}
interface TlmValue {
uuid: string;
value: TelemetryDataItem;
}
export class WebsocketHandle {
websocket: WebSocket | null; websocket: WebSocket | null;
should_be_connected: boolean; should_be_connected: boolean;
connected: Ref<boolean>; connected: Ref<boolean>;
on_telem_value: Map<string, Array<(value: any)=>void>>; on_telem_value: Map<string, Array<(value: TelemetryDataItem)=>void>>;
constructor() { constructor() {
this.websocket = null; this.websocket = null;
@@ -42,16 +52,14 @@ class WebsocketHandle {
this.websocket.addEventListener("message", (event) => { this.websocket.addEventListener("message", (event) => {
const message = JSON.parse(event.data); const message = JSON.parse(event.data);
if (message["TlmValue"]) { if (message["TlmValue"]) {
const uuid = message["TlmValue"]["uuid"]; const tlm_value = message["TlmValue"] as TlmValue;
if (uuid) { const listeners = this.on_telem_value.get(tlm_value.uuid);
const listeners = this.on_telem_value.get(uuid);
if (listeners) { if (listeners) {
listeners.forEach((listener) => { listeners.forEach((listener) => {
listener(message["TlmValue"]["value"]); listener(tlm_value.value);
}); });
} }
} }
}
}); });
} }
@@ -61,14 +69,14 @@ class WebsocketHandle {
return; return;
} }
this.connected.value = false;
this.websocket.close(); this.websocket.close();
this.websocket = null; this.websocket = null;
this.connected.value = false;
this.on_telem_value.clear(); this.on_telem_value.clear();
} }
listen_to_telemetry(telemetry: Ref<any>) { listen_to_telemetry(telemetry: Ref<any>) {
const value_result = ref(null); const value_result = ref<TelemetryDataItem | null>(null);
const uuid = computed(() => { const uuid = computed(() => {
if (telemetry.value) { if (telemetry.value) {
@@ -77,9 +85,8 @@ class WebsocketHandle {
return null; return null;
}); });
watch([uuid, this.connected], () => { watch([uuid, this.connected], ([uuid_value, connected]) => {
if (this.connected) { if (connected && uuid_value) {
let uuid_value = uuid.value
this.websocket?.send(JSON.stringify({ this.websocket?.send(JSON.stringify({
"RegisterTlmListener": { "RegisterTlmListener": {
uuid: uuid_value uuid: uuid_value
@@ -98,6 +105,8 @@ class WebsocketHandle {
} }
} }
export const WEBSOCKET_SYMBOL = Symbol()
export function useWebsocket() { export function useWebsocket() {
const handle = shallowRef<WebsocketHandle>(new WebsocketHandle()); const handle = shallowRef<WebsocketHandle>(new WebsocketHandle());

View File

@@ -0,0 +1,7 @@
import type { MaybeRefOrGetter } from '@vue/reactivity'
export const AXIS_DATA = Symbol()
export interface AxisData {
min_y: MaybeRefOrGetter<number>;
max_y: MaybeRefOrGetter<number>;
}

View File

@@ -0,0 +1,10 @@
import type { MaybeRefOrGetter } from '@vue/reactivity'
export const GRAPH_DATA = Symbol()
export interface GraphData {
min_x: MaybeRefOrGetter<number>;
max_x: MaybeRefOrGetter<number>;
width: MaybeRefOrGetter<number>;
height: MaybeRefOrGetter<number>;
}

View File

@@ -1,30 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTelemetry } from '@/composables/telemetry' import { useWebsocket, WEBSOCKET_SYMBOL } from '@/composables/websocket'
import { useWebsocket } from '@/composables/websocket' import { provide } from 'vue'
import Graph from '@/components/Graph.vue'
const { data: sin_data, error: sin_error } = useTelemetry("simple_producer/sin"); import Axis from '@/components/Axis.vue'
const { data: cos_data, error: cos_error } = useTelemetry("simple_producer/cos"); import Line from '@/components/Line.vue'
const websocket = useWebsocket(); const websocket = useWebsocket();
provide(WEBSOCKET_SYMBOL, websocket);
let sin_value = websocket.value.listen_to_telemetry(sin_data);
let cos_value = websocket.value.listen_to_telemetry(cos_data);
</script> </script>
<template> <template>
<main> <main>
<div v-if="sin_value"> <Graph :width=1500 :height=800>
{{ sin_data.name }} ({{ sin_data.data_type }}) = {{ sin_value[sin_data.data_type] }} <Axis>
</div> <Line data="simple_producer/sin" color="#FF0000"></Line>
<div v-if="sin_error"> <Line data="simple_producer/cos" color="#00FF00"></Line>
{{ sin_error }} </Axis>
</div> </Graph>
<div v-if="cos_value">
{{ cos_data.name }} ({{ cos_data.data_type }}) = {{ cos_value[cos_data.data_type] }}
</div>
<div v-if="cos_error">
{{ cos_error }}
</div>
</main> </main>
</template> </template>
<style>
</style>

View File

@@ -1,6 +1,7 @@
use std::error::Error; use std::error::Error;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use chrono::{DateTime, SecondsFormat};
use log::{error, trace}; use log::{error, trace};
use tokio::select; use tokio::select;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -13,7 +14,7 @@ use tonic::transport::Server;
use crate::core::telemetry_service_server::{TelemetryService, TelemetryServiceServer}; use crate::core::telemetry_service_server::{TelemetryService, TelemetryServiceServer};
use crate::core::{TelemetryDataType, TelemetryDefinitionRequest, TelemetryDefinitionResponse, TelemetryInsertResponse, TelemetryItem, TelemetryValue, Uuid}; use crate::core::{TelemetryDataType, TelemetryDefinitionRequest, TelemetryDefinitionResponse, TelemetryInsertResponse, TelemetryItem, TelemetryValue, Uuid};
use crate::core::telemetry_value::Value; use crate::core::telemetry_value::Value;
use crate::telemetry::{TelemetryDataValue, TelemetryManagementService}; use crate::telemetry::{TelemetryDataItem, TelemetryDataValue, TelemetryManagementService};
pub struct CoreTelemetryService { pub struct CoreTelemetryService {
pub tlm_management: Arc<TelemetryManagementService>, pub tlm_management: Arc<TelemetryManagementService>,
@@ -89,6 +90,10 @@ impl CoreTelemetryService {
return Err(Status::failed_precondition("Value Missing")); 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 { let expected_type = match value {
Value::Float32(_) => TelemetryDataType::Float32, Value::Float32(_) => TelemetryDataType::Float32,
Value::Float64(_) => TelemetryDataType::Float64, Value::Float64(_) => TelemetryDataType::Float64,
@@ -97,9 +102,16 @@ impl CoreTelemetryService {
return Err(Status::failed_precondition("Data Type Mismatch")); return Err(Status::failed_precondition("Data Type Mismatch"));
}; };
let _ = tlm_data.data.send_replace(Some(match value { 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::Float32(x) => TelemetryDataValue::Float32(x),
Value::Float64(x) => TelemetryDataValue::Float64(x), Value::Float64(x) => TelemetryDataValue::Float64(x),
},
timestamp: timestamp.to_rfc3339_opts(SecondsFormat::Millis, true)
})); }));
Ok(TelemetryInsertResponse {}) Ok(TelemetryInsertResponse {})

View File

@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use tokio::select; use tokio::select;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tonic::codegen::tokio_stream::StreamExt; use tonic::codegen::tokio_stream::StreamExt;
use crate::telemetry::{TelemetryDataValue, TelemetryManagementService}; use crate::telemetry::{TelemetryDataItem, TelemetryDataValue, TelemetryManagementService};
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
enum UserError { enum UserError {
@@ -41,7 +41,7 @@ enum WebsocketRequest {
enum WebsocketResponse { enum WebsocketResponse {
TlmValue { TlmValue {
uuid: String, uuid: String,
value: Option<TelemetryDataValue> value: Option<TelemetryDataItem>
} }
} }

View File

@@ -2,11 +2,12 @@ use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::fmt::Formatter; use std::fmt::Formatter;
use std::sync::Arc; use std::sync::Arc;
use chrono::{DateTime, Utc};
use log::trace; use log::trace;
use serde::de::Visitor; use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::core::{TelemetryDataType, TelemetryDefinitionRequest, Uuid}; use crate::core::{TelemetryDataType, TelemetryDefinitionRequest, TelemetryItem, Timestamp, Uuid};
fn tlm_data_type_serialzier<S>(tlm_data_type: &TelemetryDataType, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer { 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()) serializer.serialize_str(tlm_data_type.as_str_name())
@@ -48,10 +49,16 @@ pub enum TelemetryDataValue {
Float64(f64), Float64(f64),
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryDataItem {
pub value: TelemetryDataValue,
pub timestamp: String
}
#[derive(Clone)] #[derive(Clone)]
pub struct TelemetryData { pub struct TelemetryData {
pub definition: TelemetryDefinition, pub definition: TelemetryDefinition,
pub data: tokio::sync::watch::Sender<Option<TelemetryDataValue>>, pub data: tokio::sync::watch::Sender<Option<TelemetryDataItem>>,
} }
pub struct TelemetryManagementService { pub struct TelemetryManagementService {