diff --git a/client/src-tauri/src/main.rs b/client/src-tauri/src/main.rs index a9aca44..82fab0f 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,7 @@ 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, 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..b1b1c90 --- /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_status", "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..c9ab0b5 --- /dev/null +++ b/client/src-tauri/src/telemetry/create_subscription.rs @@ -0,0 +1,49 @@ +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) -> Result { + let mut attempts: u8 = 0; + + 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 Ok(subscription), + Err(e) => { + if cfg!(debug_assertions) { + println!("Failed to create subscription: {}", e); + } + + if attempts >= 50 { + break Err(e); + } + + attempts += 1; + 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..a10ad42 --- /dev/null +++ b/client/src-tauri/src/telemetry/mod.rs @@ -0,0 +1,90 @@ +use network_tables::v4::{MessageData, Subscription}; +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: Subscription = match create_subscription(&client).await { + Ok(subscription) => { + app_handle + .emit_all("telemetry_status", "connected") + .expect("Failed to emit telemetry_connected event"); + subscription + } + Err(_) => { + app_handle + .emit_all("telemetry_status", "disconnected") + .expect("Failed to emit telemetry_disconnected event"); + continue; + } + }; + + 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"); + + app_handle + .emit_all("telemetry_status", "connected") + .expect("Failed to emit telemetry_connected event"); + + check_triggers( + &app_handle, + &message.topic_name, + &message.data, + &previous_gpws, + ); + + if message.topic_name == "gpws" { + previous_gpws = match message.data { + network_tables::Value::Boolean(b) => b, + _ => previous_gpws, + }; + } + + tracing::debug!("{}", json_message); + } + + tracing::debug!("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) { + message.topic_name = message + .topic_name + .trim_start_matches("/SmartDashboard/") + .to_string(); +} diff --git a/client/src/App.svelte b/client/src/App.svelte index 8c9aa48..2bd309b 100644 --- a/client/src/App.svelte +++ b/client/src/App.svelte @@ -12,25 +12,8 @@ import Loading from './lib/Loading/Loading.svelte' import { settingsStore } from './lib/stores/settingsStore' import getSettings from './lib/utils/getSettings' - import { Canvas } from '@threlte/core' - import { emit } from '@tauri-apps/api/event' let activeApp: App = 'camera' - let topics: TelemetryTopics = { - doubles: [ - 'orientation', - 'chassis-x-speed', - 'chassis-y-speed', - 'accx', - 'accy', - 'accz', - 'jerk-x', - 'jerk-y', - 'voltage', - ], - strings: ['acc-profile', 'gear'], - booleans: ['ebrake', 'reorient', 'gpws'], - } let loading = $settingsStore.fastStartup ? false : true let unlistenAll: () => void @@ -42,7 +25,7 @@ } window.ResizeObserver = ResizeObserver // disabled while migrating away from python - initializeTelemetry(topics, 200).then((unsubFunction: () => void) => { + initializeTelemetry().then((unsubFunction: () => void) => { unlistenAll = unsubFunction }) setTimeout(() => { @@ -50,7 +33,7 @@ initializationSequence() }, 3000) - settingsStore.subscribe(value => { + settingsStore.subscribe((value) => { localStorage.setItem('settings', JSON.stringify(value)) }) }) diff --git a/client/src/lib/stores/settingsStore.ts b/client/src/lib/stores/settingsStore.ts index cbb96cd..817c741 100644 --- a/client/src/lib/stores/settingsStore.ts +++ b/client/src/lib/stores/settingsStore.ts @@ -15,7 +15,7 @@ export interface SettingsStoreData { export const defaults: SettingsStoreData = { disableAnnoyances: false, // disable non-critical notifications - goWoke: false, // go woke (for showing parents or other officials where DEI has taken over), disables "offensive" sequences + goWoke: true, // go woke (for showing parents or other officials where DEI has taken over), disables "offensive" sequences fastStartup: false, // skip the loading splash screen (for development purposes. Setting this from within the app has no effect.) randomWeight: 1, // the weight of random events (multiplied by the original probability) voiceLang: 'en-US', @@ -30,7 +30,7 @@ const createSequenceStore = () => { data: keyof SettingsStoreData, newValue: SettingsStoreData[typeof data] ) => { - update(store => { + update((store) => { // @ts-expect-error store[data] = newValue return store diff --git a/client/src/lib/utils/initializeTelemetry.ts b/client/src/lib/utils/initializeTelemetry.ts index 06ba0cc..35fc723 100644 --- a/client/src/lib/utils/initializeTelemetry.ts +++ b/client/src/lib/utils/initializeTelemetry.ts @@ -9,23 +9,9 @@ import { listen } from '@tauri-apps/api/event' * which will be called with the NetworkTable object every time an update is received from the backend. */ -const onUpdate = (data: TelemetryData) => { - telemetryStore.update(data) - // console.log(data) -} - -export const initializeTelemetry = async ( - topics: TelemetryTopics, - refreshRate: number -) => { - // Make sure refreshRate is valid - if (!Number.isInteger(refreshRate) || refreshRate < 1) { - throw new Error( - 'refreshRate must be an integer greater than or equal to 1.' - ) - } - +export const initializeTelemetry = async () => { const unlistenStatus = await listen('telemetry_status', event => { + console.log(event) if (event.payload === 'connected') { telemetryStore.set('connected', true) } else if (event.payload === 'disconnected') { @@ -38,6 +24,16 @@ export const initializeTelemetry = async ( telemetryStore.set(data['topic_name'], data['data']) }) +<<<<<<< HEAD +======= + const unlistenGPWS = await listen('telemetry_gpws', event => { + const data = JSON.parse(event.payload as string) as boolean + if (data) { + gpwsTriggeredSequence() + } + }) + +>>>>>>> cffa594 (fix: detect connectivity properly) const unlistenAll = () => { unlistenStatus() unlistenTelemetry()