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