initial graph
This commit is contained in:
24
frontend/src/components/Axis.vue
Normal file
24
frontend/src/components/Axis.vue
Normal 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>
|
||||||
40
frontend/src/components/Graph.vue
Normal file
40
frontend/src/components/Graph.vue
Normal 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>
|
||||||
84
frontend/src/components/Line.vue
Normal file
84
frontend/src/components/Line.vue
Normal 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>
|
||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
19
frontend/src/composables/ticker.ts
Normal file
19
frontend/src/composables/ticker.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
|
||||||
|
|||||||
7
frontend/src/graph/axis.ts
Normal file
7
frontend/src/graph/axis.ts
Normal 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>;
|
||||||
|
}
|
||||||
10
frontend/src/graph/graph.ts
Normal file
10
frontend/src/graph/graph.ts
Normal 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>;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {})
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user