1use crate::error::AprsError;
7use crate::util::parse_bytes;
8
9#[derive(Debug, Copy, Clone, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "serde", serde(transparent))]
15pub struct WindDirection(pub u16);
16
17impl WindDirection {
18 pub fn degrees(self) -> u16 {
19 self.0
20 }
21}
22
23#[derive(Debug, Copy, Clone, PartialEq, Eq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26#[cfg_attr(feature = "serde", serde(transparent))]
27pub struct WindSpeed(pub u16);
28
29impl WindSpeed {
30 pub fn mph(self) -> u16 {
31 self.0
32 }
33 pub fn knots(self) -> f32 {
34 self.0 as f32 * 0.868_976
35 }
36 pub fn kph(self) -> f32 {
37 self.0 as f32 * 1.609_344
38 }
39 pub fn m_per_s(self) -> f32 {
40 self.0 as f32 * 0.447_04
41 }
42}
43
44#[derive(Debug, Copy, Clone, PartialEq, Eq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47#[cfg_attr(feature = "serde", serde(transparent))]
48pub struct Temperature(pub i16);
49
50impl Temperature {
51 pub fn fahrenheit(self) -> i16 {
52 self.0
53 }
54 pub fn celsius(self) -> f32 {
55 (self.0 as f32 - 32.0) * 5.0 / 9.0
56 }
57 pub fn kelvin(self) -> f32 {
58 self.celsius() + 273.15
59 }
60}
61
62#[derive(Debug, Copy, Clone, PartialEq, Eq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65#[cfg_attr(feature = "serde", serde(transparent))]
66pub struct Rainfall(pub u16);
67
68impl Rainfall {
69 pub fn hundredths_inch(self) -> u16 {
70 self.0
71 }
72 pub fn inches(self) -> f32 {
73 self.0 as f32 / 100.0
74 }
75 pub fn mm(self) -> f32 {
76 self.inches() * 25.4
77 }
78}
79
80#[derive(Debug, Copy, Clone, PartialEq, Eq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83#[cfg_attr(feature = "serde", serde(transparent))]
84pub struct Humidity(pub u8);
85
86impl Humidity {
87 pub fn percent(self) -> u8 {
88 self.0
89 }
90}
91
92#[derive(Debug, Copy, Clone, PartialEq, Eq)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
97#[cfg_attr(feature = "serde", serde(transparent))]
98pub struct Pressure(pub u32);
99
100impl Pressure {
101 pub fn tenths_mbar(self) -> u32 {
102 self.0
103 }
104 pub fn hpa(self) -> f32 {
105 self.0 as f32 / 10.0
106 }
107 pub fn mbar(self) -> f32 {
108 self.hpa()
109 }
110}
111
112#[derive(Debug, Copy, Clone, PartialEq, Eq)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115#[cfg_attr(feature = "serde", serde(transparent))]
116pub struct Luminosity(pub u16);
117
118impl Luminosity {
119 pub fn w_per_m2(self) -> u16 {
120 self.0
121 }
122}
123
124#[derive(Debug, Copy, Clone, PartialEq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127#[cfg_attr(feature = "serde", serde(transparent))]
128pub struct Snowfall(pub f32);
129
130impl Snowfall {
131 pub fn tenths_inch(self) -> f32 {
132 self.0
133 }
134 pub fn inches(self) -> f32 {
135 self.0 / 10.0
136 }
137 pub fn cm(self) -> f32 {
138 self.inches() * 2.54
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Default)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
152pub struct AprsWeatherData {
153 pub wind_direction: Option<WindDirection>,
155 pub wind_speed: Option<WindSpeed>,
157 pub wind_gust: Option<WindSpeed>,
159 pub temperature: Option<Temperature>,
161 pub rain_last_hour: Option<Rainfall>,
163 pub rain_last_24h: Option<Rainfall>,
165 pub rain_since_midnight: Option<Rainfall>,
167 pub humidity: Option<Humidity>,
169 pub barometric_pressure: Option<Pressure>,
171 pub luminosity: Option<Luminosity>,
173 pub snow_last_24h: Option<Snowfall>,
175 pub raw_rain_counter: Option<u16>,
177}
178
179impl AprsWeatherData {
180 pub fn parse(b: &[u8]) -> Result<Self, AprsError> {
187 if b.len() < 7 {
188 return Err(AprsError::TruncatedPacket {
189 expected: 7,
190 got: b.len(),
191 });
192 }
193
194 let wind_direction = parse_opt_u16(&b[0..3]).map(WindDirection);
195
196 if b[3] != b'/' {
197 return Err(AprsError::TruncatedPacket {
198 expected: 7,
199 got: b.len(),
200 });
201 }
202
203 let wind_speed = parse_opt_u16(&b[4..7]).map(WindSpeed);
204
205 let mut wind_gust = None;
206 let mut temperature = None;
207 let mut rain_last_hour = None;
208 let mut rain_last_24h = None;
209 let mut rain_since_midnight = None;
210 let mut humidity = None;
211 let mut barometric_pressure = None;
212 let mut luminosity = None;
213 let mut snow_last_24h = None;
214 let mut raw_rain_counter = None;
215
216 let mut i = 7usize;
217 while i < b.len() {
218 let key = b[i];
219 i += 1;
220 match key {
221 b'g' => {
222 if i + 3 <= b.len() {
223 wind_gust = parse_opt_u16(&b[i..i + 3]).map(WindSpeed);
224 i += 3;
225 }
226 }
227 b't' => {
228 if i + 3 <= b.len() {
229 temperature = parse_opt_i16(&b[i..i + 3]).map(Temperature);
230 i += 3;
231 }
232 }
233 b'r' => {
234 if i + 3 <= b.len() {
235 rain_last_hour = parse_opt_u16(&b[i..i + 3]).map(Rainfall);
236 i += 3;
237 }
238 }
239 b'p' => {
240 if i + 3 <= b.len() {
241 rain_last_24h = parse_opt_u16(&b[i..i + 3]).map(Rainfall);
242 i += 3;
243 }
244 }
245 b'P' => {
246 if i + 3 <= b.len() {
247 rain_since_midnight = parse_opt_u16(&b[i..i + 3]).map(Rainfall);
248 i += 3;
249 }
250 }
251 b'h' => {
252 if i + 2 <= b.len() {
253 humidity = parse_opt_u16(&b[i..i + 2])
254 .map(|v| Humidity(if v == 0 { 100 } else { v as u8 }));
255 i += 2;
256 }
257 }
258 b'b' => {
259 if i + 5 <= b.len() {
260 barometric_pressure = parse_bytes::<u32>(&b[i..i + 5]).map(Pressure);
261 i += 5;
262 }
263 }
264 b'L' => {
265 if i + 3 <= b.len() {
266 luminosity = parse_opt_u16(&b[i..i + 3]).map(|v| Luminosity(v + 1000));
267 i += 3;
268 }
269 }
270 b'l' => {
271 if i + 3 <= b.len() {
272 luminosity = parse_opt_u16(&b[i..i + 3]).map(Luminosity);
273 i += 3;
274 }
275 }
276 b's' => {
277 if i + 3 <= b.len() {
278 snow_last_24h =
279 parse_opt_u16(&b[i..i + 3]).map(|v| Snowfall(v as f32 / 10.0));
280 i += 3;
281 }
282 }
283 b'#' => {
284 if i + 3 <= b.len() {
285 raw_rain_counter = parse_opt_u16(&b[i..i + 3]);
286 i += 3;
287 }
288 }
289 _ => break, }
291 }
292
293 Ok(Self {
294 wind_direction,
295 wind_speed,
296 wind_gust,
297 temperature,
298 rain_last_hour,
299 rain_last_24h,
300 rain_since_midnight,
301 humidity,
302 barometric_pressure,
303 luminosity,
304 snow_last_24h,
305 raw_rain_counter,
306 })
307 }
308
309 pub fn encode(&self, out: &mut Vec<u8>) {
311 match self.wind_direction {
312 Some(d) => out.extend_from_slice(format!("{:03}", d.degrees()).as_bytes()),
313 None => out.extend_from_slice(b"..."),
314 }
315 out.push(b'/');
316 match self.wind_speed {
317 Some(s) => out.extend_from_slice(format!("{:03}", s.mph()).as_bytes()),
318 None => out.extend_from_slice(b"..."),
319 }
320 if let Some(g) = self.wind_gust {
321 out.extend_from_slice(format!("g{:03}", g.mph()).as_bytes());
322 }
323 if let Some(t) = self.temperature {
324 out.extend_from_slice(format!("t{:03}", t.fahrenheit()).as_bytes());
325 }
326 if let Some(r) = self.rain_last_hour {
327 out.extend_from_slice(format!("r{:03}", r.hundredths_inch()).as_bytes());
328 }
329 if let Some(p) = self.rain_last_24h {
330 out.extend_from_slice(format!("p{:03}", p.hundredths_inch()).as_bytes());
331 }
332 if let Some(p) = self.rain_since_midnight {
333 out.extend_from_slice(format!("P{:03}", p.hundredths_inch()).as_bytes());
334 }
335 if let Some(h) = self.humidity {
336 let v = if h.percent() == 100 { 0 } else { h.percent() };
337 out.extend_from_slice(format!("h{:02}", v).as_bytes());
338 }
339 if let Some(b_val) = self.barometric_pressure {
340 out.extend_from_slice(format!("b{:05}", b_val.tenths_mbar()).as_bytes());
341 }
342 if let Some(l) = self.luminosity {
343 if l.w_per_m2() >= 1000 {
344 out.extend_from_slice(format!("L{:03}", l.w_per_m2() - 1000).as_bytes());
345 } else {
346 out.extend_from_slice(format!("l{:03}", l.w_per_m2()).as_bytes());
347 }
348 }
349 if let Some(s) = self.snow_last_24h {
350 out.extend_from_slice(format!("s{:03}", (s.tenths_inch() * 10.0) as u16).as_bytes());
351 }
352 if let Some(r) = self.raw_rain_counter {
353 out.extend_from_slice(format!("#{:03}", r).as_bytes());
354 }
355 }
356}
357
358#[derive(Debug, Clone, PartialEq)]
366#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
367pub struct AprsPositionlessWeather {
368 pub timestamp: Vec<u8>,
370 pub weather: AprsWeatherData,
371 pub comment: Vec<u8>,
372}
373
374impl AprsPositionlessWeather {
375 pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
377 if info.len() < 9 {
379 return Err(AprsError::TruncatedPacket {
380 expected: 9,
381 got: info.len(),
382 });
383 }
384 let timestamp = info[1..9].to_vec();
385 let weather_bytes = &info[9..];
386 let weather = AprsWeatherData::parse(weather_bytes)?;
387 Ok(Self {
388 timestamp,
389 weather,
390 comment: vec![],
391 })
392 }
393
394 pub fn encode(&self) -> Vec<u8> {
395 let mut out = vec![b'_'];
396 out.extend_from_slice(&self.timestamp);
397 self.weather.encode(&mut out);
398 out.extend_from_slice(&self.comment);
399 out
400 }
401}
402
403fn parse_opt_u16(b: &[u8]) -> Option<u16> {
407 if b.iter().all(|&c| c == b'.' || c == b' ') {
408 return None;
409 }
410 parse_bytes(b)
411}
412
413fn parse_opt_i16(b: &[u8]) -> Option<i16> {
415 if b.iter().all(|&c| c == b'.' || c == b' ') {
416 return None;
417 }
418 parse_bytes(b)
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn parse_full_weather() {
427 let wx = AprsWeatherData::parse(b"220/004g005t077r000p000P000h50b09900").unwrap();
428 assert_eq!(wx.wind_direction.unwrap().degrees(), 220);
429 assert_eq!(wx.wind_speed.unwrap().mph(), 4);
430 assert_eq!(wx.wind_gust.unwrap().mph(), 5);
431 assert_eq!(wx.temperature.unwrap().fahrenheit(), 77);
432 assert_eq!(wx.rain_last_hour.unwrap().hundredths_inch(), 0);
433 assert_eq!(wx.humidity.unwrap().percent(), 50);
434 assert_eq!(wx.barometric_pressure.unwrap().tenths_mbar(), 9900);
435 }
436
437 #[test]
438 fn temperature_conversion() {
439 let t = Temperature(32); assert!((t.celsius() - 0.0).abs() < 0.01);
441 let t = Temperature(212); assert!((t.celsius() - 100.0).abs() < 0.01);
443 }
444
445 #[test]
446 fn wind_speed_conversion() {
447 let s = WindSpeed(10); assert!((s.knots() - 8.68976).abs() < 0.001);
449 assert!((s.kph() - 16.09344).abs() < 0.001);
450 }
451
452 #[test]
453 fn pressure_conversion() {
454 let p = Pressure(10250);
455 assert!((p.hpa() - 1025.0).abs() < 0.01);
456 }
457
458 #[test]
459 fn rainfall_conversion() {
460 let r = Rainfall(100); assert!((r.inches() - 1.0).abs() < 0.001);
462 assert!((r.mm() - 25.4).abs() < 0.01);
463 }
464
465 #[test]
466 fn humidity_100_encoded_as_00() {
467 let wx = AprsWeatherData::parse(b"000/000h00").unwrap();
468 assert_eq!(wx.humidity.unwrap().percent(), 100);
469 }
470
471 #[test]
472 fn negative_temperature() {
473 let wx = AprsWeatherData::parse(b"000/000g000t-10").unwrap();
474 assert_eq!(wx.temperature.unwrap().fahrenheit(), -10);
475 }
476
477 #[test]
478 fn luminosity_high() {
479 let wx = AprsWeatherData::parse(b"000/000L042").unwrap();
480 assert_eq!(wx.luminosity.unwrap().w_per_m2(), 1042);
481 }
482
483 #[test]
484 fn unknown_fields_stop_parsing() {
485 let wx = AprsWeatherData::parse(b"220/004g005XUNKNOWN").unwrap();
487 assert_eq!(wx.wind_direction.unwrap().degrees(), 220);
488 assert_eq!(wx.wind_gust.unwrap().mph(), 5);
489 assert!(wx.temperature.is_none());
490 }
491
492 #[test]
493 fn encode_round_trip() {
494 let raw = b"220/004g005t077r000p000P000h50b09900";
495 let wx = AprsWeatherData::parse(raw).unwrap();
496 let mut out = Vec::new();
497 wx.encode(&mut out);
498 assert_eq!(out.as_slice(), raw.as_slice());
499 }
500
501 #[test]
502 fn positionless_parse() {
503 let pw = AprsPositionlessWeather::parse(b"_10071820220/004g005t077").unwrap();
504 assert_eq!(pw.timestamp, b"10071820");
505 assert_eq!(pw.weather.wind_direction.unwrap().degrees(), 220);
506 assert_eq!(pw.weather.temperature.unwrap().fahrenheit(), 77);
507 }
508
509 #[test]
510 fn positionless_encode_round_trip() {
511 let raw = b"_10071820220/004g005t077";
512 let pw = AprsPositionlessWeather::parse(raw).unwrap();
513 assert_eq!(pw.encode().as_slice(), raw.as_slice());
514 }
515}