22 CoAP Lab: Stack and Trace Contracts
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.
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.
-
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/readingseach 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.Detailresponse 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:
- 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:
- Install aiocoap:
pip install aiocoap - Run the server in one terminal
- Run the client in another terminal
- 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 requestsMessage(code=Code.GET, uri='coap://...')- How clients build requestsawait 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: ms; and finish with HTTP: ms.
- CoAP: ms
- HTTP: 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)
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())
Checkpoint: 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, orCode.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())
Checkpoint: 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.
