cooked
This commit is contained in:
Generated
+10
@@ -1564,6 +1564,15 @@ dependencies = [
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egui_ltreeview"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6afc56194563064faa79d5acc18f2a4d32bd6dfacc949e1d5b5c8058a46e82cf"
|
||||
dependencies = [
|
||||
"egui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egui_plot"
|
||||
version = "0.35.0"
|
||||
@@ -5439,6 +5448,7 @@ dependencies = [
|
||||
"can-dbc",
|
||||
"eframe",
|
||||
"egui",
|
||||
"egui_ltreeview",
|
||||
"egui_plot",
|
||||
"libsvld",
|
||||
]
|
||||
|
||||
@@ -15,3 +15,4 @@ can-dbc = "9.1"
|
||||
eframe = "0.34"
|
||||
egui = "0.34"
|
||||
egui_plot = "0.35"
|
||||
egui_ltreeview = "0.7"
|
||||
|
||||
+258
-37
@@ -2,8 +2,9 @@ use std::{fs, path::Path};
|
||||
|
||||
use can_dbc::Dbc;
|
||||
use eframe::egui;
|
||||
use egui::Vec2b;
|
||||
use egui_plot::{Line, Plot, PlotPoint, PlotPoints};
|
||||
use egui::{ScrollArea, Vec2b};
|
||||
use egui_ltreeview::{Action, TreeView};
|
||||
use egui_plot::{AxisHints, Legend, Line, Plot, PlotPoint, PlotPoints, VPlacement};
|
||||
use libsvld::DecodeResult;
|
||||
|
||||
fn main() {
|
||||
@@ -11,10 +12,30 @@ fn main() {
|
||||
eframe::run_native("My egui App", native_options, Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc))))).unwrap();
|
||||
}
|
||||
|
||||
struct SigNode {
|
||||
id: u64,
|
||||
name: String,
|
||||
}
|
||||
|
||||
struct MsgNode {
|
||||
id: u64,
|
||||
label: String,
|
||||
signals: Vec<SigNode>,
|
||||
}
|
||||
|
||||
struct PlottedSignal {
|
||||
id: u64,
|
||||
label: String,
|
||||
unit: String,
|
||||
points: Vec<PlotPoint>,
|
||||
}
|
||||
|
||||
struct MyEguiApp {
|
||||
data: DecodeResult,
|
||||
points: Vec<PlotPoint>,
|
||||
buffer: Vec<PlotPoint>,
|
||||
tree: Vec<MsgNode>,
|
||||
filter: String,
|
||||
plotted: Vec<PlottedSignal>,
|
||||
cursor_x: Option<f64>,
|
||||
}
|
||||
|
||||
impl MyEguiApp {
|
||||
@@ -29,31 +50,88 @@ impl MyEguiApp {
|
||||
|
||||
let data = libsvld::decode(&dbc, asc_path);
|
||||
|
||||
let mut points = Vec::<PlotPoint>::new();
|
||||
let apps_sig = data
|
||||
.metadata
|
||||
.get(&48)
|
||||
.unwrap()
|
||||
.signals
|
||||
.iter()
|
||||
.find(|x| x.name == "AS_Torque_Vector")
|
||||
.unwrap();
|
||||
for frame in data.data.get(&48).unwrap() {
|
||||
let bytes = u64::from_le_bytes(frame.data);
|
||||
points.push(PlotPoint {
|
||||
x: frame.timestamp,
|
||||
y: libsvld::motec::extract_signal(bytes, &apps_sig.signal)
|
||||
});
|
||||
}
|
||||
let tree = build_tree(&dbc, &data);
|
||||
|
||||
Self {
|
||||
data,
|
||||
points,
|
||||
buffer: Vec::new(),
|
||||
tree,
|
||||
filter: String::new(),
|
||||
plotted: Vec::new(),
|
||||
cursor_x: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_signal(
|
||||
data: &DecodeResult,
|
||||
msg_id: u32,
|
||||
sig_idx: usize,
|
||||
id: u64,
|
||||
) -> Option<PlottedSignal> {
|
||||
let meta = data.metadata.get(&msg_id)?;
|
||||
let sig = meta.signals.get(sig_idx)?;
|
||||
let frames = data.data.get(&msg_id)?;
|
||||
let mut points = Vec::with_capacity(frames.len());
|
||||
for frame in frames {
|
||||
let bytes = u64::from_le_bytes(frame.data);
|
||||
points.push(PlotPoint {
|
||||
x: frame.timestamp,
|
||||
y: libsvld::motec::extract_signal(bytes, &sig.signal),
|
||||
});
|
||||
}
|
||||
Some(PlottedSignal {
|
||||
id,
|
||||
label: sig.name.clone(),
|
||||
unit: sig.unit.clone(),
|
||||
points,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_tree(dbc: &Dbc, data: &DecodeResult) -> Vec<MsgNode> {
|
||||
let mut msgs: Vec<&libsvld::motec::MessageMetadata> = data.metadata.values().collect();
|
||||
msgs.sort_by_key(|m| m.id);
|
||||
msgs.into_iter()
|
||||
.map(|m| {
|
||||
let name = dbc
|
||||
.messages
|
||||
.iter()
|
||||
.find(|x| x.id.raw() == m.id)
|
||||
.map(|x| x.name.as_str())
|
||||
.unwrap_or("?");
|
||||
let label = format!("0x{:X} {}", m.id, name);
|
||||
let signals = m
|
||||
.signals
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, s)| SigNode {
|
||||
id: (1u64 << 63) | ((m.id as u64) << 16) | (i as u64),
|
||||
name: s.name.clone(),
|
||||
})
|
||||
.collect();
|
||||
MsgNode {
|
||||
id: m.id as u64,
|
||||
label,
|
||||
signals,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn nearest_y(points: &[PlotPoint], x: f64) -> Option<f64> {
|
||||
if points.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let i = points.partition_point(|p| p.x < x);
|
||||
let cand = [
|
||||
i.checked_sub(1).map(|j| &points[j]),
|
||||
points.get(i),
|
||||
];
|
||||
cand.into_iter()
|
||||
.flatten()
|
||||
.min_by(|a, b| (a.x - x).abs().total_cmp(&(b.x - x).abs()))
|
||||
.map(|p| p.y)
|
||||
}
|
||||
|
||||
// Min/max bucketing: for each horizontal pixel column in [x_min, x_max], emit
|
||||
// the min-y and max-y point so spikes survive decimation. Pushed in x order
|
||||
// (min/max swapped if needed) to keep the line monotonically increasing in x.
|
||||
@@ -108,22 +186,165 @@ fn decimate(
|
||||
|
||||
impl eframe::App for MyEguiApp {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
let full_min = self.points.first().map(|p| p.x).unwrap_or(0.0);
|
||||
let full_max = self.points.last().map(|p| p.x).unwrap_or(1.0);
|
||||
let plot = Plot::new("APPS")
|
||||
.include_x(full_min)
|
||||
.include_x(full_max)
|
||||
.allow_zoom(Vec2b{x: true, y: false});
|
||||
plot.show(ui, |pui| {
|
||||
let bounds = pui.plot_bounds();
|
||||
let x_min = bounds.min()[0];
|
||||
let x_max = bounds.max()[0];
|
||||
let px_w = pui.transform().frame().width() as usize;
|
||||
decimate(&self.points, x_min, x_max, px_w.max(1), &mut self.buffer);
|
||||
let ppoints = PlotPoints::Borrowed(&self.buffer);
|
||||
pui.line(Line::new("AS_Torque_Vector", ppoints));
|
||||
egui::Panel::left(egui::Id::new("Signals"))
|
||||
.resizable(true)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.filter)
|
||||
.hint_text("Filter messages/signals")
|
||||
.desired_width(f32::INFINITY),
|
||||
);
|
||||
let needle = self.filter.trim().to_lowercase();
|
||||
let mut toggles: Vec<u64> = Vec::new();
|
||||
ScrollArea::both()
|
||||
.scroll([
|
||||
true,
|
||||
true
|
||||
])
|
||||
.scroll_bar_visibility(
|
||||
egui::scroll_area::ScrollBarVisibility::VisibleWhenNeeded
|
||||
)
|
||||
.show(ui, |ui| {
|
||||
let (_resp, actions) = TreeView::<u64>::new(
|
||||
ui.make_persistent_id("SignalsTree"),
|
||||
)
|
||||
.show(ui, |builder| {
|
||||
for msg in &self.tree {
|
||||
let msg_hit = needle.is_empty()
|
||||
|| msg.label.to_lowercase().contains(&needle);
|
||||
let matching_sigs: Vec<&SigNode> = if msg_hit {
|
||||
msg.signals.iter().collect()
|
||||
} else {
|
||||
msg.signals
|
||||
.iter()
|
||||
.filter(|s| s.name.to_lowercase().contains(&needle))
|
||||
.collect()
|
||||
};
|
||||
if !msg_hit && matching_sigs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
builder.dir(msg.id, msg.label.as_str());
|
||||
for sig in matching_sigs {
|
||||
builder.leaf(sig.id, sig.name.as_str());
|
||||
}
|
||||
builder.close_dir();
|
||||
}
|
||||
});
|
||||
for action in actions {
|
||||
if let Action::SetSelected(ids) = action {
|
||||
for id in ids {
|
||||
if id & (1u64 << 63) != 0 {
|
||||
toggles.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
for id in toggles {
|
||||
if let Some(pos) = self.plotted.iter().position(|p| p.id == id) {
|
||||
self.plotted.remove(pos);
|
||||
} else {
|
||||
let msg_id = ((id >> 16) & 0xFFFF_FFFF) as u32;
|
||||
let sig_idx = (id & 0xFFFF) as usize;
|
||||
if let Some(sig) = compute_signal(&self.data, msg_id, sig_idx, id) {
|
||||
self.plotted.push(sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
if self.plotted.is_empty() {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label("Click a signal in the tree to plot it.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Group signals by unit, preserving the order each unit first appeared.
|
||||
let mut groups: Vec<(String, Vec<usize>)> = Vec::new();
|
||||
for (i, sig) in self.plotted.iter().enumerate() {
|
||||
if let Some(g) = groups.iter_mut().find(|(u, _)| u == &sig.unit) {
|
||||
g.1.push(i);
|
||||
} else {
|
||||
groups.push((sig.unit.clone(), vec![i]));
|
||||
}
|
||||
}
|
||||
|
||||
let full_min = self
|
||||
.plotted
|
||||
.iter()
|
||||
.filter_map(|s| s.points.first().map(|p| p.x))
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
let full_max = self
|
||||
.plotted
|
||||
.iter()
|
||||
.filter_map(|s| s.points.last().map(|p| p.x))
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
|
||||
let link_group = ui.id().with("plot-link");
|
||||
let n = groups.len();
|
||||
// ui.spacing_mut().item_spacing.y = 0.0;
|
||||
let h = (ui.available_height() / n as f32).max(80.0);
|
||||
|
||||
let cursor_x = self.cursor_x;
|
||||
let mut new_cursor_x: Option<f64> = None;
|
||||
|
||||
for (gi, (unit, idxs)) in groups.iter().enumerate() {
|
||||
let is_first = gi == 0;
|
||||
let is_last = gi + 1 == n;
|
||||
|
||||
let mut x_axes: Vec<AxisHints> = Vec::new();
|
||||
if is_first {
|
||||
x_axes.push(AxisHints::new_x().placement(VPlacement::Top));
|
||||
}
|
||||
if is_last {
|
||||
x_axes.push(AxisHints::new_x().placement(VPlacement::Bottom));
|
||||
}
|
||||
|
||||
let title = if unit.is_empty() { "(no unit)" } else { unit.as_str() };
|
||||
|
||||
let plot = Plot::new(("plot", title))
|
||||
.height(h)
|
||||
.include_x(full_min)
|
||||
.include_x(full_max)
|
||||
.allow_zoom(Vec2b { x: true, y: true })
|
||||
.link_axis(link_group, Vec2b { x: true, y: false })
|
||||
.link_cursor(link_group, Vec2b { x: true, y: false })
|
||||
.legend(Legend::default())
|
||||
.custom_x_axes(x_axes)
|
||||
.custom_y_axes(vec![AxisHints::new_y().min_thickness(48.0)]);
|
||||
|
||||
let plotted = &self.plotted;
|
||||
let plot_resp = plot.show(ui, |pui| {
|
||||
let bounds = pui.plot_bounds();
|
||||
let x_min = bounds.min()[0];
|
||||
let x_max = bounds.max()[0];
|
||||
let px_w = pui.transform().frame().width() as usize;
|
||||
let mut buf = Vec::new();
|
||||
for &i in idxs {
|
||||
let sig = &plotted[i];
|
||||
decimate(&sig.points, x_min, x_max, px_w.max(1), &mut buf);
|
||||
let ppoints = PlotPoints::Owned(std::mem::take(&mut buf));
|
||||
let unit_s = if sig.unit.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" [{}]", sig.unit)
|
||||
};
|
||||
let name = match cursor_x.and_then(|x| nearest_y(&sig.points, x)) {
|
||||
Some(v) => format!("{}: {:.4}{}", sig.label, v, unit_s),
|
||||
None => format!("{}{}", sig.label, unit_s),
|
||||
};
|
||||
pui.line(Line::new(name, ppoints));
|
||||
}
|
||||
|
||||
pui.pointer_coordinate().map(|c| c.x)
|
||||
});
|
||||
if let Some(x) = plot_resp.inner {
|
||||
new_cursor_x = Some(x);
|
||||
}
|
||||
}
|
||||
|
||||
self.cursor_x = new_cursor_x;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user