1use serde_json::{json, Map, Value};
46
47use crate::udp::{Eui, ProtocolError};
48
49pub const DISCOVERY_PATH: &str = "/router-info";
51
52pub const PROTOCOL_VERSION: u32 = 2;
54
55const MINIMUM_FRAME: usize = 5;
57
58const MIC_LEN: usize = 4;
60
61const JOIN_REQUEST_LEN: usize = 18;
63
64const FRAME_HEADER_LEN: usize = 7;
66
67const JOIN_REQUEST: u8 = 0;
69
70const UNCONFIRMED_UP: u8 = 2;
72
73const CONFIRMED_UP: u8 = 4;
75
76const PROPRIETARY: u8 = 7;
78
79const UNDEFINED_DATA_RATE: i8 = -1;
82
83pub fn id6(eui: Eui) -> String {
96 let bytes = eui.bytes();
97 let groups: [u16; 4] = [
98 u16::from_be_bytes([bytes[0], bytes[1]]),
99 u16::from_be_bytes([bytes[2], bytes[3]]),
100 u16::from_be_bytes([bytes[4], bytes[5]]),
101 u16::from_be_bytes([bytes[6], bytes[7]]),
102 ];
103
104 let mut best_at = 0;
106 let mut best_len = 0;
107 let mut at = 0;
108 while at < groups.len() {
109 if groups[at] != 0 {
110 at += 1;
111 continue;
112 }
113 let start = at;
114 while at < groups.len() && groups[at] == 0 {
115 at += 1;
116 }
117 if at - start > best_len {
118 best_at = start;
119 best_len = at - start;
120 }
121 }
122
123 if best_len == groups.len() {
124 return "::".to_string();
125 }
126 if best_len < 2 {
127 let written: Vec<String> = groups.iter().map(|group| format!("{group:x}")).collect();
128 return written.join(":");
129 }
130
131 let head: Vec<String> = groups[..best_at]
132 .iter()
133 .map(|group| format!("{group:x}"))
134 .collect();
135 let tail: Vec<String> = groups[best_at + best_len..]
136 .iter()
137 .map(|group| format!("{group:x}"))
138 .collect();
139 format!("{}::{}", head.join(":"), tail.join(":"))
140}
141
142pub fn eui_of(text: &str) -> Option<Eui> {
152 if text.contains('-') {
154 let mut bytes = [0u8; 8];
155 let mut seen = 0;
156 for part in text.split('-') {
157 if seen == bytes.len() {
158 return None;
159 }
160 bytes[seen] = u8::from_str_radix(part, 16).ok()?;
161 seen += 1;
162 }
163 return (seen == bytes.len()).then(|| Eui::new(bytes));
164 }
165
166 if !text.contains(':') {
168 return text
169 .parse::<u64>()
170 .ok()
171 .map(|value| Eui::new(value.to_be_bytes()));
172 }
173
174 let (head, tail) = match text.split_once("::") {
176 Some((head, tail)) => (head, tail),
177 None => (text, ""),
178 };
179 let elided = text.contains("::");
180
181 let mut groups: Vec<u16> = Vec::new();
182 for part in head.split(':').filter(|part| !part.is_empty()) {
183 groups.push(u16::from_str_radix(part, 16).ok()?);
184 }
185 let mut rest: Vec<u16> = Vec::new();
186 for part in tail.split(':').filter(|part| !part.is_empty()) {
187 rest.push(u16::from_str_radix(part, 16).ok()?);
188 }
189
190 if elided {
191 let missing = 4usize.checked_sub(groups.len() + rest.len())?;
192 groups.extend(core::iter::repeat_n(0u16, missing));
193 } else if groups.len() + rest.len() != 4 {
194 return None;
195 }
196 groups.extend(rest);
197 if groups.len() != 4 {
198 return None;
199 }
200
201 let mut bytes = [0u8; 8];
202 for (index, group) in groups.iter().enumerate() {
203 let pair = group.to_be_bytes();
204 bytes[index * 2] = pair[0];
205 bytes[index * 2 + 1] = pair[1];
206 }
207 Some(Eui::new(bytes))
208}
209
210const XTIME_SESSION_SHIFT: u32 = 48;
212
213const XTIME_UNIT_SHIFT: u32 = 56;
215
216const XTIME_MICROS: u64 = (1 << XTIME_SESSION_SHIFT) - 1;
218
219const XTIME_MAX_UNIT: u8 = 0x7f;
221
222#[derive(Clone, Copy, Debug, PartialEq, Eq)]
248pub struct Xtime {
249 pub unit: u8,
251 pub session: u8,
253 pub micros: u64,
255}
256
257impl Xtime {
258 pub const fn new(unit: u8, session: u8, micros: u64) -> Option<Xtime> {
270 if unit > XTIME_MAX_UNIT || micros > XTIME_MICROS {
271 return None;
272 }
273 Some(Xtime {
274 unit,
275 session,
276 micros,
277 })
278 }
279
280 pub const fn value(self) -> i64 {
286 (((self.unit & XTIME_MAX_UNIT) as i64) << XTIME_UNIT_SHIFT)
287 | ((self.session as i64) << XTIME_SESSION_SHIFT)
288 | (self.micros & XTIME_MICROS) as i64
289 }
290
291 pub const fn of(value: i64) -> Xtime {
301 Xtime {
302 unit: (value >> XTIME_UNIT_SHIFT) as u8 & XTIME_MAX_UNIT,
303 session: (value >> XTIME_SESSION_SHIFT) as u8,
304 micros: value as u64 & XTIME_MICROS,
305 }
306 }
307}
308
309#[derive(Clone, Copy, Debug, PartialEq, Eq)]
311pub struct Discovery {
312 pub router: Eui,
314}
315
316impl Discovery {
317 pub const fn new(router: Eui) -> Discovery {
327 Discovery { router }
328 }
329
330 pub fn to_json(&self) -> String {
336 json!({ "router": id6(self.router) }).to_string()
337 }
338
339 pub fn from_json(body: &[u8]) -> Result<Discovery, ProtocolError> {
353 let value: Value = serde_json::from_slice(body)
354 .map_err(|error| ProtocolError::Payload(error.to_string()))?;
355 let router = value
356 .get("router")
357 .and_then(Value::as_str)
358 .and_then(eui_of)
359 .ok_or_else(|| ProtocolError::Payload("router is not an identifier".to_string()))?;
360 Ok(Discovery { router })
361 }
362}
363
364#[derive(Clone, Debug, Default, PartialEq, Eq)]
366pub struct Router {
367 pub router: Option<Eui>,
369 pub muxs: Option<Eui>,
371 pub uri: Option<String>,
373 pub error: Option<String>,
375}
376
377impl Router {
378 pub fn accepted(router: Eui, muxs: Eui, uri: impl Into<String>) -> Router {
390 Router {
391 router: Some(router),
392 muxs: Some(muxs),
393 uri: Some(uri.into()),
394 error: None,
395 }
396 }
397
398 pub fn refused(router: Eui, error: impl Into<String>) -> Router {
409 Router {
410 router: Some(router),
411 muxs: None,
412 uri: None,
413 error: Some(error.into()),
414 }
415 }
416
417 pub fn to_json(&self) -> String {
423 let mut object = serde_json::Map::new();
424 if let Some(router) = self.router {
425 object.insert("router".to_string(), Value::String(id6(router)));
426 }
427 if let Some(muxs) = self.muxs {
428 object.insert("muxs".to_string(), Value::String(id6(muxs)));
429 }
430 if let Some(uri) = &self.uri {
431 object.insert("uri".to_string(), Value::String(uri.clone()));
432 }
433 if let Some(error) = &self.error {
434 object.insert("error".to_string(), Value::String(error.clone()));
435 }
436 Value::Object(object).to_string()
437 }
438
439 pub fn from_json(body: &[u8]) -> Result<Router, ProtocolError> {
453 let value: Value = serde_json::from_slice(body)
454 .map_err(|error| ProtocolError::Payload(error.to_string()))?;
455 Ok(Router {
456 router: value.get("router").and_then(Value::as_str).and_then(eui_of),
457 muxs: value.get("muxs").and_then(Value::as_str).and_then(eui_of),
458 uri: value.get("uri").and_then(Value::as_str).map(str::to_string),
459 error: value
460 .get("error")
461 .and_then(Value::as_str)
462 .map(str::to_string),
463 })
464 }
465}
466
467#[derive(Clone, Copy, Debug, Default, PartialEq)]
469pub struct Levels {
470 pub rctx: i64,
472 pub xtime: i64,
474 pub gpstime: Option<i64>,
476 pub rssi: f64,
478 pub snr: f64,
480}
481
482#[derive(Clone, Debug, Default, PartialEq)]
484pub struct Broadcast {
485 pub pdu: Vec<u8>,
487 pub data_rate: u8,
489 pub frequency_hz: u32,
491 pub priority: u8,
493 pub gpstime: Option<i64>,
495 pub rctx: Option<i64>,
497}
498
499impl Broadcast {
500 fn to_value(&self) -> Value {
502 let mut object = Map::new();
503 object.insert("pdu".to_owned(), Value::String(hex(&self.pdu)));
504 object.insert("DR".to_owned(), json!(self.data_rate));
505 object.insert("Freq".to_owned(), json!(self.frequency_hz));
506 object.insert("priority".to_owned(), json!(self.priority));
507 if let Some(gpstime) = self.gpstime {
508 object.insert("gpstime".to_owned(), json!(gpstime));
509 }
510 if let Some(rctx) = self.rctx {
511 object.insert("rctx".to_owned(), json!(rctx));
512 }
513 Value::Object(object)
514 }
515
516 fn from_value(value: &Value) -> Result<Broadcast, ProtocolError> {
518 let object = value
519 .as_object()
520 .ok_or_else(|| refused("a scheduled frame is not a JSON object"))?;
521 Ok(Broadcast {
522 pdu: bytes_of(object, "pdu")?,
523 data_rate: whole(object, "DR").unwrap_or(0) as u8,
524 frequency_hz: whole(object, "Freq").unwrap_or(0) as u32,
525 priority: whole(object, "priority").unwrap_or(0) as u8,
526 gpstime: whole(object, "gpstime"),
527 rctx: whole(object, "rctx"),
528 })
529 }
530}
531
532#[derive(Clone, Debug, PartialEq)]
534pub enum Message {
535 Version {
537 station: String,
539 firmware: String,
541 package: String,
543 model: String,
545 protocol: u32,
547 features: String,
549 },
550 RouterConfig {
552 net_id: Option<Vec<u32>>,
559 join_eui: Vec<(u64, u64)>,
562 region: String,
564 max_eirp: f64,
566 hwspec: String,
568 freq_range: (u32, u32),
570 data_rates: Vec<Option<(u8, u32, bool)>>,
577 },
578 JoinRequest {
580 mhdr: u8,
582 join_eui: Eui,
584 dev_eui: Eui,
586 dev_nonce: u16,
588 mic: i32,
590 data_rate: u8,
592 frequency_hz: u32,
594 levels: Levels,
596 },
597 Uplink {
599 mhdr: u8,
601 dev_addr: i32,
603 fctrl: u8,
605 fcnt: u16,
607 fopts: Vec<u8>,
609 fport: Option<u8>,
611 payload: Vec<u8>,
613 mic: i32,
615 data_rate: u8,
617 frequency_hz: u32,
619 levels: Levels,
621 },
622 Proprietary {
624 payload: Vec<u8>,
626 data_rate: u8,
628 frequency_hz: u32,
630 levels: Levels,
632 },
633 Downlink {
639 dev_eui: Eui,
641 class: u8,
643 diid: i64,
645 pdu: Vec<u8>,
647 rx_delay: Option<u8>,
649 rx1: Option<(u8, u32)>,
651 rx2: Option<(u8, u32)>,
653 ping_slot: Option<(u8, u32)>,
656 priority: u8,
658 xtime: Option<i64>,
660 rctx: Option<i64>,
662 gpstime: Option<i64>,
664 },
665 Schedule {
669 frames: Vec<Broadcast>,
671 },
672 Transmitted {
674 diid: i64,
676 dev_eui: Eui,
678 rctx: i64,
680 xtime: i64,
682 txtime: f64,
684 gpstime: Option<i64>,
686 },
687 TimeSync {
689 txtime: Option<i64>,
691 xtime: Option<i64>,
693 gpstime: Option<i64>,
695 },
696 Other {
698 msgtype: String,
700 },
701}
702
703impl Message {
704 pub fn msgtype(&self) -> &str {
710 match self {
711 Message::Version { .. } => "version",
712 Message::RouterConfig { .. } => "router_config",
713 Message::JoinRequest { .. } => "jreq",
714 Message::Uplink { .. } => "updf",
715 Message::Proprietary { .. } => "propdf",
716 Message::Downlink { .. } => "dnmsg",
717 Message::Schedule { .. } => "dnsched",
718 Message::Transmitted { .. } => "dntxed",
719 Message::TimeSync { .. } => "timesync",
720 Message::Other { msgtype } => msgtype,
721 }
722 }
723
724 pub fn heard(
771 frame: &[u8],
772 data_rate: u8,
773 frequency_hz: u32,
774 levels: Levels,
775 ) -> Result<Message, ProtocolError> {
776 if frame.len() < MINIMUM_FRAME {
778 return Err(refused(&format!(
779 "a frame of {} bytes is shorter than a header and an integrity code",
780 frame.len()
781 )));
782 }
783 let mhdr = frame[0];
784 let body = &frame[1..frame.len() - MIC_LEN];
785 let mic = i32::from_le_bytes([
786 frame[frame.len() - 4],
787 frame[frame.len() - 3],
788 frame[frame.len() - 2],
789 frame[frame.len() - 1],
790 ]);
791
792 match mhdr >> 5 {
794 JOIN_REQUEST => {
795 if body.len() != JOIN_REQUEST_LEN {
796 return Err(refused(&format!(
797 "a join request carries {JOIN_REQUEST_LEN} bytes, not {}",
798 body.len()
799 )));
800 }
801 Ok(Message::JoinRequest {
802 mhdr,
803 join_eui: least_first(&body[0..8]),
804 dev_eui: least_first(&body[8..16]),
805 dev_nonce: u16::from_le_bytes([body[16], body[17]]),
806 mic,
807 data_rate,
808 frequency_hz,
809 levels,
810 })
811 }
812 UNCONFIRMED_UP | CONFIRMED_UP => {
813 if body.len() < FRAME_HEADER_LEN {
814 return Err(refused(&format!(
815 "a data frame carries {FRAME_HEADER_LEN} bytes of header, not {}",
816 body.len()
817 )));
818 }
819 let fctrl = body[4];
820 let options = usize::from(fctrl & 0x0f);
822 let carried = FRAME_HEADER_LEN + options;
823 if body.len() < carried {
824 return Err(refused(&format!(
825 "a frame naming {options} bytes of options carries {} in all",
826 body.len()
827 )));
828 }
829 let rest = &body[carried..];
830 Ok(Message::Uplink {
831 mhdr,
832 dev_addr: i32::from_le_bytes([body[0], body[1], body[2], body[3]]),
833 fctrl,
834 fcnt: u16::from_le_bytes([body[5], body[6]]),
835 fopts: body[FRAME_HEADER_LEN..carried].to_vec(),
836 fport: rest.first().copied(),
838 payload: rest.get(1..).unwrap_or_default().to_vec(),
839 mic,
840 data_rate,
841 frequency_hz,
842 levels,
843 })
844 }
845 PROPRIETARY => Ok(Message::Proprietary {
846 payload: frame.to_vec(),
848 data_rate,
849 frequency_hz,
850 levels,
851 }),
852 kind => Err(refused(&format!(
853 "a station does not send a frame of kind {kind} up"
854 ))),
855 }
856 }
857
858 pub fn to_json(&self) -> String {
864 let mut object = Map::new();
865 object.insert(
866 "msgtype".to_owned(),
867 Value::String(self.msgtype().to_owned()),
868 );
869
870 match self {
871 Message::Version {
872 station,
873 firmware,
874 package,
875 model,
876 protocol,
877 features,
878 } => {
879 object.insert("station".to_owned(), Value::String(station.clone()));
880 object.insert("firmware".to_owned(), Value::String(firmware.clone()));
881 object.insert("package".to_owned(), Value::String(package.clone()));
882 object.insert("model".to_owned(), Value::String(model.clone()));
883 object.insert("protocol".to_owned(), json!(protocol));
884 object.insert("features".to_owned(), Value::String(features.clone()));
885 }
886 Message::RouterConfig {
887 net_id,
888 join_eui,
889 region,
890 max_eirp,
891 hwspec,
892 freq_range,
893 data_rates,
894 } => {
895 object.insert("NetID".to_owned(), json!(net_id));
896 object.insert(
897 "JoinEui".to_owned(),
898 Value::Array(
899 join_eui
900 .iter()
901 .map(|(begin, end)| json!([begin, end]))
902 .collect(),
903 ),
904 );
905 object.insert("region".to_owned(), Value::String(region.clone()));
906 object.insert("max_eirp".to_owned(), json!(max_eirp));
907 object.insert("hwspec".to_owned(), Value::String(hwspec.clone()));
908 object.insert("freq_range".to_owned(), json!([freq_range.0, freq_range.1]));
909 object.insert(
910 "DRs".to_owned(),
911 Value::Array(
912 data_rates
913 .iter()
914 .map(|entry| match entry {
915 Some((sf, bw, down)) => json!([sf, bw / 1_000, u8::from(*down)]),
916 None => json!([UNDEFINED_DATA_RATE, 0, 0]),
917 })
918 .collect(),
919 ),
920 );
921 }
922 Message::JoinRequest {
923 mhdr,
924 join_eui,
925 dev_eui,
926 dev_nonce,
927 mic,
928 data_rate,
929 frequency_hz,
930 levels,
931 } => {
932 object.insert("MHdr".to_owned(), json!(mhdr));
933 object.insert("JoinEui".to_owned(), Value::String(id6(*join_eui)));
934 object.insert("DevEui".to_owned(), Value::String(id6(*dev_eui)));
935 object.insert("DevNonce".to_owned(), json!(dev_nonce));
936 object.insert("MIC".to_owned(), json!(mic));
937 object.insert("DR".to_owned(), json!(data_rate));
938 object.insert("Freq".to_owned(), json!(frequency_hz));
939 object.insert("upinfo".to_owned(), levels.to_value());
940 }
941 Message::Uplink {
942 mhdr,
943 dev_addr,
944 fctrl,
945 fcnt,
946 fopts,
947 fport,
948 payload,
949 mic,
950 data_rate,
951 frequency_hz,
952 levels,
953 } => {
954 object.insert("MHdr".to_owned(), json!(mhdr));
955 object.insert("DevAddr".to_owned(), json!(dev_addr));
956 object.insert("FCtrl".to_owned(), json!(fctrl));
957 object.insert("FCnt".to_owned(), json!(fcnt));
958 object.insert("FOpts".to_owned(), Value::String(hex(fopts)));
959 object.insert("FPort".to_owned(), json!(fport.map_or(-1, i16::from)));
960 object.insert("FRMPayload".to_owned(), Value::String(hex(payload)));
961 object.insert("MIC".to_owned(), json!(mic));
962 object.insert("DR".to_owned(), json!(data_rate));
963 object.insert("Freq".to_owned(), json!(frequency_hz));
964 object.insert("upinfo".to_owned(), levels.to_value());
965 }
966 Message::Proprietary {
967 payload,
968 data_rate,
969 frequency_hz,
970 levels,
971 } => {
972 object.insert("FRMPayload".to_owned(), Value::String(hex(payload)));
973 object.insert("DR".to_owned(), json!(data_rate));
974 object.insert("Freq".to_owned(), json!(frequency_hz));
975 object.insert("upinfo".to_owned(), levels.to_value());
976 }
977 Message::Downlink {
978 dev_eui,
979 class,
980 diid,
981 pdu,
982 rx_delay,
983 rx1,
984 rx2,
985 ping_slot,
986 priority,
987 xtime,
988 rctx,
989 gpstime,
990 } => {
991 object.insert("DevEui".to_owned(), Value::String(id6(*dev_eui)));
992 object.insert("dC".to_owned(), json!(class));
993 object.insert("diid".to_owned(), json!(diid));
994 object.insert("pdu".to_owned(), Value::String(hex(pdu)));
995 if let Some(delay) = rx_delay {
996 object.insert("RxDelay".to_owned(), json!(delay));
997 }
998 if let Some((data_rate, frequency_hz)) = rx1 {
999 object.insert("RX1DR".to_owned(), json!(data_rate));
1000 object.insert("RX1Freq".to_owned(), json!(frequency_hz));
1001 }
1002 if let Some((data_rate, frequency_hz)) = rx2 {
1003 object.insert("RX2DR".to_owned(), json!(data_rate));
1004 object.insert("RX2Freq".to_owned(), json!(frequency_hz));
1005 }
1006 if let Some((data_rate, frequency_hz)) = ping_slot {
1007 object.insert("DR".to_owned(), json!(data_rate));
1008 object.insert("Freq".to_owned(), json!(frequency_hz));
1009 }
1010 object.insert("priority".to_owned(), json!(priority));
1011 if let Some(xtime) = xtime {
1012 object.insert("xtime".to_owned(), json!(xtime));
1013 }
1014 if let Some(rctx) = rctx {
1015 object.insert("rctx".to_owned(), json!(rctx));
1016 }
1017 if let Some(gpstime) = gpstime {
1018 object.insert("gpstime".to_owned(), json!(gpstime));
1019 }
1020 }
1021 Message::Schedule { frames } => {
1022 object.insert(
1023 "schedule".to_owned(),
1024 Value::Array(frames.iter().map(Broadcast::to_value).collect()),
1025 );
1026 }
1027 Message::Transmitted {
1028 diid,
1029 dev_eui,
1030 rctx,
1031 xtime,
1032 txtime,
1033 gpstime,
1034 } => {
1035 object.insert("diid".to_owned(), json!(diid));
1036 object.insert("DevEui".to_owned(), Value::String(id6(*dev_eui)));
1037 object.insert("rctx".to_owned(), json!(rctx));
1038 object.insert("xtime".to_owned(), json!(xtime));
1039 object.insert("txtime".to_owned(), json!(txtime));
1040 if let Some(gpstime) = gpstime {
1041 object.insert("gpstime".to_owned(), json!(gpstime));
1042 }
1043 }
1044 Message::TimeSync {
1045 txtime,
1046 xtime,
1047 gpstime,
1048 } => {
1049 if let Some(txtime) = txtime {
1050 object.insert("txtime".to_owned(), json!(txtime));
1051 }
1052 if let Some(xtime) = xtime {
1053 object.insert("xtime".to_owned(), json!(xtime));
1054 }
1055 if let Some(gpstime) = gpstime {
1056 object.insert("gpstime".to_owned(), json!(gpstime));
1057 }
1058 }
1059 Message::Other { .. } => {}
1060 }
1061
1062 Value::Object(object).to_string()
1063 }
1064
1065 pub fn from_json(body: &[u8]) -> Result<Message, ProtocolError> {
1083 let value: Value = serde_json::from_slice(body)
1084 .map_err(|error| refused(&format!("the message is not JSON: {error}")))?;
1085 let object = value
1086 .as_object()
1087 .ok_or_else(|| refused("the message is not a JSON object"))?;
1088 let msgtype =
1089 text(object, "msgtype").ok_or_else(|| refused("the message names no kind"))?;
1090
1091 Ok(match msgtype.as_str() {
1092 "version" => Message::Version {
1093 station: text(object, "station").unwrap_or_default(),
1094 firmware: text(object, "firmware").unwrap_or_default(),
1095 package: text(object, "package").unwrap_or_default(),
1096 model: text(object, "model").unwrap_or_default(),
1097 protocol: whole(object, "protocol").unwrap_or(0) as u32,
1098 features: text(object, "features").unwrap_or_default(),
1099 },
1100 "router_config" => Message::RouterConfig {
1101 net_id: numbers(object, "NetID")
1102 .map(|ids| ids.into_iter().map(|id| id as u32).collect()),
1103 join_eui: pairs(object, "JoinEui"),
1104 region: text(object, "region").unwrap_or_default(),
1105 max_eirp: number(object, "max_eirp").unwrap_or(0.0),
1106 hwspec: text(object, "hwspec").unwrap_or_default(),
1107 freq_range: {
1108 let range = numbers(object, "freq_range").unwrap_or_default();
1109 (
1110 range.first().copied().unwrap_or(0) as u32,
1111 range.get(1).copied().unwrap_or(0) as u32,
1112 )
1113 },
1114 data_rates: data_rates(object),
1115 },
1116 "jreq" => Message::JoinRequest {
1117 mhdr: whole(object, "MHdr")
1119 .or_else(|| whole(object, "Mhdr"))
1120 .unwrap_or(0) as u8,
1121 join_eui: identifier(object, "JoinEui")?,
1122 dev_eui: identifier(object, "DevEui")?,
1123 dev_nonce: whole(object, "DevNonce").unwrap_or(0) as u16,
1124 mic: whole(object, "MIC").unwrap_or(0) as i32,
1125 data_rate: whole(object, "DR").unwrap_or(0) as u8,
1126 frequency_hz: whole(object, "Freq").unwrap_or(0) as u32,
1127 levels: Levels::from_value(object.get("upinfo")),
1128 },
1129 "updf" => {
1130 let port = whole(object, "FPort").unwrap_or(-1);
1131 Message::Uplink {
1132 mhdr: whole(object, "MHdr")
1133 .or_else(|| whole(object, "Mhdr"))
1134 .unwrap_or(0) as u8,
1135 dev_addr: whole(object, "DevAddr").unwrap_or(0) as i32,
1136 fctrl: whole(object, "FCtrl").unwrap_or(0) as u8,
1137 fcnt: whole(object, "FCnt").unwrap_or(0) as u16,
1138 fopts: bytes_of(object, "FOpts")?,
1139 fport: (port >= 0).then_some(port as u8),
1140 payload: bytes_of(object, "FRMPayload")?,
1141 mic: whole(object, "MIC").unwrap_or(0) as i32,
1142 data_rate: whole(object, "DR").unwrap_or(0) as u8,
1143 frequency_hz: whole(object, "Freq").unwrap_or(0) as u32,
1144 levels: Levels::from_value(object.get("upinfo")),
1145 }
1146 }
1147 "propdf" => Message::Proprietary {
1148 payload: bytes_of(object, "FRMPayload")?,
1149 data_rate: whole(object, "DR").unwrap_or(0) as u8,
1150 frequency_hz: whole(object, "Freq").unwrap_or(0) as u32,
1151 levels: Levels::from_value(object.get("upinfo")),
1152 },
1153 "dnmsg" => Message::Downlink {
1154 dev_eui: identifier(object, "DevEui")?,
1155 class: whole(object, "dC").unwrap_or(0) as u8,
1156 diid: whole(object, "diid").unwrap_or(0),
1157 pdu: bytes_of(object, "pdu")?,
1158 rx_delay: whole(object, "RxDelay").map(|value| value as u8),
1159 rx1: window(object, "RX1DR", "RX1Freq"),
1160 rx2: window(object, "RX2DR", "RX2Freq"),
1161 ping_slot: window(object, "DR", "Freq"),
1162 priority: whole(object, "priority").unwrap_or(0) as u8,
1163 xtime: whole(object, "xtime"),
1164 rctx: whole(object, "rctx"),
1165 gpstime: whole(object, "gpstime"),
1166 },
1167 "dnsched" => {
1168 let mut frames = Vec::new();
1169 if let Some(scheduled) = object.get("schedule").and_then(Value::as_array) {
1170 for frame in scheduled {
1171 frames.push(Broadcast::from_value(frame)?);
1172 }
1173 }
1174 Message::Schedule { frames }
1175 }
1176 "dntxed" => Message::Transmitted {
1177 diid: whole(object, "diid").unwrap_or(0),
1178 dev_eui: identifier(object, "DevEui")?,
1179 rctx: whole(object, "rctx").unwrap_or(0),
1180 xtime: whole(object, "xtime").unwrap_or(0),
1181 txtime: number(object, "txtime").unwrap_or(0.0),
1182 gpstime: whole(object, "gpstime"),
1183 },
1184 "timesync" => Message::TimeSync {
1185 txtime: whole(object, "txtime"),
1186 xtime: whole(object, "xtime"),
1187 gpstime: whole(object, "gpstime"),
1188 },
1189 other => Message::Other {
1190 msgtype: other.to_owned(),
1191 },
1192 })
1193 }
1194}
1195
1196impl Levels {
1197 fn to_value(self) -> Value {
1199 let mut object = Map::new();
1200 object.insert("rctx".to_owned(), json!(self.rctx));
1201 object.insert("xtime".to_owned(), json!(self.xtime));
1202 if let Some(gpstime) = self.gpstime {
1203 object.insert("gpstime".to_owned(), json!(gpstime));
1204 }
1205 object.insert("rssi".to_owned(), json!(self.rssi));
1206 object.insert("snr".to_owned(), json!(self.snr));
1207 Value::Object(object)
1208 }
1209
1210 fn from_value(value: Option<&Value>) -> Levels {
1212 let Some(object) = value.and_then(Value::as_object) else {
1213 return Levels::default();
1214 };
1215 Levels {
1216 rctx: whole(object, "rctx").unwrap_or(0),
1217 xtime: whole(object, "xtime").unwrap_or(0),
1218 gpstime: whole(object, "gpstime"),
1219 rssi: number(object, "rssi").unwrap_or(0.0),
1220 snr: number(object, "snr").unwrap_or(0.0),
1221 }
1222 }
1223}
1224
1225fn hex(bytes: &[u8]) -> String {
1227 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
1228}
1229
1230fn bytes_of(object: &Map<String, Value>, key: &str) -> Result<Vec<u8>, ProtocolError> {
1232 let Some(written) = text(object, key) else {
1233 return Ok(Vec::new());
1234 };
1235 if written.len() % 2 != 0 {
1236 return Err(refused(&format!("{key} is not whole bytes")));
1237 }
1238 (0..written.len())
1239 .step_by(2)
1240 .map(|at| {
1241 u8::from_str_radix(&written[at..at + 2], 16)
1242 .map_err(|_| refused(&format!("{key} is not hexadecimal")))
1243 })
1244 .collect()
1245}
1246
1247fn identifier(object: &Map<String, Value>, key: &str) -> Result<Eui, ProtocolError> {
1249 text(object, key)
1250 .as_deref()
1251 .and_then(eui_of)
1252 .ok_or_else(|| refused(&format!("{key} is not an identifier")))
1253}
1254
1255fn window(object: &Map<String, Value>, rate: &str, frequency: &str) -> Option<(u8, u32)> {
1257 let rate = whole(object, rate)?;
1258 let frequency = whole(object, frequency)?;
1259 Some((rate as u8, frequency as u32))
1260}
1261
1262fn data_rates(object: &Map<String, Value>) -> Vec<Option<(u8, u32, bool)>> {
1268 let Some(Value::Array(entries)) = object.get("DRs") else {
1269 return Vec::new();
1270 };
1271 entries
1272 .iter()
1273 .map(|entry| {
1274 let triple = entry.as_array()?;
1275 let spreading = u8::try_from(integer(triple.first()?)?).ok()?;
1276 let bandwidth = u32::try_from(integer(triple.get(1)?)?).ok()?;
1277 let down = triple.get(2).and_then(integer).unwrap_or(0) != 0;
1278 Some((spreading, bandwidth.saturating_mul(1_000), down))
1279 })
1280 .collect()
1281}
1282
1283fn numbers(object: &Map<String, Value>, key: &str) -> Option<Vec<i64>> {
1285 let entries = object.get(key)?.as_array()?;
1286 Some(entries.iter().filter_map(integer).collect())
1287}
1288
1289fn pairs(object: &Map<String, Value>, key: &str) -> Vec<(u64, u64)> {
1294 let Some(Value::Array(entries)) = object.get(key) else {
1295 return Vec::new();
1296 };
1297 let bound = |value: &Value| {
1298 value
1299 .as_u64()
1300 .or_else(|| value.as_f64().map(|number| number as u64))
1301 };
1302 entries
1303 .iter()
1304 .filter_map(|entry| {
1305 let pair = entry.as_array()?;
1306 Some((bound(pair.first()?)?, bound(pair.get(1)?)?))
1307 })
1308 .collect()
1309}
1310
1311fn text(object: &Map<String, Value>, key: &str) -> Option<String> {
1313 object.get(key).and_then(Value::as_str).map(str::to_owned)
1314}
1315
1316fn number(object: &Map<String, Value>, key: &str) -> Option<f64> {
1318 object.get(key).and_then(Value::as_f64)
1319}
1320
1321fn whole(object: &Map<String, Value>, key: &str) -> Option<i64> {
1323 object.get(key).and_then(integer)
1324}
1325
1326fn integer(value: &Value) -> Option<i64> {
1331 value
1332 .as_i64()
1333 .or_else(|| value.as_f64().map(|number| number.round() as i64))
1334}
1335
1336fn least_first(bytes: &[u8]) -> Eui {
1338 let mut eui = [0u8; 8];
1339 for (index, byte) in bytes.iter().rev().enumerate() {
1340 eui[index] = *byte;
1341 }
1342 Eui::new(eui)
1343}
1344
1345fn refused(why: &str) -> ProtocolError {
1347 ProtocolError::Payload(why.to_owned())
1348}
1349
1350#[cfg(feature = "station-client")]
1359pub struct Station {
1360 socket: tokio_tungstenite::WebSocketStream<
1361 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
1362 >,
1363 config: Message,
1364}
1365
1366#[cfg(feature = "station-client")]
1367impl core::fmt::Debug for Station {
1368 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1369 f.debug_struct("Station")
1370 .field("config", &self.config)
1371 .finish_non_exhaustive()
1372 }
1373}
1374
1375#[cfg(feature = "station-client")]
1376impl Station {
1377 pub async fn connect(endpoint: &str, router: Eui) -> Result<Station, ProtocolError> {
1397 let answer = discover(endpoint, router).await?;
1398 if let Some(error) = answer.error {
1399 return Err(refused(&format!(
1400 "the server refused this station: {error}"
1401 )));
1402 }
1403 let uri = answer
1404 .uri
1405 .ok_or_else(|| refused("the server named no websocket to open"))?;
1406
1407 let (mut socket, _) = tokio_tungstenite::connect_async(uri.as_str())
1408 .await
1409 .map_err(|error| refused(&format!("the websocket did not open: {error}")))?;
1410
1411 let version = Message::Version {
1413 station: "pamoja".to_owned(),
1414 firmware: env!("CARGO_PKG_VERSION").to_owned(),
1415 package: "pamoja-gateway".to_owned(),
1416 model: "pamoja".to_owned(),
1417 protocol: PROTOCOL_VERSION,
1418 features: String::new(),
1419 };
1420 send(&mut socket, &version).await?;
1421 let config = receive(&mut socket).await?;
1422
1423 Ok(Station { socket, config })
1424 }
1425
1426 pub const fn config(&self) -> &Message {
1432 &self.config
1433 }
1434
1435 pub async fn send(&mut self, message: &Message) -> Result<(), ProtocolError> {
1449 send(&mut self.socket, message).await
1450 }
1451
1452 pub async fn recv(&mut self) -> Result<Message, ProtocolError> {
1463 receive(&mut self.socket).await
1464 }
1465}
1466
1467#[cfg(feature = "station-client")]
1469async fn discover(endpoint: &str, router: Eui) -> Result<Router, ProtocolError> {
1470 use futures_util::{SinkExt, StreamExt};
1471
1472 let (mut socket, _) = tokio_tungstenite::connect_async(asking(endpoint))
1473 .await
1474 .map_err(|error| refused(&format!("the discovery endpoint is not reachable: {error}")))?;
1475
1476 socket
1477 .send(tokio_tungstenite::tungstenite::Message::text(
1478 Discovery::new(router).to_json(),
1479 ))
1480 .await
1481 .map_err(|error| refused(&format!("the discovery request failed: {error}")))?;
1482
1483 let answer = socket
1484 .next()
1485 .await
1486 .ok_or_else(|| refused("the discovery endpoint answered nothing"))?
1487 .map_err(|error| refused(&format!("the discovery answer failed: {error}")))?;
1488 let answer = Router::from_json(
1489 answer
1490 .to_text()
1491 .map_err(|_| refused("the discovery answer is not text"))?
1492 .as_bytes(),
1493 )?;
1494
1495 let _ = socket.close(None).await;
1497 Ok(answer)
1498}
1499
1500#[cfg(feature = "station-client")]
1502fn asking(endpoint: &str) -> String {
1503 let endpoint = endpoint.trim_end_matches("/");
1504 let endpoint = match endpoint.split_once("://") {
1506 Some(("http", rest)) => format!("ws://{rest}"),
1507 Some(("https", rest)) => format!("wss://{rest}"),
1508 Some(_) => endpoint.to_owned(),
1509 None => format!("ws://{endpoint}"),
1510 };
1511 format!("{endpoint}{DISCOVERY_PATH}")
1512}
1513
1514#[cfg(feature = "station-client")]
1516async fn send<S>(socket: &mut S, message: &Message) -> Result<(), ProtocolError>
1517where
1518 S: futures_util::Sink<tokio_tungstenite::tungstenite::Message> + Unpin,
1519 S::Error: core::fmt::Display,
1520{
1521 use futures_util::SinkExt;
1522
1523 socket
1524 .send(tokio_tungstenite::tungstenite::Message::text(
1525 message.to_json(),
1526 ))
1527 .await
1528 .map_err(|error| refused(&format!("the websocket would not take a message: {error}")))
1529}
1530
1531#[cfg(feature = "station-client")]
1533async fn receive<S>(socket: &mut S) -> Result<Message, ProtocolError>
1534where
1535 S: futures_util::Stream<
1536 Item = Result<
1537 tokio_tungstenite::tungstenite::Message,
1538 tokio_tungstenite::tungstenite::Error,
1539 >,
1540 > + Unpin,
1541{
1542 use futures_util::StreamExt;
1543
1544 loop {
1545 let frame = socket
1546 .next()
1547 .await
1548 .ok_or_else(|| refused("the websocket closed"))?
1549 .map_err(|error| refused(&format!("the websocket failed: {error}")))?;
1550
1551 match frame.to_text() {
1553 Ok(text) if !text.is_empty() => return Message::from_json(text.as_bytes()),
1554 _ => continue,
1555 }
1556 }
1557}
1558
1559#[cfg(test)]
1560mod tests {
1561 use super::*;
1562
1563 #[test]
1566 fn the_published_identifier_examples_read_and_write() {
1567 let zero = Eui::new([0; 8]);
1568 assert_eq!(id6(zero), "::");
1569 assert_eq!(eui_of("::0"), Some(zero));
1570
1571 let one_high = Eui::new([0x00, 0x01, 0, 0, 0, 0, 0, 0]);
1572 assert_eq!(eui_of("1::"), Some(one_high));
1573 assert_eq!(id6(one_high), "1::");
1574
1575 let low_pair = Eui::new([0, 0, 0, 0, 0, 0x0a, 0, 0x0b]);
1576 assert_eq!(eui_of("::a:b"), Some(low_pair));
1577 assert_eq!(id6(low_pair), "::a:b");
1578 }
1579
1580 #[test]
1581 fn an_identifier_reads_in_every_form_the_protocol_accepts() {
1582 let eui = Eui::new([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
1583 assert_eq!(eui_of("102:304:506:708"), Some(eui));
1584 assert_eq!(eui_of("01-02-03-04-05-06-07-08"), Some(eui));
1585 assert_eq!(
1586 eui_of(&u64::from_be_bytes(eui.bytes()).to_string()),
1587 Some(eui)
1588 );
1589 assert_eq!(eui_of("not an identifier"), None);
1590 assert_eq!(eui_of("1:2:3"), None);
1591 }
1592
1593 #[test]
1594 fn discovery_asks_and_is_answered() {
1595 let station = Eui::from_hex("b827ebfffe010203").expect("sixteen digits");
1596 let asking = Discovery::new(station);
1597 let read = Discovery::from_json(asking.to_json().as_bytes()).expect("it round trips");
1598 assert_eq!(read.router, station);
1599
1600 let muxs = Eui::new([0, 0, 0, 0, 0, 0, 0, 1]);
1601 let answer = Router::accepted(station, muxs, "ws://lns.example.invalid:3001/router");
1602 let read = Router::from_json(answer.to_json().as_bytes()).expect("it round trips");
1603 assert_eq!(read.router, Some(station));
1604 assert_eq!(read.muxs, Some(muxs));
1605 assert_eq!(
1606 read.uri.as_deref(),
1607 Some("ws://lns.example.invalid:3001/router")
1608 );
1609 assert!(read.error.is_none());
1610 }
1611
1612 #[test]
1613 fn a_refused_station_is_told_why() {
1614 let station = Eui::from_hex("b827ebfffe010203").expect("sixteen digits");
1615 let refusal = Router::refused(station, "this gateway is not registered");
1616 let read = Router::from_json(refusal.to_json().as_bytes()).expect("it round trips");
1617 assert_eq!(
1618 read.error.as_deref(),
1619 Some("this gateway is not registered")
1620 );
1621 assert!(read.uri.is_none());
1622 }
1623
1624 #[test]
1627 fn every_message_kind_survives_a_round_trip() {
1628 let device = Eui::from_hex("1111111111111111").expect("sixteen digits");
1629 let application = Eui::from_hex("2222222222222222").expect("sixteen digits");
1630 let levels = Levels {
1631 rctx: 1,
1632 xtime: 3_512_348_611,
1633 gpstime: Some(1_364_746_877),
1634 rssi: -35.0,
1635 snr: 5.1,
1636 };
1637
1638 let kinds = [
1639 Message::Version {
1640 station: "pamoja".to_owned(),
1641 firmware: "0.1.18".to_owned(),
1642 package: "pamoja-gateway".to_owned(),
1643 model: "linux".to_owned(),
1644 protocol: PROTOCOL_VERSION,
1645 features: "gps".to_owned(),
1646 },
1647 Message::RouterConfig {
1648 net_id: Some(vec![0]),
1649 join_eui: vec![(0, u64::MAX)],
1650 region: "EU868".to_owned(),
1651 max_eirp: 16.0,
1652 hwspec: "sx1301/1".to_owned(),
1653 freq_range: (863_000_000, 870_000_000),
1654 data_rates: vec![
1655 Some((12, 125_000, false)),
1656 Some((7, 250_000, false)),
1657 Some((0, 0, true)),
1658 ],
1659 },
1660 Message::JoinRequest {
1661 mhdr: 0x00,
1662 join_eui: application,
1663 dev_eui: device,
1664 dev_nonce: 0x0102,
1665 mic: -12345,
1666 data_rate: 5,
1667 frequency_hz: 868_100_000,
1668 levels,
1669 },
1670 Message::Uplink {
1671 mhdr: 0x40,
1672 dev_addr: 0x2601_0001,
1673 fctrl: 0,
1674 fcnt: 7,
1675 fopts: vec![0x03, 0x07],
1676 fport: Some(2),
1677 payload: b"21.5".to_vec(),
1678 mic: 987,
1679 data_rate: 5,
1680 frequency_hz: 868_100_000,
1681 levels,
1682 },
1683 Message::Downlink {
1684 dev_eui: device,
1685 class: 0,
1686 diid: 42,
1687 pdu: vec![0x60, 0x01, 0x02],
1688 rx_delay: Some(1),
1689 rx1: Some((5, 868_100_000)),
1690 rx2: Some((0, 869_525_000)),
1691 ping_slot: None,
1692 priority: 10,
1693 xtime: Some(3_513_348_611),
1694 rctx: Some(1),
1695 gpstime: None,
1696 },
1697 Message::Transmitted {
1698 diid: 42,
1699 dev_eui: device,
1700 rctx: 1,
1701 xtime: 3_513_348_611,
1702 txtime: 1.5,
1703 gpstime: Some(1_364_746_878),
1704 },
1705 Message::TimeSync {
1706 txtime: Some(1_000),
1707 xtime: None,
1708 gpstime: Some(1_364_746_879),
1709 },
1710 Message::Proprietary {
1711 payload: vec![0xe0, 0x01, 0x02, 0x03],
1712 data_rate: 5,
1713 frequency_hz: 868_100_000,
1714 levels,
1715 },
1716 Message::Schedule {
1717 frames: vec![Broadcast {
1718 pdu: vec![0x60, 0x01, 0x02],
1719 data_rate: 3,
1720 frequency_hz: 869_525_000,
1721 priority: 1,
1722 gpstime: Some(1_364_746_880),
1723 rctx: Some(0),
1724 }],
1725 },
1726 ];
1727
1728 for sent in kinds {
1729 let written = sent.to_json();
1730 let read = Message::from_json(written.as_bytes()).expect("it is well formed");
1731 assert_eq!(
1732 read,
1733 sent,
1734 "{} did not survive the round trip",
1735 sent.msgtype()
1736 );
1737 assert!(
1738 written.contains(&format!("\"msgtype\":\"{}\"", sent.msgtype())),
1739 "a message names its kind"
1740 );
1741 }
1742 }
1743
1744 #[test]
1747 fn an_xtime_holds_its_parts_where_the_reference_station_reads_them() {
1748 let clock = Xtime::new(3, 0xa5, 3_512_348_611).expect("in range");
1749 let value = clock.value();
1750 assert_eq!((value >> 56) & 0x7f, 3);
1751 assert_eq!((value >> 48) & 0xff, 0xa5);
1752 assert_eq!(value & 0xffff_ffff_ffff, 3_512_348_611);
1753 assert!(value > 0, "the sign bit stays clear");
1754 assert_eq!(Xtime::of(value), clock);
1755 assert_eq!(
1756 Xtime::of(i64::MAX),
1757 Xtime {
1758 unit: 0x7f,
1759 session: 0xff,
1760 micros: (1 << 48) - 1
1761 }
1762 );
1763 }
1764
1765 #[test]
1768 fn an_xtime_a_double_cannot_hold_comes_back_to_the_microsecond() {
1769 let xtime = (0xa5_i64 << 48) | 3_512_348_611;
1770 assert!(
1771 xtime > 1 << 53,
1772 "the session byte puts it out of a double's reach"
1773 );
1774 let answer = Message::Downlink {
1775 dev_eui: Eui::from_hex("70b3d57ed0001234").expect("sixteen digits"),
1776 class: 0,
1777 diid: i64::MAX - 1,
1778 pdu: vec![0x60],
1779 rx_delay: Some(1),
1780 rx1: Some((5, 868_100_000)),
1781 rx2: Some((0, 869_525_000)),
1782 ping_slot: None,
1783 priority: 0,
1784 xtime: Some(xtime + 1_000_001),
1785 rctx: Some(0),
1786 gpstime: None,
1787 };
1788 let read = Message::from_json(answer.to_json().as_bytes()).expect("it is well formed");
1789 assert_eq!(read, answer);
1790 }
1791
1792 #[test]
1793 fn a_join_identifier_range_reads_back_exactly() {
1794 let first = u64::from_be_bytes(
1795 Eui::from_hex("70b3d57ed0000001")
1796 .expect("sixteen digits")
1797 .bytes(),
1798 );
1799 let config = Message::RouterConfig {
1800 net_id: None,
1801 join_eui: vec![(first, first + 0xff)],
1802 region: "EU868".to_owned(),
1803 max_eirp: 16.0,
1804 hwspec: "sx1301/1".to_owned(),
1805 freq_range: (863_000_000, 870_000_000),
1806 data_rates: Vec::new(),
1807 };
1808 let read = Message::from_json(config.to_json().as_bytes()).expect("it is well formed");
1809 assert_eq!(read, config);
1810 }
1811
1812 #[test]
1815 fn a_table_with_undefined_numbers_keeps_every_rate_at_its_number() {
1816 let mut table: Vec<Option<(u8, u32, bool)>> = (0..=3)
1817 .map(|index| Some((10 - index, 125_000, false)))
1818 .collect();
1819 table.push(Some((8, 500_000, false)));
1820 table.extend([None, None, None]);
1821 table.extend((0..=5).map(|index| Some((12 - index, 500_000, true))));
1822 let config = Message::RouterConfig {
1823 net_id: Some(vec![0x13]),
1824 join_eui: Vec::new(),
1825 region: "US915".to_owned(),
1826 max_eirp: 30.0,
1827 hwspec: "sx1301/1".to_owned(),
1828 freq_range: (902_000_000, 928_000_000),
1829 data_rates: table.clone(),
1830 };
1831
1832 let written = config.to_json();
1833 assert!(
1834 written.contains("[8,500,0],[-1,0,0],[-1,0,0],[-1,0,0],[12,500,1]"),
1835 "{written}"
1836 );
1837 let Message::RouterConfig { data_rates, .. } =
1838 Message::from_json(written.as_bytes()).expect("it is well formed")
1839 else {
1840 panic!("a configuration reads back as one");
1841 };
1842 assert_eq!(data_rates, table);
1843 assert_eq!(data_rates[8], Some((12, 500_000, true)));
1844 }
1845
1846 #[test]
1847 fn an_entry_that_cannot_be_read_holds_its_place() {
1848 let read = Message::from_json(
1849 br#"{"msgtype":"router_config","DRs":[[12,125,0],"none",[-1,0,0],[7,125,0]]}"#,
1850 )
1851 .expect("it is well formed");
1852 let Message::RouterConfig { data_rates, .. } = read else {
1853 panic!("a configuration reads back as one");
1854 };
1855 assert_eq!(
1856 data_rates,
1857 vec![
1858 Some((12, 125_000, false)),
1859 None,
1860 None,
1861 Some((7, 125_000, false))
1862 ]
1863 );
1864 }
1865
1866 #[test]
1869 fn no_network_filter_is_written_as_null_and_an_empty_one_as_empty() {
1870 let filtered = |net_id: Option<Vec<u32>>| Message::RouterConfig {
1871 net_id,
1872 join_eui: Vec::new(),
1873 region: "EU868".to_owned(),
1874 max_eirp: 16.0,
1875 hwspec: "sx1301/1".to_owned(),
1876 freq_range: (863_000_000, 870_000_000),
1877 data_rates: Vec::new(),
1878 };
1879
1880 let open = filtered(None);
1881 assert!(open.to_json().contains("\"NetID\":null"));
1882 assert_eq!(
1883 Message::from_json(open.to_json().as_bytes()).expect("well formed"),
1884 open
1885 );
1886
1887 let closed = filtered(Some(Vec::new()));
1888 assert!(closed.to_json().contains("\"NetID\":[]"));
1889 assert_eq!(
1890 Message::from_json(closed.to_json().as_bytes()).expect("well formed"),
1891 closed
1892 );
1893 }
1894
1895 #[test]
1896 fn a_class_b_frame_carries_its_ping_slot_and_gps_time() {
1897 let beaconed = Message::Downlink {
1898 dev_eui: Eui::from_hex("70b3d57ed0001234").expect("sixteen digits"),
1899 class: 1,
1900 diid: 7,
1901 pdu: vec![0x60, 0x01],
1902 rx_delay: None,
1903 rx1: None,
1904 rx2: None,
1905 ping_slot: Some((3, 869_525_000)),
1906 priority: 0,
1907 xtime: None,
1908 rctx: Some(0),
1909 gpstime: Some(1_442_000_000_000_000),
1910 };
1911 let written = beaconed.to_json();
1912 assert!(written.contains("\"DR\":3"), "{written}");
1913 assert!(written.contains("\"Freq\":869525000"), "{written}");
1914 assert!(
1915 written.contains("\"gpstime\":1442000000000000"),
1916 "{written}"
1917 );
1918 assert!(!written.contains("RX1DR"), "{written}");
1919 let read = Message::from_json(written.as_bytes()).expect("it is well formed");
1920 assert_eq!(read, beaconed);
1921 }
1922
1923 #[test]
1924 fn a_frame_carrying_only_options_has_no_port() {
1925 let quiet = Message::Uplink {
1926 mhdr: 0x40,
1927 dev_addr: 1,
1928 fctrl: 0,
1929 fcnt: 0,
1930 fopts: vec![0x02],
1931 fport: None,
1932 payload: Vec::new(),
1933 mic: 0,
1934 data_rate: 5,
1935 frequency_hz: 868_100_000,
1936 levels: Levels::default(),
1937 };
1938 let written = quiet.to_json();
1939 assert!(written.contains("\"FPort\":-1"), "{written}");
1941 let read = Message::from_json(written.as_bytes()).expect("it is well formed");
1942 assert_eq!(read, quiet);
1943 }
1944
1945 #[test]
1946 fn a_kind_this_crate_does_not_build_is_reported_not_refused() {
1947 let read = Message::from_json(br#"{"msgtype":"rmtsh","user":"root"}"#)
1948 .expect("an unknown kind is not an error");
1949 assert_eq!(
1950 read,
1951 Message::Other {
1952 msgtype: "rmtsh".to_owned()
1953 }
1954 );
1955 }
1956
1957 #[test]
1958 fn a_message_that_is_not_one_is_refused() {
1959 assert!(Message::from_json(b"not json").is_err());
1960 assert!(Message::from_json(br#"{"station":"pamoja"}"#).is_err());
1961 assert!(Message::from_json(br#"{"msgtype":"jreq","DevEui":"nonsense"}"#).is_err());
1962 assert!(Message::from_json(br#"{"msgtype":"dnmsg","DevEui":"::1","pdu":"abc"}"#).is_err());
1963 }
1964
1965 #[cfg(feature = "network")]
1968 #[test]
1969 fn a_join_request_a_device_built_reads_back_field_for_field() {
1970 use pamoja_lorawan::Device;
1971
1972 let dev_eui = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
1973 let join_eui = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
1974 let request = Device::new(dev_eui, join_eui, [0x2b; 16]).join_request(0x0102);
1975
1976 let heard = Message::heard(request.as_bytes(), 5, 868_100_000, Levels::default())
1977 .expect("a station sends a join request up");
1978 match heard {
1979 Message::JoinRequest {
1980 mhdr,
1981 join_eui: application,
1982 dev_eui: node,
1983 dev_nonce,
1984 ..
1985 } => {
1986 assert_eq!(mhdr, 0x00);
1987 assert_eq!(application, Eui::new(join_eui));
1988 assert_eq!(node, Eui::new(dev_eui));
1989 assert_eq!(dev_nonce, 0x0102);
1990 }
1991 other => panic!("that is a join request, not {}", other.msgtype()),
1992 }
1993 }
1994
1995 #[cfg(feature = "network")]
1996 #[test]
1997 fn an_encrypted_frame_reads_back_with_its_address_port_and_counter() {
1998 use pamoja_lorawan::{Session, Uplink as LorawanUplink};
1999
2000 let session = Session::new(0x2601_0001, [0x11; 16], [0x22; 16]);
2001 let frame = session
2002 .encode_uplink(&LorawanUplink::new(7, 2, b"21.5"))
2003 .expect("it fits one frame");
2004
2005 let heard = Message::heard(frame.as_bytes(), 5, 868_100_000, Levels::default())
2006 .expect("a station sends a data frame up");
2007 match heard {
2008 Message::Uplink {
2009 dev_addr,
2010 fcnt,
2011 fport,
2012 fopts,
2013 payload,
2014 ..
2015 } => {
2016 assert_eq!(dev_addr, 0x2601_0001);
2017 assert_eq!(fcnt, 7);
2018 assert_eq!(fport, Some(2));
2019 assert!(fopts.is_empty());
2020 assert_eq!(payload.len(), 4);
2022 assert_ne!(payload, b"21.5".to_vec());
2023 }
2024 other => panic!("that is a data frame, not {}", other.msgtype()),
2025 }
2026 }
2027
2028 #[test]
2029 fn a_frame_of_options_alone_carries_no_port() {
2030 let frame = [
2033 0x40, 0x01, 0x00, 0x00, 0x00, 0x02, 0x05, 0x00, 0x03, 0x07, 0x11, 0x22, 0x33, 0x44,
2034 ];
2035 let heard = Message::heard(&frame, 5, 868_100_000, Levels::default())
2036 .expect("a station sends it up");
2037 match heard {
2038 Message::Uplink {
2039 dev_addr,
2040 fcnt,
2041 fopts,
2042 fport,
2043 payload,
2044 ..
2045 } => {
2046 assert_eq!(dev_addr, 1);
2047 assert_eq!(fcnt, 5);
2048 assert_eq!(fopts, vec![0x03, 0x07]);
2049 assert_eq!(fport, None);
2050 assert!(payload.is_empty());
2051 }
2052 other => panic!("that is a data frame, not {}", other.msgtype()),
2053 }
2054 }
2055
2056 #[test]
2057 fn a_proprietary_frame_is_passed_along_whole() {
2058 let frame = [0xe0, 0x01, 0x02, 0x03, 0x04, 0x05];
2059 let heard = Message::heard(&frame, 5, 868_100_000, Levels::default())
2060 .expect("a station sends it up");
2061 match heard {
2062 Message::Proprietary { payload, .. } => assert_eq!(payload, frame.to_vec()),
2063 other => panic!("that one is proprietary, not {}", other.msgtype()),
2064 }
2065 }
2066
2067 #[test]
2068 fn a_frame_a_station_does_not_send_up_is_refused() {
2069 let levels = Levels::default();
2070 assert!(Message::heard(&[0x40, 0x00], 5, 868_100_000, levels).is_err());
2072 let downlink = [0x60, 1, 0, 0, 0, 0, 0, 0, 0, 0x11, 0x22, 0x33, 0x44];
2074 assert!(Message::heard(&downlink, 5, 868_100_000, levels).is_err());
2075 let overrun = [0x40, 1, 0, 0, 0, 0x0f, 0, 0, 0x11, 0x22, 0x33, 0x44];
2077 assert!(Message::heard(&overrun, 5, 868_100_000, levels).is_err());
2078 }
2079
2080 #[test]
2081 fn what_is_not_this_protocol_is_refused() {
2082 assert!(Discovery::from_json(b"not json").is_err());
2083 assert!(Discovery::from_json(br#"{"router":"not an identifier"}"#).is_err());
2084 }
2085}
2086
2087#[cfg(all(test, feature = "station-client"))]
2088mod live {
2089 use super::*;
2090
2091 use futures_util::{SinkExt, StreamExt};
2092 use tokio::net::TcpListener;
2093 use tokio_tungstenite::tungstenite::handshake::server::{Request, Response};
2094
2095 fn router() -> Eui {
2097 Eui::from_hex("0102030405060708").expect("sixteen digits")
2098 }
2099
2100 fn muxs() -> Eui {
2102 Eui::from_hex("0000000000000000").expect("sixteen digits")
2103 }
2104
2105 #[allow(clippy::result_large_err)]
2111 async fn endpoint(answer: String) -> (String, tokio::task::JoinHandle<(String, String)>) {
2112 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
2113 let address = listener.local_addr().expect("a bound port");
2114
2115 let serving = tokio::spawn(async move {
2116 let (stream, _) = listener.accept().await.expect("the station asks");
2117
2118 let seen = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
2119 let noted = std::sync::Arc::clone(&seen);
2120 let mut socket = tokio_tungstenite::accept_hdr_async(
2121 stream,
2122 move |request: &Request, response: Response| {
2123 noted
2124 .lock()
2125 .expect("the path is recorded once")
2126 .push_str(request.uri().path());
2127 Ok(response)
2128 },
2129 )
2130 .await
2131 .expect("the websocket opens");
2132 let path = seen.lock().expect("the path was recorded").clone();
2133
2134 let asked = socket
2135 .next()
2136 .await
2137 .expect("the station asks")
2138 .expect("it is a frame");
2139 let asked = asked.to_text().expect("it is text").to_owned();
2140
2141 socket
2142 .send(tokio_tungstenite::tungstenite::Message::text(answer))
2143 .await
2144 .expect("the answer is sent");
2145 let _ = socket.close(None).await;
2146
2147 (path, asked)
2148 });
2149
2150 (format!("ws://{address}"), serving)
2151 }
2152
2153 #[tokio::test]
2154 async fn a_station_asks_where_its_network_server_is() {
2155 let uri = "ws://127.0.0.1:6038/router";
2156 let (where_to_ask, serving) =
2157 endpoint(Router::accepted(router(), muxs(), uri).to_json()).await;
2158
2159 let answer = discover(&where_to_ask, router())
2160 .await
2161 .expect("the endpoint answers");
2162 assert_eq!(answer.uri.as_deref(), Some(uri));
2163
2164 let (path, asked) = serving.await.expect("the endpoint served");
2165 assert_eq!(path, DISCOVERY_PATH);
2166 assert_eq!(asked, r#"{"router":"102:304:506:708"}"#);
2168 }
2169
2170 #[tokio::test]
2171 async fn a_station_the_server_will_not_have_is_told_why() {
2172 let (where_to_ask, serving) =
2173 endpoint(Router::refused(router(), "unknown gateway").to_json()).await;
2174
2175 let refusal = Station::connect(&where_to_ask, router())
2176 .await
2177 .expect_err("a refused station does not connect");
2178 assert!(
2179 format!("{refusal}").contains("unknown gateway"),
2180 "{refusal}"
2181 );
2182
2183 serving.await.expect("the endpoint served");
2184 }
2185
2186 #[tokio::test]
2187 async fn a_station_says_what_it_is_and_is_given_a_configuration() {
2188 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
2189 let address = listener.local_addr().expect("a bound port");
2190
2191 let configuration = Message::RouterConfig {
2192 net_id: Some(vec![0]),
2193 join_eui: vec![(0, u64::MAX)],
2194 region: "EU868".to_owned(),
2195 max_eirp: 16.0,
2196 hwspec: "sx1301/1".to_owned(),
2197 freq_range: (863_000_000, 870_000_000),
2198 data_rates: vec![Some((12, 125_000, false)), Some((7, 250_000, false))],
2199 };
2200 let sent_down = Message::Downlink {
2201 dev_eui: Eui::from_hex("1111111111111111").expect("sixteen digits"),
2202 class: 0,
2203 diid: 7,
2204 pdu: vec![0x60, 0x01],
2205 rx_delay: Some(1),
2206 rx1: Some((5, 868_100_000)),
2207 rx2: None,
2208 ping_slot: None,
2209 priority: 0,
2210 xtime: Some(3_513_348_611),
2211 rctx: Some(1),
2212 gpstime: None,
2213 };
2214
2215 let answering = tokio::spawn({
2216 let configuration = configuration.clone();
2217 let sent_down = sent_down.clone();
2218 async move {
2219 let (stream, _) = listener.accept().await.expect("the station connects");
2220 let mut socket = tokio_tungstenite::accept_async(stream)
2221 .await
2222 .expect("the websocket opens");
2223
2224 let opening = socket
2226 .next()
2227 .await
2228 .expect("the station speaks")
2229 .expect("it is a frame");
2230 let opening = Message::from_json(opening.to_text().expect("it is text").as_bytes())
2231 .expect("it is this protocol");
2232
2233 socket
2234 .send(tokio_tungstenite::tungstenite::Message::text(
2235 configuration.to_json(),
2236 ))
2237 .await
2238 .expect("the configuration is sent");
2239
2240 let carried = socket
2241 .next()
2242 .await
2243 .expect("the station reports")
2244 .expect("it is a frame");
2245 let carried = Message::from_json(carried.to_text().expect("it is text").as_bytes())
2246 .expect("it is this protocol");
2247
2248 socket
2249 .send(tokio_tungstenite::tungstenite::Message::text(
2250 sent_down.to_json(),
2251 ))
2252 .await
2253 .expect("the downlink is sent");
2254
2255 (opening, carried)
2256 }
2257 });
2258
2259 let (where_to_ask, serving) = endpoint(
2260 Router::accepted(router(), muxs(), format!("ws://{address}/router")).to_json(),
2261 )
2262 .await;
2263
2264 let mut station = Station::connect(&where_to_ask, router())
2265 .await
2266 .expect("the station connects");
2267 assert_eq!(station.config(), &configuration);
2268
2269 let reported = Message::Uplink {
2270 mhdr: 0x40,
2271 dev_addr: 0x2601_0001,
2272 fctrl: 0,
2273 fcnt: 1,
2274 fopts: Vec::new(),
2275 fport: Some(2),
2276 payload: b"21.5".to_vec(),
2277 mic: 987,
2278 data_rate: 5,
2279 frequency_hz: 868_100_000,
2280 levels: Levels::default(),
2281 };
2282 station.send(&reported).await.expect("the uplink is sent");
2283 assert_eq!(station.recv().await.expect("the server answers"), sent_down);
2284
2285 serving.await.expect("the endpoint served");
2286 let (opening, carried) = answering.await.expect("the server served");
2287 assert_eq!(carried, reported);
2288 match opening {
2289 Message::Version {
2290 station, protocol, ..
2291 } => {
2292 assert_eq!(station, "pamoja");
2293 assert_eq!(protocol, PROTOCOL_VERSION);
2294 }
2295 other => panic!("a station opens with its version, not {}", other.msgtype()),
2296 }
2297 }
2298}