Skip to main content

tag_tools/
lib.rs

1//! Shared command-line handling for the Rust tag tools.
2//!
3//! Port of `host/commandline/options.cc` and the parts of `log.c` the tools
4//! rely on. Every tool that talks to a tag flattens [`BaseArgs`] into its own
5//! arguments, calls [`init_logging`], then [`select_base`] to find the
6//! ST-LINK, exactly as the C++ tools call `parse_options()`:
7//!
8//! - `-d`/`--debug` logs at DEBUG; otherwise only errors are logged.
9//! - `-b`/`--base bus:device` picks a base. It is required when more than
10//!   one ST-LINK is connected; with none connected the tool exits.
11//!
12//! Log lines go to stderr as `HH:MM:SS LEVEL file:line: message`, the
13//! format of `log.c`.
14//!
15//! Selecting a base touches no hardware beyond USB enumeration; it is the
16//! tool's later `Tag::attach` that resets the tag.
17//!
18//! # Examples
19//!
20//! The skeleton every tool follows (`Cli` is the tool's own clap parser):
21//!
22//! ```no_run
23//! use clap::Parser;
24//! use tag_tools::{BaseArgs, init_logging, select_base};
25//! use tagcore::Tag;
26//!
27//! #[derive(Parser)]
28//! struct Cli {
29//!     #[command(flatten)]
30//!     base: BaseArgs,
31//! }
32//!
33//! let cli = Cli::parse();
34//! init_logging(&cli.base);
35//! let tag = Tag::new();
36//! let dev = select_base::<Cli>(&tag, &cli.base);
37//! if !tag.attach(&dev) {
38//!     std::process::exit(1);
39//! }
40//! ```
41
42use std::io::Write as _;
43
44use clap::{Args, CommandFactory};
45use log::LevelFilter;
46use tagcore::proto::TagState;
47use tagcore::{Tag, UsbDev};
48
49/// The options every tag tool shares (`options.cc`).
50#[derive(Debug, Clone, Args)]
51pub struct BaseArgs {
52    /// Set log level to DEBUG
53    #[arg(short, long)]
54    pub debug: bool,
55
56    /// Select bus:device
57    #[arg(short, long, value_name = "BUS:DEVICE")]
58    pub base: Option<String>,
59}
60
61/// Starts logging to stderr in the `log.c` format, at DEBUG with `--debug`
62/// and at ERROR otherwise.
63///
64/// # Panics
65///
66/// Panics if a global logger is already installed, so call it once per
67/// process.
68pub fn init_logging(args: &BaseArgs) {
69    init_logging_at(if args.debug {
70        LevelFilter::Debug
71    } else {
72        LevelFilter::Error
73    });
74}
75
76/// Starts logging to stderr in the `log.c` format at `level`.
77///
78/// Each line carries the local wall-clock time and the Rust source file and
79/// line of the log call, or `?:0` when the record has none.
80///
81/// # Panics
82///
83/// Panics if a global logger is already installed.
84pub fn init_logging_at(level: LevelFilter) {
85    env_logger::Builder::new()
86        .filter_level(level)
87        .format(|buf, record| {
88            writeln!(
89                buf,
90                "{} {:<5} {}:{}: {}",
91                chrono::Local::now().format("%H:%M:%S"),
92                record.level(),
93                record.file().unwrap_or("?"),
94                record.line().unwrap_or(0),
95                record.args()
96            )
97        })
98        .init();
99}
100
101/// Changes the log level after [`init_logging`], as `log_set_level()` does.
102///
103/// Used by `tag-start` to silence the link errors expected once a started
104/// tag drops the debug link.
105pub fn set_log_level(level: LevelFilter) {
106    log::set_max_level(level);
107}
108
109/// Exits with status 1 on Ctrl-C, as the C++ tools' SIGINT handler does.
110///
111/// The process exits from the handler thread without running destructors,
112/// so no detach is sent to the base. A failure to install the handler
113/// (one is already installed) is ignored, leaving the existing handler in
114/// place.
115pub fn exit_on_interrupt() {
116    let _ = ctrlc::set_handler(|| std::process::exit(1));
117}
118
119/// Finds the ST-LINK the arguments select, or exits.
120///
121/// With one base connected and no `--base`, that base is used. Otherwise
122/// `--base bus:device` must name one: a malformed value prints the help and
123/// exits 1, and no match exits 1 with "No matching device". With no base
124/// connected it exits 1 with "No matching device" too. `C` is the tool's
125/// clap parser, whose help is printed.
126///
127/// Only USB enumeration happens here; nothing is sent to the base or tag.
128pub fn select_base<C: CommandFactory>(tag: &Tag, args: &BaseArgs) -> UsbDev {
129    let devs = tag.available();
130    let device = args.base.clone().unwrap_or_default();
131    let mut found = if devs.is_empty() { None } else { Some(0) };
132
133    if devs.len() > 1 || !device.is_empty() {
134        let Some((bus, address)) = parse_bus_device(&device) else {
135            let _ = C::command().print_help();
136            println!();
137            std::process::exit(1);
138        };
139        found = devs
140            .iter()
141            .position(|d| d.bus == bus && d.address == address);
142    }
143
144    match found {
145        Some(i) => devs[i].clone(),
146        None => {
147            eprintln!("No matching device {device}");
148            std::process::exit(1);
149        }
150    }
151}
152
153/// Parses `bus:device` as `sscanf("%u:%u")` would, into USB bus and address.
154///
155/// Whitespace around the bus number is ignored, and the device number ends
156/// at its first non-digit, as `%u` stops there. `None` if there is no `:`,
157/// either number is missing or not decimal, or either does not fit a `u8`.
158pub fn parse_bus_device(text: &str) -> Option<(u8, u8)> {
159    let (bus, address) = text.split_once(':')?;
160    let number = |s: &str| {
161        s.trim()
162            .parse::<u32>()
163            .ok()
164            .and_then(|n| u8::try_from(n).ok())
165    };
166    Some((
167        number(bus)?,
168        number(address.split(|c: char| !c.is_ascii_digit()).next()?)?,
169    ))
170}
171
172/// The protobuf name of a tag state number, such as `"IDLE"`, or the number
173/// itself when it has no name, as `TagState_Name()` prints it.
174pub fn state_name(state: i32) -> String {
175    TagState::try_from(state)
176        .map(|s| s.as_str_name().to_owned())
177        .unwrap_or_else(|_| state.to_string())
178}
179
180/// The protobuf name of an enum value of type `E`, or its number.
181///
182/// `name` is the enum's `as_str_name`, passed because prost generates it as
183/// an inherent method rather than a trait method.
184pub fn enum_name<E>(value: i32, name: fn(&E) -> &'static str) -> String
185where
186    E: TryFrom<i32>,
187{
188    E::try_from(value)
189        .map(|e| name(&e).to_owned())
190        .unwrap_or_else(|_| value.to_string())
191}
192
193/// Unit tests for argument parsing and state naming.
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    /// Checks `bus:device` parsing keeps `sscanf`'s leniency and rejects
199    /// what `sscanf` would.
200    #[test]
201    fn bus_device_parses_like_sscanf() {
202        assert_eq!(parse_bus_device("0:6"), Some((0, 6)));
203        assert_eq!(parse_bus_device("20:3x"), Some((20, 3)));
204        assert_eq!(parse_bus_device("6"), None);
205        assert_eq!(parse_bus_device(""), None);
206        assert_eq!(parse_bus_device("a:1"), None);
207    }
208
209    /// Checks a known state prints by name and an unknown one by number.
210    #[test]
211    fn state_names_fall_back_to_numbers() {
212        assert_eq!(state_name(2), "IDLE");
213        assert_eq!(state_name(99), "99");
214    }
215}