nmea_parser/
lib.rs

1/*
2Copyright 2021 Timo Saarinen
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17//! # NMEA Parser: NMEA parser for Rust
18//!
19//! This crate aims to cover all AIS sentences and the most important GNSS sentences used with
20//! NMEA 0183 standard. The parser supports AIS class A and B types. It also identifies GPS,
21//! GLONASS, Galileo, BeiDou, NavIC and QZSS satellite systems.
22//!
23//! Usage in a `#[no_std]` environment is also possible though an allocator is required
24
25#![forbid(unsafe_code)]
26#![allow(dead_code)]
27#![cfg_attr(not(test), no_std)]
28
29#[macro_use]
30extern crate log;
31
32extern crate num_traits;
33
34#[macro_use]
35extern crate alloc;
36
37use alloc::string::{String, ToString};
38use alloc::vec::Vec;
39use bitvec::prelude::*;
40pub use chrono;
41use chrono::prelude::*;
42use chrono::{DateTime, TimeZone};
43use hashbrown::HashMap;
44use core::cmp::max;
45use core::str::FromStr;
46
47#[cfg(not(test))]
48use num_traits::float::FloatCore;
49
50pub mod ais;
51mod error;
52pub mod gnss;
53mod util;
54mod json_date_time_utc;
55mod json_fixed_offset;
56
57pub use error::ParseError;
58use util::*;
59
60// -------------------------------------------------------------------------------------------------
61
62/// Result from function `NmeaParser::parse_sentence()`. If the given sentence represents only a
63/// partial message `ParsedMessage::Incomplete` is returned.
64#[derive(Clone, Debug, PartialEq)]
65pub enum ParsedMessage {
66    /// The given sentence is only part of multi-sentence message and we need more data to
67    /// create the actual result. State is stored in `NmeaParser` object.
68    Incomplete,
69
70    /// AIS VDM/VDO t1, t2, t3, t18 and t27
71    VesselDynamicData(ais::VesselDynamicData),
72
73    /// AIS VDM/VDO t5 and t24
74    VesselStaticData(ais::VesselStaticData),
75
76    /// AIS VDM/VDO type 4
77    BaseStationReport(ais::BaseStationReport),
78
79    /// AIS VDM/VDO type 6
80    BinaryAddressedMessage(ais::BinaryAddressedMessage),
81    //
82    //    /// AIS VDM/VDO type 7
83    //    BinaryAcknowledge(ais::BinaryAcknowledge),
84    //
85    //    /// AIS VDM/VDO type 8
86    //    BinaryBroadcastMessage(ais::BinaryBroadcastMessage),
87
88    // AIS VDM/VDO type 9
89    StandardSarAircraftPositionReport(ais::StandardSarAircraftPositionReport),
90
91    // AIS VDM/VDO type 10
92    UtcDateInquiry(ais::UtcDateInquiry),
93
94    // AIS VDM/VDO type 11
95    UtcDateResponse(ais::BaseStationReport),
96
97    // AIS VDM/VDO type 12
98    AddressedSafetyRelatedMessage(ais::AddressedSafetyRelatedMessage),
99
100    // AIS VDM/VDO type 13
101    SafetyRelatedAcknowledgement(ais::SafetyRelatedAcknowledgement),
102
103    // AIS VDM/VDO type 14
104    SafetyRelatedBroadcastMessage(ais::SafetyRelatedBroadcastMessage),
105
106    // AIS VDM/VRO type 15
107    Interrogation(ais::Interrogation),
108
109    // AIS VDM/VRO type 16
110    AssignmentModeCommand(ais::AssignmentModeCommand),
111
112    // AIS VDM/VRO type 17
113    DgnssBroadcastBinaryMessage(ais::DgnssBroadcastBinaryMessage),
114
115    // AIS VDM/VRO type 20
116    DataLinkManagementMessage(ais::DataLinkManagementMessage),
117
118    // AIS VDM/VDO type 21
119    AidToNavigationReport(ais::AidToNavigationReport),
120
121    // AIS VDM/VDO type 22
122    ChannelManagement(ais::ChannelManagement),
123
124    // AIS VDM/VDO type 23
125    GroupAssignmentCommand(ais::GroupAssignmentCommand),
126
127    // AIS VDM/VDO type 25
128    SingleSlotBinaryMessage(ais::SingleSlotBinaryMessage),
129
130    // AIS VDM/VDO type 26
131    MultipleSlotBinaryMessage(ais::MultipleSlotBinaryMessage),
132
133    /// GGA
134    Gga(gnss::GgaData),
135
136    /// RMC
137    Rmc(gnss::RmcData),
138
139    /// GNS
140    Gns(gnss::GnsData),
141
142    /// GSA
143    Gsa(gnss::GsaData),
144
145    /// GSV
146    Gsv(Vec<gnss::GsvData>),
147
148    /// VTG
149    Vtg(gnss::VtgData),
150
151    /// GLL
152    Gll(gnss::GllData),
153
154    /// ALM
155    Alm(gnss::AlmData),
156
157    /// DTM
158    Dtm(gnss::DtmData),
159
160    /// MSS
161    Mss(gnss::MssData),
162
163    /// STN
164    Stn(gnss::StnData),
165
166    /// VBW
167    Vbw(gnss::VbwData),
168
169    /// ZDA
170    Zda(gnss::ZdaData),
171
172    /// DPT
173    Dpt(gnss::DptData),
174
175    /// DBS
176    Dbs(gnss::DbsData),
177
178    /// MTW
179    Mtw(gnss::MtwData),
180
181    /// VHW
182    Vhw(gnss::VhwData),
183
184    /// HDT
185    Hdt(gnss::HdtData),
186
187    /// MWV
188    Mwv(gnss::MwvData),
189}
190
191// -------------------------------------------------------------------------------------------------
192
193/// Read-only access to geographical position in the implementing type.
194pub trait LatLon {
195    /// Return the latitude of the position contained by the object. If the position is not
196    /// available return `None`.
197    fn latitude(&self) -> Option<f64>;
198
199    /// Return the longitude of the position contained by the object. If the position is not
200    /// available return `None`.
201    fn longitude(&self) -> Option<f64>;
202}
203
204// -------------------------------------------------------------------------------------------------
205
206/// NMEA sentence parser which keeps multi-sentence state between `parse_sentence` calls.
207/// The parser tries to be as permissible as possible about the field formats because some NMEA
208/// encoders don't follow the standards strictly.
209#[derive(Clone)]
210pub struct NmeaParser {
211    saved_fragments: HashMap<String, String>,
212    saved_vsds: HashMap<u32, ais::VesselStaticData>,
213}
214
215impl Default for NmeaParser {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221impl NmeaParser {
222    /// Construct an empty parser which is ready to receive sentences.
223    pub fn new() -> NmeaParser {
224        NmeaParser {
225            saved_fragments: HashMap::new(),
226            saved_vsds: HashMap::new(),
227        }
228    }
229
230    /// Clear internal state of the parser. Multi-sentence state is lost when this function
231    /// is called.
232    pub fn reset(&mut self) {
233        self.saved_fragments.clear();
234        self.saved_vsds.clear();
235    }
236
237    /// Push string-to-string mapping to store.
238    fn push_string(&mut self, key: String, value: String) {
239        self.saved_fragments.insert(key, value);
240    }
241
242    /// Pull string-to-string mapping by key from store.
243    fn pull_string(&mut self, key: String) -> Option<String> {
244        self.saved_fragments.remove(&key)
245    }
246
247    /// Tests whether the given string-to-string mapping exists in the store.
248    fn contains_key(&mut self, key: String) -> bool {
249        self.saved_fragments.contains_key(&key)
250    }
251
252    /// Return number of string-to-string mappings stored.
253    fn strings_count(&self) -> usize {
254        self.saved_fragments.len()
255    }
256
257    /// Push MMSI-to-VesselStaticData mapping to store.
258    fn push_vsd(&mut self, mmsi: u32, vsd: ais::VesselStaticData) {
259        self.saved_vsds.insert(mmsi, vsd);
260    }
261
262    /// Pull MMSI-to-VesselStaticData mapping from store.
263    fn pull_vsd(&mut self, mmsi: u32) -> Option<ais::VesselStaticData> {
264        self.saved_vsds.remove(&mmsi)
265    }
266
267    /// Return number of MMSI-to-VesselStaticData mappings in store.
268    fn vsds_count(&self) -> usize {
269        self.saved_vsds.len()
270    }
271
272    /// Parse NMEA sentence into `ParsedMessage` enum. If the given sentence is part of
273    /// a multipart message the related state is saved into the parser and
274    /// `ParsedMessage::Incomplete` is returned. The actual result is returned when all the parts
275    /// have been sent to the parser.
276    pub fn parse_sentence(&mut self, sentence: &str) -> Result<ParsedMessage, ParseError> {
277        // Shed characters prefixing the message if they exist
278        let sentence = {
279            if let Some(start_idx) = sentence.find(['$', '!']) {
280                &sentence[start_idx..]
281            } else {
282                return Err(ParseError::InvalidSentence(format!(
283                    "Invalid NMEA sentence: {}",
284                    sentence
285                )));
286            }
287        };
288
289        // Calculate NMEA checksum and compare it to the given one. Also, remove the checksum part
290        // from the sentence to simplify next processing steps.
291        let mut checksum = 0;
292        let (sentence, checksum_hex_given) = {
293            if let Some(pos) = sentence.rfind('*') {
294                if pos + 3 <= sentence.len() {
295                    (
296                        sentence[0..pos].to_string(),
297                        sentence[(pos + 1)..(pos + 3)].to_string(),
298                    )
299                } else {
300                    debug!("Invalid checksum found for sentence: {}", sentence);
301                    (sentence[0..pos].to_string(), "".to_string())
302                }
303            } else {
304                debug!("No checksum found for sentence: {}", sentence);
305                (sentence.to_string(), "".to_string())
306            }
307        };
308        for c in sentence.as_str().chars().skip(1) {
309            checksum ^= c as u8;
310        }
311        let checksum_hex_calculated = format!("{:02X?}", checksum);
312        if checksum_hex_calculated != checksum_hex_given && !checksum_hex_given.is_empty() {
313            return Err(ParseError::CorruptedSentence(format!(
314                "Corrupted NMEA sentence: {:02X?} != {:02X?}",
315                checksum_hex_calculated, checksum_hex_given
316            )));
317        }
318
319        // Pick sentence type
320        let sentence_type = {
321            if let Some(i) = sentence.find(',') {
322                &sentence[0..i]
323            } else {
324                return Err(ParseError::InvalidSentence(format!(
325                    "Invalid NMEA sentence: {}",
326                    sentence
327                )));
328            }
329        };
330
331        // Validate sentence type characters
332        if !sentence_type
333            .chars()
334            .all(|c| c.is_ascii_alphanumeric() || c == '$' || c == '!')
335        {
336            return Err(ParseError::InvalidSentence(format!(
337                "Invalid characters in sentence type: {}",
338                sentence_type
339            )));
340        }
341
342        let (nav_system, station, sentence_type) = if sentence_type.starts_with('$') {
343            // Identify GNSS system by talker ID.
344            let nav_system = gnss::NavigationSystem::from_str(
345                sentence_type
346                    .get(1..)
347                    .ok_or(ParseError::CorruptedSentence("Empty String".to_string()))?,
348            )?;
349            let sentence_type = if !sentence_type.starts_with('P') && sentence_type.len() == 6 {
350                format!(
351                    "${}",
352                    sentence_type
353                        .get(3..6)
354                        .ok_or(ParseError::InvalidSentence(format!(
355                            "{sentence_type} is too short."
356                        )))?
357                )
358            } else {
359                String::from(sentence_type)
360            };
361            (nav_system, ais::Station::Other, sentence_type)
362        } else if sentence_type.starts_with('!') {
363            // Identify AIS station
364            let station = ais::Station::from_str(
365                sentence_type
366                    .get(1..)
367                    .ok_or(ParseError::CorruptedSentence("Empty String".to_string()))?,
368            )?;
369            let sentence_type = if sentence_type.len() == 6 {
370                format!(
371                    "!{}",
372                    sentence_type
373                        .get(3..6)
374                        .ok_or(ParseError::InvalidSentence(format!(
375                            "{sentence_type} is too short."
376                        )))?
377                )
378            } else {
379                String::from(sentence_type)
380            };
381            (gnss::NavigationSystem::Other, station, sentence_type)
382        } else {
383            (
384                gnss::NavigationSystem::Other,
385                ais::Station::Other,
386                String::from(sentence_type),
387            )
388        };
389
390        // Handle sentence types
391        match sentence_type.as_str() {
392            // $xxGGA - Global Positioning System Fix Data
393            "$GGA" => gnss::gga::handle(sentence.as_str(), nav_system),
394            // $xxRMC - Recommended minimum specific GPS/Transit data
395            "$RMC" => gnss::rmc::handle(sentence.as_str(), nav_system),
396            // $xxGNS - GNSS fix data
397            "$GNS" => gnss::gns::handle(sentence.as_str(), nav_system),
398            // $xxGSA - GPS DOP and active satellites
399            "$GSA" => gnss::gsa::handle(sentence.as_str(), nav_system),
400            // $xxGSV - GPS Satellites in view
401            "$GSV" => gnss::gsv::handle(sentence.as_str(), nav_system, self),
402            // $xxVTG - Track made good and ground speed
403            "$VTG" => gnss::vtg::handle(sentence.as_str(), nav_system),
404            // $xxGLL - Geographic position, latitude / longitude
405            "$GLL" => gnss::gll::handle(sentence.as_str(), nav_system),
406            // $xxALM - Almanac Data
407            "$ALM" => gnss::alm::handle(sentence.as_str(), nav_system),
408            // $xxDTM - Datum reference
409            "$DTM" => gnss::dtm::handle(sentence.as_str(), nav_system),
410            // $xxMSS - MSK receiver signal
411            "$MSS" => gnss::mss::handle(sentence.as_str(), nav_system),
412            // $xxSTN - Multiple Data ID
413            "$STN" => gnss::stn::handle(sentence.as_str(), nav_system),
414            // $xxVBW - MSK Receiver Signal
415            "$VBW" => gnss::vbw::handle(sentence.as_str(), nav_system),
416            // $xxZDA - Date and time
417            "$ZDA" => gnss::zda::handle(sentence.as_str(), nav_system),
418
419            // Received AIS data from other or own vessel
420            "!VDM" | "!VDO" => {
421                let own_vessel = sentence_type.as_str() == "!VDO";
422                let mut fragment_count = 0;
423                let mut fragment_number = 0;
424                let mut message_id = None;
425                let mut radio_channel_code = None;
426                let mut payload_string: String = "".into();
427                for (num, s) in sentence.split(',').enumerate() {
428                    match num {
429                        1 => {
430                            match s.parse::<u8>() {
431                                Ok(i) => {
432                                    fragment_count = i;
433                                }
434                                Err(_) => {
435                                    return Err(ParseError::InvalidSentence(format!(
436                                        "Failed to parse fragment count: {}",
437                                        s
438                                    )));
439                                }
440                            };
441                        }
442                        2 => {
443                            match s.parse::<u8>() {
444                                Ok(i) => {
445                                    fragment_number = i;
446                                }
447                                Err(_) => {
448                                    return Err(ParseError::InvalidSentence(format!(
449                                        "Failed to parse fragment count: {}",
450                                        s
451                                    )));
452                                }
453                            };
454                        }
455                        3 => {
456                            message_id = s.parse::<u64>().ok();
457                        }
458                        4 => {
459                            // Radio channel code
460                            radio_channel_code = Some(s);
461                        }
462                        5 => {
463                            payload_string = s.to_string();
464                        }
465                        6 => {
466                            // fill bits
467                        }
468                        _ => {}
469                    }
470                }
471
472                // Try parse the payload
473                let mut bv: Option<BitVec> = None;
474                match fragment_count {
475                    1 => bv = parse_payload(&payload_string).ok(),
476                    2 => {
477                        if let Some(msg_id) = message_id {
478                            let key1 = make_fragment_key(
479                                &sentence_type.to_string(),
480                                msg_id,
481                                fragment_count,
482                                1,
483                                radio_channel_code.unwrap_or(""),
484                            );
485                            let key2 = make_fragment_key(
486                                &sentence_type.to_string(),
487                                msg_id,
488                                fragment_count,
489                                2,
490                                radio_channel_code.unwrap_or(""),
491                            );
492                            match fragment_number {
493                                1 => {
494                                    if let Some(p) = self.pull_string(key2) {
495                                        let mut payload_string_combined = payload_string;
496                                        payload_string_combined.push_str(p.as_str());
497                                        bv = parse_payload(&payload_string_combined).ok();
498                                    } else {
499                                        self.push_string(key1, payload_string);
500                                    }
501                                }
502                                2 => {
503                                    if let Some(p) = self.pull_string(key1) {
504                                        let mut payload_string_combined = p;
505                                        payload_string_combined.push_str(payload_string.as_str());
506                                        bv = parse_payload(&payload_string_combined).ok();
507                                    } else {
508                                        self.push_string(key2, payload_string);
509                                    }
510                                }
511                                _ => {
512                                    warn!(
513                                        "Unexpected NMEA fragment number: {}/{}",
514                                        fragment_number, fragment_count
515                                    );
516                                }
517                            }
518                        } else {
519                            warn!(
520                                "NMEA message_id missing from {} than supported 2",
521                                sentence_type
522                            );
523                        }
524                    }
525                    _ => {
526                        warn!(
527                            "NMEA sentence fragment count greater ({}) than supported 2",
528                            fragment_count
529                        );
530                    }
531                }
532
533                if let Some(bv) = bv {
534                    let message_type = pick_u64(&bv, 0, 6);
535                    match message_type {
536                        // Position report with SOTDMA/ITDMA
537                        1..=3 => ais::vdm_t1t2t3::handle(&bv, station, own_vessel),
538                        // Base station report
539                        4 => ais::vdm_t4::handle(&bv, station, own_vessel),
540                        // Ship static voyage related data
541                        5 => ais::vdm_t5::handle(&bv, station, own_vessel),
542                        // Addressed binary message
543                        6 => ais::vdm_t6::handle(&bv, station, own_vessel),
544                        // Binary acknowledge
545                        7 => {
546                            // TODO: implementation
547                            Err(ParseError::UnsupportedSentenceType(format!(
548                                "Unsupported {} message type: {}",
549                                sentence_type, message_type
550                            )))
551                        }
552                        // Binary broadcast message
553                        8 => {
554                            // TODO: implementation
555                            Err(ParseError::UnsupportedSentenceType(format!(
556                                "Unsupported {} message type: {}",
557                                sentence_type, message_type
558                            )))
559                        }
560                        // Standard SAR aircraft position report
561                        9 => ais::vdm_t9::handle(&bv, station, own_vessel),
562                        // UTC and Date inquiry
563                        10 => ais::vdm_t10::handle(&bv, station, own_vessel),
564                        // UTC and date response
565                        11 => ais::vdm_t11::handle(&bv, station, own_vessel),
566                        // Addressed safety related message
567                        12 => ais::vdm_t12::handle(&bv, station, own_vessel),
568                        // Safety related acknowledge
569                        13 => ais::vdm_t13::handle(&bv, station, own_vessel),
570                        // Safety related broadcast message
571                        14 => ais::vdm_t14::handle(&bv, station, own_vessel),
572                        // Interrogation
573                        15 => ais::vdm_t15::handle(&bv, station, own_vessel),
574                        // Assigned mode command
575                        16 => ais::vdm_t16::handle(&bv, station, own_vessel),
576                        // GNSS binary broadcast message
577                        17 => ais::vdm_t17::handle(&bv, station, own_vessel),
578                        // Standard class B CS position report
579                        18 => ais::vdm_t18::handle(&bv, station, own_vessel),
580                        // Extended class B equipment position report
581                        19 => ais::vdm_t19::handle(&bv, station, own_vessel),
582                        // Data link management
583                        20 => ais::vdm_t20::handle(&bv, station, own_vessel),
584                        // Aids-to-navigation report
585                        21 => ais::vdm_t21::handle(&bv, station, own_vessel),
586                        // Channel management
587                        22 => ais::vdm_t22::handle(&bv, station, own_vessel),
588                        // Group assignment command
589                        23 => ais::vdm_t23::handle(&bv, station, own_vessel),
590                        // Class B CS static data report
591                        24 => ais::vdm_t24::handle(&bv, station, self, own_vessel),
592                        // Single slot binary message
593                        25 => ais::vdm_t25::handle(&bv, station, own_vessel),
594                        // Multiple slot binary message
595                        26 => ais::vdm_t26::handle(&bv, station, own_vessel),
596                        // Long range AIS broadcast message
597                        27 => ais::vdm_t27::handle(&bv, station, own_vessel),
598                        _ => Err(ParseError::UnsupportedSentenceType(format!(
599                            "Unsupported {} message type: {}",
600                            sentence_type, message_type
601                        ))),
602                    }
603                } else {
604                    Ok(ParsedMessage::Incomplete)
605                }
606            }
607            "$DPT" => gnss::dpt::handle(sentence.as_str()),
608            "$DBS" => gnss::dbs::handle(sentence.as_str()),
609            "$MTW" => gnss::mtw::handle(sentence.as_str()),
610            "$VHW" => gnss::vhw::handle(sentence.as_str()),
611            "$HDT" => gnss::hdt::handle(sentence.as_str()),
612            "$MWV" => gnss::mwv::handle(sentence.as_str()),
613            _ => Err(ParseError::UnsupportedSentenceType(format!(
614                "Unsupported sentence type: {}",
615                sentence_type
616            ))),
617        }
618    }
619}
620
621#[cfg(test)]
622mod test {
623    use super::*;
624    #[test]
625    fn test_parse_invalid_sentence() {
626        let mut p = NmeaParser::new();
627        assert_eq!(
628            p.parse_sentence("$޴GAGSV,,"),
629            Err(ParseError::InvalidSentence(
630                "Invalid characters in sentence type: $\u{7b4}GAGSV".to_string()
631            ))
632        );
633        assert_eq!(
634            p.parse_sentence("$WIMWV,295.4,T,"),
635            Err(ParseError::CorruptedSentence(
636                "pick string for \"wind_speed_knots\" was None".to_string()
637            ))
638        );
639        assert_eq!(
640            p.parse_sentence("!AIVDM,not,a,valid,nmea,string,0*00"),
641            Err(ParseError::CorruptedSentence(
642                "Corrupted NMEA sentence: \"17\" != \"00\"".to_string()
643            ))
644        );
645        assert_eq!(
646            p.parse_sentence("!"),
647            Err(ParseError::InvalidSentence(
648                "Invalid NMEA sentence: !".to_string()
649            ))
650        );
651    }
652    #[test]
653    fn test_parse_prefix_chars() {
654        // Try a sentence with prefix characters
655        let mut p = NmeaParser::new();
656        assert!(p
657            .parse_sentence(",1277,-106*35\r\n!AIVDM,1,1,,A,152IS=iP?w<tSF0l4Q@>4?wp0H:;,0*2")
658            .ok()
659            .is_some());
660    }
661
662    #[test]
663    fn test_parse_corrupted() {
664        // Try a sentence with mismatching checksum
665        let mut p = NmeaParser::new();
666        assert!(p
667            .parse_sentence("!AIVDM,1,1,,A,38Id705000rRVJhE7cl9n;160000,0*41")
668            .ok()
669            .is_none());
670    }
671
672    #[test]
673    fn test_parse_missing_checksum() {
674        // Try a sentence without checksum
675        let mut p = NmeaParser::new();
676        assert!(p
677            .parse_sentence("!AIVDM,1,1,,A,38Id705000rRVJhE7cl9n;160000,0")
678            .ok()
679            .is_some());
680    }
681
682    #[test]
683    fn test_parse_invalid_utc() {
684        // Try a sentence with invalite utc
685        let mut p = NmeaParser::new();
686        assert_eq!(
687            p.parse_sentence("!AIVDM,1,1,,B,4028iqT47wP00wGiNbH8H0700`2H,0*13"),
688            Err(ParseError::InvalidSentence(String::from(
689                "Failed to parse Utc Date from y:4161 m:15 d:31 h:0 m:0 s:0"
690            )))
691        );
692    }
693
694    #[test]
695    fn test_parse_proprietary() {
696        /* FIXME: The test fails
697                // Try a proprietary sentence
698                let mut p = NmeaParser::new();
699                assert_eq!(
700                    p.parse_sentence("$PGRME,15.0,M,45.0,M,25.0,M*1C"),
701                    Err(ParseError::UnsupportedSentenceType(String::from(
702                        "Unsupported sentence type: $PGRME"
703                    )))
704                );
705                // Try a proprietary sentence with four characters
706                assert_eq!(
707                    p.parse_sentence("$PGRM,00,1,,,*15"),
708                    Err(ParseError::UnsupportedSentenceType(String::from(
709                        "Unsupported sentence type: $PGRM"
710                    )))
711                );
712        */
713    }
714
715    #[test]
716    fn test_parse_invalid_talker() {
717        // Try parse malformed sentences
718        let mut p = NmeaParser::new();
719        assert_eq!(
720            p.parse_sentence("$QQ,*2C"),
721            Err(ParseError::UnsupportedSentenceType(String::from(
722                "Unsupported sentence type: $QQ"
723            )))
724        );
725        assert_eq!(
726            p.parse_sentence("$A,a0,*10"),
727            Err(ParseError::InvalidSentence(String::from(
728                "Invalid talker identifier"
729            )))
730        );
731        assert_eq!(
732            p.parse_sentence("$,0a,*51"),
733            Err(ParseError::InvalidSentence(String::from(
734                "Invalid talker identifier"
735            )))
736        );
737    }
738
739    #[test]
740    fn test_nmea_parser() {
741        let mut p = NmeaParser::new();
742
743        // String test
744        p.push_string("a".into(), "b".into());
745        assert_eq!(p.strings_count(), 1);
746        p.push_string("c".into(), "d".into());
747        assert_eq!(p.strings_count(), 2);
748        p.pull_string("a".into());
749        assert_eq!(p.strings_count(), 1);
750        p.pull_string("c".into());
751        assert_eq!(p.strings_count(), 0);
752
753        // VesselStaticData test
754        p.push_vsd(1, Default::default());
755        assert_eq!(p.vsds_count(), 1);
756        p.push_vsd(2, Default::default());
757        assert_eq!(p.vsds_count(), 2);
758        p.pull_vsd(1);
759        assert_eq!(p.vsds_count(), 1);
760        p.pull_vsd(2);
761        assert_eq!(p.vsds_count(), 0);
762    }
763
764    #[test]
765    fn test_country() {
766        assert_eq!(vsd(230992580).country().unwrap(), "FI");
767        assert_eq!(vsd(276009860).country().unwrap(), "EE");
768        assert_eq!(vsd(265803690).country().unwrap(), "SE");
769        assert_eq!(vsd(273353180).country().unwrap(), "RU");
770        assert_eq!(vsd(211805060).country().unwrap(), "DE");
771        assert_eq!(vsd(257037270).country().unwrap(), "NO");
772        assert_eq!(vsd(227232370).country().unwrap(), "FR");
773        assert_eq!(vsd(248221000).country().unwrap(), "MT");
774        assert_eq!(vsd(374190000).country().unwrap(), "PA");
775        assert_eq!(vsd(412511368).country().unwrap(), "CN");
776        assert_eq!(vsd(512003200).country().unwrap(), "NZ");
777        assert_eq!(vsd(995126020).country(), None);
778        assert_eq!(vsd(2300049).country(), None);
779        assert_eq!(vsd(0).country(), None);
780    }
781
782    /// Create a `VesselStaticData` with the given MMSI
783    fn vsd(mmsi: u32) -> ais::VesselStaticData {
784        let mut vsd = ais::VesselStaticData::default();
785        vsd.mmsi = mmsi;
786        vsd
787    }
788}