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, PartialEq)]
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, PartialEq)]
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 { dti: u8, data: Vec<u8> },
72}
73
74impl AprsPacket {
75 pub fn decode_textual(input: &[u8]) -> Result<Self, AprsError> {
82 if input.is_empty() {
83 return Err(AprsError::EmptyPacket);
84 }
85
86 let colon = input
87 .iter()
88 .position(|&b| b == b':')
89 .ok_or(AprsError::MissingInfoDelimiter)?;
90
91 let header = &input[..colon];
92 let info = &input[colon + 1..];
93
94 let arrow = header
95 .iter()
96 .position(|&b| b == b'>')
97 .ok_or(AprsError::MissingDestinationDelimiter)?;
98
99 let from_bytes = &header[..arrow];
100 let dest_via = &header[arrow + 1..];
101
102 let (to_bytes, via_bytes) = if let Some(comma) = dest_via.iter().position(|&b| b == b',') {
103 (&dest_via[..comma], &dest_via[comma + 1..])
104 } else {
105 (dest_via, &b""[..])
106 };
107
108 let from = Callsign::decode_textual(from_bytes)?;
109 let to = Callsign::decode_textual(to_bytes)?;
110 let via = parse_via(via_bytes)?;
111 let data = dispatch_data(info, &to)?;
112
113 Ok(AprsPacket {
114 from,
115 to,
116 via,
117 data,
118 })
119 }
120
121 pub fn decode_ax25(input: &[u8]) -> Result<Self, AprsError> {
127 if input.len() < 16 {
128 return Err(AprsError::Ax25FrameTooShort { len: input.len() });
129 }
130
131 let (to, _) = Callsign::decode_ax25(&input[0..7])?;
132 let (from, src_eoa) = Callsign::decode_ax25(&input[7..14])?;
133
134 let mut pos = 14usize;
135 let mut via = Vec::new();
136
137 if !src_eoa {
138 loop {
139 if pos + 7 > input.len() {
140 return Err(AprsError::Ax25MissingEoa);
141 }
142 let (digi_call, eoa) = Callsign::decode_ax25(&input[pos..pos + 7])?;
143 let heard = input[pos + 6] & 0x80 != 0;
144 via.push(Digipeater::Callsign(digi_call, heard));
145 pos += 7;
146 if eoa {
147 break;
148 }
149 if pos >= input.len() {
150 return Err(AprsError::Ax25MissingEoa);
151 }
152 }
153 }
154
155 if pos >= input.len() {
156 return Err(AprsError::TruncatedPacket {
157 expected: pos + 2,
158 got: input.len(),
159 });
160 }
161 if input[pos] != 0x03 {
162 return Err(AprsError::Ax25NotUiFrame { byte: input[pos] });
163 }
164 pos += 1;
165
166 if pos >= input.len() {
167 return Err(AprsError::TruncatedPacket {
168 expected: pos + 1,
169 got: input.len(),
170 });
171 }
172 if input[pos] != 0xF0 {
173 return Err(AprsError::Ax25NotAprsPid { byte: input[pos] });
174 }
175 pos += 1;
176
177 let info = &input[pos..];
178 let data = dispatch_data(info, &to)?;
179
180 Ok(AprsPacket {
181 from,
182 to,
183 via,
184 data,
185 })
186 }
187
188 pub fn encode_textual(&self) -> Result<Vec<u8>, AprsError> {
190 let mut out = Vec::new();
191 self.from.encode_textual(&mut out);
192 out.push(b'>');
193 self.to.encode_textual(&mut out);
194 for digi in &self.via {
195 out.push(b',');
196 digi.encode_textual(&mut out);
197 }
198 out.push(b':');
199 self.encode_info(&mut out)?;
200 Ok(out)
201 }
202
203 pub fn encode_ax25(&self) -> Result<Vec<u8>, AprsError> {
205 let mut out = Vec::new();
206 self.to.encode_ax25(&mut out, false);
208 let src_eoa = self.via.is_empty();
210 self.from.encode_ax25(&mut out, src_eoa);
211 for (i, digi) in self.via.iter().enumerate() {
213 let is_last = i + 1 == self.via.len();
214 match digi {
215 Digipeater::Callsign(call, heard) => {
216 call.encode_ax25(&mut out, is_last);
217 if *heard && let Some(last) = out.last_mut() {
220 *last |= 0x80;
221 }
222 }
223 Digipeater::QConstruct(_, gw) => {
224 gw.encode_ax25(&mut out, is_last);
225 }
226 }
227 }
228 out.push(0x03); out.push(0xF0); self.encode_info(&mut out)?;
231 Ok(out)
232 }
233
234 fn encode_info(&self, out: &mut Vec<u8>) -> Result<(), AprsError> {
235 match &self.data {
236 AprsData::Position(pos) => {
237 out.extend_from_slice(&pos.encode());
238 }
239 AprsData::Message(msg) => {
240 out.extend_from_slice(&msg.encode());
241 }
242 AprsData::Status(s) => {
243 out.extend_from_slice(&s.encode());
244 }
245 AprsData::MicE(m) => {
246 out.extend_from_slice(&m.encode());
247 }
248 AprsData::Object(o) => {
249 out.extend_from_slice(&o.encode());
250 }
251 AprsData::Item(i) => {
252 out.extend_from_slice(&i.encode());
253 }
254 AprsData::Weather(w) => {
255 out.extend_from_slice(&w.encode());
256 }
257 AprsData::Telemetry(t) => {
258 out.extend_from_slice(&t.encode());
259 }
260 AprsData::Capabilities(c) => {
261 out.extend_from_slice(&c.encode());
262 }
263 AprsData::Query(q) => {
264 out.extend_from_slice(&q.encode());
265 }
266 AprsData::GridLocator(g) => {
267 out.extend_from_slice(&g.encode());
268 }
269 AprsData::Nmea(n) => {
270 out.extend_from_slice(&n.encode());
271 }
272 AprsData::ThirdParty(tp) => {
273 out.extend_from_slice(&tp.encode()?);
274 }
275 AprsData::UserDefined(ud) => {
276 out.extend_from_slice(&ud.encode());
277 }
278 AprsData::Unknown { dti: _, data } => {
279 out.extend_from_slice(data);
280 }
281 }
282 Ok(())
283 }
284}
285
286fn dispatch_data(info: &[u8], to: &Callsign) -> Result<AprsData, AprsError> {
289 let dti = match info.first() {
290 Some(&b) => b,
291 None => {
292 return Ok(AprsData::Unknown {
293 dti: 0,
294 data: Vec::new(),
295 });
296 }
297 };
298
299 match dti {
300 b'!' | b'=' | b'/' | b'@' => AprsPosition::parse(info).map(AprsData::Position),
301 b':' => AprsMessage::parse(info).map(AprsData::Message),
302 b'>' => AprsStatus::parse(info).map(AprsData::Status),
303 b'`' | b'\'' | 0x1C | 0x1D => AprsMicE::parse(info, to).map(AprsData::MicE),
304 b';' => AprsObject::parse(info).map(AprsData::Object),
305 b')' => AprsItem::parse(info).map(AprsData::Item),
306 b'_' => AprsPositionlessWeather::parse(info).map(AprsData::Weather),
307 b'T' if info.get(1) == Some(&b'#') => AprsTelemetry::parse(info).map(AprsData::Telemetry),
310 b'<' => Ok(AprsData::Capabilities(AprsCapabilities::parse(info))),
311 b'?' => Ok(AprsData::Query(AprsQuery::parse(info))),
312 b'[' => AprsGridLocator::parse(info).map(AprsData::GridLocator),
313 b'$' => Ok(AprsData::Nmea(AprsNmea::parse(info))),
314 b'}' => AprsThirdParty::parse(info).map(AprsData::ThirdParty),
315 b'{' => Ok(AprsData::UserDefined(AprsUserDefined::parse(info))),
316 _ => Ok(AprsData::Unknown {
317 dti,
318 data: info.to_vec(),
319 }),
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 const POSITION_PACKET: &[u8] = b"W1AW-9>APRS,WIDE1-1,WIDE2-2:!4903.50N/07201.75W-Test";
328
329 const MSG_PACKET: &[u8] = b"KD9ABC>APDR15,qAR,KD9XYZ::W1AW-9 :Hello world{001";
330
331 #[test]
332 fn decode_position_full() {
333 let pkt = AprsPacket::decode_textual(POSITION_PACKET).unwrap();
334 assert_eq!(pkt.from.to_string(), "W1AW-9");
335 assert_eq!(pkt.to.to_string(), "APRS");
336 assert_eq!(pkt.via.len(), 2);
337 assert!(matches!(pkt.data, AprsData::Position(_)));
338 }
339
340 #[test]
341 fn decode_message_header() {
342 let pkt = AprsPacket::decode_textual(MSG_PACKET).unwrap();
343 assert_eq!(pkt.from.to_string(), "KD9ABC");
344 assert_eq!(pkt.to.to_string(), "APDR15");
345 assert_eq!(pkt.via.len(), 1);
346 assert!(matches!(pkt.data, AprsData::Message(_)));
347 }
348
349 #[test]
350 fn empty_input_error() {
351 assert!(AprsPacket::decode_textual(b"").is_err());
352 }
353
354 #[test]
355 fn missing_arrow_error() {
356 assert!(AprsPacket::decode_textual(b"W1AW:!hello").is_err());
357 }
358
359 #[test]
360 fn missing_colon_error() {
361 assert!(AprsPacket::decode_textual(b"W1AW>APRS,WIDE1").is_err());
362 }
363
364 #[test]
365 fn no_via_path() {
366 let pkt = AprsPacket::decode_textual(b"W1AW>APRS:>Status text").unwrap();
367 assert!(pkt.via.is_empty());
368 }
369
370 #[test]
371 fn unknown_dti_preserved() {
372 let pkt = AprsPacket::decode_textual(b"W1AW>APRS:~custom data").unwrap();
373 #[allow(unreachable_patterns)]
374 match &pkt.data {
375 AprsData::Unknown { dti, data } => {
376 assert_eq!(*dti, b'~');
377 assert_eq!(data.as_slice(), b"~custom data");
378 }
379 _ => panic!("expected Unknown"),
380 }
381 }
382
383 #[test]
384 fn telemetry_data_dispatched() {
385 let pkt = AprsPacket::decode_textual(b"W1AW>APRS:T#005,10,20,30,40,50,10101010").unwrap();
386 assert!(matches!(pkt.data, AprsData::Telemetry(_)));
387 }
388
389 #[test]
390 fn t_without_hash_is_unknown() {
391 let pkt = AprsPacket::decode_textual(b"W1AW>APRS:Tno hash here").unwrap();
394 match &pkt.data {
395 AprsData::Unknown { dti, .. } => assert_eq!(*dti, b'T'),
396 other => panic!("expected Unknown, got {other:?}"),
397 }
398 }
399
400 #[test]
401 fn encode_textual_round_trip() {
402 let pkt = AprsPacket::decode_textual(POSITION_PACKET).unwrap();
403 let encoded = pkt.encode_textual().unwrap();
404 assert_eq!(encoded, POSITION_PACKET);
405 }
406
407 #[test]
408 fn encode_ax25_round_trip() {
409 let pkt = AprsPacket::decode_textual(POSITION_PACKET).unwrap();
410 let ax25 = pkt.encode_ax25().unwrap();
411 let decoded = AprsPacket::decode_ax25(&ax25).unwrap();
412 assert_eq!(decoded.from.to_string(), "W1AW-9");
413 assert_eq!(decoded.to.to_string(), "APRS");
414 }
415
416 #[test]
417 fn ax25_struct_round_trip_preserves_heard_bit() {
418 let pkt =
421 AprsPacket::decode_textual(b"W1AW-9>APRS,W0OOD-2*,WIDE1-1:!4903.50N/07201.75W-Test")
422 .unwrap();
423 let ax25 = pkt.encode_ax25().unwrap();
424 let redecoded = AprsPacket::decode_ax25(&ax25).unwrap();
425 assert_eq!(pkt, redecoded);
426 assert!(matches!(redecoded.via[0], Digipeater::Callsign(_, true)));
427 assert!(matches!(redecoded.via[1], Digipeater::Callsign(_, false)));
428 }
429
430 #[test]
434 fn null_position_at_signs() {
435 let raw = b"N0AMY-3>BEACON,W0OOD-2*,WIDE2*,WIDE1-1:!/@@@@@@@@@@ WWW.TONYTYLER.COM WHERE IS SHAMERFACE SHE IS WITH DOMO";
436 let pkt = AprsPacket::decode_textual(raw).unwrap();
437 assert_eq!(pkt.from.to_string(), "N0AMY-3");
438 assert!(matches!(pkt.data, AprsData::Position(_)));
439 if let AprsData::Position(ref pos) = pkt.data {
440 assert!(!pos.messaging_supported);
441 assert!(pos.timestamp.is_none());
442 assert!(pos.comment.starts_with(b" WWW.TONYTYLER.COM"));
443 }
444 }
445}