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