1use std::fmt;
10
11use pamoja_lora::budget::Decibels;
12use pamoja_lora::LinkSettings;
13use serde_json::{json, Map, Value};
14
15use crate::base64;
16
17use super::ProtocolError;
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
21pub enum CrcStatus {
22 #[default]
24 Ok,
25 Failed,
27 Absent,
29}
30
31impl CrcStatus {
32 pub const fn code(self) -> i8 {
38 match self {
39 CrcStatus::Ok => 1,
40 CrcStatus::Failed => -1,
41 CrcStatus::Absent => 0,
42 }
43 }
44
45 pub const fn from_code(code: i64) -> Option<CrcStatus> {
55 match code {
56 1 => Some(CrcStatus::Ok),
57 -1 => Some(CrcStatus::Failed),
58 0 => Some(CrcStatus::Absent),
59 _ => None,
60 }
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum Modulation {
67 Lora(LinkSettings),
69 Fsk(u32),
71}
72
73impl Modulation {
74 pub const fn link(self) -> Option<LinkSettings> {
80 match self {
81 Modulation::Lora(link) => Some(link),
82 Modulation::Fsk(_) => None,
83 }
84 }
85
86 fn datarate(self) -> Value {
92 match self {
93 Modulation::Lora(link) => Value::String(format!(
94 "SF{}BW{}",
95 link.spreading_factor(),
96 link.bandwidth_hz() / 1_000
97 )),
98 Modulation::Fsk(bitrate) => json!(bitrate),
99 }
100 }
101}
102
103fn link_of(text: &str) -> Option<(u8, u32)> {
113 let (factor, bandwidth) = text.strip_prefix("SF")?.split_once("BW")?;
114 Some((
115 factor.parse().ok()?,
116 bandwidth.parse::<u32>().ok()?.checked_mul(1_000)?,
117 ))
118}
119
120fn coding_rate_of(text: &str) -> Option<u8> {
130 let (numerator, denominator) = text.split_once('/')?;
131 if numerator != "4" {
132 return None;
133 }
134 denominator.parse().ok()
135}
136
137#[derive(Clone, Debug, PartialEq)]
152pub struct Rxpk {
153 pub received_at: Option<String>,
155 pub gps_millis: Option<u64>,
157 pub timestamp_us: Option<u32>,
159 pub frequency_hz: u32,
161 pub channel: u8,
163 pub rf_chain: u8,
165 pub crc: CrcStatus,
167 pub modulation: Modulation,
169 pub rssi_dbm: Decibels,
171 pub snr_db: Option<Decibels>,
173 pub payload: Vec<u8>,
175}
176
177impl Rxpk {
178 pub fn new(frequency_hz: u32, link: LinkSettings, payload: Vec<u8>) -> Rxpk {
190 Rxpk {
191 received_at: None,
192 gps_millis: None,
193 timestamp_us: None,
194 frequency_hz,
195 channel: 0,
196 rf_chain: 0,
197 crc: CrcStatus::Ok,
198 modulation: Modulation::Lora(link),
199 rssi_dbm: Decibels::ZERO,
200 snr_db: None,
201 payload,
202 }
203 }
204
205 pub fn with_rssi_dbm(mut self, dbm: i32) -> Rxpk {
215 self.rssi_dbm = Decibels::from_db(dbm);
216 self
217 }
218
219 pub fn with_snr_db(mut self, db: f64) -> Rxpk {
229 self.snr_db = Some(Decibels::from_hundredths((db * 100.0).round() as i32));
230 self
231 }
232
233 pub fn with_timestamp_us(mut self, micros: u32) -> Rxpk {
243 self.timestamp_us = Some(micros);
244 self
245 }
246
247 pub fn with_received_at(mut self, micros_since_epoch: u64) -> Rxpk {
257 self.received_at = Some(crate::time::compact(micros_since_epoch));
258 self
259 }
260
261 pub fn on_channel(mut self, channel: u8, rf_chain: u8) -> Rxpk {
272 self.channel = channel;
273 self.rf_chain = rf_chain;
274 self
275 }
276
277 pub fn to_json(&self) -> String {
283 self.to_value().to_string()
284 }
285
286 fn to_value(&self) -> Value {
288 let mut object = Map::new();
289 if let Some(time) = &self.received_at {
290 object.insert("time".to_owned(), json!(time));
291 }
292 if let Some(millis) = self.gps_millis {
293 object.insert("tmms".to_owned(), json!(millis));
294 }
295 if let Some(micros) = self.timestamp_us {
296 object.insert("tmst".to_owned(), json!(micros));
297 }
298 object.insert("chan".to_owned(), json!(self.channel));
299 object.insert("rfch".to_owned(), json!(self.rf_chain));
300 object.insert("freq".to_owned(), megahertz(self.frequency_hz));
301 object.insert("stat".to_owned(), json!(self.crc.code()));
302 object.insert("modu".to_owned(), json!(name_of(self.modulation)));
303 object.insert("datr".to_owned(), self.modulation.datarate());
304 if let Modulation::Lora(link) = self.modulation {
305 object.insert(
306 "codr".to_owned(),
307 json!(format!("4/{}", link.coding_rate_denominator())),
308 );
309 }
310 object.insert("rssi".to_owned(), json!(self.rssi_dbm.round_db()));
311 if let Some(snr) = self.snr_db {
312 object.insert("lsnr".to_owned(), decibels(snr));
313 }
314 object.insert("size".to_owned(), json!(self.payload.len()));
315 object.insert("data".to_owned(), json!(base64::encode(&self.payload)));
316 Value::Object(object)
317 }
318
319 fn from_value(value: &Value) -> Result<Rxpk, ProtocolError> {
321 let object = object_of(value, "an rxpk entry")?;
322 let modulation = modulation_of(object, "rxpk")?;
323 Ok(Rxpk {
324 received_at: text(object, "time").map(str::to_owned),
325 gps_millis: whole(object, "tmms").map(|value| value as u64),
326 timestamp_us: whole(object, "tmst").map(|value| value as u32),
327 frequency_hz: hertz(object, "rxpk")?,
328 channel: whole(object, "chan").unwrap_or(0) as u8,
329 rf_chain: whole(object, "rfch").unwrap_or(0) as u8,
330 crc: whole(object, "stat")
331 .and_then(CrcStatus::from_code)
332 .unwrap_or(CrcStatus::Absent),
333 modulation,
334 rssi_dbm: Decibels::from_db(whole(object, "rssi").unwrap_or(0) as i32),
335 snr_db: number(object, "lsnr")
336 .map(|snr| Decibels::from_hundredths((snr * 100.0).round() as i32)),
337 payload: payload_of(object, "rxpk")?,
338 })
339 }
340}
341
342#[derive(Clone, Debug, Default, PartialEq)]
353pub struct Stat {
354 pub time: Option<String>,
356 pub latitude_deg: Option<f64>,
358 pub longitude_deg: Option<f64>,
360 pub altitude_m: Option<i32>,
362 pub received: u32,
364 pub received_ok: u32,
366 pub forwarded: u32,
368 pub acknowledged_percent: f64,
370 pub downlinks: u32,
372 pub transmitted: u32,
374}
375
376impl Stat {
377 pub fn new() -> Stat {
383 Stat::default()
384 }
385
386 pub fn at(mut self, seconds_since_epoch: u64) -> Stat {
396 self.time = Some(crate::time::expanded(seconds_since_epoch));
397 self
398 }
399
400 pub fn at_position(mut self, latitude_deg: f64, longitude_deg: f64, altitude_m: i32) -> Stat {
412 self.latitude_deg = Some(latitude_deg);
413 self.longitude_deg = Some(longitude_deg);
414 self.altitude_m = Some(altitude_m);
415 self
416 }
417
418 pub fn with_counts(mut self, received: u32, received_ok: u32, forwarded: u32) -> Stat {
430 self.received = received;
431 self.received_ok = received_ok;
432 self.forwarded = forwarded;
433 self
434 }
435
436 pub fn with_downlinks(mut self, downlinks: u32, transmitted: u32) -> Stat {
447 self.downlinks = downlinks;
448 self.transmitted = transmitted;
449 self
450 }
451
452 pub fn with_acknowledged_percent(mut self, percent: f64) -> Stat {
462 self.acknowledged_percent = percent;
463 self
464 }
465
466 pub fn to_json(&self) -> String {
472 self.to_value().to_string()
473 }
474
475 fn to_value(&self) -> Value {
477 let mut object = Map::new();
478 if let Some(time) = &self.time {
479 object.insert("time".to_owned(), json!(time));
480 }
481 if let (Some(latitude), Some(longitude)) = (self.latitude_deg, self.longitude_deg) {
482 object.insert("lati".to_owned(), json!(latitude));
483 object.insert("long".to_owned(), json!(longitude));
484 }
485 if let Some(altitude) = self.altitude_m {
486 object.insert("alti".to_owned(), json!(altitude));
487 }
488 object.insert("rxnb".to_owned(), json!(self.received));
489 object.insert("rxok".to_owned(), json!(self.received_ok));
490 object.insert("rxfw".to_owned(), json!(self.forwarded));
491 object.insert("ackr".to_owned(), json!(self.acknowledged_percent));
492 object.insert("dwnb".to_owned(), json!(self.downlinks));
493 object.insert("txnb".to_owned(), json!(self.transmitted));
494 Value::Object(object)
495 }
496
497 fn from_value(value: &Value) -> Result<Stat, ProtocolError> {
499 let object = object_of(value, "a stat object")?;
500 Ok(Stat {
501 time: text(object, "time").map(str::to_owned),
502 latitude_deg: number(object, "lati"),
503 longitude_deg: number(object, "long"),
504 altitude_m: whole(object, "alti").map(|value| value as i32),
505 received: whole(object, "rxnb").unwrap_or(0) as u32,
506 received_ok: whole(object, "rxok").unwrap_or(0) as u32,
507 forwarded: whole(object, "rxfw").unwrap_or(0) as u32,
508 acknowledged_percent: number(object, "ackr").unwrap_or(0.0),
509 downlinks: whole(object, "dwnb").unwrap_or(0) as u32,
510 transmitted: whole(object, "txnb").unwrap_or(0) as u32,
511 })
512 }
513}
514
515#[derive(Clone, Debug, Default, PartialEq)]
517pub struct Uplink {
518 pub packets: Vec<Rxpk>,
520 pub status: Option<Stat>,
522}
523
524impl From<Rxpk> for Uplink {
525 fn from(packet: Rxpk) -> Uplink {
526 Uplink {
527 packets: vec![packet],
528 status: None,
529 }
530 }
531}
532
533impl From<Stat> for Uplink {
534 fn from(status: Stat) -> Uplink {
535 Uplink {
536 packets: Vec::new(),
537 status: Some(status),
538 }
539 }
540}
541
542impl Uplink {
543 pub fn to_json(&self) -> String {
550 let mut object = Map::new();
551 if !self.packets.is_empty() {
552 object.insert(
553 "rxpk".to_owned(),
554 Value::Array(self.packets.iter().map(Rxpk::to_value).collect()),
555 );
556 }
557 if let Some(status) = &self.status {
558 object.insert("stat".to_owned(), status.to_value());
559 }
560 Value::Object(object).to_string()
561 }
562
563 pub fn from_json(body: &[u8]) -> Result<Uplink, ProtocolError> {
578 let value = parse_json(body, "a PUSH_DATA")?;
579 let object = object_of(&value, "a PUSH_DATA payload")?;
580 let packets = match object.get("rxpk") {
581 None => Vec::new(),
582 Some(Value::Array(entries)) => entries
583 .iter()
584 .map(Rxpk::from_value)
585 .collect::<Result<Vec<_>, _>>()?,
586 Some(_) => return Err(refused("\"rxpk\" is not an array")),
587 };
588 let status = match object.get("stat") {
589 None => None,
590 Some(value) => Some(Stat::from_value(value)?),
591 };
592 if packets.is_empty() && status.is_none() {
593 return Err(refused(
594 "a PUSH_DATA payload carries neither \"rxpk\" nor \"stat\"",
595 ));
596 }
597 Ok(Uplink { packets, status })
598 }
599}
600
601#[derive(Clone, Debug, PartialEq)]
615pub struct Txpk {
616 pub immediate: bool,
618 pub timestamp_us: Option<u32>,
620 pub gps_millis: Option<u64>,
622 pub frequency_hz: u32,
624 pub rf_chain: u8,
626 pub power_dbm: i8,
628 pub modulation: Modulation,
630 pub frequency_deviation_hz: Option<u32>,
632 pub invert_polarity: bool,
634 pub preamble_symbols: Option<u16>,
636 pub without_crc: bool,
638 pub payload: Vec<u8>,
640}
641
642impl Txpk {
643 pub fn immediate(frequency_hz: u32, link: LinkSettings, payload: Vec<u8>) -> Txpk {
655 Txpk {
656 immediate: true,
657 timestamp_us: None,
658 gps_millis: None,
659 frequency_hz,
660 rf_chain: 0,
661 power_dbm: 14,
662 modulation: Modulation::Lora(link),
663 frequency_deviation_hz: None,
664 invert_polarity: false,
665 preamble_symbols: None,
666 without_crc: false,
667 payload,
668 }
669 }
670
671 pub fn at(timestamp_us: u32, frequency_hz: u32, link: LinkSettings, payload: Vec<u8>) -> Txpk {
685 Txpk {
686 immediate: false,
687 timestamp_us: Some(timestamp_us),
688 ..Txpk::immediate(frequency_hz, link, payload)
689 }
690 }
691
692 pub fn with_power_dbm(mut self, dbm: i8) -> Txpk {
702 self.power_dbm = dbm;
703 self
704 }
705
706 pub fn with_inverted_polarity(mut self, inverted: bool) -> Txpk {
716 self.invert_polarity = inverted;
717 self
718 }
719
720 pub fn without_crc(mut self) -> Txpk {
726 self.without_crc = true;
727 self
728 }
729
730 pub fn to_json(&self) -> String {
736 json!({ "txpk": self.to_value() }).to_string()
737 }
738
739 fn to_value(&self) -> Value {
741 let mut object = Map::new();
742 object.insert("imme".to_owned(), json!(self.immediate));
743 if let Some(micros) = self.timestamp_us {
744 object.insert("tmst".to_owned(), json!(micros));
745 }
746 if let Some(millis) = self.gps_millis {
747 object.insert("tmms".to_owned(), json!(millis));
748 }
749 object.insert("freq".to_owned(), megahertz(self.frequency_hz));
750 object.insert("rfch".to_owned(), json!(self.rf_chain));
751 object.insert("powe".to_owned(), json!(self.power_dbm));
752 object.insert("modu".to_owned(), json!(name_of(self.modulation)));
753 object.insert("datr".to_owned(), self.modulation.datarate());
754 match self.modulation {
755 Modulation::Lora(link) => {
756 object.insert(
757 "codr".to_owned(),
758 json!(format!("4/{}", link.coding_rate_denominator())),
759 );
760 object.insert("ipol".to_owned(), json!(self.invert_polarity));
761 }
762 Modulation::Fsk(_) => {
763 if let Some(deviation) = self.frequency_deviation_hz {
764 object.insert("fdev".to_owned(), json!(deviation));
765 }
766 }
767 }
768 if let Some(symbols) = self.preamble_symbols {
769 object.insert("prea".to_owned(), json!(symbols));
770 }
771 object.insert("size".to_owned(), json!(self.payload.len()));
772 object.insert("data".to_owned(), json!(base64::encode(&self.payload)));
773 if self.without_crc {
774 object.insert("ncrc".to_owned(), json!(true));
775 }
776 Value::Object(object)
777 }
778
779 pub fn from_json(body: &[u8]) -> Result<Txpk, ProtocolError> {
794 let value = parse_json(body, "a PULL_RESP")?;
795 let object = object_of(&value, "a PULL_RESP payload")?;
796 let request = object
797 .get("txpk")
798 .ok_or_else(|| refused("a PULL_RESP payload carries no \"txpk\""))?;
799 let request = object_of(request, "a txpk object")?;
800 Ok(Txpk {
801 immediate: flag(request, "imme"),
802 timestamp_us: whole(request, "tmst").map(|value| value as u32),
803 gps_millis: whole(request, "tmms").map(|value| value as u64),
804 frequency_hz: hertz(request, "txpk")?,
805 rf_chain: whole(request, "rfch").unwrap_or(0) as u8,
806 power_dbm: whole(request, "powe").unwrap_or(14) as i8,
807 modulation: modulation_of(request, "txpk")?,
808 frequency_deviation_hz: whole(request, "fdev").map(|value| value as u32),
809 invert_polarity: flag(request, "ipol"),
810 preamble_symbols: whole(request, "prea").map(|value| value as u16),
811 without_crc: flag(request, "ncrc"),
812 payload: payload_of(request, "txpk")?,
813 })
814 }
815}
816
817#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
819pub enum TxStatus {
820 #[default]
822 None,
823 TooLate,
825 TooEarly,
827 CollisionPacket,
829 CollisionBeacon,
831 TxFreq,
833 TxPower,
835 GpsUnlocked,
837}
838
839impl TxStatus {
840 pub const fn as_str(self) -> &'static str {
846 match self {
847 TxStatus::None => "NONE",
848 TxStatus::TooLate => "TOO_LATE",
849 TxStatus::TooEarly => "TOO_EARLY",
850 TxStatus::CollisionPacket => "COLLISION_PACKET",
851 TxStatus::CollisionBeacon => "COLLISION_BEACON",
852 TxStatus::TxFreq => "TX_FREQ",
853 TxStatus::TxPower => "TX_POWER",
854 TxStatus::GpsUnlocked => "GPS_UNLOCKED",
855 }
856 }
857
858 pub fn named(text: &str) -> Option<TxStatus> {
868 [
869 TxStatus::None,
870 TxStatus::TooLate,
871 TxStatus::TooEarly,
872 TxStatus::CollisionPacket,
873 TxStatus::CollisionBeacon,
874 TxStatus::TxFreq,
875 TxStatus::TxPower,
876 TxStatus::GpsUnlocked,
877 ]
878 .into_iter()
879 .find(|status| status.as_str() == text)
880 }
881
882 pub const fn scheduled(self) -> bool {
888 matches!(self, TxStatus::None)
889 }
890
891 pub fn to_json(self) -> String {
897 json!({ "txpk_ack": { "error": self.as_str() } }).to_string()
898 }
899
900 pub fn from_json(body: &[u8]) -> Result<TxStatus, ProtocolError> {
915 if body.iter().all(u8::is_ascii_whitespace) {
916 return Ok(TxStatus::None);
917 }
918 let value = parse_json(body, "a TX_ACK")?;
919 let object = object_of(&value, "a TX_ACK payload")?;
920 let acknowledgment = object
921 .get("txpk_ack")
922 .ok_or_else(|| refused("a TX_ACK payload carries no \"txpk_ack\""))?;
923 let acknowledgment = object_of(acknowledgment, "a txpk_ack object")?;
924 match text(acknowledgment, "error") {
925 None => Ok(TxStatus::None),
926 Some(error) => TxStatus::named(error)
927 .ok_or_else(|| refused(&format!("{error} is not a TX_ACK error"))),
928 }
929 }
930}
931
932impl fmt::Display for TxStatus {
933 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
934 f.write_str(self.as_str())
935 }
936}
937
938fn name_of(modulation: Modulation) -> &'static str {
940 match modulation {
941 Modulation::Lora(_) => "LORA",
942 Modulation::Fsk(_) => "FSK",
943 }
944}
945
946fn megahertz(hertz: u32) -> Value {
948 json!(f64::from(hertz) / 1_000_000.0)
949}
950
951fn decibels(value: Decibels) -> Value {
953 json!(f64::from(value.hundredths()) / 100.0)
954}
955
956fn hertz(object: &Map<String, Value>, what: &str) -> Result<u32, ProtocolError> {
958 let megahertz =
959 number(object, "freq").ok_or_else(|| refused(&format!("a {what} carries no \"freq\"")))?;
960 let hertz = (megahertz * 1_000_000.0).round();
961 if !(0.0..=f64::from(u32::MAX)).contains(&hertz) {
962 return Err(refused(&format!("{megahertz} MHz is not a carrier")));
963 }
964 Ok(hertz as u32)
965}
966
967fn modulation_of(object: &Map<String, Value>, what: &str) -> Result<Modulation, ProtocolError> {
969 match text(object, "modu") {
970 Some("FSK") => {
971 let bitrate = whole(object, "datr")
972 .ok_or_else(|| refused(&format!("an FSK {what} carries no bitrate in \"datr\"")))?;
973 Ok(Modulation::Fsk(bitrate as u32))
974 }
975 Some("LORA") | None => {
976 let datarate = text(object, "datr")
977 .ok_or_else(|| refused(&format!("a LoRa {what} carries no \"datr\"")))?;
978 let (factor, bandwidth) = link_of(datarate)
979 .ok_or_else(|| refused(&format!("{datarate} is not a datarate identifier")))?;
980 let mut link = LinkSettings::new(factor, bandwidth);
981 if let Some(coding) = text(object, "codr") {
982 let denominator = coding_rate_of(coding)
983 .ok_or_else(|| refused(&format!("{coding} is not a coding rate")))?;
984 link = link.with_coding_rate(denominator);
985 }
986 Ok(Modulation::Lora(link))
987 }
988 Some(other) => Err(refused(&format!("{other} is not a modulation"))),
989 }
990}
991
992fn payload_of(object: &Map<String, Value>, what: &str) -> Result<Vec<u8>, ProtocolError> {
994 let data =
995 text(object, "data").ok_or_else(|| refused(&format!("a {what} carries no \"data\"")))?;
996 base64::decode(data)
997 .map_err(|error| refused(&format!("the {what} payload is not base64: {error}")))
998}
999
1000fn parse_json(body: &[u8], what: &str) -> Result<Value, ProtocolError> {
1002 serde_json::from_slice(body)
1003 .map_err(|error| refused(&format!("the payload of {what} is not JSON: {error}")))
1004}
1005
1006fn object_of<'a>(value: &'a Value, what: &str) -> Result<&'a Map<String, Value>, ProtocolError> {
1008 value
1009 .as_object()
1010 .ok_or_else(|| refused(&format!("{what} is not a JSON object")))
1011}
1012
1013fn text<'a>(object: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
1015 object.get(key).and_then(Value::as_str)
1016}
1017
1018fn number(object: &Map<String, Value>, key: &str) -> Option<f64> {
1020 object.get(key).and_then(Value::as_f64)
1021}
1022
1023fn whole(object: &Map<String, Value>, key: &str) -> Option<i64> {
1025 number(object, key).map(|value| value.round() as i64)
1026}
1027
1028fn flag(object: &Map<String, Value>, key: &str) -> bool {
1030 object.get(key).and_then(Value::as_bool).unwrap_or(false)
1031}
1032
1033fn refused(why: &str) -> ProtocolError {
1035 ProtocolError::Payload(why.to_owned())
1036}
1037
1038#[cfg(test)]
1039mod tests {
1040 use super::*;
1041
1042 const EXAMPLE_RXPK: &str = r#"{"rxpk":[{
1044 "time":"2013-03-31T16:21:17.528002Z",
1045 "tmst":3512348611,
1046 "chan":2,
1047 "rfch":0,
1048 "freq":866.349812,
1049 "stat":1,
1050 "modu":"LORA",
1051 "datr":"SF7BW125",
1052 "codr":"4/6",
1053 "rssi":-35,
1054 "lsnr":5.1,
1055 "size":32,
1056 "data":"VEVTVF9QQUNLRVRfMTIzNA=="
1057 }]}"#;
1058
1059 const EXAMPLE_STAT: &str = r#"{"stat":{
1061 "time":"2014-01-12 08:59:28 GMT",
1062 "lati":46.24000,
1063 "long":3.25230,
1064 "alti":145,
1065 "rxnb":2,
1066 "rxok":2,
1067 "rxfw":2,
1068 "ackr":100.0,
1069 "dwnb":2,
1070 "txnb":2
1071 }}"#;
1072
1073 const EXAMPLE_TXPK: &str = r#"{"txpk":{
1075 "imme":true,
1076 "freq":864.123456,
1077 "rfch":0,
1078 "powe":14,
1079 "modu":"LORA",
1080 "datr":"SF11BW125",
1081 "codr":"4/6",
1082 "ipol":false,
1083 "size":32,
1084 "data":"H3P3N2i9qc4yt7rK7ldqoeCVJGBybzPY5h1Dd7P7p8v"
1085 }}"#;
1086
1087 #[test]
1088 fn the_protocols_own_rxpk_example_reads() {
1089 let uplink = Uplink::from_json(EXAMPLE_RXPK.as_bytes()).expect("the example parses");
1090 let heard = &uplink.packets[0];
1091
1092 assert_eq!(
1093 heard.received_at.as_deref(),
1094 Some("2013-03-31T16:21:17.528002Z")
1095 );
1096 assert_eq!(heard.timestamp_us, Some(3_512_348_611));
1097 assert_eq!(heard.frequency_hz, 866_349_812);
1098 assert_eq!((heard.channel, heard.rf_chain), (2, 0));
1099 assert_eq!(heard.crc, CrcStatus::Ok);
1100 assert_eq!(
1101 heard.modulation,
1102 Modulation::Lora(LinkSettings::new(7, 125_000).with_coding_rate(6))
1103 );
1104 assert_eq!(heard.rssi_dbm.round_db(), -35);
1105 assert_eq!(heard.snr_db.map(Decibels::hundredths), Some(510));
1106 assert_eq!(heard.payload, b"TEST_PACKET_1234");
1107 }
1108
1109 #[test]
1110 fn the_protocols_own_stat_example_reads() {
1111 let uplink = Uplink::from_json(EXAMPLE_STAT.as_bytes()).expect("the example parses");
1112 let report = uplink.status.expect("it carries a report");
1113
1114 assert_eq!(report.time.as_deref(), Some("2014-01-12 08:59:28 GMT"));
1115 assert_eq!(report.latitude_deg, Some(46.24));
1116 assert_eq!(report.altitude_m, Some(145));
1117 assert_eq!(
1118 (report.received, report.received_ok, report.forwarded),
1119 (2, 2, 2)
1120 );
1121 assert_eq!(report.acknowledged_percent, 100.0);
1122 assert_eq!((report.downlinks, report.transmitted), (2, 2));
1123 assert!(uplink.packets.is_empty());
1124 }
1125
1126 #[test]
1127 fn the_protocols_own_txpk_example_reads() {
1128 let request = Txpk::from_json(EXAMPLE_TXPK.as_bytes()).expect("the example parses");
1129
1130 assert!(request.immediate && !request.invert_polarity);
1131 assert_eq!(request.frequency_hz, 864_123_456);
1132 assert_eq!(request.power_dbm, 14);
1133 assert_eq!(
1134 request.modulation,
1135 Modulation::Lora(LinkSettings::new(11, 125_000).with_coding_rate(6))
1136 );
1137 assert_eq!(request.payload.len(), 32);
1139 }
1140
1141 #[test]
1142 fn an_fsk_packet_carries_its_bitrate_as_a_number() {
1143 let fsk = r#"{"rxpk":[{"tmst":1,"freq":869.1,"stat":1,"modu":"FSK","datr":50000,
1144 "rssi":-75,"size":16,"data":"VEVTVF9QQUNLRVRfMTIzNA=="}]}"#;
1145 let uplink = Uplink::from_json(fsk.as_bytes()).expect("it parses");
1146
1147 assert_eq!(uplink.packets[0].modulation, Modulation::Fsk(50_000));
1148 assert!(uplink.to_json().contains("\"datr\":50000"));
1149 }
1150
1151 #[test]
1152 fn what_is_written_is_read_back() {
1153 let uplink = Uplink {
1154 packets: vec![Rxpk::new(
1155 868_100_000,
1156 LinkSettings::new(12, 125_000).with_coding_rate(8),
1157 b"\x00\x01\xFE\xFF".to_vec(),
1158 )
1159 .with_rssi_dbm(-107)
1160 .with_snr_db(-7.8)
1161 .with_timestamp_us(1_234_567)
1162 .with_received_at(1_364_746_877_528_002)
1163 .on_channel(3, 1)],
1164 status: Some(
1165 Stat::new()
1166 .at(1_389_517_168)
1167 .at_position(46.24, 3.2523, 145)
1168 .with_counts(7, 6, 6)
1169 .with_downlinks(2, 1)
1170 .with_acknowledged_percent(99.5),
1171 ),
1172 };
1173
1174 let round = Uplink::from_json(uplink.to_json().as_bytes()).expect("what we wrote parses");
1175 assert_eq!(round, uplink);
1176
1177 let downlink = Txpk::at(
1178 3_512_348_611,
1179 869_525_000,
1180 LinkSettings::new(9, 125_000),
1181 b"downlink".to_vec(),
1182 )
1183 .with_power_dbm(27)
1184 .with_inverted_polarity(true)
1185 .without_crc();
1186 assert_eq!(
1187 Txpk::from_json(downlink.to_json().as_bytes()).expect("what we wrote parses"),
1188 downlink
1189 );
1190 }
1191
1192 #[test]
1193 fn every_tx_ack_value_is_the_one_the_protocol_lists() {
1194 for (status, text) in [
1195 (TxStatus::None, "NONE"),
1196 (TxStatus::TooLate, "TOO_LATE"),
1197 (TxStatus::TooEarly, "TOO_EARLY"),
1198 (TxStatus::CollisionPacket, "COLLISION_PACKET"),
1199 (TxStatus::CollisionBeacon, "COLLISION_BEACON"),
1200 (TxStatus::TxFreq, "TX_FREQ"),
1201 (TxStatus::TxPower, "TX_POWER"),
1202 (TxStatus::GpsUnlocked, "GPS_UNLOCKED"),
1203 ] {
1204 assert_eq!(status.as_str(), text);
1205 assert_eq!(TxStatus::named(text), Some(status));
1206 assert_eq!(TxStatus::from_json(status.to_json().as_bytes()), Ok(status));
1207 }
1208 assert!(TxStatus::None.scheduled() && !TxStatus::TxPower.scheduled());
1209 assert_eq!(TxStatus::from_json(b""), Ok(TxStatus::None));
1210 assert!(TxStatus::named("SOMETHING_ELSE").is_none());
1211 }
1212
1213 #[test]
1214 fn a_payload_the_protocol_does_not_describe_is_refused() {
1215 for (body, why) in [
1216 (r#"{"rxpk":{}}"#, "array"),
1217 (r#"{"rxpk":[{"freq":868.1,"datr":"SF7BW125"}]}"#, "data"),
1218 (r#"{"rxpk":[{"datr":"SF7BW125","data":"aGk="}]}"#, "freq"),
1219 (
1220 r#"{"rxpk":[{"freq":868.1,"datr":"7BW125","data":"aGk="}]}"#,
1221 "datarate",
1222 ),
1223 (
1224 r#"{"rxpk":[{"freq":868.1,"modu":"GFSK","datr":50000,"data":"aGk="}]}"#,
1225 "modulation",
1226 ),
1227 (r#"{}"#, "neither"),
1228 ] {
1229 let refusal = Uplink::from_json(body.as_bytes()).expect_err("it is refused");
1230 assert!(
1231 refusal.to_string().contains(why),
1232 "{body} said {refusal} rather than naming {why}"
1233 );
1234 }
1235 }
1236}