aprs_decode/types/
extensions.rs1use crate::error::AprsError;
2use crate::util::parse_bytes;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7pub enum Directivity {
8 Omni,
9 Degrees(u16),
11}
12
13impl Directivity {
14 fn from_digit(d: u8) -> Option<Self> {
15 if d == 0 {
16 return Some(Directivity::Omni);
17 }
18 if d < 9 {
19 return Some(Directivity::Degrees(d as u16 * 45));
20 }
21 None
22 }
23
24 fn as_digit(&self) -> u8 {
25 match self {
26 Directivity::Omni => 0,
27 Directivity::Degrees(deg) => ((deg % 360) / 45) as u8,
28 }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub enum Extension {
40 DirectionSpeed {
42 direction_degrees: u16,
43 speed_knots: u16,
44 },
45 Phg {
47 power_watts: u32,
48 antenna_height_feet: u32,
49 antenna_gain_db: u8,
50 directivity: Directivity,
51 },
52 Rng { range_miles: u16 },
54 Dfs {
56 s_points: u8,
57 antenna_height_feet: u32,
58 antenna_gain_db: u8,
59 directivity: Directivity,
60 },
61}
62
63impl Extension {
64 pub fn parse(data: &[u8]) -> Option<Self> {
67 if data.len() < 7 {
68 return None;
69 }
70 let b = &data[..7];
71
72 if b[3] == b'/'
74 && b[..3].iter().all(|c| c.is_ascii_digit())
75 && b[4..7].iter().all(|c| c.is_ascii_digit())
76 {
77 let dir: u16 = parse_bytes(&b[0..3])?;
78 let spd: u16 = parse_bytes(&b[4..7])?;
79 return Some(Extension::DirectionSpeed {
81 direction_degrees: dir,
82 speed_knots: spd,
83 });
84 }
85
86 if b.starts_with(b"PHG")
88 && b[3].is_ascii_digit()
89 && b[4].is_ascii_digit()
90 && b[5].is_ascii_digit()
91 && b[6].is_ascii_digit()
92 {
93 let p = b[3] - b'0';
94 let h = b[4]; let g = b[5] - b'0';
96 let d = b[6] - b'0';
97 let power_watts = (p as u32) * (p as u32);
98 let antenna_height_feet = 10 * (1u32 << (h.saturating_sub(48) as u32));
99 let directivity = Directivity::from_digit(d)?;
100 return Some(Extension::Phg {
101 power_watts,
102 antenna_height_feet,
103 antenna_gain_db: g,
104 directivity,
105 });
106 }
107
108 if b.starts_with(b"RNG") && b[3..7].iter().all(|c| c.is_ascii_digit()) {
110 let range: u16 = parse_bytes(&b[3..7])?;
111 return Some(Extension::Rng { range_miles: range });
112 }
113
114 if b.starts_with(b"DFS")
116 && b[3].is_ascii_digit()
117 && b[4].is_ascii_digit()
118 && b[5].is_ascii_digit()
119 && b[6].is_ascii_digit()
120 {
121 let s = b[3] - b'0';
122 let h = b[4];
123 let g = b[5] - b'0';
124 let d = b[6] - b'0';
125 let antenna_height_feet = 10 * (1u32 << (h.saturating_sub(48) as u32));
126 let directivity = Directivity::from_digit(d)?;
127 return Some(Extension::Dfs {
128 s_points: s,
129 antenna_gain_db: g,
130 antenna_height_feet,
131 directivity,
132 });
133 }
134
135 None
136 }
137
138 pub fn encode(&self, out: &mut Vec<u8>) {
140 match self {
141 Extension::DirectionSpeed {
142 direction_degrees,
143 speed_knots,
144 } => {
145 out.extend_from_slice(
146 format!("{:03}/{:03}", direction_degrees, speed_knots).as_bytes(),
147 );
148 }
149 Extension::Phg {
150 power_watts,
151 antenna_height_feet,
152 antenna_gain_db,
153 directivity,
154 } => {
155 let p = (*power_watts as f64).sqrt() as u8;
156 let h_log = if *antenna_height_feet >= 10 {
157 ((*antenna_height_feet / 10) as f64).log2() as u8 + 48
158 } else {
159 48
160 };
161 out.extend_from_slice(b"PHG");
162 out.push(p + b'0');
163 out.push(h_log);
164 out.push(antenna_gain_db + b'0');
165 out.push(directivity.as_digit() + b'0');
166 }
167 Extension::Rng { range_miles } => {
168 out.extend_from_slice(format!("RNG{:04}", range_miles).as_bytes());
169 }
170 Extension::Dfs {
171 s_points,
172 antenna_height_feet,
173 antenna_gain_db,
174 directivity,
175 } => {
176 let h_log = if *antenna_height_feet >= 10 {
177 ((*antenna_height_feet / 10) as f64).log2() as u8 + 48
178 } else {
179 48
180 };
181 out.extend_from_slice(b"DFS");
182 out.push(s_points + b'0');
183 out.push(h_log);
184 out.push(antenna_gain_db + b'0');
185 out.push(directivity.as_digit() + b'0');
186 }
187 }
188 }
189
190 pub fn require(data: &[u8]) -> Result<Self, AprsError> {
192 Self::parse(data).ok_or(AprsError::UnsupportedPositionFormat)
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn direction_speed() {
202 let ext = Extension::parse(b"322/103").unwrap();
203 assert!(matches!(
204 ext,
205 Extension::DirectionSpeed {
206 direction_degrees: 322,
207 speed_knots: 103
208 }
209 ));
210 }
211
212 #[test]
213 fn direction_speed_encode_round_trip() {
214 let ext = Extension::DirectionSpeed {
215 direction_degrees: 322,
216 speed_knots: 103,
217 };
218 let mut out = Vec::new();
219 ext.encode(&mut out);
220 assert_eq!(out, b"322/103");
221 assert_eq!(Extension::parse(&out).unwrap(), ext);
222 }
223
224 #[test]
225 fn rng_parse() {
226 let ext = Extension::parse(b"RNG0050").unwrap();
227 assert!(matches!(ext, Extension::Rng { range_miles: 50 }));
228 }
229
230 #[test]
231 fn too_short_returns_none() {
232 assert!(Extension::parse(b"12/1").is_none());
233 }
234
235 #[test]
236 fn phg_valid() {
237 let ext = Extension::parse(b"PHG5132").unwrap();
238 assert!(matches!(ext, Extension::Phg { .. }));
239 }
240
241 #[test]
242 fn phg_nondigit_height_returns_none() {
243 assert!(Extension::parse(b"PHG0z00").is_none());
246 }
247
248 #[test]
249 fn dfs_nondigit_height_returns_none() {
250 assert!(Extension::parse(b"DFS0z00").is_none());
251 }
252
253 #[test]
254 fn phg_max_digit_height_no_panic() {
255 let ext = Extension::parse(b"PHG0900").unwrap();
257 assert!(matches!(
258 ext,
259 Extension::Phg {
260 antenna_height_feet: 5120,
261 ..
262 }
263 ));
264 }
265}