Система автополива расстений на Piranha ULTRA с даталоггингом, TouchScreen и RTC

Описание:

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

Нам понадобится:

Для реализации проекта необходимо установить библиотеки:

Подключение:

Для удобства подключения мы воспользуемся Trema Shield для Arduino. Так же нам понадобится: Trema-модуль Емкостной датчик влажности, Trema-модуль датчик освещённости, trema-модуль Адаптер карт  microSDЦветной графический дисплей 2.8 TFT 320x240, Сенсорный и Trema-модуль Силовой ключ.

Для начала подкючим Trema Shield к Piranha ULTRA:

Далее подключим  адаптер microSD карт и силовой ключ по следующим таблицам:

Адаптер карт microSDPiranha ULTRA
SS(CS)D10
MOSID11
MISOD12
SCKD13
VCCVCC
GNDGND
Силовой ключPiranha ULTRA
GGND
VVCC
DD8

Следующим шагом подключим датчик влажности, датчик освещённости и часы реального времени:

Датчик влажностиPiranha ULTRA
AA5
VVCC
GGND
Датчик освещённостиPiranha ULTRA
SA4
VVCC
GGND
Часы реального времениPiranha ULTRA
SCLSCL
SDASDA
VccVcc
GNDGnd

Теперь подключим дисплей с touchscreen:

ДисплейPiranha ULTRA
VCCVCC
GNDGND
D/CD2
RSTD3
CSD4
SCKD5
MOSID6
LEDD7
YUA0
XRA1
YDA2
XLA3

И финальным шагом подключим насос к внешнему питанию через уже подключенный к Piranha силовой ключ:

В результате у нас полючится вот такая система, в которую остаётся только загрузить скетч и вставить SD  карту, отформатированную в файловой системе FAT16 или FAT32:

Внимание: Вствляем microSD карту только после успешной загрузки скетча

Калибровка датчика:

Для калибровки датчика можно воспользоваться следующим скетчем. Необходимо экспериментальным путём определить минимальные и максимальные показания датчика и затем внести их в определения MY_SENSOR_MAX и MY_SENSOR_MIN в основном проекте. Так же калибровка необходима при изменении длинны провода датчика.

MY_SENSOR_MAX -  датчик сухой, в воздухе

MY_SENSOR_MIN - датчик по линию погружения в воде.

#define MOIST_SENSOR A5

void setup() {
  Serial.begin(9600);
  pinMode(MOIST_SENSOR, INPUT);
}

void loop() {
  int Sensor = analogRead(MOIST_SENSOR);
  Serial.println(Sensor);
  delay(500);
}

Скетч проекта:

// Подключаем библиотеки:
#include        <EEPROM.h>                                       // Подключаем библиотеку для работы с ПЗУ ардуино
#include        "UTFT.h"                                         // Подключаем библиотеку для работы с дисплеем
#include        "TouchScreen.h"                                  // Подключаем библиотеку для работы с TouchScreen
#include        <SPI.h>                                          // Подключаем библиотеку для работы с SPI
#include        <SD.h>                                           // Подключаем библиотеку для работы с SD картой
#include        <iarduino_RTC.h>                                 // Подключаем библиотеку для работы с часами реального времени
#define         myPumpHysteresis    1                            // часы, определяем гистерисис насоса (если насос сработал, он не будет работать независимо от показаний датчиков в течении этого времени)
// Определяем выводы датчиков и ключа насоса
#define         MOIST_SENSOR        A5                           // Ескостной датчик влажности
#define         LDR                 A4                           // Датчик освещенности
#define         PUMP                8                            // Вывод управления насосом
// определяем колибровочные значения датчика
#define         MY_SENSOR_MAX       629                          // максимальное "сырое" значение (датчик сухой)
#define         MY_SENSOR_MIN       326                          // минимальное "сырое" значение (датчик влажный)
// определяем подмену значений для меню
#define         my_backlight_timer  30                           // секунды, определяем время отключения подсветки
#define         mainMenu            0                            // Определяем номер главного меню
#define         durationMenu        1                            // Опредеояем номер меню установки насоса
#define         moistureMenu        2                            // Опредеояем номер меню установки влажности
#define         timeMenu            3                            // Опредеояем номер меню установки времени
#define         dateMenu            4                            // Опредеояем номер меню установки даты
#define         plotMenu            5                            // Опредеояем номер меню графика
#define         standBy             6                            // Опредеояем номер меню 
#define         SS                  10                           // Опредеояем номер меню
#define         draw                true                         // Определяем значение draw
#define         erase               false                        // Определяем значение erase
#define         plot_moisture_color VGA_AQUA                     // Определяем цвет кривой влажности
#define         plot_ldr_color      VGA_YELLOW                   // Определяем цвет кривой освещенности
#define         background_color    VGA_SILVER                   // Определяем цвет фона
#define         font_color          VGA_BLACK                    // Определяем цвет шрифта
#define         button_color        VGA_NAVY                     // Определяем цвет кнопки
#define         pump_color          VGA_RED                      // Определяем цвет графика мотора
#define         stopButtonColor     VGA_RED                      // Определяем цвет цвет кнопки "стоп"
//определяем кнопки
#define         up1                 1                            //
#define         down1               2                            //
#define         up2                 3                            //
#define         down2               4                            //
#define         up3                 5                            //
#define         down3               6                            //
#define         up4                 7                            //
#define         down4               8                            //
#define         back                9                            //
#define         date                10                           //
#define         moist_up            11                           //
#define         moist_down          12                           //
#define         mainDuration        13                           //
#define         mainMoisture        14                           //
#define         mainTime            15                           //
#define         mainPlot            16                           //
#define         plotNext            17                           //
#define         plotPrev            18                           //
// Определяем значения
#define         logInterval         276                          // Определяем интервал пробы датчиков (13 раз в час)
#define         height              240                          // Опрелеояем высоту дисплея
#define         width               320                          // Определяем ширину дисплея
#define         latest              0                            // Самый новый созданный файл для функции парсирования директории и меню графика
// Определяем ключевые слова для функции парсирования директории
#define         prev               -1                            // Предыдущий
#define         next                1                            // Следующий
// Определяем ключевые слова для функции меню графика
#define         oldest              2                            // самый старый файл
#define         between             1                            // между самым новым и самым старым
//
bool            mutex           =   false;                       // определяем взаимоисключение для SD.open()
uint16_t        currentFile     =   0;                           // итератор "указатель" на текущий файл
extern uint8_t  SmallFont[];                                     // подключаем шрифт имитирующий семисегментный индикатор
extern uint8_t  BigFont[];                                       // подключаем шрифт имитирующий семисегментный индикатор
extern uint8_t  SevenSegNumFont[];                               // подключаем шрифт имитирующий семисегментный индикатор
//  Определяем выводы используемые для управления дисплеем 2.8" TFT 320x240 UNO:
const uint8_t   RS              =   6;                           //
const uint8_t   WR              =   5;                           //
const uint8_t   CS              =   4;                           //
const uint8_t   RST             =   3;                           //
const uint8_t   SER             =   2;                           //
const uint8_t   LED             =   7;                           //
//  Определяем выводы используемые для чтения данных с TouchScreen:
const uint8_t   YP              =   A3;                          // Вывод Y+ должен быть подключен к аналоговому входу
const uint8_t   XM              =   A2;                          // Вывод X- должен быть подключен к аналоговому входу
const uint8_t   YM              =   A1;                          // Вывод Y-
const uint8_t   XP              =   A0;                          // Вывод X+
//  Определяем экстремумы для значений считываемых с аналоговых входов при определении точек нажатия на TouchScreen:
const int       tsMinX          =   100;                         // соответствующий точке начала координат по оси X
const int       tsMinY          =   120;                         // соответствующий точке начала координат по оси Y
const int       tsMaxX          =   950;                         // соответствующий максимальной точке координат по оси X
const int       tsMaxY          =   915;                         // соответствующий максимальной точке координат по оси Y
const int       minPress        =   10;                          // соответствующий минимальной степени нажатия на TouchScreen
const int       maxPress        =   1000;                        // соответствующий максимальной степени нажатия на TouchScreen
// переменные для хранения времени, датчика, длительности работы насоса
const int       duration_max    =   25;                          // максимальное время работы насоса
const int       duration_min    =   3;                           // минимальное время работы насоса
const int       sensor_moist_max=   MY_SENSOR_MAX;               // максимальное значение для датчика
const int       sensor_moist_min=   MY_SENSOR_MIN;               // минимальное значение для датчика
const int       set_moist_max   =   99;                          // максимальное значение установки влажности
const int       set_moist_min   =   1;                           // минимальное значение установки влажности
// массивы для вывода в график
uint8_t         plot_moist[314];                                 // массив для вывода лога датчика влажности 
uint16_t        plot_LDR[314];                                   // массив для вывода лога датчика освещенности
bool            pumpPlotFlag[312];                               // массив для вывода флага срабатывания насоса
// переменные насоса
bool            pump_stop       = false;                         // флаг нажатия на кнопку стоп
bool            halt_pump       = false;                         // флаг экстренной остановки
bool            pump_stopped    = false;                         // флаг остановленного насоса
bool            pump_flag       = false;                         // флаг сработавшего насоса
bool            pumpLogFlag     = false;                         // флаг записи в лог-файл
uint32_t        pumpStartMillis;                                 // миллисекунды старта насоса
uint32_t  pumpHysteresis  = myPumpHysteresis*60*60;              // гистерисис насоса
// структура для хранения имён файлов логов
struct files {                                                   //
String          Name            = "";                            // переменная для хранения названия файла
uint32_t        ints            = 0;                             // переменная для хранения даты файла
}               *sFiles;                                         //
// таймеры и настройки
String          realtime        = "";                            // переменная для хранения текущего времени      
int             moist           = 99;                            // переменная для хранения текущей влажности почвы
uint16_t        set_moist       = 78;                            // переменная для хранения влажности срабатывания насоса
uint8_t         duration        = 3;                             // переменная для хранения времени работы насоса
short           menuState       = 0;                             // состояние меню
bool            wakeFlag        = true;                          // флаг подсветки дисплея
bool            waitFlag        = false;                         // флаг выхода из режима ожидания
uint8_t         plotMenuDay     = latest;                        // переменная состояния меню графика
// таймеры:
unsigned long   backlightMillis;                                 // таймер подсветки
unsigned long   minuteMillis;                                    // таймер обновления минут в главном меню
unsigned long   moistureMillis;                                  // таймер обновления влажности в главном меню
unsigned long   logMillis;                                       // таймер записи лога
unsigned long   wakeFlagMillis;                                  // таймер выхода из режима ожидания
unsigned long   backlight_timer = my_backlight_timer * 1000;     // таймер подсветки
unsigned long   sec;                                             // таймер 
// UI
const int       plotStartX      = 3;                             // точка X началa графика
const int       plotStartY      = 114;                           // точка Y началa графика
const uint16_t  moist_x         = 170;                           // положение данных датчика влажности по оси Х
//  Создаём объекты библиотек:
iarduino_RTC time(RTC_DS1307);                                   // Создаём объект часов реального времени
UTFT            myGLCD(TFT01_24SP, RS, WR, CS, RST, SER);        // Создаём объект для работы с дисплеем
TouchScreen ts= TouchScreen(XP, YP, XM, YM);                     // Создаём объект для работы с TouchScreen
TSPoint         p;                                               // Создаём переменную для хранения координат и силы нажатия
void setup() {                                                   // 
  pinMode(LED, OUTPUT);                                          // Определяем вывод управления подсветкой в режим выход
  analogWrite(LED, 196);                                         // Определяем уровень ШИМ для подсаетки
  Serial.begin(9600);                                            // Инициируем передачу данных в монитор последовательного порта на скорости 9600 бит/сек
  while (!Serial);                                               // Ждём серийный протокол, если нужно
  if(!SD.begin(SS)){                                             // Если SD карта не инициировалась
    Serial.println("SD failed");                                 // Выводим текст в последовательный порт
    myGLCD.setFont(BigFont);                                     // Устанавливаем шрифт
    myGLCD.setColor(VGA_WHITE);                                  // Устанавливаем цвет шрифта
    myGLCD.setBackColor(VGA_BLACK);                              // Устанавливаем цвет фона
    myGLCD.print(("Isert SD card and restart") , 50, 120);       // Выводим текст нв дисплей
    while(true);                                                 // Скетч дальше не выполняется
  }                                                              //
  time.begin();                                                  // Инициируем работу с часами реального времени
  moist = readMoisture();                                        // Обновляем переменную влажности почвы
  realtime = time.gettime("H:i");                                // Обновляем часы реального времени
  myGLCD.InitLCD();                                              // Инициируем работу с TFT дисплеем
  myGLCD.clrScr();                                               // Отчищаем экран дисплея   
  myGLCD.setBackColor(background_color);                         // Устанавливаем цвет фона
  if (EEPROM.read(0) == 0xFF) saveSettings;                      // если первый байт ипрома не инициализирован, сохраняем настройки
  else readSettings();                                           // если нет - читаем настройки
  delay(100);                                                    //
  myGLCD.setColor(background_color);                             // Устанавливаем цвет фона
  myGLCD.fillRect(0,  0, width,  height);                        // заливаем экран
  readLog(plot_moist, plot_LDR, pumpPlotFlag, parseRoot(latest));// читаем файл лога
  delay(100);
  drawMainMenu(draw);                                            // рисуем главное меню
}                                                      
void loop() {
//Выводим время на главный экран -------------------------------
  // если прошло 60 секунд
  if(millis() - minuteMillis > 60000 && menuState == mainMenu){ 
    realtime = time.gettime("H:i");                              // записываем текущее время в переменную
    myGLCD.print(realtime , 10, 35);                             // выводим переменную в главное меню
  if (time.Hours == 0 && time.minutes == 0){                     // если полночь
      drawMainMenu(erase);                                       // стираем главное меню (обновляем график и дату)
      zero_out();                                                // обнуляем массивы графика
      drawMainMenu(draw);                                        // рисуем главное меню и пустой график
    }
    minuteMillis = millis();                                     // сбрасываем таймер минут
  }
  if (millis() - backlightMillis > backlight_timer && wakeFlag){ // если пришло время выключить подсветку и она включена
    lightOff();                                                  // выключаем подсветку
    backlightMillis = millis();                                  // обновляем таймер
  }
  //Обновляем влажность в главном меню каждую секунду;
  if(millis() - moistureMillis > 1000 && menuState == mainMenu){ //
    drawMoisture(moist = readMoisture());                        // выводим текущую влажность
    moistureMillis = millis();                                   // обновляем таймер
  } 
//Насос здесь---------------------------------------------------
  //если влажность ниже установленной и насос ещё не сработал и насос не остановлен вручную
  if (moist < set_moist && !pump_flag && !halt_pump){
    pump_stopped = false;                                        // флаг остановки насоса
    drawPumpButton(erase);                                       // стираем кнопку установок насоса
    drawStopButton(draw);                                        // рисуем кнопку остановки насоса
    pump_flag = true;                                            // флаг работающего насоса
    pumpLogFlag = true;                                          // флаг для записи в файл лога
    pumpStartMillis = millis();                                  // обновляем таймер старта насоса
    pinMode(PUMP, OUTPUT);                                       // переводим вывод управления насосом в режим выход
    digitalWrite(PUMP, HIGH);                                    // устанавливаем вывод насоса в режим логической единицы
  }
  // если время работы насоса равно уставленному и насос работает и не остановлен или нажата кнопка стоп
  if(((millis() - pumpStartMillis > duration*1000) && pump_flag && !pump_stopped) || pump_stop){
    pinMode(PUMP, INPUT);                                        // переводим вывод управления насосом в режим вход
      drawStopButton(erase);                                     // стираем кнопку стоп
      drawPumpButton(draw);                                      // рисуем кнопку установок насоса
      pump_stopped = true;                                       // насос остановлен
      if(pump_stop){                                             // если нажата кнопка стоп
        pump_flag = false;                                       // сбрасываем флаг насоса
        pump_stop = false;                                       // сбрасываем флаг ручной остановки
        halt_pump = true;                                        // насос в режиме ручной остановки
      }
  }
  // ручная остановка сбрасывается, если влажность почвы стала выше установленной
  if (moist > set_moist) halt_pump = false;
  // сбрасываем флаг насоса, если прошло время гистерисиса
  if(pump_flag && !pumpHysteresis){                              // 
    pump_flag = false;                                           // Сбрасывем флаг насоса
    pumpHysteresis = myPumpHysteresis*3600;                      // Устанавливаем значение гистерисиса
  }
//* Конец насоса -----------------------------------------------
// Логгинг здесь -----------------------------------------------
  if((millis() - logMillis) > 1000){                             // Если прошла секунда
    time.gettime();                                              // Вызываем часы реального времени
    sec = time.Hours*3600 + time.minutes*60 + time.seconds;      // Рассчитываем секунды от полуночи
    logMillis = millis();                                        // обновляем таймер
    if (pump_flag) pumpHysteresis--;                             // Если нассос сработал, декрементируем таймер гистерисиса
  }
  if (!(sec % logInterval) && !mutex) {                          // если подошёл заданный интервал времени
    String file = time.gettime("d-m-y");                         // создаём строку "день-месяц-год"
    file += ".csv";                                              // добавляем к ней расширение .csv
    saveLog(file);                                               // сохраняем в файл с названием из переменной
    if (menuState == mainMenu) updatePlot();                     // если в главном меню, обновляем график
  }
//* Конец логгинга---------------------------------------------- 
// Пользлвательский интерфейс здесь----------------------------- 
  p = ts.getPoint();                                             // Считываем координаты и интенсивность нажатия на TouchScreen в структуру p
  //  Возвращаем режим работы выводов:
  pinMode(XM, OUTPUT);                                           // Возвращаем режим работы вывода X- в значение «выход» для работы с дисплеем
  pinMode(YP, OUTPUT);                                           // Возвращаем режим работы вывода Y+ в значение «выход» для работы с дисплеем
  if(millis() - wakeFlagMillis > 100 && wakeFlag && waitFlag){   // Ожидаем 100 миллисекунд после включения подсветки
   waitFlag = false;                                             // Сбрасывем флаг ожидания
  }  
  if (p.z > minPress && p.z < maxPress && !wakeFlag){            // Если степень нажатия достаточна для фиксации координат TouchScreen и подсветка выключена
    waitFlag = true;                                             // Устанавливаем флаг ожидания
    wakeFlagMillis = millis();                                   // Обновляем таймер ожидания
    backlightMillis = millis();                                  // Обновляем таймер подсветки
    lightOn();                                                   // Включаем подсветку
  }
  if(p.z > minPress && p.z < maxPress && !waitFlag && wakeFlag){ // Если степень нажатия достаточна для фиксации координат TouchScreen, подсветка включена и мы не ждем после её включения
    backlightMillis = millis();                                  // Обновляем таймер подсветки
    p.x = map(p.x, tsMinX, tsMaxX, 0, 320);                      // Преобразуем значение p.x от диапазона tsMinX...tsMaxX, к диапазону 0...320
    p.y = map(p.y, tsMinY, tsMaxY, 0, 240);                      // Преобразуем значение p.y от диапазона tsMinY...tsMaxY, к диапазону 0...240
    switch(menuState){                                           // проверяем состояние меню
// ------------ГЛАВНОЕ МЕНЮ------------ //
      case mainMenu: mainMenuFunc(); break;                      
// -------МЕНЮ УСТАНОВОК ВРЕМЕНИ------- //    
      case timeMenu: timeMenuFunc(); break;
// -------МЕНЮ УСТАНОВОК ДАТЫ---------- //     
      case dateMenu: dateMenuFunc(); break;   
// ------МЕНЮ УСТАНОВОК ВЛАЖНОСТИ------ //  
      case moistureMenu: moistureMenuFunc(); break;
// ------МЕНЮ УСТАНОВОК ДЛИТЕЛЬНОСТИ----//
      case durationMenu: durationMenuFunc(); break;
// -------------МЕНЮ ГРАФИКА-----------//
      case plotMenu: plotMenuFunc(); break;
    default: break;
    }
  }
 // Конец пользовательского интерфейса 
}
//Функция прорисовки главного меню
void drawMainMenu(bool draw_erase){                             // принимаемые значения: true - рисуем, false - стираем
  myGLCD.setFont(SmallFont);                                    // устанавливаем шрифт
  // рисуем? да: цвет = цвет шрифта, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(font_color) : myGLCD.setColor(background_color); 
  myGLCD.print("Time, date:" , 10, 10);                         // выводим текст на экран
  myGLCD.print("Moisture:" , 125, 10);                          // выводим текст на экран
  myGLCD.print("Duration:" , 225, 10);                          // выводим текст на экран
  myGLCD.setFont(BigFont);                                      // устанавливаем шрифт
  myGLCD.print(realtime , 10, 35);                              // выводим время на экран
  String m = String(time.month);                                // записываем текущий месяц в переменную
  if (m.toInt() < 10) m = "0" + m;                              // добавляем ноль, если меньше 10
  String d = String(time.day);                                  // записываем текущий день в переменную
  if (d.toInt() < 10) d = "0" + d;                              // добавляем ноль, если меньше 10
  myGLCD.print(d + "/" + m , 10, 60);                           // выводим дату на экран
  myGLCD.setFont(SmallFont);                                    // устанавливаем шрифт
  myGLCD.print("Cur:", moist_x-32, 39);                         // выводим текст на экран
  myGLCD.print("Set:", moist_x-32, 64);                         // выводим текст на экран
  myGLCD.setFont(BigFont);                                      // устанавливаем шрифт
  drawMoisture(readMoisture());                                 // выводим показания датчика влажности на экран
  myGLCD.print(String(set_moist) , moist_x, 60);                // выводим установку влажности на экран
  drawMenuButton(2,4, draw_erase);                              // рисуем/стираем кнопку установок времени
  drawMenuButton(107,4, draw_erase);                            // рисуем/стираем кнопку установок 
  drawPumpButton(draw_erase);                                   // рисуем/стираем кнопку установок 
  plot(draw_erase);                                             // рисуем/стираем график
}
//Функция прорисовки графика
void plot(bool draw_erase){
  // рисуем? да: цвет = цвет шрифта, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(font_color) : myGLCD.setColor(background_color);
  myGLCD.setFont(SmallFont);                                    // устанавливаем шрифт
  for (int i = 0; i < 24; i+=2){                                //
    myGLCD.print(String(i) , plotStartX+5+i*13, 100);           // выводим часы графика от 0 до 22
  }
  myGLCD.print("Plot:" , plotStartX+2, 100-13);                 // выводим текст на экран
  myGLCD.print("Moisture" , plotStartX+7*8, 100-13);            // выводим текст на экран
  myGLCD.print("Sunlight" , plotStartX+7*8+10*8, 100-13);       // выводим текст на экран
  myGLCD.print("Pump" , plotStartX+7*8+10*8+10*8, 100-13);      // выводим текст на экран
  // рисуем? да: цвет графика = цвет графика влажности, нет: цвет графика = цвет фона
  draw_erase ? myGLCD.setColor(plot_moisture_color) : myGLCD.setColor(background_color);
  myGLCD.print("-" , plotStartX+6*8, 100-13);                   // выводим текст на экран
  // рисуем? да: цвет = цвет графика освещенности, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(plot_ldr_color) : myGLCD.setColor(background_color);
  myGLCD.print("-" , plotStartX+6*8+10*8, 100-13);              // выводим текст на экран
  // рисуем? да: цвет = цвет графика насоса, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(pump_color) : myGLCD.setColor(background_color);
  myGLCD.print("*" , plotStartX+6*8+10*8+10*8, 100-13);         // выводим текст на экран
  // рисуем? да: цвет = цвет кнопки, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(button_color) : myGLCD.setColor(background_color);
  for (int i = 12; i < 120; i += 12){                           //                                                      
    myGLCD.drawLine(plotStartX, 235 - i, 314, 235 - i);         // рисуем сетку Y  
  }
  for (int i = plotStartX; i < 312; i += 13){                   //
    myGLCD.drawLine(i, plotStartY, i, 235);                     // рисуем сетку X
  }
  for (int x = plotStartX; x < 314; x++) {
    int newY = map(plot_LDR[x-plotStartX], 0, 1024, 0, 115);    // определяем точку Y датчика освещённости
    int newY2 = map(plot_LDR[x-(plotStartX-1)],0,1024,0,115);   // определяем точку Y2 датчика освещённости
    // рисуем? да: цвет = цвет графика освещенности, нет: цвет = цвет фона
    draw_erase ? myGLCD.setColor(plot_ldr_color) : myGLCD.setColor(background_color);
    if(/*abs(newY - newY2) > 3 && */newY > 0 && newY2 > 0)      // если показания датчика не ноль
      myGLCD.drawLine(x, 235 - newY, x+1, 235 - newY2);         // рисуем линию от Y до Y2
    //else myGLCD.drawPixel(x, 235 - newY);                     
    newY = map(plot_moist[x-plotStartX], 0, 100, 0, 115);       // определяем точку Y датчика влажности
    newY2 = map(plot_moist[x-(plotStartX-1)], 0, 100, 0, 115);  // определяем точку Y2 датчика влажности
    // рисуем? да: цвет = цвет графика влажности, нет: цвет = цвет фона
    draw_erase ? myGLCD.setColor(plot_moisture_color) : myGLCD.setColor(background_color);
    if(/*abs(newY - newY2) > 3 && */newY > 0 && newY2 > 0)      // если показания датчика не ноль
      myGLCD.drawLine(x, 235 - newY, x+1, 235 - newY2);         // рисуем линию от Y до Y2
    //myGLCD.drawPixel(x, 235 - newY);                            // выводим точку на экран
    // рисуем? да: цвет графика = цвет графика насоса, нет: цвет графика = цвет фона 
    draw_erase ? myGLCD.setColor(pump_color) : myGLCD.setColor(background_color);
    if(pumpPlotFlag[x-plotStartX])myGLCD.fillCircle(x,235-newY,2);                //рисуем круг, если сработал насос
  }
  // рисуем? да: цвет = цвет кнопки, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(button_color) : myGLCD.setColor(background_color); 
  myGLCD.drawRoundRect(plotStartX,plotStartY,314,235);          // рисуем рамку графика
  myGLCD.drawRoundRect(plotStartX-1,plotStartY-1,315,236);      // рисуем рамку графика
  myGLCD.setColor(font_color);                                  // устанавливаем цвет обратно для шрифта
  myGLCD.setFont(BigFont);                                      // устнанавливаем шрифт
}
//функция прорисовки меню установок длительности работы насоса
void drawDurationMenu(bool draw_erase){                         // рисуем меню установок времени работы насоса
  uint8_t offset = 20;                                          // общий сдвег элементов меню
  // рисуем? да: цвет = цвет шрифта, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(font_color) : myGLCD.setColor(background_color);
  myGLCD.setFont(SmallFont);                                    // устанавливаем шрифт
  myGLCD.print("Pump duration menu", 110, 20);                  // выводим текст на экран
  myGLCD.setFont(BigFont);                                      // устанавливаем шрифт
  myGLCD.print("Sec", 200, 144);                                // выводим текст на экран
  drawSmallButton(5,5,"<-back");                                // вызываем функцию прорисовки кнопки
  drawUpButton(offset + 110, 50);                               // рисуем кнопку вверх
  drawDownButton(offset + 110, 150);                            // рисуем кнопку вниз
  drawDigits(String(duration), offset + 110, 97);
}
//функция прорисовки меню установок влажности срабатывания насоса
void drawMoistureMenu(bool draw_erase){
  uint8_t offset = 20;
  // рисуем? да: цвет = цвет шрифта, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(font_color) : myGLCD.setColor(background_color);
  drawSmallButton(5,5,"<-back");                                // вызываем функцию прорисовки кнопки "назад"
  myGLCD.setFont(SmallFont);                                    // устанавливаем шрифт
  myGLCD.print("Set moisture pump", 110, 5);                    // выводим текст на экран
  myGLCD.print("activation menu",110, 17);                      // выводим текст на экран
  drawUpButton(offset + 110, 50);                               // рисуем кнопку вверх
  drawDownButton(offset + 110, 150);                            // рисуем кнопку вниз
  drawDigits(String(set_moist), offset + 110, 97);              // выводим текст на дисплей
}
//функция прорисовки меню установок времени -------------------------------------------------------------------
void drawTimeMenu(bool draw_erase){     
  int offsetY = 80;                                             // общий сдвег элементов меню
  // рисуем? да: цвет = цвет шрифта, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(font_color) : myGLCD.setColor(background_color);
  drawSmallButton(5,5,"<-back");                                // вызываем функцию прорисовки кнопки
  drawSmallButton(312-95,5,"date->");                           // вызываем функцию прорисовки кнопки
  drawUpButton(80, offsetY);                                    // рисуем кнопку вверх
  drawDownButton(80, offsetY+100);                              // рисуем кнопку вниз
  drawUpButton(160, offsetY);                                   // рисуем кнопку вверх
  drawDownButton(160, offsetY+100);                             // рисуем кнопку вниз
  myGLCD.fillCircle(73+75, offsetY+62, 4);                      // двоеточие
  myGLCD.fillCircle(73+75, offsetY+82, 4);                      // двоеточие
  drawDigits(String(time.Hours), 80, offsetY+47);               // минуты
  drawDigits(String(time.minutes), 80 + 80, offsetY+47);        // число
}
//рисуем или стираем меню установок даты --------------------------------------------------------------------
void drawDateMenu(bool draw_erase){
  int offsetY = 80;                                             // сдвиг элементов меню по оси Y
  // рисуем? да: цвет = цвет шрифта, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(font_color) : myGLCD.setColor(background_color);
  drawSmallButton(5,5,"<-back");                                // вызываем функцию прорисовки малой кнопки
  drawUpButton(5, offsetY);                                     // рисуем кнопку вверх
  drawDownButton(5, offsetY+100);                               // рисуем кнопку вниз
  drawUpButton(90, offsetY);                                    // рисуем кнопку вверх
  drawDownButton(90, offsetY+100);                              // рисуем кнопку вниз
  drawUpButton(240, offsetY);                                   // рисуем кнопку вверх
  drawDownButton(240, offsetY+100);                             // рисуем кнопку вниз
  drawDigits(String(time.day), 5, offsetY+47);                  // выводим день
  drawDigits(String(time.month), 90, offsetY+47);               // выводим месяц
  drawDigits(String(20), 180, offsetY+47);                      // выводим текст на дисплей
  myGLCD.drawLine(71, offsetY+47+45, 84, offsetY+50);           // рисуем косую черту
  myGLCD.drawLine(72, offsetY+47+45, 85, offsetY+50);           // рисуем косую черту
  myGLCD.drawLine(73, offsetY+47+45, 86, offsetY+50);           // рисуем косую черту
  drawDigits(String(time.year), 240, offsetY+47);               // выводим год

}
// функция прорисовки меню графика
void drawPlotMenu(bool draw_erase){
  // рисуем? да: цвет шрифта = цвет шрифта, нет: цвет шрифта = цвет фона
  draw_erase ? myGLCD.setColor(font_color) : myGLCD.setColor(background_color);
  myGLCD.setFont(SmallFont);                                    // устнанавливаем шрифт
  myGLCD.print(sFiles[currentFile].Name, 120, 50);              // Выводим название файла на дисплей
  drawSmallButton(108, 5, "home");                              // рисуем кнопку "домой"
  // если самый старый файл - стираем кнопку
  if (plotMenuDay == oldest) myGLCD.setColor(background_color);
  drawSmallButton(5, 5, "<-prev");                              // рисуем кнопку
  // рисуем? да: цвет = цвет шрифта, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(font_color) : myGLCD.setColor(background_color);
  // если самый новый файл - стираем кнопку
  if (plotMenuDay == latest) myGLCD.setColor(background_color);
  drawSmallButton(width-100, 5, "next->");                      // рисуем кнопку
  plot(draw_erase);                                             // рисуем график датчиков
}
// функция включения подсветки
void lightOn(){
  analogWrite(LED, 196);                                        // устанавливаем значение ШИМ на выводе подсветки
  wakeFlag = true;                                              // устанавливаем флаг подсветки
}
// функция выключения подсветки
void lightOff(){
  digitalWrite(LED, LOW);                                       // устанавливаем логический ноль на выводе подсветки 
  wakeFlag = false;                                             // сбрасываем флаг подсветки
}
// функция вывода текущей влажности
void drawMoisture(int moist){
  if (moist < 10){                                              // если число до 10
    myGLCD.print(" " + String(moist) , moist_x, 35);            // выводим пробел
  } else myGLCD.print(String(moist) , moist_x, 35);             // выводим показания датчика
}
// функция прорисовки цифр
void drawDigits(String digits, int x, int y){
  myGLCD.setFont(SevenSegNumFont);                              // устанавливаем шрифт
    if (digits.toInt() < 10){                                   // если одна цифра
        myGLCD.print("0" + digits, x, y);                       // добавляем ноль и выводим на дисплей
    } 
    else myGLCD.print(digits, x, y);                            // выводим цифры на дисплей
}
// функция прорисовки кнопки главного меню
void drawMenuButton(uint16_t x, uint16_t y, bool draw_erase){
  // рисуем? да: цвет = цвет кнопки, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(button_color) : myGLCD.setColor(background_color);
  myGLCD.drawRoundRect(x,y,x+101,y+82);                         // выводим прямоугольник
  myGLCD.drawRoundRect(x+1,y+1,x+100,y+81);                     // выводим прямоугольник
  // рисуем? да: цвет = цвет кнопки, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(VGA_WHITE) : myGLCD.setColor(background_color);
  myGLCD.drawLine(x+1,y+3,x+1,y+80);                            // выводим линию
  myGLCD.drawLine(x+3,y+1,x+98,y+1);                            // выводим линию
  // рисуем? да: цвет = цвет шрифта, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(font_color) : myGLCD.setColor(background_color);
}
// функция прорисовки малой кнопки
void drawSmallButton(int x, int y, String bName){
  myGLCD.drawRoundRect(x, y, x+95, y+40);                       // выводим прямоугольник
  myGLCD.drawRoundRect(x+1, y+1, x+94, y+39);                   // выводим прямоугольник
  myGLCD.setFont(SmallFont);                                    // устанавливаем шрифт
  myGLCD.print(bName, x+20, y+15);                              // выводим название кнопки
}
// функция прорисовки кнопки вверх
void drawUpButton(int x, int y) {
  myGLCD.drawRoundRect(x, y, x+65, y+45);                       // выводим прямоугольник
  myGLCD.drawRoundRect(x-1, y-1, x+66, y+46);                   // выводим прямоугольник
  myGLCD.setFont(BigFont);                                      // устанавливаем шрифт
  myGLCD.print("UP", x+20, y+15);                               // выводим название кнопки
}
// функция прорисовки кнопки вниз
void drawDownButton(int x, int y){
  myGLCD.drawRoundRect(x, y, x+65, y+45);                       // выводим прямоугольник
  myGLCD.drawRoundRect(x-1, y-1, x+66, y+46);                   // выводим прямоугольник
  myGLCD.print("DOWN", x+1, y+15);                              // выводим название кнопки          
}
// функция чтения файла лога
void readLog(uint8_t* mos_val, uint16_t* ldr_val, bool* pump_log_flag, String logFile) {
  zero_out();                                                   // обнуляем массивы графика
  mutex = true;                                                 // взаимоисключение для SD.open
  File myFile = SD.open(logFile);                               // открываем файл с названием logFile
  String value = "";                                            // создаем пустую переменную строки
  byte Byte;                                                    // создаем переменную для чтения из байта из файла
  uint8_t time_flag = 0;                                        // создаем переменную флагов(часы - флаг=0, минуты - флаг=1 секунды - флаг=2)
  uint8_t log_hour, log_minute, log_second;                     // создаем переменные для хранения часов, минут, секунд
  uint32_t log_time;                                            // создаем переменную для вычисления времени взятия пробы в секундах
  while(myFile.available()){                                    // Пока файл не закончился
    Byte = myFile.read();                                       // Читаем байты в переменную
    if(Byte == '\n') {                                          // Если символ новой строки
      myFile.seek(myFile.position() - 23);                      // Отматываем на 23 байта назад
      while((Byte = myFile.read())!=','){                       // Пока не символ запятой
        if (Byte != ':'){                                       // Если не двоеточие
          value +=char(Byte);                                   // Накапливаем в строку байты
        } else {                                                // Если двоеточие
          switch(time_flag){                                    // проверяем переменную флагов времени
            //если флаг равен нулю - записываем час, устанавливаем флаг на минуты
            case 0: log_hour = value.toInt(); time_flag = 1; value = ""; break;
            //если флаг равен единице - записываем минуты, устанавливаем флаг на секунды
            case 1: log_minute = value.toInt(); time_flag = 2; value = ""; break;
            default: value = ""; break;
          }
        }          
      }
      log_second = value.toInt();                               // заисывем секунды
      //вичисляем время взятия пробы в секундах от 00:00
      log_time = (uint32_t)log_hour*3600 + log_minute*60 + log_second;
      int elem = map(log_time, 0, 86399, 0, 311);               // преобразуем значения секунд к элементам массива
      value = "";                                               // обнуляем строку
      time_flag = 0;                                            // обнуляем флаг
      while((Byte = myFile.read())!=','){                       // пока не символ запятой
        value += char(Byte);                                    // накапливаем в строку
      }                                                         //
      mos_val[elem] = value.toInt();                            // записываем показания влажности в элемент массива
      value = "";                                               // обнуляем строку
      while((Byte = myFile.read())!=','){                       // пока не запятая
        value += char(Byte);                                    // накапливаем в строку
      }
      ldr_val[elem] = value.toInt();                            // записываем показания освещенности в элемент массива 
      value = "";                                               // обнуляем строку
      myFile.read();                                            // считываем байт
      Byte = myFile.read();                                     // записываем байт
      char c = char(Byte);                                      // записываем байт как символ
      int i = c - '0';                                          // приводим символ к значению целого числа
      pump_log_flag[elem] = bool(i);                            // записываем значение в массив
      while((Byte = myFile.read())!='\n');                      // ждём конца строки
    }    
  }
  myFile.close();                                               // закрываем файл
  mutex = false;                                                // сбрасывем взаимоисключение для SD.open
}
// функция чтения датчика влажности
int readMoisture() {
  pinMode(MOIST_SENSOR, INPUT);                                 // Устанавливаем вывод датчика в режим входа
  uint16_t fluct = 0;                                           // Создаём переменную для суммы 10 считываений с датчика
  for (int i = 0; i < 10; i++){                                 // Считываем с датчика 10 раз
    fluct += analogRead(MOIST_SENSOR);                          // при жтом суммируя показания в переменную
  }                                                             //
  fluct /= 10;                                                  // Делим переменную на 10 для вычисления среднего арифметического значения
  // приводим показания датчки к диапазону от 0 до 100
  uint8_t m = map(fluct, sensor_moist_max, sensor_moist_min, 0, 100);
  if (m >= 100) m = 99;                                         // ограничиваем верхнее показание датчика
  return m;                                                     // возвращаем значение
}
// функция чтения настроек
void readSettings(){                                            //
  duration = EEPROM.read(0);                                    // читаем значение настроек из ПЗУ
  set_moist = EEPROM.read(sizeof(duration));                    // читаем значение настроек из ПЗУ
}
// функция сохранения настроек
void saveSettings(){                                            //
   EEPROM.update(0, duration);                                  // записываем значение настроек в ПЗУ
   EEPROM.update(sizeof(duration), set_moist);                  // записываем значение настроек в ПЗУ
}
// функция сохранения файла лога
void saveLog(String fileName){                 
    mutex = true;                                               // устанавливаем взаимоисключение для SD.open()
    String REAL_TIME = time.gettime("d-m-Y, H:i:s");            // записываем значение, считанное с часов реального времени, в переменную REAL_TIME
    int ldr = analogRead(LDR);                                  // записываем показания датчика освещённости
    String ldr_str = String(ldr);                               // конвертируем в строку
    if (ldr <   10) ldr_str = "0" + ldr_str;                    // добавляем ноль
    if (ldr <  100) ldr_str = "0" + ldr_str;                    // добавляем ноль
    if (ldr < 1000) ldr_str = "0" + ldr_str;                    // добавляем ноль
    File myFile = SD.open(fileName, FILE_WRITE);                // создаём или открываем файл "d-m-y.csv" для записи значений
    myFile.print(REAL_TIME);                                    // записываем переменную в файл
    myFile.print(", ");                                         // записываем разделительный символ
    myFile.print(moist);                                        // записываем переменную в файл
    myFile.print(", ");                                         // записываем разделительный символ
    myFile.print(ldr_str);                                      // записываем переменную в файл
    myFile.print(", ");                                         // записываем разделительный символ
    myFile.println(pumpLogFlag);                                // записываем переменную в файл
    myFile.flush();                                             // записываем буфер в файл
    myFile.close();                                             // закрываем файл
    pumpLogFlag = false;                                        // сбрасывем флаг насоса
    mutex = false;                                              // сбрасываем взщаимоисключение для SD.open()
}
// функция нажатия кнопок
bool buttonPress(int button) {
  uint8_t offset = 20;
  switch (button) {
    case back: if (p.x < 100 && p.y < 45) return true; else return false;
    case date: if (p.x > 217 && p.y < 45) return true; else return false;
    case moist_up: if (p.x > 110 + offset && p.x < 205 + offset && p.y > 50 && p.y < 95) return true; else return false;
    case moist_down: if (p.x > 110 + offset && p.x < 205 + offset && p.y > 150 && p.y < 200) return true; else return false;
    case up1: if (p.x > 5 && p.x < 50 && p.y > 80 && p.y < 125) return true; else return false;
    case up2: if (p.x > 80 && p.x < 80+65 && p.y > 80 && p.y < 125) return true; else return false;
    case up3: if (p.x > 160 && p.x < 160+65 && p.y > 80 && p.y < 125) return true; else return false;
    case up4: if (p.x > 240 && p.x < 240+65 && p.y > 80 && p.y < 125) return true; else return false;
    case down1: if (p.x > 5 && p.x < 50 && p.y > 180 && p.y < 225) return true; else return false;
    case down2: if (p.x > 80 && p.x < 80+65 && p.y > 180 && p.y < 225) return true; else return false;
    case down3: if (p.x > 160 && p.x < 160+65 && p.y > 180 && p.y < 225) return true; else return false;
    case down4: if (p.x > 240 && p.x < 240+65 && p.y > 180 && p.y < 225) return true; else return false;
    case mainDuration: if (p.x > 210 && p.y < 90) return true; else return false;
    case mainMoisture: if (p.x > 120 && p.x < 210 && p.y < 90) return true; else return false;
    case mainTime: if (p.x > 5 && p.x < 120 && p.y < 90) return true; else return false;
    case mainPlot: if (p.x > 5 && p.y > 120) return true; else return false;
    case plotPrev: if (p.x < 100 && p.y < 45) return true; else return false;
    case plotNext: if (p.x > width-45-95 && p.y < 45) return true; else return false;
    default: return false;
  }
}
// функция меню даты
void dateMenuFunc(){
  int8_t d = time.day;                                          // записываем текущий день в переменную
  int8_t m = time.month;                                        // записываем текущий месяц в переменную
  int8_t y = time.year;                                         // записываем текущий год в переменную
  if (buttonPress(back)){                                       // если нажата кнопка назад
    drawDateMenu(erase);                                        // стираем меню настроек
    drawTimeMenu(draw);                                         // рисуем главное меню
    menuState = timeMenu;                                       // меняем переменную состояния меню      
  } else if (buttonPress(up1)) {                                // если нажата кнопка день вверх
    d++;                                                        // приращиваем день
    switch (m){                                                 // проверяем кол-во дней в месяце
      case 1:                                                   //
      case 3:                                                   //
      case 5:                                                   //
      case 7:                                                   //
      case 8:                                                   //
      case 10:                                                  //
      case 12: if(d == 32) d = 1; break;                        // если январь, март, май, июль, август... ограничиваем до 31         
      case 4:                                                   //
      case 6:                                                   //
      case 9:                                                   //
      case 11: if(d == 31) d = 1; break;                        // если февраль, апрель, июнь... ограничиваем до 30
      case 2: if(d == 28) d = 1; break;                         // TODO: здесь можно добавить високосный год (year%4 ==0 && (year % 100 != 0 || year % 400 == 0))
      default: break;
    }
    time.settime(-1, -1, -1, d);                                //
    drawDigits(String(time.day), 5, 80+47);                     //
  } else if (buttonPress(down1)) {                              // если нажата кнопка день вниз       
    d--;                                                        //
    switch (m){                                                 // проверяем кол-во дней в месяце
      case 1:                                                   //
      case 3:                                                   //
      case 5:                                                   //
      case 7:                                                   //
      case 8:                                                   //
      case 10:                                                  //
      case 12: if(d == 0) d = 31; break;                        // если январь, март, май, июль, август... ограничиваем до 31        
      case 4:                                                   //
      case 6:                                                   //
      case 9:                                                   //
      case 11: if(d == 0) d = 30; break;                        // если февраль, апрель, июнь... ограничиваем до 30
      case 2: if(d == 0) d = 28; break;                         // TODO: здесь можно добавить високосный год (year%4 ==0 && (year % 100 != 0 || year % 400 == 0))
      default: break;
    }
    time.settime(-1, -1, -1, d);
    drawDigits(String(time.day), 5, 80+47);
  } else if (buttonPress(up2)) {                                // если кнопка месяц вверх
    m++;                                                        // приращиваем месяц
    if (m == 13) m = 1;                                         // ограничиваем месяцы до 12
    time.settime(-1, -1, -1, -1, m);                            // устанавливаем месяц в часы реального времени
    drawDigits(String(time.month), 90, 80+47);                  // выводим текст на экран
  } else if (buttonPress(down2)){                               // кнопка месяц вниз       
    m--;                                                        // уменьшаем месяц
    if (m == 0) m = 12;                                         // ограничиваем месяцы до 1
    time.settime(-1, -1, -1, -1, m);                            // устанавливаем месяц в часы реального времени
    drawDigits(String(time.month), 90, 80+47);                  // выводим текст на экран
  } else if (buttonPress(up4)){                                 // если кнопка год вверх
    y++;                                                        // приращиваем год
    if (y == 100) y = 0;                                        // ограничиваем год до 2099
    time.settime(-1, -1, -1, -1, -1, y);                        // устанавливаем год в часы реального времени
    drawDigits(String(time.year), 240, 80+47);                  // выводим текст на экран
  } else if (buttonPress(down4)){                               // кнопка год вниз
    y--;                                                        // уменьшаем год
    if (m == -1) m = 99;                                        // оганичиваем до 2000
    time.settime(-1, -1, -1, -1, -1, y);                        // устанавливаем год в часы реального времени
    drawDigits(String(time.year), 240, 80+47);                  // выводим текст на экран
  }
}
// функция меню длительности работы насоса
void durationMenuFunc() {
  if (buttonPress(back)) {                                      // если нажата кнопка назад
    drawDurationMenu(erase);                                    // стираем текущее меню
    drawMainMenu(draw);                                         // рисуем главное меню
    menuState = mainMenu;                                       // обновляем переменную состояний меню
    saveSettings();                                             // сохраняем настройки
  } else if (buttonPress(moist_up)) {                           // если кнопка вверх 
    if (duration < duration_max) duration++;                    // приращиваем переменную
    drawDigits(String(duration), 110+20, 97);                   // обновляем её на жкране
  } else if (buttonPress(moist_down)) {                         // если кнопка вних
    if (duration > duration_min) duration--;                    // уменьшаем перменную
    drawDigits(String(duration), 110+20, 97);                   // обновляем её на экране
  }
}
// функция меню установок влажности
void moistureMenuFunc() {
  if (buttonPress(back)) {                                      // если нажата кнопка назад
    drawMoistureMenu(erase);                                    // стираем текущее меню
    drawMainMenu(draw);                                         // рисуем главное меню
    menuState = mainMenu;                                       // обновляем переменную состояний меню
    saveSettings();                                             // сохраняем настройки
  } else if (buttonPress(moist_up)) {                           // если кнопка вверх  
  if (set_moist < set_moist_max) set_moist++;                   // приращиваем переменную 
    drawDigits(String(set_moist), 110+20, 97);                  // обновляем её на жкране
  } else if (buttonPress(moist_down)){                          // если кнопка вних 
  if (set_moist > set_moist_min) set_moist--;                   // уменьшаем перменную
    drawDigits(String(set_moist), 110+20, 97);                  // обновляем её на экране
  }
}
// функция меню установок часов реального времени
void timeMenuFunc(){
  int8_t H = time.Hours;                                        // создаём переменную и записываем текущие часы
  int8_t i = time.minutes;                                      // создаём переменную и записываем текущие минуты 
  if (buttonPress(back)){                                       // если нажата кнопка назад
    realtime = time.gettime("H:i");                             // обновляем переменную часов реального времени 
    drawTimeMenu(erase);                                        // стираем текущее меню         
    drawMainMenu(draw);                                         // рисуем главное меню
    menuState = mainMenu;                                       // обновляем переменную состояний меню
  } else if (buttonPress(date)){                                // если нажата кнопка даты
    time.settime(-1, i, H);                                     // записываем время 
    drawTimeMenu(erase);                                        // стираем текущее меню
    drawDateMenu(draw);                                         // рисуем главное меню
    menuState = dateMenu;                                       // обновляем переменную состояний меню
  } else if(buttonPress(up2)){                                  // если нажата кнопка часы вверх
    H++;                                                        // прибавляем часы
    if (H == 24) H = 0;                                         // ограничиваем часы до 23
    time.settime(-1, -1, H);                                    // устанавливаем новое время
    drawDigits(String(time.Hours), 80, 80+47);                  // выводим на экран
  } else if(buttonPress(down2)){                                // если нажата кнопка часы вниз
    H--;                                                        // убавляем часы
    if (H == -1) H = 23;                                        // ограничиваем часы до 00
    time.settime(-1, -1, H);                                    // устанавливаем новое время
    drawDigits(String(time.Hours), 80, 80+47);                  // выводим на экран
  } else if(buttonPress(up3)){                                  // если нажата кнопка минуты вверх
    i++;                                                        // прибавляем 
    if (i == 60) i = 0;                                         // ограничиваем часы до 23
    time.settime(-1, i, -1);                                    // устанавливаем новое время
    drawDigits(String(time.minutes), 160, 80+47);               // выводим на экран
  } else if(buttonPress(down3)){                                // если нажата кнопка минуты вниз
    i--;                                                        // убавляем
    if (i == -1) i = 59;                                        // ограничиваем часы до 00
    time.settime(-1, i, -1);                                    // устанавливаем новое время
    drawDigits(String(time.minutes), 160, 80+47);               // выводим на экран
  }
}
// функция главного меню
void mainMenuFunc(){
  if (buttonPress(mainDuration) && !pump_flag){                 // если нажата кнопка установок насса
    drawMainMenu(erase);                                        // стираем главное меню
    drawDurationMenu(draw);                                     // рисуем меню установок работы насоса
    menuState = durationMenu;                                   // меняем перменную состояний меню
  } else if(buttonPress(mainDuration) && pump_flag){            // если нажата кнопка установок длительности работы насоса и насос работает
    pump_stop = true;                                           // экстренная остановка насоса
  } else if (buttonPress(mainMoisture)) {                       // если нажата кнопка установки влажности срабатывания насоса
    drawMainMenu(erase);                                        // стираем главное меню
    drawMoistureMenu(draw);                                     // рисуем меню установок влажности
    menuState = moistureMenu;                                   // меняем переменную состояний меню
  } else if(buttonPress(mainTime)) {                            // если нажата кнопка установок времени
    drawMainMenu(erase);                                        // стираем главное меню
    drawTimeMenu(draw);                                         // рисуем меню установок времени
    menuState = timeMenu;                                       // обновляем переменную состояний меню
  } else if (buttonPress(mainPlot)){                            // если нажата кнопка меню просмотра графика
    drawMainMenu(erase);                                        // стираем главное меню
    drawPlotMenu(draw);                                         // рисуем меню графика
    menuState = plotMenu;                                       // обновляем переменную состояний меню
  }
}
// функция меню графика
void plotMenuFunc(){
  if (buttonPress(mainMoisture)){                               // если нажата кнопка "домой"
    drawPlotMenu(erase);                                        // стираем график
    readLog(plot_moist,plot_LDR,pumpPlotFlag,parseRoot(latest));// читаем последний файл лога
    drawMainMenu(draw);                                         // рисуем главное меню
    menuState = mainMenu;                                       // меняем состояние меню
  } else if (buttonPress(plotNext) && plotMenuDay != latest){   // если нажата кнопка следующего файла и мы просматриваем не самый последний файл
    drawPlotMenu(erase);                                        // стираем текущий график
    readLog(plot_moist,plot_LDR,pumpPlotFlag,parseRoot(next));  // читаем следующий файл
    drawPlotMenu(draw);                                         // рисуем новый график
  } else if (buttonPress(plotPrev) && plotMenuDay != oldest){   // если нажата кнопка предыдущего файла и мы просматриваем не самый старый файл
    drawPlotMenu(erase);                                        // стираем текущий график
    readLog(plot_moist,plot_LDR,pumpPlotFlag,parseRoot(prev));  // читаем предыдущий файл
    drawPlotMenu(draw);                                         // рисуем новый график
  }
}
// функция парсирования корневой директории
String parseRoot(int8_t action) {                      
   File dir = SD.open("/");                                     // открываем корневой каталог карты 
   File entry;                                                  // создаем объект entry
   byte buf[12];                                                // создаём буффер для названия файла (8+3)
   int nFiles = 0;                                              // переменная для хранения кол-ва файлов
   int j = 0;                                                   // переменная-итератор
   uint32_t Day, Month, Year;                                   // перемнные для хранения даты
   while(true) {                                                // Начинаем цикл
     entry = dir.openNextFile();                                // Открываем следующий файл,
     if (!entry) {                                              // если файлов не осталось
       dir.rewindDirectory();                                   // указатель на начало дериктории
       break;                                                   // выходим из цикла.
     } else if (!(entry.isDirectory())) {                       // Если файл не является директорией
        nFiles++;                                               // записываем кол-во файлов
     }                                                          //
     entry.close();                                             // закрываем файл
   }                                                            //
   sFiles = new files[nFiles];                                  // выделяем память для структуры sFiles
   int i = 0;                                                   // создаём переменную-итератор
   while(true) {                                                // входим в цикл
    entry = dir.openNextFile();                                 // открываем файл
    String check_name = entry.name();                           // записываем название файла в строку
    if (!entry){                                                // если файлов не осталось
      dir.rewindDirectory();                                    // "перематываем" директорию
      break;                                                    // выходим из цикла
    } else if(!(entry.isDirectory()) && check_name.toInt()!=0){ // если файл не директория и имя файла содержит цифры
      sFiles[i].Name = entry.name();                            // записываем названия файлов в переменные
      sFiles[i].Name.getBytes(buf, 12);                         // берем 12 байт названия в буфер
      uint8_t date_flag = 0;                                    // флаг для вычисления даты
      String s = "";                                            // переменная для сбора чисел без символа дефиса
      int k, j;                                                 // итераторы
      for (k = 0, j = 0; k < 8 || j < 6; k++){                  // запускаем цикл
        if(buf[k] != '-') {                                     // если не дефис:
          s += char(buf[k]);                                    // накапливаем в строку всё, кроме дефиса
          j++;                                                  // приращиваем j
        } else {                                                // если дефис:
          switch(date_flag){                                    // проверяем флаг даты
            // если флаг 0, записываем день, устанавливаем флаг равным 1
            case 0: Day = s.toInt(); date_flag = 1; s = ""; break;
            // если флаг 1, записываем месяц, устанавливаем флаг равным 2
            case 1: Month = s.toInt(); date_flag = 2; s = ""; break;
            default: s = ""; break;                             // обнуляем строку
          }
        }
      }
      Year = s.toInt();                                         // записываем год
      s = "";                                                   // обнуляем строку
      date_flag = 0;                                            // обнуляем флаг даты
      sFiles[i].ints = Year*10000 + Month*100 + Day;            // записываем в структуру;
      j = 0;                                                    // сбрасываем итератор j
      i++;                                                      // приращиваем итератор i
    }
    entry.close();                                              // закрываем файл
   }                                                            //
   dir.close();                                                 // закрываем корневую директорию
  mysort(sFiles, 0, nFiles-1);                                  // сортируем файлы по дате
  switch(action){                                               // проверяем переданную переменную
    case latest: currentFile = nFiles-1; break;                 // последний файл
    case next: if(currentFile < nFiles-1) currentFile++; break; // следующий файл
    case prev: if(currentFile > 0) currentFile--; break;        // предыдущий файл
    default: break;                                             //
  }                                                             //
  if (currentFile == 0) plotMenuDay = oldest;                   // если текущий файл - это начало массива, значит мы на самом старом файле
    // если текущий файл больше нуля и меньше размера массива - мы не на самом новом файле и не на самом старом
    else if (currentFile > 0 && currentFile < nFiles-1) plotMenuDay = between;
    else if (currentFile == nFiles-1) plotMenuDay = latest;     // если текущий файл - равен размеру массива - мы на самом новом файле
  return sFiles[currentFile].Name;                              // возвращаем текущий файл
}
// функция перестановки элементов для mysort, в нашем случае для структуры files
void myswap(files f[], int i, int j) {                          //
  files temp;                                                   //
  temp = f[i];                                                  //
  f[i] = f[j];                                                  //
  f[j] = temp;                                                  //
}                                                               //
// функция быстрой сортировки                                   //
void mysort(files f[], int left, int right){                    //
  int i, last;                                                  // объявляем итераторы
  if (left >= right)                                            // если мы перешли через точку сортировки - 
    return;                                                     // возвращаемся из рекурсии
  myswap(f, left, (left+right)/2);                              // переставляем элеметы, точка сортировки - середина массива
  last = left;                                                  // записываем номер элемента
  for (i = left+1; i<= right; i++)                              // обходим массив слева направо
    if (f[i].ints < f[left].ints)                               // если текущий элемент меньше левого
      myswap(f, ++last, i);                                     // переставляем местами и приащиваем last
  myswap(f, left, last);                                        // переставляем местами левый и последний
  mysort(f, left, last-1);                                      // функция вызввает сама себя для уменьшенного массива слева
  mysort(f, last+1, right);                                     // функция вызввает сама себя для уменьшенного массива справа 
}                                                               //
// функция обнуления массивов                                   //
void zero_out(){                                                //
  mutex = true;                                                 //
  memset(plot_moist,    0,       sizeof(plot_moist));           // обнуляем массив датчика вляжности
  memset(plot_LDR,      0,         sizeof(plot_LDR));           // обнуляем массив датчика освещённости
  memset(pumpPlotFlag,  false, sizeof(pumpPlotFlag));           // обнуляем массив срабатывания насоса
  mutex = false;                                                // 
}
// функция прорисовки кнопки насоса
void drawPumpButton(bool draw_erase){
  // рисуем? да: цвет = цвет шрифта, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(font_color) : myGLCD.setColor(background_color);
  myGLCD.print(String(duration) + "s"  , 260, 35);              // выводим текст на экран
  drawMenuButton(213,4, draw_erase);                            // рисуем кноку
}
// функция прорисовки кнопки стоп
void drawStopButton(bool draw_erase){
  // рисуем? да: цвет = цвет кнопки, нет: цвет = цвет фона
  draw_erase ? myGLCD.setColor(stopButtonColor) : myGLCD.setColor(background_color);
  myGLCD.print("Stop!"  , 240, 35);                             // выводим текст на экран
  int x = 213;                                                  // координата х для кнопки
  int y = 4;                                                    // координата у для кнопки
  myGLCD.drawRoundRect(x,y,x+101,y+82);                         // рисуем квадрат
  myGLCD.drawRoundRect(x+1,y+1,x+100,y+81);                     // рисуем квадрат
  myGLCD.setColor(font_color);                                  // устанавливаем цвет
}
// функция обновления текущего графика
void updatePlot(){
  // вычисляем кол-во секунд от 00:00
  uint32_t now = (uint32_t)time.Hours*3600+time.minutes*60+time.seconds;
  int i = map(now, 0, 86399, 0, 311);                           // приводим значения секунд к номеру элемента массива
  plot(erase);                                                  // стираем график
  plot_LDR[i] = analogRead(LDR);                                // считываем показания с датчика
  plot_moist[i] = readMoisture();                               // считываем показания с датчика
  pumpPlotFlag[i] = pumpLogFlag;                                // записываем флаг насоса в массив
  plot(draw);                                                   // рисуем график
}

Описание функций:

Функции draw....Menu()

  • Назначение: прорисовка или стирание пикселей меню;
  • Синтаксис: draw....Menu(true/false);
  • Параметры: true - рисуем, false - стираем
  • Возвращаемые значения: нет;
  • Примечание: ....= Main, Duration, Moisture, etc.

Функция  plot()

  • Назначение: прорисовка или стирание пикселей графиков;
  • Синтаксис: plot(true/false);
  • Параметры: true - рисуем, false - стираем
  • Возвращаемые значения: нет;
  • Примечание: функция берет данные из массивов pumpPlotFlagplot_LDR и plot_moist.

Функции draw....Button()

  • Назначение: прорисовка или стирание пикселей кнопок;
  • Синтаксис: draw....Button(int x, int y, [bool draw_erase, String bName]);
  • Параметры: x - координаты х, y - координаты y (в некоторых функциях bName - название кнопки, draw_erase - рисуеи/стираем)
  • Возвращаемые значения: нет;
  • Примечание: ....= Small, Up, Down, etc.

Функция saveLog()

  • Назначение: сохранеие текущих показаний датчиков;
  • Синтаксис: saveLog(String fileName);
  • Параметры: fileName - название файла для записи в него данных;
  • Возвращаемые значения: нет;
  • Примечание: функция записывает строку формата "ДД-ММ-ГГГГ, ЧЧ:ММ:СС, ВВ, ОООО, Ф" с последующим символом новой строки и возврата каретки.
    • ДД - день месяца;
    • ММ - месяц;
    • ГГГГ - год;
    • ЧЧ - часы от 0 до 23;
    • ММ - минуты;
    • СС - секунды;
    • ВВ - двухзначные показания датчика влажности;
    • ОООО - четырёхзначные показания датчика освещённости;
    • Ф - флаг сработавшего насоса: 1, 0;

Функция buttonPress()

  • Назначение: поиск файлов в корневом каталоге ;
  • Синтаксис: if (buttonPress(int botton)){//};
  • Параметры: значение кнопки.
  • Возвращаемые значения: bool - кнопка нажата/не нажата (true/false);
  • Примечание: возможные значения - back, datem moist_up, moist_down, up1(2,3,4), down1(2,3,4), mainDuration, mainMoisture, mainTime, mainPlot, plotPrev, plotNext.

Функция ....MenuFunc()

  • Назначение: логика конкретного меню;
  • Синтаксис: ....MenuFunc();
  • Параметры: нет;
  • Возвращаемые значения: нет;
  • Примечание: ....= main, plotm, time, etc.

Функция zero_out()

  • Назначение: функция обнуления массивов графика;
  • Синтаксис: zeroOut();
  • Параметры: нет;
  • Возвращаемые значения: нет;
  • Примечание: обнуляет массивы plot_moist, plot_LDR, pumpPlotFlag

Функция updatePlot()

  • Назначение: обновляет текущий график текущими значениями;
  • Синтаксис: updatePlot();
  • Параметры: нет;
  • Возвращаемые значения: нет;

Функция parseRoot()

  • Назначение: поиск файлов в корневом каталоге ;
  • Синтаксис: filenameparseRoot(latest);
  • Параметры: int8_t action: 0 - самый новый файл, -1 - предыдущий файл, 1 - следующий файл.
  • Возвращаемые значения: String - название файла;
  • Примечание:
    • Функция ищет файлы формата ЧЧ-ММ-ГГ.csv, где дата создания файла является именем файла;
    • Файл, в имени которого отсутствуют числа или числа равны нулю будут проигнорированны функцией;
    • Функция вызывает функцию mySort(), для сортировки структуры с названиями файлов;

Функция readLog()

  • Назначение: Функция чтения и данных из файла;
  • Синтаксис: readLog(uint8_t* mos_val, uint16_t* ldr_val, bool* pump_log_flag, String logFile);
  • Параметры: mos_val - указатель на массив со значениямаи датчика влажности, ldr_val - указатель на массив со значениями датчика освещенности, pump_log_flag - указатель на массив флага срабатывания насоса, logFile - названия файла для парсирования;
  • Возвращаемые значения: нет;
  • Примечание:
    • Функция записывает значения в массив преобразуя секунды от 0 (00:00) до 86399 (23:59) в итераторы массива от 0 до 311;
    • Функция берёт время взятия пробы из файла;

Функция mySort()

  • Назначение: функция сортировки, в данном случае структуры files;
  • Синтаксис: mySort(files f[], int i, int j);
  • Параметры: f[] - структура для сортировки, i - начало массива (или под-массива), j - конец массива (или под-массива);
  • Примечание:
    • Функция делит массив пополам и ищет слева элемент по значению больший чем средний, а справа меньше чем средний, затем меняет их местами. Затем рекурсивно вызывает себя для под-массивов слева и справа от среднего элемента;
    • Функция вызывает функцию mySwap, для перестановки элементов местами;
    • Функция является упращённой версией qsort() из книги Б. Кернигган, Д. Ритчи "Язык программирования C, второе издание", стр. 100;

Ссылки:

Обсуждение