diff --git a/ESP32_AP-Flasher/data/www/index.html b/ESP32_AP-Flasher/data/www/index.html index a18eb81f..cf05b572 100644 --- a/ESP32_AP-Flasher/data/www/index.html +++ b/ESP32_AP-Flasher/data/www/index.html @@ -29,10 +29,30 @@

-

- - +

+

Advanced options

+

+ + +

+

+ + +

+

+ + + +

+
+

+ + 🞃

@@ -159,8 +179,8 @@ Latency will be around 40 seconds.">
-
-
+
+
diff --git a/ESP32_AP-Flasher/data/www/main.css b/ESP32_AP-Flasher/data/www/main.css index 5d0f1c12..54bd0f45 100644 --- a/ESP32_AP-Flasher/data/www/main.css +++ b/ESP32_AP-Flasher/data/www/main.css @@ -142,6 +142,33 @@ select { width: 80px; } +#advancedoptions { + overflow: hidden; + transition: height 0.3s ease; +} + +#advancedoptions p:first-child { + font-weight: 700; + font-size: 1.2em; +} + +#savebar { + display: flex; + align-items: flex-end; + justify-content: space-between; +} + +#savebar:first-child { + flex-grow: 2; +} + +#cfgmore { + padding: 2px 5px; + font-weight: 700; + font-size: 1.2em; + cursor: pointer; +} + #apconfigbox { background-color: #e6f0d3; } @@ -179,10 +206,8 @@ select { } #cfgdelete { - position: absolute; - bottom: 15px; - right: 15px; - cursor:pointer; + cursor: pointer; + padding: 2px 10px; } .closebtn { diff --git a/ESP32_AP-Flasher/data/www/main.js b/ESP32_AP-Flasher/data/www/main.js index b775c95d..6aea1f36 100644 --- a/ESP32_AP-Flasher/data/www/main.js +++ b/ESP32_AP-Flasher/data/www/main.js @@ -11,8 +11,9 @@ const WAKEUP_REASON_WDT_RESET = 0xFE; const models = ["1.54\" 152x152px", "2.9\" 296x128px", "4.2\" 400x300px"]; models[240] = "Segmented tag" models[17] = "2.9\" 296x128px (UC8151)" -const displaySizeLookup = { 0: [152, 152], 1: [128, 296], 2: [400, 300] }; -displaySizeLookup[17] = [128, 296]; +const displaySizeLookup = { 0: [152, 152, 4], 1: [128, 296, 2], 2: [400, 300, 2] }; // w, h, rotate +displaySizeLookup[17] = [128, 296, 2]; +displaySizeLookup[240] = [0, 0, 0]; const colorTable = { 0: [255, 255, 255], 1: [0, 0, 0], 2: [255, 0, 0], 3: [150, 150, 150] }; const apstate = [ @@ -272,6 +273,7 @@ $('#clearlog').onclick = function () { document.querySelectorAll('.closebtn').forEach(button => { button.addEventListener('click', (event) => { event.target.parentNode.style.display = 'none'; + $('#advancedoptions').style.height = '0px'; }); }); @@ -295,19 +297,29 @@ $('#taglist').addEventListener("click", (event) => { .then(data => { var tagdata = data.tags[0]; $('#cfgalias').value = tagdata.alias; + $('#cfgmore').style.display = "none"; if (populateSelectTag(tagdata.hwType, tagdata.capabilities)) { $('#cfgcontent').parentNode.style.display = "flex"; $('#cfgcontent').value = tagdata.contentMode; $('#cfgcontent').dataset.json = tagdata.modecfgjson; contentselected(); + if (tagdata.contentMode != 12) $('#cfgmore').style.display = 'block'; } else { $('#customoptions').innerHTML = ""; $('#cfgcontent').parentNode.style.display = "none"; } + $('#cfgrotate').value = tagdata.rotate; + $('#cfglut').value = tagdata.lut; + $('#cfgmore').innerHTML = '🞃'; $('#configbox').style.display = 'block'; }) }) +$('#cfgmore').onclick = function () { + $('#cfgmore').innerHTML = $('#advancedoptions').style.height == '0px' ? '🞁' : '🞃'; + $('#advancedoptions').style.height = $('#advancedoptions').style.height == '0px' ? $('#advancedoptions').scrollHeight + 'px' : '0px'; +}; + $('#cfgsave').onclick = function () { let contentMode = $('#cfgcontent').value; let contentDef = getContentDefById(contentMode); @@ -332,6 +344,9 @@ $('#cfgsave').onclick = function () { formData.append("modecfgjson", String()); } + formData.append("rotate", $('#cfgrotate').value); + formData.append("lut", $('#cfglut').value); + fetch("/save_cfg", { method: "POST", body: formData @@ -340,26 +355,41 @@ $('#cfgsave').onclick = function () { .then(data => showMessage(data)) .catch(error => showMessage('Error: ' + error)); + $('#advancedoptions').style.height = '0px'; $('#configbox').style.display = 'none'; } -$('#cfgdelete').onclick = function () { +function sendCmd(mac, cmd) { let formData = new FormData(); - formData.append("mac", $('#cfgmac').dataset.mac); - fetch("/delete_cfg", { + formData.append("mac", mac); + formData.append("cmd", cmd); + fetch("/tag_cmd", { method: "POST", body: formData }) .then(response => response.text()) .then(data => { var div = $('#tag' + $('#cfgmac').dataset.mac); - div.remove(); + if (cmd == "del") div.remove(); showMessage(data); }) .catch(error => showMessage('Error: ' + error)); + $('#advancedoptions').style.height = '0px'; $('#configbox').style.display = 'none'; } +$('#cfgdelete').onclick = function () { + sendCmd($('#cfgmac').dataset.mac, "del"); +} + +$('#cfgclrpending').onclick = function () { + sendCmd($('#cfgmac').dataset.mac, "clear"); +} + +$('#cfgrefresh').onclick = function () { + sendCmd($('#cfgmac').dataset.mac, "refresh"); +} + $('#rebootbutton').onclick = function () { showMessage("rebooting AP....", true); fetch("/reboot", { @@ -459,7 +489,6 @@ function contentselected() { obj = JSON.parse($('#cfgcontent').dataset.json); } $('#paintbutton').style.display = 'none'; - if (contentMode) { let contentDef = getContentDefById(contentMode); if (contentDef) { @@ -514,17 +543,46 @@ function populateSelectTag(hwtype, capabilities) { var selectTag = $("#cfgcontent"); selectTag.innerHTML = ""; var optionsAdded = false; + var option; cardconfig.forEach(item => { var capcheck = item.capabilities ?? 0; var hwtypeArray = item.hwtype; if (hwtypeArray.includes(hwtype) && (capabilities & capcheck || capcheck == 0)) { - var option = document.createElement("option"); + option = document.createElement("option"); option.value = item.id; option.text = item.name; selectTag.appendChild(option); optionsAdded = true; } }); + + var rotateTag = $("#cfgrotate"); + rotateTag.innerHTML = ""; + + for (let i = 0; i < 4; i++) { + if (i == 0 || displaySizeLookup[hwtype][2] == 4 || (i == 2 && displaySizeLookup[hwtype][2] == 2)) { + option = document.createElement("option"); + option.value = i; + option.text = (i * 90) + " degrees"; + rotateTag.appendChild(option); + } + } + + var lutTag = $("#cfglut"); + lutTag.innerHTML = ""; + + option = document.createElement("option"); + option.value = "0"; + option.text = "auto"; + lutTag.appendChild(option); + + if (hwtype != 240) { + option = document.createElement("option"); + option.value = "1"; + option.text = "Always full refresh"; + lutTag.appendChild(option); + } + return optionsAdded; } diff --git a/ESP32_AP-Flasher/include/makeimage.h b/ESP32_AP-Flasher/include/makeimage.h index 4f0cf0ce..3aca9e67 100644 --- a/ESP32_AP-Flasher/include/makeimage.h +++ b/ESP32_AP-Flasher/include/makeimage.h @@ -9,6 +9,7 @@ struct imgParam { bool dither; bool grayLut = false; uint8_t bpp = 8; + uint8_t rotate = 0; char segments[12]; uint16_t symbols; diff --git a/ESP32_AP-Flasher/include/tag_db.h b/ESP32_AP-Flasher/include/tag_db.h index 1dd73e4a..c0b2db93 100644 --- a/ESP32_AP-Flasher/include/tag_db.h +++ b/ESP32_AP-Flasher/include/tag_db.h @@ -17,8 +17,8 @@ class tagRecord { public: - tagRecord() : mac{0}, alias(""), lastseen(0), nextupdate(0), contentMode(0), pending(false), md5{0}, md5pending{0}, expectedNextCheckin(0), modeConfigJson(""), LQI(0), RSSI(0), temperature(0), batteryMv(0), hwType(0), wakeupReason(0), capabilities(0), lastfullupdate(0), isExternal(false), pendingIdle(0), hasCustomLUT(false), - filename(""), data(nullptr), len(0) {} + tagRecord() : mac{0}, alias(""), lastseen(0), nextupdate(0), contentMode(0), pending(false), md5{0}, md5pending{0}, expectedNextCheckin(0), modeConfigJson(""), LQI(0), RSSI(0), temperature(0), batteryMv(0), hwType(0), wakeupReason(0), capabilities(0), lastfullupdate(0), isExternal(false), pendingIdle(0), hasCustomLUT(false), rotate(0), lut(0), + dataType(0), filename(""), data(nullptr), len(0) {} uint8_t mac[8]; String alias; @@ -41,6 +41,10 @@ class tagRecord { bool isExternal; uint16_t pendingIdle; bool hasCustomLUT; + uint8_t rotate; + uint8_t lut; + + uint8_t dataType; String filename; uint8_t* data; uint32_t len; diff --git a/ESP32_AP-Flasher/src/contentmanager.cpp b/ESP32_AP-Flasher/src/contentmanager.cpp index 9f4d9067..d7818348 100644 --- a/ESP32_AP-Flasher/src/contentmanager.cpp +++ b/ESP32_AP-Flasher/src/contentmanager.cpp @@ -128,10 +128,11 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { imageParams.hasRed = false; imageParams.dataType = DATATYPE_IMG_RAW_1BPP; imageParams.dither = false; - if (taginfo->hasCustomLUT) imageParams.grayLut = true; + if (taginfo->hasCustomLUT && taginfo->lut != 1) imageParams.grayLut = true; imageParams.invert = false; imageParams.symbols = 0; + imageParams.rotate = taginfo->rotate; switch (taginfo->contentMode) { case Image: @@ -1002,7 +1003,7 @@ void prepareLUTreq(uint8_t *dst, String input) { void getTemplate(JsonDocument &json, const char *filePath, uint8_t id, uint8_t hwtype) { File jsonFile = LittleFS.open(filePath, "r"); if (!jsonFile) { - Serial.println("Failed to open JSON file"); + Serial.println("Failed to open content template file " + String(filePath)); return; } diff --git a/ESP32_AP-Flasher/src/makeimage.cpp b/ESP32_AP-Flasher/src/makeimage.cpp index 5f0fa9c4..2cb090d0 100644 --- a/ESP32_AP-Flasher/src/makeimage.cpp +++ b/ESP32_AP-Flasher/src/makeimage.cpp @@ -80,11 +80,12 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) { fs::File f_out = LittleFS.open(fileout, "w"); - bool dither = true, rotated = false; + bool dither = true; + uint8_t rotate = imageParams.rotate; long bufw = spr.width(), bufh = spr.height(); if (bufw > bufh && bufw!=400 && bufh!=300) { - rotated = true; + rotate = (rotate + 3) % 4; bufw = spr.height(); bufh = spr.width(); } @@ -115,10 +116,19 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) { for (uint16_t y = 0; y < bufh; y++) { memset(error_buffernew, 0, bufw * sizeof(Error)); for (uint16_t x = 0; x < bufw; x++) { - if (rotated) { - color = Color(spr.readPixel(bufh - 1 - y, x)); - } else { - color = Color(spr.readPixel(x, y)); + switch (rotate) { + case 0: + color = Color(spr.readPixel(x, y)); + break; + case 1: + color = Color(spr.readPixel(y, bufw - 1 - x)); + break; + case 2: + color = Color(spr.readPixel(bufw - 1 - x, bufh - 1 - y)); + break; + case 3: + color = Color(spr.readPixel(bufh - 1 - y, x)); + break; } int best_color_index = 0; diff --git a/ESP32_AP-Flasher/src/newproto.cpp b/ESP32_AP-Flasher/src/newproto.cpp index 2bb445d0..88bc3921 100644 --- a/ESP32_AP-Flasher/src/newproto.cpp +++ b/ESP32_AP-Flasher/src/newproto.cpp @@ -98,6 +98,7 @@ void prepareDataAvail(uint8_t* data, uint16_t len, uint8_t dataType, uint8_t* ds taginfo->len = len; taginfo->expectedNextCheckin = 0; taginfo->filename = String(); + taginfo->dataType = dataType; memset(taginfo->md5pending, 0, 16 * sizeof(uint8_t)); struct pendingData pending = {0}; @@ -168,11 +169,11 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t time_t now; time(&now); time_t last_midnight = now - now % (24 * 60 * 60) + 3 * 3600; // somewhere in the middle of the night - if (taginfo->lastfullupdate < last_midnight || taginfo->hwType == SOLUM_29_UC8151) { + if (taginfo->lastfullupdate < last_midnight || taginfo->hwType == SOLUM_29_UC8151 || taginfo->lut == 1) { lut = EPD_LUT_DEFAULT; // full update once a day taginfo->lastfullupdate = now; } - if (taginfo->hasCustomLUT && taginfo->capabilities & CAPABILITY_SUPPORTS_CUSTOM_LUTS) { + if (taginfo->hasCustomLUT && taginfo->capabilities & CAPABILITY_SUPPORTS_CUSTOM_LUTS && taginfo->lut != 1) { Serial.println("using custom LUT"); lut = EPD_LUT_OTA; } @@ -190,16 +191,18 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t time_t now; time(&now); taginfo->expectedNextCheckin = now + nextCheckin * 60 + 60; + clearPending(taginfo); taginfo->filename = *filename; taginfo->len = filesize; - clearPending(taginfo); + taginfo->dataType = dataType; taginfo->pending = true; memcpy(taginfo->md5pending, md5bytes, sizeof(md5bytes)); } else { wsLog("firmware upload pending"); + clearPending(taginfo); taginfo->filename = *filename; taginfo->len = filesize; - clearPending(taginfo); + taginfo->dataType = dataType; taginfo->pending = true; } @@ -272,9 +275,10 @@ void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP) { } file.close(); + clearPending(taginfo); taginfo->filename = filename; taginfo->len = filesize; - clearPending(taginfo); + taginfo->dataType = pending->availdatainfo.dataType; taginfo->pending = true; memcpy(taginfo->md5pending, md5bytes, sizeof(md5bytes)); break; @@ -296,6 +300,7 @@ void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP) { taginfo->data = new uint8_t[len]; WiFiClient* stream = http.getStreamPtr(); stream->readBytes(taginfo->data, len); + taginfo->dataType = pending->availdatainfo.dataType; taginfo->pending = true; taginfo->len = len; } @@ -334,7 +339,7 @@ void processBlockRequest(struct espBlockRequest* br) { // not cached. open file, cache the data fs::File file = LittleFS.open(taginfo->filename); if (!file) { - Serial.print("Dunno how this happened... File pending but deleted in the meantime?\n"); + Serial.print("No current file. Canceling request\n"); prepareCancelPending(br->src); return; } diff --git a/ESP32_AP-Flasher/src/serialap.cpp b/ESP32_AP-Flasher/src/serialap.cpp index bfa19623..17311a9a 100644 --- a/ESP32_AP-Flasher/src/serialap.cpp +++ b/ESP32_AP-Flasher/src/serialap.cpp @@ -690,7 +690,7 @@ void APTask(void* parameter) { Serial.println("I wasn't able to connect to a ZBS (AP) tag.\n"); Serial.printf("This could be the first time this AP is booted and the AP-tag may be unflashed. We'll try to flash it!\n"); Serial.printf("If this tag was previously flashed succesfully but this message still shows up, there's probably something wrong with the serial connections.\n"); - Serial.printf("The build of this firmware expects an AP tag with RXD/TXD on ESP32 pins %d and %d, does this match with your wiring?\n", FLASHER_AP_RXD, FLASHER_AP_TXD); + Serial.printf("The build of this firmware expects an AP tag with TXD/RXD on ESP32 pins %d and %d, does this match with your wiring?\n", FLASHER_AP_RXD, FLASHER_AP_TXD); Serial.println("Performing firmware flash in about 30 seconds!\n"); flashCountDown(30); if (doAPFlash()) { @@ -729,8 +729,8 @@ void APTask(void* parameter) { Serial.println("Seems like you're running into some issues with the wiring, or (very small chance) the tag itself"); Serial.println("This ESP32-build expects the following pins connected to the ZBS243:"); Serial.println("--- ZBS243 based tag ESP32 ---"); - Serial.printf(" RXD ---------------- %02d\n", FLASHER_AP_RXD); - Serial.printf(" TXD ---------------- %02d\n", FLASHER_AP_TXD); + Serial.printf(" TXD ---------------- %02d\n", FLASHER_AP_RXD); + Serial.printf(" RXD ---------------- %02d\n", FLASHER_AP_TXD); Serial.printf(" CS/SS ---------------- %02d\n", FLASHER_AP_SS); Serial.printf(" MOSI ---------------- %02d\n", FLASHER_AP_MOSI); Serial.printf(" MISO ---------------- %02d\n", FLASHER_AP_MISO); diff --git a/ESP32_AP-Flasher/src/tag_db.cpp b/ESP32_AP-Flasher/src/tag_db.cpp index f9dcf46d..513b0c8e 100644 --- a/ESP32_AP-Flasher/src/tag_db.cpp +++ b/ESP32_AP-Flasher/src/tag_db.cpp @@ -117,6 +117,8 @@ void fillNode(JsonObject &tag, tagRecord* &taginfo) { tag["capabilities"] = taginfo->capabilities; tag["modecfgjson"] = taginfo->modeConfigJson; tag["isexternal"] = taginfo->isExternal; + tag["rotate"] = taginfo->rotate; + tag["lut"] = taginfo->lut; } void saveDB(String filename) { @@ -210,6 +212,8 @@ void loadDB(String filename) { taginfo->capabilities = tag["capabilities"]; taginfo->modeConfigJson = tag["modecfgjson"].as(); taginfo->isExternal = tag["isexternal"].as(); + taginfo->rotate = tag["rotate"] | 0; + taginfo->lut = tag["lut"] | 0; } } else { Serial.print(F("deserializeJson() failed: ")); @@ -251,6 +255,7 @@ uint8_t getTagCount() { } void clearPending(tagRecord* taginfo) { + taginfo->filename = String(); if (taginfo->data != nullptr) { free(taginfo->data); taginfo->data = nullptr; diff --git a/ESP32_AP-Flasher/src/web.cpp b/ESP32_AP-Flasher/src/web.cpp index fbef07cf..6eec2bfb 100644 --- a/ESP32_AP-Flasher/src/web.cpp +++ b/ESP32_AP-Flasher/src/web.cpp @@ -319,6 +319,12 @@ void init_web() { taginfo->modeConfigJson = request->getParam("modecfgjson", true)->value(); taginfo->contentMode = atoi(request->getParam("contentmode", true)->value().c_str()); taginfo->nextupdate = 0; + if (request->hasParam("rotate", true)) { + taginfo->rotate = atoi(request->getParam("rotate", true)->value().c_str()); + } + if (request->hasParam("lut", true)) { + taginfo->lut = atoi(request->getParam("lut", true)->value().c_str()); + } // memset(taginfo->md5, 0, 16 * sizeof(uint8_t)); // memset(taginfo->md5pending, 0, 16 * sizeof(uint8_t)); wsSendTaginfo(mac, SYNC_USERCFG); @@ -332,20 +338,33 @@ void init_web() { request->send(200, "text/plain", "Ok, saved"); }); - server.on("/delete_cfg", HTTP_POST, [](AsyncWebServerRequest *request) { - if (request->hasParam("mac", true)) { - String dst = request->getParam("mac", true)->value(); + server.on("/tag_cmd", HTTP_POST, [](AsyncWebServerRequest *request) { + if (request->hasParam("mac", true) && request->hasParam("cmd", true)) { uint8_t mac[8]; - if (hex2mac(dst, mac)) { - wsSendTaginfo(mac, SYNC_DELETE); - if (deleteRecord(mac)) { - request->send(200, "text/plain", "Ok, deleted"); + if (hex2mac(request->getParam("mac", true)->value(), mac)) { + tagRecord *taginfo = nullptr; + taginfo = tagRecord::findByMAC(mac); + if (taginfo != nullptr) { + const char *cmdValue = request->getParam("cmd", true)->value().c_str(); + if (strcmp(cmdValue, "del") == 0) { + wsSendTaginfo(mac, SYNC_DELETE); + deleteRecord(mac); + } + if (strcmp(cmdValue, "clear") == 0) { + clearPending(taginfo); + memcpy(taginfo->md5pending, taginfo->md5, sizeof(taginfo->md5pending)); + wsSendTaginfo(mac, SYNC_TAGSTATUS); + } + if (strcmp(cmdValue, "refresh") == 0) { + updateContent(mac); + } + request->send(200, "text/plain", "Ok, done"); } else { - request->send(200, "text/plain", "Error while saving: mac not found"); + request->send(200, "text/plain", "Error: mac not found"); } } } else { - request->send(500, "text/plain", "no mac"); + request->send(500, "text/plain", "param error"); } });