1use std::ffi::c_char;
15use std::ptr;
16use std::sync::Arc;
17use std::time::Duration;
18
19use pamoja_coap::{CoapConfig, CoapPublisher, CoapServer, CoapTransport, Reliability};
20use pamoja_core::{Receive, Transport};
21use tokio::sync::Mutex;
22
23use crate::transport::{receive_within, status, Kind, PamojaMessage, PamojaTransport};
24use crate::{read_bytes, read_str, runtime, set_last_error, PamojaStatus};
25
26#[repr(C)]
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum PamojaCoapReliability {
30 NonConfirmable = 0,
32 Confirmable = 1,
34}
35
36#[repr(C)]
38#[derive(Clone, Copy, Debug)]
39pub struct PamojaCoapConfig {
40 pub host: *const c_char,
42 pub port: u16,
44 pub bind: *const c_char,
46 pub reliability: PamojaCoapReliability,
48 pub ack_timeout_ms: u32,
51 pub max_retransmits: u32,
54}
55
56pub struct PamojaCoapClient {
58 inner: Arc<Mutex<CoapTransport>>,
59}
60
61#[no_mangle]
78pub unsafe extern "C" fn pamoja_coap_client_new(
79 config: *const PamojaCoapConfig,
80) -> *mut PamojaCoapClient {
81 let Some(settings) = coap_settings(config) else {
82 return ptr::null_mut();
83 };
84 Box::into_raw(Box::new(PamojaCoapClient {
85 inner: Arc::new(Mutex::new(CoapTransport::new(settings))),
86 }))
87}
88
89#[no_mangle]
103pub unsafe extern "C" fn pamoja_coap_client_connect(client: *mut PamojaCoapClient) -> PamojaStatus {
104 let Some(client) = client_handle(client) else {
105 return PamojaStatus::InvalidArgument;
106 };
107 let inner = Arc::clone(&client.inner);
108 status(runtime().block_on(async move { inner.lock().await.connect().await }))
109}
110
111#[no_mangle]
130pub unsafe extern "C" fn pamoja_coap_client_send(
131 client: *mut PamojaCoapClient,
132 topic: *const c_char,
133 payload: *const u8,
134 payload_len: usize,
135) -> PamojaStatus {
136 let Some(client) = client_handle(client) else {
137 return PamojaStatus::InvalidArgument;
138 };
139 let Some(topic) = read_str(topic, "topic") else {
140 return PamojaStatus::InvalidArgument;
141 };
142 let payload = match read_bytes(payload, payload_len) {
143 Ok(payload) => payload,
144 Err(status) => return status,
145 };
146 let inner = Arc::clone(&client.inner);
147 let topic = topic.to_owned();
148 status(runtime().block_on(async move { inner.lock().await.send(&topic, &payload).await }))
149}
150
151#[no_mangle]
168pub unsafe extern "C" fn pamoja_coap_client_subscribe(
169 client: *mut PamojaCoapClient,
170 topic: *const c_char,
171) -> PamojaStatus {
172 let Some(client) = client_handle(client) else {
173 return PamojaStatus::InvalidArgument;
174 };
175 let Some(topic) = read_str(topic, "topic") else {
176 return PamojaStatus::InvalidArgument;
177 };
178 let inner = Arc::clone(&client.inner);
179 let topic = topic.to_owned();
180 status(runtime().block_on(async move { inner.lock().await.subscribe(&topic).await }))
181}
182
183#[no_mangle]
200pub unsafe extern "C" fn pamoja_coap_client_recv(
201 client: *mut PamojaCoapClient,
202 out_message: *mut *mut PamojaMessage,
203) -> PamojaStatus {
204 let Some(client) = client_handle(client) else {
205 return PamojaStatus::InvalidArgument;
206 };
207 if out_message.is_null() {
208 set_last_error("out_message must not be null".to_owned());
209 return PamojaStatus::InvalidArgument;
210 }
211 *out_message = ptr::null_mut();
212
213 let inner = Arc::clone(&client.inner);
214 match runtime().block_on(async move { inner.lock().await.recv().await }) {
215 Ok(Some(message)) => {
216 *out_message = PamojaMessage::into_raw(message.topic, message.payload);
217 PamojaStatus::Ok
218 }
219 Ok(None) => PamojaStatus::Ok,
220 Err(error) => {
221 let code = PamojaStatus::from_error(&error);
222 set_last_error(error.to_string());
223 code
224 }
225 }
226}
227
228#[no_mangle]
251pub unsafe extern "C" fn pamoja_coap_client_recv_within(
252 client: *mut PamojaCoapClient,
253 timeout_ms: u64,
254 out_message: *mut *mut PamojaMessage,
255 out_timed_out: *mut bool,
256) -> PamojaStatus {
257 let Some(client) = client_handle(client) else {
258 return PamojaStatus::InvalidArgument;
259 };
260 let inner = Arc::clone(&client.inner);
261 receive_within(timeout_ms, out_message, out_timed_out, || async move {
262 inner.lock().await.recv().await
263 })
264}
265
266#[no_mangle]
280pub unsafe extern "C" fn pamoja_coap_client_is_connected(client: *mut PamojaCoapClient) -> bool {
281 let Some(client) = client_handle(client) else {
282 return false;
283 };
284 let inner = Arc::clone(&client.inner);
285 runtime().block_on(async move { inner.lock().await.is_connected() })
286}
287
288#[no_mangle]
302pub unsafe extern "C" fn pamoja_coap_client_disconnect(
303 client: *mut PamojaCoapClient,
304) -> PamojaStatus {
305 let Some(client) = client_handle(client) else {
306 return PamojaStatus::InvalidArgument;
307 };
308 let inner = Arc::clone(&client.inner);
309 status(runtime().block_on(async move { inner.lock().await.disconnect().await }))
310}
311
312#[no_mangle]
321pub unsafe extern "C" fn pamoja_coap_client_free(client: *mut PamojaCoapClient) {
322 if !client.is_null() {
323 drop(Box::from_raw(client));
324 }
325}
326
327pub struct PamojaCoapServer {
334 server: Arc<Mutex<CoapServer>>,
335 publisher: CoapPublisher,
336}
337
338#[no_mangle]
355pub unsafe extern "C" fn pamoja_coap_server_new(bind: *const c_char) -> *mut PamojaCoapServer {
356 let Some(bind) = read_str(bind, "bind") else {
357 return ptr::null_mut();
358 };
359 let server = CoapServer::new(bind);
360 let publisher = server.publisher();
361 Box::into_raw(Box::new(PamojaCoapServer {
362 server: Arc::new(Mutex::new(server)),
363 publisher,
364 }))
365}
366
367#[no_mangle]
382pub unsafe extern "C" fn pamoja_coap_server_connect(server: *mut PamojaCoapServer) -> PamojaStatus {
383 let Some(server) = server_handle(server) else {
384 return PamojaStatus::InvalidArgument;
385 };
386 let inner = Arc::clone(&server.server);
387 status(runtime().block_on(async move { inner.lock().await.connect().await }))
388}
389
390#[no_mangle]
409pub unsafe extern "C" fn pamoja_coap_server_subscribe(
410 server: *mut PamojaCoapServer,
411 filter: *const c_char,
412) -> PamojaStatus {
413 let Some(server) = server_handle(server) else {
414 return PamojaStatus::InvalidArgument;
415 };
416 let Some(filter) = read_str(filter, "filter") else {
417 return PamojaStatus::InvalidArgument;
418 };
419 let inner = Arc::clone(&server.server);
420 let filter = filter.to_owned();
421 status(runtime().block_on(async move { inner.lock().await.subscribe(&filter).await }))
422}
423
424#[no_mangle]
447pub unsafe extern "C" fn pamoja_coap_server_send(
448 server: *mut PamojaCoapServer,
449 path: *const c_char,
450 payload: *const u8,
451 payload_len: usize,
452) -> PamojaStatus {
453 let Some(server) = server_handle(server) else {
454 return PamojaStatus::InvalidArgument;
455 };
456 let Some(path) = read_str(path, "path") else {
457 return PamojaStatus::InvalidArgument;
458 };
459 let payload = match read_bytes(payload, payload_len) {
460 Ok(payload) => payload,
461 Err(status) => return status,
462 };
463 let publisher = server.publisher.clone();
464 let path = path.to_owned();
465 status(runtime().block_on(async move { publisher.publish(&path, &payload).await }))
466}
467
468#[no_mangle]
485pub unsafe extern "C" fn pamoja_coap_server_recv(
486 server: *mut PamojaCoapServer,
487 out_message: *mut *mut PamojaMessage,
488) -> PamojaStatus {
489 let Some(server) = server_handle(server) else {
490 return PamojaStatus::InvalidArgument;
491 };
492 if out_message.is_null() {
493 set_last_error("out_message must not be null".to_owned());
494 return PamojaStatus::InvalidArgument;
495 }
496 *out_message = ptr::null_mut();
497 let inner = Arc::clone(&server.server);
498 match runtime().block_on(async move { inner.lock().await.recv().await }) {
499 Ok(Some(message)) => {
500 *out_message = PamojaMessage::into_raw(message.topic, message.payload);
501 PamojaStatus::Ok
502 }
503 Ok(None) => PamojaStatus::Ok,
504 Err(error) => status(Err(error)),
505 }
506}
507
508#[no_mangle]
530pub unsafe extern "C" fn pamoja_coap_server_recv_within(
531 server: *mut PamojaCoapServer,
532 timeout_ms: u64,
533 out_message: *mut *mut PamojaMessage,
534 out_timed_out: *mut bool,
535) -> PamojaStatus {
536 let Some(server) = server_handle(server) else {
537 return PamojaStatus::InvalidArgument;
538 };
539 let inner = Arc::clone(&server.server);
540 receive_within(timeout_ms, out_message, out_timed_out, || async move {
541 inner.lock().await.recv().await
542 })
543}
544
545#[no_mangle]
561pub unsafe extern "C" fn pamoja_coap_server_observers(
562 server: *mut PamojaCoapServer,
563 path: *const c_char,
564) -> usize {
565 let Some(server) = server_handle(server) else {
566 return 0;
567 };
568 let Some(path) = read_str(path, "path") else {
569 return 0;
570 };
571 server.publisher.observers(path)
572}
573
574#[no_mangle]
589pub unsafe extern "C" fn pamoja_coap_server_local_port(server: *mut PamojaCoapServer) -> u16 {
590 let Some(server) = server_handle(server) else {
591 return 0;
592 };
593 server
594 .publisher
595 .local_addr()
596 .map_or(0, |address| address.port())
597}
598
599#[no_mangle]
613pub unsafe extern "C" fn pamoja_coap_server_is_connected(server: *mut PamojaCoapServer) -> bool {
614 let Some(server) = server_handle(server) else {
615 return false;
616 };
617 server.publisher.is_connected()
618}
619
620#[no_mangle]
634pub unsafe extern "C" fn pamoja_coap_server_disconnect(
635 server: *mut PamojaCoapServer,
636) -> PamojaStatus {
637 let Some(server) = server_handle(server) else {
638 return PamojaStatus::InvalidArgument;
639 };
640 let inner = Arc::clone(&server.server);
641 status(runtime().block_on(async move { inner.lock().await.disconnect().await }))
642}
643
644#[no_mangle]
653pub unsafe extern "C" fn pamoja_coap_server_free(server: *mut PamojaCoapServer) {
654 if !server.is_null() {
655 drop(Box::from_raw(server));
656 }
657}
658
659unsafe fn server_handle<'a>(server: *mut PamojaCoapServer) -> Option<&'a PamojaCoapServer> {
665 if server.is_null() {
666 set_last_error("server must not be null".to_owned());
667 return None;
668 }
669 Some(&*server)
670}
671
672#[no_mangle]
689pub unsafe extern "C" fn pamoja_transport_coap(
690 config: *const PamojaCoapConfig,
691) -> *mut PamojaTransport {
692 let Some(settings) = coap_settings(config) else {
693 return ptr::null_mut();
694 };
695 PamojaTransport::into_raw(Kind::Coap(CoapTransport::new(settings)))
696}
697
698unsafe fn coap_settings(config: *const PamojaCoapConfig) -> Option<CoapConfig> {
705 if config.is_null() {
706 set_last_error("config must not be null".to_owned());
707 return None;
708 }
709 let config = &*config;
710 let host = read_str(config.host, "host")?;
711
712 let mut settings = CoapConfig::new(host, config.port);
713 if !config.bind.is_null() {
714 settings = settings.bind(read_str(config.bind, "bind")?);
715 }
716 settings = settings.reliability(match config.reliability {
717 PamojaCoapReliability::NonConfirmable => Reliability::NonConfirmable,
718 PamojaCoapReliability::Confirmable => Reliability::Confirmable,
719 });
720 if config.ack_timeout_ms != 0 {
721 settings = settings.ack_timeout(Duration::from_millis(u64::from(config.ack_timeout_ms)));
722 }
723 if config.max_retransmits != 0 {
724 settings = settings.max_retransmits(config.max_retransmits);
725 }
726 Some(settings)
727}
728
729unsafe fn client_handle<'a>(client: *mut PamojaCoapClient) -> Option<&'a PamojaCoapClient> {
735 if client.is_null() {
736 set_last_error("client must not be null".to_owned());
737 return None;
738 }
739 Some(&*client)
740}
741
742#[cfg(test)]
743mod tests {
744 use std::ffi::CString;
745
746 use super::*;
747 use crate::transport::{
748 pamoja_message_free, pamoja_message_payload, pamoja_message_payload_len,
749 };
750
751 unsafe fn take(message: *mut PamojaMessage) -> Vec<u8> {
753 assert!(!message.is_null());
754 let bytes = std::slice::from_raw_parts(
755 pamoja_message_payload(message),
756 pamoja_message_payload_len(message),
757 )
758 .to_vec();
759 pamoja_message_free(message);
760 bytes
761 }
762
763 #[test]
764 fn a_server_takes_a_reading_and_notifies_an_observer() {
765 unsafe {
766 let bind = CString::new("127.0.0.1:0").expect("static");
767 let server = pamoja_coap_server_new(bind.as_ptr());
768 assert_eq!(pamoja_coap_server_connect(server), PamojaStatus::Ok);
769 let port = pamoja_coap_server_local_port(server);
770 assert_ne!(port, 0);
771 let filter = CString::new("sensors/#").expect("static");
772 assert_eq!(
773 pamoja_coap_server_subscribe(server, filter.as_ptr()),
774 PamojaStatus::Ok
775 );
776 let command = CString::new("commands/valve").expect("static");
777 assert_eq!(
778 pamoja_coap_server_send(server, command.as_ptr(), b"closed".as_ptr(), 6),
779 PamojaStatus::Ok
780 );
781
782 let host = CString::new("127.0.0.1").expect("static");
783 let config = PamojaCoapConfig {
784 host: host.as_ptr(),
785 port,
786 bind: ptr::null(),
787 reliability: PamojaCoapReliability::Confirmable,
788 ack_timeout_ms: 200,
789 max_retransmits: 1,
790 };
791 let client = pamoja_coap_client_new(&config);
792 assert_eq!(pamoja_coap_client_connect(client), PamojaStatus::Ok);
793 assert_eq!(
794 pamoja_coap_client_subscribe(client, command.as_ptr()),
795 PamojaStatus::Ok
796 );
797 assert_eq!(pamoja_coap_server_observers(server, command.as_ptr()), 1);
798
799 let mut message = ptr::null_mut();
800 let mut timed_out = false;
801 pamoja_coap_client_recv_within(client, 2000, &mut message, &mut timed_out);
802 assert!(!timed_out);
803 assert_eq!(take(message), b"closed");
804
805 let reading = CString::new("sensors/1/temperature").expect("static");
806 assert_eq!(
807 pamoja_coap_client_send(client, reading.as_ptr(), b"21.5".as_ptr(), 4),
808 PamojaStatus::Ok
809 );
810 pamoja_coap_server_recv_within(server, 2000, &mut message, &mut timed_out);
811 assert!(!timed_out);
812 assert_eq!(take(message), b"21.5");
813
814 let stray = CString::new("pumps/1").expect("static");
815 assert_eq!(
816 pamoja_coap_client_send(client, stray.as_ptr(), b"on".as_ptr(), 2),
817 PamojaStatus::Transport
818 );
819
820 pamoja_coap_client_free(client);
821 assert_eq!(pamoja_coap_server_disconnect(server), PamojaStatus::Ok);
822 assert!(!pamoja_coap_server_is_connected(server));
823 pamoja_coap_server_free(server);
824 }
825 }
826}