use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use crate::error::Result;
use crate::transport::args::ArgSpec;
use crate::transport::device::TransportDevice;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_millis(100);
#[non_exhaustive]
#[derive(Debug, Clone, Default)]
pub struct TransportOptions {
pub fdcanusb_paths: Vec<String>,
pub socketcan_interfaces: Vec<String>,
pub disable_brs: bool,
pub force_transport: Option<String>,
pub timeout: Duration,
pub extra: HashMap<String, Vec<String>>,
}
impl TransportOptions {
pub fn new() -> Self {
Self {
timeout: DEFAULT_TIMEOUT,
..Default::default()
}
}
#[must_use]
pub fn fdcanusb_paths(mut self, paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.fdcanusb_paths = paths.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn socketcan_interfaces(
mut self,
interfaces: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.socketcan_interfaces = interfaces.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn disable_brs(mut self, disable: bool) -> Self {
self.disable_brs = disable;
self
}
#[must_use]
pub fn force_transport(mut self, transport: impl Into<String>) -> Self {
self.force_transport = Some(transport.into());
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra.entry(key.into()).or_default().push(value.into());
self
}
pub fn from_pairs<'a>(
pairs: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> std::result::Result<Self, String> {
let mut opts = TransportOptions::new();
for (key, value) in pairs {
match key {
"fdcanusb" => opts.fdcanusb_paths.push(value.to_string()),
"can-chan" => opts.socketcan_interfaces.push(value.to_string()),
"can-disable-brs" => opts.disable_brs = value == "true",
"force-transport" => {
opts.force_transport = Some(value.to_string());
}
"timeout-ms" => {
let ms: u32 = value
.parse()
.map_err(|_| format!("invalid timeout: {}", value))?;
opts.timeout = Duration::from_millis(ms as u64);
}
_ => {
opts.extra
.entry(key.to_string())
.or_default()
.push(value.to_string());
}
}
}
Ok(opts)
}
}
pub trait TransportFactory: Send + Sync {
fn priority(&self) -> u32;
fn name(&self) -> &'static str;
fn arg_specs(&self) -> Vec<ArgSpec> {
Vec::new()
}
fn create(&self, options: &TransportOptions) -> Result<Vec<Box<dyn TransportDevice>>>;
}
#[cfg(target_os = "linux")]
#[derive(Debug, Default)]
pub struct FdcanusbFactory;
#[cfg(target_os = "linux")]
impl FdcanusbFactory {
pub fn new() -> Self {
Self
}
}
#[cfg(target_os = "linux")]
impl TransportFactory for FdcanusbFactory {
fn priority(&self) -> u32 {
10 }
fn name(&self) -> &'static str {
"fdcanusb"
}
fn arg_specs(&self) -> Vec<ArgSpec> {
use crate::transport::args::ArgType;
vec![ArgSpec {
name: "fdcanusb",
help: "Path to fdcanusb device (can be specified multiple times)",
arg_type: ArgType::MultiString,
default: None,
possible_values: None,
}]
}
fn create(&self, options: &TransportOptions) -> Result<Vec<Box<dyn TransportDevice>>> {
use crate::transport::discovery::{detect_fdcanusbs, FdcanusbInfo};
use crate::transport::fdcanusb::FdcanusbDevice;
let infos: Vec<FdcanusbInfo> = if options.fdcanusb_paths.is_empty() {
detect_fdcanusbs()
} else {
options
.fdcanusb_paths
.iter()
.map(|path| FdcanusbInfo {
path: path.clone(),
serial_number: None,
})
.collect()
};
let mut devices: Vec<Box<dyn TransportDevice>> = Vec::new();
for (idx, info) in infos.iter().enumerate() {
match FdcanusbDevice::with_options(&info.path, options.timeout, options.disable_brs) {
Ok(mut device) => {
device.info.id = idx;
device.info.serial_number = info.serial_number.clone();
device.info.detail =
info.serial_number.as_ref().map(|sn| format!("sn='{}'", sn));
devices.push(Box::new(device));
}
Err(_) => continue, }
}
Ok(devices)
}
}
#[cfg(target_os = "linux")]
#[derive(Debug, Default)]
pub struct SocketCanFactory;
#[cfg(target_os = "linux")]
impl SocketCanFactory {
pub fn new() -> Self {
Self
}
}
#[cfg(target_os = "linux")]
impl TransportFactory for SocketCanFactory {
fn priority(&self) -> u32 {
11 }
fn name(&self) -> &'static str {
"socketcan"
}
fn arg_specs(&self) -> Vec<ArgSpec> {
use crate::transport::args::ArgType;
vec![ArgSpec {
name: "can-chan",
help: "SocketCAN interface (can be specified multiple times)",
arg_type: ArgType::MultiString,
default: None,
possible_values: None,
}]
}
fn create(&self, options: &TransportOptions) -> Result<Vec<Box<dyn TransportDevice>>> {
use crate::transport::discovery::detect_socketcan_interfaces;
use crate::transport::socketcan::SocketCanDevice;
let interfaces = if options.socketcan_interfaces.is_empty() {
detect_socketcan_interfaces()
.into_iter()
.map(|info| info.interface)
.collect()
} else {
options.socketcan_interfaces.clone()
};
let mut devices: Vec<Box<dyn TransportDevice>> = Vec::new();
for (idx, interface) in interfaces.iter().enumerate() {
match SocketCanDevice::with_options(interface, options.timeout, options.disable_brs) {
Ok(mut device) => {
device.info.id = idx;
devices.push(Box::new(device));
}
Err(_) => continue, }
}
Ok(devices)
}
}
static REGISTRY: OnceLock<Mutex<Vec<Box<dyn TransportFactory>>>> = OnceLock::new();
fn get_registry() -> &'static Mutex<Vec<Box<dyn TransportFactory>>> {
REGISTRY.get_or_init(|| {
#[cfg(target_os = "linux")]
let factories: Vec<Box<dyn TransportFactory>> =
vec![Box::new(FdcanusbFactory), Box::new(SocketCanFactory)];
#[cfg(not(target_os = "linux"))]
let factories: Vec<Box<dyn TransportFactory>> = Vec::new();
Mutex::new(factories)
})
}
pub fn register(factory: Box<dyn TransportFactory>) {
get_registry().lock().unwrap().push(factory);
}
pub fn get_factories() -> Vec<Box<dyn TransportFactory>> {
#[cfg(target_os = "linux")]
{
vec![
Box::new(FdcanusbFactory::new()),
Box::new(SocketCanFactory::new()),
]
}
#[cfg(not(target_os = "linux"))]
{
Vec::new()
}
}
pub fn create_transports(options: &TransportOptions) -> Result<Vec<Box<dyn TransportDevice>>> {
use std::collections::HashSet;
let registry = get_registry().lock().unwrap();
let mut indices: Vec<usize> = (0..registry.len()).collect();
indices.sort_by_key(|&i| registry[i].priority());
if let Some(ref force) = options.force_transport {
indices.retain(|&i| registry[i].name() == force.as_str());
}
let mut all_devices = Vec::new();
let mut fdcanusb_serials: HashSet<String> = HashSet::new();
for &idx in &indices {
match registry[idx].create(options) {
Ok(devices) => {
if registry[idx].name() == "fdcanusb" {
for device in &devices {
if let Some(serial) = device.info().serial_number.as_ref() {
fdcanusb_serials.insert(serial.clone());
}
}
}
all_devices.extend(devices);
}
Err(_) => continue,
}
}
drop(registry);
#[cfg(target_os = "linux")]
{
use crate::transport::discovery::detect_socketcan_interfaces;
let socketcan_infos = detect_socketcan_interfaces();
let mut filtered_devices = Vec::new();
for device in all_devices {
let should_skip = socketcan_infos.iter().any(|info| {
(device.info().serial_number.as_ref() == Some(&info.interface))
&& info
.fdcanusb_serial
.as_ref()
.is_some_and(|serial| fdcanusb_serials.contains(serial))
});
if !should_skip {
filtered_devices.push(device);
}
}
all_devices = filtered_devices;
}
Ok(all_devices)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transport_options_builder() {
let opts = TransportOptions::new()
.fdcanusb_paths(vec!["/dev/ttyACM0", "/dev/ttyACM1"])
.socketcan_interfaces(vec!["can0"])
.disable_brs(true)
.timeout(Duration::from_millis(200));
assert_eq!(opts.fdcanusb_paths, vec!["/dev/ttyACM0", "/dev/ttyACM1"]);
assert_eq!(opts.socketcan_interfaces, vec!["can0"]);
assert!(opts.disable_brs);
assert_eq!(opts.timeout, Duration::from_millis(200));
}
#[test]
fn test_transport_options_extra() {
let opts = TransportOptions::new()
.extra("pi3hat-cfg", "1=1,2")
.extra("pi3hat-cfg", "3=3,4");
assert_eq!(
opts.extra.get("pi3hat-cfg").unwrap(),
&vec!["1=1,2".to_string(), "3=3,4".to_string()]
);
}
#[cfg(target_os = "linux")]
#[test]
fn test_factory_priorities() {
let fdcanusb = FdcanusbFactory::new();
assert_eq!(fdcanusb.priority(), 10);
let socketcan = SocketCanFactory::new();
assert!(fdcanusb.priority() < socketcan.priority());
}
#[cfg(target_os = "linux")]
#[test]
fn test_factory_arg_specs() {
let fdcanusb = FdcanusbFactory::new();
let specs = fdcanusb.arg_specs();
assert_eq!(specs.len(), 1);
assert_eq!(specs[0].name, "fdcanusb");
}
#[cfg(target_os = "linux")]
#[test]
fn test_socketcan_factory_arg_specs() {
let socketcan = SocketCanFactory::new();
let specs = socketcan.arg_specs();
assert_eq!(specs.len(), 1);
assert_eq!(specs[0].name, "can-chan");
}
#[test]
fn test_get_factories() {
let factories = get_factories();
assert!(!factories.is_empty());
let names: Vec<_> = factories.iter().map(|f| f.name()).collect();
assert!(names.contains(&"fdcanusb"));
}
#[test]
fn test_from_pairs_basic() {
let opts =
TransportOptions::from_pairs([("can-chan", "can0"), ("timeout-ms", "200")]).unwrap();
assert_eq!(opts.socketcan_interfaces, vec!["can0"]);
assert_eq!(opts.timeout, Duration::from_millis(200));
}
#[test]
fn test_from_pairs_multiple_devices() {
let opts = TransportOptions::from_pairs([
("fdcanusb", "/dev/ttyACM0"),
("fdcanusb", "/dev/ttyACM1"),
("can-chan", "can0"),
("can-chan", "can1"),
])
.unwrap();
assert_eq!(opts.fdcanusb_paths, vec!["/dev/ttyACM0", "/dev/ttyACM1"]);
assert_eq!(opts.socketcan_interfaces, vec!["can0", "can1"]);
}
#[test]
fn test_from_pairs_flags() {
let opts = TransportOptions::from_pairs([
("can-disable-brs", "true"),
("force-transport", "socketcan"),
])
.unwrap();
assert!(opts.disable_brs);
assert_eq!(opts.force_transport, Some("socketcan".to_string()));
}
#[test]
fn test_from_pairs_unknown_key_goes_to_extra() {
let opts = TransportOptions::from_pairs([("pi3hat-cfg", "1=1,2"), ("custom-opt", "value")])
.unwrap();
assert_eq!(
opts.extra.get("pi3hat-cfg").unwrap(),
&vec!["1=1,2".to_string()]
);
assert_eq!(
opts.extra.get("custom-opt").unwrap(),
&vec!["value".to_string()]
);
}
#[test]
fn test_from_pairs_force_transport_any_value() {
let opts = TransportOptions::from_pairs([("force-transport", "pi3hat")]).unwrap();
assert_eq!(opts.force_transport, Some("pi3hat".to_string()));
}
#[test]
fn test_from_pairs_invalid_timeout() {
let result = TransportOptions::from_pairs([("timeout-ms", "not_a_number")]);
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid timeout"));
}
}