moteus 0.5.2

Rust client library for moteus brushless motor controllers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
// Copyright 2026 mjbots Robotic Systems, LLC.  info@mjbots.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Command-line argument support for transport options.
//!
//! This module provides two approaches for CLI integration:
//!
//! # Approach 1: With clap (recommended)
//!
//! When the `clap` feature is enabled, use [`TransportArgs`] with derive:
//!
//! ```ignore
//! use clap::Parser;
//! use moteus::transport::args::TransportArgs;
//!
//! #[derive(Parser)]
//! struct Args {
//!     #[arg(long, default_value = "1")]
//!     id: u8,
//!
//!     #[command(flatten)]
//!     transport: TransportArgs,
//! }
//!
//! let args = Args::parse();
//! let opts: moteus::TransportOptions = args.transport.into();
//! ```
//!
//! # Approach 2: With any CLI parser
//!
//! Use [`transport_arg_specs()`] to generate arguments for any parser,
//! then pass results to [`TransportOptions::from_pairs()`]:
//!
//! ```
//! use moteus::transport::args::{transport_arg_specs, ArgType};
//!
//! // Print what arguments are available (includes registered factory args)
//! for spec in transport_arg_specs() {
//!     println!("--{}: {} ({:?})", spec.name, spec.help, spec.arg_type);
//! }
//! ```
//!
//! With clap's builder API:
//!
//! ```ignore
//! use clap::{Arg, ArgAction, Command};
//! use moteus::transport::args::{transport_arg_specs, ArgType};
//! use moteus::TransportOptions;
//!
//! fn main() -> Result<(), String> {
//!     let mut cmd = Command::new("myapp");
//!     for spec in transport_arg_specs() {
//!         cmd = cmd.arg(spec.to_clap_arg());
//!     }
//!     let matches = cmd.get_matches();
//!     let opts = TransportOptions::from_arg_matches(&matches)?;
//!     Ok(())
//! }
//! ```

use crate::transport::factory::TransportOptions;
use std::time::Duration;

/// Type of a command-line argument.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArgType {
    /// A string value (single occurrence)
    String,
    /// A boolean flag (presence = true)
    Bool,
    /// An unsigned integer value
    Integer,
    /// A string that can be specified multiple times
    MultiString,
}

/// Specification for a single command-line argument.
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct ArgSpec {
    /// Argument name (without dashes), e.g., "fdcanusb"
    pub name: &'static str,
    /// Short help text
    pub help: &'static str,
    /// Argument type
    pub arg_type: ArgType,
    /// Default value as a string (if any)
    pub default: Option<&'static str>,
    /// Valid values for enum-like arguments
    pub possible_values: Option<&'static [&'static str]>,
}

impl ArgSpec {
    /// Create a new argument specification.
    ///
    /// This is the only way for external transport factories to
    /// construct an `ArgSpec`, since the struct is `#[non_exhaustive]`.
    pub const fn new(name: &'static str, help: &'static str, arg_type: ArgType) -> Self {
        Self {
            name,
            help,
            arg_type,
            default: None,
            possible_values: None,
        }
    }

    /// Set the default value (builder pattern).
    #[must_use]
    pub const fn with_default(mut self, default: &'static str) -> Self {
        self.default = Some(default);
        self
    }

    /// Set the valid values for enum-like arguments (builder pattern).
    #[must_use]
    pub const fn with_possible_values(mut self, values: &'static [&'static str]) -> Self {
        self.possible_values = Some(values);
        self
    }

    /// Create a clap Arg from this specification.
    #[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
    }
}

/// Common transport arguments not owned by any specific factory.
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,
    },
];

/// Get all transport-related argument specifications.
///
/// Returns the common args plus args from all registered transport factories.
/// This is dynamic: if external factories have been registered via
/// [`register()`](super::factory::register), their arg specs are included.
pub fn transport_arg_specs() -> Vec<ArgSpec> {
    use super::factory::get_factories;

    let mut specs: Vec<ArgSpec> = COMMON_ARG_SPECS.to_vec();
    for factory in get_factories() {
        specs.extend(factory.arg_specs());
    }
    specs
}

impl TransportOptions {
    /// Create transport options from clap ArgMatches.
    ///
    /// This works with arguments created via [`ArgSpec::to_clap_arg()`] or
    /// any clap arguments using the standard transport argument names.
    /// Arguments from registered external factories are extracted into
    /// the `extra` field.
    #[cfg(feature = "clap")]
    pub fn from_arg_matches(matches: &clap::ArgMatches) -> std::result::Result<Self, String> {
        let mut opts = TransportOptions::new();

        if let Some(values) = matches.get_many::<String>("fdcanusb") {
            opts.fdcanusb_paths = values.cloned().collect();
        }

        if matches.try_contains_id("fdcanusb-baudrate").is_ok() {
            if let Some(value) = matches.get_one::<String>("fdcanusb-baudrate") {
                let baudrate: u32 = value
                    .parse()
                    .map_err(|_| format!("invalid baudrate: {}", value))?;
                opts.fdcanusb_baudrate = Some(baudrate);
            }
        }

        if let Some(values) = matches.get_many::<String>("can-chan") {
            opts.socketcan_interfaces = values.cloned().collect();
        }

        if matches.get_flag("can-disable-brs") {
            opts.disable_brs = true;
        }

        if let Some(value) = matches.get_one::<String>("force-transport") {
            opts.force_transport = Some(value.clone());
        }

        if let Some(value) = matches.get_one::<String>("timeout-ms") {
            let ms: u32 = value
                .parse()
                .map_err(|_| format!("invalid timeout: {}", value))?;
            opts.timeout = Duration::from_millis(ms as u64);
        }

        // Extract registered factory args into extra
        for factory in super::factory::get_factories() {
            for spec in factory.arg_specs() {
                // Skip args we already handled above
                if matches!(spec.name, "fdcanusb" | "fdcanusb-baudrate" | "can-chan") {
                    continue;
                }
                match spec.arg_type {
                    ArgType::MultiString => {
                        if let Some(values) = matches.get_many::<String>(spec.name) {
                            let vals: Vec<String> = values.cloned().collect();
                            if !vals.is_empty() {
                                opts.extra.insert(spec.name.to_string(), vals);
                            }
                        }
                    }
                    ArgType::Bool => {
                        if matches.get_flag(spec.name) {
                            opts.extra
                                .insert(spec.name.to_string(), vec!["true".to_string()]);
                        }
                    }
                    ArgType::String | ArgType::Integer => {
                        if let Some(value) = matches.get_one::<String>(spec.name) {
                            opts.extra
                                .insert(spec.name.to_string(), vec![value.clone()]);
                        }
                    }
                }
            }
        }

        Ok(opts)
    }
}

/// Add all transport arguments to a clap Command.
///
/// This is a convenience function for clap's builder API. It includes
/// common args plus args from all registered transport factories.
#[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
}

/// Command-line arguments for transport configuration.
///
/// This struct can be used with clap's derive API via `#[command(flatten)]`.
/// For the builder API, use [`transport_arg_specs()`].
///
/// Note: This covers built-in transport args only. External factory args
/// should be handled via `TransportOptions::from_arg_matches()` or
/// [`TransportOptions::from_pairs()`] with the builder API.
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "clap", derive(clap::Args))]
pub struct TransportArgs {
    /// Path to fdcanusb device (can be specified multiple times).
    #[cfg_attr(feature = "clap", arg(long = "fdcanusb", action = clap::ArgAction::Append))]
    pub fdcanusb: Vec<String>,

    /// Serial baud rate (only matters for UART connections).
    #[cfg_attr(feature = "clap", arg(long = "fdcanusb-baudrate"))]
    pub fdcanusb_baudrate: Option<u32>,

    /// SocketCAN interface (can be specified multiple times).
    #[cfg_attr(feature = "clap", arg(long = "can-chan", action = clap::ArgAction::Append))]
    pub can_chan: Vec<String>,

    /// Disable CAN-FD bit rate switching.
    #[cfg_attr(feature = "clap", arg(long = "can-disable-brs"))]
    pub can_disable_brs: bool,

    /// Force specific transport type.
    #[cfg_attr(feature = "clap", arg(long = "force-transport"))]
    pub force_transport: Option<String>,

    /// Communication timeout in milliseconds.
    #[cfg_attr(feature = "clap", arg(long = "timeout-ms", default_value = "100"))]
    pub timeout_ms: u32,
}

impl TransportArgs {
    /// Create new transport args with default values.
    pub fn new() -> Self {
        Self {
            timeout_ms: 100,
            ..Default::default()
        }
    }

    /// Convert these args into TransportOptions.
    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();
        // Common args
        assert!(names.contains(&"can-disable-brs"));
        assert!(names.contains(&"force-transport"));
        assert!(names.contains(&"timeout-ms"));
        // Factory-provided args
        #[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"));
                }
                _ => {} // External factory args - don't panic
            }
        }
    }
}