diff --git a/Cargo.lock b/Cargo.lock index c8dd245..1a0e23b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,7 +356,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -488,7 +488,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -523,7 +523,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -588,7 +588,7 @@ dependencies = [ "derive_utils", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -673,7 +673,7 @@ dependencies = [ "regex", "rustc-hash 2.1.2", "shlex", - "syn", + "syn 2.0.117", ] [[package]] @@ -839,7 +839,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1092,7 +1092,7 @@ checksum = "9b5dc851b7ea80cc4f69aa28632385ffd3e0f88014d53db3f2e5a781896ca783" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1314,7 +1314,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.117", "unicode-xid", ] @@ -1326,7 +1326,7 @@ checksum = "362f47930db19fe7735f527e6595e4900316b893ebf6d48ad3d31be928d57dd6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1365,7 +1365,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1610,6 +1610,15 @@ dependencies = [ "serde", ] +[[package]] +name = "embedded-can" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d2e857f87ac832df68fa498d18ddc679175cf3d2e4aa893988e5601baf9438" +dependencies = [ + "nb", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1643,7 +1652,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1654,7 +1663,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1703,7 +1712,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1946,7 +1955,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2039,7 +2048,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2415,7 +2424,7 @@ dependencies = [ "i-slint-renderer-software", "input", "memmap2", - "nix", + "nix 0.30.1", "xkbcommon", ] @@ -2584,7 +2593,7 @@ checksum = "7931356d5238281e6be994ec15241fb2d9e66359776d53b4bd6b2fdd9c257c6e" dependencies = [ "quote", "serde_json", - "syn", + "syn 2.0.117", ] [[package]] @@ -2945,7 +2954,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3010,7 +3019,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.117", ] [[package]] @@ -3038,7 +3047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3281,7 +3290,7 @@ dependencies = [ "quote", "regex-automata", "regex-syntax", - "syn", + "syn 2.0.117", ] [[package]] @@ -3519,6 +3528,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + [[package]] name = "ndk" version = "0.9.0" @@ -3549,12 +3564,50 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "neli" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93062a0dce6da2517ea35f301dfc88184ce18d3601ec786a727a87bf535deca9" +dependencies = [ + "byteorder", + "libc", + "log", + "neli-proc-macros", +] + +[[package]] +name = "neli-proc-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8034b7fbb6f9455b2a96c19e6edf8dc9fc34c70449938d8ee3b4df363f61fe" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nix" version = "0.30.1" @@ -3625,7 +3678,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3677,7 +3730,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4246,7 +4299,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4290,7 +4343,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4325,7 +4378,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4486,7 +4539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -4523,7 +4576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5050,7 +5103,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5074,7 +5127,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5367,7 +5420,36 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socketcan" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e63effc7e77256358f700e5fb0160238501022fec00b75d5da03e9e385d932" +dependencies = [ + "bitflags 2.11.1", + "embedded-can", + "hex", + "itertools 0.13.0", + "libc", + "log", + "nb", + "neli", + "nix 0.29.0", + "socket2", + "thiserror 2.0.18", ] [[package]] @@ -5467,7 +5549,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5512,6 +5594,7 @@ dependencies = [ "fuzzy-matcher", "libsvld", "rfd", + "socketcan", ] [[package]] @@ -5525,6 +5608,17 @@ dependencies = [ "zeno", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -5544,7 +5638,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5642,7 +5736,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5653,7 +5747,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5850,7 +5944,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -6138,7 +6232,7 @@ checksum = "482a37f9777480673d3a99c80ac2edc69f92382230440946855fa27ebc47cba6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -6211,7 +6305,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -6893,7 +6987,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -6904,7 +6998,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -7327,7 +7421,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -7343,7 +7437,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -7555,7 +7649,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -7612,7 +7706,7 @@ checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "zbus-lockstep", "zbus_xml", "zvariant", @@ -7627,7 +7721,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "zbus_names", "zvariant", "zvariant_utils", @@ -7679,7 +7773,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -7699,7 +7793,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -7735,7 +7829,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -7791,7 +7885,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "zvariant_utils", ] @@ -7804,6 +7898,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "syn 2.0.117", "winnow 1.0.3", ] diff --git a/libsvld/src/lib.rs b/libsvld/src/lib.rs index fce7425..9b5a01d 100644 --- a/libsvld/src/lib.rs +++ b/libsvld/src/lib.rs @@ -30,16 +30,11 @@ struct FrameOutput { error_frames: Vec, } -fn frame_worker( - mmap: &[u8], - dbc: &Dbc, -) -> FrameOutput { - let mut channel_data = HashMap::>::new(); - let mut metadatas = HashMap::::new(); - - let mut unknown_ids = HashSet::::new(); - let mut error_frames = Vec::::new(); - +/// Build the LD metadata for a single CAN id from the DBC, applying the +/// project's `ld_*`/`Frequency` attribute defaults. Returns `None` when the +/// id is not described by the DBC (an "unknown" id). Shared by the ASC decode +/// path and the live-CAN path so both produce identical metadata. +pub fn build_message_metadata(dbc: &Dbc, id: CanId) -> Option { let get_default = |name: &str| match dbc .attribute_defaults .iter() @@ -56,57 +51,66 @@ fn frame_worker( let default_scale = get_default("ld_scale"); let default_decimal_places = get_default("ld_decimal_places"); - let mut add_metadata = |id| { - let msg = match dbc.messages.iter().find(|x| x.id.raw() == id) { - Some(msg) => msg, - None => { - unknown_ids.insert(id); - return; - } - }; - let signals = &msg.signals; - let mut signals_meta = Vec::::new(); - for signal in signals { - let decimal_places = dbc - .attribute_values_signal - .iter() - .find(|x| { - x.message_id.raw() == id - && x.signal_name == signal.name - && x.name == "ld_decimal_places" - }) - .map_or(default_decimal_places, |x| match x.value { - can_dbc::AttributeValue::Uint(val) => val, - _ => panic!("Bad ld_decimal_places value type, expected Uint."), - }); - - signals_meta.push(SignalMetadata { - shift: default_shift as u16, - scale: default_scale as u16, - decimal_places: decimal_places as u16, - name: signal.name.clone(), - unit: signal.unit.clone(), - signal: signal.clone(), - }); - } - - let frequency = dbc - .attribute_values_message + let msg = dbc.messages.iter().find(|x| x.id.raw() == id)?; + let signals = &msg.signals; + let mut signals_meta = Vec::::new(); + for signal in signals { + let decimal_places = dbc + .attribute_values_signal .iter() - .find(|x| x.message_id.raw() == id && x.name == "Frequency") - .map_or(default_freqency, |x| match x.value { + .find(|x| { + x.message_id.raw() == id + && x.signal_name == signal.name + && x.name == "ld_decimal_places" + }) + .map_or(default_decimal_places, |x| match x.value { can_dbc::AttributeValue::Uint(val) => val, - _ => panic!("Bad Frequency value type, expected Uint."), + _ => panic!("Bad ld_decimal_places value type, expected Uint."), }); - metadatas.insert( - id, - MessageMetadata { - id, - frequency: frequency as u16, - signals: signals_meta, - }, - ); + signals_meta.push(SignalMetadata { + shift: default_shift as u16, + scale: default_scale as u16, + decimal_places: decimal_places as u16, + name: signal.name.clone(), + unit: signal.unit.clone(), + signal: signal.clone(), + }); + } + + let frequency = dbc + .attribute_values_message + .iter() + .find(|x| x.message_id.raw() == id && x.name == "Frequency") + .map_or(default_freqency, |x| match x.value { + can_dbc::AttributeValue::Uint(val) => val, + _ => panic!("Bad Frequency value type, expected Uint."), + }); + + Some(MessageMetadata { + id, + frequency: frequency as u16, + signals: signals_meta, + }) +} + +fn frame_worker( + mmap: &[u8], + dbc: &Dbc, +) -> FrameOutput { + let mut channel_data = HashMap::>::new(); + let mut metadatas = HashMap::::new(); + + let mut unknown_ids = HashSet::::new(); + let mut error_frames = Vec::::new(); + + let mut add_metadata = |id| match build_message_metadata(dbc, id) { + Some(meta) => { + metadatas.insert(id, meta); + } + None => { + unknown_ids.insert(id); + } }; // Caller has aligned the chunk to line boundaries, so we always start at @@ -171,6 +175,51 @@ pub struct DecodeResult { pub info: DecodeProcessInfo, } +impl DecodeResult { + /// An empty result suitable for incrementally accumulating live CAN + /// frames via [`DecodeResult::push_live`]. The `info` fields that only + /// make sense for the file-based decode are zeroed. + pub fn empty() -> Self { + DecodeResult { + data: HashMap::new(), + metadata: HashMap::new(), + info: DecodeProcessInfo { + unknown_ids: HashSet::new(), + error_frames: Vec::new(), + date: chrono::DateTime::from_timestamp(0, 0).unwrap().naive_utc(), + thread_count: 0, + page_size: 0, + pages_per_chunk: 0, + chunk_size: 0, + }, + } + } + + /// Append a single live frame, lazily resolving its metadata from the DBC + /// the first time an id is seen. Frames are expected to arrive in + /// non-decreasing timestamp order (as they do off a live bus), so no + /// re-sorting is performed — `compute_signal` consumes them in order. + pub fn push_live(&mut self, dbc: &Dbc, frame: CanFrame) { + match self.data.get_mut(&frame.can_id) { + Some(vec) => vec.push(frame), + None => { + let id = frame.can_id; + self.data.insert(id, vec![frame]); + if !self.metadata.contains_key(&id) { + match build_message_metadata(dbc, id) { + Some(meta) => { + self.metadata.insert(id, meta); + } + None => { + self.info.unknown_ids.insert(id); + } + } + } + } + } + } +} + pub fn decode(dbc: &Dbc, asc_path: &Path) -> DecodeResult { let thread_count = std::thread::available_parallelism().unwrap().get(); diff --git a/svldplotter/Cargo.toml b/svldplotter/Cargo.toml index 0e2cf6f..7421dd9 100644 --- a/svldplotter/Cargo.toml +++ b/svldplotter/Cargo.toml @@ -21,3 +21,4 @@ egui_plot = "0.35" egui_ltreeview = "0.7" fuzzy-matcher = "0.3" rfd = "0.17" +socketcan = "3.6.1" diff --git a/svldplotter/src/main.rs b/svldplotter/src/main.rs index 1d2686f..b59202e 100644 --- a/svldplotter/src/main.rs +++ b/svldplotter/src/main.rs @@ -1,3 +1,4 @@ +mod can_live; mod plot; mod plot_list; mod signal; @@ -13,7 +14,7 @@ use std::{ use can_dbc::Dbc; use eframe::egui; -use libsvld::DecodeResult; +use libsvld::{can_frame::CanFrame, DecodeResult}; use crate::plot::show_plot_panel; use crate::plot_list::{ @@ -30,13 +31,36 @@ fn main() { native_options, Box::new(|cc| { let mut app = MyEguiApp::default(); - // Optional CLI quick-load: `svldplotter [DBC [ASC]]`. Both args - // are optional and intended for dev iteration. + // Optional CLI quick-load. Positional args are an optional DBC and + // ASC, intended for dev iteration: + // svldplotter [DBC [ASC]] + // Passing `--can ` (e.g. `svldplotter car.dbc --can can0`) + // switches to live mode: instead of reading an ASC file the + // plotter streams frames straight off the CAN bus and plots them + // in real time, using the DBC for signal metadata. let args: Vec = std::env::args().collect(); - if let Some(dbc) = args.get(1) { + let mut positional: Vec = Vec::new(); + let mut can_iface: Option = None; + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--can" | "-c" => { + can_iface = args.get(i + 1).cloned(); + i += 2; + } + other => { + positional.push(other.to_string()); + i += 1; + } + } + } + if let Some(dbc) = positional.first() { app.load_dbc(PathBuf::from(dbc)); } - if let Some(asc) = args.get(2) { + if let Some(iface) = can_iface { + // Live CAN takes precedence over any ASC argument. + app.start_can_live(&iface, &cc.egui_ctx); + } else if let Some(asc) = positional.get(1) { app.asc_paths.push(PathBuf::from(asc)); app.set_active_asc(app.asc_paths.len() - 1, Some(&cc.egui_ctx)); } @@ -74,6 +98,13 @@ struct MyEguiApp { next_plot_id: u64, cursor_x: Option, left_tab: LeftTab, + /// Live CAN stream receiver, present only in `--can` mode. + live_rx: Option>, + /// Growing decode result fed by the live CAN stream. + live_data: Option, + /// Latest data x-extent from the previous frame, used to detect whether + /// the live view is pinned to the right edge for auto-scroll. + live_prev_max: Option, } impl MyEguiApp { @@ -182,15 +213,95 @@ impl MyEguiApp { self.data = Some(data); } - fn handle_dbc_toggles(&mut self, toggles: Vec) { - let Some(data) = self.data.clone() else { + /// Enter live CAN mode: open `iface` and start streaming frames into a + /// fresh, growing `DecodeResult`. Requires a DBC to already be loaded so + /// incoming frames can be resolved to signals. + fn start_can_live(&mut self, iface: &str, ctx: &egui::Context) { + if self.dbc.is_none() { + eprintln!("Cannot start live CAN: no DBC loaded."); + return; + } + match can_live::spawn_reader(iface) { + Ok(rx) => { + self.live_rx = Some(rx); + self.live_data = Some(DecodeResult::empty()); + // Clear any file-based state so the two sources don't mix. + self.active_asc = None; + self.data = None; + self.tree.clear(); + ctx.request_repaint(); + } + Err(e) => { + eprintln!("Failed to open CAN interface '{iface}': {e}"); + } + } + } + + /// Drain any frames received from the live CAN stream, fold them into the + /// live `DecodeResult`, and re-resolve the tree and plotted signals so the + /// UI reflects the latest data. A repaint is always requested while live + /// so polling continues even when no frame arrived this tick. + fn poll_live(&mut self, ctx: &egui::Context) { + if self.live_rx.is_none() { + return; + } + // Keep the UI ticking so we keep polling the stream. + ctx.request_repaint(); + + let Some(dbc) = self.dbc.clone() else { return; }; + + // Collect first so the receiver borrow is released before we mutate + // `self.live_data` below. + let frames: Vec = self + .live_rx + .as_ref() + .map(|rx| rx.try_iter().collect()) + .unwrap_or_default(); + if frames.is_empty() { + return; + } + + let Some(live) = self.live_data.as_mut() else { + return; + }; + for frame in frames { + live.push_live(&dbc, frame); + } + + // Re-resolve tree and plots against the updated live data. These touch + // disjoint fields of `self`, so the borrows don't conflict. + let data = self.live_data.as_ref().unwrap(); + self.tree = build_tree(&dbc, data); + 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; + } + } + } + } + } + + fn handle_dbc_toggles(&mut self, toggles: Vec) { 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(&data, msg_id, sig_idx, id) { + // Pull from the live stream when in `--can` mode, otherwise the + // decoded ASC. Accessed as disjoint fields so the immutable + // data borrow doesn't clash with the `self.plots` mutation. + let data = if self.live_data.is_some() { + self.live_data.as_ref() + } else { + self.data.as_deref() + }; + let Some(data) = data else { + continue; + }; + if let Some(sig) = compute_signal(data, msg_id, sig_idx, id) { add_signal_auto(&mut self.plots, &mut self.next_plot_id, sig); } } @@ -267,6 +378,7 @@ impl eframe::App for MyEguiApp { self.poll_pending(); let ctx = ui.ctx().clone(); + self.poll_live(&ctx); egui::Panel::left(egui::Id::new("LeftPanel")) .resizable(true) .show_inside(ui, |ui| { @@ -317,7 +429,13 @@ impl eframe::App for MyEguiApp { } } }); - show_plot_panel(ui, &self.plots, &mut self.cursor_x); + show_plot_panel( + ui, + &self.plots, + &mut self.cursor_x, + self.live_rx.is_some(), + &mut self.live_prev_max, + ); if let Some(job) = self.pending.as_ref() { let name = job diff --git a/svldplotter/src/plot.rs b/svldplotter/src/plot.rs index 6d2f9c3..5a257ce 100644 --- a/svldplotter/src/plot.rs +++ b/svldplotter/src/plot.rs @@ -65,7 +65,20 @@ fn line_label(sig: &PlottedSignal, cursor_x: Option) -> String { /// Clamp the visible x-range to [0, x_max]. Preserves the current zoom /// width when possible; if the zoomed-out width exceeds the data range, /// just snaps to the full range. -fn clamp_x_bounds(plot_ui: &mut egui_plot::PlotUi, x_max: f64) { +/// +/// In live mode, if the view's right edge was pinned to the latest sample +/// (`prev_max`, the data extent from the previous frame), the window is +/// scrolled right to keep the right edge at the new `x_max` so the plot +/// auto-follows incoming data. Panning away from the edge disengages this. +fn clamp_x_bounds( + plot_ui: &mut egui_plot::PlotUi, + x_max: f64, + live: bool, + prev_max: Option, +) { + if !x_max.is_finite() { + return; + } let bounds = plot_ui.plot_bounds(); let lo = bounds.min(); let hi = bounds.max(); @@ -73,6 +86,19 @@ fn clamp_x_bounds(plot_ui: &mut egui_plot::PlotUi, x_max: f64) { let mut new_hi_x = hi[0]; let width = new_hi_x - new_lo_x; + if live && width < x_max { + // Pinned when the right edge sits at (or past) the previous extent. + // A tiny epsilon absorbs float error from last frame's snap. + let pinned = match prev_max { + Some(p) => new_hi_x >= p - width * 1e-3, + None => true, + }; + if pinned { + new_hi_x = x_max; + new_lo_x = x_max - width; + } + } + if width >= x_max { new_lo_x = 0.0; new_hi_x = x_max; @@ -99,6 +125,8 @@ pub fn show_plot_panel( ui: &mut egui::Ui, plots: &[PlotGroup], cursor_x: &mut Option, + live: bool, + prev_max: &mut Option, ) { egui::CentralPanel::default().show_inside(ui, |ui| { if plots.is_empty() { @@ -109,6 +137,12 @@ pub fn show_plot_panel( } let (full_min, full_max) = full_x_range(plots); + // Snapshot the previous frame's extent for the auto-follow decision, + // then record this frame's for the next one. + let follow_prev_max = *prev_max; + if full_max.is_finite() { + *prev_max = Some(full_max); + } let link_group = ui.id().with("plot-link"); let plot_count = plots.len(); let plot_height = @@ -145,7 +179,7 @@ pub fn show_plot_panel( ]); let response = plot.show(ui, |plot_ui| { - clamp_x_bounds(plot_ui, full_max); + clamp_x_bounds(plot_ui, full_max, live, follow_prev_max); if plot_group.signals.is_empty() { plot_ui.line( Line::new(