1230  CoAP Implementation Labs

1230.1 Learning Objectives

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

  • Build CoAP Servers: Develop Python CoAP servers with resource handlers using aiocoap
  • Implement CoAP Clients: Create Python clients that send GET, PUT, and observe requests
  • Port to Embedded: Adapt CoAP examples for Arduino ESP32 with the coap-simple library
  • Handle Responses: Parse CoAP response codes and payloads correctly
  • Debug CoAP Traffic: Use appropriate tools to inspect message flow

1230.2 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: - CoAP Methods and Patterns - Tradeoffs and design decisions - CoAP Advanced Features Lab - Full ESP32 Wokwi simulation with observe/block - CoAP Comprehensive Review - Assessment and quiz

Practical Development: - Prototyping Hardware - ESP32 setup guide - Network Design - Testing tools - MQTT Implementation - Compare with MQTT patterns

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

1230.3 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

1230.3.1 Adding Multiple Resources

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

1230.4 Python CoAP Client

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

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

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

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

1230.5 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();
}

1230.5.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);
}

1230.5.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);
}

1230.6 Response Code Reference

CoAP response codes follow a Class.Detail format similar to HTTP:

Code Meaning HTTP Equivalent
2.01 Created 201 Created
2.02 Deleted 200 OK (for DELETE)
2.03 Valid 304 Not Modified
2.04 Changed 200 OK (for PUT)
2.05 Content 200 OK (for GET)
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
5.00 Internal Server Error 500 Internal Server Error
# Checking response codes in Python
if response.code.is_successful():
    print("Success!")
elif response.code == Code.NOT_FOUND:
    print("Resource not found")
elif response.code == Code.BAD_REQUEST:
    print("Invalid request format")

1230.7 Basic Simulator: CoAP UDP Communication

TipInteractive Simulator: CoAP-Style UDP Communication

What This Simulates: ESP32 demonstrating CoAP’s lightweight UDP request/response pattern

CoAP Communication Pattern:

Client (ESP32)          Server (Simulated)
      |                       |
      |------ GET Request --->|  (UDP Port 5683)
      |    (4-byte header)    |
      |                       |
      |<----- Response -------|  (2.05 Content)
      |   (Temp: 23.5C)       |
      |                       |

How to Use: 1. Click Start Simulation 2. Watch Serial Monitor show UDP request/response cycle 3. Observe message types (CON, NON, ACK) 4. See CoAP-style resource addressing 5. Monitor round-trip times (RTT)

NoteLearning Points

What You’ll Observe:

  1. UDP Transport - Connectionless, lightweight communication
  2. 4-Byte Headers - Minimal overhead compared to HTTP
  3. Request/Response - RESTful pattern like HTTP GET
  4. Message IDs - Tracking requests and responses
  5. No Handshake - Direct communication without TCP overhead

1230.8 Visual Reference: CoAP Message Structure

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor':'#E8F4F8','primaryTextColor':'#2C3E50','primaryBorderColor':'#16A085','lineColor':'#16A085','secondaryColor':'#FEF5E7'}}}%%

packet-beta
0-1: "Ver (2b)"
2-3: "Type (2b)"
4-7: "TKL (4b)"
8-15: "Code (8b)"
16-31: "Message ID (16b)"
32-39: "Token (0-8 bytes)"
40-63: "Options (variable)"
64-95: "Payload (variable)"

title CoAP Message Structure (4-byte header + options + payload)

The 4-byte fixed header contains: - Ver: Version (always 1) - Type: CON(0), NON(1), ACK(2), RST(3) - TKL: Token length (0-8 bytes) - Code: Method (GET=1, POST=2, PUT=3, DELETE=4) or response code - Message ID: 16-bit identifier for matching requests/responses

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor':'#2C3E50','primaryTextColor':'#fff','primaryBorderColor':'#16A085','lineColor':'#16A085','secondaryColor':'#E67E22'}}}%%
graph TB
    subgraph HTTP["HTTP Header (~200+ bytes)"]
        H1["GET /temp HTTP/1.1"]
        H2["Host: sensor.local"]
        H3["User-Agent: Mozilla/5.0"]
        H4["Accept: application/json"]
        H5["Connection: keep-alive"]
    end

    subgraph CoAP["CoAP Header (4 bytes)"]
        C1["Ver | Type | TKL (1 byte)"]
        C2["Code (1 byte)"]
        C3["Message ID (2 bytes)"]
    end

    HTTP -.->|"50x smaller"| CoAP

    style HTTP fill:#7F8C8D,stroke:#2C3E50
    style CoAP fill:#16A085,stroke:#2C3E50

This diagram compares header sizes: HTTP requires 200+ bytes of headers while CoAP achieves the same REST functionality with just 4 bytes.

1230.9 Summary

This chapter provided hands-on CoAP implementation examples:

  • Python Server: Built async CoAP servers with aiocoap, handling GET, PUT, POST requests via render_* methods
  • Python Client: Created clients that send requests, handle responses, and use the Observe pattern for subscriptions
  • ESP32 Arduino: Ported CoAP patterns to embedded devices using the coap-simple library
  • Response Codes: Understood the Class.Detail format (2.05 Content, 4.04 Not Found, etc.)
  • Message Structure: Visualized the 4-byte fixed header that makes CoAP 50x smaller than HTTP

1230.10 Knowledge Check

  1. In aiocoap, which method handles incoming GET requests on a resource?

aiocoap uses render_get, render_put, render_post, render_delete naming convention for resource handlers. The methods are async to support non-blocking I/O.

  1. What does response code 2.05 indicate?

2.05 Content is the standard success response for GET requests, equivalent to HTTP 200 OK. 2.01 is Created, 2.04 is Changed, and 4.04 is Not Found.

  1. To subscribe to resource updates using CoAP Observe, what do you set in the request?

The Observe option with value 0 registers a client for notifications. When the server’s resource changes, it sends notifications to registered observers using the same token.

1230.11 What’s Next

In the next chapter, CoAP Advanced Features Lab, you’ll build a comprehensive ESP32 CoAP server with: - Observe pattern with automatic notifications - Block-wise transfer for large payloads (firmware updates) - Resource discovery via /.well-known/core - Full Wokwi simulation you can run in your browser

Key Takeaways: - aiocoap provides async Python CoAP with render_* handlers - Response codes use Class.Detail format (2.xx success, 4.xx client error, 5.xx server error) - ESP32 can run CoAP clients with the coap-simple library - Observe pattern uses observe=0 option to register for notifications