CoAP Resource Discovery in ESP-IDF

Chapter 116: CoAP Resource Discovery in ESP-IDF

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the fundamental concept and importance of resource discovery in CoAP.
  • Describe the CoRE Link Format (RFC 6690) and its syntax.
  • Implement a CoAP server on an ESP32 that exposes its resources for discovery via the /.well-known/core interface.
  • Develop a CoAP client on an ESP32 capable of discovering resources offered by a CoAP server.
  • Utilize query parameters to filter discovered resources based on specific attributes.
  • Recognize differences in CoAP applicability across various ESP32 variants.

Introduction

In the previous chapter, we explored the basics of implementing the Constrained Application Protocol (CoAP) on ESP32 devices. While direct interaction with known resources is useful, in many Internet of Things (IoT) scenarios, devices and applications need a way to automatically discover what services or resources a CoAP server offers. This is particularly crucial in dynamic environments where devices may join or leave the network, or where services might change over time.

CoAP resource discovery provides a standardized mechanism for clients to ask a server, “What resources do you have?” This chapter delves into this discovery process, focusing on the /.well-known/core URI and the CoRE Link Format, enabling your ESP32 devices to intelligently interact within a CoAP ecosystem. Understanding resource discovery is a key step towards building more autonomous and interoperable IoT systems.

Theory

What is Resource Discovery?

Resource discovery in CoAP is the process by which a CoAP client can learn about the resources hosted by a CoAP server, including their URIs and metadata (attributes) such as resource type, interface description, and content type. This mechanism allows clients to dynamically adapt to the available resources without prior hardcoding of all possible resource paths.

Imagine walking into a library for the first time. You wouldn’t know where every book is located. You might look for a directory, a map, or ask a librarian. CoAP resource discovery serves a similar purpose for IoT devices. A client device can “ask” a server device for its “map” of available resources.

%%{init: { "theme": "base", "fontFamily": "Open Sans" }}%%
graph LR

    classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;

    Client[("CoAP Client<br>(New IoT Device)")]:::primary
    Librarian[("CoAP Server<br>(Existing Device/System)")]:::primary
    MapRequest["Asks: 'Do you have a map <br>of your resources/services?'<br>(GET /.well-known/core)"]:::process
    ResourceMap["Provides 'Library Map'<br>(CoRE Link Format Payload:<br></sensors/temp>;rt=\<i>temp\</i>,<br></lights/livingroom>;rt=\<i>light\</i>)"]:::success

    Client -- "1- Needs to discover services" --> Librarian
    Librarian -- "2- How do I ask?" --> MapRequest
    Client -- "3- Sends Discovery Request" --> MapRequest
    MapRequest --> Librarian
    Librarian -- "4- Responds with Resource List" --> ResourceMap
    ResourceMap --> Client

This is standardized by the CoRE (Constrained RESTful Environments) specifications, primarily RFC 6690, which defines the “Link Format.”

The /.well-known/core URI

CoAP servers that support resource discovery expose a specific, well-known URI: /.well-known/core. A client interested in discovering resources sends a CoAP GET request to this URI. The server then responds with a payload describing its available resources.

The path /.well-known/ is a convention established by RFC 8615 (Well-Known Uniform Resource Identifiers) for hosting site-wide policy or metadata. core refers to the CoRE specifications.

CoRE Link Format (RFC 6690)

The server’s response to a GET request on /.well-known/core is typically formatted using the CoRE Link Format. This format provides a concise way to list resources and their attributes. Each link description consists of a URI reference and one or more link parameters, each describing an attribute of the resource.

The basic syntax for a link is:

</uri-path>[;attr1=val1][;attr2="val2"]...

  • </uri-path>: The URI of the resource being described, enclosed in angle brackets.
  • ;attr=val: Attribute-value pairs, separated by semicolons.
    • Attribute names are typically short strings.
    • Values can be bare words (if they don’t contain special characters) or enclosed in double quotes.

Multiple link descriptions are separated by commas.

Common Link Attributes:

Attribute Abbreviation Description Example Value(s)
Resource Type rt Specifies the semantic type of the resource. Helps clients understand what the resource represents. “temperature-c”, “light-actuator”, “power-level”
Interface Description if Describes how to interact with the resource or the “pattern” it follows (e.g., sensor, actuator). “sensor”, “actuator”, “parameter”, “core.ll” (for link-listing)
Content-Type ct Specifies the content type (Media Type) of the resource’s representation. Uses numeric CoAP Content-Format codes. 0 (text/plain), 40 (application/link-format), 50 (application/json)
Maximum Size Estimate sz An optional attribute indicating the maximum expected size of the resource’s representation in bytes. 128, 1024
Observable obs If present (no value needed), indicates that the resource supports the CoAP Observe mechanism (Chapter 118). (Attribute presence implies true)
Title title A human-readable title for the link. “Living Room Thermostat”

Example Link Format Payload:

Plaintext
</sensors/temperature>;rt="temperature-c";if="sensor",
</sensors/humidity>;rt="humidity-percent";if="sensor";obs,
</actuators/led1>;rt="light";if="actuator",
</.well-known/core>

In this example:

  • The server exposes three main resources: /sensors/temperature, /sensors/humidity, and /actuators/led1.
  • The temperature sensor has resource type temperature-c and interface sensor.
  • The humidity sensor is observable.
  • The </.well-known/core> entry itself is often included, describing the discovery resource.
sequenceDiagram
    %%{init: { "theme": "base", "fontFamily": "Open Sans" }}%%
    actor Client as CoAP Client
    participant Server as CoAP Server

    Client->>Server: 1. GET coap://server-ip/.well-known/core
    activate Server

    Note over Server: Server processes request,<br/>compiles list of its resources.

    Server-->>Client: 2. Response (Code: 2.05 Content)<br/>Payload (Content-Type: application/link-format):<br/>sensors/temperature rt=temperature-c if=sensor<br/>sensors/humidity rt=humidity-percent if=sensor obs<br/>actuators/led1 rt=light if=actuator<br/>.well-known/core
    deactivate Server

    Note over Client: Client parses the<br/>link-format payload to<br/>learn about available resources.

Filtering Resources

Clients can filter the discovered resources by including query parameters in their GET request to /.well-known/core. The server then attempts to return only those resources that match the specified criteria.

For example, to discover only resources of type “temperature-c”:

GET coap://server-ip/.well-known/core?rt=temperature-c

The server would then respond with a filtered list, potentially:

</sensors/temperature>;rt="temperature-c";if="sensor"

sequenceDiagram
    %%{init: { "theme": "base", "fontFamily": "Open Sans" }}%%
    actor Client as CoAP Client
    participant Server as CoAP Server

    Client->>Server: 1. GET coap://server-ip/.well-known/core?rt=temperature-c
    activate Server

    Note over Server: Server processes request,<br/>filters resources matching "rt=temperature-c".

    Server-->>Client: 2. Response (Code: 2.05 Content)<br/>Payload (Content-Type: application/link-format):<br/>sensors/temperature rt="temperature-c" if="sensor"
    deactivate Server

    Note over Client: Client receives a filtered list,<br/>only containing temperature sensors.

Common query parameters for filtering include:

Query Parameter Description Example Usage
href Filters resources by their URI reference. Can use wildcards (e.g., * at the end of a path segment). ?href=/sensors/*
?href=/actuators/led1
rt (Resource Type) Filters resources by their Resource Type (rt attribute). ?rt=temperature-c
?rt=light_switch
if (Interface Description) Filters resources by their Interface Description (if attribute). ?if=sensor
?if=actuator
ct (Content-Type) Filters resources by their Content-Type (ct attribute). Uses numeric CoAP Content-Format codes. ?ct=0 (for text/plain)
?ct=50 (for application/json)
Multiple Parameters Combine multiple parameters to narrow down the search. Parameters are typically ANDed. ?rt=temperature-c&if=sensor

Practical Examples

We will use the esp-coap component, which is based on libcoap, for these examples. Ensure your ESP-IDF project is configured to include this component (idf.py menuconfig -> Component config -> CoAP). You’ll also need Wi-Fi connectivity.

Example 1: ESP32 CoAP Server with Discoverable Resources

This example demonstrates setting up an ESP32 as a CoAP server that offers several resources and makes them discoverable via /.well-known/core.

1. Project Setup:

Create a new ESP-IDF project or use an existing one. Ensure Wi-Fi is configured.

2. main/coap_server_discovery_main.c:

C
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/event_groups.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_event.h"
#include "esp_wifi.h"
#include "esp_coap.h"

static const char *TAG = "COAP_SERVER";

// Example resource data
static char light_state_str[8] = "OFF"; // Max "ON" or "OFF" + null
static float temperature_val = 25.5;

/* --- Resource Handlers --- */

// Handler for /light resource
static void light_put_handler(coap_resource_t *resource,
                              coap_session_t *session,
                              const coap_pdu_t *request,
                              const coap_string_t *query,
                              coap_pdu_t *response)
{
    size_t len;
    const uint8_t *data;
    coap_get_data(request, &len, &data);

    if (len > 0 && len < sizeof(light_state_str)) {
        if (strncmp((const char *)data, "ON", len) == 0) {
            strncpy(light_state_str, "ON", sizeof(light_state_str) -1);
            light_state_str[sizeof(light_state_str)-1] = '\0';
            ESP_LOGI(TAG, "Light turned ON");
        } else if (strncmp((const char *)data, "OFF", len) == 0) {
            strncpy(light_state_str, "OFF", sizeof(light_state_str) -1);
            light_state_str[sizeof(light_state_str)-1] = '\0';
            ESP_LOGI(TAG, "Light turned OFF");
        } else {
            coap_pdu_set_code(response, COAP_RESPONSE_CODE_BAD_REQUEST);
            return;
        }
        coap_pdu_set_code(response, COAP_RESPONSE_CODE_CHANGED);
    } else {
        coap_pdu_set_code(response, COAP_RESPONSE_CODE_BAD_REQUEST);
    }
}

static void light_get_handler(coap_resource_t *resource,
                              coap_session_t *session,
                              const coap_pdu_t *request,
                              const coap_string_t *query,
                              coap_pdu_t *response)
{
    coap_pdu_set_code(response, COAP_RESPONSE_CODE_CONTENT);
    coap_add_data_large_response(resource, session, request, response, query,
                                 COAP_MEDIATYPE_TEXT_PLAIN,
                                 strlen(light_state_str),
                                 (const uint8_t *)light_state_str,
                                 NULL, NULL); // No etag, no token
}


// Handler for /sensor/temp resource
static void temp_get_handler(coap_resource_t *resource,
                             coap_session_t *session,
                             const coap_pdu_t *request,
                             const coap_string_t *query,
                             coap_pdu_t *response)
{
    char temp_str[16];
    snprintf(temp_str, sizeof(temp_str), "%.1f C", temperature_val);
    coap_pdu_set_code(response, COAP_RESPONSE_CODE_CONTENT);
    coap_add_data_large_response(resource, session, request, response, query,
                                 COAP_MEDIATYPE_TEXT_PLAIN,
                                 strlen(temp_str),
                                 (const uint8_t *)temp_str,
                                 NULL, NULL);
}

// Handler for /.well-known/core resource
static void well_known_core_handler(coap_resource_t *resource,
                                    coap_session_t *session,
                                    const coap_pdu_t *request,
                                    const coap_string_t *query,
                                    coap_pdu_t *response)
{
    // This is a simplified example. A more robust implementation would iterate
    // through registered resources or have a dynamic way to build this string.
    const char *core_payload = "</light>;rt=\"light_switch\";if=\"actuator\","
                               "</sensor/temp>;rt=\"temperature-celsius\";if=\"sensor\","
                               "</.well-known/core>"; // It's good practice to list itself

    coap_pdu_set_code(response, COAP_RESPONSE_CODE_CONTENT);
    coap_add_data_large_response(resource, session, request, response, query,
                                 COAP_MEDIATYPE_APPLICATION_LINK_FORMAT,
                                 strlen(core_payload),
                                 (const uint8_t *)core_payload,
                                 NULL, NULL);
    ESP_LOGI(TAG, "Sent .well-known/core response");
}

/* --- Wi-Fi Setup --- */
#define EXAMPLE_ESP_WIFI_SSID      CONFIG_ESP_WIFI_SSID
#define EXAMPLE_ESP_WIFI_PASS      CONFIG_ESP_WIFI_PASSWORD
#define EXAMPLE_MAX_STA_CONN       CONFIG_ESP_MAX_STA_CONN

static EventGroupHandle_t wifi_event_group;
const int WIFI_CONNECTED_BIT = BIT0;

static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                               int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        ESP_LOGI(TAG, "Disconnected. Connecting to the AP again...");
        esp_wifi_connect();
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip));
        xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

void wifi_init_sta(void)
{
    wifi_event_group = xEventGroupCreate();
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    esp_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        &instance_any_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        &instance_got_ip));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .password = EXAMPLE_ESP_WIFI_PASS,
            .threshold.authmode = WIFI_AUTH_WPA2_PSK, // Adjust as per your AP
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
    ESP_ERROR_CHECK(esp_wifi_start() );

    ESP_LOGI(TAG, "wifi_init_sta finished.");
    ESP_LOGI(TAG, "connect to ap SSID:%s password:%s",
             EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);

    EventBits_t bits = xEventGroupWaitBits(wifi_event_group,
            WIFI_CONNECTED_BIT,
            pdFALSE,
            pdFALSE,
            portMAX_DELAY);

    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "Connected to ap SSID:%s", EXAMPLE_ESP_WIFI_SSID);
    } else {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }
}


/* --- CoAP Server Task --- */
static void coap_server_task(void *pvParameters)
{
    coap_context_t *ctx = NULL;
    coap_address_t serv_addr;
    coap_resource_t *res_well_known_core = NULL;
    coap_resource_t *res_light = NULL;
    coap_resource_t *res_temp = NULL;

    // Wait for Wi-Fi connection
    xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
    ESP_LOGI(TAG, "Wi-Fi Connected. Starting CoAP server...");

    // Initialize CoAP library
    coap_startup();

    // Prepare CoAP server address
    coap_address_init(&serv_addr);
    serv_addr.addr.sin.sin_family      = AF_INET;
    serv_addr.addr.sin.sin_addr.s_addr = INADDR_ANY;
    serv_addr.addr.sin.sin_port        = htons(COAP_DEFAULT_PORT); // Default CoAP port 5683

    // Create CoAP context
    ctx = coap_new_context(&serv_addr);
    if (!ctx) {
        ESP_LOGE(TAG, "Failed to create CoAP context");
        goto finish;
    }
    coap_context_set_keepalive(ctx, COAP_DEFAULT_KEEPALIVE); // Default is 60 seconds

    // Create /.well-known/core resource
    res_well_known_core = coap_resource_init(coap_make_str_const(".well-known/core"), 0);
    if (!res_well_known_core) {
        ESP_LOGE(TAG, "Failed to create .well-known/core resource");
        goto finish;
    }
    coap_register_handler(res_well_known_core, COAP_REQUEST_GET, well_known_core_handler);
    coap_add_attr(res_well_known_core, coap_make_str_const("ct"), coap_make_str_const("40"), 0); // Content-Type application/link-format
    coap_add_resource(ctx, res_well_known_core);

    // Create /light resource
    res_light = coap_resource_init(coap_make_str_const("light"), 0);
    if (!res_light) {
        ESP_LOGE(TAG, "Failed to create /light resource");
        goto finish;
    }
    coap_register_handler(res_light, COAP_REQUEST_GET, light_get_handler);
    coap_register_handler(res_light, COAP_REQUEST_PUT, light_put_handler);
    coap_add_attr(res_light, coap_make_str_const("rt"), coap_make_str_const("light_switch"), 0);
    coap_add_attr(res_light, coap_make_str_const("if"), coap_make_str_const("actuator"), 0);
    coap_add_resource(ctx, res_light);

    // Create /sensor/temp resource
    res_temp = coap_resource_init(coap_make_str_const("sensor/temp"), 0);
    if (!res_temp) {
        ESP_LOGE(TAG, "Failed to create /sensor/temp resource");
        goto finish;
    }
    coap_register_handler(res_temp, COAP_REQUEST_GET, temp_get_handler);
    coap_add_attr(res_temp, coap_make_str_const("rt"), coap_make_str_const("temperature-celsius"), 0);
    coap_add_attr(res_temp, coap_make_str_const("if"), coap_make_str_const("sensor"), 0);
    coap_add_resource(ctx, res_temp);


    ESP_LOGI(TAG, "CoAP server started on port %d", COAP_DEFAULT_PORT);

    while (1) {
        // Process CoAP I/O. This function will block for a certain time.
        // The timeout can be configured via coap_context_set_max_idle_sessions()
        // or by using coap_io_process_with_timeout() for more control.
        int result = coap_io_process(ctx, COAP_IO_WAIT);
        if (result < 0) {
            ESP_LOGE(TAG, "coap_io_process error: %d", result);
            break;
        } else if (result > 0) {
            // ESP_LOGD(TAG, "coap_io_process processed %d events", result);
        }
    }

finish:
    if (ctx) {
        coap_free_context(ctx);
    }
    coap_cleanup();
    vTaskDelete(NULL);
}

void app_main(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());
    wifi_init_sta(); // Connect to Wi-Fi

    xTaskCreate(coap_server_task, "coap_server_task", 8192, NULL, 5, NULL);
}

Note: You’ll need to configure your Wi-Fi SSID and Password in menuconfig (idf.py menuconfig -> Example Connection Configuration).

3. Build Instructions:

Bash
idf.py set-target esp32 # or esp32s2, esp32s3, esp32c3, etc.
idf.py menuconfig # Configure WiFi credentials and other settings
idf.py build

4. Run/Flash/Observe:

  1. Flash the firmware: idf.py -p /dev/ttyUSB0 flash monitor (replace /dev/ttyUSB0 with your ESP32’s serial port).
  2. Once the ESP32 connects to Wi-Fi and the CoAP server starts, note its IP address from the serial monitor logs.
  3. Use a CoAP client tool to discover resources. For example, using libcoap-utils (installable on Linux: sudo apt install libcoap2-bin or similar for libcoap- yetişkin):coap-client -m get coap://<ESP32_IP_ADDRESS>/.well-known/core
    Or using a browser extension like Copper (Cu) for Firefox (though it might be outdated) or a dedicated CoAP test tool.

Expected Output (from coap-client):

You should see the link-format string defined in well_known_core_handler:

Plaintext
</light>;rt="light_switch";if="actuator",</sensor/temp>;rt="temperature-celsius";if="sensor",</.well-known/core>

You can also try accessing individual resources:

Bash
coap-client -m get coap://<ESP32_IP_ADDRESS>/sensor/temp
coap-client -m put -e "ON" coap://<ESP32_IP_ADDRESS>/light
coap-client -m get coap://<ESP32_IP_ADDRESS>/light

Example 2: ESP32 CoAP Client to Discover Resources

This example shows an ESP32 acting as a CoAP client to discover resources from the server created in Example 1 (or any other CoAP server).

1. Project Setup:

Similar to the server, ensure Wi-Fi and esp-coap component are configured.

2. main/coap_client_discovery_main.c:

C
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/event_groups.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_event.h"
#include "esp_wifi.h"
#include "esp_coap.h"

static const char *TAG = "COAP_CLIENT";

// Target server IP address - replace with your server's IP
#define COAP_SERVER_IP_ADDR    "192.168.1.100" // CHANGE THIS
#define COAP_DEFAULT_PORT_STR  "5683"

/* --- Wi-Fi Setup (same as server example) --- */
#define EXAMPLE_ESP_WIFI_SSID      CONFIG_ESP_WIFI_SSID
#define EXAMPLE_ESP_WIFI_PASS      CONFIG_ESP_WIFI_PASSWORD
#define EXAMPLE_MAX_STA_CONN       CONFIG_ESP_MAX_STA_CONN

static EventGroupHandle_t wifi_event_group;
const int WIFI_CONNECTED_BIT = BIT0;

static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                               int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        ESP_LOGI(TAG, "Disconnected. Connecting to the AP again...");
        esp_wifi_connect();
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip));
        xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

void wifi_init_sta(void)
{
    wifi_event_group = xEventGroupCreate();
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    esp_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        &instance_any_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        &instance_got_ip));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .password = EXAMPLE_ESP_WIFI_PASS,
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
    ESP_ERROR_CHECK(esp_wifi_start() );

    ESP_LOGI(TAG, "wifi_init_sta finished.");
    EventBits_t bits = xEventGroupWaitBits(wifi_event_group,
            WIFI_CONNECTED_BIT,
            pdFALSE,
            pdFALSE,
            portMAX_DELAY);

    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "Connected to ap SSID:%s", EXAMPLE_ESP_WIFI_SSID);
    } else {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }
}


/* --- CoAP Response Handler --- */
static void coap_response_handler(coap_session_t *session,
                                  const coap_pdu_t *sent,
                                  const coap_pdu_t *received,
                                  const coap_mid_t mid)
{
    const uint8_t *data = NULL;
    size_t data_len = 0;
    coap_pdu_code_t rcv_code = coap_pdu_get_code(received);

    if (COAP_RESPONSE_CLASS(rcv_code) == 2) { // Success class (2.xx)
        if (coap_get_data(received, &data_len, &data)) {
            ESP_LOGI(TAG, "Received CoAP Response (Code: %d.%02d):",
                     COAP_RESPONSE_GET_CLASS(rcv_code), COAP_RESPONSE_GET_CODE(rcv_code));
            // Print as string - assumes text-based payload or link-format
            // For binary data, you'd process 'data' and 'data_len' differently
            printf("%.*s\n", (int)data_len, (char *)data);
        }
    } else {
        ESP_LOGE(TAG, "CoAP request failed (Code: %d.%02d)",
                 COAP_RESPONSE_GET_CLASS(rcv_code), COAP_RESPONSE_GET_CODE(rcv_code));
    }
}

/* --- CoAP Client Task --- */
static void coap_client_task(void *pvParameters)
{
    coap_context_t *ctx = NULL;
    coap_session_t *session = NULL;
    coap_address_t dst_addr;
    static coap_uri_t uri;
    const char *server_uri_path = "/.well-known/core"; // Path for discovery
    // const char *server_uri_path_filtered = "/.well-known/core?rt=temperature-celsius"; // Example filtered request

    // Wait for Wi-Fi connection
    xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
    ESP_LOGI(TAG, "Wi-Fi Connected. Starting CoAP client...");

    coap_startup();

    // Resolve server address
    struct addrinfo *res, *ainfo;
    struct addrinfo hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_socktype = SOCK_DGRAM; // CoAP uses UDP
    hints.ai_family = AF_INET;      // Assuming IPv4

    if (getaddrinfo(COAP_SERVER_IP_ADDR, COAP_DEFAULT_PORT_STR, &hints, &res) != 0) {
        ESP_LOGE(TAG, "DNS lookup failed for address: %s", COAP_SERVER_IP_ADDR);
        goto finish_client;
    }
    ainfo = res; // Use the first result

    // Copy resolved address to CoAP address structure
    coap_address_init(&dst_addr);
    switch (ainfo->ai_family) {
        case AF_INET:
            memcpy(&dst_addr.addr.sin, ainfo->ai_addr, sizeof(dst_addr.addr.sin));
            dst_addr.addr.sin.sin_port = htons(atoi(COAP_DEFAULT_PORT_STR)); // Set port
            break;
        // Add AF_INET6 case if needed
        default:
            ESP_LOGE(TAG, "Unsupported address family");
            freeaddrinfo(res);
            goto finish_client;
    }
    freeaddrinfo(res);

    // Create CoAP context and session
    ctx = coap_new_context(NULL); // NULL for client-only context
    if (!ctx) {
        ESP_LOGE(TAG, "Failed to create CoAP context");
        goto finish_client;
    }

    session = coap_new_client_session(ctx, NULL, &dst_addr, COAP_PROTO_UDP);
    if (!session) {
        ESP_LOGE(TAG, "Failed to create CoAP client session");
        goto finish_client;
    }

    coap_register_response_handler(ctx, coap_response_handler);

    // Prepare PDU for discovery request
    coap_pdu_t *pdu = coap_new_pdu(COAP_MESSAGE_CON, COAP_REQUEST_CODE_GET, coap_new_message_id(session));
    if (!pdu) {
        ESP_LOGE(TAG, "Failed to create PDU");
        goto finish_client;
    }

    // Set URI path for discovery
    if (coap_string_to_uri((const uint8_t *)server_uri_path, strlen(server_uri_path), &uri) != 0) {
         ESP_LOGE(TAG, "Cannot parse URI: %s", server_uri_path);
         coap_delete_pdu(pdu);
         goto finish_client;
    }
    if (uri.path.length) {
        coap_add_option(pdu, COAP_OPTION_URI_PATH, uri.path.length, uri.path.s);
    }
    if (uri.query.length) { // For filtered requests
        coap_add_option(pdu, COAP_OPTION_URI_QUERY, uri.query.length, uri.query.s);
    }

    // Send the CoAP request
    ESP_LOGI(TAG, "Sending CoAP GET request to %s%s", COAP_SERVER_IP_ADDR, server_uri_path);
    if (coap_send(session, pdu) == COAP_INVALID_MID) {
        ESP_LOGE(TAG, "Failed to send CoAP request");
        // pdu is freed by coap_send on error or success
    }

    // Wait for response (or timeout). coap_io_process handles this.
    // In a real app, you'd have a loop or better async handling.
    // For this example, we'll process for a few seconds.
    TickType_t start_time = xTaskGetTickCount();
    while(xTaskGetTickCount() - start_time < pdMS_TO_TICKS(5000)) { // Wait up to 5 seconds
        int result = coap_io_process(ctx, 1000); // Process with 1s timeout
        if (result < 0) {
            ESP_LOGE(TAG, "coap_io_process error: %d", result);
            break;
        } else if (result > 0 && result < 1000) {
            // Response might have been handled, or other events
            // For a single request/response, we might break here after first response
        }
    }
    ESP_LOGI(TAG, "Client task finished processing.");


finish_client:
    if (session) coap_free_session(session);
    if (ctx) coap_free_context(ctx);
    coap_cleanup();
    vTaskDelete(NULL);
}

void app_main(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());
    wifi_init_sta(); // Connect to Wi-Fi

    // Ensure the server IP address (COAP_SERVER_IP_ADDR) is correct
    ESP_LOGI(TAG, "Target CoAP Server IP: %s", COAP_SERVER_IP_ADDR);
    xTaskCreate(coap_client_task, "coap_client_task", 8192, NULL, 5, NULL);
}

3. Build Instructions:

Same as the server: idf.py build.

4. Run/Flash/Observe:

  1. Ensure your CoAP server (from Example 1 or another) is running and accessible.
  2. Crucially, update COAP_SERVER_IP_ADDR in coap_client_discovery_main.c to the IP address of your CoAP server.
  3. Flash the client firmware: idf.py -p /dev/ttyUSB0 flash monitor.
  4. Observe the serial monitor output. The client will connect to Wi-Fi, send a GET request to coap://<YOUR_SERVER_IP>/.well-known/core, and print the response.

Expected Output (on client’s serial monitor):

Plaintext
I (COAP_CLIENT): Received CoAP Response (Code: 2.05):
</light>;rt="light_switch";if="actuator",</sensor/temp>;rt="temperature-celsius";if="sensor",</.well-known/core>

Tip: To test filtered discovery, change server_uri_path in the client code to:

const char *server_uri_path = “/.well-known/core?rt=temperature-celsius”;

Rebuild and reflash. The server should then only return the temperature sensor resource.

Variant Notes

  • ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6 (Wi-Fi variants): All these variants have Wi-Fi capabilities and sufficient processing power to run both CoAP clients and servers, including resource discovery. The esp-coap component (libcoap) is supported. Performance and the number of concurrent sessions or resources might vary slightly based on available RAM and CPU speed, but for typical use cases, all are suitable.
  • ESP32-H2 (802.15.4/Thread/Zigbee & Bluetooth LE):
    • While CoAP is IP-based, the ESP32-H2 primarily targets Thread (using 6LoWPAN for IPv6) and Zigbee. CoAP is a very common application layer protocol for Thread networks.
    • The principles of CoAP resource discovery remain identical over 6LoWPAN/Thread. The underlying network stack configuration (using OpenThread, for example, as covered in later volumes) would differ from Wi-Fi.
    • The esp-coap library can still be used, but network initialization and addressing will be specific to the 6LoWPAN/Thread environment.
  • Resource Constraints: While CoAP itself is lightweight, the libcoap library and the networking stack consume resources (Flash and RAM). On more constrained variants like the ESP32-C3, if running many other services, you might need to be mindful of the overall application footprint. However, for typical CoAP client/server roles, it’s generally well within limits.
  • Ethernet: For ESP32 variants with Ethernet support (e.g., original ESP32 with an external PHY), CoAP works transparently over Ethernet once the IP stack is configured. Resource discovery mechanisms are unchanged.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Incorrect /.well-known/core Handler (Server) Client receives no response, an error, or an empty/malformed link-format string for discovery requests. Verify Handler Registration: Ensure the well_known_core_handler is registered for COAP_REQUEST_GET using coap_register_handler().
Check Link Format Syntax: Confirm the returned string strictly follows CoRE Link Format: </uri>[;attr=val], with commas separating entries. Angle brackets for URIs are crucial.
Set Content-Type: The resource for /.well-known/core should have its content type attribute set to ct=40 (application/link-format). Use coap_add_attr() on the server resource.
Client Fails to Parse Link Format Client receives the link-format string but cannot extract URIs or attributes, or misinterprets them. Implement a Parser: The raw link-format string needs parsing. libcoap doesn’t provide a built-in parser. Simple string functions (strtok_r, strstr) can work for basic needs. For robustness, consider a stateful tokenizer.
Handle Quoted Values: Attribute values can be quoted (e.g., rt=”sensor type”). Parser must handle quotes correctly.
Iterate Correctly: Split by comma for individual links, then by semicolon for attributes.
Firewall/Network Issues Client requests time out. No response from server despite server seemingly running. Check UDP Ports: Ensure UDP port 5683 (CoAP) or 5684 (CoAPS) is not blocked by firewalls on the network, server machine, or client machine.
AP Isolation: If devices are on Wi-Fi, ensure “AP Isolation” or “Client Isolation” is disabled on the Wi-Fi router, as this prevents devices on the same Wi-Fi from communicating.
Server Not Reachable / Incorrect IP (Client) Client requests time out or fail immediately (e.g., “Host not found”). Verify Server IP: Double-check the COAP_SERVER_IP_ADDR in the client code. Confirm the server’s actual IP from its serial logs or router’s DHCP client list.
Server Status: Ensure the CoAP server task is running on the ESP32 and has initialized successfully.
Network Connectivity: Ping the server’s IP from another device on the same network to confirm basic network reachability.
Forgetting coap_io_process() CoAP server/client initializes but doesn’t send/receive any CoAP messages, or only the first message works. Implement I/O Loop: The coap_io_process(ctx, timeout) function (or its non-blocking equivalent coap_io_dispatch()) MUST be called regularly in a loop in both client and server tasks. This function handles all network I/O, retransmissions, and dispatches messages.
Mismatched Wi-Fi Credentials ESP32 fails to connect to Wi-Fi, CoAP server/client doesn’t start network operations. Check SSID/Password: Ensure Wi-Fi SSID and password in menuconfig or code match the access point’s settings exactly (case-sensitive).
AP Compatibility: Verify the ESP32 supports the AP’s Wi-Fi mode (e.g., WPA2-PSK).
Insufficient Task Stack/Memory ESP32 crashes, reboots, or behaves erratically, especially when handling multiple requests or larger payloads. Guru Meditation Error. Increase Stack Size: The CoAP task (and Wi-Fi task) may require more stack. Increase the stack size in xTaskCreate().
Monitor Heap: Use esp_get_free_heap_size() to monitor available memory. Optimize memory usage if it’s critically low.

Exercises

  1. Extend the Server’s Discoverable Resources:
    • Modify the CoAP server example (Example 1).
    • Add three new resources:
      • /config/device_name (GETtable, rt="device.name", if="parameter")
      • /actuators/buzzer (PUTtable to turn ON/OFF, rt="sound.alarm", if="actuator")
      • /info/uptime (GETtable, returns system uptime, rt="device.uptime", if="sensor")
    • Update the /.well-known/core handler to include these new resources in its response.
    • Verify with a CoAP client that all resources are discoverable and their attributes are correctly listed.
  2. Advanced Client Filtering:
    • Modify the CoAP client example (Example 2).
    • Instead of a hardcoded discovery URI, make the client prompt the user via the serial monitor to enter a resource type (e.g., “sensor”, “actuator”, “light_switch”).
    • The client should then construct a discovery request like /.well-known/core?rt=<entered_type> and send it to the server.
    • Print the filtered list of resources received from the server.
  3. Dynamic Resource Registration and Discovery:
    • This is a more advanced exercise. Modify the CoAP server from Example 1.
    • Implement a mechanism (e.g., using UART commands received via the serial monitor) to “register” and “unregister” one of the existing resources (e.g., /sensor/temp) at runtime.
    • The /.well-known/core handler must dynamically build its response based on the currently registered resources. If /sensor/temp is unregistered, it should not appear in the discovery response. If re-registered, it should reappear.
    • Hint: You might need to maintain a list or array of active resource definitions that the well_known_core_handler can iterate through.

Summary

  • CoAP Resource Discovery allows clients to learn about the resources hosted by a server dynamically.
  • Discovery is primarily achieved by sending a GET request to the /.well-known/core URI on the server.
  • Servers respond with a list of their resources formatted using the CoRE Link Format (RFC 6690).
  • The Link Format uses </uri-path>[;attr=val] syntax, with common attributes like rt (resource type), if (interface description), and ct (content-type).
  • Clients can filter discovery results by appending query parameters to the /.well-known/core request (e.g., ?rt=temperature-sensor).
  • ESP-IDF’s esp-coap component (based on libcoap) provides the tools to implement both CoAP servers with discoverable resources and clients that can perform discovery.
  • Resource discovery is applicable across various ESP32 variants, with network-specific considerations for Wi-Fi vs. Thread (ESP32-H2).

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top