@@ -38,11 +38,13 @@ pub enum SubscriptionStatus {
3838#[ derive( Debug , Clone ) ]
3939struct SdoSubscription {
4040 interval_ms : u64 ,
41- plot_data : VecDeque < [ f64 ; 2 ] > ,
41+ plot_data : VecDeque < [ f64 ; 2 ] > , // [timestamp_seconds, value]
4242 data_type : SdoDataType ,
4343 last_value : Option < String > ,
4444 last_timestamp : Option < DateTime < Local > > ,
4545 status : SubscriptionStatus ,
46+ paused : bool ,
47+ start_time : DateTime < Local > , // Reference point for relative timestamps
4648}
4749struct ScreenshotInfo {
4850 filename : String ,
@@ -165,17 +167,23 @@ impl eframe::App for MyApp {
165167
166168 // Update subscription metadata
167169 if let Some ( subscription) = self . subscriptions . get_mut ( & address) {
170+ let now = Local :: now ( ) ;
168171 subscription. last_value = Some ( value. clone ( ) ) ;
169- subscription. last_timestamp = Some ( Local :: now ( ) ) ;
172+ subscription. last_timestamp = Some ( now) ;
170173 subscription. status = SubscriptionStatus :: Active ;
171174
172- // 1. Try to parse the incoming string value into a number for plotting.
173- if let Ok ( number_value) = value. parse :: < f64 > ( ) {
174- if subscription. plot_data . len ( ) >= PLOT_BUFFER_SIZE {
175- subscription. plot_data . pop_front ( ) ;
175+ // Only add to plot data if not paused
176+ if !subscription. paused {
177+ // Try to parse the incoming string value into a number for plotting.
178+ if let Ok ( number_value) = value. parse :: < f64 > ( ) {
179+ if subscription. plot_data . len ( ) >= PLOT_BUFFER_SIZE {
180+ subscription. plot_data . pop_front ( ) ;
181+ }
182+
183+ // Calculate seconds since start time for X-axis
184+ let elapsed_seconds = ( now - subscription. start_time ) . num_milliseconds ( ) as f64 / 1000.0 ;
185+ subscription. plot_data . push_back ( [ elapsed_seconds, number_value] ) ;
176186 }
177- let time = subscription. plot_data . back ( ) . map_or ( 0.0 , |p| p[ 0 ] + 1.0 ) ;
178- subscription. plot_data . push_back ( [ time, number_value] ) ;
179187 }
180188 }
181189 }
@@ -274,7 +282,7 @@ impl MyApp {
274282 ui. add_space ( 20.0 ) ;
275283
276284 let is_next_enabled = self . selected_can_interface . is_some ( ) ;
277- if ui. add_enabled ( is_next_enabled, egui:: Button :: new ( "Next → " ) ) . clicked ( ) {
285+ if ui. add_enabled ( is_next_enabled, egui:: Button :: new ( "Next ➡ " ) ) . clicked ( ) {
278286 self . current_view = AppView :: SelectNodeId ;
279287 }
280288 }
@@ -319,12 +327,12 @@ impl MyApp {
319327
320328 // Navigation buttons.
321329 ui. horizontal ( |ui| {
322- if ui. button ( "← Back" ) . clicked ( ) {
330+ if ui. button ( "⬅ Back" ) . clicked ( ) {
323331 self . current_view = AppView :: SelectInterface ;
324332 }
325333
326334 let is_start_enabled = self . selected_node_id . is_some ( ) ;
327- if ui. add_enabled ( is_start_enabled, egui:: Button :: new ( "Next → " ) ) . clicked ( ) {
335+ if ui. add_enabled ( is_start_enabled, egui:: Button :: new ( "Next ➡ " ) ) . clicked ( ) {
328336 self . current_view = AppView :: SelectEDSFile ;
329337 }
330338 } ) ;
@@ -368,10 +376,10 @@ impl MyApp {
368376
369377 // Navigation buttons
370378 ui. horizontal ( |ui| {
371- if ui. button ( "← Back" ) . clicked ( ) {
379+ if ui. button ( "⬅ Back" ) . clicked ( ) {
372380 self . current_view = AppView :: SelectNodeId ;
373381 }
374- if ui. button ( "Start" ) . clicked ( ) {
382+ if ui. button ( "🚀 Start" ) . clicked ( ) {
375383 // Update and save configuration
376384 self . config . can_interface = self . selected_can_interface . clone ( ) . unwrap ( ) ;
377385 self . config . node_id = self . selected_node_id . unwrap ( ) ;
@@ -559,6 +567,10 @@ impl MyApp {
559567 if self . subscriptions . is_empty ( ) {
560568 ui. label ( "No active subscriptions. Select an SDO to start reading." ) ;
561569 } else {
570+
571+ let mut addresses_to_clear = Vec :: new ( ) ;
572+ let mut addresses_to_export = Vec :: new ( ) ;
573+
562574 for ( address, subscription) in & self . subscriptions {
563575 // 1. Use a Frame to visually group each plot and its title.
564576 egui:: Frame :: group ( ui. style ( ) ) . show ( ui, |ui| {
@@ -575,7 +587,7 @@ impl MyApp {
575587 . allow_scroll ( false )
576588 . height ( 350.0 )
577589 . width ( ui. available_width ( ) )
578- . x_axis_label ( "Sample No " )
590+ . x_axis_label ( "Time (seconds) " )
579591 . y_axis_label ( "Value" )
580592 . legend ( Legend :: default ( ) )
581593 . show ( ui, |plot_ui| {
@@ -595,20 +607,41 @@ impl MyApp {
595607 plot_ui. line ( line) ;
596608 } ) ;
597609
598- if ui. button ( "Save Plot" ) . clicked ( ) {
599- let now = Local :: now ( ) ;
600- let timestamp = now. format ( "%Y-%m-%d %H:%M:%S" ) ;
601- let info = ScreenshotInfo {
602- filename : format ! ( "{}_{}.png" , plot_title. replace( ":" , "_" ) , timestamp) ,
603- // rect: plot_response.response.rect,
604- rect : ui. min_rect ( ) ,
605- } ;
610+ ui. horizontal ( |ui| {
611+ if ui. button ( "📸 Capture Plot" ) . clicked ( ) {
612+ let now = Local :: now ( ) ;
613+ let timestamp = now. format ( "%Y-%m-%d %H:%M:%S" ) ;
614+ let info = ScreenshotInfo {
615+ filename : format ! ( "{}_{}.png" , plot_title. replace( ":" , "_" ) , timestamp) ,
616+ // rect: plot_response.response.rect,
617+ rect : ui. min_rect ( ) ,
618+ } ;
619+
620+ let user_data = egui:: UserData :: new ( Arc :: new ( info) ) ;
621+ ui. ctx ( ) . send_viewport_cmd ( egui:: ViewportCommand :: Screenshot ( user_data) ) ;
622+ }
606623
607- let user_data = egui:: UserData :: new ( Arc :: new ( info) ) ;
608- ui. ctx ( ) . send_viewport_cmd ( egui:: ViewportCommand :: Screenshot ( user_data) ) ;
609- }
624+ if ui. button ( "🗑 Clear" ) . clicked ( ) {
625+ addresses_to_clear. push ( address. clone ( ) ) ;
626+ }
627+
628+ if ui. button ( "💾 Export to CSV" ) . clicked ( ) {
629+ addresses_to_export. push ( address. clone ( ) ) ;
630+ }
631+ } ) ;
610632 } ) ;
611633 }
634+
635+ for address in addresses_to_clear {
636+ if let Some ( subscription) = self . subscriptions . get_mut ( & address) {
637+ subscription. start_time = Local :: now ( ) ;
638+ subscription. plot_data . clear ( ) ;
639+ }
640+ }
641+
642+ for address in addresses_to_export {
643+ self . export_plot_data_to_csv ( & address) ;
644+ }
612645 }
613646 } ) ;
614647 }
@@ -760,13 +793,16 @@ impl MyApp {
760793 data_type : data_type. clone ( ) ,
761794 } ) . unwrap ( ) ;
762795 }
796+ let now = Local :: now ( ) ;
763797 self . subscriptions . insert ( address. clone ( ) , SdoSubscription {
764798 interval_ms,
765799 plot_data : VecDeque :: new ( ) ,
766800 data_type,
767801 last_value : None ,
768802 last_timestamp : None ,
769803 status : SubscriptionStatus :: Idle ,
804+ paused : false ,
805+ start_time : now,
770806 } ) ;
771807 self . modal_open_for = None ; // Close the modal
772808 }
@@ -799,6 +835,36 @@ impl MyApp {
799835 }
800836 }
801837
838+ fn export_plot_data_to_csv ( & mut self , address : & SdoAddress ) {
839+ if let Some ( subscription) = self . subscriptions . get ( address) {
840+ let file_name = format ! ( "plot_data_{:04X}_{:02X}.csv" , address. index, address. sub_index) ;
841+ if let Some ( path) = rfd:: FileDialog :: new ( ) . set_file_name ( & file_name) . save_file ( ) {
842+ match csv:: Writer :: from_path ( path) {
843+ Ok ( mut writer) => {
844+ // Write header
845+ if let Err ( e) = writer. write_record ( & [ "Sample No" , "Value" ] ) {
846+ eprintln ! ( "Failed to write CSV header: {}" , e) ;
847+ }
848+
849+ // Write data
850+ for point in & subscription. plot_data {
851+ if let Err ( e) = writer. write_record ( & [ point[ 0 ] . to_string ( ) , point[ 1 ] . to_string ( ) ] ) {
852+ eprintln ! ( "Failed to write CSV record: {}" , e) ;
853+ }
854+ }
855+
856+ if let Err ( e) = writer. flush ( ) {
857+ eprintln ! ( "Failed to flush CSV file: {}" , e) ;
858+ }
859+ } ,
860+ Err ( e) => {
861+ eprintln ! ( "Failed to create CSV file: {}" , e) ;
862+ }
863+ }
864+ }
865+ }
866+ }
867+
802868}
803869
804870
0 commit comments