diff --git a/svldplotter/src/can_live.rs b/svldplotter/src/can_live.rs new file mode 100644 index 0000000..8cfc58d --- /dev/null +++ b/svldplotter/src/can_live.rs @@ -0,0 +1,63 @@ +//! Live CAN bus source (Linux/SocketCAN). +//! +//! Opens a SocketCAN interface and spawns a background thread that reads CAN +//! frames, converting each into the same `libsvld::can_frame::CanFrame` the +//! ASC decoder produces. Frames are streamed back over an `mpsc` channel so +//! the UI thread can drain them without blocking. Timestamps are seconds +//! elapsed since the listener was opened, mirroring the relative timebase the +//! ASC files use. + +use std::sync::mpsc::{channel, Receiver}; +use std::thread; +use std::time::Instant; + +use libsvld::can_frame::{CanFrame, Direction}; +use socketcan::{CanFrame as ScFrame, EmbeddedFrame, Frame, Socket}; + +/// Open `iface` (e.g. `"can0"`) and start streaming frames. +/// +/// Returns a receiver that yields decoded [`CanFrame`]s until the interface +/// errors out or the receiver is dropped. The reader runs on its own thread. +pub fn spawn_reader(iface: &str) -> std::io::Result> { + let socket = socketcan::CanSocket::open(iface)?; + let (tx, rx) = channel::(); + let start = Instant::now(); + + thread::spawn(move || { + loop { + let frame = match socket.read_frame() { + Ok(frame) => frame, + // A read error (interface down, etc.) ends the stream. + Err(_) => break, + }; + + // Only data frames carry signal payloads; ignore remote/error + // frames for plotting purposes. + let data_frame = match frame { + ScFrame::Data(d) => d, + _ => continue, + }; + + let payload = data_frame.data(); + let dlc = payload.len().min(8); + let mut data = [0u8; 8]; + data[..dlc].copy_from_slice(&payload[..dlc]); + + let cf = CanFrame { + timestamp: start.elapsed().as_secs_f64(), + channel: 0, + can_id: data_frame.raw_id(), + direction: Direction::Rx, + dlc: dlc as u8, + data, + }; + + // Receiver dropped → UI no longer interested, stop reading. + if tx.send(cf).is_err() { + break; + } + } + }); + + Ok(rx) +} diff --git a/svldplotter/src/main.rs b/svldplotter/src/main.rs index b59202e..5e60a66 100644 --- a/svldplotter/src/main.rs +++ b/svldplotter/src/main.rs @@ -21,8 +21,10 @@ use crate::plot_list::{ add_signal_auto, apply_move, contains_signal, new_plot, remove_plot, remove_signal, show_plot_list, PlotGroup, PlotListAction, }; -use crate::signal::compute_signal; -use crate::tree::{build_tree, decode_signal_node_id, show_dbc_tree, MsgNode}; +use crate::signal::{compute_delay_signal, compute_signal, resolve_signal}; +use crate::tree::{ + build_tree, decode_signal_node_id, delay_node_id, show_dbc_tree, MsgNode, SignalAction, +}; fn main() { let native_options = eframe::NativeOptions::default(); @@ -203,10 +205,8 @@ impl MyEguiApp { // Re-resolve plotted signals so existing plots survive an ASC switch. 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; - } + if let Some(new_sig) = resolve_signal(&data, sig.id) { + *sig = new_sig; } } } @@ -276,38 +276,57 @@ impl MyEguiApp { 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; - } + if let Some(new_sig) = resolve_signal(data, 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) { - // 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); + fn handle_signal_actions(&mut self, actions: Vec) { + for action in actions { + match action { + SignalAction::Toggle(id) => self.toggle_plotted(id, false), + SignalAction::PlotDelay(sig_id) => { + // The delay plot uses a distinct node id derived from the + // signal, so it can coexist with the value plot. + self.toggle_plotted(delay_node_id(sig_id), true); } } } } + /// Toggle whether `id` is plotted. `delay` selects the inter-frame delay + /// (Δt) computation instead of the signal's value. + fn toggle_plotted(&mut self, id: u64, delay: bool) { + if contains_signal(&self.plots, id) { + remove_signal(&mut self.plots, id); + return; + } + let Some((msg_id, sig_idx)) = decode_signal_node_id(id) else { + return; + }; + // 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 { + return; + }; + let sig = if delay { + compute_delay_signal(data, msg_id, sig_idx, id) + } else { + compute_signal(data, msg_id, sig_idx, id) + }; + if let Some(sig) = sig { + add_signal_auto(&mut self.plots, &mut self.next_plot_id, sig); + } + } + fn handle_plot_actions(&mut self, actions: Vec) { for action in actions { match action { @@ -419,7 +438,7 @@ impl eframe::App for MyEguiApp { } }) .inner; - self.handle_dbc_toggles(toggles); + self.handle_signal_actions(toggles); self.handle_plot_actions(plot_actions); for action in files_actions { match action { diff --git a/svldplotter/src/signal.rs b/svldplotter/src/signal.rs index 16a4b56..b1adf7c 100644 --- a/svldplotter/src/signal.rs +++ b/svldplotter/src/signal.rs @@ -1,6 +1,8 @@ use egui_plot::PlotPoint; use libsvld::DecodeResult; +use crate::tree::{decode_signal_node_id, is_delay_node_id}; + pub struct PlottedSignal { pub id: u64, pub label: String, @@ -49,6 +51,52 @@ pub fn compute_signal( }) } +/// Build a signal whose value is the time delay between consecutive CAN +/// frames of a message (Δt, in seconds), keyed off one of its signals. +/// The first frame has no predecessor, so it produces no point. Plotted at +/// the timestamp of the later frame. +pub fn compute_delay_signal( + data: &DecodeResult, + msg_id: u32, + sig_idx: usize, + id: u64, +) -> Option { + let meta = data.metadata.get(&msg_id)?; + let sig = meta.signals.get(sig_idx)?; + let frames = data.data.get(&msg_id)?; + let mut points = Vec::with_capacity(frames.len().saturating_sub(1)); + let mut prev: Option = None; + for frame in frames { + if let Some(p) = prev { + points.push(PlotPoint { + x: frame.timestamp, + y: frame.timestamp - p, + }); + } + prev = Some(frame.timestamp); + } + let gap_threshold = compute_gap_threshold(&points); + Some(PlottedSignal { + id, + label: format!("{} Δt", sig.name), + unit: "s".to_string(), + points, + gap_threshold, + }) +} + +/// Resolve a plotted-signal node id against decoded data, dispatching to the +/// value or frame-delay computation based on the id's flags. Used to (re)build +/// a `PlottedSignal` whenever the underlying data changes (ASC switch, live). +pub fn resolve_signal(data: &DecodeResult, id: u64) -> Option { + let (msg_id, sig_idx) = decode_signal_node_id(id)?; + if is_delay_node_id(id) { + compute_delay_signal(data, msg_id, sig_idx, id) + } else { + compute_signal(data, msg_id, sig_idx, id) + } +} + pub fn nearest_y(points: &[PlotPoint], x: f64) -> Option { if points.is_empty() { return None; diff --git a/svldplotter/src/tree.rs b/svldplotter/src/tree.rs index 7024004..189adfe 100644 --- a/svldplotter/src/tree.rs +++ b/svldplotter/src/tree.rs @@ -1,7 +1,9 @@ +use std::cell::RefCell; + use can_dbc::Dbc; use eframe::egui; use egui::ScrollArea; -use egui_ltreeview::{Action, TreeView, TreeViewBuilder}; +use egui_ltreeview::{Action, NodeBuilder, TreeView, TreeViewBuilder}; use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::skim::SkimMatcherV2; use libsvld::DecodeResult; @@ -20,11 +22,33 @@ pub struct MsgNode { // Node-id encoding shared across the two tree views. // bit 63 set → signal leaf // bit 62 set → plot directory (in the lower "plots" tree) +// bit 61 set → "frame delay" variant of a signal (keeps bit 63 set too, so +// it still behaves like a signal for selection / drag-and-drop) // ROOT_NODE_ID → the implicit root of the lower tree pub const SIGNAL_FLAG: u64 = 1 << 63; pub const PLOT_FLAG: u64 = 1 << 62; +pub const DELAY_FLAG: u64 = 1 << 61; pub const ROOT_NODE_ID: u64 = 1; +/// User actions emitted by the DBC signal tree. +pub enum SignalAction { + /// Toggle plotting of a signal's value (left-click). + Toggle(u64), + /// Toggle plotting of the inter-frame delay (Δt) for a signal's message + /// (right-click → context menu). The id is the plain signal node id. + PlotDelay(u64), +} + +/// Derive the "frame delay" node id for a plain signal node id. +pub fn delay_node_id(signal_id: u64) -> u64 { + signal_id | DELAY_FLAG +} + +/// Whether a node id refers to the inter-frame delay variant of a signal. +pub fn is_delay_node_id(id: u64) -> bool { + id & DELAY_FLAG != 0 +} + pub fn signal_node_id(msg_id: u32, sig_idx: usize) -> u64 { SIGNAL_FLAG | ((msg_id as u64) << 16) | (sig_idx as u64) } @@ -79,13 +103,14 @@ pub fn build_tree(dbc: &Dbc, data: &DecodeResult) -> Vec { .collect() } -/// Render the DBC tree and return the signal IDs the user clicked to toggle. +/// Render the DBC tree and return the user's actions: left-clicking a signal +/// toggles its value plot; right-clicking offers a "plot frame delay" entry. pub fn show_dbc_tree( ui: &mut egui::Ui, filter: &mut String, tree: &[MsgNode], -) -> Vec { - let mut toggles = Vec::new(); +) -> Vec { + let events: RefCell> = RefCell::new(Vec::new()); ui.add( egui::TextEdit::singleline(filter) .hint_text("Filter messages/signals") @@ -119,7 +144,21 @@ pub fn show_dbc_tree( } builder.dir(msg.id, msg.label.as_str()); for sig in matching { - builder.leaf(sig.id, sig.name.as_str()); + let sig_id = sig.id; + let node = NodeBuilder::leaf(sig.id) + .label(sig.name.as_str()) + .context_menu(|ui| { + if ui + .button("Plot frame delay (Δt)") + .clicked() + { + events.borrow_mut().push( + SignalAction::PlotDelay(sig_id), + ); + ui.close(); + } + }); + builder.node(node); } builder.close_dir(); } @@ -128,11 +167,11 @@ pub fn show_dbc_tree( if let Action::SetSelected(ids) = action { for id in ids { if id & SIGNAL_FLAG != 0 { - toggles.push(id); + events.borrow_mut().push(SignalAction::Toggle(id)); } } } } }); - toggles + events.into_inner() }