-
Notifications
You must be signed in to change notification settings - Fork 16
created scope launcher + signal selector #435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,17 @@ | ||
| use crate::messages; | ||
| use crate::{app, messages, util}; | ||
| use eframe::egui; | ||
| use egui_plot::{Line, Plot, PlotPoints}; | ||
| use std::collections::VecDeque; | ||
|
|
||
| use super::dbc_msg_picker::{DbcMsgPickerState, no_dbc_placeholder}; | ||
|
|
||
| pub struct Scope { | ||
| pub title: String, | ||
| msg_id: u32, | ||
| msg_name: String, | ||
| signal_name: String, | ||
| msg_id: Option<u32>, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From my understanding, msg_id and msg_name are either both None or both Some at the same time. To get better production, you can make a mini struct and then the Scope just has Option |
||
| msg_name: Option<String>, | ||
| signal_name: Option<String>, | ||
| msg_picker: DbcMsgPickerState, // Picker state, only relevant while msg_id/signal_name are None (or user hit "Change Signal") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use a sum type (Rust |
||
| selected_msg: Option<can_dbc::Message>, | ||
| window: VecDeque<(f64, f64)>, // (time, value) | ||
| window_duration_seconds: f64, | ||
| decimation_factor: u64, | ||
|
|
@@ -17,13 +21,35 @@ pub struct Scope { | |
| } | ||
|
|
||
| impl Scope { | ||
| // Og constructor, unchanged | ||
| pub fn new(instance_num: usize, msg_id: u32, msg_name: String, signal_name: String) -> Self { | ||
| let title = format!("Scope #{}", instance_num); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a problem that you were supposed to solve, but is it possible to make the title include the signal name instead of the instance_num? |
||
| Self { | ||
| title, | ||
| msg_id, | ||
| msg_name, | ||
| signal_name, | ||
| msg_id: Some(msg_id), | ||
| msg_name: Some(msg_name), | ||
| signal_name: Some(signal_name), | ||
| msg_picker: DbcMsgPickerState::default(), | ||
| selected_msg: None, | ||
| window: VecDeque::new(), | ||
| window_duration_seconds: 10.0, // Default 10 seconds | ||
| decimation_factor: 0, | ||
| decimation_counter: 0, | ||
| reference_time: None, | ||
| is_paused: false, | ||
| } | ||
| } | ||
|
|
||
| // New constructor, used by sidebar, opens into picker ui | ||
| pub fn new_empty(instance_num: usize) -> Self { | ||
| let title = format!("Scope #{}", instance_num); | ||
| Self { | ||
| title, | ||
| msg_id: None, | ||
| msg_name: None, | ||
| signal_name: None, | ||
| msg_picker: DbcMsgPickerState::default(), | ||
| selected_msg: None, | ||
| window: VecDeque::new(), | ||
| window_duration_seconds: 10.0, // Default 10 seconds | ||
| decimation_factor: 0, | ||
|
|
@@ -33,6 +59,26 @@ impl Scope { | |
| } | ||
| } | ||
|
|
||
| // Clears signal assignment and buffered data and goes back to picker ui so new signal can b chosen | ||
| fn reset_to_picker(&mut self) { | ||
| self.msg_id = None; | ||
| self.msg_name = None; | ||
| self.signal_name = None; | ||
| self.selected_msg = None; | ||
| self.window.clear(); | ||
| self.reference_time = None; | ||
| } | ||
|
|
||
| // Called once the user has picked a message and signal, assigns scopes target and resets plot buffer | ||
| fn assign_signal(&mut self, msg_id: u32, msg_name: String, signal_name: String) { | ||
| self.msg_id = Some(msg_id); | ||
| self.msg_name = Some(msg_name); | ||
| self.signal_name = Some(signal_name); | ||
| self.selected_msg = None; | ||
| self.window.clear(); | ||
| self.reference_time = None; | ||
| } | ||
|
|
||
| pub fn add_point(&mut self, timestamp: chrono::DateTime<chrono::Local>, value: f64) { | ||
| if self.is_paused { | ||
| return; | ||
|
|
@@ -85,11 +131,83 @@ impl Scope { | |
| } | ||
| } | ||
|
|
||
| pub fn show(&mut self, ui: &mut egui::Ui) -> egui_tiles::UiResponse { | ||
| ui.heading(format!( | ||
| "📊 {}: {} - {}", | ||
| self.title, self.msg_name, self.signal_name | ||
| )); | ||
| // Renders picker ui when there's no specific signal selected | ||
| fn show_picker(&mut self, ui: &mut egui::Ui, parser: &app::ParserInfo) { | ||
| if let Some(msg) = self | ||
| .msg_picker | ||
| .show(ui, &parser.parser, self.selected_msg.is_none()) | ||
| { | ||
| self.selected_msg = Some(msg); | ||
| } | ||
|
|
||
| let Some(selected_msg) = self.selected_msg.clone() else { | ||
| return; | ||
| }; | ||
|
|
||
| ui.separator(); | ||
| ui.label( | ||
| egui::RichText::new(format!( | ||
| "Selected Message: {} (0x{:03X}) — pick a signal:", | ||
| selected_msg.name, | ||
| util::can::can_dbc_to_u32_without_extid_flag(&selected_msg.id) | ||
| )) | ||
| .strong(), | ||
| ); | ||
|
|
||
| let msg_id = util::can::can_dbc_to_u32_without_extid_flag(&selected_msg.id); | ||
| for sig in &selected_msg.signals { | ||
| if ui.button(&sig.name).clicked() { | ||
| self.assign_signal(msg_id, selected_msg.name.clone(), sig.name.clone()); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if ui.button("← Back to message search").clicked() { | ||
| self.selected_msg = None; | ||
| } | ||
| } | ||
|
|
||
| pub fn show( | ||
| &mut self, | ||
| ui: &mut egui::Ui, | ||
| parser: Option<&app::ParserInfo>, | ||
| ) -> egui_tiles::UiResponse { | ||
| // Since no signal is assigned ask the user to pick a signal | ||
| if self.msg_id.is_none() || self.msg_name.is_none() || self.signal_name.is_none() { | ||
| ui.heading(format!("📊 {}: No signal selected", self.title)); | ||
| ui.separator(); | ||
|
|
||
| let Some(parser) = parser else { | ||
| no_dbc_placeholder(ui); | ||
| return egui_tiles::UiResponse::None; | ||
| }; | ||
|
|
||
| self.show_picker(ui, parser); | ||
| return egui_tiles::UiResponse::None; | ||
| } | ||
|
|
||
| let msg_name = self.msg_name.clone().unwrap(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid |
||
| let signal_name = self.signal_name.clone().unwrap(); | ||
|
|
||
| // Horizontal container (heading + new Change Signal button) | ||
| ui.horizontal(|ui| { | ||
| ui.heading(format!("📊 {}: {} - {}", self.title, msg_name, signal_name)); | ||
| ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { | ||
| if ui.button("🔀 Change Signal").clicked() { | ||
| self.reset_to_picker(); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| // When change signal is clicked go back to the picker ui | ||
| if self.msg_id.is_none() { | ||
|
Comment on lines
+189
to
+203
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Quality: Change Signal renders stale heading + picker in same frameAfter a signal is assigned, Was this helpful? React with 👍 / 👎 |
||
| let Some(parser) = parser else { | ||
| no_dbc_placeholder(ui); | ||
| return egui_tiles::UiResponse::None; | ||
| }; | ||
| self.show_picker(ui, parser); | ||
| return egui_tiles::UiResponse::None; | ||
| } | ||
|
|
||
| // Horizontal container | ||
| ui.horizontal(|ui| { | ||
|
|
@@ -142,7 +260,7 @@ impl Scope { | |
| .view_aspect(2.0) | ||
| .auto_bounds(egui::Vec2b::TRUE) | ||
| .x_axis_label("Time (seconds)") | ||
| .y_axis_label(&self.signal_name) | ||
| .y_axis_label(&signal_name) | ||
| .show(ui, |plot_ui| { | ||
| if self.window.is_empty() { | ||
| return; | ||
|
|
@@ -154,7 +272,7 @@ impl Scope { | |
| .map(|(time, value)| [*time, *value]) | ||
| .collect(); | ||
|
|
||
| let line = Line::new(&self.signal_name, points) | ||
| let line = Line::new(&signal_name, points) | ||
| .color(egui::Color32::from_rgb(100, 200, 100)) | ||
| .stroke(egui::Stroke::new( | ||
| 2.0, | ||
|
|
@@ -168,16 +286,21 @@ impl Scope { | |
| } | ||
|
|
||
| pub fn handle_can_message(&mut self, msg: &messages::MsgFromCan) { | ||
| // If no signal is assigned, there's nothing to plot | ||
| let (Some(target_msg_id), Some(signal_name)) = (self.msg_id, &self.signal_name) else { | ||
| return; | ||
| }; | ||
|
|
||
| if let messages::MsgFromCan::ParsedMessage(parsed_msg) = msg { | ||
| if parsed_msg.decoded.msg_id != self.msg_id { | ||
| if parsed_msg.decoded.msg_id != target_msg_id { | ||
| return; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. highkey parsed_msg should have |
||
| } | ||
|
|
||
| let Some(signal) = parsed_msg.decoded.signals.get(&self.signal_name) else { | ||
| let Some(signal) = parsed_msg.decoded.signals.get(signal_name) else { | ||
| return; | ||
| }; | ||
|
|
||
| self.add_point(parsed_msg.timestamp, signal.value.physical); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,16 @@ impl WidgetIds { | |
| } | ||
|
|
||
| pub fn next(&mut self, kind: widget_constructor::WidgetConstructor) -> usize { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not exactly your problem as it was left over from a change I made, but I think Something like this: pub fn next(&mut self, kind: widget_constructor::WidgetConstructor) -> usize { |
||
| let disc = std::mem::discriminant(&kind); | ||
| let disc = if matches!(kind, widget_constructor::WidgetConstructor::ScopeEmpty) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see why this solution works, but I don't think it may be the best for the longer term. I think something like this could make sense? Add a new "WidgetType" enum: #[derive(Eq, PartialEq, Hash, Clone, Copy)]
pub enum WidgetKind {
ViewerTable,
ViewerList,
Bootloader,
Scope,
LogParser,
SendUi,
BusLoad,
BatteryVoltage,
BatteryTemps,
GgPlot,
GpsPlot,
Dynamics,
Jitter,
Hil,
}Then WidgetConstructor can get a method like: fn kind(&self) -> WidgetKind {
match self {
Self::ViewerTable => WidgetKind::ViewerTable,
Self::ViewerList => WidgetKind::ViewerList,
Self::Bootloader => WidgetKind::Bootloader,
Self::Scope { .. } | Self::ScopeEmpty => WidgetKind::Scope,
Self::LogParser => WidgetKind::LogParser,
Self::SendUi => WidgetKind::SendUi,
Self::BusLoad => WidgetKind::BusLoad,
Self::BatteryVoltage => WidgetKind::BatteryVoltage,
Self::BatteryTemps => WidgetKind::BatteryTemps,
Self::GgPlot => WidgetKind::GgPlot,
Self::GpsPlot => WidgetKind::GpsPlot,
Self::Dynamics => WidgetKind::Dynamics,
Self::Jitter => WidgetKind::Jitter,
Self::Hil => WidgetKind::Hil,
}
}And widget ids looks a little more like? pub fn next(&mut self, constructor: &WidgetConstructor) -> usize {
let counter = self.counters.entry(constructor.kind()).or_insert(1);
let id = *counter;
*counter += 1;
id
}I think as a side effect of this, you can also clean up the |
||
| std::mem::discriminant(&widget_constructor::WidgetConstructor::Scope { | ||
| msg_id: 0, | ||
| msg_name: String::new(), | ||
| signal_name: String::new(), | ||
| }) | ||
| } else { | ||
| std::mem::discriminant(&kind) | ||
| }; | ||
|
|
||
| let counter = self.counters.entry(disc).or_insert(1); | ||
| let id = *counter; | ||
| *counter += 1; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small: I prefer the full qualified names (ex: add
uito theuse crate::options and then useui::dbc_msg_picker::DbcMsgPickerStateeverywhere), but if you like the shorter usages elsewhere, instead of super you can do: stilluito theuse crate::but then do something likeuse ui::dbc_msg_picker::{DbcMsgPickerState, self}that way we are not relient on the folder structure for theusestatement and the free floating functionno_dbc_placeholdergets at least 1 level of namespacing.