microscpi_common/lib.rs
1use core::iter::Iterator;
2
3/// Represents a part of an SCPI command, such as "STATus" in "STATus:EVENt?".
4///
5/// Each part has both a short form (uppercase letters only) and a long form (complete word).
6/// SCPI allows using either form in commands.
7///
8/// For example, "STATus" can be written as either "STAT" (short form) or "STATUS" (long form).
9#[derive(Debug, Clone, PartialEq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize))]
11pub struct CommandPart {
12 /// Whether this command part is optional.
13 pub optional: bool,
14 /// The short form of the command part.
15 pub short: String,
16 /// The long form of the command part.
17 pub long: String,
18}
19
20/// Represents a complete SCPI command with all its parts.
21///
22/// An SCPI command consists of multiple parts separated by colons, for example:
23/// "SYSTem:ERRor:NEXT?" is a command with three parts and is a query (ends with '?').
24///
25/// The command can also have optional parts, indicated by square brackets, like:
26/// "[STATus]:EVENt?" where "STATus" is optional.
27#[derive(Debug, Clone)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize))]
29pub struct Command {
30 /// The parts of the command name.
31 pub parts: Vec<CommandPart>,
32 /// Whether the command is a query, i.e. ends with a question mark.
33 query: bool,
34}
35
36/// Represents a specific path through the command tree, with each string
37/// representing either the short or long form of a command part.
38pub type CommandPath = Vec<String>;
39
40impl TryFrom<&str> for Command {
41 type Error = Box<dyn std::error::Error>;
42
43 /// Parses a command string into a Command structure.
44 ///
45 /// # Arguments
46 /// * `value` - The SCPI command string (e.g., "SYSTem:ERRor?" or "[STATus]:EVENt?")
47 ///
48 /// # Returns
49 /// * `Ok(Command)` - Successfully parsed command
50 /// * `Err` - If the command string is invalid
51 fn try_from(mut value: &str) -> Result<Self, Self::Error> {
52 let mut parts = Vec::new();
53 let mut query = false;
54
55 // Check if the command is a query (ends with '?')
56 if let Some(prefix) = value.strip_suffix('?') {
57 value = prefix;
58 query = true;
59 }
60
61 // Process each part of the command (separated by colons)
62 for part in value.split(':').map(str::trim) {
63 if part.is_empty() {
64 continue;
65 }
66
67 // Check if this part is optional (enclosed in square brackets)
68 let (part, optional) = if part.starts_with('[') && part.ends_with(']') {
69 (&part[1..part.len() - 1], true)
70 } else {
71 (part, false)
72 };
73
74 // The short form consists of only the uppercase letters
75 let short = part.chars().filter(|c| !c.is_lowercase()).collect();
76 // The long form is the entire part in uppercase
77 let long = part.to_uppercase();
78
79 parts.push(CommandPart {
80 optional,
81 short,
82 long,
83 });
84 }
85
86 Ok(Command { parts, query })
87 }
88}
89
90impl Command {
91 /// Returns whether this command is a query (ends with a question mark).
92 pub fn is_query(&self) -> bool {
93 self.query
94 }
95
96 /// Returns the canonical (long-form) representation of this command.
97 ///
98 /// This is the complete command with all parts in their long form,
99 /// separated by colons, and with a question mark at the end if it's a query.
100 pub fn canonical_path(&self) -> String {
101 // Build the path using all long forms
102 let path = self.parts.iter().fold(String::new(), |a, b| {
103 if a.is_empty() {
104 b.long.clone()
105 } else {
106 a + ":" + &b.long
107 }
108 });
109
110 if self.query { path + "?" } else { path }
111 }
112
113 /// Generates all valid paths for this command.
114 ///
115 /// Since SCPI commands can have optional parts and each part can be
116 /// specified in either short or long form, this method generates
117 /// all possible valid combinations.
118 ///
119 /// # Returns
120 /// A vector of all valid command paths
121 pub fn paths(&self) -> Vec<CommandPath> {
122 let mut paths: Vec<CommandPath> = vec![vec![]];
123
124 for part in &self.parts {
125 let mut new_paths: Vec<CommandPath> = Vec::new();
126
127 for path in &mut paths {
128 // Add the long form
129 let mut long_path = path.clone();
130 long_path.push(part.long.clone());
131 new_paths.push(long_path);
132
133 // Add the short form if it's different from the long form
134 if part.short != part.long {
135 let mut short_path = path.clone();
136 short_path.push(part.short.clone());
137 new_paths.push(short_path);
138 }
139
140 // If this part is optional, add a path without it
141 if part.optional {
142 new_paths.push(path.clone());
143 }
144 }
145
146 paths = new_paths;
147 }
148
149 paths
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_all_paths() {
159 let cmd = Command::try_from("[STATus]:TIMe?").unwrap();
160 let paths: Vec<CommandPath> = cmd.paths();
161
162 // Ensure we generate all valid path combinations:
163 // 1. With STATUS (long) + TIME (long)
164 assert!(paths.iter().any(|p| p.as_ref() == vec!["STATUS", "TIME"]));
165 // 2. With STATUS (long) + TIM (short)
166 assert!(paths.iter().any(|p| p.as_ref() == vec!["STATUS", "TIM"]));
167 // 3. With STAT (short) + TIME (long)
168 assert!(paths.iter().any(|p| p.as_ref() == vec!["STAT", "TIME"]));
169 // 4. With STAT (short) + TIM (short)
170 assert!(paths.iter().any(|p| p.as_ref() == vec!["STAT", "TIM"]));
171 // 5. With just TIME (long) - since STATUS is optional
172 assert!(paths.iter().any(|p| p.as_ref() == vec!["TIME"]));
173 // 6. With just TIM (short) - since STATUS is optional
174 assert!(paths.iter().any(|p| p.as_ref() == vec!["TIM"]));
175 }
176
177 #[test]
178 fn test_is_query() {
179 let cmd = Command::try_from("SYSTem:ERRor?").unwrap();
180 assert!(cmd.is_query());
181
182 let cmd = Command::try_from("SYSTem:ERRor").unwrap();
183 assert!(!cmd.is_query());
184 }
185
186 #[test]
187 fn test_canonical_path() {
188 let cmd = Command::try_from("[STATus]:TIMe?").unwrap();
189 assert_eq!(cmd.canonical_path(), "STATUS:TIME?");
190
191 let cmd = Command::try_from("SYSTem:ERRor:NEXT").unwrap();
192 assert_eq!(cmd.canonical_path(), "SYSTEM:ERROR:NEXT");
193 }
194}