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 { self.0 }
19}
20
21#[derive(Debug, Copy, Clone, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[cfg_attr(feature = "serde", serde(transparent))]
25pub struct WindSpeed(pub u16);
26
27impl WindSpeed {
28 pub fn mph(self) -> u16 { self.0 }
29 pub fn knots(self) -> f32 { self.0 as f32 * 0.868_976 }
30 pub fn kph(self) -> f32 { self.0 as f32 * 1.609_344 }
31 pub fn m_per_s(self) -> f32 { self.0 as f32 * 0.447_04 }
32}
33
34#[derive(Debug, Copy, Clone, PartialEq, Eq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37#[cfg_attr(feature = "serde", serde(transparent))]
38pub struct Temperature(pub i16);
39
40impl Temperature {
41 pub fn fahrenheit(self) -> i16 { self.0 }
42 pub fn celsius(self) -> f32 { (self.0 as f32 - 32.0) * 5.0 / 9.0 }
43 pub fn kelvin(self) -> f32 { self.celsius() + 273.15 }
44}
45
46#[derive(Debug, Copy, Clone, PartialEq, Eq)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[cfg_attr(feature = "serde", serde(transparent))]
50pub struct Rainfall(pub u16);
51
52impl Rainfall {
53 pub fn hundredths_inch(self) -> u16 { self.0 }
54 pub fn inches(self) -> f32 { self.0 as f32 / 100.0 }
55 pub fn mm(self) -> f32 { self.inches() * 25.4 }
56}
57
58#[derive(Debug, Copy, Clone, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61#[cfg_attr(feature = "serde", serde(transparent))]
62pub struct Humidity(pub u8);
63
64impl Humidity {
65 pub fn percent(self) -> u8 { self.0 }
66}
67
68#[derive(Debug, Copy, Clone, PartialEq, Eq)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73#[cfg_attr(feature = "serde", serde(transparent))]
74pub struct Pressure(pub u32);
75
76impl Pressure {
77 pub fn tenths_mbar(self) -> u32 { self.0 }
78 pub fn hpa(self) -> f32 { self.0 as f32 / 10.0 }
79 pub fn mbar(self) -> f32 { self.hpa() }
80}
81
82#[derive(Debug, Copy, Clone, PartialEq, Eq)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85#[cfg_attr(feature = "serde", serde(transparent))]
86pub struct Luminosity(pub u16);
87
88impl Luminosity {
89 pub fn w_per_m2(self) -> u16 { self.0 }
90}
91
92#[derive(Debug, Copy, Clone, PartialEq)]
94#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95#[cfg_attr(feature = "serde", serde(transparent))]
96pub struct Snowfall(pub f32);
97
98impl Snowfall {
99 pub fn tenths_inch(self) -> f32 { self.0 }
100 pub fn inches(self) -> f32 { self.0 / 10.0 }
101 pub fn cm(self) -> f32 { self.inches() * 2.54 }
102}
103
104#[derive(Debug, Clone, PartialEq, Default)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114pub struct AprsWeatherData {
115 pub wind_direction: Option<WindDirection>,
117 pub wind_speed: Option<WindSpeed>,
119 pub wind_gust: Option<WindSpeed>,
121 pub temperature: Option<Temperature>,
123 pub rain_last_hour: Option<Rainfall>,
125 pub rain_last_24h: Option<Rainfall>,
127 pub rain_since_midnight: Option<Rainfall>,
129 pub humidity: Option<Humidity>,
131 pub barometric_pressure: Option<Pressure>,
133 pub luminosity: Option<Luminosity>,
135 pub snow_last_24h: Option<Snowfall>,
137 pub raw_rain_counter: Option<u16>,
139}
140
141impl AprsWeatherData {
142 pub fn parse(b: &[u8]) -> Result<Self, AprsError> {
149 if b.len() < 7 {
150 return Err(AprsError::TruncatedPacket { expected: 7, got: b.len() });
151 }
152
153 let wind_direction = parse_opt_u16(&b[0..3]).map(WindDirection);
154
155 if b[3] != b'/' {
156 return Err(AprsError::TruncatedPacket { expected: 7, got: b.len() });
157 }
158
159 let wind_speed = parse_opt_u16(&b[4..7]).map(WindSpeed);
160
161 let mut wind_gust = None;
162 let mut temperature = None;
163 let mut rain_last_hour = None;
164 let mut rain_last_24h = None;
165 let mut rain_since_midnight = None;
166 let mut humidity = None;
167 let mut barometric_pressure = None;
168 let mut luminosity = None;
169 let mut snow_last_24h = None;
170 let mut raw_rain_counter = None;
171
172 let mut i = 7usize;
173 while i < b.len() {
174 let key = b[i];
175 i += 1;
176 match key {
177 b'g' => { if i + 3 <= b.len() { wind_gust = parse_opt_u16(&b[i..i+3]).map(WindSpeed); i += 3; } }
178 b't' => { if i + 3 <= b.len() { temperature = parse_opt_i16(&b[i..i+3]).map(Temperature); i += 3; } }
179 b'r' => { if i + 3 <= b.len() { rain_last_hour = parse_opt_u16(&b[i..i+3]).map(Rainfall); i += 3; } }
180 b'p' => { if i + 3 <= b.len() { rain_last_24h = parse_opt_u16(&b[i..i+3]).map(Rainfall); i += 3; } }
181 b'P' => { if i + 3 <= b.len() { rain_since_midnight = parse_opt_u16(&b[i..i+3]).map(Rainfall); i += 3; } }
182 b'h' => {
183 if i + 2 <= b.len() {
184 humidity = parse_opt_u16(&b[i..i+2]).map(|v| Humidity(if v == 0 { 100 } else { v as u8 }));
185 i += 2;
186 }
187 }
188 b'b' => {
189 if i + 5 <= b.len() {
190 barometric_pressure = parse_bytes::<u32>(&b[i..i+5]).map(Pressure);
191 i += 5;
192 }
193 }
194 b'L' => { if i + 3 <= b.len() { luminosity = parse_opt_u16(&b[i..i+3]).map(|v| Luminosity(v + 1000)); i += 3; } }
195 b'l' => { if i + 3 <= b.len() { luminosity = parse_opt_u16(&b[i..i+3]).map(Luminosity); i += 3; } }
196 b's' => {
197 if i + 3 <= b.len() {
198 snow_last_24h = parse_opt_u16(&b[i..i+3]).map(|v| Snowfall(v as f32 / 10.0));
199 i += 3;
200 }
201 }
202 b'#' => { if i + 3 <= b.len() { raw_rain_counter = parse_opt_u16(&b[i..i+3]); i += 3; } }
203 _ => break, }
205 }
206
207 Ok(Self {
208 wind_direction,
209 wind_speed,
210 wind_gust,
211 temperature,
212 rain_last_hour,
213 rain_last_24h,
214 rain_since_midnight,
215 humidity,
216 barometric_pressure,
217 luminosity,
218 snow_last_24h,
219 raw_rain_counter,
220 })
221 }
222
223 pub fn encode(&self, out: &mut Vec<u8>) {
225 match self.wind_direction {
226 Some(d) => out.extend_from_slice(format!("{:03}", d.degrees()).as_bytes()),
227 None => out.extend_from_slice(b"..."),
228 }
229 out.push(b'/');
230 match self.wind_speed {
231 Some(s) => out.extend_from_slice(format!("{:03}", s.mph()).as_bytes()),
232 None => out.extend_from_slice(b"..."),
233 }
234 if let Some(g) = self.wind_gust { out.extend_from_slice(format!("g{:03}", g.mph()).as_bytes()); }
235 if let Some(t) = self.temperature { out.extend_from_slice(format!("t{:03}", t.fahrenheit()).as_bytes()); }
236 if let Some(r) = self.rain_last_hour { out.extend_from_slice(format!("r{:03}", r.hundredths_inch()).as_bytes()); }
237 if let Some(p) = self.rain_last_24h { out.extend_from_slice(format!("p{:03}", p.hundredths_inch()).as_bytes()); }
238 if let Some(p) = self.rain_since_midnight{ out.extend_from_slice(format!("P{:03}", p.hundredths_inch()).as_bytes()); }
239 if let Some(h) = self.humidity {
240 let v = if h.percent() == 100 { 0 } else { h.percent() };
241 out.extend_from_slice(format!("h{:02}", v).as_bytes());
242 }
243 if let Some(b_val) = self.barometric_pressure { out.extend_from_slice(format!("b{:05}", b_val.tenths_mbar()).as_bytes()); }
244 if let Some(l) = self.luminosity {
245 if l.w_per_m2() >= 1000 {
246 out.extend_from_slice(format!("L{:03}", l.w_per_m2() - 1000).as_bytes());
247 } else {
248 out.extend_from_slice(format!("l{:03}", l.w_per_m2()).as_bytes());
249 }
250 }
251 if let Some(s) = self.snow_last_24h { out.extend_from_slice(format!("s{:03}", (s.tenths_inch() * 10.0) as u16).as_bytes()); }
252 if let Some(r) = self.raw_rain_counter { out.extend_from_slice(format!("#{:03}", r).as_bytes()); }
253 }
254}
255
256#[derive(Debug, Clone, PartialEq)]
264#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
265pub struct AprsPositionlessWeather {
266 pub timestamp: Vec<u8>,
268 pub weather: AprsWeatherData,
269 pub comment: Vec<u8>,
270}
271
272impl AprsPositionlessWeather {
273 pub(crate) fn parse(info: &[u8]) -> Result<Self, AprsError> {
275 if info.len() < 9 {
277 return Err(AprsError::TruncatedPacket { expected: 9, got: info.len() });
278 }
279 let timestamp = info[1..9].to_vec();
280 let weather_bytes = &info[9..];
281 let weather = AprsWeatherData::parse(weather_bytes)?;
282 Ok(Self { timestamp, weather, comment: vec![] })
283 }
284
285 pub fn encode(&self) -> Vec<u8> {
286 let mut out = vec![b'_'];
287 out.extend_from_slice(&self.timestamp);
288 self.weather.encode(&mut out);
289 out.extend_from_slice(&self.comment);
290 out
291 }
292}
293
294fn parse_opt_u16(b: &[u8]) -> Option<u16> {
298 if b.iter().all(|&c| c == b'.' || c == b' ') {
299 return None;
300 }
301 parse_bytes(b)
302}
303
304fn parse_opt_i16(b: &[u8]) -> Option<i16> {
306 if b.iter().all(|&c| c == b'.' || c == b' ') {
307 return None;
308 }
309 parse_bytes(b)
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn parse_full_weather() {
318 let wx = AprsWeatherData::parse(b"220/004g005t077r000p000P000h50b09900").unwrap();
319 assert_eq!(wx.wind_direction.unwrap().degrees(), 220);
320 assert_eq!(wx.wind_speed.unwrap().mph(), 4);
321 assert_eq!(wx.wind_gust.unwrap().mph(), 5);
322 assert_eq!(wx.temperature.unwrap().fahrenheit(), 77);
323 assert_eq!(wx.rain_last_hour.unwrap().hundredths_inch(), 0);
324 assert_eq!(wx.humidity.unwrap().percent(), 50);
325 assert_eq!(wx.barometric_pressure.unwrap().tenths_mbar(), 9900);
326 }
327
328 #[test]
329 fn temperature_conversion() {
330 let t = Temperature(32); assert!((t.celsius() - 0.0).abs() < 0.01);
332 let t = Temperature(212); assert!((t.celsius() - 100.0).abs() < 0.01);
334 }
335
336 #[test]
337 fn wind_speed_conversion() {
338 let s = WindSpeed(10); assert!((s.knots() - 8.68976).abs() < 0.001);
340 assert!((s.kph() - 16.09344).abs() < 0.001);
341 }
342
343 #[test]
344 fn pressure_conversion() {
345 let p = Pressure(10250);
346 assert!((p.hpa() - 1025.0).abs() < 0.01);
347 }
348
349 #[test]
350 fn rainfall_conversion() {
351 let r = Rainfall(100); assert!((r.inches() - 1.0).abs() < 0.001);
353 assert!((r.mm() - 25.4).abs() < 0.01);
354 }
355
356 #[test]
357 fn humidity_100_encoded_as_00() {
358 let wx = AprsWeatherData::parse(b"000/000h00").unwrap();
359 assert_eq!(wx.humidity.unwrap().percent(), 100);
360 }
361
362 #[test]
363 fn negative_temperature() {
364 let wx = AprsWeatherData::parse(b"000/000g000t-10").unwrap();
365 assert_eq!(wx.temperature.unwrap().fahrenheit(), -10);
366 }
367
368 #[test]
369 fn luminosity_high() {
370 let wx = AprsWeatherData::parse(b"000/000L042").unwrap();
371 assert_eq!(wx.luminosity.unwrap().w_per_m2(), 1042);
372 }
373
374 #[test]
375 fn unknown_fields_stop_parsing() {
376 let wx = AprsWeatherData::parse(b"220/004g005XUNKNOWN").unwrap();
378 assert_eq!(wx.wind_direction.unwrap().degrees(), 220);
379 assert_eq!(wx.wind_gust.unwrap().mph(), 5);
380 assert!(wx.temperature.is_none());
381 }
382
383 #[test]
384 fn encode_round_trip() {
385 let raw = b"220/004g005t077r000p000P000h50b09900";
386 let wx = AprsWeatherData::parse(raw).unwrap();
387 let mut out = Vec::new();
388 wx.encode(&mut out);
389 assert_eq!(out.as_slice(), raw.as_slice());
390 }
391
392 #[test]
393 fn positionless_parse() {
394 let pw = AprsPositionlessWeather::parse(b"_10071820220/004g005t077").unwrap();
395 assert_eq!(pw.timestamp, b"10071820");
396 assert_eq!(pw.weather.wind_direction.unwrap().degrees(), 220);
397 assert_eq!(pw.weather.temperature.unwrap().fahrenheit(), 77);
398 }
399
400 #[test]
401 fn positionless_encode_round_trip() {
402 let raw = b"_10071820220/004g005t077";
403 let pw = AprsPositionlessWeather::parse(raw).unwrap();
404 assert_eq!(pw.encode().as_slice(), raw.as_slice());
405 }
406}