COAP协议 - arduino ESP32 M2M(端对端)通讯与代码详解

Posted 灵感桌面

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了COAP协议 - arduino ESP32 M2M(端对端)通讯与代码详解相关的知识,希望对你有一定的参考价值。

@TOC

前言

最近我在研究 COAP 协议,在尝试使用 COAP 协议找了到了一个能在ESP32上用的coap-simple库,虽然库并不完善关于loop处理的部分应该是没写完,但是对于第一次接触COAP的朋友来说更容易理解,方便学习,需要的朋友可以去下面下载:

我之前使用 IOT PI 的 COAP 能和 PC node coap 通讯,但是因为 coap-simple 库不完善,正常的无法与 node coap 通讯,只能和同样使用这个库设备通讯,这次就来尝试 ESP32 之间的 M2M 通讯。

获取库

使用 arduino IDE 就能下载到这个库:

如果没有看到这个库,可以去首选项添加一下附加开发板管理器网址:

具体使用可以参考的我 arduino 超详细的开发入门指导 或者直接通过我上面发的 GitHub 网址下载。

代码解析

以下代码为了方便讲解,可能经过了调换了顺序或者裁剪。

这个 demo 是客户端、服务端一体的,只需要注册对应的回调函数就行。

初始化部分

这部分包括了设备初始化,协议初始化等部分,重点在服务器/客户端的回调函数部分。和 SDDC 官方demo类似,在这注册回调函数之后,通过对应的端点找到对应的回调函数。

#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;

  // 添加服务器url端点.
  // 可以添加多个端点url.
  //      coap.server(callback_switch, "switch");
  //      coap.server(callback_env, "env/temp");
  //      coap.server(callback_env, "env/humidity");
  Serial.println("Setup Callback Light");
  // 其实就是注册服务器处理回调函数
  // 将处理函数指针与url添加到 uri.add 中 
  coap.server(callback_light, "light");

  // 注册客户端响应的回调函数。
  // this endpoint is single callback.
  Serial.println("Setup Response Callback");
  // 很上面一样,其实就是把回调函数指针注册到resp里
  coap.response(callback_response);

  // 使用默认端口5683 启动  coap server/client 
  coap.start();
}

void loop() {
  // 作为客户端时向coap服务器发送GET或PUT coap请求.
  // 可以发送给另外一个 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();
}

回调函数

// CoAP 服务器端点 URL ,对客户端发过来的命令进行处理并且回应
void callback_light(CoapPacket &packet, IPAddress ip, int port) 
{
  // 这是一个模拟控灯的回调函数,通过接收的命令
  Serial.println("[Light] ON/OFF");
  Serial.println(packet.messageid);

  // 发送响应
  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客户端响应回调
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);
}

库代码

报文结构定义:

// 确定消息类型,在 coap 消息层
typedef enum {
    COAP_CON = 0,     // 可靠传输
    COAP_NONCON = 1,  // 不可靠传输
    COAP_ACK = 2,     // 回复
    COAP_RESET = 3    // 报文异常后的被动重发请求
} COAP_TYPE;
// 命令执行的动作,在请求/响应层
typedef enum {
    COAP_GET = 1,
    COAP_POST = 2,    // 主动的重发命令
    COAP_PUT = 3,
    COAP_DELETE = 4
} COAP_METHOD;
// 响应码,相当于函数返回值或者err码之类的,在请求/响应层
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 编号 ,在 coap 消息层
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;
// 内容类型和 Accept 用于表示CoAP负载的媒体格式
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);
};

组包发送:
在这里填写包的UDP需要地址,端口,端点等路径相关信息以及 COAP 请求/响应层的信息

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);
}

在这里的组装 coap 包消息层的数据

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;

    // 制作coap包基头
    *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;
}

因为这个库解包的loop部分没做完所以这里就先不说了

结果展示

COAP 客户端发送了ID 为20125,24157,12868的三个消息,然后服务器端返回了这三个消息,并带上了数据,客户端也got 到了需要的数据。

总结

感觉很怪?怪就对了,这个 demo 并不完善,只是这个库比较简单方便理解,同时有一个基本框架,看懂这个代码更容易理解 COAP 。

以上是关于COAP协议 - arduino ESP32 M2M(端对端)通讯与代码详解的主要内容,如果未能解决你的问题,请参考以下文章

足够你学习ESP32,提供史上最全的ESP32教程(驱动/蓝牙/Wi-Fi/LVGL/Arduino...)

玩转 ESP32 + Arduino (十二) 通过MQTT协议上传数据至OneNet云平台

足够你学习ESP32,提供史上最全的ESP32教程(驱动/蓝牙/Wi-Fi/LVGL/Arduino...)

玩转 ESP32 + Arduino (十三) 通过MQTTs协议上传数据至OneNet云平台

Arduino ESP8266/ESP32 多路DS18B20温度采样读取

一文解决 CoAP 协议设备与外部网络沟通难题