COAP protocol - arduino ESP32 M2M (end-to-end) communication and code explanation

preface

Recently, I was studying the COAP protocol and found a COAP simple library that can be used on ESP32 when trying to use the COAP protocol. Although the library is not perfect, the part about loop processing should not be completed, but it is easier for friends who contact COAP for the first time to understand and learn. Friends who need it can download it below:

https://github.com/hirotakaster/CoAP-simple-library

I used to use IOT PI COAP to communicate with PC node coap, but because the COAP simple library is imperfect, I can't communicate with node coap normally. I can only communicate with the same library devices. This time, I'll try M2M communication between ESP32.

Get Library

This library can be downloaded using the arduino IDE:

If you don't see this library, you can go to the preferences and add the website of the additional development board Manager:

https://github.com/espressif/arduino-esp32/releases/download/1.0.5/package_esp32_index.json

I can refer to for specific use arduino ultra detailed introduction to development Or download it directly through the GitHub website I sent above.

Code parsing

The following code may have been changed or cropped for ease of explanation.

This demo is an integration of client and server. You only need to register the corresponding callback function.

Initialization part

This part includes device initialization, protocol initialization and other parts, focusing on the callback function of the server / client. Similar to the SDDC official demo, after registering the callback function, find the corresponding callback function through the corresponding endpoint.

#include <WiFi.h>
#include <WiFiUdp.h>
#include <coap-simple.h>

void setup() {
  Serial.begin(115200);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  // LED State
  pinMode(9, OUTPUT);
  digitalWrite(9, HIGH);
  LEDSTATE = true;
  
  // Add server url endpoint
  // You can add multiple endpoint URLs
  //      coap.server(callback_switch, "switch");
  //      coap.server(callback_env, "env/temp");
  //      coap.server(callback_env, "env/humidity");
  Serial.println("Setup Callback Light");
  // In fact, the registration server handles the callback function
  // Add the handler pointer and url to uri.add 
  coap.server(callback_light, "light");

  // Registers the callback function for the client response.
  // this endpoint is single callback.
  Serial.println("Setup Response Callback");
  // It is the same as above. In fact, it is to register the callback function pointer in resp
  coap.response(callback_response);

  // Start the soap server / client using the default port 5683 
  coap.start();
}

void loop() {
  // As a client, send a GET or put soap request to the soap server
  // Can be sent to another ESP32 
  // msgid = coap.put(IPAddress(192, 168, 128, 101), 5683, "light", "0");
  // msgid = coap.get(IPAddress(192, 168, 128, 101), 5683, "light");

  delay(1000);
  coap.loop();
}

Callback function

// The CoAP server endpoint URL processes and responds to commands sent by the client
void callback_light(CoapPacket &packet, IPAddress ip, int port) 
{
  // This is a callback function that simulates the control lamp by receiving the command
  Serial.println("[Light] ON/OFF");
  Serial.println(packet.messageid);

  // Send response
  char p[packet.payloadlen + 1];
  memcpy(p, packet.payload, packet.payloadlen);
  p[packet.payloadlen] = NULL;
  
  String message(p);

  if (message.equals("0"))
    LEDSTATE = false;
  else if(message.equals("1"))
    LEDSTATE = true;
      
  if (LEDSTATE) {
    digitalWrite(9, HIGH) ; 
      Serial.println("[Light] ON");

    coap.sendResponse(ip, port, packet.messageid, "1");
  } else { 
    digitalWrite(9, LOW) ; 
    Serial.println("[Light] OFF");
    coap.sendResponse(ip, port, packet.messageid, "0");
  }
}

// CoAP client response callback
void callback_response(CoapPacket &packet, IPAddress ip, int port) 
{
  Serial.println("[Coap Response got]");
  
  char p[packet.payloadlen + 1];
  memcpy(p, packet.payload, packet.payloadlen);
  p[packet.payloadlen] = NULL;
  
  Serial.println(p);
}

Library code

Message structure definition:

// Determine the message type in the SOAP message layer
typedef enum {
    COAP_CON = 0,     // Reliable transmission
    COAP_NONCON = 1,  // Unreliable transmission
    COAP_ACK = 2,     // reply
    COAP_RESET = 3    // Passive retransmission request after message abnormality
} COAP_TYPE;
// The actions executed by the command are at the request / response layer
typedef enum {
    COAP_GET = 1,
    COAP_POST = 2,    // Active retransmission command
    COAP_PUT = 3,
    COAP_DELETE = 4
} COAP_METHOD;
// Response code, equivalent to function return value or err code, is in the request / response layer
typedef enum {
    COAP_CREATED = RESPONSE_CODE(2, 1),
    COAP_DELETED = RESPONSE_CODE(2, 2),
    COAP_VALID = RESPONSE_CODE(2, 3),
    COAP_CHANGED = RESPONSE_CODE(2, 4),
    COAP_CONTENT = RESPONSE_CODE(2, 5),
    COAP_BAD_REQUEST = RESPONSE_CODE(4, 0),
    COAP_UNAUTHORIZED = RESPONSE_CODE(4, 1),
    COAP_BAD_OPTION = RESPONSE_CODE(4, 2),
    COAP_FORBIDDEN = RESPONSE_CODE(4, 3),
    COAP_NOT_FOUNT = RESPONSE_CODE(4, 4),
    COAP_METHOD_NOT_ALLOWD = RESPONSE_CODE(4, 5),
    COAP_NOT_ACCEPTABLE = RESPONSE_CODE(4, 6),
    COAP_PRECONDITION_FAILED = RESPONSE_CODE(4, 12),
    COAP_REQUEST_ENTITY_TOO_LARGE = RESPONSE_CODE(4, 13),
    COAP_UNSUPPORTED_CONTENT_FORMAT = RESPONSE_CODE(4, 15),
    COAP_INTERNAL_SERVER_ERROR = RESPONSE_CODE(5, 0),
    COAP_NOT_IMPLEMENTED = RESPONSE_CODE(5, 1),
    COAP_BAD_GATEWAY = RESPONSE_CODE(5, 2),
    COAP_SERVICE_UNAVALIABLE = RESPONSE_CODE(5, 3),
    COAP_GATEWAY_TIMEOUT = RESPONSE_CODE(5, 4),
    COAP_PROXYING_NOT_SUPPORTED = RESPONSE_CODE(5, 5)
} COAP_RESPONSE_CODE;
// Option number, in the SOAP message layer
typedef enum {
    COAP_IF_MATCH = 1,
    COAP_URI_HOST = 3,
    COAP_E_TAG = 4,
    COAP_IF_NONE_MATCH = 5,
    COAP_URI_PORT = 7,
    COAP_LOCATION_PATH = 8,
    COAP_URI_PATH = 11,
    COAP_CONTENT_FORMAT = 12,
    COAP_MAX_AGE = 14,
    COAP_URI_QUERY = 15,
    COAP_ACCEPT = 17,
    COAP_LOCATION_QUERY = 20,
    COAP_PROXY_URI = 35,
    COAP_PROXY_SCHEME = 39
} COAP_OPTION_NUMBER;
// The content type and Accept are used to represent the media format of the CoAP payload
typedef enum {
    COAP_NONE = -1,
    COAP_TEXT_PLAIN = 0,
    COAP_APPLICATION_LINK_FORMAT = 40,
    COAP_APPLICATION_XML = 41,
    COAP_APPLICATION_OCTET_STREAM = 42,
    COAP_APPLICATION_EXI = 47,
    COAP_APPLICATION_JSON = 50,
    COAP_APPLICATION_CBOR = 60
} COAP_CONTENT_TYPE;

class CoapOption {
    public:
    uint8_t number;
    uint8_t length;
    uint8_t *buffer;
};

class CoapPacket {
    public:
		uint8_t type = 0;
		uint8_t code = 0;
		const uint8_t *token = NULL;
		uint8_t tokenlen = 0;
		const uint8_t *payload = NULL;
		size_t payloadlen = 0;
		uint16_t messageid = 0;
		uint8_t optionnum = 0;
		CoapOption options[COAP_MAX_OPTION_NUM];

		void addOption(uint8_t number, uint8_t length, uint8_t *opt_payload);
};

Packet sending:
Fill in the UDP address, port, endpoint and other path related information of the package here, as well as the information of the COAP request / response layer

uint16_t Coap::send(IPAddress ip, int port, const char *url, COAP_TYPE type, COAP_METHOD method, const uint8_t *token, uint8_t tokenlen, const uint8_t *payload, size_t payloadlen, COAP_CONTENT_TYPE content_type) {

    // make packet
    CoapPacket packet;

    packet.type = type;
    packet.code = method;
    packet.token = token;
    packet.tokenlen = tokenlen;
    packet.payload = payload;
    packet.payloadlen = payloadlen;
    packet.optionnum = 0;
    packet.messageid = rand();

    // use URI_HOST UIR_PATH
    char ipaddress[16] = "";
    sprintf(ipaddress, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
    packet.addOption(COAP_URI_HOST, strlen(ipaddress), (uint8_t *)ipaddress);

    // parse url
    int idx = 0;
    for (int i = 0; i < strlen(url); i++) {
        if (url[i] == '/') {
			packet.addOption(COAP_URI_PATH, i-idx, (uint8_t *)(url + idx));
            idx = i + 1;
        }
    }

    if (idx <= strlen(url)) {
		packet.addOption(COAP_URI_PATH, strlen(url)-idx, (uint8_t *)(url + idx));
    }

	// if Content-Format option
	uint8_t optionBuffer[2] {0};
	if (content_type != COAP_NONE) {
		optionBuffer[0] = ((uint16_t)content_type & 0xFF00) >> 8;
		optionBuffer[1] = ((uint16_t)content_type & 0x00FF) ;
		packet.addOption(COAP_CONTENT_FORMAT, 2, optionBuffer);
	}

    // send packet
    return this->sendPacket(packet, ip, port);
}

Here's the data for assembling the message layer of the coap package

uint16_t Coap::sendPacket(CoapPacket &packet, IPAddress ip, int port) {
    uint8_t buffer[COAP_BUF_MAX_SIZE];
    uint8_t *p = buffer;
    uint16_t running_delta = 0;
    uint16_t packetSize = 0;

    // Making the base head of the coap package
    *p = 0x01 << 6;
    *p |= (packet.type & 0x03) << 4;
    *p++ |= (packet.tokenlen & 0x0F);
    *p++ = packet.code;
    *p++ = (packet.messageid >> 8);
    *p++ = (packet.messageid & 0xFF);
    p = buffer + COAP_HEADER_SIZE;
    packetSize += 4;

    // make token
    if (packet.token != NULL && packet.tokenlen <= 0x0F) {
        memcpy(p, packet.token, packet.tokenlen);
        p += packet.tokenlen;
        packetSize += packet.tokenlen;
    }

    // make option header
    for (int i = 0; i < packet.optionnum; i++)  {
        uint32_t optdelta;
        uint8_t len, delta;

        if (packetSize + 5 + packet.options[i].length >= COAP_BUF_MAX_SIZE) {
            return 0;
        }
        optdelta = packet.options[i].number - running_delta;
        COAP_OPTION_DELTA(optdelta, &delta);
        COAP_OPTION_DELTA((uint32_t)packet.options[i].length, &len);

        *p++ = (0xFF & (delta << 4 | len));
        if (delta == 13) {
            *p++ = (optdelta - 13);
            packetSize++;
        } else if (delta == 14) {
            *p++ = ((optdelta - 269) >> 8);
            *p++ = (0xFF & (optdelta - 269));
            packetSize+=2;
        } if (len == 13) {
            *p++ = (packet.options[i].length - 13);
            packetSize++;
        } else if (len == 14) {
            *p++ = (packet.options[i].length >> 8);
            *p++ = (0xFF & (packet.options[i].length - 269));
            packetSize+=2;
        }

        memcpy(p, packet.options[i].buffer, packet.options[i].length);
        p += packet.options[i].length;
        packetSize += packet.options[i].length + 1;
        running_delta = packet.options[i].number;
    }

    // make payload
    if (packet.payloadlen > 0) {
        if ((packetSize + 1 + packet.payloadlen) >= COAP_BUF_MAX_SIZE) {
            return 0;
        }
        *p++ = 0xFF;
        memcpy(p, packet.payload, packet.payloadlen);
        packetSize += 1 + packet.payloadlen;
    }

    _udp->beginPacket(ip, port);
    _udp->write(buffer, packetSize);
    _udp->endPacket();

    return packet.messageid;
}

Because the unpacking loop part of this library has not been completed, I won't talk about it here

Result display

The COAP client sent three messages with ID 201252415712868, and then the server returned the three messages with data, and the client got the required data.

summary

It feels strange? That's right. The demo is not perfect, but the library is relatively simple and easy to understand. At the same time, it has a basic framework. It's easier to understand COAP when you understand the code.

Posted on Sun, 05 Dec 2021 13:21:39 -0500 by vhaxu