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 { GpsFix::Current } else { GpsFix::Old };
47 let nmea_source = match (b & (1 << 4) != 0, b & (1 << 3) != 0) {
48 (false, false) => NmeaSource::Other,
49 (false, true) => NmeaSource::Gll,
50 (true, false) => NmeaSource::Gga,
51 (true, true) => NmeaSource::Rmc,
52 };
53 let origin = match (b & (1 << 2) != 0, b & (1 << 1) != 0, b & 1 != 0) {
54 (false, false, false) => Origin::Compressed,
55 (false, false, true) => Origin::TncBText,
56 (false, true, false) => Origin::Software,
57 (false, true, true) => Origin::Tbd,
58 (true, false, false) => Origin::Kpc3,
59 (true, false, true) => Origin::Pico,
60 (true, true, false) => Origin::Other,
61 (true, true, true) => Origin::Digipeater,
62 };
63 Self { gps_fix, nmea_source, origin }
64 }
65}
66
67impl From<CompressionType> for u8 {
68 fn from(t: CompressionType) -> u8 {
69 let b5 = t.gps_fix == GpsFix::Current;
70 let (b4, b3) = match t.nmea_source {
71 NmeaSource::Other => (false, false),
72 NmeaSource::Gll => (false, true),
73 NmeaSource::Gga => (true, false),
74 NmeaSource::Rmc => (true, true),
75 };
76 let (b2, b1, b0) = match t.origin {
77 Origin::Compressed => (false, false, false),
78 Origin::TncBText => (false, false, true),
79 Origin::Software => (false, true, false),
80 Origin::Tbd => (false, true, true),
81 Origin::Kpc3 => (true, false, false),
82 Origin::Pico => (true, false, true),
83 Origin::Other => (true, true, false),
84 Origin::Digipeater => (true, true, true),
85 };
86 (b5 as u8) << 5
87 | (b4 as u8) << 4
88 | (b3 as u8) << 3
89 | (b2 as u8) << 2
90 | (b1 as u8) << 1
91 | (b0 as u8)
92 }
93}
94
95#[derive(Debug, Copy, Clone, PartialEq)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
99pub struct CourseSpeed {
100 pub course_degrees: u16,
101 pub speed_knots: f64,
102}
103
104impl CourseSpeed {
105 pub fn new(course_degrees: u16, speed_knots: f64) -> Self {
106 Self { course_degrees, speed_knots }
107 }
108
109 pub(crate) fn from_cs(c: u8, s: u8) -> Self {
110 Self {
111 course_degrees: c as u16 * 4,
112 speed_knots: 1.08_f64.powi(s as i32) - 1.0,
113 }
114 }
115
116 pub(crate) fn to_cs(self) -> (u8, u8) {
117 let c = (self.course_degrees / 4) as u8;
118 let s = ((self.speed_knots + 1.0).ln() / 1.08_f64.ln()).round() as u8;
119 (c, s)
120 }
121}
122
123#[derive(Debug, Copy, Clone, PartialEq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub struct RadioRange {
128 pub range_miles: f64,
129}
130
131impl RadioRange {
132 pub fn new(range_miles: f64) -> Self {
133 Self { range_miles }
134 }
135
136 pub(crate) fn from_s(s: u8) -> Self {
137 Self { range_miles: 2.0 * 1.08_f64.powi(s as i32) }
138 }
139
140 pub(crate) fn to_s(self) -> u8 {
141 ((self.range_miles / 2.0).ln() / 1.08_f64.ln()).round() as u8
142 }
143}
144
145#[derive(Debug, Copy, Clone, PartialEq)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
149pub struct CompressedAltitude {
150 pub feet: f64,
151}
152
153impl CompressedAltitude {
154 pub fn new(feet: f64) -> Self {
155 Self { feet }
156 }
157
158 pub fn meters(self) -> f64 {
159 self.feet * 0.3048
160 }
161
162 pub(crate) fn from_cs(c: u8, s: u8) -> Self {
163 Self { feet: 1.002_f64.powi(c as i32 * 91 + s as i32) }
164 }
165
166 pub(crate) fn to_cs(self) -> (u8, u8) {
167 let v = (self.feet.ln() / 1.002_f64.ln()).round() as i32;
168 ((v / 91) as u8, (v % 91) as u8)
169 }
170}
171
172#[derive(Debug, Copy, Clone, PartialEq)]
175#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
176pub struct Altitude {
177 pub feet: f64,
178}
179
180impl Altitude {
181 pub fn new(feet: f64) -> Self {
182 Self { feet }
183 }
184
185 pub fn meters(self) -> f64 {
186 self.feet * 0.3048
187 }
188}
189
190#[derive(Debug, Clone, PartialEq)]
194#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
195pub enum CompressedCs {
196 CourseSpeed(CourseSpeed, CompressionType),
197 RadioRange(RadioRange, CompressionType),
198 Altitude(CompressedAltitude, CompressionType),
199 None(CompressionType),
201}
202
203impl CompressedCs {
204 pub(crate) fn parse(c: u8, s: u8, t_raw: u8) -> Result<Self, AprsError> {
206 if c == b' ' {
207 let t = CompressionType::from(
208 t_raw.checked_sub(33)
209 .ok_or(AprsError::InvalidCompressedByte { byte: t_raw })?
210 );
211 return Ok(CompressedCs::None(t));
212 }
213 let c_val = base91_decode1(c).ok_or(AprsError::InvalidCompressedByte { byte: c })?;
214 let s_val = base91_decode1(s).ok_or(AprsError::InvalidCompressedByte { byte: s })?;
215 let t = CompressionType::from(
216 t_raw.checked_sub(33)
217 .ok_or(AprsError::InvalidCompressedByte { byte: t_raw })?
218 );
219
220 let cs = if t.nmea_source == NmeaSource::Gga {
221 CompressedCs::Altitude(CompressedAltitude::from_cs(c_val, s_val), t)
222 } else {
223 match c_val {
224 0..=89 => CompressedCs::CourseSpeed(CourseSpeed::from_cs(c_val, s_val), t),
225 90 => CompressedCs::RadioRange(RadioRange::from_s(s_val), t),
226 _ => return Err(AprsError::InvalidCompressedByte { byte: c }),
227 }
228 };
229 Ok(cs)
230 }
231
232 pub(crate) fn encode(&self, out: &mut Vec<u8>) {
233 match self {
234 CompressedCs::CourseSpeed(cs, t) => {
235 let (c, s) = cs.to_cs();
236 out.push(base91_encode1(c));
237 out.push(base91_encode1(s));
238 out.push(u8::from(*t) + 33);
239 }
240 CompressedCs::RadioRange(rr, t) => {
241 out.push(b'{');
242 out.push(base91_encode1(rr.to_s()));
243 out.push(u8::from(*t) + 33);
244 }
245 CompressedCs::Altitude(alt, t) => {
246 let (c, s) = alt.to_cs();
247 out.push(base91_encode1(c));
248 out.push(base91_encode1(s));
249 out.push(u8::from(*t) + 33);
250 }
251 CompressedCs::None(t) => {
252 out.push(b' ');
253 out.push(b's');
255 out.push(u8::from(*t) + 33);
256 }
257 }
258 }
259
260 pub fn compression_type(&self) -> CompressionType {
261 match self {
262 CompressedCs::CourseSpeed(_, t) => *t,
263 CompressedCs::RadioRange(_, t) => *t,
264 CompressedCs::Altitude(_, t) => *t,
265 CompressedCs::None(t) => *t,
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn compression_type_round_trip() {
276 let t = CompressionType {
277 gps_fix: GpsFix::Current,
278 nmea_source: NmeaSource::Gga,
279 origin: Origin::Software,
280 };
281 let byte = u8::from(t);
282 assert_eq!(CompressionType::from(byte), t);
283 }
284
285 #[test]
286 fn course_speed_round_trip() {
287 for c in 0u8..90 {
288 for s in 0u8..91 {
289 let cs = CourseSpeed::from_cs(c, s);
290 assert_eq!(cs.to_cs(), (c, s));
291 }
292 }
293 }
294
295 #[test]
296 fn radio_range_round_trip() {
297 for s in 0u8..91 {
298 let rr = RadioRange::from_s(s);
299 assert_eq!(rr.to_s(), s);
300 }
301 }
302
303 #[test]
304 fn altitude_round_trip() {
305 for c in 0u8..91 {
306 for s in 0u8..91 {
307 let alt = CompressedAltitude::from_cs(c, s);
308 assert_eq!(alt.to_cs(), (c, s));
309 }
310 }
311 }
312}