Skip to main content

rns_embedded_mininode/
telemetry.rs

1use alloc::{
2    collections::BTreeMap,
3    string::{String, ToString},
4    vec,
5    vec::Vec,
6};
7
8use rmpv::Value;
9
10use crate::error::MiniNodeError;
11
12#[derive(Debug, Clone, PartialEq)]
13pub enum TelemetryValue {
14    Integer(i64),
15    Unsigned(u64),
16    Float(f64),
17    Bool(bool),
18    Text(String),
19}
20
21impl TelemetryValue {
22    pub fn to_rmpv(&self) -> Value {
23        match self {
24            Self::Integer(value) => Value::Integer((*value).into()),
25            Self::Unsigned(value) => Value::Integer((*value).into()),
26            Self::Float(value) => Value::F64(*value),
27            Self::Bool(value) => Value::Boolean(*value),
28            Self::Text(value) => Value::String(value.as_str().into()),
29        }
30    }
31}
32
33#[derive(Debug, Clone, PartialEq)]
34pub struct TelemetryPoint {
35    pub ts_ms: u64,
36    pub key: String,
37    pub value: TelemetryValue,
38    pub unit: Option<String>,
39    pub tags: BTreeMap<String, String>,
40}
41
42impl TelemetryPoint {
43    pub fn to_rmpv(&self) -> Value {
44        let mut entries = vec![
45            (Value::String("ts_ms".into()), Value::Integer(self.ts_ms.into())),
46            (Value::String("key".into()), Value::String(self.key.as_str().into())),
47            (Value::String("value".into()), self.value.to_rmpv()),
48        ];
49
50        if let Some(unit) = &self.unit {
51            entries.push((Value::String("unit".into()), Value::String(unit.as_str().into())));
52        }
53
54        if !self.tags.is_empty() {
55            let tags = self
56                .tags
57                .iter()
58                .map(|(key, value)| {
59                    (Value::String(key.as_str().into()), Value::String(value.as_str().into()))
60                })
61                .collect();
62            entries.push((Value::String("tags".into()), Value::Map(tags)));
63        }
64
65        Value::Map(entries)
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct TelemetryQuery {
71    pub from_ts_ms: Option<u64>,
72    pub to_ts_ms: Option<u64>,
73    pub key_prefix: Option<String>,
74    pub limit: Option<usize>,
75}
76
77impl TelemetryQuery {
78    pub fn matches(&self, point: &TelemetryPoint) -> bool {
79        if let Some(from) = self.from_ts_ms {
80            if point.ts_ms < from {
81                return false;
82            }
83        }
84        if let Some(to) = self.to_ts_ms {
85            if point.ts_ms > to {
86                return false;
87            }
88        }
89        if let Some(prefix) = &self.key_prefix {
90            if !point.key.starts_with(prefix) {
91                return false;
92            }
93        }
94        true
95    }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
99pub struct PositionFix {
100    pub ts_ms: u64,
101    pub lat: f64,
102    pub lon: f64,
103    pub alt_m: Option<f64>,
104    pub speed_mps: Option<f64>,
105    pub heading_deg: Option<f64>,
106    pub accuracy_m: Option<f64>,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct BatteryStatus {
111    pub ts_ms: u64,
112    pub pct: u8,
113    pub millivolts: u16,
114    pub charging: bool,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq)]
118pub struct LinkStats {
119    pub ts_ms: u64,
120    pub rssi_dbm: Option<i16>,
121    pub snr_db: Option<f32>,
122    pub airtime_ms: u32,
123    pub rx_ok: u32,
124    pub rx_drop: u32,
125    pub tx_ok: u32,
126    pub tx_drop: u32,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq)]
130pub struct DeviceHealth {
131    pub ts_ms: u64,
132    pub uptime_ms: u64,
133    pub free_bytes: Option<u32>,
134    pub temperature_c: Option<f32>,
135}
136
137#[derive(Debug, Clone, PartialEq)]
138pub enum TelemetrySample {
139    PositionFix(PositionFix),
140    BatteryStatus(BatteryStatus),
141    LinkStats(LinkStats),
142    DeviceHealth(DeviceHealth),
143    Point(TelemetryPoint),
144}
145
146impl TelemetrySample {
147    pub fn into_points(self, source_hash: [u8; 16]) -> (Vec<TelemetryPoint>, Option<PositionFix>) {
148        let mut tags = BTreeMap::new();
149        tags.insert("source".to_string(), hex::encode(source_hash));
150
151        match self {
152            Self::PositionFix(fix) => {
153                let mut points = vec![
154                    TelemetryPoint {
155                        ts_ms: fix.ts_ms,
156                        key: "position.lat".to_string(),
157                        value: TelemetryValue::Float(fix.lat),
158                        unit: Some("deg".to_string()),
159                        tags: tags.clone(),
160                    },
161                    TelemetryPoint {
162                        ts_ms: fix.ts_ms,
163                        key: "position.lon".to_string(),
164                        value: TelemetryValue::Float(fix.lon),
165                        unit: Some("deg".to_string()),
166                        tags: tags.clone(),
167                    },
168                ];
169
170                if let Some(alt_m) = fix.alt_m {
171                    points.push(TelemetryPoint {
172                        ts_ms: fix.ts_ms,
173                        key: "position.alt_m".to_string(),
174                        value: TelemetryValue::Float(alt_m),
175                        unit: Some("m".to_string()),
176                        tags: tags.clone(),
177                    });
178                }
179                if let Some(speed_mps) = fix.speed_mps {
180                    points.push(TelemetryPoint {
181                        ts_ms: fix.ts_ms,
182                        key: "position.speed_mps".to_string(),
183                        value: TelemetryValue::Float(speed_mps),
184                        unit: Some("m/s".to_string()),
185                        tags: tags.clone(),
186                    });
187                }
188                if let Some(heading_deg) = fix.heading_deg {
189                    points.push(TelemetryPoint {
190                        ts_ms: fix.ts_ms,
191                        key: "position.heading_deg".to_string(),
192                        value: TelemetryValue::Float(heading_deg),
193                        unit: Some("deg".to_string()),
194                        tags: tags.clone(),
195                    });
196                }
197                if let Some(accuracy_m) = fix.accuracy_m {
198                    points.push(TelemetryPoint {
199                        ts_ms: fix.ts_ms,
200                        key: "position.accuracy_m".to_string(),
201                        value: TelemetryValue::Float(accuracy_m),
202                        unit: Some("m".to_string()),
203                        tags,
204                    });
205                }
206                (points, Some(fix))
207            }
208            Self::BatteryStatus(status) => (
209                vec![
210                    TelemetryPoint {
211                        ts_ms: status.ts_ms,
212                        key: "battery.pct".to_string(),
213                        value: TelemetryValue::Unsigned(u64::from(status.pct)),
214                        unit: Some("%".to_string()),
215                        tags: tags.clone(),
216                    },
217                    TelemetryPoint {
218                        ts_ms: status.ts_ms,
219                        key: "battery.mv".to_string(),
220                        value: TelemetryValue::Unsigned(u64::from(status.millivolts)),
221                        unit: Some("mV".to_string()),
222                        tags: tags.clone(),
223                    },
224                    TelemetryPoint {
225                        ts_ms: status.ts_ms,
226                        key: "battery.charging".to_string(),
227                        value: TelemetryValue::Bool(status.charging),
228                        unit: None,
229                        tags,
230                    },
231                ],
232                None,
233            ),
234            Self::LinkStats(stats) => {
235                let mut points = vec![
236                    TelemetryPoint {
237                        ts_ms: stats.ts_ms,
238                        key: "link.airtime_ms".to_string(),
239                        value: TelemetryValue::Unsigned(u64::from(stats.airtime_ms)),
240                        unit: Some("ms".to_string()),
241                        tags: tags.clone(),
242                    },
243                    TelemetryPoint {
244                        ts_ms: stats.ts_ms,
245                        key: "link.rx_ok".to_string(),
246                        value: TelemetryValue::Unsigned(u64::from(stats.rx_ok)),
247                        unit: None,
248                        tags: tags.clone(),
249                    },
250                    TelemetryPoint {
251                        ts_ms: stats.ts_ms,
252                        key: "link.rx_drop".to_string(),
253                        value: TelemetryValue::Unsigned(u64::from(stats.rx_drop)),
254                        unit: None,
255                        tags: tags.clone(),
256                    },
257                    TelemetryPoint {
258                        ts_ms: stats.ts_ms,
259                        key: "link.tx_ok".to_string(),
260                        value: TelemetryValue::Unsigned(u64::from(stats.tx_ok)),
261                        unit: None,
262                        tags: tags.clone(),
263                    },
264                    TelemetryPoint {
265                        ts_ms: stats.ts_ms,
266                        key: "link.tx_drop".to_string(),
267                        value: TelemetryValue::Unsigned(u64::from(stats.tx_drop)),
268                        unit: None,
269                        tags: tags.clone(),
270                    },
271                ];
272
273                if let Some(rssi) = stats.rssi_dbm {
274                    points.push(TelemetryPoint {
275                        ts_ms: stats.ts_ms,
276                        key: "link.rssi_dbm".to_string(),
277                        value: TelemetryValue::Integer(i64::from(rssi)),
278                        unit: Some("dBm".to_string()),
279                        tags: tags.clone(),
280                    });
281                }
282                if let Some(snr) = stats.snr_db {
283                    points.push(TelemetryPoint {
284                        ts_ms: stats.ts_ms,
285                        key: "link.snr_db".to_string(),
286                        value: TelemetryValue::Float(f64::from(snr)),
287                        unit: Some("dB".to_string()),
288                        tags,
289                    });
290                }
291
292                (points, None)
293            }
294            Self::DeviceHealth(health) => {
295                let mut points = vec![TelemetryPoint {
296                    ts_ms: health.ts_ms,
297                    key: "device.uptime_ms".to_string(),
298                    value: TelemetryValue::Unsigned(health.uptime_ms),
299                    unit: Some("ms".to_string()),
300                    tags: tags.clone(),
301                }];
302
303                if let Some(free_bytes) = health.free_bytes {
304                    points.push(TelemetryPoint {
305                        ts_ms: health.ts_ms,
306                        key: "device.free_bytes".to_string(),
307                        value: TelemetryValue::Unsigned(u64::from(free_bytes)),
308                        unit: Some("bytes".to_string()),
309                        tags: tags.clone(),
310                    });
311                }
312                if let Some(temperature_c) = health.temperature_c {
313                    points.push(TelemetryPoint {
314                        ts_ms: health.ts_ms,
315                        key: "device.temperature_c".to_string(),
316                        value: TelemetryValue::Float(f64::from(temperature_c)),
317                        unit: Some("C".to_string()),
318                        tags,
319                    });
320                }
321
322                (points, None)
323            }
324            Self::Point(mut point) => {
325                point.tags.entry("source".to_string()).or_insert_with(|| hex::encode(source_hash));
326                (vec![point], None)
327            }
328        }
329    }
330}
331
332pub fn encode_recent_fields(
333    points: &[TelemetryPoint],
334    latest_position: Option<&PositionFix>,
335) -> Result<Value, MiniNodeError> {
336    let mut entries = Vec::new();
337
338    if let Some(position) = latest_position {
339        entries.push((Value::Integer(2.into()), Value::Binary(encode_position_payload(position)?)));
340    }
341
342    if !points.is_empty() {
343        let telemetry_values = points.iter().map(TelemetryPoint::to_rmpv).collect::<Vec<_>>();
344        let encoded = rmp_serde::to_vec(&Value::Array(telemetry_values))
345            .map_err(|_| MiniNodeError::StorageError)?;
346        entries.push((Value::Integer(3.into()), Value::Binary(encoded)));
347    }
348
349    Ok(Value::Map(entries))
350}
351
352fn encode_position_payload(position: &PositionFix) -> Result<Vec<u8>, MiniNodeError> {
353    let lat = ((position.lat * 1_000_000.0).round()) as i32;
354    let lon = ((position.lon * 1_000_000.0).round()) as i32;
355    let alt = ((position.alt_m.unwrap_or_default() * 100.0).round()) as i32;
356    let speed = ((position.speed_mps.unwrap_or_default() * 100.0).round()) as u32;
357    let heading = ((position.heading_deg.unwrap_or_default() * 100.0).round()) as i32;
358    let accuracy = ((position.accuracy_m.unwrap_or_default() * 100.0).round()) as u16;
359
360    let position_array = Value::Array(vec![
361        Value::Binary(lat.to_be_bytes().to_vec()),
362        Value::Binary(lon.to_be_bytes().to_vec()),
363        Value::Binary(alt.to_be_bytes().to_vec()),
364        Value::Binary(speed.to_be_bytes().to_vec()),
365        Value::Binary(heading.to_be_bytes().to_vec()),
366        Value::Binary(accuracy.to_be_bytes().to_vec()),
367        Value::Integer(position.ts_ms.into()),
368    ]);
369
370    let payload = Value::Map(vec![(Value::Integer(0x02.into()), position_array)]);
371    rmp_serde::to_vec(&payload).map_err(|_| MiniNodeError::StorageError)
372}