Chapters

22 CoAP Lab: Stack and Trace Contracts

coap
implementation

22.1 Start With the Decision

This section: Start With the Decision — A resource is the named item a client can read or change.

A resource is the named item a client can read or change. Observe is a way to subscribe to changes in that item. A trace records the messages in order. The client asks; the server handles the request. The lab goal is not to memorize every library call. Start with one resource that can be read, one setting that can be changed, one Observe stream that can be checked, and one trace that proves the client and server agree.

22.2 Route Overview

This section: Route Overview — This is part 1 of 2.

This is part 1 of 2. Continue with CoAP Lab: Message Construction.

22.3 Part Objectives

This section: Part Objectives

  • Trace one CoAP request and its response through the client and server, including errors and Observe updates.
  • Validate arduino esp32 coap client with a concrete scenario and pass criteria.
In 60 Seconds

Carry One Request Into a Physical Result

Picture a leak sensor asking a controller to close a small test valve. A successful response code can still arrive before the output moves, after a duplicate, or with content the application did not accept.

An actuator is a device that turns an electrical command into physical action. CoAP means the compact request-and-response method used here. A payload is the useful content carried inside a message.

Give one request an identity, delay its reply, drop it, repeat it, send malformed content, and restart the controller. Record request, response, parser state, output state, and time together. Keep the real valve in a safe test condition.

This runway does not prove field delivery or secure identity. The labs below show how to build, observe, repeat, reject, and bound each request path.

This lab chapter provides working code for CoAP servers and clients: Python implementations using aiocoap with async resource handlers, Arduino ESP32 implementations using the coap-simple library, and techniques for handling GET/PUT/observe requests and parsing response codes. Both platforms demonstrate RESTful IoT communication patterns ready to adapt for your own sensor and actuator projects.

22.4 Start With One Working Sensor

This section: Start With One Working Sensor — The lab goal is not to memorize every library call.

The lab goal is not to memorize every library call. Start with one resource that can be read, one setting that can be changed, one Observe stream that can be checked, and one trace that proves the client and server agree.

From there, each code block has a job in the story: create the resource, send the request, inspect the response code, measure the bytes, then compare reliability, battery, and security choices before adapting the pattern to hardware.

Chapter Roadmap
  • In 60 Seconds

  • Start With One Working Sensor

  • Key Concepts

  • Prerequisites

  • Related Chapters

  • For Beginners: How to Use These Labs

  • Continue: CoAP Implementation Stack and Trace Contracts

  • Python CoAP Server

  • Putting Numbers to It

  • Interactive Calculator: CoAP vs HTTP Message Overhead

  • Interactive Calculator: Battery Life Estimator

  • Interactive Tool: CoAP Response Code Lookup

  • Try It: CoAP Resource Tree Explorer

  • Checkpoint: Server Resource Contract

  • Python CoAP Client

  • Try It: Observe Pattern Timeline Simulator

  • Try It: CoAP-Style REST Sensor on ESP32

  • Checkpoint: Client Exchange Evidence

  • Arduino ESP32 CoAP Client

  • First: build the Python server resource contract, so /temperature, /config, and /readings each have a method, payload, and response code you can test.

  • Then: run Python clients for GET, PUT, POST, and Observe, and keep the response trace beside the code instead of trusting the library call alone.

  • Next: move the same contract to ESP32 examples, where Message ID, Token, payload bytes, and callback behavior become the evidence you inspect.

  • Finally: use response codes, Token-versus-MID correlation, and the consolidation checks to decide whether the lab is ready for constrained-network testing.

Checkpoints pause after each implementation layer; collapsed deep dives and interactives are optional evidence builders.

22.5 Learning Objectives

This section: Learning Objectives

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

  • Construct CoAP Servers: Implement Python CoAP servers with async resource handlers using aiocoap, registering GET, PUT, and POST endpoints
  • Develop CoAP Clients: Create Python clients that send GET, PUT, POST, and Observe requests and process response payloads
  • Configure Embedded CoAP: Adapt and deploy CoAP client code on Arduino ESP32 using the coap-simple library
  • Distinguish Response Codes: Interpret CoAP Class.Detail response codes (2.xx, 4.xx, 5.xx) and select the correct code for each operation
  • Diagnose Token vs MID Correlation: Explain why Token-based matching is required for Observe notifications and justify its use over Message ID matching
  • Calculate Protocol Overhead: Compare CoAP and HTTP message sizes and assess the energy savings for battery-powered IoT deployments

Key Concepts

This section: Key Concepts

  • CoAP: Constrained Application Protocol — REST-style request/response protocol using UDP instead of TCP
  • Confirmable Message (CON): Requires ACK from recipient — provides reliable delivery over UDP at the cost of one roundtrip
  • Non-confirmable Message (NON): Fire-and-forget UDP datagram — lowest latency, no delivery guarantee
  • Observe Option: CoAP extension enabling publish/subscribe: client registers to receive notifications on resource changes
  • Block-wise Transfer: Fragmentation mechanism for transferring payloads larger than a single CoAP datagram
  • Token: Client-generated value matching responses to requests — enables concurrent request/response pairing
  • DTLS: Datagram TLS — CoAP’s security layer providing encryption and authentication over UDP

22.6 Prerequisites

This section: Prerequisites

Before diving into this chapter, you should be familiar with:

  • CoAP Methods and Patterns: Understanding of GET/POST/PUT/DELETE methods and CON/NON message types
  • CoAP Fundamentals: Knowledge of CoAP message structure, tokens, and options
  • Python asyncio basics: Familiarity with async/await syntax for the aiocoap examples
  • Arduino development: Basic ESP32 programming for the embedded examples

CoAP Series:

Practical Development:

If you can install Python packages:

  1. Install aiocoap: pip install aiocoap
  2. Run the server in one terminal
  3. Run the client in another terminal
  4. Observe the request/response in both consoles

If you cannot install software:

  • Read through the code to understand the patterns
  • Focus on the message structure and response codes
  • Use the Wokwi simulation in the Advanced Features Lab

Key code patterns to understand:

  • async def render_get() - How servers handle GET requests
  • Message(code=Code.GET, uri='coap://...') - How clients build requests
  • await protocol.request(request).response - How clients wait for responses

22.7 Continue: CoAP Implementation Stack and Trace Contracts

This section: Continue: CoAP Implementation Stack and Trace Contracts — The main lab below stays focused on runnable Python and ESP32 examples.

The main lab below stays focused on runnable Python and ESP32 examples. For the deeper implementation contract behind stack selection, URI+method handler mapping, response-code and payload evidence, Token-based correlation, CON/NON policy, proxy freshness, and DTLS session reuse, continue to CoAP Implementation Stack and Trace Contracts.

22.8 Python CoAP Server

This section: Python CoAP Server

This server exposes a /temperature resource that returns a simulated reading:

22.8.1 Step 1: Start the CoAP resource on localhost

  • In a project-local Python target or isolated environment, install aiocoap, then start the server code below in one terminal.
  • Watch for CoAP server started on port 5683 and Resources: /temperature (GET, PUT) before opening a client terminal.
  • In the local run here, an equivalent aiocoap resource bound to 127.0.0.1:56830 logged server GET token=... payload=b''; the site registration supplies /temperature.
  • Keep the bind address and port in the run record; localhost proves an application exchange but no wireless link, DTLS, or remote reachability.
  • The code below registers only /temperature; /config, /readings, and Observe need additional handlers before those clients can succeed.
import asyncio
from aiocoap import *
import aiocoap.resource as resource

class TemperatureResource(resource.Resource):
    """Example resource for temperature readings"""

    async def render_get(self, request):
        """Handle GET requests - returns current temperature"""
        temperature = 22.5  # Simulated sensor reading

        payload = f"{temperature}".encode('utf-8')

        return Message(
            code=Code.CONTENT,      # 2.05 Content (success)
            payload=payload,
            content_format=0        # text/plain
        )

    async def render_put(self, request):
        """Handle PUT requests to update configuration"""
        print(f'Received PUT: {request.payload}')

        # In real implementation, parse and apply config
        return Message(code=Code.CHANGED)  # 2.04 Changed

async def main():
    # Create CoAP server with resource tree
    root = resource.Site()

    # Add temperature resource at /temperature
    root.add_resource(
        ['temperature'],
        TemperatureResource()
    )

    # Start server on default CoAP port (5683)
    await Context.create_server_context(root)

    print("CoAP server started on port 5683")
    print("Resources: /temperature (GET, PUT)")

    # Keep server running
    await asyncio.get_running_loop().create_future()

if __name__ == "__main__":
    asyncio.run(main())

The server above registers /temperature only. The /config PUT and /readings POST clients below need matching server resource handlers before they can succeed. To receive Observe updates, make the temperature handler an ObservableResource and call updated_state() after a value changes; the plain Resource above does not register an observation.

Install and run:

pip install aiocoap
python coap_server.py

CoAP’s efficiency shines in power-constrained deployments. Consider a battery-powered temperature sensor using this server pattern:

Message overhead analysis:

  • CoAP request: 4-byte header + 2-byte token + 5-byte Uri-Path option (1-byte header + 4-byte temp value) = 11 bytes
  • Response: 4-byte header + 2-byte token + 1-byte marker + 4-byte payload (22.5) = 11 bytes
  • Total exchange: 22 bytes

Compare to HTTP:

  • HTTP request: GET /temp HTTP/1.1\r\nHost: server\r\n\r\n = 36 bytes
  • Response: HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n22.5 = 42 bytes
  • Total: 78 bytes (about 3.5× larger)

Ideal payload-only transmission time (assumed 250 kbps link, excluding transport framing, handshakes, losses and idle listening):

  • CoAP: tCoAP=22×8250,000=0.704t_{CoAP} = \frac{22 \times 8}{250,000} = 0.704 ms
  • HTTP: tHTTP=78×8250,000=2.496t_{HTTP} = \frac{78 \times 8}{250,000} = 2.496 ms

Battery impact (CR2032, 225 mAh @ 3V):

  • CoAP radio cost: 0.704 ms × 20 mA = 3.91 nAh per reading
  • HTTP radio cost: 2.496 ms × 20 mA = 13.87 nAh per reading
  • At 1 reading/minute: CoAP daily transmit-only charge = 5,632 nAh/day vs HTTP = 19,968 nAh/day
  • Battery life difference: The idealized transmit-only charge quotient is about 3.5× as long (≈39,950 days vs ≈11,268 days, i.e. ≈109.5 years vs ≈30.9 years — real-world life is limited by CR2032 self-discharge and MCU sleep current, not this radio-only figure)

Interactive Calculator: CoAP vs HTTP Message Overhead

Try This:

  • Adjust payload size to see how overhead percentage changes
  • Notice how CoAP’s fixed 4-byte header maintains efficiency even with small payloads
  • Compare with/without HTTP headers; the no-header setting is a framing-only estimate, not a valid HTTP/1.1 request
Interactive Calculator: Ideal Transmit-Only Charge

Try This:

  • Increase data rate to see how frequent transmissions increase the modeled transmit charge
  • Compare different battery capacities (CR2032 vs larger cells)
  • Notice how radio rate changes both ideal airtimes; these quotients omit receive, handshake, sleep, sensing, and battery ageing costs
Interactive Tool: CoAP Response Code Lookup

Try This:

  • Select different response codes to understand when each is used
  • Notice the pattern: 2.xx = success, 4.xx = client error, 5.xx = server error
  • Compare CoAP codes to their HTTP equivalents

22.8.2 Adding Multiple Resources

The next implementation extends one server with two different resource contracts. ConfigResource supports GET for the current configuration and PUT for an idempotent update, returning Code.CHANGED only after decoded JSON has been applied. ReadingsResource is POST-only because each payload adds a reading rather than replacing the collection. As you inspect the code, keep routing, method semantics, Content-Format 50, validation, and response codes aligned; sharing a server process does not make the resources interchangeable.

class ConfigResource(resource.Resource):
    """Configuration resource with GET and PUT"""

    def __init__(self):
        super().__init__()
        self.config = {"interval": 60, "threshold": 25.0}

    async def render_get(self, request):
        import json
        payload = json.dumps(self.config).encode('utf-8')
        return Message(
            code=Code.CONTENT,
            payload=payload,
            content_format=50  # application/json
        )

    async def render_put(self, request):
        import json
        try:
            new_config = json.loads(request.payload.decode('utf-8'))
            self.config.update(new_config)
            print(f'Config updated: {self.config}')
            return Message(code=Code.CHANGED)
        except Exception as e:
            return Message(code=Code.BAD_REQUEST)

class ReadingsResource(resource.Resource):
    """POST-only resource to receive sensor readings"""

    async def render_post(self, request):
        import json
        try:
            reading = json.loads(request.payload.decode('utf-8'))
            print(f'New reading: {reading}')
            # Store reading in database...
            return Message(code=Code.CREATED)  # 2.01 Created
        except Exception:
            return Message(code=Code.BAD_REQUEST)  # 4.00 Bad Request

# In main():
root.add_resource(['config'], ConfigResource())
root.add_resource(['readings'], ReadingsResource())
Try It: CoAP Resource Tree Explorer

Preview how a URI path appears in the CoAP resource tree and how it maps to root.add_resource() in Python.

22.9 Broker Bex, Messaging and broker domain expertCheckpoint: Server Resource Contract

  • You now know how an aiocoap resource class turns a URI path into a handler method and a payload.
  • You can tell which operation should return Code.CONTENT, Code.CHANGED, Code.CREATED, or Code.BAD_REQUEST.
  • You have a resource tree that separates readable state, configurable state, and submitted readings before any client code runs.

With the server mapped, the client examples become trace recipes: request, response code, payload, and failure branch.

22.10 Python CoAP Client

This section: Python CoAP Client

22.10.1 Basic GET Request

Hands-on lab · 75 min · Contiki-NG Cooja

Request a temperature resource across a constrained mesh

I have explained resource requests; now let's run GET and PUT through a relay and compare delivery after a link loss.

Open the lab

22.10.2 Step 2: Send GET and inspect the response

  • In a second Python terminal, point the GET client below at the listening server and run it once.
  • The executed localhost exchange returned client GET -> 2.05 Content payload=b'22.5' from a real aiocoap server and client.
  • Compare the server token with the client’s request context and confirm the response payload decodes to 22.5.
  • Record the path, response code, content format, token, and payload; a successful Python print alone does not prove a separate ESP32 implementation.
  • If the server is absent, preserve the timeout or connection error instead of substituting the expected 2.05 line.
import asyncio
from aiocoap import *

async def get_temperature():
    """Fetch temperature from CoAP server"""

    # Create CoAP client protocol
    protocol = await Context.create_client_context()

    # Build GET request
    request = Message(
        code=Code.GET,
        uri='coap://localhost/temperature'
    )

    try:
        # Send request and wait for response
        response = await protocol.request(request).response

        print(f'Response Code: {response.code}')
        if response.code.is_successful():
            print(f'Temperature: {response.payload.decode("utf-8")}°C')
        else:
            print(f'GET failed: {response.code}')

    except Exception as e:
        print(f'Failed to fetch: {e}')

# Run client
asyncio.run(get_temperature())

22.10.3 PUT Request for Configuration

22.10.4 Step 3: Match the PUT path to a registered handler

  • The client below targets /config; first inspect the server resource tree, which currently registers only /temperature.
  • The local run sent PUT to /temperature and observed server PUT payload=b'23.5' followed by client PUT -> 2.04 Changed.
  • A separate local GET /config returned 4.04 Not Found, confirming that the published /config example needs its own server handler.
  • Add a /config handler in your lab copy, run the client, then read back the stored interval to prove the state actually changed.
  • Record the failed and successful paths distinctly; a 2.04 from /temperature cannot be used as evidence for /config.
async def update_config():
    """Send PUT request to update configuration"""

    protocol = await Context.create_client_context()

    request = Message(
        code=Code.PUT,
        uri='coap://localhost/config',
        payload=b'{"interval": 30}'
    )

    response = await protocol.request(request).response
    print(f'Update result: {response.code}')

    if response.code.is_successful():
        print('Configuration updated successfully')
    else:
        print(f'Update failed: {response.code}')

asyncio.run(update_config())

22.10.5 POST Request to Send Data

22.10.6 Step 4: Register and verify the reading endpoint

  • The POST client below sends JSON to /readings; inspect its application/json content format and encoded payload before sending.
  • Against the chapter’s unextended server, the local aiocoap run returned client POST /readings -> 4.04 Not Found.
  • Add a /readings POST handler to your lab server, validate the sensor ID and timestamp, and return 2.01 Created only after storing a reading.
  • Match the sent sensor ID with a server-side stored record or receipt; a client-side “submitted” print is insufficient evidence of persistence.
  • The endpoint extension was not run here, so 2.01 Created remains the expected result after the learner implements the handler.
import json

async def send_reading():
    """POST a sensor reading to the server"""

    protocol = await Context.create_client_context()

    reading = {
        "sensor_id": "temp-001",
        "temperature": 23.5,
        "humidity": 65.2,
        "timestamp": "2025-10-24T23:30:00Z"
    }

    request = Message(
        code=Code.POST,
        uri='coap://localhost/readings',
        payload=json.dumps(reading).encode('utf-8')
    )
    request.opt.content_format = 50  # application/json

    response = await protocol.request(request).response

    if response.code == Code.CREATED:
        print('Reading submitted successfully')
    else:
        print(f'Submission failed: {response.code}')

asyncio.run(send_reading())

22.10.7 Observe Pattern (Subscribe to Updates)

22.10.8 Step 5: Add a real observable resource before subscribing

  • The plain TemperatureResource above does not implement Observe; change it to an ObservableResource in your lab copy.
  • Call updated_state() after a reading changes, then run the Observe client below in a second terminal.
  • Record the initial 2.05 response, each notification token, each message ID, and the changed payload value.
  • Cancel the subscription and confirm the server stops notifications; a printed loop without exchanged CoAP packets is only a model.
  • Observe was not run in this lane because the shown server lacks the required handler; no notification trace is claimed.
async def observe_temperature():
    """Subscribe to temperature updates using Observe"""

    protocol = await Context.create_client_context()

    request = Message(
        code=Code.GET,
        uri='coap://localhost/temperature',
        observe=0  # Register for notifications
    )

    request_handle = protocol.request(request)

    # Get initial response
    response = await request_handle.response
    print(f'Initial temperature: {response.payload.decode("utf-8")}C')

    # Wait for notifications (runs until cancelled)
    print('Waiting for updates...')
    async for response in request_handle.observation:
        print(f'Update: {response.payload.decode("utf-8")}C')
        # In real code, add break condition or timeout

asyncio.run(observe_temperature())
Try It: Observe Pattern Timeline Simulator

Visualize the message exchange sequence between a CoAP client and server during an Observe subscription. Adjust parameters to see how CON/NON message types and notification intervals affect the communication pattern.

Run it: The interval-and-message-type timeline in this exercise shows how often CON acknowledgements occur; the Observe workbench below lets you drive the subscription end to end. Register observer, then push NON sensor updates and a CON alarm update and watch the Event Trace keep one Token fixed while the MID and Observe number advance. Trigger Out-of-order delivery and Max-Age expiry to see how a client rejects stale notifications, then Deregister and RST to close the subscription cleanly.

Objective: Run a serial simulation of CoAP-like GET, PUT, and Observe patterns on ESP32. This sketch prints illustrative exchanges; it does not send UDP packets or implement a CoAP server.

22.10.9 Step 6: Inspect the ESP32 serial sketch as a model

  • If Wokwi has capacity, paste the sketch below into its real ESP32 editor, start the simulation, and open Serial Monitor.
  • Look for the discovery list, GET and PUT response labels, and the periodic Observe-style lines described below.
  • Compare those serial labels with the Python aiocoap results in Steps 2–4; only the Python exchange here sent real UDP datagrams.
  • Save the serial log as evidence of program flow, and label it “CoAP-style print simulation” because the sketch sends no CoAP packets.
  • Wokwi was not run in this lane because the CI quota is exhausted; the expected serial lines below are from the sketch, not observed output.

Paste this code into the Wokwi editor:

#include <WiFi.h>

// Simulated CoAP resource registry
struct CoapResource {
  const char* path;
  float value;
  bool observable;
  int observeCount;
};

CoapResource resources[] = {
  {"/temperature", 22.5, true, 0},
  {"/humidity", 65.0, true, 0},
  {"/config/interval", 60.0, false, 0},
  {"/led", 0.0, false, 0}
};
const int NUM_RESOURCES = 4;

// CoAP response codes (RFC 7252)
const char* COAP_205_CONTENT = "2.05 Content";
const char* COAP_204_CHANGED = "2.04 Changed";
const char* COAP_201_CREATED = "2.01 Created";
const char* COAP_404_NOT_FOUND = "4.04 Not Found";
const char* COAP_405_NOT_ALLOWED = "4.05 Method Not Allowed";

unsigned long lastObserve = 0;
int messageId = 1000;

void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("=== CoAP Resource Contract Simulation ===");
  Serial.println("Printing CoAP-style methods on ESP32; no UDP packets are sent.\n");

  // Show resource discovery (/.well-known/core)
  discoverResources();

  // Demonstrate GET requests
  Serial.println("\n--- GET Requests ---");
  coapGet("/temperature");
  coapGet("/humidity");
  coapGet("/config/interval");
  coapGet("/nonexistent");

  // Demonstrate PUT requests
  Serial.println("\n--- PUT Requests (Update Resources) ---");
  coapPut("/config/interval", 30.0);
  coapPut("/led", 1.0);
  coapGet("/config/interval");  // Verify change

  // Demonstrate POST (create reading)
  Serial.println("\n--- POST Request (Submit Reading) ---");
  coapPost("/readings", 23.7);

  // Demonstrate Observe pattern
  Serial.println("\n--- Observe Pattern (Subscribe to Updates) ---");
  Serial.println("Registering observer for /temperature...");
  Serial.println("MID=" + String(messageId++) + " Token=0xAB | Observe=0 (Register)");
  Serial.println("Response: " + String(COAP_205_CONTENT) + " | Observe=1\n");

  Serial.println("Sending notifications every 2 seconds...\n");
}

void loop() {
  // Simulate Observe notifications
  if (millis() - lastObserve > 2000) {
    lastObserve = millis();

    // Update sensor values with drift
    resources[0].value += random(-10, 11) * 0.1;
    resources[1].value += random(-5, 6) * 0.1;
    resources[0].observeCount++;

    Serial.printf("Observe #%d | MID=%d | /temperature = %.1f C\n",
                  resources[0].observeCount, messageId++,
                  resources[0].value);

    // Show CON vs NON comparison
    if (resources[0].observeCount % 5 == 0) {
      Serial.println("  ^ CON (Confirmable) - requires ACK");
      Serial.println("    Client ACK received (MID=" + String(messageId - 1) + ")");
    } else {
      Serial.println("  ^ NON (Non-confirmable) - no ACK needed");
    }

    // Show message size comparison
    if (resources[0].observeCount == 3) {
      Serial.println("\n--- Protocol Overhead Comparison ---");
      Serial.println("Illustrative GET /temperature exchange: CoAP 18B request + 11B response = 29B");
      Serial.println("Literal HTTP/1.1 exchange: 43B request + 42B response = 85B");
      Serial.printf("CoAP application bytes: %.0f%% fewer in this example\n\n",
                    (1.0 - 29.0 / 85.0) * 100);
    }

    if (resources[0].observeCount >= 10) {
      Serial.println("\nDeregistering observer (Observe=1)...");
      Serial.println("Observer removed. Server stops notifications.\n");
      Serial.println("=== CoAP Demo Complete ===");
      while (1) delay(1000);
    }
  }
}

void discoverResources() {
  Serial.println("GET /.well-known/core (Resource Discovery)");
  Serial.println("Response: " + String(COAP_205_CONTENT));
  Serial.println("Content-Format: application/link-format\n");
  for (int i = 0; i < NUM_RESOURCES; i++) {
    Serial.printf("  <%s>;rt=\"sensor\";if=\"core.s\"",
                  resources[i].path);
    if (resources[i].observable) Serial.print(";obs");
    Serial.println();
  }
}

CoapResource* findResource(const char* path) {
  for (int i = 0; i < NUM_RESOURCES; i++) {
    if (strcmp(resources[i].path, path) == 0) return &resources[i];
  }
  return NULL;
}

void coapGet(const char* path) {
  Serial.printf("GET %s | MID=%d\n", path, messageId++);
  CoapResource* res = findResource(path);
  if (res) {
    Serial.printf("  Response: %s | %.1f\n", COAP_205_CONTENT, res->value);
  } else {
    Serial.printf("  Response: %s\n", COAP_404_NOT_FOUND);
  }
}

void coapPut(const char* path, float value) {
  Serial.printf("PUT %s | MID=%d | Payload: %.1f\n", path, messageId++, value);
  CoapResource* res = findResource(path);
  if (res) {
    res->value = value;
    Serial.printf("  Response: %s | Updated to %.1f\n", COAP_204_CHANGED, value);
  } else {
    Serial.printf("  Response: %s\n", COAP_404_NOT_FOUND);
  }
}

void coapPost(const char* path, float value) {
  Serial.printf("POST %s | MID=%d | Payload: {temp: %.1f}\n",
                path, messageId++, value);
  Serial.printf("  Response: %s | Location: /readings/1\n", COAP_201_CREATED);
}

What to Observe:

  1. Resource Discovery (/.well-known/core) lists all available resources with attributes — this is how CoAP clients find endpoints without configuration
  2. Response Codes match HTTP semantics: 2.05 Content (200 OK), 2.04 Changed (204), 2.01 Created (201), 4.04 Not Found (404)
  3. Observe Pattern prints simulated notifications every 2 seconds; every fifth is labelled CON, but no CoAP packets or ACKs are exchanged
  4. Overhead comparison uses a literal GET and 22.5 response: 29 CoAP application bytes versus 85 HTTP/1.1 bytes, about 66% fewer before transport overhead; the sketch only prints this model

This section:

Broker Bex, Messaging and broker domain expertCheckpoint: Client Exchange Evidence
  • You now know how Python clients build GET, PUT, POST, and Observe requests against the same resource contract.
  • You can inspect response codes instead of assuming success just because await protocol.request() returned.
  • You have seen why Observe keeps the subscription Token stable while notification MIDs advance.

After Python exercises the contract, repeat the same evidence on the constrained device. The ESP32 examples expose buffer sizes, callbacks, and loop behavior directly.

22.11 Arduino ESP32 CoAP Client

This section: Arduino ESP32 CoAP Client

This example uses the coap-simple library for ESP32:

22.11.1 Step 7: Verify the ESP32 packet path on your own hardware

  • On your own ESP32 and Wi-Fi network, set the server address and port in the sketch, then start a real CoAP server with the matching resources.
  • In the ESP32 serial monitor, record the GET, PUT, and POST send lines and the callback’s response code and payload.
  • Capture UDP port 5683 in Wireshark and match the request token, message ID, resource path, and reply to the serial record.
  • Keep /config and /readings disabled until the Python server has matching handlers, or record their 4.04 responses as expected failures.
  • No ESP32, Wi-Fi packet capture, or physical radio path was run here; the localhost aiocoap exchange is the only real CoAP network result in this lane.
#include <WiFi.h>
#include <coap-simple.h>

// WiFi credentials
const char* ssid = "YourWiFi";
const char* password = "YourPassword";

// CoAP client instance
Coap coap;

// Server details
IPAddress serverIP(192, 168, 1, 100);
int serverPort = 5683;

// Callback for CoAP responses
void callback_response(CoapPacket &packet, IPAddress ip, int port) {
  // Extract payload
  char payload[packet.payloadlen + 1];
  memcpy(payload, packet.payload, packet.payloadlen);
  payload[packet.payloadlen] = '\0';

  Serial.print("Response from ");
  Serial.print(ip);
  Serial.print(": ");
  Serial.println(payload);

  // Check response code
  Serial.print("Code: ");
  Serial.print(packet.code >> 5);  // Class (2=Success, 4=Client Error, 5=Server Error)
  Serial.print(".");
  Serial.println(packet.code & 0x1F);  // Detail
}

void setup() {
  Serial.begin(115200);

  // Connect to WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("\nWiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  // Register response callback
  coap.response(callback_response);

  // Start CoAP client
  coap.start();
}

void loop() {
  // Send GET request every 10 seconds
  static unsigned long lastRequest = 0;

  if (millis() - lastRequest > 10000) {
    int msgid = coap.get(serverIP, serverPort, "temperature");
    Serial.print("GET /temperature sent, MID: ");
    Serial.println(msgid);
    lastRequest = millis();
  }

  // Process incoming responses
  coap.loop();
}

22.11.2 ESP32 PUT Request Example

void sendConfig() {
  // Build JSON payload
  char payload[64];
  snprintf(payload, sizeof(payload), "{\"interval\": %d}", 30);

  // Send PUT request
  int msgid = coap.put(
    serverIP,
    serverPort,
    "config",
    payload,
    strlen(payload)
  );

  Serial.print("PUT /config sent, MID: ");
  Serial.println(msgid);
}

22.11.3 ESP32 POST Request Example

void sendReading() {
  // Read sensor (simulated)
  float temperature = 22.5 + (random(-10, 10) / 10.0);
  float humidity = 60.0 + (random(-50, 50) / 10.0);

  // Build JSON payload
  char payload[128];
  snprintf(payload, sizeof(payload),
    "{\"temp\": %.1f, \"hum\": %.1f}",
    temperature, humidity);

  // Send POST request
  int msgid = coap.post(
    serverIP,
    serverPort,
    "readings",
    payload,
    strlen(payload)
  );

  Serial.print("POST /readings sent: ");
  Serial.println(payload);
}

22.12 Continue to the Next Part

This section: Continue to the Next Part — Carry this evidence into CoAP Lab: Message Construction, which begins with Try It: CoAP Message Byte Builder.

Carry this evidence into CoAP Lab: Message Construction, which begins with Try It: CoAP Message Byte Builder.

Find help

3 reviewed answers.

Where should I start in a module?

Open the module page from the sidebar to see its chapters in order. Use the module map to return to a topic you need.

Modules and paths

Reviewed

What can I try when a project does not work?

Check the symptom, then work through the diagnostic steps. Keep the error message and the smallest example that reproduces it.

When you are stuck

Reviewed

How do interactive examples work?

Use the interactive guide to check how examples and simulations work before changing your project.

Interactive examples guide

Reviewed