前言
最近我在研究 COAP 協議,在嘗試使用 COAP 協議找了到了一個能在ESP32上用的coap-simple庫,雖然庫並不完善關於loop處理的部分應該是沒寫完,但是對於第一次接觸COAP的朋友來說更容易理解,方便學習,需要的朋友可以去下面下載:
我之前使用 IOT PI 的 COAP 能和 PC node coap 通訊,但是因為 coap-simple 庫不完善,正常的無法與 node coap 通訊,只能和同樣使用這個庫裝置通訊,這次就來嘗試 ESP32 之間的 M2M 通訊。
獲取庫
使用 arduino IDE 就能下載到這個庫:
如果沒有看到這個庫,可以去首選項新增一下附加開發板管理器網址:
https://github.com/espressif/arduino-esp32/releases/download/1.0.5/package_esp32_index.json
具體使用可以參考的我 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 。