From ea7a76be89eeae8767b63b65d9fdd4aeb98eaabd Mon Sep 17 00:00:00 2001 From: Nic Limper Date: Sat, 25 Feb 2023 20:26:31 +0100 Subject: [PATCH 1/2] raw buffer still some work to do: - 1.54" img url breaks communication with the tag_fw... why??? - generate canvas from raw on webpage - dithering in spr2buffer --- esp32_fw/include/commstructs.h | 9 +- esp32_fw/include/makeimage.h | 3 +- esp32_fw/src/contentmanager.cpp | 41 ++++---- esp32_fw/src/main.cpp | 2 +- esp32_fw/src/makeimage.cpp | 59 +++++++++-- esp32_fw/src/newproto.cpp | 177 +++++++++++++------------------- esp32_fw/src/serial.cpp | 38 +++---- tag_fw/userinterface.c | 6 +- 8 files changed, 176 insertions(+), 159 deletions(-) diff --git a/esp32_fw/include/commstructs.h b/esp32_fw/include/commstructs.h index a52e6c4b..868548ec 100644 --- a/esp32_fw/include/commstructs.h +++ b/esp32_fw/include/commstructs.h @@ -47,9 +47,12 @@ struct espAvailDataReq { } __packed; #define DATATYPE_NOUPDATE 0 -#define DATATYPE_IMG 1 -#define DATATYPE_IMGRAW 2 -#define DATATYPE_UPDATE 3 +#define DATATYPE_IMG_BMP 2 +#define DATATYPE_FW_UPDATE 3 +#define DATATYPE_IMG_DIFF 0x10 // always 1BPP +#define DATATYPE_IMG_RAW_1BPP 0x20 // 2888 bytes for 1.54" / 4736 2.9" / 15000 4.2" +#define DATATYPE_IMG_RAW_2BPP 0x21 // 5776 bytes for 1.54" / 9472 2.9" / 30000 4.2" +#define DATATYPE_IMG_RAW_1BPP_DIRECT 0x3F // only for 1.54", don't write to EEPROM, but straightaway to the EPD #define EPD_LUT_DEFAULT 0 #define EPD_LUT_NO_REPEATS 1 diff --git a/esp32_fw/include/makeimage.h b/esp32_fw/include/makeimage.h index 8debd701..3363e945 100644 --- a/esp32_fw/include/makeimage.h +++ b/esp32_fw/include/makeimage.h @@ -30,6 +30,7 @@ enum EinkClut { }; void spr2grays(TFT_eSprite &spr, long w, long h, String &fileout); -void jpg2grays(String filein, String fileout); +void spr2buffer(TFT_eSprite &spr, String &fileout); +void jpg2buffer(String filein, String fileout); void bmp2grays(String filein, String fileout); diff --git a/esp32_fw/src/contentmanager.cpp b/esp32_fw/src/contentmanager.cpp index c8ac6acc..556d249a 100644 --- a/esp32_fw/src/contentmanager.cpp +++ b/esp32_fw/src/contentmanager.cpp @@ -58,7 +58,7 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { sprintf(buffer, "%02X%02X%02X%02X%02X%02X\0", src[2], src[3], src[4], src[5], src[6], src[7]); String dst = (String)buffer; - String filename = "/" + dst + ".bmp"; + String filename = "/" + dst + ".raw"; struct tm time_info; getLocalTime(&time_info); @@ -79,8 +79,9 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { case Image: filename = cfgobj["filename"].as(); + //***FIXME... convert jpg to raw if (filename && filename !="null" && !cfgobj["#fetched"].as()) { - if (prepareDataAvail(&filename, DATATYPE_IMGRAW, mac, cfgobj["timetolive"].as())) { + if (prepareDataAvail(&filename, DATATYPE_IMG_BMP, mac, cfgobj["timetolive"].as())) { cfgobj["#fetched"] = true; } else { wsErr("Error accessing " + filename); @@ -137,7 +138,7 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { filename = cfgobj["filename"].as(); if (filename && filename != "null" && !cfgobj["#fetched"].as()) { - if (prepareDataAvail(&filename, DATATYPE_UPDATE, mac, cfgobj["timetolive"].as())) { + if (prepareDataAvail(&filename, DATATYPE_FW_UPDATE, mac, cfgobj["timetolive"].as())) { cfgobj["#fetched"] = true; } else { wsErr("Error accessing " + filename); @@ -183,7 +184,7 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { } bool updateTagImage(String &filename, uint8_t *dst, uint16_t nextCheckin) { - prepareDataAvail(&filename, DATATYPE_IMGRAW, dst, nextCheckin); + prepareDataAvail(&filename, DATATYPE_IMG_RAW_2BPP, dst, nextCheckin); return true; } @@ -229,13 +230,13 @@ void drawDate(String &filename, tagRecord *&taginfo) { } else if (taginfo->hwType == SOLUM_154_033) { - initSprite(spr, 154, 154); - drawString(spr, Dag[timeinfo.tm_wday], 154 / 2, 10, "fonts/calibrib30", TC_DATUM); - drawString(spr, String(Maand[timeinfo.tm_mon]), 154 / 2, 120, "fonts/calibrib30", TC_DATUM); - drawString(spr, String(timeinfo.tm_mday), 154 / 2, 42, "fonts/numbers2-1", TC_DATUM, TFT_RED); + initSprite(spr, 152, 152); + drawString(spr, Dag[timeinfo.tm_wday], 152 / 2, 10, "fonts/calibrib30", TC_DATUM); + drawString(spr, String(Maand[timeinfo.tm_mon]), 152 / 2, 120, "fonts/calibrib30", TC_DATUM); + drawString(spr, String(timeinfo.tm_mday), 152 / 2, 42, "fonts/numbers2-1", TC_DATUM, TFT_RED); } - spr2grays(spr, spr.width(), spr.height(), filename); + spr2buffer(spr, filename); spr.deleteSprite(); } @@ -263,7 +264,7 @@ void drawNumber(String &filename, int32_t count, int32_t thresholdred, tagRecord } else if (taginfo->hwType == SOLUM_154_033) { - initSprite(spr, 154, 154); + initSprite(spr, 152, 152); spr.setTextDatum(MC_DATUM); if (count > thresholdred) { spr.setTextColor(TFT_RED, TFT_WHITE); @@ -274,12 +275,12 @@ void drawNumber(String &filename, int32_t count, int32_t thresholdred, tagRecord if (count > 99) font = "fonts/numbers2-1"; if (count > 999) font = "fonts/numbers3-1"; spr.loadFont(font, LittleFS); - spr.drawString(String(count), 154 / 2, 154 / 2 + 10); + spr.drawString(String(count), 152 / 2, 152 / 2 + 10); spr.unloadFont(); } - spr2grays(spr, spr.width(), spr.height(), filename); + spr2buffer(spr, filename); spr.deleteSprite(); } @@ -377,7 +378,7 @@ void drawWeather(String &filename, String location, tagRecord *&taginfo) { } else if (taginfo->hwType == SOLUM_154_033) { - initSprite(spr, 154, 154); + initSprite(spr, 152, 152); spr.setTextDatum(TL_DATUM); spr.setTextFont(2); @@ -414,7 +415,7 @@ void drawWeather(String &filename, String location, tagRecord *&taginfo) { } - spr2grays(spr, spr.width(), spr.height(), filename); + spr2buffer(spr, filename); spr.deleteSprite(); } } @@ -517,7 +518,7 @@ void drawForecast(String &filename, String location, tagRecord *&taginfo) { } } - spr2grays(spr, spr.width(), spr.height(), filename); + spr2buffer(spr, filename); spr.deleteSprite(); } } @@ -536,12 +537,12 @@ void drawIdentify(String &filename, tagRecord *&taginfo) { drawString(spr, mac62hex(taginfo->mac), 10, 50, "fonts/bahnschrift20", TL_DATUM, TFT_RED); } else if (taginfo->hwType == SOLUM_154_033) { - initSprite(spr, 154, 154); + initSprite(spr, 152, 152); drawString(spr, taginfo->alias, 5, 5, "fonts/bahnschrift20"); drawString(spr, mac62hex(taginfo->mac), 10, 50, "fonts/bahnschrift20", TL_DATUM, TFT_RED); } - spr2grays(spr, spr.width(), spr.height(), filename); + spr2buffer(spr, filename); spr.deleteSprite(); } @@ -557,11 +558,11 @@ bool getImgURL(String &filename, String URL, time_t fetched) { http.setTimeout(5000); //timeout in ms int httpCode = http.GET(); if (httpCode == 200) { - File f = LittleFS.open(filename, "w"); + File f = LittleFS.open("/temp/temp.jpg", "w"); if (f) { http.writeToStream(&f); f.close(); - jpg2grays(filename, filename); + jpg2buffer("/temp/temp.jpg", filename); } } else { if (httpCode!=304) { @@ -624,7 +625,7 @@ bool getRSSfeed(String &filename, String URL, String title, tagRecord *&taginfo) } } - spr2grays(spr, spr.width(), spr.height(), filename); + spr2buffer(spr, filename); spr.deleteSprite(); return true; diff --git a/esp32_fw/src/main.cpp b/esp32_fw/src/main.cpp index 9a4efe08..8787f4fc 100644 --- a/esp32_fw/src/main.cpp +++ b/esp32_fw/src/main.cpp @@ -22,7 +22,7 @@ void timeTask(void* parameter) { Serial.println("Waiting for valid time from NTP-server"); } else { if (now % 10 == 0) wsSendSysteminfo(); - if (now % 60 == 3) Ping(); + if (now % 30 == 3) Ping(); if (now % 300 == 6) saveDB("/current/tagDB.json"); contentRunner(); diff --git a/esp32_fw/src/makeimage.cpp b/esp32_fw/src/makeimage.cpp index 0619b9d8..42261932 100644 --- a/esp32_fw/src/makeimage.cpp +++ b/esp32_fw/src/makeimage.cpp @@ -14,7 +14,7 @@ bool spr_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) return 1; } -void jpg2grays(String filein, String fileout) { +void jpg2buffer(String filein, String fileout) { LittleFS.begin(); TJpgDec.setSwapBytes(true); TJpgDec.setJpgScale(1); @@ -23,15 +23,15 @@ void jpg2grays(String filein, String fileout) { TJpgDec.getFsJpgSize(&w, &h, filein, LittleFS); Serial.println("jpeg conversion " + String(w) + "x" + String(h)); + spr.setColorDepth(8); spr.createSprite(w, h); if (spr.getPointer() == nullptr) { - wsErr("Failed to create sprite in jpg2grays"); + wsErr("Failed to create sprite in jpg2buffer"); } - spr.setColorDepth(8); spr.fillSprite(TFT_WHITE); TJpgDec.drawFsJpg(0, 0, filein, LittleFS); - spr2grays(spr, w, h, fileout); + spr2buffer(spr, fileout); spr.deleteSprite(); } @@ -45,8 +45,6 @@ static uint32_t repackPackedVals(uint32_t val, uint32_t pixelsPerPackedUnit, uin } void spr2grays(TFT_eSprite &spr, long w, long h, String &fileout) { - // based on bmp2grays function by Dmitry.GR - long t = millis(); LittleFS.begin(); @@ -247,6 +245,55 @@ void spr2grays(TFT_eSprite &spr, long w, long h, String &fileout) { Serial.println("finished writing BMP " + String(millis() - t) + "ms"); } +void spr2buffer(TFT_eSprite &spr, String &fileout) { + long t = millis(); + LittleFS.begin(); + + fs::File f_out = LittleFS.open(fileout, "w"); + + bool dither = true, rotated = false; + long bufw = spr.width(), bufh = spr.height(); + + if (bufw > bufh) { + rotated = true; + bufw = spr.height(); + bufh = spr.width(); + } + + int bufferSize = (bufw * bufh) / 8; + uint16_t color; + uint8_t *blackBuffer = new uint8_t[bufferSize]; + uint8_t *redBuffer = new uint8_t[bufferSize]; + memset(blackBuffer, 0, bufferSize); + memset(redBuffer, 0, bufferSize); + + for (uint16_t y = 0; y < bufh; y++) { + for (uint16_t x = 0; x < bufw; x++) { + if (rotated) { + color = spr.readPixel(bufh - 1 - y, x); + } else { + color = spr.readPixel(x, y); + } + uint16_t bitIndex = 7 - (x % 8); + uint16_t byteIndex = (y * bufw + x) / 8; + if (color == TFT_BLACK) { + blackBuffer[byteIndex] |= (1 << bitIndex); + } else if (color == TFT_RED) { + redBuffer[byteIndex] |= (1 << bitIndex); + } + } + } + + f_out.write(blackBuffer, bufferSize); + f_out.write(redBuffer, bufferSize); + + delete[] blackBuffer; + delete[] redBuffer; + + f_out.close(); + Serial.println("finished writing buffer " + String(millis() - t) + "ms"); +} + void bmp2grays(String filein, String fileout) { // based on bmp2grays function by Dmitry.GR diff --git a/esp32_fw/src/newproto.cpp b/esp32_fw/src/newproto.cpp index 88c0ed34..3f438102 100644 --- a/esp32_fw/src/newproto.cpp +++ b/esp32_fw/src/newproto.cpp @@ -13,7 +13,7 @@ #include "tag_db.h" #include "web.h" -extern void sendBlock(const void* data, const uint16_t len); +extern uint16_t sendBlock(const void* data, const uint16_t len); void addCRC(void* p, uint8_t len) { uint8_t total = 0; @@ -74,80 +74,79 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t nextCheckin = MIN_RESPONSE_TIME; } - *filename = "/" + *filename; - LittleFS.begin(); - if (!LittleFS.exists(*filename)) return false; - fs::File file = LittleFS.open(*filename); - - if (file.size() == 0) { - Serial.print("opened a file with size 0??\n"); - return false; - } - - if (filename->endsWith(".bmp") || filename->endsWith(".BMP")) { - struct BitmapFileHeader hdr; - file.read((uint8_t*)&hdr, sizeof(hdr)); - if (hdr.width == 296 && hdr.height == 128) { - //sorry, can't rotate - Serial.println("when using BMP files, remember to only use 128px width and 296px height"); - wsErr("when using BMP files, remember to only use 128px width and 296px height"); - return false; - } - if (hdr.sig[0] == 'B' && hdr.sig[1] == 'M' && hdr.bpp == 24) { - Serial.println("converting 24bpp bmp to grays"); - char fileout[64]; - sprintf(fileout, "/temp/%02X%02X%02X%02X%02X%02X.bmp\0", dst[5], dst[4], dst[3], dst[2], dst[1], dst[0]); - bmp2grays(*filename,(String)fileout); - *filename = (String)fileout; - file.close(); - file = LittleFS.open(*filename); - } - } - - if (filename->endsWith(".jpg") || filename->endsWith(".JPG")) { - Serial.println("converting jpg to grays"); - char fileout[64]; - sprintf(fileout, "/temp/%02X%02X%02X%02X%02X%02X.bmp\0", dst[5], dst[4], dst[3], dst[2], dst[1], dst[0]); - jpg2grays(*filename, (String)fileout); - *filename = (String)fileout; - file.close(); - file = LittleFS.open(*filename); - } - - uint8_t md5bytes[16]; - { - MD5Builder md5; - md5.begin(); - md5.addStream(file, file.size()); - md5.calculate(); - md5.getBytes(md5bytes); - } - - uint16_t attempts = 60 * 24; - uint8_t lut = EPD_LUT_NO_REPEATS; uint8_t src[8]; *((uint64_t*)src) = swap64(*((uint64_t*)dst)); uint8_t mac[6]; memcpy(mac, src + 2, sizeof(mac)); tagRecord* taginfo = nullptr; taginfo = tagRecord::findByMAC(mac); - if (taginfo != nullptr) { + if (taginfo == nullptr) { + wsErr("Tag not found, this shouldn't happen."); + return true; + } - if (memcmp(md5bytes, taginfo->md5pending, 16) == 0) { - wsLog("new image is the same as current or already pending image. not updating tag."); - wsSendTaginfo(mac); - return true; + *filename = "/" + *filename; + LittleFS.begin(); + + if (!LittleFS.exists(*filename)) { + wsErr("File not found. " + *filename); + return false; + } + + fs::File file = LittleFS.open(*filename); + uint32_t filesize = file.size(); + if (filesize == 0) { + file.close(); + wsErr("File has size 0. " + *filename); + return false; + } + + uint8_t md5bytes[16]; + { + MD5Builder md5; + md5.begin(); + md5.addStream(file, filesize); + md5.calculate(); + md5.getBytes(md5bytes); + } + + file.close(); + uint16_t attempts = 60 * 24; + uint8_t lut = EPD_LUT_NO_REPEATS; + + if (memcmp(md5bytes, taginfo->md5pending, 16) == 0) { + wsLog("new image is the same as current or already pending image. not updating tag."); + wsSendTaginfo(mac); + return true; + } + + 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) { + lut = EPD_LUT_DEFAULT; // full update once a day + taginfo->lastfullupdate = now; + } + + if (dataType != DATATYPE_FW_UPDATE) { + + char dst_path[64]; + sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X.pending\0", dst[5], dst[4], dst[3], dst[2], dst[1], dst[0]); + if (LittleFS.exists(dst_path)) { + LittleFS.remove(dst_path); } + LittleFS.rename(*filename, dst_path); + *filename = String(dst_path); + wsLog("new image pending: " + String(dst_path)); 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) { - lut = EPD_LUT_DEFAULT; // full update once a day - taginfo->lastfullupdate = now; - } + taginfo->pending = true; + taginfo->expectedNextCheckin = now + nextCheckin * 60 + 60; + memcpy(taginfo->md5pending, md5bytes, sizeof(md5bytes)); + } else { - wsErr("Tag not found, this shouldn't happen."); + wsLog("firmware upload pending"); } // the message that will be sent to the AP to tell the tag there is data pending @@ -155,7 +154,7 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t memcpy(pending.targetMac, dst, 8); pending.availdatainfo.dataType = dataType; pending.availdatainfo.dataVer = *((uint64_t*)md5bytes); - pending.availdatainfo.dataSize = file.size(); + pending.availdatainfo.dataSize = filesize; pending.availdatainfo.dataTypeArgument = lut; pending.availdatainfo.nextCheckIn = nextCheckin; pending.attemptsLeft = attempts; @@ -169,36 +168,8 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t pendinginfo->len = pending.availdatainfo.dataSize; pendinginfo->data = nullptr; pendinginfo->timeout = PENDING_TIMEOUT; - //pendinginfo->data = getDataForFile(&file); pendingfiles.push_back(pendinginfo); - if (dataType != DATATYPE_UPDATE) { - char dst_path[64]; - sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X.pending\0", dst[5], dst[4], dst[3], dst[2], dst[1], dst[0]); - fs::File dstfile = LittleFS.open(dst_path, "w"); - //int bytes_written = dstfile.write(pendinginfo->data, pendinginfo->len); - file.seek(0); - const int chunkSize = 512; - uint8_t buffer[chunkSize]; - size_t bytesRead = 0; - while ((bytesRead = file.read(buffer, chunkSize)) > 0) { - dstfile.write(buffer, bytesRead); - } - dstfile.close(); - - wsLog("new image pending: " + String(dst_path)); - if (taginfo != nullptr) { - time_t now; - time(&now); - taginfo->pending = true; - taginfo->expectedNextCheckin = now + nextCheckin * 60 + 60; - memcpy(taginfo->md5pending, md5bytes, sizeof(md5bytes)); - } - } else { - wsLog("firmware upload pending"); - } - file.close(); - wsSendTaginfo(mac); return true; @@ -229,7 +200,7 @@ void processBlockRequest(struct espBlockRequest* br) { file.close(); } else { // file is already cached, refresh the timeout - pd->datatimeout = PENDING_DATA_TIMEOUT; + //pd->datatimeout = PENDING_DATA_TIMEOUT; } } // check if we're not exceeding max blocks (to prevent sendBlock from exceeding its boundary) @@ -241,11 +212,11 @@ void processBlockRequest(struct espBlockRequest* br) { uint32_t len = pd->len - (BLOCK_DATA_SIZE * br->blockId); if (len > BLOCK_DATA_SIZE) len = BLOCK_DATA_SIZE; - sendBlock(pd->data + (br->blockId * BLOCK_DATA_SIZE), len); - char buffer[64]; - sprintf(buffer, "< Block Request received for MD5 %llu, block %d\n\0", br->ver, br->blockId); + uint16_t checksum = sendBlock(pd->data + (br->blockId * BLOCK_DATA_SIZE), len); + char buffer[150]; + sprintf(buffer, "< Block Request received for file %s block %d, len %d checksum %u\0", pd->filename.c_str(), br->blockId, len, checksum); wsLog((String)buffer); - Serial.printf("blockId); + Serial.printf("< Block Request received for MD5 %llu, file %s block %d, len %d checksum %u\n", br->ver, pd->filename.c_str(), br->blockId, len, checksum); } void processXferComplete(struct espXferComplete* xfc) { @@ -260,10 +231,8 @@ void processXferComplete(struct espXferComplete* xfc) { char src_path[64]; char dst_path[64]; - char tmp_path[64]; sprintf(src_path, "/current/%02X%02X%02X%02X%02X%02X.pending\0", src[2], src[3], src[4], src[5], src[6], src[7]); - sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X.bmp\0", src[2], src[3], src[4], src[5], src[6], src[7]); - sprintf(tmp_path, "/temp/%02X%02X%02X%02X%02X%02X.bmp\0", src[2], src[3], src[4], src[5], src[6], src[7]); + sprintf(dst_path, "/current/%02X%02X%02X%02X%02X%02X.raw\0", src[2], src[3], src[4], src[5], src[6], src[7]); if (LittleFS.exists(dst_path) && LittleFS.exists(src_path)) { LittleFS.remove(dst_path); } @@ -272,9 +241,6 @@ void processXferComplete(struct espXferComplete* xfc) { } else { wsErr("hm, weird, no pending image found after xfercomplete."); } - if (LittleFS.exists(tmp_path)) { - LittleFS.remove(tmp_path); - } time_t now; time(&now); @@ -362,6 +328,11 @@ void processDataReq(struct espAvailDataReq* eadr) { taginfo->hwType = eadr->adr.hwType; taginfo->wakeupReason = eadr->adr.wakeupReason; taginfo->capabilities = eadr->adr.capabilities; + if (eadr->adr.wakeupReason == WAKEUP_REASON_FIRSTBOOT && !taginfo->pending) { + taginfo->nextupdate = 0; + memset(taginfo->md5, 0, 16 * sizeof(uint8_t)); + memset(taginfo->md5pending, 0, 16 * sizeof(uint8_t)); + } } sprintf(buffer, " 10000*1000ULL) { + if (esp_timer_get_time() - waitingForVersion > 5000*1000ULL) { waitingForVersion = 0; - //performDeviceFlash(); - Serial.println("I wasn't able to connect to a ZBS tag, trying to reboot the tag."); - Serial.println("If this problem persists, please check wiring and definitions in the settings.h file, and presence of the right firmware"); - simplePowerOn(); - wsErr("The AP tag crashed. Restarting tag, regenerating all pending info."); - refreshAllPending(); + wsLog("AP doesn't respond... "+String(crashcounter + 1)); + if (++crashcounter >= 4) { + crashcounter = 0; + Serial.println("I wasn't able to connect to a ZBS tag, trying to reboot the tag."); + Serial.println("If this problem persists, please check wiring and definitions in the settings.h file, and presence of the right firmware"); + simplePowerOn(); + wsErr("The AP tag crashed. Restarting tag, regenerating all pending info."); + refreshAllPending(); + } else { + Ping(); + } } } diff --git a/tag_fw/userinterface.c b/tag_fw/userinterface.c index 73e08152..5ecfa75b 100644 --- a/tag_fw/userinterface.c +++ b/tag_fw/userinterface.c @@ -70,12 +70,12 @@ void showSplashScreen() { setColorMode(EPD_MODE_NORMAL, EPD_MODE_INVERT); #if (SCREEN_WIDTH == 152) // 1.54" - epdPrintBegin(12, 2, EPD_DIRECTION_X, EPD_SIZE_DOUBLE, EPD_COLOR_BLACK); + epdPrintBegin(5, 55, EPD_DIRECTION_X, EPD_SIZE_DOUBLE, EPD_COLOR_BLACK); epdpr("Starting"); epdPrintEnd(); - loadRawBitmap(oepli, 0, 12, EPD_COLOR_BLACK); - loadRawBitmap(cloud, 0, 0, EPD_COLOR_RED); + loadRawBitmap(oepli, 12, 12, EPD_COLOR_BLACK); + loadRawBitmap(cloud, 12, 0, EPD_COLOR_RED); epdPrintBegin(5, 136, EPD_DIRECTION_X, EPD_SIZE_SINGLE, EPD_COLOR_RED); epdpr("%02X%02X", mSelfMac[7], mSelfMac[6]); From ef09cf4999c1312320ec9207234d2cd27b63b5c5 Mon Sep 17 00:00:00 2001 From: Nic Limper Date: Thu, 16 Mar 2023 22:16:42 +0100 Subject: [PATCH 2/2] dithering/raw previews/css - fixed preview images on webpage (raw buffer is rendered client side) - added responsive css, webpage works on mobile now - content is now rendered on 4 bit sprite using indexed color - added floyd steinberg dithering --- esp32_fw/data/www/index.html | 2 +- esp32_fw/data/www/main.css | 62 +++++++++++++++++++++-- esp32_fw/data/www/main.js | 63 +++++++++++++++-------- esp32_fw/src/contentmanager.cpp | 88 +++++++++++++++++---------------- esp32_fw/src/makeimage.cpp | 78 +++++++++++++++++++++++++++-- 5 files changed, 220 insertions(+), 73 deletions(-) diff --git a/esp32_fw/data/www/index.html b/esp32_fw/data/www/index.html index a0f449f8..d67668e7 100644 --- a/esp32_fw/data/www/index.html +++ b/esp32_fw/data/www/index.html @@ -60,7 +60,7 @@
-
+
diff --git a/esp32_fw/data/www/main.css b/esp32_fw/data/www/main.css index 61c312b9..d24cf8c6 100644 --- a/esp32_fw/data/www/main.css +++ b/esp32_fw/data/www/main.css @@ -1,4 +1,4 @@ -*{ +* { margin:0; padding:0; border:0; @@ -40,7 +40,6 @@ label { height: 50px; text-indent: 50px; overflow:hidden; - background-size: 50px 50px; font-size: 2.5em; color: white; } @@ -84,9 +83,20 @@ label { flex: 1; } +textarea, +input.text, +input[type="text"], +input[type="button"], +input[type="submit"], +button { + -webkit-appearance: none; + border-radius: 0; +} + input { border: solid 1px #666666; padding: 4px; + -webkit-border-radius: 0px; } input[type=button] { @@ -99,6 +109,7 @@ input[type=button]:hover { } select { padding: 4px; + -webkit-border-radius: 0px; } #configbox { @@ -179,7 +190,7 @@ select { .tagcard { width: 225px; position: relative; - height: 170px; + min-height: 170px; margin: 5px; padding: 5px; background-color: #dddddd; @@ -203,7 +214,7 @@ select { float: right; } -.currimg img { +.currimg img, .currimg canvas { max-width: 50px; } @@ -326,3 +337,46 @@ ul.messages li.new { 50% { background-color: lightblue;} 100% { background-color: lightgray;} } + +@media screen and (max-width: 480px) { + /* styles for mobile devices in portrait mode */ + + body { + font-size: 14px; + } + + .tagcard { + width: 100%; + min-height: 200px; + } + + .logbox #sysinfo { + float: none; + display: block; + } + + #configbox { + top: 0px; + left: 0px; + width: 100%; + } + + .currimg { + float: none; + position: absolute; + right: 5px; + } + + .currimg img, + .currimg canvas { + max-width: 60px; + } + + .logo { + padding-top: 10px; + text-indent: 0px; + font-size: 1.8em; + text-align: center; + } + +} \ No newline at end of file diff --git a/esp32_fw/data/www/main.js b/esp32_fw/data/www/main.js index d9fc8d66..c4982801 100644 --- a/esp32_fw/data/www/main.js +++ b/esp32_fw/data/www/main.js @@ -10,6 +10,8 @@ const WAKEUP_REASON_WDT_RESET = 0xFE; const contentModes = ["Static image", "Current date", "Counting days", "Counting hours", "Current weather", "Firmware update", "Memo text", "Image url", "Weather forecast","RSS feed"]; const models = ["1.54\" 152x152px", "2.9\" 296x128px", "4.2\" 400x300px"]; +const displaySizeLookup = { 0: [152, 152], 1: [128, 296], 2: [400, 300] }; +const colorTable = { 0: [255, 255, 255], 1: [0, 0, 0], 2: [255, 0, 0], 3: [255, 0, 0] }; const contentModeOptions = []; contentModeOptions[0] = ["filename","timetolive"]; contentModeOptions[1] = []; @@ -63,6 +65,7 @@ function connect() { if (msg.sys) { $('#sysinfo').innerHTML = 'free heap: ' + msg.sys.heap + ' bytes ┇ db size: ' + msg.sys.dbsize + ' bytes ┇ db record count: ' + msg.sys.recordcount + ' ┇ littlefs free: ' + msg.sys.littlefsfree + ' bytes'; servertimediff = (Date.now() / 1000) - msg.sys.currtime; + console.log("timediff: " + servertimediff); } }); @@ -82,13 +85,10 @@ function processTags(tagArray) { div = $('#tagtemplate').cloneNode(true); div.setAttribute('id', 'tag'+tagmac); div.dataset.mac = tagmac; + div.dataset.hwtype = -1; $('#taglist').appendChild(div); $('#tag' + tagmac + ' .mac').innerHTML = tagmac; - var img = $('#tag' + tagmac + ' .tagimg'); - img.addEventListener('error', function handleError() { - img.style.display = 'none'; - }); } div.style.display = 'block'; @@ -97,10 +97,9 @@ function processTags(tagArray) { if (!alias) alias = tagmac; $('#tag' + tagmac + ' .alias').innerHTML = alias; - if (div.dataset.hash != element.hash) loadImage(tagmac, '/current/' + tagmac + '.bmp?' + (new Date()).getTime()); - $('#tag' + tagmac + ' .contentmode').innerHTML = contentModes[element.contentMode]; if (element.RSSI) { + div.dataset.hwtype = element.hwType; $('#tag' + tagmac + ' .model').innerHTML = models[element.hwType]; $('#tag' + tagmac + ' .rssi').innerHTML = element.RSSI; $('#tag' + tagmac + ' .lqi').innerHTML = element.LQI; @@ -112,6 +111,11 @@ function processTags(tagArray) { $('#tag' + tagmac + ' .received').style.opacity = "0"; } + if (div.dataset.hash != element.hash && div.dataset.hwtype > -1) { + loadImage(tagmac, '/current/' + tagmac + '.raw?' + (new Date()).getTime()); + div.dataset.hash = element.hash; + } + if (element.nextupdate > 1672531200 && element.nextupdate!=3216153600) { var date = new Date(element.nextupdate * 1000); var options = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; @@ -130,7 +134,6 @@ function processTags(tagArray) { $('#tag' + tagmac + ' .lastseen').style.color = "black"; div.classList.remove("tagpending"); div.dataset.lastseen = element.lastseen; - div.dataset.hash = element.hash; div.dataset.wakeupreason = element.wakeupReason; $('#tag' + tagmac + ' .warningicon').style.display = 'none'; $('#tag' + tagmac).style.background = "inherit"; @@ -174,9 +177,9 @@ function updatecards() { let tagmac = item.dataset.mac; if (item.dataset.lastseen && item.dataset.lastseen > 1672531200) { - let idletime = (Date.now() / 1000) + servertimediff - item.dataset.lastseen; + let idletime = (Date.now() / 1000) - servertimediff - item.dataset.lastseen; $('#tag' + tagmac + ' .lastseen').innerHTML = "last seen"+displayTime(Math.floor(idletime))+" ago"; - if ((Date.now() / 1000) + servertimediff - 300 > item.dataset.nextcheckin) { + if ((Date.now() / 1000) - servertimediff - 300 > item.dataset.nextcheckin) { $('#tag' + tagmac + ' .warningicon').style.display='inline-block'; $('#tag' + tagmac).classList.remove("tagpending") $('#tag' + tagmac).style.background = '#ffffcc'; @@ -190,7 +193,7 @@ function updatecards() { } if (item.dataset.nextcheckin > 1672531200 && parseInt(item.dataset.wakeupreason)==0) { - let nextcheckin = item.dataset.nextcheckin - ((Date.now() / 1000) + servertimediff); + let nextcheckin = item.dataset.nextcheckin - ((Date.now() / 1000) - servertimediff); $('#tag' + tagmac + ' .nextcheckin').innerHTML = "expected checkin" + displayTime(Math.floor(nextcheckin)); } }) @@ -338,16 +341,36 @@ function processQueue() { } isProcessing = true; const { id, imageSrc } = imageQueue.shift(); - const image = $('#tag' + id + ' .tagimg'); - image.onload = function () { - image.style.display = 'block'; - processQueue(); - } - image.onerror = function () { - image.style.display = 'none'; - processQueue(); - }; - image.src = imageSrc; + const canvas = $('#tag' + id + ' .tagimg'); + const hwtype = $('#tag' + id).dataset.hwtype; + + fetch(imageSrc) + .then(response => response.arrayBuffer()) + .then(buffer => { + [canvas.width, canvas.height] = displaySizeLookup[hwtype] || [0,0]; + const ctx = canvas.getContext('2d'); + const imageData = ctx.createImageData(canvas.width, canvas.height); + const data = new Uint8ClampedArray(buffer); + const offsetRed = (data.length >= (canvas.width * canvas.height / 8) * 2) ? canvas.width * canvas.height / 8 : 0; + var pixelValue = 0; + for (let i = 0; i < data.length; i++) { + for (let j = 0; j < 8; j++) { + const pixelIndex = i * 8 + j; + if (offsetRed) { + pixelValue = ((data[i] & (1 << (7 - j))) ? 1 : 0) | (((data[i + offsetRed] & (1 << (7 - j))) ? 1 : 0) << 1); + } else { + pixelValue = ((data[i] & (1 << (7 - j))) ? 1 : 0); + } + imageData.data[pixelIndex * 4] = colorTable[pixelValue][0]; + imageData.data[pixelIndex * 4 + 1] = colorTable[pixelValue][1]; + imageData.data[pixelIndex * 4 + 2] = colorTable[pixelValue][2]; + imageData.data[pixelIndex * 4 + 3] = 255; + } + } + + ctx.putImageData(imageData, 0, 0); + processQueue(); + }); } function displayTime(seconds) { diff --git a/esp32_fw/src/contentmanager.cpp b/esp32_fw/src/contentmanager.cpp index 556d249a..95a3b58d 100644 --- a/esp32_fw/src/contentmanager.cpp +++ b/esp32_fw/src/contentmanager.cpp @@ -14,6 +14,10 @@ #include "makeimage.h" #include "web.h" +#define PAL_BLACK 0 +#define PAL_WHITE 9 +#define PAL_RED 2 + enum contentModes { Image, Today, @@ -27,7 +31,6 @@ enum contentModes { RSSFeed, }; - void contentRunner() { time_t now; time(&now); @@ -78,10 +81,9 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { switch (taginfo->contentMode) { case Image: - filename = cfgobj["filename"].as(); - //***FIXME... convert jpg to raw - if (filename && filename !="null" && !cfgobj["#fetched"].as()) { - if (prepareDataAvail(&filename, DATATYPE_IMG_BMP, mac, cfgobj["timetolive"].as())) { + if (cfgobj["filename"].as() && cfgobj["filename"].as() != "null" && !cfgobj["#fetched"].as()) { + jpg2buffer(cfgobj["filename"].as(), filename); + if (prepareDataAvail(&filename, DATATYPE_IMG_RAW_2BPP, mac, cfgobj["timetolive"].as())) { cfgobj["#fetched"] = true; } else { wsErr("Error accessing " + filename); @@ -189,21 +191,21 @@ bool updateTagImage(String &filename, uint8_t *dst, uint16_t nextCheckin) { } void drawString(TFT_eSprite &spr, String content, uint16_t posx, uint16_t posy, String font, byte align,uint16_t color) { - // drawString(spr,"test",100,10,"bahnschrift30",TC_DATUM,TFT_RED); + // drawString(spr,"test",100,10,"bahnschrift30",TC_DATUM,PAL_RED); spr.setTextDatum(align); if (font != "") spr.loadFont(font, LittleFS); - spr.setTextColor(color, TFT_WHITE); + spr.setTextColor(color, PAL_WHITE); spr.drawString(content, posx, posy); if (font != "") spr.unloadFont(); } void initSprite(TFT_eSprite &spr, int w, int h) { - spr.setColorDepth(8); + spr.setColorDepth(4); // 4 bits per pixel, uses indexed color spr.createSprite(w, h); if (spr.getPointer() == nullptr) { wsErr("Failed to create sprite"); } - spr.fillSprite(TFT_WHITE); + spr.fillSprite(PAL_WHITE); } void drawDate(String &filename, tagRecord *&taginfo) { @@ -225,7 +227,7 @@ void drawDate(String &filename, tagRecord *&taginfo) { if (taginfo->hwType == SOLUM_29_033) { initSprite(spr,296,128); - drawString(spr, Dag[timeinfo.tm_wday], 296 / 2, 10, "fonts/calibrib62", TC_DATUM, TFT_RED); + drawString(spr, Dag[timeinfo.tm_wday], 296 / 2, 10, "fonts/calibrib62", TC_DATUM, PAL_RED); drawString(spr, String(timeinfo.tm_mday) + " " + Maand[timeinfo.tm_mon], 296 / 2, 73, "fonts/calibrib50", TC_DATUM); } else if (taginfo->hwType == SOLUM_154_033) { @@ -233,7 +235,7 @@ void drawDate(String &filename, tagRecord *&taginfo) { initSprite(spr, 152, 152); drawString(spr, Dag[timeinfo.tm_wday], 152 / 2, 10, "fonts/calibrib30", TC_DATUM); drawString(spr, String(Maand[timeinfo.tm_mon]), 152 / 2, 120, "fonts/calibrib30", TC_DATUM); - drawString(spr, String(timeinfo.tm_mday), 152 / 2, 42, "fonts/numbers2-1", TC_DATUM, TFT_RED); + drawString(spr, String(timeinfo.tm_mday), 152 / 2, 42, "fonts/numbers2-1", TC_DATUM, PAL_RED); } spr2buffer(spr, filename); @@ -251,9 +253,9 @@ void drawNumber(String &filename, int32_t count, int32_t thresholdred, tagRecord initSprite(spr, 296, 128); spr.setTextDatum(MC_DATUM); if (count > thresholdred) { - spr.setTextColor(TFT_RED, TFT_WHITE); + spr.setTextColor(PAL_RED, PAL_WHITE); } else { - spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.setTextColor(PAL_BLACK, PAL_WHITE); } String font = "fonts/numbers1-2"; if (count > 999) font = "fonts/numbers2-2"; @@ -267,15 +269,15 @@ void drawNumber(String &filename, int32_t count, int32_t thresholdred, tagRecord initSprite(spr, 152, 152); spr.setTextDatum(MC_DATUM); if (count > thresholdred) { - spr.setTextColor(TFT_RED, TFT_WHITE); + spr.setTextColor(PAL_RED, PAL_WHITE); } else { - spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.setTextColor(PAL_BLACK, PAL_WHITE); } String font = "fonts/numbers1-1"; if (count > 99) font = "fonts/numbers2-1"; if (count > 999) font = "fonts/numbers3-1"; spr.loadFont(font, LittleFS); - spr.drawString(String(count), 152 / 2, 152 / 2 + 10); + spr.drawString(String(count), 152 / 2, 152 / 2 + 7); spr.unloadFont(); } @@ -348,17 +350,17 @@ void drawWeather(String &filename, String location, tagRecord *&taginfo) { initSprite(spr, 296, 128); drawString(spr, location, 5, 5, "fonts/bahnschrift30"); - drawString(spr, String(wind), 280, 5, "fonts/bahnschrift30", TR_DATUM, (wind > 4 ? TFT_RED : TFT_BLACK)); + drawString(spr, String(wind), 280, 5, "fonts/bahnschrift30", TR_DATUM, (wind > 4 ? PAL_RED : PAL_BLACK)); char tmpOutput[5]; dtostrf(temperature, 2, 1, tmpOutput); - drawString(spr, String(tmpOutput), 5, 65, "fonts/bahnschrift70", TL_DATUM, (temperature < 0 ? TFT_RED : TFT_BLACK)); + drawString(spr, String(tmpOutput), 5, 65, "fonts/bahnschrift70", TL_DATUM, (temperature < 0 ? PAL_RED : PAL_BLACK)); spr.loadFont("fonts/weathericons70", LittleFS); if (weathercode == 55 || weathercode == 65 || weathercode == 75 || weathercode == 82 || weathercode == 86 || weathercode == 95 || weathercode == 99) { - spr.setTextColor(TFT_RED, TFT_WHITE); + spr.setTextColor(PAL_RED, PAL_WHITE); } else { - spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.setTextColor(PAL_BLACK, PAL_WHITE); } spr.setCursor(185, 32); @@ -366,11 +368,11 @@ void drawWeather(String &filename, String location, tagRecord *&taginfo) { spr.unloadFont(); spr.loadFont("fonts/weathericons30", LittleFS); - spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.setTextColor(PAL_BLACK, PAL_WHITE); spr.setCursor(235, -3); spr.printToSprite(windDirectionIcon(winddirection)); if (weathercode > 10) { - spr.setTextColor(TFT_RED, TFT_WHITE); + spr.setTextColor(PAL_RED, PAL_WHITE); spr.setCursor(190, 0); spr.printToSprite("\uf084"); } @@ -382,32 +384,32 @@ void drawWeather(String &filename, String location, tagRecord *&taginfo) { spr.setTextDatum(TL_DATUM); spr.setTextFont(2); - spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.setTextColor(PAL_BLACK, PAL_WHITE); spr.drawString(location, 10, 130); char tmpOutput[5]; dtostrf(temperature, 2, 1, tmpOutput); - drawString(spr, String(tmpOutput), 10, 10, "fonts/bahnschrift30", TL_DATUM, (temperature < 0 ? TFT_RED : TFT_BLACK)); + drawString(spr, String(tmpOutput), 10, 10, "fonts/bahnschrift30", TL_DATUM, (temperature < 0 ? PAL_RED : PAL_BLACK)); spr.loadFont("fonts/weathericons78", LittleFS); if (weathercode == 55 || weathercode == 65 || weathercode == 75 || weathercode == 82 || weathercode == 86 || weathercode == 95 || weathercode == 99) { - spr.setTextColor(TFT_RED, TFT_WHITE); + spr.setTextColor(PAL_RED, PAL_WHITE); } else { - spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.setTextColor(PAL_BLACK, PAL_WHITE); } spr.setCursor(30, 33); spr.printToSprite(weatherIcons[weathercode]); spr.unloadFont(); - drawString(spr, String(wind), 140, 10, "fonts/bahnschrift30", TR_DATUM, (wind > 4 ? TFT_RED : TFT_BLACK)); + drawString(spr, String(wind), 140, 10, "fonts/bahnschrift30", TR_DATUM, (wind > 4 ? PAL_RED : PAL_BLACK)); spr.loadFont("fonts/weathericons30", LittleFS); - spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.setTextColor(PAL_BLACK, PAL_WHITE); spr.setCursor(100, -2); spr.printToSprite(windDirectionIcon(winddirection)); if (weathercode > 10) { - spr.setTextColor(TFT_RED, TFT_WHITE); + spr.setTextColor(PAL_RED, PAL_WHITE); spr.setCursor(115, 110); spr.printToSprite("\uf084"); } @@ -475,28 +477,28 @@ void drawForecast(String &filename, String location, tagRecord *&taginfo) { initSprite(spr, 296, 128); spr.setTextFont(2); - spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.setTextColor(PAL_BLACK, PAL_WHITE); spr.drawString(location, 5, 0); for (uint8_t dag = 0; dag < 5; dag++) { time_t weatherday = doc["daily"]["time"][dag].as(); struct tm *datum = localtime(&weatherday); - drawString(spr, String(weekday_name[datum->tm_wday]), dag * 59 + 30, 18, "fonts/twbold20", TC_DATUM, TFT_BLACK); + drawString(spr, String(weekday_name[datum->tm_wday]), dag * 59 + 30, 18, "fonts/twbold20", TC_DATUM, PAL_BLACK); uint8_t weathercode = doc["daily"]["weathercode"][dag].as(); if (weathercode > 40) weathercode -= 40; spr.loadFont("fonts/weathericons30", LittleFS); if (weathercode == 55 || weathercode == 65 || weathercode == 75 || weathercode == 82 || weathercode == 86 || weathercode == 95 || weathercode == 99) { - spr.setTextColor(TFT_RED, TFT_WHITE); + spr.setTextColor(PAL_RED, PAL_WHITE); } else { - spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.setTextColor(PAL_BLACK, PAL_WHITE); } spr.setTextDatum(TL_DATUM); spr.setCursor(12 + dag * 59, 58); spr.printToSprite(weatherIcons[weathercode]); - spr.setTextColor(TFT_BLACK, TFT_WHITE); + spr.setTextColor(PAL_BLACK, PAL_WHITE); spr.setCursor(17 + dag * 59, 27); spr.printToSprite(windDirectionIcon(doc["daily"]["winddirection_10m_dominant"][dag])); spr.unloadFont(); @@ -506,13 +508,13 @@ void drawForecast(String &filename, String location, tagRecord *&taginfo) { uint8_t wind = windSpeedToBeaufort(doc["daily"]["windspeed_10m_max"][dag].as()); spr.loadFont("fonts/GillSC20", LittleFS); - drawString(spr, String(tmin) + " ", dag * 59 + 30, 108, "", TR_DATUM, (tmin < 0 ? TFT_RED : TFT_BLACK)); - drawString(spr, String(" ") + String(tmax), dag * 59 + 30, 108, "", TL_DATUM, (tmax < 0 ? TFT_RED : TFT_BLACK)); - drawString(spr, String(" ") + String(wind), dag * 59 + 30, 43, "", TL_DATUM, (wind > 5 ? TFT_RED : TFT_BLACK)); + drawString(spr, String(tmin) + " ", dag * 59 + 30, 108, "", TR_DATUM, (tmin < 0 ? PAL_RED : PAL_BLACK)); + drawString(spr, String(" ") + String(tmax), dag * 59 + 30, 108, "", TL_DATUM, (tmax < 0 ? PAL_RED : PAL_BLACK)); + drawString(spr, String(" ") + String(wind), dag * 59 + 30, 43, "", TL_DATUM, (wind > 5 ? PAL_RED : PAL_BLACK)); spr.unloadFont(); if (dag>0) { for (int i = 20; i < 128; i+=3) { - spr.drawPixel(dag * 59, i, TFT_BLACK); + spr.drawPixel(dag * 59, i, PAL_BLACK); } } } @@ -534,12 +536,12 @@ void drawIdentify(String &filename, tagRecord *&taginfo) { if (taginfo->hwType == SOLUM_29_033) { initSprite(spr, 296, 128); drawString(spr, taginfo->alias, 10, 10, "fonts/bahnschrift20"); - drawString(spr, mac62hex(taginfo->mac), 10, 50, "fonts/bahnschrift20", TL_DATUM, TFT_RED); + drawString(spr, mac62hex(taginfo->mac), 10, 50, "fonts/bahnschrift20", TL_DATUM, PAL_RED); } else if (taginfo->hwType == SOLUM_154_033) { initSprite(spr, 152, 152); drawString(spr, taginfo->alias, 5, 5, "fonts/bahnschrift20"); - drawString(spr, mac62hex(taginfo->mac), 10, 50, "fonts/bahnschrift20", TL_DATUM, TFT_RED); + drawString(spr, mac62hex(taginfo->mac), 10, 50, "fonts/bahnschrift20", TL_DATUM, PAL_RED); } spr2buffer(spr, filename); @@ -602,13 +604,13 @@ bool getRSSfeed(String &filename, String URL, String title, tagRecord *&taginfo) if (taginfo->hwType == SOLUM_29_033) { initSprite(spr, 296, 128); if (title=="" || title=="null") title="RSS feed"; - drawString(spr, title, 5, 3, "fonts/bahnschrift20", TL_DATUM, TFT_RED); + drawString(spr, title, 5, 3, "fonts/bahnschrift20", TL_DATUM, PAL_RED); u8f.setFont(u8g2_font_glasstown_nbp_tr); // select u8g2 font from here: https://github.com/olikraus/u8g2/wiki/fntlistall u8f.setFontMode(0); u8f.setFontDirection(0); - u8f.setForegroundColor(TFT_BLACK); - u8f.setBackgroundColor(TFT_WHITE); + u8f.setForegroundColor(PAL_BLACK); + u8f.setBackgroundColor(PAL_WHITE); u8f.setCursor(220, 20); u8f.print(header); diff --git a/esp32_fw/src/makeimage.cpp b/esp32_fw/src/makeimage.cpp index 42261932..28d89016 100644 --- a/esp32_fw/src/makeimage.cpp +++ b/esp32_fw/src/makeimage.cpp @@ -20,6 +20,9 @@ void jpg2buffer(String filein, String fileout) { TJpgDec.setJpgScale(1); TJpgDec.setCallback(spr_output); uint16_t w = 0, h = 0; + if (filein.c_str()[0] != '/') { + filein = "/" + filein; + } TJpgDec.getFsJpgSize(&w, &h, filein, LittleFS); Serial.println("jpeg conversion " + String(w) + "x" + String(h)); @@ -245,6 +248,26 @@ void spr2grays(TFT_eSprite &spr, long w, long h, String &fileout) { Serial.println("finished writing BMP " + String(millis() - t) + "ms"); } +struct Color { + uint8_t r, g, b; + Color() : r(0), g(0), b(0) {} + Color(uint16_t value_) : r((value_ >> 8) & 0xF8 | (value_ >> 13) & 0x07), g((value_ >> 3) & 0xFC | (value_ >> 9) & 0x03), b((value_ << 3) & 0xF8 | (value_ >> 2) & 0x07) {} + Color(uint8_t r_, uint8_t g_, uint8_t b_) : r(r_), g(g_), b(b_) {} +}; + +struct Error { + float r; + float g; + float b; +}; + +uint32_t colorDistance(const Color &c1, const Color &c2, const Error &e1) { + float r_diff = c1.r + e1.r - c2.r; + float g_diff = c1.g + e1.g - c2.g; + float b_diff = c1.b + e1.b - c2.b; + return round(r_diff * r_diff + g_diff * g_diff + b_diff * b_diff); +} + void spr2buffer(TFT_eSprite &spr, String &fileout) { long t = millis(); LittleFS.begin(); @@ -261,27 +284,72 @@ void spr2buffer(TFT_eSprite &spr, String &fileout) { } int bufferSize = (bufw * bufh) / 8; - uint16_t color; uint8_t *blackBuffer = new uint8_t[bufferSize]; uint8_t *redBuffer = new uint8_t[bufferSize]; memset(blackBuffer, 0, bufferSize); memset(redBuffer, 0, bufferSize); + std::vector palette = { + {255, 255, 255}, // White + {0, 0, 0}, // Black + {255, 0, 0} // Red + }; + int num_colors = palette.size(); + Color color; + Error error_bufferold[bufw]; + Error error_buffernew[bufw]; + + memset(error_bufferold, 0, sizeof(error_bufferold)); for (uint16_t y = 0; y < bufh; y++) { + memset(error_buffernew, 0, sizeof(error_buffernew)); for (uint16_t x = 0; x < bufw; x++) { if (rotated) { - color = spr.readPixel(bufh - 1 - y, x); + color = Color(spr.readPixel(bufh - 1 - y, x)); } else { - color = spr.readPixel(x, y); + color = Color(spr.readPixel(x, y)); } + + int best_color_index = 0; + uint32_t best_color_distance = colorDistance(color, palette[0], error_bufferold[x]); + for (int i = 1; i < num_colors; i++) { + uint32_t distance = colorDistance(color, palette[i], error_bufferold[x]); + if (distance < best_color_distance) { + best_color_distance = distance; + best_color_index = i; + } + } + uint16_t bitIndex = 7 - (x % 8); uint16_t byteIndex = (y * bufw + x) / 8; - if (color == TFT_BLACK) { + if (best_color_index & 1) { blackBuffer[byteIndex] |= (1 << bitIndex); - } else if (color == TFT_RED) { + } else if (best_color_index & 2) { redBuffer[byteIndex] |= (1 << bitIndex); } + + Error error = { + ((float)color.r + error_bufferold[x].r - palette[best_color_index].r) / 16.0f, + ((float)color.g + error_bufferold[x].g - palette[best_color_index].g) / 16.0f, + ((float)color.b + error_bufferold[x].b - palette[best_color_index].b) / 16.0f}; + + error_buffernew[x].r += error.r * 5.0f; + error_buffernew[x].g += error.g * 5.0f; + error_buffernew[x].b += error.b * 5.0f; + if (x > 0) { + error_buffernew[x - 1].r += error.r * 3.0f; + error_buffernew[x - 1].g += error.g * 3.0f; + error_buffernew[x - 1].b += error.b * 3.0f; + } + if (x < bufw - 1) { + error_buffernew[x + 1].r += error.r * 1.0f; + error_buffernew[x + 1].g += error.g * 1.0f; + error_buffernew[x + 1].b += error.b * 1.0f; + error_bufferold[x + 1].r += error.r * 7.0f; + error_bufferold[x + 1].g += error.g * 7.0f; + error_bufferold[x + 1].b += error.b * 7.0f; + } } + memcpy(error_bufferold, error_buffernew, sizeof(error_buffernew)); } f_out.write(blackBuffer, bufferSize);