something
This commit is contained in:
@@ -12,8 +12,12 @@ bench = false
|
||||
libsvld = { path = "../libsvld" }
|
||||
can-dbc = "9.1"
|
||||
|
||||
eframe = "0.34"
|
||||
eframe = {
|
||||
version = "0.34",
|
||||
features = ["persistence"]
|
||||
}
|
||||
egui = "0.34"
|
||||
egui_plot = "0.35"
|
||||
egui_ltreeview = "0.7"
|
||||
fuzzy-matcher = "0.3"
|
||||
rfd = "0.17"
|
||||
|
||||
+262
-26
@@ -3,7 +3,13 @@ mod plot_list;
|
||||
mod signal;
|
||||
mod tree;
|
||||
|
||||
use std::{fs, path::Path};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::PathBuf,
|
||||
sync::{mpsc::{channel, Receiver}, Arc},
|
||||
thread,
|
||||
};
|
||||
|
||||
use can_dbc::Dbc;
|
||||
use eframe::egui;
|
||||
@@ -20,50 +26,171 @@ use crate::tree::{build_tree, decode_signal_node_id, show_dbc_tree, MsgNode};
|
||||
fn main() {
|
||||
let native_options = eframe::NativeOptions::default();
|
||||
eframe::run_native(
|
||||
"My egui App",
|
||||
"svldplotter",
|
||||
native_options,
|
||||
Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))),
|
||||
Box::new(|cc| {
|
||||
let mut app = MyEguiApp::default();
|
||||
// Optional CLI quick-load: `svldplotter [DBC [ASC]]`. Both args
|
||||
// are optional and intended for dev iteration.
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if let Some(dbc) = args.get(1) {
|
||||
app.load_dbc(PathBuf::from(dbc));
|
||||
}
|
||||
if let Some(asc) = args.get(2) {
|
||||
app.asc_paths.push(PathBuf::from(asc));
|
||||
app.set_active_asc(app.asc_paths.len() - 1, Some(&cc.egui_ctx));
|
||||
}
|
||||
Ok(Box::new(app))
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Eq)]
|
||||
enum LeftTab {
|
||||
#[default]
|
||||
Signals,
|
||||
Files,
|
||||
}
|
||||
|
||||
struct DecodeJob {
|
||||
asc_index: usize,
|
||||
asc_path: PathBuf,
|
||||
receiver: Receiver<Arc<DecodeResult>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyEguiApp {
|
||||
data: DecodeResult,
|
||||
dbc: Option<Arc<Dbc>>,
|
||||
dbc_path: Option<PathBuf>,
|
||||
asc_paths: Vec<PathBuf>,
|
||||
active_asc: Option<usize>,
|
||||
data: Option<Arc<DecodeResult>>,
|
||||
decode_cache: HashMap<PathBuf, Arc<DecodeResult>>,
|
||||
pending: Option<DecodeJob>,
|
||||
tree: Vec<MsgNode>,
|
||||
filter: String,
|
||||
plots: Vec<PlotGroup>,
|
||||
next_plot_id: u64,
|
||||
cursor_x: Option<f64>,
|
||||
left_tab: LeftTab,
|
||||
}
|
||||
|
||||
impl MyEguiApp {
|
||||
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
|
||||
let dbc_text =
|
||||
fs::read_to_string(Path::new(&args[1])).expect("Unable to read input file");
|
||||
let dbc = Dbc::try_from(dbc_text.as_str()).expect("Failed to parse dbc file");
|
||||
|
||||
let asc_path = Path::new(&args[2]);
|
||||
let data = libsvld::decode(&dbc, asc_path);
|
||||
let tree = build_tree(&dbc, &data);
|
||||
|
||||
Self {
|
||||
data,
|
||||
tree,
|
||||
filter: String::new(),
|
||||
plots: Vec::new(),
|
||||
next_plot_id: 1,
|
||||
cursor_x: None,
|
||||
fn load_dbc(&mut self, path: PathBuf) {
|
||||
let Ok(text) = fs::read_to_string(&path) else {
|
||||
return;
|
||||
};
|
||||
let Ok(dbc) = Dbc::try_from(text.as_str()) else {
|
||||
return;
|
||||
};
|
||||
self.dbc = Some(Arc::new(dbc));
|
||||
self.dbc_path = Some(path);
|
||||
// DBC change invalidates the decode cache.
|
||||
self.decode_cache.clear();
|
||||
self.pending = None;
|
||||
self.data = None;
|
||||
self.tree.clear();
|
||||
if let Some(idx) = self.active_asc {
|
||||
self.start_or_use_cached(idx, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_asc(&mut self, path: PathBuf) {
|
||||
self.asc_paths.push(path);
|
||||
if self.active_asc.is_none() {
|
||||
self.set_active_asc(self.asc_paths.len() - 1, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch to a given ASC. Use the cached DecodeResult if present;
|
||||
/// otherwise spawn a background decode and show progress via the
|
||||
/// `pending` job.
|
||||
fn set_active_asc(&mut self, idx: usize, ctx: Option<&egui::Context>) {
|
||||
if idx >= self.asc_paths.len() {
|
||||
return;
|
||||
}
|
||||
self.active_asc = Some(idx);
|
||||
self.start_or_use_cached(idx, ctx);
|
||||
}
|
||||
|
||||
fn start_or_use_cached(&mut self, idx: usize, ctx: Option<&egui::Context>) {
|
||||
let Some(dbc) = self.dbc.clone() else {
|
||||
return;
|
||||
};
|
||||
let path = self.asc_paths[idx].clone();
|
||||
if self.decode_cache.contains_key(&path) {
|
||||
self.activate_decoded(idx);
|
||||
return;
|
||||
}
|
||||
let (tx, rx) = channel();
|
||||
let dbc_for_thread = dbc;
|
||||
let path_for_thread = path.clone();
|
||||
let ctx_for_thread = ctx.cloned();
|
||||
thread::spawn(move || {
|
||||
let result = libsvld::decode(&dbc_for_thread, &path_for_thread);
|
||||
let _ = tx.send(Arc::new(result));
|
||||
if let Some(ctx) = ctx_for_thread {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
});
|
||||
self.pending = Some(DecodeJob {
|
||||
asc_index: idx,
|
||||
asc_path: path,
|
||||
receiver: rx,
|
||||
});
|
||||
}
|
||||
|
||||
/// Pull a finished decode out of `pending` and install it.
|
||||
fn poll_pending(&mut self) {
|
||||
let Some(job) = self.pending.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Ok(result) = job.receiver.try_recv() else {
|
||||
return;
|
||||
};
|
||||
let asc_index = job.asc_index;
|
||||
let asc_path = job.asc_path.clone();
|
||||
self.pending = None;
|
||||
self.decode_cache.insert(asc_path, result);
|
||||
if self.active_asc == Some(asc_index) {
|
||||
self.activate_decoded(asc_index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Point `self.data`/`self.tree` at the cached result for `idx` and
|
||||
/// re-resolve every plotted signal against the new data.
|
||||
fn activate_decoded(&mut self, idx: usize) {
|
||||
let Some(dbc) = self.dbc.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let path = &self.asc_paths[idx];
|
||||
let Some(data) = self.decode_cache.get(path).cloned() else {
|
||||
return;
|
||||
};
|
||||
self.tree = build_tree(dbc, &data);
|
||||
// Re-resolve plotted signals so existing plots survive an ASC switch.
|
||||
for plot in &mut self.plots {
|
||||
for sig in &mut plot.signals {
|
||||
if let Some((msg_id, sig_idx)) = decode_signal_node_id(sig.id) {
|
||||
if let Some(new_sig) = compute_signal(&data, msg_id, sig_idx, sig.id) {
|
||||
*sig = new_sig;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.data = Some(data);
|
||||
}
|
||||
|
||||
fn handle_dbc_toggles(&mut self, toggles: Vec<u64>) {
|
||||
let Some(data) = self.data.clone() else {
|
||||
return;
|
||||
};
|
||||
for id in toggles {
|
||||
if contains_signal(&self.plots, id) {
|
||||
remove_signal(&mut self.plots, id);
|
||||
} else if let Some((msg_id, sig_idx)) = decode_signal_node_id(id) {
|
||||
if let Some(sig) = compute_signal(&self.data, msg_id, sig_idx, id) {
|
||||
if let Some(sig) = compute_signal(&data, msg_id, sig_idx, id) {
|
||||
add_signal_auto(&mut self.plots, &mut self.next_plot_id, sig);
|
||||
}
|
||||
}
|
||||
@@ -82,8 +209,64 @@ impl MyEguiApp {
|
||||
}
|
||||
}
|
||||
|
||||
enum FilesAction {
|
||||
LoadDbc(PathBuf),
|
||||
AddAsc(PathBuf),
|
||||
SetActive(usize),
|
||||
}
|
||||
|
||||
fn show_files_tab(
|
||||
ui: &mut egui::Ui,
|
||||
dbc_path: Option<&PathBuf>,
|
||||
asc_paths: &[PathBuf],
|
||||
active: Option<usize>,
|
||||
) -> Vec<FilesAction> {
|
||||
let mut actions = Vec::new();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Load DBC").clicked() {
|
||||
if let Some(p) = rfd::FileDialog::new()
|
||||
.add_filter("DBC files", &["dbc"])
|
||||
.pick_file()
|
||||
{
|
||||
actions.push(FilesAction::LoadDbc(p));
|
||||
}
|
||||
}
|
||||
let label = dbc_path
|
||||
.map(|p| p.file_name().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default())
|
||||
.unwrap_or_else(|| "(none)".to_string());
|
||||
ui.label(label);
|
||||
});
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Add ASC").clicked() {
|
||||
if let Some(p) = rfd::FileDialog::new()
|
||||
.add_filter("ASC files", &["asc"])
|
||||
.pick_file()
|
||||
{
|
||||
actions.push(FilesAction::AddAsc(p));
|
||||
}
|
||||
}
|
||||
ui.label(format!("{} file(s)", asc_paths.len()));
|
||||
});
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
for (i, path) in asc_paths.iter().enumerate() {
|
||||
let name = path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| path.display().to_string());
|
||||
if ui.selectable_label(active == Some(i), name).clicked() {
|
||||
actions.push(FilesAction::SetActive(i));
|
||||
}
|
||||
}
|
||||
});
|
||||
actions
|
||||
}
|
||||
|
||||
impl eframe::App for MyEguiApp {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.poll_pending();
|
||||
|
||||
let ctx = ui.ctx().clone();
|
||||
egui::Panel::left(egui::Id::new("LeftPanel"))
|
||||
.resizable(true)
|
||||
.show_inside(ui, |ui| {
|
||||
@@ -92,13 +275,66 @@ impl eframe::App for MyEguiApp {
|
||||
.default_size(220.0)
|
||||
.show_inside(ui, |ui| show_plot_list(ui, &self.plots))
|
||||
.inner;
|
||||
let toggles = egui::CentralPanel::default()
|
||||
.show_inside(ui, |ui| show_dbc_tree(ui, &mut self.filter, &self.tree))
|
||||
let (toggles, files_actions) = egui::CentralPanel::default()
|
||||
.show_inside(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(
|
||||
&mut self.left_tab,
|
||||
LeftTab::Signals,
|
||||
"Signals",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.left_tab,
|
||||
LeftTab::Files,
|
||||
"Files",
|
||||
);
|
||||
});
|
||||
ui.separator();
|
||||
match self.left_tab {
|
||||
LeftTab::Signals => (
|
||||
show_dbc_tree(ui, &mut self.filter, &self.tree),
|
||||
Vec::new(),
|
||||
),
|
||||
LeftTab::Files => (
|
||||
Vec::new(),
|
||||
show_files_tab(
|
||||
ui,
|
||||
self.dbc_path.as_ref(),
|
||||
&self.asc_paths,
|
||||
self.active_asc,
|
||||
),
|
||||
),
|
||||
}
|
||||
})
|
||||
.inner;
|
||||
self.handle_dbc_toggles(toggles);
|
||||
self.handle_plot_actions(plot_actions);
|
||||
for action in files_actions {
|
||||
match action {
|
||||
FilesAction::LoadDbc(p) => self.load_dbc(p),
|
||||
FilesAction::AddAsc(p) => self.add_asc(p),
|
||||
FilesAction::SetActive(i) => self.set_active_asc(i, Some(&ctx)),
|
||||
}
|
||||
}
|
||||
});
|
||||
show_plot_panel(ui, &self.plots, &mut self.cursor_x);
|
||||
|
||||
if let Some(job) = self.pending.as_ref() {
|
||||
let name = job
|
||||
.asc_path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| job.asc_path.display().to_string());
|
||||
egui::Modal::new(egui::Id::new("DecodingModal")).show(&ctx, |ui| {
|
||||
ui.set_min_width(280.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Decoding...");
|
||||
ui.add_space(6.0);
|
||||
ui.label(name);
|
||||
ui.add_space(6.0);
|
||||
ui.spinner();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user