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 { return Some(Directivity::Omni); }
16 if d < 9 { return Some(Directivity::Degrees(d as u16 * 45)); }
17 None
18 }
19
20 fn as_digit(&self) -> u8 {
21 match self {
22 Directivity::Omni => 0,
23 Directivity::Degrees(deg) => ((deg % 360) / 45) as u8,
24 }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35pub enum Extension {
36 DirectionSpeed {
38 direction_degrees: u16,
39 speed_knots: u16,
40 },
41 Phg {
43 power_watts: u32,
44 antenna_height_feet: u32,
45 antenna_gain_db: u8,
46 directivity: Directivity,
47 },
48 Rng {
50 range_miles: u16,
51 },
52 Dfs {
54 s_points: u8,
55 antenna_height_feet: u32,
56 antenna_gain_db: u8,
57 directivity: Directivity,
58 },
59}
60
61impl Extension {
62 pub fn parse(data: &[u8]) -> Option<Self> {
65 if data.len() < 7 {
66 return None;
67 }
68 let b = &data[..7];
69
70 if b[3] == b'/' && b[..3].iter().all(|c| c.is_ascii_digit())
72 && b[4..7].iter().all(|c| c.is_ascii_digit())
73 {
74 let dir: u16 = parse_bytes(&b[0..3])?;
75 let spd: u16 = parse_bytes(&b[4..7])?;
76 return Some(Extension::DirectionSpeed {
78 direction_degrees: dir,
79 speed_knots: spd,
80 });
81 }
82
83 if b.starts_with(b"PHG") && b[3].is_ascii_digit() && b[5].is_ascii_digit() && b[6].is_ascii_digit() {
85 let p = b[3] - b'0';
86 let h = b[4]; let g = b[5] - b'0';
88 let d = b[6] - b'0';
89 let power_watts = (p as u32) * (p as u32);
90 let antenna_height_feet = 10 * (1u32 << (h.saturating_sub(48) as u32));
91 let directivity = Directivity::from_digit(d)?;
92 return Some(Extension::Phg {
93 power_watts,
94 antenna_height_feet,
95 antenna_gain_db: g,
96 directivity,
97 });
98 }
99
100 if b.starts_with(b"RNG") && b[3..7].iter().all(|c| c.is_ascii_digit()) {
102 let range: u16 = parse_bytes(&b[3..7])?;
103 return Some(Extension::Rng { range_miles: range });
104 }
105
106 if b.starts_with(b"DFS") && b[3].is_ascii_digit() && b[5].is_ascii_digit() && b[6].is_ascii_digit() {
108 let s = b[3] - b'0';
109 let h = b[4];
110 let g = b[5] - b'0';
111 let d = b[6] - b'0';
112 let antenna_height_feet = 10 * (1u32 << (h.saturating_sub(48) as u32));
113 let directivity = Directivity::from_digit(d)?;
114 return Some(Extension::Dfs {
115 s_points: s,
116 antenna_gain_db: g,
117 antenna_height_feet,
118 directivity,
119 });
120 }
121
122 None
123 }
124
125 pub fn encode(&self, out: &mut Vec<u8>) {
127 match self {
128 Extension::DirectionSpeed { direction_degrees, speed_knots } => {
129 out.extend_from_slice(
130 format!("{:03}/{:03}", direction_degrees, speed_knots).as_bytes()
131 );
132 }
133 Extension::Phg { power_watts, antenna_height_feet, antenna_gain_db, directivity } => {
134 let p = (*power_watts as f64).sqrt() as u8;
135 let h_log = if *antenna_height_feet >= 10 {
136 ((*antenna_height_feet / 10) as f64).log2() as u8 + 48
137 } else {
138 48
139 };
140 out.extend_from_slice(b"PHG");
141 out.push(p + b'0');
142 out.push(h_log);
143 out.push(antenna_gain_db + b'0');
144 out.push(directivity.as_digit() + b'0');
145 }
146 Extension::Rng { range_miles } => {
147 out.extend_from_slice(format!("RNG{:04}", range_miles).as_bytes());
148 }
149 Extension::Dfs { s_points, antenna_height_feet, antenna_gain_db, directivity } => {
150 let h_log = if *antenna_height_feet >= 10 {
151 ((*antenna_height_feet / 10) as f64).log2() as u8 + 48
152 } else {
153 48
154 };
155 out.extend_from_slice(b"DFS");
156 out.push(s_points + b'0');
157 out.push(h_log);
158 out.push(antenna_gain_db + b'0');
159 out.push(directivity.as_digit() + b'0');
160 }
161 }
162 }
163
164 pub fn require(data: &[u8]) -> Result<Self, AprsError> {
166 Self::parse(data).ok_or(AprsError::UnsupportedPositionFormat)
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn direction_speed() {
176 let ext = Extension::parse(b"322/103").unwrap();
177 assert!(matches!(ext, Extension::DirectionSpeed { direction_degrees: 322, speed_knots: 103 }));
178 }
179
180 #[test]
181 fn direction_speed_encode_round_trip() {
182 let ext = Extension::DirectionSpeed { direction_degrees: 322, speed_knots: 103 };
183 let mut out = Vec::new();
184 ext.encode(&mut out);
185 assert_eq!(out, b"322/103");
186 assert_eq!(Extension::parse(&out).unwrap(), ext);
187 }
188
189 #[test]
190 fn rng_parse() {
191 let ext = Extension::parse(b"RNG0050").unwrap();
192 assert!(matches!(ext, Extension::Rng { range_miles: 50 }));
193 }
194
195 #[test]
196 fn too_short_returns_none() {
197 assert!(Extension::parse(b"12/1").is_none());
198 }
199}