coap-zero 0.3.0

CoAP protocol implementation for no_std without alloc
Documentation
// Copyright Open Logistics Foundation
//
// Licensed under the Open Logistics Foundation License 1.3.
// For details on the licensing terms, see the LICENSE file.
// SPDX-License-Identifier: OLFL-1.3

#![warn(missing_docs)]
#![cfg_attr(not(test), no_std)]

//! This crate provides a heapless `no_std` implementation for the CoAP protocol.
//!
//! It aims to be zero-copy as much as feasible without sacrificing usability too much. The
//! implemented CoAP endpoint implements automatic retransmissions for reliable message
//! transmission (for confirmable messages) and message de-duplication. The endpoint automatically
//! responds to specific messages, e.g. it sends out reset messages when appropriate or retransmits
//! the last response if a duplicated request is detected.
//!
//! The implementation is fixed to NSTART=1, i.e. only a single incoming request and a single
//! outgoing request may be processed at the same time.
//!
//! Currently, only the base CoAP specification is implemented. In the future, it is planned to
//! also support CoAP Observe which is required for LwM2M, the PATCH, FETCH and iPATCH methods
//! which significantly improve LwM2M capabilities and Block-Wise transfers which are required for
//! firmware upgrades in LwM2M.
//!
//! # Resources
//!
//! - [RFC 7252 - The Constrained Application Protocol (CoAP)](https://www.rfc-editor.org/rfc/rfc7252)
//! - [RFC 7641 - Observing Resources in the Constrained Application Protocol (CoAP)](https://www.rfc-editor.org/rfc/rfc7641)
//! - [RFC 7959 - Block-Wise Transfers in the Constrained Application Protocol (CoAP)](https://www.rfc-editor.org/rfc/rfc7959)
//! - [RFC 8132 - PATCH and FETCH Methods for the Constrained Application Protocol (CoAP)](https://www.rfc-editor.org/rfc/rfc8132.html)
//!
//! # Usage
//!
//! The main structure is the `coap_zero::endpoint::CoapEndpoint` type. It manages the connection
//! to another coap endpoint via an `embedded_nal::UdpClientStack` (not owned).
//! It contains two separate types, `OutgoingCommunication` and `IncomingCommunication`, to handle
//! the two communication paths separately.
//!
//! All operations are driven by internal state machines. Therefore, all calls are non-blocking
//! and the user has to call the `CoapEndpoint::process` method repeatedly in order to
//! send/receive CoAP messages. Whenever one of the state machines makes progress, a corresponding
//! event is returned. Some events _must_ be handled by the user in order to not get stuck in a
//! specific wait state. For example, an incoming request _must_ be responded to by the user
//! although this decision may be postponed or may involve sending a reset message.
//!
//! If only message parsing is required, the message submodule can be used.
//!
//! # Usage Example
//!
//! For more detailed examples, refer to the examples in the examples folder.
//!
//! ```
//! # use coap_zero::endpoint::incoming::IncomingEvent;
//! # use coap_zero::endpoint::outgoing::OutgoingEvent;
//! # use coap_zero::endpoint::{CoapEndpoint, EndpointEvent, TransmissionParameters};
//! # use coap_zero::message::codes::RequestCode;
//! # use core::time::Duration;
//! # use embedded_nal::SocketAddr;
//! # use embedded_timers::clock::{Clock, ClockError, Instant};
//! # use heapless::Vec;
//! # #[derive(Debug)]
//! # struct Rng;
//! # impl embedded_hal::blocking::rng::Read for Rng {
//! #     type Error = std::io::Error;
//! #     fn read(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
//! #         buf[0] = 42;
//! #         Ok(())
//! #     }
//! # }
//! # #[derive(Debug)]
//! # struct StackError;
//! # struct Socket;
//! # struct Stack;
//! # impl embedded_nal::UdpClientStack for Stack {
//! #     type UdpSocket = Socket;
//! #     type Error = StackError;
//! #     fn socket(&mut self) -> Result<Socket, StackError> {
//! #         Ok(Socket)
//! #     }
//! #     fn connect(
//! #         &mut self,
//! #         _socket: &mut Socket,
//! #         _remote: SocketAddr,
//! #     ) -> Result<(), StackError> {
//! #         Ok(())
//! #     }
//! #     fn send(
//! #         &mut self,
//! #         _socket: &mut Socket,
//! #         _buffer: &[u8],
//! #     ) -> Result<(), nb::Error<StackError>> {
//! #         Ok(())
//! #     }
//! #     fn receive(
//! #         &mut self,
//! #         _socket: &mut Socket,
//! #         _buffer: &mut [u8],
//! #     ) -> Result<(usize, SocketAddr), nb::Error<StackError>> {
//! #         Err(nb::Error::WouldBlock)
//! #     }
//! #     fn close(&mut self, _socket: Socket) -> Result<(), StackError> {
//! #         Ok(())
//! #     }
//! # }
//! # impl embedded_nal::Dns for Stack {
//! #     type Error = StackError;
//! #     fn get_host_by_name(
//! #         &mut self,
//! #         _hostname: &str,
//! #         _addr_type: embedded_nal::AddrType,
//! #     ) -> nb::Result<embedded_nal::IpAddr, Self::Error> {
//! #         use core::str::FromStr;
//! #         Ok(embedded_nal::IpAddr::from_str("1.1.1.1").unwrap())
//! #     }
//! #     fn get_host_by_address(
//! #         &mut self,
//! #         _addr: embedded_nal::IpAddr,
//! #     ) -> nb::Result<heapless::String<256>, Self::Error> {
//! #         Ok(heapless::String::from("rust-lang.org"))
//! #     }
//! # }
//! # #[derive(Debug)]
//! # struct SystemClock;
//! # impl Clock for SystemClock {
//! #     fn try_now(&self) -> Result<Instant, ClockError> {
//! #         Ok(Instant::new(0, 0))
//! #     }
//! # }
//! static CLOCK: SystemClock = SystemClock;
//!
//! let mut stack = Stack;
//! let mut receive_buffer = [0_u8; coap_zero::DEFAULT_COAP_MESSAGE_SIZE];
//!
//! let mut endpoint: CoapEndpoint<'_, Stack, Rng, SystemClock> = CoapEndpoint::try_new(
//!     TransmissionParameters::default(),
//!     Rng,
//!     &CLOCK,
//!     &mut receive_buffer,
//! )
//! .unwrap();
//!
//! endpoint
//!     .connect_to_url(&mut stack, "coap://coap.me:5683")
//!     .unwrap();
//!
//! let outgoing = endpoint.outgoing();
//! outgoing
//!     .schedule_con(
//!         RequestCode::Get,
//!         outgoing
//!             .parse_options("coap://coap.me:5683/hello", Vec::new())
//!             .unwrap(),
//!         None,
//!         Duration::from_secs(5),
//!     )
//!     .unwrap();
//!
//! loop {
//!     let (incoming_event, outgoing_event, endpoint_event) =
//!         endpoint.process(&mut stack).unwrap();
//!
//!     match incoming_event.unwrap() {
//!         IncomingEvent::Nothing => {
//!             // Whenever nothing else happens, the Nothing event is generated. Ignore it
//!             // silently.
//!         }
//!         IncomingEvent::Request(_confirmable, _message) => {
//!             // Handle request
//!         }
//!         event => println!("Other incoming event: {event:?}"),
//!     }
//!
//!     match outgoing_event.unwrap() {
//!         OutgoingEvent::Nothing => {}
//!         OutgoingEvent::Success(response) => println!("Request succeeded: {response:?}"),
//!         OutgoingEvent::Timeout
//!         | OutgoingEvent::PiggybackedWrongToken
//!         | OutgoingEvent::ResetReceived => {
//!             println!("Request failed");
//!         }
//!         event => println!("Other outgoing event: {event:?}"),
//!     }
//!
//!     match endpoint_event {
//!         EndpointEvent::Nothing => {}
//!         EndpointEvent::MsgFormatErr(_err) => {
//!             // A message format error was detected. This is reported as an event instead of
//!             // an error because it is caused by the other endpoint.
//!             println!("endpoint event MsgFormatErr");
//!         }
//!         EndpointEvent::Ping => {
//!             println!("A ping has been received and a response has been sent");
//!         }
//!         EndpointEvent::Unhandled(message) => {
//!             println!("Unhandled message received: {message:?}");
//!         }
//!     }
//!
//! #   break; // Finish test
//! }
//! ```
//!
//! # Examples
//!
//! Multiple examples are provided. They use [coap.me](https://coap.me/) as coap test server. The lwm2m example shows the usage in the lightweight m2m context using the [Leshan](https://leshan.eclipseprojects.io) public test server.
//!
//! - ping - Sends a ping and terminates when the expected reset was received
//! - simple_get - Sends a CON Get and receives a piggy-backed response
//! - get_separate_response - Sends a CON Get and received a separate response
//! - lwm2m - Registers to the leshan server and supplies the time object current time resource. The registration won't update and will be silently closed after 120 seconds.
//!
//! # License
//!
//! Open Logistics Foundation License\
//! Version 1.3, January 2023
//!
//! See the LICENSE file in the top-level directory.
//!
//!
//! # Contact
//!
//! Fraunhofer IML Embedded Rust Group - <embedded-rust@iml.fraunhofer.de>

pub mod endpoint;
pub mod message;

/// Default for how many options can be send in a CoAP Message
pub const DEFAULT_MAX_OPTION_COUNT: usize = 8;

/// Default for maximum value size \[Bytes\] of CoAP Options
pub const DEFAULT_MAX_OPTION_SIZE: usize = 32;

/// Default buffer size \[Bytes\] for a CoAP Message according to <https://www.rfc-editor.org/rfc/rfc7252#section-4.6>
pub const DEFAULT_COAP_MESSAGE_SIZE: usize = 1152;

#[cfg(test)]
mod doctests {

    // Required to write `use coap_zero::...` in the test below
    use crate as coap_zero;

    /// After this test has been fixed, copy it to the main doctest at the top of this file,
    /// comment all the clutter with `#` and run `cargo readme > README.md` to regenerate the README
    #[test]
    fn readme_test() {
        // The code is squeezed here because empty `#` lines show up in the generated README.md
        // The upper part of the code is intended to be prefixed with `//! # `, the lower part
        // should only be prefixed with `//!`.

        use coap_zero::endpoint::incoming::IncomingEvent;
        use coap_zero::endpoint::outgoing::OutgoingEvent;
        use coap_zero::endpoint::{CoapEndpoint, EndpointEvent, TransmissionParameters};
        use coap_zero::message::codes::RequestCode;
        use core::time::Duration;
        use embedded_nal::SocketAddr;
        use embedded_timers::clock::{Clock, ClockError, Instant};
        use heapless::Vec;
        #[derive(Debug)]
        struct Rng;
        impl embedded_hal::blocking::rng::Read for Rng {
            type Error = std::io::Error;
            fn read(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
                buf[0] = 42;
                Ok(())
            }
        }
        #[derive(Debug)]
        struct StackError;
        struct Socket;
        struct Stack;
        impl embedded_nal::UdpClientStack for Stack {
            type UdpSocket = Socket;
            type Error = StackError;
            fn socket(&mut self) -> Result<Socket, StackError> {
                Ok(Socket)
            }
            fn connect(
                &mut self,
                _socket: &mut Socket,
                _remote: SocketAddr,
            ) -> Result<(), StackError> {
                Ok(())
            }
            fn send(
                &mut self,
                _socket: &mut Socket,
                _buffer: &[u8],
            ) -> Result<(), nb::Error<StackError>> {
                Ok(())
            }
            fn receive(
                &mut self,
                _socket: &mut Socket,
                _buffer: &mut [u8],
            ) -> Result<(usize, SocketAddr), nb::Error<StackError>> {
                Err(nb::Error::WouldBlock)
            }
            fn close(&mut self, _socket: Socket) -> Result<(), StackError> {
                Ok(())
            }
        }
        impl embedded_nal::Dns for Stack {
            type Error = StackError;
            fn get_host_by_name(
                &mut self,
                _hostname: &str,
                _addr_type: embedded_nal::AddrType,
            ) -> nb::Result<embedded_nal::IpAddr, Self::Error> {
                use core::str::FromStr;
                Ok(embedded_nal::IpAddr::from_str("1.1.1.1").unwrap())
            }
            fn get_host_by_address(
                &mut self,
                _addr: embedded_nal::IpAddr,
            ) -> nb::Result<heapless::String<256>, Self::Error> {
                Ok(heapless::String::from("rust-lang.org"))
            }
        }
        #[derive(Debug)]
        struct SystemClock;
        impl Clock for SystemClock {
            fn try_now(&self) -> Result<Instant, ClockError> {
                Ok(Instant::new(0, 0))
            }
        }

        static CLOCK: SystemClock = SystemClock;

        let mut stack = Stack;
        let mut receive_buffer = [0_u8; coap_zero::DEFAULT_COAP_MESSAGE_SIZE];

        let mut endpoint: CoapEndpoint<'_, Stack, Rng, SystemClock> = CoapEndpoint::try_new(
            TransmissionParameters::default(),
            Rng,
            &CLOCK,
            &mut receive_buffer,
        )
        .unwrap();

        endpoint
            .connect_to_url(&mut stack, "coap://coap.me:5683")
            .unwrap();

        let outgoing = endpoint.outgoing();
        outgoing
            .schedule_con(
                RequestCode::Get,
                outgoing
                    .parse_options("coap://coap.me:5683/hello", Vec::new())
                    .unwrap(),
                None,
                Duration::from_secs(5),
            )
            .unwrap();

        loop {
            let (incoming_event, outgoing_event, endpoint_event) =
                endpoint.process(&mut stack).unwrap();

            match incoming_event.unwrap() {
                IncomingEvent::Nothing => {
                    // Whenever nothing else happens, the Nothing event is generated. Ignore it
                    // silently.
                }
                IncomingEvent::Request(_confirmable, _message) => {
                    // Handle request
                }
                event => println!("Other incoming event: {event:?}"),
            }

            match outgoing_event.unwrap() {
                OutgoingEvent::Nothing => {}
                OutgoingEvent::Success(response) => println!("Request succeeded: {response:?}"),
                OutgoingEvent::Timeout
                | OutgoingEvent::PiggybackedWrongToken
                | OutgoingEvent::ResetReceived => {
                    println!("Request failed");
                }
                event => println!("Other outgoing event: {event:?}"),
            }

            match endpoint_event {
                EndpointEvent::Nothing => {}
                EndpointEvent::MsgFormatErr(_err) => {
                    // A message format error was detected. This is reported as an event instead of
                    // an error because it is caused by the other endpoint.
                    println!("endpoint event MsgFormatErr");
                }
                EndpointEvent::Ping => {
                    println!("A ping has been received and a response has been sent");
                }
                EndpointEvent::Unhandled(message) => {
                    println!("Unhandled message received: {message:?}");
                }
            }

            break; // Finish test
        }
    }
}