GUI Show post decoding info
This commit is contained in:
Generated
+1
@@ -4768,6 +4768,7 @@ name = "svldgui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"can-dbc 9.1.0",
|
||||
"chrono",
|
||||
"libsvld",
|
||||
"rfd",
|
||||
"slint",
|
||||
|
||||
@@ -13,6 +13,7 @@ libsvld = { path = "../libsvld" }
|
||||
can-dbc = "9.1.0"
|
||||
slint = "1.16.1"
|
||||
rfd = "0.17.2"
|
||||
chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
|
||||
|
||||
[build-dependencies]
|
||||
slint-build = "1.16.1"
|
||||
|
||||
+233
-10
@@ -9,7 +9,8 @@ use std::{
|
||||
};
|
||||
|
||||
use can_dbc::Dbc;
|
||||
use slint::ComponentHandle;
|
||||
use chrono::Local;
|
||||
use slint::{ComponentHandle, ModelRc, StandardListViewItem, VecModel};
|
||||
|
||||
slint::include_modules!();
|
||||
|
||||
@@ -20,10 +21,213 @@ struct State {
|
||||
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() {
|
||||
let window = MainWindow::new().unwrap();
|
||||
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({
|
||||
let window_weak = window.as_weak();
|
||||
let state = state.clone();
|
||||
@@ -105,26 +309,45 @@ fn main() {
|
||||
|
||||
let window_weak = window_weak.clone();
|
||||
thread::spawn(move || {
|
||||
let result = (|| -> Result<(), String> {
|
||||
let result = (|| -> Result<Vec<FileResult>, String> {
|
||||
let data = fs::read_to_string(&dbc_path)
|
||||
.map_err(|e| format!("Failed to read DBC: {}", e))?;
|
||||
let dbc = Dbc::try_from(data.as_str())
|
||||
.map_err(|e| format!("Failed to parse DBC: {:?}", e))?;
|
||||
let mut out = Vec::with_capacity(asc_paths.len());
|
||||
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 || {
|
||||
if let Some(w) = window_weak.upgrade() {
|
||||
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();
|
||||
|
||||
+152
-37
@@ -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 {
|
||||
title: "Sane Vector Log Decoder";
|
||||
@@ -12,11 +18,13 @@ export component MainWindow inherits Window {
|
||||
in-out property <bool> laps;
|
||||
in-out property <bool> busy;
|
||||
in-out property <string> status;
|
||||
in-out property <bool> has-results;
|
||||
|
||||
callback browse-dbc();
|
||||
callback browse-asc();
|
||||
callback browse-output();
|
||||
callback decode();
|
||||
callback open-results();
|
||||
|
||||
VerticalBox {
|
||||
padding: 12px;
|
||||
@@ -26,48 +34,21 @@ export component MainWindow inherits Window {
|
||||
spacing: 8px;
|
||||
|
||||
Row {
|
||||
Text {
|
||||
text: "DBC file:";
|
||||
vertical-alignment: center;
|
||||
}
|
||||
LineEdit {
|
||||
read-only: true;
|
||||
text: root.dbc-path;
|
||||
}
|
||||
Button {
|
||||
text: "Browse...";
|
||||
clicked => { root.browse-dbc(); }
|
||||
}
|
||||
Text { text: "DBC file:"; vertical-alignment: center; }
|
||||
LineEdit { read-only: true; text: root.dbc-path; }
|
||||
Button { text: "Browse..."; clicked => { root.browse-dbc(); } }
|
||||
}
|
||||
|
||||
Row {
|
||||
Text {
|
||||
text: "ASC file(s):";
|
||||
vertical-alignment: center;
|
||||
}
|
||||
LineEdit {
|
||||
read-only: true;
|
||||
text: root.asc-label;
|
||||
}
|
||||
Button {
|
||||
text: "Browse...";
|
||||
clicked => { root.browse-asc(); }
|
||||
}
|
||||
Text { text: "ASC file(s):"; vertical-alignment: center; }
|
||||
LineEdit { read-only: true; text: root.asc-label; }
|
||||
Button { text: "Browse..."; clicked => { root.browse-asc(); } }
|
||||
}
|
||||
|
||||
Row {
|
||||
Text {
|
||||
text: "Output dir:";
|
||||
vertical-alignment: center;
|
||||
}
|
||||
LineEdit {
|
||||
read-only: true;
|
||||
text: root.output-dir;
|
||||
}
|
||||
Button {
|
||||
text: "Browse...";
|
||||
clicked => { root.browse-output(); }
|
||||
}
|
||||
Text { text: "Output dir:"; vertical-alignment: center; }
|
||||
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;
|
||||
}
|
||||
|
||||
if root.has-results: Button {
|
||||
text: "Show results";
|
||||
clicked => { root.open-results(); }
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Decode!";
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user