From 48267f9af90b8740efa4db38ef760d7ebd0f67ae Mon Sep 17 00:00:00 2001 From: Youwen Wu Date: Wed, 6 Mar 2024 23:01:36 -0800 Subject: [PATCH] refactor: make telemetry code more maintainable --- client/src-tauri/src/main.rs | 6 +- client/src-tauri/src/telemetry.rs | 75 ------------------- .../src-tauri/src/telemetry/create_client.rs | 52 +++++++++++++ .../src/telemetry/create_subscription.rs | 42 +++++++++++ client/src-tauri/src/telemetry/mod.rs | 61 +++++++++++++++ 5 files changed, 160 insertions(+), 76 deletions(-) delete mode 100644 client/src-tauri/src/telemetry.rs create mode 100644 client/src-tauri/src/telemetry/create_client.rs create mode 100644 client/src-tauri/src/telemetry/create_subscription.rs create mode 100644 client/src-tauri/src/telemetry/mod.rs diff --git a/client/src-tauri/src/main.rs b/client/src-tauri/src/main.rs index a9aca44..f328956 100644 --- a/client/src-tauri/src/main.rs +++ b/client/src-tauri/src/main.rs @@ -9,6 +9,9 @@ struct Payload { message: String, } +const NTABLE_IP: (u8, u8, u8, u8) = (10, 12, 80, 2); +const NTABLE_PORT: u16 = 5810; + fn main() { let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); @@ -19,7 +22,8 @@ fn main() { let app_handle = app.app_handle(); tokio::spawn(async move { - crate::telemetry::subscribe_topics(app_handle.clone()).await; + crate::telemetry::subscribe_topics(app_handle.clone(), NTABLE_IP, NTABLE_PORT) + .await; }); Ok(()) diff --git a/client/src-tauri/src/telemetry.rs b/client/src-tauri/src/telemetry.rs deleted file mode 100644 index 3be2175..0000000 --- a/client/src-tauri/src/telemetry.rs +++ /dev/null @@ -1,75 +0,0 @@ -use network_tables::v4::client_config::Config; -use network_tables::v4::{Client, SubscriptionOptions}; -use serde_json::to_string; -use std::net::{Ipv4Addr, SocketAddrV4}; -use tauri::{AppHandle, Manager}; -use tokio::time::{sleep, Duration}; - -const NTABLE_IP: (u8, u8, u8, u8) = (10, 12, 80, 2); -const NTABLE_PORT: u16 = 5810; - -pub async fn subscribe_topics(app_handle: AppHandle) { - loop { - // I hope this doesn't lead to a catastrophic infinite loop failure - let client = loop { - match Client::try_new_w_config( - SocketAddrV4::new( - Ipv4Addr::new(NTABLE_IP.0, NTABLE_IP.1, NTABLE_IP.2, NTABLE_IP.3), - NTABLE_PORT, - ), - Config { - ..Default::default() - }, - ) - .await - { - Ok(client) => { - println!("Client created"); - app_handle - .emit_all("telemetry_connected", "connected") - .expect("Failed to emit telemetry_status connected event"); - break client; // Exit the loop if the client is successfully created - } - Err(e) => { - println!("Failed to create client: {}. Retrying in 3 seconds...", e); - app_handle - .emit_all("telemetry_status", "disconnected") - .expect("Failed to emit telemetry_status disconnected event"); - - sleep(Duration::from_secs(3)).await; // Wait for 3 seconds before retrying - continue; // Continue the loop to retry - } - }; - }; - - let mut subscription = client - .subscribe_w_options( - &["/SmartDashboard"], - Some(SubscriptionOptions { - all: Some(true), - prefix: Some(true), - ..Default::default() - }), - ) - .await - .expect("Failed to subscribe"); - while let Some(message) = subscription.next().await { - let mut modified_message = message.clone(); - - if let Some(stripped) = modified_message.topic_name.strip_prefix("/SmartDashboard/") { - modified_message.topic_name = stripped.to_string(); - } - - let json_message = to_string(&modified_message).expect("Failed to serialize message"); - app_handle - .emit_all("telemetry_data", json_message.clone()) - .expect("Failed to send telemetry message"); - - println!("{}", json_message); - } - println!("disconnected"); - app_handle - .emit_all("telemetry_status", "disconnected") - .expect("Failed to emit telemetry_disconnected event"); - } -} diff --git a/client/src-tauri/src/telemetry/create_client.rs b/client/src-tauri/src/telemetry/create_client.rs new file mode 100644 index 0000000..f1987fc --- /dev/null +++ b/client/src-tauri/src/telemetry/create_client.rs @@ -0,0 +1,52 @@ +use network_tables::v4::client_config::Config; +use network_tables::v4::Client; +use std::net::{Ipv4Addr, SocketAddrV4}; +use tauri::{AppHandle, Manager}; +use tokio::time::{sleep, Duration}; + +/// Creates a NetworkTables client +/// +/// This function will keep trying to create a NetworkTables client with the given +/// IP address and port until it is successful. It will sleep for 3 seconds between +/// attempts. If successful, it will emit the `telemetry_connected` event on the +/// `app_handle` with the payload `"connected"`. If unsuccessful, it will emit the +/// `telemetry_status` event with the payload `"disconnected"` instead. +pub async fn create_client( + app_handle: &AppHandle, + ntable_ip: &(u8, u8, u8, u8), + ntable_port: &u16, +) -> Client { + loop { + let client_attempt = Client::try_new_w_config( + SocketAddrV4::new( + Ipv4Addr::new(ntable_ip.0, ntable_ip.1, ntable_ip.2, ntable_ip.3), + *ntable_port, + ), + Config { + ..Default::default() + }, + ) + .await; + + match client_attempt { + Ok(client) => { + println!("Client created"); + app_handle + .emit_all("telemetry_connected", "connected") + .expect("Failed to emit telemetry_status connected event"); + break client; // Exit the loop if the client is successfully created + } + Err(e) => { + if cfg!(debug_assertions) { + println!("Failed to create client: {}. Retrying in 3 seconds...", e); + } + app_handle + .emit_all("telemetry_status", "disconnected") + .expect("Failed to emit telemetry_status disconnected event"); + + sleep(Duration::from_secs(3)).await; // Wait for 3 seconds before retrying + continue; // Continue the loop to retry + } + }; + } +} diff --git a/client/src-tauri/src/telemetry/create_subscription.rs b/client/src-tauri/src/telemetry/create_subscription.rs new file mode 100644 index 0000000..6442707 --- /dev/null +++ b/client/src-tauri/src/telemetry/create_subscription.rs @@ -0,0 +1,42 @@ +use network_tables::v4::{Client, Subscription, SubscriptionOptions}; +use tokio::time::{sleep, Duration}; + +/// Create a subscription to all SmartDashboard values +/// +/// The subscription will receive updates to all values in the +/// SmartDashboard, and any future values added to it. +/// +/// The subscription will be created with the following options: +/// +/// * `all`: `true` - receive updates to all values +/// * `prefix`: `true` - receive updates to all keys with the +/// prefix `/SmartDashboard` +/// +/// This function will retry creating a subscription every 3 seconds +/// if it fails. +pub async fn create_subscription(client: &Client) -> Subscription { + loop { + let subscription_attempt = client + .subscribe_w_options( + &["/SmartDashboard"], + Some(SubscriptionOptions { + all: Some(true), + prefix: Some(true), + ..Default::default() + }), + ) + .await; + + match subscription_attempt { + Ok(subscription) => break subscription, + Err(e) => { + if cfg!(debug_assertions) { + println!("Failed to create subscription: {}", e); + } + + sleep(Duration::from_secs(3)).await; // Wait for 3 seconds before retrying + continue; + } + } + } +} diff --git a/client/src-tauri/src/telemetry/mod.rs b/client/src-tauri/src/telemetry/mod.rs new file mode 100644 index 0000000..0c6b588 --- /dev/null +++ b/client/src-tauri/src/telemetry/mod.rs @@ -0,0 +1,61 @@ +use network_tables::v4::MessageData; +use serde_json::to_string; +use tauri::{AppHandle, Manager}; +mod create_client; +mod create_subscription; + +use crate::telemetry::create_client::create_client; +use create_subscription::create_subscription; + +/// Attempts to subscribe to NetworkTables topics and send the data to the frontend. +/// +/// This function creates a NetworkTables client and subscribes to all +/// topics. When new data is received, it is serialized as JSON and emitted +/// to all connected frontends using the "telemetry_data" event. +/// +/// The function loops forever, retrying connection every 3 seconds, reconnecting if the client disconnects. +pub async fn subscribe_topics( + app_handle: AppHandle, + ntable_ip: (u8, u8, u8, u8), + ntable_port: u16, +) { + loop { + // I hope this doesn't lead to a catastrophic infinite loop failure + let client = create_client(&app_handle, &ntable_ip, &ntable_port).await; + + let mut subscription = create_subscription(&client).await; + + while let Some(mut message) = subscription.next().await { + process_message(&mut message); + + let json_message = to_string(&message).expect("Failed to serialize message"); + app_handle + .emit_all("telemetry_data", json_message.clone()) + .expect("Failed to send telemetry message"); + + if cfg!(debug_assertions) { + println!("{}", json_message); + } + } + if cfg!(debug_assertions) { + println!("disconnected"); + } + app_handle + .emit_all("telemetry_status", "disconnected") + .expect("Failed to emit telemetry_disconnected event"); + } +} + +/// Strips the '/SmartDashboard/' prefix from NetworkTables topic names if present. +/// +/// NetworkTables uses the '/SmartDashboard/' prefix to indicate that the topic +/// was published to the SmartDashboard. The SmartDashboard is a way for the robot +/// to send data to any coprocessors, and it's published to the /SmartDashboard topic. +/// +/// This function strips the '/SmartDashboard/' prefix from the topic name if +/// it is present. This allows easier data processing from the frontend. +fn process_message(message: &mut MessageData) { + if let Some(stripped) = message.topic_name.strip_prefix("/SmartDashboard/") { + message.topic_name = stripped.to_string(); + } +}