1use crate::error::AprsError;
2use crate::types::{Extension, Position, Timestamp};
3use crate::weather::AprsWeatherData;
4
5#[derive(Debug, Clone, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct AprsPosition {
15 pub timestamp: Option<Timestamp>,
16 pub messaging_supported: bool,
17 pub position: Position,
18 pub extension: Option<Extension>,
20 pub weather: Option<AprsWeatherData>,
22 pub frequency_mhz: Option<f32>,
25 pub comment: Vec<u8>,
26}
27
28impl AprsPosition {
29 pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
31 let dti = *info.first().ok_or(AprsError::EmptyPacket)?;
32 let messaging_supported = dti == b'=' || dti == b'@';
33 let has_timestamp = dti == b'@' || dti == b'/';
34
35 let (b, timestamp) = if has_timestamp {
36 let ts_bytes = info.get(1..8).ok_or(AprsError::TruncatedPacket {
37 expected: 8,
38 got: info.len(),
39 })?;
40 (
41 info.get(8..).unwrap_or_default(),
42 Some(Timestamp::parse(ts_bytes)?),
43 )
44 } else {
45 (info.get(1..).unwrap_or_default(), None)
46 };
47
48 let (remaining, position) = Position::parse(b)?;
49 let comment = remaining.unwrap_or_default().to_vec();
50
51 let extension = Extension::parse(&comment);
53
54 let weather = if position.symbol.table == '/' && position.symbol.code == '_' {
58 crate::weather::AprsWeatherData::parse(&comment).ok()
59 } else {
60 None
61 };
62
63 let frequency_mhz = crate::util::extract_frequency_mhz(&comment);
65
66 Ok(Self {
67 timestamp,
68 messaging_supported,
69 position,
70 extension,
71 weather,
72 frequency_mhz,
73 comment,
74 })
75 }
76
77 pub fn encode(&self) -> Vec<u8> {
79 let mut out = Vec::new();
80 let dti = match (self.timestamp.is_some(), self.messaging_supported) {
81 (true, true) => b'@',
82 (true, false) => b'/',
83 (false, true) => b'=',
84 (false, false) => b'!',
85 };
86 out.push(dti);
87
88 if let Some(ref ts) = self.timestamp {
89 ts.encode(&mut out);
90 }
91
92 if self.position.compressed_cs.is_some() {
93 self.position.encode_compressed(&mut out);
94 } else {
95 self.position.encode_uncompressed(&mut out);
96 }
97
98 out.extend_from_slice(&self.comment);
99 out
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use approx::assert_relative_eq;
107
108 #[test]
109 fn no_timestamp_no_messaging() {
110 let pos = AprsPosition::parse(b"!4903.50N/07201.75W-").unwrap();
111 assert!(pos.timestamp.is_none());
112 assert!(!pos.messaging_supported);
113 assert_relative_eq!(
114 pos.position.latitude.value(),
115 49.05833333333333,
116 epsilon = 1e-9
117 );
118 assert_relative_eq!(
119 pos.position.longitude.value(),
120 -72.02916666666667,
121 epsilon = 1e-9
122 );
123 assert_eq!(pos.comment, b"");
124 }
125
126 #[test]
127 fn no_timestamp_with_messaging() {
128 let pos = AprsPosition::parse(b"=4903.50N/07201.75W-").unwrap();
129 assert!(pos.timestamp.is_none());
130 assert!(pos.messaging_supported);
131 }
132
133 #[test]
134 fn with_timestamp_no_messaging() {
135 let pos = AprsPosition::parse(b"/074849h4821.61N\\01224.49E^322/103/A=003054").unwrap();
136 assert_eq!(pos.timestamp.unwrap(), Timestamp::Hhmmss(7, 48, 49));
137 assert!(!pos.messaging_supported);
138 assert_relative_eq!(
139 pos.position.latitude.value(),
140 48.36016666666667,
141 epsilon = 1e-9
142 );
143 assert_relative_eq!(
144 pos.position.longitude.value(),
145 12.408166666666666,
146 epsilon = 1e-9
147 );
148 assert_eq!(pos.position.symbol.table, '\\');
149 assert_eq!(pos.position.symbol.code, '^');
150 assert_eq!(pos.comment, b"322/103/A=003054");
151 }
152
153 #[test]
154 fn with_timestamp_and_messaging() {
155 let pos = AprsPosition::parse(b"@074849h4821.61N\\01224.49E^322/103/A=003054").unwrap();
156 assert!(pos.timestamp.is_some());
157 assert!(pos.messaging_supported);
158 }
159
160 #[test]
161 fn with_comment_and_altitude() {
162 let pos = AprsPosition::parse(b"!4903.50N/07201.75W-Hello/A=001000").unwrap();
163 assert_eq!(pos.comment, b"Hello/A=001000");
164 assert!(pos.position.altitude.is_some());
165 }
166
167 #[test]
168 fn extension_course_speed_parsed() {
169 let pos = AprsPosition::parse(b"/074849h4821.61N\\01224.49E^322/103/A=003054").unwrap();
170 assert!(pos.extension.is_some());
171 assert!(matches!(
172 pos.extension.unwrap(),
173 Extension::DirectionSpeed {
174 direction_degrees: 322,
175 speed_knots: 103
176 }
177 ));
178 }
179
180 #[test]
181 fn compressed_no_timestamp() {
182 let pos = AprsPosition::parse(b"!/ABCD#$%^- sT").unwrap();
183 assert!(pos.timestamp.is_none());
184 assert_relative_eq!(
185 pos.position.latitude.value(),
186 25.97004667573229,
187 epsilon = 0.001
188 );
189 assert_relative_eq!(
190 pos.position.longitude.value(),
191 -171.95429033460567,
192 epsilon = 0.001
193 );
194 }
195
196 #[test]
197 fn encode_round_trip_uncompressed() {
198 let raw = b"!4903.50N/07201.75W-";
199 let pos = AprsPosition::parse(raw).unwrap();
200 let encoded = pos.encode();
201 assert_eq!(&encoded, raw);
202 }
203
204 #[test]
205 fn encode_round_trip_with_timestamp() {
206 let raw = b"/074849h4821.61N\\01224.49E^322/103/A=003054";
207 let pos = AprsPosition::parse(raw).unwrap();
208 let encoded = pos.encode();
209 assert_eq!(encoded, raw);
210 }
211
212 #[test]
213 fn encode_round_trip_compressed() {
214 let raw = b"!/ABCD#$%^- sT";
215 let pos = AprsPosition::parse(raw).unwrap();
216 let encoded = pos.encode();
217 assert_eq!(&encoded, raw);
218 }
219
220 #[test]
221 fn timestamp_validates_strictly() {
222 let err = AprsPosition::parse(b"/092460z4903.50N/07201.75W-");
224 assert!(err.is_err());
225 }
226}