This commit is contained in:
2026-05-24 12:11:12 +02:00
parent d1bfb7702d
commit 738fb727d2
3 changed files with 269 additions and 37 deletions
Generated
+10
View File
@@ -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",
]
+1
View File
@@ -15,3 +15,4 @@ can-dbc = "9.1"
eframe = "0.34"
egui = "0.34"
egui_plot = "0.35"
egui_ltreeview = "0.7"
+250 -29
View File
@@ -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,29 +50,86 @@ 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,
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,
buffer: Vec::new(),
})
}
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
@@ -108,22 +186,165 @@ fn decimate(
impl eframe::App for MyEguiApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
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| {
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")
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: false});
plot.show(ui, |pui| {
.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;
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));
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;
});
}
}