1use crate::error::AprsError;
2use crate::util::trim_spaces_end;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7pub enum MessageSubtype {
8 Directed { id: Option<Vec<u8>> },
10 Ack { id: Vec<u8> },
12 Rej { id: Vec<u8> },
14 Bulletin,
16 NwsBulletin,
19 TelemetryParm,
21 TelemetryUnit,
23 TelemetryEqns,
25 TelemetryBits,
27 DirectedQuery,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct AprsMessage {
40 pub addressee: Vec<u8>,
42 pub text: Vec<u8>,
44 pub subtype: MessageSubtype,
46}
47
48impl AprsMessage {
49 pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
51 if info.len() < 11 {
54 return Err(AprsError::InvalidMessageMissingDelimiter);
55 }
56 if info[10] != b':' {
58 return Err(AprsError::InvalidMessageMissingDelimiter);
59 }
60 let mut addressee = info[1..10].to_vec();
61 trim_spaces_end(&mut addressee);
62
63 let body = &info[11..]; let (text_bytes, id_bytes) = if let Some(pos) = body.iter().position(|&b| b == b'{') {
67 (&body[..pos], Some(&body[pos + 1..]))
68 } else {
69 (body, None)
70 };
71
72 let text = text_bytes.to_vec();
73 let subtype = discriminate_subtype(&addressee, &text, id_bytes);
74
75 Ok(Self { addressee, text, subtype })
76 }
77
78 pub fn encode(&self) -> Vec<u8> {
79 let mut out = Vec::new();
80 out.push(b':');
81 out.extend_from_slice(&self.addressee);
82 out.extend(std::iter::repeat_n(b' ', 9usize.saturating_sub(self.addressee.len())));
84 out.push(b':');
85
86 match &self.subtype {
87 MessageSubtype::Ack { id } => {
88 out.extend_from_slice(b"ack");
89 out.extend_from_slice(id);
90 }
91 MessageSubtype::Rej { id } => {
92 out.extend_from_slice(b"rej");
93 out.extend_from_slice(id);
94 }
95 MessageSubtype::Directed { id } => {
96 out.extend_from_slice(&self.text);
97 if let Some(id) = id {
98 out.push(b'{');
99 out.extend_from_slice(id);
100 }
101 }
102 _ => {
103 out.extend_from_slice(&self.text);
105 }
106 }
107 out
108 }
109}
110
111fn discriminate_subtype(addressee: &[u8], text: &[u8], id: Option<&[u8]>) -> MessageSubtype {
112 if text.starts_with(b"ack") {
114 return MessageSubtype::Ack { id: text[3..].to_vec() };
115 }
116 if text.starts_with(b"rej") {
117 return MessageSubtype::Rej { id: text[3..].to_vec() };
118 }
119
120 if addressee.starts_with(b"BLN") {
122 return MessageSubtype::Bulletin;
123 }
124
125 if addressee.starts_with(b"NWS")
127 || addressee.starts_with(b"SKY")
128 || addressee.starts_with(b"CWA")
129 || addressee.starts_with(b"BOM")
130 {
131 return MessageSubtype::NwsBulletin;
132 }
133
134 if text.starts_with(b"PARM.") {
136 return MessageSubtype::TelemetryParm;
137 }
138 if text.starts_with(b"UNIT.") {
139 return MessageSubtype::TelemetryUnit;
140 }
141 if text.starts_with(b"EQNS.") {
142 return MessageSubtype::TelemetryEqns;
143 }
144 if text.starts_with(b"BITS.") {
145 return MessageSubtype::TelemetryBits;
146 }
147
148 if text.starts_with(b"?") {
150 return MessageSubtype::DirectedQuery;
151 }
152
153 MessageSubtype::Directed { id: id.map(|b| b.to_vec()) }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn directed_no_id() {
162 let m = AprsMessage::parse(b":W1AW-9 :Hello world").unwrap();
163 assert_eq!(m.addressee, b"W1AW-9");
164 assert_eq!(m.text, b"Hello world");
165 assert!(matches!(m.subtype, MessageSubtype::Directed { id: None }));
166 }
167
168 #[test]
169 fn directed_with_id() {
170 let m = AprsMessage::parse(b":DESTINATI:Hello World! This msg has a : colon {329A7D5Z4").unwrap();
171 assert_eq!(m.addressee, b"DESTINATI");
172 assert_eq!(m.text, b"Hello World! This msg has a : colon ");
173 assert!(matches!(
174 m.subtype,
175 MessageSubtype::Directed { id: Some(ref id) } if id == b"329A7D5Z4"
176 ));
177 }
178
179 #[test]
180 fn ack() {
181 let m = AprsMessage::parse(b":W1AW-9 :ack001").unwrap();
182 assert!(matches!(m.subtype, MessageSubtype::Ack { ref id } if id == b"001"));
183 }
184
185 #[test]
186 fn rej() {
187 let m = AprsMessage::parse(b":W1AW-9 :rej001").unwrap();
188 assert!(matches!(m.subtype, MessageSubtype::Rej { ref id } if id == b"001"));
189 }
190
191 #[test]
192 fn bulletin() {
193 let m = AprsMessage::parse(b":BLN3 :Net at 21:00z tonight").unwrap();
194 assert!(matches!(m.subtype, MessageSubtype::Bulletin));
195 }
196
197 #[test]
198 fn nws_bulletin() {
199 let m = AprsMessage::parse(b":NWS-WARN :Tornado warning in effect").unwrap();
200 assert!(matches!(m.subtype, MessageSubtype::NwsBulletin));
201 }
202
203 #[test]
204 fn telemetry_parm() {
205 let m = AprsMessage::parse(b":KD9ABC :PARM.Bat1,Bat2,Temp,Hum,Pres").unwrap();
206 assert!(matches!(m.subtype, MessageSubtype::TelemetryParm));
207 }
208
209 #[test]
210 fn telemetry_bits() {
211 let m = AprsMessage::parse(b":KD9ABC :BITS.11111111,My Project").unwrap();
212 assert!(matches!(m.subtype, MessageSubtype::TelemetryBits));
213 }
214
215 #[test]
216 fn directed_query() {
217 let m = AprsMessage::parse(b":KD9ABC :?APRSD").unwrap();
218 assert!(matches!(m.subtype, MessageSubtype::DirectedQuery));
219 }
220
221 #[test]
222 fn too_short() {
223 assert!(AprsMessage::parse(b":W1AW:hi").is_err());
224 }
225
226 #[test]
227 fn encode_round_trip_directed() {
228 let raw = b":DESTINATI:Hello World! This msg has a : colon {329A7D5Z4";
229 let m = AprsMessage::parse(raw).unwrap();
230 assert_eq!(m.encode(), raw);
231 }
232
233 #[test]
234 fn encode_round_trip_bulletin() {
235 let raw = b":BLN3 :Net at 21:00z tonight";
236 let m = AprsMessage::parse(raw).unwrap();
237 assert_eq!(m.encode(), raw);
238 }
239
240 #[test]
241 fn encode_ack() {
242 let raw = b":W1AW-9 :ack001";
243 let m = AprsMessage::parse(raw).unwrap();
244 assert_eq!(m.encode(), raw);
245 }
246}