ogn_parser/
packet.rs

1use std::fmt::Write;
2use std::str::FromStr;
3
4use serde::Serialize;
5
6use crate::AprsError;
7use crate::AprsMessage;
8use crate::AprsPosition;
9use crate::AprsStatus;
10use crate::Callsign;
11use crate::EncodeError;
12
13#[derive(PartialEq, Debug, Clone, Serialize)]
14pub struct AprsPacket {
15    pub from: Callsign,
16    pub to: Callsign,
17    pub via: Vec<Callsign>,
18    #[serde(flatten)]
19    pub data: AprsData,
20}
21
22impl FromStr for AprsPacket {
23    type Err = AprsError;
24
25    fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
26        if !s.is_ascii() {
27            return Err(AprsError::InvalidCoding(s.to_owned()));
28        }
29        let header_delimiter = s
30            .find(':')
31            .ok_or_else(|| AprsError::InvalidPacket(s.to_owned()))?;
32        let (header, rest) = s.split_at(header_delimiter);
33        let body = &rest[1..];
34
35        let from_delimiter = header
36            .find('>')
37            .ok_or_else(|| AprsError::InvalidPacket(s.to_owned()))?;
38        let (from, rest) = header.split_at(from_delimiter);
39        let from = Callsign::from_str(from)?;
40
41        let to_and_via = &rest[1..];
42        let to_and_via: Vec<_> = to_and_via.split(',').collect();
43
44        let to = to_and_via
45            .first()
46            .ok_or_else(|| AprsError::InvalidPacket(s.to_owned()))?;
47        let to = Callsign::from_str(to)?;
48
49        let mut via = vec![];
50        for v in to_and_via.iter().skip(1) {
51            via.push(Callsign::from_str(v)?);
52        }
53
54        let data = AprsData::from_str(body)?;
55
56        Ok(AprsPacket {
57            from,
58            to,
59            via,
60            data,
61        })
62    }
63}
64
65impl AprsPacket {
66    pub fn encode<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
67        write!(buf, "{}>{}", self.from, self.to)?;
68        for v in &self.via {
69            write!(buf, ",{v}").unwrap();
70        }
71        write!(buf, ":")?;
72        self.data.encode(buf)?;
73
74        Ok(())
75    }
76}
77
78#[derive(PartialEq, Debug, Clone, Serialize)]
79#[serde(rename_all = "snake_case")]
80pub enum AprsData {
81    Position(AprsPosition),
82    Message(AprsMessage),
83    Status(AprsStatus),
84    Unknown,
85}
86
87impl FromStr for AprsData {
88    type Err = AprsError;
89
90    fn from_str(s: &str) -> Result<Self, AprsError> {
91        Ok(match s.chars().next().unwrap_or(0 as char) {
92            ':' => AprsData::Message(AprsMessage::from_str(&s[1..])?),
93            '!' | '/' | '=' | '@' => AprsData::Position(AprsPosition::from_str(s)?),
94            '>' => AprsData::Status(AprsStatus::from_str(&s[1..])?),
95            _ => AprsData::Unknown,
96        })
97    }
98}
99
100impl AprsData {
101    fn encode<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
102        match self {
103            Self::Position(p) => {
104                p.encode(buf)?;
105            }
106            Self::Message(m) => {
107                write!(buf, "{m}")?;
108            }
109            Self::Status(s) => {
110                write!(buf, "{s}")?;
111            }
112            Self::Unknown => return Err(EncodeError::InvalidData),
113        }
114
115        Ok(())
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::Timestamp;
123
124    #[test]
125    fn parse() {
126        let result = r"ICA3D17F2>APRS,qAS,dl4mea:/074849h4821.61N\01224.49E^322/103/A=003054 !W46! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1".parse::<AprsPacket>().unwrap();
127        assert_eq!(result.from, Callsign::new("ICA3D17F2", None));
128        assert_eq!(result.to, Callsign::new("APRS", None));
129        assert_eq!(
130            result.via,
131            vec![Callsign::new("qAS", None), Callsign::new("dl4mea", None),]
132        );
133
134        match result.data {
135            AprsData::Position(position) => {
136                assert_eq!(position.timestamp, Some(Timestamp::HHMMSS(7, 48, 49)));
137                assert_relative_eq!(*position.latitude, 48.36023333333334);
138                assert_relative_eq!(*position.longitude, 12.408266666666666);
139                /*assert_eq!(
140                    position.comment,
141                    "322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1"
142                );*/
143            }
144            _ => panic!("Unexpected data type"),
145        }
146    }
147
148    #[test]
149    fn parse_error_no_ascii() {
150        let result =
151            r"ICA3D17F2>APRS,qAS,dl4mea:/074849h4821.61N\01224.49E^322/103/A=003054 Hochkönig"
152                .parse::<AprsPacket>();
153        assert_eq!(result.is_err(), true);
154    }
155
156    #[test]
157    fn parse_message() {
158        let result =
159            r"ICA3D17F2>Aprs,qAS,dl4mea::DEST     :Hello World! This msg has a : colon {32975"
160                .parse::<AprsPacket>()
161                .unwrap();
162        assert_eq!(result.from, Callsign::new("ICA3D17F2", None));
163        assert_eq!(result.to, Callsign::new("Aprs", None));
164        assert_eq!(
165            result.via,
166            vec![Callsign::new("qAS", None), Callsign::new("dl4mea", None),]
167        );
168
169        match result.data {
170            AprsData::Message(msg) => {
171                assert_eq!(msg.addressee, "DEST");
172                assert_eq!(msg.text, "Hello World! This msg has a : colon ");
173                assert_eq!(msg.id, Some(32975));
174            }
175            _ => panic!("Unexpected data type"),
176        }
177    }
178
179    #[test]
180    fn parse_status() {
181        let result = r"ICA3D17F2>APRS,qAS,dl4mea:>312359zStatus seems okay!"
182            .parse::<AprsPacket>()
183            .unwrap();
184        assert_eq!(result.from, Callsign::new("ICA3D17F2", None));
185        assert_eq!(result.to, Callsign::new("APRS", None));
186        assert_eq!(
187            result.via,
188            vec![Callsign::new("qAS", None), Callsign::new("dl4mea", None),]
189        );
190
191        match result.data {
192            AprsData::Status(msg) => {
193                assert_eq!(msg.timestamp, Some(Timestamp::DDHHMM(31, 23, 59)));
194                assert_eq!(msg.comment.unparsed.unwrap(), "Status seems okay!");
195            }
196            _ => panic!("Unexpected data type"),
197        }
198    }
199
200    #[ignore = "status_comment and position_comment serialization not implemented"]
201    #[test]
202    fn e2e_serialize_deserialize() {
203        let valids = vec![
204            r"ICA3D17F2>APRS,qAS,dl4mea:/074849h4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
205            r"ICA3D17F2>APRS,qAS,dl4mea:@074849h4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
206            r"ICA3D17F2>APRS,qAS,dl4mea:!4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
207            r"ICA3D17F2>APRS,qAS,dl4mea:=4821.61N\01224.49E^322/103/A=003054 !W09! id213D17F2 -039fpm +0.0rot 2.5dB 3e -0.0kHz gps1x1",
208            r"ICA3D17F2>Aprs,qAS,dl4mea::DEST     :Hello World! This msg has a : colon {32975",
209            r"ICA3D17F2>Aprs,qAS,dl4mea::DESTINATI:Hello World! This msg has a : colon ",
210            r"ICA3D17F2>APRS,qAS,dl4mea:>312359zStatus seems okay!",
211            r"ICA3D17F2>APRS,qAS,dl4mea:>184050hAlso with HMS format...",
212        ];
213
214        for v in valids {
215            let mut buf = String::new();
216            v.parse::<AprsPacket>().unwrap().encode(&mut buf).unwrap();
217            assert_eq!(buf, v)
218        }
219    }
220}