Skip to content

Commit 2ae393b

Browse files
committed
- Putting timestamp on x-axis in the graph
- Exporting the graph in csv form - Clearing the graph all data from a button on the graph
1 parent df3de6c commit 2ae393b

1 file changed

Lines changed: 91 additions & 25 deletions

File tree

canopen-viewer/src/main.rs

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ pub enum SubscriptionStatus {
3838
#[derive(Debug, Clone)]
3939
struct 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
}
4749
struct 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

Comments
 (0)