Skip to main content

aprs_decode/
query.rs

1/// An APRS General Query packet.
2///
3/// DTI: `?`
4///
5/// Format: `?TYPE?` or `?TYPE?lat,lon,radius` (with optional geographic footprint).
6///
7/// Common query types: `APRS`, `IGATE`, `WX`, `VERSION`, `STATUS`.
8#[derive(Debug, Clone, PartialEq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct AprsQuery {
11    /// The query type (between the two `?` characters).
12    pub query_type: Vec<u8>,
13    /// Optional geographic footprint: (latitude°, longitude°, radius km).
14    pub footprint: Option<QueryFootprint>,
15    /// Any trailing bytes after the footprint (preserved verbatim).
16    pub trailing: Vec<u8>,
17}
18
19/// A geographic footprint associated with a query.
20#[derive(Debug, Clone, PartialEq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct QueryFootprint {
23    pub latitude: f64,
24    pub longitude: f64,
25    pub radius_km: f32,
26}
27
28impl AprsQuery {
29    /// Decode from the information field (including the leading `?` DTI byte).
30    pub(crate) fn parse(info: &[u8]) -> Self {
31        // Format: ?TYPE?[lat,lon,radius]
32        // The first `?` is the DTI; the second `?` ends the type token.
33        let body = info.get(1..).unwrap_or_default();
34
35        let second_q = body.iter().position(|&b| b == b'?');
36        let (query_type, after_type) = match second_q {
37            Some(pos) => (
38                body[..pos].to_vec(),
39                body.get(pos + 1..).unwrap_or_default(),
40            ),
41            None => (body.to_vec(), &b""[..]),
42        };
43
44        let (footprint, trailing) = parse_footprint(after_type);
45
46        Self {
47            query_type,
48            footprint,
49            trailing,
50        }
51    }
52
53    pub fn encode(&self) -> Vec<u8> {
54        let mut out = vec![b'?'];
55        out.extend_from_slice(&self.query_type);
56        out.push(b'?');
57        if let Some(ref fp) = self.footprint {
58            out.extend_from_slice(
59                format!("{},{},{}", fp.latitude, fp.longitude, fp.radius_km).as_bytes(),
60            );
61        }
62        out.extend_from_slice(&self.trailing);
63        out
64    }
65}
66
67fn parse_footprint(b: &[u8]) -> (Option<QueryFootprint>, Vec<u8>) {
68    if b.is_empty() {
69        return (None, vec![]);
70    }
71    let parts: Vec<&[u8]> = b.splitn(4, |&c| c == b',').collect();
72    if parts.len() >= 3 {
73        let lat = parse_f64(parts[0]);
74        let lon = parse_f64(parts[1]);
75        let radius = parse_f32(parts[2]);
76        if let (Some(lat), Some(lon), Some(radius)) = (lat, lon, radius) {
77            let trailing = parts.get(3).map(|p| p.to_vec()).unwrap_or_default();
78            return (
79                Some(QueryFootprint {
80                    latitude: lat,
81                    longitude: lon,
82                    radius_km: radius,
83                }),
84                trailing,
85            );
86        }
87    }
88    (None, b.to_vec())
89}
90
91fn parse_f64(b: &[u8]) -> Option<f64> {
92    std::str::from_utf8(b).ok()?.trim().parse().ok()
93}
94
95fn parse_f32(b: &[u8]) -> Option<f32> {
96    std::str::from_utf8(b).ok()?.trim().parse().ok()
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn simple_aprs_query() {
105        let q = AprsQuery::parse(b"?APRS?");
106        assert_eq!(q.query_type, b"APRS");
107        assert!(q.footprint.is_none());
108    }
109
110    #[test]
111    fn query_with_footprint() {
112        let q = AprsQuery::parse(b"?APRS?49.0,-72.0,10");
113        assert_eq!(q.query_type, b"APRS");
114        let fp = q.footprint.unwrap();
115        assert!((fp.latitude - 49.0).abs() < 0.01);
116        assert!((fp.longitude - -72.0).abs() < 0.01);
117        assert!((fp.radius_km - 10.0).abs() < 0.01);
118    }
119
120    #[test]
121    fn no_second_question_mark() {
122        // Some implementations omit the second `?`
123        let q = AprsQuery::parse(b"?APRS");
124        assert_eq!(q.query_type, b"APRS");
125    }
126
127    #[test]
128    fn encode_round_trip() {
129        let raw = b"?IGATE?";
130        let q = AprsQuery::parse(raw);
131        assert_eq!(q.encode().as_slice(), raw.as_slice());
132    }
133}