Контроль влажности подвала ver.2.0 (stm32 + сеть)

pav2000
Offline
Зарегистрирован: 15.12.2014

Осушитель подвала ver.2.0 stm32, доступ через сеть

Потребовалось мне еще один осушитель, для предполья бани. Решил сделать версию с удаленным управлением.

Вся информация размещена на гитхабе https://github.com/pav2000/Dehumidifier-2.0

Короткий ролик поясняющий работу и конструкцию. https://youtu.be/ywdXSmak6OI

Данный проект - дальнейшее развитие проекта:  http://arduino.ru/forum/proekty/kontrol-vlazhnosti-podvala-arduino-pro-mini

Принцип работы остался прежним - Идея контроля влажности подвала была подсмотрена на http://geektimes.ru (http://geektimes.ru/post/255298/) Вся идея состоит в том чтобы измерить температуру и относительную влажность в подвале и на улице, на основании температуры и относительной влажности рассчитать абсолютную влажность и принять решение о включении вытяжного вентилятора в подвале. Теория для расчета изложена здесь - https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/

Аппаратная часть полностью переделана. Теперь используется STM32 и сетевой чип wiznet w5500. На самом чипе поднят простейший веб сервер, через который проводится настройка устройства и его контроль. Разрабатывая этот блок я планировал сделать универсальный модуль для домашней автоматизации, и заложил избыточность в схеме для реализации будущих устройств. Сам блок реализован на плате maple mini (STM32F103CBT6). 

Для упрощения программирования используется free RTOS 8 немного заточенная под себя

Схема платы:

На схеме (и плате) разведены следующие узлы:

  • кварц для часов реального времени stm32
  • батарейка для часов
  • два порта i2c для подключения внешних устройств
  • флеш память spi (можно установить до 32 мбит)
  • разъем sd карты (режим spi)
  • дисплей 2.8 дюйма на контроллере ili9341 подключение spi
  • модуль питания 220->5в
  • два ssr реле со схемами гашения помех и варисторами
  • датчик переменного тока (0-50A) для измерения токов нагрузки 220
  • разъем для подключения отладчика
  • разъем для подключения nrf24f01 - для работы с беспроводными датчиками
  • два светодида на отдельных gpio
  • мост i2c oneWire со схемой защиты для подключения OneWire датчиков
  • пищалка
  • разъем расширения (uart+gpio)

Под этот проект была разработана и изготовлена печатная плата. Плата предназначена для установки в корпус G212C (https://www.chipdip.ru/product/g212c)

Сборка.

Косяки разводки:

  • шелкография - на разъеме отладчика SWD перепутаны надписи DIO CLK
  • шелкография - на разъеме 220 и реле перепутаны надписи ssr1 ssr2
  • разводка - надо бросить сигнальный провод (почему то не развелся) от датчика тока до maple mini
  • дополнительно было распаяно (частично учтено на схеме) RC фильтр на датчик тока, конденсатор на ножку питания, резистор 6.8к между землей и DO usb (без него и без подключения к usb МК переходил в режим загрузчика)

Работа.

Первоначально планировалось использовать датчики температуры и влажности AHT10 на шине i2c. Два датчика отказывались работfь на одной шине (хотя были разнесены по разным адресам), т.е. работают корректно 20-30 минут а потом отваливаются от шины и шину клинит, было лень разбираться. Один датчик работает хорошо. При переходе на программный i2c (ногодрыг) оба датчика нормально заработали.
НО при увеличении длины провода более 1.5 метра на программном i2c датчики отказались работать (в принципе это ожидаемо). По этому принято решение использовать проверенные временем DHT22 (шина типа 1-wire). Разъем i2c имеет 4 контакта (питание SDA SCL) и был перепрограммирован для подключения двух датчиков DHT22. Длина проводов: внутренний датчик 3 метра, внешний 8 метров, все работает с разумным количеством ошибок.
Датчик тока ACS758 (диапазон 50A - применен для универсальности) позволяет измерять ток потребления устройством (есть задумка в его использовании в других устройствах). При этом при работающем вентиляторе вытяжки можно измерить общий ток и сделать вывод об работоспособности вентилятора. Обычно вентиляторе есть термо предохранитель, который срабатывает при их отказе. Так можно следить за работой вентилятора через инет. Единственная проблема, что устройство потребляет достаточно мало и много шумов, но эту проблему удалось побороть усреднением и дополнительной обработкой данных.

Web морда устройства:
Графики используют google chart api, по этому без инета они работать не будут.

Установка на объекте.

Исходники почти 150к по просьбе модератора напрягся и вставил сюда -)):

Podval20.ino

//  SPI - 1 использует сетевая карта w5500 (библиотека настроена на использование ТОЛЬКО SPI1), подключение
//  D4   PA7   1_MOSI  
//  D5   PA6   1_MISO  
//  D6   PA5   1_SCK   
//  D7   PA4   1_NSS  CSN_PIN
//  D3   PB0   1_RST  // Сброс W5500

//  SPI - 2 использует дисплей (библиотеке надо указать какой SPI исрользовать), подключение
//  D28   PB15  2_MOSI  Yes+
//  D29   PB14  2_MISO  Yes+
//  D30   PB13  2_SCK   Yes+
//  D19   PB12  2_NSS   Yes+    ИЛИ  PB11 
//  D31   PB11  2_DC    TFT_CS  или PB4   
//  D27   PB10  2_RST   TFT_RST или PA8

#include <MapleFreeRTOS821.h>
#include <SPI.h>
#include <EEPROM.h>
#include "Adafruit_GFX_AS.h"
#include "Adafruit_ILI9341_STM.h" //
#include <Ethernet_STM.h>  // Сеть прицецеплена на 1 spi
#include <Wire.h>
#include "utility/w5100.h"
#include <libmaple/iwdg.h>
#include <RTClock.h>

#define DEBUG          // Выводить отладочную информацию в консоль
//#define DEMO           // Режим демо 
#define VERSION    44  // Версия программы
//#define USE_HEAT     // Использовать нагреватель (калорифер) используется SSR2

#include "Podval20.h"  
#include "webserver.h" 
#include "DHT.h"

// Флаги настроек
#define fTFT_180          0                     // Флаг поворота дисплея на 180 градусов
#define fTFT_OFF          1                     // Флаг выключения дисплея 1 выключен
#define fDHCP             2                     // Флаг использования DHCP
#define fNTP              3                     // Флаг обновления времени по NTP
#define fUPDATE           4                     // Флаг обновления страницы каждые 60 сек
#define fBEEP             5                     // Флаг разрешения пищалки
#define fTEST             6                     // Флаг ежедневного тестирования и определениея "0" датчика тока
#define fFULL_WEB         7                     // Флаг показа настроек
#define fAUTO             8                     // Флаг автокалибровки датчика ACS758 
static struct type_setting_eeprom               // Структура для сохранения данных в eeprom
 {
     char name[24]="Dehumidifier pav2000";      // Название прибора  
     TYPE_MODE mode = BLOCK_OFF;                // Какой набор настроек используется
     uint16_t dH_min,T_min;                     // Расшифровка настроек срабатывания см mode  (в сотых грамма на м*3)(в сотых градуса)
     uint16_t flag=0;                           // флаги (бинарные настройки)
     IPAddress ip;                              // ИП адрес
     IPAddress dns;                             // DNS
     IPAddress gateway;                         // Шлюз
     IPAddress mask;                            // Маска подсети
     unsigned long hour_unit;                   // мото часы блок измеряется в интервалах вывода данных = NUM_SAMPLES*TIME_SCAN_SENSOR
     unsigned long hour_motor;                  // мото часы мотора измеряется в интервалах вывода данных = NUM_SAMPLES*TIME_SCAN_SENSOR
     unsigned long hour_heat;                   // мото часы нагревателя измеряется в интервалах вывода данных = NUM_SAMPLES*TIME_SCAN_SENSOR
     uint16_t CurMin=100;                       // минимальный ток потребления вентилятора, если меньше то проблема
     uint16_t CurMax=2000;                      // максимальный ток потребления вентилятора, если больше то проблема
     uint16_t constACS758=2519;                 // Смещене 0 датчика заданное через веб морду
     int16_t eTIN=0;                            // ошибка внутреннего датчика температуры (сотые градуса) ОШИБКА ДОБАВЛЯЕТСЯ!
     int16_t eTOUT=0;                           // ошибка внешнего датчика температуры (сотые градуса) ОШИБКА ДОБАВЛЯЕТСЯ!
     int16_t eHIN=0;                            // ошибка внутреннего датчика влажности (сотые %) ОШИБКА ДОБАВЛЯЕТСЯ!
     int16_t eHOUT=0;                           // ошибка внешнего датчика влажности (сотые %) ОШИБКА ДОБАВЛЯЕТСЯ!
     uint32_t reserve1;                         // Резерв 
     uint32_t reserve2; 
     uint32_t reserve3; 
  } setting;                                    // Рабочая копия настроек в памяти

static struct type_sensors                          // структура датчиков
{
       uint8_t  flags=0x00;                         // байт флагов состояния  0 бит - мотор включен/выключен 1 бит - калорифер включен/выключен 2 бит ошибка вентилятора
       // Ошибки
       uint8_t  inErr,outErr;                       // Последние ошибки чтения датчиков
       uint32_t  numErrIn=0,numErrOut=0;            // Число ошибок чтения датчика
       // Усреднение, с  чем работаем (реализовано скользящее среднее)
       int16_t  av_tOut=-5000,av_tIn=-5000;         // Текущие температуры в сотых градуса ЕСТЬ УСРЕДНЕНИЕ
       int16_t  av_relHOut=5555,av_relHIn=5555;     // Относительные влажности сотых процента ЕСТЬ УСРЕДНЕНИЕ
       int16_t  av_absHOut=5555,av_absHIn=5555;     // Абсолютные влажности в сотых грамма на м*3 ЕСТЬ УСРЕДНЕНИЕ
       // Данные  для усреднения и массив накопленных данных для реализации скользящего среднего
       int16_t  index=0;                            // Текущий индекс в массиве усреднений
       boolean  first=true;                         // признак первого прохода (нет полного массива данных)
       int16_t  tOut=-5000,tIn=-5000;               // Текущие температуры в сотых градуса НЕТ УСРЕДНЕНИЯ    // С датчиков (не обработано)
       int16_t  relHOut=5000,relHIn=5000;           // Относительные влажности сотых процента НЕТ УСРЕДНЕНИЯ  // С датчиков (не обработано)
       int16_t  dat_tOut[NUM_SAMPLES],dat_tIn[NUM_SAMPLES]; // Массивы для усреднения температур
       int16_t  dat_relHOut[NUM_SAMPLES],dat_relHIn[NUM_SAMPLES]; // Массивы для усреднения относительных влажностей
       int32_t  sum_tOut=0,sum_tIn=0;               // Сумма для усреднения Текущие температуры в сотых градуса !!! место экономим
       int32_t  sum_relHOut=0,sum_relHIn=0;         // Сумма для усреднения Относительные влажности сотых процента !!! место экономим
       // Датчик тока
        uint16_t CurrentACS758=0;                   // Текущий ток потребляения mA
        uint16_t offsetACS758=0;                    // ТЕКУЩЕЕ Смещене 0 датчика ДЕСЯТЫЕ мВ!! (в зависимости от настроек)
        uint16_t autoACS758=0;                      // Смещене 0 датчика по автокалибровке ДЕСЯТЫЕ мВ!!
} sensors;

struct type_gchart // Структура для вывода графиков 
{
 int16_t dataIn[CHART_POINT];    // Массив данных подвал температура хранится в сотых градуса, влажность в сотых грамма (дополнительно 14 бит данных влажности используется для хрананеия признака работы вентилятора)
 int16_t dataOut[CHART_POINT];   // Массив данных улица
} chartTemp,chartAbsH;           // Графики температуры и влажности
    
static uint8_t  posChart=0;             // Позиция в массиве графиков - начало вывода от 0 до 120-1
static bool     ChartMotor=false;       // Признак работы мотора во время интервала графика если мотор был включен на любое время то на графике фон меняется в этом месте
static uint16_t last_error=0;           // Предыдущая ошибка чтения датчиков

// Handle задач
TaskHandle_t hReadSensor;
TaskHandle_t hUpdateTFT;
TaskHandle_t hWebTask;
SemaphoreHandle_t xTFTSemaphore;             // Семафор для дисплея

// RTC NTP ----------------------------
#define TZ  3*60*60                     // Часовой пояс в секундах !!!
RTClock rt(RTCSEL_LSE);                 // используется внешний часовой кварц
tm_t tm;                                // хранение текущего времени
tm_t rTime;                             // время последнего сброса
EthernetUDP Udp;                        // Создаем экземпляр класса EthernetUDP, который позволит нам отправлять и получать пакеты через UDP:
unsigned int localPort = 8888;          // локальный порт, который будет прослушиваться на предмет UDP-пакетов
#define _rcc_csr *(uint32_t*)0x40021024 // Адрес Регистра управления/статуса RCC_CSR
uint32_t reg_RCC_CSR;                   // Значение регистр сброса при старте


Adafruit_ILI9341_STM tft = Adafruit_ILI9341_STM(pinTFT_CS, pinTFT_DC,pinTFT_RST);  // дисплей
static boolean fullTftUpdate=false;      // Признак необходимости полного обновления дисплея
SimpleDHT22 inDHT(PIN_IN_DHT);
SimpleDHT22 outDHT(PIN_OUT_DHT);

EthernetServer server(80);
//char inBufGet[2048];                         // входной буффер ответа GET (web server)
//uint32_t connectTime[MAX_SOCK_NUM];          // времена для чистки сокетов
IPAddress realIP;                              // текущий IP адрес (прочитали из сетевого чипа)
uint8_t mac[] = { 0x90, 0xA2, 0xDA, 0x00, 0x51, 0x00 };
  
// Для работы необходимо добавить строчку #define INCLUDE_xTaskGetSchedulerState    1 в файл FreeRTOSConfig.h
inline void _delay(uint t) // Функция задержки (мсек) в зависимости от шедулера задач FreeRtos
{ if(xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) vTaskDelay(t/portTICK_PERIOD_MS); else delay(t);}
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// ПРОГРАММА
///////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup(){
  uint8_t ip[]  = { 192, 168, 1, 175};
  uint8_t i;
  float temperature = 0;
  float humidity = 0;

#ifdef  DEBUG  
   Serial.begin(115200); 
   Serial.println("DEBUG MODE"); 
#endif
   
   // Причина сброса
   reg_RCC_CSR=(uint32_t)_rcc_csr;    // Получить и запомнить причину последнего сброса
   bitSet(_rcc_csr,RCC_CSR_RMVF_BIT);  // Сброс состояния (Очистка флагов сброса)

   // Запретить неиспользуемые устройства на шине SPI1
   pinMode(PIN_CS_W25Q8, OUTPUT);      //  Настройка ноги для CS чипа памяти W25Q8ODVSSIG
   digitalWrite(PIN_CS_W25Q8, HIGH);   //  запретить 
   pinMode(PIN_CS_SDCARD, OUTPUT);     //  Настройка ноги для CS карты памяти
   digitalWrite(PIN_CS_SDCARD, HIGH);  //  запретить 
   // Сброс SPI устройств
   reset_ili9341();                    // Сброс дисплей через ножку
   reset_w5500();                      // Сброс W5500 через ножку
   // Настройка периферии
   pinMode(PIN_RELAY1, OUTPUT);        //  Настройка ноги для вентилятора (SSR1)
   digitalWrite(PIN_RELAY1, LOW);      //  выкл SSR1
   pinMode(PIN_RELAY2, OUTPUT);        //  Настройка ноги для калорифера (SSR2)
   digitalWrite(PIN_RELAY2, LOW);      //  выкл SSR2
   pinMode(PIN_KEY, INPUT);            //  Настройка ноги кнопки ВХОД
   pinMode(PIN_ACS758 , INPUT_ANALOG); //  Датчик тока
   pinMode(PIN_BEEP, OUTPUT);          //  Настройка ноги для динамика
   digitalWrite(PIN_BEEP, LOW); 
   pinMode(PIN_LED1, OUTPUT);          //  Настройка ноги для LED1
   pinMode(PIN_LED2, OUTPUT);          //  Настройка ноги для LED2
   digitalWrite(PIN_LED1, LOW);   
   digitalWrite(PIN_LED2, LOW);   
//   _delay(200);
   sensors.autoACS758=CalibrACS758(); // Автокалибровка датчика тока, используется как пауза для включения светодиодов (делаем всегда)
   digitalWrite(PIN_LED1, HIGH);   
   digitalWrite(PIN_LED2, HIGH);   
  
   // Подготовка переменных и графиков
   reset_sum();
   for (i=0;i<CHART_POINT;i++){chartTemp.dataOut[i]=-3000;chartTemp.dataIn[i]=-3000;chartAbsH.dataOut[i]=0;chartAbsH.dataIn[i]=0;}// Обнуление графиков
   // Установки по умолчанию
   setting.mode = BLOCK_OFF;    
   setting.flag = 0x00;
   SETBIT1(setting.flag,fBEEP); // Разрешить звук по умолчанию
   SETBIT1(setting.flag,fDHCP); // Разрешить DHCP умолчанию (на экране можно посмотреть полученный адрес)
   SETBIT1(setting.flag,fAUTO); // Автоматическое определение 0 датчика тока
   setting.hour_unit = 0;
   setting.hour_motor = 0;
   setting.hour_heat = 0;
   setting.CurMin=400;
   setting.CurMax=1000;
   parseIPAddress("192.168.1.176",'.',setting.ip);
   parseIPAddress("192.168.1.1",'.',setting.dns);
   parseIPAddress("192.168.1.1",'.',setting.gateway);
   parseIPAddress("255.255.255.0",'.',setting.mask);
   sensors.CurrentACS758=0;
   sensors.flags=0x00;  
   FLAG_FAN_OFF;
   #ifdef USE_HEAT
      FLAG_HEAT_OFF;
   #endif
   FLAG_TEST_OK; // По умолчанию мотор исправен
   FLAG_NTP_ERR; // По умолчанию время не обновлено

    #ifdef  DEBUG  
      Serial.print(setting.name);Serial.println("  start . . ."); 
    #endif    
   // Попытка прочитать сохраненные настройки
   if(initEEPROM()!=0) {formatEEPROM(); saveEEPROM();} // Запись настроек по умолчанию
   else loadEEPROM();                                  // восстановление настроек 
   if(GETBIT(setting.flag,fAUTO))sensors.offsetACS758=sensors.autoACS758;else sensors.offsetACS758=setting.constACS758*10; //Установить смещение в зависимости от настроек
   
  //  SETBIT1(setting.flag,fDHCP);    
  
   init_w5500();  // Инициализация сети перед дисплеем что бы показать текущий IP
  
   // разбираемся с часами и обновляем время по сети если возможно     
   rtc_set_prescaler_load(0x7fff);  // установка делителя для часов  
       Udp.begin(localPort);
       getNtpTime();    
       #ifdef DEBUG
       rt.breakTime(rt.now(),tm);
       Serial.print("RTC timestamp:");Serial.print(tm.year+1970);Serial.print("/");Serial.print(tm.month);Serial.print("/");Serial.print(tm.day);Serial.print(" ");Serial.print(tm.hour);Serial.print(":");Serial.print(tm.minute);Serial.print(":");Serial.println(tm.second);
       #endif

   rt.breakTime(rt.now(),rTime);  // Запомнить время последнего сброса для вывода на вебморде
   #ifdef  DEBUG  
      Serial.print("Register RCC_CSR:"); Serial.println(uint32ToHex(reg_RCC_CSR));  
   #endif 
   
     // Тестирование датчиков и если удачно то полученными значенми заполнить массивы графиков
     if ((sensors.inErr = inDHT.read2(&temperature, &humidity, NULL)) != SimpleDHTErrSuccess){// Внутренний датчик
         #ifdef DEBUG
         Serial.print("Read inDHT failed, err="); Serial.println(uint8ToHex(sensors.inErr)); 
         #endif
         }
         else {
          #ifdef DEBUG
          Serial.println("Read inDHT Ok");
          #endif
          sensors.av_tIn    = temperature*100;
          sensors.av_relHIn = humidity*100;
          sensors.av_absHIn = calculationAbsH(temperature, humidity)*100; 
          for (i=0;i<CHART_POINT;i++){chartTemp.dataIn[i]=sensors.av_tIn; chartAbsH.dataIn[i]=sensors.av_absHIn;}
          }
    if ((sensors.outErr = outDHT.read2(&temperature, &humidity, NULL)) != SimpleDHTErrSuccess) { // Наружний датчик
         #ifdef DEBUG
         Serial.print("Read outDHT failed, err="); Serial.println(uint8ToHex(sensors.outErr)); 
           #endif
         }
         else {
          #ifdef DEBUG
          Serial.println("Read outDHT Ok");
          #endif
          sensors.av_tOut    = temperature*100; 
          sensors.av_relHOut = humidity*100;
          sensors.av_absHOut = calculationAbsH(temperature, humidity)*100; 
          for (i=0;i<CHART_POINT;i++){chartTemp.dataOut[i]=sensors.av_tOut; chartAbsH.dataOut[i]=sensors.av_absHOut;}
          }
     
   // Вывод на дисплей
   SPI.setModule(2);
   SPI.setClockDivider(SPI_CLOCK_DIV2);
   tft.begin();
   fullTftUpdate=true;  // Нужна полная прорисовка дисплея при старте ОС
   
   // Создание задач  
   xTaskCreate(vReadSensor, "ReadSensor",150,NULL,4,&hReadSensor); // было 100 пробуем 150
   xTaskCreate(vUpdateTFT,  "UpdateTFT",150,NULL,2,&hUpdateTFT);   // стек минимум 150  пробуем 200
   xTaskCreate(vWebTask,    "Web" ,350,NULL,1,&hWebTask);          // стек минимум 200?  250 падает  последний вариант 350
   vSemaphoreCreateBinary(xTFTSemaphore); // Создание семафора
     
   if (xTFTSemaphore==NULL){
    #ifdef DEBUG
    Serial.println("Error create semaphore (low memory?)");
    #endif 
    exit;
   }

   iwdg_init(IWDG_PRE_256, 2*1250);      // Сторожевой таймер 2*8 секунд. 12 бит Максимальное значение 4096 !!!
   setup_vdd_tempr_sensor();             // Подготовить 16 17 каналы ацп
   #ifdef DEBUG
    Serial.println(" . . . Start free RTOS . . .");
   #endif 
   vTaskStartScheduler();
   #ifdef DEBUG
   Serial.println("vTaskStartScheduler() NOT RUNNING!!!"); 
   #endif 
}

void loop()
{
}
// Расчет загрузки контроллера 
static const uint8 periodLen = 9; // 2^periodLen ticks - 512 x 1ms ticks 
volatile TickType_t curIdleTicks = 0;
volatile TickType_t lastCountedTick = 0;
volatile TickType_t lastCountedPeriod = 0;
static TickType_t lastPeriodIdleValue = 0;  // Простой контроллера за последний период
volatile TickType_t minIdleValue = 1 << periodLen; // минимальный простой контроллера с момента пуска
extern "C" void vApplicationIdleHook( void ) //В конфиге freertos  должно быть #define configUSE_IDLE_HOOK   1 в файл FreeRTOSConfig.h
{
    // Process idle tick counter
    volatile TickType_t curTick = xTaskGetTickCount();
    if(curTick != lastCountedTick)
    {
          curIdleTicks++;
          lastCountedTick = curTick;
    }
    // Store idle metrics each ~0.5 seconds (512 ticks)
    curTick >>= periodLen;
    if(curTick >  lastCountedPeriod)
    {
        lastPeriodIdleValue = curIdleTicks;
        curIdleTicks = 0;
        lastCountedPeriod = curTick;
          // Store the max value
          if(lastPeriodIdleValue < minIdleValue)
          minIdleValue = lastPeriodIdleValue;
    }  
   
}

// Задача по чтению датчиков ------------------------------------------------------------
static void vReadSensor(void *pvParameters) {
uint16_t i;
int32_t sum,rawVolt;
static TickType_t DHT22Tick = 0;     // счетчик тиков для суточных отсчетов (обновление времени и тестирование мотора)
static TickType_t curTick = 0;
static TickType_t sockTick = 0;     // сброс зависших сокетов
// При первом пуске автокалибровка датчика тока
//sensors.offsetACS758=CalibrACS758(); // Автокалибровка датчика тока, используется как пауза для включения светодиодов
 
  for (;;) {  
      iwdg_feed();  // Вачдог сброс
      SPI.setModule(1);
      SPI.setClockDivider(SPI_CLOCK_DIV2);
      xSemaphoreGive(xTFTSemaphore);

      if(curTick < sockTick) {DHT22Tick=0;sockTick=0;}// если счетик тиков xTaskGetTickCount() переполнился  
      // 1. Измерение тока (мА) с датчика ACS758  
      sum=0;
      for (i=0;i<CURRENT_SAMPLES;i++) // Копим отсчеты
      {
      rawVolt=(analogRead(PIN_ACS758)*UREF_VCC*10)/(4096-1); 
      if(rawVolt>sensors.offsetACS758) sum=sum+rawVolt-sensors.offsetACS758;else sum=sum+sensors.offsetACS758-rawVolt; // суммирование + выпремление тока и вычетание смещения (не забываем что у нас ток переменный) надо суммировать обе полуволны и убирать смещение vcc/2
     // _delay(1);
      delayMicroseconds(100);
      }
      sensors.CurrentACS758 = (sum/CURRENT_SAMPLES)*100/miliVoltsPerAmp;  // ток в мА
      curTick = xTaskGetTickCount();
      if(curTick < sockTick) {DHT22Tick=0;sockTick=0;}// если счетик тиков xTaskGetTickCount() переполнился  
    
      // 2. Пора чистить сокеты  
      if (curTick-sockTick>10*60*1000){  // Раз в 10 минут
      sockTick=curTick;
      SPI.setModule(1);
      SPI.setClockDivider(SPI_CLOCK_DIV2);
      xSemaphoreGive(xTFTSemaphore);
      checkSockStatus();  
      }
      
     // 3. Пора читать датчики DHT22 
    if (curTick-DHT22Tick>TIME_SCAN_SENSOR-CURRENT_SAMPLES/10){ // Пора читать датчики DHT22 период корректируется на время измерения тока CURRENT_SAMPLES
      DHT22Tick=curTick;
      digitalWrite(PIN_LED1,LOW);  
      measurement(); 
      digitalWrite(PIN_LED1,HIGH);
     }  
    //  vTaskDelay(TIME_SCAN_SENSOR-CURRENT_SAMPLES/10);    // период корректируется на время измерения тока CURRENT_SAMPLES
    vTaskDelay(200);
   }     
}

// Задача обновлению дисплея с частотой усреднения ------------------------------------------------------------
static void vUpdateTFT(void *pvParameters) {
static TickType_t UpdateDataTick = 0;    // счетчик тиков обновления данных
static TickType_t UpdateChartTick = 0;   // счетчик тиков обновления графика
static TickType_t UpdateDayTick = 0;     // счетчик тиков для суточных отсчетов (обновление времени и тестирование мотора)
static TickType_t curTick = 0;
  for (;;) { 
   iwdg_feed();      // Вачдог сброс 
   curTick = xTaskGetTickCount();
   if(curTick<UpdateDataTick) {UpdateDataTick=0;UpdateChartTick=0;UpdateDayTick=0;}// если счетик тиков xTaskGetTickCount() переполнился  
   if((GETBIT(setting.flag,fTFT_OFF))&&(fullTftUpdate)) // выключение дисплея 
   {
     fullTftUpdate=false;                    // Сбросить признак полного обновления дисплей
     if(switchTFT()) tft.fillScreen(ILI9341_BLACK); // Стереть экран 
     switchNET();
   }
   if (curTick-UpdateDayTick>24*TIME_HOUR){ // Время пришло раз в сутки
       UpdateDayTick=curTick;
       if(!checkNetLink()) {reset_w5500();_delay(100); init_w5500();}  // Сбросить контроллер если сети (нет линка) может он завис (для увеличения надежности)
       if (GETBIT(setting.flag,fNTP))// Если надо обновлять время по NTP
       {
          Udp.begin(localPort);
          getNtpTime();
          #ifdef DEBUG
          rt.breakTime(rt.now(),tm);
          Serial.print("RTC update NTP:");Serial.print(tm.year+1970);Serial.print("/");Serial.print(tm.month);Serial.print("/");Serial.print(tm.day);Serial.print(" ");Serial.print(tm.hour);Serial.print(":");Serial.print(tm.minute);Serial.print(":");Serial.println(tm.second);
          #endif  
       }
       if (GETBIT(setting.flag,fTEST)) testMotorAndACS758(); // Если надо тестировать 

     }
   
   if (!GETBIT(setting.flag,fTFT_OFF)) // Дисплей не выключен
     {
         if (fullTftUpdate){ // если надо полностью обновить дисплей
              fullTftUpdate=false;                  // Сбросить признак полного обновления дисплей
              print_static();                       // распечатать таблицу
              print_setting();                      // показать настройки   и обновить переменные границ
              print_data();                         // вывод усредненных значений 
              print_status(checkNetLink());         // панель состояния
              printChart();
              last_error=100;                       // Показать ошибку                    
             }
              print_LoadCPU();                      // Показ графика загрузки
              print_time();
          if (curTick-UpdateDataTick>TIME_SCAN_SENSOR){ //  Вывод усредненных значений
              UpdateDataTick=curTick;
              print_error_AHT();                    // Вывод ошибки чтения датчика при каждом чтении контроль за качеством связи с датчиком
              print_data();                          // вывод усредненных значений 
              print_status(checkNetLink());          // панель состояния
              if (FLAG_FAN_CHECK) ChartMotor=true;   // Признак того что надо показывать включение мотора на графике
             }
        if (curTick-UpdateChartTick>TIME_PRINT_CHART){ //  Вывод Графиков  
               UpdateChartTick=curTick;   
               printChart(); // Сдвиг графика и вывод новой точки
               #ifdef  DEBUG  
               Serial.println("Point add chart");
               #endif  
               if (GETBIT(setting.flag,fBEEP)) beep(30);  // Звук добавления точки
            } 
      } // if (!GETBIT(setting.flag,fTFT_OFF)) 
      vTaskDelay(TIME_UPDATE_TFT);
  }   // for  
}

// Задача работы веб сервера  ------------------------------------------------------------
static void vWebTask(void *pvParameters) {
static TickType_t NetTick = 0;     // счетчик тиков для проверки работы сети (мигание точки часов)
static TickType_t curTick = 0;  
for (;;) {
  curTick = xTaskGetTickCount();
  if(curTick<NetTick) {NetTick=0;}// если счетик тиков xTaskGetTickCount() переполнился  
  if(curTick-NetTick>TIME_UPDATE_TFT){NetTick=curTick; if(FLAG_NET_CHECK) FLAG_NET_TRUE; else FLAG_NET_FALSE;} // Мигание двоеточие часов - веб сервер работает
  webserver();
  vTaskDelay(5);    // было 10
  }
}

control.ino

void reset_sum()  // Сброс расчета скользящего среднего
{
uint16_t i=0;
sensors.first=true;
sensors.sum_tOut=0;
sensors.sum_tIn=0;
sensors.sum_relHOut=0;
sensors.sum_relHIn=0;

for(i=0;i<NUM_SAMPLES;i++) // Массивы для усреднения
    {
      sensors.dat_tOut[i]=0;
      sensors.dat_tIn[i]=0;
      sensors.dat_relHOut[i]=0;
      sensors.dat_relHIn[1]=0;
    }  
}
// Измерение и обработка данных чтение датчиков --------------------------------------------------------------------------------
void measurement()
{ 
  float temperature = 0;
  float humidity = 0;
  uint32_t temp; 
 
  // Проверка работоспособности вентилятора если он включен (если он работает)
  if(FLAG_FAN_CHECK) {if(sensors.CurrentACS758<setting.CurMin) FLAG_TEST_ERR; else FLAG_TEST_OK;}

  #ifdef  DEMO1  // Внутренний датчик
   sensors.inErr = 0;
   sensors.tIn=sensors.tIn+random(-60,95); 
   if (sensors.tIn>2000) sensors.tIn=1900;
   if (sensors.tIn<-1000) sensors.tIn=-900;
   sensors.relHIn=sensors.relHIn+(float)random(-60,90);
   if (sensors.relHIn>9600) sensors.relHIn=9000;
   if (sensors.relHIn<500) sensors.relHIn=1000;
  #else 
  if ((sensors.inErr = inDHT.read2(&temperature, &humidity, NULL)) != SimpleDHTErrSuccess) {
     sensors.numErrIn++; 
     #ifdef DEBUG
     Serial.print("Read inDHT failed, err="); Serial.println(uint8ToHex(sensors.inErr));
     #endif
  } else
   {
   temp=temperature*100+setting.eTIN;  // Добавим ошибку
    if (sensors.tIn==-5000) sensors.tIn=temp; else {
    sensors.tIn=constrain(temp, sensors.tIn-GAAP_TEMP, sensors.tIn+GAAP_TEMP); } // Убираем резкие скачки
     
   temp=humidity*100+setting.eHIN; 
    if (sensors.relHIn==5000) sensors.relHIn=temp; else {
    sensors.relHIn=constrain(temp, sensors.relHIn-GAAP_REALH, sensors.relHIn+GAAP_REALH); } 
     
  #ifdef DEBUG
 //  Serial.print("Temperature IN: "); Serial.println(sensors.tIn);
 //  Serial.print("Humidity IN...: "); Serial.println(sensors.relHIn); 
   #endif 
   }
  #endif 

  #ifdef  DEMO1  // Внешний датчик
   sensors.outErr = 0;
   sensors.tOut=sensors.tOut+random(-60,95); 
   if (sensors.tOut>2000) sensors.tOut=1900;
   if (sensors.tOut<-1000) sensors.tOut=-900;
   sensors.relHOut=sensors.relHOut+(float)random(-70,90);
   if (sensors.relHOut>9600) sensors.relHOut=9000;
   if (sensors.relHOut<500) sensors.relHOut=1000;
  #else  
    _delay(10); // Пауза между чтениями датиков (иначе часто ошибки вылезают при чтении второго датчика)
    if ((sensors.outErr = outDHT.read2(&temperature, &humidity, NULL)) != SimpleDHTErrSuccess) {
    sensors.numErrOut++;
    #ifdef DEBUG
    Serial.print("Read outDHT failed, err="); Serial.println(uint8ToHex(sensors.outErr)); 
    #endif
  } else
  {
    temp=temperature*100+setting.eTOUT; 
    if (sensors.tOut==-5000) sensors.tOut=temp; else {
    sensors.tOut=constrain(temp, sensors.tOut-GAAP_TEMP, sensors.tOut+GAAP_TEMP); } // Убираем резкие скачки
 
    temp=humidity*100+setting.eHOUT; 
    if (sensors.relHOut==5000) sensors.relHOut=temp; else {
    sensors.relHOut=constrain(temp, sensors.relHOut-GAAP_REALH, sensors.relHOut+GAAP_REALH); } 
    
    #ifdef DEBUG
 //   Serial.print("Temperature OUT: "); Serial.println(sensors.tOut);
 //   Serial.print("Humidity OUT...: "); Serial.println(sensors.relHOut); 
    #endif  
  }
  #endif
  // Скользящее среднее 
  if ((sensors.outErr==SimpleDHTErrSuccess)&&(sensors.inErr==SimpleDHTErrSuccess))// Если чтение без ошибок у ДВУХ датчиков обновляем данные
  {
  // 1. Удаляем самое старое значение из суммы
  sensors.sum_tOut=sensors.sum_tOut-sensors.dat_tOut[sensors.index];
  sensors.sum_tIn=sensors.sum_tIn-sensors.dat_tIn[sensors.index];
  sensors.sum_relHOut=sensors.sum_relHOut-sensors.dat_relHOut[sensors.index];
  sensors.sum_relHIn=sensors.sum_relHIn-sensors.dat_relHIn[sensors.index];
  // 2. Запоминаем новое значение
  sensors.dat_tOut[sensors.index]=sensors.tOut;
  sensors.dat_tIn[sensors.index]=sensors.tIn;
  sensors.dat_relHOut[sensors.index]=sensors.relHOut;
  sensors.dat_relHIn[sensors.index]=sensors.relHIn;
  //3. Добавляем к суммме новое значение
  sensors.sum_tOut=sensors.sum_tOut+sensors.tOut;
  sensors.sum_tIn=sensors.sum_tIn+sensors.tIn;
  sensors.sum_relHOut=sensors.sum_relHOut+sensors.relHOut;
  sensors.sum_relHIn=sensors.sum_relHIn+sensors.relHIn;  
  // 4. Вычисляем среднее значение
  if (sensors.first){ // первый проход - нет полного массива
       sensors.av_tIn=sensors.sum_tIn/(sensors.index+1);
       sensors.av_relHIn=sensors.sum_relHIn/(sensors.index+1);
       sensors.av_tOut=sensors.sum_tOut/(sensors.index+1);
       sensors.av_relHOut=sensors.sum_relHOut/(sensors.index+1);   
  }else{ 
       sensors.av_tIn=sensors.sum_tIn/NUM_SAMPLES;
       sensors.av_relHIn=sensors.sum_relHIn/NUM_SAMPLES;
       sensors.av_tOut=sensors.sum_tOut/NUM_SAMPLES;
       sensors.av_relHOut=sensors.sum_relHOut/NUM_SAMPLES;
  }
   // вычисление абсолютной влажности
   sensors.av_absHIn=(int)(calculationAbsH((float)(sensors.av_tIn/100.0),(float)(sensors.av_relHIn/100.0))*100.0);
   sensors.av_absHOut=(int)(calculationAbsH((float)(sensors.av_tOut/100.0),(float)(sensors.av_relHOut/100.0))*100.0);  
   // 5. Изменяем индекс 
   sensors.index++;
   if (sensors.index==NUM_SAMPLES) {sensors.index=0;sensors.first=false;} // Первая итерация
    #ifdef  DEBUG  
       Serial.println("Average value");
       Serial.print("IN T=");Serial.print(sensors.av_tIn);Serial.print(" H="); Serial.print(sensors.av_relHIn); Serial.print(" abs H="); Serial.println(sensors.av_absHIn);
       Serial.print("OUT T=");Serial.print(sensors.av_tOut);Serial.print(" H="); Serial.print(sensors.av_relHOut); Serial.print(" abs H="); Serial.println(sensors.av_absHOut);
 //      Serial.print("setting.T_min=");Serial.print(setting.T_min); Serial.print(" setting.dH_min=");Serial.println(setting.dH_min);
     #endif   
     // Данные обновились, надо оценить что включать
     CheckON();   // Проверка статуса вентилятора
   }   
}

// Калибровка датчика ACS758 --------------------------------------------------------------------------------
// Возвращает напряжение смещения 0 датчика
uint16_t CalibrACS758(void)
{
uint16_t i;
uint32_t sumOffset=0;
  for (i=0;i<CALIBR_SAMPLES;i++) // Копим отсчеты
  {
    sumOffset=sumOffset+(analogRead(PIN_ACS758)*UREF_VCC*10)/(4096-1);  // напряжение в ДЕСЯТЫХ мВ   
  // _delay(1);
  delayMicroseconds(100);
  }
   #ifdef  DEBUG  
     Serial.print("ACS758 '0' (mV)=");Serial.println(sumOffset/CALIBR_SAMPLES/10);
   #endif  
  return sumOffset/CALIBR_SAMPLES; // усредненный отсчет в мвольтах  
}

// установка АЦП для чтения температуры инапряжения питания ----------------------------------------------
// https://www.stm32duino.com/viewtopic.php?t=598   https://sparklogic.ru/arduino-for-stm32/reading-internal-temperature-sen...
#define ADC_CR2_TSVREFE_BIT 23
#define ADC_CR2_TSVREFE (1U << ADC_CR2_TSVREFE_BIT)
void setup_vdd_tempr_sensor() {
    adc_reg_map *regs = ADC1->regs;
    regs->CR2 |= ADC_CR2_TSVREFE;    // enable VREFINT and Temperature sensor
    // sample rate for TSVREFE ADC channel and for Temperature sensor
    regs->SMPR1 |=  (0b111 << 18);  // sample rate temperature
    regs->SMPR1 |=  (0b111 << 21);  // sample rate vrefint
    adc_calibrate(ADC1);
}

// Проверка статуса вытяжки, не пора ли переключится ----------------------------------------------------------
void CheckON(void)
{
#ifdef USE_HEAT // Если определен калорифер
// 0.  Проверить замораживание подвала КАЛОРИФЕР -- РАБОТАЕТ ВСЕГДА НЕЛЬЗЯ ОТКЛЮЧИТЬ
if (sensors.av_tIn<=TEMP_LOW) { FAN_OFF; HEAT_ON; return;}   // Контроль от промораживания подвала по идеи здесь надо включать калорифер
if ((FLAG_HEAT_CHECK)&&(sensors.av_tIn>TEMP_LOW+dT_OFF)) HEAT_OFF;    // Выключить калорифер
#endif

// 1. Режимы не зависящие от влажности и температуры ВЫСШИЙ приоритет
if ((setting.mode==BLOCK_OFF)&&(!FLAG_FAN_CHECK))  return;
if ((setting.mode==BLOCK_OFF)&&(FLAG_FAN_CHECK))  { FAN_OFF ; return;}
if ((setting.mode==HOOD_ON )&&(FLAG_FAN_CHECK))   return;
if ((setting.mode==HOOD_ON )&&(!FLAG_FAN_CHECK))  { FAN_ON  ; return;}

// 2. Режим охлаждения (второй приоритет) температура внутри больше 10 градусов темература снаружи меньше на 2 градуса чем внутри, на влажность не смотрим
if (setting.mode==COOLING)          // Режим охлаждение
  {
    if ((!FLAG_FAN_CHECK)&&(sensors.av_tIn>setting.T_min)&&((sensors.av_tIn-sensors.av_tOut)>setting.dH_min)) // dH_min используется не штатно для температуры
       {FAN_ON; return;}            // мотор выключен, температура выше установленной и снаружи температура ниже на 2 градуса  то ВКЛЮЧЕНИЕ мотора
    if ((FLAG_FAN_CHECK)&&(sensors.av_tIn<=sensors.av_tOut))   
       {FAN_OFF; return;}          // мотор включен и темература внутри ниже наружней то ВЫКЛЮЧЕННИЕ мотора
   return;                         // изменений нет выходим    
  } 
  
// 3. В режиме осушения (мотор рабатает) - проверка на достижение минимальной температуры помещения в режиме осушения - СРОЧНО ВЫКЛЮЧИТЬ  третий приоритет
if ((sensors.av_tIn<=setting.T_min)&&(FLAG_FAN_CHECK)){FAN_OFF;return;}         // выключить и выйти

if ((sensors.av_absHIn<=sensors.av_absHOut)&&(FLAG_FAN_CHECK)){FAN_OFF;return;} // влажность внутри меньше чем снаружи выключить и выйти

// 4. Режимы зависящие от температуры и влажности низший приоритет (что осталось)
if(FLAG_FAN_CHECK){  // Мотор включен
if ((sensors.av_tIn<=(setting.T_min))||(sensors.av_absHIn<(sensors.av_absHOut+dH_OFF))){FAN_OFF;return;}     // мотор включен и темература ниже критической ИЛИ абс влажность внутри ниже то ВЫКЛЮЧЕННИЕ мотора  
}else{               // Мотор выключен
if((sensors.av_tIn>setting.T_min)&&(sensors.av_absHIn>(sensors.av_absHOut+setting.dH_min))){FAN_ON;return;}// мотор выключен, темература выше критической, И абс влажность с наружи меньше то ВКЛЮЧЕНИЕ мотора
}

} 

// Тестирование мотора и определение 0 датчика ACS758 --------------------------------------------------------------------------------
// Возвращает true если тест пройден и false если не пройден
boolean testMotorAndACS758(void)
{
 boolean state=FLAG_FAN_CHECK; // запомнить состяние мотора до тестирования
 if(GETBIT(setting.flag,fAUTO)){ // Если требуется автокалиброва датчика тока
 #ifdef DEBUG
 Serial.println("Аutocalibration ACS758 . . .");
 #endif
 if(state){FAN_OFF; _delay(4000);}      // если надо то выключить мотор для калибровки ACS758
 vTaskSuspendAll(); 
 sensors.autoACS758=CalibrACS758();   // Автокалибровка датчика тока,
 xTaskResumeAll();
 if(GETBIT(setting.flag,fAUTO))sensors.offsetACS758=sensors.autoACS758;else sensors.offsetACS758=setting.constACS758*10; //Установить смещение в зависимости от настроек
 }
 #ifdef DEBUG
 Serial.print("Offset ACS758 (mV): "); Serial.println(sensors.offsetACS758/10);
 Serial.println("Test fan . . .");
 #endif
 if(!state){ FAN_ON; _delay(5000);} // Если мотор не включен, включить мотор и пауза для разгона
 if(sensors.CurrentACS758<setting.CurMin)FLAG_TEST_ERR;else FLAG_TEST_OK;
 #ifdef DEBUG
 if(FLAG_TEST_CHECK)  Serial.println("Fan OK"); else Serial.println("Fan not work!!"); 
 #endif 
 if(!state){_delay(2000);FAN_OFF;} // если надо выключить потор - если перед тестом он был выключен (возвращаем в состояние которые было)
 
}

Podval20.h

// Макросы работы с битами байта, используется для флагов
#define GETBIT(b,f)   ((b&(1<<(f)))?true:false)              // получить состяние бита
#define SETBIT1(b,f)  (b|=(1<<(f)))                          // установка бита в 1
#define SETBIT0(b,f)  (b&=~(1<<(f)))                         // установка бита в 0 
#define SWAPB(x)      ((unsigned short)x>>8) | (((unsigned short)x&0x00FF)<<8)

//  НОГИ к которым прицеплена переферия 
#define pinTFT_CS        D19   // SPI2 PB4        // CS
#define pinTFT_DC        D31   // SPI2 PB12       // DC
#define pinTFT_RST       D27   // SPI2 PA8       // сброс дисплея
#define PIN_BEEP         D18   // Ножка куда повешена пищалка
#define PIN_KEY          D24   // Ножка куда повешена кнопка (пока не поддерживается)
#define PIN_RELAY1       D8    // Ножка на которую повешено реле (SSR1) вентилятора
#define PIN_RELAY2       D9    // Ножка на которую повешено реле (SSR2) Калорифер
#define PIN_LED1         D20   // Ножка на которую повешен первый светодиод (LED1)
#define PIN_LED2         D17   // Ножка на которую повешен второй светодиод (LED2)
#define PIN_IN_DHT       PB6   // Ножка на которую повешен внутренний датчик IN 
#define PIN_OUT_DHT      PB7   // Ножка на которую повешен внешний датчик OUT
#define PIN_ACS758       PA0   // A0 Нога куда прицеплен токовый датчик ACS758
#define PIN_W5500        D3    // SPI1 сброс W5500
#define PIN_CS_W25Q8     D10   // SPI1 CS чипа памяти W25Q8ODVSSIG
#define PIN_CS_SDCARD    D23   // SPI1 CS карты памяти

// Мои макросы
#define FAN_BIT                   0            // бит мотора в sensors.flags
#define HEAT_BIT                  1            // бит калорифера в sensors.flags
#define TEST_BIT                  2            // бит отказа вентилятора (false-отказ)
#define NTP_BIT                   3            // бит удачного обновления времени по NTP 
#define NET_BIT                   4            // бит контроля здачи вебсервера 

#define FLAG_FAN_ON               (SETBIT1(sensors.flags,FAN_BIT))                   // бит мотора установить в 1
#define FLAG_FAN_OFF              (SETBIT0(sensors.flags,FAN_BIT))                   // бит мотора установить в 0
#define FLAG_FAN_CHECK            (GETBIT(sensors.flags,FAN_BIT))                    // бит мотора проверить на 1
#define FAN_ON                    {digitalWrite(PIN_RELAY1,HIGH); FLAG_FAN_ON;  }    // включить мотор
#define FAN_OFF                   {digitalWrite(PIN_RELAY1, LOW) ; FLAG_FAN_OFF; }    // выключить мотор

#ifdef USE_HEAT
#define FLAG_HEAT_ON              (SETBIT1(sensors.flags,HEAT_BIT))                  // бит калорифера установить в 1
#define FLAG_HEAT_OFF             (SETBIT0(sensors.flags,HEAT_BIT))                  // бит калорифера установить в 0
#define FLAG_HEAT_CHECK           (GETBIT(sensors.flags,HEAT_BIT))                   // бит калорифера проверить на 1
#define HEAT_ON                   {digitalWrite(PIN_RELAY2, HIGH); FLAG_HEAT_ON; }   // включить калорифер
#define HEAT_OFF                  {digitalWrite(PIN_RELAY2, LOW); FLAG_HEAT_OFF; }   // выключить калорифер
#endif

#define FLAG_TEST_OK             (SETBIT1(sensors.flags,TEST_BIT))                   // бит теста установить в 1 
#define FLAG_TEST_ERR            (SETBIT0(sensors.flags,TEST_BIT))                   // бит теста установить в 0 - ОТКАЗ ВЕНТИЛЯТОРА
#define FLAG_TEST_CHECK          (GETBIT(sensors.flags,TEST_BIT))                    // бит теста проверить на 1
 
#define FLAG_NTP_OK              (SETBIT1(sensors.flags,NTP_BIT))                    // бит NTP установить в 1 
#define FLAG_NTP_ERR             (SETBIT0(sensors.flags,NTP_BIT))                    // бит NTP установить в 0 - Время не обновилось по ntp
#define FLAG_NTP_CHECK           (GETBIT(sensors.flags,NTP_BIT))                     // бит NTP проверить на 1

#define FLAG_NET_FALSE           (SETBIT1(sensors.flags,NET_BIT))                    // бит NET установить в 1 
#define FLAG_NET_TRUE            (SETBIT0(sensors.flags,NET_BIT))                    // бит NET установить в 0 
#define FLAG_NET_CHECK           (GETBIT(sensors.flags,NET_BIT))                     // бит NET проверить на 1

// - КОНСТАНТЫ --------------------------------------
#define dH_OFF          20                       // Гистерезис абсолютной влажности в сотых грамма на куб (фактически сколько не доходит до уличной влажности и выключается)
#define dT_OFF          20                       // Гистерезис температуры в сотых градуса 
#define TEMP_LOW        200                      // Температура подвала критическая (в сотых градуса) - система выключается и включается калорифер
// Датчик тока ACS758
#define UREF_VCC        3330                      // опорное напряжение (Напряжение питания контроллера в данном случае) для ацп stm mV (для калибровки необходимо померить 3.3 вольта и занести сюда точное значение)
#define CALIBR_SAMPLES  800                       // ACS758 число усреднений при калибровке должно быть кратно 20
#define CURRENT_SAMPLES 400                       // ACS758 число усреднений при измернии должно быть кратно 20
#define miliVoltsPerAmp 40                        // ACS758 Чуствительность датчика см даташит
#define GAAP_TEMP  50                             // Максимальное изменение температуры между измерениями (в сотых градуса)
#define GAAP_REALH 100                            // Максимальное изменение температуры между измерениями (в сотых %)
// Границы графиков google 
#define CHART_POINT     120                       // Число точек графика 

// - ВРЕМЕНА ---------------------------------------
#ifdef DEMO                                     // Для демо все быстрее и случайным образом
    #define NUM_SAMPLES      2                  // Число усреднений измерений датчика
    #define TIME_SCAN_SENSOR 1000               // Время опроса датчиков мсек, для демки быстрее
    #define TIME_UPDATE_TFT  1000               // Время обновления дисплея   
    #define TIME_PRINT_CHART 4000               // Время вывода точки графика мсек, для демки быстрее
    #define TIME_HOUR        50000              // Число мсек в часе, для демки быстрее  
#else   
   #define NUM_SAMPLES      50                  // Число усреднений измерений датчика (скользящее среднее) NUM_SAMPLES*TIME_SCAN_SENSOR=Время полного обновления данных
   #define TIME_SCAN_SENSOR 6000                // Время опроса датчиков мсек
   #define TIME_UPDATE_TFT  2000                // Время обновления дисплея      
   #define TIME_PRINT_CHART 300000              // Время вывода точки графика мсек
   #define TIME_HOUR        3600000             // Число мсек в часе
#endif

// Настройки варианты 
#define NUM_SETTING    8                        // Число вариантов настроек 
const char *strMode[]= {"Выключено",
                        "Режим вытяжки",
                        "Охлаждение T>10 dT>5",
                        "Осушение T>+3 dH>0.3",
                        "Осушение T>+3 dH>0.6",
                        "Осушение T>+4 dH>0.4",
                        "Осушение T>+4 dH>0.8",
                        "Осушение T>+5 dH>0.8" 
                                   };
enum TYPE_MODE      //  Перечисляемый тип - режим работы блока
{
    BLOCK_OFF, 
    HOOD_ON, 
    COOLING,   // dH_min используется не штатно для температуры     
    DEHUMIDIFIER_T3_H3,
    DEHUMIDIFIER_T3_H6,
    DEHUMIDIFIER_T4_H4,
    DEHUMIDIFIER_T4_H8,
    DEHUMIDIFIER_T5_H8
};
              

webserver.ino

#include "webserver.h" 
const char IP_FORMAT[]="\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}" ;        // формат ввода ип адреса
const char INT3_FORMAT[]="-?[0-9]{1,3}";                                    // формат ввода целого 3 разряда
const char INT4_FORMAT[]="\\d{1,4}" ;                                       // формат ввода положительного целого 4 разряда
const char RED_FONT[]="<font color=\"red\">";                               // установка красного шрифта
const char GREEN_FONT[]="<font color=\"green\">";                           // установка зеленого шрифта
const char END_FONT[]="</font>" ;                                           // завершения тега фонт

char inBufGet[2048];                                                        // входной буффер ответа GET (web server)
uint32_t connectTime[MAX_SOCK_NUM];                                         // времена для чистки сокетов

// Проверка подключения сетевого провода ------------------------------------------------------------
#define W5500_LINK        0x01  // МАСКА регистра PHYCFGR  (W5500 PHY Configuration Register) [R/W] [0x002E] при котором считается что связь есть
#define W5500_SPEED       0x02  // МАСКА регистра PHYCFGR  (W5500 PHY Configuration Register) [R/W] [0x002E] определяется скорость Speed Status
#define W5500_DUPLEX      0x04  // МАСКА регистра PHYCFGR  (W5500 PHY Configuration Register) [R/W] [0x002E] определяется дуплекс Duplex Status
boolean checkNetLink()
{
 uint8_t st,i;
 for(i=0;i<20;i++){  // Пробуем линк прочитать до 20 раз
     st= W5100.readPHYCFGR();
     if(st & W5500_LINK) break;
     _delay(50);
 }
 if(st & W5500_LINK) return true;  else return false; 
}
// Аппаратный сброс (через ножку reset) чипа w5500 
void reset_w5500(void)
{
  pinMode(PIN_W5500, OUTPUT);   // активным является LOW
  digitalWrite(PIN_W5500, LOW);  
  _delay(50);
  digitalWrite(PIN_W5500, HIGH); 
}

// Инициализация чипа w5500, запомнить полученный IP адрес
void init_w5500(void){
  #ifdef DEBUG
  Serial.println("Init w5500 . . .");
  #endif    
Ethernet.begin(mac, setting.ip, setting.dns, setting.gateway,setting.mask);  // Пишем адрес статик вначале иначе дальше проблема при работе с сетью при отсутсвии линка, 
if (GETBIT(setting.flag,fBEEP)) beep(200); else _delay(200); // Пищалка на старте и пауза перед возможным обращением к серверу DHCP
if(checkNetLink()) 
{ 
   if (GETBIT(setting.flag,fDHCP)){digitalWrite(PIN_LED1, LOW); Ethernet.begin(mac); digitalWrite(PIN_LED1, HIGH); // Если есть линк то пытаемся получить адрес DHCP, мигнуть LED1 в момент получения адреса
   #ifdef DEBUG
   Serial.print("Local IP:");Serial.println(Ethernet.localIP());
   Serial.print("Gateway IP:");Serial.println(Ethernet.gatewayIP());
   Serial.print("DNS server IP:");Serial.println(Ethernet.dnsServerIP());
   Serial.print("Subnet mask:");Serial.println(Ethernet.subnetMask());
   #endif
    }
  }

  server.begin();
  realIP=Ethernet.localIP(); // Запомнить полученный адрес РЕАЛЬНЫЙ
  #ifdef DEBUG
  Serial.print("Web server is at "); Serial.println(realIP);
  #endif  
}

// Сброс зависших сокетов ------------------------------------------------------------
void checkSockStatus()
{
 for (uint8_t i = 0; i < MAX_SOCK_NUM; i++) {        // По всем сокетам!!
     uint8_t s = W5100.readSnSR(i);                                          // Прочитать статус сокета
//     Serial.print("Socket ");Serial.print(i); Serial.print(" state:");Serial.println(s,HEX); 
     if((s == SnSR::ESTABLISHED) || (s == SnSR::CLOSE_WAIT) /*|| (s == 0x22)*/ ) { // если он "кандидат"
        if(abs(millis() - connectTime[i]) > 5000) {        // Время пришло millis() может переполняться
          #ifdef DEBUG
          Serial.print(" Socket frozen:");Serial.println(i);
          #endif        
          W5100.execCmdSn(i, Sock_CLOSE);
          W5100.writeSnIR(i, 0xFF);
        }
    } // if((s == 0x17) || (s == 0x1C))
    else connectTime[i] = millis();                                         // Обновить время если статус не кандидат
  } // for
}

EthernetClient client; 
// В данной задаче нельзя использовать вывод на дисплей
// Веб сервер для настройки моста -------------------------------------------------------------
void webserver()
{
  boolean modeParser; // Флаг начала работы парсера Referer:GET /get?IP=192.168.1.102
  uint8_t getCount=0,i;
  uint16_t num=0;
  char arg[6];
  client = server.available();
  if (client) {
 //   Serial.println("new client");
    modeParser=false;
    getCount=0;
    num=0;
    inBufGet[num]=0;
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        if (getCount!=9)
        {
        if (c=='G') getCount=1; else 
        if((c=='E')&&(getCount==1))getCount=2; else
        if((c=='T')&&(getCount==2))getCount=3; else 
        if((c==' ')&&(getCount==3))getCount=4; else 
        if((c=='/')&&(getCount==4))getCount=5; else 
        if((c=='g')&&(getCount==5))getCount=6; else 
        if((c=='e')&&(getCount==6))getCount=7; else
        if((c=='t')&&(getCount==7))getCount=8; else 
        if((c=='?')&&(getCount==8)){ modeParser=true;getCount=9; }else  getCount=0;
         }
  //     else  if ((c=='?')&&(getCount==3)) modeParser=true;
        if (modeParser==true){
        if (c != '\n') { inBufGet[num]=c; num++;}
        else { modeParser=false; parserGET(inBufGet);}
        }
        // if you've gotten to the end of the line (received a newline
        // character) and the line is blank, the http request has ended,
        // so you can send a reply
        if (c == '\n' && currentLineIsBlank) {
          // send a standard http response header

          vTaskSuspendAll();    
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html; charset=utf-8");
          client.println("Connection: close");  // the connection will be closed after completion of the response
          if (GETBIT(setting.flag,fUPDATE)) client.println("Refresh:60");  //авто обновление страницы раз в 60 секунд, по настройкам
          client.println();
          client.println(indexMain0);  // Секция заголовок
          client.println("<form action=\"/get\"enctype=\"text/plain\"id=\"mode\"method=\"get\">");   // Маленькая форма что бы поменять режим работы блока
          oneInputMode(client, (char*)"Режим работы: ",setting.mode);
          // чекбокс показа настроек 
          strcpy(inBufGet,(char*)"&nbsp;&nbsp;Показать настройки");strcat(inBufGet,"&nbsp;<input name=\"");strcat(inBufGet,(char*)"QUICK");strcat(inBufGet,"\" type=\"checkbox\"");
          if (GETBIT(setting.flag,fFULL_WEB)) strcat(inBufGet,"checked");
          strcat(inBufGet,"/>");
          client.println(inBufGet);    
          client.println("<input name=\"send\" type=\"submit\"value=\">>\"/></form>"); // Надпись на кнопке ">>" не менять, она используется для определения что вводится
          client.println("</td></tr></table></body><hr/>");
          xTaskResumeAll();
          updateData(client);          // Секция данные
          if (GETBIT(setting.flag,fFULL_WEB)){
           //       vTaskSuspendAll();  
              client.println(indexMain1);  // Секция настройки
    //          xTaskResumeAll();
              client.println("<tr><td><i>&nbsp;&nbsp;&nbsp;&nbsp;1. Сетевые настройки W5500</i></td><td></td></tr>");
              oneInputCheckbox(client, (char*)"Получение сетевых настроек по DHCP",(char*)"DHCP", GETBIT(setting.flag,fDHCP)); 
              oneInputText(client, (char*)"IP адрес",                 (char*)"IP",     (char*)IPAddress2String(setting.ip),      16,(char*)IP_FORMAT);
              oneInputText(client, (char*)"DNS",                      (char*)"DNS",    (char*)IPAddress2String(setting.dns),     16,(char*)IP_FORMAT);
              oneInputText(client, (char*)"Шлюз",                     (char*)"gateway",(char*)IPAddress2String(setting.gateway), 16,(char*)IP_FORMAT);
              oneInputText(client, (char*)"Маска подсети",            (char*)"mask",   (char*)IPAddress2String(setting.mask),    16,(char*)IP_FORMAT);
              client.println("<tr><td><i>&nbsp;&nbsp;&nbsp;&nbsp;2. Опции</i></td><td></td></tr>");
              oneInputCheckbox(client, (char*)"Обновление времени раз в сутки по NTP",(char*)"NTP", GETBIT(setting.flag,fNTP)); 
              oneInputCheckbox(client, (char*)"Авто обновление web страницы раз в 60 сек.",(char*)"UPDATE", GETBIT(setting.flag,fUPDATE)); 
              oneInputCheckbox(client, (char*)"Повернуть изображение на дисплее на 180 градусов",(char*)"TFT_180", GETBIT(setting.flag,fTFT_180));           
              oneInputCheckbox(client, (char*)"Выключить дисплей",(char*)"TFT_OFF", GETBIT(setting.flag,fTFT_OFF)); 
              oneInputCheckbox(client, (char*)"Включить биппер",(char*)"BEEP", GETBIT(setting.flag,fBEEP)); 
              client.println("<tr><td><i>&nbsp;&nbsp;&nbsp;&nbsp;3. Датчик тока ACS758</i></td><td></td></tr>");
              oneInputCheckbox(client, (char*)"Ежедневное тестирование вентилятора",(char*)"TEST", GETBIT(setting.flag,fTEST));
              oneInputCheckbox(client, (char*)"Автоматическое определение смещения \"0\" ACS758",(char*)"AUTO", GETBIT(setting.flag,fAUTO));
              arg[0]=0; _itoa(setting.constACS758,arg); oneInputText(client, (char*)"Смещение \"0\" ACS758, если нет автоматического определения (мВ)", (char*)"CONST",arg,4,(char*)INT4_FORMAT); 
              arg[0]=0; _itoa(setting.CurMin,arg); oneInputText(client, (char*)"Минимальный ток потребления вентилятора при тесте (мА)", (char*)"CurMin",arg,4,(char*)INT4_FORMAT); 
              client.println("<tr><td><i>&nbsp;&nbsp;&nbsp;&nbsp;4. Ошибки датчиков DHT22 (добавляются)</i></td><td></td></tr>");
              arg[0]=0; _itoa(setting.eTIN,arg); oneInputText(client, (char*)"Ошибка внутреннего датчика температуры (сотые доли C°)", (char*)"eTIN",arg,4,(char*)INT3_FORMAT); 
              arg[0]=0; _itoa(setting.eTOUT,arg); oneInputText(client, (char*)"Ошибка внешнего датчика температуры (сотые доли C°)", (char*)"eTOUT",arg,4,(char*)INT3_FORMAT); 
              arg[0]=0; _itoa(setting.eHIN,arg); oneInputText(client, (char*)"Ошибка внутреннего датчика влажности (сотые доли %)", (char*)"eHIN",arg,4,(char*)INT3_FORMAT); 
              arg[0]=0; _itoa(setting.eHOUT,arg); oneInputText(client, (char*)"Ошибка внешнего датчика влажности (сотые доли %)", (char*)"eHOUT",arg,4,(char*)INT3_FORMAT); 
              client.println(indexMain2); // инфо блок
          }
          vTaskSuspendAll();    
          client.print("<i>Версия прошивки: "); client.print(VERSION); 
          client.print("&nbsp;&nbsp;&nbsp;&nbsp;Текуший IP адрес: "); client.print(IPAddress2String(realIP));          
          client.print("&nbsp;&nbsp; mac:"); for(i=0;i<6;i++){client.print(mac[i],HEX);if(i!=5) client.print(":");} client.print("</i><br>"); 

          client.println(indexMain3);
          xTaskResumeAll();
          break;
        }
        if (c == '\n') {  // you're starting a new line
          currentLineIsBlank = true; 
          modeParser=false;
        } 
        else if (c != '\r') {
          // you've gotten a character on the current line
          currentLineIsBlank = false;
        }
      }
    }
  }
   vTaskDelay(5);    // give the web browser time to receive the data
   iwdg_feed();      // Вачдог сброс
   client.stop();    // close the connection:
 //  Serial.println("client disonnected");
}

// Парсер ответа get - сохранение настроек в рабочей структуре без сохранения во флеше (для обоих форм!!)
// вход - буфер который надо разобрать
// возврат true - надо настройки сохранить, false настройки не изменились
void parserGET(char *buf)
{ 
uint8_t code;  
char *str, *strGet;  
boolean update=false, tft_off=false, tft_180=false, ChangeTFT=false, upTFT=false, dhcp=false, ntp=false, _update=false, _beep=false, test=false, keyForm=false, quick=false, _auto=false;
IPAddress temp;
str = strstr(buf,"HTTP");  str[0]=0;     //Обрезать хвост

while ((str = strtok_r(buf, "&", &buf)) != NULL) // разбор отдельных комманд
  {
//  Serial.println(str);

  if (strstr(str,"IP")!=NULL) // Команда ip найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL){parseIPAddress(strGet+1,'.',temp); if(setting.ip!=temp){setting.ip=temp; update=true;}}
   }
  if (strstr(str,"DNS")!=NULL) // Команда DNS найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL){parseIPAddress(strGet+1,'.',temp); if(setting.dns!=temp){setting.dns=temp; update=true;}}
   }   
  if (strstr(str,"gateway")!=NULL) // Команда gateway найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL){parseIPAddress(strGet+1,'.',temp); if(setting.gateway!=temp){setting.gateway=temp; update=true;}}
   }
  if (strstr(str,"mask")!=NULL) // Команда mask найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL){parseIPAddress(strGet+1,'.',temp);if(setting.mask!=temp){setting.mask=temp; update=true;}}
   }
 if (strstr(str,"mode")!=NULL) // Команда mode найдена
   {
    strGet=strchr(str,'='); // содержит = и код символа от 0 до 7
    code=((uint8_t)strGet[1])-0x30;// преобразовать код символа в число (грязная техника)  
    if (strGet!=NULL){ if((setting.mode!=(TYPE_MODE)code)&&(code>=0)&&(code<=7)){setting.mode=(TYPE_MODE)code; update=true;ChangeTFT=true;}}   // Обновление на дисплее режима работы
 //   Serial.println(setting.mode);
   } 
if (strstr(str,"QUICK")!=NULL) // Команда QUICK найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL) 
        if(strcmp(strGet+1,"on")==0) quick=true;
   }       
if (strstr(str,"TFT_OFF")!=NULL) // Команда TFT_OFF найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL) 
        if(strcmp(strGet+1,"on")==0) tft_off=true;
   }    
if (strstr(str,"TFT_180")!=NULL) // Команда TFT_180 найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL) 
        if(strcmp(strGet+1,"on")==0) tft_180=true;
   }    
if (strstr(str,"DHCP")!=NULL) // Команда DHCP найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL) 
        if(strcmp(strGet+1,"on")==0) dhcp=true;
   }             
if (strstr(str,"NTP")!=NULL) // Команда NTP найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL) 
        if(strcmp(strGet+1,"on")==0) ntp=true;
   }             
if (strstr(str,"UPDATE")!=NULL) // Команда UPDATE найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL) 
        if(strcmp(strGet+1,"on")==0) _update=true;
   }             
if (strstr(str,"BEEP")!=NULL) // Команда BEEP найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL) 
        if(strcmp(strGet+1,"on")==0) _beep=true;
   } 
if (strstr(str,"TEST")!=NULL) // Команда TEST найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL) 
        if(strcmp(strGet+1,"on")==0) test=true;
   }    
if (strstr(str,"AUTO")!=NULL) // Команда TEST найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL) 
        if(strcmp(strGet+1,"on")==0) _auto=true;
   } 
   
 if (strstr(str,"CONST")!=NULL) // Команда CONST найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL){ if(setting.constACS758!=atoi(strGet+1)) {setting.constACS758=atoi(strGet+1);update=true;}}
   }
     
 if (strstr(str,"CurMin")!=NULL) // Команда CurMin найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL){ if(setting.CurMin!=atoi(strGet+1)) {setting.CurMin=atoi(strGet+1);update=true;}}
   }
   
 if (strstr(str,"eTIN")!=NULL) // Команда eTIN найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL){ if(setting.eTIN!=atoi(strGet+1)) {setting.eTIN=atoi(strGet+1);update=true;}}
   }
if (strstr(str,"eTOUT")!=NULL) // Команда eTOUT найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL){ if(setting.eTOUT!=atoi(strGet+1)) {setting.eTOUT=atoi(strGet+1);update=true;}}
   }
if (strstr(str,"eHIN")!=NULL) // Команда eHIN найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL){ if(setting.eHIN!=atoi(strGet+1)) {setting.eHIN=atoi(strGet+1);update=true;}}
   }
if (strstr(str,"eHOUT")!=NULL) // Команда eHOUT найдена
   {
    strGet=strchr(str,'=');
    if (strGet!=NULL){ if(setting.eHOUT!=atoi(strGet+1)) {setting.eHOUT=atoi(strGet+1);update=true;}}
   }
// определяем какая кнопка обрабатывается (есть две кнопки ">>" и "Примнить") ДВЕ ФОРМЫ!!!
 if (strstr(str,"send")!=NULL) // определяем кнопку
   {
    strGet=strchr(str,'=');
    if (strstr(strGet+1,"%3E%3E")!=NULL)keyForm=false;else keyForm=true; //Кодировка >> в %3E%3E
   } 
   
  } //while ((str = strtok_r(buf, "&", &buf)) != NULL)
// Отдельный анализ чекбоксов
if (keyForm) { // если используется кнопка "применить" нижняя форма

if ((!tft_off)&&(GETBIT(setting.flag,fTFT_OFF ))){SETBIT0(setting.flag,fTFT_OFF );update=true;ChangeTFT=true;}
else if ((tft_off)&&(!GETBIT(setting.flag,fTFT_OFF ))){SETBIT1(setting.flag,fTFT_OFF );update=true;ChangeTFT=true;}

if ((!tft_180)&&(GETBIT(setting.flag,fTFT_180))){SETBIT0(setting.flag,fTFT_180);update=true;ChangeTFT=true;}
else if ((tft_180)&&(!GETBIT(setting.flag,fTFT_180))){SETBIT1(setting.flag,fTFT_180);update=true;ChangeTFT=true;}

if ((!dhcp)&&(GETBIT(setting.flag,fDHCP))){SETBIT0(setting.flag,fDHCP);update=true;}
else if ((dhcp)&&(!GETBIT(setting.flag,fDHCP))){SETBIT1(setting.flag,fDHCP);update=true;}

if ((!ntp)&&(GETBIT(setting.flag,fNTP))){SETBIT0(setting.flag,fNTP);update=true;}
else if ((ntp)&&(!GETBIT(setting.flag,fNTP))){SETBIT1(setting.flag,fNTP);update=true;}

if ((!_update)&&(GETBIT(setting.flag,fUPDATE))){SETBIT0(setting.flag,fUPDATE);update=true;}
else if ((_update)&&(!GETBIT(setting.flag,fUPDATE))){SETBIT1(setting.flag,fUPDATE);update=true;}

if ((!_beep)&&(GETBIT(setting.flag,fBEEP))){SETBIT0(setting.flag,fBEEP);update=true;}
else if ((_beep)&&(!GETBIT(setting.flag,fBEEP))){SETBIT1(setting.flag,fBEEP);update=true;}

if ((!test)&&(GETBIT(setting.flag,fTEST))){SETBIT0(setting.flag,fTEST);update=true;}
else if ((test)&&(!GETBIT(setting.flag,fTEST))){SETBIT1(setting.flag,fTEST);update=true;}

if ((!_auto)&&(GETBIT(setting.flag,fAUTO))){SETBIT0(setting.flag,fAUTO);update=true;}
else if ((_auto)&&(!GETBIT(setting.flag,fAUTO))){SETBIT1(setting.flag,fAUTO);update=true;}

}
else // Верхняя форма  кнопка ">>"
{
if ((!quick)&&(GETBIT(setting.flag,fFULL_WEB))){SETBIT0(setting.flag,fFULL_WEB);update=true;quick=true;}
else if ((quick)&&(!GETBIT(setting.flag,fFULL_WEB))){SETBIT1(setting.flag,fFULL_WEB);update=true;quick=true;}
  
}
  
if (update) {
   if(ChangeTFT)            // Настройки дисплея были изменены
        fullTftUpdate=true; // Установить признак включения дисплея
   if(GETBIT(setting.flag,fAUTO))sensors.offsetACS758=sensors.autoACS758;else sensors.offsetACS758=setting.constACS758*10; //Установить смещение в зависимости от настроек
   saveEEPROM();
   }
}

// Вывести одно поле ввода параметра Text - функция универсальна ----------------------------------------------------------------
const char constInputText[]="<tr><td>%s&nbsp;</td><td valign=\"top\"><input maxlength=\"16\" name=\"%s\" required=\"required\" size=\"%d\" type=\"text\" value=\"%s\" pattern=\"%s\" /></td></tr>";
void oneInputText(EthernetClient client, char *label, char *name, char *val, int len, char *pattern)
{
char tmp[8];  
    tmp[0]=0;
    strcpy(inBufGet,"<tr><td>");
    strcat(inBufGet,label);
    strcat(inBufGet,"&nbsp;</td><td valign=\"top\"><input maxlength=\"16\" name=\"");
    strcat(inBufGet,name);
    strcat(inBufGet,"\" required=\"required\" size=\"");
    strcat(inBufGet,_itoa(len,tmp));
    strcat(inBufGet,"\" type=\"text\" value=\"");
    strcat(inBufGet,val);
    strcat(inBufGet,"\" pattern=\"");
    strcat(inBufGet,pattern);
    strcat(inBufGet,"\" /></td></tr>\r\n");
vTaskSuspendAll();
client.println(inBufGet);  
xTaskResumeAll();
}

// выпадающий список - режим работы блока  --------------------------------------------------------------------------------------
//const char constInputOrder1234[]="<tr><td>%s&nbsp;</td><td valign=\"top\"><select name=\"order\" size=\"1\"><option selected=\"selected\" value=\"o1234\">1234</option><option value=\"o4321\">4321</option><option value=\"o2143\">2143</option><option value=\"o3412\">3412</option></select></td></tr>\r\n";
void oneInputMode(EthernetClient client, char *label, TYPE_MODE mode)
{
uint8_t i; 
char tmp[8]; 
inBufGet[0]=0; // стереть буфер     
//  strcpy(inBufGet,"<tr><td>");
  strcpy(inBufGet,"<br><br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
  strcat(inBufGet,label);
//  strcat(inBufGet,"&nbsp;</td><td valign=\"top\"><select name=\"mode\" size=\"1\">");
  strcat(inBufGet,"<select name=\"mode\" size=\"1\">");
  for(i=0;i<NUM_SETTING;i++)
  {
    if (i==mode) strcat(inBufGet,"<option selected=\"selected\" value=\""); 
    else strcat(inBufGet,"<option value=\"");  
    tmp[0]=0;
    strcat(inBufGet,_itoa(i,tmp)); // Кодировка - просто по порядку цифры от 0 до 7
    strcat(inBufGet,"\">");
    strcat(inBufGet,strMode[i]);
    strcat(inBufGet,"</option>");
  }
//  strcat(inBufGet,"</select></td></tr>");
  strcat(inBufGet,"</select>");
vTaskSuspendAll();    
client.println(inBufGet);  
xTaskResumeAll();
}

// Чек бокс в форме - - функция универсальна  --------------------------------------------------------------------------------------------------------------
// <form name="1">Переставлять байты в передаче текстовых данных<input name="orText" type="checkbox" />&nbsp;</form>
//const char constInputCheckboxOn[]= "<tr><td>%s&nbsp;</td><td valign=\"top\"><input name=\"%s\" type=\"checkbox\" checked /></td></tr>\r\n";
//const char constInputCheckboxOff[]="<tr><td>%s&nbsp;</td><td valign=\"top\"><input name=\"%s\" type=\"checkbox\" /></td></tr>\r\n";
void oneInputCheckbox(EthernetClient client, char *label, char *name, boolean b)
{
  strcpy(inBufGet,"<tr><td>");strcat(inBufGet,label);strcat(inBufGet,"&nbsp;</td><td valign=\"top\"><input name=\"");strcat(inBufGet,name);strcat(inBufGet,"\" type=\"checkbox\"");
  if (b) strcat(inBufGet,"checked");
  strcat(inBufGet,"/></td></tr>");
vTaskSuspendAll();
client.println(inBufGet);    
xTaskResumeAll();
}

// Вывод данных ----------------------------------------------------------------------------------------
void updateData(EthernetClient cl)
{
int i,x; 
char b[8];  
vTaskSuspendAll();
cl.println("<span style=\"font-size:18px\"><strong>Состояние вентилятора:"); // Показ состояния вентилятора
if(FLAG_FAN_CHECK)   cl.print(" ВКЛЮЧЕН "); else cl.print(" ВЫКЛЮЧЕН ");
if(FLAG_TEST_CHECK)  cl.println("(исправен)."); else cl.println("<font color=\"red\">(отказ мотора!).</font>"); 
cl.println("</strong></span>"); 
xTaskResumeAll();

Table_Show(cl);   // вывод таблицы
inBufGet[0]=0; // стереть буфер
vTaskSuspendAll();
cl.print(ChartHeader);
cl.print(startTemp);   // График температур
xTaskResumeAll();
        x=posChart;
        inBufGet[0]=0; // стереть буфер
        for(i=0;i<CHART_POINT;i++)  // первый график
         { 
           strcat(inBufGet,"[");_itoa(CHART_POINT-i,inBufGet);strcat(inBufGet,",");strcat(inBufGet,ftoa(b,(float)(chartTemp.dataIn[x])/100.0,2));strcat(inBufGet,",");strcat(inBufGet,ftoa(b,(float)(chartTemp.dataOut[x])/100.0,2));strcat(inBufGet,"]"); 
           if (x<CHART_POINT-1) x++; else x=0; 
           if (i<CHART_POINT-1) strcat(inBufGet,","); 
         }
vTaskSuspendAll();
cl.print(inBufGet);
cl.print(endTemp);
xTaskResumeAll();

inBufGet[0]=0; // стереть буфер
vTaskSuspendAll();
cl.print(startAbsH);  // График Абсолютных влажносей
xTaskResumeAll();
        x=posChart;
        inBufGet[0]=0; // стереть буфер
        for(i=0;i<CHART_POINT;i++)  // первый график
         { 
          strcat(inBufGet,"[");_itoa(CHART_POINT-i,inBufGet);strcat(inBufGet,",");strcat(inBufGet,ftoa(b,(float)((chartAbsH.dataIn[x]&0xbfff))/100.0,2));strcat(inBufGet,",");strcat(inBufGet,ftoa(b,(float)(chartAbsH.dataOut[x])/100.0,2));strcat(inBufGet,"]"); // Обнулить 14 разряд - признак включения мотора
           if (x<CHART_POINT-1) x++; else x=0; 
           if (i<CHART_POINT-1) strcat(inBufGet,","); 
         }
vTaskSuspendAll();
cl.print(inBufGet);
cl.print(endAbsH);
cl.print(ChartBotton);
xTaskResumeAll();
} 
// --------------------------------------------------------------------
// Вывод отдельной таблицы с данными на экран
#define END_TD_TR "</td></tr>"
#define TD_TD "</td><td>"
void Table_Show(EthernetClient cl)
{
float tempr;
int vdd;
char buf[8];  
vTaskSuspendAll();
// Внутрення опора 1.2 вольта, я поставил 1208 после калибровки по питанию
vdd = 1208*4096.0/adc_read(ADC1,17); // (мВ) following 1.43 and 0.0043 parameters come from F103 datasheet - ch. 5.9.13  and need to be calibrated for every chip (large fab parameters variance)
tempr = (1.43 - (vdd/1000.0/4096.0*adc_read(ADC1,16)))/0.0043 + 25.0;
xTaskResumeAll();
strcpy(inBufGet,"<table border=0>");
strcat(inBufGet,"<tr><td>");
strcat(inBufGet,"<table border=1>");
strcat(inBufGet,"<tr><td> Параметр </td><td>Подпол</td><td>Улица</td></tr>");
strcat(inBufGet,"<tr><td>Температура (C°)</td> <td>");strcat(inBufGet,RED_FONT);strcat(inBufGet,ftoa(buf,(float)(sensors.av_tIn)/100.0,2));strcat(inBufGet,END_FONT);strcat(inBufGet,TD_TD);strcat(inBufGet,GREEN_FONT);strcat(inBufGet,ftoa(buf,(float)(sensors.av_tOut)/100.0,2));strcat(inBufGet,END_FONT);strcat(inBufGet,END_TD_TR);
strcat(inBufGet,"<tr><td>Относительная влажность (%)</td> <td>");strcat(inBufGet,RED_FONT);strcat(inBufGet,ftoa(buf,(float)(sensors.av_relHIn)/100.0,2));strcat(inBufGet,END_FONT);strcat(inBufGet,TD_TD);strcat(inBufGet,GREEN_FONT);strcat(inBufGet,ftoa(buf,(float)(sensors.av_relHOut)/100.0,2));strcat(inBufGet,END_FONT);strcat(inBufGet,END_TD_TR);
strcat(inBufGet,"<tr><td>Абсолютная влажность (г/м<sup>3</sup>)</td><td>");strcat(inBufGet,RED_FONT);strcat(inBufGet,ftoa(buf,(float)(sensors.av_absHIn)/100.0,2));strcat(inBufGet,END_FONT);strcat(inBufGet,TD_TD);strcat(inBufGet,GREEN_FONT);strcat(inBufGet,ftoa(buf,(float)(sensors.av_absHOut)/100.0,2));strcat(inBufGet,END_FONT);strcat(inBufGet,END_TD_TR);
if (GETBIT(setting.flag,fFULL_WEB)){
    strcat(inBufGet,"<tr><td>Код последней ошибки чтения датчика DHT22 </td><td>"); strcat(inBufGet,uint8ToHex(sensors.inErr));strcat(inBufGet,TD_TD);strcat(inBufGet,uint8ToHex(sensors.outErr));;strcat(inBufGet,END_TD_TR);
    strcat(inBufGet,"<tr><td>Число ошибок чтения датчика DHT22</td><td>");_itoa(sensors.numErrIn,inBufGet);strcat(inBufGet,TD_TD);_itoa(sensors.numErrOut,inBufGet);strcat(inBufGet,END_TD_TR);
    strcat(inBufGet,"<tr><td>Текущий ток потребления (мА)</td><td colspan=\"2\" align=\"center\">");_itoa(sensors.CurrentACS758,inBufGet);strcat(inBufGet,END_TD_TR);
    strcat(inBufGet,"<tr><td>Напряжение \"0\" ACS758 (мВ)</td><td colspan=\"2\" align=\"center\">");_itoa(sensors.offsetACS758/10,inBufGet);strcat(inBufGet,END_TD_TR);
    strcat(inBufGet,"<tr><td>Время последнего сброса</td><td colspan=\"2\" align=\"center\">");
    _itoa(rTime.year+1970,inBufGet);strcat(inBufGet,"/");_itoa(rTime.month,inBufGet);strcat(inBufGet,"/");_itoa(rTime.day,inBufGet);strcat(inBufGet," ");
    _itoa(rTime.hour,inBufGet);strcat(inBufGet,":");_itoa(rTime.minute,inBufGet);strcat(inBufGet,":");_itoa(rTime.second,inBufGet);strcat(inBufGet,END_TD_TR);
    strcat(inBufGet,"<tr><td>Причина последнего сброса</td><td colspan=\"2\" align=\"center\">");strcat(inBufGet,whatReset(reg_RCC_CSR));strcat(inBufGet,END_TD_TR);
    strcat(inBufGet,"<tr><td>Напряжение питания stm32 (мВ)</td><td colspan=\"2\" align=\"center\">");_itoa(vdd,inBufGet);strcat(inBufGet,END_TD_TR);
    strcat(inBufGet,"<tr><td>Температура stm32 (C°)</td><td colspan=\"2\" align=\"center\">");strcat(inBufGet,ftoa(buf,tempr,2));strcat(inBufGet,END_TD_TR);
   }
strcat(inBufGet,"</table>");
vTaskSuspendAll();
cl.print(inBufGet);
xTaskResumeAll();
if (GETBIT(setting.flag,fFULL_WEB)){
    strcpy(inBufGet,"</td><td>");
    strcat(inBufGet,"<p style=\"margin-left: 50px;\"><small><i>");
    strcat(inBufGet,"Коды ошибок чтения датчиков DHT22<br>");
    strcat(inBufGet,"0x10 - Error to wait for start low signal<br>");
    strcat(inBufGet,"0x11 - Error to wait for start high signal<br>");
    strcat(inBufGet,"0x12 - Error to wait for data start low signal<br>");
    strcat(inBufGet,"0x13 - Error to wait for data read signal<br>");
    strcat(inBufGet,"0x14 - Error to wait for data EOF signal<br>");
    strcat(inBufGet,"0x15 - Error to validate the checksum<br>");
    strcat(inBufGet,"0x16 - Error when temperature and humidity are zero, it shouldn't happen<br>");
    strcat(inBufGet,"0x17 - Error when pin is not initialized<br>");
    strcat(inBufGet,"</i></small></p></td></tr>");
    strcat(inBufGet,"</table>");
    vTaskSuspendAll();
    cl.print(inBufGet);
    xTaskResumeAll();
  }
}  

// Часы по NTP -------------------------------------------
// В момент обновления горит LED2
const int NTP_PACKET_SIZE= 48;             // временная отметка NTP находится в первых 48 байтах сообщения
byte packetBuffer[NTP_PACKET_SIZE];        // буфер, в котором будут храниться входящие и исходящие пакеты 
 IPAddress timeServer(88,147,254,235);     // NTP-сервер ntp3.stratum2.ru
// IPAddress timeServer(132, 163, 4, 102); // NTP-сервер time-b.timefreq.bldrdoc.gov
// IPAddress timeServer(132, 163, 4, 103); // NTP-сервер time-c.timefreq.bldrdoc.gov NTP
time_t getNtpTime()
{
  #ifdef DEBUG
  Serial.println("Update time NTP");
  #endif 
  
  if(!checkNetLink()) { //Если нет линка то выходим
  #ifdef DEBUG
  Serial.println("No link ethernet");
  #endif
  FLAG_NTP_ERR; // время не обновлено
  return 0;  
  }
  digitalWrite(PIN_LED2, LOW); // В процессе обновления горит LED2
  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  #ifdef DEBUG
  Serial.println("Transmit NTP Request");
  #endif
  sendNTPpacket(timeServer);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      #ifdef DEBUG
      Serial.println("Receive NTP Response");
      #endif
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
       // Временная отметка начинается с 40 байта полученного пакета
       // и его длина составляет четыре байта или два слова.
       // Для начала извлекаем два этих слова:
       unsigned long highWord = packetBuffer[40]<<8 | packetBuffer[41];
       unsigned long lowWord =  packetBuffer[42]<<8 | packetBuffer[43];
       // Совмещаем четыре байта (два слова) в длинное целое.
       // Это и будет NTP-временем (секунды начиная с 1 января 1990 года):
        unsigned long secsSince1900 = highWord << 16 | lowWord;
        // Время Unix стартует с 1 января 1970 года. В секундах это 2208988800:
        const unsigned long seventyYears = 2208988800UL;
        uint32 epoch = secsSince1900-seventyYears+TZ;  // Вычитаем 70 лет и делаем поправку на часовой пояс
         #ifdef DEBUG  
         Serial.print("NTP ok, unix time = ");Serial.println(epoch);
         #endif
         rt.setTime(epoch);
         FLAG_NTP_OK; // время обновлено
         digitalWrite(PIN_LED2, HIGH); 
         return epoch;
    }
  }
  #ifdef DEBUG
  Serial.println("No NTP Response :-(");
  #endif
  FLAG_NTP_ERR; // время не обновлено
  digitalWrite(PIN_LED2, HIGH); 
  return 0; // return 0 if unable to get the time
}

// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:                 
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

tft.ino

// Цвета которые используются, подбор цвета https://trolsoft.ru/ru/articles/rgb565-color-picker
#define CHART_FON       0x0883  // цвет фона графика если вентилятор не работает
#define CHART_FON_FAN   0x39EA  // цвет фона графика если вентилятор работает
#define CHART_GRID      0xD6EE  // цвет сетки графика
#define CHART_AXIS      0x6338  // цвет осей графика
#define CHART_LABEL     0xDEC4  // цвет надписей графика
#define ILI9341_GREY    0x6B6D  // серый
#ifdef DEMO
  const char TIME_LABEL[] = "0m   2m   4m   6m   8m";  // метки времени на оси х для демо
#else
  const char TIME_LABEL[] = "0h        5h        10h"; // метки времени на оси х для work
#endif
#define hRow 18 // высота в пикселях ряда
#define maxString 64
static char OutputBuf[maxString + 1] = "";

// Переключиться на вывод на дисплей true  если удалось
__attribute__((always_inline)) inline boolean switchTFT()  
{
   if(xSemaphoreTake(xTFTSemaphore, 5000) == pdFALSE) {  // захватываем семафор
            #ifdef DEBUG
            Serial.println("Error take semaphore");
            #endif 
           return false;
        } 
  vTaskSuspendAll();       
  SPI.setModule(2);
  SPI.setClockDivider(SPI_CLOCK_DIV2);
  return true;
}

 // Переключиться на сеть, обычный режим
__attribute__((always_inline)) inline void  switchNET()
{
  SPI.setModule(1);
  SPI.setClockDivider(SPI_CLOCK_DIV2);
  xSemaphoreGive(xTFTSemaphore);
  xTaskResumeAll();
}

void reset_ili9341(void)
{
  pinMode(pinTFT_RST, OUTPUT);                    // Сброс дисплея сигнал активным является LOW
  digitalWrite(pinTFT_RST, LOW);  
  _delay(50);
  digitalWrite(pinTFT_RST, HIGH);  
}
void print_static()  // Печать статической картинки 
{
  uint8_t i;
  switchTFT(); 
  if (GETBIT(setting.flag,fTFT_180)) tft.setRotation(3); else tft.setRotation(1); // Установить орентацию дисплея
  tft.fillScreen(ILI9341_BLACK);
  tft.fillRect(0, 0, 320, 20, ILI9341_BLUE);
  tft.setTextColor(ILI9341_WHITE); 
  tft.drawString(utf8rus((char*)"Ток (мА)"),2,2,2);
  #ifdef DEMO
     tft.setTextColor(ILI9341_RED); 
     tft.drawString(utf8rus((char*)"DEMO"),2+105,2,2);
  #endif

 // Таблица для данных
  for(i=0;i<5;i++) {tft.drawLine(0, (1+i)*hRow+2, 320, (1+i)*hRow+2, ILI9341_GREEN);} // Горизонтальные линии
   tft.drawLine(210-2, 1*hRow+2, 210-2, 5*hRow+2, ILI9341_GREEN);
   tft.drawLine(270-2, 1*hRow+2, 270-2, 5*hRow+2, ILI9341_GREEN);
   
   tft.setTextColor(ILI9341_YELLOW);  // Заголовки
   tft.drawString(utf8rus((char*)"Подпол"),0+210,3+hRow*1,2);  
   tft.drawString(utf8rus((char*)"Улица"),0+270,3+hRow*1,2);
   tft.drawString(utf8rus((char*)"Температура градусы C\xB0"),0,3+hRow*2,2);  
   tft.drawString(utf8rus((char*)"Относительная влаж. %"),0,3+hRow*3,2);
   tft.drawString(utf8rus((char*)"Абсолют. влаж. г/м*3" ),0,3+hRow*4,2);  

  // Графики
   tft.setTextColor(CHART_LABEL);  // Заголовки
   tft.drawString(utf8rus((char*)"Температура"),0+20,3+hRow*5,2);  
   tft.drawString(utf8rus((char*)"Абс. влажность"),0+170,3+hRow*5,2);  
 
   tft.drawLine(1, 240-5-hRow, 130, 240-5-hRow, CHART_AXIS);
   tft.drawLine(320-130-25, 240-5-hRow, 320-1-25, 240-5-hRow, CHART_AXIS);
   
   tft.drawLine(1, 240-5-hRow, 1, 240-5-105-hRow, CHART_AXIS);                    // гор
   tft.drawLine(320-130-25, 240-5-hRow,320-130-25, 240-1-5-105-hRow, CHART_AXIS); // вер
 
   // надписи на графиках
   tft.setTextColor(CHART_LABEL);
   tft.setCursor(0+130,123); tft.print("+20.0");
   tft.setCursor(0+130,162); tft.print("0.0");
   tft.setCursor(0+130,202); tft.print("-20.0");

   tft.setCursor(0+295,112); tft.print("20.0");
   tft.setCursor(0+295,137); tft.print("15.0");
   tft.setCursor(0+295,162); tft.print("10.0");
   tft.setCursor(0+295,187); tft.print("5.0");
   tft.setCursor(0+295,212); tft.print("0.0");
   // ось времени
   tft.setTextColor(CHART_LABEL); 
   tft.setCursor(2,219);
   tft.print((char*)TIME_LABEL); tft.setCursor(162,220); tft.print((char*)TIME_LABEL); // Вывод шкалы времени

   
// Нижня строка статуса
   tft.setTextColor(ILI9341_WHITE); 
   tft.setCursor(2,229);
   tft.print("ver. ");  tft.print(VERSION);  tft.print("   IP:");tft.print(IPAddress2String(realIP));  tft.print(" mac:");
   for(i=0;i<6;i++){tft.print(mac[i],HEX);if(i!=5) tft.print(":");}
 
 switchNET();
}

//  вывод на экран данных (то что меняется)
void print_data(void)
{ 
char buf[10];  
// Статистика по моточасам, время ведется в интервалах вывода данных = NUM_SAMPLES*TIME_SCAN_SENSOR а потом пересчитывается в часы при выводе.
setting.hour_unit++;
if (FLAG_FAN_CHECK) setting.hour_motor++;  // если мотор включен
#ifdef USE_HEAT
if (FLAG_HEAT_CHECK) setting.hour_heat++;    // если нагреватель включен
#endif
buf[0]=0;
switchTFT();  
 // Печать тока потребления
   tft.fillRect(65,2,42,hRow-1, ILI9341_BLUE);
   if(!FLAG_TEST_CHECK) tft.setTextColor(ILI9341_RED); else tft.setTextColor(ILI9341_WHITE); // цвет в зависимости то теста
   tft.drawString(_itoa(sensors.CurrentACS758,buf),65,2,2);  
 // Печать значений для дома
   tft.setTextColor(ILI9341_RED); 
   tft.fillRect(0+212,3+hRow*2,55,hRow-1, ILI9341_BLACK); tft.drawString(ftoa(buf,(float)(sensors.av_tIn)/100.0,2),0+212,3+hRow*2,2);  
   tft.fillRect(0+212,3+hRow*3,55,hRow-1, ILI9341_BLACK); tft.drawString(ftoa(buf,(float)(sensors.av_relHIn)/100.0,2),0+212,3+hRow*3,2);  
   tft.fillRect(0+212,3+hRow*4,55,hRow-1, ILI9341_BLACK); tft.drawString(ftoa(buf,(float)(sensors.av_absHIn)/100.0,2),0+212,3+hRow*4,2);  
 
 // Печать значений для улицы
   tft.setTextColor(ILI9341_GREEN); 
   tft.fillRect(0+272,3+hRow*2,55,hRow-1, ILI9341_BLACK); tft.drawString(ftoa(buf,(float)(sensors.av_tOut)/100.0,2),0+272,3+hRow*2,2); 
   tft.fillRect(0+272,3+hRow*3,55,hRow-1, ILI9341_BLACK); tft.drawString(ftoa(buf,(float)(sensors.av_relHOut)/100.0,2),0+272,3+hRow*3,2); 
   tft.fillRect(0+272,3+hRow*4,55,hRow-1, ILI9341_BLACK); tft.drawString(ftoa(buf,(float)(sensors.av_absHOut)/100.0,2),0+272,3+hRow*4,2); 
       
 switchNET();  
} 

// Печать графика на экране, добавляется одна точка и график сдвигается 
void printChart() 
{
byte i,x=0,y;
int16_t tmp;
// Работаем через кольцевой буфер
// Добавить новую точку в кольцевой буфер
     chartTemp.dataIn[posChart]=sensors.av_tIn; // Температура в доме. диапазон -25 . . . +25 растягиваем на 100 точек  
     chartTemp.dataOut[posChart]=sensors.av_tOut; // Температура на улице. диапазон -25 . . . +25 растягиваем на 100 точек 
     chartAbsH.dataIn[posChart]=sensors.av_absHIn; // Абсолютная влажность в доме диапазон от 0 до 20 грамм на кубометр, растягиваем на 100 точек 
     if (ChartMotor==true) chartAbsH.dataIn[posChart]=chartAbsH.dataIn[posChart]+0x4000;  // Признак включения мотора- 14 бит в 1 - цвет фона на графике меняется
     ChartMotor=false;
     chartAbsH.dataOut[posChart]=sensors.av_absHOut; // Абсолютная влажность на улицу диапазон от 0 до 20 грамм на кубометр, растягиваем на 100 точек  

 //  digitalWrite(PIN_LED2,LOW);  
   switchTFT();
   for(i=0;i<CHART_POINT;i++)    // График слева на право
     { 
     // Вычислить координаты текущей точки x в кольцевом буфере. Изменяются от 0 до 120-1
     if (posChart<i) x=120+posChart-i; else x=posChart-i;
     // нарисовать фон в зависимости от статуса мотора
     if  (chartAbsH.dataIn[x]&0x4000){tft.drawLine(3+i,240-6-hRow,3+i,240-6-105-hRow,CHART_FON_FAN);tft.drawLine(320-130-23+i,240-6-hRow,320-130-23+i,240-6-105-hRow,CHART_FON_FAN);}// Мотор был ключен
     else                            {tft.drawLine(3+i,240-6-hRow,3+i,240-6-105-hRow,CHART_FON); tft.drawLine(320-130-23+i,240-6-hRow,320-130-23+i,240-6-105-hRow,CHART_FON);}// Мотор был выключен

     if (x%5==0) // Пунктирные линии графика
     {
       tft.drawPixel(3+i,240-6-hRow-10,CHART_GRID);
       tft.drawPixel(3+i,240-6-hRow-50,CHART_GRID);
       tft.drawPixel(3+i,240-6-hRow-90,CHART_GRID);
       tft.drawPixel(320-130-23+i,240-6-hRow-25,CHART_GRID);
       tft.drawPixel(320-130-23+i,240-6-hRow-50,CHART_GRID);
       tft.drawPixel(320-130-23+i,240-6-hRow-75,CHART_GRID);
       tft.drawPixel(320-130-23+i,240-6-hRow-100,CHART_GRID);
     }

     // Вывести новую точку
      if (chartTemp.dataIn[x]<=-2500) y=0;          // Если температура меньше -25 то округляем до -25
      else  if (chartTemp.dataIn[x]>=2500) y=100;   // Если температура больше 25  то округляем до 25
      else y=((2*chartTemp.dataIn[x])+5000)/100;    // внутри -25...+25 растягиваем в два раза
      if ((y==0)||(y==100)) tft.drawPixel(3+i,240-6-hRow-y,ILI9341_WHITE); else  tft.drawPixel(3+i,240-6-hRow-y,ILI9341_RED);
      
      if (chartTemp.dataOut[x]<=-2500) y=0;          // Если температура меньше -25 то округляем до -25
      else  if (chartTemp.dataOut[x]>=2500) y=100;   // Если температура больше 25  то округляем до 25
      else y=((2*chartTemp.dataOut[x])+5000)/100;    // внутри -25...+25 растягиваем в два раза
      if ((y==0)||(y==100)) tft.drawPixel(3+i,240-6-hRow-y,ILI9341_WHITE); else tft.drawPixel(3+i,240-6-hRow-y,ILI9341_GREEN);
   
      tmp=chartAbsH.dataIn[x]&0xbfff;             // Обнулить 14 разряд - признак включения мотора
      if (tmp>=2000) y=100;
      else y=(5*tmp)/100;                         // внутри 0...20 растягиваем в пять  раз     
      if (y==100) tft.drawPixel(320-130-23+i,240-6-hRow-y,ILI9341_WHITE);else tft.drawPixel(320-130-23+i,240-6-hRow-y,ILI9341_RED);
       
      if (chartAbsH.dataOut[x]>=2000) y=100;
      else y=(5*chartAbsH.dataOut[x])/100;   
      if (y==100) tft.drawPixel(320-130-23+i,240-6-hRow-y,ILI9341_WHITE);else tft.drawPixel(320-130-23+i,240-6-hRow-y,ILI9341_GREEN);    
  //    digitalWrite(PIN_LED2,HIGH);  
        }
 if (posChart<120-1) posChart++; else posChart=0;            // Изменили положение в буфере и Замкнули буфер
 switchNET();
} 

// Индикатор загрузки контроллера
#define hLoad 1  // Толщина индикатора
static uint16_t cpu_old=0;
void print_LoadCPU()
{
 uint16_t cpu; 
 cpu = 320 - 320*lastPeriodIdleValue/(1 << periodLen);
 if (cpu==cpu_old) return; // рисовать нечего выходим
 switchTFT();
 tft.fillRect(0, 239-hLoad , 320, hLoad, ILI9341_BLACK); // Стирание
 tft.fillRect(0, 239-hLoad, cpu, hLoad, ILI9341_WHITE); // Загрузка
 switchNET();
 cpu_old=cpu;
}

const unsigned char ventLogo []  = {  // перевод картинки http://javl.github.io/image2cpp/
  0x00, 0x00, 0x00, 0x01, 0xf0, 0x00, 0x01, 0xf8, 0x00, 0x03, 0xf8, 0x00, 0x03, 0xf0, 0x00, 0x01, 
  0xe0, 0x00, 0x71, 0xc0, 0x00, 0xf9, 0x2f, 0x80, 0xfe, 0x1f, 0x80, 0xfe, 0xdf, 0xc0, 0x7e, 0x0f, 
  0xc0, 0x38, 0xc7, 0x80, 0x00, 0xe3, 0x00, 0x01, 0xe0, 0x00, 0x07, 0xf0, 0x00, 0x07, 0xe0, 0x00, 
  0x03, 0xe0, 0x00, 0x01, 0x80, 0x00
};
const unsigned char heatLogo []  = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x01, 0xc0, 0x00, 0x01, 0xe0, 0x00, 0x11, 
  0xf0, 0x00, 0x1b, 0xf6, 0x00, 0x1f, 0xf6, 0x00, 0x1f, 0xfe, 0x00, 0x1f, 0xbf, 0x00, 0x3f, 0xbf, 
  0x00, 0x3f, 0x9f, 0x00, 0x37, 0x19, 0x00, 0x36, 0x03, 0x00, 0x30, 0x03, 0x00, 0x18, 0x06, 0x00, 
  0x08, 0x04, 0x00, 0x00, 0x00, 0x00
};
const unsigned char netLogo [] = { 
  0x00, 0x00, 0x00, 0x01, 0xe0, 0x00, 0x02, 0x10, 0x00, 0x02, 0x10, 0x00, 0x02, 0x10, 0x00, 0x03, 
  0x30, 0x00, 0x00, 0xc0, 0x00, 0x00, 0xc0, 0x00, 0x1f, 0xfe, 0x00, 0x3f, 0xff, 0x00, 0x20, 0xc1, 
  0x00, 0x30, 0xc3, 0x00, 0x8b, 0x34, 0x40, 0x8a, 0x14, 0x40, 0x8a, 0x14, 0x40, 0x8a, 0x14, 0x40, 
  0xf9, 0xe7, 0xc0, 0x00, 0x00, 0x00
};
#define icoX 210-65 // Положение иконок на панеле управления
static boolean blinkFan=false;
void print_status(boolean net) // Печать панели статуса Значки на статус панели
{
 switchTFT();
 if (!FLAG_TEST_CHECK) // Тест не пройден
 {if(blinkFan) tft.drawBitmap(icoX, 1, ventLogo, 18, 18, ILI9341_RED); else tft.drawBitmap(icoX, 1, ventLogo, 18, 18, ILI9341_GREY);  blinkFan=!blinkFan;} // мигание иконки при отказе вентилятора
 else{ 
 if (FLAG_FAN_CHECK) tft.drawBitmap(icoX, 1, ventLogo, 18, 18, ILI9341_WHITE); else tft.drawBitmap(icoX, 1, ventLogo, 18, 18, ILI9341_GREY);} // 1. Признак включения мотора
#ifdef USE_HEAT
 if (FLAG_HEAT_CHECK) tft.drawBitmap(icoX+20, 1, heatLogo, 18, 18, ILI9341_WHITE); else tft.drawBitmap(icoX+20, 1, heatLogo, 18, 18, ILI9341_GREY); // 2. Признак включения калорифера
#else
 tft.drawBitmap(icoX+20, 1, heatLogo, 18, 18, ILI9341_GREY);
#endif 
 if (net) tft.drawBitmap(icoX+40, 1, netLogo, 18, 18, ILI9341_WHITE); else tft.drawBitmap(icoX+40, 1, netLogo, 18, 18, ILI9341_GREY);// 1. Признак линка сети
 switchNET();
}

#define errX 280-68    // координата Х для кода ошибки
void print_error_AHT(void) // Печать ошибки чтения датчиков выводится 1 раз за TIME_UPDATE_TFT мсек
{
  uint16_t err=sensors.outErr+(sensors.inErr<<8);  // Суммараная ошибка
  if (err!=last_error)     // если статус ошибки поменялся то надо вывести если нет то не выводим - экономия время и нет мерцания
  {
      last_error=err; 
      switchTFT();
      tft.fillRect(errX,2,65-1,hRow,ILI9341_BLUE);  // Сначала стереть
      tft.setTextColor(ILI9341_WHITE);  
      if (err>0) tft.drawString(uint16ToHex(err),errX,2,2);
      else       tft.drawString((char*)"   Ok",errX,2,2);
      switchNET();  
   }   
}   
#define timeX 276    // координата Х для времени
void print_time(void) // Печать ошибки чтения датчиков выводится 1 раз за TIME_UPDATE_TFT мсек
{
  
rt.breakTime(rt.now(),tm);
OutputBuf[0]=0;
if(tm.hour<10)strcpy(OutputBuf,"0");_itoa(tm.hour,OutputBuf);strcat(OutputBuf," ");if(tm.minute<10)strcat(OutputBuf,"0");_itoa(tm.minute,OutputBuf);
 switchTFT();
 tft.fillRect(timeX,2,42,hRow,ILI9341_BLUE);  // Сначала стереть
 if(FLAG_NTP_CHECK) tft.setTextColor(ILI9341_WHITE); else tft.setTextColor(ILI9341_PINK); // Разный цвет в зависимости от обновления по NTP
 tft.drawString(OutputBuf,timeX,2,2);
 if(FLAG_NET_CHECK)  tft.drawString((char*)":",timeX+19,2,2); // Мигающее двоеточие - признак работы веб серевера
 switchNET();  
}   

// Вывод информации о настройках и сохрание индекса настроек в eeprom ---------------------------------
void print_setting()
{
  switchTFT();
 // Настройка
  tft.fillRect(0,3+hRow*1,208-1,hRow-1, ILI9341_BLACK);// Стереть
  tft.setTextColor(ILI9341_WHITE);  // Заголовки
  switch (setting.mode)
        {
        case  BLOCK_OFF:          tft.drawString(utf8rus((char*)strMode[BLOCK_OFF]),0,3+hRow*1,2);  setting.dH_min=255;setting.T_min=255; break; 
        case  HOOD_ON:            tft.drawString(utf8rus((char*)strMode[HOOD_ON]),0,3+hRow*1,2);  setting.dH_min=0;  setting.T_min=0;   break; 
        case  COOLING:            tft.drawString(utf8rus((char*)strMode[COOLING]),0,3+hRow*1,2);  setting.dH_min=500;setting.T_min=1000;break;   // dH_min используется не штатно для температуры     
        case  DEHUMIDIFIER_T3_H3: tft.drawString(utf8rus((char*)strMode[DEHUMIDIFIER_T3_H3]),0,3+hRow*1,2);  setting.dH_min=30; setting.T_min=300; break;
        case  DEHUMIDIFIER_T3_H6: tft.drawString(utf8rus((char*)strMode[DEHUMIDIFIER_T3_H6]),0,3+hRow*1,2);  setting.dH_min=60; setting.T_min=300; break;
        case  DEHUMIDIFIER_T4_H4: tft.drawString(utf8rus((char*)strMode[DEHUMIDIFIER_T4_H4]),0,3+hRow*1,2);  setting.dH_min=40; setting.T_min=400; break;
        case  DEHUMIDIFIER_T4_H8: tft.drawString(utf8rus((char*)strMode[DEHUMIDIFIER_T4_H8]),0,3+hRow*1,2);  setting.dH_min=80; setting.T_min=400; break;
        case  DEHUMIDIFIER_T5_H8: tft.drawString(utf8rus((char*)strMode[DEHUMIDIFIER_T5_H8]),0,3+hRow*1,2);  setting.dH_min=80; setting.T_min=500; break;
        default:                  tft.drawString(utf8rus((char*)strMode[BLOCK_OFF]),0,3+hRow*1,2);  setting.dH_min=255;setting.T_min=255; setting.mode=BLOCK_OFF;break; 
        } 
 switchNET();
}

util.ino

#include <itoa.h>  

// ------------------------------ Различные преобразования ------------------------------------------
// Float to String   - экономим место 
char *ftoa(char *a, double f, int precision)
{
 long p[] = {0,10,100,1000,10000,100000,1000000,10000000,100000000};
 char *ret = a;
 long heiltal = (long)f;
 itoa(heiltal, a, 10);
 while (*a != '\0') a++;
 *a++ = '.';
 long desimal = abs((long)((f - heiltal) * p[precision]));
 itoa(desimal, a, 10);
 return ret;
}

//int в *char в строку ДОБАВЛЯЕТ!! к строке 
char* _itoa( int value, char *string)
{
  char *ret = string;
 //   string[0]=0; // стереть строку
    while(*string) string++;

  char *pbuffer = string;
  unsigned char negative = 0;

  if (value < 0) {
    negative = 1;
    value = -value;
  }

  /* This builds the string back to front ... */
  do {
    *(pbuffer++) = '0' + value % 10;
    value /= 10;
  } while (value > 0);

  if (negative)
    *(pbuffer++) = '-';

  *(pbuffer) = '\0';

  /* ... now we reverse it (could do it recursively but will
   * conserve the stack space) */
  uint32_t len = (pbuffer - string);
  for (uint32_t i = 0; i < len / 2; i++) {
    char j = string[i];
    string[i] = string[len-i-1];
    string[len-i-1] = j;
  }
  return ret; 
}
// Разбор строки в IP адрес, если удачно то возвращает true, если не удачно то возвращает false и адрес не меняет
 boolean parseIPAddress(const char* str, char sep, IPAddress &ip)
{   int i,x;
    char y;
    byte tmp[4];
    for (i = 0; i < 4; i++) 
        {
        y=str[0];  
        x= strtoul(str, NULL, 10);             // Convert byte
        if (x>255) return false;               // Значение байта не верно
        if ((x==0)&&(y!='0')) return false;    // Значение байта не верно
        tmp[i]=x;
        str = strchr(str, sep);               // Find next separator
        if (str == NULL || *str == '\0') {
            break;                            // No more separators, exit
        }
        str++;                                // Point to next character after separator
    //    Serial.println(tmp[i]);
    }
 if (i<4-1) return false; else { ip=tmp;return true; }  
}
// IP адрес в строку
char _bufTemp[18];
char *IPAddress2String(IPAddress & ip) 
{
  _bufTemp[0] = '\0';
  for(uint8_t i = 0; i < 4; i++) {
    _itoa(ip[i], _bufTemp);
    if(i < 3) strcat(_bufTemp, ".");
  }
  return _bufTemp;
}
const char codeHex[]={"0123456789ABCDEF"};
// uint32_t в текстовую строку вида 0xabcdf50e
char* uint32ToHex(uint32_t f)
{ 
_bufTemp[0]='0';
_bufTemp[1]='x';
_bufTemp[2]=codeHex[0x0f & (f>>28)];
_bufTemp[3]=codeHex[0x0f & (f>>24)];
_bufTemp[4]=codeHex[0x0f & (f>>20)];
_bufTemp[5]=codeHex[0x0f & (f>>16)];
_bufTemp[6]=codeHex[0x0f & (f>>12)];
_bufTemp[7]=codeHex[0x0f & (f>>8)];
_bufTemp[8]=codeHex[0x0f & (f>>4)];
_bufTemp[9]=codeHex[0x0f & (f)];
_bufTemp[10]=0; // Конец строки
return _bufTemp;
}
// uint16_t в текстовую строку вида 0xf50e
char* uint16ToHex(uint16_t f)
{ 
_bufTemp[0]='0';
_bufTemp[1]='x';
_bufTemp[2]=codeHex[0x0f & (unsigned char)(highByte(f)>>4)];
_bufTemp[3]=codeHex[0x0f & (unsigned char)(highByte(f))];
_bufTemp[4]=codeHex[0x0f & (unsigned char)(lowByte(f)>>4)];
_bufTemp[5]=codeHex[0x0f & (unsigned char)(lowByte(f))];
_bufTemp[6]=0; // Конец строки
return _bufTemp;
}
// uint8_t в текстовую строку вида 0xf5
char* uint8ToHex(uint8_t f)
{ 
_bufTemp[0]='0';
_bufTemp[1]='x';
_bufTemp[2]=codeHex[0x0f & (unsigned char)(f>>4)];
_bufTemp[3]=codeHex[0x0f & (unsigned char)(f)];
_bufTemp[4]=0; // Конец строки
return _bufTemp;
}

void beep(int x)  // Пищать х мсек
{ digitalWrite(PIN_BEEP, HIGH);  
    _delay(x);
    digitalWrite(PIN_BEEP, LOW);   
} 

char *utf8rus(char *source)
{
  int i,j,k;
  unsigned char n;
  char m[2] = { '0', '\0' };

  strcpy(OutputBuf, ""); k = strlen(source); i = j = 0;

  while (i < k) {
    n = source[i]; i++;

    if (n >= 0xC0) {
      switch (n) {
        case 0xD0: {
          n = source[i]; i++;
          if (n == 0x81) { n = 0xA8; break; }
          if (n >= 0x90 && n <= 0xBF) n = n + 0x30;
          break;
        }
        case 0xD1: {
          n = source[i]; i++;
          if (n == 0x91) { n = 0xB8; break; }
          if (n >= 0x80 && n <= 0x8F) n = n + 0x70;
          break;
        }
      }
    }

    m[0] = n; strcat(OutputBuf, m);
    j++; if (j >= maxString) break;
  }
  return OutputBuf;
}

// Функция переводит относительную влажность в абсолютную 
// t-температура в градусах Цельсия h-относительная влажность в процентах
float calculationAbsH(float t, float h)
{
 float temp;
 temp=pow(2.718281828,(17.67*t)/(t+243.5));
 return (6.112*temp*h*2.1674)/(273.15+t);
}

// определение причины сброса stm32, на входе значение регистра RCC_CSR на выходе строка с описанием причины сброса
char bRes[32];
char* whatReset(uint32_t reg){
bRes[0]=0;
if (bitRead(reg,RCC_CSR_LPWRRSTF_BIT)) strcat(bRes,"Low power<br>");
if (bitRead(reg,RCC_CSR_WWDGRSTF_BIT)) strcat(bRes,"Window watchdog<br>");
if (bitRead(reg,RCC_CSR_IWDGRSTF_BIT)) strcat(bRes,"Independent watchdog<br>");
if (bitRead(reg,RCC_CSR_SFTRSTF_BIT))  strcat(bRes,"Software reset<br>");
if (bitRead(reg,RCC_CSR_PORRSTF_BIT))  strcat(bRes,"Power up<br>");
if (bitRead(reg,RCC_CSR_PINRSTF_BIT)) if(strlen(bRes)<1) strcat(bRes,"Reset key<br>"); // Если только нажата клавиша ресет (отальных источников нет)
return bRes;
}



// ------------------------------ EEPROM ------------------------------------------
// Сохранение настроек в EEPROM
uint16_t saveEEPROM()
{
    uint16_t *ptr,  i, count, Status;
    ptr=(uint16_t*)&setting;
    for (i=0;i<sizeof(setting)/2;i++) {Status=EEPROM.write(i,ptr[i]);if (Status!=0) break; }
#ifdef DEBUG  
    EEPROM.count(&count);
    Serial.print("Save setting, status=");Serial.print(Status);Serial.print(" number of variable:"); Serial.println(count);
#endif  
return Status;    
}

// Восстановление настроек из EEPROM
uint16_t loadEEPROM()
{
uint16_t *ptr,i,Status,count;  
ptr=(uint16_t*)&setting;
  for (i=0;i<sizeof(setting)/2; i+= 1) {Status=EEPROM.read(i,&ptr[i]); if (Status!=0) break;}
#ifdef DEBUG  
    EEPROM.count(&count);
    Serial.print("Load setting, status=");Serial.print(Status);Serial.print(" number of variable:"); Serial.println(count);
#endif  
return Status;    
}

// Инициализация EEPROM
uint16_t initEEPROM()
{
uint16_t Status;  
    // Установка области сохраненя
    EEPROM.PageBase0 = 0x801F000;
    EEPROM.PageBase1 = 0x801F800;
    EEPROM.PageSize  = 0x400;
    Status = EEPROM.init();// Инициализация
#ifdef DEBUG  
    Serial.print("EEPROM.init() : ");Serial.println(Status, HEX);
#endif      
return Status;     
}

// форматирование EEPROM
uint16_t formatEEPROM()
{
uint16_t Status;  
    Status = EEPROM.format();// Форматирование области
#ifdef DEBUG  
    Serial.print("EEPROM.format() : ");Serial.println(Status, HEX);
#endif      
return Status;     
}

webserver.h

#ifndef webserver_h
#define webserver_h
// Редактор https://be1.ru/html-redaktor-online/
// http://translit-online.ru/image-base64-converter.html
// https://texttools.ru/add-line-breaks
// https://www.buildmystring.com/default.php

// Константы для веб страницы --------------------------------------------------------------------------
const char  indexMain0[] = 
"<!DOCTYPE html>"
"<html>"
"<head>"
"<title>Осушитель 2.0</title>"
// https://base64.guru/converter/encode/image/ico
"<link href=\"data:image/x-icon;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQs"
"NFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAAgACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0K"
"xwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQE"
"BAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqO"
"kpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD7T+Nnx21Hwjr9j4J8EaUviHx5qCeYkDsTDZRn/lpKQRnpnqOOScYDeSatpPi1fEFpp3iz9oSbSPEt5DJOmnaRbEQRhCgdfkZAQC68FR39Can+Ed9"
"Pq1x8WPGxkCa9qniM6LFckfNbQKyBQPQBXUf8AX0qP9oHxFonwZ+L/wAHL2HS3kTTxqBuFVkR7uCdYbd1TcQZpg8kcnljkhT0zX0lWp/Z8lh6EVzJatpNt2u907Jbab7s4P4nvSehtW/xY+InwHksLrxtf23xB+H11IsJ8SadHtubMk4DSBRyM8dyem7OFP1BY31tqlj"
"b3lnNHc2lxGssM0RDJIjDKsMdQQQcduteT+IfB+mza/c+HJIF/sPxRYXEd1ZqBsSVAv71R0UkN27gGsX9ifXLrV/gRYWt05lOl3txYRyFiSyKwdeT0A37R7KBXNXUMVhniVFRnFpOysmneztsndNO2jNabcZ8j2PPdQW0+B3xd8XeGfE0jaf4G8dTnU9N1c8RWd2W3Mr"
"N0ADEc9AFjJwCxHu3ir4e6V8QtP8ADJ1O5muG0XUbXWbS7tnVS88OSrE4IKtk5A6g9a6vxf4M0Tx9oU+jeINNh1TTZuWgmQ8MOjLjBVh7EEdc14ef2MdL0t3i8O+PPF/h3THOW0+01H90MnkKQBx7nPNOVXDY1KdabhNJJ6XTtpfTVO2/R7goypt8quif48fEXT/h3cT"
"SWl7/AGh411S1Gm6PpKEEwNIcGYgchc4OT1xge179nnS4vhXpNh4QL7o5YxK0mP8Al4YbiSP9oY47fL6VZ8P/ALMPhP4c6bd3ui2VzrPiFyrPqOqTeddSAMC6qcAKSMggDOOpNI2k32s+L/NsY5vnnWXzHiZPIGcjdkcY6fhxXq4eOEnhZ0Kcrrq3pqttOiWtu+p4mNq"
"16VenKK+S633P/9k=\"  rel=\"icon\" "
"type=\"image/x-icon\" />"
"</head>"
"<body>"

"<table border=0><tr><td>" // Не видная таблица из двух ячеек картинка и надпись

"<img alt=\"\" src=\"data:image/x-icon;base64,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" 
"Aq1f8BMr8ZSHtbbjwAAAAASUVORK5CYII=\" />"
//"</body>\r\n"

"</td><td>"
"<span style=\"font-size:28px\"><strong>&nbsp;&nbsp;&nbsp;&nbsp;Осушитель 2.0</strong></span>";

//"</td></tr></table></body><hr/>"

const char  indexMain1[] = 
"<hr/>"
"<form action=\"/get\"enctype=\"text/plain\"id=\"network\"method=\"get\"name=\"Настройки\">"
" <table rules=\"none\"cellspacing=\"0\"cellpadding=\"2\"border=\"0\"width=\"640\">"
   "<caption>Настройки</caption>"
   "<tr>"
    "<th align=\"left\">Наименование параметра</th>"
    "<th align=\"left\">Значение</th>"
    "</tr>";


const char  indexMain2[] = 
"</table>"
//"<p>&nbsp;</p>\r\n"
"<p><input name=\"send\" type=\"submit\"value=\"Применить\" /></p>"
"</form>"
"<hr/><p></p>";

const char  indexMain3[] =
"<p>&nbsp;</p>"
"</body>"
"</html>";

// ------------------------------------- ГРАФИКИ ------------------------------------------------------------
// Google Chart  константы для вывода графиков
const char  ChartHeader[] =  // заголовок страницы
      "<html>"
      "<head>"
      "<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>"
      "<script type=\"text/javascript\">"
      "google.charts.load('current', {'packages':['corechart']});"
      "google.charts.setOnLoadCallback(drawTemp);"
      "google.charts.setOnLoadCallback(drawAbsH);";

const char  startTemp[] = // заголовок графика температуры 
      "function drawTemp(){"
      "var data=google.visualization.arrayToDataTable(["
      "['Время','Подпол','Улица'],";
const char  endTemp[] = // окончание графика температуры
      "]);"
      "var options={"
      "title:'Температура C°',"
      "titleTextStyle:{fontSize:16},"
      "curveType:'none',"//Сглаживать(function) или нет(none) линию
      "legend:{position:'bottom'},"
      "colors:['red','green'],"  // Цвет линий
      "chartArea:{left:40,top:20,width:'99%',height:'80%',backgroundColor:'#FAFAF5'}"
      "};"
      "var chart=new google.visualization.LineChart(document.getElementById('Temp_chart'));"
      "chart.draw(data,options);"
      "}";

const char  startAbsH[] = // заголовок графика влажности
      "function drawAbsH() {"
      "var data=google.visualization.arrayToDataTable(["
      "['Время','Подпол','Улица'],";
const char  endAbsH[] = // Окончание графика влажности
      "]);"
      "var options={"
      "title:'Абсолютная влажность г/м*3',"
      "titleTextStyle:{fontSize:16},"
      "curveType:'none',"//Сглаживать(function) или нет(none) линию
      "legend:{position:'bottom'},"
      "colors:['red','green']," // Цвет линий
      "chartArea:{left:40,top:20,width:'99%',height:'80%',backgroundColor:'#FAFAF5'}" // Положение и размеры области графика цвет области рисования
      "};"
      "var chart=new google.visualization.LineChart(document.getElementById('AbsH_chart'));"
      "chart.draw(data,options);"
      "}";
      
const char  ChartBotton[] = // окончание страницы графиков
      "</script>"
      "</head>"
      "<body>"
      "<table class=\"columns\">"
      "<tr>"
      "<td><div id=\"Temp_chart\" style=\"width:500px;height:300px\" \"border: 1px solid #ccc\"></div></td>"  // размеры графиков
      "<td><div id=\"AbsH_chart\" style=\"width:500px;height:300px\" \"border: 1px solid #ccc\"></div></td>"
      "</tr>"
      "</table>"
      "</body>"
      "</html>";
#endif      

DHT.h

/*
 The MIT License (MIT)

 Copyright (c) 2016-2017 winlin

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 */
// pav2000 переделано под stm32+freeRTOS

#ifndef _DHT_H
#define _DHT_H
#include <Arduino.h>

// High 8bits are time duration.
// Low 8bits are error code.
// For example, 0x0310 means t=0x03 and code=0x10,
// which is start low signal(0x10) error.
// @see https://github.com/winlinvip/SimpleDHT/issues/25
#define simpleDHTCombileError(t, err) ((t << 8) & 0xff00) | (err & 0x00ff)
#define SimpleDHTErrSuccess      0    // Success.
#define SimpleDHTErrStartLow     0x10 // Error to wait for start low signal.
#define SimpleDHTErrStartHigh    0x11 // Error to wait for start high signal.
#define SimpleDHTErrDataLow      0x12 // Error to wait for data start low signal.
#define SimpleDHTErrDataRead     0x13 // Error to wait for data read signal.
#define SimpleDHTErrDataEOF      0x14 // Error to wait for data EOF signal.
#define SimpleDHTErrDataChecksum 0x15 // Error to validate the checksum.
#define SimpleDHTErrZeroSamples  0x16 // Error when temperature and humidity are zero, it shouldn't happen.
#define SimpleDHTErrNoPin        0x17 // Error when pin is not initialized.

class SimpleDHT {
protected:
    long levelTimeout = 5000000; // 500ms
    int pin = -1;
public:
    SimpleDHT();
    SimpleDHT(int pin);
public:
    // to read from dht11 or dht22.
    // @param pin the DHT11 pin.
    // @param ptemperature output, NULL to igore. In Celsius.
    // @param phumidity output, NULL to ignore.
    //      For DHT11, in H, such as 35H.
    //      For DHT22, in RH%, such as 53%RH.
    // @param pdata output 40bits sample, NULL to ignore.
    // @remark the min delay for this method is 1s(DHT11) or 2s(DHT22).
    // @return SimpleDHTErrSuccess is success; otherwise, failed.
    virtual int read(byte* ptemperature, byte* phumidity, byte pdata[40]);
    virtual int read(int pin, byte* ptemperature, byte* phumidity, byte pdata[40]);
    // to get a more accurate data.
    // @remark it's available for dht22. for dht11, it's the same of read().
    virtual int read2(float* ptemperature, float* phumidity, byte pdata[40]) = 0;
    virtual int read2(int pin, float* ptemperature, float* phumidity, byte pdata[40]) = 0;
protected:
    // (eventually) change the pin configuration for existing instance
    // @param pin the DHT11 pin.
    void setPin( int pin );
protected:
    // measure and return time (in microseconds)
    // with precision defined by interval between checking the state
    // while pin is in specified state (HIGH or LOW)
    // @param level    state which time is measured.
    // @param interval time interval between consecutive state checks.
    // @return measured time (microseconds). -1 if timeout.
    virtual long levelTime(byte level, int firstWait = 10, int interval = 6);
    // @data the bits of a byte.
    // @remark please use simple_dht11_read().
    virtual byte bits2byte(byte data[8]);
    // read temperature and humidity from dht11.
    // @param data a byte[40] to read bits to 5bytes.
    // @return 0 success; otherwise, error.
    // @remark please use simple_dht11_read().
    virtual int sample(byte data[40]) = 0;
    // parse the 40bits data to temperature and humidity.
    // @remark please use simple_dht11_read().
    virtual int parse(byte data[40], short* ptemperature, short* phumidity);
};

/*
    Simple DHT11
    Simple, Stable and Fast DHT11 library.
    The circuit:
    * VCC: 5V or 3V
    * GND: GND
    * DATA: Digital ping, for instance 2.
    23 Jan 2016 By winlin <winlin@vip.126.com>
    https://github.com/winlinvip/SimpleDHT#usage
    https://akizukidenshi.com/download/ds/aosong/DHT11.pdf
    https://cdn-shop.adafruit.com/datasheets/DHT11-chinese.pdf
*/
class SimpleDHT11 : public SimpleDHT {
public:
    SimpleDHT11();
    SimpleDHT11(int pin);
public:
    virtual int read2(float* ptemperature, float* phumidity, byte pdata[40]);
    virtual int read2(int pin, float* ptemperature, float* phumidity, byte pdata[40]);
protected:
    virtual int sample(byte data[40]);
};

/*
    Simple DHT22
    Simple, Stable and Fast DHT22 library.
    The circuit:
    * VCC: 5V or 3V
    * GND: GND
    * DATA: Digital ping, for instance 2.
    3 Jun 2017 By winlin <winlin@vip.126.com>
    https://github.com/winlinvip/SimpleDHT#usage
    http://akizukidenshi.com/download/ds/aosong/AM2302.pdf
    https://cdn-shop.adafruit.com/datasheets/DHT22.pdf
*/
class SimpleDHT22 : public SimpleDHT {
public:
    SimpleDHT22();
    SimpleDHT22(int pin);
public:
    virtual int read2(float* ptemperature, float* phumidity, byte pdata[40]);
    virtual int read2(int pin, float* ptemperature, float* phumidity, byte pdata[40]);
protected:
    virtual int sample(byte data[40]);
};

#endif 

DHT.ino

/*
The MIT License (MIT)
Copyright (c) 2016-2017 winlin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// pav2000 переделано под stm32+freeRTOS

#include "DHT.h"

SimpleDHT::SimpleDHT() { }

SimpleDHT::SimpleDHT(int pin) { setPin(pin);}

int SimpleDHT::read(byte* ptemperature, byte* phumidity, byte pdata[40]) {
    int ret = SimpleDHTErrSuccess;
    if (pin == -1) { return SimpleDHTErrNoPin;  }
    float temperature = 0;
    float humidity = 0;
    if ((ret = read2(&temperature, &humidity, pdata)) != SimpleDHTErrSuccess) { return ret; }
    if (ptemperature) { *ptemperature = (byte)(int)temperature;  }
    if (phumidity) { *phumidity = (byte)(int)humidity;  }
    return ret;
}

int SimpleDHT::read(int pin, byte* ptemperature, byte* phumidity, byte pdata[40]) {
    setPin(pin);
    return read(ptemperature, phumidity, pdata);
}

void SimpleDHT::setPin(int pin) { this->pin = pin;}


long SimpleDHT::levelTime(byte level, int firstWait, int interval) {
    unsigned long time_start = micros();
    long time = 0;
    bool loop = true;
    for (int i = 0 ; loop; i++) {
        if (time < 0 || time > levelTimeout) { return -1;  }

        if (i == 0) {
            if (firstWait > 0)   {delayMicroseconds(firstWait);  }
        } else if (interval > 0) {delayMicroseconds(interval); }

        // for an unsigned int type, the difference have a correct value
        // even if overflow, explanation here:
        //     https://arduino.stackexchange.com/questions/33572/arduino-countdown-with...
        time = micros() - time_start;
        loop = (digitalRead(pin) == level);
    }
    return time;
}

byte SimpleDHT::bits2byte(byte data[8]) {
    byte v = 0;
    for (int i = 0; i < 8; i++) { v += data[i] << (7 - i);  }
    return v;
}

int SimpleDHT::parse(byte data[40], short* ptemperature, short* phumidity) {
    short humidity = bits2byte(data);
    short humidity2 = bits2byte(data + 8);
    short temperature = bits2byte(data + 16);
    short temperature2 = bits2byte(data + 24);
    byte check = bits2byte(data + 32);
    byte expect = (byte)humidity + (byte)humidity2 + (byte)temperature + (byte)temperature2;
    if (check != expect) { return SimpleDHTErrDataChecksum; }
    *ptemperature = temperature<<8 | temperature2;
    *phumidity = humidity<<8 | humidity2;
    return SimpleDHTErrSuccess;
}

SimpleDHT11::SimpleDHT11() {}

SimpleDHT11::SimpleDHT11(int pin) : SimpleDHT (pin) {}

int SimpleDHT11::read2(float* ptemperature, float* phumidity, byte pdata[40]) {
    int ret = SimpleDHTErrSuccess;
    if (pin == -1) { return SimpleDHTErrNoPin;  }
    byte data[40] = {0};
    if ((ret = sample(data)) != SimpleDHTErrSuccess) { return ret; }
    short temperature = 0;
    short humidity = 0;
    if ((ret = parse(data, &temperature, &humidity)) != SimpleDHTErrSuccess) { return ret;  }
    if (pdata) { memcpy(pdata, data, 40);  }
    if (ptemperature) { *ptemperature = (int)(temperature>>8);  }
    if (phumidity)    { *phumidity = (int)(humidity>>8);    }
    // For example, when remove the data line, it will be success with zero data.
    if (temperature == 0 && humidity == 0) {return SimpleDHTErrZeroSamples; }
    return ret;
}

int SimpleDHT11::read2(int pin, float* ptemperature, float* phumidity, byte pdata[40]) {
    setPin(pin);
    return read2(ptemperature, phumidity, pdata);
}

int SimpleDHT11::sample(byte data[40]) {
    // empty output data.
    memset(data, 0, 40);
    // According to protocol: [1] https://akizukidenshi.com/download/ds/aosong/DHT11.pdf
    // notify DHT11 to start:
    //    1. PULL LOW 20ms.
    //    2. PULL HIGH 20-40us.
    //    3. SET TO INPUT.
    // Changes in timing done according to:
    //  [2] https://www.mouser.com/ds/2/758/DHT11-Technical-Data-Sheet-Translated-Ve...
    // - original values specified in code
    // - since they were not working (MCU-dependent timing?), replace in code with
    //   _working_ values based on measurements done with levelTimePrecise()
    pinMode(pin, OUTPUT);
    digitalWrite(pin, LOW);            // 1.
    _delay(20);                        // specs [2]: 18us

    // Pull high and set to input, before wait 40us.
    // @see https://github.com/winlinvip/SimpleDHT/issues/4
    // @see https://github.com/winlinvip/SimpleDHT/pull/5
    digitalWrite(pin, HIGH);           // 2.
   // pinMode(pin, INPUT);
    pinMode(pin,INPUT_PULLUP);
    delayMicroseconds(25);             // specs [2]: 20-40us

    // DHT11 starting:
    //    1. PULL LOW 80us
    //    2. PULL HIGH 80us
    long t = levelTime(LOW);          // 1.
    if (t < 30) {                    // specs [2]: 80us
        return simpleDHTCombileError(t, SimpleDHTErrStartLow);
    }

    t = levelTime(HIGH);             // 2.
    if (t < 50) {                    // specs [2]: 80us
        return simpleDHTCombileError(t, SimpleDHTErrStartHigh);
    }

    // DHT11 data transmite:
    //    1. 1bit start, PULL LOW 50us
    //    2. PULL HIGH:
    //         - 26-28us, bit(0)
    //         - 70us, bit(1)
    for (int j = 0; j < 40; j++) {
          t = levelTime(LOW);          // 1.
          if (t < 24) {                    // specs says: 50us
              return simpleDHTCombileError(t, SimpleDHTErrDataLow);
          }
          // read a bit
          t = levelTime(HIGH);              // 2.
          if (t < 11) {                     // specs say: 20us
              return simpleDHTCombileError(t, SimpleDHTErrDataRead);
          }
          data[ j ] = (t > 40 ? 1 : 0);     // specs: 26-28us -> 0, 70us -> 1
    }

    // DHT11 EOF:
    //    1. PULL LOW 50us.
    t = levelTime(LOW);                     // 1.
    if (t < 24) {                           // specs say: 50us
        return simpleDHTCombileError(t, SimpleDHTErrDataEOF);
    }

    return SimpleDHTErrSuccess;
}

SimpleDHT22::SimpleDHT22() {}

SimpleDHT22::SimpleDHT22(int pin) : SimpleDHT (pin) {}

int SimpleDHT22::read2(float* ptemperature, float* phumidity, byte pdata[40]) {
    int ret = SimpleDHTErrSuccess;
    if (pin == -1) {return SimpleDHTErrNoPin; }
    byte data[40] = {0};
    if ((ret = sample(data)) != SimpleDHTErrSuccess) { return ret;  }

    short temperature = 0;
    short humidity = 0;
    if ((ret = parse(data, &temperature, &humidity)) != SimpleDHTErrSuccess) { return ret;  }
    if (pdata) {memcpy(pdata, data, 40); }
    if (ptemperature) {*ptemperature = (float)((temperature & 0x8000 ? -1 : 1) * (temperature & 0x7FFF)) / 10.0; }
    if (phumidity) {*phumidity = (float)humidity / 10.0; }
    return ret;
}

int SimpleDHT22::read2(int pin, float* ptemperature, float* phumidity, byte pdata[40]) {
    setPin(pin);
    return read2(ptemperature, phumidity, pdata);
}

int SimpleDHT22::sample(byte data[40]) {
    // empty output data.
    memset(data, 0, 40);
    // According to protocol: http://akizukidenshi.com/download/ds/aosong/AM2302.pdf
    // notify DHT11 to start:
    //    1. T(be), PULL LOW 1ms(0.8-20ms).
    //    2. T(go), PULL HIGH 30us(20-200us), use 40us.
    //    3. SET TO INPUT.
    pinMode(pin, OUTPUT);
    digitalWrite(pin, LOW);
    //delayMicroseconds(1000);
    _delay(1); // Вместо delayMicroseconds(1000);
    // Pull high and set to input, before wait 40us.
    // @see https://github.com/winlinvip/SimpleDHT/issues/4
    // @see https://github.com/winlinvip/SimpleDHT/pull/5
    digitalWrite(pin, HIGH);
  //  pinMode(pin, INPUT);
      pinMode(pin, INPUT_PULLUP);
    delayMicroseconds(40);
    // DHT11 starting:
    //    1. T(rel), PULL LOW 80us(75-85us).
    //    2. T(reh), PULL HIGH 80us(75-85us).
    long t = 0;
    if ((t = levelTime(LOW)) < 30) {
        return simpleDHTCombileError(t, SimpleDHTErrStartLow);
    }
    if ((t = levelTime(HIGH)) < 50) {
        return simpleDHTCombileError(t, SimpleDHTErrStartHigh);
    }

    // DHT11 data transmite:
    //    1. T(LOW), 1bit start, PULL LOW 50us(48-55us).
    //    2. T(H0), PULL HIGH 26us(22-30us), bit(0)
    //    3. T(H1), PULL HIGH 70us(68-75us), bit(1)
    for (int j = 0; j < 40; j++) {
          t = levelTime(LOW);          // 1.
          if (t < 24) {                    // specs says: 50us
              return simpleDHTCombileError(t, SimpleDHTErrDataLow);
          }

          // read a bit
          t = levelTime(HIGH);              // 2.
          if (t < 11) {                     // specs say: 26us
              return simpleDHTCombileError(t, SimpleDHTErrDataRead);
          }
          data[ j ] = (t > 40 ? 1 : 0);     // specs: 22-30us -> 0, 70us -> 1
    }

    // DHT11 EOF:
    //    1. T(en), PULL LOW 50us(45-55us).
    t = levelTime(LOW);
    if (t < 24) {
        return simpleDHTCombileError(t, SimpleDHTErrDataEOF);
    }
    return SimpleDHTErrSuccess;
} 

Конструктивная критика приветствуется, особенно по коду.

 

 

-NMi-
Offline
Зарегистрирован: 20.08.2018

Классно, конечно, но интернет зачем? Имхо без нета кошернее.

pav2000
Offline
Зарегистрирован: 15.12.2014

-NMi- пишет:
Классно, конечно, но интернет зачем? Имхо без нета кошернее.

Мне проще сделать управление и настройку прибора через веб морду, чем на экране менюшки городить. И кнопок не надо-)))

Скорость разработки выше, плюс бонусом удалённый мониторинг.

DetSimen пишет:

Это не код, это лютый писец какой-то. 

А чем код то не нравиться? 

 

b707
Онлайн
Зарегистрирован: 26.05.2017

DetSimen пишет:

за printf(...), dtostrf() и другие стандартные функции слышал чонить? 

да что printf ...

Pv2000 - вы в курсе, что для вывода на печать шестнадцатиричных чисел достаточно написать

 Serial.print(value, HEX) ?

К чему у вас там эти зубодробительные функции типа uint32_to_HEX ? :)

pav2000
Offline
Зарегистрирован: 15.12.2014

Маленькое задание для шибко умных

Сделайте маленькую программу со стандартными функциями и потом с моими.

Выпишите на листок размер кода и максимальный размер стека для обоих вариантов. 

Медитируете на эти числа и наступит Вам просветление, зачем это сделано. 

По поводу вывода hEX  посмотрите что выводит ваш вариант и что мой, если разницы нет то конечно все равно, но разница есть.

 

Ихмо Вот как раз использование стандартной printf в микроконтроллерах это боль.

 

andycat
andycat аватар
Offline
Зарегистрирован: 07.09.2017

какая то доля истины в ваших словах конечно есть, но писать три функции uintXXToHex вместо одной мне кажется перебор

void writeLOGhex(const unsigned long inHex){ // преобразует число в шестнадцатеричную строку и кладет на отправку
    unsigned long tmHex = inHex; // сохраняем число во временную переменную
    char hexBuf[20]; // строковый буфер где мы будем сохранять текст
    unsigned char i = 0; // позиция строкового буфера
    do {
        unsigned char k = tmHex % 16; // остаток от деления на 16 временного числа
        if (k > 9) hexBuf[i++] = k + '7'; else hexBuf[i++] = k + '0'; // кладем в строковый буфер цифру или букву, если число больше 9
    }
    while ((tmHex /= 16) > 0); // до тех пор, пока деленное на 16 временная переменная больше нуля
    if (i % 2) hexBuf[i++] = '0'; // если коичество символов не четное - добавлем нолик слева (на самом деле справа буфера)
    hexBuf[i] = '\0';  // заканчиваем строку нулем
    for(unsigned char j=i; j>0; --j) { // бежим циклом по строке в обратном порядке
        writeLOGbyte(hexBuf[j-1]); // и кидаем байты в отправляемый буфер
    }
}

 

b707
Онлайн
Зарегистрирован: 26.05.2017

pav2000 пишет:

По поводу вывода hEX  посмотрите что выводит ваш вариант и что мой, если разницы нет то конечно все равно, но разница есть.

а поподробнее?  что там стандартный принт не выводит? "0x" в начале числа не ставит? :)

andycat
andycat аватар
Offline
Зарегистрирован: 07.09.2017

нолик в начале не выводит если количество символов не четное и нолик перед x тоже не выводит

ЗЫ. И то не факт, может я ей просто пользоваться не умею

b707
Онлайн
Зарегистрирован: 26.05.2017

andycat пишет:

нолик в начале не выводит если количество символов не четное и нолик перед x тоже не выводит

беда :)

и ради этого надо писать отдельное преобразование для каждого hex разряда? (8 одинаковых строк для uint32 ?) Циклы уже не модны?

b707
Онлайн
Зарегистрирован: 26.05.2017

pav2000 пишет:

Конструктивная критика приветствуется, особенно по коду.

просили - получите

andycat
andycat аватар
Offline
Зарегистрирован: 07.09.2017

b707 пишет:

и ради этого надо писать отдельное преобразование для каждого hex разряда? (8 одинаковых строк для uint32 ?) Циклы уже не модны?

ну может человеку каждый такт и байт МК на вес золота, не хочет цикл делать :)

b707
Онлайн
Зарегистрирован: 26.05.2017

andycat пишет:

ну может человеку каждый такт и байт МК на вес золота, не хочет цикл делать :)

а не проще было написать что-нить типа

void hex_to_print(uint32_t val) {
 Serial.print("0x");
 Serial.print(val, HEX);
}

 

Upper
Offline
Зарегистрирован: 23.06.2020

b707 пишет:

andycat пишет:

ну может человеку каждый такт и байт МК на вес золота, не хочет цикл делать :)

а не проще было написать что-нить типа

void hex_to_print(uint32_t val) {
 Serial.print("0x");
 Serial.print(val, HEX);
}

Так вам уже писали - попробуйте и сравните результат.

b707
Онлайн
Зарегистрирован: 26.05.2017

Upper пишет:

Так вам уже писали - попробуйте и сравните результат.

что я должен там увидеть?

У меня сейчас нет под рукой ардуины. но предположу, что всей разницы то

print("0x"); print(255, HEX);

выдаст на выходе 0xFF. а функция ТС - 0x00FF в случае uint16

Вы правда считаете, что ради этого надо было городить три отдельных функции с повторяющимися строчками?

Что мешало написать нормальный вывод с нужным число нулей, независмый, кстати, от типа входящей переменной?

 

pav2000
Offline
Зарегистрирован: 15.12.2014

b707 пишет:

Upper пишет:

Неужели вы на полном серьезе считаете реплики DetSemen конструктивной критикой?

ну может быть резковато, но вообще все по теме. Человек просил покритиковать код - критикуем.

Даже такая критика, хоть и обидная - дает (умному) человеку пищу для ума Может быть он не подозревает, что существуют иные пути решения каких-то задач. Например в случае с преобразованием HEX - это очевидно из кода

DetSemen - это вообще не критика а просто балаган. По поводу hex критику принимаю, перепишу функцию.

по hex -мне нужно три варианта работы 1,2,4 (3,5 байт не интересно) байта с результатом в буфере.

Вариант andycat не нравится, тоже далеко до оптимальности, размер большой и скорость. Мой вариант точно быстрее и скорее всего по размеру может оказаться меньше.

Вариант b707 не подходит мне нужно в буфер

По поводу printf добавлю (в стиле DetSemen без пояснений) посмотрите механизм  распределение памяти задач для free RTOS, и тогда поймете вредность функций с большим стеком.

 

 

 

 

-NMi-
Offline
Зарегистрирован: 20.08.2018

А в этой rtos всё по честному со всякими там потоками, семафорами и тд?

b707
Онлайн
Зарегистрирован: 26.05.2017

pav2000 пишет:

далеко до оптимальности, размер большой и скорость. Мой вариант точно быстрее и скорее всего по размеру может оказаться меньше.

посмотрите механизм  распределение памяти задач для free RTOS, и тогда поймете вредность функций с большим стеком.

pav2000, Вы не обижайтесь - подскажите, какой у вас опыт в программировании на Си вообще и микроконтроллеров в частности? По коду я бы сделал предположение, что вы любитель, освоивший Си не так давно.
 Вы такими словами оперируете, как скорость процедур, и размер кода и размер стека... и все это в условиях RTOS. Вы эти парметры чем-то измеряли или просто на глазок прикинули, какая больше а какая меньше? Скорость исполнения как замерена? Загрузку стека чем смотрите?
andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

DetSimen пишет:

особенно понравился util.ino.  До слёз пробирает. 

ПыСы: Ты за printf(...), dtostrf() и другие стандартные функции слышал чонить? 

Вот смех смехом, а я недавно попытался в одном из своих проектов воспользоваться printf(). Прошивка сразу увеличилась на 14к и вылезла за пределы 64к. В общем, от printf() я отказался и заменил его самописной функцией на 300 байт, которая делает ровно то, что мне нужно, и ничего сверх этого.

Так что использование в МК написанных для ПК "стандартных функций" не всегда целесообразно, а иногда и совершенно невозможно.

pav2000
Offline
Зарегистрирован: 15.12.2014

b707 пишет:

pav2000, Вы не обижайтесь - подскажите, какой у вас опыт в программировании на Си вообще и микроконтроллеров в частности? По коду я бы сделал предположение, что вы любитель, освоивший Си не так давно.

 Вы такими словами оперируете, как скорость процедур, и размер кода и размер стека... и все это в условиях RTOS. Вы эти параметры чем-то измеряли или просто на глазок прикинули, какая больше а какая меньше? Скорость исполнения как замерена? Загрузку стека чем смотрите?

На критику не обижаюсь она весьма полезна но она должна быть обоснованной и корректной.

Опыт написания на Си более 10 лет, микроконтроллеры 4-5 лет, но не в очень интенсивном режиме. Да мой стиль далек от идеала. И я допустил ошибку портируя старый проект в новый, надо было с нуля писать (хотелось  просто побыстрее написать код).

Размер кода можно оценить по изменению бинарника, (с нужным кодом и без) именно так и был выявлен объем кода printf. 

Размер стека можно оценить отладочными средствами ОС.    Вот пример (фрагмент вебморды с четырех поточным web сервером) правда из другого проекта но он отражает возможности отладки freeRTOS

Быстродейстие - на глазок.

-NMi-

"А в этой rtos всё по честному со всякими там потоками, семафорами и тд?"

Да это присутствует, есть еще и очереди. Более того скажу что без семафоров и мютексов не обойтись при делении аппаратных ресурсов (в этом проекте spi делится между двумя задачами).

andriano

"Вот смех смехом, а я недавно попытался в одном из своих проектов воспользоваться printf(). Прошивка сразу увеличилась на 14к и вылезла за пределы 64к. В общем, от printf() я отказался и заменил его самописной функцией на 300 байт, которая делает ровно то, что мне нужно, и ничего сверх этого."

Вот и я про это, у меня тоже есть самописный принтф который реализует только то что мне нужно и имеет размер 1-1.5кб и стек бережно расходует.

Для понимания еще добавлю о распределении памяти в rtos.

1. Каждая задача имеет свой стек. Размер которого задается при создании задачи:

xTaskCreate(vReadSensor, "ReadSensor",150,NULL,4,&hReadSensor); 

150 это размер стека в 4 байтных словах. Размер стека определяется опытным путем, и надо понимать что это сложная задача т.к. алгоритм может быть сложным и по некоторым веткам при отладке можно не пройтись. По этому нужен запас.

2. Если стек по факту получается больше отведенного то все падает или есть не адекватное поведение.

3. В RTOS есть механизм контроля стека (включается отдельно) и при отладке можно получить минимальный размер (зарезервировано-максимальный стек по факту) стека (включается отдельно) см. картинку выше.

4. Из все выше сказанного получается следующее: Допустим есть три задачи с требуемыми объемами стеков 100 слов (4 байта). Есть функция printf которая требует 250-300 слов на стек. Мы эту функцию используем для вывода отладочных сообщений и используем во всех трех задачах.  Как итог получается что требуемый объем стеков для всех трех задач 100+250 слов как минимум.

Уж если мы полезли в такие дебри скажу что реализация spi в ардуиновской версии не совсем адекватная.

Если посмотреть на схему то у меня сеть висит на одном spi а дисплей на втором spi, это сделано специально -я хотел работать параллельно. т.е делать вывод одновременно (конечно переключаясь) в оба устройства. Это возможно. Но в ардуиновской версии библиотек при каждом использовании spi надо указывать с каким spi работать будем, что есть не гуд т.е надо переключать spi.

DetSimen

"Ладно, pav-лик, я больше не буду лезть с бессмысленными комментариями кода, извини."

принимается.