From 36c3c455103b33a50df1b1fe4c615d3f34b92724 Mon Sep 17 00:00:00 2001 From: Moritz Wirger Date: Fri, 8 Sep 2023 11:06:15 +0200 Subject: [PATCH] Json url file template (#128) json template url + file implementation. The file contains the json template, and can contain variables that will be extracted from the json in the url. The url is fetched at regular intervals. --- ESP32_AP-Flasher/include/contentmanager.h | 3 +- ESP32_AP-Flasher/include/util.h | 4 +- ESP32_AP-Flasher/src/contentmanager.cpp | 187 ++++++++++++++++++++-- 3 files changed, 181 insertions(+), 13 deletions(-) diff --git a/ESP32_AP-Flasher/include/contentmanager.h b/ESP32_AP-Flasher/include/contentmanager.h index 6639b84a..e9c9b79a 100644 --- a/ESP32_AP-Flasher/include/contentmanager.h +++ b/ESP32_AP-Flasher/include/contentmanager.h @@ -31,7 +31,8 @@ bool getCalFeed(String &filename, String URL, String title, tagRecord *&taginfo, void drawQR(String &filename, String qrcontent, String title, tagRecord *&taginfo, imgParam &imageParams); uint8_t drawBuienradar(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgParam &imageParams); void drawAPinfo(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgParam &imageParams); -int getJsonTemplateFile(String &filename, String jsonfile, tagRecord *&taginfo, imgParam &imageParams); +bool getJsonTemplateFile(String &filename, String jsonfile, tagRecord *&taginfo, imgParam &imageParams); +extern bool getJsonTemplateFileExtractVariables(String &filename, String jsonfile, JsonDocument &variables, tagRecord *&taginfo, imgParam &imageParams); int getJsonTemplateUrl(String &filename, String URL, time_t fetched, String MAC, tagRecord *&taginfo, imgParam &imageParams); void drawJsonStream(Stream &stream, String &filename, tagRecord *&taginfo, imgParam &imageParams); void drawElement(const JsonObject &element, TFT_eSprite &spr); diff --git a/ESP32_AP-Flasher/include/util.h b/ESP32_AP-Flasher/include/util.h index 551c4cb5..4eebe187 100644 --- a/ESP32_AP-Flasher/include/util.h +++ b/ESP32_AP-Flasher/include/util.h @@ -78,7 +78,7 @@ static bool httpGetJson(String &url, JsonDocument &json, const uint16_t timeout, const int httpCode = http.GET(); if (httpCode != 200) { http.end(); - wsErr("http " + httpCode); + wsErr(String("[httpGetJson] http code") + httpCode); return false; } @@ -90,7 +90,7 @@ static bool httpGetJson(String &url, JsonDocument &json, const uint16_t timeout, } http.end(); if (error) { - Serial.println(error.c_str()); + Serial.printf("[httpGetJson] JSON: %s\n", error.c_str()); return false; } return true; diff --git a/ESP32_AP-Flasher/src/contentmanager.cpp b/ESP32_AP-Flasher/src/contentmanager.cpp index b5686839..b2b8f74e 100644 --- a/ESP32_AP-Flasher/src/contentmanager.cpp +++ b/ESP32_AP-Flasher/src/contentmanager.cpp @@ -377,13 +377,31 @@ void drawNew(const uint8_t mac[8], const bool buttonPressed, tagRecord *&taginfo { const String configFilename = cfgobj["filename"].as(); if (!util::isEmptyOrNull(configFilename)) { - const int result = getJsonTemplateFile(filename, configFilename, taginfo, imageParams); - if (result) { - updateTagImage(filename, mac, cfgobj["interval"].as(), taginfo, imageParams); + String configUrl = cfgobj["url"].as(); + if (!util::isEmptyOrNull(configUrl)) { + StaticJsonDocument<1000> json; + Serial.println("Get json url + file"); + if (util::httpGetJson(configUrl, json, 1000)) { + if (getJsonTemplateFileExtractVariables(filename, configFilename, json, taginfo, imageParams)) { + updateTagImage(filename, mac, cfgobj["interval"].as(), taginfo, imageParams); + } else { + wsErr("error opening file " + configFilename); + } + const int interval = cfgobj["interval"].as(); + taginfo->nextupdate = now + 60 * (interval < 3 ? 15 : interval); + } else { + taginfo->nextupdate = now + 600; + } + } else { - wsErr("error opening file " + configFilename); + const bool result = getJsonTemplateFile(filename, configFilename, taginfo, imageParams); + if (result) { + updateTagImage(filename, mac, cfgobj["interval"].as(), taginfo, imageParams); + } else { + wsErr("error opening file " + configFilename); + } + taginfo->nextupdate = 3216153600; } - taginfo->nextupdate = 3216153600; } else { const int httpcode = getJsonTemplateUrl(filename, cfgobj["url"], (time_t)cfgobj["#fetched"], String(hexmac), taginfo, imageParams); const int interval = cfgobj["interval"].as(); @@ -541,11 +559,10 @@ void drawDate(String &filename, tagRecord *&taginfo, imgParam &imageParams) { return; } - TFT_eSprite spr = TFT_eSprite(&tft); - StaticJsonDocument<512> loc; getTemplate(loc, 1, taginfo->hwType); + TFT_eSprite spr = TFT_eSprite(&tft); initSprite(spr, imageParams.width, imageParams.height, imageParams); const auto &date = loc["date"]; @@ -1099,7 +1116,7 @@ void drawAPinfo(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgPa spr.deleteSprite(); } -int getJsonTemplateFile(String &filename, String jsonfile, tagRecord *&taginfo, imgParam &imageParams) { +bool getJsonTemplateFile(String &filename, String jsonfile, tagRecord *&taginfo, imgParam &imageParams) { if (jsonfile.c_str()[0] != '/') { jsonfile = "/" + jsonfile; } @@ -1108,9 +1125,159 @@ int getJsonTemplateFile(String &filename, String jsonfile, tagRecord *&taginfo, drawJsonStream(file, filename, taginfo, imageParams); file.close(); // contentFS->remove(jsonfile); - return 1; + return true; } - return 0; + return false; +} + +/// @brief Extract a variable with the given path from the given json +/// @note Float and double values are rounded to 2 decimal places +/// @param json Json document +/// @param path Path in form of a.b.1.c +/// @return Value as string +String extractValueFromJson(JsonDocument &json, const String &path) { + JsonVariant currentObj = json.as(); + char *segment = strtok(const_cast(path.c_str()), "."); + + while (segment != NULL) { + if (currentObj.is()) { + currentObj = currentObj.as()[segment]; + } else if (currentObj.is()) { + int index = atoi(segment); + currentObj = currentObj.as()[index]; + } else { + Serial.printf("Invalid JSON structure at path segment: %s\n", segment); + return ""; + } + segment = strtok(NULL, "."); + } + + if (!currentObj.is() && currentObj.is()) { + return String(currentObj.as(), 2); + } + + return currentObj.as(); +} + +/// @brief Replaces json placeholders ({.a.b.1.c}) with variables +class DataInterceptor : public Stream { + private: + /// @brief Stream being wrapped + Stream &_stream; + /// @brief Json containing variables + JsonDocument &_variables; + /// @brief Parsing buffer + String _buffer; + /// @brief Buffer size + const size_t _bufferSize = 32; + + public: + DataInterceptor(Stream &stream, JsonDocument &variables) + : _stream(stream), _variables(variables) { + } + + int available() override { + return _buffer.length() + _stream.available(); + } + + int read() override { + findAndReplace(); + + if (_buffer.length() > 0) { + const int data = _buffer[0]; + _buffer.remove(0, 1); + return data; + } + + return -1; // No more data + } + + int peek() override { + findAndReplace(); + return _buffer.length() ? _buffer[0] : -1; + } + + size_t write(uint8_t data) override { + return _stream.write(data); + } + + private: + /// @brief Fill buffer, find and replace json variables + void findAndReplace() { + unsigned int len; + while ((len = _buffer.length()) < _bufferSize) { + const int data = _stream.read(); + if (data == -1) { + break; // No more data to read + } + _buffer += (char)data; + } + + if (len < 4) { + // There are no variables with less than 4 characters + return; + } + + int endIndex = findVar(_buffer, 0); + if (endIndex == -1) { + return; + } + + const String varCleaned = _buffer.substring(1, endIndex - 1); + String replacement = extractValueFromJson(_variables, varCleaned); + + // Check for operator and second variable + if (endIndex + 3 < len) { + const char op = _buffer[endIndex]; + if ((op == '*' || op == '/' || op == '+' || op == '-')) { + const int endIndex2 = findVar(_buffer, endIndex + 1); + if (endIndex2 != -1) { + const String var2Cleaned = _buffer.substring(endIndex + 2, endIndex2 - 1); + const float v2 = extractValueFromJson(_variables, var2Cleaned).toFloat(); + endIndex = endIndex2; + + if (op == '*') { + replacement = String(replacement.toFloat() * v2, 0); + } else if (op == '/') { + replacement = abs(v2) > 0.0f ? String(replacement.toFloat() / v2, 0) : "0"; + } else if (op == '+') { + replacement = String(replacement.toFloat() + v2, 0); + } else if (op == '-') { + replacement = String(replacement.toFloat() - v2, 0); + } + } + } + } + + _buffer = replacement + _buffer.substring(endIndex); + } + + /// @brief Find a var at given start index + /// @param buffer Buffer to search in + /// @param index Index to look at + /// @return Endindex + int findVar(const String &buffer, const int index) { + if (buffer[index] != '{' || buffer[index + 1] != '.') { + return -1; + } + + return buffer.indexOf("}", index + 2) + 1; + } +}; + +bool getJsonTemplateFileExtractVariables(String &filename, String jsonfile, JsonDocument &variables, tagRecord *&taginfo, imgParam &imageParams) { + if (jsonfile.c_str()[0] != '/') { + jsonfile = "/" + jsonfile; + } + File file = contentFS->open(jsonfile, "r"); + if (file) { + auto interceptor = DataInterceptor(file, variables); + drawJsonStream(interceptor, filename, taginfo, imageParams); + file.close(); + // contentFS->remove(jsonfile); + return true; + } + return false; } int getJsonTemplateUrl(String &filename, String URL, time_t fetched, String MAC, tagRecord *&taginfo, imgParam &imageParams) {