Chapter 115: CoAP Protocol Implementation
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the fundamentals of the CoAP protocol and its advantages for constrained IoT devices.
- Differentiate CoAP from HTTP and identify scenarios where CoAP is more suitable.
- Understand CoAP message types, request/response model, and URI schemes.
- Implement a CoAP server on an ESP32 using the
esp-coapcomponent (based on libcoap). - Define CoAP resources and handle GET, POST, PUT, and DELETE requests.
- Implement a CoAP client on an ESP32 to interact with CoAP servers.
- Understand the basics of CoAP resource discovery.
- Recognize considerations for CoAP (and DTLS for security) on different ESP32 variants.
Introduction
In previous chapters, we explored HTTP/HTTPS for client-server communication and building web interfaces. While HTTP is ubiquitous on the wider internet, it can be verbose and resource-intensive for many IoT devices, especially those operating on constrained networks (e.g., low power, low bandwidth like 6LoWPAN, or even Wi-Fi where efficiency is key).
This chapter introduces the Constrained Application Protocol (CoAP), a specialized web transfer protocol designed for use with constrained nodes and constrained networks in the Internet of Things. CoAP provides a lightweight, RESTful approach similar to HTTP but runs over UDP (User Datagram Protocol) and has a much smaller message overhead. This makes it an excellent choice for applications like sensor networks, smart metering, and other M2M (Machine-to-Machine) scenarios where efficiency is paramount. We will learn how to leverage CoAP on the ESP32 to create efficient, RESTful services.
Prerequisite Note: A basic understanding of UDP (Chapter 87), REST principles (Chapter 111 & 113), and client-server architecture is assumed. Wi-Fi connectivity is also required for the examples.
Theory
What is CoAP?
CoAP (Constrained Application Protocol) is an IETF (Internet Engineering Task Force) standard (RFC 7252) designed to bring the RESTful web paradigm to resource-constrained IoT devices and networks. It aims to easily translate to HTTP for integration with the existing web while adding key features for IoT, such as low overhead, multicast support, and asynchronous message exchanges.
Key Features of CoAP:
- Lightweight: Small message headers and simple parsing logic reduce processing and power consumption.
- UDP-based: Runs over UDP, which is connectionless and has less overhead than TCP (used by HTTP).
- RESTful: Uses the same request/response model as HTTP (GET, POST, PUT, DELETE methods) and URIs to identify resources.
- Asynchronous Message Exchange: Supports both reliable (Confirmable) and unreliable (Non-confirmable) messaging.
- Resource Discovery: Built-in mechanism for discovering available resources on a server (
/.well-known/core). - Observe Functionality: Allows clients to “subscribe” to resource changes, receiving notifications when a resource state is updated (covered in Chapter 118).
- Block-wise Transfers: Supports transferring large payloads in smaller blocks (covered in Chapter 117).
- Security: Can be secured using Datagram Transport Layer Security (DTLS), which provides equivalent security to TLS for datagram protocols.
CoAP vs. HTTP
| Feature | CoAP | HTTP |
|---|---|---|
| Transport | UDP | TCP |
| Overhead | Low (small binary header) | Higher (text-based headers, TCP overhead) |
| Messaging | Asynchronous (CON, NON, ACK, RST) | Synchronous (request-response) |
| Multicast | Supported (via underlying UDP) | Not directly supported (requires extensions) |
| Complexity | Simpler, designed for constrained devices | More complex |
| Proxying | Easily proxied to/from HTTP | Native web protocol |
| Default Port | 5683 (CoAP), 5684 (CoAPS – CoAP+DTLS) | 80 (HTTP), 443 (HTTPS) |
CoAP is not intended to replace HTTP entirely but to provide a “web of things” for devices where HTTP might be too heavy. Gateways can easily translate between CoAP and HTTP, allowing CoAP devices to interact with the broader internet.
sequenceDiagram
actor Client
participant CoAP_Server
participant HTTP_Server
rect rgb(240, 248, 255)
Note over Client, CoAP_Server: CoAP (over UDP - Lightweight)
Client->>CoAP_Server: CON GET /resource<br/>(Small Binary Header + Options + Token)
Note over CoAP_Server: Processes request
CoAP_Server-->>Client: ACK 2.05 Content (Payload)<br/>(Smaller Response)
end
rect rgb(255, 248, 240)
Note over Client, HTTP_Server: HTTP (over TCP - More Overhead)
Client->>HTTP_Server: TCP Handshake (SYN, SYN-ACK, ACK)
Note over Client,HTTP_Server: Establish TCP Connection
Client->>HTTP_Server: GET /resource HTTP/1.1<br/>(Text Headers: Host, User-Agent, etc.)
Note over HTTP_Server: Processes request
HTTP_Server-->>Client: HTTP/1.1 200 OK<br/>(Text Headers: Content-Type, Content-Length, etc. + Payload)
Client->>HTTP_Server: TCP Teardown (FIN, ACK, FIN, ACK)
endCoAP Architecture
CoAP follows a client-server model similar to HTTP.
- CoAP Client: An application that sends a CoAP request to a server.
- CoAP Server: An application that listens for CoAP requests, processes them, and sends CoAP responses. An ESP32 can act as both.
Message Layer:
CoAP messages are exchanged asynchronously over UDP. There are four types of messages:
- Confirmable (CON): These messages require an Acknowledgment (ACK) from the receiver. If an ACK is not received within a timeout, the sender retransmits the CON message. Used for reliable delivery of requests or responses.
- Non-confirmable (NON): These messages do not require an acknowledgment. They are used when reliability is not critical (e.g., repetitive sensor readings where an occasional loss is acceptable). “Fire and forget.”
- Acknowledgement (ACK): Sent in response to a CON message to confirm its receipt. It can be piggybacked with a response payload or sent separately (empty ACK).
- Reset (RST): Sent when a received message (CON or NON) cannot be processed (e.g., malformed, or no context to handle it).
sequenceDiagram
actor C as Client
actor S as Server
Note over C,S: CoAP Message Layer Interactions
%% Flow 1: Confirmable (CON) Request with Piggybacked ACK/Response
rect rgb(173, 216, 230)
Note over C,S: CON Request with Piggybacked ACK/Response
C->>S: CON [MID=123, Token=0x01] GET /temp
Note over S: Processes request...
S-->>C: ACK [MID=123, Token=0x01] 2.05 Content (Payload)
end
%% Flow 2: Confirmable (CON) Request with Separate ACK and CON Response
rect rgb(173, 216, 230)
Note over C,S: CON Request with Separate ACK & Response
C->>S: CON [MID=456, Token=0x02] POST /led
S-->>C: ACK [MID=456] (Empty Acknowledgement)
Note over S: Long processing...
S->>C: CON [MID=789, Token=0x02] 2.04 Changed (Response to POST)
C-->>S: ACK [MID=789] (Acknowledging server's CON response)
end
%% Flow 3: Non-confirmable (NON) Request and NON Response
rect rgb(173, 216, 230)
Note over C,S: NON Request and NON Response
C->>S: NON [MID=101, Token=0x03] GET /status<br/>(e.g., frequent sensor update)
Note over S: Processes request...
S-->>C: NON [MID=102, Token=0x03] 2.05 Content (Payload)<br/>(Server may choose not to respond if NON)
end
%% Flow 4: Confirmable (CON) Request resulting in Reset (RST)
rect rgb(255, 182, 193)
Note over C,S: CON Request resulting in RST
C->>S: CON [MID=202, Token=0x04] GET /unknown_resource<br/>(Malformed or unprocessable)
S-->>C: RST [MID=202] (Indicates message could not be processed)
endEach message has a unique Message ID to correlate ACKs/RSTs with CONs/NONs.
| Message Type | Code (Type field) | Purpose | Reliability |
|---|---|---|---|
| Confirmable (CON) | 0 | Requests or responses that require an acknowledgment. Used for reliable delivery. | Reliable (requires ACK) |
| Non-confirmable (NON) | 1 | Requests or responses where reliability is not critical (e.g., repetitive sensor readings). “Fire and forget.” | Unreliable (no ACK required) |
| Acknowledgement (ACK) | 2 | Sent in response to a CON message to confirm its receipt. Can be empty or piggyback a response. | Confirms CON |
| Reset (RST) | 3 | Sent when a received CON or NON message cannot be processed (e.g., malformed, no context). | Indicates error in processing |
Request/Response Layer:
CoAP requests and responses are embedded within CoAP messages.

- Methods: CoAP supports GET, POST, PUT, DELETE methods, similar to HTTP, for interacting with resources.
- Response Codes: CoAP uses response codes that are semantically similar to HTTP status codes (e.g.,
2.05 Contentis like HTTP200 OK,4.04 Not Foundis like HTTP404 Not Found). CoAP codes are inClass.Detailformat (e.g., 2.xx for Success, 4.xx for Client Error, 5.xx for Server Error).
| CoAP Code (Class.Detail) | Meaning | Example HTTP Equivalent |
|---|---|---|
| 2.01 | Created | 201 Created |
| 2.04 | Changed | 204 No Content (often implies success of PUT/POST) |
| 2.05 | Content | 200 OK (with payload) |
| 4.00 | Bad Request | 400 Bad Request |
| 4.01 | Unauthorized | 401 Unauthorized |
| 4.04 | Not Found | 404 Not Found |
| 4.05 | Method Not Allowed | 405 Method Not Allowed |
| 4.06 | Not Acceptable | 406 Not Acceptable |
| 5.00 | Internal Server Error | 500 Internal Server Error |
| 5.03 | Service Unavailable | 503 Service Unavailable |
- URIs: Resources are identified using CoAP URIs, e.g.,
coap://<host>:<port>/path/to/resource. - Options: CoAP messages can include options (similar to HTTP headers) to convey metadata like
Content-Format,Uri-Path,Uri-Query,Max-Age,ETag, etc.
| Option Name | Number | Purpose | Example Value(s) |
|---|---|---|---|
| Uri-Host | 3 | The host name or IP address of the resource. | “example.com”, “192.168.1.100” |
| ETag | 4 | An opaque identifier for a specific version of a resource. | 0x1234ABCD (binary) |
| If-Match | 1 | Conditional request based on ETag; perform action only if ETag matches. | 0x1234ABCD |
| Uri-Port | 7 | The UDP port number of the resource. | 5683 |
| Uri-Path | 11 | The path segments of the resource URI. Repeatable. | “sensors”, “temperature” |
| Content-Format | 12 | Indicates the media type of the payload. | 0 (text/plain), 50 (application/json) |
| Max-Age | 14 | Maximum time a response is considered fresh (in seconds). Default 60. | 120 (for 2 minutes) |
| Uri-Query | 15 | Query parameters of the resource URI. Repeatable. | “unit=celsius”, “id=123” |
| Accept | 17 | Indicates the desired media type(s) for the response payload. | 50 (application/json) |
| Location-Path | 8 | Specifies the URI path of a created resource (used in 2.01 Created responses). | “new”, “item” |
| Proxy-Uri | 35 | Absolute URI of the resource to be requested from a forward-proxy. | “coap://proxied.example.com/path” |
| Size1 | 60 | Size of the resource representation in a request or response payload, in bytes. | 128 |
Resource Discovery
CoAP clients can discover the resources hosted by a CoAP server by sending a GET request to the well-known URI /.well-known/core. The server responds with a list of its available resources and their attributes, typically in CoRE Link Format (RFC 6690).
Example response to GET /.well-known/core:
</sensors/temperature>;ct=41;rt="temperature-c",
</sensors/humidity>;ct=41;rt="humidity-pct",
</lights/livingroom>;ct=0;rt="actuator"
</path/to/resource>: The URI of the resource.ct: Content-Format code (e.g., 41 for application/vnd.oma.lwm2m+json, 0 for text/plain, 50 for application/json).rt: Resource Type (a semantic description).if: Interface Description.sz: Maximum size estimate.
ESP-IDF esp-coap Component
ESP-IDF provides the esp-coap component, which is a port of the popular libcoap C library. This component simplifies the implementation of CoAP clients and servers on the ESP32.
Key esp-coap (libcoap) Concepts:
- Context (
coap_context_t): Represents a CoAP communication instance. You create a context for your server or client. - Endpoint (
coap_endpoint_t): Represents a network interface and port the server listens on (e.g., UDP port 5683 on all available IP addresses). - Resource (
coap_resource_t): On the server, a resource represents a data source or an actuator that can be interacted with via CoAP. Each resource is associated with one or more handler functions for different request methods (GET, POST, etc.). - PDU (
coap_pdu_t): Protocol Data Unit. Represents a CoAP message (request or response). You construct PDUs to send requests (client) or responses (server), and you parse received PDUs. - Session (
coap_session_t): Represents a communication session between a client and a server. Clients create sessions to send requests. - Handlers: Callback functions you write to process incoming requests for specific resources on the server. Handler signature typically looks like:void resource_handler(coap_resource_t *resource, coap_session_t *session, const coap_pdu_t *request, const coap_string_t *query, coap_pdu_t *response)
Security with DTLS
CoAP itself does not provide encryption. To secure CoAP communication, Datagram Transport Layer Security (DTLS) is used. DTLS provides security features similar to TLS (used by HTTPS) but is adapted for datagram-based protocols like UDP.
sequenceDiagram
actor Client
participant Server
Note over Client,Server: DTLS Handshake (Simplified)
Client->>Server: 1. ClientHello (Proposes Ciphers, Random Bytes)
Server-->>Client: 2. ServerHello (Selects Cipher, Random Bytes)<br>Certificate*<br>ServerKeyExchange*<br>CertificateRequest*<br>ServerHelloDone
Note left of Server: Server sends its certificate and<br>key exchange parameters.<br>May request client certificate.
Client->>Server: 3. Certificate*<br>ClientKeyExchange<br>CertificateVerify*<br>ChangeCipherSpec<br>Finished (Encrypted Handshake Hash)
Note right of Client: Client sends its certificate (if requested),<br>key exchange parameters, and verifies.<br>Switches to encrypted communication.
Server-->>Client: 4. ChangeCipherSpec<br>Finished (Encrypted Handshake Hash)
Note left of Server: Server verifies client messages.<br>Switches to encrypted communication.
rect rgb(209, 250, 229)
Note over Client,Server: Secure DTLS Tunnel Established (CoAPS)
Client->>Server: Encrypted CoAP Request (e.g., CON GET coaps:///secure/data)
Server-->>Client: Encrypted CoAP Response (e.g., ACK 2.05 Content with Encrypted Payload)
end
Note over Client,Server: *Optional messages, depending on authentication method (e.g., PSK vs Certificates)
- CoAPS: Secure CoAP is often referred to as CoAPS and typically runs on UDP port 5684.
esp-coap(libcoap) can be compiled with DTLS support (usually requiring an underlying TLS/DTLS library like Mbed TLS, which is part of ESP-IDF).- DTLS involves certificate management and key exchange, adding complexity and resource overhead (RAM, CPU, flash for certificates).
Note: Implementing full DTLS is an advanced topic. This chapter will primarily focus on unsecured CoAP for simplicity, but it’s crucial to be aware of DTLS for production deployments requiring security.
Practical Examples
These examples will use the esp-coap component. Ensure your ESP-IDF project is configured to include it. If it’s not a default component, you might need to add it from esp-protocols or manage it as an external component. For ESP-IDF v5.x, esp-coap is usually available.
Project Setup:
- Ensure Wi-Fi connectivity is established (STA mode, similar to previous chapters).
- In menuconfig, ensure CoAP support is enabled:idf.py menuconfig -> Component config -> CoAP -> Enable CoAP support (The exact path might vary slightly).You might also need to adjust settings like max PDU size if necessary.
Common main.c includes and Wi-Fi setup:
(Adapt the Wi-Fi setup from Chapter 111/112 as needed.)
#include <string.h>
#include <sys/socket.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "lwip/err.h"
#include "lwip/sys.h"
// For CoAP
#include "coap3/coap.h" // Main libcoap header
static const char *TAG_COAP = "COAP_EXAMPLE";
// (Include your Wi-Fi connection logic here - e.g., wifi_init_sta())
// Assume wifi_init_sta() connects and waits for IP.
// ... (Wi-Fi setup code from previous chapters, including s_wifi_event_group and event_handler) ...
// Remember to get and log the IP address: static esp_ip4_addr_t s_ip_addr;
Example 1: Basic CoAP Server with a GET Resource
This example creates a CoAP server with a simple resource /hello that responds to GET requests.
main.c (CoAP Server Logic):
// ... (includes and Wi-Fi setup from above) ...
#define COAP_DEFAULT_PORT 5683
// Handler for the /hello resource
static void hello_get_handler(coap_resource_t *resource,
coap_session_t *session,
const coap_pdu_t *request,
const coap_string_t *query,
coap_pdu_t *response)
{
ESP_LOGI(TAG_COAP, "Received GET request for /hello");
unsigned char buf[64]; // Buffer for response data
size_t len;
const char *response_data = "Hello from ESP32 CoAP Server!";
response->code = COAP_RESPONSE_CODE_CONTENT; // 2.05 Content
coap_add_option(response, COAP_OPTION_CONTENT_FORMAT,
coap_encode_var_safe(buf, sizeof(buf), COAP_MEDIATYPE_TEXT_PLAIN), buf);
len = strlen(response_data);
coap_add_data(response, len, (const uint8_t *)response_data);
ESP_LOGI(TAG_COAP, "Sent response: %s", response_data);
}
static void coap_server_task(void *pvParameters)
{
coap_context_t *ctx = NULL;
coap_address_t serv_addr;
coap_resource_t *res_hello = NULL;
// Initialize libcoap library
coap_startup();
// coap_set_log_level(COAP_LOG_DEBUG); // Optional: for more verbose logging
// Prepare the server address structure
coap_address_init(&serv_addr);
serv_addr.addr.sin.sin_family = AF_INET;
serv_addr.addr.sin.sin_addr.s_addr = INADDR_ANY; // Listen on all interfaces
serv_addr.addr.sin.sin_port = htons(COAP_DEFAULT_PORT);
// Create CoAP context and endpoint
ctx = coap_new_context(NULL);
if (!ctx || !(coap_new_endpoint(ctx, &serv_addr, COAP_PROTO_UDP))) {
ESP_LOGE(TAG_COAP, "Cannot create CoAP context or endpoint");
goto finish;
}
// Create the /hello resource
res_hello = coap_resource_init(coap_make_str_const("hello"), 0);
if (!res_hello) {
ESP_LOGE(TAG_COAP, "Cannot create /hello resource");
goto finish;
}
coap_register_handler(res_hello, COAP_REQUEST_GET, hello_get_handler);
coap_add_resource(ctx, res_hello);
ESP_LOGI(TAG_COAP, "CoAP server started on port %d", COAP_DEFAULT_PORT);
// Main server loop
while (1) {
// Process CoAP messages, timeout is in milliseconds
// A value of 0 means coap_io_process will block until a packet arrives or an internal event.
// A positive value means it will block for at most that many milliseconds.
// Using a small positive value allows other FreeRTOS tasks to run.
int result = coap_io_process(ctx, 1000);
if (result < 0) {
ESP_LOGE(TAG_COAP, "coap_io_process error");
break;
} else if (result > 0 && result < COAP_IO_WAIT) {
// ESP_LOGD(TAG_COAP, "coap_io_process processed %d events", result);
}
}
finish:
coap_free_context(ctx);
coap_cleanup();
vTaskDelete(NULL);
}
void app_main(void)
{
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
wifi_init_sta(); // Ensure this connects to your Wi-Fi and sets s_ip_addr
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG_COAP, "WiFi Connected. IP: " IPSTR, IP2STR(&s_ip_addr)); // Assuming s_ip_addr is set in wifi_init_sta
xTaskCreate(coap_server_task, "coap_server_task", 8192, NULL, 5, NULL);
} else {
ESP_LOGE(TAG_COAP, "WiFi connection failed. Cannot start CoAP server.");
}
}
graph TD
A[Start: CoAP Server Task Running] --> B{Receive UDP Packet};
B --> C["coap_io_process(ctx)"];
C --> D{Valid CoAP PDU?};
D -- Yes --> E[Parse PDU: Method, Token, URI, Options];
D -- No --> B;
E --> F{Find Matching Resource by URI?};
F -- Yes --> G[Get Registered Handler for Method];
F -- No --> H[Prepare 4.04 Not Found Response PDU];
G --> I{Handler Found?};
I -- Yes --> J[Execute Resource Handler Function];
I -- No --> K[Prepare 4.05 Method Not Allowed PDU];
J --> L[Handler Logic: <br>Process Request, <br>Access Data/Hardware, <br>Prepare Response Data];
L --> M["Create Response PDU (e.g., 2.05 Content)"];
M --> N[Add Options & Payload to Response PDU];
N --> O["Send Response PDU (coap_send)"];
H --> O;
K --> O;
O --> P[End Request Cycle / Continue coap_io_process];
P --> B;
%% Styling
classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px
classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
classDef endNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
classDef errorNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
class A startNode;
class B,C,E,G,J,L,M,N,O processNode;
class D,F,I decisionNode;
class H,K errorNode;
class P endNode;
Build Instructions:
- Save as
main/main.c. Update Wi-Fi credentials. - Ensure
esp-coapis correctly linked. Inmain/CMakeLists.txt:idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES esp_wifi esp_event nvs_flash esp_netif lwip esp-coap) # Add esp-coap if not pulled in automaticallyOften, just including"coap3/coap.h"and linkinglwipis enough ifesp-coapis a standard component. If you addedesp-protocolsas an extra component, you might need to specifyesp_coapinPRIV_REQUIRESorREQUIRES. - Build:
idf.py build
Run/Flash/Observe Steps:
- Flash:
idf.py -p /dev/ttyUSB0 flash. - Monitor:
idf.py -p /dev/ttyUSB0 monitor. Note the ESP32’s IP address. - Use a CoAP client tool (e.g.,
libcoap-utils-binon Linux which providescoap-client, or a browser plugin like CoAP Simple Client for Chrome/Firefox, or a standalone tool like Copper (Cu)).- Command line example:
coap-client -m get coap://<ESP32_IP_ADDRESS>/hello - You should see “Hello from ESP32 CoAP Server!” as the response.
- Command line example:
Example 2: CoAP Server with POST/PUT Resource (LED Control)
This example adds a resource /led to control an LED.
GET /led: Returns current LED state ({"state":"on"}or{"state":"off"}).PUT /ledorPOST /led: Sets LED state. Payload:onoroff(text/plain) or{"state":"on"}(application/json).
main.c (Add LED control logic and handlers):
// ... (includes, Wi-Fi, coap_server_task structure from Example 1) ...
#include "driver/gpio.h"
#define LED_GPIO GPIO_NUM_2 // Or your LED GPIO
static bool led_state = false; // false = OFF, true = ON
static void configure_led(void) {
gpio_reset_pin(LED_GPIO);
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
gpio_set_level(LED_GPIO, led_state);
}
// Handler for GET /led
static void led_get_handler(coap_resource_t *resource,
coap_session_t *session,
const coap_pdu_t *request,
const coap_string_t *query,
coap_pdu_t *response)
{
ESP_LOGI(TAG_COAP, "Received GET request for /led");
unsigned char buf[32];
response->code = COAP_RESPONSE_CODE_CONTENT;
coap_add_option(response, COAP_OPTION_CONTENT_FORMAT,
coap_encode_var_safe(buf, sizeof(buf), COAP_MEDIATYPE_APPLICATION_JSON), buf);
snprintf((char*)buf, sizeof(buf), "{\"state\":\"%s\"}", led_state ? "on" : "off");
coap_add_data(response, strlen((char*)buf), buf);
}
// Handler for PUT or POST /led
static void led_put_post_handler(coap_resource_t *resource,
coap_session_t *session,
const coap_pdu_t *request,
const coap_string_t *query,
coap_pdu_t *response)
{
ESP_LOGI(TAG_COAP, "Received PUT/POST request for /led");
size_t len;
const uint8_t *data;
bool new_state_set = false;
if (coap_get_data(request, &len, &data)) {
ESP_LOGI(TAG_COAP, "Payload (len %d): %.*s", len, len, data);
if (len == 2 && strncmp((const char*)data, "on", 2) == 0) {
led_state = true;
new_state_set = true;
} else if (len == 3 && strncmp((const char*)data, "off", 3) == 0) {
led_state = false;
new_state_set = true;
}
// Add JSON parsing if you expect JSON payload like {"state":"on"}
}
if (new_state_set) {
gpio_set_level(LED_GPIO, led_state);
ESP_LOGI(TAG_COAP, "LED state changed to: %s", led_state ? "ON" : "OFF");
response->code = COAP_RESPONSE_CODE_CHANGED; // 2.04 Changed
} else {
ESP_LOGW(TAG_COAP, "Invalid payload for /led");
response->code = COAP_RESPONSE_CODE_BAD_REQUEST; // 4.00 Bad Request
}
}
// Modify coap_server_task:
static void coap_server_task(void *pvParameters)
{
coap_context_t *ctx = NULL;
coap_address_t serv_addr;
coap_resource_t *res_hello = NULL;
coap_resource_t *res_led = NULL; // New resource for LED
coap_startup();
// ... (address init, context creation as in Example 1) ...
ctx = coap_new_context(NULL);
if (!ctx || !(coap_new_endpoint(ctx, &serv_addr, COAP_PROTO_UDP))) { /* ... error handling ... */ goto finish; }
// Create /hello resource (from Example 1)
res_hello = coap_resource_init(coap_make_str_const("hello"), 0);
if (!res_hello) { /* ... error handling ... */ goto finish; }
coap_register_handler(res_hello, COAP_REQUEST_GET, hello_get_handler);
coap_add_resource(ctx, res_hello);
// Create /led resource
res_led = coap_resource_init(coap_make_str_const("led"), 0);
if (!res_led) {
ESP_LOGE(TAG_COAP, "Cannot create /led resource");
goto finish;
}
coap_register_handler(res_led, COAP_REQUEST_GET, led_get_handler);
coap_register_handler(res_led, COAP_REQUEST_PUT, led_put_post_handler);
coap_register_handler(res_led, COAP_REQUEST_POST, led_put_post_handler); // Handle POST same as PUT
coap_add_resource(ctx, res_led);
// Add /.well-known/core resource for discovery
coap_add_well_known_core(ctx);
ESP_LOGI(TAG_COAP, "CoAP server started with /hello and /led resources.");
// ... (while loop for coap_io_process as in Example 1) ...
while (1) { /* ... coap_io_process ... */ }
finish:
coap_free_context(ctx);
coap_cleanup();
vTaskDelete(NULL);
}
// In app_main:
// Call configure_led(); after NVS and before Wi-Fi init.
// ...
// if (bits & WIFI_CONNECTED_BIT) {
// configure_led(); // Initialize LED GPIO
// ESP_LOGI(TAG_COAP, "WiFi Connected. IP: " IPSTR, IP2STR(&s_ip_addr));
// xTaskCreate(coap_server_task, "coap_server_task", 8192, NULL, 5, NULL);
// }
// ...
Build, Flash, Observe:
- Add
configure_led()call. - Build, flash, monitor.
- Test with a CoAP client:
coap-client -m get coap://<ESP32_IP_ADDRESS>/led(Should return JSON state)coap-client -m put -t text/plain -e "on" coap://<ESP32_IP_ADDRESS>/led(Turn LED on)coap-client -m post -t text/plain -e "off" coap://<ESP32_IP_ADDRESS>/led(Turn LED off)coap-client -m get coap://<ESP32_IP_ADDRESS>/.well-known/core(Discover resources)
Example 3: Basic CoAP Client on ESP32
This example shows the ESP32 acting as a CoAP client to send a GET request to a public CoAP test server (e.g., coap://coap.me).
main.c (CoAP Client Logic):
// ... (includes and Wi-Fi setup) ...
// CoAP client response handler
static void client_response_handler(coap_session_t *session,
const coap_pdu_t *received,
const coap_pdu_t *sent, // The request PDU that was sent
const coap_mid_t id)
{
size_t len;
const uint8_t *databuf;
unsigned char type = 0; // To store content type
ESP_LOGI(TAG_COAP, "Client: Got response code %d.%02d",
COAP_RESPONSE_CLASS(received->code), COAP_RESPONSE_CODE(received->code));
if (COAP_RESPONSE_CLASS(received->code) == 2) { // Success class
if (coap_get_data(received, &len, &databuf)) {
ESP_LOGI(TAG_COAP, "Client: Received %d bytes: %.*s", len, (int)len, databuf);
}
// Check content type option
coap_get_option(received, COAP_OPTION_CONTENT_FORMAT, &type, NULL);
ESP_LOGI(TAG_COAP, "Client: Content-Type: %u", type);
}
}
static void coap_client_task(void *pvParameters)
{
coap_context_t *ctx = NULL;
coap_session_t *session = NULL;
coap_address_t dst_addr;
coap_pdu_t *pdu = NULL; // Request PDU
int result;
// Target CoAP server URI (e.g., a public test server)
const char *server_uri_str = "coap://coap.me/hello"; // Example public server
// const char *server_uri_str = "coap://<YOUR_ESP32_SERVER_IP>/hello"; // To test against its own server
coap_uri_t uri;
if (coap_split_uri((const uint8_t *)server_uri_str, strlen(server_uri_str), &uri) == -1) {
ESP_LOGE(TAG_COAP, "Client: Invalid CoAP URI: %s", server_uri_str);
goto finish_client;
}
coap_startup();
// coap_set_log_level(COAP_LOG_DEBUG);
// Resolve destination address
// Note: In a real application, you'd use DNS or a fixed IP.
// For coap.me, you might need to resolve its IP first.
// For simplicity, if testing against local server, use its IP.
// If coap.me, its IP is dynamic. Let's assume you know an IP for a test server.
// For coap.me, one IP is 54.172.106.115 (this can change!)
coap_address_init(&dst_addr);
// Example: If testing against local server at s_ip_addr
// dst_addr.addr.sin.sin_family = AF_INET;
// dst_addr.addr.sin.sin_addr.s_addr = s_ip_addr.addr; // Assuming s_ip_addr is ESP32's IP
// dst_addr.addr.sin.sin_port = htons(COAP_DEFAULT_PORT);
// For a public server like coap.me, you'd typically use DNS.
// For this example, let's hardcode an IP for a generic test or assume local.
// If using coap.me, find its current IP.
// This part needs proper address resolution for external servers.
// For now, let's assume we are targeting the ESP32 itself if it's running the server.
if (s_ip_addr.addr == 0) { // Check if local IP is set
ESP_LOGE(TAG_COAP, "Client: Local IP not set, cannot target self. Exiting client task.");
goto finish_client;
}
dst_addr.addr.sin.sin_family = AF_INET;
dst_addr.addr.sin.sin_addr.s_addr = s_ip_addr.addr; // Targetting self for demo
dst_addr.addr.sin.sin_port = htons(COAP_DEFAULT_PORT);
// Create CoAP client context and session
ctx = coap_new_context(NULL);
if (!ctx) { ESP_LOGE(TAG_COAP, "Client: Cannot create context"); goto finish_client; }
session = coap_new_client_session(ctx, NULL, &dst_addr, COAP_PROTO_UDP);
if (!session) { ESP_LOGE(TAG_COAP, "Client: Cannot create client session"); goto finish_client; }
coap_register_response_handler(ctx, client_response_handler);
// Create GET PDU
pdu = coap_new_pdu(COAP_MESSAGE_CON, COAP_REQUEST_CODE_GET, coap_new_message_id(session));
if (!pdu) { ESP_LOGE(TAG_COAP, "Client: Cannot create PDU"); goto finish_client; }
// Add URI path option from parsed URI
if (uri.path.length) {
coap_add_option(pdu, COAP_OPTION_URI_PATH, uri.path.length, uri.path.s);
}
// Add other options like Uri-Query if uri.query.length > 0
ESP_LOGI(TAG_COAP, "Client: Sending GET request to %s", server_uri_str);
result = coap_send(session, pdu); // coap_send will free the pdu
if (result == COAP_INVALID_MID) {
ESP_LOGE(TAG_COAP, "Client: coap_send failed");
// coap_delete_pdu(pdu); // Only if coap_send fails and doesn't take ownership
} else {
// Wait for response (or timeout) by processing I/O
// This loop is for a single request-response. For multiple, structure differently.
int wait_ms = COAP_DEFAULT_RESPONSE_TIMEOUT * 1000; // Default is ~2s
while(wait_ms > 0) {
result = coap_io_process(ctx, 100); // Process for 100ms
if (result >= 0 ) {
wait_ms -=100;
} else {
break; // Error in processing
}
// A more robust client would check if a response for the specific MID was received.
}
}
finish_client:
coap_free_session(session);
coap_free_context(ctx);
coap_cleanup();
ESP_LOGI(TAG_COAP, "CoAP client task finished.");
vTaskDelete(NULL);
}
// In app_main, after Wi-Fi is connected and server (if any) is started:
// You might want to run the client task after a delay or based on an event.
// ...
// if (bits & WIFI_CONNECTED_BIT) {
// // ... (start server task if needed) ...
// ESP_LOGI(TAG_COAP, "WiFi Connected. Starting CoAP client task in 5 seconds...");
// vTaskDelay(pdMS_TO_TICKS(5000)); // Give server time to start if testing locally
// xTaskCreate(coap_client_task, "coap_client_task", 4096, NULL, 5, NULL);
// }
// ...
Build, Flash, Observe:
- Modify
app_mainto launchcoap_client_task. - If testing against the ESP32’s own server (from Example 1 or 2), ensure the server task is also started and
server_uri_strpoints tocoap://<ESP32_LOCAL_IP>/hello. - Build, flash, monitor. You should see client logs for sending the request and receiving the response.
Variant Notes
- Memory (RAM & Flash):
libcoapitself has a memory footprint. Complex CoAP applications with many resources, active sessions, or DTLS enabled will consume more RAM.- Storing DTLS certificates and keys consumes flash memory.
- ESP32-C3, C6, H2, and S2 have less RAM/flash than ESP32/S3. For these, keep CoAP resource definitions minimal and consider if DTLS is feasible given other application needs.
- Processing Power:
- Basic CoAP operations are lightweight.
- DTLS handshakes and encryption/decryption are CPU-intensive. Hardware cryptographic acceleration on ESP32 variants helps significantly, but frequent DTLS handshakes on single-core variants (S2, C3, C6, H2) can impact performance.
- Network Stack: CoAP relies on the underlying UDP/IP stack (lwIP on ESP32). Ensure lwIP is configured with sufficient buffers if handling many concurrent CoAP sessions or large block-wise transfers, though default CoAP usage is typically low-bandwidth.
Common Mistakes & Troubleshooting Tips
| Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
|---|---|---|
| Firewall Issues | CoAP client cannot reach the server, or responses are not received. Timeouts. | Ensure UDP port 5683 (CoAP) and 5684 (CoAPS/DTLS) are not blocked by firewalls on the client, server, or any intermediate network devices. Check router/OS firewall settings. |
| Incorrect Message Type Usage (CON vs. NON) | Critical requests are lost (if using NON). Excessive network traffic and server load due to unnecessary ACKs (if overusing CON for non-critical data). | Use CON for requests/responses that must be reliably delivered (e.g., commands, critical status updates). Use NON for frequent sensor data or other updates where occasional loss is acceptable. |
| Resource Handler Not Being Called | Client sends a request, but the server doesn’t seem to process it for that specific resource. Client might receive a 4.04 Not Found or timeout. |
1. Verify URI path: Client URI must exactly match server’s coap_resource_init() path. 2. Correct Method Handler: Ensure coap_register_handler() was called for the correct method (e.g., COAP_REQUEST_GET). 3. Resource Added: Check that coap_add_resource() was called to add the resource to the CoAP context. 4. Server Loop: Ensure coap_io_process() is being called regularly and frequently in the server’s main loop/task. |
| DTLS Handshake Failures (if using CoAPS) | Client and server fail to establish a secure DTLS session. Connection errors, no secure communication. |
1. DTLS Support: Ensure both client and server are compiled with DTLS support and correctly configured (certificates, keys, pre-shared keys if used). 2. Certificate Validity: Verify certificates are valid, not expired, and the CA is trusted by both parties (if using certificate-based auth). 3. Cipher Suites: Check for matching and supported cipher suites. 4. Logging: Enable detailed logging in libcoap and Mbed TLS for diagnostics. |
| Client Not Receiving Responses or Timing Out | CoAP client sends a CON request but never receives an ACK or response payload. |
1. Network/Firewall: Re-check connectivity and firewalls (see “Firewall Issues”). 2. Server Status: Ensure the server is running and its coap_io_process() loop is active. 3. Server ACK: Verify the server is sending an ACK (empty or piggybacked) for CON requests. 4. Client Timeout: The client’s response timeout (COAP_DEFAULT_RESPONSE_TIMEOUT in libcoap, typically a few seconds) might be too short for network latency or server processing time. Consider increasing if appropriate. |
| Incorrect PDU Construction / Parsing | Server or client receives malformed messages, leading to RST messages or unexpected behavior. Options not set/read correctly. |
1. Double-check option adding/reading logic (coap_add_option(), coap_get_option()). 2. Ensure correct data types and lengths when adding data/options. 3. Use debugging tools (Wireshark with CoAP dissector) to inspect PDU contents. |
| Memory Leaks or Corruption | Application crashes, behaves erratically over time, or runs out of memory. |
1. Properly free PDUs (coap_delete_pdu() if not sent, coap_send() usually takes ownership), sessions (coap_free_session()), contexts (coap_free_context()), resources (coap_delete_resource() – though often managed by context). 2. Be careful with string lifecycles, especially with coap_make_str_const() vs. dynamically allocated strings. 3. Check buffer sizes for payloads and options. |
Exercises
- CoAP Thermostat Resource:
- On the ESP32 CoAP server, create a resource
/thermostat/target_temp. - Implement a GET handler to return the current target temperature (e.g., in JSON:
{"target": 22.5}). - Implement a PUT handler that accepts a plain text float value (e.g., “23.0”) to set a new target temperature. Store this value in a global variable.
- Test with a CoAP client tool.
- On the ESP32 CoAP server, create a resource
- CoAP Resource Discovery Implementation:
- Ensure your CoAP server (from Example 1 or 2) correctly responds to
GET /.well-known/core. - Add attributes to your existing
/helloand/ledresources (e.g.,rt="greeting"for hello,rt="actuator" if="control"for led) usingcoap_resource_set_attr(). - Verify the response using a CoAP client.
- Ensure your CoAP server (from Example 1 or 2) correctly responds to
- ESP32 CoAP Client for Local LED Control:
- Run the CoAP server with the
/ledresource (Example 2) on one ESP32 (ESP32-Server). - On a second ESP32 (ESP32-Client), implement a CoAP client that:
- Sends a
GETrequest tocoap://<ESP32-Server_IP>/ledto query the LED state. - Sends a
PUTrequest tocoap://<ESP32-Server_IP>/ledwith payload “on” or “off” based on a button press on ESP32-Client. - Log the responses and observe the LED on ESP32-Server.
- Sends a
- Run the CoAP server with the
Summary
- CoAP is a lightweight, UDP-based RESTful protocol designed for constrained IoT devices, offering lower overhead than HTTP.
- It uses CON, NON, ACK, RST message types for asynchronous communication and methods like GET, POST, PUT, DELETE for resource interaction.
- URIs identify resources, and
/.well-known/coreenables resource discovery. - The ESP-IDF
esp-coapcomponent (libcoap) provides APIs for building CoAP servers and clients on ESP32. - CoAP servers define resources and handlers to process incoming requests.
- CoAP clients create sessions and PDUs to send requests and handle responses.
- DTLS can be used to secure CoAP communication (CoAPS).
- Resource constraints (RAM, flash, CPU) on ESP32 variants should be considered, especially when using DTLS or managing many resources/sessions.
Further Reading
- RFC 7252: The Constrained Application Protocol (CoAP):
- libcoap Official Documentation:
- https://libcoap.net/doc/ (The
esp-coapcomponent is based on this)
- https://libcoap.net/doc/ (The
- ESP-IDF
esp-coapExamples https://components.espressif.com/components/espressif/coap/versions/4.3.1~3/examples/coap_server?language= - RFC 6690: Constrained RESTful Environments (CoRE) Link Format:
- https://datatracker.ietf.org/doc/html/rfc6690 (For
/.well-known/coreformat)
- https://datatracker.ietf.org/doc/html/rfc6690 (For
- DTLS (Datagram Transport Layer Security):
- RFC 6347: Datagram Transport Layer Security Version 1.2

