1use std::os::raw::c_char;
21use std::ptr;
22
23use pamoja_gateway::station::{eui_of, id6, Broadcast, Discovery, Levels, Message, Router, Xtime};
24use pamoja_gateway::udp::Eui;
25
26use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
27
28pub const PAMOJA_GATEWAY_STATION_JOIN_REQUEST: u8 = 0;
30
31pub const PAMOJA_GATEWAY_STATION_UPLINK: u8 = 1;
33
34pub const PAMOJA_GATEWAY_STATION_PROPRIETARY: u8 = 2;
36
37pub const PAMOJA_GATEWAY_STATION_VERSION: u8 = 3;
39
40pub const PAMOJA_GATEWAY_STATION_ROUTER_CONFIG: u8 = 4;
42
43pub const PAMOJA_GATEWAY_STATION_DOWNLINK: u8 = 5;
45
46pub const PAMOJA_GATEWAY_STATION_SCHEDULE: u8 = 6;
48
49pub const PAMOJA_GATEWAY_STATION_TRANSMITTED: u8 = 7;
51
52pub const PAMOJA_GATEWAY_STATION_TIME_SYNC: u8 = 8;
54
55pub const PAMOJA_GATEWAY_STATION_OTHER: u8 = 9;
57
58pub const PAMOJA_GATEWAY_STATION_PROTOCOL_VERSION: u32 = 2;
60
61const _: () =
64 assert!(PAMOJA_GATEWAY_STATION_PROTOCOL_VERSION == pamoja_gateway::station::PROTOCOL_VERSION);
65
66pub const PAMOJA_GATEWAY_STATION_TEXT_STATION: u8 = 0;
68
69pub const PAMOJA_GATEWAY_STATION_TEXT_FIRMWARE: u8 = 1;
71
72pub const PAMOJA_GATEWAY_STATION_TEXT_PACKAGE: u8 = 2;
74
75pub const PAMOJA_GATEWAY_STATION_TEXT_MODEL: u8 = 3;
77
78pub const PAMOJA_GATEWAY_STATION_TEXT_FEATURES: u8 = 4;
80
81pub const PAMOJA_GATEWAY_STATION_TEXT_REGION: u8 = 5;
83
84pub const PAMOJA_GATEWAY_STATION_TEXT_HWSPEC: u8 = 6;
86
87pub const PAMOJA_GATEWAY_STATION_TEXT_MSGTYPE: u8 = 7;
90
91pub struct PamojaGatewayStationMessage {
93 message: Message,
94}
95
96#[repr(C)]
98#[derive(Clone, Copy, Debug)]
99pub struct PamojaGatewayStationLevels {
100 pub rctx: i64,
102 pub xtime: i64,
104 pub gpstime: i64,
106 pub has_gpstime: bool,
108 pub rssi: f64,
110 pub snr: f64,
112}
113
114#[repr(C)]
116#[derive(Clone, Copy, Debug)]
117pub struct PamojaGatewayStationWindow {
118 pub data_rate: u8,
120 pub frequency_hz: u32,
122 pub present: bool,
124}
125
126#[repr(C)]
128#[derive(Clone, Copy, Debug)]
129pub struct PamojaGatewayStationDataRate {
130 pub spreading_factor: u8,
132 pub bandwidth_hz: u32,
134 pub downlink_only: bool,
136 pub defined: bool,
139}
140
141#[repr(C)]
143#[derive(Clone, Copy, Debug)]
144pub struct PamojaGatewayStationJoinRange {
145 pub first: u64,
147 pub last: u64,
149}
150
151#[repr(C)]
153#[derive(Clone, Copy, Debug)]
154pub struct PamojaGatewayStationBroadcast {
155 pub data_rate: u8,
157 pub frequency_hz: u32,
159 pub priority: u8,
161 pub gpstime: i64,
163 pub has_gpstime: bool,
165 pub rctx: i64,
167 pub has_rctx: bool,
169}
170
171#[repr(C)]
173#[derive(Clone, Copy, Debug)]
174pub struct PamojaGatewayStationXtime {
175 pub unit: u8,
177 pub session: u8,
179 pub micros: u64,
181}
182
183#[repr(C)]
185#[derive(Clone, Copy, Debug)]
186pub struct PamojaGatewayStationRouterIds {
187 pub router: [u8; 8],
189 pub has_router: bool,
191 pub muxs: [u8; 8],
193 pub has_muxs: bool,
195}
196
197#[repr(C)]
203#[derive(Clone, Copy, Debug)]
204pub struct PamojaGatewayStationFields {
205 pub kind: u8,
207 pub mhdr: u8,
209 pub join_eui: [u8; 8],
211 pub dev_eui: [u8; 8],
213 pub dev_nonce: u16,
215 pub dev_addr: i32,
217 pub fctrl: u8,
219 pub fcnt: u16,
221 pub fport: u8,
223 pub has_fport: bool,
225 pub mic: i32,
227 pub data_rate: u8,
229 pub frequency_hz: u32,
231 pub levels: PamojaGatewayStationLevels,
233 pub class: u8,
235 pub diid: i64,
237 pub rx_delay: u8,
239 pub has_rx_delay: bool,
241 pub priority: u8,
243 pub protocol: u32,
245 pub max_eirp: f64,
247 pub freq_min_hz: u32,
249 pub freq_max_hz: u32,
251 pub filters_networks: bool,
254 pub net_id_count: usize,
256 pub join_range_count: usize,
258 pub data_rate_count: usize,
260 pub broadcast_count: usize,
262 pub rx1: PamojaGatewayStationWindow,
264 pub rx2: PamojaGatewayStationWindow,
266 pub ping_slot: PamojaGatewayStationWindow,
268 pub xtime: i64,
271 pub has_xtime: bool,
273 pub rctx: i64,
275 pub has_rctx: bool,
277 pub gpstime: i64,
279 pub has_gpstime: bool,
281 pub txtime: f64,
284 pub has_txtime: bool,
286}
287
288#[no_mangle]
308pub unsafe extern "C" fn pamoja_gateway_station_heard(
309 frame: *const u8,
310 frame_len: usize,
311 data_rate: u8,
312 frequency_hz: u32,
313 levels: PamojaGatewayStationLevels,
314) -> *mut PamojaGatewayStationMessage {
315 let Ok(frame) = read_bytes(frame, frame_len) else {
316 return ptr::null_mut();
317 };
318 match Message::heard(&frame, data_rate, frequency_hz, levels_of(levels)) {
319 Ok(message) => held(message),
320 Err(error) => {
321 set_last_error(error.to_string());
322 ptr::null_mut()
323 }
324 }
325}
326
327#[no_mangle]
343pub unsafe extern "C" fn pamoja_gateway_station_message_parse(
344 text: *const u8,
345 text_len: usize,
346) -> *mut PamojaGatewayStationMessage {
347 let Ok(text) = read_bytes(text, text_len) else {
348 return ptr::null_mut();
349 };
350 match Message::from_json(&text) {
351 Ok(message) => held(message),
352 Err(error) => {
353 set_last_error(error.to_string());
354 ptr::null_mut()
355 }
356 }
357}
358
359#[no_mangle]
378pub unsafe extern "C" fn pamoja_gateway_station_message_new(
379 fields: *const PamojaGatewayStationFields,
380) -> *mut PamojaGatewayStationMessage {
381 let Some(fields) = fields.as_ref() else {
382 set_last_error("fields must not be null".to_owned());
383 return ptr::null_mut();
384 };
385 match message_from(fields) {
386 Ok(message) => held(message),
387 Err(why) => {
388 set_last_error(why);
389 ptr::null_mut()
390 }
391 }
392}
393
394#[no_mangle]
410pub unsafe extern "C" fn pamoja_gateway_station_message_json(
411 message: *const PamojaGatewayStationMessage,
412 out_text: *mut *mut PamojaBuffer,
413) -> PamojaStatus {
414 if message.is_null() || out_text.is_null() {
415 set_last_error("message and out_text must not be null".to_owned());
416 return PamojaStatus::InvalidArgument;
417 }
418 *out_text = PamojaBuffer::into_raw((*message).message.to_json().into_bytes());
419 PamojaStatus::Ok
420}
421
422#[no_mangle]
437pub unsafe extern "C" fn pamoja_gateway_station_message_kind(
438 message: *const PamojaGatewayStationMessage,
439) -> u8 {
440 message
441 .as_ref()
442 .map_or(PAMOJA_GATEWAY_STATION_OTHER, |held| kind_of(&held.message))
443}
444
445#[no_mangle]
460pub unsafe extern "C" fn pamoja_gateway_station_message_fields(
461 message: *const PamojaGatewayStationMessage,
462 out_fields: *mut PamojaGatewayStationFields,
463) -> PamojaStatus {
464 if message.is_null() || out_fields.is_null() {
465 set_last_error("message and out_fields must not be null".to_owned());
466 return PamojaStatus::InvalidArgument;
467 }
468 *out_fields = fields_of(&(*message).message);
469 PamojaStatus::Ok
470}
471
472#[no_mangle]
488pub unsafe extern "C" fn pamoja_gateway_station_message_payload(
489 message: *const PamojaGatewayStationMessage,
490 out_payload: *mut *mut PamojaBuffer,
491) -> PamojaStatus {
492 if message.is_null() || out_payload.is_null() {
493 set_last_error("message and out_payload must not be null".to_owned());
494 return PamojaStatus::InvalidArgument;
495 }
496 let bytes = match &(*message).message {
497 Message::Uplink { payload, .. } | Message::Proprietary { payload, .. } => payload.clone(),
498 Message::Downlink { pdu, .. } => pdu.clone(),
499 _ => Vec::new(),
500 };
501 *out_payload = PamojaBuffer::into_raw(bytes);
502 PamojaStatus::Ok
503}
504
505#[no_mangle]
521pub unsafe extern "C" fn pamoja_gateway_station_message_options(
522 message: *const PamojaGatewayStationMessage,
523 out_options: *mut *mut PamojaBuffer,
524) -> PamojaStatus {
525 if message.is_null() || out_options.is_null() {
526 set_last_error("message and out_options must not be null".to_owned());
527 return PamojaStatus::InvalidArgument;
528 }
529 let bytes = match &(*message).message {
530 Message::Uplink { fopts, .. } => fopts.clone(),
531 _ => Vec::new(),
532 };
533 *out_options = PamojaBuffer::into_raw(bytes);
534 PamojaStatus::Ok
535}
536
537#[no_mangle]
555pub unsafe extern "C" fn pamoja_gateway_station_message_text(
556 message: *const PamojaGatewayStationMessage,
557 field: u8,
558 out_text: *mut *mut PamojaBuffer,
559) -> PamojaStatus {
560 if message.is_null() || out_text.is_null() {
561 set_last_error("message and out_text must not be null".to_owned());
562 return PamojaStatus::InvalidArgument;
563 }
564 if field > PAMOJA_GATEWAY_STATION_TEXT_MSGTYPE {
565 set_last_error(format!("{field} names no text a message carries"));
566 return PamojaStatus::InvalidArgument;
567 }
568 let text = text_of(&(*message).message, field);
569 *out_text = PamojaBuffer::into_raw(text.as_bytes().to_vec());
570 PamojaStatus::Ok
571}
572
573#[no_mangle]
591pub unsafe extern "C" fn pamoja_gateway_station_message_set_text(
592 message: *mut PamojaGatewayStationMessage,
593 field: u8,
594 text: *const u8,
595 text_len: usize,
596) -> PamojaStatus {
597 let Some(held) = message.as_mut() else {
598 set_last_error("message must not be null".to_owned());
599 return PamojaStatus::InvalidArgument;
600 };
601 let Ok(bytes) = read_bytes(text, text_len) else {
602 return PamojaStatus::InvalidArgument;
603 };
604 let Ok(text) = String::from_utf8(bytes) else {
605 set_last_error("the text is not valid UTF-8".to_owned());
606 return PamojaStatus::InvalidArgument;
607 };
608 let kind = held.message.msgtype().to_owned();
609 match text_slot(&mut held.message, field) {
610 Some(slot) => {
611 *slot = text;
612 PamojaStatus::Ok
613 }
614 None => {
615 set_last_error(format!("a {kind} message carries no text field {field}"));
616 PamojaStatus::InvalidArgument
617 }
618 }
619}
620
621#[no_mangle]
639pub unsafe extern "C" fn pamoja_gateway_station_message_set_payload(
640 message: *mut PamojaGatewayStationMessage,
641 bytes: *const u8,
642 bytes_len: usize,
643) -> PamojaStatus {
644 let Some(held) = message.as_mut() else {
645 set_last_error("message must not be null".to_owned());
646 return PamojaStatus::InvalidArgument;
647 };
648 let Ok(bytes) = read_bytes(bytes, bytes_len) else {
649 return PamojaStatus::InvalidArgument;
650 };
651 match &mut held.message {
652 Message::Uplink { payload, .. } | Message::Proprietary { payload, .. } => *payload = bytes,
653 Message::Downlink { pdu, .. } => *pdu = bytes,
654 other => return refused(other, "no payload"),
655 }
656 PamojaStatus::Ok
657}
658
659#[no_mangle]
676pub unsafe extern "C" fn pamoja_gateway_station_message_set_options(
677 message: *mut PamojaGatewayStationMessage,
678 bytes: *const u8,
679 bytes_len: usize,
680) -> PamojaStatus {
681 let Some(held) = message.as_mut() else {
682 set_last_error("message must not be null".to_owned());
683 return PamojaStatus::InvalidArgument;
684 };
685 let Ok(bytes) = read_bytes(bytes, bytes_len) else {
686 return PamojaStatus::InvalidArgument;
687 };
688 match &mut held.message {
689 Message::Uplink { fopts, .. } => *fopts = bytes,
690 other => return refused(other, "no frame options"),
691 }
692 PamojaStatus::Ok
693}
694
695#[no_mangle]
714pub unsafe extern "C" fn pamoja_gateway_station_message_add_net_id(
715 message: *mut PamojaGatewayStationMessage,
716 net_id: u32,
717) -> PamojaStatus {
718 let Some(held) = message.as_mut() else {
719 set_last_error("message must not be null".to_owned());
720 return PamojaStatus::InvalidArgument;
721 };
722 match &mut held.message {
723 Message::RouterConfig { net_id: ids, .. } => {
724 ids.get_or_insert_with(Vec::new).push(net_id);
725 PamojaStatus::Ok
726 }
727 other => refused(other, "no network list"),
728 }
729}
730
731#[no_mangle]
747pub unsafe extern "C" fn pamoja_gateway_station_message_add_join_range(
748 message: *mut PamojaGatewayStationMessage,
749 range: *const PamojaGatewayStationJoinRange,
750) -> PamojaStatus {
751 let (Some(held), Some(range)) = (message.as_mut(), range.as_ref()) else {
752 set_last_error("message and range must not be null".to_owned());
753 return PamojaStatus::InvalidArgument;
754 };
755 match &mut held.message {
756 Message::RouterConfig { join_eui, .. } => {
757 join_eui.push((range.first, range.last));
758 PamojaStatus::Ok
759 }
760 other => refused(other, "no join identifier ranges"),
761 }
762}
763
764#[no_mangle]
780pub unsafe extern "C" fn pamoja_gateway_station_message_add_data_rate(
781 message: *mut PamojaGatewayStationMessage,
782 rate: *const PamojaGatewayStationDataRate,
783) -> PamojaStatus {
784 let (Some(held), Some(rate)) = (message.as_mut(), rate.as_ref()) else {
785 set_last_error("message and rate must not be null".to_owned());
786 return PamojaStatus::InvalidArgument;
787 };
788 match &mut held.message {
789 Message::RouterConfig { data_rates, .. } => {
790 data_rates.push(rate.defined.then_some((
791 rate.spreading_factor,
792 rate.bandwidth_hz,
793 rate.downlink_only,
794 )));
795 PamojaStatus::Ok
796 }
797 other => refused(other, "no data-rate table"),
798 }
799}
800
801#[no_mangle]
820pub unsafe extern "C" fn pamoja_gateway_station_message_add_broadcast(
821 message: *mut PamojaGatewayStationMessage,
822 frame: *const PamojaGatewayStationBroadcast,
823 pdu: *const u8,
824 pdu_len: usize,
825) -> PamojaStatus {
826 let (Some(held), Some(frame)) = (message.as_mut(), frame.as_ref()) else {
827 set_last_error("message and frame must not be null".to_owned());
828 return PamojaStatus::InvalidArgument;
829 };
830 let Ok(pdu) = read_bytes(pdu, pdu_len) else {
831 return PamojaStatus::InvalidArgument;
832 };
833 match &mut held.message {
834 Message::Schedule { frames } => {
835 frames.push(Broadcast {
836 pdu,
837 data_rate: frame.data_rate,
838 frequency_hz: frame.frequency_hz,
839 priority: frame.priority,
840 gpstime: frame.has_gpstime.then_some(frame.gpstime),
841 rctx: frame.has_rctx.then_some(frame.rctx),
842 });
843 PamojaStatus::Ok
844 }
845 other => refused(other, "no frames"),
846 }
847}
848
849#[no_mangle]
866pub unsafe extern "C" fn pamoja_gateway_station_message_net_id(
867 message: *const PamojaGatewayStationMessage,
868 index: usize,
869 out_net_id: *mut u32,
870) -> PamojaStatus {
871 if message.is_null() || out_net_id.is_null() {
872 set_last_error("message and out_net_id must not be null".to_owned());
873 return PamojaStatus::InvalidArgument;
874 }
875 let found = match &(*message).message {
876 Message::RouterConfig { net_id, .. } => {
877 net_id.as_deref().and_then(|ids| ids.get(index)).copied()
878 }
879 _ => None,
880 };
881 match found {
882 Some(id) => {
883 *out_net_id = id;
884 PamojaStatus::Ok
885 }
886 None => missing(index, "network"),
887 }
888}
889
890#[no_mangle]
907pub unsafe extern "C" fn pamoja_gateway_station_message_join_range(
908 message: *const PamojaGatewayStationMessage,
909 index: usize,
910 out_range: *mut PamojaGatewayStationJoinRange,
911) -> PamojaStatus {
912 if message.is_null() || out_range.is_null() {
913 set_last_error("message and out_range must not be null".to_owned());
914 return PamojaStatus::InvalidArgument;
915 }
916 let found = match &(*message).message {
917 Message::RouterConfig { join_eui, .. } => join_eui.get(index).copied(),
918 _ => None,
919 };
920 match found {
921 Some((first, last)) => {
922 *out_range = PamojaGatewayStationJoinRange { first, last };
923 PamojaStatus::Ok
924 }
925 None => missing(index, "join identifier range"),
926 }
927}
928
929#[no_mangle]
947pub unsafe extern "C" fn pamoja_gateway_station_message_data_rate(
948 message: *const PamojaGatewayStationMessage,
949 index: usize,
950 out_rate: *mut PamojaGatewayStationDataRate,
951) -> PamojaStatus {
952 if message.is_null() || out_rate.is_null() {
953 set_last_error("message and out_rate must not be null".to_owned());
954 return PamojaStatus::InvalidArgument;
955 }
956 let found = match &(*message).message {
957 Message::RouterConfig { data_rates, .. } => data_rates.get(index).copied(),
958 _ => None,
959 };
960 match found {
961 Some(entry) => {
962 let (spreading_factor, bandwidth_hz, downlink_only) = entry.unwrap_or_default();
963 *out_rate = PamojaGatewayStationDataRate {
964 spreading_factor,
965 bandwidth_hz,
966 downlink_only,
967 defined: entry.is_some(),
968 };
969 PamojaStatus::Ok
970 }
971 None => missing(index, "data rate"),
972 }
973}
974
975#[no_mangle]
994pub unsafe extern "C" fn pamoja_gateway_station_message_broadcast(
995 message: *const PamojaGatewayStationMessage,
996 index: usize,
997 out_frame: *mut PamojaGatewayStationBroadcast,
998 out_pdu: *mut *mut PamojaBuffer,
999) -> PamojaStatus {
1000 if message.is_null() || out_frame.is_null() || out_pdu.is_null() {
1001 set_last_error("message, out_frame and out_pdu must not be null".to_owned());
1002 return PamojaStatus::InvalidArgument;
1003 }
1004 let found = match &(*message).message {
1005 Message::Schedule { frames } => frames.get(index),
1006 _ => None,
1007 };
1008 match found {
1009 Some(frame) => {
1010 *out_frame = PamojaGatewayStationBroadcast {
1011 data_rate: frame.data_rate,
1012 frequency_hz: frame.frequency_hz,
1013 priority: frame.priority,
1014 gpstime: frame.gpstime.unwrap_or_default(),
1015 has_gpstime: frame.gpstime.is_some(),
1016 rctx: frame.rctx.unwrap_or_default(),
1017 has_rctx: frame.rctx.is_some(),
1018 };
1019 *out_pdu = PamojaBuffer::into_raw(frame.pdu.clone());
1020 PamojaStatus::Ok
1021 }
1022 None => missing(index, "scheduled frame"),
1023 }
1024}
1025
1026#[no_mangle]
1037pub unsafe extern "C" fn pamoja_gateway_station_message_free(
1038 message: *mut PamojaGatewayStationMessage,
1039) {
1040 if !message.is_null() {
1041 drop(Box::from_raw(message));
1042 }
1043}
1044
1045#[no_mangle]
1061pub unsafe extern "C" fn pamoja_gateway_station_discovery(
1062 router: *const u8,
1063 out_text: *mut *mut PamojaBuffer,
1064) -> PamojaStatus {
1065 let (Some(router), false) = (eight(router), out_text.is_null()) else {
1066 set_last_error("router and out_text must not be null".to_owned());
1067 return PamojaStatus::InvalidArgument;
1068 };
1069 let asking = Discovery::new(Eui::new(router));
1070 *out_text = PamojaBuffer::into_raw(asking.to_json().into_bytes());
1071 PamojaStatus::Ok
1072}
1073
1074#[no_mangle]
1091pub unsafe extern "C" fn pamoja_gateway_station_discovery_parse(
1092 text: *const u8,
1093 text_len: usize,
1094 out_router: *mut u8,
1095) -> PamojaStatus {
1096 if out_router.is_null() {
1097 set_last_error("out_router must not be null".to_owned());
1098 return PamojaStatus::InvalidArgument;
1099 }
1100 let Ok(text) = read_bytes(text, text_len) else {
1101 return PamojaStatus::InvalidArgument;
1102 };
1103 match Discovery::from_json(&text) {
1104 Ok(asked) => {
1105 ptr::copy_nonoverlapping(asked.router.bytes().as_ptr(), out_router, 8);
1106 PamojaStatus::Ok
1107 }
1108 Err(error) => {
1109 set_last_error(error.to_string());
1110 PamojaStatus::Codec
1111 }
1112 }
1113}
1114
1115#[no_mangle]
1135pub unsafe extern "C" fn pamoja_gateway_station_router_accepted(
1136 router: *const u8,
1137 muxs: *const u8,
1138 uri: *const u8,
1139 uri_len: usize,
1140 out_text: *mut *mut PamojaBuffer,
1141) -> PamojaStatus {
1142 let (Some(router), Some(muxs), false) = (eight(router), eight(muxs), out_text.is_null()) else {
1143 set_last_error("router, muxs and out_text must not be null".to_owned());
1144 return PamojaStatus::InvalidArgument;
1145 };
1146 let Some(uri) = utf8(uri, uri_len) else {
1147 return PamojaStatus::InvalidArgument;
1148 };
1149 let answer = Router::accepted(Eui::new(router), Eui::new(muxs), uri);
1150 *out_text = PamojaBuffer::into_raw(answer.to_json().into_bytes());
1151 PamojaStatus::Ok
1152}
1153
1154#[no_mangle]
1173pub unsafe extern "C" fn pamoja_gateway_station_router_refused(
1174 router: *const u8,
1175 error: *const u8,
1176 error_len: usize,
1177 out_text: *mut *mut PamojaBuffer,
1178) -> PamojaStatus {
1179 let (Some(router), false) = (eight(router), out_text.is_null()) else {
1180 set_last_error("router and out_text must not be null".to_owned());
1181 return PamojaStatus::InvalidArgument;
1182 };
1183 let Some(error) = utf8(error, error_len) else {
1184 return PamojaStatus::InvalidArgument;
1185 };
1186 let answer = Router::refused(Eui::new(router), error);
1187 *out_text = PamojaBuffer::into_raw(answer.to_json().into_bytes());
1188 PamojaStatus::Ok
1189}
1190
1191#[no_mangle]
1209pub unsafe extern "C" fn pamoja_gateway_station_router_parse(
1210 text: *const u8,
1211 text_len: usize,
1212 out_uri: *mut *mut PamojaBuffer,
1213 out_error: *mut *mut PamojaBuffer,
1214) -> PamojaStatus {
1215 if out_uri.is_null() || out_error.is_null() {
1216 set_last_error("out_uri and out_error must not be null".to_owned());
1217 return PamojaStatus::InvalidArgument;
1218 }
1219 let Ok(text) = read_bytes(text, text_len) else {
1220 return PamojaStatus::InvalidArgument;
1221 };
1222 match Router::from_json(&text) {
1223 Ok(answer) => {
1224 *out_uri = PamojaBuffer::into_raw(answer.uri.unwrap_or_default().into_bytes());
1225 *out_error = PamojaBuffer::into_raw(answer.error.unwrap_or_default().into_bytes());
1226 PamojaStatus::Ok
1227 }
1228 Err(error) => {
1229 set_last_error(error.to_string());
1230 PamojaStatus::Codec
1231 }
1232 }
1233}
1234
1235#[no_mangle]
1252pub unsafe extern "C" fn pamoja_gateway_station_router_identities(
1253 text: *const u8,
1254 text_len: usize,
1255 out_ids: *mut PamojaGatewayStationRouterIds,
1256) -> PamojaStatus {
1257 if out_ids.is_null() {
1258 set_last_error("out_ids must not be null".to_owned());
1259 return PamojaStatus::InvalidArgument;
1260 }
1261 let Ok(text) = read_bytes(text, text_len) else {
1262 return PamojaStatus::InvalidArgument;
1263 };
1264 match Router::from_json(&text) {
1265 Ok(answer) => {
1266 *out_ids = PamojaGatewayStationRouterIds {
1267 router: answer.router.map(|eui| eui.bytes()).unwrap_or_default(),
1268 has_router: answer.router.is_some(),
1269 muxs: answer.muxs.map(|eui| eui.bytes()).unwrap_or_default(),
1270 has_muxs: answer.muxs.is_some(),
1271 };
1272 PamojaStatus::Ok
1273 }
1274 Err(error) => {
1275 set_last_error(error.to_string());
1276 PamojaStatus::Codec
1277 }
1278 }
1279}
1280
1281#[no_mangle]
1300pub unsafe extern "C" fn pamoja_gateway_station_xtime(
1301 unit: u8,
1302 session: u8,
1303 micros: u64,
1304 out_value: *mut i64,
1305) -> PamojaStatus {
1306 if out_value.is_null() {
1307 set_last_error("out_value must not be null".to_owned());
1308 return PamojaStatus::InvalidArgument;
1309 }
1310 match Xtime::new(unit, session, micros) {
1311 Some(clock) => {
1312 *out_value = clock.value();
1313 PamojaStatus::Ok
1314 }
1315 None => {
1316 set_last_error(format!(
1317 "a station clock takes a unit up to 127 and 48 bits of microseconds, not unit {unit} and {micros} us"
1318 ));
1319 PamojaStatus::InvalidArgument
1320 }
1321 }
1322}
1323
1324#[no_mangle]
1339pub unsafe extern "C" fn pamoja_gateway_station_xtime_parts(
1340 value: i64,
1341 out_parts: *mut PamojaGatewayStationXtime,
1342) -> PamojaStatus {
1343 if out_parts.is_null() {
1344 set_last_error("out_parts must not be null".to_owned());
1345 return PamojaStatus::InvalidArgument;
1346 }
1347 let clock = Xtime::of(value);
1348 *out_parts = PamojaGatewayStationXtime {
1349 unit: clock.unit,
1350 session: clock.session,
1351 micros: clock.micros,
1352 };
1353 PamojaStatus::Ok
1354}
1355
1356#[no_mangle]
1372pub unsafe extern "C" fn pamoja_gateway_station_id6(
1373 eui: *const u8,
1374 out_text: *mut *mut PamojaBuffer,
1375) -> PamojaStatus {
1376 let (Some(eui), false) = (eight(eui), out_text.is_null()) else {
1377 set_last_error("eui and out_text must not be null".to_owned());
1378 return PamojaStatus::InvalidArgument;
1379 };
1380 *out_text = PamojaBuffer::into_raw(id6(Eui::new(eui)).into_bytes());
1381 PamojaStatus::Ok
1382}
1383
1384#[no_mangle]
1401pub unsafe extern "C" fn pamoja_gateway_station_eui_of(
1402 text: *const c_char,
1403 out_eui: *mut u8,
1404) -> PamojaStatus {
1405 if text.is_null() || out_eui.is_null() {
1406 set_last_error("text and out_eui must not be null".to_owned());
1407 return PamojaStatus::InvalidArgument;
1408 }
1409 let Ok(text) = std::ffi::CStr::from_ptr(text).to_str() else {
1410 set_last_error("the identifier is not valid UTF-8".to_owned());
1411 return PamojaStatus::InvalidArgument;
1412 };
1413 match eui_of(text) {
1414 Some(eui) => {
1415 ptr::copy_nonoverlapping(eui.bytes().as_ptr(), out_eui, 8);
1416 PamojaStatus::Ok
1417 }
1418 None => {
1419 set_last_error(format!("{text} is not an identifier"));
1420 PamojaStatus::Codec
1421 }
1422 }
1423}
1424
1425fn held(message: Message) -> *mut PamojaGatewayStationMessage {
1427 Box::into_raw(Box::new(PamojaGatewayStationMessage { message }))
1428}
1429
1430fn refused(message: &Message, carries: &str) -> PamojaStatus {
1432 set_last_error(format!("a {} message carries {carries}", message.msgtype()));
1433 PamojaStatus::InvalidArgument
1434}
1435
1436fn missing(index: usize, what: &str) -> PamojaStatus {
1438 set_last_error(format!("the message carries no {what} at index {index}"));
1439 PamojaStatus::InvalidArgument
1440}
1441
1442unsafe fn utf8(text: *const u8, text_len: usize) -> Option<String> {
1444 let bytes = read_bytes(text, text_len).ok()?;
1445 match String::from_utf8(bytes) {
1446 Ok(text) => Some(text),
1447 Err(_) => {
1448 set_last_error("the text is not valid UTF-8".to_owned());
1449 None
1450 }
1451 }
1452}
1453
1454fn levels_of(levels: PamojaGatewayStationLevels) -> Levels {
1456 Levels {
1457 rctx: levels.rctx,
1458 xtime: levels.xtime,
1459 gpstime: levels.has_gpstime.then_some(levels.gpstime),
1460 rssi: levels.rssi,
1461 snr: levels.snr,
1462 }
1463}
1464
1465fn levels_to_c(levels: Levels) -> PamojaGatewayStationLevels {
1467 PamojaGatewayStationLevels {
1468 rctx: levels.rctx,
1469 xtime: levels.xtime,
1470 gpstime: levels.gpstime.unwrap_or_default(),
1471 has_gpstime: levels.gpstime.is_some(),
1472 rssi: levels.rssi,
1473 snr: levels.snr,
1474 }
1475}
1476
1477fn window_of(window: PamojaGatewayStationWindow) -> Option<(u8, u32)> {
1479 window
1480 .present
1481 .then_some((window.data_rate, window.frequency_hz))
1482}
1483
1484fn window_to_c(window: Option<(u8, u32)>) -> PamojaGatewayStationWindow {
1486 let (data_rate, frequency_hz) = window.unwrap_or_default();
1487 PamojaGatewayStationWindow {
1488 data_rate,
1489 frequency_hz,
1490 present: window.is_some(),
1491 }
1492}
1493
1494fn kind_of(message: &Message) -> u8 {
1496 match message {
1497 Message::JoinRequest { .. } => PAMOJA_GATEWAY_STATION_JOIN_REQUEST,
1498 Message::Uplink { .. } => PAMOJA_GATEWAY_STATION_UPLINK,
1499 Message::Proprietary { .. } => PAMOJA_GATEWAY_STATION_PROPRIETARY,
1500 Message::Version { .. } => PAMOJA_GATEWAY_STATION_VERSION,
1501 Message::RouterConfig { .. } => PAMOJA_GATEWAY_STATION_ROUTER_CONFIG,
1502 Message::Downlink { .. } => PAMOJA_GATEWAY_STATION_DOWNLINK,
1503 Message::Schedule { .. } => PAMOJA_GATEWAY_STATION_SCHEDULE,
1504 Message::Transmitted { .. } => PAMOJA_GATEWAY_STATION_TRANSMITTED,
1505 Message::TimeSync { .. } => PAMOJA_GATEWAY_STATION_TIME_SYNC,
1506 Message::Other { .. } => PAMOJA_GATEWAY_STATION_OTHER,
1507 }
1508}
1509
1510fn message_from(fields: &PamojaGatewayStationFields) -> Result<Message, String> {
1512 let clock = |value: i64, present: bool| present.then_some(value);
1513 Ok(match fields.kind {
1514 PAMOJA_GATEWAY_STATION_JOIN_REQUEST => Message::JoinRequest {
1515 mhdr: fields.mhdr,
1516 join_eui: Eui::new(fields.join_eui),
1517 dev_eui: Eui::new(fields.dev_eui),
1518 dev_nonce: fields.dev_nonce,
1519 mic: fields.mic,
1520 data_rate: fields.data_rate,
1521 frequency_hz: fields.frequency_hz,
1522 levels: levels_of(fields.levels),
1523 },
1524 PAMOJA_GATEWAY_STATION_UPLINK => Message::Uplink {
1525 mhdr: fields.mhdr,
1526 dev_addr: fields.dev_addr,
1527 fctrl: fields.fctrl,
1528 fcnt: fields.fcnt,
1529 fopts: Vec::new(),
1530 fport: fields.has_fport.then_some(fields.fport),
1531 payload: Vec::new(),
1532 mic: fields.mic,
1533 data_rate: fields.data_rate,
1534 frequency_hz: fields.frequency_hz,
1535 levels: levels_of(fields.levels),
1536 },
1537 PAMOJA_GATEWAY_STATION_PROPRIETARY => Message::Proprietary {
1538 payload: Vec::new(),
1539 data_rate: fields.data_rate,
1540 frequency_hz: fields.frequency_hz,
1541 levels: levels_of(fields.levels),
1542 },
1543 PAMOJA_GATEWAY_STATION_VERSION => Message::Version {
1544 station: String::new(),
1545 firmware: String::new(),
1546 package: String::new(),
1547 model: String::new(),
1548 protocol: fields.protocol,
1549 features: String::new(),
1550 },
1551 PAMOJA_GATEWAY_STATION_ROUTER_CONFIG => Message::RouterConfig {
1552 net_id: fields.filters_networks.then(Vec::new),
1553 join_eui: Vec::new(),
1554 region: String::new(),
1555 max_eirp: fields.max_eirp,
1556 hwspec: String::new(),
1557 freq_range: (fields.freq_min_hz, fields.freq_max_hz),
1558 data_rates: Vec::new(),
1559 },
1560 PAMOJA_GATEWAY_STATION_DOWNLINK => Message::Downlink {
1561 dev_eui: Eui::new(fields.dev_eui),
1562 class: fields.class,
1563 diid: fields.diid,
1564 pdu: Vec::new(),
1565 rx_delay: fields.has_rx_delay.then_some(fields.rx_delay),
1566 rx1: window_of(fields.rx1),
1567 rx2: window_of(fields.rx2),
1568 ping_slot: window_of(fields.ping_slot),
1569 priority: fields.priority,
1570 xtime: clock(fields.xtime, fields.has_xtime),
1571 rctx: clock(fields.rctx, fields.has_rctx),
1572 gpstime: clock(fields.gpstime, fields.has_gpstime),
1573 },
1574 PAMOJA_GATEWAY_STATION_SCHEDULE => Message::Schedule { frames: Vec::new() },
1575 PAMOJA_GATEWAY_STATION_TRANSMITTED => Message::Transmitted {
1576 diid: fields.diid,
1577 dev_eui: Eui::new(fields.dev_eui),
1578 rctx: fields.rctx,
1579 xtime: fields.xtime,
1580 txtime: fields.txtime,
1581 gpstime: clock(fields.gpstime, fields.has_gpstime),
1582 },
1583 PAMOJA_GATEWAY_STATION_TIME_SYNC => Message::TimeSync {
1584 txtime: fields.has_txtime.then_some(fields.txtime.round() as i64),
1585 xtime: clock(fields.xtime, fields.has_xtime),
1586 gpstime: clock(fields.gpstime, fields.has_gpstime),
1587 },
1588 PAMOJA_GATEWAY_STATION_OTHER => Message::Other {
1589 msgtype: String::new(),
1590 },
1591 kind => return Err(format!("{kind} names no message kind")),
1592 })
1593}
1594
1595fn fields_of(message: &Message) -> PamojaGatewayStationFields {
1597 let mut fields = PamojaGatewayStationFields {
1598 kind: kind_of(message),
1599 mhdr: 0,
1600 join_eui: [0; 8],
1601 dev_eui: [0; 8],
1602 dev_nonce: 0,
1603 dev_addr: 0,
1604 fctrl: 0,
1605 fcnt: 0,
1606 fport: 0,
1607 has_fport: false,
1608 mic: 0,
1609 data_rate: 0,
1610 frequency_hz: 0,
1611 levels: levels_to_c(Levels::default()),
1612 class: 0,
1613 diid: 0,
1614 rx_delay: 0,
1615 has_rx_delay: false,
1616 priority: 0,
1617 protocol: 0,
1618 max_eirp: 0.0,
1619 freq_min_hz: 0,
1620 freq_max_hz: 0,
1621 filters_networks: false,
1622 net_id_count: 0,
1623 join_range_count: 0,
1624 data_rate_count: 0,
1625 broadcast_count: 0,
1626 rx1: window_to_c(None),
1627 rx2: window_to_c(None),
1628 ping_slot: window_to_c(None),
1629 xtime: 0,
1630 has_xtime: false,
1631 rctx: 0,
1632 has_rctx: false,
1633 gpstime: 0,
1634 has_gpstime: false,
1635 txtime: 0.0,
1636 has_txtime: false,
1637 };
1638
1639 match message {
1640 Message::JoinRequest {
1641 mhdr,
1642 join_eui,
1643 dev_eui,
1644 dev_nonce,
1645 mic,
1646 data_rate,
1647 frequency_hz,
1648 levels,
1649 } => {
1650 fields.mhdr = *mhdr;
1651 fields.join_eui = join_eui.bytes();
1652 fields.dev_eui = dev_eui.bytes();
1653 fields.dev_nonce = *dev_nonce;
1654 fields.mic = *mic;
1655 fields.data_rate = *data_rate;
1656 fields.frequency_hz = *frequency_hz;
1657 fields.levels = levels_to_c(*levels);
1658 }
1659 Message::Uplink {
1660 mhdr,
1661 dev_addr,
1662 fctrl,
1663 fcnt,
1664 fport,
1665 mic,
1666 data_rate,
1667 frequency_hz,
1668 levels,
1669 ..
1670 } => {
1671 fields.mhdr = *mhdr;
1672 fields.dev_addr = *dev_addr;
1673 fields.fctrl = *fctrl;
1674 fields.fcnt = *fcnt;
1675 fields.fport = fport.unwrap_or_default();
1676 fields.has_fport = fport.is_some();
1677 fields.mic = *mic;
1678 fields.data_rate = *data_rate;
1679 fields.frequency_hz = *frequency_hz;
1680 fields.levels = levels_to_c(*levels);
1681 }
1682 Message::Proprietary {
1683 data_rate,
1684 frequency_hz,
1685 levels,
1686 ..
1687 } => {
1688 fields.data_rate = *data_rate;
1689 fields.frequency_hz = *frequency_hz;
1690 fields.levels = levels_to_c(*levels);
1691 }
1692 Message::Version { protocol, .. } => fields.protocol = *protocol,
1693 Message::RouterConfig {
1694 net_id,
1695 join_eui,
1696 max_eirp,
1697 freq_range,
1698 data_rates,
1699 ..
1700 } => {
1701 fields.max_eirp = *max_eirp;
1702 fields.freq_min_hz = freq_range.0;
1703 fields.freq_max_hz = freq_range.1;
1704 fields.filters_networks = net_id.is_some();
1705 fields.net_id_count = net_id.as_ref().map_or(0, Vec::len);
1706 fields.join_range_count = join_eui.len();
1707 fields.data_rate_count = data_rates.len();
1708 }
1709 Message::Downlink {
1710 dev_eui,
1711 class,
1712 diid,
1713 rx_delay,
1714 rx1,
1715 rx2,
1716 ping_slot,
1717 priority,
1718 xtime,
1719 rctx,
1720 gpstime,
1721 ..
1722 } => {
1723 fields.dev_eui = dev_eui.bytes();
1724 fields.class = *class;
1725 fields.diid = *diid;
1726 fields.rx_delay = rx_delay.unwrap_or_default();
1727 fields.has_rx_delay = rx_delay.is_some();
1728 fields.rx1 = window_to_c(*rx1);
1729 fields.rx2 = window_to_c(*rx2);
1730 fields.ping_slot = window_to_c(*ping_slot);
1731 fields.priority = *priority;
1732 fields.xtime = xtime.unwrap_or_default();
1733 fields.has_xtime = xtime.is_some();
1734 fields.rctx = rctx.unwrap_or_default();
1735 fields.has_rctx = rctx.is_some();
1736 fields.gpstime = gpstime.unwrap_or_default();
1737 fields.has_gpstime = gpstime.is_some();
1738 }
1739 Message::Schedule { frames } => fields.broadcast_count = frames.len(),
1740 Message::Transmitted {
1741 diid,
1742 dev_eui,
1743 rctx,
1744 xtime,
1745 txtime,
1746 gpstime,
1747 } => {
1748 fields.diid = *diid;
1749 fields.dev_eui = dev_eui.bytes();
1750 fields.rctx = *rctx;
1751 fields.has_rctx = true;
1752 fields.xtime = *xtime;
1753 fields.has_xtime = true;
1754 fields.txtime = *txtime;
1755 fields.has_txtime = true;
1756 fields.gpstime = gpstime.unwrap_or_default();
1757 fields.has_gpstime = gpstime.is_some();
1758 }
1759 Message::TimeSync {
1760 txtime,
1761 xtime,
1762 gpstime,
1763 } => {
1764 fields.txtime = txtime.unwrap_or_default() as f64;
1765 fields.has_txtime = txtime.is_some();
1766 fields.xtime = xtime.unwrap_or_default();
1767 fields.has_xtime = xtime.is_some();
1768 fields.gpstime = gpstime.unwrap_or_default();
1769 fields.has_gpstime = gpstime.is_some();
1770 }
1771 Message::Other { .. } => {}
1772 }
1773
1774 fields
1775}
1776
1777fn text_of(message: &Message, field: u8) -> &str {
1779 match (message, field) {
1780 (_, PAMOJA_GATEWAY_STATION_TEXT_MSGTYPE) => message.msgtype(),
1781 (Message::Version { station, .. }, PAMOJA_GATEWAY_STATION_TEXT_STATION) => station,
1782 (Message::Version { firmware, .. }, PAMOJA_GATEWAY_STATION_TEXT_FIRMWARE) => firmware,
1783 (Message::Version { package, .. }, PAMOJA_GATEWAY_STATION_TEXT_PACKAGE) => package,
1784 (Message::Version { model, .. }, PAMOJA_GATEWAY_STATION_TEXT_MODEL) => model,
1785 (Message::Version { features, .. }, PAMOJA_GATEWAY_STATION_TEXT_FEATURES) => features,
1786 (Message::RouterConfig { region, .. }, PAMOJA_GATEWAY_STATION_TEXT_REGION) => region,
1787 (Message::RouterConfig { hwspec, .. }, PAMOJA_GATEWAY_STATION_TEXT_HWSPEC) => hwspec,
1788 _ => "",
1789 }
1790}
1791
1792fn text_slot(message: &mut Message, field: u8) -> Option<&mut String> {
1794 match (message, field) {
1795 (Message::Version { station, .. }, PAMOJA_GATEWAY_STATION_TEXT_STATION) => Some(station),
1796 (Message::Version { firmware, .. }, PAMOJA_GATEWAY_STATION_TEXT_FIRMWARE) => Some(firmware),
1797 (Message::Version { package, .. }, PAMOJA_GATEWAY_STATION_TEXT_PACKAGE) => Some(package),
1798 (Message::Version { model, .. }, PAMOJA_GATEWAY_STATION_TEXT_MODEL) => Some(model),
1799 (Message::Version { features, .. }, PAMOJA_GATEWAY_STATION_TEXT_FEATURES) => Some(features),
1800 (Message::RouterConfig { region, .. }, PAMOJA_GATEWAY_STATION_TEXT_REGION) => Some(region),
1801 (Message::RouterConfig { hwspec, .. }, PAMOJA_GATEWAY_STATION_TEXT_HWSPEC) => Some(hwspec),
1802 (Message::Other { msgtype }, PAMOJA_GATEWAY_STATION_TEXT_MSGTYPE) => Some(msgtype),
1803 _ => None,
1804 }
1805}
1806
1807unsafe fn eight(bytes: *const u8) -> Option<[u8; 8]> {
1809 if bytes.is_null() {
1810 return None;
1811 }
1812 let mut read = [0u8; 8];
1813 ptr::copy_nonoverlapping(bytes, read.as_mut_ptr(), read.len());
1814 Some(read)
1815}
1816
1817#[cfg(test)]
1818mod tests {
1819 use super::*;
1820 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1821
1822 unsafe fn taken(buffer: *mut PamojaBuffer) -> Vec<u8> {
1824 let bytes =
1825 std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
1826 .to_vec();
1827 pamoja_buffer_free(buffer);
1828 bytes
1829 }
1830
1831 unsafe fn written(message: *mut PamojaGatewayStationMessage) -> String {
1833 assert!(!message.is_null(), "the message was built");
1834 let mut text = ptr::null_mut();
1835 assert_eq!(
1836 pamoja_gateway_station_message_json(message, &mut text),
1837 PamojaStatus::Ok
1838 );
1839 pamoja_gateway_station_message_free(message);
1840 String::from_utf8(taken(text)).expect("the text is UTF-8")
1841 }
1842
1843 fn blank(kind: u8) -> PamojaGatewayStationFields {
1845 PamojaGatewayStationFields {
1846 kind,
1847 ..fields_of(&Message::Other {
1848 msgtype: String::new(),
1849 })
1850 }
1851 }
1852
1853 #[test]
1854 fn a_configuration_built_through_the_boundary_matches_the_one_built_in_rust() {
1855 let expected = Message::RouterConfig {
1856 net_id: Some(vec![0x13]),
1857 join_eui: vec![(0x70b3_d57e_d000_0000, 0x70b3_d57e_d000_00ff)],
1858 region: "US915".to_owned(),
1859 max_eirp: 30.0,
1860 hwspec: "sx1301/1".to_owned(),
1861 freq_range: (902_000_000, 928_000_000),
1862 data_rates: vec![Some((10, 125_000, false)), None, Some((12, 500_000, true))],
1863 };
1864
1865 unsafe {
1866 let mut fields = blank(PAMOJA_GATEWAY_STATION_ROUTER_CONFIG);
1867 fields.max_eirp = 30.0;
1868 fields.freq_min_hz = 902_000_000;
1869 fields.freq_max_hz = 928_000_000;
1870 let built = pamoja_gateway_station_message_new(&fields);
1871 for (field, text) in [
1872 (PAMOJA_GATEWAY_STATION_TEXT_REGION, "US915"),
1873 (PAMOJA_GATEWAY_STATION_TEXT_HWSPEC, "sx1301/1"),
1874 ] {
1875 assert_eq!(
1876 pamoja_gateway_station_message_set_text(
1877 built,
1878 field,
1879 text.as_ptr(),
1880 text.len()
1881 ),
1882 PamojaStatus::Ok
1883 );
1884 }
1885 assert_eq!(
1886 pamoja_gateway_station_message_add_net_id(built, 0x13),
1887 PamojaStatus::Ok
1888 );
1889 let range = PamojaGatewayStationJoinRange {
1890 first: 0x70b3_d57e_d000_0000,
1891 last: 0x70b3_d57e_d000_00ff,
1892 };
1893 assert_eq!(
1894 pamoja_gateway_station_message_add_join_range(built, &range),
1895 PamojaStatus::Ok
1896 );
1897 for rate in [
1898 PamojaGatewayStationDataRate {
1899 spreading_factor: 10,
1900 bandwidth_hz: 125_000,
1901 downlink_only: false,
1902 defined: true,
1903 },
1904 PamojaGatewayStationDataRate {
1905 spreading_factor: 0,
1906 bandwidth_hz: 0,
1907 downlink_only: false,
1908 defined: false,
1909 },
1910 PamojaGatewayStationDataRate {
1911 spreading_factor: 12,
1912 bandwidth_hz: 500_000,
1913 downlink_only: true,
1914 defined: true,
1915 },
1916 ] {
1917 assert_eq!(
1918 pamoja_gateway_station_message_add_data_rate(built, &rate),
1919 PamojaStatus::Ok
1920 );
1921 }
1922 assert_eq!(written(built), expected.to_json());
1923 }
1924 }
1925
1926 #[test]
1927 fn a_configuration_reads_back_entry_for_entry() {
1928 let config = Message::RouterConfig {
1929 net_id: None,
1930 join_eui: vec![(1, 2)],
1931 region: "EU868".to_owned(),
1932 max_eirp: 16.0,
1933 hwspec: "sx1301/1".to_owned(),
1934 freq_range: (863_000_000, 870_000_000),
1935 data_rates: vec![Some((12, 125_000, false)), None],
1936 };
1937 let text = config.to_json();
1938
1939 unsafe {
1940 let read = pamoja_gateway_station_message_parse(text.as_ptr(), text.len());
1941 let mut fields = blank(PAMOJA_GATEWAY_STATION_OTHER);
1942 assert_eq!(
1943 pamoja_gateway_station_message_fields(read, &mut fields),
1944 PamojaStatus::Ok
1945 );
1946 assert_eq!(fields.kind, PAMOJA_GATEWAY_STATION_ROUTER_CONFIG);
1947 assert!(!fields.filters_networks, "no filter was written as null");
1948 assert_eq!(fields.join_range_count, 1);
1949 assert_eq!(fields.data_rate_count, 2);
1950
1951 let mut rate = PamojaGatewayStationDataRate {
1952 spreading_factor: 0,
1953 bandwidth_hz: 0,
1954 downlink_only: false,
1955 defined: true,
1956 };
1957 assert_eq!(
1958 pamoja_gateway_station_message_data_rate(read, 1, &mut rate),
1959 PamojaStatus::Ok
1960 );
1961 assert!(!rate.defined, "the second number is left undefined");
1962 assert_eq!(
1963 pamoja_gateway_station_message_data_rate(read, 2, &mut rate),
1964 PamojaStatus::InvalidArgument
1965 );
1966
1967 let mut region = ptr::null_mut();
1968 assert_eq!(
1969 pamoja_gateway_station_message_text(
1970 read,
1971 PAMOJA_GATEWAY_STATION_TEXT_REGION,
1972 &mut region
1973 ),
1974 PamojaStatus::Ok
1975 );
1976 assert_eq!(taken(region), b"EU868");
1977 pamoja_gateway_station_message_free(read);
1978 }
1979 }
1980
1981 #[test]
1982 fn a_downlink_carries_its_windows_and_clock_both_ways() {
1983 let xtime = (0xa5_i64 << 48) | 1_000_000;
1984 let expected = Message::Downlink {
1985 dev_eui: Eui::new([0x11; 8]),
1986 class: 0,
1987 diid: 42,
1988 pdu: vec![0x60, 0x01],
1989 rx_delay: Some(1),
1990 rx1: Some((5, 868_100_000)),
1991 rx2: Some((0, 869_525_000)),
1992 ping_slot: None,
1993 priority: 0,
1994 xtime: Some(xtime),
1995 rctx: Some(0),
1996 gpstime: None,
1997 };
1998
1999 unsafe {
2000 let mut fields = blank(PAMOJA_GATEWAY_STATION_DOWNLINK);
2001 fields.dev_eui = [0x11; 8];
2002 fields.diid = 42;
2003 fields.rx_delay = 1;
2004 fields.has_rx_delay = true;
2005 fields.rx1 = window_to_c(Some((5, 868_100_000)));
2006 fields.rx2 = window_to_c(Some((0, 869_525_000)));
2007 fields.xtime = xtime;
2008 fields.has_xtime = true;
2009 fields.has_rctx = true;
2010 let built = pamoja_gateway_station_message_new(&fields);
2011 let pdu = [0x60, 0x01];
2012 assert_eq!(
2013 pamoja_gateway_station_message_set_payload(built, pdu.as_ptr(), pdu.len()),
2014 PamojaStatus::Ok
2015 );
2016 let text = written(built);
2017 assert_eq!(text, expected.to_json());
2018
2019 let read = pamoja_gateway_station_message_parse(text.as_ptr(), text.len());
2020 let mut back = blank(PAMOJA_GATEWAY_STATION_OTHER);
2021 pamoja_gateway_station_message_fields(read, &mut back);
2022 assert_eq!(back.xtime, xtime, "the clock comes back to the microsecond");
2023 assert!(back.rx1.present && back.rx2.present && !back.ping_slot.present);
2024 pamoja_gateway_station_message_free(read);
2025 }
2026 }
2027
2028 #[test]
2029 fn what_a_kind_does_not_carry_is_refused() {
2030 unsafe {
2031 let built = pamoja_gateway_station_message_new(&blank(PAMOJA_GATEWAY_STATION_VERSION));
2032 let text = "EU868";
2033 assert_eq!(
2034 pamoja_gateway_station_message_set_text(
2035 built,
2036 PAMOJA_GATEWAY_STATION_TEXT_REGION,
2037 text.as_ptr(),
2038 text.len()
2039 ),
2040 PamojaStatus::InvalidArgument
2041 );
2042 assert_eq!(
2043 pamoja_gateway_station_message_add_net_id(built, 1),
2044 PamojaStatus::InvalidArgument
2045 );
2046 let mut out = ptr::null_mut();
2047 assert_eq!(
2048 pamoja_gateway_station_message_text(built, 99, &mut out),
2049 PamojaStatus::InvalidArgument
2050 );
2051 pamoja_gateway_station_message_free(built);
2052
2053 assert!(pamoja_gateway_station_message_new(&blank(42)).is_null());
2054 assert!(pamoja_gateway_station_message_new(ptr::null()).is_null());
2055 }
2056 }
2057
2058 #[test]
2059 fn a_station_clock_is_built_and_taken_apart() {
2060 unsafe {
2061 let mut value = 0i64;
2062 assert_eq!(
2063 pamoja_gateway_station_xtime(0, 0xa5, 3_512_348_611, &mut value),
2064 PamojaStatus::Ok
2065 );
2066 assert_eq!(value, (0xa5_i64 << 48) | 3_512_348_611);
2067
2068 let mut parts = PamojaGatewayStationXtime {
2069 unit: 9,
2070 session: 0,
2071 micros: 0,
2072 };
2073 assert_eq!(
2074 pamoja_gateway_station_xtime_parts(value, &mut parts),
2075 PamojaStatus::Ok
2076 );
2077 assert_eq!(
2078 (parts.unit, parts.session, parts.micros),
2079 (0, 0xa5, 3_512_348_611)
2080 );
2081
2082 assert_eq!(
2083 pamoja_gateway_station_xtime(128, 1, 0, &mut value),
2084 PamojaStatus::InvalidArgument
2085 );
2086 assert_eq!(
2087 pamoja_gateway_station_xtime(0, 1, 1 << 48, &mut value),
2088 PamojaStatus::InvalidArgument
2089 );
2090 }
2091 }
2092
2093 #[test]
2094 fn a_server_answers_a_station_and_reads_who_asked() {
2095 let station = [0xb8, 0x27, 0xeb, 0xff, 0xfe, 0x01, 0x02, 0x03];
2096 let asked = Discovery::new(Eui::new(station)).to_json();
2097 let uri = "ws://lns.example.invalid:3001/router";
2098
2099 unsafe {
2100 let mut router = [0u8; 8];
2101 assert_eq!(
2102 pamoja_gateway_station_discovery_parse(
2103 asked.as_ptr(),
2104 asked.len(),
2105 router.as_mut_ptr()
2106 ),
2107 PamojaStatus::Ok
2108 );
2109 assert_eq!(router, station);
2110
2111 let muxs = [0u8; 8];
2112 let mut text = ptr::null_mut();
2113 assert_eq!(
2114 pamoja_gateway_station_router_accepted(
2115 router.as_ptr(),
2116 muxs.as_ptr(),
2117 uri.as_ptr(),
2118 uri.len(),
2119 &mut text
2120 ),
2121 PamojaStatus::Ok
2122 );
2123 let answer = taken(text);
2124 assert_eq!(
2125 answer,
2126 Router::accepted(Eui::new(station), Eui::new(muxs), uri)
2127 .to_json()
2128 .into_bytes()
2129 );
2130
2131 let mut ids = PamojaGatewayStationRouterIds {
2132 router: [0; 8],
2133 has_router: false,
2134 muxs: [1; 8],
2135 has_muxs: false,
2136 };
2137 assert_eq!(
2138 pamoja_gateway_station_router_identities(answer.as_ptr(), answer.len(), &mut ids),
2139 PamojaStatus::Ok
2140 );
2141 assert!(ids.has_router && ids.has_muxs);
2142 assert_eq!(ids.router, station);
2143 assert_eq!(ids.muxs, muxs);
2144
2145 let why = "this gateway is not registered";
2146 assert_eq!(
2147 pamoja_gateway_station_router_refused(
2148 router.as_ptr(),
2149 why.as_ptr(),
2150 why.len(),
2151 &mut text
2152 ),
2153 PamojaStatus::Ok
2154 );
2155 assert_eq!(
2156 taken(text),
2157 Router::refused(Eui::new(station), why)
2158 .to_json()
2159 .into_bytes()
2160 );
2161 }
2162 }
2163}