This commit is contained in:
2026-07-04 20:44:00 +02:00
parent 5e2cfeafb9
commit 6851ab988b
4 changed files with 205 additions and 36 deletions
+63
View File
@@ -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<Receiver<CanFrame>> {
let socket = socketcan::CanSocket::open(iface)?;
let (tx, rx) = channel::<CanFrame>();
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)
}
+48 -29
View File
@@ -21,8 +21,10 @@ use crate::plot_list::{
add_signal_auto, apply_move, contains_signal, new_plot, remove_plot, remove_signal, add_signal_auto, apply_move, contains_signal, new_plot, remove_plot, remove_signal,
show_plot_list, PlotGroup, PlotListAction, show_plot_list, PlotGroup, PlotListAction,
}; };
use crate::signal::compute_signal; use crate::signal::{compute_delay_signal, compute_signal, resolve_signal};
use crate::tree::{build_tree, decode_signal_node_id, show_dbc_tree, MsgNode}; use crate::tree::{
build_tree, decode_signal_node_id, delay_node_id, show_dbc_tree, MsgNode, SignalAction,
};
fn main() { fn main() {
let native_options = eframe::NativeOptions::default(); let native_options = eframe::NativeOptions::default();
@@ -203,10 +205,8 @@ impl MyEguiApp {
// Re-resolve plotted signals so existing plots survive an ASC switch. // Re-resolve plotted signals so existing plots survive an ASC switch.
for plot in &mut self.plots { for plot in &mut self.plots {
for sig in &mut plot.signals { for sig in &mut plot.signals {
if let Some((msg_id, sig_idx)) = decode_signal_node_id(sig.id) { if let Some(new_sig) = resolve_signal(&data, sig.id) {
if let Some(new_sig) = compute_signal(&data, msg_id, sig_idx, sig.id) { *sig = new_sig;
*sig = new_sig;
}
} }
} }
} }
@@ -276,38 +276,57 @@ impl MyEguiApp {
self.tree = build_tree(&dbc, data); self.tree = build_tree(&dbc, data);
for plot in &mut self.plots { for plot in &mut self.plots {
for sig in &mut plot.signals { for sig in &mut plot.signals {
if let Some((msg_id, sig_idx)) = decode_signal_node_id(sig.id) { if let Some(new_sig) = resolve_signal(data, sig.id) {
if let Some(new_sig) = compute_signal(data, msg_id, sig_idx, sig.id) { *sig = new_sig;
*sig = new_sig;
}
} }
} }
} }
} }
fn handle_dbc_toggles(&mut self, toggles: Vec<u64>) { fn handle_signal_actions(&mut self, actions: Vec<SignalAction>) {
for id in toggles { for action in actions {
if contains_signal(&self.plots, id) { match action {
remove_signal(&mut self.plots, id); SignalAction::Toggle(id) => self.toggle_plotted(id, false),
} else if let Some((msg_id, sig_idx)) = decode_signal_node_id(id) { SignalAction::PlotDelay(sig_id) => {
// Pull from the live stream when in `--can` mode, otherwise the // The delay plot uses a distinct node id derived from the
// decoded ASC. Accessed as disjoint fields so the immutable // signal, so it can coexist with the value plot.
// data borrow doesn't clash with the `self.plots` mutation. self.toggle_plotted(delay_node_id(sig_id), true);
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);
} }
} }
} }
} }
/// 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<PlotListAction>) { fn handle_plot_actions(&mut self, actions: Vec<PlotListAction>) {
for action in actions { for action in actions {
match action { match action {
@@ -419,7 +438,7 @@ impl eframe::App for MyEguiApp {
} }
}) })
.inner; .inner;
self.handle_dbc_toggles(toggles); self.handle_signal_actions(toggles);
self.handle_plot_actions(plot_actions); self.handle_plot_actions(plot_actions);
for action in files_actions { for action in files_actions {
match action { match action {
+48
View File
@@ -1,6 +1,8 @@
use egui_plot::PlotPoint; use egui_plot::PlotPoint;
use libsvld::DecodeResult; use libsvld::DecodeResult;
use crate::tree::{decode_signal_node_id, is_delay_node_id};
pub struct PlottedSignal { pub struct PlottedSignal {
pub id: u64, pub id: u64,
pub label: String, 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<PlottedSignal> {
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<f64> = 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<PlottedSignal> {
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<f64> { pub fn nearest_y(points: &[PlotPoint], x: f64) -> Option<f64> {
if points.is_empty() { if points.is_empty() {
return None; return None;
+46 -7
View File
@@ -1,7 +1,9 @@
use std::cell::RefCell;
use can_dbc::Dbc; use can_dbc::Dbc;
use eframe::egui; use eframe::egui;
use egui::ScrollArea; use egui::ScrollArea;
use egui_ltreeview::{Action, TreeView, TreeViewBuilder}; use egui_ltreeview::{Action, NodeBuilder, TreeView, TreeViewBuilder};
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::skim::SkimMatcherV2;
use libsvld::DecodeResult; use libsvld::DecodeResult;
@@ -20,11 +22,33 @@ pub struct MsgNode {
// Node-id encoding shared across the two tree views. // Node-id encoding shared across the two tree views.
// bit 63 set → signal leaf // bit 63 set → signal leaf
// bit 62 set → plot directory (in the lower "plots" tree) // 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 // ROOT_NODE_ID → the implicit root of the lower tree
pub const SIGNAL_FLAG: u64 = 1 << 63; pub const SIGNAL_FLAG: u64 = 1 << 63;
pub const PLOT_FLAG: u64 = 1 << 62; pub const PLOT_FLAG: u64 = 1 << 62;
pub const DELAY_FLAG: u64 = 1 << 61;
pub const ROOT_NODE_ID: u64 = 1; 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 { pub fn signal_node_id(msg_id: u32, sig_idx: usize) -> u64 {
SIGNAL_FLAG | ((msg_id as u64) << 16) | (sig_idx as 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<MsgNode> {
.collect() .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( pub fn show_dbc_tree(
ui: &mut egui::Ui, ui: &mut egui::Ui,
filter: &mut String, filter: &mut String,
tree: &[MsgNode], tree: &[MsgNode],
) -> Vec<u64> { ) -> Vec<SignalAction> {
let mut toggles = Vec::new(); let events: RefCell<Vec<SignalAction>> = RefCell::new(Vec::new());
ui.add( ui.add(
egui::TextEdit::singleline(filter) egui::TextEdit::singleline(filter)
.hint_text("Filter messages/signals") .hint_text("Filter messages/signals")
@@ -119,7 +144,21 @@ pub fn show_dbc_tree(
} }
builder.dir(msg.id, msg.label.as_str()); builder.dir(msg.id, msg.label.as_str());
for sig in matching { 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(); builder.close_dir();
} }
@@ -128,11 +167,11 @@ pub fn show_dbc_tree(
if let Action::SetSelected(ids) = action { if let Action::SetSelected(ids) = action {
for id in ids { for id in ids {
if id & SIGNAL_FLAG != 0 { if id & SIGNAL_FLAG != 0 {
toggles.push(id); events.borrow_mut().push(SignalAction::Toggle(id));
} }
} }
} }
} }
}); });
toggles events.into_inner()
} }