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.
53.1 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
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
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
53.3 Python CoAP Server
This server exposes a /temperature resource that returns a simulated reading:
import asynciofrom aiocoap import*class TemperatureResource(resource.Resource):"""Example resource for temperature readings"""asyncdef 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 )asyncdef render_put(self, request):"""Handle PUT requests to update configuration"""print(f'Received PUT: {request.payload}')# In real implementation, parse and apply configreturn Message(code=Code.CHANGED) # 2.04 Changedasyncdef 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 runningawait asyncio.get_running_loop().create_future()if__name__=="__main__": asyncio.run(main())
Install and run:
pip install aiocoappython coap_server.py
Putting Numbers to It
CoAP’s efficiency shines in power-constrained deployments. Consider a battery-powered temperature sensor using this server pattern:
Radio transmission time (250 kbps LoRa): - CoAP: \(t_{CoAP} = \frac{24 \times 8}{250,000} = 0.768\) ms - HTTP: \(t_{HTTP} = \frac{97 \times 8}{250,000} = 3.104\) ms
Battery impact (CR2032, 225 mAh @ 3V): - CoAP radio cost: 0.768 ms × 20 mA = 4.27 µAh per reading - HTTP radio cost: 3.104 ms × 20 mA = 17.24 µAh per reading - At 1 reading/minute: CoAP daily energy = 6,149 µAh/day vs HTTP = 24,826 µAh/day - Battery life difference: CoAP runs ~4× longer on same battery (≈36.6 days vs ≈9.1 days)
Interactive Calculator: CoAP vs HTTP Message Overhead
asyncdef 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.responseprint(f'Initial temperature: {response.payload.decode("utf-8")}C')# Wait for notifications (runs until cancelled)print('Waiting for updates...')asyncfor response in request_handle.observation:print(f'Update: {response.payload.decode("utf-8")}C')# In real code, add break condition or timeoutasyncio.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.
Show code
viewof observeInterval = Inputs.range([1,10], {value:2,step:1,label:"Notification interval (seconds)"})viewof observeCount = Inputs.range([3,12], {value:6,step:1,label:"Number of notifications"})viewof conFrequency = Inputs.range([1,6], {value:5,step:1,label:"CON message every Nth notification"})viewof observeMessageType = Inputs.select(["Mixed (CON + NON)","All CON (Confirmable)","All NON (Non-confirmable)"], {value:"Mixed (CON + NON)",label:"Message type strategy"})
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.
Resource Discovery (/.well-known/core) lists all available resources with attributes – this is how CoAP clients find endpoints without configuration
Response Codes match HTTP semantics: 2.05 Content (200 OK), 2.04 Changed (204), 2.01 Created (201), 4.04 Not Found (404)
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)
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
53.5 Arduino ESP32 CoAP Client
This example uses the coap-simple library for ESP32:
Construct a CoAP message byte-by-byte and see exactly how the 4-byte header is encoded. This helps you understand what the coap-simple library constructs behind the scenes when you call coap.get() or coap.put().
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 Pythonif 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")
Check Your Understanding: CoAP Response Codes
53.7 Basic Simulator: CoAP UDP Communication
Interactive 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:
Click Start Simulation
Watch Serial Monitor show UDP request/response cycle
Observe message types (CON, NON, ACK)
See CoAP-style resource addressing
Monitor round-trip times (RTT)
Learning Points
What You’ll Observe:
UDP Transport - Connectionless, lightweight communication
4-Byte Headers - Minimal overhead compared to HTTP
Request/Response - RESTful pattern like HTTP GET
Message IDs - Tracking requests and responses
No Handshake - Direct communication without TCP overhead
53.8 Visual Reference: CoAP Message Structure
Visual: CoAP Message Structure
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
Alternative View: CoAP vs HTTP Header Comparison
This diagram compares header sizes: HTTP requires 200+ bytes of headers while CoAP achieves the same REST functionality with just 4 bytes.
Common Mistake: Misunderstanding CoAP Token vs Message ID Correlation
The Error: Developers new to CoAP often confuse Message ID (MID) and Token, attempting to match responses to requests using MID alone. This causes failures with Observe notifications and separate responses.
Why It Happens: HTTP developers expect a single request-response correlation mechanism. CoAP has two: MID for duplicate detection and Token for logical request-response matching.
Example of Failure:
# WRONG: Matching by Message ID onlyrequest_mid =12345response =await get_response()if response.message_id == request_mid: # Fails with Observe! process(response)
The Fix: Always use Token for request-response correlation:
# CORRECT: Matching by Tokenimport secrets# Client sends request with unique tokenrequest_token = secrets.token_bytes(4) # e.g., 0xAB12CD34request = Message(code=Code.GET, uri='coap://sensor/temp')request.token = request_token# For Observe, server sends multiple responses with DIFFERENT MIDs# but SAME token as original requestasyncfor response in request_handle.observation:if response.token == request_token: # Correct correlationprint(f'Update: {response.payload}')
Why This Matters:
Separate Responses: Server sends empty ACK (MID=12345) immediately, then data CON (MID=12346) later. Both share the same Token.
Observe Notifications: Each notification has a new MID (12347, 12348, 12349…) but the original Token throughout the subscription lifetime.
Concurrent Requests: Client sends multiple requests simultaneously. Responses may arrive out-of-order. Token uniquely identifies which request each response answers.
Real Production Impact: A building automation system lost 30% of sensor readings because the client discarded Observe notifications with “mismatched” MIDs. After fixing to use Token matching, all notifications were correctly correlated.
Try It: Token vs Message ID Correlation Visualizer
See why Token-based matching works but MID-based matching fails. This simulator shows concurrent requests and Observe notifications where MIDs change but Tokens remain constant.
1. Using Confirmable Messages for Every CoAP Request
CON messages require an ACK roundtrip — on lossy networks with 20% packet loss, a 4-attempt retry with exponential backoff can delay responses by 45 seconds. Use NON for periodic telemetry where data freshness matters more than guaranteed delivery; reserve CON for actuation commands.
2. Ignoring CoAP Proxy Caching Semantics
CoAP proxies cache GET responses based on Max-Age option — a sensor returning temperature with Max-Age=60 will serve cached values for 60 seconds even if the physical reading changes. Set Max-Age to match your data freshness requirement, not the default 60 seconds.
3. Forgetting DTLS Session Management
DTLS handshake (6-8 roundtrips) dominates latency for short-lived CoAP connections — repeatedly creating new DTLS sessions for each request adds 500-2000ms overhead. Use DTLS session resumption (RFC 5077) to reduce reconnection to 1 roundtrip after the initial handshake.
Label the Diagram
Order the Steps
53.9 Concept Relationships
This implementation chapter bridges theory to practice: