#![forbid(unsafe_code)]
#![allow(dead_code)]
#![cfg_attr(not(test), no_std)]
#[macro_use]
extern crate log;
extern crate num_traits;
#[macro_use]
extern crate alloc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use bitvec::prelude::*;
pub use chrono;
use chrono::prelude::*;
use chrono::{DateTime, TimeZone};
use hashbrown::HashMap;
use core::cmp::max;
use core::str::FromStr;
#[cfg(not(test))]
use num_traits::float::FloatCore;
pub mod ais;
mod error;
pub mod gnss;
mod util;
mod json_date_time_utc;
mod json_fixed_offset;
pub use error::ParseError;
use util::*;
#[derive(Clone, Debug, PartialEq)]
pub enum ParsedMessage {
Incomplete,
VesselDynamicData(ais::VesselDynamicData),
VesselStaticData(ais::VesselStaticData),
BaseStationReport(ais::BaseStationReport),
BinaryAddressedMessage(ais::BinaryAddressedMessage),
StandardSarAircraftPositionReport(ais::StandardSarAircraftPositionReport),
UtcDateInquiry(ais::UtcDateInquiry),
UtcDateResponse(ais::BaseStationReport),
AddressedSafetyRelatedMessage(ais::AddressedSafetyRelatedMessage),
SafetyRelatedAcknowledgement(ais::SafetyRelatedAcknowledgement),
SafetyRelatedBroadcastMessage(ais::SafetyRelatedBroadcastMessage),
Interrogation(ais::Interrogation),
AssignmentModeCommand(ais::AssignmentModeCommand),
DgnssBroadcastBinaryMessage(ais::DgnssBroadcastBinaryMessage),
DataLinkManagementMessage(ais::DataLinkManagementMessage),
AidToNavigationReport(ais::AidToNavigationReport),
ChannelManagement(ais::ChannelManagement),
GroupAssignmentCommand(ais::GroupAssignmentCommand),
SingleSlotBinaryMessage(ais::SingleSlotBinaryMessage),
MultipleSlotBinaryMessage(ais::MultipleSlotBinaryMessage),
Gga(gnss::GgaData),
Rmc(gnss::RmcData),
Gns(gnss::GnsData),
Gsa(gnss::GsaData),
Gsv(Vec<gnss::GsvData>),
Vtg(gnss::VtgData),
Gll(gnss::GllData),
Alm(gnss::AlmData),
Dtm(gnss::DtmData),
Mss(gnss::MssData),
Stn(gnss::StnData),
Vbw(gnss::VbwData),
Zda(gnss::ZdaData),
Dpt(gnss::DptData),
Dbs(gnss::DbsData),
Mtw(gnss::MtwData),
Vhw(gnss::VhwData),
Hdt(gnss::HdtData),
Mwv(gnss::MwvData),
}
pub trait LatLon {
fn latitude(&self) -> Option<f64>;
fn longitude(&self) -> Option<f64>;
}
#[derive(Clone)]
pub struct NmeaParser {
saved_fragments: HashMap<String, String>,
saved_vsds: HashMap<u32, ais::VesselStaticData>,
}
impl Default for NmeaParser {
fn default() -> Self {
Self::new()
}
}
impl NmeaParser {
pub fn new() -> NmeaParser {
NmeaParser {
saved_fragments: HashMap::new(),
saved_vsds: HashMap::new(),
}
}
pub fn reset(&mut self) {
self.saved_fragments.clear();
self.saved_vsds.clear();
}
fn push_string(&mut self, key: String, value: String) {
self.saved_fragments.insert(key, value);
}
fn pull_string(&mut self, key: String) -> Option<String> {
self.saved_fragments.remove(&key)
}
fn contains_key(&mut self, key: String) -> bool {
self.saved_fragments.contains_key(&key)
}
fn strings_count(&self) -> usize {
self.saved_fragments.len()
}
fn push_vsd(&mut self, mmsi: u32, vsd: ais::VesselStaticData) {
self.saved_vsds.insert(mmsi, vsd);
}
fn pull_vsd(&mut self, mmsi: u32) -> Option<ais::VesselStaticData> {
self.saved_vsds.remove(&mmsi)
}
fn vsds_count(&self) -> usize {
self.saved_vsds.len()
}
pub fn parse_sentence(&mut self, sentence: &str) -> Result<ParsedMessage, ParseError> {
let sentence = {
if let Some(start_idx) = sentence.find(['$', '!']) {
&sentence[start_idx..]
} else {
return Err(ParseError::InvalidSentence(format!(
"Invalid NMEA sentence: {}",
sentence
)));
}
};
let mut checksum = 0;
let (sentence, checksum_hex_given) = {
if let Some(pos) = sentence.rfind('*') {
if pos + 3 <= sentence.len() {
(
sentence[0..pos].to_string(),
sentence[(pos + 1)..(pos + 3)].to_string(),
)
} else {
debug!("Invalid checksum found for sentence: {}", sentence);
(sentence[0..pos].to_string(), "".to_string())
}
} else {
debug!("No checksum found for sentence: {}", sentence);
(sentence.to_string(), "".to_string())
}
};
for c in sentence.as_str().chars().skip(1) {
checksum ^= c as u8;
}
let checksum_hex_calculated = format!("{:02X?}", checksum);
if checksum_hex_calculated != checksum_hex_given && !checksum_hex_given.is_empty() {
return Err(ParseError::CorruptedSentence(format!(
"Corrupted NMEA sentence: {:02X?} != {:02X?}",
checksum_hex_calculated, checksum_hex_given
)));
}
let sentence_type = {
if let Some(i) = sentence.find(',') {
&sentence[0..i]
} else {
return Err(ParseError::InvalidSentence(format!(
"Invalid NMEA sentence: {}",
sentence
)));
}
};
if !sentence_type
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '$' || c == '!')
{
return Err(ParseError::InvalidSentence(format!(
"Invalid characters in sentence type: {}",
sentence_type
)));
}
let (nav_system, station, sentence_type) = if sentence_type.starts_with('$') {
let nav_system = gnss::NavigationSystem::from_str(
sentence_type
.get(1..)
.ok_or(ParseError::CorruptedSentence("Empty String".to_string()))?,
)?;
let sentence_type = if !sentence_type.starts_with('P') && sentence_type.len() == 6 {
format!(
"${}",
sentence_type
.get(3..6)
.ok_or(ParseError::InvalidSentence(format!(
"{sentence_type} is too short."
)))?
)
} else {
String::from(sentence_type)
};
(nav_system, ais::Station::Other, sentence_type)
} else if sentence_type.starts_with('!') {
let station = ais::Station::from_str(
sentence_type
.get(1..)
.ok_or(ParseError::CorruptedSentence("Empty String".to_string()))?,
)?;
let sentence_type = if sentence_type.len() == 6 {
format!(
"!{}",
sentence_type
.get(3..6)
.ok_or(ParseError::InvalidSentence(format!(
"{sentence_type} is too short."
)))?
)
} else {
String::from(sentence_type)
};
(gnss::NavigationSystem::Other, station, sentence_type)
} else {
(
gnss::NavigationSystem::Other,
ais::Station::Other,
String::from(sentence_type),
)
};
match sentence_type.as_str() {
"$GGA" => gnss::gga::handle(sentence.as_str(), nav_system),
"$RMC" => gnss::rmc::handle(sentence.as_str(), nav_system),
"$GNS" => gnss::gns::handle(sentence.as_str(), nav_system),
"$GSA" => gnss::gsa::handle(sentence.as_str(), nav_system),
"$GSV" => gnss::gsv::handle(sentence.as_str(), nav_system, self),
"$VTG" => gnss::vtg::handle(sentence.as_str(), nav_system),
"$GLL" => gnss::gll::handle(sentence.as_str(), nav_system),
"$ALM" => gnss::alm::handle(sentence.as_str(), nav_system),
"$DTM" => gnss::dtm::handle(sentence.as_str(), nav_system),
"$MSS" => gnss::mss::handle(sentence.as_str(), nav_system),
"$STN" => gnss::stn::handle(sentence.as_str(), nav_system),
"$VBW" => gnss::vbw::handle(sentence.as_str(), nav_system),
"$ZDA" => gnss::zda::handle(sentence.as_str(), nav_system),
"!VDM" | "!VDO" => {
let own_vessel = sentence_type.as_str() == "!VDO";
let mut fragment_count = 0;
let mut fragment_number = 0;
let mut message_id = None;
let mut radio_channel_code = None;
let mut payload_string: String = "".into();
for (num, s) in sentence.split(',').enumerate() {
match num {
1 => {
match s.parse::<u8>() {
Ok(i) => {
fragment_count = i;
}
Err(_) => {
return Err(ParseError::InvalidSentence(format!(
"Failed to parse fragment count: {}",
s
)));
}
};
}
2 => {
match s.parse::<u8>() {
Ok(i) => {
fragment_number = i;
}
Err(_) => {
return Err(ParseError::InvalidSentence(format!(
"Failed to parse fragment count: {}",
s
)));
}
};
}
3 => {
message_id = s.parse::<u64>().ok();
}
4 => {
radio_channel_code = Some(s);
}
5 => {
payload_string = s.to_string();
}
6 => {
}
_ => {}
}
}
let mut bv: Option<BitVec> = None;
match fragment_count {
1 => bv = parse_payload(&payload_string).ok(),
2 => {
if let Some(msg_id) = message_id {
let key1 = make_fragment_key(
&sentence_type.to_string(),
msg_id,
fragment_count,
1,
radio_channel_code.unwrap_or(""),
);
let key2 = make_fragment_key(
&sentence_type.to_string(),
msg_id,
fragment_count,
2,
radio_channel_code.unwrap_or(""),
);
match fragment_number {
1 => {
if let Some(p) = self.pull_string(key2) {
let mut payload_string_combined = payload_string;
payload_string_combined.push_str(p.as_str());
bv = parse_payload(&payload_string_combined).ok();
} else {
self.push_string(key1, payload_string);
}
}
2 => {
if let Some(p) = self.pull_string(key1) {
let mut payload_string_combined = p;
payload_string_combined.push_str(payload_string.as_str());
bv = parse_payload(&payload_string_combined).ok();
} else {
self.push_string(key2, payload_string);
}
}
_ => {
warn!(
"Unexpected NMEA fragment number: {}/{}",
fragment_number, fragment_count
);
}
}
} else {
warn!(
"NMEA message_id missing from {} than supported 2",
sentence_type
);
}
}
_ => {
warn!(
"NMEA sentence fragment count greater ({}) than supported 2",
fragment_count
);
}
}
if let Some(bv) = bv {
let message_type = pick_u64(&bv, 0, 6);
match message_type {
1..=3 => ais::vdm_t1t2t3::handle(&bv, station, own_vessel),
4 => ais::vdm_t4::handle(&bv, station, own_vessel),
5 => ais::vdm_t5::handle(&bv, station, own_vessel),
6 => ais::vdm_t6::handle(&bv, station, own_vessel),
7 => {
Err(ParseError::UnsupportedSentenceType(format!(
"Unsupported {} message type: {}",
sentence_type, message_type
)))
}
8 => {
Err(ParseError::UnsupportedSentenceType(format!(
"Unsupported {} message type: {}",
sentence_type, message_type
)))
}
9 => ais::vdm_t9::handle(&bv, station, own_vessel),
10 => ais::vdm_t10::handle(&bv, station, own_vessel),
11 => ais::vdm_t11::handle(&bv, station, own_vessel),
12 => ais::vdm_t12::handle(&bv, station, own_vessel),
13 => ais::vdm_t13::handle(&bv, station, own_vessel),
14 => ais::vdm_t14::handle(&bv, station, own_vessel),
15 => ais::vdm_t15::handle(&bv, station, own_vessel),
16 => ais::vdm_t16::handle(&bv, station, own_vessel),
17 => ais::vdm_t17::handle(&bv, station, own_vessel),
18 => ais::vdm_t18::handle(&bv, station, own_vessel),
19 => ais::vdm_t19::handle(&bv, station, own_vessel),
20 => ais::vdm_t20::handle(&bv, station, own_vessel),
21 => ais::vdm_t21::handle(&bv, station, own_vessel),
22 => ais::vdm_t22::handle(&bv, station, own_vessel),
23 => ais::vdm_t23::handle(&bv, station, own_vessel),
24 => ais::vdm_t24::handle(&bv, station, self, own_vessel),
25 => ais::vdm_t25::handle(&bv, station, own_vessel),
26 => ais::vdm_t26::handle(&bv, station, own_vessel),
27 => ais::vdm_t27::handle(&bv, station, own_vessel),
_ => Err(ParseError::UnsupportedSentenceType(format!(
"Unsupported {} message type: {}",
sentence_type, message_type
))),
}
} else {
Ok(ParsedMessage::Incomplete)
}
}
"$DPT" => gnss::dpt::handle(sentence.as_str()),
"$DBS" => gnss::dbs::handle(sentence.as_str()),
"$MTW" => gnss::mtw::handle(sentence.as_str()),
"$VHW" => gnss::vhw::handle(sentence.as_str()),
"$HDT" => gnss::hdt::handle(sentence.as_str()),
"$MWV" => gnss::mwv::handle(sentence.as_str()),
_ => Err(ParseError::UnsupportedSentenceType(format!(
"Unsupported sentence type: {}",
sentence_type
))),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse_invalid_sentence() {
let mut p = NmeaParser::new();
assert_eq!(
p.parse_sentence("$Þ´GAGSV,,"),
Err(ParseError::InvalidSentence(
"Invalid characters in sentence type: $\u{7b4}GAGSV".to_string()
))
);
assert_eq!(
p.parse_sentence("$WIMWV,295.4,T,"),
Err(ParseError::CorruptedSentence(
"pick string for \"wind_speed_knots\" was None".to_string()
))
);
assert_eq!(
p.parse_sentence("!AIVDM,not,a,valid,nmea,string,0*00"),
Err(ParseError::CorruptedSentence(
"Corrupted NMEA sentence: \"17\" != \"00\"".to_string()
))
);
assert_eq!(
p.parse_sentence("!"),
Err(ParseError::InvalidSentence(
"Invalid NMEA sentence: !".to_string()
))
);
}
#[test]
fn test_parse_prefix_chars() {
let mut p = NmeaParser::new();
assert!(p
.parse_sentence(",1277,-106*35\r\n!AIVDM,1,1,,A,152IS=iP?w<tSF0l4Q@>4?wp0H:;,0*2")
.ok()
.is_some());
}
#[test]
fn test_parse_corrupted() {
let mut p = NmeaParser::new();
assert!(p
.parse_sentence("!AIVDM,1,1,,A,38Id705000rRVJhE7cl9n;160000,0*41")
.ok()
.is_none());
}
#[test]
fn test_parse_missing_checksum() {
let mut p = NmeaParser::new();
assert!(p
.parse_sentence("!AIVDM,1,1,,A,38Id705000rRVJhE7cl9n;160000,0")
.ok()
.is_some());
}
#[test]
fn test_parse_invalid_utc() {
let mut p = NmeaParser::new();
assert_eq!(
p.parse_sentence("!AIVDM,1,1,,B,4028iqT47wP00wGiNbH8H0700`2H,0*13"),
Err(ParseError::InvalidSentence(String::from(
"Failed to parse Utc Date from y:4161 m:15 d:31 h:0 m:0 s:0"
)))
);
}
#[test]
fn test_parse_proprietary() {
}
#[test]
fn test_parse_invalid_talker() {
let mut p = NmeaParser::new();
assert_eq!(
p.parse_sentence("$QQ,*2C"),
Err(ParseError::UnsupportedSentenceType(String::from(
"Unsupported sentence type: $QQ"
)))
);
assert_eq!(
p.parse_sentence("$A,a0,*10"),
Err(ParseError::InvalidSentence(String::from(
"Invalid talker identifier"
)))
);
assert_eq!(
p.parse_sentence("$,0a,*51"),
Err(ParseError::InvalidSentence(String::from(
"Invalid talker identifier"
)))
);
}
#[test]
fn test_nmea_parser() {
let mut p = NmeaParser::new();
p.push_string("a".into(), "b".into());
assert_eq!(p.strings_count(), 1);
p.push_string("c".into(), "d".into());
assert_eq!(p.strings_count(), 2);
p.pull_string("a".into());
assert_eq!(p.strings_count(), 1);
p.pull_string("c".into());
assert_eq!(p.strings_count(), 0);
p.push_vsd(1, Default::default());
assert_eq!(p.vsds_count(), 1);
p.push_vsd(2, Default::default());
assert_eq!(p.vsds_count(), 2);
p.pull_vsd(1);
assert_eq!(p.vsds_count(), 1);
p.pull_vsd(2);
assert_eq!(p.vsds_count(), 0);
}
#[test]
fn test_country() {
assert_eq!(vsd(230992580).country().unwrap(), "FI");
assert_eq!(vsd(276009860).country().unwrap(), "EE");
assert_eq!(vsd(265803690).country().unwrap(), "SE");
assert_eq!(vsd(273353180).country().unwrap(), "RU");
assert_eq!(vsd(211805060).country().unwrap(), "DE");
assert_eq!(vsd(257037270).country().unwrap(), "NO");
assert_eq!(vsd(227232370).country().unwrap(), "FR");
assert_eq!(vsd(248221000).country().unwrap(), "MT");
assert_eq!(vsd(374190000).country().unwrap(), "PA");
assert_eq!(vsd(412511368).country().unwrap(), "CN");
assert_eq!(vsd(512003200).country().unwrap(), "NZ");
assert_eq!(vsd(995126020).country(), None);
assert_eq!(vsd(2300049).country(), None);
assert_eq!(vsd(0).country(), None);
}
fn vsd(mmsi: u32) -> ais::VesselStaticData {
let mut vsd = ais::VesselStaticData::default();
vsd.mmsi = mmsi;
vsd
}
}