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}