agent_can/cli/
commands.rs1use 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}