Skip to content

Conversation

@carl-tud
Copy link
Contributor

@carl-tud carl-tud commented Jul 7, 2025

This PR is the second in a series to introduce unicoap, a unified and modular CoAP implementation for RIOT. An overview of all PRs related to unicoap is presented in #21389, including reasons why unicoap is needed and a performance analysis.

What does this PR include?

  • RFC 7252 messaging implementation
  • Implementation of the CoAP over UDP and CoAP over DTLS drivers, plus an additional zero-copy optimization for UDP
  • Basic server functionality, including XFA resource definitions and helpful debug logs
  • A sample server application
  • Structured documentation, including a tutorial from the first line of code to running the example and testing it using the included client script
  • Support for running unicoap on a thread of your choice, e.g., the main thread.

The new API is more flexible. CoAP endpoints are abstracted into a unicoap_endpoint_t structure and transport-specific settings are controlled by flags. For example, this is a simple resource that responds with "Hello, World!".

// Define request handler for /hello
static int handle_hello_request(unicoap_message_t* message, 
    const unicoap_aux_t* aux, unicoap_request_context_t* ctx, void* arg) {
    // The aux parameter provides access to auxiliary information, such as local and remote endpoints,
    // transport-specific data and internal properties such as the message token.

    // Retrieve remote (client) endpoint and log string description of protocol number.
    printf("/hello/world resource invoked over %s\n", unicoap_string_from_proto(aux->remote->proto));

    // Craft response using convenience initializer. Vectored payloads are supported, too.
    unicoap_response_init_string(message, UNICOAP_STATUS_CONTENT, "Hello, World!");

    // Send response.
    return unicoap_send_response(message, ctx);
}

// Statically define resource, but dynamic registrations are also possible.
UNICOAP_RESOURCE(helloworld) {
    // When you specify a path, you now need to specify each component separately.
    .path = UNICOAP_PATH("hello", "world"), // /hello/world,
    
    // Instruct unicoap to send confirmable messages when communicating over UDP or DTLS.
    // This flag abstracts the transport-specific message type.
    .flags = UNICOAP_RESOURCE_FLAG_RELIABLE,
    
    // Specify what methods to allow. In unicoap, there are no duplicate defines for method flags.
    .methods = UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_PUT),
    
    // Optionally, you can also restrict the resource to a set of transports.
    .protocols = UNICOAP_PROTOCOLS(UNICOAP_PROTO_DTLS, UNICOAP_PROTO_UDP),
    
    .handler = handle_hello_request,
    .handler_arg = NULL
};

More in the documentation (CI build sometimes not available, e.g., due to failing tests).

@github-actions github-actions bot added Area: network Area: Networking Area: doc Area: Documentation Area: tests Area: tests and testing framework Area: build system Area: Build system Area: CoAP Area: Constrained Application Protocol implementations Area: sys Area: System Area: examples Area: Example Applications Area: Kconfig Area: Kconfig integration labels Jul 7, 2025
@carl-tud carl-tud changed the title net/unicoap: Messaging and Minimal Server (pt 2) net/unicoap: Unified and Modular CoAP stack: Messaging and Minimal Server (pt 2) Jul 7, 2025
@crasbe crasbe added Type: new feature The issue requests / The PR implemements a new feature for RIOT CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR labels Jul 7, 2025
@riot-ci
Copy link

riot-ci commented Jul 7, 2025

Murdock results

✔️ PASSED

c307339 fixup: forgot to check in util_macros.h

Success Failures Total Runtime
10533 0 10534 12m:14s

Artifacts

@carl-tud carl-tud force-pushed the unicoap-02-server-minimal branch from da014c7 to 4d00949 Compare July 9, 2025 14:37
@github-actions github-actions bot removed the Area: tests Area: tests and testing framework label Jul 9, 2025
@carl-tud carl-tud marked this pull request as ready for review July 9, 2025 14:42
@carl-tud
Copy link
Contributor Author

Note that this change drastically lowers the complexity of our path matching functions (UNICOAP_PATH to /a/string/path and UNCOAP_PATH to Uri-Path options)

@carl-tud
Copy link
Contributor Author

Also, note that this change can potentially decrease ROM size given an application that defines a lot of deeply nested paths. Each path component is stored as a separate string, hence multiple paths sharing a bunch of components do not necessarily replicate the same information in memory (either through compiler optimisation or thru manually reusing the same string).

@carl-tud
Copy link
Contributor Author

@mguetschow Do you want to have a look before I rebase this onto the current master to fix DTLS (and thereby rewrite history (at least this PR's...))

@carl-tud carl-tud force-pushed the unicoap-02-server-minimal branch from 305f40f to 5c3c66f Compare December 20, 2025 17:01
@carl-tud
Copy link
Contributor Author

@carl-tud i need to fix this: unicoap holds stale dTLS session until they are explicitly closed by the peer

@LasseRosenow
Copy link
Member

I changed the way paths are defined in unicoap applications to something more semantically unique instead of parsing compile-time slash-separated path strings. Why parse paths at runtime when we can force developers to give us a parsed representation directly?

This is the way! Really nice change!

@carl-tud
Copy link
Contributor Author

carl-tud commented Dec 21, 2025

#21582 (comment)

@carl-tud i need to fix this: unicoap holds stale dTLS session until they are explicitly closed by the peer

@mguetschow Is there really a bug to fix here? Just tried examples/networking/coap/unicoap_server rebased on top of master's d51045bd9d..2bd24b6522 commits. Just works and the DTLS sessions is deconstructed once the server has sent its reponse.

Doesn't waiting for the client to close the session make sense given that the client may want to further observe the resource or continue to send requests to that particular host?

@carl-tud
Copy link
Contributor Author

@carl-tud can you work around resources and handle all requests in one function? I.e., does a listener’s matcher need to return a resource? Figure out if that can be NULL.

Copy link
Contributor

@mguetschow mguetschow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just gone through your pre-Christmas changes (Github UI shows me 22 commits).

Mostly nits as usual, some static tests failing - and a 👍 for the path spec!


# This is the full version of debug logging, including trace logs. When this is enabled,
# the previously mentioned API misuse, missing modules, and fix-its will also be diagnosed.
# So when you enable debug logging, you do not need to set CONFIG_UNICOAP_ASSIST.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be @ref CONFIG_UNICOAP_ASSIST, I guess

import aiocoap.resource as resource
from aiocoap.transports.tinydtls import DTLSClientConnection

logging.basicConfig(level=logging.DEBUG)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably want to move these down to fix static tests.

Comment on lines 331 to 337
error:
SERVER_DEBUG("failed to send response, trying to send 5.05 unreliably\n");

/* try to send 5.05, */
unicoap_response_init_empty(packet->message, UNICOAP_STATUS_INTERNAL_SERVER_ERROR);
unicoap_messaging_send(packet, _messaging_flags_resource(resource->flags) &
~UNICOAP_MESSAGING_FLAG_RELIABLE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should i not send a 500 instead (thereby leaving the option to send a 500 to the app)?

Were both supposed to be 500? If so, I don't understand your proposal.

LOG_PREFIX = Path(__file__).name

tell = lambda message: print(f"{LOG_PREFIX}: {message}")
complain = lambda message: print(f"{LOG_PREFIX}: error: {message}", file=sys.stderr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E731 (static tests) complain about your lambda :P

*/
typedef struct {
/**
* @brief Pointer to contiguously stored character pointers to null-terminated strings
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

..., terminated by NULL.


_TEST_ASSERT_FALSE(_MATCH_STRING(&r, "/"));
_TEST_ASSERT_FALSE(_MATCH_STRING(&r, "a"));
_TEST_ASSERT_TRUE(_MATCH_STRING(&r, "a"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, interesting, so we consider a the same as /a?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Background: Technically, UNICOAP_PATH doesn't necessarily carry the relative-to-root semantics anymore, I.e. there's nothing like a leading slash, or something like UNICOAP_PATH(ROOT, "a", "b"). Theoretically, we could employ UNICOAP_PATH for relative (albeit static) paths because it doesn't say "I'm a path starting at the root" anymore, though this is more of a subjective beauty argument.
I don't know what restricting a path object to be relative to the root would be useful for (e.g., by introducing a private path object property).
The only thing the path object is used for, and in the foreseeable future will be, is the resource definition. And in that context, the path is interpreted to be relative to the root. Consequently, I think it's fine to allow both "a" and "/a" to match UNICOAP_PATH("a"). Furthermore, this a resource matching function, so that context /interpretation is baked into the API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see and agree, I don't see a reason for adding that boolean either. Maybe this fact could be somehow reflected in the UNICOAP_PATH doc, though.

Comment on lines +122 to +132
/*
char test_buffer[20] = {};
ssize_t res = unicoap_path_serialize(&r.path, test_buffer, sizeof(test_buffer));
TEST_ASSERT_EQUAL_INT(res, static_strlen("/a123/a567"));
printf("'%.*s'\n", (int)res, test_buffer);
printf("'");
unicoap_path_print(&r.path);
printf("'\n");
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can probably be removed?

const unicoap_pathspec_t my_root = UNICOAP_PATH_ROOT;
TEST_ASSERT_EQUAL_INT(my_root._components, NULL);

const unicoap_pathspec_t my_root2 = UNICOAP_PATH(NULL);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason we allow both ways of representing the root path? Otherwise I'd drop this one and maybe have a static error when doing this? Could potentially simplify some code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static asserts won't work for that in a macro, but I'll come up with something hacky.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason we allow both ways of representing the root path? Otherwise I'd drop this one and maybe have a static error when doing this? Could potentially simplify some code?

You're right to point this out, the runtime checks should be asserts and that case generally be disallowed.


static void test_path_object_is_root(void) {
const unicoap_pathspec_t my_path1 = UNICOAP_PATH("a");
_TEST_ASSERT_FALSE(unicoap_path_is_root(&my_path1));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could have used unicoap_path_is_root in several places instead of explicitly checking for the null pointers, but, on the other hand, it's maybe good to be explicit.

_TEST_ASSERT_FALSE(path_is_equal(UNICOAP_PATH(NULL), UNICOAP_PATH("a")));
}

static void test_path_object_subtree_is_equal(void) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test does not check for subtree equality as the name implies, or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant was, "Given a path object that has a subtree beneath the root, does the equality check work?"

@mguetschow
Copy link
Contributor

#21582 (comment)

@carl-tud i need to fix this: unicoap holds stale dTLS session until they are explicitly closed by the peer

@mguetschow Is there really a bug to fix here? Just tried examples/networking/coap/unicoap_server rebased on top of master's d51045bd9d..2bd24b6522 commits. Just works and the DTLS sessions is deconstructed once the server has sent its reponse.

Doesn't waiting for the client to close the session make sense given that the client may want to further observe the resource or continue to send requests to that particular host?

Sounds sensible to me, indeed. Doesn't tinydtls have a (large) timeout on those anyways?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: build system Area: Build system Area: CoAP Area: Constrained Application Protocol implementations Area: doc Area: Documentation Area: examples Area: Example Applications Area: Kconfig Area: Kconfig integration Area: network Area: Networking Area: sys Area: System Area: tests Area: tests and testing framework Type: new feature The issue requests / The PR implemements a new feature for RIOT

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants