This is an old revision of the document!


Lab 4. The CoAP Protocol

CoAP — the Constrained Application Protocol — is a compact, web-inspired protocol designed for tiny devices and lossy networks. Think of it as “HTTP’s cousin” for the Internet of Things: it preserves RESTful ideas (resources, methods, content types) but trims the bandwidth and processing overhead so microcontrollers running on batteries can still speak the language of the web.

Why CoAP Exists

Classic HTTP works well on desktops and phones, but it assumes abundant memory, stable links, and generous power budgets. Constrained nodes and low-power lossy networks (LoWPAN, BLE, long‑range sub‑GHz, shared mesh) rarely have those luxuries. CoAP targets this environment. It keeps the resource model developers love while reducing message size, connection setup cost, and round‑trips. The protocol is simple enough to fit in a few kilobytes of code, yet expressive enough to model real devices and digital twins.

Core Model (REST, but lightweight)

CoAP embeds a REST design: resources are identified by URIs, and clients interact using familiar methods. A minimal binary header replaces HTTP’s verbose text format, and the transport defaults to UDP. Reliable delivery is still achievable through confirmable messages and retransmissions handled by the CoAP layer, not the transport.

Methods mirror HTTP semantics with a leaner encoding: GET, POST, PUT, DELETE. Servers can expose well-known endpoints (for example, /.well-known/core/) to advertise available resources and interfaces using the CoRE Link Format. Content types are represented by compact numeric Content‑Format identifiers rather than long MIME strings, cutting repeated overhead dramatically.

Messages, Types, and Reliability

Every CoAP message carries a tiny fixed header followed by Options and an optional Payload. Options are encoded in a delta/length scheme that compresses repeated numbers and elides absent fields.

Four message types shape reliability and control flow:

  • CON (Confirmable) requires an ACK and is retransmitted with exponential backoff until acknowledged or timed out.
  • NON (Non‑confirmable) is “fire‑and‑forget”, suitable for telemetry where occasional loss is acceptable.
  • ACK acknowledges a CON; it may echo a piggybacked response to save one round‑trip.
  • RST signals that a message was unrecognized or could not be processed in context.

The combination allows request/response exchanges over UDP without a connection handshake. Message IDs and Tokens help match responses to requests, even across retransmissions or when responses are sent later (separate response).

URIs, Discovery, and Content

A CoAP URI looks like coap://host:port/path?query (or coaps:// for DTLS-secured). Discovery usually begins with GET coap://device/.well-known/core, which returns a set of typed links describing resources, interfaces, and content formats. Because links and attributes are compactly encoded, devices can publish meaningful self‑descriptions without burning through budgeted bytes.

CoAP supports content negotiation via Accept and Content‑Format options. Common formats include application/json, application/cbor, and sensor‑oriented binary representations. CBOR pairs well with CoAP because it offers dense, schema‑agnostic encoding with minimal overhead on tiny MCUs.

Caching and ETags

CoAP borrows HTTP’s cache validators (ETag, Max‑Age) so intermediaries and clients can avoid redundant transfers. An ETag is a small opaque token that changes when the resource representation changes. With Max‑Age, clients can confidently reuse cached data until it expires, then revalidate or fetch an update.

Observe: Push‑Style Updates over Pull Semantics

Polling wastes energy. CoAP’s Observe extension lets a client register interest in a resource, turning the server into a notifier. After the initial GET with an Observe option, the server streams changes as normal CoAP responses, each correlated by the original token. Ordering and freshness are guided by a sequence number (often a monotonic value) so clients can discard stale deliveries. Observe works through intermediaries, letting gateways or proxies fan out updates to many clients efficiently.

Block‑Wise Transfer: When Payloads Don’t Fit

Small MTUs and duty‑cycle limits make large payloads tricky. Block1 and Block2 options slice requests and responses into manageable chunks. The client or server moves block‑by‑block, using the options to indicate size and index. This allows firmware downloads, log retrieval, or model updates without exceeding link constraints, while keeping memory footprints small on each side.

Security: DTLS/TLS and OSCORE

Security comes in layers. The simplest path is DTLS (for UDP) or TLS (when using CoAP over TCP), providing channel protection similar to HTTPS. In constrained meshes, channel security alone can be fragile; topologies change, and intermediaries may need to route or cache.

OSCORE (Object Security for Constrained RESTful Environments) protects the message payload and options end‑to‑end using COSE, independent of the transport. This enables proxies to function while keeping application data confidential and authenticated between true endpoints. Keys can be derived from pre‑shared material, EDHOC handshakes, or other provisioning schemes.

Transports and Variants

While UDP is the default, CoAP also runs over TCP, TLS, and WebSockets. The TCP/TLS mapping preserves CoAP’s method and option model but leans on the stream for reliability and congestion control. This is handy when middleboxes block UDP or when a deployment prefers a connection‑oriented flow for policy reasons. The coap+ws and coaps+ws schemes allow traversal of web infrastructure while retaining CoAP semantics.

Proxies and HTTP Interop

A powerful feature is native proxying. CoAP‑to‑HTTP and HTTP‑to‑CoAP gateways translate methods, status codes, options/headers, and payloads. This is how a tiny sensor can appear as a normal web resource or how a cloud app can issue requests to a sleepy field device without teaching every service a new protocol. Proper mapping of caching directives and content types ensures the two worlds remain consistent.

Congestion Control and Timers

Because it rides over UDP by default, CoAP specifies conservative retransmission backoff, leaky‑bucket style pacing, and limits on simultaneous confirmable messages. Implementations typically expose tunables for ACK_TIMEOUT, ACK_RANDOM_FACTOR, and MAX_RETRANSMIT. The aim is politeness on shared, lossy links: back off early, avoid bursts, and prefer NON traffic when eventual consistency suffices.

Simple CoAP Client

Let's build a simple CoAP client on our ESP32-C6 Sparrow. You will need to include hirotakaster/CoAP simple library @ ^1.1.2 in your platformio.ini file.

The sketch brings up the ESP32-C6 on Wi-Fi, binds a UDP socket, and sets up the CoAP simple library to act as a client. On boot it registers a response callback (coap.response(onCoapResponse)), starts the CoAP stack (coap.start()), DNS-resolves californium.eclipseprojects.io, and immediately sends a CoAP GET to the server’s root resource (path ””, i.e., /). A timer then repeats that GET every ~10 seconds. The main loop continuously calls coap.loop(), which lets the library service the UDP socket, match replies to outstanding requests, and trigger the response callback when packets arrive.

On the CoAP side, the library handles all the protocol details—message type, message ID, token management, options, and parsing—so your sketch only deals with high-level actions. You issue a client request with coap.get(remoteIP, 5683, ””), and when the server replies, the callback receives a parsed CoapPacket. The code prints the CoAP code (e.g., 2.xx success), basic header info, and the payload as text.

main.cpp
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <coap-simple.h>   // hirotakaster/CoAP simple library
 
// ===== Wi-Fi creds =====
const char* WIFI_SSID = "UPB-Guest";
const char* WIFI_PASS = "";
 
// ===== CoAP server (Californium demo) =====
const char* COAP_HOST = "californium.eclipseprojects.io";
const uint16_t COAP_PORT = 5683;   // RFC 7252 default port
 
// UDP + CoAP objects
WiFiUDP udp;
Coap coap(udp);
 
// ----- Response callback: prints payload as text -----
void onCoapResponse(CoapPacket &packet, IPAddress ip, int port) {
  // Copy payload into a printable buffer
  const size_t n = packet.payloadlen;
  String from = ip.toString() + ":" + String(port);
  Serial.printf("[CoAP] Response from %s  code=%u (0x%02X)  len=%u\n",
                from.c_str(), (packet.code >> 5), packet.code, (unsigned)n);
 
  if (n > 0) {
    // Make it printable; not all payloads are ASCII
    size_t m = n;
    if (m > 1023) m = 1023;     // clamp for serial
    char buf[1024];
    memcpy(buf, packet.payload, m);
    buf[m] = '\0';
    Serial.println(F("[CoAP Payload]"));
    Serial.println(buf);
  }
}
 
// Re-resolves hostname and sends GET to "/"
bool coapGetRoot() {
  IPAddress remote;
  if (!WiFi.hostByName(COAP_HOST, remote)) {
    Serial.println(F("[CoAP] DNS failed"));
    return false;
  }
  Serial.printf("[CoAP] GET coap://%s:%u/\n", COAP_HOST, COAP_PORT);
  // Path is "" for "/", per library examples
  int msgid = coap.get(remote, COAP_PORT, "");
  (void)msgid; // not used further in this demo
  return true;
}
 
unsigned long lastGetMs = 0;
const unsigned long GET_PERIOD_MS = 10000;
 
void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println(F("\n== CoAP client (Californium demo) =="));
 
  // Wi-Fi
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  Serial.print(F("Connecting"));
  while (WiFi.status() != WL_CONNECTED) { delay(300); Serial.print('.'); }
  Serial.println();
  Serial.print(F("Wi-Fi OK. IP: ")); Serial.println(WiFi.localIP());
 
  // Bind UDP so we can receive responses (use standard CoAP port or 0 for ephemeral)
  udp.begin(5683);
 
  // Register response handler and start CoAP
  coap.response(onCoapResponse);
  coap.start();
 
  // First GET right away
  coapGetRoot();
  lastGetMs = millis();
}
 
void loop() {
  // Let the library process incoming UDP (and fire onCoapResponse)
  coap.loop();
 
  // Periodic GET
  unsigned long now = millis();
  if (now - lastGetMs >= GET_PERIOD_MS) {
    lastGetMs = now;
    coapGetRoot();
  }
}

After building, you should get a similar print-out in the console:

Connecting....
Wi-Fi OK. IP: 10.41.163.62
[CoAP] GET coap://californium.eclipseprojects.io:5683/
[CoAP] Response from 20.47.97.44:5683  code=4 (0x84)  len=0

CoAP Server

In this step we will focus on turning your Sparrow into a tiny RESTful CoAP server with discoverable resources. Below is a single PlatformIO (Arduino) sketch that hosts these resources: /sys/uptime (seconds), /net/rssi (dBm), /led (GET state, PUT “on/off”), /echo (echoes payload)

The code also implements /.well-known/core in CoRE Link Format (RFC 6690) so tools can discover your endpoints.

You will have to install libcoap on your machine in order to communicate with the server.

Debian/Ubuntu sudo apt update sudo apt install -y libcoap2-bin

# binaries: coap-client, coap-server

macOS

# needs Homebrew: https://brew.sh

brew update

brew install libcoap

# binaries: coap-client, coap-server

Windows

# Install WSL (once)

wsl –install

# Open Ubuntu in WSL, then:

sudo apt update

sudo apt install -y libcoap2-bin

main.cpp
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <Adafruit_NeoPixel.h>
#include <coap-simple.h>   // hirotakaster CoAP simple library
 
// ===== Wi-Fi creds =====
const char* WIFI_SSID = "UPB-Guest";
const char* WIFI_PASS = "";
 
// ===== UDP + CoAP objects =====
WiFiUDP udp;
Coap coap(udp);
 
// ===== On-board NeoPixel (GPIO 3) =====
constexpr uint8_t kNeoPixelPin = 3;
constexpr uint8_t kNeoPixelCount = 1;
constexpr uint8_t kNeoPixelBrightness = 128; // 50% to limit power draw
 
Adafruit_NeoPixel g_pixel(kNeoPixelCount, kNeoPixelPin, NEO_GRB + NEO_KHZ800);
bool g_pixelReady = false;
 
// ===== Simple LED state =====
static bool g_ledOn = false;
 
void applyLedState() {
  if (!g_pixelReady) {
    return;
  }
  if (g_ledOn) {
    g_pixel.setPixelColor(0, g_pixel.Color(255, 255, 255));
  } else {
    g_pixel.setPixelColor(0, 0, 0, 0);
  }
  g_pixel.show();
}
 
// ----- helper: send text/plain 2.05 Content -----
static void coapSendText(IPAddress ip, int port, const CoapPacket& req,
                         const char* text) {
  Serial.printf("[CoAP] Replying to %s:%d mid=%u code=%u tokensz=%u\n",
                ip.toString().c_str(), port, req.messageid, req.code, req.tokenlen);
  coap.sendResponse(
    ip, port, req.messageid,
    text, strlen(text),
    COAP_RESPONSE_CODE(205),      // 2.05 Content
    COAP_TEXT_PLAIN,
    req.token, req.tokenlen
  );
}
 
// ----- /sys/uptime (GET) -----
void h_sys_uptime(CoapPacket &packet, IPAddress ip, int port) {
  unsigned long secs = millis() / 1000UL;
  char buf[32];
  snprintf(buf, sizeof(buf), "%lu", (unsigned long)secs);
  coapSendText(ip, port, packet, buf);
}
 
// ----- /net/rssi (GET) -----
void h_net_rssi(CoapPacket &packet, IPAddress ip, int port) {
  long rssi = WiFi.RSSI();
  char buf[16];
  snprintf(buf, sizeof(buf), "%ld", rssi);
  coapSendText(ip, port, packet, buf);
}
 
// ----- /led (GET state, PUT/POST "on" or "off") -----
void h_led(CoapPacket &packet, IPAddress ip, int port) {
  if (packet.code == COAP_GET) {
    coapSendText(ip, port, packet, g_ledOn ? "on" : "off");
    return;
  }
 
  if (packet.code == COAP_PUT || packet.code == COAP_POST) {
    String body;
    body.reserve(packet.payloadlen + 1);
    for (size_t i = 0; i < packet.payloadlen; ++i) body += (char)packet.payload[i];
    body.trim();
    if (body.equalsIgnoreCase("on"))  g_ledOn = true;
    if (body.equalsIgnoreCase("off")) g_ledOn = false;
 
    applyLedState();
 
    // Acknowledge change (2.04 Changed) and return new state
    const char* state = g_ledOn ? "on" : "off";
    coap.sendResponse(
      ip, port, packet.messageid,
      state, strlen(state),
      COAP_RESPONSE_CODE(204),      // 2.04 Changed
      COAP_TEXT_PLAIN,
      packet.token, packet.tokenlen
    );
    return;
  }
 
  // Method not allowed
  coap.sendResponse(
    ip, port, packet.messageid,
    "Method Not Allowed", 18,
    COAP_RESPONSE_CODE(405),
    COAP_TEXT_PLAIN,
    packet.token, packet.tokenlen
  );
}
 
// ----- /echo (POST/PUT echoes payload; GET returns hint) -----
void h_echo(CoapPacket &packet, IPAddress ip, int port) {
  if (packet.payloadlen == 0 || packet.code == COAP_GET) {
    const char* hint = "POST/PUT a payload and I'll echo it.";
    coapSendText(ip, port, packet, hint);
    return;
  }
  coap.sendResponse(
    ip, port, packet.messageid,
    (const char*)packet.payload, packet.payloadlen,
    COAP_RESPONSE_CODE(205),      // 2.05 Content
    COAP_TEXT_PLAIN,
    packet.token, packet.tokenlen
  );
}
 
// ----- /.well-known/core (RFC 6690 CoRE Link Format) -----
void h_wkc(CoapPacket &packet, IPAddress ip, int port) {
  Serial.printf("[CoAP] Discovery hit from %s:%d type=%u code=%u accept=%s\n",
                ip.toString().c_str(),
                port,
                packet.type,
                packet.code,
                packet.optionnum ? "yes" : "no");
  // Advertise resources and basic attributes
  const char* linkfmt =
    "</sys/uptime>;rt=\"sys.uptime\";if=\"core.a\";ct=0,"
    "</net/rssi>;rt=\"net.rssi\";if=\"core.a\";ct=0,"
    "</led>;rt=\"dev.led\";if=\"core.a\";ct=0,"
    "</echo>;rt=\"util.echo\";if=\"core.p\";ct=0";
 
  coap.sendResponse(
    ip, port, packet.messageid,
    linkfmt, strlen(linkfmt),
    COAP_RESPONSE_CODE(205),
    COAP_APPLICATION_LINK_FORMAT, // ct=40
    packet.token, packet.tokenlen
  );
}
 
void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println("\n== CoAP RESTful mapping & discovery (server-only) ==");
 
  // Wi-Fi
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  Serial.print("Connecting");
  while (WiFi.status() != WL_CONNECTED) { delay(300); Serial.print("."); }
  Serial.println();
  Serial.print("Wi-Fi OK. IP: "); Serial.println(WiFi.localIP());
  Serial.printf("Listening for CoAP on udp/%d\n", COAP_DEFAULT_PORT);
 
  g_pixel.begin();
  g_pixel.setBrightness(kNeoPixelBrightness);
  g_pixel.clear();
  g_pixel.show();
  g_pixelReady = true;
  applyLedState();
 
  // Start CoAP server on udp/5683
  udp.begin(5683);
 
  // Register resources
  coap.server(h_sys_uptime, "sys/uptime");
  coap.server(h_net_rssi,   "net/rssi");
  coap.server(h_led,        "led");
  coap.server(h_echo,       "echo");
  coap.server(h_wkc,        ".well-known/core"); // discovery
 
  // Start CoAP stack
  coap.start();
}
 
void loop() {
  // Process incoming CoAP requests
  coap.loop();
}

Test it from your computer with the following commands, where esp-ip is the IP the node prints after connecting in the serial terminal:

# discovery
coap-client -v9 -m get -A 40 coap://<esp-ip>/.well-known/core

# other resources
coap-client -m get coap://<esp-ip>/sys/uptime
coap-client -m get coap://<esp-ip>/net/rssi
coap-client -m get coap://<esp-ip>/led
coap-client -m put -e "on"  coap://<esp-ip>/led
coap-client -m put -e "off" coap://<esp-ip>/led
coap-client -m post -e "hello" coap://<esp-ip>/echo
iothings/laboratoare/2025/lab4.1760441571.txt.gz · Last modified: 2025/10/14 14:32 by dan.tudose
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0