cooked
This commit is contained in:
Generated
+10
@@ -1564,6 +1564,15 @@ dependencies = [
|
|||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "egui_ltreeview"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6afc56194563064faa79d5acc18f2a4d32bd6dfacc949e1d5b5c8058a46e82cf"
|
||||||
|
dependencies = [
|
||||||
|
"egui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "egui_plot"
|
name = "egui_plot"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
@@ -5439,6 +5448,7 @@ dependencies = [
|
|||||||
"can-dbc",
|
"can-dbc",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
|
"egui_ltreeview",
|
||||||
"egui_plot",
|
"egui_plot",
|
||||||
"libsvld",
|
"libsvld",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ can-dbc = "9.1"
|
|||||||
eframe = "0.34"
|
eframe = "0.34"
|
||||||
egui = "0.34"
|
egui = "0.34"
|
||||||
egui_plot = "0.35"
|
egui_plot = "0.35"
|
||||||
|
egui_ltreeview = "0.7"
|
||||||
|
|||||||
+258
-37
@@ -2,8 +2,9 @@ use std::{fs, path::Path};
|
|||||||
|
|
||||||
use can_dbc::Dbc;
|
use can_dbc::Dbc;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui::Vec2b;
|
use egui::{ScrollArea, Vec2b};
|
||||||
use egui_plot::{Line, Plot, PlotPoint, PlotPoints};
|
use egui_ltreeview::{Action, TreeView};
|
||||||
|
use egui_plot::{AxisHints, Legend, Line, Plot, PlotPoint, PlotPoints, VPlacement};
|
||||||
use libsvld::DecodeResult;
|
use libsvld::DecodeResult;
|
||||||
|
|
||||||
fn main() {
|
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();
|
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 {
|
struct MyEguiApp {
|
||||||
data: DecodeResult,
|
data: DecodeResult,
|
||||||
points: Vec<PlotPoint>,
|
tree: Vec<MsgNode>,
|
||||||
buffer: Vec<PlotPoint>,
|
filter: String,
|
||||||
|
plotted: Vec<PlottedSignal>,
|
||||||
|
cursor_x: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MyEguiApp {
|
impl MyEguiApp {
|
||||||
@@ -29,31 +50,88 @@ impl MyEguiApp {
|
|||||||
|
|
||||||
let data = libsvld::decode(&dbc, asc_path);
|
let data = libsvld::decode(&dbc, asc_path);
|
||||||
|
|
||||||
let mut points = Vec::<PlotPoint>::new();
|
let tree = build_tree(&dbc, &data);
|
||||||
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)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
data,
|
data,
|
||||||
points,
|
tree,
|
||||||
buffer: Vec::new(),
|
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
|
// 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
|
// 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.
|
// (min/max swapped if needed) to keep the line monotonically increasing in x.
|
||||||
@@ -108,22 +186,165 @@ fn decimate(
|
|||||||
|
|
||||||
impl eframe::App for MyEguiApp {
|
impl eframe::App for MyEguiApp {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
egui::Panel::left(egui::Id::new("Signals"))
|
||||||
let full_min = self.points.first().map(|p| p.x).unwrap_or(0.0);
|
.resizable(true)
|
||||||
let full_max = self.points.last().map(|p| p.x).unwrap_or(1.0);
|
.show_inside(ui, |ui| {
|
||||||
let plot = Plot::new("APPS")
|
ui.add(
|
||||||
.include_x(full_min)
|
egui::TextEdit::singleline(&mut self.filter)
|
||||||
.include_x(full_max)
|
.hint_text("Filter messages/signals")
|
||||||
.allow_zoom(Vec2b{x: true, y: false});
|
.desired_width(f32::INFINITY),
|
||||||
plot.show(ui, |pui| {
|
);
|
||||||
let bounds = pui.plot_bounds();
|
let needle = self.filter.trim().to_lowercase();
|
||||||
let x_min = bounds.min()[0];
|
let mut toggles: Vec<u64> = Vec::new();
|
||||||
let x_max = bounds.max()[0];
|
ScrollArea::both()
|
||||||
let px_w = pui.transform().frame().width() as usize;
|
.scroll([
|
||||||
decimate(&self.points, x_min, x_max, px_w.max(1), &mut self.buffer);
|
true,
|
||||||
let ppoints = PlotPoints::Borrowed(&self.buffer);
|
true
|
||||||
pui.line(Line::new("AS_Torque_Vector", ppoints));
|
])
|
||||||
|
.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