22 CoAP Lab: Stack and Trace Contracts
More
Depth: Practitioner
Progress saves as you read. Mark sections complete as you go; your progress appears in the sidebar.
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.
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.
-
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
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.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
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:
- 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
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 5683andResources: /temperature (GET, PUT)before opening a client terminal. - In the local run here, an equivalent
aiocoapresource bound to127.0.0.1:56830loggedserver 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
tempvalue) = 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: ms
- HTTP: 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)
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())
22.9
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.10 Python CoAP Client
This section: Python CoAP Client
22.10.1 Basic GET Request
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 realaiocoapserver 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.05line.
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
/temperatureand observedserver PUT payload=b'23.5'followed byclient PUT -> 2.04 Changed. - A separate local GET
/configreturned4.04 Not Found, confirming that the published/configexample needs its own server handler. - Add a
/confighandler 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.04from/temperaturecannot 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 itsapplication/jsoncontent format and encoded payload before sending. - Against the chapter’s unextended server, the local
aiocoaprun returnedclient POST /readings -> 4.04 Not Found. - Add a
/readingsPOST handler to your lab server, validate the sensor ID and timestamp, and return2.01 Createdonly 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 Createdremains 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
TemperatureResourceabove does not implement Observe; change it to anObservableResourcein your lab copy. - Call
updated_state()after a reading changes, then run the Observe client below in a second terminal. - Record the initial
2.05response, 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())
This section:
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.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
/configand/readingsdisabled until the Python server has matching handlers, or record their4.04responses as expected failures. - No ESP32, Wi-Fi packet capture, or physical radio path was run here; the localhost
aiocoapexchange 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 pathsReviewed
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 stuckReviewed
How do interactive examples work?
Use the interactive guide to check how examples and simulations work before changing your project.
Interactive examples guideReviewed
