mirror of
https://github.com/OpenEPaperLink/OpenEPaperLink.git
synced 2026-03-21 07:06:36 +01:00
zeroconfig multiAP over UDP
This commit is contained in:
@@ -8,7 +8,7 @@ const WAKEUP_REASON_FIRSTBOOT = 0xFC;
|
||||
const WAKEUP_REASON_NETWORK_SCAN = 0xFD;
|
||||
const WAKEUP_REASON_WDT_RESET = 0xFE;
|
||||
|
||||
const contentModes = ["Static image", "Current date", "Counting days", "Counting hours", "Current weather", "Firmware update", "Memo text", "Image url", "Weather forecast", "RSS feed", "QR code", "Calendar"];
|
||||
const contentModes = ["Static image", "Current date", "Counting days", "Counting hours", "Current weather", "Firmware update", "Memo text", "Image url", "Weather forecast", "RSS feed", "QR code", "Calendar", "Remote AP"];
|
||||
const models = ["1.54\" 152x152px", "2.9\" 296x128px", "4.2\" 400x300px"];
|
||||
const displaySizeLookup = { 0: [152, 152], 1: [128, 296], 2: [400, 300] };
|
||||
const colorTable = { 0: [255, 255, 255], 1: [0, 0, 0], 2: [255, 0, 0], 3: [255, 0, 0] };
|
||||
@@ -25,6 +25,7 @@ contentModeOptions[8] = ["location"];
|
||||
contentModeOptions[9] = ["title", "url", "interval"];
|
||||
contentModeOptions[10] = ["title", "qr-content"];
|
||||
contentModeOptions[11] = ["title", "apps_script_url", "interval"];
|
||||
contentModeOptions[12] = [];
|
||||
|
||||
const imageQueue = [];
|
||||
let isProcessing = false;
|
||||
@@ -67,7 +68,6 @@ function connect() {
|
||||
if (msg.sys) {
|
||||
$('#sysinfo').innerHTML = 'free heap: ' + msg.sys.heap + ' bytes ┇ db size: ' + msg.sys.dbsize + ' bytes ┇ db record count: ' + msg.sys.recordcount + ' ┇ littlefs free: ' + msg.sys.littlefsfree + ' bytes';
|
||||
servertimediff = (Date.now() / 1000) - msg.sys.currtime;
|
||||
console.log("timediff: " + servertimediff);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,12 +89,15 @@ function processTags(tagArray) {
|
||||
div.dataset.mac = tagmac;
|
||||
div.dataset.hwtype = -1;
|
||||
$('#taglist').appendChild(div);
|
||||
|
||||
$('#tag' + tagmac + ' .mac').innerHTML = tagmac;
|
||||
}
|
||||
|
||||
div.style.display = 'block';
|
||||
|
||||
if (element.isexternal) {
|
||||
$('#tag' + tagmac + ' .mac').innerHTML = tagmac + " via ext AP";
|
||||
} else {
|
||||
$('#tag' + tagmac + ' .mac').innerHTML = tagmac;
|
||||
}
|
||||
let alias = element.alias;
|
||||
if (!alias) alias = tagmac;
|
||||
$('#tag' + tagmac + ' .alias').innerHTML = alias;
|
||||
@@ -227,7 +230,6 @@ $('#taglist').addEventListener("click", (event) => {
|
||||
fetch("/get_db?mac=" + mac)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
var tagdata = data.tags[0];
|
||||
$('#cfgalias').value = tagdata.alias;
|
||||
$('#cfgcontent').value = tagdata.contentMode;
|
||||
@@ -244,22 +246,24 @@ $('#cfgsave').onclick = function () {
|
||||
let contentMode = $('#cfgcontent').value;
|
||||
let extraoptions = contentModeOptions[contentMode];
|
||||
let obj={};
|
||||
extraoptions.forEach(element => {
|
||||
obj[element] = $('#opt' + element).value;
|
||||
});
|
||||
if (contentMode) {
|
||||
extraoptions.forEach(element => {
|
||||
obj[element] = $('#opt' + element).value;
|
||||
});
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("mac", $('#cfgmac').dataset.mac);
|
||||
formData.append("alias", $('#cfgalias').value);
|
||||
formData.append("contentmode", contentMode);
|
||||
formData.append("modecfgjson", JSON.stringify(obj));
|
||||
fetch("/save_cfg", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => showMessage(data))
|
||||
.catch(error => showMessage('Error: ' + error));
|
||||
let formData = new FormData();
|
||||
formData.append("mac", $('#cfgmac').dataset.mac);
|
||||
formData.append("alias", $('#cfgalias').value);
|
||||
formData.append("contentmode", contentMode);
|
||||
formData.append("modecfgjson", JSON.stringify(obj));
|
||||
fetch("/save_cfg", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => showMessage(data))
|
||||
.catch(error => showMessage('Error: ' + error));
|
||||
}
|
||||
$('#configbox').style.display = 'none';
|
||||
}
|
||||
|
||||
@@ -296,20 +300,21 @@ function contentselected() {
|
||||
if ($('#cfgcontent').dataset.json && ($('#cfgcontent').dataset.json!="null")) {
|
||||
obj = JSON.parse($('#cfgcontent').dataset.json);
|
||||
}
|
||||
console.log(obj);
|
||||
extraoptions.forEach(element => {
|
||||
var label = document.createElement("label");
|
||||
label.innerHTML = element;
|
||||
label.setAttribute("for", 'opt' + element);
|
||||
var input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.id = 'opt' + element;
|
||||
if (obj[element]) input.value = obj[element];
|
||||
var p = document.createElement("p");
|
||||
p.appendChild(label);
|
||||
p.appendChild(input);
|
||||
$('#customoptions').appendChild(p);
|
||||
});
|
||||
if (contentMode) {
|
||||
extraoptions.forEach(element => {
|
||||
var label = document.createElement("label");
|
||||
label.innerHTML = element;
|
||||
label.setAttribute("for", 'opt' + element);
|
||||
var input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.id = 'opt' + element;
|
||||
if (obj[element]) input.value = obj[element];
|
||||
var p = document.createElement("p");
|
||||
p.appendChild(label);
|
||||
p.appendChild(input);
|
||||
$('#customoptions').appendChild(p);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showMessage(message,iserr) {
|
||||
|
||||
@@ -6,6 +6,7 @@ extern bool checkCRC(void* p, uint8_t len);
|
||||
extern void processBlockRequest(struct espBlockRequest* br);
|
||||
extern void prepareIdleReq(uint8_t* dst, uint16_t nextCheckin);
|
||||
extern bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t nextCheckin);
|
||||
extern void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP);
|
||||
extern void processXferComplete(struct espXferComplete* xfc);
|
||||
extern void processXferTimeout(struct espXferComplete* xfc);
|
||||
extern void processDataReq(struct espAvailDataReq* adr);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
class tagRecord {
|
||||
public:
|
||||
uint16_t nextCheckinpending;
|
||||
tagRecord() : mac{0}, alias(""), lastseen(0), nextupdate(0), contentMode(0), pending(false), md5{0}, md5pending{0}, expectedNextCheckin(0), modeConfigJson(""), LQI(0), RSSI(0), temperature(0), batteryMv(0), hwType(0), wakeupReason(0), capabilities(0), lastfullupdate(0) {}
|
||||
tagRecord() : mac{0}, alias(""), lastseen(0), nextupdate(0), contentMode(0), pending(false), md5{0}, md5pending{0}, expectedNextCheckin(0), modeConfigJson(""), LQI(0), RSSI(0), temperature(0), batteryMv(0), hwType(0), wakeupReason(0), capabilities(0), lastfullupdate(0), isExternal(false) {}
|
||||
|
||||
uint8_t mac[6];
|
||||
String alias;
|
||||
@@ -38,6 +38,7 @@ class tagRecord {
|
||||
uint8_t wakeupReason;
|
||||
uint8_t capabilities;
|
||||
uint32_t lastfullupdate;
|
||||
bool isExternal;
|
||||
static tagRecord* findByMAC(uint8_t mac[6]);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,17 +2,24 @@
|
||||
|
||||
#include "AsyncUDP.h"
|
||||
|
||||
#ifndef defudpcomm
|
||||
#define defudpcomm
|
||||
|
||||
class UDPcomm {
|
||||
public:
|
||||
UDPcomm();
|
||||
~UDPcomm();
|
||||
void init();
|
||||
void send(uint8_t* output);
|
||||
void processDataReq(struct espAvailDataReq* eadr);
|
||||
|
||||
private:
|
||||
void netProcessDataReq(struct espAvailDataReq* eadr);
|
||||
void netProcessXferComplete(struct espXferComplete* xfc);
|
||||
void netProcessXferTimeout(struct espXferComplete* xfc);
|
||||
void netSendDataAvail(struct pendingData* pending);
|
||||
|
||||
private:
|
||||
AsyncUDP udp;
|
||||
void processPacket(AsyncUDPPacket packet);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
void init_udp();
|
||||
@@ -32,6 +32,7 @@ enum contentModes {
|
||||
RSSFeed,
|
||||
QRcode,
|
||||
Calendar,
|
||||
RemoteAP,
|
||||
};
|
||||
|
||||
void contentRunner() {
|
||||
@@ -99,8 +100,8 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) {
|
||||
} else {
|
||||
wsErr("Error accessing " + filename);
|
||||
}
|
||||
taginfo->nextupdate = 3216153600;
|
||||
}
|
||||
taginfo->nextupdate = 3216153600;
|
||||
break;
|
||||
|
||||
case Today:
|
||||
@@ -208,6 +209,11 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) {
|
||||
taginfo->nextupdate = now + 300;
|
||||
}
|
||||
break;
|
||||
|
||||
case RemoteAP:
|
||||
|
||||
taginfo->nextupdate = 3216153600;
|
||||
break;
|
||||
}
|
||||
|
||||
taginfo->modeConfigJson = doc.as<String>();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "newproto.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <FS.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <LittleFS.h>
|
||||
#include <MD5Builder.h>
|
||||
#include <makeimage.h>
|
||||
#include <time.h>
|
||||
@@ -11,9 +14,11 @@
|
||||
#include "serial.h"
|
||||
#include "settings.h"
|
||||
#include "tag_db.h"
|
||||
#include "udp.h"
|
||||
#include "web.h"
|
||||
|
||||
extern uint16_t sendBlock(const void* data, const uint16_t len);
|
||||
extern UDPcomm udpsync;
|
||||
|
||||
void addCRC(void* p, uint8_t len) {
|
||||
uint8_t total = 0;
|
||||
@@ -138,7 +143,7 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t
|
||||
LittleFS.rename(*filename, dst_path);
|
||||
*filename = String(dst_path);
|
||||
|
||||
wsLog("new image pending: " + String(dst_path));
|
||||
wsLog("new image: " + String(dst_path));
|
||||
time_t now;
|
||||
time(&now);
|
||||
taginfo->pending = true;
|
||||
@@ -158,7 +163,11 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t
|
||||
pending.availdatainfo.dataTypeArgument = lut;
|
||||
pending.availdatainfo.nextCheckIn = nextCheckin;
|
||||
pending.attemptsLeft = attempts;
|
||||
sendDataAvail(&pending);
|
||||
if (taginfo->isExternal == false) {
|
||||
sendDataAvail(&pending);
|
||||
} else {
|
||||
udpsync.netSendDataAvail(&pending);
|
||||
}
|
||||
|
||||
// data for the cache on the esp32; needs to hold the data longer than the maximum timeout on the AP
|
||||
pendingdata* pendinginfo = nullptr;
|
||||
@@ -175,6 +184,72 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t
|
||||
return true;
|
||||
}
|
||||
|
||||
void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP) {
|
||||
uint8_t src[8];
|
||||
*((uint64_t*)src) = swap64(*((uint64_t*)pending->targetMac));
|
||||
uint8_t mac[6];
|
||||
memcpy(mac, src + 2, sizeof(mac));
|
||||
tagRecord* taginfo = nullptr;
|
||||
taginfo = tagRecord::findByMAC(mac);
|
||||
if (taginfo == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (taginfo->isExternal == false) {
|
||||
LittleFS.begin();
|
||||
|
||||
char buffer[64];
|
||||
sprintf(buffer, "%02X%02X%02X%02X%02X%02X\0", src[2], src[3], src[4], src[5], src[6], src[7]);
|
||||
String filename = "/current/" + (String)buffer + ".pending";
|
||||
String imageUrl = "http://" + remoteIP.toString() + filename;
|
||||
wsLog("GET " + imageUrl);
|
||||
HTTPClient http;
|
||||
http.begin(imageUrl);
|
||||
int httpCode = http.GET();
|
||||
if (httpCode == 200) {
|
||||
File file = LittleFS.open(filename, "w");
|
||||
http.writeToStream(&file);
|
||||
file.close();
|
||||
}
|
||||
http.end();
|
||||
|
||||
fs::File file = LittleFS.open(filename);
|
||||
uint32_t filesize = file.size();
|
||||
if (filesize == 0) {
|
||||
file.close();
|
||||
wsErr("File has size 0. " + filename);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t md5bytes[16];
|
||||
{
|
||||
MD5Builder md5;
|
||||
md5.begin();
|
||||
md5.addStream(file, filesize);
|
||||
md5.calculate();
|
||||
md5.getBytes(md5bytes);
|
||||
}
|
||||
|
||||
file.close();
|
||||
sendDataAvail(pending);
|
||||
|
||||
pendingdata* pendinginfo = nullptr;
|
||||
pendinginfo = new pendingdata;
|
||||
pendinginfo->filename = filename;
|
||||
pendinginfo->ver = *((uint64_t*)md5bytes);
|
||||
pendinginfo->len = filesize;
|
||||
pendinginfo->data = nullptr;
|
||||
pendinginfo->timeout = PENDING_TIMEOUT;
|
||||
pendingfiles.push_back(pendinginfo);
|
||||
|
||||
taginfo->pending = true;
|
||||
memcpy(taginfo->md5pending, md5bytes, sizeof(md5bytes));
|
||||
taginfo->contentMode = 12;
|
||||
taginfo->nextupdate = 3216153600;
|
||||
|
||||
wsSendTaginfo(mac);
|
||||
}
|
||||
}
|
||||
|
||||
void processBlockRequest(struct espBlockRequest* br) {
|
||||
if (!checkCRC(br, sizeof(struct espBlockRequest))) {
|
||||
Serial.print("Failed CRC on a blockrequest received by the AP");
|
||||
@@ -239,9 +314,7 @@ void processXferComplete(struct espXferComplete* xfc) {
|
||||
}
|
||||
if (LittleFS.exists(src_path)) {
|
||||
LittleFS.rename(src_path, dst_path);
|
||||
} else {
|
||||
wsErr("hm, weird, no pending image found after xfercomplete.");
|
||||
}
|
||||
}
|
||||
|
||||
time_t now;
|
||||
time(&now);
|
||||
@@ -307,6 +380,12 @@ void processDataReq(struct espAvailDataReq* eadr) {
|
||||
time(&now);
|
||||
taginfo->lastseen = now;
|
||||
|
||||
if (eadr->src[7] == 0xFF) {
|
||||
taginfo->isExternal = true;
|
||||
} else {
|
||||
taginfo->isExternal = false;
|
||||
}
|
||||
|
||||
uint16_t minutesUntilNextUpdate = 0;
|
||||
if (taginfo->nextupdate > now + 2 * 60) {
|
||||
minutesUntilNextUpdate = (taginfo->nextupdate - now) / 60;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
QueueHandle_t rxCmdQueue;
|
||||
SemaphoreHandle_t txActive;
|
||||
extern UDPcomm udpsync;
|
||||
|
||||
#define CMD_REPLY_WAIT 0x00
|
||||
#define CMD_REPLY_ACK 0x01
|
||||
@@ -198,12 +199,15 @@ void rxCmdProcessor(void* parameter) {
|
||||
break;
|
||||
case RX_CMD_ADR:
|
||||
processDataReq((struct espAvailDataReq*)rxcmd->data);
|
||||
udpsync.netProcessDataReq((struct espAvailDataReq*)rxcmd->data);
|
||||
break;
|
||||
case RX_CMD_XFC:
|
||||
processXferComplete((struct espXferComplete*)rxcmd->data);
|
||||
udpsync.netProcessXferComplete((struct espXferComplete*)rxcmd->data);
|
||||
break;
|
||||
case RX_CMD_XTO:
|
||||
processXferTimeout((struct espXferComplete*)rxcmd->data);
|
||||
udpsync.netProcessXferTimeout((struct espXferComplete*)rxcmd->data);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ void fillNode(JsonObject &tag, tagRecord* &taginfo) {
|
||||
tag["wakeupReason"] = taginfo->wakeupReason;
|
||||
tag["capabilities"] = taginfo->capabilities;
|
||||
tag["modecfgjson"] = taginfo->modeConfigJson;
|
||||
tag["isexternal"] = taginfo->isExternal;
|
||||
}
|
||||
|
||||
void saveDB(String filename) {
|
||||
@@ -180,6 +181,7 @@ void loadDB(String filename) {
|
||||
taginfo->wakeupReason = tag["wakeupReason"];
|
||||
taginfo->capabilities = tag["capabilities"];
|
||||
taginfo->modeConfigJson = tag["modecfgjson"].as<String>();
|
||||
taginfo->isExternal = tag["isexternal"].as<bool>();
|
||||
}
|
||||
} else {
|
||||
Serial.print(F("deserializeJson() failed: "));
|
||||
|
||||
@@ -5,21 +5,17 @@
|
||||
#include "commstructs.h"
|
||||
#include "newproto.h"
|
||||
|
||||
#define PKT_AVAIL_DATA_SHORTREQ 0xE3
|
||||
#define PKT_AVAIL_DATA_REQ 0xE5
|
||||
#define PKT_AVAIL_DATA_INFO 0xE6
|
||||
#define PKT_BLOCK_PARTIAL_REQUEST 0xE7
|
||||
#define PKT_BLOCK_REQUEST_ACK 0xE9
|
||||
#define PKT_BLOCK_REQUEST 0xE4
|
||||
#define PKT_BLOCK_PART 0xE8
|
||||
#define PKT_XFER_COMPLETE 0xEA
|
||||
#define PKT_XFER_COMPLETE_ACK 0xEB
|
||||
#define PKT_XFER_TIMEOUT 0xED
|
||||
#define PKT_CANCEL_XFER 0xEC
|
||||
#define PKT_PING 0xED
|
||||
#define PKT_PONG 0xEE
|
||||
#define PKT_ID_APS 0x80
|
||||
|
||||
UDPcomm udpsync;
|
||||
|
||||
uint8_t channelList[6] = {11, 15, 20, 25, 26, 27};
|
||||
|
||||
void init_udp() {
|
||||
udpsync.init();
|
||||
}
|
||||
@@ -34,29 +30,62 @@ UDPcomm::~UDPcomm() {
|
||||
|
||||
void UDPcomm::init() {
|
||||
if (udp.listenMulticast(IPAddress(239, 10, 0, 1), 16033)) {
|
||||
Serial.print("UDP Listening on IP: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
udp.onPacket([this](AsyncUDPPacket packet) {
|
||||
this->processPacket(packet);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void UDPcomm::send(uint8_t* output) {
|
||||
udp.writeTo(output, strlen((char*)output), IPAddress(239, 10, 0, 1), 16572);
|
||||
}
|
||||
|
||||
void UDPcomm::processPacket(AsyncUDPPacket packet) {
|
||||
if (packet.data()[0] == 0xFD) {
|
||||
if (packet.data()[0] == PKT_AVAIL_DATA_INFO) {
|
||||
espAvailDataReq* adr = (espAvailDataReq*)&packet.data()[1];
|
||||
adr->src[7] = 0xFF;
|
||||
processDataReq(adr);
|
||||
}
|
||||
if (packet.data()[0] == PKT_XFER_COMPLETE) {
|
||||
espXferComplete* xfc = (espXferComplete*)&packet.data()[1];
|
||||
processXferComplete(xfc);
|
||||
}
|
||||
if (packet.data()[0] == PKT_XFER_TIMEOUT) {
|
||||
espXferComplete* xfc = (espXferComplete*)&packet.data()[1];
|
||||
processXferTimeout(xfc);
|
||||
}
|
||||
if (packet.data()[0] == PKT_AVAIL_DATA_REQ) {
|
||||
pendingData* pending = (pendingData*)&packet.data()[1];
|
||||
prepareExternalDataAvail(pending, packet.remoteIP());
|
||||
}
|
||||
if (packet.data()[0] == PKT_ID_APS) {
|
||||
Serial.println("ap list req");
|
||||
IPAddress senderIP = packet.remoteIP();
|
||||
unsigned int senderPort = packet.remotePort();
|
||||
//todo: autoselect channel
|
||||
}
|
||||
}
|
||||
|
||||
void UDPcomm::processDataReq(struct espAvailDataReq* eadr) {
|
||||
void UDPcomm::netProcessDataReq(struct espAvailDataReq* eadr) {
|
||||
uint8_t buffer[sizeof(struct espAvailDataReq) + 1];
|
||||
buffer[0] = PKT_AVAIL_DATA_INFO;
|
||||
memcpy(buffer + 1, eadr, sizeof(struct espAvailDataReq));
|
||||
udp.writeTo(buffer, sizeof(buffer), IPAddress(239, 10, 0, 1), 16572);
|
||||
udp.writeTo(buffer, sizeof(buffer), IPAddress(239, 10, 0, 1), 16033);
|
||||
}
|
||||
|
||||
void UDPcomm::netProcessXferComplete(struct espXferComplete* xfc) {
|
||||
uint8_t buffer[sizeof(struct espXferComplete) + 1];
|
||||
buffer[0] = PKT_XFER_COMPLETE;
|
||||
memcpy(buffer + 1, xfc, sizeof(struct espXferComplete));
|
||||
udp.writeTo(buffer, sizeof(buffer), IPAddress(239, 10, 0, 1), 16033);
|
||||
}
|
||||
|
||||
void UDPcomm::netProcessXferTimeout(struct espXferComplete* xfc) {
|
||||
uint8_t buffer[sizeof(struct espXferComplete) + 1];
|
||||
buffer[0] = PKT_XFER_TIMEOUT;
|
||||
memcpy(buffer + 1, xfc, sizeof(struct espXferComplete));
|
||||
udp.writeTo(buffer, sizeof(buffer), IPAddress(239, 10, 0, 1), 16033);
|
||||
}
|
||||
|
||||
void UDPcomm::netSendDataAvail(struct pendingData* pending) {
|
||||
uint8_t buffer[sizeof(struct pendingData) + 1];
|
||||
buffer[0] = PKT_AVAIL_DATA_REQ;
|
||||
memcpy(buffer + 1, pending, sizeof(struct pendingData));
|
||||
udp.writeTo(buffer, sizeof(buffer), IPAddress(239, 10, 0, 1), 16033);
|
||||
}
|
||||
Reference in New Issue
Block a user