nmea-kit 0.5.0

Bidirectional NMEA 0183 parser and encoder with AIS decoding
Documentation
use crate::nmea::field::{FieldReader, FieldWriter, NmeaEncodable};

/// DSC — Digital Selective Calling.
///
/// Wire: `format_specifier,address,category,cmd1,cmd2,position,time_or_tel,mmsi,distress_cause,ack,expansion`
#[derive(Debug, Clone, PartialEq)]
pub struct Dsc {
    /// Format specifier (e.g. "12" for distress).
    pub format_specifier: Option<String>,
    /// DSC address.
    pub address: Option<String>,
    /// Category code.
    pub category: Option<String>,
    /// First command/nature of distress.
    pub cmd1: Option<String>,
    /// Second command.
    pub cmd2: Option<String>,
    /// Position (lat/lon encoded).
    pub position: Option<String>,
    /// Time or telephone number.
    pub time_or_tel: Option<String>,
    /// MMSI of transmitter.
    pub mmsi: Option<String>,
    /// Distress cause.
    pub distress_cause: Option<String>,
    /// Acknowledgement type.
    pub ack: Option<String>,
    /// Expansion indicator.
    pub expansion: Option<String>,
}

impl Dsc {
    /// Parse fields from a decoded NMEA frame.
    /// Always returns `Some`; missing or malformed fields become `None`.
    pub fn parse(fields: &[&str]) -> Option<Self> {
        let mut r = FieldReader::new(fields);
        let format_specifier = r.string();
        let address = r.string();
        let category = r.string();
        let cmd1 = r.string();
        let cmd2 = r.string();
        let position = r.string();
        let time_or_tel = r.string();
        let mmsi = r.string();
        let distress_cause = r.string();
        let ack = r.string();
        let expansion = r.string();
        Some(Self {
            format_specifier,
            address,
            category,
            cmd1,
            cmd2,
            position,
            time_or_tel,
            mmsi,
            distress_cause,
            ack,
            expansion,
        })
    }
}

impl NmeaEncodable for Dsc {
    const SENTENCE_TYPE: &str = "DSC";

    fn encode(&self) -> Vec<String> {
        let mut w = FieldWriter::new();
        w.string(self.format_specifier.as_deref());
        w.string(self.address.as_deref());
        w.string(self.category.as_deref());
        w.string(self.cmd1.as_deref());
        w.string(self.cmd2.as_deref());
        w.string(self.position.as_deref());
        w.string(self.time_or_tel.as_deref());
        w.string(self.mmsi.as_deref());
        w.string(self.distress_cause.as_deref());
        w.string(self.ack.as_deref());
        w.string(self.expansion.as_deref());
        w.finish()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parse_frame;

    #[test]
    fn dsc_empty() {
        let s = Dsc {
            format_specifier: None,
            address: None,
            category: None,
            cmd1: None,
            cmd2: None,
            position: None,
            time_or_tel: None,
            mmsi: None,
            distress_cause: None,
            ack: None,
            expansion: None,
        }
        .to_sentence("CD");
        let f = parse_frame(s.trim()).expect("valid");
        let d = Dsc::parse(&f.fields).expect("parse");
        assert!(d.format_specifier.is_none());
        assert!(d.address.is_none());
        assert!(d.expansion.is_none());
    }

    #[test]
    fn dsc_encode_roundtrip() {
        let original = Dsc {
            format_specifier: Some("12".to_string()),
            address: Some("3380400790".to_string()),
            category: Some("12".to_string()),
            cmd1: Some("06".to_string()),
            cmd2: Some("00".to_string()),
            position: Some("1423108312".to_string()),
            time_or_tel: Some("2019".to_string()),
            mmsi: None,
            distress_cause: None,
            ack: Some("S".to_string()),
            expansion: Some("E".to_string()),
        };
        let sentence = original.to_sentence("CD");
        let frame = parse_frame(sentence.trim()).expect("re-parse");
        let parsed = Dsc::parse(&frame.fields).expect("parse");
        assert_eq!(original, parsed);
    }

    #[test]
    fn dsc_cddsc_gonmea() {
        // Note: fields with spaces like " " are returned as None by r.string()
        let f = parse_frame("$CDDSC,12,3380400790,12,06,00,1423108312,2019,S,E*6A")
            .expect("valid DSC");
        let d = Dsc::parse(&f.fields).expect("parse DSC");
        assert_eq!(d.format_specifier, Some("12".to_string()));
        assert_eq!(d.address, Some("3380400790".to_string()));
        assert_eq!(d.category, Some("12".to_string()));
        assert_eq!(d.cmd1, Some("06".to_string()));
        assert_eq!(d.cmd2, Some("00".to_string()));
        assert_eq!(d.position, Some("1423108312".to_string()));
        assert_eq!(d.time_or_tel, Some("2019".to_string()));
        assert_eq!(d.mmsi, Some("S".to_string()));
        assert_eq!(d.distress_cause, Some("E".to_string()));
    }
}