Skip to main content

serial_codec/
urc.rs

1//! Typed parsers for 3GPP TS 27.007 / 27.005 information lines and unsolicited result codes.
2//!
3//! Every parser takes ONE logical line (framing into lines is done elsewhere, e.g. by
4//! [`crate::at::split_lines`]) and returns `None` when the line is not that command or is
5//! malformed. Prefixes match ASCII case-insensitively; surrounding whitespace / CR LF is ignored.
6//! Multi-line responses (`+CMGL`, `+CMGR`, `+CMT`, `+CDS` in PDU mode) parse the header only:
7//! the PDU or text body is the next line.
8//!
9//! [`parse_any`] dispatches the unsolicited forms.
10//!
11//! ```
12//! use serial_codec::urc::{parse_any, AccessTech, RegStatus, Urc};
13//!
14//! let Some(Urc::Registration(r)) = parse_any("+CEREG: 1,\"1F00\",\"79D903\",7") else { panic!() };
15//! assert_eq!(r.n, None); // unsolicited form
16//! assert_eq!(r.stat, RegStatus::Home);
17//! assert_eq!((r.lac, r.ci, r.act), (Some(0x1F00), Some(0x79D903), Some(AccessTech::Eutran)));
18//! ```
19
20use 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// ---------------------------------------------------------------------------------------------
36// Network registration: +CREG / +CGREG / +CEREG / +C5GREG (27.007 §7.2, §10.1.20-22, §10.1.68)
37
38/// Which registration command a line came from.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub enum RegKind {
42    /// `+CREG`, circuit switched.
43    Creg,
44    /// `+CGREG`, GPRS.
45    Cgreg,
46    /// `+CEREG`, EPS.
47    Cereg,
48    /// `+C5GREG`, 5GS.
49    C5greg,
50}
51
52/// `<stat>` of the registration commands.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub enum RegStatus {
56    /// 0: not registered, not searching.
57    NotRegistered,
58    /// 1: registered, home network.
59    Home,
60    /// 2: not registered, searching.
61    Searching,
62    /// 3: registration denied.
63    Denied,
64    /// 4: unknown (e.g. out of coverage).
65    Unknown,
66    /// 5: registered, roaming.
67    Roaming,
68    /// 6: registered for SMS only, home.
69    HomeSmsOnly,
70    /// 7: registered for SMS only, roaming.
71    RoamingSmsOnly,
72    /// 8: attached for emergency bearer services only.
73    EmergencyOnly,
74    /// 9: registered for CSFB not preferred, home.
75    HomeCsfbNotPreferred,
76    /// 10: registered for CSFB not preferred, roaming.
77    RoamingCsfbNotPreferred,
78    /// 11: attached for access to RLOS.
79    AttachedRlos,
80    /// Any other value.
81    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    /// Registered (home or roaming, any service restriction).
105    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/// `<AcT>` access technology (27.007 §7.3).
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
120#[serde(rename_all = "camelCase")]
121pub enum AccessTech {
122    /// 0 GSM.
123    Gsm,
124    /// 1 GSM Compact.
125    GsmCompact,
126    /// 2 UTRAN.
127    Utran,
128    /// 3 GSM w/EGPRS.
129    GsmEgprs,
130    /// 4 UTRAN w/HSDPA.
131    UtranHsdpa,
132    /// 5 UTRAN w/HSUPA.
133    UtranHsupa,
134    /// 6 UTRAN w/HSDPA and HSUPA.
135    UtranHsdpaHsupa,
136    /// 7 E-UTRAN.
137    Eutran,
138    /// 8 EC-GSM-IoT (A/Gb mode).
139    EcGsmIot,
140    /// 9 E-UTRAN (NB-S1 mode).
141    EutranNbS1,
142    /// 10 E-UTRA connected to a 5GCN.
143    Eutra5gcn,
144    /// 11 NR connected to a 5GCN.
145    Nr5gcn,
146    /// 12 NG-RAN.
147    NgRan,
148    /// 13 E-UTRA-NR dual connectivity.
149    EutraNrDual,
150    /// Any other value.
151    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/// A `+CREG` / `+CGREG` / `+CEREG` / `+C5GREG` line, solicited (`<n>,<stat>,...`) or
182/// unsolicited (`<stat>,...`).
183#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
184#[serde(rename_all = "camelCase")]
185pub struct Registration {
186    pub kind: RegKind,
187    /// `<n>` (URC mode); present only in the read-command response form.
188    pub n: Option<u8>,
189    pub stat: RegStatus,
190    /// `<lac>` (CREG/CGREG) or `<tac>` (CEREG/C5GREG).
191    pub lac: Option<u32>,
192    /// `<ci>` cell id (NR cell ids are 36 bits).
193    pub ci: Option<u64>,
194    pub act: Option<AccessTech>,
195    /// `<rac>` (+CGREG only).
196    pub rac: Option<u8>,
197    /// `<Allowed_NSSAI>` (+C5GREG only).
198    pub allowed_nssai: Option<String>,
199    pub cause_type: Option<u8>,
200    pub reject_cause: Option<u32>,
201}
202
203impl Registration {
204    /// Parses any of the four registration commands.
205    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        // Response form: the second field is an unquoted small number (<stat>). A 3-field line
216        // is always `<stat>,<lac>,<ci>`.
217        // ponytail: an unquoted 1-2 digit LAC (`+CREG: 1,12,3456,0`) reads as the response form;
218        // real modems pad LAC/TAC to 4 hex digits, so this has not been seen.
219        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// ---------------------------------------------------------------------------------------------
259// Signal quality: +CSQ (§8.5), +CESQ (§8.69)
260
261/// `+CSQ: <rssi>,<ber>` raw values; 99 = not known.
262#[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    /// RSSI in dBm: 0 -> -113, 31 -> -51 or better; `None` for 99 / out of range.
279    pub fn rssi_dbm(&self) -> Option<i32> {
280        (self.rssi <= 31).then(|| -113 + 2 * self.rssi as i32)
281    }
282}
283
284/// `+CESQ: <rxlev>,<ber>,<rscp>,<ecno>,<rsrq>,<rsrp>[,<ss_rsrq>,<ss_rsrp>,<ss_sinr>]` raw
285/// values; 99 (rxlev, ber) / 255 (others) = not known.
286#[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    /// GSM RSSI in dBm (lower bound of the 1 dB step): 0 -> -111 or less, 63 -> -48.
317    pub fn rxlev_dbm(&self) -> Option<f64> {
318        (self.rxlev <= 63).then(|| -111.0 + self.rxlev as f64)
319    }
320    /// UTRAN RSCP in dBm: 0 -> -121, 96 -> -25.
321    pub fn rscp_dbm(&self) -> Option<f64> {
322        (self.rscp <= 96).then(|| -121.0 + self.rscp as f64)
323    }
324    /// UTRAN Ec/No in dB: 0 -> -24.5, 49 -> 0.
325    pub fn ecno_db(&self) -> Option<f64> {
326        (self.ecno <= 49).then(|| -24.5 + self.ecno as f64 * 0.5)
327    }
328    /// E-UTRAN RSRQ in dB: 0 -> -20, 34 -> -3.
329    pub fn rsrq_db(&self) -> Option<f64> {
330        (self.rsrq <= 34).then(|| -20.0 + self.rsrq as f64 * 0.5)
331    }
332    /// E-UTRAN RSRP in dBm: 0 -> -141, 97 -> -44.
333    pub fn rsrp_dbm(&self) -> Option<f64> {
334        (self.rsrp <= 97).then(|| -141.0 + self.rsrp as f64)
335    }
336}
337
338// ---------------------------------------------------------------------------------------------
339// +CPIN (§8.3)
340
341/// `+CPIN: <code>`.
342#[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    /// Any other code, verbatim.
362    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// ---------------------------------------------------------------------------------------------
393// Calls: +CLIP (§7.6), +COLP (§7.8), +CCWA (§7.12), +CLCC (§7.18)
394
395/// `+CLIP: <number>,<type>[,<subaddr>,<satype>[,[<alpha>][,<CLI validity>]]]` and
396/// `+COLP: <number>,<type>[,<subaddr>,<satype>[,<alpha>]]`.
397#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
398#[serde(rename_all = "camelCase")]
399pub struct CallerId {
400    pub number: String,
401    /// Type of address octet (129 national/unknown, 145 international).
402    #[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    /// 0 valid, 1 withheld, 2 not available (+CLIP only).
408    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        // 27.007 always quotes <number>; the `AT+CLIP?` read response `<n>,<m>` does not.
415        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/// `+CCWA: <number>,<type>,<class>,[<alpha>][,<CLI validity>[,<subaddr>,<satype>[,<priority>]]]`
441/// (the call waiting URC; the `+CCWA: <status>,<class>` query response is not this).
442#[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        // `+CCWA: 1,1` (query response) has an unquoted number and no type.
460        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/// `<dir>` of +CLCC.
477#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
478#[serde(rename_all = "camelCase")]
479pub enum CallDirection {
480    MobileOriginated,
481    MobileTerminated,
482}
483
484/// `<stat>` of +CLCC.
485#[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/// `+CLCC: <ccid>,<dir>,<stat>,<mode>,<mpty>[,<number>,<type>[,<alpha>[,<priority>[,<CLI validity>]]]]`.
498#[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    /// 0 voice, 1 data, 2 fax, 9 unknown, ...
505    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        // <number> and <type> come as a pair.
535        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// ---------------------------------------------------------------------------------------------
554// Indicators: +CIEV / +CIND (§8.9, §8.10)
555
556/// `+CIEV: <ind>,<value>`. `<ind>` is the 1-based +CIND index; some modems send the name instead.
557#[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/// `+CIND: <ind>,<ind>,...` read-command response: current values in +CIND=? order.
584#[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/// One entry of `+CIND: ("name",(range)),...`; `index` is 1-based.
602#[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/// `+CIND: ("<descr>",(list of supported <ind>s)),...` test-command response.
611#[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; // trailing `()` some phones send
625            }
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// ---------------------------------------------------------------------------------------------
637// SMS (27.005)
638
639/// `+CMTI: <mem>,<index>` / `+CDSI: <mem>,<index>` (§3.4.1).
640#[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/// `<stat>` of stored messages; numeric in PDU mode, a string in text mode.
664#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
665#[serde(rename_all = "camelCase")]
666pub enum MessageStatus {
667    /// 0 / "REC UNREAD"
668    ReceivedUnread,
669    /// 1 / "REC READ"
670    ReceivedRead,
671    /// 2 / "STO UNSENT"
672    StoredUnsent,
673    /// 3 / "STO SENT"
674    StoredSent,
675    /// 4 / "ALL"
676    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/// SMS mode a header was written in.
699#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
700#[serde(rename_all = "camelCase")]
701pub enum SmsMode {
702    Pdu,
703    Text,
704}
705
706/// `+CMGL` (§3.4.2) / `+CMGR` (§3.4.3) header; the body is the next line.
707///
708/// PDU mode: `+CMGL: <index>,<stat>,[<alpha>],<length>`, `+CMGR: <stat>,[<alpha>],<length>`.
709/// Text mode: `+CMGL: <index>,<stat>,<oa/da>,[<alpha>],[<scts>][,<tooa/toda>,<length>]`,
710/// `+CMGR: <stat>,<oa/da>,[<alpha>][,<scts>][,<tooa/toda>,<fo>,<pid>,<dcs>,<sca>,<tosca>,<length>]`.
711/// Text-mode status reports and commands are not decoded field by field.
712#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
713#[serde(rename_all = "camelCase")]
714pub struct MessageHeader {
715    pub mode: SmsMode,
716    /// +CMGL only.
717    pub index: Option<u32>,
718    pub stat: MessageStatus,
719    pub alpha: Option<String>,
720    /// PDU mode: TPDU octets (SMSC address excluded). Text mode: text length, when given.
721    pub length: Option<u32>,
722    /// Text mode `<oa>` / `<da>`.
723    pub address: Option<String>,
724    /// Text mode `<tooa>` / `<toda>`.
725    pub address_type: Option<u8>,
726    /// Text mode `<scts>` verbatim (`yy/MM/dd,hh:mm:ss±zz`).
727    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            // PDU mode: <stat>,[<alpha>],<length>
749            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        // A status report (`<stat>,<fo>,<mr>,…`) or command has an unquoted number
761        // where a message has its address: not decoded.
762        if a.get(1).is_some_and(|x| !x.quoted && !x.text.is_empty()) {
763            return None;
764        }
765        // Text mode (27.005 §3.4.2/§3.4.3): CMGL <oa/da>,[<alpha>],[<scts>][,<tooa/toda>,<length>];
766        // CMGR DELIVER <oa>,[<alpha>],<scts>[,<tooa>,<fo>,<pid>,<dcs>,<sca>,<tosca>,<length>];
767        // CMGR SUBMIT <da>,[<alpha>][,<toda>,<fo>,<pid>,<dcs>,[<vp>],<sca>,<tosca>,<length>].
768        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/// `+CMT` header (§3.4.1). PDU: `+CMT: [<alpha>],<length>`; text:
792/// `+CMT: <oa>,[<alpha>],<scts>[,<tooa>,<fo>,<pid>,<dcs>,<sca>,<tosca>,<length>]`.
793#[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/// `+CDS` (§3.4.1). PDU: `+CDS: <length>`; text: `+CDS: <fo>,<mr>,[<ra>],[<tora>],<scts>,<dt>,<st>`.
832#[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/// `+CMGS: <mr>[,<scts>|<ackpdu>]` (§3.5.1) / `+CMSS: <mr>[,...]` (§3.5.2).
880#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
881#[serde(rename_all = "camelCase")]
882pub struct MessageReference {
883    pub mr: u8,
884    /// `<scts>` (text mode) or `<ackpdu>` (PDU mode), verbatim.
885    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/// One storage of `+CPMS?`.
905#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
906#[serde(rename_all = "camelCase")]
907pub struct StorageUsage {
908    /// `None` in the set-command response, which has counts only.
909    pub mem: Option<String>,
910    pub used: u32,
911    pub total: u32,
912}
913
914/// `+CPMS: <mem1>,<used1>,<total1>,<mem2>,...` (read) or `+CPMS: <used1>,<total1>,...` (set).
915#[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/// `+CPMS: (list of <mem1>s),(list of <mem2>s),(list of <mem3>s)` test-command response.
949#[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            // A single storage may come without parentheses.
965            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/// `+CNMI: <mode>,<mt>,<bm>,<ds>,<bfr>` (§3.4.1, read).
983#[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/// `+CNMI: (list of <mode>s),(list of <mt>s),...` test-command response, ranges expanded.
1007#[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// ---------------------------------------------------------------------------------------------
1036// Operator selection: +COPS (§7.3)
1037
1038/// `+COPS: <mode>[,<format>,<oper>[,<AcT>]]` read-command response.
1039#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1040#[serde(rename_all = "camelCase")]
1041pub struct Cops {
1042    /// 0 automatic, 1 manual, 2 deregister, 3 set format only, 4 manual/automatic.
1043    pub mode: u8,
1044    /// 0 long alphanumeric, 1 short alphanumeric, 2 numeric.
1045    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; // test-command response
1055        }
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/// `<stat>` of a +COPS=? entry.
1066#[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/// One `(<stat>,long,short,numeric[,<AcT>])` entry of +COPS=?.
1077#[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/// `+COPS: [list of (<stat>,long,short,numeric[,<AcT>])s][,,(list of <mode>s),(list of <format>s)]`.
1088/// The trailing mode/format lists are skipped.
1089#[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            // Fields outside a tuple (the empty `,,` separator) carry no operator.
1104            let Some(g) = f.group() else {
1105                continue;
1106            };
1107            // Mode / format lists have no quoted names.
1108            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// ---------------------------------------------------------------------------------------------
1133// Packet domain events: +CGEV (§10.1.19)
1134
1135/// A `+CGEV` event. Old-style address forms (`NW DEACT <PDP_type>,<PDP_addr>[,<cid>]`) and
1136/// the current cid forms are both decoded; anything else is [`CgevEvent::Other`].
1137#[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>,<PDP_addr>`
1145    Reject { pdp_type: String, pdp_addr: String },
1146    /// `NW REACT <PDP_type>,<PDP_addr>[,<cid>]`
1147    NwReact {
1148        pdp_type: String,
1149        pdp_addr: String,
1150        cid: Option<u32>,
1151    },
1152    /// `NW DEACT <PDP_type>,<PDP_addr>[,<cid>]`
1153    NwDeactPdp {
1154        pdp_type: String,
1155        pdp_addr: String,
1156        cid: Option<u32>,
1157    },
1158    /// `ME DEACT <PDP_type>,<PDP_addr>[,<cid>]`
1159    MeDeactPdp {
1160        pdp_type: String,
1161        pdp_addr: String,
1162        cid: Option<u32>,
1163    },
1164    /// `NW PDN ACT <cid>[,<WLAN_Offload>]`
1165    NwPdnAct { cid: u32 },
1166    /// `ME PDN ACT <cid>[,<reason>[,<cid_other>]]`
1167    MePdnAct {
1168        cid: u32,
1169        reason: Option<u32>,
1170        cid_other: Option<u32>,
1171    },
1172    /// `NW PDN DEACT <cid>[,<WLAN_Offload>]`
1173    NwPdnDeact { cid: u32 },
1174    /// `ME PDN DEACT <cid>`
1175    MePdnDeact { cid: u32 },
1176    /// `NW ACT <p_cid>,<cid>,<event_type>[,<WLAN_Offload>]`
1177    NwAct {
1178        p_cid: u32,
1179        cid: u32,
1180        event_type: u32,
1181    },
1182    /// `ME ACT <p_cid>,<cid>,<event_type>[,<WLAN_Offload>]`
1183    MeAct {
1184        p_cid: u32,
1185        cid: u32,
1186        event_type: u32,
1187    },
1188    /// `NW DEACT <p_cid>,<cid>,<event_type>[,<WLAN_Offload>]`
1189    NwDeact {
1190        p_cid: u32,
1191        cid: u32,
1192        event_type: u32,
1193    },
1194    /// `ME DEACT <p_cid>,<cid>,<event_type>`
1195    MeDeact {
1196        p_cid: u32,
1197        cid: u32,
1198        event_type: u32,
1199    },
1200    /// `NW MODIFY <cid>,<change_reason>,<event_type>[,<WLAN_Offload>]`
1201    NwModify {
1202        cid: u32,
1203        change_reason: u32,
1204        event_type: u32,
1205    },
1206    /// `ME MODIFY <cid>,<change_reason>,<event_type>[,<WLAN_Offload>]`
1207    MeModify {
1208        cid: u32,
1209        change_reason: u32,
1210        event_type: u32,
1211    },
1212    /// `NW DETACH`
1213    NwDetach,
1214    /// `ME DETACH`
1215    MeDetach,
1216    /// `NW CLASS <class>`
1217    NwClass { class: String },
1218    /// `ME CLASS <class>`
1219    MeClass { class: String },
1220    /// Anything else (vendor or newer events).
1221    Other,
1222}
1223
1224/// `+CGEV: <event>`: the decoded event plus the text after `+CGEV:`.
1225#[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    /// Decodes the text after `+CGEV:` (e.g. `NW PDN ACT 1`).
1238    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        // Keywords first; the argument list starts at the first token that is not a keyword.
1250        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        // Old-style forms have a non-numeric <PDP_type> first.
1267        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// ---------------------------------------------------------------------------------------------
1347// Dispatcher
1348
1349/// An unsolicited result code recognised by [`parse_any`].
1350#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1351#[serde(tag = "urc", content = "data", rename_all = "camelCase")]
1352pub enum Urc {
1353    /// +CREG / +CGREG / +CEREG / +C5GREG (either form).
1354    Registration(Registration),
1355    Clip(CallerId),
1356    Colp(CallerId),
1357    Ccwa(CallWaiting),
1358    Ciev(Ciev),
1359    Cmti(MessageIndication),
1360    Cdsi(MessageIndication),
1361    /// Header; the PDU / text is the next line.
1362    Cmt(Cmt),
1363    /// Header in PDU mode (PDU on the next line); the whole report in text mode.
1364    Cds(Cds),
1365    Cgev(Cgev),
1366}
1367
1368/// Parses one line as any supported URC. `+CUSD` is handled by the USSD module.
1369pub 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}