Skip to main content

agent_can/cli/
commands.rs

1use crate::cli::args::{
2    AdaptersCommand, CliArgs, Command, ConnectArgs, MessageArgs, MessageCommand, MessageSendArgs,
3    TraceCommand,
4};
5use crate::cli::error::CliError;
6use crate::protocol::{
7    ConnectRequest, DbcSpec, MessageListRequest, MessagePayload, MessageReadRequest,
8    MessageSendRequest, MessageStopRequest, Request, RequestAction, SchemaRequest, Selector,
9    TraceStartRequest,
10};
11use uuid::Uuid;
12
13pub fn to_request(args: &CliArgs) -> Result<Request, CliError> {
14    let command = args.command.as_ref().ok_or(CliError::MissingCommand)?;
15    let action = match command {
16        Command::Adapters(adapters) => match adapters.command {
17            AdaptersCommand::List => RequestAction::AdaptersList,
18        },
19        Command::Connect(connect) => RequestAction::Connect(parse_connect_request(connect)?),
20        Command::Disconnect => RequestAction::Disconnect,
21        Command::Status => RequestAction::Status,
22        Command::Schema(schema) => RequestAction::Schema(SchemaRequest {
23            filter: schema.filter.clone(),
24        }),
25        Command::Message(MessageArgs { command }) => match command {
26            MessageCommand::List(list) => RequestAction::MessageList(MessageListRequest {
27                filter: list.filter.clone(),
28                allow_raw: list.allow_raw,
29                include_tx: list.include_tx,
30            }),
31            MessageCommand::Read(read) => RequestAction::MessageRead(MessageReadRequest {
32                select: read.select.clone(),
33                count: read.count,
34                include_tx: read.include_tx,
35            }),
36            MessageCommand::Send(send) => RequestAction::MessageSend(parse_message_send(send)?),
37            MessageCommand::Stop(stop) => RequestAction::MessageStop(MessageStopRequest {
38                target: stop.target.clone(),
39            }),
40        },
41        Command::Trace(trace) => match &trace.command {
42            TraceCommand::Start(start) => RequestAction::TraceStart(TraceStartRequest {
43                path: start.path.clone(),
44            }),
45            TraceCommand::Stop => RequestAction::TraceStop,
46        },
47    };
48    Ok(Request {
49        id: Uuid::new_v4(),
50        action,
51    })
52}
53
54fn parse_connect_request(args: &ConnectArgs) -> Result<ConnectRequest, CliError> {
55    let mut dbcs = Vec::with_capacity(args.dbcs.len());
56    for entry in &args.dbcs {
57        let Some((alias, path)) = entry.split_once('=') else {
58            return Err(CliError::InvalidDbcMapping(entry.clone()));
59        };
60        dbcs.push(DbcSpec {
61            alias: alias.trim().to_string(),
62            path: path.trim().to_string(),
63        });
64    }
65    Ok(ConnectRequest {
66        adapter: args.adapter.clone(),
67        bitrate: args.bitrate,
68        bitrate_data: args.bitrate_data,
69        fd: args.fd,
70        dbcs,
71    })
72}
73
74fn parse_message_send(args: &MessageSendArgs) -> Result<MessageSendRequest, CliError> {
75    let data = match Selector::parse(&args.target).map_err(CliError::CommandFailed)? {
76        Selector::ArbId(_) => MessagePayload::RawHex(args.data.clone()),
77        Selector::SemanticPattern(_) => MessagePayload::Signals(parse_signal_map(args)?),
78    };
79    Ok(MessageSendRequest {
80        target: args.target.clone(),
81        data,
82        periodicity_ms: args.periodicity_ms,
83    })
84}
85
86fn parse_signal_map(
87    args: &MessageSendArgs,
88) -> Result<std::collections::BTreeMap<String, f64>, CliError> {
89    serde_json::from_str(&args.data).map_err(|err| {
90        CliError::CommandFailed(format!(
91            "semantic `message send` data must be a JSON object mapping signal names to numeric values: {err}"
92        ))
93    })
94}
95
96#[cfg(test)]
97mod tests {
98    use super::{parse_message_send, parse_signal_map};
99    use crate::cli::args::MessageSendArgs;
100    use crate::protocol::MessagePayload;
101
102    #[test]
103    fn semantic_message_send_data_uses_json_signal_maps() {
104        let args = MessageSendArgs {
105            target: "powertrain.VehicleControl".to_string(),
106            data: r#"{"enable":1,"torque":12.5}"#.to_string(),
107            periodicity_ms: None,
108        };
109        let parsed = parse_signal_map(&args).expect("signal map should parse");
110        assert_eq!(parsed.get("enable"), Some(&1.0));
111        assert_eq!(parsed.get("torque"), Some(&12.5));
112    }
113
114    #[test]
115    fn raw_message_send_data_uses_hex_strings() {
116        let args = MessageSendArgs {
117            target: "0x123".to_string(),
118            data: "DEADBEEF".to_string(),
119            periodicity_ms: Some(100),
120        };
121        let parsed = parse_message_send(&args).expect("raw send should parse");
122        assert_eq!(parsed.data, MessagePayload::RawHex("DEADBEEF".to_string()));
123        assert_eq!(parsed.periodicity_ms, Some(100));
124    }
125}