diff --git a/PrepareProject.py b/PrepareProject.py index a848dbf7..2f8c1241 100644 --- a/PrepareProject.py +++ b/PrepareProject.py @@ -201,6 +201,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']: # проверяем поддерживает ли модуль текущее устройство @@ -210,9 +211,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 29ab1fa8..19d2f663 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 105e6deb..d4dc70e4 100644 Binary files a/data_full/build/bundle.js.gz and b/data_full/build/bundle.js.gz differ diff --git a/data_full/index.html b/data_full/index.html index bbcd166f..fe91a206 100644 --- a/data_full/index.html +++ b/data_full/index.html @@ -4,7 +4,7 @@ - IoT Manager 4.5.5 + IoT Manager 4.6.2 diff --git a/data_svelte/build/bundle.css.gz b/data_svelte/build/bundle.css.gz index 29ab1fa8..19d2f663 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 105e6deb..d4dc70e4 100644 Binary files a/data_svelte/build/bundle.js.gz and b/data_svelte/build/bundle.js.gz differ diff --git a/data_svelte/index.html b/data_svelte/index.html index bbcd166f..fe91a206 100644 --- a/data_svelte/index.html +++ b/data_svelte/index.html @@ -4,7 +4,7 @@ - IoT Manager 4.5.5 + IoT Manager 4.6.2 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/Const.h b/include/Const.h index 155bcac5..a948a78b 100644 --- a/include/Const.h +++ b/include/Const.h @@ -2,7 +2,8 @@ #include "BuildTime.h" // Версия прошивки -#define FIRMWARE_VERSION 460 + +#define FIRMWARE_VERSION 462 #ifdef esp8266_1mb_ota #define FIRMWARE_NAME "esp8266_1mb_ota" @@ -68,6 +69,9 @@ #define FIRMWARE_NAME "esp32c6_8mb" #endif +#ifdef esp32_wifirep +#define FIRMWARE_NAME "esp32_wifirep" +#endif // Размер буфера json #define JSON_BUFFER_SIZE 4096 // держим 2 кб не меняем @@ -108,7 +112,7 @@ WEB_SOCKETS_FRAME_SIZE создан для того что бы не загру //#define WIFI_ASYNC #endif -#ifdef ESP32 +#if defined(ESP32) && !defined(esp32_wifirep) #define WIFI_ASYNC #endif 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/include/StandWebServer.h b/include/StandWebServer.h index 419a4538..db177ca2 100644 --- a/include/StandWebServer.h +++ b/include/StandWebServer.h @@ -10,6 +10,8 @@ extern void handleFileUpload(); extern void handleFileDelete(); extern void handleFileCreate(); extern void handleLocalOTA(); +extern void handleUpdateOTA(); +extern void handleCors(); extern void handleLocalOTA_Handler(); extern void handleFileList(); //void printDirectory(File dir, String& out); diff --git a/include/utils/WiFiUtils.h b/include/utils/WiFiUtils.h index 90934b90..9e60a2cb 100644 --- a/include/utils/WiFiUtils.h +++ b/include/utils/WiFiUtils.h @@ -2,6 +2,9 @@ #include "Global.h" #include "MqttClient.h" + +void addPortMap(String TCP_UDP, String maddr, u16_t mport, String daddr, u16_t dport); + boolean isNetworkActive(); uint8_t getNumAPClients(); bool startAPMode(); diff --git a/myProfile.json b/myProfile.json index 9a32b8bc..fd7485a8 100644 --- a/myProfile.json +++ b/myProfile.json @@ -3,6 +3,12 @@ "name": "IoTmanagerVer4", "apssid": "IoTmanager", "appass": "", + "wifirep_apchanel": 7, + "wifirep_apip": "192.168.4.1", + "wifirep_staip": "192.168.1.160", + "wifirep_netmask": "255.255.255.0", + "wifirep_gateway": "192.168.4.1", + "wifirep_dns": "192.168.4.1", "routerssid": "iot", "routerpass": "hostel3333", "timezone": 2, @@ -27,7 +33,7 @@ "projectProp": { "platformio": { "default_envs": "esp32_4mb3f", - "comments_default_envs": "choose from: esp8266_4mb, esp32_4mb, esp32_4mb3f, esp8266_16mb, esp32_16mb, esp32cam_4mb, esp32s2_4mb, esp32s3_16mb, esp32c3m_4mb, esp8266_1mb, esp8266_1mb_ota, esp8266_2mb, esp8266_2mb_ota, esp8285_1mb, esp8285_1mb_ota, esp32c6_4mb, esp32c6_8mb, bk7231n", + "comments_default_envs": "choose from: esp8266_4mb, esp32_4mb, esp32_4mb3f, esp8266_16mb, esp32_16mb, esp32cam_4mb, esp32s2_4mb, esp32s3_16mb, esp32c3m_4mb, esp8266_1mb, esp8266_1mb_ota, esp8266_2mb, esp8266_2mb_ota, esp8285_1mb, esp8285_1mb_ota, esp32c6_4mb, esp32c6_8mb, bk7231n, esp32_wifirep", "envs": [ { "name": "esp8266_4mb", @@ -55,6 +61,14 @@ "partitions": "0x8000", "littlefs": "0x310000" }, + { + "name": "esp32_wifirep", + "boot_app0": "0xe000", + "bootloader_qio_80m": "0x1000", + "firmware": "0x10000", + "partitions": "0x8000", + "littlefs": "0x310000" + }, { "name": "esp32cam_4mb", "boot_app0": "0xe000", @@ -170,6 +184,14 @@ "path": "src/modules/virtual/Loging", "active": true }, + { + "path": "src/modules/virtual/Loging2", + "active": false + }, + { + "path": "src/modules/virtual/Loging3", + "active": false + }, { "path": "src/modules/virtual/LogingDaily", "active": true @@ -428,7 +450,7 @@ }, { "path": "src/modules/exec/Buzzer", - "active": true + "active": false }, { "path": "src/modules/exec/EctoControlAdapter", @@ -506,6 +528,10 @@ "path": "src/modules/exec/SysExt", "active": false }, + { + "path": "src/modules/exec/Tca9555", + "active": false + }, { "path": "src/modules/exec/Telegram", "active": false diff --git a/platformio.ini b/platformio.ini index 5b6945af..07ff82ef 100644 --- a/platformio.ini +++ b/platformio.ini @@ -375,6 +375,30 @@ build_src_filter = + ${env:esp32_16mb_fromitems.build_src_filter} +[env:esp32_wifirep] +lib_deps = + ${common_env_data.lib_deps_external} + ${env:esp32_wifirep_fromitems.lib_deps} +lib_ignore = LT_WebSockets +build_flags = + -Desp32_wifirep="esp32_wifirep" + -Wl,--wrap=esp_panic_handler +framework = arduino +board = esp32dev +platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.5.3/platform-espressif32-2.0.5.3.zip +monitor_filters = esp32_exception_decoder +upload_speed = 921600 +monitor_speed = 115200 +debug_tool = esp-prog +board_build.partitions = tools/partitions_custom.csv +board_build.filesystem = littlefs +build_src_filter = + +<*.cpp> + + + + + + + ${env:esp32_wifirep_fromitems.build_src_filter} + [env:esp32c6_4mb] extra_scripts = pre:tools/patch32c6.py lib_deps = @@ -894,6 +918,47 @@ build_src_filter = + + +[env:esp32_wifirep_fromitems] +lib_deps = + https://github.com/enjoyneering/AHTxx.git + adafruit/Adafruit BME280 Library + adafruit/Adafruit BMP280 Library + beegee-tokyo/DHT sensor library for ESPx + https://github.com/milesburton/Arduino-Temperature-Control-Library + plerup/EspSoftwareSerial + gyverlibs/EncButton @ ^2.0 +build_src_filter = + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [env:esp32c6_4mb_fromitems] lib_deps = https://github.com/enjoyneering/AHTxx.git diff --git a/src/Main.cpp b/src/Main.cpp index 18fd4a31..d512c7c5 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); @@ -276,9 +276,9 @@ void setup() { }, nullptr, true); - // ловим пинги от WS (5сек) и дисконнектим если их нет (20сек) + // ловим пинги от WS (2сек) и дисконнектим если их нет 3 раза 3сек*2прохода = 6сек ts.add( - PiWS, 6000, [&](void*) { + PiWS, 3000, [&](void*) { if (isNetworkActive()) { for (size_t i = 0; i < WEBSOCKETS_CLIENT_MAX; i++) { diff --git a/src/NTP.cpp b/src/NTP.cpp index c6b3a227..bac99e72 100644 --- a/src/NTP.cpp +++ b/src/NTP.cpp @@ -64,6 +64,10 @@ void synchTime() { if (sntp_enabled()) { sntp_stop(); } + sntp_setoperatingmode(SNTP_OPMODE_POLL); + sntp_setservername(0, jsonReadStr(settingsFlashJson, F("ntp")).c_str()); + sntp_setservername(1, "pool.ntp.org"); + sntp_setservername(2, "ru.pool.ntp.org"); sntp_init(); #else diff --git a/src/StandWebServer.cpp b/src/StandWebServer.cpp index f28ed01d..4ff4c90c 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,11 +94,18 @@ 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); + + HTTP.on("/update", HTTP_OPTIONS, handleCors); + // Default handler for all URIs not defined above // Use it to read files from filesystem HTTP.onNotFound(handleNotFound); @@ -164,6 +177,66 @@ void handleLocalOTA() { String page = "
"; HTTP.send(200, "text/html", page);} + +void handleCors() { + HTTP.sendHeader("Access-Control-Allow-Origin", "*"); + HTTP.send(200); + } + + 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; + } + #ifdef ESP8266 + size_t size = upload.totalSize; + int updatePartition = (typeOTAfile == FIRMWARE)? U_FLASH : U_FS; //U_FS + //#endif + #else //ESP32 + size_t size = UPDATE_SIZE_UNKNOWN; + int updatePartition = (typeOTAfile == FIRMWARE)? U_FLASH : U_SPIFFS; //U_FS + #endif + if (upload.status == UPLOAD_FILE_START) { + //Serial.print("Начало загрузки: "); + //Serial.println(upload.filename); + SerialPrint("i", F("OTA"), "Начало загрузки файла: " + upload.filename); + if (!Update.begin(size, updatePartition)) { // UPDATE_SIZE_UNKNOWN 0xFFFFFFFF + Update.end(); + SerialPrint("E", F("OTA"), "Ошибка: Недостаточно памяти"); + HTTP.send(500, "text/plain", "Ошибка: Недостаточно памяти"); + return; + } + } + 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", "Обновление успешно"); + ESP.restart(); + } else { + Update.end(); + SerialPrint("E", F("OTA"), "Ошибка завершения обновления"); + HTTP.send(500, "text/plain", "Ошибка завершения обновления"); + } + } + } void handleLocalOTA_Handler() { String serverValue = HTTP.arg("server"); upgrade_firmware(3,serverValue); diff --git a/src/WsServer.cpp b/src/WsServer.cpp index a4f5268a..0d2b3301 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -59,9 +59,9 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) //----------------------------------------------------------------------// // Страница веб интерфейса dashboard //----------------------------------------------------------------------// - if (headerStr == "p|") { - standWebSocket.sendTXT(num, "p|"); - //Serial.printf("Ping client: %u\n", num); + if (headerStr == "/pi|") { + standWebSocket.sendTXT(num, "/po|"); + Serial.printf("Ping client: %u\n", num); ws_clients[num]=1; } // публикация всех виджетов @@ -233,6 +233,50 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) sendStringToWs("settin", settingsFlashJson, num); } + if (headerStr == "/localt|") { + String timeStr = String((char*)payload + 8); + //Serial.println("Время с фронта: /localt|" + timeStr); + + // Обрезаем дробную часть, если есть + int dotIndex = timeStr.indexOf('.'); + if (dotIndex != -1) { + timeStr = timeStr.substring(0, dotIndex); + } + + // Парсим UNIX-время в секундах + time_t unixTime = (time_t)timeStr.toInt(); + + // Создаём структуру timeval + timeval tv; + tv.tv_sec = unixTime; // Секунды эпохи + tv.tv_usec = 0; // Микросекунды + + // Устанавливаем время + if (settimeofday(&tv, NULL) == 0) { + //Serial.printf("Время установлено: %ld\n", unixTime); + #ifdef LIBRETINY + SerialPrint("i", F("Time"), "Время установлено из браузера: "); + #else + SerialPrint("i", F("Time"), "Время установлено из браузера: " + String(unixTime)); + #endif + } else { + #ifdef LIBRETINY + //Serial.printf("Ошибка установки времени: %ld\n", unixTime); + SerialPrint("i", F("=>WS"), "Ошибка установки времени: "); + #else + SerialPrint("i", F("=>WS"), "Ошибка установки времени: " + String(unixTime)); + #endif + } + // timeval tv2{0, 0}; + // timezone tz = timezone{0, 0}; + // time_t epoch = 0; + // if (gettimeofday(&tv2, &tz) != -1) { + // epoch = tv2.tv_sec; + // } + // unixTime = epoch; + // SerialPrint("I", F("NTP"), "TIME " + String(unixTime)); + } + //----------------------------------------------------------------------// // Страница веб интерфейса dev //----------------------------------------------------------------------// diff --git a/src/classes/IoTScenario.cpp b/src/classes/IoTScenario.cpp index 7efa73ca..d920c77b 100644 --- a/src/classes/IoTScenario.cpp +++ b/src/classes/IoTScenario.cpp @@ -2,6 +2,7 @@ #include "classes/IoTItem.h" #include "classes/IoTScenario.h" #include "utils/FileUtils.h" +#include "utils/WiFiUtils.h" #include "NTP.h" @@ -358,7 +359,8 @@ enum SysOp { sysop_getUptime, sysop_mqttIsConnect, sysop_wifiIsConnect, - sysop_setInterval + sysop_setInterval, + sysop_addPortMap }; IoTValue sysExecute(SysOp command, std::vector ¶m) { @@ -470,6 +472,11 @@ IoTValue sysExecute(SysOp command, std::vector ¶m) { } break; + case sysop_addPortMap: + if (param.size() == 5) { + addPortMap(param[0].valS, param[1].valS, param[2].valD, param[3].valS, param[4].valD); + } + break; } return value; @@ -530,6 +537,8 @@ class SysCallExprAST : public ExprAST { operation = sysop_wifiIsConnect; else if (Callee == F("setInterval")) operation = sysop_setInterval; + else if (Callee == F("addPortMap")) + operation = sysop_addPortMap; else operation = sysop_notfound; } diff --git a/src/modules/exec/EctoControlAdapter/EctoControlAdapter.cpp b/src/modules/exec/EctoControlAdapter/EctoControlAdapter.cpp index ea4700cc..18511cc6 100644 --- a/src/modules/exec/EctoControlAdapter/EctoControlAdapter.cpp +++ b/src/modules/exec/EctoControlAdapter/EctoControlAdapter.cpp @@ -8,7 +8,6 @@ // #include "Stream.h" #include - // class ModbusUart; Stream *_modbusUART = nullptr; @@ -21,7 +20,6 @@ uint8_t _DIR_PIN = 0; #define MODBUS_TX_PIN 19 // Tx pin #define MODBUS_SERIAL_BAUD 9600 // Baud rate for esp32 and max485 communication - void modbusPreTransmission() { // delay(500); @@ -52,7 +50,7 @@ private: int protocol = SERIAL_8N1; uint8_t _addr = 0xF0; // Адрес слейва от 1 до 247 uint8_t _type = 0x14; // Тип устройства: 0x14 – адаптер OpenTherm (вторая версия); 0x11 – адаптер OpenTherm (первая версия, снята с производства) - bool _debugLevel; // Дебаг + uint8_t _debugLevel; // Дебаг ModbusMaster node; uint8_t _debug; @@ -136,36 +134,39 @@ public: { SerialPrint("E", "EctoControlAdapter", "Не подходящее устройство, type: " + String(type, HEX)); } + getModelVersion(); + getBoilerInfo(); + getBoilerStatus(); } else if (_addr == 0) { // если адреса нет, то шлем широковещательный запрос адреса uint8_t addr = node.readAddresEctoControl(); SerialPrint("I", "EctoControlAdapter", "readAddresEctoControl, addr: " + String(addr, HEX) + " - Enter to configuration"); } - getModelVersion(); - getBoilerInfo(); - getBoilerStatus(); } void doByInterval() { - // readBoilerInfo(); - getBoilerStatus(); + if (_addr > 0) + { + // readBoilerInfo(); + getBoilerStatus(); - getCodeError(); - getCodeErrorExt(); - if (info.adapterType == 0) - getFlagErrorOT(); - // getFlowRate(); - // getMaxSetCH(); - // getMaxSetDHW(); - // getMinSetCH(); - // getMinSetDHW(); - getModLevel(); - getPressure(); - getTempCH(); - getTempDHW(); - getTempOutside(); + getCodeError(); + getCodeErrorExt(); + if (info.adapterType == 0) + getFlagErrorOT(); + // getFlowRate(); + // getMaxSetCH(); + // getMaxSetDHW(); + // getMinSetCH(); + // getMinSetDHW(); + getModLevel(); + getPressure(); + getTempCH(); + getTempDHW(); + getTempOutside(); + } } void loop() @@ -176,10 +177,6 @@ public: IoTValue execute(String command, std::vector ¶m) { - if (command == "getModelVersion") - { - getModelVersion(); - } if (command == "getModelVersion") { getModelVersion(); @@ -324,13 +321,12 @@ public: tlgrmItem->sendTelegramMsg(false, msg); } - ~EctoControlAdapter() { - }; + ~EctoControlAdapter(){}; bool writeFunctionModBus(const uint16_t ®, uint16_t &data) { // set word 0 of TX buffer to least-significant word of counter (bits 15..0) - //node.setTransmitBuffer(1, lowWord(data)); + // node.setTransmitBuffer(1, lowWord(data)); // set word 1 of TX buffer to most-significant word of counter (bits 31..16) node.setTransmitBuffer(0, data); // slave: write TX buffer to (2) 16-bit registers starting at register 0 @@ -348,6 +344,7 @@ public: bool readFunctionModBus(const uint16_t ®, uint16_t &reading) { + if (_addr == 0) return false; // float retValue = 0; if (_modbusUART) { diff --git a/src/modules/exec/EctoControlAdapter/ModbusEC.cpp b/src/modules/exec/EctoControlAdapter/ModbusEC.cpp index 029e448b..d9d0818e 100644 --- a/src/modules/exec/EctoControlAdapter/ModbusEC.cpp +++ b/src/modules/exec/EctoControlAdapter/ModbusEC.cpp @@ -1,6 +1,9 @@ #include "ModbusEC.h" +#define COUNT_BIT_AVAIL 5 +#define COUNT_BIT_AVAIL_46F 4 + ModbusMaster::ModbusMaster(void) { _idle = 0; @@ -256,8 +259,8 @@ uint8_t ModbusMaster::readHoldingRegisters(uint16_t u16ReadAddress, /** Modbus function 0x06 Write Single Register. -This function code is used to write a single holding register in a -remote device. The request specifies the address of the register to be +This function code is used to write a single holding register in a +remote device. The request specifies the address of the register to be written. Registers are addressed starting at zero. @param u16WriteAddress address of the holding register (0x0000..0xFFFF) @@ -266,7 +269,7 @@ written. Registers are addressed starting at zero. @ingroup register */ uint8_t ModbusMaster::writeSingleRegister(uint16_t u16WriteAddress, - uint16_t u16WriteValue) + uint16_t u16WriteValue) { _u16WriteAddress = u16WriteAddress; _u16WriteQty = 0; @@ -274,7 +277,6 @@ uint8_t ModbusMaster::writeSingleRegister(uint16_t u16WriteAddress, return ModbusMasterTransaction(ku8MBWriteSingleRegister); } - /** Modbus function 0x10 Write Multiple Registers. @@ -308,7 +310,8 @@ uint8_t ModbusMaster::readAddresEctoControl() { _u16ReadAddress = 0x00; _u16ReadQty = 1; - return ModbusMasterTransaction(ku8MBProgRead46); + ModbusMasterTransaction(ku8MBProgRead46); + return getResponseBuffer(0x00); } uint8_t ModbusMaster::writeAddresEctoControl(uint8_t addr) { @@ -393,8 +396,17 @@ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) { u16CRC = crc16_update(u16CRC, u8ModbusADU[i]); } + // if (u8MBFunction == ku8MBProgRead46 || u8MBFunction == ku8MBProgWrite47) + // { + // u8ModbusADU[u8ModbusADUSize++] = highByte(u16CRC); + // u8ModbusADU[u8ModbusADUSize++] = lowByte(u16CRC); + // } + // else + // { u8ModbusADU[u8ModbusADUSize++] = lowByte(u16CRC); u8ModbusADU[u8ModbusADUSize++] = highByte(u16CRC); + // } + u8ModbusADU[u8ModbusADUSize] = 0; // flush receive buffer before transmitting request @@ -427,7 +439,9 @@ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) #if __MODBUSMASTER_DEBUG__ digitalWrite(__MODBUSMASTER_DEBUG_PIN_A__, true); #endif - u8ModbusADU[u8ModbusADUSize++] = _serial->read(); + uint8_t req = _serial->read(); + u8ModbusADU[u8ModbusADUSize++] = req; + Serial.print(req, HEX); u8BytesLeft--; #if __MODBUSMASTER_DEBUG__ digitalWrite(__MODBUSMASTER_DEBUG_PIN_A__, false); @@ -448,26 +462,34 @@ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) } // evaluate slave ID, function code once enough bytes have been read - if (u8ModbusADUSize == 5) + uint8_t count; + if (u8MBFunction == ku8MBProgRead46 || u8MBFunction == ku8MBProgWrite47) + count = COUNT_BIT_AVAIL_46F; + else + count = COUNT_BIT_AVAIL; + + if (u8ModbusADUSize == count) { - // verify response is for correct Modbus slave - if (u8ModbusADU[0] != _u8MBSlave) + if (u8MBFunction != ku8MBProgRead46 && u8MBFunction != ku8MBProgWrite47) { - // Serial.print(u8ModbusADU[0], HEX); - // Serial.print(" != "); - // Serial.println(_u8MBSlave, HEX); - - u8MBStatus = ku8MBInvalidSlaveID; - break; - } + // verify response is for correct Modbus slave + if (u8ModbusADU[0] != _u8MBSlave) + { + // Serial.print(u8ModbusADU[0], HEX); + // Serial.print(" != "); + // Serial.println(_u8MBSlave, HEX); - // verify response is for correct Modbus function code (mask exception bit 7) - if ((u8ModbusADU[1] & 0x7F) != u8MBFunction) - { - u8MBStatus = ku8MBInvalidFunction; - break; - } + u8MBStatus = ku8MBInvalidSlaveID; + break; + } + // verify response is for correct Modbus function code (mask exception bit 7) + if ((u8ModbusADU[1] & 0x7F) != u8MBFunction) + { + u8MBStatus = ku8MBInvalidFunction; + break; + } + } // check whether Modbus exception occurred; return Modbus Exception Code if (bitRead(u8ModbusADU[1], 7)) { @@ -485,6 +507,16 @@ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) case ku8MBWriteMultipleRegisters: u8BytesLeft = 3; break; + + case ku8MBProgRead46: + u8BytesLeft = 1; + break; + + case ku8MBProgWrite47: + u8BytesLeft = 1; + break; + + default: } } if ((millis() - u32StartTime) > ku16MBResponseTimeout) @@ -493,24 +525,26 @@ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) } } - // verify response is large enough to inspect further - if (!u8MBStatus && u8ModbusADUSize >= 5) + if (u8MBFunction != ku8MBProgRead46 && u8MBFunction != ku8MBProgWrite47) { - // calculate CRC - u16CRC = 0xFFFF; - for (i = 0; i < (u8ModbusADUSize - 2); i++) + // verify response is large enough to inspect further + if (!u8MBStatus && u8ModbusADUSize >= COUNT_BIT_AVAIL) { - u16CRC = crc16_update(u16CRC, u8ModbusADU[i]); - } + // calculate CRC + u16CRC = 0xFFFF; + for (i = 0; i < (u8ModbusADUSize - 2); i++) + { + u16CRC = crc16_update(u16CRC, u8ModbusADU[i]); + } - // verify CRC - if (!u8MBStatus && (lowByte(u16CRC) != u8ModbusADU[u8ModbusADUSize - 2] || - highByte(u16CRC) != u8ModbusADU[u8ModbusADUSize - 1])) - { - u8MBStatus = ku8MBInvalidCRC; + // verify CRC + if (!u8MBStatus && (lowByte(u16CRC) != u8ModbusADU[u8ModbusADUSize - 2] || + highByte(u16CRC) != u8ModbusADU[u8ModbusADUSize - 1])) + { + u8MBStatus = ku8MBInvalidCRC; + } } } - // disassemble ADU into words if (!u8MBStatus) { @@ -530,6 +564,12 @@ uint8_t ModbusMaster::ModbusMasterTransaction(uint8_t u8MBFunction) } break; case ku8MBProgRead46: + Serial.print("ku8MBProgRead46"); + for (i = 0; i < (u8ModbusADUSize); i++) + { + Serial.println(u8ModbusADU[i], HEX); + } + _u16ResponseBuffer[0] = (uint16_t)u8ModbusADU[2]; _u8ResponseBufferLength = 1; break; diff --git a/src/modules/virtual/Loging2/Loging2.cpp b/src/modules/virtual/Loging2/Loging2.cpp new file mode 100644 index 00000000..3749bbb8 --- /dev/null +++ b/src/modules/virtual/Loging2/Loging2.cpp @@ -0,0 +1,495 @@ +#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); // в минутах + setInterval(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 value2 = getItemValue(logid2); + String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + ",\"y2\":" + value2 + "}]}"; + 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..fa9254f3 --- /dev/null +++ b/src/modules/virtual/Loging3/Loging3.cpp @@ -0,0 +1,498 @@ +#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); // в минутах + setInterval(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 value2 = getItemValue(logid2); + String value3 = getItemValue(logid3); + String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + ",\"y2\":" + value2 + ",\"y3\":" + value3 + "}]}"; + 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/modules/virtual/LogingHourly/LogingHourly.cpp b/src/modules/virtual/LogingHourly/LogingHourly.cpp index 4d4ed935..74ff9c74 100644 --- a/src/modules/virtual/LogingHourly/LogingHourly.cpp +++ b/src/modules/virtual/LogingHourly/LogingHourly.cpp @@ -160,7 +160,11 @@ public: SerialPrint("E", F("LogingHourly"), "'" + id + "' db file writing error, return"); return; } + #ifdef LIBRETINY + SerialPrint("i", F("LogingHourly"), "'" + id + "' file created http://" + ipToString(WiFi.localIP()) + path); + #else SerialPrint("i", F("LogingHourly"), "'" + id + "' file created http://" + WiFi.localIP().toString() + path); + #endif } void addNewDataToExistingFile(String &path, String &logData) @@ -171,7 +175,11 @@ public: SerialPrint("i", F("LogingHourly"), "'" + id + "' file writing error, return"); return; }; + #ifdef LIBRETINY + SerialPrint("i", F("LogingHourly"), "'" + id + "' LogingHourly in file http://" + ipToString(WiFi.localIP()) + path); + #else SerialPrint("i", F("LogingHourly"), "'" + id + "' LogingHourly in file http://" + WiFi.localIP().toString() + path); + #endif } const String getTimeLocal_hh() { diff --git a/src/utils/WiFiUtils.cpp b/src/utils/WiFiUtils.cpp index 6ebcc37d..0bda37e0 100644 --- a/src/utils/WiFiUtils.cpp +++ b/src/utils/WiFiUtils.cpp @@ -3,9 +3,43 @@ #if defined(ESP32) #include #endif +#include "DebugTrace.h" #define TRIESONE 20 // количество секунд ожидания подключения к одной сети из несколких #define TRIES 30 // количество секунд ожидания подключения сети если она одна +#if defined(esp32_wifirep) +#include "lwip/lwip_napt.h" +// #include "lwip/ip_route.h" +#define PROTO_TCP 6 +#define PROTO_UDP 17 + +IPAddress stringToIp(String strIp) +{ + IPAddress ip; + ip.fromString(strIp); + return ip; +} +#endif +void addPortMap(String TCP_UDP, String maddr, u16_t mport, String daddr, u16_t dport) +{ +#if defined(esp32_wifirep) + uint8_t tcp_udp; + if (TCP_UDP == "TCP") + tcp_udp = PROTO_TCP; + else if (TCP_UDP == "UDP") + tcp_udp = PROTO_UDP; + else + SerialPrint("E", "WIFI", "Add port map: ERROR, Must be 'TCP' or 'UDP'"); + + ip_portmap_add(tcp_udp, stringToIp(maddr), mport, stringToIp(daddr), dport); + SerialPrint("i", "WIFI", "Add port map: " + String(tcp_udp) + ", " + maddr + ":" + String(mport) + " -> " + daddr + ":" + String(dport)); +#else + SerialPrint("E", "WIFI", "Add port map: ERROR, change board to esp32_wifirep"); +#endif +} + + + #ifdef WIFI_ASYNC std::vector _ssidList; std::vector _passwordList; @@ -50,6 +84,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 +108,7 @@ void WiFiEvent(arduino_event_t *event) } else { // если попытки подключения исчерпаны, то переходим в AP + sendDebugTraceAndFreeMemory(false); startAPMode(); } break; @@ -280,10 +319,65 @@ void ScanAsync() WiFi.scanNetworks(true, false); } } -#else -// ESP8266 +#else //WIFI_ASYNC + void routerConnect() { +#if defined(esp32_wifirep) +// Set custom dns server address for dhcp server +#define MY_DNS_IP_ADDR 0xC0A80401 // 192.168.4.1 // 0x08080808 // 8.8.8.8 + ip_addr_t dnsserver; + + String _ssidAP = jsonReadStr(settingsFlashJson, "apssid"); + String _passwordAP = jsonReadStr(settingsFlashJson, "appass"); + int _chanelAP = 0; + jsonRead(settingsFlashJson, "wifirep_apchanel", _chanelAP); + if (_chanelAP == 0) + _chanelAP = 7; + + // WiFi.begin(ssid, password); + WiFi.mode(WIFI_AP_STA); + + String s_apip = ""; + bool ap_ip = jsonRead(settingsFlashJson, "wifirep_apip", s_apip); + if (ap_ip && s_apip != "") + { + WiFi.softAPConfig(stringToIp(s_apip), stringToIp(s_apip), stringToIp("255.255.255.0")); + // bool softAPConfig(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dhcp_lease_start = (uint32_t) 0); + dnsserver.u_addr.ip4.addr = stringToIp(s_apip); + } + else + dnsserver.u_addr.ip4.addr = htonl(MY_DNS_IP_ADDR); + + dnsserver.type = IPADDR_TYPE_V4; + dhcps_dns_setserver(&dnsserver); + + WiFi.softAP(_ssidAP.c_str(), _passwordAP.c_str(), _chanelAP, 0, 5); + jsonWriteStr(settingsFlashJson, "ip", WiFi.softAPIP().toString()); + SerialPrint("i", "WIFI", "AP SSID: " + WiFi.softAPSSID()); + SerialPrint("i", "WIFI", "AP IP: " + WiFi.softAPIP().toString()); + SerialPrint("i", "WIFI", "AP pass: " + _passwordAP); + + String s_staip = ""; + bool static_ip = jsonRead(settingsFlashJson, "wifirep_staip", s_staip); + String s_gateway = jsonReadStr(settingsFlashJson, "wifirep_gateway"); + String s_netmask = jsonReadStr(settingsFlashJson, "wifirep_netmask"); + String s_dns = jsonReadStr(settingsFlashJson, "wifirep_dns"); + + if (static_ip == true && s_staip != "") + { + SerialPrint("i", "WIFI", "Use static IP"); + WiFi.config(stringToIp(s_staip), stringToIp(s_gateway), stringToIp(s_netmask), stringToIp(s_dns)); + // bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = (uint32_t)0x00000000, IPAddress dns2 = (uint32_t)0x00000000); + SerialPrint("i", "WIFI", "Static IP: " + s_staip); + SerialPrint("i", "WIFI", "Gateway: " + s_gateway); + SerialPrint("i", "WIFI", "Netmask: " + s_netmask); + SerialPrint("i", "WIFI", "DNS: " + s_dns); + } +#else + WiFi.mode(WIFI_STA); +#endif + #if !defined LIBRETINY #if defined(esp32c6_4mb) || defined(esp32c6_8mb) WiFi.setAutoReconnect(false); @@ -303,7 +397,7 @@ void routerConnect() SerialPrint("i", "WIFI", "Gateway: " + s_gateway); SerialPrint("i", "WIFI", "Netmask: " + s_netmask); SerialPrint("i", "WIFI", "DNS: " + s_dns); */ - WiFi.mode(WIFI_STA); + //WiFi.mode(WIFI_STA); byte triesOne = TRIESONE; std::vector _ssidList; @@ -390,6 +484,25 @@ void routerConnect() jsonWriteStr(settingsFlashJson, "ip", WiFi.localIP().toString()); #endif createItemFromNet("onWifi", "1", 1); + +#if defined(esp32_wifirep) + // Enable DNS (offer) for dhcp server + dhcps_offer_t dhcps_dns_value = OFFER_DNS; + dhcps_set_option_info(6, &dhcps_dns_value, sizeof(dhcps_dns_value)); + u32_t napt_netif_ip; + if (ap_ip && s_apip != "") + napt_netif_ip = stringToIp(s_apip); + else + { + napt_netif_ip = 0xC0A80401; // Set to ip address of softAP netif (Default is 192.168.4.1) + napt_netif_ip = htonl(napt_netif_ip); + } + // get_esp_interface_netif(ESP_IF_WIFI_AP) + ip_napt_enable(napt_netif_ip, 1); + // ip_napt_enable_no(ESP_IF_WIFI_AP, 1); + +#endif + mqttInit(); } SerialPrint("i", F("WIFI"), F("Network Init"));