nmea_kit/ais/messages/
long_range.rs1use crate::ais::armor::{extract_i32, extract_u32};
8
9use super::common::NavigationStatus;
10
11#[derive(Debug, Clone, PartialEq)]
26pub struct LongRangePosition {
27 pub mmsi: u32,
28 pub position_accuracy: bool,
30 pub raim: bool,
32 pub nav_status: Option<NavigationStatus>,
34 pub longitude: Option<f64>,
36 pub latitude: Option<f64>,
38 pub sog: Option<u8>,
40 pub cog: Option<u16>,
42 pub gnss_position_status: bool,
44}
45
46impl LongRangePosition {
47 pub(crate) fn decode(bits: &[u8]) -> Option<Self> {
49 if bits.len() < 96 {
50 return None;
51 }
52
53 let mmsi = extract_u32(bits, 8, 30)?;
54 let accuracy = extract_u32(bits, 38, 1)? == 1;
55 let raim = extract_u32(bits, 39, 1)? == 1;
56 let nav_raw = extract_u32(bits, 40, 4)? as u8;
57 let lon_raw = extract_i32(bits, 44, 18)?;
58 let lat_raw = extract_i32(bits, 62, 17)?;
59 let sog_raw = extract_u32(bits, 79, 6)? as u8;
60 let cog_raw = extract_u32(bits, 85, 9)? as u16;
61 let gnss = extract_u32(bits, 94, 1)? == 1;
62
63 let longitude = {
65 let deg = f64::from(lon_raw) / 10.0;
66 if !(-180.0..=180.0).contains(°) {
67 None
68 } else {
69 Some(deg)
70 }
71 };
72 let latitude = {
73 let deg = f64::from(lat_raw) / 10.0;
74 if !(-90.0..=90.0).contains(°) {
75 None
76 } else {
77 Some(deg)
78 }
79 };
80
81 Some(Self {
82 mmsi,
83 position_accuracy: accuracy,
84 raim,
85 nav_status: Some(NavigationStatus::from(nav_raw)),
86 longitude,
87 latitude,
88 sog: if sog_raw == 63 { None } else { Some(sog_raw) },
89 cog: if cog_raw == 511 { None } else { Some(cog_raw) },
90 gnss_position_status: gnss,
91 })
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use crate::ais::{AisMessage, AisParser};
98 use crate::parse_frame;
99
100 #[test]
101 fn long_range_gpsd() {
102 let mut parser = AisParser::new();
103 let frame = parse_frame("!AIVDM,1,1,,A,KCQ9r=hrFUnH7P00,0*41").expect("valid");
105 let msg = parser.decode(&frame).expect("decoded");
106 if let AisMessage::LongRangePosition(pos) = msg {
107 assert!(pos.mmsi > 0, "MMSI must be set");
108 if let (Some(lat), Some(lon)) = (pos.latitude, pos.longitude) {
109 assert!((-90.0..=90.0).contains(&lat), "lat out of range: {lat}");
110 assert!((-180.0..=180.0).contains(&lon), "lon out of range: {lon}");
111 }
112 } else {
113 panic!("expected LongRangePosition, got {msg:?}");
114 }
115 }
116
117 #[test]
118 fn long_range_position_sentinel() {
119 let mut parser = AisParser::new();
120 let frame = parse_frame("!AIVDM,1,1,,A,KCQ9r=hrFUnH7P00,0*41").expect("valid");
121 let msg = parser.decode(&frame).expect("decoded");
122 if let AisMessage::LongRangePosition(pos) = msg {
123 if let Some(lat) = pos.latitude {
124 assert!(
125 (-90.0..=90.0).contains(&lat),
126 "lat sentinel not filtered: {lat}"
127 );
128 }
129 if let Some(lon) = pos.longitude {
130 assert!(
131 (-180.0..=180.0).contains(&lon),
132 "lon sentinel not filtered: {lon}"
133 );
134 }
135 } else {
136 panic!("expected LongRangePosition, got {msg:?}");
137 }
138 }
139}