From 57cf6e1063f76222ec6340727f788c51874716e6 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Sat, 8 Apr 2023 23:07:14 +0300 Subject: [PATCH 1/7] =?UTF-8?q?=D0=9D=D0=BE=D0=B2=D1=8B=D0=B9=20Acs712,=20?= =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8=D0=BB=20=D0=BE=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20Pzem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/Acs712/Acs712.cpp | 101 ++++++++++++++++------ src/modules/sensors/Acs712/modinfo.json | 36 +++++--- src/modules/sensors/Pzem004t/modinfo.json | 2 +- 3 files changed, 102 insertions(+), 37 deletions(-) diff --git a/src/modules/sensors/Acs712/Acs712.cpp b/src/modules/sensors/Acs712/Acs712.cpp index 79d9bc76..ed4984e2 100644 --- a/src/modules/sensors/Acs712/Acs712.cpp +++ b/src/modules/sensors/Acs712/Acs712.cpp @@ -3,6 +3,14 @@ extern IoTGpio IoTgpio; +#ifdef ESP32 + #define SC_ADC 4095. //Scale ADC +#else + #define SC_ADC 1023. //Scale ADC +#endif + +#define DEF_NAN 50 //количество отсчетов АПЦ которые мы принимаем за отсутствие датчика (в идеале 0, но все равно Acs712 выдает минимум 0,5В) + class Acs712 : public IoTItem { private: @@ -10,56 +18,97 @@ private: const unsigned long _sampleTime = 100000UL; // sample over 100ms, it is an exact number of cycles for both 50Hz and 60Hz mains const unsigned long _numSamples = 250UL; // choose the number of samples to divide sampleTime exactly, but low enough for the ADC to keep up const unsigned long _sampleInterval = _sampleTime / _numSamples; // the sampling interval, must be longer than then ADC conversion time - int _adc_zero1; //Переменная автоматической калибровки + int _adc_zero1; // Переменная автоматической калибровки + int _fl_rms; // 1 - подсчет средне-квадратического тока (переменный), 0 - подсчет средне-арифмитического тока (постоянный) + int _sens = 100; //Чувствительность датчика тока: 5A = 185mВ/A, 20A = 100mВ/A, 30A = 66mВ/A + int _vref; //"Vref (мВ) - Опороное наряжение питания Acs712, по умолчанию = 5000мВ", + float k ; //Чувствительность(разрешение) Acs712 по току, сколько тока в одном отсчете АЦП, k=VACP/sens (30А->74mА, 20A->49mA, 5A->26mA для esp8266) + bool f_nan = false; //Флаг отсутствия входа на АЦП + float vacp; //Напряжение в мВ для смещения одного разряда АЦП esp8266 = 4.887, esp32 = 1.221; vacp = vcc*1000/1023 public: - Acs712(String parameters) : IoTItem(parameters) { String tmp; jsonRead(parameters, "pin", tmp); _pin = tmp.toInt(); - _adc_zero1 = determineVQ(_pin); + jsonRead(parameters, "adczero", tmp); + _adc_zero1 = tmp.toInt(); // determineVQ(_pin); + jsonRead(parameters, "rms", tmp); + _fl_rms = tmp.toInt(); + jsonRead(parameters, "sens", tmp); + _sens = tmp.toInt(); + jsonRead(parameters, "vref", tmp); + _vref = tmp.toInt(); + vacp = _vref/SC_ADC; + k = vacp / (float)_sens; //коэффециент для домножения измерений АЦП } - + void doByInterval() { - - unsigned long currentAcc = 0; - unsigned int count = 0; - unsigned long prevMicros = micros() - _sampleInterval; - while (count < _numSamples) + f_nan = false; + unsigned long currentAcc = 0; + unsigned int count = 0; + unsigned long prevMicros = micros() - _sampleInterval; + while (count < _numSamples) + { + if (micros() - prevMicros >= _sampleInterval) { - if (micros() - prevMicros >= _sampleInterval) - { - int adc_raw = IoTgpio.analogRead(_pin) - _adc_zero1; + int adc_raw = IoTgpio.analogRead(_pin); + if (adc_raw > DEF_NAN) f_nan = true; //Если за цикл измерений не было АЦП больше 50, то считаем что нет датчика + adc_raw -= _adc_zero1; + if (_fl_rms == 0) + currentAcc += (unsigned long)abs(adc_raw); + else currentAcc += (unsigned long)(adc_raw * adc_raw); - ++count; - prevMicros += _sampleInterval; - } + ++count; + prevMicros += _sampleInterval; } - #ifdef ESP32 - value.valD = int(sqrt((float)currentAcc / (float)_numSamples) * (75.7576 / 4095.0)); - #else - value.valD = int(sqrt((float)currentAcc / (float)_numSamples) * (75.7576 / 1023.0)); - #endif + } + + if (_fl_rms == 0) + { +#ifdef ESP32 + value.valD = ((float)currentAcc / (float)_numSamples) * k; +#else + value.valD = ((float)currentAcc / (float)_numSamples) * k; +#endif + } + else + { +#ifdef ESP32 + value.valD = (sqrt((float)currentAcc / (float)_numSamples) * k); +#else + value.valD = (sqrt((float)currentAcc / (float)_numSamples) * k); +#endif + } + if (f_nan) regEvent(value.valD, "Acs712"); + else + regEvent(NAN, "Acs712"); } - + + void onModuleOrder(String &key, String &value) + { + if (key == "setZero") + { + _adc_zero1 = determineVQ(_pin); + SerialPrint("i", F("Acs712"), "User run calibration ADC zero: " + String(_adc_zero1)); + // TODO wtitejson to config.json????? + } + } + int determineVQ(int PIN) { long VQ = 0; - // read 5000 samples to stabilise value - for (int i = 0; i < 5000; i++) + for (int i = 0; i < 100; i++) { VQ += IoTgpio.analogRead(PIN); - //delay(1); // depends on sampling (on filter capacitor), can be 1/80000 (80kHz) max. } - VQ /= 5000; + VQ /= 100; return int(VQ); } - ~Acs712(){}; }; diff --git a/src/modules/sensors/Acs712/modinfo.json b/src/modules/sensors/Acs712/modinfo.json index b9b15347..a3540924 100644 --- a/src/modules/sensors/Acs712/modinfo.json +++ b/src/modules/sensors/Acs712/modinfo.json @@ -3,7 +3,7 @@ "configItem": [ { "name": "Acs712 Ток", - "type": "Reading", + "type": "Reading", "subtype": "Acs712", "id": "amp", "widget": "anydataAmp", @@ -11,26 +11,42 @@ "descr": "Ток", "round": 3, "pin": 39, - "int": 5 + "int": 5, + "rms": 1, + "vref": 5000, + "sens": 100, + "adczero" : 512, + "btn-setZero": "nil" } ], "about": { - "authorName": "Yuriy Kuneev", - "authorContact": "https://t.me/Kuneev07", - "authorGit": "", + "authorName": "Bubnov Mikhail", + "authorContact": "https://t.me/Mitchel", + "authorGit": "https://github.com/Mit4el", "exampleURL": "https://iotmanager.org/wiki", "specialThanks": "", "moduleName": "Acs712", - "moduleVersion": "1.0", - "moduleDesc": "Позволяет получить текущее значение тока на аналоговом пине с помощью модуля Acs712.", + "moduleVersion": "2.0", + "moduleDesc": "Позволяет получить текущее значение тока на аналоговом пине с помощью модуля Acs712. Не забываем про делитель для входа на АЦП/", "propInfo": { - "pin": "Аналоговый GPIO номер, к которому подключен датчик.", - "int": "Количество секунд между опросами датчика." + "pin": "Аналоговый GPIO номер, к которому подключен датчик. Для esp8266 0", + "int": "Количество секунд между опросами датчика.", + "rms": "1 - подсчет средне-квадратического тока (переменный), 0 - подсчет средне-арифмитического тока (постоянный)", + "vref": "Vref (мВ) - Опороное наряжение питания Acs712, по умолчанию = 5000мВ", + "sens": "Чувствительность датчика тока: 5A = 185mВ/A , 20A = 100mВ/A , 30A = 66mВ/A ", + "adczero" : "Переменная калибровки нулевого значения отсчетов АЦП при нулевой нагрузке. Для ESP8266 - 512, Для ESP32 -2048, это 2.5В = 0А (1,65 с делителем) для Acs712 20A и 30A при стабильном токе 5В", + "btn-setZero": "Кнопка калибровки нулевого значения отсчетов АЦП при нулевой нагрузке. Нагрузка в момент калибровки должна быть отключена! После перезагрузки будет установлено в значение по умолчанию adczero. Для сохранение смотрим лог, и изменияем adczero" } }, "defActive": true, "usedLibs": { "esp32_4mb": [], - "esp8266_4mb": [] + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [], + "esp8285_1mb": [], + "esp8285_1mb_ota": [], + "esp8266_2mb": [], + "esp8266_2mb_ota": [] } } \ No newline at end of file diff --git a/src/modules/sensors/Pzem004t/modinfo.json b/src/modules/sensors/Pzem004t/modinfo.json index 56a1fd58..697015f9 100644 --- a/src/modules/sensors/Pzem004t/modinfo.json +++ b/src/modules/sensors/Pzem004t/modinfo.json @@ -119,7 +119,7 @@ "moduleDesc": "Считает потраченную электроэнергию, измеряет напряжение, частоту, силу тока и прочие параметры", "propInfo": { "addr": "Адрес modbus", - "int": "Количество секунд между опросами датчика. Желателно устанавливать разные интервалы для параметров что бы опросы происходили в разное время.", + "int": "Количество секунд между опросами датчика. Желателно устанавливать одинаковые интервалы для параметров (для одного адреса Pzem) что опрос происходил один раз, остальные из 500мс буфера.", "changeaddr": "Поставьте этот параметр равным 1 и перезагрузите esp - будет установлен адрес указанный в setaddr. Смотрите в логе результат: [i] Pzem address set: 0x01", "setaddr": "Новый адрес который нужно назначить", "reset": "Поставьте этот параметр равным 1 и pzem будет сброшен к нулю. Смотрите в логе результат: [i] Pzem reset done" From 4a93fd09dc3a154051e9f2f64d18848cbe72026a Mon Sep 17 00:00:00 2001 From: Mit4el Date: Tue, 25 Apr 2023 22:39:21 +0300 Subject: [PATCH 2/7] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83=20lib=5Fdeps=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20esp8266=5F2mb=5Fota?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- platformio.ini | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 7ba76a98..221c5224 100644 --- a/platformio.ini +++ b/platformio.ini @@ -94,7 +94,7 @@ build_src_filter = [env:esp8266_2mb_ota] lib_deps = ${common_env_data.lib_deps_external} - ${env:esp8266_2mb_fromitems.lib_deps} + ${env:esp8266_2mb_ota_fromitems.lib_deps} ESPAsyncUDP build_flags = -Desp8266_2mb_ota="esp8266_2mb_ota" framework = arduino @@ -110,7 +110,7 @@ build_src_filter = + + + - ${env:esp8266_2mb_fromitems.build_src_filter} + ${env:esp8266_2mb_ota_fromitems.build_src_filter} [env:esp8285_1mb] lib_deps = @@ -232,6 +232,7 @@ build_src_filter = [env:esp8266_2mb_ota_fromitems] lib_deps = + adafruit/Adafruit BME280 Library plerup/EspSoftwareSerial build_src_filter = + @@ -241,6 +242,7 @@ build_src_filter = + + + + + + + + From eeff84201dcd4f629780a327efa66d2b5d7e6308 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Tue, 25 Apr 2023 22:41:25 +0300 Subject: [PATCH 3/7] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20A02Distance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/widgets.json | 7 ++ .../sensors/A02Distance/A02Distance.cpp | 99 +++++++++++++++++++ src/modules/sensors/A02Distance/modinfo.json | 40 ++++++++ 3 files changed, 146 insertions(+) create mode 100644 src/modules/sensors/A02Distance/A02Distance.cpp create mode 100644 src/modules/sensors/A02Distance/modinfo.json diff --git a/data_svelte/widgets.json b/data_svelte/widgets.json index 4c681bff..1354e09d 100644 --- a/data_svelte/widgets.json +++ b/data_svelte/widgets.json @@ -230,6 +230,13 @@ "after": "mWt", "icon": "speedometer" }, + { + "name": "anydataCm", + "label": "Сантиметры", + "widget": "anydata", + "after": "cm", + "icon": "speedometer" + }, { "name": "nil", "label": "Без виджета" diff --git a/src/modules/sensors/A02Distance/A02Distance.cpp b/src/modules/sensors/A02Distance/A02Distance.cpp new file mode 100644 index 00000000..a3a9ac33 --- /dev/null +++ b/src/modules/sensors/A02Distance/A02Distance.cpp @@ -0,0 +1,99 @@ + +#include "Global.h" +#include "classes/IoTItem.h" + +#include "modules/sensors/UART/Uart.h" + +#define READ_TIMEOUT 100 + +class A02Distance : public IoTItem +{ +private: +public: + A02Distance(String parameters) : IoTItem(parameters) + { + if (myUART) + { + } + } + +//Периодическое выполнение программы, в int секунд, которые зададим в конфигурации + void doByInterval() + { + if (myUART) + { + static uint8_t data[4]; + + if (recieve(data, 4) == 4) + { + if (data[0] == 0xff) + { + int sum; + sum = (data[0] + data[1] + data[2]) & 0x00FF; + if (sum == data[3]) + { + float distance = (data[1] << 8) + data[2]; + if (distance > 30) + { + value.valD = distance / 10; + //SerialPrint("i", F("A02Distance"), "distance = " + String(value.valD) + "cm"); + regEvent(value.valD, "A02Distance"); + } + else + { + SerialPrint("E", "A02Distance", "Below the lower limit"); + regEvent(NAN, "A02Distance"); + } + } + else + { + regEvent(NAN, "A02Distance"); + SerialPrint("E", "A02Distance", "Distance data error"); + } + } + } else { + regEvent(NAN, "A02Distance"); + SerialPrint("E", "A02Distance", "Recieve data error"); + } + } else + { + regEvent(NAN, "A02Distance"); + SerialPrint("E", "A02Distance", "Not find UART"); + } + + } + + + //Приём данных из COM порта + uint16_t recieve(uint8_t *resp, uint16_t len) + { + ((SoftwareSerial *)myUART)->listen(); // Start software serial listen + unsigned long startTime = millis(); // Start time for Timeout + uint8_t index = 0; // Bytes we have read + while ((index < len) && (millis() - startTime < READ_TIMEOUT)) + { + if (myUART->available() > 0) + { + uint8_t c = (uint8_t)myUART->read(); + resp[index++] = c; + } + } + return index; + } + + ~A02Distance(){}; +}; + + +//Функиця ядра, чтобы нашел наш модуль +void *getAPI_A02Distance(String subtype, String param) +{ + if (subtype == F("A02Distance")) + { + return new A02Distance(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/sensors/A02Distance/modinfo.json b/src/modules/sensors/A02Distance/modinfo.json new file mode 100644 index 00000000..4e6d2368 --- /dev/null +++ b/src/modules/sensors/A02Distance/modinfo.json @@ -0,0 +1,40 @@ +{ + "menuSection": "Сенсоры", + "configItem": [ + { + "name": "A02 Дальность", + "type": "Reading", + "subtype": "A02Distance", + "id": "dist", + "widget": "anydataCm", + "page": "Сенсоры", + "descr": "Дальность", + "int": 5, + "round": 1 + } + ], + "about": { + "authorName": "Bubnov Mikhail", + "authorContact": "https://t.me/Mitchel", + "authorGit": "https://github.com/Mit4el", + "exampleURL": "https://iotmanager.org/wiki", + "specialThanks": "", + "moduleName": "A02Distance", + "moduleVersion": "0.1", + "moduleDesc": "A0221AU, A02YYUW Ультразвуковой датчик. Позволяет получить дальность с ультрозвуковых датчиков A0221AU, A02YYUW", + "propInfo": { + "int": "Количество секунд между опросами датчика." + } + }, + "defActive": true, + "usedLibs": { + "esp32_4mb": [], + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [], + "esp8285_1mb": [], + "esp8285_1mb_ota": [], + "esp8266_2mb": [], + "esp8266_2mb_ota": [] + } +} \ No newline at end of file From 80f8a917cd85c6879f7e60074fd4b8b6911f8f5f Mon Sep 17 00:00:00 2001 From: Mit4el Date: Tue, 25 Apr 2023 22:43:15 +0300 Subject: [PATCH 4/7] =?UTF-8?q?config.json=20=D1=82=D0=B5=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D1=8C=20=D0=BD=D0=B5=20=D0=BB=D0=BE=D0=BC=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B5=D1=81=D0=BB=D0=B8=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D1=88=D0=BE=D0=B9=20(large->[])?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/FileUtils.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/FileUtils.cpp b/src/utils/FileUtils.cpp index ccee03a1..f59316d5 100644 --- a/src/utils/FileUtils.cpp +++ b/src/utils/FileUtils.cpp @@ -124,6 +124,8 @@ const String readFile(const String& filename, size_t max_size) { size_t size = file.size(); if (size > max_size) { file.close(); + if (path == "/config.json") + return "[]"; return "large"; } String temp = file.readString(); From 4342269e448aa0e23b32c087d63f6b26e0c140fa Mon Sep 17 00:00:00 2001 From: Mit4el Date: Thu, 27 Apr 2023 01:29:23 +0300 Subject: [PATCH 5/7] =?UTF-8?q?=D0=9D=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D1=83=D0=B7=D0=B5=D1=80=20/edit.=20=D0=9E=D1=81?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BD=D0=BE=D0=B5:=20=D0=BB=D0=B5=D0=B3?= =?UTF-8?q?=D1=87=D0=B5,=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B8,=20=D0=B2=D0=B8?= =?UTF-8?q?=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/edit.htm.gz | Bin 5286 -> 6352 bytes data_svelte_lite/edit.htm.gz | Bin 5286 -> 6352 bytes include/StandWebServer.h | 9 +- src/StandWebServer.cpp | 769 ++++++++++++++++++++++++++++------- 4 files changed, 619 insertions(+), 159 deletions(-) diff --git a/data_svelte/edit.htm.gz b/data_svelte/edit.htm.gz index d41d10c1c56f308ad3cac07e98993703426b577d..06931fac0664792be15627e4bb544cefd041e4f9 100644 GIT binary patch literal 6352 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 0) + { + path = path.substring(0, path.lastIndexOf('/')); + } + else + { + path = String(); // No slash => the top folder does not exist + } + } +// DBG_OUTPUT_PORT.println(String("Last existing parent: ") + path); + return path; } -// Здесь функции для работы с файловой системой -void handleFileUpload() { - if (HTTP.uri() != "/edit") return; - HTTPUpload& upload = HTTP.upload(); - if (upload.status == UPLOAD_FILE_START) { +/* + Handle a file upload request +*/ +void handleFileUpload() +{ + if (HTTP.uri() != "/edit") + { + return; + } + HTTPUpload &upload = HTTP.upload(); + if (upload.status == UPLOAD_FILE_START) + { String filename = upload.filename; - if (!filename.startsWith("/")) filename = "/" + filename; - fsUploadFile = FileFS.open(filename, "w"); - filename = String(); - } else if (upload.status == UPLOAD_FILE_WRITE) { - // Serial.print("handleFileUpload Data: "); Serial.println(upload.currentSize); - if (fsUploadFile) - fsUploadFile.write(upload.buf, upload.currentSize); - } else if (upload.status == UPLOAD_FILE_END) { - if (fsUploadFile) - fsUploadFile.close(); + // Make sure paths always start with "/" + if (!filename.startsWith("/")) + { + filename = "/" + filename; + } +// DBG_OUTPUT_PORT.println(String("handleFileUpload Name: ") + filename); + uploadFile = FileFS.open(filename, "w"); + if (!uploadFile) + { + return replyServerError(F("CREATE FAILED")); + } +// DBG_OUTPUT_PORT.println(String("Upload: START, filename: ") + filename); + } + else if (upload.status == UPLOAD_FILE_WRITE) + { + if (uploadFile) + { + size_t bytesWritten = uploadFile.write(upload.buf, upload.currentSize); + if (bytesWritten != upload.currentSize) + { + return replyServerError(F("WRITE FAILED")); + } + } +// DBG_OUTPUT_PORT.println(String("Upload: WRITE, Bytes: ") + upload.currentSize); + } + else if (upload.status == UPLOAD_FILE_END) + { + if (uploadFile) + { + uploadFile.close(); + } +// DBG_OUTPUT_PORT.println(String("Upload: END, Size: ") + upload.totalSize); } } -void handleFileDelete() { - if (HTTP.args() == 0) return HTTP.send(500, "text/plain", "BAD ARGS"); + +#ifdef ESP8266 +void deleteRecursive(String path) +{ + File file = FileFS.open(path, "r"); + bool isDir = file.isDirectory(); + file.close(); + + // If it's a plain file, delete it + if (!isDir) + { + FileFS.remove(path); + return; + } + Dir dir = FileFS.openDir(path); + while (dir.next()) + { + deleteRecursive(path + '/' + dir.fileName()); + } + + // Then delete the folder itself + FileFS.rmdir(path); +} +#endif + +#ifdef ESP32 +struct treename{ + uint8_t type; + char *name; +}; + + +void deleteRecursive( String path ){ + fs::File dir = FileFS.open( path ); + + if(!dir.isDirectory()){ + Serial.printf("%s is a file\n", path); + dir.close(); + Serial.printf( "result of removing file %s: %d\n", path, FileFS.remove( path ) ); + return; + } + + Serial.printf("%s is a directory\n", path); + + fs::File entry, nextentry; + + while ( entry = dir.openNextFile() ){ + +if ( entry.isDirectory() ){ + deleteRecursive( entry.path() ); + } else{ + String tmpname = path+"/"+strdup( entry.name() ); // buffer file name + entry.close(); + Serial.printf( "result of removing file %s: %d\n", tmpname, FileFS.remove( tmpname ) ); + } + + } + + dir.close(); + Serial.printf( "result of removing directory %s: %d\n", path, FileFS.rmdir( path ) ); + +} +#endif +/* + Handle a file deletion request + Operation | req.responseText + ---------------+-------------------------------------------------------------- + Delete file | parent of deleted file, or remaining ancestor + Delete folder | parent of deleted folder, or remaining ancestor +*/ +void handleFileDelete() +{ String path = HTTP.arg(0); - if (path == "/") - return HTTP.send(500, "text/plain", "BAD PATH"); + if (path.isEmpty() || path == "/") + { + return replyBadRequest("BAD PATH"); + } + +// DBG_OUTPUT_PORT.println(String("handleFileDelete: ") + path); if (!FileFS.exists(path)) - return HTTP.send(404, "text/plain", "FileNotFound"); - FileFS.remove(path); - HTTP.send(200, "text/plain", ""); - path = String(); + { + return replyNotFound(FPSTR(FILE_NOT_FOUND)); + } + deleteRecursive(path); + + + + replyOKWithMsg(lastExistingParent(path)); } -void handleFileCreate() { - if (HTTP.args() == 0) - return HTTP.send(500, "text/plain", "BAD ARGS"); - String path = HTTP.arg(0); +/* + Handle the creation/rename of a new file + Operation | req.responseText + ---------------+-------------------------------------------------------------- + Create file | parent of created file + Create folder | parent of created folder + Rename file | parent of source file + Move file | parent of source file, or remaining ancestor + Rename folder | parent of source folder + Move folder | parent of source folder, or remaining ancestor +*/ +void handleFileCreate() +{ + String path = HTTP.arg("path"); + if (path.isEmpty()) + { + return replyBadRequest(F("PATH ARG MISSING")); + } + +#ifdef USE_SPIFFS + if (checkForUnsupportedPath(path).length() > 0) + { + return replyServerError(F("INVALID FILENAME")); + } +#endif + if (path == "/") - return HTTP.send(500, "text/plain", "BAD PATH"); + { + return replyBadRequest("BAD PATH"); + } if (FileFS.exists(path)) - return HTTP.send(500, "text/plain", "FILE EXISTS"); - File file = FileFS.open(path, "w"); - if (file) - file.close(); + { + return replyBadRequest(F("PATH FILE EXISTS")); + } + + String src = HTTP.arg("src"); + if (src.isEmpty()) + { + // No source specified: creation +// DBG_OUTPUT_PORT.println(String("handleFileCreate: ") + path); + if (path.endsWith("/")) + { + // Create a folder + path.remove(path.length() - 1); + if (!FileFS.mkdir(path)) + { + return replyServerError(F("MKDIR FAILED")); + } + } + else + { + // Create a file + File file = FileFS.open(path, "w"); + if (file) + { +#ifdef ESP8266 + file.write((const char *)0); +#endif +#ifdef ESP32 + file.write(0); +#endif + file.close(); + } + else + { + return replyServerError(F("CREATE FAILED")); + } + } + if (path.lastIndexOf('/') > -1) + { + path = path.substring(0, path.lastIndexOf('/')); + } + replyOKWithMsg(path); + } else - return HTTP.send(500, "text/plain", "CREATE FAILED"); - HTTP.send(200, "text/plain", ""); - path = String(); + { + // Source specified: rename + if (src == "/") + { + return replyBadRequest("BAD SRC"); + } + if (!FileFS.exists(src)) + { + return replyBadRequest(F("SRC FILE NOT FOUND")); + } + +// DBG_OUTPUT_PORT.println(String("handleFileCreate: ") + path + " from " + src); + + if (path.endsWith("/")) + { + path.remove(path.length() - 1); + } + if (src.endsWith("/")) + { + src.remove(src.length() - 1); + } + if (!FileFS.rename(src, path)) + { + return replyServerError(F("RENAME FAILED")); + } + replyOKWithMsg(lastExistingParent(src)); + } } -void handleFileList() { - File dir = FileFS.open("/", "r"); - String output = "["; - File entry; - while (entry = dir.openNextFile()) { - if (output != "[") output += ','; - bool isDir = entry.isDirectory(); +/* + Return the list of files in the directory specified by the "dir" query string parameter. + Also demonstrates the use of chunked responses. +*/ +#ifdef ESP8266 +void handleFileList() +{ + if (!HTTP.hasArg("dir")) + { + return replyBadRequest(F("DIR ARG MISSING")); + } + + String path = HTTP.arg("dir"); + if (path != "/" && !FileFS.exists(path)) + { + return replyBadRequest("BAD PATH"); + } + +// DBG_OUTPUT_PORT.println(String("handleFileList: ") + path); + Dir dir = FileFS.openDir(path); + path.clear(); + + // use HTTP/1.1 Chunked response to avoid building a huge temporary string + if (!HTTP.chunkedResponseModeStart(200, "text/json")) + { + HTTP.send(505, F("text/html"), F("HTTP1.1 required")); + return; + } + + // use the same string for every line + String output; + output.reserve(64); + while (dir.next()) + { +#ifdef USE_SPIFFS + String error = checkForUnsupportedPath(dir.fileName()); + if (error.length() > 0) + { +// DBG_OUTPUT_PORT.println(String("Ignoring ") + error + dir.fileName()); + continue; + } +#endif + if (output.length()) + { + // send string from previous iteration + // as an HTTP chunk + HTTP.sendContent(output); + output = ','; + } + else + { + output = '['; + } + output += "{\"type\":\""; - output += (isDir) ? "dir" : "file"; - output += "\",\"name\":\""; - output += String(entry.name()); + if (dir.isDirectory()) + { + output += "dir"; + } + else + { + output += F("file\",\"size\":\""); + output += dir.fileSize(); + } + + output += F("\",\"name\":\""); + // Always return names without leading "/" + if (dir.fileName()[0] == '/') + { + output += &(dir.fileName()[1]); + } + else + { + output += dir.fileName(); + } + output += "\"}"; - entry.close(); } + + // send last string output += "]"; - //Serial.println(output); - HTTP.send(200, "text/json", output); + HTTP.sendContent(output); + HTTP.chunkedResponseFinalize(); +} +#endif + +#ifdef ESP32 +void handleFileList() { + if (!HTTP.hasArg("dir")) { + HTTP.send(500, "text/plain", "BAD ARGS"); + return; + } + + String path = HTTP.arg("dir"); +// DBG_OUTPUT_PORT.println("handleFileList: " + path); + + + File root = FileFS.open(path); + path = String(); + + String output = "["; + if(root.isDirectory()){ + File file = root.openNextFile(); + while(file){ + if (output != "[") { + output += ','; + } + output += "{\"type\":\""; + // output += (file.isDirectory()) ? "dir" : "file"; + if (file.isDirectory()) + { + output += "dir"; + } + else + { + output += F("file\",\"size\":\""); + output += file.size(); + } + + output += "\",\"name\":\""; + output += String(file.name()); + output += "\"}"; + file = root.openNextFile(); + } + } + output += "]"; + HTTP.send(200, "text/json", output); + +} +#endif + + +/* + The "Not Found" handler catches all URI not explicitly declared in code + First try to find and return the requested file from the filesystem, + and if it fails, return a 404 page with debug information +*/ +void handleNotFound() +{ +#ifdef ESP8266 + String uri = ESP8266WebServer::urlDecode(HTTP.uri()); // required to read paths with blanks +#endif +#ifdef ESP32 + String uri = WebServer::urlDecode(HTTP.uri()); // required to read paths with blanks +#endif + if (handleFileRead(uri)) + { + return; + } + + // Dump debug data + String message; + message.reserve(100); + message = F("Error: File not found\n\nURI: "); + message += uri; + message += F("\nMethod: "); + message += (HTTP.method() == HTTP_GET) ? "GET" : "POST"; + message += F("\nArguments: "); + message += HTTP.args(); + message += '\n'; + for (uint8_t i = 0; i < HTTP.args(); i++) + { + message += F(" NAME:"); + message += HTTP.argName(i); + message += F("\n VALUE:"); + message += HTTP.arg(i); + message += '\n'; + } + message += "path="; + message += HTTP.arg("path"); + message += '\n'; +// DBG_OUTPUT_PORT.print(message); + + return replyNotFound(message); } -void printDirectory(File dir, String& out) { - while (true) { - File entry = dir.openNextFile(); - if (!entry) { - break; - } - if (entry.isDirectory()) { - out += entry.name(); - out += "/"; - printDirectory(entry, out); - } else { - out += entry.name(); - out += "\r\n"; - } +/* + This specific handler returns the index.htm (or a gzipped version) from the /edit folder. + If the file is not present but the flag INCLUDE_FALLBACK_INDEX_HTM has been set, falls back to the version + embedded in the program code. + Otherwise, fails with a 404 page with debug information +*/ +void handleGetEdit() +{ + if (handleFileRead(F("/edit.htm"))) + { + return; } + +#ifdef INCLUDE_FALLBACK_INDEX_HTM + server.sendHeader(F("Content-Encoding"), "gzip"); + server.send(200, "text/html", index_htm_gz, index_htm_gz_len); +#else + replyNotFound(FPSTR(FILE_NOT_FOUND)); +#endif } #endif From 311a379e1a1bdad08f26bd9797a92d42b99eb634 Mon Sep 17 00:00:00 2001 From: avaksru Date: Thu, 27 Apr 2023 11:46:16 +0300 Subject: [PATCH 6/7] =?UTF-8?q?=D0=90=D0=BD=D0=B0=D0=BB=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=B9=20=D1=81=D1=87=D0=B5=D1=82=D1=87=D0=B8=D0=BA?= =?UTF-8?q?=20=D0=B8=D0=BC=D0=BF=D1=83=D0=BB=D1=8C=D1=81=D0=BE=D0=B2.=20Mu?= =?UTF-8?q?ltitouch=20V2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/items.json | 116 ++++++++++++--------- data_svelte/widgets.json | 59 ++++++++++- myProfile.json | 4 + platformio.ini | 1 + src/modules/API.cpp | 2 + src/modules/exec/Multitouch/Multitouch.cpp | 55 +++++----- src/modules/exec/Multitouch/modinfo.json | 6 +- src/modules/sensors/Impulse/Impulse.cpp | 87 ++++++++++++++++ src/modules/sensors/Impulse/modinfo.json | 49 +++++++++ 9 files changed, 296 insertions(+), 83 deletions(-) create mode 100644 src/modules/sensors/Impulse/Impulse.cpp create mode 100644 src/modules/sensors/Impulse/modinfo.json diff --git a/data_svelte/items.json b/data_svelte/items.json index c5190460..9e0e2f57 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -382,7 +382,23 @@ }, { "global": 0, - "name": "26. PZEM 004t Напряжение", + "name": "26. Аналоговый счетчик импульсов", + "type": "Writing", + "subtype": "Impulse", + "id": "impulse", + "widget": "anydataDef", + "page": "Счетчики", + "descr": "Импульсов", + "needSave": 0, + "int": 1, + "pin": 16, + "pinMode": "INPUT", + "debounceDelay": 3, + "num": 26 + }, + { + "global": 0, + "name": "27. PZEM 004t Напряжение", "type": "Reading", "subtype": "Pzem004v", "id": "v", @@ -392,11 +408,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 26 + "num": 27 }, { "global": 0, - "name": "27. PZEM 004t Сила тока", + "name": "28. PZEM 004t Сила тока", "type": "Reading", "subtype": "Pzem004a", "id": "a", @@ -406,11 +422,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 27 + "num": 28 }, { "global": 0, - "name": "28. PZEM 004t Мощность", + "name": "29. PZEM 004t Мощность", "type": "Reading", "subtype": "Pzem004w", "id": "w", @@ -420,11 +436,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 28 + "num": 29 }, { "global": 0, - "name": "29. PZEM 004t Энергия", + "name": "30. PZEM 004t Энергия", "type": "Reading", "subtype": "Pzem004wh", "id": "wh", @@ -434,11 +450,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 29 + "num": 30 }, { "global": 0, - "name": "30. PZEM 004t Частота", + "name": "31. PZEM 004t Частота", "type": "Reading", "subtype": "Pzem004hz", "id": "hz", @@ -448,11 +464,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 30 + "num": 31 }, { "global": 0, - "name": "31. PZEM 004t Косинус", + "name": "32. PZEM 004t Косинус", "type": "Reading", "subtype": "Pzem004pf", "id": "pf", @@ -462,11 +478,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 31 + "num": 32 }, { "global": 0, - "name": "32. PZEM настройка", + "name": "33. PZEM настройка", "type": "Reading", "subtype": "Pzem004cmd", "id": "set", @@ -478,11 +494,11 @@ "changeaddr": 0, "setaddr": "0x01", "reset": 0, - "num": 32 + "num": 33 }, { "global": 0, - "name": "33. Часы реального времени", + "name": "34. Часы реального времени", "type": "Reading", "subtype": "RTC", "id": "rtc", @@ -498,11 +514,11 @@ "int": 5, "btn-setUTime": "0", "btn-setSysTime": "nil", - "num": 33 + "num": 34 }, { "global": 0, - "name": "34. Sht20 Температура", + "name": "35. Sht20 Температура", "type": "Reading", "subtype": "Sht20t", "id": "tmp2", @@ -511,11 +527,11 @@ "descr": "Температура", "int": 15, "round": 1, - "num": 34 + "num": 35 }, { "global": 0, - "name": "35. Sht20 Влажность", + "name": "36. Sht20 Влажность", "type": "Reading", "subtype": "Sht20h", "id": "Hum2", @@ -524,11 +540,11 @@ "descr": "Влажность", "int": 15, "round": 1, - "num": 35 + "num": 36 }, { "global": 0, - "name": "36. Sht30 Температура", + "name": "37. Sht30 Температура", "type": "Reading", "subtype": "Sht30t", "id": "tmp30", @@ -537,11 +553,11 @@ "descr": "SHT30 Температура", "int": 15, "round": 1, - "num": 36 + "num": 37 }, { "global": 0, - "name": "37. Sht30 Влажность", + "name": "38. Sht30 Влажность", "type": "Reading", "subtype": "Sht30h", "id": "Hum30", @@ -550,12 +566,12 @@ "descr": "SHT30 Влажность", "int": 15, "round": 1, - "num": 37 + "num": 38 }, { "global": 0, - "name": "38. HC-SR04 Ультразвуковой дальномер", - "num": 38, + "name": "39. HC-SR04 Ультразвуковой дальномер", + "num": 39, "type": "Reading", "subtype": "Sonar", "id": "sonar", @@ -567,7 +583,7 @@ "int": 5 }, { - "name": "39. UART", + "name": "40. UART", "type": "Reading", "subtype": "UART", "page": "", @@ -579,14 +595,14 @@ "line": 2, "speed": 9600, "eventFormat": 0, - "num": 39 + "num": 40 }, { "header": "Исполнительные устройства" }, { "global": 0, - "name": "40. Кнопка подключенная к пину", + "name": "41. Кнопка подключенная к пину", "type": "Writing", "subtype": "ButtonIn", "id": "btn", @@ -601,11 +617,11 @@ "debounceDelay": 50, "fixState": 0, "inv": 0, - "num": 40 + "num": 41 }, { "global": 0, - "name": "41. Управление пином", + "name": "42. Управление пином", "type": "Writing", "subtype": "ButtonOut", "needSave": 0, @@ -616,11 +632,11 @@ "int": 0, "inv": 0, "pin": 2, - "num": 41 + "num": 42 }, { "global": 0, - "name": "42. Сервопривод", + "name": "43. Сервопривод", "type": "Writing", "subtype": "IoTServo", "id": "servo", @@ -631,11 +647,11 @@ "pin": 12, "apin": -1, "amap": "0, 4096, 0, 180", - "num": 42 + "num": 43 }, { "global": 0, - "name": "43. Расширитель портов Mcp23017", + "name": "44. Расширитель портов Mcp23017", "type": "Reading", "subtype": "Mcp23017", "id": "Mcp", @@ -645,11 +661,11 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 43 + "num": 44 }, { "global": 0, - "name": "44. MP3 плеер", + "name": "45. MP3 плеер", "type": "Reading", "subtype": "Mp3", "id": "mp3", @@ -659,11 +675,11 @@ "int": 1, "pins": "14,12", "volume": 20, - "num": 44 + "num": 45 }, { "global": 0, - "name": "45. Сенсорная кнопка", + "name": "46. Сенсорная кнопка", "type": "Writing", "subtype": "Multitouch", "id": "impulse", @@ -677,11 +693,11 @@ "pinMode": "INPUT", "debounceDelay": 50, "PWMDelay": 500, - "num": 45 + "num": 46 }, { "global": 0, - "name": "46. Расширитель портов Pcf8574", + "name": "47. Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", "id": "Pcf", @@ -691,11 +707,11 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 46 + "num": 47 }, { "global": 0, - "name": "47. PWM ESP8266", + "name": "48. PWM ESP8266", "type": "Writing", "subtype": "Pwm8266", "id": "pwm", @@ -707,11 +723,11 @@ "freq": 5000, "val": 0, "apin": -1, - "num": 47 + "num": 48 }, { "global": 0, - "name": "48. Телеграм-Лайт", + "name": "49. Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", "id": "tg", @@ -720,14 +736,14 @@ "descr": "", "token": "", "chatID": "", - "num": 48 + "num": 49 }, { "header": "Экраны" }, { "global": 0, - "name": "49. LCD экран 2004", + "name": "50. LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -739,10 +755,10 @@ "size": "20,4", "coord": "0,0", "id2show": "id датчика", - "num": 49 + "num": 50 }, { - "name": "50. LCD экран 1602", + "name": "51. LCD экран 1602", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -754,6 +770,6 @@ "size": "16,2", "coord": "0,0", "id2show": "id датчика", - "num": 50 + "num": 51 } ] \ No newline at end of file diff --git a/data_svelte/widgets.json b/data_svelte/widgets.json index 4c681bff..8522af99 100644 --- a/data_svelte/widgets.json +++ b/data_svelte/widgets.json @@ -41,7 +41,25 @@ "label": "Ватты", "widget": "anydata", "after": "Wt", - "icon": "speedometer" + "icon": "speedometer", + "color": [ + { + "level": 0, + "value": "" + }, + { + "level": 200, + "value": "#009933" + }, + { + "level": 2000, + "value": "#FF9900" + }, + { + "level": 4000, + "value": "red" + } + ] }, { "name": "anydataWth", @@ -62,7 +80,42 @@ "label": "Температура", "widget": "anydata", "after": "°С", - "icon": "thermometer" + "icon": "thermometer", + "font": "OCR A Std", + "color": [ + { + "level": -20, + "value": "#0000CC" + }, + { + "level": -10, + "value": "#0000CC" + }, + { + "level": 0, + "value": "#0000CC" + }, + { + "level": 12, + "value": "#3366FF" + }, + { + "level": 16, + "value": "#33CCFF" + }, + { + "level": 18, + "value": "#009933" + }, + { + "level": 30, + "value": "#FF9900" + }, + { + "level": 40, + "value": "red" + } + ] }, { "name": "anydataMm", @@ -225,7 +278,7 @@ }, { "name": "anydatamWt", - "label": "миллиВатты", + "label": "миллиВатты", "widget": "anydata", "after": "mWt", "icon": "speedometer" diff --git a/myProfile.json b/myProfile.json index 8bfe873d..17ee601d 100644 --- a/myProfile.json +++ b/myProfile.json @@ -136,6 +136,10 @@ "path": "src/modules/sensors/Hx711", "active": false }, + { + "path": "src/modules/sensors/Impulse", + "active": true + }, { "path": "src/modules/sensors/Ina219", "active": false diff --git a/platformio.ini b/platformio.ini index 7ba76a98..fcc0ec51 100644 --- a/platformio.ini +++ b/platformio.ini @@ -332,6 +332,7 @@ build_src_filter = + + + + + + + + diff --git a/src/modules/API.cpp b/src/modules/API.cpp index 4de43be5..12480f10 100644 --- a/src/modules/API.cpp +++ b/src/modules/API.cpp @@ -14,6 +14,7 @@ void* getAPI_Bme280(String subtype, String params); void* getAPI_Bmp280(String subtype, String params); void* getAPI_Dht1122(String subtype, String params); void* getAPI_Ds18b20(String subtype, String params); +void* getAPI_Impulse(String subtype, String params); void* getAPI_Pzem004(String subtype, String params); void* getAPI_RTC(String subtype, String params); void* getAPI_Sht20(String subtype, String params); @@ -47,6 +48,7 @@ if ((tmpAPI = getAPI_Bme280(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Bmp280(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Dht1122(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Ds18b20(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Impulse(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Pzem004(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_RTC(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Sht20(subtype, params)) != nullptr) return tmpAPI; diff --git a/src/modules/exec/Multitouch/Multitouch.cpp b/src/modules/exec/Multitouch/Multitouch.cpp index 9e6b30ba..383d5be4 100644 --- a/src/modules/exec/Multitouch/Multitouch.cpp +++ b/src/modules/exec/Multitouch/Multitouch.cpp @@ -6,23 +6,22 @@ extern IoTGpio IoTgpio; class Multitouch : public IoTItem { private: - int _pin; int _int; - int _inv; - String _pinMode; - int _lastButtonState = LOW; - unsigned long _lastDebounceTime = 0; unsigned long timing; - long _debounceDelay = 50; long _PWMDelay = 500; - int _buttonState; - int _reading; int _count = 0; int duration = 0; + int _pin; + bool _execLevel, _fixState, _inv, _buttonState, _reading; + bool _lastButtonState = LOW; + unsigned long _lastDebounceTime = 0; + int _debounceDelay = 50; + public: Multitouch(String parameters) : IoTItem(parameters) { + String _pinMode; jsonRead(parameters, "pin", _pin); jsonRead(parameters, "pinMode", _pinMode); jsonRead(parameters, "debounceDelay", _debounceDelay); @@ -31,15 +30,20 @@ public: jsonRead(parameters, "inv", _inv); _round = 0; - IoTgpio.pinMode(_pin, INPUT); - if (_pinMode == "INPUT_PULLUP") - IoTgpio.digitalWrite(_pin, HIGH); - else if (_pinMode == "INPUT_PULLDOWN") + if (_pinMode == F("INPUT")) + IoTgpio.pinMode(_pin, INPUT); + else if (_pinMode == F("INPUT_PULLUP")) + IoTgpio.pinMode(_pin, INPUT_PULLUP); + else if (_pinMode == F("INPUT_PULLDOWN")) + { + IoTgpio.pinMode(_pin, INPUT); IoTgpio.digitalWrite(_pin, LOW); + } value.valD = _buttonState = IoTgpio.digitalRead(_pin); // сообщаем всем о стартовом статусе без генерации события regEvent(_buttonState, "", false, false); + SerialPrint("I", F("Multitouch"), "_buttonState " + String(_buttonState)); } void loop() @@ -49,35 +53,34 @@ public: { _lastDebounceTime = millis(); } - if ((millis() - _lastDebounceTime) > _debounceDelay) { - if (millis() - timing > _int && _reading == _inv && millis() - _lastDebounceTime > _PWMDelay) - { - timing = millis(); - duration = millis() - _lastDebounceTime - _PWMDelay; - value.valD = duration / 50; - regEvent(value.valD, "Multitouch"); - _count = -1; - } - if (_reading != _buttonState) { - _buttonState = _reading; _count++; duration = 0; + _buttonState = _reading; } - if (1 < _count && millis() > _lastDebounceTime + _PWMDelay) { value.valD = _count / 2; - regEvent(value.valD, "Multitouch"); + regEvent(value.valD, F("Multitouch")); _count = 0; } + + if (millis() - timing > _int && _reading == _inv && millis() - _lastDebounceTime > _PWMDelay) + { + SerialPrint("I", F("Multitouch"), "Считаем задержку"); + timing = millis(); + duration = millis() - _lastDebounceTime - _PWMDelay; + value.valD = duration / _int + 1; + regEvent(value.valD, F("Multitouch")); + _count = -1; + } } + _lastButtonState = _reading; } - ~Multitouch(){}; }; diff --git a/src/modules/exec/Multitouch/modinfo.json b/src/modules/exec/Multitouch/modinfo.json index b83a1f9e..d596113e 100644 --- a/src/modules/exec/Multitouch/modinfo.json +++ b/src/modules/exec/Multitouch/modinfo.json @@ -25,7 +25,7 @@ "authorGit": "https://github.com/avaksru", "specialThanks": "", "moduleName": "Multitouch", - "moduleVersion": "1.0", + "moduleVersion": "2.0", "usedRam": { "esp32_4mb": 15, "esp8266_4mb": 15 @@ -48,8 +48,6 @@ "esp8266_1mb": [], "esp8266_1mb_ota": [], "esp8285_1mb": [], - "esp8285_1mb_ota": [], - "esp8266_2mb": [], - "esp8266_2mb_ota": [] + "esp8285_1mb_ota": [] } } diff --git a/src/modules/sensors/Impulse/Impulse.cpp b/src/modules/sensors/Impulse/Impulse.cpp new file mode 100644 index 00000000..e84a1123 --- /dev/null +++ b/src/modules/sensors/Impulse/Impulse.cpp @@ -0,0 +1,87 @@ +#include "Global.h" +#include "classes/IoTItem.h" + +extern IoTGpio IoTgpio; + +class Impulse : public IoTItem +{ +private: + int _int; + int _pin; + bool _buttonState, _reading; + bool _lastButtonState = LOW; + unsigned long _lastDebounceTime = 0; + int _debounceDelay = 50; + int _count = 0; + unsigned long timing; + +public: + Impulse(String parameters) : IoTItem(parameters) + { + String _pinMode; + jsonRead(parameters, F("pin"), _pin); + jsonRead(parameters, F("pinMode"), _pinMode); + jsonRead(parameters, F("debounceDelay"), _debounceDelay); + jsonRead(parameters, "int", _int); + _round = 0; + + if (_pinMode == F("INPUT")) + IoTgpio.pinMode(_pin, INPUT); + else if (_pinMode == F("INPUT_PULLUP")) + IoTgpio.pinMode(_pin, INPUT_PULLUP); + else if (_pinMode == F("INPUT_PULLDOWN")) + { + IoTgpio.pinMode(_pin, INPUT); + IoTgpio.digitalWrite(_pin, LOW); + } + + value.valD = _buttonState = IoTgpio.digitalRead(_pin); + regEvent(_buttonState, "", false, false); + } + + void loop() + { + _reading = IoTgpio.digitalRead(_pin); + if (_reading != _lastButtonState) + { + + _lastDebounceTime = millis(); + } + + if ((millis() - _lastDebounceTime) > _debounceDelay) + { + if (_reading != _buttonState) + { + _buttonState = _reading; + _count++; + } + if (_count == 1) + { + timing = millis(); + } + if (millis() - timing > _int * 1000 && _count > 1) + { + timing = millis(); + value.valD = _count; + regEvent(value.valD, F("Impulse")); + _count = 0; + } + } + + _lastButtonState = _reading; + } + + ~Impulse(){}; +}; + +void *getAPI_Impulse(String subtype, String param) +{ + if (subtype == F("Impulse")) + { + return new Impulse(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/sensors/Impulse/modinfo.json b/src/modules/sensors/Impulse/modinfo.json new file mode 100644 index 00000000..18d6ab6a --- /dev/null +++ b/src/modules/sensors/Impulse/modinfo.json @@ -0,0 +1,49 @@ +{ + "menuSection": "Сенсоры", + "configItem": [ + { + "global": 0, + "name": "Аналоговый счетчик импульсов", + "type": "Writing", + "subtype": "Impulse", + "id": "impulse", + "widget": "anydataDef", + "page": "Счетчики", + "descr": "Импульсов", + "needSave": 0, + "int": 1, + "pin": 16, + "pinMode": "INPUT", + "debounceDelay": 3 + } + ], + "about": { + "authorName": "AVAKS", + "authorContact": "https://t.me/@avaks_dev", + "authorGit": "https://github.com/avaksru", + "specialThanks": "", + "moduleName": "Impulse", + "moduleVersion": "2.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Аналоговый счетчик импульсов. Датчик Холла ", + "moduleDesc": "Считает количество импульсов за период времени", + "propInfo": { + "int": "Период сбора импульсов в секундах", + "pin": "Укажите GPIO номер пина для чтения импульсов", + "pinMode": "Может быть INPUT_PULLUP INPUT_PULLDOWN INPUT", + "debounceDelay": "Время обработки дребезга (миллисекунд)" + } + }, + "defActive": true, + "usedLibs": { + "esp32_4mb": [], + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [], + "esp8285_1mb": [], + "esp8285_1mb_ota": [] + } +} \ No newline at end of file From 0b6d7ac11212ed1fa53a73943720f5c83ff7803d Mon Sep 17 00:00:00 2001 From: avaksru Date: Thu, 27 Apr 2023 12:01:50 +0300 Subject: [PATCH 7/7] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20"multiply"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/Impulse/modinfo.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/sensors/Impulse/modinfo.json b/src/modules/sensors/Impulse/modinfo.json index 18d6ab6a..e35ecf29 100644 --- a/src/modules/sensors/Impulse/modinfo.json +++ b/src/modules/sensors/Impulse/modinfo.json @@ -14,7 +14,8 @@ "int": 1, "pin": 16, "pinMode": "INPUT", - "debounceDelay": 3 + "debounceDelay": 3, + "multiply": 1 } ], "about": {