mirror of
https://github.com/OpenEPaperLink/OpenEPaperLink.git
synced 2026-03-21 12:05:51 +01:00
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:
@@ -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);
|
||||
|
||||
@@ -35,5 +35,8 @@ class DynStorage {
|
||||
|
||||
extern DynStorage Storage;
|
||||
extern fs::FS *contentFS;
|
||||
extern void copyFile(File in, File out);
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#endif
|
||||
@@ -66,6 +66,8 @@ struct Config {
|
||||
uint8_t preview;
|
||||
uint8_t wifiPower;
|
||||
char timeZone[52];
|
||||
uint8_t sleepTime1;
|
||||
uint8_t sleepTime2;
|
||||
};
|
||||
|
||||
struct HwType {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user