dstar_gateway_core/dprs/
parser.rs1use crate::types::Callsign;
4
5use super::coordinates::{Latitude, Longitude};
6use super::error::DprsError;
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct DprsReport {
11 pub callsign: Callsign,
13 pub latitude: Latitude,
15 pub longitude: Longitude,
17 pub symbol: char,
19 pub comment: Option<String>,
21}
22
23pub fn parse_dprs(sentence: &str) -> Result<DprsReport, DprsError> {
45 if !sentence.starts_with("$$CRC") {
46 return Err(DprsError::MissingCrcPrefix);
47 }
48 if sentence.len() < 40 {
49 return Err(DprsError::TooShort {
50 got: sentence.len(),
51 });
52 }
53
54 let after_crc = sentence.get(10..).ok_or(DprsError::MalformedCoordinates)?;
60
61 let cs_bytes = after_crc.get(..8).ok_or(DprsError::MalformedCoordinates)?;
63 let callsign =
64 Callsign::try_from_str(cs_bytes.trim_end()).map_err(|_| DprsError::InvalidCallsign {
65 reason: "not a valid D-STAR callsign",
66 })?;
67
68 let bang_pos = sentence.find('!').ok_or(DprsError::MalformedCoordinates)?;
70 let pos_data = sentence
71 .get(bang_pos + 1..)
72 .ok_or(DprsError::MalformedCoordinates)?;
73
74 if pos_data.len() < 19 {
82 return Err(DprsError::MalformedCoordinates);
83 }
84
85 let lat_str = pos_data.get(..8).ok_or(DprsError::MalformedCoordinates)?;
86 let lat_hemi = lat_str
87 .chars()
88 .nth(7)
89 .ok_or(DprsError::MalformedCoordinates)?;
90 let lat_numeric = lat_str.get(..7).ok_or(DprsError::MalformedCoordinates)?;
91 let lat_deg = parse_aprs_degrees(lat_numeric, 2).ok_or(DprsError::MalformedCoordinates)?;
92 let lat_deg = if lat_hemi == 'S' { -lat_deg } else { lat_deg };
93 let latitude = Latitude::try_new(lat_deg)?;
94
95 let lon_str = pos_data.get(9..18).ok_or(DprsError::MalformedCoordinates)?;
96 let lon_hemi = lon_str
97 .chars()
98 .nth(8)
99 .ok_or(DprsError::MalformedCoordinates)?;
100 let lon_numeric = lon_str.get(..8).ok_or(DprsError::MalformedCoordinates)?;
101 let lon_deg = parse_aprs_degrees(lon_numeric, 3).ok_or(DprsError::MalformedCoordinates)?;
102 let lon_deg = if lon_hemi == 'W' { -lon_deg } else { lon_deg };
103 let longitude = Longitude::try_new(lon_deg)?;
104
105 let symbol = pos_data.chars().nth(19).unwrap_or('/');
108 let comment_str = pos_data.get(20..).map(ToString::to_string);
109 let comment = comment_str.filter(|s| !s.is_empty());
110
111 Ok(DprsReport {
112 callsign,
113 latitude,
114 longitude,
115 symbol,
116 comment,
117 })
118}
119
120fn parse_aprs_degrees(s: &str, degree_digits: usize) -> Option<f64> {
124 if s.len() < degree_digits + 5 {
125 return None;
126 }
127 let deg_str = s.get(..degree_digits)?;
128 let min_str = s.get(degree_digits..)?;
129 let degrees = deg_str.parse::<f64>().ok()?;
130 let minutes = min_str.parse::<f64>().ok()?;
131 Some(degrees + minutes / 60.0)
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 type TestResult = Result<(), Box<dyn std::error::Error>>;
139
140 #[test]
141 fn parse_w1aw_asheville() -> TestResult {
142 let sentence = "$$CRC0000,W1AW *>APDPRS,DSTAR*:!3530.00N/08233.00W#/Asheville test";
144 let report = parse_dprs(sentence)?;
145 assert_eq!(report.callsign.as_str().trim(), "W1AW");
146 assert!((report.latitude.degrees() - 35.5).abs() < 0.001);
147 assert!((report.longitude.degrees() - (-82.55)).abs() < 0.001);
148 assert_eq!(report.symbol, '/');
149 assert_eq!(report.comment.as_deref(), Some("Asheville test"));
150 Ok(())
151 }
152
153 #[test]
154 fn parse_missing_crc_prefix_errors() {
155 let sentence = "HELLO,W1AW *>APDPRS,DSTAR*:!3530.00N/08233.00W#/";
156 let result = parse_dprs(sentence);
157 assert!(
158 matches!(result, Err(DprsError::MissingCrcPrefix)),
159 "expected MissingCrcPrefix, got {result:?}"
160 );
161 }
162
163 #[test]
164 fn parse_too_short_errors() {
165 let sentence = "$$CRC1234,short";
166 let result = parse_dprs(sentence);
167 assert!(
168 matches!(result, Err(DprsError::TooShort { .. })),
169 "expected TooShort, got {result:?}"
170 );
171 }
172
173 #[test]
174 fn parse_southern_hemisphere() -> TestResult {
175 let sentence = "$$CRC0000,VK2ABC *>APDPRS,DSTAR*:!3351.00S/15112.00E#/Sydney";
176 let report = parse_dprs(sentence)?;
177 assert!(
178 report.latitude.degrees() < 0.0,
179 "southern hemisphere is negative"
180 );
181 assert!(
182 report.longitude.degrees() > 0.0,
183 "eastern hemisphere is positive"
184 );
185 Ok(())
186 }
187
188 #[test]
189 fn parse_without_comment() -> TestResult {
190 let sentence = "$$CRC0000,W1AW *>APDPRS,DSTAR*:!3530.00N/08233.00W#";
191 let report = parse_dprs(sentence)?;
192 assert!(report.comment.is_none());
193 Ok(())
194 }
195
196 #[test]
197 fn parse_extreme_latitude() -> TestResult {
198 let sentence = "$$CRC0000,NORTH *>APDPRS,DSTAR*:!9000.00N/00000.00E#/North Pole";
200 let report = parse_dprs(sentence)?;
201 assert!((report.latitude.degrees() - 90.0).abs() < 0.001);
202 Ok(())
203 }
204}