1use crate::callsign::Callsign;
2use crate::capabilities::AprsCapabilities;
3use crate::digipeater::{Digipeater, parse_via};
4use crate::error::AprsError;
5use crate::grid::AprsGridLocator;
6use crate::item::AprsItem;
7use crate::message::AprsMessage;
8use crate::mic_e::AprsMicE;
9use crate::nmea::AprsNmea;
10use crate::object::AprsObject;
11use crate::position::AprsPosition;
12use crate::query::AprsQuery;
13use crate::status::AprsStatus;
14use crate::telemetry::AprsTelemetry;
15use crate::third_party::AprsThirdParty;
16use crate::user_defined::AprsUserDefined;
17use crate::weather::AprsPositionlessWeather;
18
19#[derive(Debug, Clone)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct AprsPacket {
23 pub from: Callsign,
25 pub to: Callsign,
27 pub via: Vec<Digipeater>,
29 pub data: AprsData,
31}
32
33#[derive(Debug, Clone)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38#[non_exhaustive]
39pub enum AprsData {
40 Position(AprsPosition),
42 Message(AprsMessage),
44 Status(AprsStatus),
46 MicE(AprsMicE),
48 Object(AprsObject),
50 Item(AprsItem),
52 Weather(AprsPositionlessWeather),
54 Telemetry(AprsTelemetry),
56 Capabilities(AprsCapabilities),
58 Query(AprsQuery),
60 GridLocator(AprsGridLocator),
62 Nmea(AprsNmea),
64 ThirdParty(AprsThirdParty),
66 UserDefined(AprsUserDefined),
68
69 Unknown {
72 dti: u8,
73 data: Vec<u8>,
74 },
75}
76
77impl AprsPacket {
78 pub fn decode_textual(input: &[u8]) -> Result<Self, AprsError> {
85 if input.is_empty() {
86 return Err(AprsError::EmptyPacket);
87 }
88
89 let colon = input.iter().position(|&b| b == b':')
90 .ok_or(AprsError::MissingInfoDelimiter)?;
91
92 let header = &input[..colon];
93 let info = &input[colon + 1..];
94
95 let arrow = header.iter().position(|&b| b == b'>')
96 .ok_or(AprsError::MissingDestinationDelimiter)?;
97
98 let from_bytes = &header[..arrow];
99 let dest_via = &header[arrow + 1..];
100
101 let (to_bytes, via_bytes) = if let Some(comma) = dest_via.iter().position(|&b| b == b',') {
102 (&dest_via[..comma], &dest_via[comma + 1..])
103 } else {
104 (dest_via, &b""[..])
105 };
106
107 let from = Callsign::decode_textual(from_bytes)?;
108 let to = Callsign::decode_textual(to_bytes)?;
109 let via = parse_via(via_bytes)?;
110 let data = dispatch_data(info, &to)?;
111
112 Ok(AprsPacket { from, to, via, data })
113 }
114
115 pub fn decode_ax25(input: &[u8]) -> Result<Self, AprsError> {
121 if input.len() < 16 {
122 return Err(AprsError::Ax25FrameTooShort { len: input.len() });
123 }
124
125 let (to, _) = Callsign::decode_ax25(&input[0..7])?;
126 let (from, src_eoa) = Callsign::decode_ax25(&input[7..14])?;
127
128 let mut pos = 14usize;
129 let mut via = Vec::new();
130
131 if !src_eoa {
132 loop {
133 if pos + 7 > input.len() {
134 return Err(AprsError::Ax25MissingEoa);
135 }
136 let (digi_call, eoa) = Callsign::decode_ax25(&input[pos..pos + 7])?;
137 let heard = input[pos + 6] & 0x80 != 0;
138 via.push(Digipeater::Callsign(digi_call, heard));
139 pos += 7;
140 if eoa { break; }
141 if pos >= input.len() {
142 return Err(AprsError::Ax25MissingEoa);
143 }
144 }
145 }
146
147 if pos >= input.len() {
148 return Err(AprsError::TruncatedPacket { expected: pos + 2, got: input.len() });
149 }
150 if input[pos] != 0x03 {
151 return Err(AprsError::Ax25NotUiFrame { byte: input[pos] });
152 }
153 pos += 1;
154
155 if pos >= input.len() {
156 return Err(AprsError::TruncatedPacket { expected: pos + 1, got: input.len() });
157 }
158 if input[pos] != 0xF0 {
159 return Err(AprsError::Ax25NotAprsPid { byte: input[pos] });
160 }
161 pos += 1;
162
163 let info = &input[pos..];
164 let data = dispatch_data(info, &to)?;
165
166 Ok(AprsPacket { from, to, via, data })
167 }
168
169 pub fn encode_textual(&self) -> Result<Vec<u8>, AprsError> {
171 let mut out = Vec::new();
172 self.from.encode_textual(&mut out);
173 out.push(b'>');
174 self.to.encode_textual(&mut out);
175 for digi in &self.via {
176 out.push(b',');
177 digi.encode_textual(&mut out);
178 }
179 out.push(b':');
180 self.encode_info(&mut out)?;
181 Ok(out)
182 }
183
184 pub fn encode_ax25(&self) -> Result<Vec<u8>, AprsError> {
186 let mut out = Vec::new();
187 self.to.encode_ax25(&mut out, false);
189 let src_eoa = self.via.is_empty();
191 self.from.encode_ax25(&mut out, src_eoa);
192 for (i, digi) in self.via.iter().enumerate() {
194 let is_last = i + 1 == self.via.len();
195 match digi {
196 Digipeater::Callsign(call, _heard) => {
197 call.encode_ax25(&mut out, is_last);
198 }
199 Digipeater::QConstruct(_, gw) => {
200 gw.encode_ax25(&mut out, is_last);
201 }
202 }
203 }
204 out.push(0x03); out.push(0xF0); self.encode_info(&mut out)?;
207 Ok(out)
208 }
209
210 fn encode_info(&self, out: &mut Vec<u8>) -> Result<(), AprsError> {
211 match &self.data {
212 AprsData::Position(pos) => {
213 out.extend_from_slice(&pos.encode());
214 }
215 AprsData::Message(msg) => {
216 out.extend_from_slice(&msg.encode());
217 }
218 AprsData::Status(s) => {
219 out.extend_from_slice(&s.encode());
220 }
221 AprsData::MicE(m) => {
222 out.extend_from_slice(&m.encode());
223 }
224 AprsData::Object(o) => {
225 out.extend_from_slice(&o.encode());
226 }
227 AprsData::Item(i) => {
228 out.extend_from_slice(&i.encode());
229 }
230 AprsData::Weather(w) => {
231 out.extend_from_slice(&w.encode());
232 }
233 AprsData::Telemetry(t) => {
234 out.extend_from_slice(&t.encode());
235 }
236 AprsData::Capabilities(c) => {
237 out.extend_from_slice(&c.encode());
238 }
239 AprsData::Query(q) => {
240 out.extend_from_slice(&q.encode());
241 }
242 AprsData::GridLocator(g) => {
243 out.extend_from_slice(&g.encode());
244 }
245 AprsData::Nmea(n) => {
246 out.extend_from_slice(&n.encode());
247 }
248 AprsData::ThirdParty(tp) => {
249 out.extend_from_slice(&tp.encode()?);
250 }
251 AprsData::UserDefined(ud) => {
252 out.extend_from_slice(&ud.encode());
253 }
254 AprsData::Unknown { dti: _, data } => {
255 out.extend_from_slice(data);
256 }
257 }
258 Ok(())
259 }
260}
261
262fn dispatch_data(info: &[u8], to: &Callsign) -> Result<AprsData, AprsError> {
265 let dti = match info.first() {
266 Some(&b) => b,
267 None => return Ok(AprsData::Unknown { dti: 0, data: Vec::new() }),
268 };
269
270 match dti {
271 b'!' | b'=' | b'/' | b'@' => AprsPosition::parse(info).map(AprsData::Position),
272 b':' => AprsMessage::parse(info).map(AprsData::Message),
273 b'>' => AprsStatus::parse(info).map(AprsData::Status),
274 b'`' | b'\'' | 0x1C | 0x1D => AprsMicE::parse(info, to).map(AprsData::MicE),
275 b';' => AprsObject::parse(info).map(AprsData::Object),
276 b')' => AprsItem::parse(info).map(AprsData::Item),
277 b'_' => AprsPositionlessWeather::parse(info).map(AprsData::Weather),
278 b'T' => AprsTelemetry::parse(info).map(AprsData::Telemetry),
279 b'<' => Ok(AprsData::Capabilities(AprsCapabilities::parse(info))),
280 b'?' => Ok(AprsData::Query(AprsQuery::parse(info))),
281 b'[' => AprsGridLocator::parse(info).map(AprsData::GridLocator),
282 b'$' => Ok(AprsData::Nmea(AprsNmea::parse(info))),
283 b'}' => AprsThirdParty::parse(info).map(AprsData::ThirdParty),
284 b'{' => Ok(AprsData::UserDefined(AprsUserDefined::parse(info))),
285 _ => Ok(AprsData::Unknown { dti, data: info.to_vec() }),
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 const POSITION_PACKET: &[u8] =
294 b"W1AW-9>APRS,WIDE1-1,WIDE2-2:!4903.50N/07201.75W-Test";
295
296 const MSG_PACKET: &[u8] =
297 b"KD9ABC>APDR15,qAR,KD9XYZ::W1AW-9 :Hello world{001";
298
299 #[test]
300 fn decode_position_full() {
301 let pkt = AprsPacket::decode_textual(POSITION_PACKET).unwrap();
302 assert_eq!(pkt.from.to_string(), "W1AW-9");
303 assert_eq!(pkt.to.to_string(), "APRS");
304 assert_eq!(pkt.via.len(), 2);
305 assert!(matches!(pkt.data, AprsData::Position(_)));
306 }
307
308 #[test]
309 fn decode_message_header() {
310 let pkt = AprsPacket::decode_textual(MSG_PACKET).unwrap();
311 assert_eq!(pkt.from.to_string(), "KD9ABC");
312 assert_eq!(pkt.to.to_string(), "APDR15");
313 assert_eq!(pkt.via.len(), 1);
314 assert!(matches!(pkt.data, AprsData::Message(_)));
315 }
316
317 #[test]
318 fn empty_input_error() {
319 assert!(AprsPacket::decode_textual(b"").is_err());
320 }
321
322 #[test]
323 fn missing_arrow_error() {
324 assert!(AprsPacket::decode_textual(b"W1AW:!hello").is_err());
325 }
326
327 #[test]
328 fn missing_colon_error() {
329 assert!(AprsPacket::decode_textual(b"W1AW>APRS,WIDE1").is_err());
330 }
331
332 #[test]
333 fn no_via_path() {
334 let pkt = AprsPacket::decode_textual(b"W1AW>APRS:>Status text").unwrap();
335 assert!(pkt.via.is_empty());
336 }
337
338 #[test]
339 fn unknown_dti_preserved() {
340 let pkt = AprsPacket::decode_textual(b"W1AW>APRS:~custom data").unwrap();
341 #[allow(unreachable_patterns)]
342 match &pkt.data {
343 AprsData::Unknown { dti, data } => {
344 assert_eq!(*dti, b'~');
345 assert_eq!(data.as_slice(), b"~custom data");
346 }
347 _ => panic!("expected Unknown"),
348 }
349 }
350
351 #[test]
352 fn encode_textual_round_trip() {
353 let pkt = AprsPacket::decode_textual(POSITION_PACKET).unwrap();
354 let encoded = pkt.encode_textual().unwrap();
355 assert_eq!(encoded, POSITION_PACKET);
356 }
357
358 #[test]
359 fn encode_ax25_round_trip() {
360 let pkt = AprsPacket::decode_textual(POSITION_PACKET).unwrap();
361 let ax25 = pkt.encode_ax25().unwrap();
362 let decoded = AprsPacket::decode_ax25(&ax25).unwrap();
363 assert_eq!(decoded.from.to_string(), "W1AW-9");
364 assert_eq!(decoded.to.to_string(), "APRS");
365 }
366
367 #[test]
371 fn null_position_at_signs() {
372 let raw = b"N0AMY-3>BEACON,W0OOD-2*,WIDE2*,WIDE1-1:!/@@@@@@@@@@ WWW.TONYTYLER.COM WHERE IS SHAMERFACE SHE IS WITH DOMO";
373 let pkt = AprsPacket::decode_textual(raw).unwrap();
374 assert_eq!(pkt.from.to_string(), "N0AMY-3");
375 assert!(matches!(pkt.data, AprsData::Position(_)));
376 if let AprsData::Position(ref pos) = pkt.data {
377 assert!(!pos.messaging_supported);
378 assert!(pos.timestamp.is_none());
379 assert!(pos.comment.starts_with(b" WWW.TONYTYLER.COM"));
380 }
381 }
382}