369 lines
11 KiB
Rust
369 lines
11 KiB
Rust
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
|
|
|
|
use std::{
|
|
cell::RefCell,
|
|
fs,
|
|
path::PathBuf,
|
|
rc::Rc,
|
|
thread,
|
|
};
|
|
|
|
use can_dbc::Dbc;
|
|
use chrono::Local;
|
|
use slint::{ComponentHandle, ModelRc, StandardListViewItem, VecModel};
|
|
|
|
slint::include_modules!();
|
|
|
|
#[derive(Default)]
|
|
struct State {
|
|
dbc_path: Option<PathBuf>,
|
|
asc_paths: Vec<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() {
|
|
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();
|
|
move || {
|
|
if let Some(path) = rfd::FileDialog::new()
|
|
.set_title("Select DBC")
|
|
.add_filter("DBC files", &["dbc"])
|
|
.pick_file()
|
|
{
|
|
let display = path.display().to_string();
|
|
state.borrow_mut().dbc_path = Some(path);
|
|
if let Some(w) = window_weak.upgrade() {
|
|
w.set_dbc_path(display.into());
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
window.on_browse_asc({
|
|
let window_weak = window.as_weak();
|
|
let state = state.clone();
|
|
move || {
|
|
if let Some(paths) = rfd::FileDialog::new()
|
|
.set_title("Select ASC file(s)")
|
|
.add_filter("ASC files", &["asc"])
|
|
.pick_files()
|
|
{
|
|
let label = match paths.len() {
|
|
0 => String::new(),
|
|
1 => paths[0].display().to_string(),
|
|
n => format!("{} file(s) selected", n),
|
|
};
|
|
let has_asc = !paths.is_empty();
|
|
state.borrow_mut().asc_paths = paths;
|
|
if let Some(w) = window_weak.upgrade() {
|
|
w.set_asc_label(label.into());
|
|
w.set_has_asc(has_asc);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
window.on_browse_output({
|
|
let window_weak = window.as_weak();
|
|
let state = state.clone();
|
|
move || {
|
|
if let Some(path) = rfd::FileDialog::new()
|
|
.set_title("Select output directory")
|
|
.pick_folder()
|
|
{
|
|
let display = path.display().to_string();
|
|
state.borrow_mut().output_dir = Some(path);
|
|
if let Some(w) = window_weak.upgrade() {
|
|
w.set_output_dir(display.into());
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
window.on_decode({
|
|
let window_weak = window.as_weak();
|
|
let state = state.clone();
|
|
move || {
|
|
let s = state.borrow();
|
|
let (Some(dbc_path), Some(output_dir)) = (s.dbc_path.clone(), s.output_dir.clone())
|
|
else {
|
|
return;
|
|
};
|
|
let asc_paths = s.asc_paths.clone();
|
|
if asc_paths.is_empty() {
|
|
return;
|
|
}
|
|
drop(s);
|
|
|
|
let total = asc_paths.len();
|
|
if let Some(w) = window_weak.upgrade() {
|
|
w.set_busy(true);
|
|
w.set_status(format!("Decoding... 0/{}", total).into());
|
|
}
|
|
|
|
let window_weak = window_weak.clone();
|
|
thread::spawn(move || {
|
|
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(total);
|
|
for (i, asc) in asc_paths.iter().enumerate() {
|
|
let info = libsvld::decode_to_motec(&dbc, asc, &output_dir);
|
|
out.push(build_file_result(asc, info));
|
|
let done = i + 1;
|
|
let w = window_weak.clone();
|
|
slint::invoke_from_event_loop(move || {
|
|
if let Some(w) = w.upgrade() {
|
|
w.set_status(format!("Decoding... {}/{}", done, total).into());
|
|
}
|
|
})
|
|
.ok();
|
|
}
|
|
Ok(out)
|
|
})();
|
|
|
|
slint::invoke_from_event_loop(move || {
|
|
if let Some(w) = window_weak.upgrade() {
|
|
w.set_busy(false);
|
|
}
|
|
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();
|
|
});
|
|
}
|
|
});
|
|
|
|
window.run().unwrap();
|
|
}
|