diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 84807fba..7609629f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -26,7 +26,7 @@ jobs: - name: Build NRF firmware run: | cd ARM_Tag_FW/Newton_M3_nRF52811 - pio run --environment Newton_M3_29_BWR + pio run --environment Newton_M3_Universal - name: Build Simple_AP run: | diff --git a/.gitignore b/.gitignore index 1603dcba..3ba084fe 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ *.o sdcc/sdcc +ESP32_AP-Flasher/.vscode/extensions.json diff --git a/ESP32_AP-Flasher/data/fonts/bahnschrift20.vlw b/ESP32_AP-Flasher/data/fonts/bahnschrift20.vlw index 17e3f44d..b79a7400 100644 Binary files a/ESP32_AP-Flasher/data/fonts/bahnschrift20.vlw and b/ESP32_AP-Flasher/data/fonts/bahnschrift20.vlw differ diff --git a/ESP32_AP-Flasher/data/fonts/bahnschrift30.vlw b/ESP32_AP-Flasher/data/fonts/bahnschrift30.vlw index 3089c796..e1282067 100644 Binary files a/ESP32_AP-Flasher/data/fonts/bahnschrift30.vlw and b/ESP32_AP-Flasher/data/fonts/bahnschrift30.vlw differ diff --git a/ESP32_AP-Flasher/data/fonts/calibrib30.vlw b/ESP32_AP-Flasher/data/fonts/calibrib30.vlw index 4062ba89..0a9d29c0 100644 Binary files a/ESP32_AP-Flasher/data/fonts/calibrib30.vlw and b/ESP32_AP-Flasher/data/fonts/calibrib30.vlw differ diff --git a/ESP32_AP-Flasher/data/fonts/calibrib50.vlw b/ESP32_AP-Flasher/data/fonts/calibrib50.vlw index ff5063fb..312ac2e7 100644 Binary files a/ESP32_AP-Flasher/data/fonts/calibrib50.vlw and b/ESP32_AP-Flasher/data/fonts/calibrib50.vlw differ diff --git a/ESP32_AP-Flasher/data/fonts/calibrib60.vlw b/ESP32_AP-Flasher/data/fonts/calibrib60.vlw index ca432a92..86d55959 100644 Binary files a/ESP32_AP-Flasher/data/fonts/calibrib60.vlw and b/ESP32_AP-Flasher/data/fonts/calibrib60.vlw differ diff --git a/ESP32_AP-Flasher/data/fonts/twcondensed20.vlw b/ESP32_AP-Flasher/data/fonts/twcondensed20.vlw index 6fe23703..d9fa24e0 100644 Binary files a/ESP32_AP-Flasher/data/fonts/twcondensed20.vlw and b/ESP32_AP-Flasher/data/fonts/twcondensed20.vlw differ diff --git a/ESP32_AP-Flasher/data/www/index.html.gz b/ESP32_AP-Flasher/data/www/index.html.gz index e76fb1fa..b2c3b440 100644 Binary files a/ESP32_AP-Flasher/data/www/index.html.gz and b/ESP32_AP-Flasher/data/www/index.html.gz differ diff --git a/ESP32_AP-Flasher/data/www/main.js.gz b/ESP32_AP-Flasher/data/www/main.js.gz index 5f56f52c..47a53f8c 100644 Binary files a/ESP32_AP-Flasher/data/www/main.js.gz and b/ESP32_AP-Flasher/data/www/main.js.gz differ diff --git a/ESP32_AP-Flasher/include/contentmanager.h b/ESP32_AP-Flasher/include/contentmanager.h index e9c9b79a..2db3eaf8 100644 --- a/ESP32_AP-Flasher/include/contentmanager.h +++ b/ESP32_AP-Flasher/include/contentmanager.h @@ -35,7 +35,8 @@ bool getJsonTemplateFile(String &filename, String jsonfile, tagRecord *&taginfo, extern bool getJsonTemplateFileExtractVariables(String &filename, String jsonfile, JsonDocument &variables, tagRecord *&taginfo, imgParam &imageParams); int getJsonTemplateUrl(String &filename, String URL, time_t fetched, String MAC, tagRecord *&taginfo, imgParam &imageParams); void drawJsonStream(Stream &stream, String &filename, tagRecord *&taginfo, imgParam &imageParams); -void drawElement(const JsonObject &element, TFT_eSprite &spr); +void rotateBuffer(uint8_t rotation, uint8_t ¤tOrientation, TFT_eSprite &spr, imgParam &imageParams); +void drawElement(const JsonObject &element, TFT_eSprite &spr, imgParam &imageParams, uint8_t ¤tOrientation); uint16_t getColor(const String &color); char *formatHttpDate(const time_t t); String urlEncode(const char *msg); diff --git a/ESP32_AP-Flasher/include/language.h b/ESP32_AP-Flasher/include/language.h index a30a1ebd..68d3b0f0 100644 --- a/ESP32_AP-Flasher/include/language.h +++ b/ESP32_AP-Flasher/include/language.h @@ -4,8 +4,6 @@ extern int defaultLanguage; -extern String languageList[]; - /*EN English language section*/ extern String languageEnDaysShort[]; extern String languageEnDays[]; diff --git a/ESP32_AP-Flasher/include/tag_db.h b/ESP32_AP-Flasher/include/tag_db.h index 4cbd85e1..47615a8a 100644 --- a/ESP32_AP-Flasher/include/tag_db.h +++ b/ESP32_AP-Flasher/include/tag_db.h @@ -69,6 +69,7 @@ struct Config { uint8_t stopsleep; uint8_t runStatus; uint8_t preview; + uint8_t lock; uint8_t wifiPower; char timeZone[52]; uint8_t sleepTime1; diff --git a/ESP32_AP-Flasher/src/contentmanager.cpp b/ESP32_AP-Flasher/src/contentmanager.cpp index 2fa9e311..82e79115 100644 --- a/ESP32_AP-Flasher/src/contentmanager.cpp +++ b/ESP32_AP-Flasher/src/contentmanager.cpp @@ -1173,12 +1173,13 @@ void drawAPinfo(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa TFT_eSprite spr = TFT_eSprite(&tft); DynamicJsonDocument loc(2048); + uint8_t screenCurrentOrientation = 0; getTemplate(loc, 21, taginfo->hwType); initSprite(spr, imageParams.width, imageParams.height, imageParams); const JsonArray jsonArray = loc.as(); for (const JsonVariant &elem : jsonArray) { - drawElement(elem, spr); + drawElement(elem, spr, imageParams, screenCurrentOrientation); } spr2buffer(spr, filename, imageParams); @@ -1371,8 +1372,11 @@ int getJsonTemplateUrl(String &filename, String URL, time_t fetched, String MAC, } void drawJsonStream(Stream &stream, String &filename, tagRecord *&taginfo, imgParam &imageParams) { - TFT_eSprite spr = TFT_eSprite(&tft); +TFT_eSprite spr = TFT_eSprite(&tft); initSprite(spr, imageParams.width, imageParams.height, imageParams); + uint8_t screenCurrentOrientation = 0; + //spr.setRotation(2); + //imageParams.rotatebuffer = imageParams.rotatebuffer + 1; DynamicJsonDocument doc(500); if (stream.find("[")) { do { @@ -1381,7 +1385,7 @@ void drawJsonStream(Stream &stream, String &filename, tagRecord *&taginfo, imgPa wsErr("json error " + String(error.c_str())); break; } else { - drawElement(doc.as(), spr); + drawElement(doc.as(), spr, imageParams, screenCurrentOrientation); doc.clear(); } } while (stream.findUntil(",", "]")); @@ -1391,7 +1395,44 @@ void drawJsonStream(Stream &stream, String &filename, tagRecord *&taginfo, imgPa spr.deleteSprite(); } -void drawElement(const JsonObject &element, TFT_eSprite &spr) { +void rotateBuffer(uint8_t rotation, uint8_t ¤tOrientation, TFT_eSprite &spr, imgParam &imageParams){ + rotation = rotation % 4; //First of all, let's be sure that the rotation have a valid value (0, 1, 2 or 3) + if(rotation != currentOrientation){ //If we have a rotation to do, let's do it + int stepToDo = currentOrientation - rotation; //rotation we have to do + //-2, 2: upside down + //-1, 3: 270° rotation + //-3, 1: 90° rotation + + if(abs(stepToDo) == 2){ //If we have to do a 180° rotation: + TFT_eSprite sprCpy = TFT_eSprite(&tft); //We create a new sprite that will act as a buffer + initSprite(sprCpy, spr.width(), spr.height(), imageParams); //initialisation of the new sprite + spr.pushRotated(&sprCpy, 180, TFT_WHITE); //We fill the new sprite with the old one rotated by 180° + spr.fillSprite(TFT_WHITE); //We fill the old one in white as anything that's white will be ignored by the pushRotated function + sprCpy.pushRotated(&spr, 0, TFT_WHITE); //We copy the buffer sprite to the main one + sprCpy.deleteSprite(); //We delete the buffer sprite to avoid memory leak + }else{ + int angle = 90; + if(stepToDo == -1 || stepToDo == 3){ + angle = 270; + } + TFT_eSprite sprCpy = TFT_eSprite(&tft); + initSprite(sprCpy, spr.height(), spr.width(), imageParams); + spr.pushRotated(&sprCpy, angle, TFT_WHITE); + spr.deleteSprite(); + initSprite(spr, sprCpy.width(), sprCpy.height(), imageParams); + sprCpy.pushRotated(&spr, 0, TFT_WHITE); + sprCpy.deleteSprite(); + if(imageParams.rotatebuffer==1){ + imageParams.rotatebuffer = 0; + }else{ + imageParams.rotatebuffer = 1; + } + } + currentOrientation = rotation; + } +} + +void drawElement(const JsonObject &element, TFT_eSprite &spr, imgParam &imageParams, uint8_t ¤tOrientation) { if (element.containsKey("text")) { const JsonArray &textArray = element["text"]; const uint16_t align = textArray[5] | 0; @@ -1414,6 +1455,9 @@ void drawElement(const JsonObject &element, TFT_eSprite &spr) { } else if (element.containsKey("circle")) { const JsonArray &circleArray = element["circle"]; spr.fillCircle(circleArray[0].as(), circleArray[1].as(), circleArray[2].as(), getColor(circleArray[3])); + } else if (element.containsKey("rotate")) { + uint8_t rotation = element["rotate"].as(); + rotateBuffer(rotation, currentOrientation, spr, imageParams); } } diff --git a/ESP32_AP-Flasher/src/language.cpp b/ESP32_AP-Flasher/src/language.cpp index 61eabf54..2e40fc95 100644 --- a/ESP32_AP-Flasher/src/language.cpp +++ b/ESP32_AP-Flasher/src/language.cpp @@ -7,8 +7,6 @@ int defaultLanguage = 0; -String languageList[] = {"EN - English", "NL - Nederlands", "DE - Deutsch", "NO - Norwegian", "FR - French"}; - /*EN English language section*/ String languageEnDaysShort[] = {"SU", "MO", "TU", "WE", "TH", "FR", "SA"}; String languageEnDays[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; @@ -33,21 +31,45 @@ String languageNoDays[] = {"Søndag", "Mandag", "Tirsdag", "Onsdag", "Torsdag", String languageNoMonth[] = {"Januar", "Februar", "Mars", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Desember"}; /*END Norwegian language section END*/ +/*CZ Czech language section*/ +String languageCzDaysShort[] = {"NE", "PO", "ÚT", "ST", "ČT", "PÁ", "SO"}; +String languageCzDays[] = {"Neděle", "Pondělí", "Úterý", "Středa", "Čtvrtek", "Pátek", "Sobota"}; +String languageCzMonth[] = {"Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec"}; +/*END Czech language section END*/ + +/*SK Slovak language section*/ +String languageSkDaysShort[] = {"NE", "PO", "UT", "ST", "ŠT", "PI", "SO"}; +String languageSkDays[] = {"Nedeľa", "Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota"}; +String languageSkMonth[] = {"Január", "Február", "Marec", "Apríl", "Máj", "Jún", "Júl", "August", "September", "Oktober", "November", "December"}; +/*END Slovak language section END*/ + +/*PL Polish language section*/ +String languagePlDaysShort[] = {"Ni", "Po", "Wt", "Śr", "Cz", "Pt", "So"}; +String languagePlDays[] = {"Niedziela", "Poniedziałek", "Wtorek", "Środa", "Czwartek", "Piątek", "Sobota"}; +String languagePlMonth[] = {"Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec", "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień"}; +/*END Polish language section END*/ + +/*ES Spanish language section*/ +String languageEsDaysShort[] = {"D", "L", "MA", "MI", "J", "V", "S"}; +String languageEsDays[] = {"Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"}; +String languageEsMonth[] = {"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"}; +/*END Spanish language section END*/ + /*FR French language section*/ String languageFrDaysShort[] = {"DI", "LU", "MA", "ME", "JE", "VE", "SA"}; String languageFrDays[] = {"Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"}; String languageFrMonth[] = {"Janvier", "Fevrier", "Mars", "Avril", "Mai", "Juin", "Juillet", "Aout", "Septembre", "Octobre", "Novembre", "Decembre"}; /*END French language section END*/ -String* languageDaysShort[] = {languageEnDaysShort, languageNlDaysShort, languageDeDaysShort, languageNoDaysShort, languageFrDaysShort}; -String* languageDays[] = {languageEnDays, languageNlDays, languageDeDays, languageNoDays, languageFrDays}; -String* languageMonth[] = {languageEnMonth, languageNlMonth, languageDeMonth, languageNoMonth, languageFrMonth}; +String* languageDaysShort[] = {languageEnDaysShort, languageNlDaysShort, languageDeDaysShort, languageNoDaysShort, languageFrDaysShort, languageCzDaysShort, languageSkDaysShort, languagePlDaysShort, languageEsDaysShort}; +String* languageDays[] = {languageEnDays, languageNlDays, languageDeDays, languageNoDays, languageFrDays, languageCzDays, languageSkDays, languagePlDays, languageEsDays}; +String* languageMonth[] = {languageEnMonth, languageNlMonth, languageDeMonth, languageNoMonth, languageFrMonth, languageCzMonth, languageSkMonth, languagePlMonth, languageEsMonth}; int currentLanguage = defaultLanguage; void updateLanguageFromConfig() { int tempLang = config.language; - if (tempLang < 0 || tempLang >= sizeof(languageList)) { + if (tempLang < 0 || tempLang > 8) { Serial.println("Language not supported"); return; } diff --git a/ESP32_AP-Flasher/src/newproto.cpp b/ESP32_AP-Flasher/src/newproto.cpp index ca1d6823..ec28d665 100644 --- a/ESP32_AP-Flasher/src/newproto.cpp +++ b/ESP32_AP-Flasher/src/newproto.cpp @@ -56,6 +56,7 @@ void prepareCancelPending(const uint8_t dst[8]) { tagRecord* taginfo = tagRecord::findByMAC(dst); if (taginfo == nullptr) { + if (config.lock) return; wsErr("Tag not found, this shouldn't happen."); return; } @@ -80,6 +81,7 @@ void prepareIdleReq(const uint8_t* dst, uint16_t nextCheckin) { void prepareDataAvail(uint8_t* data, uint16_t len, uint8_t dataType, const uint8_t* dst) { tagRecord* taginfo = tagRecord::findByMAC(dst); if (taginfo == nullptr) { + if (config.lock) return; wsErr("Tag not found, this shouldn't happen."); return; } @@ -129,6 +131,7 @@ bool prepareDataAvail(String& filename, uint8_t dataType, uint8_t dataTypeArgume tagRecord* taginfo = tagRecord::findByMAC(dst); if (taginfo == nullptr) { + if (config.lock) return true; wsErr("Tag not found, this shouldn't happen."); return true; } @@ -345,6 +348,7 @@ void processBlockRequest(struct espBlockRequest* br) { tagRecord* taginfo = tagRecord::findByMAC(br->src); if (taginfo == nullptr) { + if (config.lock) return; prepareCancelPending(br->src); Serial.printf("blockrequest: couldn't find taginfo %02X%02X%02X%02X%02X%02X%02X%02X\n", br->src[7], br->src[6], br->src[5], br->src[4], br->src[3], br->src[2], br->src[1], br->src[0]); return; @@ -458,6 +462,7 @@ void processDataReq(struct espAvailDataReq* eadr, bool local, IPAddress remoteIP tagRecord* taginfo = tagRecord::findByMAC(eadr->src); if (taginfo == nullptr) { + if (config.lock) return; taginfo = new tagRecord; memcpy(taginfo->mac, eadr->src, sizeof(taginfo->mac)); taginfo->pending = false; @@ -657,6 +662,7 @@ void updateTaginfoitem(struct TagInfo* taginfoitem, IPAddress remoteIP) { tagRecord* taginfo = tagRecord::findByMAC(taginfoitem->mac); if (taginfo == nullptr) { + if (config.lock) return; taginfo = new tagRecord; memcpy(taginfo->mac, taginfoitem->mac, sizeof(taginfo->mac)); taginfo->pending = false; diff --git a/ESP32_AP-Flasher/src/tag_db.cpp b/ESP32_AP-Flasher/src/tag_db.cpp index 553c2b47..b9ed81bb 100644 --- a/ESP32_AP-Flasher/src/tag_db.cpp +++ b/ESP32_AP-Flasher/src/tag_db.cpp @@ -130,9 +130,17 @@ void saveDB(const String& filename) { Storage.begin(); xSemaphoreTake(fsMutex, portMAX_DELAY); + + fs::File existingFile = contentFS->open(filename, "r"); + if (existingFile) { + existingFile.close(); + String backupFilename = filename + ".bak"; + contentFS->rename(filename.c_str(), backupFilename.c_str()); + } + fs::File file = contentFS->open(filename, "w"); if (!file) { - Serial.println("saveDB: Failed to open file"); + Serial.println("saveDB: Failed to open file for writing"); xSemaphoreGive(fsMutex); return; } @@ -307,6 +315,7 @@ void initAPconfig() { config.maxsleep = APconfig["maxsleep"] | 10; config.stopsleep = APconfig["stopsleep"] | 1; config.preview = APconfig["preview"] | 1; + config.lock = APconfig["lock"] | 0; config.sleepTime1 = APconfig["sleeptime1"] | 0; config.sleepTime2 = APconfig["sleeptime2"] | 0; // default wifi power 8.5 dbM @@ -333,6 +342,7 @@ void saveAPconfig() { APconfig["maxsleep"] = config.maxsleep; APconfig["stopsleep"] = config.stopsleep; APconfig["preview"] = config.preview; + APconfig["lock"] = config.lock; APconfig["wifipower"] = config.wifiPower; APconfig["timezone"] = config.timeZone; APconfig["sleeptime1"] = config.sleepTime1; diff --git a/ESP32_AP-Flasher/src/web.cpp b/ESP32_AP-Flasher/src/web.cpp index cf0d3d2b..fd0fef67 100644 --- a/ESP32_AP-Flasher/src/web.cpp +++ b/ESP32_AP-Flasher/src/web.cpp @@ -471,6 +471,9 @@ void init_web() { if (request->hasParam("preview", true)) { config.preview = static_cast(request->getParam("preview", true)->value().toInt()); } + if (request->hasParam("lock", true)) { + config.lock = static_cast(request->getParam("lock", 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()); diff --git a/ESP32_AP-Flasher/wwwroot/index.html b/ESP32_AP-Flasher/wwwroot/index.html index 88ea06d1..c83b115e 100644 --- a/ESP32_AP-Flasher/wwwroot/index.html +++ b/ESP32_AP-Flasher/wwwroot/index.html @@ -21,14 +21,11 @@
- + - - + +
@@ -116,68 +113,55 @@

Currently active tags

-
+
group by
- +
- +
- +
- +
sort by
- +
- +
- +
filter
- +
- +
- +
- +
- +
@@ -193,8 +177,7 @@
-
- ↻
+
@@ -206,8 +189,7 @@
-
+
@@ -287,8 +269,12 @@ - - + + + + + +

+ title="Stops updates at night, and put the tags to sleep. + During the configured night time, this overrides the maximum sleep time."> and @@ -327,6 +314,17 @@

+

+ + +

download latest version

+
+ + download latest version +

@@ -525,4 +522,4 @@ - + \ No newline at end of file diff --git a/ESP32_AP-Flasher/wwwroot/main.js b/ESP32_AP-Flasher/wwwroot/main.js index 71bb6f40..03dcdd35 100644 --- a/ESP32_AP-Flasher/wwwroot/main.js +++ b/ESP32_AP-Flasher/wwwroot/main.js @@ -590,6 +590,7 @@ document.addEventListener("loadTab", function (event) { $("#apclatency").value = data.maxsleep; $("#apcpreventsleep").value = data.stopsleep; $("#apcpreview").value = data.preview; + $("#apclock").value = data.lock; $("#apcwifipower").value = data.wifipower; $("#apctimezone").value = data.timezone; $("#apcnight1").value = data.sleeptime1; @@ -614,6 +615,7 @@ $('#apcfgsave').onclick = function () { formData.append('maxsleep', $('#apclatency').value); formData.append('stopsleep', $('#apcpreventsleep').value); formData.append('preview', $('#apcpreview').value); + formData.append('lock', $('#apclock').value); formData.append('wifipower', $('#apcwifipower').value); formData.append('timezone', $('#apctimezone').value); formData.append('sleeptime1', $('#apcnight1').value); @@ -858,7 +860,7 @@ function processQueue() { return; } const { id, imageSrc } = imageQueue.shift(); - const hwtype = $('#tag' + id).dataset.hwtype; + const hwtype = $('#tag' + id).dataset?.hwtype; if (tagTypes[hwtype]?.busy) { imageQueue.push({ id, imageSrc }); setTimeout(processQueue, 100); @@ -1076,7 +1078,7 @@ async function getTagtype(hwtype) { tagTypes[hwtype] = { busy: 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 }; + let data = { name: 'unknown id ' + hwtype.toString(16), width: 0, height: 0, bpp: 0, rotatebuffer: 0, colortable: [], busy: false }; tagTypes[hwtype] = data; getTagtypeBusy = false; return data; @@ -1231,22 +1233,25 @@ $('#taglist').addEventListener('contextmenu', (e) => { if (clickedGridItem) { let mac = clickedGridItem.dataset.mac; const hwtype = clickedGridItem.dataset.hwtype; - let contextMenuOptions = [ - { id: 'refresh', label: 'Force refresh' }, - { id: 'clear', label: 'Clear pending status' } - ]; - if (clickedGridItem.dataset.isexternal == "false") { + let contextMenuOptions = []; + if (tagTypes[hwtype]?.width > 0) { contextMenuOptions.push( - { id: 'scan', label: 'Scan channels' }, - { id: 'reboot', label: 'Reboot tag' }, - ); - }; - if (tagTypes[hwtype].options?.includes("led")) { - contextMenuOptions.push( - { id: 'ledflash', label: 'Flash the LED' }, - { id: 'ledflash_long', label: 'Flash the LED (long)' }, - { id: 'ledflash_stop', label: 'Stop flashing' } + { id: 'refresh', label: 'Force refresh' }, + { id: 'clear', label: 'Clear pending status' } ); + if (clickedGridItem.dataset.isexternal == "false") { + contextMenuOptions.push( + { id: 'scan', label: 'Scan channels' }, + { id: 'reboot', label: 'Reboot tag' }, + ); + }; + if (tagTypes[hwtype]?.options?.includes("led")) { + contextMenuOptions.push( + { id: 'ledflash', label: 'Flash the LED' }, + { id: 'ledflash_long', label: 'Flash the LED (long)' }, + { id: 'ledflash_stop', label: 'Stop flashing' } + ); + } } contextMenuOptions.push( { id: 'del', label: 'Delete tag from list' }