Loading...
Searching...
No Matches
Writing a Client Application

Learn to write a simple client application with unicoap

Learn to write a simple client application with unicoap

Sample code. Find the sample code in examples/networking/coap/unicoap_client.

In this tutorial, you will learn how you can create a simple CoAP client with unicoap. We will support CoAP over UDP and DTLS and try out the application using RIOT's native board. Hence, you will need a Linux host and support for tuntap (tap interfaces).

Getting Started

To start, we will create a Makefile and add unicoap as a dependency. Because we want to support UDP and DTLS, we need to add the respective drivers in the Makefile. We will build a small shell application that accepts a URI and a request payload.

USEMODULE += unicoap
USEMODULE += unicoap_client
USEMODULE += unicoap_driver_udp
USEMODULE += unicoap_driver_dtls

Because RIOT allows you to switch the network backend, we need to specify one. In this tutorial, we choose Generic (GNRC) network stack.

# Include packages that pull up and auto-init the link layer.
# NOTE: 6LoWPAN will be included if IEEE802.15.4 devices are present
USEMODULE += netdev_default
# Automatically initialize GNRC upon startup
USEMODULE += auto_init_gnrc_netif
# Specify the mandatory networking modules
USEMODULE += gnrc_ipv6_default
# Additional networking modules that can be dropped if not needed
USEMODULE += gnrc_icmpv6_echo

In main.c, we include unicoap.

#include "net/unicoap.h"
Main header for unicoap

Setting up the shell

Because we're using the shell to accept a URI, we need to import the shell:

USEMODULE += shell
#include "shell.h"
Shell interface definition.

At the bottom of main(), let's initialize the shell. shell_run will run in a loop, accepting commands.

char line_buf[SHELL_DEFAULT_BUFSIZE];
return 0;
static void shell_run(const shell_command_t *commands, char *line_buf, int len)
Back-porting alias for shell_run_forever.
Definition shell.h:237
#define SHELL_DEFAULT_BUFSIZE
Default shell buffer size (maximum line length shell can handle)
Definition shell.h:115

We will define a custom command called unicoap (You can import support for more, predefined shell commands, e.g., to see a list of network interfaces, specify USEMODULE += shell_cmds_default in your Makefile, and type ifconfig in the shell). Place this static definition for the command at the top-level (outside any functions) in your main.c.

SHELL_COMMAND(unicoap, "unicoap sample client", _cli);
#define SHELL_COMMAND(cmd, help, func)
Define shell command.
Definition shell.h:325

The unicoap shell command and its arguments will be handled by a function called _cli, but you can choose any name you would like. _cli is very similar to the way main() handles command-line arguments: it accepts an argc function parameter (number of whitespace-separated command-line arguments) and an argv function parameter (pointer to an array of strings). For brevity, we will omit all the sanity checks on arguments, but you can look up the full implementation in examples/networking/coap/unicoap_client.

static int _cli(int argc, char** argv) {}

Creating a custom command-line interface

Let's make our unicoap shell command accept three positional arguments. Followed by the request method and URI, we want to optionally accept a payload string for the request. Trivially, this only makes sense for, e.g., the PUT and POST methods, as GET request do not have a payload.

unicoap <method> <uri> [<payload>]

First, we need to translate the method string to unicoap's unicoap_method_t type. Note that argv[0] will be the name of the command itself, i.e., the first overall command-line argument. Hence, the method argument will be at argv[1].

if (strcmp(argv[1], "get") == 0) {
} else if (strcmp(argv[1], "post") == 0) {
} else if (strcmp(argv[1], "put") == 0) {
} else {
printf("error: unsupported method '%s'\n", argv[1]);
return -1;
}
#define printf(...)
A wrapper for the printf() function that passes arguments through unmodified, but fails to compile if...
Definition stdio.h:57
unicoap_method_t
CoAP request method codes (0.xx range)
Definition constants.h:139
@ UNICOAP_METHOD_GET
GET request (no paylod)
Definition constants.h:141
@ UNICOAP_METHOD_PUT
PUT request (update resource with payload)
Definition constants.h:147
@ UNICOAP_METHOD_POST
POST request (resource processes payload)
Definition constants.h:144

Next, we will extract the URI and payload.

const char* uri = argv[2];
const char* payload = argc > 3 ? argv[3] : NULL;
if (payload && method == UNICOAP_METHOD_GET) {
printf("error: GET request has payload?\n");
return -1;
}

Preparing a request

Let's prepare a request.

unicoap_request_init_string(&request, method, payload);
Generic CoAP message.
Definition message.h:72

We will specify to which server to send the request by passing a Uniform Resource Identifier (URI). To make that work, we need to import the URI Client Support module in our Makefile.

USEMODULE += unicoap_client_uri

unicoap supports multiple ways of addressing a server resource. You can create a unicoap_endpoint_t, manually set the IP address, or even resolve a Fully Qualified Domain Name (FQDN) using the Domain Name System (DNS), but we want users of our shell command to be able to type a string like they would in a web browser starting with the URI scheme coap://. The URI way of addressing a resource also saves us the trouble of having to add the URI path manually as CoAP options to the message, e.g., using unicoap_options_t::unicoap_options_add_uri_path.

As we will only be sending requests through a tap interface to the Python server script running on your linux host, we will use an IP address. If you want to use a domain name instead in the URI, please refer to the instructions in URI Client Support.

unicoap_destination_t destination = unicoap_destination_uri_string(uri);
A type abstracting over CoAP resource identifiers.
Definition transport.h:421

Now we can send the request. There are multiple versions unicoap provides — we choose the asynchronous version that calls a handler function on the internal processing thread when the response has arrived or the request fails.

if ((res = unicoap_send_request_async(&request, &destination, _on_response, NULL, 0)) < 0) {
printf("sending request failed (error %i, %s)\n", res, strerror(-res));
return res;
}
int unicoap_send_request_async(unicoap_message_t *request, unicoap_destination_t *destination, unicoap_response_callback_t callback, unicoap_request_parameters_t *parameters, unicoap_request_flags_t flags)
Sends a request asynchronously.

Handling responses

Finally, we specify a function that is going to handle response. We're going to implement _on_response next. _on_response will take three arguments: the response, auxiliary information like the server address, and an error argument. Always first check if it is nonzero and bail out if so.

static int _on_response(unicoap_message_t* message, const unicoap_aux_t* aux, int error) {
/* Check if success or failure. Only proceed if no error occurred. */
if (error) {
printf("error: request failed (%i, %s)\n", error, strerror(-error));
return error;
}
/* ... */
}
Auxiliary exchange information.
Definition transport.h:808

Inside _on_response we're going to log the request initially. We use unicoap_print_code to get a string representation of the CoAP status, i.e., the CoAP message code, and unicoap_message_t::unicoap_message_payload_get_size for the number of payload bytes. This will log a string like response 2.05 Content (42 bytes).

/* Print status code in c.dd (class.detail) form. */
printf("response ");
unicoap_print_code(response->code);
/* Print length of response payload in bytes. */
printf(" %s (%" PRIuSIZE " bytes)\n",
unicoap_string_from_status(unicoap_response_get_status(response)),
unicoap_message_payload_get_size(response));
int unicoap_print_code(uint8_t code)
Prints a c.dd class-detail string.
const char * unicoap_string_from_status(unicoap_status_t status)
Generates short descriptive label from CoAP status code.
#define PRIuSIZE
Macro holding the format specifier to print an size_t variable in decimal representation.

Next, we check if the response format is text/plain and log the response as text. Otherwise, we dump the response payload as hex. To do that, include the object dump header and import the od module.

USEMODULE += od

Top of C file:

#include "od.h"

We also need to check if the payload in the response message is contiguous, i.e., is not chunked into individual vector elements. Normally, you can expect responses to always be contiguous, but you must check this.

if ((error = unicoap_options_get_content_format(response->options, &format)) >= 0
printf("text response: '%.*s'\n",
(int)unicoap_message_payload_get_size(response),
(char*)unicoap_message_payload_get(response));
}
else if (IS_USED(MODULE_OD)
&& response->payload_representation == UNICOAP_PAYLOAD_CONTIGUOUS
&& unicoap_message_payload_get_size(response) > 0) {
/* Print as hex */
unicoap_message_payload_get(response),
unicoap_message_payload_get_size(response),
16);
}
@ UNICOAP_PAYLOAD_CONTIGUOUS
Contiguous payload buffer.
Definition message.h:46
static bool unicoap_content_format_is_human_readable(unicoap_content_format_t format)
Determines whether the given Content-Format is human-readable.
Definition constants.h:1703
unicoap_content_format_t
Content-Format option values
Definition constants.h:709
static void od_hex_dump(const void *data, size_t data_len, uint8_t width)
Dumps memory stored at data byte-wise up to data_len in hexadecimal representation to stdout.
Definition od.h:62
#define IS_USED(module)
Checks whether a module is being used or not.
Definition modules.h:67

Supporting DTLS

To enable Datagram Transport Layer Security, we already imported the CoAP over DTLS Driver. However, we need to include additional headers to add a DTLS credential to unicoap.

#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS)
# include "net/credman.h"
# include "net/dsm.h"
# include "unicoap_example_dtls.h"
# define EXAMPLE_DTLS_CREDENTIAL_TAG 42
static const uint8_t psk_id_0[] = PSK_DEFAULT_IDENTITY;
static const uint8_t psk_key_0[] = PSK_DEFAULT_KEY;
static const credman_credential_t credential = {
.tag = EXAMPLE_DTLS_CREDENTIAL_TAG,
.params = {
.psk = {
.key = { .s = psk_key_0, .len = sizeof(psk_key_0) - 1, },
.id = { .s = psk_id_0, .len = sizeof(psk_id_0) - 1, },
}
},
};
#endif
(D)TLS credentials management module definitions
DTLS sock definitions.
DTLS session management module definition.
@ CREDMAN_TYPE_PSK
PSK credential type.
Definition credman.h:109
Credential information.
Definition credman.h:116

In the main function, we then need to add this credential to the DTLS socket which we can retrieve using unicoap_transport_dtls_get_socket.

int main(void) {
# if IS_USED(MODULE_UNICOAP_DRIVER_DTLS)
int res = credman_add(&credential);
if (res < 0 && res != CREDMAN_EXIST) {
/* ignore duplicate credentials */
printf("app: cannot add credential to system: %d\n", res);
return 1;
}
assert(dtls_socket);
if ((res = sock_dtls_add_credential(dtls_socket, EXAMPLE_DTLS_CREDENTIAL_TAG)) < 0) {
printf("app: cannot add credential to DTLS sock: %d\n", res);
return 1;
}
# endif
}
#define assert(cond)
abort the program if assertion is false
Definition assert.h:143
int credman_add(const credman_credential_t *credential)
Adds a credential to the credential pool.
@ CREDMAN_EXIST
Credential already exist in system pool.
Definition credman.h:130
struct sock_dtls sock_dtls_t
forward declare for async
Definition types.h:44
int sock_dtls_add_credential(sock_dtls_t *sock, credman_tag_t tag)
Adds a credential tag to list of available credentials for sock.
sock_dtls_t * unicoap_transport_dtls_get_socket(void)
Returns the internal DTLS socket.

Time to test it!

Testing the CoAP client

The sample code includes a server.py script that uses aiocoap. To use it we need to compile the client application, run it and then send a CoAP request. We're going to use RIOT's native board for this, i.e., both the client and server will run on your Linux host and there will be no wireless network involved — no antenna needed. To compile and run the app, run this shell command:

cd RIOT/examples/networking/coap/unicoap_client
BOARD=native make -j flash term

This should show something similar to this:

RIOT/dist/tools/pyterm/pyterm
-ps RIOT/examples/networking/coap/unicoap_client/bin/native64/unicoap_client.elf
--process-args tap0
Welcome to pyterm!
Type '/exit' to exit.
# RIOT native interrupts/signals initialized.
# RIOT native64 board initialized.
# RIOT native hardware initialization complete.
# coap: registered 2 XFA resources
# coap.transport.udp: zero_copy_guarantees=1 creating UDP sock, port=5683 if=0 family=inet6
# coap.transport.dtls: creating DTLS sock, port=5684 if=0 family=inet6
# main(): This is RIOT! (Version: 2025.04-devel-634-RED-unicoap-02-server-minimal)
# app: listening at UDP <sock_tl_ep port=5683 netif=0 ipv6=::>
# app: listening at DTLS <sock_tl_ep port=5684 netif=0 ipv6=::>
# app: using credential: type=PSK id=Client_identity key=secretPSK
Remarks
You might need to enable the tap interface:
sudo ip tuntap add tap0 mode tap user ${USER}
sudo ip link set tap0 up
Note
unicoap will emit debug logs depending on CONFIG_UNICOAP_DEBUG_LOGGING and CONFIG_UNICOAP_ASSIST. These settings are enabled in the sample code in app.config.

In a second terminal session, run

python3 server.py --interface tap0

You should see output like this:

python3 server.py --interface tap0
server bound to tap interface fe80::dead:beef%tap0
using port=5683

And then type in the RIOT shell (the running example application)

unicoap get coap://[fe80::dead:beef]

You should see the following response:

response 2.05 Content (26 bytes)
text response: 'Welcome to the demo server'

Congrats, the client shell is working!

If you want to capture the actual CoAP messages being sent, open a third terminal session and run tcpdump -i tap0 -w coap-welcome.pcap, then execute the client script as described above, then terminate tcpdump with CTRL+C. You can now open coap-greeting.pcap with Wireshark to inspect CoAP messages. You should see a NON request followed by a CON response which elicits another ACK message from the client.