Skip to main content

pamoja_ffi/
coap.rs

1//! The C ABI for CoAP.
2//!
3//! These functions wrap [`pamoja_coap`] for callers that reach the SDK through
4//! the flat C boundary. CoAP is the transport for links where MQTT is more than
5//! the budget allows: it runs over UDP, its headers are a handful of bytes, and
6//! a node can fire a reading and forget it rather than holding a session open.
7//!
8//! A client holds a socket and a background loop, so it crosses as an opaque
9//! handle and every call blocks on the shared runtime. To use CoAP as one rung
10//! of a ladder rather than driving it directly, build a
11//! [`PamojaTransport`] with
12//! [`pamoja_transport_coap`] instead.
13
14use 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/// Whether a CoAP request is acknowledged and retried.
27#[repr(C)]
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum PamojaCoapReliability {
30    /// Fire and forget: the request is sent once and not acknowledged.
31    NonConfirmable = 0,
32    /// The request is acknowledged, and retransmitted until an ACK arrives.
33    Confirmable = 1,
34}
35
36/// The settings a CoAP endpoint is built from.
37#[repr(C)]
38#[derive(Clone, Copy, Debug)]
39pub struct PamojaCoapConfig {
40    /// The peer hostname or IP address, as null-terminated UTF-8.
41    pub host: *const c_char,
42    /// The peer UDP port, conventionally 5683 for plaintext CoAP.
43    pub port: u16,
44    /// The local address to bind, or null for the default.
45    pub bind: *const c_char,
46    /// Whether requests are acknowledged and retried.
47    pub reliability: PamojaCoapReliability,
48    /// How long to wait for an acknowledgment, in milliseconds, or 0 for the
49    /// default.
50    pub ack_timeout_ms: u32,
51    /// How many times to retransmit an unacknowledged request, or 0 for the
52    /// default.
53    pub max_retransmits: u32,
54}
55
56/// An opaque handle to a CoAP endpoint.
57pub struct PamojaCoapClient {
58    inner: Arc<Mutex<CoapTransport>>,
59}
60
61/// Creates a disconnected CoAP endpoint from the given settings.
62///
63/// # Arguments
64///
65/// * `config` - the endpoint settings.
66///
67/// # Returns
68///
69/// A handle the caller must release with [`pamoja_coap_client_free`], or null on
70/// failure with the reason available from
71/// [`pamoja_last_error_message`](crate::pamoja_last_error_message).
72///
73/// # Safety
74///
75/// `config` must point to a valid [`PamojaCoapConfig`] whose strings are valid
76/// null-terminated UTF-8 for the duration of the call.
77#[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/// Binds the local socket so the endpoint can carry traffic.
90///
91/// # Arguments
92///
93/// * `client` - the endpoint.
94///
95/// # Returns
96///
97/// [`PamojaStatus::Ok`] once bound.
98///
99/// # Safety
100///
101/// `client` must be a live handle from [`pamoja_coap_client_new`].
102#[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/// Sends a payload to a resource path.
112///
113/// # Arguments
114///
115/// * `client` - the endpoint.
116/// * `topic` - the resource path, as null-terminated UTF-8.
117/// * `payload` - the bytes to send.
118/// * `payload_len` - the length of `payload`.
119///
120/// # Returns
121///
122/// [`PamojaStatus::Ok`] once the request has gone out.
123///
124/// # Safety
125///
126/// `client` must be a live handle, `topic` a valid null-terminated UTF-8
127/// string, and `payload` must point to at least `payload_len` readable bytes or
128/// be null when that length is 0.
129#[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/// Observes a resource path, so messages published to it arrive at
152/// [`pamoja_coap_client_recv`].
153///
154/// # Arguments
155///
156/// * `client` - the endpoint.
157/// * `topic` - the resource path, as null-terminated UTF-8.
158///
159/// # Returns
160///
161/// [`PamojaStatus::Ok`] once observing.
162///
163/// # Safety
164///
165/// `client` must be a live handle and `topic` a valid null-terminated UTF-8
166/// string.
167#[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/// Waits for the next message on an observed path.
184///
185/// # Arguments
186///
187/// * `client` - the endpoint.
188/// * `out_message` - receives a message handle, or null when the endpoint is
189///   closed.
190///
191/// # Returns
192///
193/// [`PamojaStatus::Ok`] on success. A null `out_message` with an `Ok` status
194/// means the endpoint closed rather than that anything failed.
195///
196/// # Safety
197///
198/// `client` must be a live handle and `out_message` must be writable.
199#[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/// Waits a limited time for the next message on an observed path.
229///
230/// Running out of time loses nothing: a message that arrives afterwards waits for
231/// the next receive.
232///
233/// # Arguments
234///
235/// * `client` - the endpoint.
236/// * `timeout_ms` - how long to wait, in milliseconds.
237/// * `out_message` - receives a message handle, or null when the time ran out or
238///   the endpoint is closed.
239/// * `out_timed_out` - receives whether the time ran out before a message arrived.
240///
241/// # Returns
242///
243/// [`PamojaStatus::Ok`] with a message, with the time run out, or with null once
244/// the endpoint has closed.
245///
246/// # Safety
247///
248/// `client` must be a live handle, and `out_message` and `out_timed_out` must be
249/// writable.
250#[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/// Reports whether the endpoint is bound.
267///
268/// # Arguments
269///
270/// * `client` - the endpoint.
271///
272/// # Returns
273///
274/// `true` when bound, or `false` if `client` is null.
275///
276/// # Safety
277///
278/// `client` must be a live handle from [`pamoja_coap_client_new`], or null.
279#[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/// Releases the socket the endpoint holds.
289///
290/// # Arguments
291///
292/// * `client` - the endpoint.
293///
294/// # Returns
295///
296/// [`PamojaStatus::Ok`] once closed.
297///
298/// # Safety
299///
300/// `client` must be a live handle from [`pamoja_coap_client_new`].
301#[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/// Releases a CoAP endpoint handle.
313///
314/// Passing null is a no-op.
315///
316/// # Safety
317///
318/// `client` must be a handle from [`pamoja_coap_client_new`] that has not
319/// already been freed, or null. After this call it must not be used again.
320#[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
327/// An opaque handle to a CoAP server.
328///
329/// Connecting, subscribing, receiving, and disconnecting take the server one call
330/// at a time, and a receive holds it while it waits. Sending and counting observers
331/// go through the server's publisher instead, so a command goes out while another
332/// thread waits for a reading.
333pub struct PamojaCoapServer {
334    server: Arc<Mutex<CoapServer>>,
335    publisher: CoapPublisher,
336}
337
338/// Creates a CoAP server that will listen on a local address.
339///
340/// # Arguments
341///
342/// * `bind` - the local `host:port`, as null-terminated UTF-8, such as
343///   `0.0.0.0:5683`, or port 0 for a free one.
344///
345/// # Returns
346///
347/// A handle the caller must release with [`pamoja_coap_server_free`], or null if
348/// `bind` is null or not UTF-8.
349///
350/// # Safety
351///
352/// `bind` must be a valid null-terminated UTF-8 string for the duration of the
353/// call, or null.
354#[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/// Binds the server's socket and starts answering requests.
368///
369/// # Arguments
370///
371/// * `server` - the server.
372///
373/// # Returns
374///
375/// [`PamojaStatus::Ok`] once bound, or [`PamojaStatus::Transport`] if the address
376/// cannot be bound.
377///
378/// # Safety
379///
380/// `server` must be a live handle from [`pamoja_coap_server_new`].
381#[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/// Takes the readings sent to the paths a filter matches.
391///
392/// A PUT or POST to any other path is answered 4.04 Not Found.
393///
394/// # Arguments
395///
396/// * `server` - the server.
397/// * `filter` - the filter, as null-terminated UTF-8, with `+` for one level and
398///   `#` for the rest.
399///
400/// # Returns
401///
402/// [`PamojaStatus::Ok`] once the filter is in place.
403///
404/// # Safety
405///
406/// `server` must be a live handle and `filter` a valid null-terminated UTF-8
407/// string.
408#[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/// Sets the state of a resource and notifies every observer of it.
425///
426/// This goes through the server's publisher, so it does not wait for a receive
427/// running on another thread.
428///
429/// # Arguments
430///
431/// * `server` - the server.
432/// * `path` - the resource's path, as null-terminated UTF-8.
433/// * `payload` - its new state.
434/// * `payload_len` - the length of `payload`.
435///
436/// # Returns
437///
438/// [`PamojaStatus::Ok`] once each notification has left, or
439/// [`PamojaStatus::Closed`] if the server is not connected.
440///
441/// # Safety
442///
443/// `server` must be a live handle, `path` a valid null-terminated UTF-8 string, and
444/// `payload` must point to at least `payload_len` readable bytes or be null when
445/// that length is 0.
446#[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/// Waits for the next reading sent to a path the server takes.
469///
470/// # Arguments
471///
472/// * `server` - the server.
473/// * `out_message` - receives a message handle to release with
474///   [`pamoja_message_free`](crate::transport::pamoja_message_free).
475///
476/// # Returns
477///
478/// [`PamojaStatus::Ok`] with a message, or [`PamojaStatus::Closed`] if the server is
479/// not connected.
480///
481/// # Safety
482///
483/// `server` must be a live handle and `out_message` must be writable.
484#[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/// Waits a limited time for the next reading sent to a path the server takes.
509///
510/// Running out of time loses nothing: a reading that arrives afterwards waits for
511/// the next receive.
512///
513/// # Arguments
514///
515/// * `server` - the server.
516/// * `timeout_ms` - how long to wait, in milliseconds.
517/// * `out_message` - receives a message handle, or null when the time ran out.
518/// * `out_timed_out` - receives whether the time ran out before a reading arrived.
519///
520/// # Returns
521///
522/// [`PamojaStatus::Ok`] with a message or with the time run out, or
523/// [`PamojaStatus::Closed`] if the server is not connected.
524///
525/// # Safety
526///
527/// `server` must be a live handle, and `out_message` and `out_timed_out` must be
528/// writable.
529#[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/// Counts the clients observing a resource.
546///
547/// # Arguments
548///
549/// * `server` - the server.
550/// * `path` - the resource's path, as null-terminated UTF-8.
551///
552/// # Returns
553///
554/// How many observers the resource has, or 0 if `server` or `path` is null.
555///
556/// # Safety
557///
558/// `server` must be a live handle and `path` a valid null-terminated UTF-8 string,
559/// or either may be null.
560#[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/// The port the server listens on, which names the one the system chose when it
575/// bound port 0.
576///
577/// # Arguments
578///
579/// * `server` - the server.
580///
581/// # Returns
582///
583/// The port, or 0 while the server is not connected or if `server` is null.
584///
585/// # Safety
586///
587/// `server` must be a live handle from [`pamoja_coap_server_new`], or null.
588#[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/// Reports whether the server holds a bound socket.
600///
601/// # Arguments
602///
603/// * `server` - the server.
604///
605/// # Returns
606///
607/// `true` while connected, or `false` if `server` is null.
608///
609/// # Safety
610///
611/// `server` must be a live handle from [`pamoja_coap_server_new`], or null.
612#[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/// Closes the server's socket, keeping its resources and filters.
621///
622/// # Arguments
623///
624/// * `server` - the server.
625///
626/// # Returns
627///
628/// [`PamojaStatus::Ok`] once closed.
629///
630/// # Safety
631///
632/// `server` must be a live handle from [`pamoja_coap_server_new`].
633#[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/// Releases a CoAP server handle.
645///
646/// Passing null is a no-op.
647///
648/// # Safety
649///
650/// `server` must be a handle from [`pamoja_coap_server_new`] that has not already
651/// been freed, or null. After this call it must not be used again.
652#[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
659/// Borrows a server handle, rejecting a null pointer.
660///
661/// # Safety
662///
663/// `server` must be a live handle from [`pamoja_coap_server_new`], or null.
664unsafe 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/// Creates a CoAP transport for composing into a ladder or a wrapper.
673///
674/// # Arguments
675///
676/// * `config` - the endpoint settings.
677///
678/// # Returns
679///
680/// A handle the caller must release with
681/// [`pamoja_transport_free`](crate::transport::pamoja_transport_free) or hand to
682/// a call that consumes it, or null on failure.
683///
684/// # Safety
685///
686/// `config` must point to a valid [`PamojaCoapConfig`] whose strings are valid
687/// null-terminated UTF-8 for the duration of the call.
688#[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
698/// Reads the endpoint settings a config describes.
699///
700/// # Safety
701///
702/// `config` must point to a valid [`PamojaCoapConfig`] whose strings are valid
703/// null-terminated UTF-8 for the duration of the call, or be null.
704unsafe 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
729/// Borrows a client handle, rejecting a null pointer.
730///
731/// # Safety
732///
733/// `client` must be a live handle from [`pamoja_coap_client_new`], or null.
734unsafe 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    /// Reads a message handle's payload and releases it.
752    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}