ogn_parser/
status_comment.rs

1use serde::Serialize;
2use std::{convert::Infallible, str::FromStr};
3
4use crate::utils::{extract_values, split_value_unit};
5
6#[derive(Debug, PartialEq, Default, Clone, Serialize)]
7pub struct StatusComment {
8    #[serde(skip_serializing_if = "Option::is_none")]
9    pub version: Option<String>,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub platform: Option<String>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub cpu_load: Option<f32>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub ram_free: Option<f32>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub ram_total: Option<f32>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub ntp_offset: Option<f32>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub ntp_correction: Option<f32>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub voltage: Option<f32>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub amperage: Option<f32>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub cpu_temperature: Option<f32>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub visible_senders: Option<u16>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub latency: Option<f32>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub senders: Option<u16>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub rf_correction_manual: Option<i16>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub rf_correction_automatic: Option<f32>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub noise: Option<f32>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub senders_signal_quality: Option<f32>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub senders_messages: Option<u32>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub good_senders_signal_quality: Option<f32>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub good_senders: Option<u16>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub good_and_bad_senders: Option<u16>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub unparsed: Option<String>,
52}
53
54impl FromStr for StatusComment {
55    type Err = Infallible;
56    fn from_str(s: &str) -> Result<Self, Self::Err> {
57        let mut status_comment = StatusComment {
58            ..Default::default()
59        };
60        let mut unparsed: Vec<_> = vec![];
61        for part in s.split_whitespace() {
62            // receiver software version: vX.Y.Z
63            // X (major)
64            // Y (minor)
65            // Z (bugfix)
66            if &part[0..1] == "v"
67                && part.matches('.').count() == 3
68                && status_comment.version.is_none()
69            {
70                let (first, second) = part
71                    .match_indices('.')
72                    .nth(2)
73                    .map(|(idx, _)| part.split_at(idx))
74                    .unwrap();
75                status_comment.version = Some(first[1..].into());
76                status_comment.platform = Some(second[1..].into());
77
78            // cpu load: CPU:x.x
79            // x.x: cpu load as percentage
80            } else if part.len() > 4
81                && part.starts_with("CPU:")
82                && status_comment.cpu_load.is_none()
83            {
84                if let Ok(cpu_load) = part[4..].parse::<f32>() {
85                    status_comment.cpu_load = Some(cpu_load);
86                } else {
87                    unparsed.push(part);
88                }
89
90            // RAM usage: RAM:x.x/y.yMB
91            // x.x: free RAM in MB
92            // y.y: total RAM in MB
93            } else if part.len() > 6
94                && part.starts_with("RAM:")
95                && part.ends_with("MB")
96                && part.find('/').is_some()
97                && status_comment.ram_free.is_none()
98            {
99                let subpart = &part[4..part.len() - 2];
100                let split_point = subpart.find('/').unwrap();
101                let (first, second) = subpart.split_at(split_point);
102                let ram_free = first.parse::<f32>().ok();
103                let ram_total = second[1..].parse::<f32>().ok();
104                if ram_free.is_some() && ram_total.is_some() {
105                    status_comment.ram_free = ram_free;
106                    status_comment.ram_total = ram_total;
107                } else {
108                    unparsed.push(part);
109                }
110
111            // time synchronisation: NTP:x.xms/y.yppm
112            // x.x: NTP offset in [ms]
113            // y.y: NTP correction in [ppm]
114            } else if part.len() > 6
115                && part.starts_with("NTP:")
116                && part.find('/').is_some()
117                && status_comment.ntp_offset.is_none()
118            {
119                let subpart = &part[4..part.len() - 3];
120                let split_point = subpart.find('/').unwrap();
121                let (first, second) = subpart.split_at(split_point);
122                let ntp_offset = first[0..first.len() - 2].parse::<f32>().ok();
123                let ntp_correction = second[1..].parse::<f32>().ok();
124                if ntp_offset.is_some() && ntp_correction.is_some() {
125                    status_comment.ntp_offset = ntp_offset;
126                    status_comment.ntp_correction = ntp_correction;
127                } else {
128                    unparsed.push(part);
129                }
130
131            // senders count: x/yAcfts[1h]
132            // x: visible senders in the last hour
133            // y: total senders in the last hour
134            } else if part.len() >= 11
135                && part.ends_with("Acfts[1h]")
136                && part.find('/').is_some()
137                && status_comment.visible_senders.is_none()
138            {
139                let subpart = &part[0..part.len() - 9];
140                let split_point = subpart.find('/').unwrap();
141                let (first, second) = subpart.split_at(split_point);
142                let visible_senders = first.parse::<u16>().ok();
143                let senders = second[1..].parse::<u16>().ok();
144                if visible_senders.is_some() && senders.is_some() {
145                    status_comment.visible_senders = visible_senders;
146                    status_comment.senders = senders;
147                } else {
148                    unparsed.push(part);
149                }
150
151            // latency: Lat:x.xs
152            // x.x: latency in [s]
153            } else if part.len() > 5
154                && part.starts_with("Lat:")
155                && part.ends_with("s")
156                && status_comment.latency.is_none()
157            {
158                let latency = part[4..part.len() - 1].parse::<f32>().ok();
159                if latency.is_some() {
160                    status_comment.latency = latency;
161                } else {
162                    unparsed.push(part);
163                }
164
165            // radio frequency informations start with "RF:"
166            } else if part.len() >= 11
167                && part.starts_with("RF:")
168                && status_comment.rf_correction_manual.is_none()
169            {
170                let values = extract_values(part);
171                // short RF format: RF:+x.x/y.yppm/+z.zdB
172                // x.x: manual correction in [ppm]
173                // y.y: automatic correction in [ppm]
174                // z.z: background noise in [dB]
175                if values.len() == 3 {
176                    let rf_correction_manual = values[0].parse::<i16>().ok();
177                    let rf_correction_automatic = values[1].parse::<f32>().ok();
178                    let noise = values[2].parse::<f32>().ok();
179
180                    if rf_correction_manual.is_some()
181                        && rf_correction_automatic.is_some()
182                        && noise.is_some()
183                    {
184                        status_comment.rf_correction_manual = rf_correction_manual;
185                        status_comment.rf_correction_automatic = rf_correction_automatic;
186                        status_comment.noise = noise;
187                    } else {
188                        unparsed.push(part);
189                        continue;
190                    }
191                // medium RF format: RF:+x.x/y.yppm/+z.zdB/+a.adB@10km[b]
192                // a.a: sender signal quality [dB]
193                // b: number of messages
194                } else if values.len() == 6 {
195                    let rf_correction_manual = values[0].parse::<i16>().ok();
196                    let rf_correction_automatic = values[1].parse::<f32>().ok();
197                    let noise = values[2].parse::<f32>().ok();
198                    let senders_signal_quality = values[3].parse::<f32>().ok();
199                    let senders_messages = values[5].parse::<u32>().ok();
200                    if rf_correction_manual.is_some()
201                        && rf_correction_automatic.is_some()
202                        && noise.is_some()
203                        && senders_signal_quality.is_some()
204                        && senders_messages.is_some()
205                    {
206                        status_comment.rf_correction_manual = rf_correction_manual;
207                        status_comment.rf_correction_automatic = rf_correction_automatic;
208                        status_comment.noise = noise;
209                        status_comment.senders_signal_quality = senders_signal_quality;
210                        status_comment.senders_messages = senders_messages;
211                    } else {
212                        unparsed.push(part);
213                        continue;
214                    }
215                // long RF format: RF:+x.x/y.yppm/+z.zdB/+a.adB@10km[b]/+c.cdB@10km[d/e]
216                // c.c: good senders signal quality [dB]
217                // d: number of good senders
218                // e: number of good and bad senders
219                } else if values.len() == 10 {
220                    let rf_correction_manual = values[0].parse::<i16>().ok();
221                    let rf_correction_automatic = values[1].parse::<f32>().ok();
222                    let noise = values[2].parse::<f32>().ok();
223                    let senders_signal_quality = values[3].parse::<f32>().ok();
224                    let senders_messages = values[5].parse::<u32>().ok();
225                    let good_senders_signal_quality = values[6].parse::<f32>().ok();
226                    let good_senders = values[8].parse::<u16>().ok();
227                    let good_and_bad_senders = values[9].parse::<u16>().ok();
228                    if rf_correction_manual.is_some()
229                        && rf_correction_automatic.is_some()
230                        && noise.is_some()
231                        && senders_signal_quality.is_some()
232                        && senders_messages.is_some()
233                        && good_senders_signal_quality.is_some()
234                        && good_senders.is_some()
235                        && good_and_bad_senders.is_some()
236                    {
237                        status_comment.rf_correction_manual = rf_correction_manual;
238                        status_comment.rf_correction_automatic = rf_correction_automatic;
239                        status_comment.noise = noise;
240                        status_comment.senders_signal_quality = senders_signal_quality;
241                        status_comment.senders_messages = senders_messages;
242                        status_comment.good_senders_signal_quality = good_senders_signal_quality;
243                        status_comment.good_senders = good_senders;
244                        status_comment.good_and_bad_senders = good_and_bad_senders;
245                    } else {
246                        unparsed.push(part);
247                        continue;
248                    }
249                } else {
250                    unparsed.push(part);
251                    continue;
252                }
253            } else if let Some((value, unit)) = split_value_unit(part) {
254                // cpu temperature: +x.xC
255                // x.x: cpu temperature in [°C]
256                if unit == "C" && status_comment.cpu_temperature.is_none() {
257                    status_comment.cpu_temperature = value.parse::<f32>().ok();
258                // voltage: +x.xV
259                // x.x: voltage in [V]
260                } else if unit == "V" && status_comment.voltage.is_none() {
261                    status_comment.voltage = value.parse::<f32>().ok();
262                // currency: +x.xA
263                // x.x: currency in [A]
264                } else if unit == "A" && status_comment.amperage.is_none() {
265                    status_comment.amperage = value.parse::<f32>().ok();
266                } else {
267                    unparsed.push(part);
268                }
269            } else {
270                unparsed.push(part);
271            }
272        }
273        status_comment.unparsed = if !unparsed.is_empty() {
274            Some(unparsed.join(" "))
275        } else {
276            None
277        };
278
279        Ok(status_comment)
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_sdr() {
289        let result = "v0.2.7.RPI-GPU CPU:0.7 RAM:770.2/968.2MB NTP:1.8ms/-3.3ppm +55.7C 7/8Acfts[1h] RF:+54-1.1ppm/-0.16dB/+7.1dB@10km[19481]/+16.8dB@10km[7/13]".parse::<StatusComment>().unwrap();
290        assert_eq!(
291            result,
292            StatusComment {
293                version: Some("0.2.7".into()),
294                platform: Some("RPI-GPU".into()),
295                cpu_load: Some(0.7),
296                ram_free: Some(770.2),
297                ram_total: Some(968.2),
298                ntp_offset: Some(1.8),
299                ntp_correction: Some(-3.3),
300                voltage: None,
301                amperage: None,
302                cpu_temperature: Some(55.7),
303                visible_senders: Some(7),
304                senders: Some(8),
305                rf_correction_manual: Some(54),
306                rf_correction_automatic: Some(-1.1),
307                noise: Some(-0.16),
308                senders_signal_quality: Some(7.1),
309                senders_messages: Some(19481),
310                good_senders_signal_quality: Some(16.8),
311                good_senders: Some(7),
312                good_and_bad_senders: Some(13),
313                ..Default::default()
314            }
315        );
316    }
317
318    #[test]
319    fn test_sdr_different_order() {
320        let result = "NTP:1.8ms/-3.3ppm +55.7C CPU:0.7 RAM:770.2/968.2MB 7/8Acfts[1h] RF:+54-1.1ppm/-0.16dB/+7.1dB@10km[19481]/+16.8dB@10km[7/13] v0.2.7.RPI-GPU".parse::<StatusComment>().unwrap();
321        assert_eq!(
322            result,
323            StatusComment {
324                version: Some("0.2.7".into()),
325                platform: Some("RPI-GPU".into()),
326                cpu_load: Some(0.7),
327                ram_free: Some(770.2),
328                ram_total: Some(968.2),
329                ntp_offset: Some(1.8),
330                ntp_correction: Some(-3.3),
331                voltage: None,
332                amperage: None,
333                cpu_temperature: Some(55.7),
334                visible_senders: Some(7),
335                senders: Some(8),
336                rf_correction_manual: Some(54),
337                rf_correction_automatic: Some(-1.1),
338                noise: Some(-0.16),
339                senders_signal_quality: Some(7.1),
340                senders_messages: Some(19481),
341                good_senders_signal_quality: Some(16.8),
342                good_senders: Some(7),
343                good_and_bad_senders: Some(13),
344                ..Default::default()
345            }
346        );
347    }
348
349    #[test]
350    fn test_rf_3() {
351        let result = "RF:+29+0.0ppm/+35.22dB".parse::<StatusComment>().unwrap();
352        assert_eq!(
353            result,
354            StatusComment {
355                rf_correction_manual: Some(29),
356                rf_correction_automatic: Some(0.0),
357                noise: Some(35.22),
358                ..Default::default()
359            }
360        )
361    }
362
363    #[test]
364    fn test_rf_6() {
365        let result = "RF:+41+56.0ppm/-1.87dB/+0.1dB@10km[1928]"
366            .parse::<StatusComment>()
367            .unwrap();
368        assert_eq!(
369            result,
370            StatusComment {
371                rf_correction_manual: Some(41),
372                rf_correction_automatic: Some(56.0),
373                noise: Some(-1.87),
374                senders_signal_quality: Some(0.1),
375                senders_messages: Some(1928),
376                ..Default::default()
377            }
378        )
379    }
380
381    #[test]
382    fn test_rf_10() {
383        let result = "RF:+54-1.1ppm/-0.16dB/+7.1dB@10km[19481]/+16.8dB@10km[7/13]"
384            .parse::<StatusComment>()
385            .unwrap();
386        assert_eq!(
387            result,
388            StatusComment {
389                rf_correction_manual: Some(54),
390                rf_correction_automatic: Some(-1.1),
391                noise: Some(-0.16),
392                senders_signal_quality: Some(7.1),
393                senders_messages: Some(19481),
394                good_senders_signal_quality: Some(16.8),
395                good_senders: Some(7),
396                good_and_bad_senders: Some(13),
397                ..Default::default()
398            }
399        )
400    }
401}