Skip to content

Commit df3de6c

Browse files
committed
Showing active subscriptions at the bottom of the UI
1 parent f2f9385 commit df3de6c

1 file changed

Lines changed: 141 additions & 6 deletions

File tree

canopen-viewer/src/main.rs

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::process::Command as process_command;
1616
use std::path::PathBuf;
1717
use std::sync::mpsc::{Sender, Receiver};
1818
use egui_plot::{Plot, PlotPoints, Line, Legend};
19-
use chrono::Local;
19+
use chrono::{Local, DateTime};
2020
use std::sync::Arc;
2121

2222
const 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)]
3239
struct 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
}
3647
struct 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

Comments
 (0)