From 9f0dbdd46cfcfe71c5d6ae03920aa950d10d624e Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 7 Aug 2023 14:28:20 -0500 Subject: [PATCH 1/7] Add optional tracing feature for development logging --- Cargo.toml | 8 +- examples/forward.rs | 19 ++-- src/driver/socketcan.rs | 32 +++++- src/lib.rs | 1 + src/tracing.rs | 228 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 269 insertions(+), 19 deletions(-) create mode 100644 src/tracing.rs diff --git a/Cargo.toml b/Cargo.toml index 5d79d06..4185d23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,18 +9,20 @@ keywords = ["agriculture", "can", "canbus", "isobus", "j1939", "agritech", "smar [dependencies] rand = "0.8.5" socketcan = { version = "2.0.0", optional = true } +tracing = { version = "0.1.37", optional = true } [features] default = [] +# Socketcan driver socketcan = ["dep:socketcan"] +# Optional logging instrumentation +tracing = ["dep:tracing"] [dev-dependencies] clap = { version = "4.3.19", features = ["derive"] } ctrlc = "3.4.0" -# TODO: Add optional tracing to the main library -tracing = "0.1.37" tracing-subscriber = "0.3.17" [[example]] name = "forward" -required-features = ["socketcan"] +required-features = ["socketcan", "tracing"] diff --git a/examples/forward.rs b/examples/forward.rs index a90c5fd..e843bca 100644 --- a/examples/forward.rs +++ b/examples/forward.rs @@ -2,7 +2,8 @@ use std::sync::mpsc::channel; -use ag_iso_stack::driver::{Driver, DriverReadError, Frame, SocketcanDriver}; +use ag_iso_stack::driver::{Driver, Frame, SocketcanDriver}; +use ag_iso_stack::tracing; use clap::Parser; /// Forward CAN traffic from one interface to another @@ -38,7 +39,7 @@ fn main() { let opts = Options::parse(); let subscriber = tracing_subscriber::fmt() - // ... add configuration + .with_max_level(opts.log_level) .finish(); tracing::subscriber::set_global_default(subscriber) .map_err(|_err| eprintln!("Unable to set global default subscriber")) @@ -66,17 +67,11 @@ fn main() { let mut frame = Frame::default(); - match input.read_nonblocking(&mut frame) { - Ok(_) => { - tracing::info!("Read frame: {frame:?}"); - tracing::info!("Attempting to write frame"); - match output.write_nonblocking(&frame) { - Ok(_) => tracing::info!("Wrote frame: {frame:?}"), - Err(e) => tracing::info!("Failed to write frame: {e:?}"), - } + #[allow(clippy::collapsible_if)] + if input.read_nonblocking(&mut frame).is_ok() { + if output.write_nonblocking(&frame).is_err() { + break; } - Err(DriverReadError::NoFrameReady) => {} - Err(e) => tracing::error!("Failed to read frame: {e:?}"), } } } diff --git a/src/driver/socketcan.rs b/src/driver/socketcan.rs index 09b0e35..4d41b3a 100644 --- a/src/driver/socketcan.rs +++ b/src/driver/socketcan.rs @@ -8,6 +8,7 @@ use crate::driver::{ CanId, Channel, Driver, DriverCloseError, DriverOpenError, DriverReadError, DriverWriteError, Frame as InternalFrame, Type, }; +use crate::tracing; impl From for DriverReadError { fn from(e: socketcan::Error) -> DriverReadError { @@ -39,6 +40,7 @@ impl From<&InternalFrame> for socketcan::frame::CanDataFrame { } } +#[derive(Debug, Clone, PartialEq, Eq)] enum SocketcanIface { Name(String), Index(u32), @@ -116,9 +118,17 @@ impl Driver for SocketcanDriver { self.sock.is_some() } fn open(&mut self) -> Result<(), DriverOpenError> { - match &self.iface { - SocketcanIface::Name(s) => self.sock = Some(CanSocket::open(s)?), - SocketcanIface::Index(i) => self.sock = Some(CanSocket::open_iface(*i)?), + tracing::info!("Opening interface {:?}", self.iface); + let result = match &self.iface { + SocketcanIface::Name(s) => CanSocket::open(s), + SocketcanIface::Index(i) => CanSocket::open_iface(*i), + }; + match result { + Ok(sock) => self.sock = Some(sock), + Err(e) => { + tracing::error!("Error '{e:?}' opening interface {:?}", self.iface); + return Err(e.into()); + } } self.opened_timestamp = Instant::now(); @@ -127,6 +137,7 @@ impl Driver for SocketcanDriver { Ok(()) } fn close(&mut self) -> Result<(), DriverCloseError> { + tracing::info!("Closing interface {:?}", self.iface); self.sock = None; Ok(()) } @@ -136,18 +147,31 @@ impl Driver for SocketcanDriver { /// The timestamp on the frame is the duration since [`open`](Self::open) was last called. fn read_nonblocking(&mut self, frame: &mut InternalFrame) -> Result<(), DriverReadError> { let Some(sock) = self.sock.as_mut() else { + tracing::warn!("Failed to read from closed interface {:?}", self.iface); return Err(DriverReadError::DriverClosed); }; let socketcan_frame = sock.read_frame()?; *frame = self.to_frame(socketcan_frame); + tracing::trace!("Read frame {frame:?} from interface {:?}", self.iface); Ok(()) } fn write_nonblocking(&mut self, frame: &InternalFrame) -> Result<(), DriverWriteError> { let Some(sock) = self.sock.as_mut() else { + tracing::warn!("Tried to write to closed interface {:?}", self.iface); return Err(DriverWriteError::DriverClosed); }; let socketcan_frame: socketcan::frame::CanDataFrame = frame.into(); - sock.write_frame(&socketcan_frame)?; + match sock.write_frame(&socketcan_frame) { + Ok(_) => tracing::trace!("Wrote frame {frame:?} to interface {:?}", self.iface), + Err(_e) => { + if _e.kind() != std::io::ErrorKind::WouldBlock { + tracing::error!( + "Error '{_e:?}' writing frame {frame:?} to interface {:?}", + self.iface + ); + } + } + } Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 2b835fb..dcbcb6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,4 @@ pub mod driver; pub mod network_management; +pub mod tracing; diff --git a/src/tracing.rs b/src/tracing.rs new file mode 100644 index 0000000..4ed0748 --- /dev/null +++ b/src/tracing.rs @@ -0,0 +1,228 @@ +//! # Tracing +//! +//! AgIsoStack-rs provides tracing under the non-default `tracing` Cargo feature. Tracing is +//! intended for use by Crate maintainers for introspection. We use the [tracing] crate, but rather +//! than invoke `tracing`s macros directly, we provide our own shim layer, so that we don't need to +//! sprinkle `#[cfg(feature = "tracing")]` a bazillion places throughout the crate. + +#[cfg(feature = "tracing")] +pub use tracing::*; + +/// A conditional compilation shim around [tracing::debug!] +// NOTE: I tried to make this mess less gross by implementing the macro like +// +// macro_rules! debug { +// ( $($all_tokens:tt)* ) => { +// #[cfg(feature = "tracing")] +// ::tracing::debug!($($all_tokens)*) +// }; +// } +// +// with various combinations of semi-colons and curly braces. No matter what, I couldn't find a way +// to get this macro to correctly expand when using the log statements as expressions as in +// +// match foo() { +// Ok(x) => tracing::debug!("Ok: {x:?}"), +// Err(e) => tracing::debug("Err: {e:?}"), +// } +// +// So here, I hoist the #[cfg] attribute up a level to the actual macro definition. It's gross, way +// more typing than I wanted, but it works. +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __debug { + ( $($all_tokens:tt)* ) => { ::tracing::debug!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __debug { + ( $($all_tokens:tt)* ) => {{}}; +} +// HACK: This is a gross and dirty hack that shouldn't need to exist. When you use #[macro_export], +// that exports the macro under the top-level crate module for technical reasons (I think because +// when macros are expanded the module tree doesn't exist yet?). This re-export trick came from the +// rust-analyzer source code (thanks matklad!) and makes these macros available under the +// crate::tracing module. +pub use __debug as debug; + +/// A conditional compilation shim around [tracing::debug_span!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __debug_span { + ( $($all_tokens:tt)* ) => { ::tracing::debug_span!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __debug_span { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __debug_span as debug_span; + +/// A conditional compilation shim around [tracing::enabled!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __enabled { + ( $($all_tokens:tt)* ) => { ::tracing::enabled!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __enabled { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __enabled as enabled; + +/// A conditional compilation shim around [tracing::error!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __error { + ( $($all_tokens:tt)* ) => { ::tracing::error!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __error { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __error as error; + +/// A conditional compilation shim around [tracing::error_span!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __error_span { + ( $($all_tokens:tt)* ) => { ::tracing::error_span!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __error_span { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __error_span as error_span; + +/// A conditional compilation shim around [tracing::event!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __event { + ( $($all_tokens:tt)* ) => { ::tracing::event!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __event { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __event as event; + +/// A conditional compilation shim around [tracing::event_enabled!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __event_enabled { + ( $($all_tokens:tt)* ) => { ::tracing::event_enabled!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __event_enabled { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __event_enabled as event_enabled; + +/// A conditional compilation shim around [tracing::info!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __info { + ( $($all_tokens:tt)* ) => { ::tracing::info!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __info { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __info as info; + +/// A conditional compilation shim around [tracing::info_span!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __info_span { + ( $($all_tokens:tt)* ) => { ::tracing::info_span!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __info_span { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __info_span as info_span; + +/// A conditional compilation shim around [tracing::span!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __span { + ( $($all_tokens:tt)* ) => { ::tracing::span!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __span { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __span as span; + +/// A conditional compilation shim around [tracing::span_enabled!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __span_enabled { + ( $($all_tokens:tt)* ) => { ::tracing::span_enabled!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __span_enabled { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __span_enabled as span_enabled; + +/// A conditional compilation shim around [tracing::trace!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __trace { + ( $($all_tokens:tt)* ) => { ::tracing::trace!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __trace { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __trace as trace; + +/// A conditional compilation shim around [tracing::trace_span!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __trace_span { + ( $($all_tokens:tt)* ) => { ::tracing::trace_span!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __trace_span { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __trace_span as trace_span; + +/// A conditional compilation shim around [tracing::warn!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __warn { + ( $($all_tokens:tt)* ) => { ::tracing::warn!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __warn { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __warn as warn; + +/// A conditional compilation shim around [tracing::warn_span!] +#[macro_export] +#[cfg(feature = "tracing")] +macro_rules! __warn_span { +( $($all_tokens:tt)* ) => { ::tracing::warn_span!($($all_tokens)*) }; +} +#[macro_export] +#[cfg(not(feature = "tracing"))] +macro_rules! __warn_span { + ( $($all_tokens:tt)* ) => {{}}; +} +pub use __warn_span as warn_span; From dc2d922e75f319119a25f12092e682d31df56ceb Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 6 Nov 2023 20:12:14 -0600 Subject: [PATCH 2/7] clippy: Fix unwrap-or-default lint I suspect this lint was added to clippy since we last merged to main, and that the CI/CD pipeline uses the latest clippy release. --- src/network_management/network_manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network_management/network_manager.rs b/src/network_management/network_manager.rs index 7314bd1..764e08d 100644 --- a/src/network_management/network_manager.rs +++ b/src/network_management/network_manager.rs @@ -158,7 +158,7 @@ impl NetworkManager { self.get_control_function_address_by_name(destination.get_name()), priority, ) - .unwrap_or(CanId::default()); + .unwrap_or_default(); if message_id.raw() != CanId::default().raw() { self.enqueue_can_message( From 6ea247679e0d1065682da9a04453ebdd7c8c2dc1 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 6 Nov 2023 20:33:02 -0600 Subject: [PATCH 3/7] Whitespace cleanup --- src/network_management/control_function.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/network_management/control_function.rs b/src/network_management/control_function.rs index 34999a6..51d94b8 100644 --- a/src/network_management/control_function.rs +++ b/src/network_management/control_function.rs @@ -12,23 +12,23 @@ use super::network_manager::{MessageQueuePriority, NetworkManager}; pub enum AddressClaimingState { /// Address claiming is uninitialized None, - /// State machine is waiting for the random delay time + /// State machine is waiting for the random delay time WaitForClaim, - /// State machine is sending the request for address claim + /// State machine is sending the request for address claim SendRequestForClaim, - /// State machine is waiting for the address claim contention period + /// State machine is waiting for the address claim contention period WaitForRequestContentionPeriod, /// State machine is claiming the preferred address SendPreferredAddressClaim, - /// State machine is contending the preferred address + /// State machine is contending the preferred address ContendForPreferredAddress, - /// State machine is claiming an address + /// State machine is claiming an address SendArbitraryAddressClaim, - /// An ECU requested address claim, inform the bus of our current address + /// An ECU requested address claim, inform the bus of our current address SendReclaimAddressOnRequest, /// State machine could not claim an address UnableToClaim, - /// Address claiming is complete and we have an address + /// Address claiming is complete and we have an address AddressClaimingComplete, } From c2200b11d062b38ae2024628a7b499d6fc22938b Mon Sep 17 00:00:00 2001 From: Jannes Brands Date: Tue, 8 Aug 2023 11:41:38 -0500 Subject: [PATCH 4/7] Add Peak CAN driver builds, driver works, no functional test (no pcan available) --------- Co-authored-by: Jannes Brands Co-authored-by: Austin Gill --- Cargo.toml | 6 +- examples/forward.rs | 73 +++++++-- src/driver/driver.rs | 18 +++ src/driver/mod.rs | 9 +- src/driver/peak.rs | 357 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 452 insertions(+), 11 deletions(-) create mode 100644 src/driver/peak.rs diff --git a/Cargo.toml b/Cargo.toml index 4185d23..d2b5980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,14 @@ keywords = ["agriculture", "can", "canbus", "isobus", "j1939", "agritech", "smar [dependencies] rand = "0.8.5" +pcan-basic = { version = "1.0.2", optional = true } socketcan = { version = "2.0.0", optional = true } tracing = { version = "0.1.37", optional = true } [features] default = [] +# Peak driver +peak = ["dep:pcan-basic"] # Socketcan driver socketcan = ["dep:socketcan"] # Optional logging instrumentation @@ -21,8 +24,9 @@ tracing = ["dep:tracing"] [dev-dependencies] clap = { version = "4.3.19", features = ["derive"] } ctrlc = "3.4.0" +tracing = "0.1.37" tracing-subscriber = "0.3.17" [[example]] name = "forward" -required-features = ["socketcan", "tracing"] +required-features = ["tracing"] diff --git a/examples/forward.rs b/examples/forward.rs index e843bca..a71ef8f 100644 --- a/examples/forward.rs +++ b/examples/forward.rs @@ -2,9 +2,46 @@ use std::sync::mpsc::channel; -use ag_iso_stack::driver::{Driver, Frame, SocketcanDriver}; +#[cfg(feature = "peak")] +use ag_iso_stack::driver::PeakDriver; +#[cfg(feature = "socketcan")] +use ag_iso_stack::driver::SocketcanDriver; +use ag_iso_stack::driver::{Driver, Frame}; use ag_iso_stack::tracing; -use clap::Parser; +use clap::{Parser, ValueEnum}; +#[cfg(feature = "peak")] +use pcan_basic::bus::UsbBus; + +fn parse_usb_bus(s: &str) -> Option { + let s = s.to_uppercase(); + match s.as_str() { + "USB1" => Some(UsbBus::USB1), + "USB2" => Some(UsbBus::USB2), + "USB3" => Some(UsbBus::USB3), + "USB4" => Some(UsbBus::USB4), + "USB5" => Some(UsbBus::USB5), + "USB6" => Some(UsbBus::USB6), + "USB7" => Some(UsbBus::USB7), + "USB8" => Some(UsbBus::USB8), + "USB9" => Some(UsbBus::USB9), + "USB10" => Some(UsbBus::USB10), + "USB11" => Some(UsbBus::USB11), + "USB12" => Some(UsbBus::USB12), + "USB13" => Some(UsbBus::USB13), + "USB14" => Some(UsbBus::USB14), + "USB15" => Some(UsbBus::USB15), + "USB16" => Some(UsbBus::USB16), + _ => None, + } +} + +#[derive(Debug, Clone, ValueEnum)] +enum CanDriver { + #[cfg(feature = "socketcan")] + Socketcan, + #[cfg(feature = "peak")] + Pcan, +} /// Forward CAN traffic from one interface to another #[derive(Debug, Parser)] @@ -25,13 +62,29 @@ struct Options { /// Can be either a string interface name, or an integer interface index #[clap(short, long, default_value_t = String::from("can1"))] pub output_interface: String, + + #[clap(short, long)] + pub driver: CanDriver, } -fn create_driver(iface: &str) -> impl Driver { - if let Ok(index) = iface.parse::() { - SocketcanDriver::new_by_index(index) - } else { - SocketcanDriver::new_by_name(iface) +fn create_driver(iface: &str, driver: CanDriver) -> Box { + match driver { + #[cfg(feature = "socketcan")] + CanDriver::Socketcan => { + if let Ok(index) = iface.parse::() { + Box::new(SocketcanDriver::new_by_index(index)) + } else { + Box::new(SocketcanDriver::new_by_name(iface)) + } + } + #[cfg(feature = "peak")] + CanDriver::Pcan => { + let bus = parse_usb_bus(iface).unwrap(); + let baud = ag_iso_stack::driver::Baudrate::Baud250K; + Box::new(PeakDriver::new_usb(bus, baud)) + } + #[allow(unreachable_patterns)] + _ => unreachable!(), } } @@ -45,14 +98,16 @@ fn main() { .map_err(|_err| eprintln!("Unable to set global default subscriber")) .unwrap(); + tracing::info!("AgIsoStack-rs example starts..."); + tracing::info!( "Forwarding CAN traffic from {} to {}", opts.input_interface, opts.output_interface ); - let mut input = create_driver(&opts.input_interface); - let mut output = create_driver(&opts.output_interface); + let mut input = create_driver(&opts.input_interface, opts.driver.clone()); + let mut output = create_driver(&opts.output_interface, opts.driver); input.open().unwrap(); output.open().unwrap(); diff --git a/src/driver/driver.rs b/src/driver/driver.rs index fa7d0dc..1dc6eec 100644 --- a/src/driver/driver.rs +++ b/src/driver/driver.rs @@ -1,6 +1,24 @@ // Copyright 2023 Raven Industries inc. use crate::driver::Frame; +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum Baudrate { + Baud1M, + Baud800K, + Baud500K, + Baud250K, + Baud125K, + Baud100K, + Baud95K, + Baud83K, + Baud50K, + Baud47K, + Baud33K, + Baud20K, + Baud10K, + Baud5K, +} + #[derive(Debug)] #[non_exhaustive] pub enum DriverOpenError { diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 0e39c02..0646413 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -15,11 +15,18 @@ mod pgn; #[cfg(feature = "socketcan")] mod socketcan; +#[cfg(feature = "peak")] +mod peak; + pub use address::Address; pub use can_id::{CanId, Priority, Type}; -pub use driver::{Driver, DriverCloseError, DriverOpenError, DriverReadError, DriverWriteError}; +pub use driver::{ + Baudrate, Driver, DriverCloseError, DriverOpenError, DriverReadError, DriverWriteError, +}; pub use frame::{Channel, Frame}; pub use pgn::Pgn; +#[cfg(feature = "peak")] +pub use self::peak::PeakDriver; #[cfg(feature = "socketcan")] pub use self::socketcan::SocketcanDriver; diff --git a/src/driver/peak.rs b/src/driver/peak.rs new file mode 100644 index 0000000..9bbadb1 --- /dev/null +++ b/src/driver/peak.rs @@ -0,0 +1,357 @@ +// Copyright 2023 Raven Industries inc. +use std::io::ErrorKind; +use std::time::Instant; + +use pcan_basic::bus::{DngBus, IsaBus, LanBus, PccBus, PciBus, UsbBus}; +use pcan_basic::error::PcanError; +use pcan_basic::socket::dng::DngCanSocket; +use pcan_basic::socket::isa::IsaCanSocket; +use pcan_basic::socket::lan::LanCanSocket; +use pcan_basic::socket::pcc::PccCanSocket; +use pcan_basic::socket::pci::PciCanSocket; +use pcan_basic::socket::usb::UsbCanSocket; +use pcan_basic::socket::{Baudrate as PeakBaudrate, CanFrame, MessageType, RecvCan, SendCan}; + +use crate::driver::{ + Baudrate, CanId, Channel, Driver, DriverCloseError, DriverOpenError, DriverReadError, + DriverWriteError, Frame as InternalFrame, Type, +}; + +impl From for DriverOpenError { + fn from(e: PcanError) -> DriverOpenError { + match e { + PcanError::RegTest => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::Other, + "Test of the CAN controller hardware registers failed (no hardware found)", + )), + PcanError::NoDriver => { + DriverOpenError::IoError(std::io::Error::new(ErrorKind::Other, "Driver not loaded")) + } + PcanError::HwInUse => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::AddrInUse, + "Hardware already in use by a Net", + )), + PcanError::NetInUse => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::AddrInUse, + "A Client is already connected to the Net", + )), + PcanError::IllHw => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::Other, + "Hardware handle is invalid", + )), + PcanError::IllNet => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::Other, + "Net handle is invalid", + )), + PcanError::IllClient => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::Other, + "Client handle is invalid", + )), + PcanError::Resource => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::Other, + "Resource (FIFO, Client, timeout) cannot be created", + )), + PcanError::IllParamType => { + DriverOpenError::IoError(std::io::Error::new(ErrorKind::Other, "Invalid parameter")) + } + PcanError::IllParamVal => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::Other, + "Invalid parameter value", + )), + PcanError::Unknown => { + DriverOpenError::IoError(std::io::Error::new(ErrorKind::Other, "Unknown error")) + } + PcanError::IllData => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::InvalidData, + "Invalid data, function, or action", + )), + PcanError::IllMode => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::Other, + "Driver object state is wrong for the attempted operation", + )), + PcanError::Initialize => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::Other, + "Channel is not initialized [Value was changed from 0x40000 to 0x4000000]", + )), + PcanError::IllOperation => DriverOpenError::IoError(std::io::Error::new( + ErrorKind::Other, + "Invalid operation [Value was changed from 0x80000 to 0x8000000]", + )), + _ => DriverOpenError::IoError(std::io::Error::new(ErrorKind::Other, "Unknown")), + } + } +} + +impl From for DriverReadError { + fn from(e: PcanError) -> DriverReadError { + match e { + PcanError::QrcvEmpty | PcanError::QOverrun => DriverReadError::NoFrameReady, + PcanError::Resource => DriverReadError::IoError(std::io::Error::new( + ErrorKind::Other, + "Resource (FIFO, Client, timeout) cannot be created", + )), + PcanError::IllParamType => { + DriverReadError::IoError(std::io::Error::new(ErrorKind::Other, "Invalid parameter")) + } + PcanError::IllParamVal => DriverReadError::IoError(std::io::Error::new( + ErrorKind::Other, + "Invalid parameter value", + )), + PcanError::Unknown => { + DriverReadError::IoError(std::io::Error::new(ErrorKind::Other, "Unknown error")) + } + PcanError::IllData => DriverReadError::IoError(std::io::Error::new( + ErrorKind::InvalidData, + "Invalid data, function, or action", + )), + _ => DriverReadError::IoError(std::io::Error::new(ErrorKind::Other, "Unknown")), + } + } +} + +impl From for DriverWriteError { + fn from(e: PcanError) -> DriverWriteError { + match e { + PcanError::AnyBusErr => DriverWriteError::BusError(), + PcanError::XmtFull | PcanError::QxmtFull => DriverWriteError::NotReady, + PcanError::IllOperation => DriverWriteError::IoError(std::io::Error::new( + ErrorKind::Other, + "Invalid operation [Value was changed from 0x80000 to 0x8000000]", + )), + _ => DriverWriteError::IoError(std::io::Error::new(ErrorKind::Other, "Unknown")), + } + } +} + +impl From<&InternalFrame> for CanFrame { + fn from(f: &InternalFrame) -> CanFrame { + let msg_type = match f.id.type_() { + Type::Standard => MessageType::Standard, + Type::Extended => MessageType::Extended, + }; + CanFrame::new( + f.id.raw(), + msg_type, + &f.data[..f.data_length.min(8) as usize], + ) + // guaranteed to not crash, because `f.data` is an [u8; 8] + .expect("Can frame had too much data") + } +} + +// It's unfortunate that we have to duplicate pcan's baudrate enum, but it doesn't implement Clone +// or Debug, so we have to. +impl From for PeakBaudrate { + fn from(b: Baudrate) -> PeakBaudrate { + match b { + Baudrate::Baud1M => PeakBaudrate::Baud1M, + Baudrate::Baud800K => PeakBaudrate::Baud800K, + Baudrate::Baud500K => PeakBaudrate::Baud500K, + Baudrate::Baud250K => PeakBaudrate::Baud250K, + Baudrate::Baud125K => PeakBaudrate::Baud125K, + Baudrate::Baud100K => PeakBaudrate::Baud100K, + Baudrate::Baud95K => PeakBaudrate::Baud95K, + Baudrate::Baud83K => PeakBaudrate::Baud83, + Baudrate::Baud50K => PeakBaudrate::Baud50K, + Baudrate::Baud47K => PeakBaudrate::Baud47K, + Baudrate::Baud33K => PeakBaudrate::Baud33K, + Baudrate::Baud20K => PeakBaudrate::Baud20K, + Baudrate::Baud10K => PeakBaudrate::Baud10K, + Baudrate::Baud5K => PeakBaudrate::Baud5K, + } + } +} + +enum PeakIface { + Dng(DngBus), + Isa(IsaBus), + Lan(LanBus), + Pcc(PccBus), + Pci(PciBus), + Usb(UsbBus), +} + +enum PeakSocket { + Dng(DngCanSocket), + Isa(IsaCanSocket), + Lan(LanCanSocket), + Pcc(PccCanSocket), + Pci(PciCanSocket), + Usb(UsbCanSocket), +} + +/// PCan Basic Driver [Driver] +/// +/// Enabled with the optional `pcan-basic` feature +pub struct PeakDriver { + iface: PeakIface, + baudrate: Baudrate, + socket: Option, + opened_timestamp: Instant, +} + +impl PeakDriver { + pub fn new_dng(if_bus: DngBus, baudrate: Baudrate) -> Self { + Self { + iface: PeakIface::Dng(if_bus), + baudrate, + socket: None, + opened_timestamp: Instant::now(), + } + } + + pub fn new_isa(if_bus: IsaBus, baudrate: Baudrate) -> Self { + Self { + iface: PeakIface::Isa(if_bus), + baudrate, + socket: None, + opened_timestamp: Instant::now(), + } + } + + pub fn new_lan(if_bus: LanBus, baudrate: Baudrate) -> Self { + Self { + iface: PeakIface::Lan(if_bus), + baudrate, + socket: None, + opened_timestamp: Instant::now(), + } + } + + pub fn new_pcc(if_bus: PccBus, baudrate: Baudrate) -> Self { + Self { + iface: PeakIface::Pcc(if_bus), + socket: None, + baudrate, + opened_timestamp: Instant::now(), + } + } + + pub fn new_pci(if_bus: PciBus, baudrate: Baudrate) -> Self { + Self { + iface: PeakIface::Pci(if_bus), + baudrate, + socket: None, + opened_timestamp: Instant::now(), + } + } + + pub fn new_usb(if_bus: UsbBus, baudrate: Baudrate) -> Self { + Self { + iface: PeakIface::Usb(if_bus), + baudrate, + socket: None, + opened_timestamp: Instant::now(), + } + } + + fn to_frame(&self, frame: CanFrame) -> InternalFrame { + let timestamp = self.opened_timestamp.elapsed(); + let raw_id = frame.can_id(); + let extended = frame.is_extended_frame(); + let frame_type = if extended { + Type::Extended + } else { + Type::Standard + }; + + let id = CanId::new(raw_id, frame_type); + // TODO: The Driver trait doesn't know anything about Channels yet. + // + // The channel exists so that we can tie Frames and CANMessages back to the network + // manager they originated from. This channel value should be passed to the Driver + // when it's created (or opened?) + let channel = Channel::default(); + let mut data = [0; 8]; + let data_length = usize::from(frame.dlc().min(8)); + data[..data_length].copy_from_slice(frame.data()); + let data_length = data_length as u8; + + InternalFrame { + timestamp, + id, + channel, + data, + data_length, + extended, + } + } +} + +impl Driver for PeakDriver { + fn is_valid(&self) -> bool { + self.socket.is_some() + } + fn open(&mut self) -> Result<(), DriverOpenError> { + self.socket = match &self.iface { + PeakIface::Dng(dng) => Some(PeakSocket::Dng(DngCanSocket::open( + *dng, + PeakBaudrate::from(self.baudrate), + )?)), + PeakIface::Isa(isa) => Some(PeakSocket::Isa(IsaCanSocket::open( + *isa, + PeakBaudrate::from(self.baudrate), + )?)), + PeakIface::Lan(lan) => Some(PeakSocket::Lan(LanCanSocket::open( + *lan, + PeakBaudrate::from(self.baudrate), + )?)), + PeakIface::Pcc(pcc) => Some(PeakSocket::Pcc(PccCanSocket::open( + *pcc, + PeakBaudrate::from(self.baudrate), + )?)), + PeakIface::Pci(pci) => Some(PeakSocket::Pci(PciCanSocket::open( + *pci, + PeakBaudrate::from(self.baudrate), + )?)), + PeakIface::Usb(usb) => Some(PeakSocket::Usb(UsbCanSocket::open( + *usb, + PeakBaudrate::from(self.baudrate), + )?)), + }; + self.opened_timestamp = Instant::now(); + + Ok(()) + } + fn close(&mut self) -> Result<(), DriverCloseError> { + self.socket = None; + Ok(()) + } + + /// Read a frame from the driver, if possible + /// + /// The timestamp on the frame is the duration since [`open`](Self::open) was last called. + fn read_nonblocking(&mut self, frame: &mut InternalFrame) -> Result<(), DriverReadError> { + let Some(sock) = self.socket.as_mut() else { + return Err(DriverReadError::DriverClosed); + }; + let peak_frame = match sock { + PeakSocket::Dng(dng) => dng.recv()?, + PeakSocket::Isa(isa) => isa.recv()?, + PeakSocket::Lan(lan) => lan.recv()?, + PeakSocket::Pcc(pcc) => pcc.recv()?, + PeakSocket::Pci(pci) => pci.recv()?, + PeakSocket::Usb(usb) => usb.recv()?, + }; + + *frame = self.to_frame(peak_frame.0); + Ok(()) + } + fn write_nonblocking(&mut self, frame: &InternalFrame) -> Result<(), DriverWriteError> { + let Some(sock) = self.socket.as_mut() else { + return Err(DriverWriteError::DriverClosed); + }; + let peak_frame: CanFrame = frame.into(); + + match sock { + PeakSocket::Dng(dng) => dng.send(peak_frame)?, + PeakSocket::Isa(isa) => isa.send(peak_frame)?, + PeakSocket::Lan(lan) => lan.send(peak_frame)?, + PeakSocket::Pcc(pcc) => pcc.send(peak_frame)?, + PeakSocket::Pci(pci) => pci.send(peak_frame)?, + PeakSocket::Usb(usb) => usb.send(peak_frame)?, + }; + + Ok(()) + } +} From 753c5aa3ac29c02517d171c6da0a07341cee99f3 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 6 Nov 2023 21:47:31 -0600 Subject: [PATCH 5/7] Document project feature matrix in the README --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index cf7890a..59b95e6 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,42 @@ Tests for this library are run with Cargo ```sh cargo test ``` + +## Features + +This crate provides multiple optional features that allow the user to pick and choose what they want +to compile. + +| Feature name | Description | Enabled by default | +|--------------|----------------------------------------------------------------|--------------------| +| `peak` | Enables the `ag_iso_stack::driver::PeakDriver` | No | +| `socketcan` | Enables the `ag_iso_stack::driver::SocketcanDriver` | No | +| `tracing` | Enables developer diagnostic logging using the `tracing` crate | No | + +Pass these features to Cargo when building like: `cargo build --features socketcan,tracing`. + +### PCAN-Basic +Note that the `peak` CAN driver requires kernel support (should be enabled by default) _and_ the +`pcanbasic` library, for which there does not seem to be support for in the Ubuntu or Fedora package +repositories. + +For Linux, you'll have to build and install from source after downloading the library here: + + +```sh +## Fedora: +# sudo dnf install kernel-devel popt-devel +sudo apt install linux-headers-generic libpopt-dev +tar -xzvf peak-linux-driver-8.16.0.tar.gz +cd peak-linux-driver-8.16.0/ +make clean +make all +sudo make install +## Fedora: +# make KERNEL_LOCATION=/usr/src/kernels/6.5.9-200.fc38.x86_64/ clean +# EXTRA_CFLAGS=-Wno-error=incompatible-pointer-types make KERNEL_LOCATION=/usr/src/kernels/6.5.9-200.fc38.x86_64/ NET=NETDEV_SUPPORT all +# sudo make KERNEL_LOCATION=/usr/src/kernels/6.5.9-200.fc38.x86_64/ install +``` + +For Windows, it appears you can install the driver and PCAN-Basic library from +. From 548b87ae215a6e18f36bdb63cc930ee397254a7a Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 6 Nov 2023 21:48:00 -0600 Subject: [PATCH 6/7] Enable separate input and output driver types --- examples/forward.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/forward.rs b/examples/forward.rs index a71ef8f..a17eee6 100644 --- a/examples/forward.rs +++ b/examples/forward.rs @@ -54,17 +54,22 @@ struct Options { /// The interface to read traffic from /// /// Can be either a string interface name, or an integer interface index - #[clap(short, long, default_value_t = String::from("can0"))] + #[clap(short = 'i', long, default_value_t = String::from("can0"))] pub input_interface: String, /// The interface to write traffic to /// /// Can be either a string interface name, or an integer interface index - #[clap(short, long, default_value_t = String::from("can1"))] + #[clap(short = 'o', long, default_value_t = String::from("can1"))] pub output_interface: String, - #[clap(short, long)] - pub driver: CanDriver, + /// The driver type to use for the input + #[clap(short = 'I', long)] + pub input_driver: CanDriver, + + /// The driver type to use for the output + #[clap(short = 'O', long)] + pub output_driver: CanDriver, } fn create_driver(iface: &str, driver: CanDriver) -> Box { @@ -106,8 +111,8 @@ fn main() { opts.output_interface ); - let mut input = create_driver(&opts.input_interface, opts.driver.clone()); - let mut output = create_driver(&opts.output_interface, opts.driver); + let mut input = create_driver(&opts.input_interface, opts.input_driver); + let mut output = create_driver(&opts.output_interface, opts.output_driver); input.open().unwrap(); output.open().unwrap(); From 623c3ebc74b0ecc9110dd0639c7f94f0b6b304b2 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 6 Nov 2023 21:49:48 -0600 Subject: [PATCH 7/7] Disable pcan in CI tests Including pcan support in the tests requires the test environment have libpcanbasic available, which has to be built from source. --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25df3a6..fa41185 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ name: CI on: - push: + push: pull_request: types: [opened, synchronize, reopened] @@ -28,7 +28,7 @@ jobs: profile: minimal override: true components: rustfmt - + - name: Setup cache uses: Swatinem/rust-cache@v2 @@ -37,7 +37,7 @@ jobs: with: command: fmt args: --check - + lint: name: Lint runs-on: ubuntu-latest @@ -53,7 +53,7 @@ jobs: profile: minimal override: true components: clippy - + - name: Setup cache uses: Swatinem/rust-cache@v2 @@ -77,7 +77,7 @@ jobs: toolchain: stable profile: minimal override: true - + - name: Setup cache uses: Swatinem/rust-cache@v2 @@ -90,8 +90,8 @@ jobs: uses: actions-rs/cargo@v1 with: command: tarpaulin - args: --release --all-features --engine llvm --out xml - + args: --release --features tracing,socketcan --engine llvm --out xml + - name: Upload to codecov.io uses: codecov/codecov-action@v2 with: