GUI Show post decoding info

This commit is contained in:
2026-05-23 17:04:18 +02:00
parent 6d82ad9c02
commit e9c505b625
4 changed files with 388 additions and 48 deletions
Generated
+1
View File
@@ -4768,6 +4768,7 @@ name = "svldgui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"can-dbc 9.1.0", "can-dbc 9.1.0",
"chrono",
"libsvld", "libsvld",
"rfd", "rfd",
"slint", "slint",
+1
View File
@@ -13,6 +13,7 @@ libsvld = { path = "../libsvld" }
can-dbc = "9.1.0" can-dbc = "9.1.0"
slint = "1.16.1" slint = "1.16.1"
rfd = "0.17.2" rfd = "0.17.2"
chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
[build-dependencies] [build-dependencies]
slint-build = "1.16.1" slint-build = "1.16.1"
+234 -11
View File
@@ -9,7 +9,8 @@ use std::{
}; };
use can_dbc::Dbc; use can_dbc::Dbc;
use slint::ComponentHandle; use chrono::Local;
use slint::{ComponentHandle, ModelRc, StandardListViewItem, VecModel};
slint::include_modules!(); slint::include_modules!();
@@ -20,10 +21,213 @@ struct State {
output_dir: Option<PathBuf>, output_dir: Option<PathBuf>,
} }
struct FileResult {
file_name: String,
date: String,
unknown_count: i32,
error_count: i32,
unknown_ids: String,
error_rows: Vec<(f64, u8, u8)>,
runtime_stats: String,
}
struct Run {
label: String,
files: Vec<FileResult>,
}
#[derive(Default)]
struct Results {
runs: Vec<Run>,
window: Option<ResultsWindow>,
run_counter: usize,
}
thread_local! {
static RESULTS: RefCell<Results> = RefCell::new(Results::default());
}
fn build_file_result(
path: &PathBuf,
info: libsvld::DecodeProcessInfo,
) -> FileResult {
let file_name = path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string());
let date = info
.date
.and_utc()
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let unknown_count = info.unknown_ids.len() as i32;
let error_count = info.error_frames.len() as i32;
let mut unknown_sorted: Vec<u32> = info.unknown_ids.iter().copied().collect();
unknown_sorted.sort();
let unknown_ids = if unknown_sorted.is_empty() {
"(none)".to_string()
} else {
unknown_sorted
.iter()
.map(|id| format!("0x{:X}", id))
.collect::<Vec<_>>()
.join(", ")
};
let error_rows: Vec<(f64, u8, u8)> = info
.error_frames
.iter()
.map(|f| (f.timestamp, f.channel, f.data))
.collect();
let runtime_stats = format!(
"threads: {} · page size: {} · pages/chunk: {} · chunk size: {}",
info.thread_count, info.page_size, info.pages_per_chunk, info.chunk_size
);
FileResult {
file_name,
date,
unknown_count,
error_count,
unknown_ids,
error_rows,
runtime_stats,
}
}
fn rebuild_entries(results: &Results) -> Vec<ResultEntry> {
let last_idx = results.runs.len().saturating_sub(1);
let mut entries = Vec::new();
for (run_idx, run) in results.runs.iter().enumerate() {
let is_new = run_idx == last_idx;
entries.push(ResultEntry {
is_header: true,
is_new,
label: run.label.clone().into(),
});
for f in &run.files {
entries.push(ResultEntry {
is_header: false,
is_new: false,
label: f.file_name.clone().into(),
});
}
}
entries
}
fn find_file(results: &Results, target: usize) -> Option<&FileResult> {
let mut i = 0;
for run in &results.runs {
if i == target {
return None;
}
i += 1;
for f in &run.files {
if i == target {
return Some(f);
}
i += 1;
}
}
None
}
fn apply_selection(window: &ResultsWindow, results: &Results, idx: i32) {
if idx < 0 {
window.set_selected_index(-1);
return;
}
let Some(f) = find_file(results, idx as usize) else {
return;
};
window.set_selected_index(idx);
window.set_sel_label(f.file_name.clone().into());
window.set_sel_date(f.date.clone().into());
window.set_sel_unknown_count(f.unknown_count);
window.set_sel_error_count(f.error_count);
window.set_sel_unknown_ids(f.unknown_ids.clone().into());
window.set_sel_runtime_stats(f.runtime_stats.clone().into());
let rows: Vec<ModelRc<StandardListViewItem>> = f
.error_rows
.iter()
.map(|(t, ch, d)| {
let cells: Vec<StandardListViewItem> = vec![
slint::format!("{}", t).into(),
slint::format!("{}", ch).into(),
slint::format!("0b{:08b}", d).into(),
];
ModelRc::new(VecModel::from(cells))
})
.collect();
window.set_sel_error_rows(ModelRc::new(VecModel::from(rows)));
}
fn show_results(results: &mut Results, new_run: Run) {
results.runs.push(new_run);
let entries = rebuild_entries(results);
let new_first_file_idx = entries
.iter()
.enumerate()
.rev()
.find(|(_, e)| !e.is_header)
.map(|(i, _)| i as i32)
.unwrap_or(-1);
let new_first_file_idx = {
// We want the first non-header entry of the last run, not the very last.
let last_header_idx = entries
.iter()
.enumerate()
.rev()
.find(|(_, e)| e.is_header)
.map(|(i, _)| i)
.unwrap_or(0);
entries
.iter()
.enumerate()
.skip(last_header_idx + 1)
.find(|(_, e)| !e.is_header)
.map(|(i, _)| i as i32)
.unwrap_or(new_first_file_idx)
};
if results.window.is_none() {
let win = ResultsWindow::new().unwrap();
let win_weak = win.as_weak();
win.on_select(move |i| {
if let Some(w) = win_weak.upgrade() {
RESULTS.with(|cell| {
apply_selection(&w, &cell.borrow(), i);
});
}
});
results.window = Some(win);
}
let win = results.window.as_ref().unwrap();
let model = ModelRc::new(VecModel::from(entries));
win.set_entries(model);
apply_selection(win, results, new_first_file_idx);
win.show().unwrap();
}
fn main() { fn main() {
let window = MainWindow::new().unwrap(); let window = MainWindow::new().unwrap();
let state = Rc::new(RefCell::new(State::default())); let state = Rc::new(RefCell::new(State::default()));
window.on_open_results(|| {
RESULTS.with(|cell| {
if let Some(w) = cell.borrow().window.as_ref() {
w.show().ok();
}
});
});
window.on_browse_dbc({ window.on_browse_dbc({
let window_weak = window.as_weak(); let window_weak = window.as_weak();
let state = state.clone(); let state = state.clone();
@@ -105,29 +309,48 @@ fn main() {
let window_weak = window_weak.clone(); let window_weak = window_weak.clone();
thread::spawn(move || { thread::spawn(move || {
let result = (|| -> Result<(), String> { let result = (|| -> Result<Vec<FileResult>, String> {
let data = fs::read_to_string(&dbc_path) let data = fs::read_to_string(&dbc_path)
.map_err(|e| format!("Failed to read DBC: {}", e))?; .map_err(|e| format!("Failed to read DBC: {}", e))?;
let dbc = Dbc::try_from(data.as_str()) let dbc = Dbc::try_from(data.as_str())
.map_err(|e| format!("Failed to parse DBC: {:?}", e))?; .map_err(|e| format!("Failed to parse DBC: {:?}", e))?;
let mut out = Vec::with_capacity(asc_paths.len());
for asc in &asc_paths { for asc in &asc_paths {
libsvld::decode(&dbc, asc, &output_dir); let info = libsvld::decode(&dbc, asc, &output_dir);
out.push(build_file_result(asc, info));
} }
Ok(()) Ok(out)
})(); })();
let message = match result {
Ok(()) => "Done.".to_string(),
Err(e) => format!("Error: {}", e),
};
slint::invoke_from_event_loop(move || { slint::invoke_from_event_loop(move || {
if let Some(w) = window_weak.upgrade() { if let Some(w) = window_weak.upgrade() {
w.set_busy(false); w.set_busy(false);
w.set_status(message.into()); }
match result {
Ok(files) => {
if let Some(w) = window_weak.upgrade() {
w.set_status("Done.".into());
w.set_has_results(true);
}
RESULTS.with(|cell| {
let mut r = cell.borrow_mut();
r.run_counter += 1;
let label = format!(
"Run {}{}",
r.run_counter,
Local::now().format("%H:%M:%S")
);
show_results(&mut r, Run { label, files });
});
}
Err(e) => {
if let Some(w) = window_weak.upgrade() {
w.set_status(format!("Error: {}", e).into());
}
}
} }
}) })
.ok(); .ok();
}); });
} }
}); });
+152 -37
View File
@@ -1,4 +1,10 @@
import { Button, LineEdit, CheckBox, Spinner, GridBox, VerticalBox, HorizontalBox } from "std-widgets.slint"; import { Button, LineEdit, CheckBox, Spinner, GridBox, VerticalBox, HorizontalBox, ListView, ScrollView, StandardTableView } from "std-widgets.slint";
export struct ResultEntry {
is-header: bool,
is-new: bool,
label: string,
}
export component MainWindow inherits Window { export component MainWindow inherits Window {
title: "Sane Vector Log Decoder"; title: "Sane Vector Log Decoder";
@@ -12,11 +18,13 @@ export component MainWindow inherits Window {
in-out property <bool> laps; in-out property <bool> laps;
in-out property <bool> busy; in-out property <bool> busy;
in-out property <string> status; in-out property <string> status;
in-out property <bool> has-results;
callback browse-dbc(); callback browse-dbc();
callback browse-asc(); callback browse-asc();
callback browse-output(); callback browse-output();
callback decode(); callback decode();
callback open-results();
VerticalBox { VerticalBox {
padding: 12px; padding: 12px;
@@ -26,48 +34,21 @@ export component MainWindow inherits Window {
spacing: 8px; spacing: 8px;
Row { Row {
Text { Text { text: "DBC file:"; vertical-alignment: center; }
text: "DBC file:"; LineEdit { read-only: true; text: root.dbc-path; }
vertical-alignment: center; Button { text: "Browse..."; clicked => { root.browse-dbc(); } }
}
LineEdit {
read-only: true;
text: root.dbc-path;
}
Button {
text: "Browse...";
clicked => { root.browse-dbc(); }
}
} }
Row { Row {
Text { Text { text: "ASC file(s):"; vertical-alignment: center; }
text: "ASC file(s):"; LineEdit { read-only: true; text: root.asc-label; }
vertical-alignment: center; Button { text: "Browse..."; clicked => { root.browse-asc(); } }
}
LineEdit {
read-only: true;
text: root.asc-label;
}
Button {
text: "Browse...";
clicked => { root.browse-asc(); }
}
} }
Row { Row {
Text { Text { text: "Output dir:"; vertical-alignment: center; }
text: "Output dir:"; LineEdit { read-only: true; text: root.output-dir; }
vertical-alignment: center; Button { text: "Browse..."; clicked => { root.browse-output(); } }
}
LineEdit {
read-only: true;
text: root.output-dir;
}
Button {
text: "Browse...";
clicked => { root.browse-output(); }
}
} }
} }
@@ -95,6 +76,11 @@ export component MainWindow inherits Window {
overflow: elide; overflow: elide;
} }
if root.has-results: Button {
text: "Show results";
clicked => { root.open-results(); }
}
Button { Button {
text: "Decode!"; text: "Decode!";
enabled: !root.busy enabled: !root.busy
@@ -106,3 +92,132 @@ export component MainWindow inherits Window {
} }
} }
} }
export component ResultsWindow inherits Window {
title: "Decode results";
preferred-width: 900px;
preferred-height: 520px;
in property <[ResultEntry]> entries;
in-out property <int> selected-index: -1;
in property <string> sel-label;
in property <string> sel-date;
in property <int> sel-unknown-count;
in property <int> sel-error-count;
in property <string> sel-unknown-ids;
in property <[[StandardListViewItem]]> sel-error-rows;
in property <string> sel-runtime-stats;
callback select(int);
HorizontalBox {
padding: 8px;
spacing: 8px;
Rectangle {
width: 280px;
border-width: 1px;
border-color: #cccccc;
ListView {
for entry[i] in root.entries: Rectangle {
height: 26px;
background: i == root.selected-index ? #3a78d8 : transparent;
TouchArea {
clicked => { root.select(i); }
HorizontalLayout {
padding-left: entry.is-header ? 6px : 24px;
padding-right: 6px;
spacing: 6px;
Text {
text: entry.label;
font-weight: entry.is-header ? 700 : 400;
vertical-alignment: center;
horizontal-stretch: 1;
overflow: elide;
}
if entry.is-header && entry.is-new: Rectangle {
background: #2ecc71;
border-radius: 3px;
width: 38px;
height: 16px;
y: (parent.height - self.height) / 2;
Text {
text: "NEW";
color: white;
font-size: 10px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
}
}
}
}
VerticalBox {
padding: 0px;
spacing: 6px;
horizontal-stretch: 1;
Text {
text: root.selected-index >= 0 ? root.sel-label : "Select an ASC file on the left.";
font-size: 14px;
font-weight: 700;
}
if root.selected-index >= 0: Text {
text: "Date: " + root.sel-date;
}
if root.selected-index >= 0: Text {
text: "Unknown IDs: " + root.sel-unknown-count
+ " Error frames: " + root.sel-error-count;
}
if root.selected-index >= 0: Text {
text: "Unknown CAN IDs";
font-weight: 700;
}
if root.selected-index >= 0: Rectangle {
border-width: 1px;
border-color: #cccccc;
height: 90px;
ScrollView {
Text {
text: root.sel-unknown-ids;
wrap: word-wrap;
}
}
}
if root.selected-index >= 0: Text {
text: "Error frames";
font-weight: 700;
}
if root.selected-index >= 0: StandardTableView {
vertical-stretch: 1;
columns: [
{ title: "Timestamp" },
{ title: "Channel" },
{ title: "Data (binary)" },
];
rows: root.sel-error-rows;
}
if root.selected-index >= 0: Text {
text: "Runtime stats";
font-weight: 700;
}
if root.selected-index >= 0: Text {
text: root.sel-runtime-stats;
}
}
}
}