1use crate::error::AprsError;
2use crate::types::lonlat::{base91_decode1, base91_encode1};
3
4#[derive(Debug, Copy, Clone, PartialEq, Eq)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub enum GpsFix {
9 Old,
10 Current,
11}
12
13#[derive(Debug, Copy, Clone, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum NmeaSource {
16 Other,
17 Gll,
18 Gga,
19 Rmc,
20}
21
22#[derive(Debug, Copy, Clone, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum Origin {
25 Compressed,
26 TncBText,
27 Software,
28 Tbd,
29 Kpc3,
30 Pico,
31 Other,
32 Digipeater,
33}
34
35#[derive(Debug, Copy, Clone, PartialEq, Eq)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub struct CompressionType {
39 pub gps_fix: GpsFix,
40 pub nmea_source: NmeaSource,
41 pub origin: Origin,
42}
43
44impl From<u8> for CompressionType {
45 fn from(b: u8) -> Self {
46 let gps_fix = if b & (1 << 5) != 0 {
47 GpsFix::Current
48 } else {
49 GpsFix::Old
50 };
51 let nmea_source = match (b & (1 << 4) != 0, b & (1 << 3) != 0) {
52 (false, false) => NmeaSource::Other,
53 (false, true) => NmeaSource::Gll,
54 (true, false) => NmeaSource::Gga,
55 (true, true) => NmeaSource::Rmc,
56 };
57 let origin = match (b & (1 << 2) != 0, b & (1 << 1) != 0, b & 1 != 0) {
58 (false, false, false) => Origin::Compressed,
59 (false, false, true) => Origin::TncBText,
60 (false, true, false) => Origin::Software,
61 (false, true, true) => Origin::Tbd,
62 (true, false, false) => Origin::Kpc3,
63 (true, false, true) => Origin::Pico,
64 (true, true, false) => Origin::Other,
65 (true, true, true) => Origin::Digipeater,
66 };
67 Self {
68 gps_fix,
69 nmea_source,
70 origin,
71 }
72 }
73}
74
75impl From<CompressionType> for u8 {
76 fn from(t: CompressionType) -> u8 {
77 let b5 = t.gps_fix == GpsFix::Current;
78 let (b4, b3) = match t.nmea_source {
79 NmeaSource::Other => (false, false),
80 NmeaSource::Gll => (false, true),
81 NmeaSource::Gga => (true, false),
82 NmeaSource::Rmc => (true, true),
83 };
84 let (b2, b1, b0) = match t.origin {
85 Origin::Compressed => (false, false, false),
86 Origin::TncBText => (false, false, true),
87 Origin::Software => (false, true, false),
88 Origin::Tbd => (false, true, true),
89 Origin::Kpc3 => (true, false, false),
90 Origin::Pico => (true, false, true),
91 Origin::Other => (true, true, false),
92 Origin::Digipeater => (true, true, true),
93 };
94 (b5 as u8) << 5
95 | (b4 as u8) << 4
96 | (b3 as u8) << 3
97 | (b2 as u8) << 2
98 | (b1 as u8) << 1
99 | (b0 as u8)
100 }
101}
102
103#[derive(Debug, Copy, Clone, PartialEq)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107pub struct CourseSpeed {
108 pub course_degrees: u16,
109 pub speed_knots: f64,
110}
111
112impl CourseSpeed {
113 pub fn new(course_degrees: u16, speed_knots: f64) -> Self {
114 Self {
115 course_degrees,
116 speed_knots,
117 }
118 }
119
120 pub(crate) fn from_cs(c: u8, s: u8) -> Self {
121 Self {
122 course_degrees: c as u16 * 4,
123 speed_knots: 1.08_f64.powi(s as i32) - 1.0,
124 }
125 }
126
127 pub(crate) fn to_cs(self) -> (u8, u8) {
128 let c = (self.course_degrees / 4) as u8;
129 let s = ((self.speed_knots + 1.0).ln() / 1.08_f64.ln()).round() as u8;
130 (c, s)
131 }
132}
133
134#[derive(Debug, Copy, Clone, PartialEq)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
138pub struct RadioRange {
139 pub range_miles: f64,
140}
141
142impl RadioRange {
143 pub fn new(range_miles: f64) -> Self {
144 Self { range_miles }
145 }
146
147 pub(crate) fn from_s(s: u8) -> Self {
148 Self {
149 range_miles: 2.0 * 1.08_f64.powi(s as i32),
150 }
151 }
152
153 pub(crate) fn to_s(self) -> u8 {
154 ((self.range_miles / 2.0).ln() / 1.08_f64.ln()).round() as u8
155 }
156}
157
158#[derive(Debug, Copy, Clone, PartialEq)]
161#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
162pub struct CompressedAltitude {
163 pub feet: f64,
164}
165
166impl CompressedAltitude {
167 pub fn new(feet: f64) -> Self {
168 Self { feet }
169 }
170
171 pub fn meters(self) -> f64 {
172 self.feet * 0.3048
173 }
174
175 pub(crate) fn from_cs(c: u8, s: u8) -> Self {
176 Self {
177 feet: 1.002_f64.powi(c as i32 * 91 + s as i32),
178 }
179 }
180
181 pub(crate) fn to_cs(self) -> (u8, u8) {
182 let v = (self.feet.ln() / 1.002_f64.ln()).round() as i32;
183 ((v / 91) as u8, (v % 91) as u8)
184 }
185}
186
187#[derive(Debug, Copy, Clone, PartialEq)]
190#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
191pub struct Altitude {
192 pub feet: f64,
193}
194
195impl Altitude {
196 pub fn new(feet: f64) -> Self {
197 Self { feet }
198 }
199
200 pub fn meters(self) -> f64 {
201 self.feet * 0.3048
202 }
203}
204
205#[derive(Debug, Clone, PartialEq)]
209#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
210pub enum CompressedCs {
211 CourseSpeed(CourseSpeed, CompressionType),
212 RadioRange(RadioRange, CompressionType),
213 Altitude(CompressedAltitude, CompressionType),
214 None(CompressionType),
216}
217
218impl CompressedCs {
219 pub(crate) fn parse(c: u8, s: u8, t_raw: u8) -> Result<Self, AprsError> {
221 if c == b' ' {
225 let t_val = t_raw.saturating_sub(33);
226 return Ok(CompressedCs::None(CompressionType::from(t_val)));
227 }
228 let c_val = match base91_decode1(c) {
229 Some(v) => v,
230 None => return Ok(CompressedCs::None(CompressionType::from(0))),
231 };
232 let s_val = match base91_decode1(s) {
233 Some(v) => v,
234 None => return Ok(CompressedCs::None(CompressionType::from(0))),
235 };
236 let t = CompressionType::from(t_raw.saturating_sub(33));
237
238 let cs = if t.nmea_source == NmeaSource::Gga {
239 CompressedCs::Altitude(CompressedAltitude::from_cs(c_val, s_val), t)
240 } else {
241 match c_val {
242 0..=89 => CompressedCs::CourseSpeed(CourseSpeed::from_cs(c_val, s_val), t),
243 90 => CompressedCs::RadioRange(RadioRange::from_s(s_val), t),
244 _ => return Err(AprsError::InvalidCompressedByte { byte: c }),
245 }
246 };
247 Ok(cs)
248 }
249
250 pub(crate) fn encode(&self, out: &mut Vec<u8>) {
251 match self {
252 CompressedCs::CourseSpeed(cs, t) => {
253 let (c, s) = cs.to_cs();
254 out.push(base91_encode1(c));
255 out.push(base91_encode1(s));
256 out.push(u8::from(*t) + 33);
257 }
258 CompressedCs::RadioRange(rr, t) => {
259 out.push(b'{');
260 out.push(base91_encode1(rr.to_s()));
261 out.push(u8::from(*t) + 33);
262 }
263 CompressedCs::Altitude(alt, t) => {
264 let (c, s) = alt.to_cs();
265 out.push(base91_encode1(c));
266 out.push(base91_encode1(s));
267 out.push(u8::from(*t) + 33);
268 }
269 CompressedCs::None(t) => {
270 out.push(b' ');
271 out.push(b's');
273 out.push(u8::from(*t) + 33);
274 }
275 }
276 }
277
278 pub fn compression_type(&self) -> CompressionType {
279 match self {
280 CompressedCs::CourseSpeed(_, t) => *t,
281 CompressedCs::RadioRange(_, t) => *t,
282 CompressedCs::Altitude(_, t) => *t,
283 CompressedCs::None(t) => *t,
284 }
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn compression_type_round_trip() {
294 let t = CompressionType {
295 gps_fix: GpsFix::Current,
296 nmea_source: NmeaSource::Gga,
297 origin: Origin::Software,
298 };
299 let byte = u8::from(t);
300 assert_eq!(CompressionType::from(byte), t);
301 }
302
303 #[test]
304 fn course_speed_round_trip() {
305 for c in 0u8..90 {
306 for s in 0u8..91 {
307 let cs = CourseSpeed::from_cs(c, s);
308 assert_eq!(cs.to_cs(), (c, s));
309 }
310 }
311 }
312
313 #[test]
314 fn radio_range_round_trip() {
315 for s in 0u8..91 {
316 let rr = RadioRange::from_s(s);
317 assert_eq!(rr.to_s(), s);
318 }
319 }
320
321 #[test]
322 fn altitude_round_trip() {
323 for c in 0u8..91 {
324 for s in 0u8..91 {
325 let alt = CompressedAltitude::from_cs(c, s);
326 assert_eq!(alt.to_cs(), (c, s));
327 }
328 }
329 }
330}