diff --git a/Cargo.toml b/Cargo.toml index 2f96c2e..4946882 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,12 @@ [package] name = "can-viewer" -version = "0.2.0" +version = "0.2.1" edition = "2021" description = "view real-time CAN packages" [dependencies] +chrono = "0.4.38" +sudo = "0.6" can-dbc = { git="https://github.com/TuEmb/can-dbc.git", branch="dev" } rfd = "0.14.1" slint = { version = "1.7.1", default-features = false, features = ["backend-winit", "compat-1-2", "renderer-winit-femtovg"] } @@ -15,6 +17,7 @@ winapi = { version = "0.3.9", features = ["winuser"] } pcan-basic = { git = "https://github.com/TuEmb/pcan-basic.git", branch="main"} [target.'cfg(unix)'.dependencies] +privilege-rs = "0.1.0" socketcan = { git = "https://github.com/socketcan-rs/socketcan-rs.git", rev="e0d7760eca8085b247f37ea22f0aa41e00fa25fa", features = ["enumerate"] } [build-dependencies] @@ -24,7 +27,7 @@ winresource = "0.1.17" [package.metadata.bundle] name = "can-viewer" icon = ["ui/images/can_viewer_32px.png", "ui/images/can_viewer_128px.png", "ui/images/can_viewer_256px.png"] -version = "1.0.0" +version = "0.2.1" copyright = "Copyright (c) Tu Nguyen 2024. All rights reserved." category = "Developer Tool" short_description = "view real-time CAN packages" @@ -34,5 +37,5 @@ can-view can records real-time can packages and parse data with DBC input. """ [package.metadata.winresource] -OriginalFilename = "can-viewer.exe" +OriginalFilename = "can-viewer_0.2.1.exe" LegalCopyright = "Copyright © 2024" diff --git a/src/event_handler/can_handler.rs b/src/event_handler/can_handler.rs index 6f60e6b..5ccd7ad 100644 --- a/src/event_handler/can_handler.rs +++ b/src/event_handler/can_handler.rs @@ -1,10 +1,11 @@ use can_dbc::DBC; +use chrono::Utc; #[cfg(target_os = "windows")] use pcan_basic::socket::usb::UsbCanSocket; use slint::{Model, VecModel, Weak}; use slint::{ModelRc, SharedString}; #[cfg(target_os = "linux")] -use socketcan::{CanSocket, EmbeddedFrame, Frame, Socket}; +use socketcan::{CanInterface, CanSocket, EmbeddedFrame, Frame, Socket}; use std::collections::HashMap; use std::fmt::Write; use std::rc::Rc; @@ -12,6 +13,7 @@ use std::sync::mpsc::Receiver; use std::sync::{Arc, Mutex}; use std::thread::sleep; use std::time::Duration; +use std::time::Instant; use crate::slint_generatedAppWindow::AppWindow; use crate::slint_generatedAppWindow::CanData; @@ -23,6 +25,7 @@ pub struct CanHandler<'a> { pub iface: UsbCanSocket, pub ui_handle: &'a Weak, pub mspc_rx: &'a Arc>>, + pub bitrate: String, } static mut NEW_DBC_CHECK: bool = false; @@ -33,8 +36,12 @@ impl<'a> CanHandler<'a> { if let Ok(dbc) = self.mspc_rx.lock().unwrap().try_recv() { #[cfg(target_os = "linux")] { + let can_if = CanInterface::open(self.iface).unwrap(); + let _ = can_if.bring_down(); + let _ = can_if.set_bitrate(self.bitrate().unwrap(), None); + let _ = can_if.bring_up(); let can_socket = self.open_can_socket(); - self.process_ui_events(dbc, can_socket); + self.process_ui_events(dbc, can_socket, can_if); } #[cfg(target_os = "windows")] self.process_ui_events(dbc); @@ -60,8 +67,27 @@ impl<'a> CanHandler<'a> { } } #[cfg(target_os = "linux")] - fn process_ui_events(&self, dbc: DBC, can_socket: CanSocket) { + fn process_ui_events(&self, dbc: DBC, can_socket: CanSocket, can_if: CanInterface) { + let mut start_bus_load = Instant::now(); + let mut total_bits = 0; loop { + let bus_state = match can_if.state().unwrap().unwrap() { + socketcan::nl::CanState::ErrorActive => "ERR_ACTIVE", + socketcan::nl::CanState::ErrorWarning => "ERR_WARNING", + socketcan::nl::CanState::ErrorPassive => "ERR_PASSIVE", + socketcan::nl::CanState::BusOff => "BUSOFF", + socketcan::nl::CanState::Stopped => "STOPPED", + socketcan::nl::CanState::Sleeping => "SLEEPING", + }; + let bitrate = can_if.bit_rate().unwrap().unwrap(); + let busload = if start_bus_load.elapsed() >= Duration::from_millis(1000) { + start_bus_load = Instant::now(); + let bus_load = (total_bits as f64 / bitrate as f64) * 100.0; + total_bits = 0; + bus_load + } else { + 0.0 + }; let _ = self.ui_handle.upgrade_in_event_loop(move |ui| unsafe { if ui.get_is_new_dbc() { if ui.get_is_first_open() { @@ -71,6 +97,11 @@ impl<'a> CanHandler<'a> { } ui.set_is_new_dbc(false); } + ui.set_state(bus_state.into()); + ui.set_bitrate(bitrate as i32); + if busload > 0.0 { + ui.set_bus_load(busload as i32); + } }); unsafe { if NEW_DBC_CHECK { @@ -79,6 +110,7 @@ impl<'a> CanHandler<'a> { } } if let Ok(frame) = can_socket.read_frame() { + total_bits += (frame.len() + 6) * 8; // Data length + overhead (approximation) let frame_id = frame.raw_id() & !0x80000000; for message in dbc.messages() { if frame_id == (message.message_id().raw() & !0x80000000) { @@ -106,9 +138,19 @@ impl<'a> CanHandler<'a> { } #[cfg(target_os = "windows")] fn process_ui_events(&mut self, dbc: DBC) { - use pcan_basic::socket::RecvCan; - + use pcan_basic::{error::PcanError, socket::RecvCan}; + let mut start_bus_load = Instant::now(); + let mut total_bits = 0; loop { + let bitrate = self.bitrate().unwrap(); + let busload = if start_bus_load.elapsed() >= Duration::from_millis(1000) { + start_bus_load = Instant::now(); + let bus_load = (total_bits as f64 / bitrate as f64) * 100.0; + total_bits = 0; + bus_load + } else { + 0.0 + }; let _ = self.ui_handle.upgrade_in_event_loop(move |ui| unsafe { if ui.get_is_new_dbc() { if ui.get_is_first_open() { @@ -118,6 +160,10 @@ impl<'a> CanHandler<'a> { } ui.set_is_new_dbc(false); } + ui.set_bitrate(bitrate as i32); + if busload > 0.0 { + ui.set_bus_load(busload as i32); + } }); unsafe { if NEW_DBC_CHECK { @@ -125,32 +171,43 @@ impl<'a> CanHandler<'a> { break; } } - if let Ok(frame) = self.iface.recv_frame() { - let id = frame.can_id(); - let frame_id = id & !0x80000000; - for message in dbc.messages() { - if frame_id == (message.message_id().raw() & !0x80000000) { - let padding_data = Self::pad_to_8_bytes(frame.data()); - let hex_string = Self::array_to_hex_string(frame.data()); - let signal_data = message.parse_from_can(&padding_data); - let _ = self.ui_handle.upgrade_in_event_loop(move |ui| { - let is_filter = ui.get_is_filter(); - let messages: ModelRc = if !is_filter { - ui.get_messages() - } else { - ui.get_filter_messages() - }; - Self::update_ui_with_signals( - &messages, - frame_id, - signal_data, - hex_string, - ); - }); + match self.iface.recv_frame() { + Ok(frame) => { + let _ = self.ui_handle.upgrade_in_event_loop(move |ui| { + ui.set_state("OK".into()); + }); + total_bits += (frame.dlc() as u32 + 6) * 8; // Data length + overhead (approximation) + let id = frame.can_id(); + let frame_id = id & !0x80000000; + for message in dbc.messages() { + if frame_id == (message.message_id().raw() & !0x80000000) { + let padding_data = Self::pad_to_8_bytes(frame.data()); + let hex_string = Self::array_to_hex_string(frame.data()); + let signal_data = message.parse_from_can(&padding_data); + let _ = self.ui_handle.upgrade_in_event_loop(move |ui| { + let is_filter = ui.get_is_filter(); + let messages: ModelRc = if !is_filter { + ui.get_messages() + } else { + ui.get_filter_messages() + }; + Self::update_ui_with_signals( + &messages, + frame_id, + signal_data, + hex_string, + ); + }); + } } } - } else { - sleep(Duration::from_millis(50)); + Err(e) => { + let _ = self.ui_handle.upgrade_in_event_loop(move |ui| { + if e != PcanError::QrcvEmpty { + ui.set_state(format!("{:?}", e).into()); + } + }); + } } } } @@ -163,7 +220,10 @@ impl<'a> CanHandler<'a> { ) { for (message_count, message) in messages.iter().enumerate() { if message.can_id == format!("{:08X}", frame_id) { + let now = Utc::now().timestamp_micros(); let can_signals = Self::create_can_signals(&message, &signal_data); + let circle_time = + (now - (message.time_stamp).parse::().unwrap()) as f32 / 1000.0; messages.set_row_data( message_count, CanData { @@ -177,6 +237,8 @@ impl<'a> CanHandler<'a> { } else { ODD_COLOR }, + circle_time: format!("{:.02} ms", circle_time).into(), + time_stamp: now.to_string().into(), }, ); break; @@ -240,4 +302,28 @@ impl<'a> CanHandler<'a> { hex_string.pop(); // Remove the trailing space hex_string } + + fn bitrate(&self) -> Option { + let bitrate_map: HashMap<&str, u32> = [ + ("1 Mbit/s", 1_000_000), + ("800 kbit/s", 800_000), + ("500 kbit/s", 500_000), + ("250 kbit/s", 250_000), + ("125 kbit/s", 125_000), + ("100 kbit/s", 100_000), + ("95.238 kbit/s", 95_238), + ("83.333 kbit/s", 83_333), + ("50 kbit/s", 50_000), + ("47.619 kbit/s", 47_619), + ("33.333 kbit/s", 33_333), + ("20 kbit/s", 20_000), + ("10 kbit/s", 10_000), + ("5 kbit/s", 5_000), + ] + .iter() + .cloned() + .collect(); + + bitrate_map.get(self.bitrate.as_str()).copied() + } } diff --git a/src/event_handler/dbc_file.rs b/src/event_handler/dbc_file.rs index 691fe3d..d68e025 100644 --- a/src/event_handler/dbc_file.rs +++ b/src/event_handler/dbc_file.rs @@ -39,6 +39,8 @@ impl<'a> DBCFile<'a> { counter: 0, raw_can: SharedString::from("default"), color: ODD_COLOR, + circle_time: "0.0".into(), + time_stamp: "0".into(), }] .to_vec(), )); @@ -83,6 +85,8 @@ impl<'a> DBCFile<'a> { } else { ODD_COLOR }, + circle_time: "0.0".into(), + time_stamp: "0".into(), }; if message_count == 0 { diff --git a/src/event_handler/mod.rs b/src/event_handler/mod.rs index 965e5dd..a6fd7fd 100644 --- a/src/event_handler/mod.rs +++ b/src/event_handler/mod.rs @@ -5,7 +5,30 @@ pub(crate) mod packet_filter; pub use can_handler::CanHandler; pub use dbc_file::DBCFile; pub use packet_filter::PacketFilter; +#[cfg(target_os = "windows")] +use pcan_basic::socket::Baudrate; use slint::Color; const ODD_COLOR: Color = Color::from_rgb_u8(0x18, 0x1c, 0x27); const EVEN_COLOR: Color = Color::from_rgb_u8(0x13, 0x16, 0x1f); + +#[cfg(target_os = "windows")] +pub fn p_can_bitrate(bitrate: &str) -> Option { + match bitrate { + "1 Mbit/s" => Some(Baudrate::Baud1M), + "800 kbit/s" => Some(Baudrate::Baud800K), + "500 kbit/s" => Some(Baudrate::Baud500K), + "250 kbit/s" => Some(Baudrate::Baud250K), + "125 kbit/s" => Some(Baudrate::Baud125K), + "100 kbit/s" => Some(Baudrate::Baud100K), + "95.238 kbit/s" => Some(Baudrate::Baud95K), + "83.333 kbit/s" => Some(Baudrate::Baud83), + "50 kbit/s" => Some(Baudrate::Baud50K), + "47.619 kbit/s" => Some(Baudrate::Baud47K), + "33.333 kbit/s" => Some(Baudrate::Baud33K), + "20 kbit/s" => Some(Baudrate::Baud20K), + "10 kbit/s" => Some(Baudrate::Baud10K), + "5 kbit/s" => Some(Baudrate::Baud5K), + _ => None, + } +} diff --git a/src/event_handler/packet_filter.rs b/src/event_handler/packet_filter.rs index 29d930b..3ce5842 100644 --- a/src/event_handler/packet_filter.rs +++ b/src/event_handler/packet_filter.rs @@ -29,6 +29,8 @@ impl<'a> PacketFilter<'a> { packet_name: self.filter.packet_name, raw_can: self.filter.raw_can, signal_value: self.filter.signal_value, + circle_time: "0.0".into(), + time_stamp: "0".into(), }); } else { // Remove filter ID diff --git a/src/main.rs b/src/main.rs index caf75cb..c6c220f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,13 +6,13 @@ use std::time::Duration; mod event_handler; use can_dbc::DBC; +#[cfg(target_os = "windows")] +use event_handler::p_can_bitrate; use event_handler::{CanHandler, DBCFile, PacketFilter}; #[cfg(target_os = "windows")] -use pcan_basic::{ - bus::UsbBus, - hw::attached_channels, - socket::{usb::UsbCanSocket, Baudrate}, -}; +use pcan_basic::{bus::UsbBus, hw::attached_channels, socket::usb::UsbCanSocket}; +#[cfg(target_os = "linux")] +use privilege_rs::privilege_request; #[cfg(target_os = "windows")] use slint::Model; use slint::{ModelRc, SharedString, VecModel}; @@ -25,6 +25,8 @@ slint::include_modules!(); #[tokio::main] async fn main() -> io::Result<()> { + #[cfg(target_os = "linux")] + privilege_request(); let ui = AppWindow::new().unwrap(); let (tx, rx) = mpsc::channel::(); @@ -161,7 +163,7 @@ async fn main() -> io::Result<()> { let (start_tx, start_rx) = mpsc::channel(); // Handle start event let ui_handle = ui.as_weak(); - ui.on_start(move |_name, _index| { + ui.on_start(move |_name, _index, bitrate| { // start_tx.send((_name, _index)); #[cfg(target_os = "linux")] { @@ -170,7 +172,7 @@ async fn main() -> io::Result<()> { ui.set_init_string(SharedString::from("No device found!!!")); } else { ui.set_is_init(true); - let _ = start_tx.send(_name); + let _ = start_tx.send((_name, bitrate)); } } #[cfg(target_os = "windows")] @@ -185,10 +187,11 @@ async fn main() -> io::Result<()> { }; let usb_can = UsbBus::try_from(get_device_handle as u16).unwrap(); let ui_handle = ui.as_weak(); - match UsbCanSocket::open(usb_can, Baudrate::Baud250K) { + let baudrate = p_can_bitrate(&bitrate).unwrap(); + match UsbCanSocket::open(usb_can, baudrate) { Ok(socket) => { ui_handle.unwrap().set_is_init(true); - let _ = start_tx.send(socket); + let _ = start_tx.send((socket, bitrate)); } Err(e) => { ui_handle @@ -201,7 +204,7 @@ async fn main() -> io::Result<()> { let ui_handle = ui.as_weak(); tokio::spawn(async move { - if let Ok(can_if) = start_rx.recv() { + if let Ok((can_if, bitrate)) = start_rx.recv() { let mut can_handler = CanHandler { #[cfg(target_os = "windows")] iface: can_if, @@ -209,6 +212,7 @@ async fn main() -> io::Result<()> { iface: &can_if, ui_handle: &ui_handle, mspc_rx: &rx, + bitrate: bitrate.to_string(), }; loop { can_handler.process_can_messages(); diff --git a/ui/app.slint b/ui/app.slint index ebd77b7..a48026b 100644 --- a/ui/app.slint +++ b/ui/app.slint @@ -17,12 +17,15 @@ export component AppWindow inherits Window { in property can_sockets; in property <[CanData]> messages; in property <[CanData]> filter_messages; + in-out property state; + in-out property bus_load; + in-out property bitrate; in-out property active-page: 0; callback open_dbc_file(); callback filter_id(CanData, bool); - callback start(string, int); + callback start(string, int, string); title: @tr("CAN VIEWER (version 0.2.0)"); icon: @image-url("images/can_viewer_128px.png"); background: #1a1f2b; @@ -35,8 +38,8 @@ export component AppWindow inherits Window { initPage { out: init_string; can_sockets: can_sockets; - start(name, index) => { - start(name, index) + start(name, index, bitrate) => { + start(name, index, bitrate); } } @@ -96,6 +99,9 @@ export component AppWindow inherits Window { } if root.active-page == 0: viewPage { + state: state; + bitrate: bitrate; + bus_load: bus_load; page-num: 0; is_filter: root.is_filter; messages: root.messages; diff --git a/ui/init_page.slint b/ui/init_page.slint index a01ced6..3ba2539 100644 --- a/ui/init_page.slint +++ b/ui/init_page.slint @@ -12,7 +12,7 @@ export component initPage inherits Rectangle { background: #1a1f2b; in property can_sockets; in property out: "Please select CAN device to start"; - callback start(string, int); + callback start(string /* name */, int /* index */, string /* */); VerticalLayout { Rectangle {} Text { @@ -31,10 +31,16 @@ export component initPage inherits Rectangle { current-value: can_sockets.name[0]; } } + bitrate_box := ComboBox { + model: ["1 Mbit/s", "800 kbit/s", "500 kbit/s", "250 kbit/s", "125 kbit/s", + "100 kbit/s", "95.238 kbit/s", "83.333 kbit/s", "50 kbit/s", "47.619 kbit/s", + "33.333 kbit/s", "20 kbit/s", "10 kbit/s", "5 kbit/s"]; + current-value: "250 kbit/s"; + } Button { text: "start"; clicked => { - start(socket_can_box.current-value, socket_can_box.current-index); + start(socket_can_box.current-value, socket_can_box.current-index, bitrate_box.current-value); } } Rectangle {} diff --git a/ui/messages.slint b/ui/messages.slint index 3060c21..61f3c4c 100644 --- a/ui/messages.slint +++ b/ui/messages.slint @@ -12,6 +12,8 @@ export struct CanData { packet_name: string, raw_can: string, counter: int, + time_stamp: string, + circle_time: string, signal_value: [CanSignal], color: color } @@ -21,15 +23,16 @@ export component CanMessage inherits VerticalLayout { in property raw_data: "0x01 0x02 0x03 0x04"; in property message_name: "packet_xxx"; in property counter: 0; + in property circle_time: 0; in property back_ground; in property <[CanSignal]> signals: [ - {signal_name: "signal_1", signal_value: "100", unit: "Hz", factor: "1.0"}, - {signal_name: "signal_2", signal_value: "999", unit: "Km", factor: "1.0"}, - {signal_name: "signal_3", signal_value: "203", unit: "second", factor: "1.0"}, - {signal_name: "signal_4", signal_value: "15.6", unit: "mWh", factor: "1.0"}, - {signal_name: "signal_5", signal_value: "20.9", unit: "A", factor: "1.0"}, - {signal_name: "signal_6", signal_value: "10", unit: "Volt", factor: "1.0"}, - {signal_name: "signal_7", signal_value: "1", unit: "mA", factor: "1.0"}]; + {signal_name: "signal_1", signal_value: "100", unit: "Hz", factor: "1.0", circle_time: "1 ms"}, + {signal_name: "signal_2", signal_value: "999", unit: "Km", factor: "1.0", circle_time: "1 ms"}, + {signal_name: "signal_3", signal_value: "203", unit: "second", factor: "1.0", circle_time: "1 ms"}, + {signal_name: "signal_4", signal_value: "15.6", unit: "mWh", factor: "1.0", circle_time: "1 ms"}, + {signal_name: "signal_5", signal_value: "20.9", unit: "A", factor: "1.0", circle_time: "1 ms"}, + {signal_name: "signal_6", signal_value: "10", unit: "Volt", factor: "1.0", circle_time: "1 ms"}, + {signal_name: "signal_7", signal_value: "1", unit: "mA", factor: "1.0", circle_time: "1 ms"}]; height: (signals.length < 3)? 75px: signals.length * 25px; Rectangle { background: back_ground; @@ -70,7 +73,7 @@ export component CanMessage inherits VerticalLayout { } Rectangle { - width: root.width * 40%; + width: root.width * 30%; border-color: white; border-width: 0.25px; Text { @@ -80,6 +83,17 @@ export component CanMessage inherits VerticalLayout { color: white; } } + Rectangle { + width: root.width * 10%; + border-color: white; + border-width: 0.25px; + Text { + text: root.circle_time; + horizontal-alignment: center; + vertical-alignment: center; + color: white; + } + } } } } diff --git a/ui/view_page.slint b/ui/view_page.slint index b2197d3..11f444c 100644 --- a/ui/view_page.slint +++ b/ui/view_page.slint @@ -7,6 +7,9 @@ export component viewPage inherits Rectangle { in-out property <[CanData]> messages; in-out property <[CanData]> filter_messages; in-out property page-num; + in-out property state; + in-out property bus_load: 0; + in-out property bitrate: 0; callback open_dbc_file(); VerticalLayout { HorizontalLayout { @@ -18,6 +21,29 @@ export component viewPage inherits Rectangle { } } Rectangle {} + Rectangle { + Text { + text: "State: " + state; + color: white; + } + } + Rectangle {} + Rectangle { + Text { + text: "Bitrate: " + bitrate; + color: white; + } + } + Rectangle {} + Rectangle { + Text { + text: "Bus Load: " + bus_load + "%"; + color: white; + } + } + Rectangle { + width: 50px; + } } Rectangle { height: 1px; @@ -27,6 +53,7 @@ export component viewPage inherits Rectangle { ListView { for message in messages: CanMessage { message_id: message.can-id; + circle_time: message.circle-time; message_name: message.packet-name; counter: message.counter; signals: message.signal-value; @@ -38,6 +65,7 @@ export component viewPage inherits Rectangle { ListView { for message in filter_messages: CanMessage { message_id: message.can-id; + circle_time: message.circle-time; message_name: message.packet-name; counter: message.counter; signals: message.signal-value;