Skip to content

Commit e1e1a61

Browse files
authored
Merge pull request #157 from jacobcalvert/jacobcalvert/add-objects
refactoring parsing to support addition of Objects and Items
2 parents 45fb3b0 + f755f29 commit e1e1a61

File tree

14 files changed

+1330
-310
lines changed

14 files changed

+1330
-310
lines changed

src/components/extensions.rs

Lines changed: 423 additions & 0 deletions
Large diffs are not rendered by default.

src/lonlat.rs renamed to src/components/lonlat.rs

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,72 @@
11
use std::io::Write;
2-
use std::ops::Deref;
2+
use std::ops::{Deref, RangeInclusive};
33

44
use base91;
55
use bytes::parse_bytes;
66
use DecodeError;
77
use EncodeError;
8-
use Precision;
8+
9+
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq)]
10+
pub enum Precision {
11+
TenDegree,
12+
OneDegree,
13+
TenMinute,
14+
OneMinute,
15+
TenthMinute,
16+
HundredthMinute,
17+
}
18+
19+
impl Precision {
20+
/// Returns the width of the precision in degrees.
21+
/// For example, `Precision::OneDegree` would return 1.0.
22+
pub fn width(&self) -> f64 {
23+
match self {
24+
Precision::HundredthMinute => 1.0 / 6000.0,
25+
Precision::TenthMinute => 1.0 / 600.0,
26+
Precision::OneMinute => 1.0 / 60.0,
27+
Precision::TenMinute => 1.0 / 6.0,
28+
Precision::OneDegree => 1.0,
29+
Precision::TenDegree => 10.0,
30+
}
31+
}
32+
33+
pub(crate) fn range(&self, center: f64) -> RangeInclusive<f64> {
34+
let width = self.width();
35+
36+
(center - (width / 2.0))..=(center + (width / 2.0))
37+
}
38+
39+
pub(crate) fn num_digits(&self) -> u8 {
40+
match self {
41+
Precision::HundredthMinute => 0,
42+
Precision::TenthMinute => 1,
43+
Precision::OneMinute => 2,
44+
Precision::TenMinute => 3,
45+
Precision::OneDegree => 4,
46+
Precision::TenDegree => 5,
47+
}
48+
}
49+
50+
pub(crate) fn from_num_digits(digits: u8) -> Option<Self> {
51+
let res = match digits {
52+
0 => Precision::HundredthMinute,
53+
1 => Precision::TenthMinute,
54+
2 => Precision::OneMinute,
55+
3 => Precision::TenMinute,
56+
4 => Precision::OneDegree,
57+
5 => Precision::TenDegree,
58+
_ => return None,
59+
};
60+
61+
Some(res)
62+
}
63+
}
64+
65+
impl Default for Precision {
66+
fn default() -> Self {
67+
Self::HundredthMinute
68+
}
69+
}
970

1071
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Default)]
1172
pub struct Latitude(f64);
@@ -276,7 +337,7 @@ impl Longitude {
276337
// returns the parsed value as well as the number of spaces we found
277338
fn parse_bytes_trailing_spaces(b: &[u8; 2], only_spaces: bool) -> Option<(u32, u8)> {
278339
if only_spaces {
279-
if b == &[b' ', b' '] {
340+
if b == b" " {
280341
return Some((0, 2));
281342
} else {
282343
return None;
@@ -293,6 +354,13 @@ fn parse_bytes_trailing_spaces(b: &[u8; 2], only_spaces: bool) -> Option<(u32, u
293354
mod tests {
294355
use super::*;
295356

357+
#[test]
358+
fn precision_e2e() {
359+
for i in 0..6 {
360+
assert_eq!(i, Precision::from_num_digits(i).unwrap().num_digits());
361+
}
362+
}
363+
296364
#[test]
297365
fn test_latitude_out_of_bounds() {
298366
assert_eq!(None, Latitude::new(90.1));

src/components/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub mod extensions;
2+
pub mod lonlat;
3+
pub mod position;
4+
pub mod timestamp;

src/components/position.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use std::{
2+
io::{Read, Write},
3+
ops::RangeInclusive,
4+
};
5+
6+
use crate::{AprsCompressedCs, AprsCompressionType, DecodeError, EncodeError};
7+
8+
use super::lonlat::{Latitude, Longitude, Precision};
9+
10+
#[derive(PartialEq, Debug, Clone)]
11+
pub enum AprsCst {
12+
CompressedSome {
13+
cs: AprsCompressedCs,
14+
t: AprsCompressionType,
15+
},
16+
CompressedNone,
17+
Uncompressed,
18+
}
19+
20+
#[derive(PartialEq, Debug, Clone)]
21+
pub struct Position {
22+
pub latitude: Latitude,
23+
pub longitude: Longitude,
24+
pub precision: Precision,
25+
pub symbol_table: char,
26+
pub symbol_code: char,
27+
pub cst: AprsCst,
28+
}
29+
30+
impl Position {
31+
/// Latitudes in APRS aren't perfectly precise - they have a configurable level of ambiguity. This is stored in the `precision` field on the `Position` struct. This method returns a range of what the actual latitude value might be.
32+
pub fn latitude_bounding(&self) -> RangeInclusive<f64> {
33+
self.precision.range(self.latitude.value())
34+
}
35+
36+
/// Longitudes in APRS aren't perfectly precise - they have a configurable level of ambiguity. This is stored in the `precision` field on the `Position` struct. This method returns a range of what the actual longitude value might be.
37+
pub fn longitude_bounding(&self) -> RangeInclusive<f64> {
38+
self.precision.range(self.longitude.value())
39+
}
40+
41+
pub(crate) fn encode_uncompressed<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
42+
self.latitude.encode_uncompressed(buf, self.precision)?;
43+
write!(buf, "{}", self.symbol_table)?;
44+
self.longitude.encode_uncompressed(buf)?;
45+
write!(buf, "{}", self.symbol_code)?;
46+
Ok(())
47+
}
48+
49+
pub(crate) fn encode_compressed<W: Write>(&self, buf: &mut W) -> Result<(), EncodeError> {
50+
write!(buf, "{}", self.symbol_table)?;
51+
52+
self.latitude.encode_compressed(buf)?;
53+
self.longitude.encode_compressed(buf)?;
54+
55+
write!(buf, "{}", self.symbol_code)?;
56+
57+
match self.cst {
58+
AprsCst::CompressedSome { cs, t } => cs.encode(buf, t)?,
59+
AprsCst::CompressedNone => write!(buf, " sT")?,
60+
AprsCst::Uncompressed => unreachable!(),
61+
};
62+
63+
Ok(())
64+
}
65+
/// this function assumes we are getting the head of a byte list
66+
/// representing a compressed or uncompressed position
67+
///
68+
/// all position representations interleave the symbol table and code
69+
/// so we stuff it all in here
70+
pub(crate) fn decode(b: &[u8]) -> Result<(Option<&[u8]>, Self), DecodeError> {
71+
// make sure we're not empty
72+
if b.is_empty() {
73+
return Err(DecodeError::InvalidPosition(b.to_vec()));
74+
}
75+
let is_uncompressed_position = (*b.first().unwrap_or(&0) as char).is_numeric();
76+
if is_uncompressed_position {
77+
if b.len() < 19 {
78+
return Err(DecodeError::InvalidPosition(b.to_vec()));
79+
}
80+
let (latitude, precision) = Latitude::parse_uncompressed(&b[0..8])?;
81+
let longitude = Longitude::parse_uncompressed(&b[9..18], precision)?;
82+
83+
let symbol_table = b[8] as char;
84+
let symbol_code = b[18] as char;
85+
86+
Ok((
87+
b.get(19..),
88+
Self {
89+
latitude,
90+
longitude,
91+
precision,
92+
symbol_code,
93+
symbol_table,
94+
cst: AprsCst::Uncompressed,
95+
},
96+
))
97+
} else {
98+
if b.len() < 13 {
99+
return Err(DecodeError::InvalidPosition(b.to_vec()));
100+
}
101+
let symbol_table = b[0] as char;
102+
let comp_lat = &b[1..5];
103+
let comp_lon = &b[5..9];
104+
let symbol_code = b[9] as char;
105+
let course_speed = &b[10..12];
106+
let comp_type = b[12];
107+
108+
b.take(12);
109+
110+
let latitude = Latitude::parse_compressed(comp_lat)?;
111+
let longitude = Longitude::parse_compressed(comp_lon)?;
112+
113+
// From the APRS spec - if the c value is a space,
114+
// the csT doesn't matter
115+
let cst = match course_speed[0] {
116+
b' ' => AprsCst::CompressedNone,
117+
_ => {
118+
let t = comp_type
119+
.checked_sub(33)
120+
.ok_or_else(|| DecodeError::InvalidPosition(b.to_owned()))?
121+
.into();
122+
let cs = AprsCompressedCs::parse(course_speed[0], course_speed[1], t)?;
123+
AprsCst::CompressedSome { cs, t }
124+
}
125+
};
126+
Ok((
127+
b.get(13..),
128+
Self {
129+
latitude,
130+
longitude,
131+
precision: Precision::default(),
132+
symbol_code,
133+
symbol_table,
134+
cst,
135+
},
136+
))
137+
}
138+
}
139+
}
File renamed without changes.

src/error.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use Callsign;
22

3+
use crate::Extension;
34
#[derive(Debug, Eq, PartialEq, thiserror::Error)]
45
pub enum DecodeError {
56
#[error("Invalid Callsign: {0:?}")]
@@ -28,6 +29,33 @@ pub enum DecodeError {
2829
InvalidMicEDestination(Callsign),
2930
#[error("Invalid Mic-E information field: {0:?}")]
3031
InvalidMicEInformation(Vec<u8>),
32+
#[error("Invalid Object name {0:?}")]
33+
InvalidObjectName(Vec<u8>),
34+
#[error("Invalid Object liveness, expected '*' or '(space)', got '{0:?}'")]
35+
InvalidObjectLiveness(char),
36+
#[error("Invalid Object format")]
37+
InvalidObject,
38+
#[error("Invalid Item format")]
39+
InvalidItem,
40+
#[error("Invalid Item name {0:?}")]
41+
InvalidItemName(Vec<u8>),
42+
#[error("Invalid Item liveness, expected '!' or '(space)', got '{0:?}'")]
43+
InvalidItemLiveness(char),
44+
#[error("Invalid Extension data: {0:?}")]
45+
InvalidExtension(Vec<u8>),
46+
47+
#[error("Invalid Extension Range value: {0:?}")]
48+
InvalidExtensionRange(Vec<u8>),
49+
50+
#[error("Invalid Extension Direction/Speed value: {0:?}")]
51+
InvalidExtensionDirectionSpeed(Vec<u8>),
52+
#[error("Invalid Extension PHG value: {0:?}")]
53+
InvalidExtensionPhg(Vec<u8>),
54+
#[error("Invalid Extension DFS value: {0:?}")]
55+
InvalidExtensionDfs(Vec<u8>),
56+
57+
#[error("Invalid Extension Area value: {0:?}")]
58+
InvalidExtensionArea(Vec<u8>),
3159
}
3260

3361
#[derive(Debug, thiserror::Error)]
@@ -46,4 +74,7 @@ pub enum EncodeError {
4674
NonGgaAltitude,
4775
#[error(transparent)]
4876
Write(#[from] std::io::Error),
77+
78+
#[error("Invalid Extension value: {0:?}")]
79+
InvalidExtension(Extension),
4980
}

0 commit comments

Comments
 (0)