nmea-kit 0.5.0

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

/// ABM — AIS Addressed Binary Message.
///
/// Wire: `num_frags,frag_num,msg_id,mmsi,channel,vdl_msg_num,payload,fill_bits`
///
/// Note: ABM sentences use the `!` prefix on the wire but field parsing
/// is identical to `$`-prefixed sentences at the field layer.
#[derive(Debug, Clone, PartialEq)]
pub struct Abm {
    /// Total number of sentences needed.
    pub num_frags: Option<u8>,
    /// Fragment number of this sentence.
    pub frag_num: Option<u8>,
    /// Sequential message identifier.
    pub msg_id: Option<u8>,
    /// Destination MMSI.
    pub mmsi: Option<u32>,
    /// VHF channel (A or B).
    pub channel: Option<char>,
    /// VDL message number.
    pub vdl_msg_num: Option<u8>,
    /// Encoded AIS payload (armored ASCII).
    pub payload: Option<String>,
    /// Number of fill bits (0–5).
    pub fill_bits: Option<u8>,
}

impl Abm {
    /// 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 num_frags = r.u8();
        let frag_num = r.u8();
        let msg_id = r.u8();
        let mmsi = r.u32();
        let channel = r.char();
        let vdl_msg_num = r.u8();
        let payload = r.string();
        let fill_bits = r.u8();
        Some(Self {
            num_frags,
            frag_num,
            msg_id,
            mmsi,
            channel,
            vdl_msg_num,
            payload,
            fill_bits,
        })
    }
}

impl NmeaEncodable for Abm {
    const SENTENCE_TYPE: &str = "ABM";

    fn encode(&self) -> Vec<String> {
        let mut w = FieldWriter::new();
        w.u8(self.num_frags);
        w.u8(self.frag_num);
        w.u8(self.msg_id);
        w.u32(self.mmsi);
        w.char(self.channel);
        w.u8(self.vdl_msg_num);
        w.string(self.payload.as_deref());
        w.u8(self.fill_bits);
        w.finish()
    }
}

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

    #[test]
    fn abm_empty() {
        let s = Abm {
            num_frags: None,
            frag_num: None,
            msg_id: None,
            mmsi: None,
            channel: None,
            vdl_msg_num: None,
            payload: None,
            fill_bits: None,
        }
        .to_sentence("AI");
        let f = parse_frame(s.trim()).expect("valid");
        let a = Abm::parse(&f.fields).expect("parse");
        assert!(a.num_frags.is_none());
        assert!(a.mmsi.is_none());
        assert!(a.payload.is_none());
    }

    #[test]
    fn abm_encode_roundtrip() {
        let original = Abm {
            num_frags: Some(1),
            frag_num: Some(1),
            msg_id: Some(0),
            mmsi: Some(123456789),
            channel: Some('A'),
            vdl_msg_num: Some(6),
            payload: Some("test".to_string()),
            fill_bits: Some(0),
        };
        let sentence = original.to_sentence("AI");
        let frame = parse_frame(sentence.trim()).expect("re-parse");
        let parsed = Abm::parse(&frame.fields).expect("parse");
        assert_eq!(original, parsed);
    }

    #[test]
    fn abm_aiabm_gonmea() {
        let original = Abm {
            num_frags: Some(1),
            frag_num: Some(1),
            msg_id: Some(0),
            mmsi: Some(987654321),
            channel: Some('B'),
            vdl_msg_num: Some(12),
            payload: Some("payload".to_string()),
            fill_bits: Some(2),
        };
        let sentence = original.to_sentence("AI");
        let frame = parse_frame(sentence.trim()).expect("re-parse");
        let a = Abm::parse(&frame.fields).expect("parse ABM");
        assert_eq!(a.num_frags, Some(1));
        assert_eq!(a.mmsi, Some(987654321));
        assert_eq!(a.channel, Some('B'));
        assert_eq!(a.payload, Some("payload".to_string()));
        assert_eq!(a.fill_bits, Some(2));
    }
}