From 0edb93be1a6f56656f5d1c2011f5cde80fc206ac Mon Sep 17 00:00:00 2001 From: Nic Limper Date: Wed, 26 Apr 2023 17:17:02 +0200 Subject: [PATCH] zeroconfig multiAP over UDP --- ESP32_AP-Flasher/data/www/main.js | 73 ++++++++++---------- ESP32_AP-Flasher/include/newproto.h | 1 + ESP32_AP-Flasher/include/tag_db.h | 3 +- ESP32_AP-Flasher/include/udp.h | 15 +++-- ESP32_AP-Flasher/src/contentmanager.cpp | 8 ++- ESP32_AP-Flasher/src/newproto.cpp | 89 +++++++++++++++++++++++-- ESP32_AP-Flasher/src/serial.cpp | 4 ++ ESP32_AP-Flasher/src/tag_db.cpp | 2 + ESP32_AP-Flasher/src/udp.cpp | 63 ++++++++++++----- 9 files changed, 196 insertions(+), 62 deletions(-) diff --git a/ESP32_AP-Flasher/data/www/main.js b/ESP32_AP-Flasher/data/www/main.js index 531d7ad0..6cd58191 100644 --- a/ESP32_AP-Flasher/data/www/main.js +++ b/ESP32_AP-Flasher/data/www/main.js @@ -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) { diff --git a/ESP32_AP-Flasher/include/newproto.h b/ESP32_AP-Flasher/include/newproto.h index 9ecafc9e..4a755815 100644 --- a/ESP32_AP-Flasher/include/newproto.h +++ b/ESP32_AP-Flasher/include/newproto.h @@ -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); diff --git a/ESP32_AP-Flasher/include/tag_db.h b/ESP32_AP-Flasher/include/tag_db.h index 50606a72..8632047d 100644 --- a/ESP32_AP-Flasher/include/tag_db.h +++ b/ESP32_AP-Flasher/include/tag_db.h @@ -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]); }; diff --git a/ESP32_AP-Flasher/include/udp.h b/ESP32_AP-Flasher/include/udp.h index 53d562c1..980a6174 100644 --- a/ESP32_AP-Flasher/include/udp.h +++ b/ESP32_AP-Flasher/include/udp.h @@ -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(); \ No newline at end of file diff --git a/ESP32_AP-Flasher/src/contentmanager.cpp b/ESP32_AP-Flasher/src/contentmanager.cpp index bcfe89e2..6aa39060 100644 --- a/ESP32_AP-Flasher/src/contentmanager.cpp +++ b/ESP32_AP-Flasher/src/contentmanager.cpp @@ -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(); diff --git a/ESP32_AP-Flasher/src/newproto.cpp b/ESP32_AP-Flasher/src/newproto.cpp index ba6d6499..a48e9fa0 100644 --- a/ESP32_AP-Flasher/src/newproto.cpp +++ b/ESP32_AP-Flasher/src/newproto.cpp @@ -1,6 +1,9 @@ #include "newproto.h" #include +#include +#include +#include #include #include #include @@ -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; diff --git a/ESP32_AP-Flasher/src/serial.cpp b/ESP32_AP-Flasher/src/serial.cpp index 6c4118b9..4d423e3f 100644 --- a/ESP32_AP-Flasher/src/serial.cpp +++ b/ESP32_AP-Flasher/src/serial.cpp @@ -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; } diff --git a/ESP32_AP-Flasher/src/tag_db.cpp b/ESP32_AP-Flasher/src/tag_db.cpp index 2f585d24..1401f25e 100644 --- a/ESP32_AP-Flasher/src/tag_db.cpp +++ b/ESP32_AP-Flasher/src/tag_db.cpp @@ -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(); + taginfo->isExternal = tag["isexternal"].as(); } } else { Serial.print(F("deserializeJson() failed: ")); diff --git a/ESP32_AP-Flasher/src/udp.cpp b/ESP32_AP-Flasher/src/udp.cpp index 37ca198f..8215c0e6 100644 --- a/ESP32_AP-Flasher/src/udp.cpp +++ b/ESP32_AP-Flasher/src/udp.cpp @@ -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); +} \ No newline at end of file