@@ -16,7 +16,7 @@ use std::process::Command as process_command;
1616use std:: path:: PathBuf ;
1717use std:: sync:: mpsc:: { Sender , Receiver } ;
1818use egui_plot:: { Plot , PlotPoints , Line , Legend } ;
19- use chrono:: Local ;
19+ use chrono:: { Local , DateTime } ;
2020use std:: sync:: Arc ;
2121
2222const PLOT_BUFFER_SIZE : usize = 500 ;
@@ -28,10 +28,21 @@ enum AppView {
2828 Main
2929}
3030
31+ #[ derive( Debug , Clone ) ]
32+ pub enum SubscriptionStatus {
33+ Active , // Currently receiving data
34+ Error ( String ) , // Error occurred (with error message)
35+ Idle , // Subscribed but no recent data
36+ }
37+
3138#[ derive( Debug , Clone ) ]
3239struct SdoSubscription {
3340 interval_ms : u64 ,
3441 plot_data : VecDeque < [ f64 ; 2 ] > ,
42+ data_type : SdoDataType ,
43+ last_value : Option < String > ,
44+ last_timestamp : Option < DateTime < Local > > ,
45+ status : SubscriptionStatus ,
3546}
3647struct ScreenshotInfo {
3748 filename : String ,
@@ -152,11 +163,14 @@ impl eframe::App for MyApp {
152163 value : value. clone ( ) ,
153164 } ) ;
154165
155- // 1. Try to parse the incoming string value into a number.
156- if let Ok ( number_value) = value. parse :: < f64 > ( ) {
157- // 2. Find the subscription this data belongs to.
158- if let Some ( subscription) = self . subscriptions . get_mut ( & address) {
166+ // Update subscription metadata
167+ if let Some ( subscription) = self . subscriptions . get_mut ( & address) {
168+ subscription. last_value = Some ( value. clone ( ) ) ;
169+ subscription. last_timestamp = Some ( Local :: now ( ) ) ;
170+ subscription. status = SubscriptionStatus :: Active ;
159171
172+ // 1. Try to parse the incoming string value into a number for plotting.
173+ if let Ok ( number_value) = value. parse :: < f64 > ( ) {
160174 if subscription. plot_data . len ( ) >= PLOT_BUFFER_SIZE {
161175 subscription. plot_data . pop_front ( ) ;
162176 }
@@ -186,6 +200,11 @@ impl eframe::App for MyApp {
186200 error : error. clone ( ) ,
187201 } ) ;
188202
203+ // Update subscription status to error
204+ if let Some ( subscription) = self . subscriptions . get_mut ( & address) {
205+ subscription. status = SubscriptionStatus :: Error ( error. clone ( ) ) ;
206+ }
207+
189208 self . error_message = Some ( format ! ( "SDO Read Error [{:#06X}:{:02X}]: {}" , address. index, address. sub_index, error) ) ;
190209 }
191210 _ => {
@@ -472,6 +491,11 @@ impl MyApp {
472491 }
473492 } ) ;
474493
494+ // Bottom panel for subscription management
495+ egui:: TopBottomPanel :: bottom ( "subscription_panel" ) . show_inside ( ui, |ui| {
496+ self . draw_subscription_management ( ui) ;
497+ } ) ;
498+
475499 // Creating panels. Left panel for SDO data, right panel for graphing.
476500 egui:: SidePanel :: left ( "sdo_list_panel" ) . show_inside ( ui, |ui| {
477501 self . draw_sdo_list ( ui) ;
@@ -589,6 +613,113 @@ impl MyApp {
589613 } ) ;
590614 }
591615
616+ fn draw_subscription_management ( & mut self , ui : & mut egui:: Ui ) {
617+ ui. horizontal ( |ui| {
618+ ui. heading ( "Active Subscriptions" ) ;
619+ ui. with_layout ( egui:: Layout :: right_to_left ( egui:: Align :: Center ) , |ui| {
620+ // Stop All button
621+ let stop_all_enabled = !self . subscriptions . is_empty ( ) ;
622+ if ui. add_enabled ( stop_all_enabled, egui:: Button :: new ( "🛑 Stop All" ) ) . clicked ( ) {
623+ // Send unsubscribe commands for all active subscriptions
624+ if let Some ( tx) = & self . command_tx {
625+ for address in self . subscriptions . keys ( ) {
626+ let _ = tx. send ( Command :: Unsubscribe ( address. clone ( ) ) ) ;
627+ }
628+ }
629+ self . subscriptions . clear ( ) ;
630+ }
631+
632+ // Subscription statistics
633+ let active_count = self . subscriptions . iter ( )
634+ . filter ( |( _, sub) | matches ! ( sub. status, SubscriptionStatus :: Active ) )
635+ . count ( ) ;
636+ let error_count = self . subscriptions . iter ( )
637+ . filter ( |( _, sub) | matches ! ( sub. status, SubscriptionStatus :: Error ( _) ) )
638+ . count ( ) ;
639+
640+ ui. label ( format ! ( "Total: {} | Active: {} | Errors: {}" ,
641+ self . subscriptions. len( ) , active_count, error_count) ) ;
642+ } ) ;
643+ } ) ;
644+
645+ ui. separator ( ) ;
646+
647+ if self . subscriptions . is_empty ( ) {
648+ ui. label ( "No active subscriptions. Select an SDO from the list above to start monitoring." ) ;
649+ } else {
650+ egui:: ScrollArea :: horizontal ( ) . show ( ui, |ui| {
651+ egui:: Grid :: new ( "subscription_grid" )
652+ . num_columns ( 7 )
653+ . spacing ( [ 10.0 , 4.0 ] )
654+ . striped ( true )
655+ . show ( ui, |ui| {
656+ // Header row
657+ ui. label ( "Status" ) ;
658+ ui. label ( "Address" ) ;
659+ ui. label ( "Data Type" ) ;
660+ ui. label ( "Interval" ) ;
661+ ui. label ( "Last Value" ) ;
662+ ui. label ( "Last Update" ) ;
663+ ui. label ( "Actions" ) ;
664+ ui. end_row ( ) ;
665+
666+ // Data rows
667+ let mut to_remove = Vec :: new ( ) ;
668+ for ( address, subscription) in & self . subscriptions {
669+ // Status indicator with color
670+ match & subscription. status {
671+ SubscriptionStatus :: Active => {
672+ ui. colored_label ( Color32 :: from_rgb ( 0 , 200 , 0 ) , "🟢 Active" ) ;
673+ } ,
674+ SubscriptionStatus :: Error ( err) => {
675+ ui. colored_label ( Color32 :: from_rgb ( 200 , 0 , 0 ) , "🔴 Error" )
676+ . on_hover_text ( err) ;
677+ } ,
678+ SubscriptionStatus :: Idle => {
679+ ui. colored_label ( Color32 :: from_rgb ( 200 , 200 , 0 ) , "🟡 Idle" ) ;
680+ } ,
681+ } ;
682+
683+ // Address
684+ ui. label ( format ! ( "{:#06X}:{:02X}" , address. index, address. sub_index) ) ;
685+
686+ // Data type
687+ ui. label ( format ! ( "{:?}" , subscription. data_type) ) ;
688+
689+ // Interval
690+ ui. label ( format ! ( "{} ms" , subscription. interval_ms) ) ;
691+
692+ // Last value (truncate if too long)
693+ let value_text = subscription. last_value . as_ref ( )
694+ . map ( |v| if v. len ( ) > 20 { format ! ( "{}..." , & v[ ..17 ] ) } else { v. clone ( ) } )
695+ . unwrap_or_else ( || "—" . to_string ( ) ) ;
696+ ui. label ( value_text) ;
697+
698+ // Last timestamp
699+ let timestamp_text = subscription. last_timestamp . as_ref ( )
700+ . map ( |t| t. format ( "%H:%M:%S" ) . to_string ( ) )
701+ . unwrap_or_else ( || "—" . to_string ( ) ) ;
702+ ui. label ( timestamp_text) ;
703+
704+ // Actions (Stop button)
705+ if ui. button ( "🛑 Stop" ) . clicked ( ) {
706+ if let Some ( tx) = & self . command_tx {
707+ let _ = tx. send ( Command :: Unsubscribe ( address. clone ( ) ) ) ;
708+ }
709+ to_remove. push ( address. clone ( ) ) ;
710+ }
711+ ui. end_row ( ) ;
712+ }
713+
714+ // Remove stopped subscriptions
715+ for address in to_remove {
716+ self . subscriptions . remove ( & address) ;
717+ }
718+ } ) ;
719+ } ) ;
720+ }
721+ }
722+
592723 fn draw_subscription_modal ( & mut self , ui : & mut egui:: Ui ) {
593724 if let Some ( address) = self . modal_open_for . clone ( ) {
594725 let mut is_open = true ;
@@ -626,12 +757,16 @@ impl MyApp {
626757 tx. send ( Command :: Subscribe {
627758 address : address. clone ( ) ,
628759 interval_ms,
629- data_type,
760+ data_type : data_type . clone ( ) ,
630761 } ) . unwrap ( ) ;
631762 }
632763 self . subscriptions . insert ( address. clone ( ) , SdoSubscription {
633764 interval_ms,
634765 plot_data : VecDeque :: new ( ) ,
766+ data_type,
767+ last_value : None ,
768+ last_timestamp : None ,
769+ status : SubscriptionStatus :: Idle ,
635770 } ) ;
636771 self . modal_open_for = None ; // Close the modal
637772 }
0 commit comments