1use std::ffi::c_char;
12use std::ptr;
13
14use pamoja_gateway::chirpstack::{uplink_topic, UplinkEvent};
15
16use crate::{read_bytes, read_str, set_last_error, PamojaBuffer, PamojaStatus, PamojaString};
17
18#[repr(C)]
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub struct PamojaChirpstackUplinkSummary {
22 pub dev_addr: u32,
24 pub fcnt: u32,
26 pub frequency_hz: u32,
28 pub reception_count: u32,
30 pub dev_eui: [u8; 8],
32 pub has_dev_addr: u8,
34 pub adr: u8,
36 pub data_rate: u8,
38 pub has_fport: u8,
40 pub fport: u8,
42 pub confirmed: u8,
44 pub has_frequency: u8,
46}
47
48#[repr(C)]
50#[derive(Clone, Copy, Debug, PartialEq)]
51pub struct PamojaChirpstackReception {
52 pub rssi_dbm: i32,
54 pub snr_db: f32,
56 pub gateway: [u8; 8],
58}
59
60pub struct PamojaChirpstackUplink {
64 event: UplinkEvent,
65}
66
67#[no_mangle]
91pub unsafe extern "C" fn pamoja_chirpstack_uplink_parse(
92 text: *const u8,
93 text_len: usize,
94 out_uplink: *mut *mut PamojaChirpstackUplink,
95) -> PamojaStatus {
96 if out_uplink.is_null() {
97 set_last_error("out_uplink must not be null".to_owned());
98 return PamojaStatus::InvalidArgument;
99 }
100 let slot = &mut *out_uplink;
101 *slot = ptr::null_mut();
102 let bytes = match read_bytes(text, text_len) {
103 Ok(bytes) => bytes,
104 Err(status) => return status,
105 };
106 let Ok(text) = std::str::from_utf8(&bytes) else {
107 set_last_error("the event is not UTF-8".to_owned());
108 return PamojaStatus::Codec;
109 };
110 match UplinkEvent::from_json(text) {
111 Ok(event) => {
112 *slot = Box::into_raw(Box::new(PamojaChirpstackUplink { event }));
113 PamojaStatus::Ok
114 }
115 Err(error) => {
116 set_last_error(error.to_string());
117 PamojaStatus::Codec
118 }
119 }
120}
121
122#[no_mangle]
131pub unsafe extern "C" fn pamoja_chirpstack_uplink_free(uplink: *mut PamojaChirpstackUplink) {
132 if !uplink.is_null() {
133 drop(Box::from_raw(uplink));
134 }
135}
136
137#[no_mangle]
156pub unsafe extern "C" fn pamoja_chirpstack_uplink_summary(
157 uplink: *const PamojaChirpstackUplink,
158 out_summary: *mut PamojaChirpstackUplinkSummary,
159) -> PamojaStatus {
160 let (Some(uplink), false) = (uplink.as_ref(), out_summary.is_null()) else {
161 set_last_error("uplink and out_summary must not be null".to_owned());
162 return PamojaStatus::InvalidArgument;
163 };
164 let event = &uplink.event;
165 *out_summary = PamojaChirpstackUplinkSummary {
166 dev_addr: event.dev_addr.unwrap_or(0),
167 fcnt: event.fcnt,
168 frequency_hz: event.frequency_hz.unwrap_or(0),
169 reception_count: event.receptions.len() as u32,
170 dev_eui: event.dev_eui.bytes(),
171 has_dev_addr: u8::from(event.dev_addr.is_some()),
172 adr: u8::from(event.adr),
173 data_rate: event.data_rate,
174 has_fport: u8::from(event.fport.is_some()),
175 fport: event.fport.unwrap_or(0),
176 confirmed: u8::from(event.confirmed),
177 has_frequency: u8::from(event.frequency_hz.is_some()),
178 };
179 PamojaStatus::Ok
180}
181
182fn text_of(event: &UplinkEvent, field: u8) -> Option<Option<&str>> {
184 match field {
185 PAMOJA_CHIRPSTACK_DEDUPLICATION_ID => Some(Some(&event.deduplication_id)),
186 PAMOJA_CHIRPSTACK_TIME => Some(event.time.as_deref()),
187 PAMOJA_CHIRPSTACK_APPLICATION_ID => Some(Some(&event.application_id)),
188 PAMOJA_CHIRPSTACK_DEVICE_NAME => Some(Some(&event.device_name)),
189 _ => None,
190 }
191}
192
193pub const PAMOJA_CHIRPSTACK_DEDUPLICATION_ID: u8 = 0;
195pub const PAMOJA_CHIRPSTACK_TIME: u8 = 1;
197pub const PAMOJA_CHIRPSTACK_APPLICATION_ID: u8 = 2;
199pub const PAMOJA_CHIRPSTACK_DEVICE_NAME: u8 = 3;
201
202#[no_mangle]
219pub unsafe extern "C" fn pamoja_chirpstack_uplink_text(
220 uplink: *const PamojaChirpstackUplink,
221 field: u8,
222) -> *mut PamojaString {
223 let Some(uplink) = uplink.as_ref() else {
224 set_last_error("uplink must not be null".to_owned());
225 return ptr::null_mut();
226 };
227 match text_of(&uplink.event, field) {
228 Some(Some(text)) => PamojaString::into_raw(text.to_owned()),
229 Some(None) => ptr::null_mut(),
230 None => {
231 set_last_error(format!("{field} names no text field of an uplink event"));
232 ptr::null_mut()
233 }
234 }
235}
236
237#[no_mangle]
257pub unsafe extern "C" fn pamoja_chirpstack_uplink_data(
258 uplink: *const PamojaChirpstackUplink,
259 out_data: *mut *mut PamojaBuffer,
260) -> PamojaStatus {
261 let (Some(uplink), false) = (uplink.as_ref(), out_data.is_null()) else {
262 set_last_error("uplink and out_data must not be null".to_owned());
263 return PamojaStatus::InvalidArgument;
264 };
265 *out_data = PamojaBuffer::into_raw(uplink.event.data.clone());
266 PamojaStatus::Ok
267}
268
269#[no_mangle]
289pub unsafe extern "C" fn pamoja_chirpstack_uplink_reception(
290 uplink: *const PamojaChirpstackUplink,
291 index: u32,
292 out_reception: *mut PamojaChirpstackReception,
293) -> PamojaStatus {
294 let (Some(uplink), false) = (uplink.as_ref(), out_reception.is_null()) else {
295 set_last_error("uplink and out_reception must not be null".to_owned());
296 return PamojaStatus::InvalidArgument;
297 };
298 let Some(reception) = uplink.event.receptions.get(index as usize) else {
299 set_last_error(format!("the event has no reception {index}"));
300 return PamojaStatus::InvalidArgument;
301 };
302 *out_reception = PamojaChirpstackReception {
303 rssi_dbm: reception.rssi_dbm,
304 snr_db: reception.snr_db,
305 gateway: reception.gateway.bytes(),
306 };
307 PamojaStatus::Ok
308}
309
310#[no_mangle]
331pub unsafe extern "C" fn pamoja_chirpstack_uplink_best_reception(
332 uplink: *const PamojaChirpstackUplink,
333 out_index: *mut u32,
334) -> PamojaStatus {
335 let (Some(uplink), false) = (uplink.as_ref(), out_index.is_null()) else {
336 set_last_error("uplink and out_index must not be null".to_owned());
337 return PamojaStatus::InvalidArgument;
338 };
339 let event = &uplink.event;
340 let Some(best) = event.best_reception() else {
341 set_last_error("the event named no gateway".to_owned());
342 return PamojaStatus::InvalidArgument;
343 };
344 let index = event
345 .receptions
346 .iter()
347 .position(|reception| ptr::eq(reception, best))
348 .unwrap_or(0);
349 *out_index = index as u32;
350 PamojaStatus::Ok
351}
352
353#[no_mangle]
370pub unsafe extern "C" fn pamoja_chirpstack_uplink_topic(
371 application_id: *const c_char,
372) -> *mut PamojaString {
373 let Some(application_id) = read_str(application_id, "application_id") else {
374 return ptr::null_mut();
375 };
376 PamojaString::into_raw(uplink_topic(application_id))
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len, pamoja_string_free};
384
385 const TWO: &str = r#"{
386 "deduplicationId": "3ac7e3c4-4401-4b8d-9386-a5c902f9202d",
387 "deviceInfo": { "applicationId": "17c8", "deviceName": "Test device", "devEui": "0101010101010101" },
388 "devAddr": "00189440", "dr": 1, "fPort": 1, "fCnt": 7, "confirmed": true, "data": "qg==",
389 "rxInfo": [
390 { "gatewayId": "0202020202020202", "rssi": -110, "snr": -4.25 },
391 { "gatewayId": "0016c001f153a14c", "rssi": -36, "snr": 10.5 }
392 ],
393 "txInfo": { "frequency": 867100000 }
394 }"#;
395
396 #[test]
397 fn an_event_reads_back_across_the_boundary() {
398 unsafe {
399 let mut uplink = ptr::null_mut();
400 assert_eq!(
401 pamoja_chirpstack_uplink_parse(TWO.as_ptr(), TWO.len(), &mut uplink),
402 PamojaStatus::Ok
403 );
404 let mut summary = std::mem::zeroed::<PamojaChirpstackUplinkSummary>();
405 assert_eq!(
406 pamoja_chirpstack_uplink_summary(uplink, &mut summary),
407 PamojaStatus::Ok
408 );
409 assert_eq!(summary.dev_eui, [1; 8]);
410 assert_eq!((summary.has_dev_addr, summary.dev_addr), (1, 0x0018_9440));
411 assert_eq!((summary.fcnt, summary.confirmed, summary.adr), (7, 1, 0));
412 assert_eq!(
413 (summary.has_fport, summary.fport, summary.data_rate),
414 (1, 1, 1)
415 );
416 assert_eq!(
417 (summary.has_frequency, summary.frequency_hz),
418 (1, 867_100_000)
419 );
420 assert_eq!(summary.reception_count, 2);
421
422 let name = pamoja_chirpstack_uplink_text(uplink, PAMOJA_CHIRPSTACK_DEVICE_NAME);
423 assert!(!name.is_null());
424 pamoja_string_free(name);
425 assert!(pamoja_chirpstack_uplink_text(uplink, PAMOJA_CHIRPSTACK_TIME).is_null());
426 assert!(pamoja_chirpstack_uplink_text(uplink, 9).is_null());
427
428 let mut data = ptr::null_mut();
429 assert_eq!(
430 pamoja_chirpstack_uplink_data(uplink, &mut data),
431 PamojaStatus::Ok
432 );
433 assert_eq!(
434 std::slice::from_raw_parts(pamoja_buffer_data(data), pamoja_buffer_len(data)),
435 [0xAA]
436 );
437 pamoja_buffer_free(data);
438
439 let mut best = 9;
440 assert_eq!(
441 pamoja_chirpstack_uplink_best_reception(uplink, &mut best),
442 PamojaStatus::Ok
443 );
444 assert_eq!(best, 1);
445 let mut reception = std::mem::zeroed::<PamojaChirpstackReception>();
446 assert_eq!(
447 pamoja_chirpstack_uplink_reception(uplink, best, &mut reception),
448 PamojaStatus::Ok
449 );
450 assert_eq!((reception.rssi_dbm, reception.snr_db), (-36, 10.5));
451 assert_eq!(
452 reception.gateway,
453 [0x00, 0x16, 0xC0, 0x01, 0xF1, 0x53, 0xA1, 0x4C]
454 );
455 assert_eq!(
456 pamoja_chirpstack_uplink_reception(uplink, 2, &mut reception),
457 PamojaStatus::InvalidArgument
458 );
459 pamoja_chirpstack_uplink_free(uplink);
460
461 let broken = r#"{ "deviceInfo": {} }"#;
462 assert_eq!(
463 pamoja_chirpstack_uplink_parse(broken.as_ptr(), broken.len(), &mut uplink),
464 PamojaStatus::Codec
465 );
466 assert!(uplink.is_null());
467 }
468 }
469}