1 // Helper methods for parsing HFP AT command codes.  While most AT commands are processed at lower
2 // levels, some commands are not part of the HFP specification or need to be parsed within Floss for
3 // whatever reason.
4 
5 use std::collections::HashMap;
6 
7 /// The AT command type indicated.
8 #[derive(Clone, Debug, PartialEq)]
9 pub enum AtCommandType {
10     Set,
11     Query,
12     Test,
13     Execute,
14 }
15 
16 // Delimiters for AT commands. Execute has no delimiter.
17 const AT_COMMAND_DELIMITER_SET: &str = "=";
18 const AT_COMMAND_DELIMITER_QUERY: &str = "?";
19 const AT_COMMAND_DELIMITER_TEST: &str = "=?";
20 
21 // Strings for indicating which spec is being used. Apple's XAPL/IPHONEACCEV and Plantronics/Poly's
22 // XEVENT are supported.
23 const AT_COMMAND_VENDOR_APPLE: &str = "Apple";
24 const AT_COMMAND_VENDOR_PLANTRONICS: &str = "Plantronics";
25 
26 // Vendor-specific commands and attributes.
27 const AT_COMMAND_VENDOR_XAPL: &str = "XAPL";
28 const AT_COMMAND_VENDOR_IPHONEACCEV: &str = "IPHONEACCEV";
29 const AT_COMMAND_VENDOR_IPHONEACCEV_BATTERY: &str = "1";
30 const AT_COMMAND_VENDOR_XEVENT: &str = "XEVENT";
31 
32 /// Known types of data contained within commands.
33 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
34 pub enum AtCommandDataType {
35     IPhoneAccevBatteryLevel,
36     XeventBatteryLevel,
37     XeventBatteryLevelRange,
38     XeventEvent,
39 }
40 
41 /// Details of an AtCommand broken into parts representing varying degrees of extraction and
42 /// interpretation.
43 #[derive(Clone)]
44 pub struct AtCommand {
45     // The original, unparsed, AT command
46     pub raw: String,
47     // The nature of the command according to AT command specifications
48     pub at_type: AtCommandType,
49     // The actual command being sent (AT+<command>=?)
50     pub command: String,
51     // Unparsed arguments from the raw command string, in order
52     pub raw_args: Option<Vec<String>>,
53     // For vendor-specific AT commands
54     pub vendor: Option<String>,
55     // For commands with known value types
56     pub data: Option<HashMap<AtCommandDataType, String>>,
57 }
58 
59 const AT_COMMAND_ARG_DELIMITER: &str = ",";
60 
61 /// Attempt to extract as much data as possible from the AT command. For commands of a known type,
62 /// attempt to extract known fields and validate the format.
parse_at_command_data(at_string: String) -> Result<AtCommand, String>63 pub fn parse_at_command_data(at_string: String) -> Result<AtCommand, String> {
64     // All AT commands should be of the form AT+<command> but may be passed around as +<command> or
65     // <command>. We remove those here for convenience.
66     let clean_at_string = at_string.strip_prefix("+").unwrap_or(&at_string);
67     let clean_at_string = clean_at_string.strip_prefix("AT+").unwrap_or(&clean_at_string);
68     if clean_at_string.is_empty() {
69         return Err("Cannot parse empty AT command".to_string());
70     }
71     let at_type = parse_at_command_type(clean_at_string.to_string());
72     let at_type_delimiter = match at_type {
73         AtCommandType::Set => AT_COMMAND_DELIMITER_SET,
74         AtCommandType::Query => AT_COMMAND_DELIMITER_QUERY,
75         AtCommandType::Test => AT_COMMAND_DELIMITER_TEST,
76         AtCommandType::Execute => "",
77     };
78     // We want to keep the flow of this method consistent, but AtCommandType::Execute commands do
79     // not have arguments. To resolve this we split those commands differently.
80     let mut command_parts = clean_at_string
81         .splitn(if at_type == AtCommandType::Execute { 1 } else { 2 }, at_type_delimiter);
82     let command = match command_parts.next() {
83         Some(command) => command,
84         // In practice this cannot happen as parse_at_command_type already found the delimiter.
85         None => return Err("No command supplied".to_string()),
86     };
87     let vendor = match command {
88         AT_COMMAND_VENDOR_XAPL => Some(AT_COMMAND_VENDOR_APPLE.to_string()),
89         AT_COMMAND_VENDOR_IPHONEACCEV => Some(AT_COMMAND_VENDOR_APPLE.to_string()),
90         AT_COMMAND_VENDOR_XEVENT => Some(AT_COMMAND_VENDOR_PLANTRONICS.to_string()),
91         _ => None,
92     };
93     let raw_args = match command_parts.next() {
94         Some(arg_string) => {
95             if arg_string == "" {
96                 None
97             } else {
98                 Some(
99                     arg_string
100                         .split(AT_COMMAND_ARG_DELIMITER)
101                         .map(|arg| arg.to_string())
102                         .collect::<Vec<String>>(),
103                 )
104             }
105         }
106         None => None,
107     };
108     let data = match (raw_args.clone(), command) {
109         (Some(args), AT_COMMAND_VENDOR_IPHONEACCEV) => Some(extract_iphoneaccev_data(args)?),
110         (Some(args), AT_COMMAND_VENDOR_XEVENT) => Some(extract_xevent_data(args)?),
111         (Some(_), _) => None,
112         (None, _) => None,
113     };
114     Ok(AtCommand {
115         raw: at_string.to_string(),
116         at_type: at_type,
117         command: command.to_string(),
118         raw_args: raw_args,
119         vendor: vendor,
120         data: data,
121     })
122 }
123 
124 /// If present, battery data is extracted and returned as an integer in the range of [0, 100]. If
125 /// there is no battery data or the improperly formatted data, an error is returned.
calculate_battery_percent(at_command: AtCommand) -> Result<u32, String>126 pub fn calculate_battery_percent(at_command: AtCommand) -> Result<u32, String> {
127     match at_command.data {
128         Some(data) => {
129             match data.get(&AtCommandDataType::IPhoneAccevBatteryLevel) {
130                 Some(battery_level) => match battery_level.parse::<u32>() {
131                     // The Apple Accessory Design Guidelines indicate
132                     // this will be a value in the range [0, 9]. The
133                     // guidelines do not specify that this maps to
134                     // [10, 100] but that is how other Bluetooth
135                     // stacks interpret it so we do so as well.
136                     // See https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf
137                     // Section 27.1 HFP Command AT+IPHONEACCEV
138                     Ok(level) => return Ok((level + 1) * 10),
139                     Err(e) => return Err(e.to_string()),
140                 },
141                 None => (),
142             }
143             match data.get(&AtCommandDataType::XeventBatteryLevel) {
144                 Some(battery_level) => {
145                     match data.get(&AtCommandDataType::XeventBatteryLevelRange) {
146                         Some(battery_level_range) => {
147                             match (battery_level.parse::<u32>(), battery_level_range.parse::<u32>())
148                             {
149                                 (Ok(level), Ok(range)) => {
150                                     if level > range {
151                                         return Err(format!(
152                                             "Invalid battery level {}/{}",
153                                             level, range
154                                         ));
155                                     }
156                                     // Mathematically it is not possible to represent anything
157                                     // meaningful if there are not at least two options for
158                                     // BatteryLevel.
159                                     if range < 2 {
160                                         return Err(
161                                             "BatteryLevelRange must be at least 2".to_string()
162                                         );
163                                     }
164                                     return Ok((f64::from(level) / f64::from(range - 1) * 100.0)
165                                         .floor()
166                                         as u32);
167                                 }
168                                 (Err(e), _) => return Err(e.to_string()),
169                                 (Ok(_), Err(e)) => return Err(e.to_string()),
170                             }
171                         }
172                         None => return Err("BatteryLevelRange missing".to_string()),
173                     }
174                 }
175                 None => (),
176             }
177         }
178         None => return Err("No battery data found".to_string()),
179     }
180     Err("No battery data found".to_string())
181 }
182 
parse_at_command_type(command: String) -> AtCommandType183 fn parse_at_command_type(command: String) -> AtCommandType {
184     if command.contains(AT_COMMAND_DELIMITER_TEST) {
185         return AtCommandType::Test;
186     }
187     if command.contains(AT_COMMAND_DELIMITER_QUERY) {
188         return AtCommandType::Query;
189     }
190     if command.contains(AT_COMMAND_DELIMITER_SET) {
191         return AtCommandType::Set;
192     }
193     return AtCommandType::Execute;
194 }
195 
196 // Format:
197 // AT+IPHONEACCEV=[NumberOfIndicators],[IndicatorType],[IndicatorValue]
extract_iphoneaccev_data( args: Vec<String>, ) -> Result<HashMap<AtCommandDataType, String>, String>198 fn extract_iphoneaccev_data(
199     args: Vec<String>,
200 ) -> Result<HashMap<AtCommandDataType, String>, String> {
201     let num_provided_args: u32 = match args.len().try_into() {
202         Ok(num) => num,
203         Err(e) => return Err(e.to_string()),
204     };
205     let mut args = args.iter();
206     match args.next() {
207         Some(num_claimed) => {
208             let num_claimed = match num_claimed.parse::<u32>() {
209                 Ok(num) => num * 2 + 1,
210                 Err(e) => return Err(e.to_string()),
211             };
212             if num_claimed != num_provided_args {
213                 return Err(format!(
214                     "{} indicators were claimed but only {} were found",
215                     num_claimed, num_provided_args
216                 ));
217             }
218         }
219         None => return Err("Expected at least one argument (NumberOfIndicators)".to_string()),
220     };
221     let mut data = HashMap::new();
222     while let Some(indicator_type) = args.next() {
223         let indicator_value = args
224             .next()
225             .ok_or(format!("Failed to find matching value for indicator {}", indicator_type))?;
226         // We currently only support battery-related data
227         let indicator_type: &str = indicator_type;
228         match indicator_type {
229             AT_COMMAND_VENDOR_IPHONEACCEV_BATTERY => {
230                 data.insert(
231                     AtCommandDataType::IPhoneAccevBatteryLevel,
232                     indicator_value.to_string(),
233                 );
234             }
235             _ => continue,
236         }
237     }
238     Ok(data)
239 }
240 
extract_xevent_data(args: Vec<String>) -> Result<HashMap<AtCommandDataType, String>, String>241 fn extract_xevent_data(args: Vec<String>) -> Result<HashMap<AtCommandDataType, String>, String> {
242     let mut data = HashMap::new();
243     let mut args = args.iter();
244     let xevent_type = match args.next() {
245         Some(event_type) => event_type,
246         None => return Err("Expected at least one argument".to_string()),
247     };
248     data.insert(AtCommandDataType::XeventEvent, xevent_type.to_string());
249 
250     // For now we only support BATTERY events.
251     if xevent_type != "BATTERY" {
252         return Ok(data);
253     }
254     // Format:
255     // AT+XEVENT=BATTERY,[Level],[NumberOfLevel],[MinutesOfTalk],[IsCharging]
256     // Battery percentage = 100 * ( Level / (NumberOfLevel - 1 ) )
257     match args.next() {
258         Some(battery_level) => {
259             data.insert(AtCommandDataType::XeventBatteryLevel, battery_level.to_string());
260         }
261         None => return Err("Expected BatteryLevel argument".to_string()),
262     }
263     match args.next() {
264         Some(battery_level_range) => {
265             data.insert(
266                 AtCommandDataType::XeventBatteryLevelRange,
267                 battery_level_range.to_string(),
268             );
269         }
270         None => return Err("Expected BatterLevelRange".to_string()),
271     }
272     // There are more arguments but we don't yet use them.
273     Ok(data)
274 }
275 
276 #[cfg(test)]
277 mod tests {
278     use super::*;
279 
280     #[test]
test_parse_empty_fails()281     fn test_parse_empty_fails() {
282         let at_command = parse_at_command_data("".to_string());
283         assert!(at_command.is_err());
284 
285         let at_command = parse_at_command_data("+".to_string());
286         assert!(at_command.is_err());
287 
288         let at_command = parse_at_command_data("AT+".to_string());
289         assert!(at_command.is_err());
290     }
291 
292     #[test]
test_at_string_copied()293     fn test_at_string_copied() {
294         // A basic command with + preceding
295         let at_command = parse_at_command_data("+CMD".to_string()).unwrap();
296         assert_eq!(at_command.raw, "+CMD");
297     }
298 
299     #[test]
test_parse_command_type()300     fn test_parse_command_type() {
301         let at_command = parse_at_command_data("CMD=".to_string()).unwrap();
302         assert_eq!(at_command.at_type, AtCommandType::Set);
303 
304         let at_command = parse_at_command_data("CMD?".to_string()).unwrap();
305         assert_eq!(at_command.at_type, AtCommandType::Query);
306 
307         let at_command = parse_at_command_data("CMD=?".to_string()).unwrap();
308         assert_eq!(at_command.at_type, AtCommandType::Test);
309 
310         let at_command = parse_at_command_data("CMD".to_string()).unwrap();
311         assert_eq!(at_command.at_type, AtCommandType::Execute);
312     }
313 
314     #[test]
test_parse_command()315     fn test_parse_command() {
316         // A basic command
317         let at_command = parse_at_command_data("CMD".to_string()).unwrap();
318         assert_eq!(at_command.command, "CMD");
319 
320         // A basic command with AT+ preceding
321         let at_command = parse_at_command_data("AT+CMD".to_string()).unwrap();
322         assert_eq!(at_command.command, "CMD");
323 
324         // A basic command with arguments
325         let at_command = parse_at_command_data("CMD=a,b,c".to_string()).unwrap();
326         assert_eq!(at_command.command, "CMD");
327     }
328 
329     #[test]
test_parse_args()330     fn test_parse_args() {
331         // No args
332         let at_command = parse_at_command_data("AT+CMD".to_string()).unwrap();
333         assert_eq!(at_command.raw_args, None);
334 
335         // With args
336         let at_command = parse_at_command_data("AT+CMD=a,b,c".to_string()).unwrap();
337         assert_eq!(
338             at_command.raw_args,
339             Some(vec!["a".to_string(), "b".to_string(), "c".to_string()])
340         );
341     }
342 
343     #[test]
test_parse_vendor()344     fn test_parse_vendor() {
345         // With no known vendor
346         let at_command = parse_at_command_data("AT+CMD".to_string()).unwrap();
347         assert_eq!(at_command.vendor, None);
348 
349         // With XAPL
350         let at_command = parse_at_command_data("AT+XAPL".to_string()).unwrap();
351         assert_eq!(at_command.vendor, Some(AT_COMMAND_VENDOR_APPLE.to_string()));
352 
353         // With IPHONEACCEV
354         let at_command = parse_at_command_data("AT+IPHONEACCEV".to_string()).unwrap();
355         assert_eq!(at_command.vendor, Some(AT_COMMAND_VENDOR_APPLE.to_string()));
356 
357         // With XEVENT
358         let at_command = parse_at_command_data("AT+XEVENT".to_string()).unwrap();
359         assert_eq!(at_command.vendor, Some(AT_COMMAND_VENDOR_PLANTRONICS.to_string()));
360     }
361 
362     #[test]
test_parse_iphoneaccev_data()363     fn test_parse_iphoneaccev_data() {
364         // No args
365         let at_command = parse_at_command_data("AT+IPHONEACCEV=".to_string()).unwrap();
366         assert_eq!(at_command.data, None);
367 
368         // Battery args
369         let at_command = parse_at_command_data("AT+IPHONEACCEV=1,1,2".to_string()).unwrap();
370         assert_eq!(
371             at_command.data,
372             Some(HashMap::from([(AtCommandDataType::IPhoneAccevBatteryLevel, "2".to_string())]))
373         );
374 
375         // Multiple args
376         let at_command = parse_at_command_data("AT+IPHONEACCEV=2,2,3,1,2".to_string()).unwrap();
377         assert_eq!(
378             at_command.data,
379             Some(HashMap::from([(AtCommandDataType::IPhoneAccevBatteryLevel, "2".to_string())]))
380         );
381 
382         // Invalid arg count
383         let at_command = parse_at_command_data("AT+IPHONEACCEV=3,1,2".to_string());
384         assert!(at_command.is_err());
385     }
386 
387     #[test]
test_parse_xevent_data()388     fn test_parse_xevent_data() {
389         // No args
390         let at_command = parse_at_command_data("AT+XEVENT=".to_string()).unwrap();
391         assert_eq!(at_command.data, None);
392 
393         // No args
394         let at_command = parse_at_command_data("AT+XEVENT=DON".to_string()).unwrap();
395         assert_eq!(
396             at_command.data,
397             Some(HashMap::from([(AtCommandDataType::XeventEvent, "DON".to_string())]))
398         );
399     }
400 
401     #[test]
test_parse_xevent_battery_data()402     fn test_parse_xevent_battery_data() {
403         // Missing args
404         let at_command = parse_at_command_data("AT+XEVENT=BATTERY".to_string());
405         assert!(at_command.is_err());
406 
407         let at_command = parse_at_command_data("AT+XEVENT=BATTERY,5,9,10,0".to_string()).unwrap();
408         assert_eq!(
409             at_command.data,
410             Some(HashMap::from([
411                 (AtCommandDataType::XeventEvent, "BATTERY".to_string()),
412                 (AtCommandDataType::XeventBatteryLevel, "5".to_string()),
413                 (AtCommandDataType::XeventBatteryLevelRange, "9".to_string()),
414             ]))
415         );
416     }
417 
418     #[test]
test_calculate_battery_percent()419     fn test_calculate_battery_percent() {
420         // Non-battery command
421         let at_command = parse_at_command_data("AT+CMD".to_string());
422         assert!(!at_command.is_err());
423         let battery_level = calculate_battery_percent(at_command.unwrap());
424         assert!(battery_level.is_err());
425 
426         // Apple - no battery
427         let at_command = parse_at_command_data("AT+IPHONEACCEV=1,2,3".to_string());
428         assert!(!at_command.is_err());
429         let battery_level = calculate_battery_percent(at_command.unwrap());
430         assert!(battery_level.is_err());
431 
432         // Apple
433         let at_command = parse_at_command_data("AT+IPHONEACCEV=1,1,2".to_string());
434         assert!(!at_command.is_err());
435         let battery_level = calculate_battery_percent(at_command.unwrap()).unwrap();
436         assert_eq!(battery_level, 30);
437 
438         // Plantronics - missing args
439         let at_command = parse_at_command_data("AT+XEVENT=BATTERY".to_string());
440         assert!(at_command.is_err());
441 
442         // Plantronics
443         let at_command = parse_at_command_data("AT+XEVENT=BATTERY,5,11,10,0".to_string());
444         assert!(!at_command.is_err());
445         let battery_level = calculate_battery_percent(at_command.unwrap()).unwrap();
446         assert_eq!(battery_level, 50);
447     }
448 }
449