1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "camelCase")]
8pub enum AtResultFormat {
9 #[default]
10 Verbose,
11 Numeric,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16#[serde(rename_all = "camelCase")]
17pub struct ParsedAtResponse {
18 pub status: AtParseStatus,
19 pub lines: Vec<String>,
20 pub solicited_body: Vec<String>,
21 pub urc_lines: Vec<String>,
22 pub final_line: Option<String>,
23}
24
25#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
26#[serde(rename_all = "lowercase")]
27pub enum AtParseStatus {
28 Ok,
29 Error,
30 Cme,
31 Cms,
32 Unknown,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(rename_all = "camelCase")]
38pub enum ExchangeMatch {
39 Ok,
40 Error,
41 CmeError {
44 code: Option<i32>,
45 },
46 CmsError {
48 code: Option<i32>,
49 },
50 NoCarrier,
51 Busy,
52 NoAnswer,
53 NoDialtone,
54 SendOk,
56 SendFail,
58 Intermediate {
59 line: String,
60 },
61 Substring {
62 term: String,
63 },
64 Idle,
65}
66
67impl ExchangeMatch {
68 pub fn to_parse_status(&self) -> AtParseStatus {
69 match self {
70 ExchangeMatch::Ok | ExchangeMatch::SendOk => AtParseStatus::Ok,
71 ExchangeMatch::Error
72 | ExchangeMatch::NoCarrier
73 | ExchangeMatch::Busy
74 | ExchangeMatch::NoAnswer
75 | ExchangeMatch::NoDialtone
76 | ExchangeMatch::SendFail => AtParseStatus::Error,
77 ExchangeMatch::CmeError { .. } => AtParseStatus::Cme,
78 ExchangeMatch::CmsError { .. } => AtParseStatus::Cms,
79 ExchangeMatch::Intermediate { .. }
80 | ExchangeMatch::Substring { .. }
81 | ExchangeMatch::Idle => AtParseStatus::Unknown,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct ExchangeResponse {
90 pub raw: Vec<u8>,
91 pub matched: ExchangeMatch,
92 pub lines: Vec<String>,
93 pub status: AtParseStatus,
94 pub solicited_body: Vec<String>,
95 pub urc_lines: Vec<String>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct AtCommandResult {
102 pub command: String,
103 pub response: String,
104 pub raw: Vec<u8>,
105 pub matched: ExchangeMatch,
106 pub lines: Vec<String>,
107 pub status: AtParseStatus,
108 pub solicited_body: Vec<String>,
109 pub urc_lines: Vec<String>,
110 pub timed_out: bool,
111}
112
113impl AtCommandResult {
114 pub fn from_exchange(command: String, exchange: ExchangeResponse) -> Self {
115 let response = String::from_utf8_lossy(&exchange.raw).into_owned();
116 Self {
117 command,
118 response,
119 raw: exchange.raw,
120 matched: exchange.matched,
121 lines: exchange.lines,
122 status: exchange.status,
123 solicited_body: exchange.solicited_body,
124 urc_lines: exchange.urc_lines,
125 timed_out: false,
126 }
127 }
128
129 pub fn failed(command: String, reason: String) -> Self {
130 Self {
131 command,
132 response: reason.clone(),
133 raw: reason.into_bytes(),
134 matched: ExchangeMatch::Error,
135 lines: Vec::new(),
136 status: AtParseStatus::Error,
137 solicited_body: Vec::new(),
138 urc_lines: Vec::new(),
139 timed_out: false,
140 }
141 }
142}
143
144impl ExchangeResponse {
145 pub fn from_raw(
146 raw: Vec<u8>,
147 matched: ExchangeMatch,
148 command: &str,
149 solicited_prefixes: &[String],
150 result_format: AtResultFormat,
151 ) -> Self {
152 let merged = merge_solicited_prefixes(command, solicited_prefixes);
153 let parsed = parse_at_response(&raw, command, &merged, result_format);
154 Self {
155 status: parsed.status,
156 lines: parsed.lines,
157 solicited_body: parsed.solicited_body,
158 urc_lines: parsed.urc_lines,
159 matched,
160 raw,
161 }
162 }
163}
164
165pub fn merge_solicited_prefixes(command: &str, user: &[String]) -> Vec<String> {
167 let mut out: Vec<String> = derive_solicited_prefixes(command);
168 for p in user {
169 let t = p.trim().to_string();
170 if !t.is_empty() && !out.iter().any(|x| x == &t) {
171 out.push(t);
172 }
173 }
174 out
175}
176
177pub fn derive_solicited_prefixes(command: &str) -> Vec<String> {
182 let cmd = normalize_command(command);
183 if !cmd.get(..2).is_some_and(|at| at.eq_ignore_ascii_case("AT")) {
184 return Vec::new();
185 }
186 let mut out: Vec<String> = Vec::new();
187 for header in cmd[2..].split(';').filter_map(part_header) {
188 if !out.contains(&header) {
189 out.push(header);
190 }
191 }
192 out
193}
194
195fn part_header(part: &str) -> Option<String> {
199 let part = part.trim();
200 let b = part.as_bytes();
201 let digits = |mut i: usize| {
202 while b.get(i).is_some_and(u8::is_ascii_digit) {
203 i += 1;
204 }
205 i
206 };
207 let mut i = 0;
208 while let Some(&c) = b.get(i) {
209 i = match c.to_ascii_uppercase() {
210 b' ' => i + 1,
211 b'&' if b.get(i + 1).is_some_and(u8::is_ascii_alphabetic) => digits(i + 2),
212 b'D' => break,
213 b'S' if b.get(i + 1).is_some_and(u8::is_ascii_digit) => {
214 let j = digits(i + 1);
215 match b.get(j) {
216 Some(b'=') => digits(j + 1),
217 Some(b'?') => j + 1,
218 _ => j,
219 }
220 }
221 c if c.is_ascii_alphabetic() => digits(i + 1),
222 _ => break,
223 };
224 }
225 match b.get(i) {
226 Some(c) if i > 0 && b"+^#$%*".contains(c) => command_header(&part[i..]),
227 _ => command_header(part),
228 }
229}
230
231fn command_header(part: &str) -> Option<String> {
233 let part = part.trim();
234 let prefix_char = part.chars().next().filter(|c| "+^#$%*&".contains(*c));
235 let name: String = part
236 .trim_start_matches(['+', '^', '#', '$', '%', '*', '&'])
237 .chars()
238 .take_while(|c| c.is_ascii_alphanumeric())
239 .collect::<String>()
240 .to_ascii_uppercase();
241 if name.is_empty() {
242 return None;
243 }
244 Some(match prefix_char {
245 Some(ch) => format!("{ch}{name}:"),
246 None => format!("{name}:"),
247 })
248}
249
250const MAX_CUSD_SPAN: usize = 2_048;
252
253pub fn split_lines(text: &str) -> Vec<String> {
262 let segments: Vec<&str> = text.split('\n').collect();
263 group_segments(&segments, false).0
264}
265
266fn group_segments(segments: &[&str], wait_for_quote: bool) -> (Vec<String>, usize) {
270 let mut out = Vec::new();
271 let mut i = 0;
272 while i < segments.len() {
273 let emitted = out.len();
274 let mut rest = segments[i].trim_start();
275 if let Some(head) = glued_final_head(rest.trim_end()) {
276 out.push(head.to_string());
277 rest = &rest[head.len()..];
278 }
279 let line = rest.trim_end();
280 if line.starts_with("+CUSD:") {
281 if has_open_quote(line) {
282 match close_cusd_quote(segments, i, line.len()) {
283 QuoteEnd::Closed(j) => {
284 let mut joined = rest.to_string();
285 for seg in &segments[i + 1..=j] {
286 joined.push('\n');
287 joined.push_str(seg);
288 }
289 out.push(joined.trim_end().to_string());
290 i = j + 1;
291 continue;
292 }
293 QuoteEnd::Open if wait_for_quote => {
294 out.truncate(emitted);
295 return (out, i);
296 }
297 QuoteEnd::Open | QuoteEnd::Cut => {}
298 }
299 }
300 out.push(line.to_string());
301 } else {
302 for line in line.split('\r').map(str::trim).filter(|l| !l.is_empty()) {
303 out.push(line.to_string());
304 }
305 }
306 i += 1;
307 }
308 (out, segments.len())
309}
310
311enum QuoteEnd {
312 Closed(usize),
314 Cut,
316 Open,
318}
319
320fn close_cusd_quote(segments: &[&str], first: usize, first_len: usize) -> QuoteEnd {
321 let mut span = first_len;
322 for (j, seg) in segments.iter().enumerate().skip(first + 1) {
323 span += seg.len() + 1;
324 let seg = seg.trim();
325 if span > MAX_CUSD_SPAN || classify_final_line(seg).is_some() || is_urc_shape(seg) {
326 return QuoteEnd::Cut;
327 }
328 if has_open_quote(seg) {
329 return QuoteEnd::Closed(j);
330 }
331 }
332 QuoteEnd::Open
333}
334
335fn has_open_quote(s: &str) -> bool {
336 s.matches('"').count() % 2 == 1
337}
338
339fn glued_final_head(line: &str) -> Option<&str> {
341 let idx = line.find('+').filter(|&i| i > 0)?;
342 let (head, rest) = line.split_at(idx);
343 (classify_final_line(head).is_some() && is_urc_shape(rest)).then_some(head)
344}
345
346fn is_urc_shape(line: &str) -> bool {
349 let t = line.trim();
350 if t == "RING" {
351 return true;
352 }
353 let Some(body) = t.strip_prefix(['+', '^', '#', '$', '%', '*']) else {
354 return false;
355 };
356 body.split_once(':').is_some_and(|(name, _)| {
357 name.starts_with(|c: char| c.is_ascii_uppercase())
358 && name
359 .chars()
360 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
361 })
362}
363
364fn two_line_header(line: &str) -> Option<bool> {
368 if line.starts_with("+CMGR:") || line.starts_with("+CMGL:") {
369 return Some(true);
370 }
371 if line.starts_with("+CMT:") || line.starts_with("+CBM:") {
372 return Some(false);
373 }
374 let len = line.strip_prefix("+CDS:")?.trim();
375 (!len.is_empty() && len.chars().all(|c| c.is_ascii_digit())).then_some(false)
376}
377
378pub fn is_ext_info_line(line: &str) -> bool {
380 let t = line.trim();
381 t.starts_with('+')
382 || t.starts_with('^')
383 || t.starts_with('#')
384 || t.starts_with('$')
385 || t.starts_with('%')
386 || t.starts_with('*')
387}
388
389pub fn is_numeric_result_line(line: &str) -> bool {
391 let t = line.trim();
392 !t.is_empty() && t.len() <= 2 && t.chars().all(|c| c.is_ascii_digit())
393}
394
395pub fn classify_intermediate_line(line: &str) -> bool {
397 let t = line.trim();
398 t == "CONNECT" || t == ">" || t.starts_with("CONNECT ")
399}
400
401pub fn classify_intermediate_line_with_format(line: &str, format: AtResultFormat) -> bool {
403 classify_intermediate_line(line) || (format == AtResultFormat::Numeric && line.trim() == "1")
404}
405
406pub fn classify_numeric_final_line(line: &str) -> Option<ExchangeMatch> {
414 if !is_numeric_result_line(line) {
415 return None;
416 }
417 match line.trim() {
418 "0" => Some(ExchangeMatch::Ok),
419 "3" => Some(ExchangeMatch::NoCarrier),
420 "4" => Some(ExchangeMatch::Error),
421 "6" => Some(ExchangeMatch::NoDialtone),
422 "7" => Some(ExchangeMatch::Busy),
423 "8" => Some(ExchangeMatch::NoAnswer),
424 _ => None,
425 }
426}
427
428fn classify_final_line_with_format(
429 line: &str,
430 format: AtResultFormat,
431 ignore_case: bool,
432) -> Option<ExchangeMatch> {
433 classify_final_line_cased(line, ignore_case).or_else(|| {
434 if format == AtResultFormat::Numeric {
435 classify_numeric_final_line(line)
436 } else {
437 None
438 }
439 })
440}
441
442fn complete_end(buf: &[u8], format: AtResultFormat) -> Option<usize> {
447 buf.iter()
448 .rposition(|&b| b == b'\n' || (b == b'\r' && format == AtResultFormat::Numeric))
449}
450
451pub fn at_final_line_complete(buf: &[u8], result_format: AtResultFormat) -> Option<ExchangeMatch> {
458 scan_completion(buf, result_format, false)
459}
460
461fn scan_completion(
463 buf: &[u8],
464 format: AtResultFormat,
465 intermediate: bool,
466) -> Option<ExchangeMatch> {
467 AtCompletion::new(intermediate, format).push(buf)
468}
469
470#[derive(Debug)]
475pub struct AtCompletion {
476 lines: LineAssembler,
477 classifier: LineClassifier,
478 format: AtResultFormat,
479 intermediate: bool,
480 last: Option<ExchangeMatch>,
481}
482
483impl AtCompletion {
484 pub fn new(intermediate: bool, format: AtResultFormat) -> Self {
486 Self {
487 lines: LineAssembler::new(format),
488 classifier: LineClassifier::new("", &[], format),
489 format,
490 intermediate,
491 last: None,
492 }
493 }
494
495 pub fn push(&mut self, chunk: &[u8]) -> Option<ExchangeMatch> {
497 for line in self.lines.push(chunk) {
498 self.judge(&line);
499 }
500 if self.intermediate && String::from_utf8_lossy(self.lines.tail()).trim() == ">" {
502 return Some(ExchangeMatch::Intermediate { line: ">".into() });
503 }
504 self.last.clone()
505 }
506
507 fn judge(&mut self, line: &str) {
508 let in_data = self.classifier.in_data();
509 match self.classifier.feed(line) {
510 LineKind::Final(m) => self.last = Some(m),
511 _ if in_data => {}
513 _ if self.intermediate && classify_intermediate_line_with_format(line, self.format) => {
514 self.last = Some(ExchangeMatch::Intermediate {
515 line: line.to_string(),
516 })
517 }
518 _ if line.get(..2).is_some_and(|p| p.eq_ignore_ascii_case("AT")) => self.last = None,
521 _ => {}
522 }
523 }
524}
525
526pub fn at_intermediate_line_complete(
533 buf: &[u8],
534 result_format: AtResultFormat,
535) -> Option<ExchangeMatch> {
536 scan_completion(buf, result_format, true)
537}
538
539pub fn classify_final_line(line: &str) -> Option<ExchangeMatch> {
544 classify_final_line_cased(line, true)
545}
546
547fn classify_final_line_cased(line: &str, ignore_case: bool) -> Option<ExchangeMatch> {
548 let trimmed = line.trim();
549 let upper;
550 let key = if ignore_case {
551 upper = trimmed.to_ascii_uppercase();
552 upper.as_str()
553 } else {
554 trimmed
555 };
556 match key {
558 "OK" => Some(ExchangeMatch::Ok),
559 "ERROR" => Some(ExchangeMatch::Error),
560 "NO CARRIER" => Some(ExchangeMatch::NoCarrier),
561 "BUSY" => Some(ExchangeMatch::Busy),
562 "NO ANSWER" => Some(ExchangeMatch::NoAnswer),
563 "NO DIALTONE" => Some(ExchangeMatch::NoDialtone),
564 "SEND OK" => Some(ExchangeMatch::SendOk),
565 "SEND FAIL" => Some(ExchangeMatch::SendFail),
566 s if s.starts_with("+CME ERROR:") => {
567 let (code, _) = parse_error_detail(&trimmed[11..], CME_TEXT_CODES);
568 Some(ExchangeMatch::CmeError { code })
569 }
570 s if s.starts_with("+CMS ERROR:") => {
571 let (code, _) = parse_error_detail(&trimmed[11..], CMS_TEXT_CODES);
572 Some(ExchangeMatch::CmsError { code })
573 }
574 _ => None,
575 }
576}
577
578const CME_TEXT_CODES: &[(&str, i32)] = &[
581 ("phone failure", 0),
582 ("no connection to phone", 1),
583 ("no connection", 1),
584 ("phone-adaptor link reserved", 2),
585 ("link reserved", 2),
586 ("operation not allowed", 3),
587 ("operation not supported", 4),
588 ("PH-SIM PIN required", 5),
589 ("PH-FSIM PIN required", 6),
590 ("PH-FSIM PUK required", 7),
591 ("SIM not inserted", 10),
592 ("SIM PIN required", 11),
593 ("SIM PUK required", 12),
594 ("SIM failure", 13),
595 ("SIM busy", 14),
596 ("SIM wrong", 15),
597 ("incorrect password", 16),
598 ("SIM PIN2 required", 17),
599 ("SIM PUK2 required", 18),
600 ("memory full", 20),
601 ("invalid index", 21),
602 ("not found", 22),
603 ("memory failure", 23),
604 ("text string too long", 24),
605 ("invalid characters in text string", 25),
606 ("dial string too long", 26),
607 ("invalid characters in dial string", 27),
608 ("no network service", 30),
609 ("no network", 30),
610 ("network timeout", 31),
611 ("timeout", 31),
612 ("network not allowed - emergency calls only", 32),
613 ("network personalization PIN required", 40),
614 ("network personalization PUK required", 41),
615 ("network subset personalization PIN required", 42),
616 ("network subset personalization PUK required", 43),
617 ("service provider personalization PIN required", 44),
618 ("service provider personalization PUK required", 45),
619 ("corporate personalization PIN required", 46),
620 ("corporate personalization PUK required", 47),
621 ("hidden key required", 48),
622 ("EAP method not supported", 49),
623 ("incorrect parameters", 50),
624 ("command implemented but currently disabled", 51),
625 ("command aborted by user", 52),
626 (
627 "not attached to network due to MT functionality restrictions",
628 53,
629 ),
630 (
631 "modem not allowed - MT restricted to emergency calls only",
632 54,
633 ),
634 (
635 "operation not allowed because of MT functionality restrictions",
636 55,
637 ),
638 (
639 "fixed dial number only allowed - called number is not a fixed dial number",
640 56,
641 ),
642 ("temporarily out of service due to other MT usage", 57),
643 ("language/alphabet not supported", 58),
644 ("language or alphabet not supported", 58),
645 ("unexpected data value", 59),
646 ("system failure", 60),
647 ("data missing", 61),
648 ("call barred", 62),
649 ("message waiting indication subscription failure", 63),
650 ("unknown", 100),
651 ("illegal MS", 103),
653 ("illegal ME", 106),
654 ("GPRS services not allowed", 107),
655 ("PLMN not allowed", 111),
656 ("location area not allowed", 112),
657 ("roaming not allowed in this location area", 113),
658 ("service option not supported", 132),
659 ("requested service option not subscribed", 133),
660 ("service option temporarily out of order", 134),
661 ("unspecified GPRS error", 148),
662 ("PDP authentication failure", 149),
663 ("invalid mobile class", 150),
664];
665
666const CMS_TEXT_CODES: &[(&str, i32)] = &[
668 ("ME failure", 300),
669 ("SMS service of ME reserved", 301),
670 ("SMS service reserved", 301),
671 ("operation not allowed", 302),
672 ("operation not supported", 303),
673 ("invalid PDU mode parameter", 304),
674 ("invalid PDU parameter", 304),
675 ("invalid text mode parameter", 305),
676 ("invalid text parameter", 305),
677 ("SIM not inserted", 310),
678 ("SIM PIN required", 311),
679 ("PH-SIM PIN required", 312),
680 ("SIM failure", 313),
681 ("SIM busy", 314),
682 ("SIM wrong", 315),
683 ("SIM PUK required", 316),
684 ("SIM PIN2 required", 317),
685 ("SIM PUK2 required", 318),
686 ("memory failure", 320),
687 ("invalid memory index", 321),
688 ("invalid index", 321),
689 ("memory full", 322),
690 ("SMSC address unknown", 330),
691 ("no network service", 331),
692 ("no network", 331),
693 ("network timeout", 332),
694 ("no +CNMA acknowledgement expected", 340),
695 ("unknown error", 500),
696];
697
698fn text_key(text: &str) -> String {
702 text.chars()
703 .filter(char::is_ascii_alphanumeric)
704 .collect::<String>()
705 .to_ascii_lowercase()
706 .replace("isation", "ization")
707}
708
709fn parse_error_detail(detail: &str, table: &[(&str, i32)]) -> (Option<i32>, Option<String>) {
711 let detail = detail.trim();
712 if let Ok(code) = detail.parse() {
713 return (Some(code), None);
714 }
715 if let Some((code, text)) = detail.split_once(':') {
716 if let Ok(code) = code.trim().parse() {
717 let text = text.trim();
718 return (Some(code), (!text.is_empty()).then(|| text.to_string()));
719 }
720 }
721 let text = detail.trim_matches('"').trim();
722 let key = text_key(text);
723 let code = table
724 .iter()
725 .find(|(message, _)| text_key(message) == key)
726 .map(|(_, code)| *code);
727 (code, (!text.is_empty()).then(|| text.to_string()))
728}
729
730#[derive(Debug, Clone, PartialEq, Eq)]
732enum LineKind {
733 Echo,
734 Body,
735 Urc,
736 Final(ExchangeMatch),
737}
738
739#[derive(Debug)]
742struct LineClassifier {
743 command: String,
744 prefixes: Vec<String>,
745 format: AtResultFormat,
746 echo_seen: bool,
747 pending: Option<(LineKind, bool)>,
750 in_text_body: bool,
752}
753
754impl LineClassifier {
755 fn new(command: &str, prefixes: &[String], format: AtResultFormat) -> Self {
756 Self {
757 command: normalize_command(command),
758 prefixes: prefixes.to_vec(),
759 format,
760 echo_seen: false,
761 pending: None,
762 in_text_body: false,
763 }
764 }
765
766 fn in_data(&self) -> bool {
768 self.pending.is_some() || self.in_text_body
769 }
770
771 fn feed(&mut self, line: &str) -> LineKind {
772 let in_data = self.in_data();
774 if let Some(m) = classify_final_line_with_format(line, self.format, !in_data) {
775 self.pending = None;
776 self.in_text_body = false;
777 return LineKind::Final(m);
778 }
779 if let Some((kind, text_body)) = self.pending.take() {
780 self.in_text_body = text_body;
781 return kind;
782 }
783 if !self.echo_seen && !self.command.is_empty() && lines_equivalent(line, &self.command) {
784 self.echo_seen = true;
785 return LineKind::Echo;
786 }
787 let strict = self.in_text_body || self.echo_seen || !self.command.is_empty();
792 let kind = if strict {
793 if is_urc_shape(line) && is_likely_urc(line, &self.prefixes) {
794 LineKind::Urc
795 } else {
796 LineKind::Body
797 }
798 } else if is_likely_urc(line, &self.prefixes) {
799 LineKind::Urc
800 } else {
801 LineKind::Body
802 };
803 if let Some(text_body) = two_line_header(line) {
804 self.pending = Some((kind.clone(), text_body));
805 }
806 kind
807 }
808}
809
810pub fn parse_at_response(
815 raw: &[u8],
816 command: &str,
817 solicited_prefixes: &[String],
818 result_format: AtResultFormat,
819) -> ParsedAtResponse {
820 let merged = merge_solicited_prefixes(command, solicited_prefixes);
821 let complete = match complete_end(raw, result_format) {
824 Some(end) => &raw[..=end],
825 None => raw,
826 };
827 let lines = split_lines(&String::from_utf8_lossy(complete));
828 let mut classifier = LineClassifier::new(command, &merged, result_format);
829 let kinds: Vec<LineKind> = lines.iter().map(|l| classifier.feed(l)).collect();
830 let echo_idx = kinds.iter().position(|k| *k == LineKind::Echo);
831 let final_idx = kinds
832 .iter()
833 .rposition(|k| matches!(k, LineKind::Final(_)))
834 .filter(|&i| echo_idx.is_none_or(|e| i > e));
835 let final_pos = final_idx.unwrap_or(lines.len());
838 let final_line = lines.get(final_pos).cloned();
839 let status = match final_idx.map(|i| &kinds[i]) {
840 Some(LineKind::Final(m)) => m.to_parse_status(),
841 _ => AtParseStatus::Unknown,
842 };
843
844 let mut urc_lines = Vec::new();
845 let mut solicited_body = Vec::new();
846 for (line, kind) in lines.iter().zip(&kinds).take(final_pos) {
847 match kind {
848 LineKind::Echo => {}
849 LineKind::Urc => urc_lines.push(line.clone()),
850 LineKind::Body => solicited_body.push(line.clone()),
851 LineKind::Final(_) if is_likely_urc(line, &merged) => urc_lines.push(line.clone()),
853 LineKind::Final(_) => solicited_body.push(line.clone()),
854 }
855 }
856 if let Some(i) = final_idx {
857 urc_lines.extend(lines.iter().skip(i + 1).cloned());
858 }
859
860 ParsedAtResponse {
861 status,
862 lines,
863 solicited_body,
864 urc_lines,
865 final_line,
866 }
867}
868
869fn normalize_command(command: &str) -> String {
870 command
871 .trim()
872 .trim_end_matches('\r')
873 .trim_end_matches('\n')
874 .to_string()
875}
876
877fn lines_equivalent(line: &str, command: &str) -> bool {
878 line.trim().eq_ignore_ascii_case(command)
879}
880
881pub fn is_likely_urc(line: &str, solicited_prefixes: &[String]) -> bool {
882 let trimmed = line.trim();
883 if !is_ext_info_line(trimmed) {
884 return matches!(trimmed, "RING" | "NO CARRIER" | "BUSY" | "NO ANSWER");
885 }
886 !solicited_prefixes
887 .iter()
888 .any(|p| trimmed.starts_with(p.trim()))
889}
890
891#[derive(Debug, Clone, Copy, PartialEq, Eq)]
893pub enum ExchangePhase {
894 AwaitingFirstLine,
896 CollectingSolicited,
898}
899
900pub const MAX_DEMUX_LINE: usize = 65_536;
903
904#[derive(Debug)]
909pub struct ExchangeDemux {
910 phase: ExchangePhase,
911 command: String,
912 solicited_prefixes: Vec<String>,
913 format: AtResultFormat,
914 lines: LineAssembler,
915 classifier: LineClassifier,
916}
917
918#[derive(Debug)]
922struct LineAssembler {
923 numeric: bool,
924 partial: Vec<u8>,
926 skipping: bool,
928}
929
930impl LineAssembler {
931 fn new(format: AtResultFormat) -> Self {
932 Self {
933 numeric: format == AtResultFormat::Numeric,
934 partial: Vec::new(),
935 skipping: false,
936 }
937 }
938
939 fn is_end(&self, b: u8) -> bool {
940 b == b'\n' || (self.numeric && b == b'\r')
941 }
942
943 fn tail(&self) -> &[u8] {
945 &self.partial
946 }
947
948 fn clear(&mut self) {
949 self.partial.clear();
950 self.skipping = false;
951 }
952
953 fn push(&mut self, mut chunk: &[u8]) -> Vec<String> {
954 if self.skipping {
955 let Some(e) = chunk.iter().position(|&b| self.is_end(b)) else {
956 return Vec::new();
957 };
958 self.skipping = false;
959 chunk = &chunk[e + 1..];
960 }
961 let Some(e) = chunk.iter().rposition(|&b| self.is_end(b)) else {
962 self.partial.extend_from_slice(chunk);
963 if self.partial.len() > MAX_DEMUX_LINE {
964 self.partial.clear();
965 self.skipping = true;
966 }
967 return Vec::new();
968 };
969 let end = self.partial.len() + e;
970 self.partial.extend_from_slice(chunk);
971 let text = String::from_utf8_lossy(&self.partial[..end]).into_owned();
972 let segments: Vec<&str> = text.split('\n').collect();
973 let (lines, used) = group_segments(&segments, true);
974 let cut = match used {
977 0 => 0,
978 n if n == segments.len() => end + 1,
979 n => self
980 .partial
981 .iter()
982 .enumerate()
983 .filter(|(_, &b)| b == b'\n')
984 .nth(n - 1)
985 .map_or(0, |(i, _)| i + 1),
986 };
987 self.partial.drain(..cut);
988 if self.partial.len() > MAX_DEMUX_LINE {
989 self.skipping = !self.partial.last().is_some_and(|&b| self.is_end(b));
990 self.partial.clear();
991 }
992 lines
993 }
994}
995
996impl ExchangeDemux {
997 pub fn new(command: &str, solicited_prefixes: &[String]) -> Self {
999 Self::with_format(command, solicited_prefixes, AtResultFormat::Verbose)
1000 }
1001
1002 pub fn with_format(
1004 command: &str,
1005 solicited_prefixes: &[String],
1006 format: AtResultFormat,
1007 ) -> Self {
1008 let command = normalize_command(command);
1009 let solicited_prefixes = merge_solicited_prefixes(&command, solicited_prefixes);
1010 Self {
1011 phase: ExchangePhase::AwaitingFirstLine,
1012 classifier: LineClassifier::new(&command, &solicited_prefixes, format),
1013 command,
1014 solicited_prefixes,
1015 format,
1016 lines: LineAssembler::new(format),
1017 }
1018 }
1019
1020 pub fn phase(&self) -> ExchangePhase {
1022 self.phase
1023 }
1024
1025 pub fn reset(&mut self) {
1026 self.phase = ExchangePhase::AwaitingFirstLine;
1027 self.lines.clear();
1028 self.classifier = LineClassifier::new(&self.command, &self.solicited_prefixes, self.format);
1029 }
1030
1031 pub fn process_chunk(&mut self, chunk: &[u8]) -> Vec<String> {
1036 let lines = self.lines.push(chunk);
1037 let mut live_urc = Vec::new();
1038 for line in lines {
1039 match self.classifier.feed(&line) {
1040 LineKind::Echo => self.phase = ExchangePhase::CollectingSolicited,
1041 LineKind::Urc => live_urc.push(line),
1042 LineKind::Body | LineKind::Final(_) => {}
1043 }
1044 }
1045 live_urc
1046 }
1047}
1048
1049pub fn urc_lines_from_drain(bytes: &[u8], solicited_prefixes: &[String]) -> Vec<String> {
1052 let mut classifier = LineClassifier::new("", solicited_prefixes, AtResultFormat::Verbose);
1053 split_lines(&String::from_utf8_lossy(bytes))
1054 .into_iter()
1055 .filter(|l| match classifier.feed(l) {
1056 LineKind::Urc => true,
1057 LineKind::Final(_) => is_likely_urc(l, solicited_prefixes),
1058 LineKind::Echo | LineKind::Body => false,
1059 })
1060 .collect()
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065 use super::AtResultFormat as F;
1066
1067 #[test]
1068 fn urc_after_echo_is_still_emitted() {
1069 let mut demux = ExchangeDemux::new("AT+CSQ", &[]);
1070 let urc = demux.process_chunk(b"AT+CSQ\r\r\n+CMTI: \"SM\",3\r\n+CSQ: 20,0\r\n\r\nOK\r\n");
1071 assert_eq!(urc, vec!["+CMTI: \"SM\",3".to_string()]);
1072 }
1073
1074 #[test]
1075 fn sms_prompt_without_trailing_lf_completes() {
1076 let m = at_intermediate_line_complete(b"AT+CMGS=20\r\r\n> ", F::Verbose);
1077 assert!(
1078 matches!(m, Some(ExchangeMatch::Intermediate { .. })),
1079 "{m:?}"
1080 );
1081 }
1082
1083 #[test]
1084 fn prompt_command_rejected_with_cms_error_completes() {
1085 let m = at_intermediate_line_complete(b"AT+CMGS=20\r\r\n+CMS ERROR: 304\r\n", F::Verbose);
1086 assert!(
1087 matches!(
1088 m,
1089 Some(ExchangeMatch::CmsError {
1090 code: Some(304),
1091 ..
1092 })
1093 ),
1094 "{m:?}"
1095 );
1096 }
1097
1098 #[test]
1100 fn trailing_bytes_after_final_line_do_not_hide_it() {
1101 let m = at_final_line_complete(b"AT+CMUX=0\r\r\nOK\r\n\xF9\xF9\xF9\xF9", F::Verbose);
1102 assert_eq!(m, Some(ExchangeMatch::Ok));
1103 }
1104
1105 #[test]
1106 fn parsed_status_ignores_trailing_bytes_and_urcs() {
1107 let p = parse_at_response(
1108 b"AT+CMUX=0\r\r\nOK\r\n\xF9\xF9",
1109 "AT+CMUX=0",
1110 &[],
1111 F::Verbose,
1112 );
1113 assert_eq!(p.status, AtParseStatus::Ok);
1114 let p = parse_at_response(b"AT\r\r\nOK\r\n\r\nRING\r\n", "AT", &[], F::Verbose);
1115 assert_eq!(p.status, AtParseStatus::Ok);
1116 assert_eq!(p.urc_lines, vec!["RING".to_string()]);
1117 }
1118
1119 #[test]
1120 fn non_utf8_noise_does_not_block_final_line() {
1121 let m = at_final_line_complete(b"\xFF\xFEAT\r\r\nOK\r\n", F::Verbose);
1122 assert_eq!(m, Some(ExchangeMatch::Ok));
1123 }
1124
1125 #[test]
1126 fn urc_in_same_read_after_ok_does_not_hide_it() {
1127 let m = at_final_line_complete(b"AT\r\r\nOK\r\n\r\nRING\r\n", F::Verbose);
1128 assert_eq!(m, Some(ExchangeMatch::Ok));
1129 assert_eq!(at_final_line_complete(b"+CSQ: 20,0\r\n", F::Verbose), None);
1130 }
1131
1132 use super::*;
1133
1134 #[test]
1135 fn final_line_ok_only_at_end() {
1136 let buf = b"AT\r\r\nOK\r\n";
1137 assert!(matches!(
1138 at_final_line_complete(buf, AtResultFormat::Verbose),
1139 Some(ExchangeMatch::Ok)
1140 ));
1141 assert!(at_final_line_complete(b"AT\r\r\nO", AtResultFormat::Verbose).is_none());
1142 }
1143
1144 #[test]
1145 fn numeric_atv0_ok() {
1146 let buf = b"AT\r\r\n0\r\n";
1147 assert!(at_final_line_complete(buf, AtResultFormat::Verbose).is_none());
1148 assert!(matches!(
1149 at_final_line_complete(buf, AtResultFormat::Numeric),
1150 Some(ExchangeMatch::Ok)
1151 ));
1152 }
1153
1154 #[test]
1155 fn numeric_no_carrier_is_final() {
1156 assert!(matches!(
1157 at_final_line_complete(b"ATD\r\r\n3\r\n", AtResultFormat::Numeric),
1158 Some(ExchangeMatch::NoCarrier)
1159 ));
1160 }
1161
1162 #[test]
1163 fn numeric_one_is_intermediate_not_final() {
1164 assert!(at_final_line_complete(b"ATD\r\r\n1\r\n", AtResultFormat::Numeric).is_none());
1165 assert!(matches!(
1166 at_intermediate_line_complete(b"ATD\r\r\n1\r\n", AtResultFormat::Numeric),
1167 Some(ExchangeMatch::Intermediate { .. })
1168 ));
1169 }
1170
1171 #[test]
1172 fn prompt_is_intermediate_complete() {
1173 assert!(matches!(
1174 at_intermediate_line_complete(b"AT+CMGS=1\r\r\n>\r\n", AtResultFormat::Verbose),
1175 Some(ExchangeMatch::Intermediate { line }) if line == ">"
1176 ));
1177 }
1178
1179 #[test]
1180 fn connect_is_not_final() {
1181 assert!(
1182 at_final_line_complete(b"ATD123\r\r\nCONNECT\r\n", AtResultFormat::Verbose).is_none()
1183 );
1184 assert!(classify_intermediate_line("CONNECT 9600"));
1185 }
1186
1187 #[test]
1188 fn no_carrier_is_final() {
1189 assert!(matches!(
1190 at_final_line_complete(b"ATD\r\r\nNO CARRIER\r\n", AtResultFormat::Verbose),
1191 Some(ExchangeMatch::NoCarrier)
1192 ));
1193 }
1194
1195 #[test]
1196 fn derive_prefix_csq() {
1197 assert_eq!(
1198 derive_solicited_prefixes("AT+CSQ"),
1199 vec!["+CSQ:".to_string()]
1200 );
1201 }
1202
1203 #[test]
1204 fn derive_prefix_syscfg() {
1205 assert_eq!(
1206 derive_solicited_prefixes("AT^SYSCFG?"),
1207 vec!["^SYSCFG:".to_string()]
1208 );
1209 }
1210
1211 #[test]
1212 fn csq_is_solicited_not_urc() {
1213 let raw = b"AT+CSQ\r\r\n+CSQ: 20,99\r\nOK\r\n";
1214 let parsed = parse_at_response(raw, "AT+CSQ", &[], AtResultFormat::Verbose);
1215 assert_eq!(parsed.status, AtParseStatus::Ok);
1216 assert!(parsed.solicited_body.iter().any(|l| l.starts_with("+CSQ:")));
1217 assert!(parsed.urc_lines.is_empty());
1218 }
1219
1220 #[test]
1221 fn syscfg_solicited_with_derived_prefix() {
1222 let raw = b"AT^SYSCFG?\r\r\n^SYSCFG: 2,2,3FFFFFFF,1,1\r\nOK\r\n";
1223 let parsed = parse_at_response(raw, "AT^SYSCFG?", &[], AtResultFormat::Verbose);
1224 assert!(parsed
1225 .solicited_body
1226 .iter()
1227 .any(|l| l.starts_with("^SYSCFG:")));
1228 assert!(parsed.urc_lines.is_empty());
1229 }
1230
1231 #[test]
1232 fn creg_in_blob_classified_as_urc_before_echo() {
1233 let raw = b"+CREG: 1,2\r\nAT\r\r\nOK\r\n";
1234 let parsed = parse_at_response(raw, "AT", &[], AtResultFormat::Verbose);
1235 assert!(parsed.urc_lines.iter().any(|l| l.starts_with("+CREG:")));
1236 }
1237
1238 #[test]
1239 fn cme_error_text_status() {
1240 let raw = b"AT+FOO\r\r\n+CME ERROR: SIM not inserted\r\n";
1241 let parsed = parse_at_response(raw, "AT+FOO", &[], AtResultFormat::Verbose);
1242 assert_eq!(parsed.status, AtParseStatus::Cme);
1243 }
1244
1245 #[test]
1246 fn demux_emits_urc_before_echo() {
1247 let mut demux = ExchangeDemux::new("AT", &[]);
1248 let urc = demux.process_chunk(b"+CREG: 1\r\nAT\r\n");
1249 assert!(urc.iter().any(|l| l.starts_with("+CREG")));
1250 }
1251}