use crate::transport::factory::TransportOptions;
use std::time::Duration;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArgType {
String,
Bool,
Integer,
MultiString,
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct ArgSpec {
pub name: &'static str,
pub help: &'static str,
pub arg_type: ArgType,
pub default: Option<&'static str>,
pub possible_values: Option<&'static [&'static str]>,
}
impl ArgSpec {
pub const fn new(name: &'static str, help: &'static str, arg_type: ArgType) -> Self {
Self {
name,
help,
arg_type,
default: None,
possible_values: None,
}
}
#[must_use]
pub const fn with_default(mut self, default: &'static str) -> Self {
self.default = Some(default);
self
}
#[must_use]
pub const fn with_possible_values(mut self, values: &'static [&'static str]) -> Self {
self.possible_values = Some(values);
self
}
#[cfg(feature = "clap")]
pub fn to_clap_arg(&self) -> clap::Arg {
use clap::{Arg, ArgAction};
let mut arg = Arg::new(self.name).long(self.name).help(self.help);
match self.arg_type {
ArgType::Bool => {
arg = arg.action(ArgAction::SetTrue);
}
ArgType::MultiString => {
arg = arg.action(ArgAction::Append);
}
ArgType::String | ArgType::Integer => {
arg = arg.action(ArgAction::Set);
}
}
if let Some(default) = self.default {
arg = arg.default_value(default);
}
if let Some(values) = self.possible_values {
arg = arg.value_parser(values.to_vec());
}
arg
}
}
pub static COMMON_ARG_SPECS: &[ArgSpec] = &[
ArgSpec {
name: "can-disable-brs",
help: "Disable CAN-FD bit rate switching",
arg_type: ArgType::Bool,
default: None,
possible_values: None,
},
ArgSpec {
name: "force-transport",
help: "Force specific transport type",
arg_type: ArgType::String,
default: None,
possible_values: None,
},
ArgSpec {
name: "timeout-ms",
help: "Communication timeout in milliseconds",
arg_type: ArgType::Integer,
default: Some("100"),
possible_values: None,
},
];
pub fn transport_arg_specs() -> Vec<ArgSpec> {
let mut specs: Vec<ArgSpec> = COMMON_ARG_SPECS.to_vec();
specs.extend(super::factory::registered_arg_specs());
#[cfg(feature = "tokio")]
specs.extend(super::async_factory::registered_arg_specs());
let mut seen = std::collections::HashSet::new();
specs.retain(|spec| seen.insert(spec.name));
specs
}
impl TransportOptions {
#[cfg(feature = "clap")]
pub fn from_arg_matches(matches: &clap::ArgMatches) -> std::result::Result<Self, String> {
let mut pairs: Vec<(&'static str, String)> = Vec::new();
for spec in transport_arg_specs() {
match spec.arg_type {
ArgType::MultiString => {
if let Ok(Some(values)) = matches.try_get_many::<String>(spec.name) {
pairs.extend(values.map(|value| (spec.name, value.clone())));
}
}
ArgType::Bool => {
if matches!(matches.try_get_one::<bool>(spec.name), Ok(Some(true))) {
pairs.push((spec.name, "true".to_string()));
}
}
ArgType::String | ArgType::Integer => {
if let Ok(Some(value)) = matches.try_get_one::<String>(spec.name) {
pairs.push((spec.name, value.clone()));
}
}
}
}
Self::from_pairs(pairs.iter().map(|(name, value)| (*name, value.as_str())))
}
}
#[cfg(feature = "clap")]
pub fn add_transport_args(mut cmd: clap::Command) -> clap::Command {
for spec in transport_arg_specs() {
cmd = cmd.arg(spec.to_clap_arg());
}
cmd
}
#[cfg(feature = "clap")]
pub fn parse_with_transport_args<T: clap::Parser>(
) -> std::result::Result<(T, TransportOptions), String> {
let mut cmd = T::command();
let existing: std::collections::HashSet<String> = cmd
.get_arguments()
.map(|arg| arg.get_id().to_string())
.collect();
for spec in transport_arg_specs() {
if !existing.contains(spec.name) {
cmd = cmd.arg(spec.to_clap_arg());
}
}
let matches = cmd.get_matches();
let args = T::from_arg_matches(&matches).unwrap_or_else(|err| err.exit());
let options = TransportOptions::from_arg_matches(&matches)?;
Ok((args, options))
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "clap", derive(clap::Args))]
pub struct TransportArgs {
#[cfg_attr(feature = "clap", arg(long = "fdcanusb", action = clap::ArgAction::Append))]
pub fdcanusb: Vec<String>,
#[cfg_attr(feature = "clap", arg(long = "fdcanusb-baudrate"))]
pub fdcanusb_baudrate: Option<u32>,
#[cfg_attr(feature = "clap", arg(long = "can-chan", action = clap::ArgAction::Append))]
pub can_chan: Vec<String>,
#[cfg_attr(feature = "clap", arg(long = "can-disable-brs"))]
pub can_disable_brs: bool,
#[cfg_attr(feature = "clap", arg(long = "force-transport"))]
pub force_transport: Option<String>,
#[cfg_attr(feature = "clap", arg(long = "timeout-ms", default_value = "100"))]
pub timeout_ms: u32,
}
impl TransportArgs {
pub fn new() -> Self {
Self {
timeout_ms: 100,
..Default::default()
}
}
pub fn into_options(self) -> TransportOptions {
TransportOptions {
fdcanusb_paths: self.fdcanusb,
socketcan_interfaces: self.can_chan,
disable_brs: self.can_disable_brs,
force_transport: self.force_transport,
fdcanusb_baudrate: self.fdcanusb_baudrate,
timeout: Duration::from_millis(self.timeout_ms as u64),
extra: Default::default(),
}
}
}
impl From<TransportArgs> for TransportOptions {
fn from(args: TransportArgs) -> Self {
args.into_options()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transport_args_default() {
let args = TransportArgs::new();
assert_eq!(args.timeout_ms, 100);
assert!(args.fdcanusb.is_empty());
assert!(args.can_chan.is_empty());
assert!(!args.can_disable_brs);
assert!(args.force_transport.is_none());
}
#[test]
fn test_transport_args_to_options() {
let args = TransportArgs {
fdcanusb: vec!["/dev/ttyACM0".to_string()],
fdcanusb_baudrate: Some(3_000_000),
can_chan: vec!["can0".to_string(), "can1".to_string()],
can_disable_brs: true,
force_transport: Some("socketcan".to_string()),
timeout_ms: 200,
};
let opts: TransportOptions = args.into();
assert_eq!(opts.fdcanusb_paths, vec!["/dev/ttyACM0"]);
assert_eq!(opts.fdcanusb_baudrate, Some(3_000_000));
assert_eq!(opts.socketcan_interfaces, vec!["can0", "can1"]);
assert!(opts.disable_brs);
assert_eq!(opts.force_transport, Some("socketcan".to_string()));
assert_eq!(opts.timeout, Duration::from_millis(200));
}
#[test]
fn test_common_arg_specs_complete() {
let names: Vec<_> = COMMON_ARG_SPECS.iter().map(|s| s.name).collect();
assert!(names.contains(&"can-disable-brs"));
assert!(names.contains(&"force-transport"));
assert!(names.contains(&"timeout-ms"));
}
#[test]
fn test_transport_arg_specs_includes_factory_args() {
let specs = transport_arg_specs();
let names: Vec<_> = specs.iter().map(|s| s.name).collect();
assert!(names.contains(&"can-disable-brs"));
assert!(names.contains(&"force-transport"));
assert!(names.contains(&"timeout-ms"));
#[cfg(feature = "serialport")]
assert!(names.contains(&"fdcanusb"));
#[cfg(target_os = "linux")]
assert!(names.contains(&"can-chan"));
}
#[test]
fn test_arg_spec_new() {
let spec = ArgSpec::new("pi3hat-cpu", "CPU to pin to", ArgType::Integer)
.with_default("0")
.with_possible_values(&["0", "1", "2", "3"]);
assert_eq!(spec.name, "pi3hat-cpu");
assert_eq!(spec.help, "CPU to pin to");
assert_eq!(spec.arg_type, ArgType::Integer);
assert_eq!(spec.default, Some("0"));
assert_eq!(spec.possible_values, Some(&["0", "1", "2", "3"][..]));
}
#[test]
fn test_arg_specs_types() {
let specs = transport_arg_specs();
for spec in &specs {
match spec.name {
"fdcanusb" | "can-chan" => {
assert_eq!(spec.arg_type, ArgType::MultiString);
}
"can-disable-brs" => {
assert_eq!(spec.arg_type, ArgType::Bool);
}
"force-transport" => {
assert_eq!(spec.arg_type, ArgType::String);
}
"timeout-ms" => {
assert_eq!(spec.arg_type, ArgType::Integer);
assert_eq!(spec.default, Some("100"));
}
_ => {} }
}
}
#[test]
#[cfg(all(feature = "clap", feature = "serialport"))]
fn test_from_arg_matches_round_trip() {
let cmd = add_transport_args(clap::Command::new("test"));
let matches = cmd.get_matches_from([
"test",
"--fdcanusb",
"/dev/ttyACM0",
"--fdcanusb",
"/dev/ttyACM1",
"--can-disable-brs",
"--force-transport",
"fdcanusb",
"--timeout-ms",
"250",
]);
let opts = TransportOptions::from_arg_matches(&matches).unwrap();
assert_eq!(opts.fdcanusb_paths, vec!["/dev/ttyACM0", "/dev/ttyACM1"]);
assert!(opts.disable_brs);
assert_eq!(opts.force_transport, Some("fdcanusb".to_string()));
assert_eq!(opts.timeout, Duration::from_millis(250));
}
}