From 0db81c5de7dd0bdedf55e8283233f0b9fda8c507 Mon Sep 17 00:00:00 2001 From: Carl Seifert <115627588+carl-tud@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:07:51 +0200 Subject: [PATCH] examples/unicoap_message: add demo of message APIs --- .../networking/coap/unicoap_message/Makefile | 25 ++ .../coap/unicoap_message/Makefile.ci | 8 + .../networking/coap/unicoap_message/README.md | 14 + .../networking/coap/unicoap_message/main.c | 252 ++++++++++++++++++ 4 files changed, 299 insertions(+) create mode 100644 examples/networking/coap/unicoap_message/Makefile create mode 100644 examples/networking/coap/unicoap_message/Makefile.ci create mode 100644 examples/networking/coap/unicoap_message/README.md create mode 100644 examples/networking/coap/unicoap_message/main.c diff --git a/examples/networking/coap/unicoap_message/Makefile b/examples/networking/coap/unicoap_message/Makefile new file mode 100644 index 0000000000..87da8aef33 --- /dev/null +++ b/examples/networking/coap/unicoap_message/Makefile @@ -0,0 +1,25 @@ +# Default Makefile, for unicoap message APIs + +# name of your application +APPLICATION = unicoap_message + +# If no BOARD is found in the environment, use this default: +BOARD ?= native + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../../../.. + +USEMODULE += unicoap + +# This module is needed to get the RFC 7252 PDU parser +USEMODULE += unicoap_driver_rfc7252_pdu + +# Comment this out to disable code in RIOT that does safety checking +# which is not needed in a production environment but helps in the +# development process: +DEVELHELP ?= 1 + +# Change this to 0 show compiler invocation lines by default: +QUIET ?= 1 + +include $(RIOTBASE)/Makefile.include diff --git a/examples/networking/coap/unicoap_message/Makefile.ci b/examples/networking/coap/unicoap_message/Makefile.ci new file mode 100644 index 0000000000..3dfb5c722c --- /dev/null +++ b/examples/networking/coap/unicoap_message/Makefile.ci @@ -0,0 +1,8 @@ +BOARD_INSUFFICIENT_MEMORY := \ + arduino-duemilanove \ + arduino-nano \ + arduino-uno \ + atmega328p \ + atmega328p-xplained-mini \ + atmega8 \ + # diff --git a/examples/networking/coap/unicoap_message/README.md b/examples/networking/coap/unicoap_message/README.md new file mode 100644 index 0000000000..e77da7c533 --- /dev/null +++ b/examples/networking/coap/unicoap_message/README.md @@ -0,0 +1,14 @@ +# Demo of `unicoap` Message APIs + +This application provides an example of how to create messages and how to set and get option values. +The example also demonstrates how to parse and serialize CoAP messages using the RFC 7252 PDU format. + +This example corresponds to the [sample code _Using Message APIs_ on doc.riot-os.org](https://doc.riot-os.org/group__net__unicoap__message__example.html). + +## Running the Example + +To try this example on your host, run: +```sh +BOARD=native make flash term +``` +This will compile and run the application. diff --git a/examples/networking/coap/unicoap_message/main.c b/examples/networking/coap/unicoap_message/main.c new file mode 100644 index 0000000000..63f49d5b68 --- /dev/null +++ b/examples/networking/coap/unicoap_message/main.c @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +/** + * @file + * @ingroup examples + * @brief Demonstration of `unicoap` message, options, and parsing APIs + * @author Carl Seifert + */ +#include +#include +#include + +#include "assert.h" +#include "msg.h" +#include "modules.h" +#include "architecture.h" + +#include "net/unicoap.h" + +static void _example_handle_message(const unicoap_message_t* message) +{ + /* Use unicoap_message_is_request, unicoap_message_is_response, unicoap_message_is_signal + * to determine what class of message you are dealing with. + * In this case, we expect a request. */ + assert(unicoap_message_is_request(message->code)); + + /* The unicoap message type supports different typed views of the CoAP message code: + * message->method, message->status, and message->signal. + * + * Now, we want to get a string from the method: GET, PUT, POST, etc. + * unicoap_string_from_* methods generate a null-terminated constant C string + * from typed values. */ + const char* method_name = unicoap_string_from_method(message->method); + /* Also possible, more general: unicoap_string_from_code(uint8_t code). */ + + printf("PDU is a CoAP %s request" + " with payload=<%" PRIuSIZE " bytes>\n", + method_name, message->payload_size); + + /* Options ----------------- */ + + /* If you need to dump options quickly, use this function: */ + unicoap_options_dump_all(message->options); + + /* Content-Format is an option that can occur no more than once. */ + unicoap_content_format_t format = 0; + if (unicoap_options_get_content_format(message->options, &format) < 0) { + puts("Error: could not read Content-Format!"); + return; + } + + if (format != UNICOAP_FORMAT_JSON) { + puts("Error: was expecting JSON request!"); + return; + } + + /* Uri-Query can occur more than once. Hence, unicoap provides multiple convenience + * accessors. */ + const char* query = NULL; + + /* The first getter provides a view into the PDU buffer. The returned string + * is thus not null-terminated. */ + ssize_t res = unicoap_options_get_first_uri_query(message->options, &query); + if (res < 0) { + /* The getter also fails in cases where no option was found */ + if (res == -ENOENT) { + puts("Message has no Uri-Query option"); + } + printf("Error: could read first Uri-Query option"); + } + + printf("First URI query: '%.*s'\n", (int)res, query); + + /* You can also get a query by name: */ + res = unicoap_options_get_first_uri_query_by_name(message->options, "color", &query); + if (res < 0) { + /* The getter also fails in cases where no option was found */ + if (res == -ENOENT) { + puts("Message has no 'color' query"); + } + printf("Error: could read first 'color' query"); + } + + /* If you do want to access all values of a repeatable option, + * unicoap offers two ways: + * a, unicoap can generate a contiguous string from repeatable options: ?a=1&b=2&c=3 + * b, you iterate over all options using the unicoap option iterator */ + char query_string[50] = { 0 }; + res = unicoap_options_copy_uri_queries(message->options, query_string, sizeof(query_string)); + if (res < 0) { + puts("Error: could not generate URI query string"); + } + + /* - or - */ + + unicoap_options_iterator_t iterator; + unicoap_options_iterator_init(&iterator, message->options); + + while ((res = unicoap_options_get_next_uri_query(&iterator, &query)) >= 0) { + printf("- URI query: '%.*s'\n", (int)res, query); + } + + /* You can also use the option iterator to iterate over all options */ + unicoap_options_iterator_init(&iterator, message->options); + unicoap_option_number_t number; + const uint8_t* value = NULL; + + while ((res = unicoap_options_get_next(&iterator, &number, &value)) >= 0) { + const char* name = unicoap_string_from_option_number(number); + printf("- option %s nr=%i contains %" PRIuSIZE " bytes\n", name, number, res); + } +} + +static void _example_parse_pdu(void) +{ + ssize_t res = 0; + + /* + { + "type": "Confirmable", + "code": "POST", + "id": 65201, + "token": 0, + "options": [ + "Uri-Path: actuators", + "Uri-Path: leds", + "Content-Format: application/json", + "Uri-Query: color=g", + "Accept: application/json" + ] + } + */ + const uint8_t pdu[] = { + 0x40, 0x02, 0xfe, 0xb1, 0xb9, 0x61, 0x63, 0x74, 0x75, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x04, + 0x6c, 0x65, 0x64, 0x73, 0x11, 0x32, 0x37, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x67, 0x21, + 0x32, 0xff, 0x6d, 0x6f, 0x64, 0x65, 0x3d, 0x6f, 0x6e + }; + + /* Allocate a helper structure */ + unicoap_parser_result_t parsed = { 0 }; + + /* Parse using helper function, _result version initializes options structure for us. */ + if ((res = unicoap_pdu_parse_rfc7252_result((uint8_t*)pdu, sizeof(pdu), &parsed)) < 0) { + puts("Error: parsing failed"); + return; + } + + /* To read the header, use the message properties: */ + printf("CoAP message has token=<%i bytes>\n", parsed.properties.token_length); + + /* If the message was sent over CoAP over UDP or DTLS, it has the + * RFC 7252 CoAP header. This is how you access these properties: */ + printf("CoAP over UDP/DTLS has id=%i type=%s\n", + parsed.properties.rfc7252.id, + unicoap_string_from_rfc7252_type(parsed.properties.rfc7252.type)); + + _example_handle_message(&parsed.message); +} + +static void _example_serialize_message(const unicoap_message_t* message) +{ + /* Well, first we need a buffer. */ + uint8_t pdu[200]; + + /* The header format varies depending on the transport. Let's use + * CoAP over UDP or CoAP over DTLS, i.e., the RFC 7252 format. + * Also, let's not use a token (very constrained nodes may only handle a request at a time + * and thus use an empty token value). */ + unicoap_message_properties_t properties = { + .token = NULL, + .token_length = 0, + .rfc7252 = { + .id = 0xABCD, + .type = UNICOAP_TYPE_NON + } + }; + + ssize_t res = unicoap_pdu_build_rfc7252(pdu, sizeof(pdu), message, &properties); + if (res < 0) { + if (res == -ENOBUFS) { + puts("Error: PDU buffer too small"); + } + puts("Error: could not serialize message"); + return; + } + + printf("The final PDU has a size of %" PRIuSIZE " bytes.\n", res); +} + +static void _example_create_message(void) +{ + /* If you want options, you need to allocate an options object and provide + * a capacity for the options buffer. The object is stack-allocated. The + * macro just saves you the boilerplate of allocating a buffer and initializing + * the options struct. */ + UNICOAP_OPTIONS_ALLOC(options, 100); + + const char payload[] = "Dear thermostat, please adjust your target temperature to 22.5 °C"; + unicoap_message_t message; + unicoap_request_init_string_with_options(&message, UNICOAP_METHOD_POST, payload, &options); + + /* Options ----------------- */ + + /* To set the path, use the following method. There are two versions for null-terminated + * strings and for ones that aren't. */ + int res = unicoap_options_add_uri_path_string(&options, "/thermostat/temperature"); + if (res < 0) { + if (res == -ENOBUFS) { + puts("Error: options buffer too small"); + } + puts("Error: could not add URI path"); + } + + /* Repeatable options like Uri-Path can also be added individually. + * This results in /thermostat/temperature/target */ + res = unicoap_options_add_uri_path_component_string(&options, "target"); + if (res < 0) { + puts("Error: could not add path component"); + } + + /* Uri-Query is a repeatable option, and can thus also be added individually. */ + res = unicoap_options_add_uri_queries_string(&options, "unit=C&cool=yes"); + if (res < 0) { + puts("Error: could not add URI query"); + } + + /* Simple options like Content-Format can be set as follows: */ + res = unicoap_options_set_content_format(&options, UNICOAP_FORMAT_TEXT); + if (res < 0) { + puts("Error: could not set Content-Format"); + } + + /* Let's see if everything has been added: */ + unicoap_options_dump_all(&options); + + _example_serialize_message(&message); +} + +int main(void) +{ + puts("Parsing a sample message ============="); + _example_parse_pdu(); + puts("Creating a sample message ============"); + _example_create_message(); +}