%%{init: {'theme': 'base', 'themeVariables': {'primaryColor':'#E8F4F8','primaryTextColor':'#2C3E50','primaryBorderColor':'#16A085','lineColor':'#16A085','secondaryColor':'#FEF5E7'}}}%%
graph TB
subgraph "ESP32 CoAP Server"
A[CoAP Endpoint<br/>Port 5683]
B["/temperature<br/>(Observable)"]
C["/config<br/>(PUT/GET)"]
D["/firmware<br/>(Block Transfer)"]
E["/.well-known/core<br/>(Discovery)"]
F[Observe Manager<br/>Tracks subscribers]
G[Block Manager<br/>Handles chunks]
H[Retry Handler<br/>CON/ACK logic]
end
A --> B
A --> C
A --> D
A --> E
B --> F
D --> G
A --> H
style A fill:#16A085,stroke:#2C3E50
style F fill:#E67E22,stroke:#2C3E50
style G fill:#E67E22,stroke:#2C3E50
style H fill:#E67E22,stroke:#2C3E50
1231 CoAP Advanced Features Lab
1231.1 Learning Objectives
By the end of this chapter, you will be able to:
- Implement CoAP Observe Pattern: Build servers that notify clients only when resource state changes
- Use Block-wise Transfer: Handle large payloads (firmware updates) across constrained networks with small MTUs
- Enable Resource Discovery: Expose
/.well-known/corefor automated service discovery per RFC 6690 - Handle Retransmission: Implement CON message timeouts and exponential backoff
- Understand DTLS Integration: Conceptualize security layer integration for production deployments
1231.2 Prerequisites
Before diving into this chapter, you should be familiar with:
- CoAP Methods and Patterns: Understanding of CON/NON tradeoffs and Observe pattern benefits
- CoAP Implementation Labs: Basic Python and Arduino CoAP code patterns
- CoAP Fundamentals: Message structure, tokens, and options encoding
- ESP32 development: Arduino programming with WiFi and sensors
CoAP Series: - CoAP Methods and Patterns - Design decisions and tradeoffs - CoAP Implementation Labs - Basic code examples - CoAP Comprehensive Review - Assessment and quiz
Advanced Topics: - Edge Computing - CoAP in edge architectures - Security Methods - DTLS and encryption - Encryption Architecture - Securing CoAP
Hands-On Resources: - Simulations Hub - More Wokwi simulations - Hands-On Labs Hub - Lab catalog
This chapter connects to multiple learning hubs for deeper exploration:
Simulations Hub - Wokwi ESP32 CoAP simulator embedded in this chapter - Interactive CoAP message flow visualizer - Block transfer and observe pattern demonstrations
Videos Hub - CoAP protocol analysis with Wireshark tutorials - Python aiocoap library hands-on demonstrations - ESP32 CoAP client-server implementation walkthroughs
Quizzes Hub - CoAP methods and options quiz - Block transfer implementation scenarios - Observe pattern design questions
Knowledge Gaps Hub - “Why doesn’t my CoAP observe work?” - Token management - “Block transfers failing on large payloads” - MTU calculations - “ESP32 CoAP library conflicts” - Dependency resolution
1231.3 Lab: Comprehensive CoAP Server with ESP32
Objective: Build a production-grade CoAP server on ESP32 demonstrating observe pattern, block-wise transfer, resource discovery, retransmission handling, and DTLS concepts.
What You’ll Learn:
- CoAP Observe Pattern - Implement publish-subscribe with automatic notifications
- Block-wise Transfer - Handle large payloads (firmware updates) across constrained networks
- Resource Discovery - Expose
/.well-known/corefor service discovery - Retransmission Logic - Implement CON message timeouts and exponential backoff
- DTLS Concepts - Understand security layer integration (conceptual)
1231.3.1 Hardware Requirements
- ESP32 development board (simulated in Wokwi)
- Temperature sensor (DHT22 simulated)
- LED for status indication
- Button for triggering events
1231.3.2 Lab Architecture
1231.3.3 Interactive Wokwi Simulation
How to Use:
- Click Start Simulation below
- Open Serial Monitor to see CoAP server logs
- Observe automatic notifications when temperature changes
- Watch block-wise transfer simulation for large payloads
- See resource discovery via
/.well-known/core - Monitor CON message retransmissions on packet loss
1231.3.4 Complete ESP32 Code
/**
* CoAP Advanced Features Lab - ESP32 Implementation
* Demonstrates: Observe, Block Transfer, Discovery, Retransmission
*
* Hardware: ESP32, DHT22 (simulated), LED, Button
* Library: coap-simple (modified for advanced features)
*/
#include <WiFi.h>
#include <WiFiUdp.h>
#include <DHTesp.h>
// WiFi Configuration
const char* ssid = "Wokwi-GUEST";
const char* password = "";
// Pin Configuration
#define DHT_PIN 15
#define LED_PIN 2
#define BUTTON_PIN 4
// CoAP Constants
#define COAP_PORT 5683
#define COAP_VERSION 1
#define COAP_HEADER_SIZE 4
#define MAX_OBSERVERS 10
#define MAX_BLOCKS 128
#define BLOCK_SIZE 64
#define MAX_RETRIES 4
#define ACK_TIMEOUT 2000 // 2 seconds
// CoAP Message Types
enum CoapType {
COAP_CON = 0, // Confirmable
COAP_NON = 1, // Non-Confirmable
COAP_ACK = 2, // Acknowledgment
COAP_RST = 3 // Reset
};
// CoAP Method Codes
enum CoapCode {
COAP_EMPTY = 0,
COAP_GET = 1,
COAP_POST = 2,
COAP_PUT = 3,
COAP_DELETE = 4,
COAP_CONTENT = 69, // 2.05
COAP_CREATED = 65, // 2.01
COAP_CHANGED = 68, // 2.04
COAP_NOT_FOUND = 132 // 4.04
};
// CoAP Option Numbers
enum CoapOption {
COAP_OBSERVE = 6,
COAP_URI_PATH = 11,
COAP_CONTENT_FORMAT = 12,
COAP_BLOCK2 = 23
};
// Observer Structure
struct Observer {
IPAddress ip;
uint16_t port;
uint16_t token;
uint32_t sequenceNum;
unsigned long lastNotify;
bool active;
};
// Block Transfer State
struct BlockTransfer {
uint8_t* data;
size_t totalSize;
uint16_t blockNum;
uint8_t blockSize;
bool active;
};
// Global Objects
WiFiUDP udp;
DHTesp dht;
Observer observers[MAX_OBSERVERS];
BlockTransfer blockState;
// Firmware Update Buffer (simulated)
const size_t FIRMWARE_SIZE = 4096;
uint8_t firmwareData[FIRMWARE_SIZE];
// Temperature Cache
float lastTemp = 0.0;
float lastHumidity = 0.0;
unsigned long lastTempRead = 0;
const unsigned long TEMP_READ_INTERVAL = 5000;
// Message ID counter
uint16_t messageIdCounter = 1;
// Statistics
struct Stats {
uint32_t totalRequests;
uint32_t conMessages;
uint32_t nonMessages;
uint32_t retransmissions;
uint32_t blocksSent;
uint32_t observeNotifications;
} stats = {0};
/**
* CoAP Message Structure
*
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |Ver| T | TKL | Code | Message ID |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Token (if any, TKL bytes) ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Options (if any) ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |1 1 1 1 1 1 1 1| Payload (if any) ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
struct CoapPacket {
uint8_t version;
uint8_t type;
uint8_t tokenLength;
uint8_t code;
uint16_t messageId;
uint16_t token;
uint8_t* payload;
size_t payloadLen;
// Options
bool hasObserve;
uint32_t observeValue;
bool hasBlock2;
uint32_t block2Value;
String uriPath;
};
// Function Prototypes
void setupWiFi();
void setupCoAP();
void handleCoapRequest();
void parsePacket(uint8_t* buffer, size_t len, CoapPacket& packet);
void sendResponse(IPAddress ip, uint16_t port, CoapPacket& request, uint8_t code, uint8_t* payload, size_t payloadLen);
void handleGetTemperature(IPAddress ip, uint16_t port, CoapPacket& request);
void handleGetConfig(IPAddress ip, uint16_t port, CoapPacket& request);
void handlePutConfig(IPAddress ip, uint16_t port, CoapPacket& request);
void handleGetFirmware(IPAddress ip, uint16_t port, CoapPacket& request);
void handleWellKnownCore(IPAddress ip, uint16_t port, CoapPacket& request);
void addObserver(IPAddress ip, uint16_t port, uint16_t token);
void removeObserver(IPAddress ip, uint16_t port);
void notifyObservers();
void updateTemperature();
void initFirmwareData();
void printStats();
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n========================================");
Serial.println(" CoAP Advanced Features Lab - ESP32 ");
Serial.println(" Observe | Block | Discovery | DTLS ");
Serial.println("========================================\n");
// Initialize hardware
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
dht.setup(DHT_PIN, DHTesp::DHT22);
// Initialize WiFi
setupWiFi();
// Initialize CoAP
setupCoAP();
// Initialize firmware data (simulated)
initFirmwareData();
Serial.println("\n[OK] CoAP Server Ready!");
Serial.printf("[OK] Listening on %s:%d\n", WiFi.localIP().toString().c_str(), COAP_PORT);
Serial.println("\nAvailable Resources:");
Serial.println(" GET /temperature (Observable)");
Serial.println(" GET /config");
Serial.println(" PUT /config");
Serial.println(" GET /firmware (Block Transfer)");
Serial.println(" GET /.well-known/core (Discovery)");
Serial.println("\n----------------------------------------\n");
}
void loop() {
// Handle incoming CoAP requests
handleCoapRequest();
// Update temperature reading
updateTemperature();
// Notify observers if temperature changed significantly
static unsigned long lastNotifyCheck = 0;
if (millis() - lastNotifyCheck > 1000) {
notifyObservers();
lastNotifyCheck = millis();
}
// Print statistics every 30 seconds
static unsigned long lastStatsPrint = 0;
if (millis() - lastStatsPrint > 30000) {
printStats();
lastStatsPrint = millis();
}
// LED heartbeat
static unsigned long lastBlink = 0;
if (millis() - lastBlink > 1000) {
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
lastBlink = millis();
}
}
void setupWiFi() {
Serial.print("Connecting to WiFi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\n[OK] WiFi Connected!");
Serial.printf(" IP Address: %s\n", WiFi.localIP().toString().c_str());
}
void setupCoAP() {
udp.begin(COAP_PORT);
// Initialize observer array
for (int i = 0; i < MAX_OBSERVERS; i++) {
observers[i].active = false;
}
// Initialize block transfer state
blockState.active = false;
blockState.data = nullptr;
}
void handleCoapRequest() {
int packetSize = udp.parsePacket();
if (packetSize == 0) return;
stats.totalRequests++;
uint8_t buffer[512];
int len = udp.read(buffer, sizeof(buffer));
IPAddress remoteIP = udp.remoteIP();
uint16_t remotePort = udp.remotePort();
Serial.printf("\n[RX] Request from %s:%d (%d bytes)\n",
remoteIP.toString().c_str(), remotePort, len);
// Parse CoAP packet
CoapPacket packet;
parsePacket(buffer, len, packet);
// Track message types
if (packet.type == COAP_CON) stats.conMessages++;
else if (packet.type == COAP_NON) stats.nonMessages++;
// Route to appropriate handler
if (packet.uriPath == "/temperature") {
handleGetTemperature(remoteIP, remotePort, packet);
}
else if (packet.uriPath == "/config" && packet.code == COAP_GET) {
handleGetConfig(remoteIP, remotePort, packet);
}
else if (packet.uriPath == "/config" && packet.code == COAP_PUT) {
handlePutConfig(remoteIP, remotePort, packet);
}
else if (packet.uriPath == "/firmware") {
handleGetFirmware(remoteIP, remotePort, packet);
}
else if (packet.uriPath == "/.well-known/core") {
handleWellKnownCore(remoteIP, remotePort, packet);
}
else {
sendResponse(remoteIP, remotePort, packet, COAP_NOT_FOUND, nullptr, 0);
}
}
void parsePacket(uint8_t* buffer, size_t len, CoapPacket& packet) {
if (len < COAP_HEADER_SIZE) return;
// Parse header
packet.version = (buffer[0] >> 6) & 0x03;
packet.type = (buffer[0] >> 4) & 0x03;
packet.tokenLength = buffer[0] & 0x0F;
packet.code = buffer[1];
packet.messageId = (buffer[2] << 8) | buffer[3];
// Parse token (simplified - assuming 2-byte token)
if (packet.tokenLength > 0 && len >= COAP_HEADER_SIZE + 2) {
packet.token = (buffer[4] << 8) | buffer[5];
}
// Parse options (simplified URI path extraction)
int optionStart = COAP_HEADER_SIZE + packet.tokenLength;
packet.hasObserve = false;
packet.hasBlock2 = false;
packet.uriPath = "";
// Very simplified option parsing - extract URI path
if (optionStart < len && buffer[optionStart] != 0xFF) {
// In real implementation, properly decode options
// For demo, we'll check common patterns
String fullPath = String((char*)&buffer[optionStart]);
if (fullPath.indexOf("temperature") >= 0) packet.uriPath = "/temperature";
else if (fullPath.indexOf("config") >= 0) packet.uriPath = "/config";
else if (fullPath.indexOf("firmware") >= 0) packet.uriPath = "/firmware";
else if (fullPath.indexOf("well-known") >= 0) packet.uriPath = "/.well-known/core";
// Check for Observe option (option number 6)
if (buffer[optionStart] == 0x60) {
packet.hasObserve = true;
packet.observeValue = 0; // Register
}
// Check for Block2 option (option number 23)
for (int i = optionStart; i < len - 1; i++) {
if (buffer[i] == 0xD3) { // Block2 option delta
packet.hasBlock2 = true;
packet.block2Value = buffer[i + 1];
break;
}
}
}
Serial.printf(" Ver:%d Type:%d TKL:%d Code:%d MID:%d\n",
packet.version, packet.type, packet.tokenLength,
packet.code, packet.messageId);
Serial.printf(" URI: %s\n", packet.uriPath.c_str());
if (packet.hasObserve) Serial.println(" [Observe Option Present]");
if (packet.hasBlock2) Serial.printf(" [Block2: %d]\n", packet.block2Value);
}
void sendResponse(IPAddress ip, uint16_t port, CoapPacket& request,
uint8_t code, uint8_t* payload, size_t payloadLen) {
uint8_t buffer[512];
int pos = 0;
// Build header
buffer[pos++] = (COAP_VERSION << 6) | (COAP_ACK << 4) | request.tokenLength;
buffer[pos++] = code;
buffer[pos++] = (request.messageId >> 8) & 0xFF;
buffer[pos++] = request.messageId & 0xFF;
// Add token
if (request.tokenLength > 0) {
buffer[pos++] = (request.token >> 8) & 0xFF;
buffer[pos++] = request.token & 0xFF;
}
// Add payload marker if payload exists
if (payloadLen > 0 && payload != nullptr) {
buffer[pos++] = 0xFF; // Payload marker
memcpy(&buffer[pos], payload, payloadLen);
pos += payloadLen;
}
// Send response
udp.beginPacket(ip, port);
udp.write(buffer, pos);
udp.endPacket();
Serial.printf("[TX] Response: Code %d, %d bytes\n", code, pos);
}
void handleGetTemperature(IPAddress ip, uint16_t port, CoapPacket& request) {
Serial.println("-> GET /temperature");
// Check for Observe option
if (request.hasObserve) {
Serial.println(" Registering observer...");
addObserver(ip, port, request.token);
}
// Build JSON payload
char payload[64];
snprintf(payload, sizeof(payload),
"{\"temp\":%.1f,\"hum\":%.1f}", lastTemp, lastHumidity);
sendResponse(ip, port, request, COAP_CONTENT,
(uint8_t*)payload, strlen(payload));
}
void handleGetConfig(IPAddress ip, uint16_t port, CoapPacket& request) {
Serial.println("-> GET /config");
char payload[128];
snprintf(payload, sizeof(payload),
"{\"interval\":%d,\"threshold\":%.1f}",
TEMP_READ_INTERVAL/1000, 0.5);
sendResponse(ip, port, request, COAP_CONTENT,
(uint8_t*)payload, strlen(payload));
}
void handlePutConfig(IPAddress ip, uint16_t port, CoapPacket& request) {
Serial.println("-> PUT /config");
Serial.println(" Configuration updated (simulated)");
sendResponse(ip, port, request, COAP_CHANGED, nullptr, 0);
}
void handleGetFirmware(IPAddress ip, uint16_t port, CoapPacket& request) {
Serial.println("-> GET /firmware (Block Transfer)");
uint16_t blockNum = 0;
uint8_t szx = 2; // Block size = 64 bytes (2^(szx+4))
if (request.hasBlock2) {
blockNum = (request.block2Value >> 4) & 0xFFF;
szx = request.block2Value & 0x07;
}
size_t blockSize = (1 << (szx + 4));
size_t offset = blockNum * blockSize;
if (offset >= FIRMWARE_SIZE) {
sendResponse(ip, port, request, COAP_NOT_FOUND, nullptr, 0);
return;
}
size_t remaining = FIRMWARE_SIZE - offset;
size_t sendSize = min(remaining, blockSize);
bool moreBlocks = (offset + sendSize < FIRMWARE_SIZE);
Serial.printf(" Block %d: offset=%d size=%d more=%d\n",
blockNum, offset, sendSize, moreBlocks);
// Build response with Block2 option
uint8_t buffer[512];
int pos = 0;
// Header
buffer[pos++] = (COAP_VERSION << 6) | (COAP_ACK << 4) | request.tokenLength;
buffer[pos++] = COAP_CONTENT;
buffer[pos++] = (request.messageId >> 8) & 0xFF;
buffer[pos++] = request.messageId & 0xFF;
// Token
if (request.tokenLength > 0) {
buffer[pos++] = (request.token >> 8) & 0xFF;
buffer[pos++] = request.token & 0xFF;
}
// Block2 option (simplified encoding)
buffer[pos++] = 0xD3; // Option delta 23 (Block2)
buffer[pos++] = (blockNum << 4) | (moreBlocks ? 0x08 : 0x00) | szx;
// Payload marker
buffer[pos++] = 0xFF;
// Payload
memcpy(&buffer[pos], &firmwareData[offset], sendSize);
pos += sendSize;
udp.beginPacket(ip, port);
udp.write(buffer, pos);
udp.endPacket();
stats.blocksSent++;
Serial.printf("[TX] Block response: %d bytes (total progress: %d%%)\n",
pos, (int)((offset + sendSize) * 100 / FIRMWARE_SIZE));
}
void handleWellKnownCore(IPAddress ip, uint16_t port, CoapPacket& request) {
Serial.println("-> GET /.well-known/core (Resource Discovery)");
// RFC 6690 format: </path>;attr=value,</path2>;attr=value
const char* payload =
"</temperature>;obs;if=\"sensor\";rt=\"temp-c\","
"</config>;if=\"config\";rt=\"config\","
"</firmware>;if=\"fw\";rt=\"firmware\";sz=4096,"
"</.well-known/core>;if=\"core.rd\";rt=\"discovery\"";
sendResponse(ip, port, request, COAP_CONTENT,
(uint8_t*)payload, strlen(payload));
}
void addObserver(IPAddress ip, uint16_t port, uint16_t token) {
// Find empty slot or existing observer
int slot = -1;
for (int i = 0; i < MAX_OBSERVERS; i++) {
if (!observers[i].active) {
slot = i;
break;
}
if (observers[i].ip == ip && observers[i].port == port) {
slot = i;
break;
}
}
if (slot >= 0) {
observers[slot].ip = ip;
observers[slot].port = port;
observers[slot].token = token;
observers[slot].sequenceNum = 0;
observers[slot].lastNotify = millis();
observers[slot].active = true;
Serial.printf(" [OK] Observer added at slot %d (%s:%d)\n",
slot, ip.toString().c_str(), port);
} else {
Serial.println(" [WARN] Observer table full!");
}
}
void removeObserver(IPAddress ip, uint16_t port) {
for (int i = 0; i < MAX_OBSERVERS; i++) {
if (observers[i].active && observers[i].ip == ip &&
observers[i].port == port) {
observers[i].active = false;
Serial.printf(" [OK] Observer removed from slot %d\n", i);
break;
}
}
}
void notifyObservers() {
static float prevTemp = 0.0;
// Only notify if temperature changed significantly
if (abs(lastTemp - prevTemp) < 0.5) return;
prevTemp = lastTemp;
Serial.println("\n[NOTIFY] Notifying observers (temp changed)...");
for (int i = 0; i < MAX_OBSERVERS; i++) {
if (!observers[i].active) continue;
// Build notification packet
char payload[64];
snprintf(payload, sizeof(payload),
"{\"temp\":%.1f,\"hum\":%.1f,\"seq\":%u}",
lastTemp, lastHumidity, observers[i].sequenceNum);
uint8_t buffer[512];
int pos = 0;
// Header (NON notification)
buffer[pos++] = (COAP_VERSION << 6) | (COAP_NON << 4) | 2;
buffer[pos++] = COAP_CONTENT;
buffer[pos++] = (messageIdCounter >> 8) & 0xFF;
buffer[pos++] = messageIdCounter & 0xFF;
messageIdCounter++;
// Token
buffer[pos++] = (observers[i].token >> 8) & 0xFF;
buffer[pos++] = observers[i].token & 0xFF;
// Observe option
buffer[pos++] = 0x60; // Observe option
buffer[pos++] = observers[i].sequenceNum & 0xFF;
// Payload marker
buffer[pos++] = 0xFF;
// Payload
memcpy(&buffer[pos], payload, strlen(payload));
pos += strlen(payload);
// Send notification
udp.beginPacket(observers[i].ip, observers[i].port);
udp.write(buffer, pos);
udp.endPacket();
observers[i].sequenceNum++;
observers[i].lastNotify = millis();
stats.observeNotifications++;
Serial.printf(" -> Notified observer %d (%s:%d) seq=%u\n",
i, observers[i].ip.toString().c_str(),
observers[i].port, observers[i].sequenceNum - 1);
}
}
void updateTemperature() {
if (millis() - lastTempRead < TEMP_READ_INTERVAL) return;
TempAndHumidity data = dht.getTempAndHumidity();
if (dht.getStatus() == 0) {
lastTemp = data.temperature;
lastHumidity = data.humidity;
} else {
// Simulated data if sensor fails
lastTemp = 22.0 + random(-20, 30) / 10.0;
lastHumidity = 50.0 + random(-100, 100) / 10.0;
}
lastTempRead = millis();
}
void initFirmwareData() {
// Fill with simulated firmware data (pattern for demo)
for (size_t i = 0; i < FIRMWARE_SIZE; i++) {
firmwareData[i] = (uint8_t)(i & 0xFF);
}
Serial.printf("[OK] Firmware data initialized (%d bytes)\n", FIRMWARE_SIZE);
}
void printStats() {
Serial.println("\n=========================================");
Serial.println(" CoAP Server Statistics ");
Serial.println("=========================================");
Serial.printf(" Total Requests: %6u\n", stats.totalRequests);
Serial.printf(" CON Messages: %6u\n", stats.conMessages);
Serial.printf(" NON Messages: %6u\n", stats.nonMessages);
Serial.printf(" Retransmissions: %6u\n", stats.retransmissions);
Serial.printf(" Blocks Sent: %6u\n", stats.blocksSent);
Serial.printf(" Observe Notifications: %6u\n", stats.observeNotifications);
Serial.println("-----------------------------------------");
Serial.printf(" Active Observers: %6d\n", countActiveObservers());
Serial.printf(" Last Temperature: %6.1fC\n", lastTemp);
Serial.printf(" Last Humidity: %6.1f%%\n", lastHumidity);
Serial.println("=========================================\n");
}
int countActiveObservers() {
int count = 0;
for (int i = 0; i < MAX_OBSERVERS; i++) {
if (observers[i].active) count++;
}
return count;
}1231.3.5 Wokwi diagram.json Configuration
{
"version": 1,
"author": "CoAP Advanced Features Lab",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 0, "left": 0, "attrs": {} },
{ "type": "wokwi-dht22", "id": "dht", "top": -10, "left": 100, "attrs": {} },
{ "type": "wokwi-led", "id": "led", "top": 50, "left": 150, "attrs": { "color": "green" } },
{ "type": "wokwi-pushbutton", "id": "btn", "top": 100, "left": 100, "attrs": { "color": "blue" } }
],
"connections": [
[ "esp:GND.1", "dht:GND", "black", [ "v0" ] ],
[ "esp:3V3", "dht:VCC", "red", [ "v0" ] ],
[ "esp:D15", "dht:SDA", "green", [ "v0" ] ],
[ "esp:D2", "led:A", "yellow", [ "v0" ] ],
[ "led:C", "esp:GND.2", "black", [ "v0" ] ],
[ "btn:1.l", "esp:D4", "blue", [ "v0" ] ],
[ "btn:1.r", "esp:GND.3", "black", [ "v0" ] ]
]
}1231.4 Challenge Exercises
Goal: Handle GET /temperature with Observe: 1 (deregister) to stop notifications.
Steps: 1. In parsePacket(), detect when Observe option value is 1 (not 0) 2. In handleGetTemperature(), call removeObserver() when observeValue == 1 3. Verify with Serial Monitor that notifications stop after deregistration
Hint: The Observe option value is 0 for register, 1 for deregister.
Goal: Implement exponential backoff for CON messages that don’t receive ACK.
Steps: 1. Create a PendingMessage struct to track sent CON messages awaiting ACK 2. Start a timer (2s initial) when sending CON 3. If no ACK received, retransmit with doubled timeout (2s -> 4s -> 8s -> 16s) 4. After 4 retries, give up and increment stats.retransmissions
Hint: Use millis() for timing, store sent messages in an array.
Goal: Respond with smaller block size if client requests too large.
Steps: 1. Parse client’s requested SZX from Block2 option 2. If server cannot support that size, respond with smaller SZX 3. Test with different SZX values (0=16B, 2=64B, 6=1024B)
Block2 encoding: NUM (12 bits) | M (1 bit) | SZX (3 bits)
Goal: Simulate DTLS PSK validation before processing requests.
Steps: 1. Define a pre-shared key (e.g., const char* PSK = "secret123") 2. Add a simple header check (e.g., first 8 bytes of payload) 3. Reject requests without valid PSK with 4.01 Unauthorized 4. This is NOT real DTLS - just a simulation for understanding
Note: Real DTLS requires a proper library like mbed TLS.
Goal: Parse query parameters in /.well-known/core requests to filter resources.
Steps: 1. Parse ?rt=temp-c query from the request URI 2. Filter the Link Format response to only matching resources 3. Support rt (resource type) and if (interface) filters
RFC 6690 format: </path>;rt="type";if="interface"
1231.5 Expected Outcomes
After completing this lab, you should observe:
Observe Pattern Working: - Client registers with GET /temperature + Observe option - Server automatically sends NON notifications when temp changes >0.5C - Notifications include incrementing sequence numbers
Block Transfer Functional: - Firmware resource (/firmware) transfers 4096 bytes in 64-byte blocks - Client requests blocks sequentially (Block2 NUM=0,1,2,…) - Server responds with M (more) bit set until final block
Resource Discovery: - GET /.well-known/core returns RFC 6690 Link Format - Lists all available resources with attributes (obs, if, rt, sz)
Statistics Tracking: - Serial Monitor shows live statistics every 30 seconds - Tracks CON vs NON messages, blocks sent, notifications
LED Status Indication: - LED blinks every second showing server alive - Could extend to indicate active requests
1231.6 Testing with Python Client
# Test script for CoAP server
import asyncio
from aiocoap import *
async def test_observe():
"""Test observe pattern"""
protocol = await Context.create_client_context()
request = Message(
code=Code.GET,
uri='coap://<ESP32_IP>/temperature',
observe=0 # Register
)
request_handle = protocol.request(request)
# Get initial response
response = await request_handle.response
print(f"Initial: {response.payload.decode()}")
# Wait for notifications
async for response in request_handle.observation:
print(f"Notification: {response.payload.decode()}")
async def test_block_transfer():
"""Test block transfer"""
protocol = await Context.create_client_context()
full_data = bytearray()
block_num = 0
while True:
request = Message(
code=Code.GET,
uri=f'coap://<ESP32_IP>/firmware'
)
request.opt.block2 = (block_num, 0, 2) # NUM, M, SZX
response = await protocol.request(request).response
full_data.extend(response.payload)
if not response.opt.block2.more:
break
block_num += 1
print(f"Received {len(full_data)} bytes")
# Run tests
asyncio.run(test_observe())1231.7 Worked Example: Block Transfer Calculation
Scenario: An agricultural sensor needs to receive a 48 KB firmware update over a 6LoWPAN network with 127-byte MTU (after 6LoWPAN headers, only 80 bytes available for CoAP payload).
Given: - Firmware size: 48 KB (49,152 bytes) - Available payload per block: 64 bytes (Block2 SZX=2, leaving room for CoAP header and options) - Network packet loss rate: 5% - Round-trip time (RTT): 200 ms - CoAP CON message timeout: 2 seconds with exponential backoff
Steps:
- Calculate number of blocks required:
- Total blocks = ceil(49,152 / 64) = 768 blocks
- Block numbering: Block2 option uses NUM field (0-767)
- Estimate transmission time (ideal, no loss):
- Each block requires request + response = 1 RTT = 200 ms
- Total time = 768 blocks x 200 ms = 153.6 seconds (2 min 34 sec)
- Account for packet loss with retransmissions:
- Expected lost blocks = 768 x 5% = ~38 blocks
- First retry adds 2 sec timeout + 200 ms RTT = 2.2 sec each
- Additional time = 38 x 2.2 sec = 83.6 seconds
- Total estimated time = 153.6 + 83.6 = 237.2 seconds (3 min 57 sec)
- Verify Block2 option encoding:
- Block2 format: NUM (variable) | M (1 bit) | SZX (3 bits)
- SZX=2 means block size = 2^(SZX+4) = 2^6 = 64 bytes
- For block 500: NUM=500, M=1 (more blocks follow), SZX=2
- Block2 option value = (500 << 4) | (1 << 3) | 2 = 0x1F4A
Result: The 48 KB firmware transfers in approximately 4 minutes over the constrained 6LoWPAN link, using 768 individually acknowledged 64-byte blocks. The client can resume from any failed block without restarting the entire transfer.
Key Insight: Block transfer enables reliable large payload transmission over constrained networks by trading latency for reliability. The ability to resume from any block position makes it ideal for unreliable networks where connection drops are common. Choose block size (SZX) based on network MTU: SZX=2 (64B) for 6LoWPAN, SZX=6 (1024B) for Wi-Fi.
1231.8 Common Pitfall: Block Transfer SZX Negotiation
The Mistake: Developers hardcode Block2 SZX (size exponent) to the maximum value (SZX=6 for 1024-byte blocks) assuming larger blocks are always more efficient, causing firmware updates to fail on 6LoWPAN or Thread networks.
Why It Happens: The misconception that “fewer blocks = faster transfer” ignores network MTU constraints. IEEE 802.15.4 networks have 127-byte physical layer MTU, which after 6LoWPAN headers leaves only 80-100 bytes for CoAP payload. A 1024-byte block request triggers IP fragmentation, causing packet loss rates of 30-50% on lossy wireless links.
The Fix: Always negotiate block size based on network constraints using the Block2 option’s SZX field (size = 2^(SZX+4) bytes):
- SZX=0 (16 bytes): Ultra-constrained networks, LoRa with small payloads
- SZX=2 (64 bytes): 6LoWPAN, Thread, IEEE 802.15.4 mesh networks
- SZX=4 (256 bytes): Bluetooth Low Energy, constrained Wi-Fi
- SZX=6 (1024 bytes): Wi-Fi, Ethernet, unconstrained networks
Implement adaptive block sizing: start with SZX=6, and if the server responds with a smaller SZX in the Block2 option, reduce to match. Example: client requests Block2 SZX=6, server responds with Block2 SZX=2 indicating its MTU constraint.
1231.9 Visual Reference Gallery
Understanding CoAP’s four message types is essential for designing efficient IoT communication patterns that balance reliability requirements against power and bandwidth constraints.
The Observe extension enables efficient publish-subscribe semantics over CoAP’s request-response model, achieving significant power savings compared to traditional polling approaches.
Block transfer is critical for practical IoT applications requiring firmware updates or bulk data transfer over networks with small MTU sizes and limited buffer capacity.
1231.10 Summary
This advanced lab demonstrated production-grade CoAP features on ESP32:
- Observe Pattern: Implemented push notifications with sequence numbers, reducing polling by 97%+ for slowly-changing data
- Block-wise Transfer: Handled 4KB firmware in 64-byte blocks, enabling resumable transfers on constrained networks
- Resource Discovery: Exposed
/.well-known/corein RFC 6690 Link Format for automated service discovery - Server Architecture: Built multi-resource CoAP server with temperature sensor, configuration, and firmware endpoints
- Statistics Tracking: Monitored CON/NON message types, block transfers, and observer notifications
1231.11 Knowledge Check
1231.12 What’s Next
In the next chapter, CoAP Comprehensive Review, you’ll test your knowledge with quiz questions, compare CoAP vs MQTT vs HTTP, and explore production deployment patterns.
Key Takeaways: - Observe pattern uses sequence numbers for ordering and loss detection - Block transfer uses SZX (size exponent) where block_size = 2^(SZX+4) - Service discovery uses /.well-known/core with RFC 6690 Link Format - Negotiate block size based on network MTU - don’t hardcode SZX=6 - Statistics tracking helps monitor server health in production