diff --git a/PrepareProject.py b/PrepareProject.py index 7e8a495d..6d480783 100644 --- a/PrepareProject.py +++ b/PrepareProject.py @@ -199,6 +199,7 @@ for section, modules in profJson['modules'].items(): configItemsJson['num'] = itemsCount configItemsJson['name'] = str(itemsCount) + ". " + configItemsJson['name'] itemsCount = itemsCount + 1 + configItemsJson['moduleName'] = moduleJson['about']['moduleName'] itemsJson.append(configItemsJson) else: # В первую очередь ищем по имени deviceName, чтобы для данной платы можно было уточнить либы. Если не нашли плату по имени в usedLibs пробуем найти её по типу deviceType if deviceType in moduleJson['usedLibs']: # проверяем поддерживает ли модуль текущее устройство @@ -208,9 +209,10 @@ for section, modules in profJson['modules'].items(): allLibs = allLibs + "\n" + libPath for configItemsJson in moduleJson['configItem']: configItemsJson['num'] = itemsCount - configItemsJson['name'] = str(itemsCount) + ". " + configItemsJson['name'] - itemsCount = itemsCount + 1 - itemsJson.append(configItemsJson) + configItemsJson['name'] = str(itemsCount) + ". " + configItemsJson['name'] + itemsCount = itemsCount + 1 + itemsJson.append(configItemsJson) + configItemsJson['moduleName'] = moduleJson['about']['moduleName'] with open("data_svelte/items.json", "w", encoding='utf-8') as write_file: json.dump(itemsJson, write_file, ensure_ascii=False, indent=4, sort_keys=False) diff --git a/data_full/build/bundle.css.gz b/data_full/build/bundle.css.gz index cb7a83a5..de44f689 100644 Binary files a/data_full/build/bundle.css.gz and b/data_full/build/bundle.css.gz differ diff --git a/data_full/build/bundle.js.gz b/data_full/build/bundle.js.gz index e8df792b..d2e526eb 100644 Binary files a/data_full/build/bundle.js.gz and b/data_full/build/bundle.js.gz differ diff --git a/data_svelte/build/bundle.css.gz b/data_svelte/build/bundle.css.gz index cb7a83a5..de44f689 100644 Binary files a/data_svelte/build/bundle.css.gz and b/data_svelte/build/bundle.css.gz differ diff --git a/data_svelte/build/bundle.js.gz b/data_svelte/build/bundle.js.gz index e8df792b..d2e526eb 100644 Binary files a/data_svelte/build/bundle.js.gz and b/data_svelte/build/bundle.js.gz differ diff --git a/data_svelte/widgets.json b/data_svelte/widgets.json index 21a983fc..ecd0a7e8 100644 --- a/data_svelte/widgets.json +++ b/data_svelte/widgets.json @@ -177,6 +177,39 @@ "maxCount": 86400, "type": "bar" }, + { + "name": "chart4", + "label": "График Часовой", + "widget": "chart", + "dateFormat": "HH:mm", + "maxCount": 3600, + "type": "bar" + }, + { + "name": "chart5", + "label": "График двойной", + "widget": "chart", + "series": [ + "Температура, С", + "Влажность, %" + ], + "dateFormat": "HH:mm", + "maxCount": 86400, + "pointRadius": 0 + }, + { + "name": "chart6", + "label": "График тройной", + "widget": "chart", + "series": [ + "Температура, С", + "Влажность, %", + "Давление, кПа" + ], + "dateFormat": "HH:mm", + "maxCount": 86400, + "pointRadius": 0 + }, { "name": "fillgauge", "label": "Бочка", @@ -321,10 +354,6 @@ "widget": "anydata", "after": "°", "icon": "speedometer" - }, - { - "name": "nil", - "label": "Без виджета" }, { "name": "anydataBar", @@ -332,5 +361,9 @@ "widget": "anydata", "after": "Kg/cm²", "icon": "speedometer" + }, + { + "name": "nil", + "label": "Без виджета" } ] \ No newline at end of file diff --git a/include/Global.h b/include/Global.h index 0df14fe3..278630f0 100644 --- a/include/Global.h +++ b/include/Global.h @@ -10,6 +10,7 @@ #include #ifdef LIBRETINY +#include #include #include #ifdef STANDARD_WEB_SERVER diff --git a/src/Main.cpp b/src/Main.cpp index 0e4272cb..5dadadf7 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -224,9 +224,9 @@ void setup() { stopErrorMarker(SETUPINET_ERRORMARKER); - bool postMsgTelegram; - if (!jsonRead(settingsFlashJson, "debugTraceMsgTlgrm", postMsgTelegram, false)) postMsgTelegram = 1; - sendDebugTraceAndFreeMemory(postMsgTelegram); + // bool postMsgTelegram; + // if (!jsonRead(settingsFlashJson, "debugTraceMsgTlgrm", postMsgTelegram, false)) postMsgTelegram = 1; + // sendDebugTraceAndFreeMemory(postMsgTelegram); initErrorMarker(SETUPLAST_ERRORMARKER); diff --git a/src/StandWebServer.cpp b/src/StandWebServer.cpp index 79be8f59..6ac28403 100644 --- a/src/StandWebServer.cpp +++ b/src/StandWebServer.cpp @@ -9,6 +9,12 @@ static const char FS_INIT_ERROR[] PROGMEM = "FS INIT ERROR"; static const char FILE_NOT_FOUND[] PROGMEM = "FileNotFound"; // static bool fsOK; // const char* fsName = "LittleFS"; +// Типы обновлений +enum UpdateType { + FIRMWARE, + FILESYSTEM + }; + void standWebServerInit() { // Кэшировать файлы для быстрой работы @@ -88,10 +94,12 @@ void standWebServerInit() { // - first callback is called after the request has ended with all parsed arguments // - second callback handles file upload at that location HTTP.on("/edit", HTTP_POST, replyOK, handleFileUpload); - + // отображение страницы с полем ввода для сервера обновления HTTP.on("/localota", HTTP_GET, handleLocalOTA); - + // непосредственно ОТА обновление со стороннего сервера HTTP.on("/localota_handler", HTTP_GET, handleLocalOTA_Handler); + + // Обработка обновления от WS drag&drop HTTP.on("/update", HTTP_POST, []() { HTTP.send(200); // Для CORS }, handleUpdateOTA); @@ -176,13 +184,28 @@ void handleCors() { } void handleUpdateOTA() { + UpdateType typeOTAfile = FIRMWARE; HTTPUpload& upload = HTTP.upload(); - + if (upload.filename != "firmware.bin" && upload.filename != "littlefs.bin") + { + SerialPrint("E", F("OTA"), "Неверное имя файла: " + upload.filename); + return; + } + if (upload.filename == "firmware.bin") + { + typeOTAfile = FIRMWARE; + } else if (upload.filename == "littlefs.bin") + { + typeOTAfile = FILESYSTEM; + } + int updatePartition = (typeOTAfile == FIRMWARE)? U_FLASH : U_SPIFFS; if (upload.status == UPLOAD_FILE_START) { - Serial.print("Начало загрузки: "); - Serial.println(upload.filename); - if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { + //Serial.print("Начало загрузки: "); + //Serial.println(upload.filename); + SerialPrint("i", F("OTA"), "Начало загрузки файла: " + upload.filename); + if (!Update.begin(UPDATE_SIZE_UNKNOWN, updatePartition)) { Update.end(); + SerialPrint("E", F("OTA"), "Ошибка: Недостаточно памяти"); HTTP.send(500, "text/plain", "Ошибка: Недостаточно памяти"); return; } @@ -190,17 +213,19 @@ void handleCors() { else if (upload.status == UPLOAD_FILE_WRITE) { if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { Update.end(); + SerialPrint("E", F("OTA"), "Ошибка записи данных"); HTTP.send(500, "text/plain", "Ошибка записи данных"); return; } } else if (upload.status == UPLOAD_FILE_END) { if (Update.end(true)) { // true - перезагрузка после обновления + SerialPrint("i", F("OTA"), "Обновление завершено"); HTTP.send(200, "text/plain", "Обновление успешно"); - Serial.println("Обновление завершено"); ESP.restart(); } else { Update.end(); + SerialPrint("E", F("OTA"), "Ошибка завершения обновления"); HTTP.send(500, "text/plain", "Ошибка завершения обновления"); } } diff --git a/src/WsServer.cpp b/src/WsServer.cpp index 7a27698b..2adb1f5d 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -235,7 +235,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) if (headerStr == "/localt|") { String timeStr = String((char*)payload + 8); - Serial.println("Время с фронта: /localt|" + timeStr); + //Serial.println("Время с фронта: /localt|" + timeStr); // Обрезаем дробную часть, если есть int dotIndex = timeStr.indexOf('.'); @@ -253,9 +253,11 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) // Устанавливаем время if (settimeofday(&tv, NULL) == 0) { - Serial.printf("Время установлено: %ld\n", unixTime); + //Serial.printf("Время установлено: %ld\n", unixTime); + SerialPrint("i", F("Time"), "Время установлено из браузера: " + String(unixTime)); } else { - Serial.printf("Ошибка установки времени: %ld\n", unixTime); + //Serial.printf("Ошибка установки времени: %ld\n", unixTime); + SerialPrint("i", F("=>WS"), "Ошибка установки времени: " + String(unixTime)); } // timeval tv2{0, 0}; // timezone tz = timezone{0, 0}; diff --git a/src/modules/virtual/Loging2/Loging2.cpp b/src/modules/virtual/Loging2/Loging2.cpp new file mode 100644 index 00000000..71573552 --- /dev/null +++ b/src/modules/virtual/Loging2/Loging2.cpp @@ -0,0 +1,415 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include "ESPConfiguration.h" +#include "NTP.h" + +void *getAPI_Date2(String params); + +class Loging2 : public IoTItem { + private: + String logid1; + String logid2; + String id; + String tmpValue; + String filesList = ""; + + int _publishType = -2; + int _wsNum = -1; + + int points; + //int keepdays; + + IoTItem *dateIoTItem; + + String prevDate = ""; + bool firstTimeInit = true; + + // long interval; + + public: + Loging2(String parameters) : IoTItem(parameters) { + jsonRead(parameters, F("logid1"), logid1); + jsonRead(parameters, F("logid2"), logid2); + jsonRead(parameters, F("id"), id); + jsonRead(parameters, F("points"), points); + if (points > 300) { + points = 300; + SerialPrint("E", F("Loging2"), "'" + id + "' user set more points than allowed, value reset to 300"); + } + long interval; + jsonRead(parameters, F("int"), interval); + interval = interval * 60; // приводим к милисекундам + //jsonRead(parameters, F("keepdays"), keepdays, false); + + // создадим экземпляр класса даты + dateIoTItem = (IoTItem *)getAPI_Date2("{\"id\": \"" + id + "-date\",\"int\":\"20\",\"subtype\":\"date\"}"); + IoTItems.push_back(dateIoTItem); + SerialPrint("I", F("Loging2"), "created date instance " + id); + } + + void doByInterval() { + // если объект логгирования не был создан + if (!isItemExist(logid1)) { + SerialPrint("E", F("Loging2"), "'" + id + "' loging object not exist, return"); + return; + } + + String value = getItemValue(logid1); + // если значение логгирования пустое + if (value == "") { + SerialPrint("E", F("Loging2"), "'" + id + "' loging value is empty, return"); + return; + } + String value2 = getItemValue(logid2); + // если значение логгирования пустое + if (value == "") { + SerialPrint("E", F("Loging2"), "'" + id + "' loging value is empty, return"); + return; + } + + // если время не было получено из интернета + if (!isTimeSynch) { + SerialPrint("E", F("Loging2"), "'" + id + "' Сant loging - time not synchronized, return"); + return; + } + + regEvent(value, F("Loging2")); + + String logData2; + + jsonWriteInt(logData2, "x", unixTime, false); + jsonWriteFloat(logData2, "y1", value.toFloat(), false); + jsonWriteFloat(logData2, "y2", value2.toFloat(), false); + + // прочитаем путь к файлу последнего сохранения + String filePath = readDataDB(id); + + // если данные о файле отсутствуют, создадим новый + if (filePath == "failed" || filePath == "") { + SerialPrint("E", F("Loging2"), "'" + id + "' file path not found, start create new file"); + createNewFileWithData(logData2); + return; + } else { + // если файл все же есть но был создан не сегодня, то создаем сегодняшний + if (getTodayDateDotFormated() != getDateDotFormatedFromUnix(getFileUnixLocalTime(filePath))) { + SerialPrint("E", F("Loging2"), "'" + id + "' file too old, start create new file"); + createNewFileWithData(logData2); + return; + } + } + + // считаем количество строк и определяем размер файла + size_t size = 0; + int lines = countJsonObj(filePath, size); + SerialPrint("i", F("Loging2"), "'" + id + "' " + "lines = " + String(lines) + ", size = " + String(size)); + + // если количество строк до заданной величины и дата не менялась + if (lines <= points && !hasDayChanged()) { + // просто добавим в существующий файл новые данные + addNewDataToExistingFile(filePath, logData2); + // если больше или поменялась дата то создадим следующий файл + } else { + createNewFileWithData(logData2); + } + // запускаем процедуру удаления старых файлов если память переполняется + deleteLastFile(); + } + + void SetDoByInterval(String valse) { + String value = valse; + // если значение логгирования пустое + if (value == "") { + SerialPrint("E", F("Loging2Event"), "'" + id + "' loging value is empty, return"); + return; + } + // если время не было получено из интернета + if (!isTimeSynch) { + SerialPrint("E", F("Loging2Event"), "'" + id + "' Сant loging - time not synchronized, return"); + return; + } + regEvent(value, F("Loging2Event")); + String logData2; + jsonWriteInt(logData2, "x", unixTime, false); + jsonWriteFloat(logData2, "y1", value.toFloat(), false); + jsonWriteFloat(logData2, "y2", value.toFloat(), false); + // прочитаем путь к файлу последнего сохранения + String filePath = readDataDB(id); + + // если данные о файле отсутствуют, создадим новый + if (filePath == "failed" || filePath == "") { + SerialPrint("E", F("Loging2Event"), "'" + id + "' file path not found, start create new file"); + createNewFileWithData(logData2); + return; + } else { + // если файл все же есть но был создан не сегодня, то создаем сегодняшний + if (getTodayDateDotFormated() != getDateDotFormatedFromUnix(getFileUnixLocalTime(filePath))) { + SerialPrint("E", F("Loging2Event"), "'" + id + "' file too old, start create new file"); + createNewFileWithData(logData2); + return; + } + } + + // считаем количество строк и определяем размер файла + size_t size = 0; + int lines = countJsonObj(filePath, size); + SerialPrint("i", F("Loging2Event"), "'" + id + "' " + "lines = " + String(lines) + ", size = " + String(size)); + + // если количество строк до заданной величины и дата не менялась + if (lines <= points && !hasDayChanged()) { + // просто добавим в существующий файл новые данные + addNewDataToExistingFile(filePath, logData2); + // если больше или поменялась дата то создадим следующий файл + } else { + createNewFileWithData(logData2); + } + // запускаем процедуру удаления старых файлов если память переполняется + deleteLastFile(); + } + void createNewFileWithData(String &logData) { + logData = logData + ","; + String path = "/lg2/" + id + "/" + String(unixTimeShort) + ".txt"; // создадим путь вида /lg/id/133256622333.txt + // создадим пустой файл + if (writeEmptyFile(path) != "success") { + SerialPrint("E", F("Loging2"), "'" + id + "' file writing error, return"); + return; + } + + // запишем в него данные + if (addFile(path, logData) != "success") { + SerialPrint("E", F("Loging2"), "'" + id + "' data writing error, return"); + return; + } + // запишем путь к нему в базу данных + if (saveDataDB(id, path) != "success") { + SerialPrint("E", F("Loging2"), "'" + id + "' db file writing error, return"); + return; + } + SerialPrint("i", F("Loging2"), "'" + id + "' file created http://" + WiFi.localIP().toString() + path); + } + + void addNewDataToExistingFile(String &path, String &logData) { + logData = logData + ","; + if (addFile(path, logData) != "success") { + SerialPrint("i", F("Loging2"), "'" + id + "' file writing error, return"); + return; + }; + SerialPrint("i", F("Loging2"), "'" + id + "' loging in file http://" + WiFi.localIP().toString() + path); + } + + // данная функция уже перенесена в ядро и будет удалена в последствии + bool hasDayChanged() { + bool changed = false; + String currentDate = getTodayDateDotFormated(); + if (!firstTimeInit) { + if (prevDate != currentDate) { + changed = true; + SerialPrint("i", F("NTP"), F("Change day event")); +#if defined(ESP8266) + FileFS.gc(); +#endif +#if defined(ESP32) +#endif + } + } + firstTimeInit = false; + prevDate = currentDate; + return changed; + } + + void publishValue() { + String dir = "/lg2/" + id; + filesList = getFilesList(dir); + + SerialPrint("i", F("Loging2"), "file list: " + filesList); + + int f = 0; + + bool noData = true; + + while (filesList.length()) { + String path = selectToMarker(filesList, ";"); + + path = "/lg2/" + id + path; + + f++; + + unsigned long fileUnixTimeLocal = getFileUnixLocalTime(path); + + unsigned long reqUnixTime = strDateToUnix(getItemValue(id + "-date")); + if (fileUnixTimeLocal > reqUnixTime && fileUnixTimeLocal < reqUnixTime + 86400) { + noData = false; + String json = getAdditionalJson(); + if (_publishType == TO_MQTT) { + publishChartFileToMqtt(path, id, calculateMaxCount()); + } else if (_publishType == TO_WS) { + sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); + } else if (_publishType == TO_MQTT_WS) { + sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); + publishChartFileToMqtt(path, id, calculateMaxCount()); + } + SerialPrint("i", F("Loging2"), String(f) + ") " + path + ", " + getDateTimeDotFormatedFromUnix(fileUnixTimeLocal) + ", sent"); + } else { + SerialPrint("i", F("Loging2"), String(f) + ") " + path + ", " + getDateTimeDotFormatedFromUnix(fileUnixTimeLocal) + ", skipped"); + } + + filesList = deleteBeforeDelimiter(filesList, ";"); + } + // если данных нет отправляем пустой грфик + if (noData) { + clearValue(); + } + } + + String getAdditionalJson() { + String topic = mqttRootDevice + "/" + id; + String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\"}"; + return json; + } + + void publishChartToWsSinglePoint(String value) { + String topic = mqttRootDevice + "/" + id; + String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + ",\"y2\":" + value + "}]}"; + sendStringToWs("chartb", json, -1); + } + + void clearValue() { + String topic = mqttRootDevice + "/" + id; + String json = "{\"maxCount\":0,\"topic\":\"" + topic + "\",\"status\":[]}"; + sendStringToWs("chartb", json, -1); + } + + void clearHistory() { + String dir = "/lg2/" + id; + cleanDirectory(dir); + } + + void deleteLastFile() { + IoTFSInfo tmp = getFSInfo(); + SerialPrint("i", "Loging2", String(tmp.freePer) + " % free flash remaining"); + if (tmp.freePer <= 20.00) { + String dir = "/lg/" + id; + filesList = getFilesList(dir); + int i = 0; + while (filesList.length()) { + String path = selectToMarker(filesList, ";"); + path = dir + path; + i++; + if (i == 1) { + removeFile(path); + SerialPrint("!", "Loging2", String(i) + ") " + path + " => oldest files been deleted"); + return; + } + + filesList = deleteBeforeDelimiter(filesList, ";"); + } + } + } + + void setPublishDestination(int publishType, int wsNum) { + _publishType = publishType; + _wsNum = wsNum; + } + + String getValue() { + return ""; + } +/* + void loop() { + if (enableDoByInt) { + currentMillis = millis(); + difference = currentMillis - prevMillis; + if (difference >= interval) { + prevMillis = millis(); + if (interval != 0) { + this->doByInterval(); + } + } + } + } +*/ + void regEvent(const String &value, const String &consoleInfo, bool error = false, bool genEvent = true) { + String userDate = getItemValue(id + "-date"); + String currentDate = getTodayDateDotFormated(); + // отправляем в график данные только когда выбран сегодняшний день + if (userDate == currentDate) { + // generateEvent(_id, value); + // publishStatusMqtt(_id, value); + + publishChartToWsSinglePoint(value); + // SerialPrint("i", "Sensor " + consoleInfo, "'" + _id + "' data: " + value + "'"); + } + } + + // просто максимальное количество точек + int calculateMaxCount() { + return 86400; + } + + // путь вида: /lg/log/1231231.txt + unsigned long getFileUnixLocalTime(String path) { + return gmtTimeToLocal(selectToMarkerLast(deleteToMarkerLast(path, "."), "/").toInt() + START_DATETIME); + } + void setValue(const IoTValue &Value, bool genEvent = true) { + value = Value; + this->SetDoByInterval(String(value.valD)); + SerialPrint("i", "Loging2", "setValue:" + String(value.valD)); + regEvent(value.valS, "Loging2", false, genEvent); + } +}; + +void *getAPI_Loging2(String subtype, String param) { + if (subtype == F("Loging2")) { + return new Loging2(param); + } else { + return nullptr; + } +} + +class Date : public IoTItem { + private: + bool firstTime = true; + + public: + String id; + Date(String parameters) : IoTItem(parameters) { + jsonRead(parameters, F("id"), id); + value.isDecimal = false; + } + + void setValue(const String &valStr, bool genEvent = true) { + value.valS = valStr; + setValue(value, genEvent); + } + + void setValue(const IoTValue &Value, bool genEvent = true) { + value = Value; + regEvent(value.valS, "", false, genEvent); + // отправка данных при изменении даты + for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { + if ((*it)->getSubtype() == "Loging2") { + if ((*it)->getID() == selectToMarker(id, "-")) { + (*it)->setPublishDestination(TO_MQTT_WS, -1); + (*it)->publishValue(); + } + } + } + } + + void setTodayDate() { + setValue(getTodayDateDotFormated()); + SerialPrint("E", F("Loging2"), "today date set " + getTodayDateDotFormated()); + } + + void doByInterval() { + if (isTimeSynch) { + if (firstTime) { + setTodayDate(); + firstTime = false; + } + } + } +}; + +void *getAPI_Date2(String param) { + return new Date(param); +} diff --git a/src/modules/virtual/Loging2/modinfo.json b/src/modules/virtual/Loging2/modinfo.json new file mode 100644 index 00000000..0841e795 --- /dev/null +++ b/src/modules/virtual/Loging2/modinfo.json @@ -0,0 +1,48 @@ +{ + "menuSection": "virtual_elments", + "configItem": [ + { + "global": 0, + "name": "Двойной график", + "type": "Writing", + "subtype": "Loging2", + "id": "log2", + "widget": "chart5", + "page": "Графики", + "descr": "Датчик", + "num": 1, + "int": 5, + "logid1": "t", + "logid2": "h", + "points": 300, + "series1": "Температура, С", + "series2": "Влажность, %" + } + ], + "about": { + "authorName": "Serghei Crasnicov", + "authorContact": "https://t.me/Serghei63", + "authorGit": "https://github.com/Serghei63", + "specialThanks": "@itsid1 @Valiuhaaa Serg", + "moduleName": "Loging2", + "moduleVersion": "0.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Логирование в график", + "moduleDesc": "Расширение позволяющее логировать любую величину в график. Графики доступны в мобильном приложении и в веб интерфейсе. Данные графиков хранятся в встроенной памяти esp. В окне ввода даты можно выбирать день, историю которого вы хотите посмотреть. Старые файлы будут удаляться автоматически после того как объем оставшейся flesh памяти устройства будет менее 20 процентов", + "propInfo": { + "int": "Интервал логирования в мнутах, рекомендуется для esp8266 использоать интервал не менее 5-ти минут", + "logid1": "ID 1 величины которую будем логировать (температура)", + "logid2": "ID 2 величины которую будем логировать (влажность)", + "points": "Максимальное количество точек в одном файле, может быть не более 300. Не рекомендуется менять этот параметр" + } + }, + "defActive": false, + "usedLibs": { + "esp32*": [], + "esp82*": [] + + } +} \ No newline at end of file diff --git a/src/modules/virtual/Loging3/Loging3.cpp b/src/modules/virtual/Loging3/Loging3.cpp new file mode 100644 index 00000000..0a2dcfa9 --- /dev/null +++ b/src/modules/virtual/Loging3/Loging3.cpp @@ -0,0 +1,427 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include "ESPConfiguration.h" +#include "NTP.h" + +void *getAPI_Date3(String params); + +class Loging3 : public IoTItem { + private: + String logid1; + String logid2; + String logid3; + String id; + String tmpValue; + String filesList = ""; + + int _publishType = -2; + int _wsNum = -1; + + int points; + //int keepdays; + + IoTItem *dateIoTItem; + + String prevDate = ""; + bool firstTimeInit = true; + + //long interval; + + public: + Loging3(String parameters) : IoTItem(parameters) { + jsonRead(parameters, F("logid1"), logid1); + jsonRead(parameters, F("logid2"), logid2); + jsonRead(parameters, F("logid3"), logid3); + jsonRead(parameters, F("id"), id); + jsonRead(parameters, F("points"), points); + if (points > 300) { + points = 300; + SerialPrint("E", F("Loging3"), "'" + id + "' user set more points than allowed, value reset to 300"); + } + + long interval; + jsonRead(parameters, F("int"), interval); + interval = interval * 60; // приводим к милисекундам + //jsonRead(parameters, F("keepdays"), keepdays, false); + + // создадим экземпляр класса даты + dateIoTItem = (IoTItem *)getAPI_Date3("{\"id\": \"" + id + "-date\",\"int\":\"20\",\"subtype\":\"date\"}"); + IoTItems.push_back(dateIoTItem); + SerialPrint("I", F("Loging3"), "created date instance " + id); + } + + void doByInterval() { + // если объект логгирования не был создан + if (!isItemExist(logid1)) { + SerialPrint("E", F("Loging3"), "'" + id + "' loging object not exist, return"); + return; + } + + String value = getItemValue(logid1); + // если значение логгирования пустое + if (value == "") { + SerialPrint("E", F("Loging3"), "'" + id + "' loging value is empty, return"); + return; + } + String value2 = getItemValue(logid2); + // если значение логгирования пустое + if (value == "") { + SerialPrint("E", F("Loging3"), "'" + id + "' loging value is empty, return"); + return; + } + String value3 = getItemValue(logid3); + // если значение логгирования пустое + if (value == "") { + SerialPrint("E", F("Loging3"), "'" + id + "' loging value is empty, return"); + return; + } + + // если время не было получено из интернета + if (!isTimeSynch) { + SerialPrint("E", F("Loging"), "'" + id + "' Сant loging - time not synchronized, return"); + return; + } + + regEvent(value, F("Loging3")); + + //String logData2; + String logData3; + + jsonWriteInt(logData3, "x", unixTime, false); + jsonWriteFloat(logData3, "y1", value.toFloat(), false); + jsonWriteFloat(logData3, "y2", value2.toFloat(), false); + jsonWriteFloat(logData3, "y3", value3.toFloat(), false); + + // прочитаем путь к файлу последнего сохранения + String filePath = readDataDB(id); + + // если данные о файле отсутствуют, создадим новый + if (filePath == "failed" || filePath == "") { + SerialPrint("E", F("Loging3"), "'" + id + "' file path not found, start create new file"); + createNewFileWithData(logData3); + return; + } else { + // если файл все же есть но был создан не сегодня, то создаем сегодняшний + if (getTodayDateDotFormated() != getDateDotFormatedFromUnix(getFileUnixLocalTime(filePath))) { + SerialPrint("E", F("Loging3"), "'" + id + "' file too old, start create new file"); + createNewFileWithData(logData3); + return; + } + } + + // считаем количество строк и определяем размер файла + size_t size = 0; + int lines = countJsonObj(filePath, size); + SerialPrint("i", F("Loging3"), "'" + id + "' " + "lines = " + String(lines) + ", size = " + String(size)); + + // если количество строк до заданной величины и дата не менялась + if (lines <= points && !hasDayChanged()) { + // просто добавим в существующий файл новые данные + addNewDataToExistingFile(filePath, logData3); + // если больше или поменялась дата то создадим следующий файл + } else { + createNewFileWithData(logData3); + } + // запускаем процедуру удаления старых файлов если память переполняется + deleteLastFile(); + } + + void SetDoByInterval(String valse) { + String value = valse; + // если значение логгирования пустое + if (value == "") { + SerialPrint("E", F("Loging3Event"), "'" + id + "' loging value is empty, return"); + return; + } + // если время не было получено из интернета + if (!isTimeSynch) { + SerialPrint("E", F("Loging3Event"), "'" + id + "' Сant loging - time not synchronized, return"); + return; + } + regEvent(value, F("Loging3Event")); + String logData3; + jsonWriteInt(logData3, "x", unixTime, false); + jsonWriteFloat(logData3, "y1", value.toFloat(), false); + jsonWriteFloat(logData3, "y2", value.toFloat(), false); + jsonWriteFloat(logData3, "y3", value.toFloat(), false); + // прочитаем путь к файлу последнего сохранения + String filePath = readDataDB(id); + + // если данные о файле отсутствуют, создадим новый + if (filePath == "failed" || filePath == "") { + SerialPrint("E", F("Loging3Event"), "'" + id + "' file path not found, start create new file"); + createNewFileWithData(logData3); + return; + } else { + // если файл все же есть но был создан не сегодня, то создаем сегодняшний + if (getTodayDateDotFormated() != getDateDotFormatedFromUnix(getFileUnixLocalTime(filePath))) { + SerialPrint("E", F("Loging3Event"), "'" + id + "' file too old, start create new file"); + createNewFileWithData(logData3); + return; + } + } + + // считаем количество строк и определяем размер файла + size_t size = 0; + int lines = countJsonObj(filePath, size); + SerialPrint("i", F("Loging3Event"), "'" + id + "' " + "lines = " + String(lines) + ", size = " + String(size)); + + // если количество строк до заданной величины и дата не менялась + if (lines <= points && !hasDayChanged()) { + // просто добавим в существующий файл новые данные + addNewDataToExistingFile(filePath, logData3); + // если больше или поменялась дата то создадим следующий файл + } else { + createNewFileWithData(logData3); + } + // запускаем процедуру удаления старых файлов если память переполняется + deleteLastFile(); + } + void createNewFileWithData(String &logData) { + logData = logData + ","; + String path = "/lg3/" + id + "/" + String(unixTimeShort) + ".txt"; // создадим путь вида /lg/id/133256622333.txt + // создадим пустой файл + if (writeEmptyFile(path) != "success") { + SerialPrint("E", F("Loging"), "'" + id + "' file writing error, return"); + return; + } + + // запишем в него данные + if (addFile(path, logData) != "success") { + SerialPrint("E", F("Loging3"), "'" + id + "' data writing error, return"); + return; + } + // запишем путь к нему в базу данных + if (saveDataDB(id, path) != "success") { + SerialPrint("E", F("Loging3"), "'" + id + "' db file writing error, return"); + return; + } + SerialPrint("i", F("Loging3"), "'" + id + "' file created http://" + WiFi.localIP().toString() + path); + } + + void addNewDataToExistingFile(String &path, String &logData) { + logData = logData + ","; + if (addFile(path, logData) != "success") { + SerialPrint("i", F("Loging3"), "'" + id + "' file writing error, return"); + return; + }; + SerialPrint("i", F("Loging3"), "'" + id + "' loging in file http://" + WiFi.localIP().toString() + path); + } + + // данная функция уже перенесена в ядро и будет удалена в последствии + bool hasDayChanged() { + bool changed = false; + String currentDate = getTodayDateDotFormated(); + if (!firstTimeInit) { + if (prevDate != currentDate) { + changed = true; + SerialPrint("i", F("NTP"), F("Change day event")); +#if defined(ESP8266) + FileFS.gc(); +#endif +#if defined(ESP32) +#endif + } + } + firstTimeInit = false; + prevDate = currentDate; + return changed; + } + + void publishValue() { + String dir = "/lg3/" + id; + filesList = getFilesList(dir); + + SerialPrint("i", F("Loging3"), "file list: " + filesList); + + int f = 0; + + bool noData = true; + + while (filesList.length()) { + String path = selectToMarker(filesList, ";"); + + path = "/lg3/" + id + path; + + f++; + + unsigned long fileUnixTimeLocal = getFileUnixLocalTime(path); + + unsigned long reqUnixTime = strDateToUnix(getItemValue(id + "-date")); + if (fileUnixTimeLocal > reqUnixTime && fileUnixTimeLocal < reqUnixTime + 86400) { + noData = false; + String json = getAdditionalJson(); + if (_publishType == TO_MQTT) { + publishChartFileToMqtt(path, id, calculateMaxCount()); + } else if (_publishType == TO_WS) { + sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); + } else if (_publishType == TO_MQTT_WS) { + sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); + publishChartFileToMqtt(path, id, calculateMaxCount()); + } + SerialPrint("i", F("Loging3"), String(f) + ") " + path + ", " + getDateTimeDotFormatedFromUnix(fileUnixTimeLocal) + ", sent"); + } else { + SerialPrint("i", F("Loging3"), String(f) + ") " + path + ", " + getDateTimeDotFormatedFromUnix(fileUnixTimeLocal) + ", skipped"); + } + + filesList = deleteBeforeDelimiter(filesList, ";"); + } + // если данных нет отправляем пустой грфик + if (noData) { + clearValue(); + } + } + + String getAdditionalJson() { + String topic = mqttRootDevice + "/" + id; + String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\"}"; + return json; + } + + void publishChartToWsSinglePoint(String value) { + String topic = mqttRootDevice + "/" + id; + String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + ",\"y2\":" + value + ",\"y3\":" + value + "}]}"; + sendStringToWs("chartb", json, -1); + } + + void clearValue() { + String topic = mqttRootDevice + "/" + id; + String json = "{\"maxCount\":0,\"topic\":\"" + topic + "\",\"status\":[]}"; + sendStringToWs("chartb", json, -1); + } + + void clearHistory() { + String dir = "/lg3/" + id; + cleanDirectory(dir); + } + + void deleteLastFile() { + IoTFSInfo tmp = getFSInfo(); + SerialPrint("i", "Loging3", String(tmp.freePer) + " % free flash remaining"); + if (tmp.freePer <= 20.00) { + String dir = "/lg3/" + id; + filesList = getFilesList(dir); + int i = 0; + while (filesList.length()) { + String path = selectToMarker(filesList, ";"); + path = dir + path; + i++; + if (i == 1) { + removeFile(path); + SerialPrint("!", "Loging3", String(i) + ") " + path + " => oldest files been deleted"); + return; + } + + filesList = deleteBeforeDelimiter(filesList, ";"); + } + } + } + + void setPublishDestination(int publishType, int wsNum) { + _publishType = publishType; + _wsNum = wsNum; + } + + String getValue() { + return ""; + } +/* + void loop() { + if (enableDoByInt) { + currentMillis = millis(); + difference = currentMillis - prevMillis; + if (difference >= interval) { + prevMillis = millis(); + if (interval != 0) { + this->doByInterval(); + } + } + } + } +*/ + void regEvent(const String &value, const String &consoleInfo, bool error = false, bool genEvent = true) { + String userDate = getItemValue(id + "-date"); + String currentDate = getTodayDateDotFormated(); + // отправляем в график данные только когда выбран сегодняшний день + if (userDate == currentDate) { + // generateEvent(_id, value); + // publishStatusMqtt(_id, value); + + publishChartToWsSinglePoint(value); + // SerialPrint("i", "Sensor " + consoleInfo, "'" + _id + "' data: " + value + "'"); + } + } + + // просто максимальное количество точек + int calculateMaxCount() { + return 86400; + } + + // путь вида: /lg/log/1231231.txt + unsigned long getFileUnixLocalTime(String path) { + return gmtTimeToLocal(selectToMarkerLast(deleteToMarkerLast(path, "."), "/").toInt() + START_DATETIME); + } + void setValue(const IoTValue &Value, bool genEvent = true) { + value = Value; + this->SetDoByInterval(String(value.valD)); + SerialPrint("i", "Loging3", "setValue:" + String(value.valD)); + regEvent(value.valS, "Loging3", false, genEvent); + } +}; + +void *getAPI_Loging3(String subtype, String param) { + if (subtype == F("Loging3")) { + return new Loging3(param); + } else { + return nullptr; + } +} + +class Date : public IoTItem { + private: + bool firstTime = true; + + public: + String id; + Date(String parameters) : IoTItem(parameters) { + jsonRead(parameters, F("id"), id); + value.isDecimal = false; + } + + void setValue(const String &valStr, bool genEvent = true) { + value.valS = valStr; + setValue(value, genEvent); + } + + void setValue(const IoTValue &Value, bool genEvent = true) { + value = Value; + regEvent(value.valS, "", false, genEvent); + // отправка данных при изменении даты + for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { + if ((*it)->getSubtype() == "Loging3") { + if ((*it)->getID() == selectToMarker(id, "-")) { + (*it)->setPublishDestination(TO_MQTT_WS, -1); + (*it)->publishValue(); + } + } + } + } + + void setTodayDate() { + setValue(getTodayDateDotFormated()); + SerialPrint("E", F("Loging3"), "today date set " + getTodayDateDotFormated()); + } + + void doByInterval() { + if (isTimeSynch) { + if (firstTime) { + setTodayDate(); + firstTime = false; + } + } + } +}; + +void *getAPI_Date3(String param) { + return new Date(param); +} diff --git a/src/modules/virtual/Loging3/modinfo.json b/src/modules/virtual/Loging3/modinfo.json new file mode 100644 index 00000000..11a9420d --- /dev/null +++ b/src/modules/virtual/Loging3/modinfo.json @@ -0,0 +1,51 @@ +{ + "menuSection": "virtual_elments", + "configItem": [ + { + "global": 0, + "name": "Тройной график", + "type": "Writing", + "subtype": "Loging3", + "id": "log3", + "widget": "chart6", + "page": "Графики", + "descr": "Датчик", + "num": 1, + "int": 5, + "logid1": "t", + "logid2": "h", + "logid3": "p", + "points": 300, + "series1": "Температура, С", + "series2": "Влажность, %", + "series3": "Давление, кПа" + } + ], + "about": { + "authorName": "Serghei Crasnicov", + "authorContact": "https://t.me/Serghei63", + "authorGit": "https://github.com/Serghei63", + "specialThanks": "@itsid1 @Valiuhaaa Serg", + "moduleName": "Loging3", + "moduleVersion": "0.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Логирование в график", + "moduleDesc": "Расширение позволяющее логировать любую величину в график. Графики доступны в мобильном приложении и в веб интерфейсе. Данные графиков хранятся в встроенной памяти esp. В окне ввода даты можно выбирать день, историю которого вы хотите посмотреть. Старые файлы будут удаляться автоматически после того как объем оставшейся flesh памяти устройства будет менее 20 процентов", + "propInfo": { + "int": "Интервал логирования в мнутах, рекомендуется для esp8266 использоать интервал не менее 5-ти минут", + "logid1": "ID 1 величины которую будем логировать (температура)", + "logid2": "ID 2 величины которую будем логировать (влажность)", + "logid3": "ID 3 величины которую будем логировать (давление)", + "points": "Максимальное количество точек в одном файле, может быть не более 300. Не рекомендуется менять этот параметр" + } + }, + "defActive": false, + "usedLibs": { + "esp32*": [], + "esp82*": [] + + } +} \ No newline at end of file diff --git a/src/utils/WiFiUtils.cpp b/src/utils/WiFiUtils.cpp index 5b7bf79d..7170db7e 100644 --- a/src/utils/WiFiUtils.cpp +++ b/src/utils/WiFiUtils.cpp @@ -3,6 +3,7 @@ #if defined(ESP32) #include #endif +#include "DebugTrace.h" #define TRIESONE 20 // количество секунд ожидания подключения к одной сети из несколких #define TRIES 30 // количество секунд ожидания подключения сети если она одна @@ -50,6 +51,10 @@ void WiFiEvent(arduino_event_t *event) mqttInit(); SerialPrint("i", F("WIFI"), F("Network Init")); + bool postMsgTelegram; + if (!jsonRead(settingsFlashJson, "debugTraceMsgTlgrm", postMsgTelegram, false)) postMsgTelegram = 1; + sendDebugTraceAndFreeMemory(postMsgTelegram); + // Отключаем AP при успешном подключении WiFi.softAPdisconnect(true); break; @@ -70,6 +75,7 @@ void WiFiEvent(arduino_event_t *event) } else { // если попытки подключения исчерпаны, то переходим в AP + sendDebugTraceAndFreeMemory(false); startAPMode(); } break;