various fixes / sleep at night / resend image at tag boot

- apconfig: configure night time, where the system is not updating tags, and the tags are sleeping
- fixed bug calculating expected checkin
- throttle down apinfo update to once per minute
- fixed too many concurrent requests getting tagtypes
- resend current image when a tag reboots and the content type is 'static image' (all other content types were already regenerating the content)
- fixed timing of main loop
This commit is contained in:
Nic Limper
2023-08-20 20:16:34 +02:00
parent 46fb23b70f
commit 2edbf27033
11 changed files with 167 additions and 39 deletions

View File

@@ -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);

View File

@@ -35,5 +35,8 @@ class DynStorage {
extern DynStorage Storage;
extern fs::FS *contentFS;
extern void copyFile(File in, File out);
#endif
#endif

View File

@@ -66,6 +66,8 @@ struct Config {
uint8_t preview;
uint8_t wifiPower;
char timeZone[52];
uint8_t sleepTime1;
uint8_t sleepTime2;
};
struct HwType {

View File

@@ -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

View File

@@ -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<String>();
if (!util::isEmptyOrNull(configFilename) && !cfgobj["#fetched"].as<bool>()) {
imageParams.dither = cfgobj["dither"] && cfgobj["dither"] == "1";
jpg2buffer(configFilename, filename, imageParams);
String configFilename = cfgobj["filename"].as<String>();
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<int>(), 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<int>())) {
cfgobj["#fetched"] = true;
if (cfgobj["delete"].as<String>() == "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;

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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<String>());
xSemaphoreGive(wsMutex);
@@ -357,6 +366,10 @@ void init_web() {
if (request->hasParam("preview", true)) {
config.preview = static_cast<uint8_t>(request->getParam("preview", true)->value().toInt());
}
if (request->hasParam("sleeptime1", true)) {
config.sleepTime1 = static_cast<uint8_t>(request->getParam("sleeptime1", true)->value().toInt());
config.sleepTime2 = static_cast<uint8_t>(request->getParam("sleeptime2", true)->value().toInt());
}
if (request->hasParam("wifipower", true)) {
config.wifiPower = static_cast<uint8_t>(request->getParam("wifipower", true)->value().toInt());
WiFi.setTxPower(static_cast<wifi_power_t>(config.wifiPower));

View File

@@ -117,6 +117,12 @@ Latency will be around 40 seconds.">
<option value="1" selected>yes</option>
</select>
</p>
<p title="Don't update at night. Overrides the maximum sleep time: at night, tags are put to sleep.">
<label for="apcnight1">No updates between</label>
<select id="apcnight1"></select>
<span style="align-self:center;">and</span>
<select id="apcnight2"></select>
</p>
<p title="Turn off preview images on the webpage if you want to manage many tags,
to save file system space">
<label for="apcpreview">Preview images</label>

View File

@@ -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);
}
}