websocket to web

This commit is contained in:
2024-10-20 19:14:33 -07:00
parent 51af825b27
commit 8e4a94f8c5
8 changed files with 274 additions and 12 deletions

14
Cargo.lock generated
View File

@@ -685,6 +685,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.31" version = "0.3.31"
@@ -704,9 +715,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-macro",
"futures-task", "futures-task",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab",
] ]
[[package]] [[package]]
@@ -1491,6 +1504,7 @@ dependencies = [
"chrono", "chrono",
"derive_more 1.0.0", "derive_more 1.0.0",
"fern", "fern",
"futures-util",
"hex", "hex",
"log", "log",
"prost", "prost",

View File

@@ -0,0 +1,112 @@
import { computed, onMounted, onUnmounted, ref, type Ref, shallowRef, watch } from 'vue'
class WebsocketHandle {
websocket: WebSocket | null;
should_be_connected: boolean;
connected: Ref<boolean>;
on_telem_value: Map<string, Array<(value: any)=>void>>;
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<any>) {
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 function useWebsocket() {
const handle = shallowRef<WebsocketHandle>(new WebsocketHandle());
onMounted(() => {
handle.value.connect();
});
onUnmounted(() => {
handle.value.disconnect();
})
return handle;
}

View File

@@ -1,20 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTelemetry } from '@/composables/telemetry' import { useTelemetry } from '@/composables/telemetry'
import { useWebsocket } from '@/composables/websocket'
const { data: sin_data, error: sin_error } = useTelemetry("simple_producer/sin"); const { data: sin_data, error: sin_error } = useTelemetry("simple_producer/sin");
const { data: cos_data, error: cos_error } = useTelemetry("simple_producer/cos"); const { data: cos_data, error: cos_error } = useTelemetry("simple_producer/cos");
const websocket = useWebsocket();
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_data"> <div v-if="sin_value">
{{ sin_data.name }} ({{ sin_data.uuid }}): {{ sin_data.data_type }} {{ sin_data.name }} ({{ sin_data.data_type }}) = {{ sin_value[sin_data.data_type] }}
</div> </div>
<div v-if="sin_error"> <div v-if="sin_error">
{{ sin_error }} {{ sin_error }}
</div> </div>
<div v-if="cos_data"> <div v-if="cos_value">
{{ cos_data.name }} ({{ cos_data.uuid }}): {{ cos_data.data_type }} {{ cos_data.name }} ({{ cos_data.data_type }}) = {{ cos_value[cos_data.data_type] }}
</div> </div>
<div v-if="cos_error"> <div v-if="cos_error">
{{ cos_error }} {{ cos_error }}

View File

@@ -18,7 +18,12 @@ export default defineConfig({
server: { server: {
port: 9000, port: 9000,
proxy: { proxy: {
'/api': 'http://localhost:8080' '/api': 'http://localhost:8080',
'/ws': {
target: 'ws://localhost:8080',
ws: true,
rewriteWsOrigin: true,
}
} }
} }
}) })

View File

@@ -20,6 +20,7 @@ serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.132" serde_json = "1.0.132"
derive_more = { version = "1.0.0", features = ["full"] } derive_more = { version = "1.0.0", features = ["full"] }
hex = "0.4.3" hex = "0.4.3"
futures-util = "0.3.31"
[build-dependencies] [build-dependencies]
tonic-build = "0.12.3" tonic-build = "0.12.3"

View File

@@ -3,8 +3,8 @@ syntax = "proto3";
package core; package core;
enum TelemetryDataType { enum TelemetryDataType {
FLOAT_32 = 0; Float32 = 0;
FLOAT_64 = 1; Float64 = 1;
} }
message TelemetryValue { message TelemetryValue {

View File

@@ -1,10 +1,15 @@
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::{error, get, web, App, HttpResponse, HttpServer, Responder}; use actix_web::{rt, error, get, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use std::sync::Arc; use std::sync::Arc;
use log::trace; use actix_ws::AggregatedMessage;
use crate::telemetry::TelemetryManagementService; use log::{error, trace};
use serde::{Deserialize, Serialize};
use tokio::select;
use tokio_util::sync::CancellationToken;
use tonic::codegen::tokio_stream::StreamExt;
use crate::telemetry::{TelemetryDataValue, TelemetryManagementService};
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
enum UserError { enum UserError {
@@ -25,6 +30,21 @@ impl error::ResponseError for UserError {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
enum WebsocketRequest {
RegisterTlmListener {
uuid: String
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
enum WebsocketResponse {
TlmValue {
uuid: String,
value: Option<TelemetryDataValue>
}
}
#[get("/tlm/{name:[\\w\\d/_-]+}")] #[get("/tlm/{name:[\\w\\d/_-]+}")]
async fn get_tlm_definition(data: web::Data<Arc<TelemetryManagementService>>, name: web::Path<String>) -> Result<impl Responder, UserError> { async fn get_tlm_definition(data: web::Data<Arc<TelemetryManagementService>>, name: web::Path<String>) -> Result<impl Responder, UserError> {
let string = name.to_string(); let string = name.to_string();
@@ -36,18 +56,121 @@ async fn get_tlm_definition(data: web::Data<Arc<TelemetryManagementService>>, na
Ok(web::Json(data.definition.clone())) Ok(web::Json(data.definition.clone()))
} }
async fn websocket_connect(req: HttpRequest, stream: web::Payload, data: web::Data<Arc<TelemetryManagementService>>, cancel_token: web::Data<CancellationToken>) -> Result<HttpResponse, actix_web::Error> {
trace!("websocket_connect");
let (res, mut session, stream) = actix_ws::handle(&req, stream)?;
let mut stream = stream
.aggregate_continuations()
// up to 1 MiB
.max_continuation_size(2_usize.pow(20));
let cancel_token = cancel_token.get_ref().clone();
rt::spawn(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel::<WebsocketResponse>(128);
loop {
select! {
_ = cancel_token.cancelled() => {
break;
},
Some(msg) = rx.recv() => {
let msg_json = match serde_json::to_string(&msg) {
Ok(msg_json) => msg_json,
Err(err) => {
error!("JSON Serialization Error Encountered {err}");
break;
},
};
if let Err(err) = session.text(msg_json).await {
error!("Tx Error Encountered {err}");
break;
}
},
Some(msg) = stream.next() => {
let result = match msg {
Ok(AggregatedMessage::Close(_)) => {
break;
},
Ok(AggregatedMessage::Text(msg)) => {
match serde_json::from_str::<WebsocketRequest>(&msg) {
Ok(request) => {
match request {
WebsocketRequest::RegisterTlmListener{ uuid } => {
if let Some(tlm_data) = data.get_by_uuid(&uuid).await {
let mut rx = tlm_data.data.subscribe();
let tx = tx.clone();
rt::spawn(async move {
loop {
select! {
_ = tx.closed() => {
break;
}
Ok(_) = rx.changed() => {
let value = rx.borrow_and_update().clone();
let _ = tx.send(WebsocketResponse::TlmValue {
uuid: uuid.clone(),
value,
}).await;
}
}
}
});
}
}
}
},
Err(err) => {
error!("JSON Deserialization Error Encountered {err}");
break;
}
}
Ok(())
},
Ok(AggregatedMessage::Ping(msg)) => {
session.pong(&msg).await
},
Err(err) => {
error!("Rx Error Encountered {err}");
break;
},
_ => {
error!("Unexpected Message");
break;
}
};
if let Err(err) = result {
error!("Tx Error Encountered {err}");
break;
}
},
else => {
break;
},
}
}
rx.close();
let _ = session.close(None).await;
});
Ok(res)
}
fn setup_api(cfg: &mut web::ServiceConfig) { fn setup_api(cfg: &mut web::ServiceConfig) {
cfg cfg
.service(get_tlm_definition); .service(get_tlm_definition);
} }
pub async fn setup(telemetry_definitions: Arc<TelemetryManagementService>) -> Result<(), Box<dyn Error>> { pub async fn setup(cancellation_token: CancellationToken, telemetry_definitions: Arc<TelemetryManagementService>) -> Result<(), Box<dyn Error>> {
let data = web::Data::new(telemetry_definitions); let data = web::Data::new(telemetry_definitions);
let cancel_token = web::Data::new(cancellation_token);
trace!("Starting HTTP Server"); trace!("Starting HTTP Server");
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.app_data(data.clone()) .app_data(data.clone())
.app_data(cancel_token.clone())
.route("/ws", web::get().to(websocket_connect))
.service(web::scope("/api").configure(setup_api)) .service(web::scope("/api").configure(setup_api))
}) })
.bind("localhost:8080")? .bind("localhost:8080")?

View File

@@ -26,7 +26,7 @@ pub async fn setup() -> Result<(), Box<dyn Error>> {
let grpc_server = grpc::setup(cancellation_token.clone(), tlm.clone())?; let grpc_server = grpc::setup(cancellation_token.clone(), tlm.clone())?;
let result = http::setup(tlm).await; let result = http::setup(cancellation_token.clone(), tlm).await;
cancellation_token.cancel(); cancellation_token.cancel();
result?; result?;
grpc_server.await?; grpc_server.await?;