1use std::fmt;
55
56use serde_json::{Map, Value};
57
58use crate::base64;
59use crate::udp::Eui;
60
61pub const UPLINK_TOPIC: &str = "application/+/device/+/event/up";
63
64pub fn uplink_topic(application_id: &str) -> String {
85 format!("application/{application_id}/device/+/event/up")
86}
87
88#[derive(Clone, Debug, PartialEq)]
90pub struct Reception {
91 pub gateway: Eui,
93 pub rssi_dbm: i32,
95 pub snr_db: f32,
97}
98
99#[derive(Clone, Debug, PartialEq)]
101pub struct UplinkEvent {
102 pub deduplication_id: String,
104 pub time: Option<String>,
106 pub application_id: String,
108 pub device_name: String,
110 pub dev_eui: Eui,
112 pub dev_addr: Option<u32>,
114 pub adr: bool,
116 pub data_rate: u8,
118 pub fcnt: u32,
120 pub fport: Option<u8>,
122 pub confirmed: bool,
124 pub data: Vec<u8>,
126 pub frequency_hz: Option<u32>,
128 pub receptions: Vec<Reception>,
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
134pub enum EventError {
135 Json(String),
137 Missing(&'static str),
139 Invalid(&'static str),
141}
142
143impl fmt::Display for EventError {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 match self {
146 EventError::Json(why) => write!(f, "the event is not a JSON object: {why}"),
147 EventError::Missing(field) => write!(f, "the event has no {field}"),
148 EventError::Invalid(field) => write!(f, "the event's {field} is not valid"),
149 }
150 }
151}
152
153impl std::error::Error for EventError {}
154
155impl UplinkEvent {
156 pub fn from_json(text: &str) -> Result<UplinkEvent, EventError> {
173 let value: Value =
174 serde_json::from_str(text).map_err(|error| EventError::Json(error.to_string()))?;
175 let event = value
176 .as_object()
177 .ok_or_else(|| EventError::Json("the top level is not an object".to_owned()))?;
178 let empty = Map::new();
179 let device = match event.get("deviceInfo") {
180 Some(Value::Object(device)) => device,
181 Some(_) => return Err(EventError::Invalid("deviceInfo")),
182 None => &empty,
183 };
184
185 let dev_eui = text_field(device, "devEui")
186 .ok_or(EventError::Missing("deviceInfo.devEui"))
187 .and_then(|hex| Eui::from_hex(&hex).ok_or(EventError::Invalid("deviceInfo.devEui")))?;
188 let dev_addr = match text_field(event, "devAddr") {
189 Some(hex) => {
190 Some(u32::from_str_radix(&hex, 16).map_err(|_| EventError::Invalid("devAddr"))?)
191 }
192 None => None,
193 };
194 let data = match text_field(event, "data") {
195 Some(encoded) => base64::decode(&encoded).map_err(|_| EventError::Invalid("data"))?,
196 None => Vec::new(),
197 };
198
199 let mut receptions = Vec::new();
200 if let Some(Value::Array(heard)) = event.get("rxInfo") {
201 for entry in heard {
202 let Some(entry) = entry.as_object() else {
203 return Err(EventError::Invalid("rxInfo"));
204 };
205 let gateway = text_field(entry, "gatewayId")
206 .and_then(|hex| Eui::from_hex(&hex))
207 .ok_or(EventError::Invalid("rxInfo.gatewayId"))?;
208 receptions.push(Reception {
209 gateway,
210 rssi_dbm: whole(entry, "rssi", "rxInfo.rssi")?.unwrap_or(0),
211 snr_db: entry.get("snr").and_then(Value::as_f64).unwrap_or(0.0) as f32,
212 });
213 }
214 }
215
216 let frequency_hz = match event.get("txInfo") {
217 Some(Value::Object(transmitted)) => {
218 whole(transmitted, "frequency", "txInfo.frequency")?
219 }
220 _ => None,
221 };
222
223 Ok(UplinkEvent {
224 deduplication_id: text_field(event, "deduplicationId").unwrap_or_default(),
225 time: text_field(event, "time"),
226 application_id: text_field(device, "applicationId").unwrap_or_default(),
227 device_name: text_field(device, "deviceName").unwrap_or_default(),
228 dev_eui,
229 dev_addr,
230 adr: event.get("adr").and_then(Value::as_bool).unwrap_or(false),
231 data_rate: whole(event, "dr", "dr")?.unwrap_or(0),
232 fcnt: whole(event, "fCnt", "fCnt")?.unwrap_or(0),
233 fport: whole(event, "fPort", "fPort")?,
234 confirmed: event
235 .get("confirmed")
236 .and_then(Value::as_bool)
237 .unwrap_or(false),
238 data,
239 frequency_hz,
240 receptions,
241 })
242 }
243
244 pub fn best_reception(&self) -> Option<&Reception> {
251 self.receptions
252 .iter()
253 .max_by(|one, other| one.snr_db.total_cmp(&other.snr_db))
254 }
255}
256
257fn text_field(object: &Map<String, Value>, key: &str) -> Option<String> {
258 object.get(key).and_then(Value::as_str).map(str::to_owned)
259}
260
261fn whole<T: TryFrom<i64>>(
263 object: &Map<String, Value>,
264 key: &str,
265 field: &'static str,
266) -> Result<Option<T>, EventError> {
267 match object.get(key) {
268 None | Some(Value::Null) => Ok(None),
269 Some(value) => value
270 .as_i64()
271 .and_then(|number| T::try_from(number).ok())
272 .map(Some)
273 .ok_or(EventError::Invalid(field)),
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 const DOCUMENTED: &str = r#"{
283 "deduplicationId": "3ac7e3c4-4401-4b8d-9386-a5c902f9202d",
284 "time": "2022-07-18T09:34:15.775023242+00:00",
285 "deviceInfo": {
286 "tenantId": "52f14cd4-c6f1-4fbd-8f87-4025e1d49242",
287 "tenantName": "ChirpStack",
288 "applicationId": "17c82e96-be03-4f38-aef3-f83d48582d97",
289 "applicationName": "Test application",
290 "deviceProfileId": "14855bf7-d10d-4aee-b618-ebfcb64dc7ad",
291 "deviceProfileName": "Test device-profile",
292 "deviceName": "Test device",
293 "devEui": "0101010101010101",
294 "tags": {
295 "key": "value"
296 }
297 },
298 "devAddr": "00189440",
299 "dr": 1,
300 "fPort": 1,
301 "data": "qg==",
302 "rxInfo": [{
303 "gatewayId": "0016c001f153a14c",
304 "uplinkId": 4217106255,
305 "rssi": -36,
306 "snr": 10.5,
307 "context": "E3OWOQ==",
308 "metadata": {
309 "region_name": "eu868",
310 "region_common_name": "EU868"
311 }
312 }],
313 "txInfo": {
314 "frequency": 867100000,
315 "modulation": {
316 "lora": {
317 "bandwidth": 125000,
318 "spreadingFactor": 11,
319 "codeRate": "CR_4_5"
320 }
321 }
322 }
323}"#;
324
325 #[test]
326 fn the_documented_event_reads_field_for_field() {
327 let event = UplinkEvent::from_json(DOCUMENTED).expect("reads");
328 assert_eq!(
329 event.deduplication_id,
330 "3ac7e3c4-4401-4b8d-9386-a5c902f9202d"
331 );
332 assert_eq!(
333 event.time.as_deref(),
334 Some("2022-07-18T09:34:15.775023242+00:00")
335 );
336 assert_eq!(event.application_id, "17c82e96-be03-4f38-aef3-f83d48582d97");
337 assert_eq!(event.device_name, "Test device");
338 assert_eq!(event.dev_eui, Eui::new([1; 8]));
339 assert_eq!(event.dev_addr, Some(0x0018_9440));
340 assert_eq!(event.data_rate, 1);
341 assert_eq!(event.fport, Some(1));
342 assert_eq!(event.data, [0xAA]);
343 assert_eq!(event.frequency_hz, Some(867_100_000));
344 assert_eq!(
345 event.receptions,
346 [Reception {
347 gateway: Eui::new([0x00, 0x16, 0xC0, 0x01, 0xF1, 0x53, 0xA1, 0x4C]),
348 rssi_dbm: -36,
349 snr_db: 10.5,
350 }]
351 );
352 }
353
354 #[test]
355 fn fields_left_at_their_defaults_read_as_zero_and_false() {
356 let event = UplinkEvent::from_json(DOCUMENTED).expect("reads");
359 assert_eq!(event.fcnt, 0);
360 assert!(!event.adr);
361 assert!(!event.confirmed);
362
363 let later = DOCUMENTED.replacen(
364 r#""dr": 1,"#,
365 r#""dr": 1, "fCnt": 70000, "adr": true, "confirmed": true,"#,
366 1,
367 );
368 let event = UplinkEvent::from_json(&later).expect("reads");
369 assert_eq!(event.fcnt, 70_000);
370 assert!(event.adr && event.confirmed);
371 }
372
373 #[test]
374 fn the_best_reception_is_the_clearest_one() {
375 let two = DOCUMENTED.replacen(
376 r#""rxInfo": [{"#,
377 r#""rxInfo": [{ "gatewayId": "0202020202020202", "rssi": -110, "snr": -4.25 }, {"#,
378 1,
379 );
380 let event = UplinkEvent::from_json(&two).expect("reads");
381 assert_eq!(event.receptions.len(), 2);
382 assert_eq!(
383 event.best_reception().map(|heard| heard.gateway.to_hex()),
384 Some("0016c001f153a14c".to_owned())
385 );
386 }
387
388 #[test]
389 fn an_event_without_its_device_or_with_a_bad_field_is_refused() {
390 assert_eq!(
391 UplinkEvent::from_json(r#"{"fPort": 2}"#),
392 Err(EventError::Missing("deviceInfo.devEui"))
393 );
394 assert_eq!(
395 UplinkEvent::from_json(&DOCUMENTED.replacen("qg==", "q", 1)),
396 Err(EventError::Invalid("data"))
397 );
398 assert_eq!(
399 UplinkEvent::from_json(&DOCUMENTED.replacen(r#""fPort": 1"#, r#""fPort": 300"#, 1)),
400 Err(EventError::Invalid("fPort"))
401 );
402 assert!(matches!(
403 UplinkEvent::from_json("[]"),
404 Err(EventError::Json(_))
405 ));
406 }
407
408 #[test]
409 fn the_topic_filters_match_what_chirpstack_publishes_on() {
410 assert_eq!(UPLINK_TOPIC, "application/+/device/+/event/up");
411 assert!(uplink_topic("app").ends_with("/device/+/event/up"));
412 }
413}