#![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, asc_paths: Vec, output_dir: Option, } 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, } #[derive(Default)] struct Results { runs: Vec, window: Option, run_counter: usize, } thread_local! { static RESULTS: RefCell = 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 = 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::>() .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 { 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> = f .error_rows .iter() .map(|(t, ch, d)| { let cells: Vec = 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, 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(); }