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 {
76 addressee,
77 text,
78 subtype,
79 })
80 }
81
82 pub fn encode(&self) -> Vec<u8> {
83 let mut out = Vec::new();
84 out.push(b':');
85 out.extend_from_slice(&self.addressee);
86 out.extend(std::iter::repeat_n(
88 b' ',
89 9usize.saturating_sub(self.addressee.len()),
90 ));
91 out.push(b':');
92
93 match &self.subtype {
94 MessageSubtype::Ack { id } => {
95 out.extend_from_slice(b"ack");
96 out.extend_from_slice(id);
97 }
98 MessageSubtype::Rej { id } => {
99 out.extend_from_slice(b"rej");
100 out.extend_from_slice(id);
101 }
102 MessageSubtype::Directed { id } => {
103 out.extend_from_slice(&self.text);
104 if let Some(id) = id {
105 out.push(b'{');
106 out.extend_from_slice(id);
107 }
108 }
109 _ => {
110 out.extend_from_slice(&self.text);
112 }
113 }
114 out
115 }
116}
117
118fn discriminate_subtype(addressee: &[u8], text: &[u8], id: Option<&[u8]>) -> MessageSubtype {
119 if text.starts_with(b"ack") {
121 return MessageSubtype::Ack {
122 id: text[3..].to_vec(),
123 };
124 }
125 if text.starts_with(b"rej") {
126 return MessageSubtype::Rej {
127 id: text[3..].to_vec(),
128 };
129 }
130
131 if addressee.starts_with(b"BLN") {
133 return MessageSubtype::Bulletin;
134 }
135
136 if addressee.starts_with(b"NWS")
138 || addressee.starts_with(b"SKY")
139 || addressee.starts_with(b"CWA")
140 || addressee.starts_with(b"BOM")
141 {
142 return MessageSubtype::NwsBulletin;
143 }
144
145 if text.starts_with(b"PARM.") {
147 return MessageSubtype::TelemetryParm;
148 }
149 if text.starts_with(b"UNIT.") {
150 return MessageSubtype::TelemetryUnit;
151 }
152 if text.starts_with(b"EQNS.") {
153 return MessageSubtype::TelemetryEqns;
154 }
155 if text.starts_with(b"BITS.") {
156 return MessageSubtype::TelemetryBits;
157 }
158
159 if text.starts_with(b"?") {
161 return MessageSubtype::DirectedQuery;
162 }
163
164 MessageSubtype::Directed {
165 id: id.map(|b| b.to_vec()),
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn directed_no_id() {
175 let m = AprsMessage::parse(b":W1AW-9 :Hello world").unwrap();
176 assert_eq!(m.addressee, b"W1AW-9");
177 assert_eq!(m.text, b"Hello world");
178 assert!(matches!(m.subtype, MessageSubtype::Directed { id: None }));
179 }
180
181 #[test]
182 fn directed_with_id() {
183 let m = AprsMessage::parse(b":DESTINATI:Hello World! This msg has a : colon {329A7D5Z4")
184 .unwrap();
185 assert_eq!(m.addressee, b"DESTINATI");
186 assert_eq!(m.text, b"Hello World! This msg has a : colon ");
187 assert!(matches!(
188 m.subtype,
189 MessageSubtype::Directed { id: Some(ref id) } if id == b"329A7D5Z4"
190 ));
191 }
192
193 #[test]
194 fn ack() {
195 let m = AprsMessage::parse(b":W1AW-9 :ack001").unwrap();
196 assert!(matches!(m.subtype, MessageSubtype::Ack { ref id } if id == b"001"));
197 }
198
199 #[test]
200 fn rej() {
201 let m = AprsMessage::parse(b":W1AW-9 :rej001").unwrap();
202 assert!(matches!(m.subtype, MessageSubtype::Rej { ref id } if id == b"001"));
203 }
204
205 #[test]
206 fn bulletin() {
207 let m = AprsMessage::parse(b":BLN3 :Net at 21:00z tonight").unwrap();
208 assert!(matches!(m.subtype, MessageSubtype::Bulletin));
209 }
210
211 #[test]
212 fn nws_bulletin() {
213 let m = AprsMessage::parse(b":NWS-WARN :Tornado warning in effect").unwrap();
214 assert!(matches!(m.subtype, MessageSubtype::NwsBulletin));
215 }
216
217 #[test]
218 fn telemetry_parm() {
219 let m = AprsMessage::parse(b":KD9ABC :PARM.Bat1,Bat2,Temp,Hum,Pres").unwrap();
220 assert!(matches!(m.subtype, MessageSubtype::TelemetryParm));
221 }
222
223 #[test]
224 fn telemetry_bits() {
225 let m = AprsMessage::parse(b":KD9ABC :BITS.11111111,My Project").unwrap();
226 assert!(matches!(m.subtype, MessageSubtype::TelemetryBits));
227 }
228
229 #[test]
230 fn directed_query() {
231 let m = AprsMessage::parse(b":KD9ABC :?APRSD").unwrap();
232 assert!(matches!(m.subtype, MessageSubtype::DirectedQuery));
233 }
234
235 #[test]
236 fn too_short() {
237 assert!(AprsMessage::parse(b":W1AW:hi").is_err());
238 }
239
240 #[test]
241 fn encode_round_trip_directed() {
242 let raw = b":DESTINATI:Hello World! This msg has a : colon {329A7D5Z4";
243 let m = AprsMessage::parse(raw).unwrap();
244 assert_eq!(m.encode(), raw);
245 }
246
247 #[test]
248 fn encode_round_trip_bulletin() {
249 let raw = b":BLN3 :Net at 21:00z tonight";
250 let m = AprsMessage::parse(raw).unwrap();
251 assert_eq!(m.encode(), raw);
252 }
253
254 #[test]
255 fn encode_ack() {
256 let raw = b":W1AW-9 :ack001";
257 let m = AprsMessage::parse(raw).unwrap();
258 assert_eq!(m.encode(), raw);
259 }
260}