1use crate::error::AprsError;
2use crate::types::{Extension, Position, Timestamp};
3use crate::util::trim_spaces_end;
4
5#[derive(Debug, Clone, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct AprsObject {
15 pub name: Vec<u8>,
17 pub live: bool,
19 pub timestamp: Timestamp,
21 pub position: Position,
22 pub extension: Option<Extension>,
24 pub frequency_mhz: Option<f32>,
26 pub comment: Vec<u8>,
27}
28
29impl AprsObject {
30 pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
32 if info.len() < 18 {
39 return Err(AprsError::InvalidObject {
40 detail: "packet too short",
41 });
42 }
43
44 let mut name = info[1..10].to_vec();
45 trim_spaces_end(&mut name);
46
47 let live = match info[10] {
48 b'*' => true,
49 b'_' | b' ' => false, _ => {
51 return Err(AprsError::InvalidObject {
52 detail: "invalid liveness byte",
53 });
54 }
55 };
56
57 let timestamp = Timestamp::parse(&info[11..18])?;
58
59 let position_bytes = info.get(18..).ok_or(AprsError::InvalidObject {
60 detail: "truncated after timestamp",
61 })?;
62
63 let (remaining, position) = Position::parse(position_bytes)?;
64 let comment_raw = remaining.unwrap_or_default();
65
66 let (extension, comment) = if position.compressed_cs.is_none() {
67 if let Some(ext) = Extension::parse(comment_raw) {
69 (Some(ext), comment_raw.get(7..).unwrap_or_default().to_vec())
70 } else {
71 (None, comment_raw.to_vec())
72 }
73 } else {
74 (None, comment_raw.to_vec())
75 };
76
77 let frequency_mhz = crate::util::extract_frequency_mhz(&comment);
78 Ok(Self {
79 name,
80 live,
81 timestamp,
82 position,
83 extension,
84 frequency_mhz,
85 comment,
86 })
87 }
88
89 pub fn encode(&self) -> Vec<u8> {
90 let mut out = vec![b';'];
91 out.extend_from_slice(&self.name);
92 out.extend(std::iter::repeat_n(
93 b' ',
94 9usize.saturating_sub(self.name.len()),
95 ));
96 out.push(if self.live { b'*' } else { b'_' });
97 self.timestamp.encode(&mut out);
98
99 if self.extension.is_some() || self.position.compressed_cs.is_none() {
100 self.position.encode_uncompressed(&mut out);
101 if let Some(ref ext) = self.extension {
102 ext.encode(&mut out);
103 }
104 } else {
105 self.position.encode_compressed(&mut out);
106 }
107
108 out.extend_from_slice(&self.comment);
109 out
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use approx::assert_relative_eq;
117
118 const LIVE_OBJ: &[u8] =
119 b";HFEST-18H*170403z3443.55N\\08635.47Wh146.940MHz T100 Huntsville Hamfest";
120
121 #[test]
122 fn parse_live_object() {
123 let o = AprsObject::parse(LIVE_OBJ).unwrap();
124 assert_eq!(o.name, b"HFEST-18H");
125 assert!(o.live);
126 assert_eq!(o.timestamp, Timestamp::Ddhhmm(17, 4, 3));
127 assert_relative_eq!(
128 o.position.latitude.value(),
129 34.725833333333334,
130 epsilon = 1e-9
131 );
132 assert_relative_eq!(
133 o.position.longitude.value(),
134 -86.59116666666667,
135 epsilon = 1e-9
136 );
137 assert_eq!(o.position.symbol.table, '\\');
138 assert_eq!(o.position.symbol.code, 'h');
139 assert_eq!(o.comment, b"146.940MHz T100 Huntsville Hamfest");
140 }
141
142 #[test]
143 fn parse_dead_object_space_liveness() {
144 let o = AprsObject::parse(b";HFEST 170403z3443.55N\\08635.47Wh").unwrap();
146 assert_eq!(o.name, b"HFEST");
147 assert!(!o.live);
148 }
149
150 #[test]
151 fn parse_dead_object_underscore_liveness() {
152 let o = AprsObject::parse(b";HFEST _170403z3443.55N\\08635.47Wh").unwrap();
153 assert_eq!(o.name, b"HFEST");
154 assert!(!o.live);
155 }
156
157 #[test]
158 fn parse_with_extension() {
159 let o = AprsObject::parse(b";HFEST _170403z3443.55N\\08635.47WhPHG5132Comment").unwrap();
160 assert!(o.extension.is_some());
161 }
162
163 #[test]
164 fn parse_compressed_object() {
165 let o = AprsObject::parse(b";CAR _092345z/5L!!<*e7>7P[Moving to the north").unwrap();
166 assert_eq!(o.name, b"CAR");
167 assert!(!o.live);
168 assert_relative_eq!(o.position.latitude.value(), 49.5, epsilon = 0.01);
169 assert_eq!(o.comment, b"Moving to the north");
170 }
171
172 #[test]
173 fn encode_round_trip_live() {
174 let o = AprsObject::parse(LIVE_OBJ).unwrap();
176 assert_eq!(o.encode(), LIVE_OBJ);
177 }
178
179 #[test]
180 fn timestamp_validates_strictly() {
181 assert!(AprsObject::parse(b";HFEST-18H*002345z3443.55N\\08635.47Wh").is_err());
183 }
184}