diff --git a/ESP32_AP-Flasher/include/newproto.h b/ESP32_AP-Flasher/include/newproto.h index 7746a2db..90a95986 100644 --- a/ESP32_AP-Flasher/include/newproto.h +++ b/ESP32_AP-Flasher/include/newproto.h @@ -7,7 +7,7 @@ extern void processBlockRequest(struct espBlockRequest* br); extern void prepareCancelPending(const uint8_t dst[8]); extern void prepareIdleReq(const uint8_t* dst, uint16_t nextCheckin); extern void prepareDataAvail(uint8_t* data, uint16_t len, uint8_t dataType, const uint8_t* dst); -extern bool prepareDataAvail(String& filename, uint8_t dataType, const uint8_t* dst, uint16_t nextCheckin); +extern bool prepareDataAvail(String& filename, uint8_t dataType, const uint8_t* dst, uint16_t nextCheckin, bool resend = false); extern void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP); extern void processXferComplete(struct espXferComplete* xfc, bool local); extern void processXferTimeout(struct espXferComplete* xfc, bool local); diff --git a/ESP32_AP-Flasher/include/storage.h b/ESP32_AP-Flasher/include/storage.h index a1732e25..c17713e8 100644 --- a/ESP32_AP-Flasher/include/storage.h +++ b/ESP32_AP-Flasher/include/storage.h @@ -35,5 +35,8 @@ class DynStorage { extern DynStorage Storage; extern fs::FS *contentFS; +extern void copyFile(File in, File out); + +#endif + -#endif \ No newline at end of file diff --git a/ESP32_AP-Flasher/include/tag_db.h b/ESP32_AP-Flasher/include/tag_db.h index 2125dc08..1d415fb7 100644 --- a/ESP32_AP-Flasher/include/tag_db.h +++ b/ESP32_AP-Flasher/include/tag_db.h @@ -66,6 +66,8 @@ struct Config { uint8_t preview; uint8_t wifiPower; char timeZone[52]; + uint8_t sleepTime1; + uint8_t sleepTime2; }; struct HwType { diff --git a/ESP32_AP-Flasher/include/util.h b/ESP32_AP-Flasher/include/util.h index 6719cacc..551c4cb5 100644 --- a/ESP32_AP-Flasher/include/util.h +++ b/ESP32_AP-Flasher/include/util.h @@ -104,4 +104,24 @@ static inline bool isEmptyOrNull(const String &str) { return str.isEmpty() || str == "null"; } +/// @brief checks if the current time is between sleeptime1 and sleeptime2 +/// +/// @param sleeptime1 Start of time block +/// @param sleeptime2 End of time block +/// @return True if within time block, false is outside time block +static bool isSleeping(int sleeptime1, int sleeptime2) { + if (sleeptime1 == sleeptime2) return false; + + struct tm timeinfo; + getLocalTime(&timeinfo); + int currentHour = timeinfo.tm_hour; + + if (sleeptime1 < sleeptime2) { + return currentHour >= sleeptime1 && currentHour < sleeptime2; + } else { + return currentHour >= sleeptime1 || currentHour < sleeptime2; + } +} + + } // namespace util diff --git a/ESP32_AP-Flasher/src/contentmanager.cpp b/ESP32_AP-Flasher/src/contentmanager.cpp index 82b2635d..34e2a188 100644 --- a/ESP32_AP-Flasher/src/contentmanager.cpp +++ b/ESP32_AP-Flasher/src/contentmanager.cpp @@ -20,9 +20,7 @@ #include "storage.h" -#if defined CONTENT_RSS || defined CONTENT_CAL #include "U8g2_for_TFT_eSPI.h" -#endif #include "commstructs.h" #include "makeimage.h" #include "newproto.h" @@ -45,19 +43,31 @@ void contentRunner() { time(&now); for (tagRecord *taginfo : tagDB) { - if (taginfo->RSSI && (now >= taginfo->nextupdate || taginfo->wakeupReason == WAKEUP_REASON_GPIO || taginfo->wakeupReason == WAKEUP_REASON_NFC) && config.runStatus == RUNSTATUS_RUN && Storage.freeSpace() > 31000) { + if (taginfo->RSSI && (now >= taginfo->nextupdate || taginfo->wakeupReason == WAKEUP_REASON_GPIO || taginfo->wakeupReason == WAKEUP_REASON_NFC) && config.runStatus == RUNSTATUS_RUN && Storage.freeSpace() > 31000 && !util::isSleeping(config.sleepTime1, config.sleepTime2)) { drawNew(taginfo->mac, (taginfo->wakeupReason == WAKEUP_REASON_GPIO), taginfo); taginfo->wakeupReason = 0; } if (taginfo->expectedNextCheckin > now - 10 && taginfo->expectedNextCheckin < now + 30 && taginfo->pendingIdle == 0 && taginfo->pending == false) { - uint16_t minutesUntilNextUpdate = (taginfo->nextupdate - now) / 60; + int16_t minutesUntilNextUpdate = (taginfo->nextupdate - now) / 60; if (minutesUntilNextUpdate > config.maxsleep) { minutesUntilNextUpdate = config.maxsleep; } + if (util::isSleeping(config.sleepTime1, config.sleepTime2)) { + struct tm timeinfo; + getLocalTime(&timeinfo); + struct tm nextSleepTimeinfo = timeinfo; + nextSleepTimeinfo.tm_hour = config.sleepTime2; + nextSleepTimeinfo.tm_min = 0; + nextSleepTimeinfo.tm_sec = 0; + time_t nextWakeTime = mktime(&nextSleepTimeinfo); + if (nextWakeTime < now) nextWakeTime += 24 * 3600; + minutesUntilNextUpdate = (nextWakeTime - now) / 60 - 2; + } if (minutesUntilNextUpdate > 1 && (wsClientCount() == 0 || config.stopsleep == 0)) { taginfo->pendingIdle = minutesUntilNextUpdate; if (taginfo->isExternal == false) { + Serial.printf("sleeping for %d more minutes\n", minutesUntilNextUpdate); prepareIdleReq(taginfo->mac, minutesUntilNextUpdate); } } @@ -186,15 +196,29 @@ void drawNew(const uint8_t mac[8], const bool buttonPressed, tagRecord *&taginfo switch (taginfo->contentMode) { case 0: // Image { - const String configFilename = cfgobj["filename"].as(); - if (!util::isEmptyOrNull(configFilename) && !cfgobj["#fetched"].as()) { - imageParams.dither = cfgobj["dither"] && cfgobj["dither"] == "1"; - jpg2buffer(configFilename, filename, imageParams); + String configFilename = cfgobj["filename"].as(); + if (!util::isEmptyOrNull(configFilename)) { + if (!configFilename.startsWith("/")) { + configFilename = "/" + configFilename; + } + if (contentFS->exists(configFilename)) { + imageParams.dither = cfgobj["dither"] && cfgobj["dither"] == "1"; + jpg2buffer(configFilename, filename, imageParams); + } else { + filename = "/current/" + String(hexmac) + ".raw"; + if (contentFS->exists(filename)) { + prepareDataAvail(filename, imageParams.dataType, mac, cfgobj["timetolive"].as(), true); + wsLog("File " + configFilename + " not found, resending image " + filename); + } else { + wsErr("File " + configFilename + " not found"); + } + taginfo->nextupdate = 3216153600; + break; + } if (imageParams.hasRed) { imageParams.dataType = DATATYPE_IMG_RAW_2BPP; } if (prepareDataAvail(filename, imageParams.dataType, mac, cfgobj["timetolive"].as())) { - cfgobj["#fetched"] = true; if (cfgobj["delete"].as() == "1") { contentFS->remove("/" + configFilename); } @@ -509,7 +533,6 @@ void drawDate(String &filename, tagRecord *&taginfo, imgParam &imageParams) { struct tm timeinfo; localtime_r(&now, &timeinfo); - // const int weekday_number = timeinfo.tm_wday; const int month_number = timeinfo.tm_mon; const int year_number = timeinfo.tm_year + 1900; diff --git a/ESP32_AP-Flasher/src/main.cpp b/ESP32_AP-Flasher/src/main.cpp index 0a67e62d..357edea9 100644 --- a/ESP32_AP-Flasher/src/main.cpp +++ b/ESP32_AP-Flasher/src/main.cpp @@ -37,19 +37,25 @@ void timeTask(void* parameter) { wsSendSysteminfo(); util::printHeap(); while (1) { + unsigned long startMillis = millis(); time_t now; time(&now); - if (now % 5 == 0 || apInfo.state != AP_STATE_ONLINE || config.runStatus != RUNSTATUS_RUN) { wsSendSysteminfo(); } - if (now % 10 == 8 && config.runStatus != RUNSTATUS_STOP) { + if (now % 10 == 9 && config.runStatus != RUNSTATUS_STOP) { checkVars(); } - if (now % 300 == 6 && config.runStatus != RUNSTATUS_STOP) saveDB("/current/tagDB.json"); - if (apInfo.state == AP_STATE_ONLINE) contentRunner(); + if (now % 300 == 7 && config.runStatus != RUNSTATUS_STOP) { + saveDB("/current/tagDB.json"); + } + if (apInfo.state == AP_STATE_ONLINE) { + contentRunner(); + } - vTaskDelay(1000 / portTICK_PERIOD_MS); + if (millis() - startMillis < 1000) { + vTaskDelay((1000 - millis() + startMillis) / portTICK_PERIOD_MS); + } } } diff --git a/ESP32_AP-Flasher/src/newproto.cpp b/ESP32_AP-Flasher/src/newproto.cpp index 9a881393..f759c09a 100644 --- a/ESP32_AP-Flasher/src/newproto.cpp +++ b/ESP32_AP-Flasher/src/newproto.cpp @@ -64,7 +64,6 @@ void prepareCancelPending(const uint8_t dst[8]) { } void prepareIdleReq(const uint8_t* dst, uint16_t nextCheckin) { - if (nextCheckin > config.maxsleep) nextCheckin = config.maxsleep; if (nextCheckin > 0) { struct pendingData pending = {0}; memcpy(pending.targetMac, dst, 8); @@ -93,7 +92,7 @@ void prepareDataAvail(uint8_t* data, uint16_t len, uint8_t dataType, const uint8 memcpy(taginfo->data, data, len); taginfo->pending = true; taginfo->len = len; - taginfo->expectedNextCheckin = 0; + taginfo->pendingIdle = 0; taginfo->filename = String(); taginfo->dataType = dataType; memset(taginfo->md5pending, 0, 16 * sizeof(uint8_t)); @@ -114,7 +113,7 @@ void prepareDataAvail(uint8_t* data, uint16_t len, uint8_t dataType, const uint8 wsSendTaginfo(dst, SYNC_TAGSTATUS); } -bool prepareDataAvail(String& filename, uint8_t dataType, const uint8_t* dst, uint16_t nextCheckin) { +bool prepareDataAvail(String& filename, uint8_t dataType, const uint8_t* dst, uint16_t nextCheckin, bool resend) { if (nextCheckin > config.maxsleep) nextCheckin = config.maxsleep; if (wsClientCount() && config.stopsleep == 1) nextCheckin = 0; #ifdef YELLOW_IPS_AP @@ -164,7 +163,7 @@ bool prepareDataAvail(String& filename, uint8_t dataType, const uint8_t* dst, ui if (memcmp(md5bytes, taginfo->md5pending, 16) == 0) { wsLog("new image is the same as current or already pending image. not updating tag."); wsSendTaginfo(dst, SYNC_TAGSTATUS); - if (contentFS->exists(filename)) { + if (contentFS->exists(filename) && resend == false) { contentFS->remove(filename); } return true; @@ -188,13 +187,15 @@ bool prepareDataAvail(String& filename, uint8_t dataType, const uint8_t* dst, ui if (contentFS->exists(dst_path)) { contentFS->remove(dst_path); } - contentFS->rename(filename, dst_path); - filename = String(dst_path); + if (resend == false) { + contentFS->rename(filename, dst_path); + filename = String(dst_path); + wsLog("new image: " + String(dst_path)); + } - wsLog("new image: " + String(dst_path)); time_t now; time(&now); - taginfo->expectedNextCheckin = now + nextCheckin * 60 + 60; + taginfo->pendingIdle = now + nextCheckin * 60 + 60; clearPending(taginfo); taginfo->filename = filename; taginfo->len = filesize; @@ -255,6 +256,16 @@ void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP) { File file = contentFS->open(filename, "w"); http.writeToStream(&file); file.close(); + } else if (httpCode == 404) { + imageUrl = "http://" + remoteIP.toString() + "/current/" + String(hexmac) + ".raw"; + http.end(); + http.begin(imageUrl); + httpCode = http.GET(); + if (httpCode == 200) { + File file = contentFS->open(filename, "w"); + http.writeToStream(&file); + file.close(); + } } http.end(); @@ -424,7 +435,7 @@ void processXferTimeout(struct espXferComplete* xfc, bool local) { time(&now); tagRecord* taginfo = tagRecord::findByMAC(xfc->src); if (taginfo != nullptr) { - taginfo->expectedNextCheckin = now + 60; + taginfo->pendingIdle = now + 60; memset(taginfo->md5pending, 0, 16 * sizeof(uint8_t)); clearPending(taginfo); } diff --git a/ESP32_AP-Flasher/src/tag_db.cpp b/ESP32_AP-Flasher/src/tag_db.cpp index dc3007d0..a08309b6 100644 --- a/ESP32_AP-Flasher/src/tag_db.cpp +++ b/ESP32_AP-Flasher/src/tag_db.cpp @@ -300,6 +300,8 @@ void initAPconfig() { config.maxsleep = APconfig["maxsleep"] | 10; config.stopsleep = APconfig["stopsleep"] | 1; config.preview = APconfig["preview"] | 1; + config.sleepTime1 = APconfig["sleeptime1"] | 0; + config.sleepTime2 = APconfig["sleeptime2"] | 0; // default wifi power 8.5 dbM // see https://github.com/espressif/arduino-esp32/blob/master/libraries/WiFi/src/WiFiGeneric.h#L111 config.wifiPower = APconfig["wifipower"] | 34; @@ -322,6 +324,8 @@ void saveAPconfig() { APconfig["preview"] = config.preview; APconfig["wifipower"] = config.wifiPower; APconfig["timezone"] = config.timeZone; + APconfig["sleeptime1"] = config.sleepTime1; + APconfig["sleeptime2"] = config.sleepTime2; serializeJsonPretty(APconfig, configFile); configFile.close(); } diff --git a/ESP32_AP-Flasher/src/web.cpp b/ESP32_AP-Flasher/src/web.cpp index c03c14b1..ae2b85cf 100644 --- a/ESP32_AP-Flasher/src/web.cpp +++ b/ESP32_AP-Flasher/src/web.cpp @@ -76,11 +76,15 @@ void wsSendSysteminfo() { JsonObject sys = doc.createNestedObject("sys"); time_t now; time(&now); + static int freeSpaceLastRun = 0; sys["currtime"] = now; sys["heap"] = ESP.getFreeHeap(); sys["recordcount"] = tagDB.size(); sys["dbsize"] = dbSize(); - sys["littlefsfree"] = Storage.freeSpace(); + if (millis() - freeSpaceLastRun > 30000) { + sys["littlefsfree"] = Storage.freeSpace(); + freeSpaceLastRun = millis(); + } sys["apstate"] = apInfo.state; sys["runstate"] = config.runStatus; #if !defined(CONFIG_IDF_TARGET_ESP32) @@ -90,18 +94,23 @@ void wsSendSysteminfo() { sys["wifistatus"] = WiFi.status(); sys["wifissid"] = WiFi.SSID(); - uint32_t timeoutcount = 0; - uint32_t tagcount = getTagCount(timeoutcount); - char result[40]; - if (timeoutcount > 0) { - snprintf(result, sizeof(result), "%lu / %lu, %lu timed out", tagcount, tagDB.size(), timeoutcount); - } else { - snprintf(result, sizeof(result), "%lu / %lu", tagcount, tagDB.size()); - } - setVarDB("ap_tagcount", result); setVarDB("ap_ip", WiFi.localIP().toString()); setVarDB("ap_ch", String(apInfo.channel)); + static uint32_t tagcounttimer = 0; + if (millis() - tagcounttimer > 60000) { + uint32_t timeoutcount = 0; + uint32_t tagcount = getTagCount(timeoutcount); + char result[40]; + if (timeoutcount > 0) { + snprintf(result, sizeof(result), "%lu / %lu, %lu timed out", tagcount, tagDB.size(), timeoutcount); + } else { + snprintf(result, sizeof(result), "%lu / %lu", tagcount, tagDB.size()); + } + setVarDB("ap_tagcount", result); + tagcounttimer = millis(); + } + xSemaphoreTake(wsMutex, portMAX_DELAY); ws.textAll(doc.as()); xSemaphoreGive(wsMutex); @@ -357,6 +366,10 @@ void init_web() { if (request->hasParam("preview", true)) { config.preview = static_cast(request->getParam("preview", true)->value().toInt()); } + if (request->hasParam("sleeptime1", true)) { + config.sleepTime1 = static_cast(request->getParam("sleeptime1", true)->value().toInt()); + config.sleepTime2 = static_cast(request->getParam("sleeptime2", true)->value().toInt()); + } if (request->hasParam("wifipower", true)) { config.wifiPower = static_cast(request->getParam("wifipower", true)->value().toInt()); WiFi.setTxPower(static_cast(config.wifiPower)); diff --git a/ESP32_AP-Flasher/wwwroot/index.html b/ESP32_AP-Flasher/wwwroot/index.html index 817771c2..3438374d 100644 --- a/ESP32_AP-Flasher/wwwroot/index.html +++ b/ESP32_AP-Flasher/wwwroot/index.html @@ -117,6 +117,12 @@ Latency will be around 40 seconds.">

+

+ + + and + +

diff --git a/ESP32_AP-Flasher/wwwroot/main.js b/ESP32_AP-Flasher/wwwroot/main.js index 74e35d44..7d49c84e 100644 --- a/ESP32_AP-Flasher/wwwroot/main.js +++ b/ESP32_AP-Flasher/wwwroot/main.js @@ -32,6 +32,9 @@ let servertimediff = 0; let paintLoaded = false, paintShow = false; let cardconfig; let otamodule; +let socket; +let finishedInitialLoading = false; +let getTagtypeBusy = false; window.addEventListener("load", function () { fetch('/content_cards.json') @@ -55,16 +58,17 @@ window.addEventListener("load", function () { } }); dropUpload(); + populateTimes($('#apcnight1')); + populateTimes($('#apcnight2')); }); -let socket; - 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); + finishedInitialLoading = true; }) //.catch(error => showMessage('loadTags error: ' + error)); } @@ -489,6 +493,8 @@ $('#apconfigbutton').onclick = function () { $("#apcpreview").value = data.preview; $("#apcwifipower").value = data.wifipower; $("#apctimezone").value = data.timezone; + $("#apcnight1").value = data.sleeptime1; + $("#apcnight2").value = data.sleeptime2; }) $('#apconfigbox').style.display = 'block' } @@ -504,6 +510,8 @@ $('#apcfgsave').onclick = function () { formData.append('preview', $('#apcpreview').value); formData.append('wifipower', $('#apcwifipower').value); formData.append('timezone', $('#apctimezone').value); + formData.append('sleeptime1', $('#apcnight1').value); + formData.append('sleeptime2', $('#apcnight2').value); fetch("/save_apcfg", { method: "POST", body: formData @@ -706,11 +714,15 @@ function processQueue() { return; } isProcessing = true; + if (!finishedInitialLoading) { + setTimeout(processQueue, 100); + return; + } const { id, imageSrc } = imageQueue.shift(); const hwtype = $('#tag' + id).dataset.hwtype; if (tagTypes[hwtype]?.busy) { imageQueue.push({ id, imageSrc }); - setTimeout(processQueue, 50); + setTimeout(processQueue, 100); return; }; @@ -869,6 +881,21 @@ $('#toggleFilters').addEventListener('click', () => { }); async function getTagtype(hwtype) { + if (tagTypes[hwtype]) { + return tagTypes[hwtype]; + } + + if (getTagtypeBusy) { + await new Promise(resolve => { + const checkBusy = setInterval(() => { + if (!getTagtypeBusy) { + clearInterval(checkBusy); + resolve(); + } + }, 50); + }); + } + if (tagTypes[hwtype]?.busy) { await new Promise(resolve => { const checkBusy = setInterval(() => { @@ -886,10 +913,12 @@ async function getTagtype(hwtype) { try { tagTypes[hwtype] = { busy: true }; + getTagtypeBusy = true; const response = await fetch('/tagtypes/' + hwtype.toString(16).padStart(2, '0').toUpperCase() + '.json'); if (!response.ok) { let data = { name: 'unknown id ' + hwtype, width: 0, height: 0, bpp: 0, rotatebuffer: 0, colortable: [], busy: false }; tagTypes[hwtype] = data; + getTagtypeBusy = false; return data; } const jsonData = await response.json(); @@ -904,10 +933,12 @@ async function getTagtype(hwtype) { busy: false }; tagTypes[hwtype] = data; + getTagtypeBusy = false; return data; } catch (error) { console.error('Error fetching data:', error); + getTagtypeBusy = false; return null; } } @@ -1074,3 +1105,12 @@ $('#taglist').addEventListener('contextmenu', (e) => { document.addEventListener('click', () => { contextMenu.style.display = 'none'; }); + +function populateTimes(element) { + for (let i = 0; i < 24; i++) { + const option = document.createElement("option"); + option.value = i; + option.text = i.toString().padStart(2, "0") + ":00"; + element.appendChild(option); + } +} \ No newline at end of file