1use crate::args::{parse_values, split, strip_prefix, Arg};
21use serde::Serialize;
22
23fn body(line: &str, prefix: &str) -> Option<Vec<Arg>> {
24 strip_prefix(line, prefix).map(split)
25}
26
27fn int<T: std::str::FromStr>(a: &[Arg], i: usize) -> Option<T> {
28 a.get(i)?.int()
29}
30
31fn text(a: &[Arg], i: usize) -> Option<String> {
32 a.get(i)?.text().map(str::to_string)
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub enum RegKind {
42 Creg,
44 Cgreg,
46 Cereg,
48 C5greg,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub enum RegStatus {
56 NotRegistered,
58 Home,
60 Searching,
62 Denied,
64 Unknown,
66 Roaming,
68 HomeSmsOnly,
70 RoamingSmsOnly,
72 EmergencyOnly,
74 HomeCsfbNotPreferred,
76 RoamingCsfbNotPreferred,
78 AttachedRlos,
80 Other(u8),
82}
83
84impl RegStatus {
85 pub fn from_code(v: u8) -> Self {
86 use RegStatus::*;
87 match v {
88 0 => NotRegistered,
89 1 => Home,
90 2 => Searching,
91 3 => Denied,
92 4 => Unknown,
93 5 => Roaming,
94 6 => HomeSmsOnly,
95 7 => RoamingSmsOnly,
96 8 => EmergencyOnly,
97 9 => HomeCsfbNotPreferred,
98 10 => RoamingCsfbNotPreferred,
99 11 => AttachedRlos,
100 v => Other(v),
101 }
102 }
103
104 pub fn is_registered(self) -> bool {
106 use RegStatus::*;
107 matches!(
108 self,
109 Home | Roaming
110 | HomeSmsOnly
111 | RoamingSmsOnly
112 | HomeCsfbNotPreferred
113 | RoamingCsfbNotPreferred
114 )
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
120#[serde(rename_all = "camelCase")]
121pub enum AccessTech {
122 Gsm,
124 GsmCompact,
126 Utran,
128 GsmEgprs,
130 UtranHsdpa,
132 UtranHsupa,
134 UtranHsdpaHsupa,
136 Eutran,
138 EcGsmIot,
140 EutranNbS1,
142 Eutra5gcn,
144 Nr5gcn,
146 NgRan,
148 EutraNrDual,
150 Other(u8),
152}
153
154impl AccessTech {
155 pub fn from_code(v: u8) -> Self {
156 use AccessTech::*;
157 match v {
158 0 => Gsm,
159 1 => GsmCompact,
160 2 => Utran,
161 3 => GsmEgprs,
162 4 => UtranHsdpa,
163 5 => UtranHsupa,
164 6 => UtranHsdpaHsupa,
165 7 => Eutran,
166 8 => EcGsmIot,
167 9 => EutranNbS1,
168 10 => Eutra5gcn,
169 11 => Nr5gcn,
170 12 => NgRan,
171 13 => EutraNrDual,
172 v => Other(v),
173 }
174 }
175
176 fn from_arg(a: &[Arg], i: usize) -> Option<Self> {
177 int::<u8>(a, i).map(Self::from_code)
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
184#[serde(rename_all = "camelCase")]
185pub struct Registration {
186 pub kind: RegKind,
187 pub n: Option<u8>,
189 pub stat: RegStatus,
190 pub lac: Option<u32>,
192 pub ci: Option<u64>,
194 pub act: Option<AccessTech>,
195 pub rac: Option<u8>,
197 pub allowed_nssai: Option<String>,
199 pub cause_type: Option<u8>,
200 pub reject_cause: Option<u32>,
201}
202
203impl Registration {
204 pub fn parse(line: &str) -> Option<Self> {
206 let (kind, a) = [
207 ("+CREG", RegKind::Creg),
208 ("+CGREG", RegKind::Cgreg),
209 ("+CEREG", RegKind::Cereg),
210 ("+C5GREG", RegKind::C5greg),
211 ]
212 .into_iter()
213 .find_map(|(p, k)| body(line, p).map(|a| (k, a)))?;
214
215 let response = a.len() != 3
220 && a.get(1)
221 .is_some_and(|f| !f.quoted && f.text.len() <= 2 && f.int::<u8>().is_some());
222 let (n, a) = if response {
223 (Some(int::<u8>(&a, 0)?), &a[1..])
224 } else {
225 (None, &a[..])
226 };
227 let stat = RegStatus::from_code(int(a, 0)?);
228 let hex = |i: usize| a.get(i).and_then(Arg::hex);
229 let mut r = Registration {
230 kind,
231 n,
232 stat,
233 lac: hex(1).and_then(|v| u32::try_from(v).ok()),
234 ci: hex(2),
235 act: AccessTech::from_arg(a, 3),
236 rac: None,
237 allowed_nssai: None,
238 cause_type: None,
239 reject_cause: None,
240 };
241 let cause_at = match kind {
242 RegKind::Creg | RegKind::Cereg => 4,
243 RegKind::Cgreg => {
244 r.rac = hex(4).and_then(|v| u8::try_from(v).ok());
245 5
246 }
247 RegKind::C5greg => {
248 r.allowed_nssai = text(a, 5);
249 6
250 }
251 };
252 r.cause_type = int(a, cause_at);
253 r.reject_cause = int(a, cause_at + 1);
254 Some(r)
255 }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
263#[serde(rename_all = "camelCase")]
264pub struct Csq {
265 pub rssi: u8,
266 pub ber: u8,
267}
268
269impl Csq {
270 pub fn parse(line: &str) -> Option<Self> {
271 let a = body(line, "+CSQ")?;
272 Some(Csq {
273 rssi: int(&a, 0)?,
274 ber: int(&a, 1)?,
275 })
276 }
277
278 pub fn rssi_dbm(&self) -> Option<i32> {
280 (self.rssi <= 31).then(|| -113 + 2 * self.rssi as i32)
281 }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
287#[serde(rename_all = "camelCase")]
288pub struct Cesq {
289 pub rxlev: u8,
290 pub ber: u8,
291 pub rscp: u8,
292 pub ecno: u8,
293 pub rsrq: u8,
294 pub rsrp: u8,
295 pub ss_rsrq: Option<u8>,
296 pub ss_rsrp: Option<u8>,
297 pub ss_sinr: Option<u8>,
298}
299
300impl Cesq {
301 pub fn parse(line: &str) -> Option<Self> {
302 let a = body(line, "+CESQ")?;
303 Some(Cesq {
304 rxlev: int(&a, 0)?,
305 ber: int(&a, 1)?,
306 rscp: int(&a, 2)?,
307 ecno: int(&a, 3)?,
308 rsrq: int(&a, 4)?,
309 rsrp: int(&a, 5)?,
310 ss_rsrq: int(&a, 6),
311 ss_rsrp: int(&a, 7),
312 ss_sinr: int(&a, 8),
313 })
314 }
315
316 pub fn rxlev_dbm(&self) -> Option<f64> {
318 (self.rxlev <= 63).then(|| -111.0 + self.rxlev as f64)
319 }
320 pub fn rscp_dbm(&self) -> Option<f64> {
322 (self.rscp <= 96).then(|| -121.0 + self.rscp as f64)
323 }
324 pub fn ecno_db(&self) -> Option<f64> {
326 (self.ecno <= 49).then(|| -24.5 + self.ecno as f64 * 0.5)
327 }
328 pub fn rsrq_db(&self) -> Option<f64> {
330 (self.rsrq <= 34).then(|| -20.0 + self.rsrq as f64 * 0.5)
331 }
332 pub fn rsrp_dbm(&self) -> Option<f64> {
334 (self.rsrp <= 97).then(|| -141.0 + self.rsrp as f64)
335 }
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
343#[serde(rename_all = "camelCase")]
344pub enum Cpin {
345 Ready,
346 SimPin,
347 SimPuk,
348 PhSimPin,
349 PhFsimPin,
350 PhFsimPuk,
351 SimPin2,
352 SimPuk2,
353 PhNetPin,
354 PhNetPuk,
355 PhNetsubPin,
356 PhNetsubPuk,
357 PhSpPin,
358 PhSpPuk,
359 PhCorpPin,
360 PhCorpPuk,
361 Other(String),
363}
364
365impl Cpin {
366 pub fn parse(line: &str) -> Option<Self> {
367 let a = body(line, "+CPIN")?;
368 let code = a.first()?.text()?.to_string();
369 use Cpin::*;
370 Some(match code.to_ascii_uppercase().as_str() {
371 "READY" => Ready,
372 "SIM PIN" => SimPin,
373 "SIM PUK" => SimPuk,
374 "PH-SIM PIN" => PhSimPin,
375 "PH-FSIM PIN" => PhFsimPin,
376 "PH-FSIM PUK" => PhFsimPuk,
377 "SIM PIN2" => SimPin2,
378 "SIM PUK2" => SimPuk2,
379 "PH-NET PIN" => PhNetPin,
380 "PH-NET PUK" => PhNetPuk,
381 "PH-NETSUB PIN" => PhNetsubPin,
382 "PH-NETSUB PUK" => PhNetsubPuk,
383 "PH-SP PIN" => PhSpPin,
384 "PH-SP PUK" => PhSpPuk,
385 "PH-CORP PIN" => PhCorpPin,
386 "PH-CORP PUK" => PhCorpPuk,
387 _ => Other(code),
388 })
389 }
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
398#[serde(rename_all = "camelCase")]
399pub struct CallerId {
400 pub number: String,
401 #[serde(rename = "type")]
403 pub number_type: Option<u8>,
404 pub subaddr: Option<String>,
405 pub satype: Option<u8>,
406 pub alpha: Option<String>,
407 pub cli_validity: Option<u8>,
409}
410
411impl CallerId {
412 fn parse_with(line: &str, prefix: &str) -> Option<Self> {
413 let a = body(line, prefix)?;
414 if !a.first()?.quoted {
416 return None;
417 }
418 Some(CallerId {
419 number: a.first()?.text.clone(),
420 number_type: int(&a, 1),
421 subaddr: text(&a, 2),
422 satype: int(&a, 3),
423 alpha: text(&a, 4),
424 cli_validity: int(&a, 5),
425 })
426 }
427
428 pub fn parse_clip(line: &str) -> Option<Self> {
429 Self::parse_with(line, "+CLIP")
430 }
431
432 pub fn parse_colp(line: &str) -> Option<Self> {
433 Self::parse_with(line, "+COLP").map(|c| CallerId {
434 cli_validity: None,
435 ..c
436 })
437 }
438}
439
440#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
443#[serde(rename_all = "camelCase")]
444pub struct CallWaiting {
445 pub number: String,
446 #[serde(rename = "type")]
447 pub number_type: Option<u8>,
448 pub class: Option<u8>,
449 pub alpha: Option<String>,
450 pub cli_validity: Option<u8>,
451 pub subaddr: Option<String>,
452 pub satype: Option<u8>,
453 pub priority: Option<u8>,
454}
455
456impl CallWaiting {
457 pub fn parse(line: &str) -> Option<Self> {
458 let a = body(line, "+CCWA")?;
459 if !a.first()?.quoted && a.len() < 3 {
461 return None;
462 }
463 Some(CallWaiting {
464 number: a[0].text.clone(),
465 number_type: int(&a, 1),
466 class: int(&a, 2),
467 alpha: text(&a, 3),
468 cli_validity: int(&a, 4),
469 subaddr: text(&a, 5),
470 satype: int(&a, 6),
471 priority: int(&a, 7),
472 })
473 }
474}
475
476#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
478#[serde(rename_all = "camelCase")]
479pub enum CallDirection {
480 MobileOriginated,
481 MobileTerminated,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
486#[serde(rename_all = "camelCase")]
487pub enum CallState {
488 Active,
489 Held,
490 Dialing,
491 Alerting,
492 Incoming,
493 Waiting,
494 Other(u8),
495}
496
497#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
499#[serde(rename_all = "camelCase")]
500pub struct Clcc {
501 pub id: u32,
502 pub dir: CallDirection,
503 pub stat: CallState,
504 pub mode: u8,
506 pub mpty: bool,
507 pub number: Option<String>,
508 #[serde(rename = "type")]
509 pub number_type: Option<u8>,
510 pub alpha: Option<String>,
511 pub priority: Option<u8>,
512 pub cli_validity: Option<u8>,
513}
514
515impl Clcc {
516 pub fn parse(line: &str) -> Option<Self> {
517 let a = body(line, "+CLCC")?;
518 let dir = match int::<u8>(&a, 1)? {
519 0 => CallDirection::MobileOriginated,
520 1 => CallDirection::MobileTerminated,
521 _ => return None,
522 };
523 let stat = match int::<u8>(&a, 2)? {
524 0 => CallState::Active,
525 1 => CallState::Held,
526 2 => CallState::Dialing,
527 3 => CallState::Alerting,
528 4 => CallState::Incoming,
529 5 => CallState::Waiting,
530 v => CallState::Other(v),
531 };
532 let number = a.get(5).map(|n| n.text.clone());
533 let number_type = int(&a, 6);
534 if number.is_some() && number_type.is_none() {
536 return None;
537 }
538 Some(Clcc {
539 id: int(&a, 0)?,
540 dir,
541 stat,
542 mode: int(&a, 3)?,
543 mpty: int::<u8>(&a, 4)? == 1,
544 number,
545 number_type,
546 alpha: text(&a, 7),
547 priority: int(&a, 8),
548 cli_validity: int(&a, 9),
549 })
550 }
551}
552
553#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
558#[serde(rename_all = "camelCase")]
559pub struct Ciev {
560 pub index: Option<u32>,
561 pub name: Option<String>,
562 pub value: i64,
563}
564
565impl Ciev {
566 pub fn parse(line: &str) -> Option<Self> {
567 let a = body(line, "+CIEV")?;
568 let ind = a.first()?;
569 let value = a.get(1)?.text.parse().ok()?;
570 let index = ind.int();
571 Some(Ciev {
572 index,
573 name: if index.is_none() {
574 ind.text().map(str::to_string)
575 } else {
576 None
577 },
578 value,
579 })
580 }
581}
582
583#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
585#[serde(rename_all = "camelCase")]
586pub struct CindValues {
587 pub values: Vec<i64>,
588}
589
590impl CindValues {
591 pub fn parse(line: &str) -> Option<Self> {
592 let a = body(line, "+CIND")?;
593 let values = a
594 .iter()
595 .map(|v| v.text.parse().ok())
596 .collect::<Option<Vec<_>>>()?;
597 (!values.is_empty()).then_some(CindValues { values })
598 }
599}
600
601#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
603#[serde(rename_all = "camelCase")]
604pub struct IndicatorRange {
605 pub index: u32,
606 pub name: String,
607 pub values: Vec<u32>,
608}
609
610#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
612#[serde(rename_all = "camelCase")]
613pub struct CindRanges {
614 pub indicators: Vec<IndicatorRange>,
615}
616
617impl CindRanges {
618 pub fn parse(line: &str) -> Option<Self> {
619 let a = body(line, "+CIND")?;
620 let mut indicators = Vec::new();
621 for (i, g) in a.iter().enumerate() {
622 let g = g.group()?;
623 if g.is_empty() {
624 continue; }
626 indicators.push(IndicatorRange {
627 index: i as u32 + 1,
628 name: g.first()?.str()?.to_string(),
629 values: parse_values(&g.get(1)?.text)?,
630 });
631 }
632 (!indicators.is_empty()).then_some(CindRanges { indicators })
633 }
634}
635
636#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
641#[serde(rename_all = "camelCase")]
642pub struct MessageIndication {
643 pub mem: String,
644 pub index: u32,
645}
646
647impl MessageIndication {
648 fn parse_with(line: &str, prefix: &str) -> Option<Self> {
649 let a = body(line, prefix)?;
650 Some(MessageIndication {
651 mem: a.first()?.text()?.to_string(),
652 index: int(&a, 1)?,
653 })
654 }
655 pub fn parse_cmti(line: &str) -> Option<Self> {
656 Self::parse_with(line, "+CMTI")
657 }
658 pub fn parse_cdsi(line: &str) -> Option<Self> {
659 Self::parse_with(line, "+CDSI")
660 }
661}
662
663#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
665#[serde(rename_all = "camelCase")]
666pub enum MessageStatus {
667 ReceivedUnread,
669 ReceivedRead,
671 StoredUnsent,
673 StoredSent,
675 All,
677}
678
679impl MessageStatus {
680 fn from_arg(a: &Arg) -> Option<Self> {
681 use MessageStatus::*;
682 if let Some(v) = a.int::<u8>() {
683 return [ReceivedUnread, ReceivedRead, StoredUnsent, StoredSent, All]
684 .get(v as usize)
685 .copied();
686 }
687 match a.text.to_ascii_uppercase().as_str() {
688 "REC UNREAD" => Some(ReceivedUnread),
689 "REC READ" => Some(ReceivedRead),
690 "STO UNSENT" => Some(StoredUnsent),
691 "STO SENT" => Some(StoredSent),
692 "ALL" => Some(All),
693 _ => None,
694 }
695 }
696}
697
698#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
700#[serde(rename_all = "camelCase")]
701pub enum SmsMode {
702 Pdu,
703 Text,
704}
705
706#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
713#[serde(rename_all = "camelCase")]
714pub struct MessageHeader {
715 pub mode: SmsMode,
716 pub index: Option<u32>,
718 pub stat: MessageStatus,
719 pub alpha: Option<String>,
720 pub length: Option<u32>,
722 pub address: Option<String>,
724 pub address_type: Option<u8>,
726 pub scts: Option<String>,
728}
729
730impl MessageHeader {
731 pub fn parse_cmgl(line: &str) -> Option<Self> {
732 let a = body(line, "+CMGL")?;
733 let index = int(&a, 0)?;
734 let mut h = Self::parse_rest(&a[1..], true)?;
735 h.index = Some(index);
736 Some(h)
737 }
738
739 pub fn parse_cmgr(line: &str) -> Option<Self> {
740 let a = body(line, "+CMGR")?;
741 Self::parse_rest(&a, false)
742 }
743
744 fn parse_rest(a: &[Arg], list: bool) -> Option<Self> {
745 let st = a.first()?;
746 let stat = MessageStatus::from_arg(st)?;
747 if !st.quoted {
748 return Some(MessageHeader {
750 mode: SmsMode::Pdu,
751 index: None,
752 stat,
753 alpha: text(a, 1),
754 length: Some(int(a, 2)?),
755 address: None,
756 address_type: None,
757 scts: None,
758 });
759 }
760 if a.get(1).is_some_and(|x| !x.quoted && !x.text.is_empty()) {
763 return None;
764 }
765 let submit = !list
769 && matches!(
770 stat,
771 MessageStatus::StoredUnsent | MessageStatus::StoredSent
772 );
773 let (scts, type_at, len_at) = match (list, submit) {
774 (true, _) => (text(a, 3), 4, 5),
775 (false, true) => (None, 3, 10),
776 (false, false) => (text(a, 3), 4, 10),
777 };
778 Some(MessageHeader {
779 mode: SmsMode::Text,
780 index: None,
781 stat,
782 address: text(a, 1),
783 alpha: text(a, 2),
784 scts,
785 address_type: int(a, type_at),
786 length: int(a, len_at),
787 })
788 }
789}
790
791#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
794#[serde(rename_all = "camelCase")]
795pub struct Cmt {
796 pub mode: SmsMode,
797 pub alpha: Option<String>,
798 pub length: Option<u32>,
799 pub address: Option<String>,
800 pub address_type: Option<u8>,
801 pub scts: Option<String>,
802}
803
804impl Cmt {
805 pub fn parse(line: &str) -> Option<Self> {
806 let a = body(line, "+CMT")?;
807 if a.len() == 2 && !a[1].quoted {
808 return Some(Cmt {
809 mode: SmsMode::Pdu,
810 alpha: text(&a, 0),
811 length: Some(int(&a, 1)?),
812 address: None,
813 address_type: None,
814 scts: None,
815 });
816 }
817 if a.len() < 3 {
818 return None;
819 }
820 Some(Cmt {
821 mode: SmsMode::Text,
822 address: text(&a, 0),
823 alpha: text(&a, 1),
824 scts: text(&a, 2),
825 address_type: int(&a, 3),
826 length: int(&a, 9),
827 })
828 }
829}
830
831#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
833#[serde(rename_all = "camelCase")]
834pub struct Cds {
835 pub mode: SmsMode,
836 pub length: Option<u32>,
837 pub fo: Option<u8>,
838 pub mr: Option<u8>,
839 pub ra: Option<String>,
840 pub tora: Option<u8>,
841 pub scts: Option<String>,
842 pub dt: Option<String>,
843 pub st: Option<u8>,
844}
845
846impl Cds {
847 pub fn parse(line: &str) -> Option<Self> {
848 let a = body(line, "+CDS")?;
849 if a.len() == 1 {
850 return Some(Cds {
851 mode: SmsMode::Pdu,
852 length: Some(int(&a, 0)?),
853 fo: None,
854 mr: None,
855 ra: None,
856 tora: None,
857 scts: None,
858 dt: None,
859 st: None,
860 });
861 }
862 if a.len() < 7 {
863 return None;
864 }
865 Some(Cds {
866 mode: SmsMode::Text,
867 length: None,
868 fo: Some(int(&a, 0)?),
869 mr: Some(int(&a, 1)?),
870 ra: text(&a, 2),
871 tora: int(&a, 3),
872 scts: text(&a, 4),
873 dt: text(&a, 5),
874 st: Some(int(&a, 6)?),
875 })
876 }
877}
878
879#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
881#[serde(rename_all = "camelCase")]
882pub struct MessageReference {
883 pub mr: u8,
884 pub ack: Option<String>,
886}
887
888impl MessageReference {
889 fn parse_with(line: &str, prefix: &str) -> Option<Self> {
890 let a = body(line, prefix)?;
891 Some(MessageReference {
892 mr: int(&a, 0)?,
893 ack: text(&a, 1),
894 })
895 }
896 pub fn parse_cmgs(line: &str) -> Option<Self> {
897 Self::parse_with(line, "+CMGS")
898 }
899 pub fn parse_cmss(line: &str) -> Option<Self> {
900 Self::parse_with(line, "+CMSS")
901 }
902}
903
904#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
906#[serde(rename_all = "camelCase")]
907pub struct StorageUsage {
908 pub mem: Option<String>,
910 pub used: u32,
911 pub total: u32,
912}
913
914#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
916#[serde(rename_all = "camelCase")]
917pub struct CpmsStatus {
918 pub storages: Vec<StorageUsage>,
919}
920
921impl CpmsStatus {
922 pub fn parse(line: &str) -> Option<Self> {
923 let a = body(line, "+CPMS")?;
924 let with_mem = a.first()?.int::<u32>().is_none();
925 let step = if with_mem { 3 } else { 2 };
926 if a.is_empty() || a.len() % step != 0 {
927 return None;
928 }
929 let storages = a
930 .chunks(step)
931 .map(|c| {
932 let o = step - 2;
933 Some(StorageUsage {
934 mem: if with_mem {
935 Some(c[0].text()?.to_string())
936 } else {
937 None
938 },
939 used: c[o].int()?,
940 total: c[o + 1].int()?,
941 })
942 })
943 .collect::<Option<_>>()?;
944 Some(CpmsStatus { storages })
945 }
946}
947
948#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
950#[serde(rename_all = "camelCase")]
951pub struct CpmsSupport {
952 pub mem1: Vec<String>,
953 pub mem2: Vec<String>,
954 pub mem3: Vec<String>,
955}
956
957impl CpmsSupport {
958 pub fn parse(line: &str) -> Option<Self> {
959 let a = body(line, "+CPMS")?;
960 let list = |i: usize| -> Vec<String> {
961 let Some(g) = a.get(i) else {
962 return Vec::new();
963 };
964 let items = g.group().unwrap_or_else(|| vec![g.clone()]);
966 items
967 .iter()
968 .filter_map(|m| m.text().map(str::to_string))
969 .collect()
970 };
971 if !a.first()?.text.starts_with('(') {
972 return None;
973 }
974 Some(CpmsSupport {
975 mem1: list(0),
976 mem2: list(1),
977 mem3: list(2),
978 })
979 }
980}
981
982#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
984#[serde(rename_all = "camelCase")]
985pub struct Cnmi {
986 pub mode: u8,
987 pub mt: Option<u8>,
988 pub bm: Option<u8>,
989 pub ds: Option<u8>,
990 pub bfr: Option<u8>,
991}
992
993impl Cnmi {
994 pub fn parse(line: &str) -> Option<Self> {
995 let a = body(line, "+CNMI")?;
996 Some(Cnmi {
997 mode: int(&a, 0)?,
998 mt: int(&a, 1),
999 bm: int(&a, 2),
1000 ds: int(&a, 3),
1001 bfr: int(&a, 4),
1002 })
1003 }
1004}
1005
1006#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1008#[serde(rename_all = "camelCase")]
1009pub struct CnmiSupport {
1010 pub mode: Vec<u32>,
1011 pub mt: Vec<u32>,
1012 pub bm: Vec<u32>,
1013 pub ds: Vec<u32>,
1014 pub bfr: Vec<u32>,
1015}
1016
1017impl CnmiSupport {
1018 pub fn parse(line: &str) -> Option<Self> {
1019 let a = body(line, "+CNMI")?;
1020 let get = |i: usize| match a.get(i) {
1021 Some(g) if g.text.starts_with('(') => parse_values(&g.text),
1022 Some(_) => None,
1023 None => Some(Vec::new()),
1024 };
1025 Some(CnmiSupport {
1026 mode: get(0)?,
1027 mt: get(1)?,
1028 bm: get(2)?,
1029 ds: get(3)?,
1030 bfr: get(4)?,
1031 })
1032 }
1033}
1034
1035#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1040#[serde(rename_all = "camelCase")]
1041pub struct Cops {
1042 pub mode: u8,
1044 pub format: Option<u8>,
1046 pub oper: Option<String>,
1047 pub act: Option<AccessTech>,
1048}
1049
1050impl Cops {
1051 pub fn parse(line: &str) -> Option<Self> {
1052 let a = body(line, "+COPS")?;
1053 if a.first()?.text.starts_with('(') {
1054 return None; }
1056 Some(Cops {
1057 mode: int(&a, 0)?,
1058 format: int(&a, 1),
1059 oper: text(&a, 2),
1060 act: AccessTech::from_arg(&a, 3),
1061 })
1062 }
1063}
1064
1065#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1067#[serde(rename_all = "camelCase")]
1068pub enum OperatorStatus {
1069 Unknown,
1070 Available,
1071 Current,
1072 Forbidden,
1073 Other(u8),
1074}
1075
1076#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1078#[serde(rename_all = "camelCase")]
1079pub struct OperatorEntry {
1080 pub stat: OperatorStatus,
1081 pub long: Option<String>,
1082 pub short: Option<String>,
1083 pub numeric: Option<String>,
1084 pub act: Option<AccessTech>,
1085}
1086
1087#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1090#[serde(rename_all = "camelCase")]
1091pub struct CopsList {
1092 pub operators: Vec<OperatorEntry>,
1093}
1094
1095impl CopsList {
1096 pub fn parse(line: &str) -> Option<Self> {
1097 let a = body(line, "+COPS")?;
1098 if !a.first()?.text.starts_with('(') {
1099 return None;
1100 }
1101 let mut operators: Vec<OperatorEntry> = Vec::new();
1102 for f in &a {
1103 let Some(g) = f.group() else {
1105 continue;
1106 };
1107 if g.len() < 4 || !g[1..4].iter().any(|x| x.quoted) {
1109 continue;
1110 }
1111 let stat = match g[0].int::<u8>() {
1112 Some(0) => OperatorStatus::Unknown,
1113 Some(1) => OperatorStatus::Available,
1114 Some(2) => OperatorStatus::Current,
1115 Some(3) => OperatorStatus::Forbidden,
1116 Some(v) => OperatorStatus::Other(v),
1117 None => continue,
1118 };
1119 let act = AccessTech::from_arg(&g, 4);
1120 operators.push(OperatorEntry {
1121 stat,
1122 long: text(&g, 1).filter(|s| !s.is_empty()),
1123 short: text(&g, 2).filter(|s| !s.is_empty()),
1124 numeric: text(&g, 3).filter(|s| !s.is_empty()),
1125 act,
1126 });
1127 }
1128 Some(CopsList { operators })
1129 }
1130}
1131
1132#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1138#[serde(
1139 tag = "event",
1140 rename_all = "camelCase",
1141 rename_all_fields = "camelCase"
1142)]
1143pub enum CgevEvent {
1144 Reject { pdp_type: String, pdp_addr: String },
1146 NwReact {
1148 pdp_type: String,
1149 pdp_addr: String,
1150 cid: Option<u32>,
1151 },
1152 NwDeactPdp {
1154 pdp_type: String,
1155 pdp_addr: String,
1156 cid: Option<u32>,
1157 },
1158 MeDeactPdp {
1160 pdp_type: String,
1161 pdp_addr: String,
1162 cid: Option<u32>,
1163 },
1164 NwPdnAct { cid: u32 },
1166 MePdnAct {
1168 cid: u32,
1169 reason: Option<u32>,
1170 cid_other: Option<u32>,
1171 },
1172 NwPdnDeact { cid: u32 },
1174 MePdnDeact { cid: u32 },
1176 NwAct {
1178 p_cid: u32,
1179 cid: u32,
1180 event_type: u32,
1181 },
1182 MeAct {
1184 p_cid: u32,
1185 cid: u32,
1186 event_type: u32,
1187 },
1188 NwDeact {
1190 p_cid: u32,
1191 cid: u32,
1192 event_type: u32,
1193 },
1194 MeDeact {
1196 p_cid: u32,
1197 cid: u32,
1198 event_type: u32,
1199 },
1200 NwModify {
1202 cid: u32,
1203 change_reason: u32,
1204 event_type: u32,
1205 },
1206 MeModify {
1208 cid: u32,
1209 change_reason: u32,
1210 event_type: u32,
1211 },
1212 NwDetach,
1214 MeDetach,
1216 NwClass { class: String },
1218 MeClass { class: String },
1220 Other,
1222}
1223
1224#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1226#[serde(rename_all = "camelCase")]
1227pub struct Cgev {
1228 pub event: CgevEvent,
1229 pub raw: String,
1230}
1231
1232impl Cgev {
1233 pub fn parse(line: &str) -> Option<Self> {
1234 Some(Self::parse_event(strip_prefix(line, "+CGEV")?))
1235 }
1236
1237 pub fn parse_event(text: &str) -> Self {
1239 let raw = text.trim().to_string();
1240 Cgev {
1241 event: Self::decode(&raw).unwrap_or(CgevEvent::Other),
1242 raw,
1243 }
1244 }
1245
1246 fn decode(raw: &str) -> Option<CgevEvent> {
1247 use CgevEvent::*;
1248 let upper = raw.to_ascii_uppercase();
1249 const KEYS: [&str; 10] = [
1251 "NW", "ME", "PDN", "ACT", "DEACT", "REACT", "MODIFY", "DETACH", "CLASS", "REJECT",
1252 ];
1253 let mut cut = 0;
1254 let mut words = Vec::new();
1255 for w in upper.split_whitespace() {
1256 if !KEYS.contains(&w) {
1257 break;
1258 }
1259 words.push(w);
1260 cut = upper[cut..].find(w).map(|p| cut + p + w.len())?;
1261 }
1262 let a = split(&raw[cut..]);
1263 let n = |i: usize| int::<u32>(&a, i);
1264 let s = |i: usize| text(&a, i);
1265 let addr = || Some((s(0)?, s(1)?));
1266 let addr_form = a
1268 .first()
1269 .is_some_and(|f| !f.is_empty() && f.int::<u32>().is_none());
1270 Some(match words.as_slice() {
1271 ["REJECT"] => {
1272 let (pdp_type, pdp_addr) = addr()?;
1273 Reject { pdp_type, pdp_addr }
1274 }
1275 ["NW", "REACT"] => {
1276 let (pdp_type, pdp_addr) = addr()?;
1277 NwReact {
1278 pdp_type,
1279 pdp_addr,
1280 cid: n(2),
1281 }
1282 }
1283 ["NW", "DEACT"] if addr_form => {
1284 let (pdp_type, pdp_addr) = addr()?;
1285 NwDeactPdp {
1286 pdp_type,
1287 pdp_addr,
1288 cid: n(2),
1289 }
1290 }
1291 ["ME", "DEACT"] if addr_form => {
1292 let (pdp_type, pdp_addr) = addr()?;
1293 MeDeactPdp {
1294 pdp_type,
1295 pdp_addr,
1296 cid: n(2),
1297 }
1298 }
1299 ["NW", "PDN", "ACT"] => NwPdnAct { cid: n(0)? },
1300 ["ME", "PDN", "ACT"] => MePdnAct {
1301 cid: n(0)?,
1302 reason: n(1),
1303 cid_other: n(2),
1304 },
1305 ["NW", "PDN", "DEACT"] => NwPdnDeact { cid: n(0)? },
1306 ["ME", "PDN", "DEACT"] => MePdnDeact { cid: n(0)? },
1307 ["NW", "ACT"] => NwAct {
1308 p_cid: n(0)?,
1309 cid: n(1)?,
1310 event_type: n(2)?,
1311 },
1312 ["ME", "ACT"] => MeAct {
1313 p_cid: n(0)?,
1314 cid: n(1)?,
1315 event_type: n(2)?,
1316 },
1317 ["NW", "DEACT"] => NwDeact {
1318 p_cid: n(0)?,
1319 cid: n(1)?,
1320 event_type: n(2)?,
1321 },
1322 ["ME", "DEACT"] => MeDeact {
1323 p_cid: n(0)?,
1324 cid: n(1)?,
1325 event_type: n(2)?,
1326 },
1327 ["NW", "MODIFY"] => NwModify {
1328 cid: n(0)?,
1329 change_reason: n(1)?,
1330 event_type: n(2)?,
1331 },
1332 ["ME", "MODIFY"] => MeModify {
1333 cid: n(0)?,
1334 change_reason: n(1)?,
1335 event_type: n(2)?,
1336 },
1337 ["NW", "DETACH"] => NwDetach,
1338 ["ME", "DETACH"] => MeDetach,
1339 ["NW", "CLASS"] => NwClass { class: s(0)? },
1340 ["ME", "CLASS"] => MeClass { class: s(0)? },
1341 _ => return None,
1342 })
1343 }
1344}
1345
1346#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1351#[serde(tag = "urc", content = "data", rename_all = "camelCase")]
1352pub enum Urc {
1353 Registration(Registration),
1355 Clip(CallerId),
1356 Colp(CallerId),
1357 Ccwa(CallWaiting),
1358 Ciev(Ciev),
1359 Cmti(MessageIndication),
1360 Cdsi(MessageIndication),
1361 Cmt(Cmt),
1363 Cds(Cds),
1365 Cgev(Cgev),
1366}
1367
1368pub fn parse_any(line: &str) -> Option<Urc> {
1370 let head = line.trim_start().get(..3)?.to_ascii_uppercase();
1371 match head.as_str() {
1372 "+CR" | "+CG" | "+CE" | "+C5" => Registration::parse(line)
1373 .map(Urc::Registration)
1374 .or_else(|| Cgev::parse(line).map(Urc::Cgev)),
1375 _ => CallerId::parse_clip(line)
1376 .map(Urc::Clip)
1377 .or_else(|| CallerId::parse_colp(line).map(Urc::Colp))
1378 .or_else(|| CallWaiting::parse(line).map(Urc::Ccwa))
1379 .or_else(|| Ciev::parse(line).map(Urc::Ciev))
1380 .or_else(|| MessageIndication::parse_cmti(line).map(Urc::Cmti))
1381 .or_else(|| MessageIndication::parse_cdsi(line).map(Urc::Cdsi))
1382 .or_else(|| Cmt::parse(line).map(Urc::Cmt))
1383 .or_else(|| Cds::parse(line).map(Urc::Cds)),
1384 }
1385}