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