diff --git a/ESP32_AP-Flasher/data/gradient.jpg b/ESP32_AP-Flasher/data/gradient.jpg new file mode 100644 index 00000000..34300c0a Binary files /dev/null and b/ESP32_AP-Flasher/data/gradient.jpg differ diff --git a/ESP32_AP-Flasher/data/www/content_cards.json b/ESP32_AP-Flasher/data/www/content_cards.json index 4012c520..5a35dd7f 100644 --- a/ESP32_AP-Flasher/data/www/content_cards.json +++ b/ESP32_AP-Flasher/data/www/content_cards.json @@ -344,5 +344,22 @@ "type": "text" } ] + }, + { + "id": 15, + "name": "Send custom LUT", + "desc": "EXPERIMENTAL. Don't use. YOU RISK DAMAGING YOUR SCREEN.", + "hwtype": [ + 1 + ], + "capabilities": 4, + "param": [ + { + "key": "bytes", + "name": "bytes", + "desc": "70 bytes, formatted as 0x00,0x00,...", + "type": "text" + } + ] } ] \ No newline at end of file diff --git a/ESP32_AP-Flasher/data/www/main.js b/ESP32_AP-Flasher/data/www/main.js index 25662212..0a979ae6 100644 --- a/ESP32_AP-Flasher/data/www/main.js +++ b/ESP32_AP-Flasher/data/www/main.js @@ -13,7 +13,7 @@ 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 colorTable = { 0: [255, 255, 255], 1: [0, 0, 0], 2: [255, 0, 0], 3: [255, 0, 0] }; +const colorTable = { 0: [255, 255, 255], 1: [0, 0, 0], 2: [255, 0, 0], 3: [150, 150, 150] }; const imageQueue = []; let isProcessing = false; @@ -64,7 +64,7 @@ function connect() { }); socket.addEventListener("message", (event) => { - //console.log(event.data) + console.log(event.data) const msg = JSON.parse(event.data); if (msg.logMsg) { showMessage(msg.logMsg, false); @@ -213,7 +213,7 @@ function updatecards() { if (item.dataset.lastseen && item.dataset.lastseen > 1672531200) { 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 - 600 > item.dataset.nextcheckin) { $('#tag' + tagmac + ' .warningicon').style.display = 'inline-block'; $('#tag' + tagmac).classList.remove("tagpending") $('#tag' + tagmac).style.background = '#e0e0a0'; diff --git a/ESP32_AP-Flasher/include/commstructs.h b/ESP32_AP-Flasher/include/commstructs.h index 3d02ec51..73732d25 100644 --- a/ESP32_AP-Flasher/include/commstructs.h +++ b/ESP32_AP-Flasher/include/commstructs.h @@ -48,6 +48,7 @@ struct espAvailDataReq { #define EPD_LUT_NO_REPEATS 1 #define EPD_LUT_FAST_NO_REDS 2 #define EPD_LUT_FAST 3 +#define EPD_LUT_OTA 0x10 struct AvailDataInfo { uint8_t checksum; diff --git a/ESP32_AP-Flasher/include/contentmanager.h b/ESP32_AP-Flasher/include/contentmanager.h index 3cbc0f2c..2af05e3e 100644 --- a/ESP32_AP-Flasher/include/contentmanager.h +++ b/ESP32_AP-Flasher/include/contentmanager.h @@ -33,5 +33,6 @@ char *formatHttpDate(time_t t); String urlEncode(const char *msg); int windSpeedToBeaufort(float windSpeed); String windDirectionIcon(int degrees); -String mac62hex(uint8_t *mac); void getLocation(JsonObject &cfgobj); +void prepareNFCReq(uint8_t* dst, const char* url); +void prepareLUTreq(uint8_t *dst, String input); \ No newline at end of file diff --git a/ESP32_AP-Flasher/include/makeimage.h b/ESP32_AP-Flasher/include/makeimage.h index b9369d18..165a2f2e 100644 --- a/ESP32_AP-Flasher/include/makeimage.h +++ b/ESP32_AP-Flasher/include/makeimage.h @@ -7,6 +7,8 @@ struct imgParam { bool hasRed; uint8_t dataType; bool dither; + bool grayLut = false; + char segments[12]; uint16_t symbols; bool invert; diff --git a/ESP32_AP-Flasher/include/newproto.h b/ESP32_AP-Flasher/include/newproto.h index e8758464..a0348c8f 100644 --- a/ESP32_AP-Flasher/include/newproto.h +++ b/ESP32_AP-Flasher/include/newproto.h @@ -5,7 +5,7 @@ extern bool checkCRC(void* p, uint8_t len); extern void processBlockRequest(struct espBlockRequest* br); extern void prepareIdleReq(uint8_t* dst, uint16_t nextCheckin); -extern void prepareNFCReq(uint8_t* dst, const char* url); +extern void prepareDataAvail(uint8_t* data, uint16_t len, uint8_t dataType, uint8_t* dst); extern bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t nextCheckin); extern void prepareExternalDataAvail(struct pendingData* pending, IPAddress remoteIP); extern void processXferComplete(struct espXferComplete* xfc, bool local); diff --git a/ESP32_AP-Flasher/include/tag_db.h b/ESP32_AP-Flasher/include/tag_db.h index 3ce6c409..b49ae093 100644 --- a/ESP32_AP-Flasher/include/tag_db.h +++ b/ESP32_AP-Flasher/include/tag_db.h @@ -13,7 +13,7 @@ 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), + 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) {} uint8_t mac[8]; @@ -36,7 +36,7 @@ class tagRecord { uint32_t lastfullupdate; bool isExternal; uint16_t pendingIdle; - + bool hasCustomLUT; 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 452f7e89..4db48a59 100644 --- a/ESP32_AP-Flasher/src/contentmanager.cpp +++ b/ESP32_AP-Flasher/src/contentmanager.cpp @@ -1,19 +1,30 @@ #include "contentmanager.h" +// possibility to turn off, to save space if needed +#define CONTENT_QR +#define CONTENT_RSS +#define CONTENT_CAL + #include #include #include #include #include +#ifdef CONTENT_RSS #include +#endif #include #include +#if defined CONTENT_RSS || defined CONTENT_CAL #include "U8g2_for_TFT_eSPI.h" +#endif #include "commstructs.h" #include "makeimage.h" #include "newproto.h" +#ifdef CONTENT_QR #include "qrcode.h" +#endif #include "tag_db.h" #include "settings.h" #include "web.h" @@ -95,6 +106,7 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { imageParams.hasRed = false; imageParams.dataType = DATATYPE_IMG_RAW_1BPP; imageParams.dither = true; + if (taginfo->hasCustomLUT) imageParams.grayLut = true; imageParams.invert = false; imageParams.symbols = 0; @@ -108,7 +120,7 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { if (imageParams.hasRed) imageParams.dataType = DATATYPE_IMG_RAW_2BPP; if (prepareDataAvail(&filename, imageParams.dataType, mac, cfgobj["timetolive"].as())) { cfgobj["#fetched"] = true; - if (cfgobj["delete"].as()) LittleFS.remove("/"+cfgobj["filename"].as()); + if (cfgobj["delete"].as()=="1") LittleFS.remove("/"+cfgobj["filename"].as()); } else { wsErr("Error accessing " + filename); } @@ -237,10 +249,15 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) { case 14: // NFC URL taginfo->nextupdate = 3216153600; - memset(taginfo->md5, 0, 16 * sizeof(uint8_t)); - memset(taginfo->md5pending, 0, 16 * sizeof(uint8_t)); prepareNFCReq(mac, cfgobj["url"].as()); break; + + case 15: // send gray LUT + + taginfo->nextupdate = 3216153600; + prepareLUTreq(mac, cfgobj["bytes"]); + taginfo->hasCustomLUT = true; + break; } taginfo->modeConfigJson = doc.as(); @@ -783,9 +800,12 @@ bool getImgURL(String &filename, String URL, time_t fetched, imgParam &imagePara return (httpCode == 200 || httpCode == 304); } +#ifdef CONTENT_RSS rssClass reader; +#endif bool getRssFeed(String &filename, String URL, String title, tagRecord *&taginfo, imgParam &imageParams) { +#ifdef CONTENT_RSS // https://github.com/garretlab/shoddyxml2 // http://feeds.feedburner.com/tweakers/nieuws @@ -854,6 +874,7 @@ bool getRssFeed(String &filename, String URL, String title, tagRecord *&taginfo, spr2buffer(spr, filename, imageParams); spr.deleteSprite(); +#endif return true; } @@ -878,6 +899,7 @@ char *epoch_to_display(time_t utc) { } bool getCalFeed(String &filename, String URL, String title, tagRecord *&taginfo, imgParam &imageParams) { +#ifdef CONTENT_CAL // google apps scripts method to retrieve calendar // see /data/calendar.txt for description @@ -988,11 +1010,12 @@ bool getCalFeed(String &filename, String URL, String title, tagRecord *&taginfo, spr2buffer(spr, filename, imageParams); spr.deleteSprite(); - +#endif return true; } void drawQR(String &filename, String qrcontent, String title, tagRecord *&taginfo, imgParam &imageParams) { +#ifdef CONTENT_QR TFT_eSPI tft = TFT_eSPI(); TFT_eSprite spr = TFT_eSprite(&tft); LittleFS.begin(); @@ -1037,6 +1060,7 @@ void drawQR(String &filename, String qrcontent, String title, tagRecord *&taginf spr2buffer(spr, filename, imageParams); spr.deleteSprite(); +#endif } char *formatHttpDate(time_t t) { @@ -1111,4 +1135,41 @@ void getLocation(JsonObject &cfgobj) { cfgobj["#tz"] = tz; } } -} \ No newline at end of file +} + +void prepareNFCReq(uint8_t *dst, const char *url) { + uint8_t *data; + size_t len = strlen(url); + data = new uint8_t[len + 8]; + + // TLV + data[0] = 0x03; // NDEF message (TLV type) + data[1] = 4 + len + 1; + // ndef record + data[2] = 0xD1; + data[3] = 0x01; // well known record type + data[4] = len + 1; // payload length + data[5] = 0x55; // payload type (URI record) + data[6] = 0x00; // URI identifier code (no prepending) + + memcpy(data + 7, reinterpret_cast(url), len); + len = 7 + len; + data[len] = 0xFE; + len = 1 + len; + prepareDataAvail(data, len, DATATYPE_NFC_RAW_CONTENT, dst); +} + +void prepareLUTreq(uint8_t *dst, String input) { + const char *delimiters = ", \t"; + const int maxValues = 70; + uint8_t waveform[maxValues]; + char *ptr = strtok(const_cast(input.c_str()), delimiters); + int i = 0; + while (ptr != nullptr && i < maxValues) { + waveform[i++] = static_cast(strtol(ptr, nullptr, 16)); + ptr = strtok(nullptr, delimiters); + } + Serial.println(String(i) + " bytes found"); + size_t waveformLen = sizeof(waveform); + prepareDataAvail(waveform, waveformLen, DATATYPE_CUSTOM_LUT_OTA, dst); +} diff --git a/ESP32_AP-Flasher/src/makeimage.cpp b/ESP32_AP-Flasher/src/makeimage.cpp index 3408b6b6..168af98f 100644 --- a/ESP32_AP-Flasher/src/makeimage.cpp +++ b/ESP32_AP-Flasher/src/makeimage.cpp @@ -116,6 +116,11 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) { {0, 0, 0}, // Black {255, 0, 0} // Red }; + if (imageParams.grayLut) { + Color newColor = {150, 150, 150}; + palette.push_back(newColor); + Serial.println("rendering with gray"); + } int num_colors = palette.size(); Color color; Error *error_bufferold = new Error[bufw]; @@ -143,13 +148,40 @@ void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) { uint16_t bitIndex = 7 - (x % 8); uint16_t byteIndex = (y * bufw + x) / 8; + + // this looks a bit ugly, but it's performing better than shorter notations + switch (best_color_index) { + case 1: + blackBuffer[byteIndex] |= (1 << bitIndex); + break; + case 2: + imageParams.hasRed = true; + redBuffer[byteIndex] |= (1 << bitIndex); + break; + case 3: + blackBuffer[byteIndex] |= (1 << bitIndex); + redBuffer[byteIndex] |= (1 << bitIndex); + imageParams.hasRed = true; + break; + } + /* + alt 1: + if (best_color_index & 1) { blackBuffer[byteIndex] |= (1 << bitIndex); - } else if (best_color_index & 2) { + } + if (best_color_index & 2) { imageParams.hasRed = true; redBuffer[byteIndex] |= (1 << bitIndex); } + alt 2: + + blackBuffer[byteIndex] |= ((best_color_index & 1) << bitIndex); + redBuffer[byteIndex] |= ((best_color_index & 2) << bitIndex); + imageParams.hasRed |= (best_color_index & 2); + */ + if (imageParams.dither) { Error error = { ((float)color.r + error_bufferold[x].r - palette[best_color_index].r) / 16.0f, diff --git a/ESP32_AP-Flasher/src/newproto.cpp b/ESP32_AP-Flasher/src/newproto.cpp index 4bbfd12d..ea9ecb76 100644 --- a/ESP32_AP-Flasher/src/newproto.cpp +++ b/ESP32_AP-Flasher/src/newproto.cpp @@ -79,7 +79,7 @@ void prepareIdleReq(uint8_t* dst, uint16_t nextCheckin) { } } -void prepareNFCReq(uint8_t* dst, const char* url) { +void prepareDataAvail(uint8_t* data, uint16_t len, uint8_t dataType, uint8_t* dst) { tagRecord* taginfo = nullptr; taginfo = tagRecord::findByMAC(dst); if (taginfo == nullptr) { @@ -88,32 +88,22 @@ void prepareNFCReq(uint8_t* dst, const char* url) { } clearPending(taginfo); - size_t len = strlen(url); - taginfo->data = new uint8_t[len + 8]; - - // TLV - taginfo->data[0] = 0x03; // NDEF message (TLV type) - taginfo->data[1] = 4 + len + 1; - - // ndef record - taginfo->data[2] = 0xD1; - taginfo->data[3] = 0x01; // well known record type - taginfo->data[4] = len + 1; // payload length - taginfo->data[5] = 0x55; // payload type (URI record) - taginfo->data[6] = 0x00; // URI identifier code (no prepending) - - memcpy(taginfo->data + 7, reinterpret_cast(url), len); - len = 7 + len; - taginfo->data[len] = 0xFE; - len = 1 + len; - + taginfo->data = (uint8_t*)malloc(len); + if (taginfo->data == nullptr) { + wsErr("no memory allocation for data"); + return; + } + memcpy(taginfo->data, data, len); taginfo->pending = true; taginfo->len = len; + taginfo->expectedNextCheckin = 0; + taginfo->filename = String(); + memset(taginfo->md5pending, 0, 16 * sizeof(uint8_t)); struct pendingData pending = {0}; memcpy(pending.targetMac, dst, 8); pending.availdatainfo.dataSize = len; - pending.availdatainfo.dataType = DATATYPE_NFC_RAW_CONTENT; + pending.availdatainfo.dataType = dataType; pending.availdatainfo.nextCheckIn = 0; pending.availdatainfo.dataVer = millis(); pending.attemptsLeft = 10; @@ -182,6 +172,10 @@ bool prepareDataAvail(String* filename, uint8_t dataType, uint8_t* dst, uint16_t lut = EPD_LUT_DEFAULT; // full update once a day taginfo->lastfullupdate = now; } + if (taginfo->hasCustomLUT && taginfo->capabilities & CAPABILITY_SUPPORTS_CUSTOM_LUTS) { + Serial.println("using custom LUT"); + lut = EPD_LUT_OTA; + } if (dataType != DATATYPE_FW_UPDATE) { char dst_path[64];