diff --git a/esp32_fw/data/index.html b/esp32_fw/data/index.html index ea1ffddd..9cd7fc7a 100644 --- a/esp32_fw/data/index.html +++ b/esp32_fw/data/index.html @@ -18,12 +18,12 @@

00000000

- - + +

- - @@ -31,17 +31,19 @@

- - - - - + + + +

+

-
- - -
-
-
-

- - -

-

- - min -

-

- -

-
-
-

- - -

-

- -

-
-
-

- -

-
-
+
+ Currently active tags:
+
@@ -96,9 +68,10 @@
-
- last: +
+
+
diff --git a/esp32_fw/data/kat-bw29.jpg b/esp32_fw/data/kat-bw29.jpg new file mode 100644 index 00000000..ae6c765e Binary files /dev/null and b/esp32_fw/data/kat-bw29.jpg differ diff --git a/esp32_fw/data/main.css b/esp32_fw/data/main.css index f7ae66c3..183074a4 100644 --- a/esp32_fw/data/main.css +++ b/esp32_fw/data/main.css @@ -82,7 +82,7 @@ input { input[type=button] { border: 0px; - padding: 4px; + padding: 4px 10px; cursor:pointer; } input[type=button]:hover { @@ -113,6 +113,15 @@ select { font-weight: bold; } +#configbox input { + border: solid 1px #666666; + padding: 4px; +} + +#configbox label { + text-transform: capitalize; +} + #cfgdelete { position: absolute; bottom: 15px; @@ -167,6 +176,10 @@ select { background-color: #dddddd; } +.tagcard .pending { + padding-bottom:15px; +} + .currimg { float: right; } diff --git a/esp32_fw/data/main.js b/esp32_fw/data/main.js index 64410a45..bf4c1890 100644 --- a/esp32_fw/data/main.js +++ b/esp32_fw/data/main.js @@ -1,21 +1,35 @@ const $ = document.querySelector.bind(document); -const contentModes = ["static image", "current date", "count days", "count hours","current weather","public transport","memo text"]; -const models = ["unknown", "1.54\" 152x152px", "2.9\" 296x128px", "4.2\" 400x300px"]; +const contentModes = ["static image", "current date", "counting days", "counting hours", "current weather", "firmware update", "memo text", "image url"]; +const models = ["unknown type", "1.54\" 152x152px", "2.9\" 296x128px", "4.2\" 400x300px"]; +const contentModeOptions = []; +contentModeOptions[0] = ["filename","timetolive"]; +contentModeOptions[1] = []; +contentModeOptions[2] = ["counter", "thresholdred"]; +contentModeOptions[3] = ["counter", "thresholdred"]; +contentModeOptions[4] = ["location"]; +contentModeOptions[5] = ["filename"]; +contentModeOptions[6] = ["text"]; +contentModeOptions[7] = ["url","interval"]; +const imageQueue = []; +let isProcessing = false; +let servertimediff = 0; let socket; connect(); setInterval(updatecards, 1000); -window.addEventListener("load", function () { - fetch("/get_db") +window.addEventListener("load", function () { loadTags(0) }); + +function loadTags(pos) { + fetch("/get_db?pos="+pos) .then(response => response.json()) .then(data => { processTags(data.tags); + if (data.continu && data.continu>pos) loadTags(data.continu); }) - .catch(error => showMessage('Error: ' + error)); -}); - + //.catch(error => showMessage('loadTags error: ' + error)); +} function connect() { socket = new WebSocket("ws://" + location.host + "/ws"); @@ -35,6 +49,7 @@ 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; } }); @@ -69,52 +84,51 @@ function processTags(tagArray) { if (!alias) alias = tagmac; $('#tag' + tagmac + ' .alias').innerHTML = alias; - var img = $('#tag' + tagmac + ' .tagimg'); - img.style.display = 'block'; - img.src = '/edit?edit=current/' + tagmac + '.bmp&' + (new Date()).getTime(); + if (div.dataset.hash != element.hash) loadImage(tagmac, '/current/' + tagmac + '.bmp?' + (new Date()).getTime()); $('#tag' + tagmac + ' .contentmode').innerHTML = contentModes[element.contentmode]; $('#tag' + tagmac + ' .model').innerHTML = models[element.model]; - var date = new Date(element.nextupdate * 1000); - var options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; - $('#tag' + tagmac + ' .nextupdate').innerHTML = date.toLocaleString('nl-NL', options).replace(',', ''); + if (element.nextupdate > 1672531200 && element.nextupdate!=3216153600) { + var date = new Date(element.nextupdate * 1000); + var options = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + $('#tag' + tagmac + ' .nextupdate').innerHTML = "next update: " + date.toLocaleString('nl-NL', options); + } else { + $('#tag' + tagmac + ' .nextupdate').innerHTML = ""; + } - date = new Date(element.lastseen * 1000); - var options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false}; - $('#tag' + tagmac + ' .lastseen').innerHTML = date.toLocaleString('nl-NL', options).replace(',', ''); + if (element.nextcheckin > 1672531200) { + div.dataset.nextcheckin = element.nextcheckin; + } else { + div.dataset.nextcheckin = element.lastseen + 1800; + } div.dataset.lastseen = element.lastseen; - div.dataset.lastseenlocaltime = Date.now(); + div.dataset.hash = element.hash; $('#tag' + tagmac + ' .warningicon').style.display = 'none'; - if (element.pending) $('#tag' + tagmac + ' .pending').innerHTML = "pending..."; else $('#tag' + tagmac + ' .pending').innerHTML = ""; + if (element.pending) $('#tag' + tagmac + ' .pending').innerHTML = "pending update..."; else $('#tag' + tagmac + ' .pending').innerHTML = ""; } } function updatecards() { document.querySelectorAll('[data-mac]').forEach(item => { - var tagmac = item.dataset.mac; - var idletime = Date.now() - item.dataset.lastseenlocaltime; - $('#tag' + tagmac + ' .idletime').innerHTML = int(idletime); - if (idletime > 300000) $('#tag' + tagmac + ' .warningicon').style.display='inline-block'; - if (idletime > 1800000) $('#tag' + tagmac).style.display = 'none'; - }) -} + let tagmac = item.dataset.mac; -$('#send_image').onclick = function() { - let formData = new FormData(); - formData.append("dst", $("#dstmac").value); - formData.append("filename", $("#imgfile").value); - formData.append("ttl", $("#ttl").value); - fetch("/send_image", { - method: "POST", - body: formData + if (item.dataset.lastseen && item.dataset.lastseen > 1672531200) { + let idletime = (Date.now() / 1000) + servertimediff - item.dataset.lastseen; + $('#tag' + tagmac + ' .lastseen').innerHTML = "last seen: "+displayTime(Math.floor(idletime))+" ago"; + if ((Date.now() / 1000) + servertimediff > item.dataset.nextcheckin) $('#tag' + tagmac + ' .warningicon').style.display='inline-block'; + } else { + $('#tag' + tagmac + ' .lastseen').innerHTML = "" + } + + if (item.dataset.nextcheckin > 1672531200) { + let nextcheckin = item.dataset.nextcheckin - ((Date.now() / 1000) + servertimediff); + $('#tag' + tagmac + ' .nextcheckin').innerHTML = "expecting next checkin: " + displayTime(Math.floor(nextcheckin)); + } }) - .then(response => response.text()) - .then(data => showMessage(data)) - .catch(error => showMessage('Error: ' + error)); } $('#send_fw').onclick = function () { @@ -130,18 +144,6 @@ $('#send_fw').onclick = function () { .catch(error => showMessage('Error: ' + error)); } -$('#req_checkin').onclick = function () { - let formData = new FormData(); - formData.append("dst", $("#dstmac").value); - fetch("/req_checkin", { - method: "POST", - body: formData - }) - .then(response => response.text()) - .then(data => showMessage(data)) - .catch(error => showMessage('Error: ' + error)); -} - $('#clearlog').onclick = function () { $('#messages').innerHTML=''; } @@ -176,6 +178,8 @@ $('#taglist').addEventListener("click", (event) => { $('#cfgalias').value = tagdata.alias; $('#cfgcontent').value = tagdata.contentmode; $('#cfgmodel').value = tagdata.model; + $('#cfgcontent').dataset.json = tagdata.modecfgjson; + contentselected(); $('#configbox').style.display = 'block'; }) .catch(error => showMessage('Error: ' + error)); @@ -183,11 +187,20 @@ $('#taglist').addEventListener("click", (event) => { }) $('#cfgsave').onclick = function () { + + let contentmode = $('#cfgcontent').value; + let extraoptions = contentModeOptions[contentmode]; + let obj={}; + 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", $('#cfgcontent').value); + formData.append("contentmode", contentmode); formData.append("model", $('#cfgmodel').value); + formData.append("modecfgjson", JSON.stringify(obj)); fetch("/save_cfg", { method: "POST", body: formData @@ -202,6 +215,30 @@ $('#cfgdelete').onclick = function () { let mac = $('#cfgmac').dataset.mac; } +function contentselected() { + let contentmode=$('#cfgcontent').value; + let extraoptions = contentModeOptions[contentmode]; + $('#customoptions').innerHTML=""; + var obj = {}; + 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); + }); +} + function showMessage(message) { const messages = $('#messages'); var date = new Date(), @@ -213,4 +250,37 @@ function htmlEncode(input) { const textArea = document.createElement("textarea"); textArea.innerText = input; return textArea.innerHTML.split("
").join("\n"); -} \ No newline at end of file +} + +function loadImage(id, imageSrc) { + imageQueue.push({ id, imageSrc }); + if (!isProcessing) { + processQueue(); + } +} + +function processQueue() { + if (imageQueue.length === 0) { + isProcessing = false; + return; + } + isProcessing = true; + const { id, imageSrc } = imageQueue.shift(); + const image = $('#tag' + id + ' .tagimg'); + image.onload = function () { + image.style.display = 'block'; + processQueue(); + } + image.onerror = function () { + image.style.display = 'none'; + processQueue(); + }; + image.src = imageSrc; +} + +function displayTime(seconds) { + let hours = Math.floor(Math.abs(seconds) / 3600); + let minutes = Math.floor((Math.abs(seconds) % 3600) / 60); + let remainingSeconds = Math.abs(seconds) % 60; + return (seconds < 0 ? '-' : '') + (hours > 0 ? `${hours}:${String(minutes).padStart(2, '0')}` : `${minutes}`) + `:${String(remainingSeconds).padStart(2, '0')}`; +} diff --git a/esp32_fw/data/numbers1-1.vlw b/esp32_fw/data/numbers1-1.vlw new file mode 100644 index 00000000..e5e2e87a Binary files /dev/null and b/esp32_fw/data/numbers1-1.vlw differ diff --git a/esp32_fw/data/numbers1-2.vlw b/esp32_fw/data/numbers1-2.vlw new file mode 100644 index 00000000..c6f615be Binary files /dev/null and b/esp32_fw/data/numbers1-2.vlw differ diff --git a/esp32_fw/data/numbers2-1.vlw b/esp32_fw/data/numbers2-1.vlw new file mode 100644 index 00000000..1dbfa21b Binary files /dev/null and b/esp32_fw/data/numbers2-1.vlw differ diff --git a/esp32_fw/data/numbers2-2.vlw b/esp32_fw/data/numbers2-2.vlw new file mode 100644 index 00000000..60ade58b Binary files /dev/null and b/esp32_fw/data/numbers2-2.vlw differ diff --git a/esp32_fw/data/numbers3-1.vlw b/esp32_fw/data/numbers3-1.vlw new file mode 100644 index 00000000..fff661e3 Binary files /dev/null and b/esp32_fw/data/numbers3-1.vlw differ diff --git a/esp32_fw/data/numbers3-2.vlw b/esp32_fw/data/numbers3-2.vlw new file mode 100644 index 00000000..1dbfa21b Binary files /dev/null and b/esp32_fw/data/numbers3-2.vlw differ diff --git a/esp32_fw/include/commstructs.h b/esp32_fw/include/commstructs.h index bf1f2a24..ac93dba7 100644 --- a/esp32_fw/include/commstructs.h +++ b/esp32_fw/include/commstructs.h @@ -56,4 +56,6 @@ struct pendingData { } __packed; #define BLOCK_DATA_SIZE 4096 -#define BLOCK_XFER_BUFFER_SIZE BLOCK_DATA_SIZE + sizeof(struct blockData) \ No newline at end of file +#define BLOCK_XFER_BUFFER_SIZE BLOCK_DATA_SIZE + sizeof(struct blockData) + +#pragma pack(pop) \ No newline at end of file diff --git a/esp32_fw/include/contentmanager.h b/esp32_fw/include/contentmanager.h new file mode 100644 index 00000000..64fe7a25 --- /dev/null +++ b/esp32_fw/include/contentmanager.h @@ -0,0 +1,15 @@ +#include + +#include +#include "makeimage.h" +#include +#include "tag_db.h" +#include + +void contentRunner(); +void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo); +bool updateTagImage(String &filename, uint8_t *dst, uint16_t nextCheckin); +void drawDate(String &filename); +void drawNumber(String &filename, int32_t count, int32_t thresholdred); +bool getImgURL(String &filename, String URL, time_t fetched); +char *formatHttpDate(time_t t); \ No newline at end of file diff --git a/esp32_fw/include/makeimage.h b/esp32_fw/include/makeimage.h index 71d747ef..3ce9ef15 100644 --- a/esp32_fw/include/makeimage.h +++ b/esp32_fw/include/makeimage.h @@ -1,6 +1,8 @@ #include #include +#pragma once + struct BitmapFileHeader { uint8_t sig[2]; uint32_t fileSz; @@ -27,7 +29,6 @@ enum EinkClut { EinkClutThreeBlacksAndRed, }; -void tftinit(); void spr2grays(TFT_eSprite &spr, long w, long h, String fileout); void jpg2grays(String filein, String fileout); void bmp2grays(String filein, String fileout); diff --git a/esp32_fw/include/pendingdata.h b/esp32_fw/include/pendingdata.h index b4116bfb..1bbc1e9f 100644 --- a/esp32_fw/include/pendingdata.h +++ b/esp32_fw/include/pendingdata.h @@ -21,3 +21,5 @@ class pendingdata { void garbageCollection(void* parameter); extern std::vector pendingfiles; + +#pragma pack(pop) \ No newline at end of file diff --git a/esp32_fw/include/tag_db.h b/esp32_fw/include/tag_db.h index 2d44313a..3c6a77c2 100644 --- a/esp32_fw/include/tag_db.h +++ b/esp32_fw/include/tag_db.h @@ -1,4 +1,5 @@ #include +#include #include @@ -11,26 +12,36 @@ enum contentModes { CountDays, CountHours, Weather, - PubTrans, + Firmware, Memo, + ImageUrl, }; class tagRecord { - public: - tagRecord() : mac{0}, model(0), alias(""), lastseen(0), nextupdate(0), contentMode(Image), pending(false), button(false), currFilename(""), pendingFilename("") {} + public: + uint16_t nextCheckinpending; + tagRecord() : mac{0}, model(0), alias(""), lastseen(0), nextupdate(0), contentMode(Image), pending(false), button(false), md5{0}, md5pending{0}, CheckinInMinPending(0), expectedNextCheckin(0), modeConfigJson("") {} - uint8_t mac[6]; - u_int8_t model; - String alias; - uint32_t lastseen; - uint32_t nextupdate; - contentModes contentMode; - bool pending; - bool button; - String currFilename; - String pendingFilename; - static tagRecord* findByMAC(uint8_t mac[6]); + uint8_t mac[6]; + u_int8_t model; + String alias; + uint32_t lastseen; + uint32_t nextupdate; + contentModes contentMode; + bool pending; + bool button; + uint8_t md5[16]; + uint8_t md5pending[16]; + uint16_t CheckinInMinPending; + uint32_t expectedNextCheckin; + String modeConfigJson; + static tagRecord* findByMAC(uint8_t mac[6]); }; extern std::vector tagDB; -String tagDBtoJson(uint8_t mac[6] = nullptr); \ No newline at end of file +String tagDBtoJson(uint8_t mac[6] = nullptr, uint8_t startPos = 0); +void fillNode(JsonObject &tag, tagRecord* &taginfo); +void saveDB(String filename); +void loadDB(String filename); + +#pragma pack(pop) \ No newline at end of file diff --git a/esp32_fw/include/web.h b/esp32_fw/include/web.h index 71c40f5c..7badb987 100644 --- a/esp32_fw/include/web.h +++ b/esp32_fw/include/web.h @@ -1,11 +1,12 @@ -#include + +#include #include #include void init_web(); void doImageUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); -//extern void webSocketSendProcess(void *parameter); +extern void webSocketSendProcess(void *parameter); void wsString(String text); void wsSendTaginfo(uint8_t mac[6]); void wsSendSysteminfo(); diff --git a/esp32_fw/src/contentmanager.cpp b/esp32_fw/src/contentmanager.cpp new file mode 100644 index 00000000..1558159a --- /dev/null +++ b/esp32_fw/src/contentmanager.cpp @@ -0,0 +1,228 @@ +#include "contentmanager.h" + +#include +#include +#include +#include "newproto.h" +#include +#include +#include + +#include "commstructs.h" +#include "makeimage.h" +#include "web.h" + +void contentRunner() { + time_t now; + time(&now); + + for (int16_t c = 0; c < tagDB.size(); c++) { + tagRecord* taginfo = nullptr; + taginfo = tagDB.at(c); + + if (now >= taginfo->nextupdate || taginfo->button) { + uint8_t mac8[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + memcpy(mac8 + 2, taginfo->mac, 6); + uint8_t src[8]; + *((uint64_t *)src) = swap64(*((uint64_t *)mac8)); + + drawNew(src, taginfo->button, taginfo); + taginfo->button = false; + } + } +} + +void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { + time_t now; + time(&now); + struct tm *time_info = gmtime(&now); + + char buffer[64]; + uint8_t src[8]; + *((uint64_t *)src) = swap64(*((uint64_t *)mac)); + sprintf(buffer, "%02X%02X%02X%02X%02X%02X\0", src[2], src[3], src[4], src[5], src[6], src[7]); + String dst = (String)buffer; + + String filename = "/" + dst + ".bmp"; + + time_info->tm_hour = 0; + time_info->tm_min = 0; + time_info->tm_sec = 0; + time_info->tm_mday++; + time_t midnight = mktime(time_info); + + DynamicJsonDocument doc(500); + deserializeJson(doc, taginfo->modeConfigJson); + JsonObject cfgobj = doc.as(); + + switch (taginfo->contentMode) { + case Image: + + filename = cfgobj["filename"].as(); + if (filename && filename !="null" && !cfgobj["#fetched"].as()) { + if (prepareDataAvail(&filename, DATATYPE_IMGRAW, mac, cfgobj["timetolive"].as())) { + cfgobj["#fetched"] = true; + } else { + wsString("Error accessing " + filename); + } + taginfo->nextupdate = 3216153600; + } + break; + + case Today: + + drawDate(filename); + updateTagImage(filename, mac, (midnight - now) / 60 - 10); + taginfo->nextupdate = midnight; + break; + + case CountDays: + + if (buttonPressed) cfgobj["counter"] = 0; + drawNumber(filename, (int32_t)cfgobj["counter"], (int32_t)cfgobj["thresholdred"]); + updateTagImage(filename, mac, (midnight - now) / 60 - 5); + cfgobj["counter"] = (int32_t)cfgobj["counter"] + 1; + taginfo->nextupdate = midnight; + break; + + case CountHours: + + if (buttonPressed) cfgobj["counter"] = 0; + drawNumber(filename, (int32_t)cfgobj["counter"], (int32_t)cfgobj["thresholdred"]); + // updateTagImage(&filename, mac, (3600 - now % 3600) / 60); + // taginfo->nextupdate = now + 3600 - (now % 3600); + updateTagImage(filename, mac, 3); + cfgobj["counter"] = (int32_t)cfgobj["counter"] + 1; + taginfo->nextupdate = now + 300; + break; + + case Weather: + + // https://open-meteo.com/ + break; + + case Firmware: + + filename = cfgobj["filename"].as(); + if (filename && filename != "null" && !cfgobj["#fetched"].as()) { + if (prepareDataAvail(&filename, DATATYPE_UPDATE, mac, cfgobj["timetolive"].as())) { + cfgobj["#fetched"] = true; + } else { + wsString("Error accessing " + filename); + } + taginfo->nextupdate = 3216153600; + taginfo->contentMode = Image; + } + break; + + case Memo: + break; + case ImageUrl: + + if (getImgURL(filename, cfgobj["url"], (time_t)cfgobj["#fetched"])) { + updateTagImage(filename, mac, cfgobj["interval"].as()); + cfgobj["#fetched"] = now; + } + taginfo->nextupdate = now + 60 * cfgobj["interval"].as(); + break; + } + + taginfo->modeConfigJson = doc.as(); +} + +bool updateTagImage(String &filename, uint8_t *dst, uint16_t nextCheckin) { + prepareDataAvail(&filename, DATATYPE_IMGRAW, dst, nextCheckin); + return true; +} + +void drawDate(String &filename) { + + TFT_eSPI tft = TFT_eSPI(); + TFT_eSprite spr = TFT_eSprite(&tft); + time_t now; + time(&now); + struct tm timeinfo; + localtime_r(&now, &timeinfo); + String Dag[] = {"zondag","maandag","dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"}; + String Maand[] = {"januari", "februari", "maart", "april", "mei", "juni","juli", "augustus", "september", "oktober", "november", "december"}; + int weekday_number = timeinfo.tm_wday; + int month_number = timeinfo.tm_mon; + + LittleFS.begin(); + long w = 296, h = 128; // mag staand of liggend + spr.createSprite(w, h); + spr.setColorDepth(8); + spr.fillSprite(TFT_WHITE); + spr.setTextDatum(TC_DATUM); + spr.loadFont("calibrib62", LittleFS); + spr.setTextColor(TFT_RED, TFT_WHITE); + spr.drawString(Dag[timeinfo.tm_wday], w / 2, 10); + spr.loadFont("calibrib50", LittleFS); + spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.drawString(String(timeinfo.tm_mday) + " " + Maand[timeinfo.tm_mon], w / 2, 73); + spr.unloadFont(); + + spr2grays(spr, w, h, filename); + + spr.deleteSprite(); +} + +void drawNumber(String &filename, int32_t count, int32_t thresholdred) { + TFT_eSPI tft = TFT_eSPI(); + TFT_eSprite spr = TFT_eSprite(&tft); + + LittleFS.begin(); + long w = 296, h = 128; + spr.createSprite(w, h); + spr.setColorDepth(8); + spr.fillSprite(TFT_WHITE); + spr.setTextDatum(MC_DATUM); + if (count > thresholdred) { + spr.setTextColor(TFT_RED, TFT_WHITE); + } else { + spr.setTextColor(TFT_BLACK, TFT_WHITE); + } + String font = "numbers1-2"; + if (count>999) font="numbers2-2"; + if (count>9999) font="numbers3-2"; + spr.loadFont(font, LittleFS); + spr.drawString(String(count), w/2, h/2+10); + spr.unloadFont(); + + spr2grays(spr, w, h, filename); + + spr.deleteSprite(); +} + +bool getImgURL(String &filename, String URL, time_t fetched) { + // https://images.klari.net/kat-bw29.jpg + + LittleFS.begin(); + + Serial.println("get external " + URL); + HTTPClient http; + http.begin(URL); + http.addHeader("If-Modified-Since", formatHttpDate(fetched)); + http.setTimeout(5000); //timeout in ms + int httpCode = http.GET(); + if (httpCode == 200) { + File f = LittleFS.open(filename, "w"); + if (f) { + http.writeToStream(&f); + f.close(); + jpg2grays(filename, filename); + } + } else { + Serial.println("http " + String(httpCode)); + } + http.end(); + return (httpCode == 200); +} + +char *formatHttpDate(time_t t) { + static char buf[40]; + struct tm *timeinfo; + timeinfo = gmtime(&t); + strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", timeinfo); + return buf; +} diff --git a/esp32_fw/src/main.cpp b/esp32_fw/src/main.cpp index 71f04968..e46ea598 100644 --- a/esp32_fw/src/main.cpp +++ b/esp32_fw/src/main.cpp @@ -3,6 +3,7 @@ #include #include +#include "contentmanager.h" #include "flasher.h" #include "makeimage.h" #include "pendingdata.h" @@ -11,31 +12,35 @@ #include "tag_db.h" #include "web.h" -void freeHeapTask(void* parameter) { +void timeTask(void* parameter) { while (1) { - //Serial.printf("Free heap=%d\n", ESP.getFreeHeap()); - //time_t now; + time_t now; + time(&now); tm tm; if (!getLocalTime(&tm)) { Serial.println("Failed to obtain time"); + } else { + if (now % 10 == 0) wsSendSysteminfo(); + contentRunner(); } - wsSendSysteminfo(); - vTaskDelay(30000 / portTICK_PERIOD_MS); + vTaskDelay(1000 / portTICK_PERIOD_MS); } } void setup() { Serial.begin(115200); Serial.print(">\n"); - init_web(); configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "europe.pool.ntp.org", "time.nist.gov"); // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv - xTaskCreate(freeHeapTask, "print free heap", 10000, NULL, 2, NULL); + init_web(); + loadDB("/tagDB.json"); + + xTaskCreate(timeTask, "timed tasks", 10000, NULL, 2, NULL); xTaskCreate(zbsRxTask, "zbsRX Process", 10000, NULL, 2, NULL); xTaskCreate(garbageCollection, "pending-data cleanup", 5000, NULL, 1, NULL); - //xTaskCreate(webSocketSendProcess, "ws", 5000, NULL,configMAX_PRIORITIES-10, NULL); + xTaskCreate(webSocketSendProcess, "ws", 5000, NULL,configMAX_PRIORITIES-10, NULL); } void loop() { diff --git a/esp32_fw/src/makeimage.cpp b/esp32_fw/src/makeimage.cpp index ff01570a..90cc66ee 100644 --- a/esp32_fw/src/makeimage.cpp +++ b/esp32_fw/src/makeimage.cpp @@ -8,28 +8,6 @@ TFT_eSPI tft = TFT_eSPI(); TFT_eSprite spr = TFT_eSprite(&tft); -void tftinit() { - //tijdelijk: voorbeeld voor aanmaak van plaatje - - LittleFS.begin(); - long w = 296, h = 128; // mag staand of liggend - spr.createSprite(w, h); - spr.setColorDepth(8); - spr.fillSprite(TFT_WHITE); - spr.setTextDatum(TC_DATUM); - spr.loadFont("calibrib62", LittleFS); - spr.setTextColor(TFT_RED, TFT_WHITE); - spr.drawString("zondag", w / 2, 10); - spr.loadFont("calibrib50", LittleFS); - spr.setTextColor(TFT_BLACK, TFT_WHITE); - spr.drawString("29 januari", w / 2, 73); - spr.unloadFont(); - - spr2grays(spr, w, h, "/testspr3.bmp"); - - spr.deleteSprite(); -} - bool spr_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) { spr.pushImage(x, y, w, h, bitmap); return 1; @@ -40,6 +18,7 @@ void jpg2grays(String filein, String fileout) { TJpgDec.setCallback(spr_output); uint16_t w = 0, h = 0; TJpgDec.getFsJpgSize(&w, &h, filein); + Serial.println("jpeg conversion " + String(w) + "x" + String(h)); spr.createSprite(w, h); spr.setColorDepth(8); diff --git a/esp32_fw/src/newproto.cpp b/esp32_fw/src/newproto.cpp index 64f48fd7..297dd2b1 100644 --- a/esp32_fw/src/newproto.cpp +++ b/esp32_fw/src/newproto.cpp @@ -1,4 +1,3 @@ -#pragma pack(push, 1) #include "newproto.h" #include @@ -51,6 +50,12 @@ void prepareCancelPending(uint64_t ver) { } bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t nextCheckin) { + + if (nextCheckin > 1440) { + //to prevent very long sleeps of the tag + nextCheckin = 0; + } + *filename = "/" + *filename; if (!LittleFS.exists(*filename)) return false; fs::File file = LittleFS.open(*filename); @@ -101,6 +106,20 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t md5.getBytes(md5bytes); } + uint8_t src[8]; + *((uint64_t*)src) = swap64(*((uint64_t*)dst)); + uint8_t mac[6]; + memcpy(mac, src + 2, sizeof(mac)); + tagRecord* taginfo = nullptr; + taginfo = tagRecord::findByMAC(mac); + if (taginfo != nullptr) { + if (memcmp(md5bytes, taginfo->md5pending, 16) == 0) { + wsString("new image is the same as current image. not updating tag."); + wsSendTaginfo(mac); + return false; + } + } + // the message that will be sent to the AP to tell the tag there is data pending struct pendingData pending = {0}; memcpy(pending.targetMac, dst, 8); @@ -124,21 +143,23 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t pendinginfo->timeout = 1800; pendingfiles.push_back(pendinginfo); - char dst_path[64]; - sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X.pending\0", dst[5], dst[4], dst[3], dst[2], dst[1], dst[0]); - file = LittleFS.open(dst_path, "w"); - int bytes_written = file.write(pendinginfo->data, pendinginfo->len); - file.close(); + if (dataType != DATATYPE_UPDATE) { + char dst_path[64]; + sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X.pending\0", dst[5], dst[4], dst[3], dst[2], dst[1], dst[0]); + file = LittleFS.open(dst_path, "w"); + int bytes_written = file.write(pendinginfo->data, pendinginfo->len); + file.close(); - uint8_t src[8]; - *((uint64_t*)src) = swap64(*((uint64_t*)dst)); - uint8_t mac[6]; - memcpy(mac, src + 2, sizeof(mac)); - tagRecord* taginfo = nullptr; - taginfo = tagRecord::findByMAC(mac); - if (taginfo != nullptr) { - taginfo->pending = true; + wsString("new image pending: " + String(dst_path)); + if (taginfo != nullptr) { + taginfo->pending = true; + taginfo->CheckinInMinPending = nextCheckin + 1; + memcpy(taginfo->md5pending, md5bytes, sizeof(md5bytes)); + } + } else { + Serial.println("firmware upload pending"); } + wsSendTaginfo(mac); return true; @@ -196,23 +217,30 @@ void processXferComplete(struct espXferComplete* xfc) { uint8_t mac[6]; memcpy(mac, src + 2, sizeof(mac)); - tagRecord* taginfo = nullptr; - taginfo = tagRecord::findByMAC(mac); - if (taginfo != nullptr) { - taginfo->pending = false; - } - wsSendTaginfo(mac); - char src_path[64]; char dst_path[64]; char tmp_path[64]; sprintf(src_path, "/current/%02X%02X%02X%02X%02X%02X.pending\0", src[2], src[3], src[4], src[5], src[6], src[7]); sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X.bmp\0", src[2], src[3], src[4], src[5], src[6], src[7]); sprintf(tmp_path, "/temp/%02X%02X%02X%02X%02X%02X.bmp\0", src[2], src[3], src[4], src[5], src[6], src[7]); + if (LittleFS.exists(dst_path)) { + LittleFS.remove(dst_path); + } LittleFS.rename(src_path, dst_path); if (LittleFS.exists(tmp_path)) { LittleFS.remove(tmp_path); } + + time_t now; + time(&now); + tagRecord* taginfo = nullptr; + taginfo = tagRecord::findByMAC(mac); + if (taginfo != nullptr) { + taginfo->pending = false; + taginfo->expectedNextCheckin = now + 60 * taginfo->CheckinInMinPending + 30; + memcpy(taginfo->md5, taginfo->md5pending, sizeof(taginfo->md5pending)); + } + wsSendTaginfo(mac); } void processDataReq(struct espAvailDataReq* eadr) { @@ -225,19 +253,20 @@ void processDataReq(struct espAvailDataReq* eadr) { memcpy(mac, src + 2, sizeof(mac)); taginfo = tagRecord::findByMAC(mac); - time_t now; - time(&now); if (taginfo == nullptr) { taginfo = new tagRecord; memcpy(taginfo->mac, src + 2, sizeof(taginfo->mac)); taginfo->pending = false; tagDB.push_back(taginfo); } + time_t now; + time(&now); taginfo->lastseen = now; + taginfo->expectedNextCheckin = now + 300; taginfo->button = (eadr->adr.buttonState == 1); sprintf(buffer, " #include #include diff --git a/esp32_fw/src/tag_db.cpp b/esp32_fw/src/tag_db.cpp index 162ec82c..e1af5f24 100644 --- a/esp32_fw/src/tag_db.cpp +++ b/esp32_fw/src/tag_db.cpp @@ -1,9 +1,12 @@ -#include - #include "tag_db.h" + +#include #include + #include +#include "LittleFS.h" + std::vector tagDB; tagRecord* tagRecord::findByMAC(uint8_t mac[6]) { @@ -17,35 +20,149 @@ tagRecord* tagRecord::findByMAC(uint8_t mac[6]) { return nullptr; } -String tagDBtoJson(uint8_t mac[6]) { - DynamicJsonDocument doc(1000); +String tagDBtoJson(uint8_t mac[6], uint8_t startPos) { + DynamicJsonDocument doc(2500); JsonArray tags = doc.createNestedArray("tags"); - for (int16_t c = 0; c < tagDB.size(); c++) { + for (int16_t c = startPos; c < tagDB.size(); c++) { tagRecord* taginfo = nullptr; taginfo = tagDB.at(c); - bool select = false; - if (mac) { + bool select = false; + if (mac) { if (memcmp(taginfo->mac, mac, 6) == 0) { select = true; } } else { - select = true; - } - if (select) { - JsonObject tag = tags.createNestedObject(); - char buffer[64]; - sprintf(buffer, "%02X%02X%02X%02X%02X%02X\0", taginfo->mac[0], taginfo->mac[1], taginfo->mac[2], taginfo->mac[3], taginfo->mac[4], taginfo->mac[5]); - tag["mac"] = (String)buffer; - tag["lastseen"] = taginfo->lastseen; - tag["nextupdate"] = taginfo->nextupdate; - tag["model"] = taginfo->model; - tag["pending"] = taginfo->pending; - tag["button"] = taginfo->button; - tag["alias"] = taginfo->alias; - tag["contentmode"] = taginfo->contentMode; + select = true; + } + if (select) { + JsonObject tag = tags.createNestedObject(); + fillNode(tag, taginfo); + if (mac) { + break; + } + } + if (doc.capacity()-doc.memoryUsage() < doc.memoryUsage()/(c+1) + 100) { + doc["continu"] = c+1; + break; } } return doc.as(); -} \ No newline at end of file +} + +void fillNode(JsonObject &tag, tagRecord* &taginfo) { + char buffer[16]; + sprintf(buffer, "%02X%02X%02X%02X%02X%02X\0", taginfo->mac[0], taginfo->mac[1], taginfo->mac[2], taginfo->mac[3], taginfo->mac[4], taginfo->mac[5]); + tag["mac"] = (String)buffer; + char hex[7]; + sprintf(hex, "%02x%02x%02x\0", taginfo->md5[0], taginfo->md5[1], taginfo->md5[2]); + tag["hash"] = hex; + tag["lastseen"] = taginfo->lastseen; + tag["nextupdate"] = taginfo->nextupdate; + tag["nextcheckin"] = taginfo->expectedNextCheckin; + tag["model"] = taginfo->model; + tag["pending"] = taginfo->pending; + tag["button"] = taginfo->button; + tag["alias"] = taginfo->alias; + tag["contentmode"] = taginfo->contentMode; + tag["modecfgjson"] = taginfo->modeConfigJson; +} + +void saveDB(String filename) { + DynamicJsonDocument doc(2500); + + Serial.println("start writing DB to file"); + long t = millis(); + + LittleFS.begin(); + fs::File file = LittleFS.open(filename, "w"); + if (!file) { + Serial.println("saveDB: Failed to open file"); + return; + } + + file.write('['); + + for (int16_t c = 0; c < tagDB.size(); c++) { + doc.clear(); + tagRecord* taginfo = nullptr; + taginfo = tagDB.at(c); + + JsonObject tag = doc.createNestedObject(); + fillNode(tag, taginfo); + if (c > 0) { + file.write(','); + } + serializeJson(doc, file); + } + file.write(']'); + + file.close(); + Serial.println(millis() - t); + Serial.println("finished writing file"); + + return; +} + +void loadDB(String filename) { + StaticJsonDocument<400> doc; + + Serial.println("start reading DB from file"); + long t = millis(); + + LittleFS.begin(); + fs::File readfile = LittleFS.open(filename, "r"); + if (!readfile) { + Serial.println("loadDB: Failed to open file"); + return; + } + + time_t now; + time(&now); + bool parsing = true; + + if (readfile.find("[")) { + while (parsing) { + DeserializationError err = deserializeJson(doc, readfile); + if (!err) { + JsonObject tag = doc[0]; + String dst = tag["mac"].as(); + uint8_t mac[12]; + if (sscanf(dst.c_str(), "%02X%02X%02X%02X%02X%02X", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) == 6) { + tagRecord* taginfo = nullptr; + taginfo = tagRecord::findByMAC(mac); + if (taginfo == nullptr) { + taginfo = new tagRecord; + memcpy(taginfo->mac, mac, sizeof(taginfo->mac)); + tagDB.push_back(taginfo); + } + //taginfo->lastseen = (uint32_t)tag["lastseen"]; + taginfo->lastseen = 0; + taginfo->nextupdate = (uint32_t)tag["nextupdate"]; + taginfo->expectedNextCheckin = (uint16_t)tag["nextcheckin"]; + if (taginfo->expectedNextCheckin < now - 1800) { + taginfo->expectedNextCheckin = now + 1800; + } + taginfo->model = (uint8_t)tag["model"]; + taginfo->pending = false; + taginfo->button = false; + taginfo->alias = tag["alias"].as(); + taginfo->contentMode = static_cast(tag["contentmode"]); + taginfo->modeConfigJson = tag["modecfgjson"].as(); + } + } else { + Serial.print(F("deserializeJson() failed: ")); + Serial.println(err.c_str()); + parsing = false; + } + parsing = parsing && readfile.find(","); + } + } + + readfile.close(); + Serial.println(millis() - t); + Serial.println("finished reading file"); + + return; +} diff --git a/esp32_fw/src/web.cpp b/esp32_fw/src/web.cpp index 3eb35563..5b8618a3 100644 --- a/esp32_fw/src/web.cpp +++ b/esp32_fw/src/web.cpp @@ -2,6 +2,7 @@ #include #include + #include #include #include @@ -54,62 +55,15 @@ void webSocketSendProcess(void *parameter) { // sendStatus(STATUS_WIFI_ACTIVITY); DynamicJsonDocument doc(1500); if (ulNotificationValue & 2) { // WS_SEND_MODE_STATUS) { - /* doc["rxActive"] = status.rxActive; - doc["txActive"] = status.txActive; - doc["freq"] = status.freq; - doc["txMode"] = status.currentmode; - */ } /* JsonArray statusframes = doc.createNestedArray("frames"); - for (uint8_t c = 0; c < STATUSFRAMELISTSIZE; c++) { - if (statusframearr[c]) { - JsonObject statusframe = statusframes.createNestedObject(); - statusframe["frame"] = statusframearr[c]->frameno; - statusframe["isTX"] = statusframearr[c]->isTX; - statusframe["freq"] = statusframearr[c]->freq; - statusframe["txSkipped"] = statusframearr[c]->txCancelled; - switch (statusframearr[c]->rxtype) { - case flexsynctype::SYNC_FLEX_1600: - statusframe["rxType"] = "FLEX_1600"; - break; - case flexsynctype::SYNC_FLEX_3200_2: - statusframe["rxType"] = "FLEX_3200_2"; - break; - case flexsynctype::SYNC_FLEX_3200_4: - statusframe["rxType"] = "FLEX_3200_4"; - break; - case flexsynctype::SYNC_FLEX_6400: - statusframe["rxType"] = "FLEX_3200_4"; - break; - default: - break; - } - switch (statusframearr[c]->txformat) { - case txframe::FORMAT_FLEX: - statusframe["txType"] = "FLEX"; - break; - case txframe::FORMAT_POCSAG: - statusframe["txType"] = "POCSAG"; - break; - case txframe::FORMAT_IDLE: - statusframe["txType"] = "IDLE"; - break; - case txframe::FORMAT_BLOCKED: - statusframe["txType"] = "BLOCKED"; - break; - default: - break; - } - } - } }*/ size_t len = measureJson(doc); xSemaphoreTake(wsMutex, portMAX_DELAY); auto buffer = std::make_shared>(len); serializeJson(doc, buffer->data(), len); // ws.textAll((char*)buffer->data()); - //ws.textAll("ohai"); xSemaphoreGive(wsMutex); } } @@ -214,28 +168,14 @@ void wsSendSysteminfo() { } void wsSendTaginfo(uint8_t mac[6]) { - DynamicJsonDocument doc(1000); - JsonArray tags = doc.createNestedArray("tags"); - JsonObject tag = tags.createNestedObject(); - char buffer[64]; - sprintf(buffer, "%02X%02X%02X%02X%02X%02X\0", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - tag["mac"] = (String)buffer; - tagRecord *taginfo = nullptr; - taginfo = tagRecord::findByMAC(mac); - if (taginfo != nullptr) { - tag["lastseen"] = taginfo->lastseen; - tag["nextupdate"] = taginfo->nextupdate; - tag["model"] = taginfo->model; - tag["pending"] = taginfo->pending; - tag["button"] = taginfo->button; - tag["alias"] = taginfo->alias; - tag["contentmode"] = taginfo->contentMode; - } + String json = ""; + json = tagDBtoJson(mac); xSemaphoreTake(wsMutex, portMAX_DELAY); - ws.textAll(doc.as()); + ws.textAll(json); xSemaphoreGive(wsMutex); + } void init_web() { @@ -282,70 +222,6 @@ void init_web() { }, doImageUpload); - server.on("/send_image", HTTP_POST, [](AsyncWebServerRequest *request) { - String filename; - String dst; - uint16_t nextCheckin; - if (request->hasParam("filename", true) && request->hasParam("dst", true)) { - filename = request->getParam("filename", true)->value(); - dst = request->getParam("dst", true)->value(); - nextCheckin = request->getParam("ttl",true)->value().toInt(); - uint8_t mac_addr[12]; // I expected this to return like 8 values, but if I make the array 8 bytes long, things die. - mac_addr[0] = 0x00; - mac_addr[1] = 0x00; - if (sscanf(dst.c_str(), "%02X%02X%02X%02X%02X%02X", - &mac_addr[2], - &mac_addr[3], - &mac_addr[4], - &mac_addr[5], - &mac_addr[6], - &mac_addr[7]) != 6) { - request->send(200, "text/plain", "Something went wrong trying to parse the mac address"); - } else { - *((uint64_t *)mac_addr) = swap64(*((uint64_t *)mac_addr)); - if (prepareDataAvail(&filename, DATATYPE_IMGRAW, mac_addr, nextCheckin)) { - request->send(200, "text/plain", "Sending to " + dst); - } else { - request->send(200, "text/plain", "Couldn't find filename :("); - } - } - return; - } - request->send(200, "text/plain", "Didn't get the required filename + dst"); - return; - }); - - server.on("/send_fw", HTTP_POST, [](AsyncWebServerRequest *request) { - String filename; - String dst; - if (request->hasParam("filename", true) && request->hasParam("dst", true)) { - filename = request->getParam("filename", true)->value(); - dst = request->getParam("dst", true)->value(); - uint8_t mac_addr[12]; // I expected this to return like 8 values, but if I make the array 8 bytes long, things die. - mac_addr[0] = 0x00; - mac_addr[1] = 0x00; - if (sscanf(dst.c_str(), "%02X%02X%02X%02X%02X%02X", - &mac_addr[2], - &mac_addr[3], - &mac_addr[4], - &mac_addr[5], - &mac_addr[6], - &mac_addr[7]) != 6) { - request->send(200, "text/plain", "Something went wrong trying to parse the mac address"); - } else { - *((uint64_t *)mac_addr) = swap64(*((uint64_t *)mac_addr)); - if (prepareDataAvail(&filename, DATATYPE_UPDATE, mac_addr, 0)) { - request->send(200, "text/plain", "Sending FW to " + dst); - } else { - request->send(200, "text/plain", "Couldn't find filename :("); - } - } - return; - } - request->send(200, "text/plain", "Didn't get the required filename + dst"); - return; - }); - server.on("/req_checkin", HTTP_POST, [](AsyncWebServerRequest *request) { String filename; String dst; @@ -383,8 +259,12 @@ void init_web() { json = tagDBtoJson(mac); } } else { - json = tagDBtoJson(); - } + uint8_t startPos=0; + if (request->hasParam("pos")) { + startPos = atoi(request->getParam("pos")->value().c_str()); + } + json = tagDBtoJson(nullptr,startPos); + } request->send(200, "application/json", json); }); @@ -397,9 +277,12 @@ void init_web() { taginfo = tagRecord::findByMAC(mac); if (taginfo != nullptr) { taginfo->alias = request->getParam("alias", true)->value(); + taginfo->modeConfigJson = request->getParam("modecfgjson", true)->value(); taginfo->contentMode = (contentModes)atoi(request->getParam("contentmode", true)->value().c_str()); taginfo->model = atoi(request->getParam("model", true)->value().c_str()); + taginfo->nextupdate = 0; wsSendTaginfo(mac); + saveDB("/tagDB.json"); request->send(200, "text/plain", "Ok, saved"); } else { request->send(200, "text/plain", "Error while saving: mac not found"); @@ -414,55 +297,6 @@ void init_web() { request->send(200, "text/html", "-"); return; } - Serial.printf("NOT_FOUND: "); - - switch (request->method()) { - case HTTP_GET: - Serial.printf("GET"); - break; - case HTTP_POST: - Serial.printf("POST"); - break; - case HTTP_DELETE: - Serial.printf("DELETE"); - break; - case HTTP_PUT: - Serial.printf("PUT"); - break; - case HTTP_PATCH: - Serial.printf("PATCH"); - break; - case HTTP_HEAD: - Serial.printf("HEAD"); - break; - case HTTP_OPTIONS: - Serial.printf("OPTIONS"); - break; - - default: - Serial.printf("UNKNOWN"); - break; - } - Serial.printf(" http://%s%s\n", request->host().c_str(), request->url().c_str()); - - if (request->contentLength()) { - Serial.printf("_CONTENT_TYPE: %s\n", request->contentType().c_str()); - Serial.printf("_CONTENT_LENGTH: %u\n", request->contentLength()); - } - for (int i = 0; i < request->headers(); i++) { - AsyncWebHeader *h = request->getHeader(i); - Serial.printf("_HEADER[%s]: %s\n", h->name().c_str(), h->value().c_str()); - } - for (int i = 0; i < request->params(); i++) { - AsyncWebParameter *p = request->getParam(i); - if (p->isFile()) { - Serial.printf("_FILE[%s]: %s, size: %u\n", p->name().c_str(), p->value().c_str(), p->size()); - } else if (p->isPost()) { - Serial.printf("_POST[%s]: %s\n", p->name().c_str(), p->value().c_str()); - } else { - Serial.printf("_GET[%s]: %s\n", p->name().c_str(), p->value().c_str()); - } - } request->send(404); });