diff --git a/src/coordinator/coord_state.rs b/src/coordinator/coord_state.rs index 24a66e9..a9e6afb 100644 --- a/src/coordinator/coord_state.rs +++ b/src/coordinator/coord_state.rs @@ -20,6 +20,7 @@ use crate::remote_sources::TrackerState; use crate::{gstreamer_pipeline, remote_sources}; use crate::{joystick_source::joystick_loop, ui::GuiUpdate}; +use super::perf_state::TrackerMetrics; use super::remote_video_processor::remote_video_loop; use super::{ApplicationEvent, ConnectionType}; @@ -32,6 +33,7 @@ pub struct SocketState { #[derive(Debug)] pub struct CoordState<'a> { pub settings: Arc>, + pub tracker_metrics: Arc>, pub sck_outbound: Option>, Message>>, pub remote_sources_state: Arc, @@ -60,9 +62,11 @@ impl<'a> CoordState<'a> { rt: Handle, tracker_state: Arc>, settings: Arc>, + tracker_header: Arc>, ) -> Self { let this = CoordState { settings, + tracker_metrics: Arc::new(tokio::sync::Mutex::new(TrackerMetrics::new(tracker_header))), sck_outbound: None, stay_alive_sck_recvr: Arc::new(AtomicBool::new(false)), @@ -181,6 +185,7 @@ impl<'a> CoordState<'a> { self.to_mec.clone(), self.tracker_connection_state.clone(), self.tracker_state.clone(), + self.tracker_metrics.clone(), self.rt.clone(), )); } diff --git a/src/coordinator/mod.rs b/src/coordinator/mod.rs index 857a332..ae4be26 100644 --- a/src/coordinator/mod.rs +++ b/src/coordinator/mod.rs @@ -19,6 +19,7 @@ use tracing::{debug, error, info, instrument}; mod coord_state; mod process_box_string; mod remote_video_processor; +mod perf_state; use crate::config::AppConfig; use crate::remote_sources::TrackerState; @@ -56,12 +57,13 @@ pub async fn start_coordinator( runtime: Handle, tracker_state: Arc>, settings: Arc>, + tracker_header: Arc>, ) { info!("Starting coordinator!"); let mec = pin!(mec); - let mut state = CoordState::new(mec, to_mec, to_gui, runtime, tracker_state, settings); + let mut state = CoordState::new(mec, to_mec, to_gui, runtime, tracker_state, settings, tracker_header); state .pipeline diff --git a/src/coordinator/perf_state.rs b/src/coordinator/perf_state.rs new file mode 100644 index 0000000..82f679b --- /dev/null +++ b/src/coordinator/perf_state.rs @@ -0,0 +1,57 @@ +use std::{sync, time::Duration, collections::VecDeque}; + +const MAX_RECORDED_TIMES: usize = 10; +const DEGRADED_TRACKER_TIME: u128 = 100; + +#[derive(Debug)] +pub struct TrackerMetrics { + pub header_text: sync::Arc>, + tracker_times: VecDeque, + +} + +impl TrackerMetrics { + pub fn new(text_reference: sync::Arc>) -> Self { + TrackerMetrics { + header_text: text_reference, + + tracker_times: VecDeque::with_capacity(MAX_RECORDED_TIMES), + } + } + + pub fn clear_times(&mut self) { + for _ in 0..10 { + self.tracker_times.pop_front(); + } + self.insert_time(Duration::new(0, 0)); + } + + pub fn insert_time(&mut self, new_measurement: Duration) { + if self.tracker_times.len() > MAX_RECORDED_TIMES { + let _ = self.tracker_times.pop_front(); + } + self.tracker_times.push_back(new_measurement.as_millis()); + + let avg_time = self.tracker_times.iter().sum::() / self.tracker_times.len() as u128; + + if avg_time == 0 { + if let Ok(mut writer) = self.header_text.write() { + writer.clear(); + writer.push_str(&format!("Status: Failed Avg Response: {} ms", avg_time.to_string())); + } + } + + if avg_time > DEGRADED_TRACKER_TIME { + if let Ok(mut writer) = self.header_text.write() { + writer.clear(); + writer.push_str(&format!("Status: Degraded Avg Response: {} ms", avg_time.to_string())); + } + } else { + if let Ok(mut writer) = self.header_text.write() { + writer.clear(); + writer.push_str(&format!("Status: Nominal Avg Response: {} ms", avg_time.to_string())); + } + } + + } +} \ No newline at end of file diff --git a/src/coordinator/remote_video_processor.rs b/src/coordinator/remote_video_processor.rs index 60fac12..f3d8ca5 100644 --- a/src/coordinator/remote_video_processor.rs +++ b/src/coordinator/remote_video_processor.rs @@ -18,7 +18,7 @@ use tracing::{error, info, instrument}; use crate::remote_sources::TrackerState; -use super::{process_box_string::process_incoming_string, ApplicationEvent, SocketState}; +use super::{perf_state::TrackerMetrics, process_box_string::process_incoming_string, ApplicationEvent, SocketState}; #[instrument] pub async fn remote_video_loop( @@ -27,12 +27,14 @@ pub async fn remote_video_loop( to_mec: Sender, socket_state: Arc, tracker_state: Arc>, + tracker_metrics: Arc>, runtime: Handle, ) { info!( "Starting remote tracker processing connection to: {}", conn_string ); + let video_info = gstreamer_video::VideoInfo::builder(gstreamer_video::VideoFormat::Rgb, 640, 480) .build() @@ -54,8 +56,11 @@ pub async fn remote_video_loop( tracker_state.clone(), socket_state.clone(), )); + + let mut last_iter: Instant; loop { + last_iter = Instant::now(); // Do this in an encloser to not keep a lock on the appsink let video_frame = match { let appsnk = match appsink.lock() { @@ -100,6 +105,12 @@ pub async fn remote_video_loop( info!("Shutting down remote video loop"); break; } + + { + let mut tm = tracker_metrics.lock().await; + tm.insert_time(Instant::now() - last_iter); + } + // rate limit updates sleep_until(Instant::now() + Duration::from_millis(50)).await; } diff --git a/src/ui/liveview_panel.rs b/src/ui/liveview_panel.rs new file mode 100644 index 0000000..8534a03 --- /dev/null +++ b/src/ui/liveview_panel.rs @@ -0,0 +1,78 @@ +use gtk::{gdk::Paintable, prelude::BoxExt, AspectFrame, Box, DrawingArea, Label, Overlay, Picture}; + + +pub struct LiveViewPanel { + top_level: gtk::Box, + + pub tracker_status_label: Label, + pub cam_status_label: Label, + pub adjustment_label: Label, + picture: Picture, + overlay: Overlay, +} + +impl LiveViewPanel { + pub fn new() -> Self { + + let right_box = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .hexpand(true) + .valign(gtk::Align::Center) + .build(); + + let tracker_status_label = Label::builder() + .label("No Status Yet".to_string()) + .can_focus(true) + .css_classes(vec!["large-label"]) + .build(); + + // let conn_status_label = Label::new(Some(&"No Connection".to_string())); + let cam_status_label = Label::builder() + .label("No Connection".to_string()) + .can_focus(true) + .build(); + + let adjustment_label = Label::builder() + .label("X: 0 Y: )") + .justify(gtk::Justification::Center) + .css_classes(vec!["JoystickCurrent"]) + .build(); + + let webcam_picture = gtk::Picture::builder().can_focus(false).build(); + + let overlay_box = gtk::Overlay::builder().build(); + overlay_box.set_child(Some(&webcam_picture)); + + let aspect = AspectFrame::builder() + .ratio(16.0 / 9.0) + .obey_child(false) + .child(&overlay_box) + .build(); + + right_box.append(&tracker_status_label); + right_box.append(&aspect); + right_box.append(&cam_status_label); + right_box.append(&adjustment_label); + + LiveViewPanel { + adjustment_label, + tracker_status_label, + cam_status_label, + overlay: overlay_box, + picture: webcam_picture, + top_level: right_box, + } + } + + pub fn get_top_level(&self) -> &Box { + &self.top_level + } + + pub fn set_drawable(&self, new_drawable: &DrawingArea) { + self.overlay.add_overlay(new_drawable); + } + + pub fn set_paintable(&self, new_paintable: &Paintable) { + self.picture.set_paintable(Some(new_paintable)); + } +} \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b6e2df6..9e1b1b7 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,7 +5,7 @@ use std::time::Instant; use gtk::cairo::Context; use gtk::gdk::Paintable; use gtk::glib::clone; -use gtk::{gio, glib, prelude::*, AspectFrame, CssProvider, Label}; +use gtk::{gio, glib, prelude::*, CssProvider}; use gtk::{Application, ApplicationWindow}; use log::{error, info}; use tokio::runtime::Handle; @@ -17,8 +17,10 @@ use crate::remote_sources::TrackerState; mod control_panel; mod settings_modal; +mod liveview_panel; use control_panel::ControlPanel; +use liveview_panel::LiveViewPanel; pub enum GuiUpdate { SocketDisconnected, @@ -115,12 +117,6 @@ pub fn build_ui(app: &Application, config: Arc>, runtime: Hand .orientation(gtk::Orientation::Vertical) .build(); - let right_box = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .hexpand(true) - .valign(gtk::Align::Center) - .build(); - // Create a window let window = ApplicationWindow::builder() .application(app) @@ -156,6 +152,8 @@ pub fn build_ui(app: &Application, config: Arc>, runtime: Hand })); let coord_config = config.clone(); + + let tracker_header = Arc::new(std::sync::RwLock::new(String::new())); runtime.spawn(start_coordinator( mec, @@ -164,42 +162,19 @@ pub fn build_ui(app: &Application, config: Arc>, runtime: Hand runtime.clone(), tracker_state.clone(), coord_config, + tracker_header.clone(), )); - // let conn_status_label = Label::new(Some(&"No Connection".to_string())); - let conn_status_label = Label::builder() - .label("No Connection".to_string()) - .can_focus(true) - .build(); - let control_panel = Arc::new(ControlPanel::new(tracker_state.clone())); control_panel.connect_button_callbacks(to_mec.clone()); - let axis_label = Label::builder() - .label("X: 0 Y: )") - .justify(gtk::Justification::Center) - .css_classes(vec!["JoystickCurrent"]) - .build(); - // left_box.append(&conn_status_label); left_box.append(control_panel.get_top_level()); main_box.append(&left_box); - let webcam_picture = gtk::Picture::builder().can_focus(false).build(); - - let overlay_box = gtk::Overlay::builder().build(); - let aspect = AspectFrame::builder() - .ratio(16.0 / 9.0) - .obey_child(false) - .child(&overlay_box) - .build(); - - right_box.append(&aspect); - right_box.append(&conn_status_label); - right_box.append(&axis_label); - - main_box.append(&right_box); + let liveview_panel = LiveViewPanel::new(); + main_box.append(liveview_panel.get_top_level()); let drawable = gtk::DrawingArea::builder().build(); @@ -208,8 +183,7 @@ pub fn build_ui(app: &Application, config: Arc>, runtime: Hand draw_boxes(width, height, &ctx, &drawable_ts); }); - overlay_box.set_child(Some(&webcam_picture)); - overlay_box.add_overlay(&drawable); + liveview_panel.set_drawable(&drawable); let items = control_panel.items.clone(); let id_label = control_panel.current_id.clone(); @@ -248,42 +222,45 @@ pub fn build_ui(app: &Application, config: Arc>, runtime: Hand ); glib::spawn_future_local( - glib::clone!(@weak axis_label, @weak conn_status_label, @weak drawable => async move { + glib::clone!(@weak drawable => async move { while let Ok(d) = gui_recv.recv().await { drawable.queue_draw(); + if let Ok(reader) = tracker_header.read() { + liveview_panel.tracker_status_label.set_text(reader.as_str()); + } match d { GuiUpdate::MoveEvent(msg) => { - axis_label.set_text( + liveview_panel.adjustment_label.set_text( format!("X: {:>4} Y: {:>4}", msg.x, msg.y).as_str() ); } GuiUpdate::SocketConnected => { control_panel.connection_buttons.camera_connection.set_sensitive(true); control_panel.connection_buttons.camera_connection.set_label("Press to Disconnect"); - conn_status_label.set_label("Connected"); + liveview_panel.cam_status_label.set_label("Connected"); - conn_status_label.set_css_classes(&["YesConnection"]); + liveview_panel.cam_status_label.set_css_classes(&["YesConnection"]); }, GuiUpdate::SocketConnecting => { control_panel.connection_buttons.camera_connection.set_sensitive(false); control_panel.connection_buttons.camera_connection.set_label("Please wait"); - conn_status_label.set_label("Connecting"); + liveview_panel.cam_status_label.set_label("Connecting"); - conn_status_label.set_css_classes(&["LoadingConnection"]); + liveview_panel.cam_status_label.set_css_classes(&["LoadingConnection"]); }, GuiUpdate::SocketDisconnected => { control_panel.connection_buttons.camera_connection.set_sensitive(true); control_panel.connection_buttons.camera_connection.set_label("Press to Connect to Camera"); - conn_status_label.set_label("Not Connected to Camera"); + liveview_panel.cam_status_label.set_label("Not Connected to Camera"); - conn_status_label.set_css_classes(&["NoConnection"]); + liveview_panel.cam_status_label.set_css_classes(&["NoConnection"]); } GuiUpdate::UpdatePaintable(sink) => { let paintable = sink.property::("paintable"); - webcam_picture.set_paintable(Some(&paintable)); + liveview_panel.set_paintable(&paintable); } } } diff --git a/style.css b/style.css index 631260d..83be33b 100644 --- a/style.css +++ b/style.css @@ -15,6 +15,10 @@ label.current-id { margin-bottom: 4px; } +label.large-label { + font-size: 10pt; +} + entry { font-size: 16pt; }