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) {
const data = ref<any | null>(null);
export interface TelemetryDefinition {
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);
fetch(`/api/tlm/${name}`)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err));
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 };
}

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'
class WebsocketHandle {
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<boolean>;
on_telem_value: Map<string, Array<(value: any)=>void>>;
on_telem_value: Map<string, Array<(value: TelemetryDataItem)=>void>>;
constructor() {
this.websocket = null;
@@ -42,16 +52,14 @@ class WebsocketHandle {
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);
const tlm_value = message["TlmValue"] as TlmValue;
const listeners = this.on_telem_value.get(tlm_value.uuid);
if (listeners) {
listeners.forEach((listener) => {
listener(message["TlmValue"]["value"]);
listener(tlm_value.value);
});
}
}
}
});
}
@@ -61,14 +69,14 @@ class WebsocketHandle {
return;
}
this.connected.value = false;
this.websocket.close();
this.websocket = null;
this.connected.value = false;
this.on_telem_value.clear();
}
listen_to_telemetry(telemetry: Ref<any>) {
const value_result = ref(null);
const value_result = ref<TelemetryDataItem | null>(null);
const uuid = computed(() => {
if (telemetry.value) {
@@ -77,9 +85,8 @@ class WebsocketHandle {
return null;
});
watch([uuid, this.connected], () => {
if (this.connected) {
let uuid_value = uuid.value
watch([uuid, this.connected], ([uuid_value, connected]) => {
if (connected && uuid_value) {
this.websocket?.send(JSON.stringify({
"RegisterTlmListener": {
uuid: uuid_value
@@ -98,6 +105,8 @@ class WebsocketHandle {
}
}
export const WEBSOCKET_SYMBOL = Symbol()
export function useWebsocket() {
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">
import { useTelemetry } from '@/composables/telemetry'
import { useWebsocket } from '@/composables/websocket'
const { data: sin_data, error: sin_error } = useTelemetry("simple_producer/sin");
const { data: cos_data, error: cos_error } = useTelemetry("simple_producer/cos");
import { useWebsocket, WEBSOCKET_SYMBOL } from '@/composables/websocket'
import { provide } from 'vue'
import Graph from '@/components/Graph.vue'
import Axis from '@/components/Axis.vue'
import Line from '@/components/Line.vue'
const websocket = useWebsocket();
let sin_value = websocket.value.listen_to_telemetry(sin_data);
let cos_value = websocket.value.listen_to_telemetry(cos_data);
provide(WEBSOCKET_SYMBOL, websocket);
</script>
<template>
<main>
<div v-if="sin_value">
{{ sin_data.name }} ({{ sin_data.data_type }}) = {{ sin_value[sin_data.data_type] }}
</div>
<div v-if="sin_error">
{{ sin_error }}
</div>
<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>
<Graph :width=1500 :height=800>
<Axis>
<Line data="simple_producer/sin" color="#FF0000"></Line>
<Line data="simple_producer/cos" color="#00FF00"></Line>
</Axis>
</Graph>
</main>
</template>
<style>
</style>

View File

@@ -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<TelemetryManagementService>,
@@ -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 {
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 {})

View File

@@ -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<TelemetryDataValue>
value: Option<TelemetryDataItem>
}
}

View File

@@ -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<S>(tlm_data_type: &TelemetryDataType, serializer: S) -> Result<S::Ok, S::Error> 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<Option<TelemetryDataValue>>,
pub data: tokio::sync::watch::Sender<Option<TelemetryDataItem>>,
}
pub struct TelemetryManagementService {