added advanced tag options

- added advanced tag options: image rotation / always use default lut / force refresh / cancel pending
- fixed rx/tx swap in documentation
This commit is contained in:
Nic Limper
2023-06-09 12:56:58 +02:00
parent c68b582be7
commit e7fbaffbab
11 changed files with 194 additions and 46 deletions

View File

@@ -29,10 +29,30 @@
<button id="paintbutton"><i>A</i>&#128396;</button>
</p>
<div id="customoptions"></div>
<p>
<input type="button" value="Save" id="cfgsave">
<span id="cfgdelete"><img src="data:image/gif;base64,R0lGODlhEAAQAPMAANXV1e3t7d/f39HR0dvb2/Hx8dTU1OLi4urq6mZmZpmZmf///wAAAAAAAAAAAAAAACH5BAEAAAwALAAAAAAQABAAAARBkMlJq71Yrp3ZXkr4WWCYnOZSgQVyEMYwJCq1nHhe20qgCAoA7QLyAYU7njE4JPV+zOSkCEUSFbmTVPPpbjvgTAQAOw==
"></span>
<div id="advancedoptions" style="height: 0px;">
<p>Advanced options</p>
<p>
<label for="cfgrotate">Rotate image</label>
<select id="cfgrotate">
<option value="0">0 degrees</option>
</select>
</p>
<p>
<label for="cfglut">LUT</label>
<select id="cfglut">
<option value="0">auto</option>
</select>
</p>
<p>
<button id="cfgrefresh">force refresh</button>
<button id="cfgclrpending">clear pending</button>
<button id="cfgdelete"><img src="data:image/gif;base64,R0lGODlhEAAQAPMAANXV1e3t7d/f39HR0dvb2/Hx8dTU1OLi4urq6mZmZpmZmf///wAAAAAAAAAAAAAAACH5BAEAAAwALAAAAAAQABAAAARBkMlJq71Yrp3ZXkr4WWCYnOZSgQVyEMYwJCq1nHhe20qgCAoA7QLyAYU7njE4JPV+zOSkCEUSFbmTVPPpbjvgTAQAOw==
"></button>
</p>
</div>
<p id="savebar">
<span><input type="button" value="Save" id="cfgsave"></span>
<span id="cfgmore" title="advanced options">&#x1f783;</span>
</p>
</div>
@@ -159,8 +179,8 @@ Latency will be around 40 seconds.">
<div class="nextcheckin"></div>
<div class="nextupdate"></div>
<div class="corner">
<div class="pendingicon">&circlearrowright;</div>
<div class="warningicon">&#9888;</div>
<div class="pendingicon" title="A new message is waiting for the tag to pick up">&circlearrowright;</div>
<div class="warningicon" title="This tag has nog been seen for a long time">&#9888;</div>
</div>
</div>
</div>

View File

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

View File

@@ -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 = '&#x1f783;';
$('#configbox').style.display = 'block';
})
})
$('#cfgmore').onclick = function () {
$('#cfgmore').innerHTML = $('#advancedoptions').style.height == '0px' ? '&#x1f781;' : '&#x1f783;';
$('#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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String>();
taginfo->isExternal = tag["isexternal"].as<bool>();
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;

View File

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