Skip to main content

agent_can/cli/
commands.rs

1use crate::cli::args::{CliArgs, Command, SendMessageArgs};
2use crate::cli::error::CliError;
3use crate::daemon::config::DaemonConfig;
4use crate::protocol::{Request, RequestAction};
5use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7use uuid::Uuid;
8
9pub fn daemon_config(args: &CliArgs) -> Result<DaemonConfig, CliError> {
10    let Some(Command::Open(open)) = args.command.as_ref() else {
11        return Err(CliError::CommandFailed(
12            "open config requested for non-open command".to_string(),
13        ));
14    };
15    Ok(DaemonConfig {
16        bus: args.bus.clone(),
17        adapter: open.adapter.clone(),
18        dbc_path: canonicalize_cli_path(&open.dbc)?,
19        bitrate: open.bitrate,
20        bitrate_data: open.bitrate_data,
21        fd_capable: open.fd,
22    })
23}
24
25pub fn to_request(args: &CliArgs) -> Result<Request, CliError> {
26    let command = args.command.as_ref().ok_or(CliError::MissingCommand)?;
27    let action = match command {
28        Command::Open(_) => {
29            return Err(CliError::CommandFailed(
30                "open is handled by the CLI bootstrap path".to_string(),
31            ));
32        }
33        Command::Status => RequestAction::Status,
34        Command::Mailboxes => RequestAction::Mailboxes,
35        Command::Mailbox(mailbox) => RequestAction::Mailbox {
36            message: mailbox.message.clone(),
37        },
38        Command::SendRaw(raw) => RequestAction::SendRaw {
39            arb_id: parse_arb_id(&raw.arb_id)?,
40            data_hex: raw.data_hex.clone(),
41            flags: raw.flags,
42        },
43        Command::SendMessage(send) => RequestAction::SendMessage {
44            message: send.message.clone(),
45            signals: parse_signal_assignments(send)?,
46        },
47        Command::Bus(_) => {
48            return Err(CliError::CommandFailed(
49                "bus list is handled directly by the CLI".to_string(),
50            ));
51        }
52        Command::Close => RequestAction::Close,
53    };
54    Ok(Request {
55        id: Uuid::new_v4(),
56        action,
57    })
58}
59
60fn parse_signal_assignments(args: &SendMessageArgs) -> Result<BTreeMap<String, f64>, CliError> {
61    let mut out = BTreeMap::new();
62    for entry in &args.signals {
63        let Some((name, raw)) = entry.split_once('=') else {
64            return Err(CliError::InvalidSignalAssignment(entry.clone()));
65        };
66        let value = raw.trim().parse::<f64>().map_err(|_| {
67            CliError::CommandFailed(format!("invalid numeric signal value in '{entry}'"))
68        })?;
69        out.insert(name.trim().to_string(), value);
70    }
71    Ok(out)
72}
73
74fn canonicalize_cli_path(raw_path: &str) -> Result<String, CliError> {
75    let path = Path::new(raw_path);
76    let candidate: PathBuf = if path.is_absolute() {
77        path.to_path_buf()
78    } else {
79        std::env::current_dir()
80            .map_err(|e| {
81                CliError::CommandFailed(format!(
82                    "failed to determine current working directory while resolving DBC path '{raw_path}': {e}"
83                ))
84            })?
85            .join(path)
86    };
87    let canonical = std::fs::canonicalize(&candidate).map_err(|e| {
88        CliError::CommandFailed(format!(
89            "failed to resolve DBC path '{raw_path}' to an absolute path (candidate '{}'): {e}",
90            candidate.display()
91        ))
92    })?;
93    Ok(canonical.to_string_lossy().into_owned())
94}
95
96pub fn parse_arb_id(value: &str) -> Result<u32, CliError> {
97    let trimmed = value.trim();
98    if let Some(hex) = trimmed
99        .strip_prefix("0x")
100        .or_else(|| trimmed.strip_prefix("0X"))
101    {
102        u32::from_str_radix(hex, 16)
103            .map_err(|_| CliError::CommandFailed(format!("invalid arbitration id '{value}'")))
104    } else {
105        trimmed
106            .parse::<u32>()
107            .map_err(|_| CliError::CommandFailed(format!("invalid arbitration id '{value}'")))
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::{parse_arb_id, parse_signal_assignments};
114    use crate::cli::args::SendMessageArgs;
115
116    #[test]
117    fn arb_id_parser_accepts_hex_and_decimal() {
118        assert_eq!(parse_arb_id("0x7FF").expect("hex"), 0x7FF);
119        assert_eq!(parse_arb_id("2048").expect("decimal"), 2048);
120        assert!(parse_arb_id("xyz").is_err());
121    }
122
123    #[test]
124    fn signal_assignments_require_equals_syntax() {
125        let args = SendMessageArgs {
126            message: "VehicleControl".to_string(),
127            signals: vec!["enable=1".to_string(), "torque=12.5".to_string()],
128        };
129        let parsed = parse_signal_assignments(&args).expect("assignments should parse");
130        assert_eq!(parsed.get("enable"), Some(&1.0));
131        assert_eq!(parsed.get("torque"), Some(&12.5));
132    }
133}