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.
17.1 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
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.
17.2 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
17.4 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.
17.5 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.
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.
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
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.
17.7 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().
You now know where the ESP32 callback extracts payload bytes and decodes the response-code class and detail.
You can connect coap.get(), coap.put(), and coap.post() calls to resource paths already tested in Python.
You have a byte-level header view that explains what the library sends before the payload leaves the device.
Once packets can be decoded, response codes become the lab’s acceptance language. Use the table below to make server behavior explicit.
17.8 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 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
17.9 Basic Simulator: CoAP UDP Communication
Interactive Simulator: CoAP-Style UDP Communication
What This Simulates: ESP32 demonstrating CoAP’s lightweight UDP request/response pattern
Run it: Before you open the Wokwi build, drive the same request/response in the browser workbench below. Step through a Piggybacked CON GET, then a Separate response and a NON request/response, and watch the Current Packet Format and Event Trace show how CON, NON, ACK, and RST differ on the wire. Try Duplicate CON handling and a Reset message to expose the edge cases the serial monitor alone hides, and adjust Token length, Payload bytes, and Uri-Path segments to feel the 4-byte-header economy. The Wokwi steps below then reproduce the same exchange on the ESP32.
CoAP Communication Pattern:
Client (ESP32)
|
| GET /temperature
| 4-byte header over UDP 5683
v
Server (Simulated)
|
| 2.05 Content
| Temp: 23.5C
v
Client receives response
How to Use:
Click Start Simulation
Watch Serial Monitor show UDP request/response cycle
The direct project link stays readable on mobile and avoids the stalled local embed seen during visual review.
Open it in a new tab to run the ESP32 simulation and inspect the serial monitor output.
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
17.10 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.
You now know the boundary between MID duplicate detection and Token request-response correlation.
You can explain why Observe notifications keep the original Token even when each notification has a new MID.
You have a concrete failure pattern to look for when a client drops valid updates from a working server.
The remaining checks turn those protocol facts into operational judgment before you reuse this lab pattern in a production prototype.
Common Pitfalls
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
17.11 Concept Relationships
This implementation chapter bridges theory to practice:
When extending the basic labs into a fuller ESP32-style CoAP server, keep the feature set small enough to test:
Resource or feature
Lab behavior to prove
Failure mode to observe
/temperature with Observe
Client registers once, then receives updates only when the reading changes
Lost notification, cancelled observation, or stale token
/firmware with Block2
Client can fetch a large payload in numbered blocks and resume from the failed block
Restarting the whole transfer after one missing block
/.well-known/core
Discovery lists only supported resources and content formats
Client assumes hidden or unsupported paths
Configuration PUT
Idempotent update can be retried without creating duplicate state
Retried POST creates repeated configuration records
CON/NON selection
Commands and rare critical alerts use CON; periodic telemetry can use NON when the next value replaces the previous one
High-frequency CON drains batteries, while NON commands lose safety-critical acknowledgements
For battery labs, write the message policy beside the resource table. A common hybrid is NON for routine telemetry and CON for user actions, safety alerts, firmware blocks, or configuration changes. That preserves the energy benefit of UDP while still proving delivery for messages that cannot be silently lost.
17.14.1 Block Transfer Planning Check
Before using Block2 for firmware, images, or larger JSON payloads, record the block size, total block count, resume behavior, and retry limit. A small planning record prevents the lab from hiding a production risk:
Run it: Fill this planning record from the block-wise workbench below instead of guessing. Pick a direction (Block2 download or Block1 upload) and a Scenario such as Firmware upload or Sensor log download, choose a Requested block size (SZX), and step the transfer while watching NUM, the M bit, and the Byte range advance – that tells you how many blocks a payload needs. Then run the Lossy field link and Server asks for smaller blocks scenarios and read the Event trace to confirm the client retries only the missing block rather than restarting, which is exactly the resume behavior this check must capture.
Question
Evidence to capture
How many blocks are needed?
Payload bytes divided by negotiated block size, with the final partial block noted
What happens after packet loss?
Client retries only the missing block instead of restarting the whole payload
How is progress verified?
Server logs block number, token, ETag or version, and final checksum
When is Block2 not enough?
If payloads are frequent or very large, move the transfer to a gateway or HTTP/TLS path and keep CoAP for control
Checkpoint: Lab Readiness Evidence
You now know which records to capture for resource discovery, configuration updates, Observe cancellation, Block2 resume behavior, and security setup.
You can choose NON for replaceable telemetry and CON for commands, alerts, firmware blocks, and configuration changes.
You have a final checklist for deciding whether the implementation is tested under constrained-network conditions rather than only compiled.
17.15 Summary
CoAP implementation labs turn protocol rules into testable behavior: resource handlers, confirmable messages, retransmission, Observe notifications, and security setup. The goal is to verify behavior with real requests rather than assuming the stack handles every edge case.
17.16 Key Takeaway
A CoAP implementation is ready only when resource paths, content formats, timeout handling, retransmission, Observe cancellation, and security settings have all been exercised under constrained-network conditions.