1use std::ptr;
18
19use pamoja_gateway::udp::{
20 CrcStatus, Eui, Modulation, Packet, PacketKind, Rxpk, Stat, TxStatus, Txpk, Uplink,
21};
22use pamoja_lora::budget::Decibels;
23use pamoja_lora::LinkSettings;
24
25use crate::lora::{settings, PamojaLoraLink};
26use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
27
28pub const PAMOJA_GATEWAY_PROTOCOL_VERSION: u8 = 2;
30
31pub const PAMOJA_GATEWAY_EUI_LEN: usize = 8;
33
34pub const PAMOJA_GATEWAY_PUSH_DATA: u8 = 0x00;
36
37pub const PAMOJA_GATEWAY_PUSH_ACK: u8 = 0x01;
39
40pub const PAMOJA_GATEWAY_PULL_DATA: u8 = 0x02;
42
43pub const PAMOJA_GATEWAY_PULL_RESP: u8 = 0x03;
45
46pub const PAMOJA_GATEWAY_PULL_ACK: u8 = 0x04;
48
49pub const PAMOJA_GATEWAY_TX_ACK: u8 = 0x05;
51
52pub const PAMOJA_GATEWAY_MODULATION_LORA: u8 = 0;
54
55pub const PAMOJA_GATEWAY_MODULATION_FSK: u8 = 1;
57
58pub const PAMOJA_GATEWAY_CRC_OK: i8 = 1;
60
61pub const PAMOJA_GATEWAY_CRC_FAILED: i8 = -1;
63
64pub const PAMOJA_GATEWAY_CRC_ABSENT: i8 = 0;
66
67pub const PAMOJA_GATEWAY_TX_NONE: u8 = 0;
69
70pub const PAMOJA_GATEWAY_TX_TOO_LATE: u8 = 1;
72
73pub const PAMOJA_GATEWAY_TX_TOO_EARLY: u8 = 2;
75
76pub const PAMOJA_GATEWAY_TX_COLLISION_PACKET: u8 = 3;
78
79pub const PAMOJA_GATEWAY_TX_COLLISION_BEACON: u8 = 4;
81
82pub const PAMOJA_GATEWAY_TX_FREQ: u8 = 5;
84
85pub const PAMOJA_GATEWAY_TX_POWER: u8 = 6;
87
88pub const PAMOJA_GATEWAY_TX_GPS_UNLOCKED: u8 = 7;
90
91const _: () = assert!(
92 PAMOJA_GATEWAY_PROTOCOL_VERSION == pamoja_gateway::udp::PROTOCOL_VERSION,
93 "the protocol version must match the crate"
94);
95const _: () = assert!(
96 PAMOJA_GATEWAY_EUI_LEN == pamoja_gateway::udp::EUI_LEN,
97 "the identifier length must match the crate"
98);
99const _: () = assert!(PAMOJA_GATEWAY_PUSH_DATA == PacketKind::PushData.identifier());
100const _: () = assert!(PAMOJA_GATEWAY_PUSH_ACK == PacketKind::PushAck.identifier());
101const _: () = assert!(PAMOJA_GATEWAY_PULL_DATA == PacketKind::PullData.identifier());
102const _: () = assert!(PAMOJA_GATEWAY_PULL_RESP == PacketKind::PullResp.identifier());
103const _: () = assert!(PAMOJA_GATEWAY_PULL_ACK == PacketKind::PullAck.identifier());
104const _: () = assert!(PAMOJA_GATEWAY_TX_ACK == PacketKind::TxAck.identifier());
105const _: () = assert!(PAMOJA_GATEWAY_CRC_OK == CrcStatus::Ok.code());
106const _: () = assert!(PAMOJA_GATEWAY_CRC_FAILED == CrcStatus::Failed.code());
107const _: () = assert!(PAMOJA_GATEWAY_CRC_ABSENT == CrcStatus::Absent.code());
108
109pub struct PamojaGatewayPacket {
114 packet: Packet,
115}
116
117pub struct PamojaGatewayUplink {
122 uplink: Uplink,
123}
124
125#[repr(C)]
127#[derive(Clone, Copy, Debug, Default, PartialEq)]
128pub struct PamojaGatewayRxpk {
129 pub received_at_us: u64,
131 pub gps_millis: u64,
133 pub frequency_hz: u32,
135 pub timestamp_us: u32,
137 pub link: PamojaLoraLink,
139 pub bitrate_bps: u32,
141 pub rssi_centi_dbm: i32,
143 pub snr_centi_db: i32,
145 pub channel: u8,
147 pub rf_chain: u8,
149 pub crc: i8,
151 pub modulation: u8,
153 pub has_received_at: bool,
155 pub has_gps_millis: bool,
157 pub has_timestamp: bool,
159 pub has_snr: bool,
161}
162
163#[repr(C)]
165#[derive(Clone, Copy, Debug, Default, PartialEq)]
166pub struct PamojaGatewayStat {
167 pub time_s: u64,
169 pub latitude_deg: f64,
171 pub longitude_deg: f64,
173 pub acknowledged_percent: f64,
175 pub altitude_m: i32,
177 pub received: u32,
179 pub received_ok: u32,
181 pub forwarded: u32,
183 pub downlinks: u32,
185 pub transmitted: u32,
187 pub has_time: bool,
189 pub has_position: bool,
191 pub has_altitude: bool,
193}
194
195#[repr(C)]
197#[derive(Clone, Copy, Debug, Default, PartialEq)]
198pub struct PamojaGatewayTxpk {
199 pub gps_millis: u64,
201 pub frequency_hz: u32,
203 pub timestamp_us: u32,
205 pub link: PamojaLoraLink,
207 pub bitrate_bps: u32,
209 pub frequency_deviation_hz: u32,
211 pub preamble_symbols: u16,
213 pub rf_chain: u8,
215 pub power_dbm: i8,
217 pub modulation: u8,
219 pub immediate: bool,
221 pub invert_polarity: bool,
223 pub without_crc: bool,
225 pub has_timestamp: bool,
227 pub has_gps_millis: bool,
229 pub has_deviation: bool,
231 pub has_preamble: bool,
233}
234
235#[no_mangle]
242pub extern "C" fn pamoja_gateway_uplink_new() -> *mut PamojaGatewayUplink {
243 Box::into_raw(Box::new(PamojaGatewayUplink {
244 uplink: Uplink::default(),
245 }))
246}
247
248#[no_mangle]
267pub unsafe extern "C" fn pamoja_gateway_uplink_add_rxpk(
268 uplink: *mut PamojaGatewayUplink,
269 packet: PamojaGatewayRxpk,
270 payload: *const u8,
271 payload_len: usize,
272) -> PamojaStatus {
273 let Some(uplink) = uplink.as_mut() else {
274 return missing("uplink");
275 };
276 let payload = match read_bytes(payload, payload_len) {
277 Ok(payload) => payload,
278 Err(status) => return status,
279 };
280 uplink.uplink.packets.push(rxpk_of(packet, payload));
281 PamojaStatus::Ok
282}
283
284#[no_mangle]
299pub unsafe extern "C" fn pamoja_gateway_uplink_set_stat(
300 uplink: *mut PamojaGatewayUplink,
301 status: PamojaGatewayStat,
302) -> PamojaStatus {
303 let Some(uplink) = uplink.as_mut() else {
304 return missing("uplink");
305 };
306 uplink.uplink.status = Some(stat_of(status));
307 PamojaStatus::Ok
308}
309
310#[no_mangle]
321pub unsafe extern "C" fn pamoja_gateway_uplink_free(uplink: *mut PamojaGatewayUplink) {
322 if !uplink.is_null() {
323 drop(Box::from_raw(uplink));
324 }
325}
326
327#[no_mangle]
345pub unsafe extern "C" fn pamoja_gateway_push_data(
346 token: u16,
347 gateway: *const u8,
348 uplink: *mut PamojaGatewayUplink,
349 out_packet: *mut *mut PamojaGatewayPacket,
350) -> PamojaStatus {
351 if !clear(out_packet) {
352 return PamojaStatus::InvalidArgument;
353 }
354 let Some(identifier) = eui_of(gateway) else {
355 return missing("gateway");
356 };
357 if uplink.is_null() {
358 return missing("uplink");
359 }
360 let uplink = Box::from_raw(uplink).uplink;
361 *out_packet = wrap(Packet::PushData {
362 token,
363 gateway: identifier,
364 uplink,
365 });
366 PamojaStatus::Ok
367}
368
369#[no_mangle]
379pub extern "C" fn pamoja_gateway_push_ack(token: u16) -> *mut PamojaGatewayPacket {
380 wrap(Packet::PushAck { token })
381}
382
383#[no_mangle]
400pub unsafe extern "C" fn pamoja_gateway_pull_data(
401 token: u16,
402 gateway: *const u8,
403 out_packet: *mut *mut PamojaGatewayPacket,
404) -> PamojaStatus {
405 if !clear(out_packet) {
406 return PamojaStatus::InvalidArgument;
407 }
408 let Some(identifier) = eui_of(gateway) else {
409 return missing("gateway");
410 };
411 *out_packet = wrap(Packet::PullData {
412 token,
413 gateway: identifier,
414 });
415 PamojaStatus::Ok
416}
417
418#[no_mangle]
428pub extern "C" fn pamoja_gateway_pull_ack(token: u16) -> *mut PamojaGatewayPacket {
429 wrap(Packet::PullAck { token })
430}
431
432#[no_mangle]
451pub unsafe extern "C" fn pamoja_gateway_pull_resp(
452 token: u16,
453 transmit: PamojaGatewayTxpk,
454 payload: *const u8,
455 payload_len: usize,
456 out_packet: *mut *mut PamojaGatewayPacket,
457) -> PamojaStatus {
458 if !clear(out_packet) {
459 return PamojaStatus::InvalidArgument;
460 }
461 let payload = match read_bytes(payload, payload_len) {
462 Ok(payload) => payload,
463 Err(status) => return status,
464 };
465 *out_packet = wrap(Packet::PullResp {
466 token,
467 transmit: txpk_of(transmit, payload),
468 });
469 PamojaStatus::Ok
470}
471
472#[no_mangle]
491pub unsafe extern "C" fn pamoja_gateway_tx_ack(
492 token: u16,
493 gateway: *const u8,
494 status: u8,
495 out_packet: *mut *mut PamojaGatewayPacket,
496) -> PamojaStatus {
497 if !clear(out_packet) {
498 return PamojaStatus::InvalidArgument;
499 }
500 let Some(identifier) = eui_of(gateway) else {
501 return missing("gateway");
502 };
503 let Some(status) = tx_status_of(status) else {
504 set_last_error(format!("{status} is not a TX_ACK status"));
505 return PamojaStatus::InvalidArgument;
506 };
507 *out_packet = wrap(Packet::TxAck {
508 token,
509 gateway: identifier,
510 status,
511 });
512 PamojaStatus::Ok
513}
514
515#[no_mangle]
532pub unsafe extern "C" fn pamoja_gateway_packet_parse(
533 bytes: *const u8,
534 len: usize,
535 out_packet: *mut *mut PamojaGatewayPacket,
536) -> PamojaStatus {
537 if !clear(out_packet) {
538 return PamojaStatus::InvalidArgument;
539 }
540 let datagram = match read_bytes(bytes, len) {
541 Ok(datagram) => datagram,
542 Err(status) => return status,
543 };
544 match Packet::parse(&datagram) {
545 Ok(packet) => {
546 *out_packet = wrap(packet);
547 PamojaStatus::Ok
548 }
549 Err(error) => {
550 set_last_error(error.to_string());
551 PamojaStatus::Codec
552 }
553 }
554}
555
556#[no_mangle]
571pub unsafe extern "C" fn pamoja_gateway_packet_kind(packet: *const PamojaGatewayPacket) -> u8 {
572 match packet.as_ref() {
573 Some(packet) => packet.packet.kind().identifier(),
574 None => u8::MAX,
575 }
576}
577
578#[no_mangle]
592pub unsafe extern "C" fn pamoja_gateway_packet_token(packet: *const PamojaGatewayPacket) -> u16 {
593 packet.as_ref().map_or(0, |packet| packet.packet.token())
594}
595
596#[no_mangle]
612pub unsafe extern "C" fn pamoja_gateway_packet_gateway(
613 packet: *const PamojaGatewayPacket,
614 out_gateway: *mut u8,
615) -> bool {
616 let Some(packet) = packet.as_ref() else {
617 return false;
618 };
619 let Some(identifier) = packet.packet.gateway() else {
620 return false;
621 };
622 if out_gateway.is_null() {
623 return false;
624 }
625 ptr::copy_nonoverlapping(
626 identifier.bytes().as_ptr(),
627 out_gateway,
628 PAMOJA_GATEWAY_EUI_LEN,
629 );
630 true
631}
632
633#[no_mangle]
647pub unsafe extern "C" fn pamoja_gateway_packet_rxpk_count(
648 packet: *const PamojaGatewayPacket,
649) -> usize {
650 match packet.as_ref().map(|packet| &packet.packet) {
651 Some(Packet::PushData { uplink, .. }) => uplink.packets.len(),
652 _ => 0,
653 }
654}
655
656#[no_mangle]
672pub unsafe extern "C" fn pamoja_gateway_packet_rxpk(
673 packet: *const PamojaGatewayPacket,
674 index: usize,
675 out_rxpk: *mut PamojaGatewayRxpk,
676) -> bool {
677 let Some(heard) = heard_at(packet, index) else {
678 return false;
679 };
680 if out_rxpk.is_null() {
681 return false;
682 }
683 *out_rxpk = rxpk_to_c(heard);
684 true
685}
686
687#[no_mangle]
705pub unsafe extern "C" fn pamoja_gateway_packet_rxpk_payload(
706 packet: *const PamojaGatewayPacket,
707 index: usize,
708) -> *const u8 {
709 heard_at(packet, index).map_or(ptr::null(), |heard| heard.payload.as_ptr())
710}
711
712#[no_mangle]
727pub unsafe extern "C" fn pamoja_gateway_packet_rxpk_payload_len(
728 packet: *const PamojaGatewayPacket,
729 index: usize,
730) -> usize {
731 heard_at(packet, index).map_or(0, |heard| heard.payload.len())
732}
733
734#[no_mangle]
749pub unsafe extern "C" fn pamoja_gateway_packet_stat(
750 packet: *const PamojaGatewayPacket,
751 out_stat: *mut PamojaGatewayStat,
752) -> bool {
753 let report = match packet.as_ref().map(|packet| &packet.packet) {
754 Some(Packet::PushData { uplink, .. }) => uplink.status.as_ref(),
755 _ => None,
756 };
757 let Some(report) = report else {
758 return false;
759 };
760 if out_stat.is_null() {
761 return false;
762 }
763 *out_stat = stat_to_c(report);
764 true
765}
766
767#[no_mangle]
782pub unsafe extern "C" fn pamoja_gateway_packet_txpk(
783 packet: *const PamojaGatewayPacket,
784 out_txpk: *mut PamojaGatewayTxpk,
785) -> bool {
786 let Some(request) = requested(packet) else {
787 return false;
788 };
789 if out_txpk.is_null() {
790 return false;
791 }
792 *out_txpk = txpk_to_c(request);
793 true
794}
795
796#[no_mangle]
813pub unsafe extern "C" fn pamoja_gateway_packet_txpk_payload(
814 packet: *const PamojaGatewayPacket,
815) -> *const u8 {
816 requested(packet).map_or(ptr::null(), |request| request.payload.as_ptr())
817}
818
819#[no_mangle]
833pub unsafe extern "C" fn pamoja_gateway_packet_txpk_payload_len(
834 packet: *const PamojaGatewayPacket,
835) -> usize {
836 requested(packet).map_or(0, |request| request.payload.len())
837}
838
839#[no_mangle]
854pub unsafe extern "C" fn pamoja_gateway_packet_tx_status(packet: *const PamojaGatewayPacket) -> u8 {
855 match packet.as_ref().map(|packet| &packet.packet) {
856 Some(Packet::TxAck { status, .. }) => tx_status_code(*status),
857 _ => u8::MAX,
858 }
859}
860
861#[no_mangle]
878pub unsafe extern "C" fn pamoja_gateway_packet_acknowledgment(
879 packet: *const PamojaGatewayPacket,
880 out_packet: *mut *mut PamojaGatewayPacket,
881) -> bool {
882 if !clear(out_packet) {
883 return false;
884 }
885 let Some(packet) = packet.as_ref() else {
886 return false;
887 };
888 match packet.packet.acknowledgment() {
889 Some(acknowledgment) => {
890 *out_packet = wrap(acknowledgment);
891 true
892 }
893 None => false,
894 }
895}
896
897#[no_mangle]
911pub unsafe extern "C" fn pamoja_gateway_packet_to_buffer(
912 packet: *const PamojaGatewayPacket,
913) -> *mut PamojaBuffer {
914 match packet.as_ref() {
915 Some(packet) => PamojaBuffer::into_raw(packet.packet.to_bytes()),
916 None => ptr::null_mut(),
917 }
918}
919
920#[no_mangle]
931pub unsafe extern "C" fn pamoja_gateway_packet_free(packet: *mut PamojaGatewayPacket) {
932 if !packet.is_null() {
933 drop(Box::from_raw(packet));
934 }
935}
936
937fn wrap(packet: Packet) -> *mut PamojaGatewayPacket {
939 Box::into_raw(Box::new(PamojaGatewayPacket { packet }))
940}
941
942unsafe fn clear(out_packet: *mut *mut PamojaGatewayPacket) -> bool {
944 if out_packet.is_null() {
945 set_last_error("out_packet must not be null".to_owned());
946 return false;
947 }
948 *out_packet = ptr::null_mut();
949 true
950}
951
952pub(crate) fn missing(name: &str) -> PamojaStatus {
954 set_last_error(format!("{name} must not be null"));
955 PamojaStatus::InvalidArgument
956}
957
958unsafe fn eui_of(gateway: *const u8) -> Option<Eui> {
960 if gateway.is_null() {
961 return None;
962 }
963 let mut bytes = [0u8; PAMOJA_GATEWAY_EUI_LEN];
964 ptr::copy_nonoverlapping(gateway, bytes.as_mut_ptr(), PAMOJA_GATEWAY_EUI_LEN);
965 Some(Eui::new(bytes))
966}
967
968unsafe fn heard_at<'a>(packet: *const PamojaGatewayPacket, index: usize) -> Option<&'a Rxpk> {
970 match packet.as_ref().map(|packet| &packet.packet) {
971 Some(Packet::PushData { uplink, .. }) => uplink.packets.get(index),
972 _ => None,
973 }
974}
975
976unsafe fn requested<'a>(packet: *const PamojaGatewayPacket) -> Option<&'a Txpk> {
978 match packet.as_ref().map(|packet| &packet.packet) {
979 Some(Packet::PullResp { transmit, .. }) => Some(transmit),
980 _ => None,
981 }
982}
983
984fn tx_status_of(code: u8) -> Option<TxStatus> {
986 Some(match code {
987 PAMOJA_GATEWAY_TX_NONE => TxStatus::None,
988 PAMOJA_GATEWAY_TX_TOO_LATE => TxStatus::TooLate,
989 PAMOJA_GATEWAY_TX_TOO_EARLY => TxStatus::TooEarly,
990 PAMOJA_GATEWAY_TX_COLLISION_PACKET => TxStatus::CollisionPacket,
991 PAMOJA_GATEWAY_TX_COLLISION_BEACON => TxStatus::CollisionBeacon,
992 PAMOJA_GATEWAY_TX_FREQ => TxStatus::TxFreq,
993 PAMOJA_GATEWAY_TX_POWER => TxStatus::TxPower,
994 PAMOJA_GATEWAY_TX_GPS_UNLOCKED => TxStatus::GpsUnlocked,
995 _ => return None,
996 })
997}
998
999fn tx_status_code(status: TxStatus) -> u8 {
1001 match status {
1002 TxStatus::None => PAMOJA_GATEWAY_TX_NONE,
1003 TxStatus::TooLate => PAMOJA_GATEWAY_TX_TOO_LATE,
1004 TxStatus::TooEarly => PAMOJA_GATEWAY_TX_TOO_EARLY,
1005 TxStatus::CollisionPacket => PAMOJA_GATEWAY_TX_COLLISION_PACKET,
1006 TxStatus::CollisionBeacon => PAMOJA_GATEWAY_TX_COLLISION_BEACON,
1007 TxStatus::TxFreq => PAMOJA_GATEWAY_TX_FREQ,
1008 TxStatus::TxPower => PAMOJA_GATEWAY_TX_POWER,
1009 TxStatus::GpsUnlocked => PAMOJA_GATEWAY_TX_GPS_UNLOCKED,
1010 }
1011}
1012
1013fn modulation_of(code: u8, link: PamojaLoraLink, bitrate_bps: u32) -> Modulation {
1015 if code == PAMOJA_GATEWAY_MODULATION_FSK {
1016 Modulation::Fsk(bitrate_bps)
1017 } else {
1018 Modulation::Lora(settings(link))
1019 }
1020}
1021
1022fn modulation_to_c(modulation: Modulation) -> (u8, PamojaLoraLink, u32) {
1024 match modulation {
1025 Modulation::Lora(link) => (PAMOJA_GATEWAY_MODULATION_LORA, link_to_c(link), 0),
1026 Modulation::Fsk(bitrate) => (
1027 PAMOJA_GATEWAY_MODULATION_FSK,
1028 link_to_c(LinkSettings::new(7, 125_000)),
1029 bitrate,
1030 ),
1031 }
1032}
1033
1034pub(crate) fn link_to_c(link: LinkSettings) -> PamojaLoraLink {
1036 PamojaLoraLink {
1037 bandwidth_hz: link.bandwidth_hz(),
1038 preamble_symbols: link.preamble_symbols(),
1039 spreading_factor: link.spreading_factor(),
1040 coding_rate_denominator: link.coding_rate_denominator(),
1041 explicit_header: u8::from(link.explicit_header()),
1042 crc: u8::from(link.crc()),
1043 }
1044}
1045
1046pub(crate) fn rxpk_of(packet: PamojaGatewayRxpk, payload: Vec<u8>) -> Rxpk {
1048 Rxpk {
1049 received_at: packet
1050 .has_received_at
1051 .then(|| pamoja_gateway::time::compact(packet.received_at_us)),
1052 gps_millis: packet.has_gps_millis.then_some(packet.gps_millis),
1053 timestamp_us: packet.has_timestamp.then_some(packet.timestamp_us),
1054 frequency_hz: packet.frequency_hz,
1055 channel: packet.channel,
1056 rf_chain: packet.rf_chain,
1057 crc: CrcStatus::from_code(i64::from(packet.crc)).unwrap_or(CrcStatus::Absent),
1058 modulation: modulation_of(packet.modulation, packet.link, packet.bitrate_bps),
1059 rssi_dbm: Decibels::from_hundredths(packet.rssi_centi_dbm),
1060 snr_db: packet
1061 .has_snr
1062 .then(|| Decibels::from_hundredths(packet.snr_centi_db)),
1063 payload,
1064 }
1065}
1066
1067fn rxpk_to_c(heard: &Rxpk) -> PamojaGatewayRxpk {
1069 let (modulation, link, bitrate_bps) = modulation_to_c(heard.modulation);
1070 let received_at = heard
1071 .received_at
1072 .as_deref()
1073 .and_then(pamoja_gateway::time::from_compact);
1074 PamojaGatewayRxpk {
1075 received_at_us: received_at.unwrap_or(0),
1076 gps_millis: heard.gps_millis.unwrap_or(0),
1077 frequency_hz: heard.frequency_hz,
1078 timestamp_us: heard.timestamp_us.unwrap_or(0),
1079 link,
1080 bitrate_bps,
1081 rssi_centi_dbm: heard.rssi_dbm.hundredths(),
1082 snr_centi_db: heard.snr_db.map_or(0, Decibels::hundredths),
1083 channel: heard.channel,
1084 rf_chain: heard.rf_chain,
1085 crc: heard.crc.code(),
1086 modulation,
1087 has_received_at: received_at.is_some(),
1088 has_gps_millis: heard.gps_millis.is_some(),
1089 has_timestamp: heard.timestamp_us.is_some(),
1090 has_snr: heard.snr_db.is_some(),
1091 }
1092}
1093
1094fn stat_of(report: PamojaGatewayStat) -> Stat {
1096 Stat {
1097 time: report
1098 .has_time
1099 .then(|| pamoja_gateway::time::expanded(report.time_s)),
1100 latitude_deg: report.has_position.then_some(report.latitude_deg),
1101 longitude_deg: report.has_position.then_some(report.longitude_deg),
1102 altitude_m: report.has_altitude.then_some(report.altitude_m),
1103 received: report.received,
1104 received_ok: report.received_ok,
1105 forwarded: report.forwarded,
1106 acknowledged_percent: report.acknowledged_percent,
1107 downlinks: report.downlinks,
1108 transmitted: report.transmitted,
1109 }
1110}
1111
1112fn stat_to_c(report: &Stat) -> PamojaGatewayStat {
1114 let clock = report
1115 .time
1116 .as_deref()
1117 .and_then(pamoja_gateway::time::from_expanded);
1118 PamojaGatewayStat {
1119 time_s: clock.unwrap_or(0),
1120 latitude_deg: report.latitude_deg.unwrap_or(0.0),
1121 longitude_deg: report.longitude_deg.unwrap_or(0.0),
1122 acknowledged_percent: report.acknowledged_percent,
1123 altitude_m: report.altitude_m.unwrap_or(0),
1124 received: report.received,
1125 received_ok: report.received_ok,
1126 forwarded: report.forwarded,
1127 downlinks: report.downlinks,
1128 transmitted: report.transmitted,
1129 has_time: clock.is_some(),
1130 has_position: report.latitude_deg.is_some() && report.longitude_deg.is_some(),
1131 has_altitude: report.altitude_m.is_some(),
1132 }
1133}
1134
1135fn txpk_of(request: PamojaGatewayTxpk, payload: Vec<u8>) -> Txpk {
1137 Txpk {
1138 immediate: request.immediate,
1139 timestamp_us: request.has_timestamp.then_some(request.timestamp_us),
1140 gps_millis: request.has_gps_millis.then_some(request.gps_millis),
1141 frequency_hz: request.frequency_hz,
1142 rf_chain: request.rf_chain,
1143 power_dbm: request.power_dbm,
1144 modulation: modulation_of(request.modulation, request.link, request.bitrate_bps),
1145 frequency_deviation_hz: request
1146 .has_deviation
1147 .then_some(request.frequency_deviation_hz),
1148 invert_polarity: request.invert_polarity,
1149 preamble_symbols: request.has_preamble.then_some(request.preamble_symbols),
1150 without_crc: request.without_crc,
1151 payload,
1152 }
1153}
1154
1155pub(crate) fn txpk_to_c(request: &Txpk) -> PamojaGatewayTxpk {
1157 let (modulation, link, bitrate_bps) = modulation_to_c(request.modulation);
1158 PamojaGatewayTxpk {
1159 gps_millis: request.gps_millis.unwrap_or(0),
1160 frequency_hz: request.frequency_hz,
1161 timestamp_us: request.timestamp_us.unwrap_or(0),
1162 link,
1163 bitrate_bps,
1164 frequency_deviation_hz: request.frequency_deviation_hz.unwrap_or(0),
1165 preamble_symbols: request.preamble_symbols.unwrap_or(0),
1166 rf_chain: request.rf_chain,
1167 power_dbm: request.power_dbm,
1168 modulation,
1169 immediate: request.immediate,
1170 invert_polarity: request.invert_polarity,
1171 without_crc: request.without_crc,
1172 has_timestamp: request.timestamp_us.is_some(),
1173 has_gps_millis: request.gps_millis.is_some(),
1174 has_deviation: request.frequency_deviation_hz.is_some(),
1175 has_preamble: request.preamble_symbols.is_some(),
1176 }
1177}
1178
1179#[cfg(test)]
1180mod tests {
1181 use super::*;
1182 use crate::lora::pamoja_lora_link_default;
1183 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1184
1185 const GATEWAY: [u8; 8] = [0xB8, 0x27, 0xEB, 0xFF, 0xFE, 0x01, 0x02, 0x03];
1186
1187 fn bytes_of(packet: *const PamojaGatewayPacket) -> Vec<u8> {
1188 unsafe {
1189 let buffer = pamoja_gateway_packet_to_buffer(packet);
1190 let len = pamoja_buffer_len(buffer);
1191 let bytes = std::slice::from_raw_parts(pamoja_buffer_data(buffer), len).to_vec();
1192 pamoja_buffer_free(buffer);
1193 bytes
1194 }
1195 }
1196
1197 #[test]
1198 fn a_push_data_crosses_and_comes_back() {
1199 unsafe {
1200 let uplink = pamoja_gateway_uplink_new();
1201 let heard = PamojaGatewayRxpk {
1202 frequency_hz: 868_100_000,
1203 link: pamoja_lora_link_default(7, 125_000),
1204 rssi_centi_dbm: -3_500,
1205 snr_centi_db: 510,
1206 has_snr: true,
1207 timestamp_us: 3_512_348_611,
1208 has_timestamp: true,
1209 received_at_us: 1_364_746_877_528_002,
1210 has_received_at: true,
1211 crc: PAMOJA_GATEWAY_CRC_OK,
1212 channel: 2,
1213 ..PamojaGatewayRxpk::default()
1214 };
1215 let payload = b"TEST_PACKET_1234";
1216 assert_eq!(
1217 pamoja_gateway_uplink_add_rxpk(uplink, heard, payload.as_ptr(), payload.len()),
1218 PamojaStatus::Ok
1219 );
1220
1221 let mut packet = ptr::null_mut();
1222 assert_eq!(
1223 pamoja_gateway_push_data(0x1234, GATEWAY.as_ptr(), uplink, &mut packet),
1224 PamojaStatus::Ok
1225 );
1226 let datagram = bytes_of(packet);
1227 pamoja_gateway_packet_free(packet);
1228
1229 let mut read = ptr::null_mut();
1230 assert_eq!(
1231 pamoja_gateway_packet_parse(datagram.as_ptr(), datagram.len(), &mut read),
1232 PamojaStatus::Ok
1233 );
1234 assert_eq!(pamoja_gateway_packet_kind(read), PAMOJA_GATEWAY_PUSH_DATA);
1235 assert_eq!(pamoja_gateway_packet_token(read), 0x1234);
1236 let mut identifier = [0u8; 8];
1237 assert!(pamoja_gateway_packet_gateway(read, identifier.as_mut_ptr()));
1238 assert_eq!(identifier, GATEWAY);
1239 assert_eq!(pamoja_gateway_packet_rxpk_count(read), 1);
1240
1241 let mut back = PamojaGatewayRxpk::default();
1242 assert!(pamoja_gateway_packet_rxpk(read, 0, &mut back));
1243 assert_eq!(back, heard);
1244 let len = pamoja_gateway_packet_rxpk_payload_len(read, 0);
1245 let carried =
1246 std::slice::from_raw_parts(pamoja_gateway_packet_rxpk_payload(read, 0), len);
1247 assert_eq!(carried, payload);
1248
1249 let mut ack = ptr::null_mut();
1250 assert!(pamoja_gateway_packet_acknowledgment(read, &mut ack));
1251 assert_eq!(bytes_of(ack), [2, 0x12, 0x34, 0x01]);
1252 pamoja_gateway_packet_free(ack);
1253 pamoja_gateway_packet_free(read);
1254 }
1255 }
1256
1257 #[test]
1258 fn a_pull_resp_and_its_tx_ack_cross() {
1259 unsafe {
1260 let request = PamojaGatewayTxpk {
1261 frequency_hz: 869_525_000,
1262 link: pamoja_lora_link_default(9, 125_000),
1263 power_dbm: 27,
1264 timestamp_us: 3_512_348_611,
1265 has_timestamp: true,
1266 invert_polarity: true,
1267 without_crc: true,
1268 ..PamojaGatewayTxpk::default()
1269 };
1270 let payload = b"downlink";
1271 let mut packet = ptr::null_mut();
1272 assert_eq!(
1273 pamoja_gateway_pull_resp(
1274 0x00AB,
1275 request,
1276 payload.as_ptr(),
1277 payload.len(),
1278 &mut packet
1279 ),
1280 PamojaStatus::Ok
1281 );
1282 let datagram = bytes_of(packet);
1283 pamoja_gateway_packet_free(packet);
1284
1285 let mut read = ptr::null_mut();
1286 assert_eq!(
1287 pamoja_gateway_packet_parse(datagram.as_ptr(), datagram.len(), &mut read),
1288 PamojaStatus::Ok
1289 );
1290 let mut back = PamojaGatewayTxpk::default();
1291 assert!(pamoja_gateway_packet_txpk(read, &mut back));
1292 assert_eq!(back, request);
1293 let len = pamoja_gateway_packet_txpk_payload_len(read);
1294 assert_eq!(
1295 std::slice::from_raw_parts(pamoja_gateway_packet_txpk_payload(read), len),
1296 payload
1297 );
1298 assert!(!pamoja_gateway_packet_acknowledgment(
1299 read,
1300 &mut ptr::null_mut()
1301 ));
1302 pamoja_gateway_packet_free(read);
1303
1304 let mut ack = ptr::null_mut();
1305 assert_eq!(
1306 pamoja_gateway_tx_ack(
1307 0x00AB,
1308 GATEWAY.as_ptr(),
1309 PAMOJA_GATEWAY_TX_COLLISION_PACKET,
1310 &mut ack
1311 ),
1312 PamojaStatus::Ok
1313 );
1314 let datagram = bytes_of(ack);
1315 pamoja_gateway_packet_free(ack);
1316
1317 let mut read = ptr::null_mut();
1318 assert_eq!(
1319 pamoja_gateway_packet_parse(datagram.as_ptr(), datagram.len(), &mut read),
1320 PamojaStatus::Ok
1321 );
1322 assert_eq!(
1323 pamoja_gateway_packet_tx_status(read),
1324 PAMOJA_GATEWAY_TX_COLLISION_PACKET
1325 );
1326 pamoja_gateway_packet_free(read);
1327 }
1328 }
1329
1330 #[test]
1331 fn the_acknowledgments_are_four_bytes() {
1332 unsafe {
1333 let push = pamoja_gateway_push_ack(0x0102);
1334 let pull = pamoja_gateway_pull_ack(0x0304);
1335 assert_eq!(bytes_of(push), [2, 0x01, 0x02, 0x01]);
1336 assert_eq!(bytes_of(pull), [2, 0x03, 0x04, 0x04]);
1337 pamoja_gateway_packet_free(push);
1338 pamoja_gateway_packet_free(pull);
1339
1340 let mut pull_data = ptr::null_mut();
1341 assert_eq!(
1342 pamoja_gateway_pull_data(0x0506, GATEWAY.as_ptr(), &mut pull_data),
1343 PamojaStatus::Ok
1344 );
1345 assert_eq!(bytes_of(pull_data).len(), 12);
1346 pamoja_gateway_packet_free(pull_data);
1347 }
1348 }
1349
1350 #[test]
1351 fn a_null_argument_or_a_bad_datagram_is_refused() {
1352 unsafe {
1353 assert_eq!(
1354 pamoja_gateway_uplink_add_rxpk(
1355 ptr::null_mut(),
1356 PamojaGatewayRxpk::default(),
1357 ptr::null(),
1358 0
1359 ),
1360 PamojaStatus::InvalidArgument
1361 );
1362 let mut packet = ptr::null_mut();
1363 assert_eq!(
1364 pamoja_gateway_pull_data(1, ptr::null(), &mut packet),
1365 PamojaStatus::InvalidArgument
1366 );
1367 assert!(packet.is_null());
1368 assert_eq!(
1369 pamoja_gateway_tx_ack(1, GATEWAY.as_ptr(), 9, &mut packet),
1370 PamojaStatus::InvalidArgument
1371 );
1372 let datagram = [1u8, 0, 1, 0];
1373 assert_eq!(
1374 pamoja_gateway_packet_parse(datagram.as_ptr(), datagram.len(), &mut packet),
1375 PamojaStatus::Codec
1376 );
1377 assert_eq!(pamoja_gateway_packet_kind(ptr::null()), u8::MAX);
1378 assert_eq!(pamoja_gateway_packet_tx_status(ptr::null()), u8::MAX);
1379 pamoja_gateway_packet_free(ptr::null_mut());
1380 pamoja_gateway_uplink_free(ptr::null_mut());
1381 }
1382 }
1383}