1use crate::error::AprsError;
2use crate::types::{Extension, Position};
3
4#[derive(Debug, Clone, PartialEq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct AprsItem {
14 pub name: Vec<u8>,
16 pub live: bool,
18 pub position: Position,
19 pub extension: Option<Extension>,
21 pub comment: Vec<u8>,
22}
23
24impl AprsItem {
25 pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
27 if info.len() < 5 {
31 return Err(AprsError::InvalidItem {
32 detail: "packet too short",
33 });
34 }
35
36 let mut name = Vec::with_capacity(9);
38 let mut liveness_idx = None;
39
40 for (i, &b) in info.iter().enumerate().skip(1).take(9) {
41 if b == b'!' || b == b' ' {
42 liveness_idx = Some(i);
43 break;
44 }
45 name.push(b);
46 }
47
48 if name.len() < 3 {
49 return Err(AprsError::InvalidItem {
50 detail: "name too short (< 3 chars)",
51 });
52 }
53
54 let liveness_idx = liveness_idx.ok_or(AprsError::InvalidItem {
55 detail: "liveness byte not found",
56 })?;
57
58 let live = match info[liveness_idx] {
59 b'!' => true,
60 b' ' | b'_' => false,
61 _ => {
62 return Err(AprsError::InvalidItem {
63 detail: "invalid liveness byte",
64 });
65 }
66 };
67
68 let position_bytes = info.get(liveness_idx + 1..).ok_or(AprsError::InvalidItem {
69 detail: "truncated after liveness",
70 })?;
71
72 let (remaining, position) = Position::parse(position_bytes)?;
73 let comment_raw = remaining.unwrap_or_default();
74
75 let (extension, comment) = if position.compressed_cs.is_none() {
76 if let Some(ext) = Extension::parse(comment_raw) {
77 (Some(ext), comment_raw.get(7..).unwrap_or_default().to_vec())
78 } else {
79 (None, comment_raw.to_vec())
80 }
81 } else {
82 (None, comment_raw.to_vec())
83 };
84
85 Ok(Self {
86 name,
87 live,
88 position,
89 extension,
90 comment,
91 })
92 }
93
94 pub fn encode(&self) -> Vec<u8> {
95 let mut out = vec![b')'];
96 out.extend_from_slice(&self.name);
97 out.push(if self.live { b'!' } else { b' ' });
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 #[test]
119 fn parse_live_item() {
120 let item = AprsItem::parse(b")AIDV#2!4903.50N/07201.75WA").unwrap();
121 assert_eq!(item.name, b"AIDV#2");
122 assert!(item.live);
123 assert_relative_eq!(
124 item.position.latitude.value(),
125 49.05833333333333,
126 epsilon = 1e-9
127 );
128 assert_relative_eq!(
129 item.position.longitude.value(),
130 -72.02916666666667,
131 epsilon = 1e-9
132 );
133 assert_eq!(item.position.symbol.table, '/');
134 assert_eq!(item.position.symbol.code, 'A');
135 }
136
137 #[test]
138 fn parse_dead_item() {
139 let item = AprsItem::parse(b")AID 4903.50N/07201.75WA").unwrap();
140 assert_eq!(item.name, b"AID");
141 assert!(!item.live);
142 }
143
144 #[test]
145 fn parse_with_extension() {
146 let item = AprsItem::parse(b")AID 4903.50N/07201.75WAPHG5132").unwrap();
147 assert!(item.extension.is_some());
148 assert!(item.comment.is_empty());
149 }
150
151 #[test]
152 fn parse_compressed_item() {
153 let item = AprsItem::parse(b")MOBIL!\\5L!!<*e79 sT").unwrap();
154 assert_eq!(item.name, b"MOBIL");
155 assert!(item.live);
156 assert_relative_eq!(item.position.latitude.value(), 49.5, epsilon = 0.01);
157 }
158
159 #[test]
160 fn name_too_short() {
161 assert!(AprsItem::parse(b")AB!4903.50N/07201.75WA").is_err());
162 }
163
164 #[test]
165 fn encode_round_trip_live() {
166 let raw = b")AIDV#2!4903.50N/07201.75WA";
167 let item = AprsItem::parse(raw).unwrap();
168 assert_eq!(item.encode(), raw);
169 }
170
171 #[test]
172 fn encode_round_trip_dead() {
173 let raw = b")AID 4903.50N/07201.75WA";
174 let item = AprsItem::parse(raw).unwrap();
175 assert_eq!(item.encode(), raw);
176 }
177
178 #[test]
179 fn encode_round_trip_with_extension() {
180 let raw = b")AID 4903.50N/07201.75WAPHG5132";
181 let item = AprsItem::parse(raw).unwrap();
182 assert_eq!(item.encode(), raw);
183 }
184}