Chapters

22 CoAP Lab: Stack and Trace Contracts

coap
implementation

22.1 Start With the Decision

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 is part 1 of 2. Continue with CoAP Lab: Message Construction.

22.3 Part Objectives

  • Define continue: coap implementation stack and trace contracts with explicit inputs, errors, and change rules.
  • 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

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

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

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

  • 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

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

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 server exposes a /temperature resource that returns a simulated reading:

import asyncio
from aiocoap import *

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())

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:

Read these points as one connected sequence: start with CoAP request: 4-byte header + 2-byte token + 6-byte URI (/temp) + 1-byte marker = 13 bytes; then Response: 4-byte header + 2-byte token + 5-byte payload (22.5) = 11 bytes; and finish with Total exchange: 24 bytes.

  • CoAP request: 4-byte header + 2-byte token + 6-byte URI (/temp) + 1-byte marker = 13 bytes
  • Response: 4-byte header + 2-byte token + 5-byte payload (22.5) = 11 bytes
  • Total exchange: 24 bytes

Compare to HTTP: Read these points as one connected sequence: start with HTTP request: GET /temp HTTP/1.1\r\nHost: server\r\n\r\n = ~45 bytes; then Response: HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n22.5 = ~52 bytes; and finish with Total: 97 bytes (4× larger).

  • HTTP request: GET /temp HTTP/1.1\r\nHost: server\r\n\r\n = ~45 bytes
  • Response: HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n22.5 = ~52 bytes
  • Total: 97 bytes (4× larger)

Radio transmission time (250 kbps LoRa): Read these points as one connected sequence: start with CoAP: tCoAP=24×8250,000=0.768t_{CoAP} = \frac{24 \times 8}{250,000} = 0.768 ms; and finish with HTTP: tHTTP=97×8250,000=3.104t_{HTTP} = \frac{97 \times 8}{250,000} = 3.104 ms.

  • CoAP: tCoAP=24×8250,000=0.768t_{CoAP} = \frac{24 \times 8}{250,000} = 0.768 ms
  • HTTP: tHTTP=97×8250,000=3.104t_{HTTP} = \frac{97 \times 8}{250,000} = 3.104 ms

Battery impact (CR2032, 225 mAh @ 3V): Read these points as one connected sequence: start with CoAP radio cost: 0.768 ms × 20 mA = 4.27 nAh per reading; then HTTP radio cost: 3.104 ms × 20 mA = 17.24 nAh per reading; then At 1 reading/minute: CoAP daily energy = 6,149 nAh/day vs HTTP = 24,826 nAh/day; and finish with Battery life difference: CoAP runs ~4× longer on same battery, purely on radio energy (≈36,591 days vs ≈9,063 days, i.e. ≈100.3 years vs ≈24.8 years — real-world life is limited by CR2032 self-discharge and MCU sleep current, not this radio-only figure).

  • CoAP radio cost: 0.768 ms × 20 mA = 4.27 nAh per reading
  • HTTP radio cost: 3.104 ms × 20 mA = 17.24 nAh per reading
  • At 1 reading/minute: CoAP daily energy = 6,149 nAh/day vs HTTP = 24,826 nAh/day
  • Battery life difference: CoAP runs ~4× longer on same battery, purely on radio energy (≈36,591 days vs ≈9,063 days, i.e. ≈100.3 years vs ≈24.8 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 to understand real-world savings
Interactive Calculator: Battery Life Estimator

Try This:

  • Increase data rate to see how frequent transmissions drain battery faster
  • Compare different battery capacities (CR2032 vs larger cells)
  • Notice how radio efficiency impacts both protocols equally, but CoAP’s smaller messages still win
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.1 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.

Broker BexCheckpoint: 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.9 Python CoAP Client

22.9.1 Basic GET Request

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}')
        print(f'Temperature: {response.payload.decode("utf-8")}°C')

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

# Run client
asyncio.run(get_temperature())

22.9.2 PUT Request for Configuration

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.9.3 POST Request to Send Data

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.9.4 Observe Pattern (Subscribe to Updates)

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: Build a CoAP-like RESTful sensor server on ESP32 that demonstrates GET, PUT, and Observe patterns — the same request/response semantics used by the Python examples above, running directly on a microcontroller.

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 RESTful Sensor Server ===");
  Serial.println("Demonstrating CoAP methods on ESP32\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("CoAP message: 4B header + 2B token + 8B options + 4B payload = 18 bytes");
      Serial.println("HTTP equivalent: ~200 bytes (GET /temperature HTTP/1.1 + headers)");
      Serial.printf("CoAP efficiency: %.0f%% less overhead\n\n",
                    (1.0 - 18.0 / 200.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:

Read these points as one connected sequence: start with Resource Discovery (/.well-known/core) lists all available resources with attributes — this is how CoAP clients find endpoints without configuration; then Response Codes match HTTP semantics: 2.05 Content (200 OK), 2.04 Changed (204), 2.01 Created (201), 4.04 Not Found (404); then Observe Pattern sends server-push notifications every 2 seconds — every 5th notification uses CON (Confirmable) requiring an ACK, the rest use NON (fire-and-forget); and finish with Overhead comparison shows CoAP uses ~18 bytes vs HTTP’s ~200 bytes for the same temperature reading — a 91% reduction ideal for constrained IoT devices.

  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 sends server-push notifications every 2 seconds — every 5th notification uses CON (Confirmable) requiring an ACK, the rest use NON (fire-and-forget)
  4. Overhead comparison shows CoAP uses ~18 bytes vs HTTP’s ~200 bytes for the same temperature reading — a 91% reduction ideal for constrained IoT devices

Broker BexCheckpoint: 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.10 Arduino ESP32 CoAP Client

This example uses the coap-simple library for ESP32:

#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.10.1 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.10.2 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.11 Continue to the Next Part

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