comiit
This commit is contained in:
@@ -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)
|
||||
}
|
||||
+37
-18
@@ -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,13 +205,11 @@ 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) {
|
||||
if let Some(new_sig) = resolve_signal(&data, sig.id) {
|
||||
*sig = new_sig;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.data = Some(data);
|
||||
}
|
||||
|
||||
@@ -276,37 +276,56 @@ 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) {
|
||||
if let Some(new_sig) = resolve_signal(data, sig.id) {
|
||||
*sig = new_sig;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_signal_actions(&mut self, actions: Vec<SignalAction>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_dbc_toggles(&mut self, toggles: Vec<u64>) {
|
||||
for id in toggles {
|
||||
/// 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);
|
||||
} 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.
|
||||
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 {
|
||||
continue;
|
||||
return;
|
||||
};
|
||||
if let Some(sig) = compute_signal(data, msg_id, sig_idx, id) {
|
||||
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>) {
|
||||
for action in actions {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<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> {
|
||||
if points.is_empty() {
|
||||
return None;
|
||||
|
||||
+46
-7
@@ -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<MsgNode> {
|
||||
.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<u64> {
|
||||
let mut toggles = Vec::new();
|
||||
) -> Vec<SignalAction> {
|
||||
let events: RefCell<Vec<SignalAction>> = 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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user