Каждая погода хороша: индикатор погоды, который косплеит +15




Давайте представим, что после стычки с Дэйвом Боуменом остатков некогда могучего интеллекта HAL 9000 хватило, чтобы вернуть Discovery One к Земле и под лозунгом «смерть всем человекам» разбить его о планету, чтобы впредь неповадно было так поступать с компьютерами.

Разразившийся катаклизм, конечно, уничтожил все разумное человечество, и остались только особи вроде меня — с молотком в одной руке и Arduino — в другой. Поэтому неудивительно, что найденный в горе еще дымящихся остатков космического корабля таинственный прибор означенный представитель человечества может применить разве что в метеостанции. Ну и поделом ему!

Если же отбросить романтику, то суровая правда жизни заключалась в том, что небезызвестная ITEAD предложила на тест дисплей Nextion HMI NX4832T035.

Так как здесь уже неоднократно описывали изделия Nextion, то конспективно о его возможностях. Это экран с собственным простым API и сенсорной панелью, чтобы изготавливать блоки управления с графическими меню без особых хлопот.

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

Грубо говоря, здесь не нужно мучиться и собирать массивы точек или подключать тучу всяких библиотек. Одна строчка — и на экране поменялся фон. Нажали — открылось меню. Подключили Arduino — строим графики по данным, которые поступают через последовательный порт.

То есть, судя о описанию, дисплей настолько умен, что может показывать и реагировать (есть сенсорная панель) на что угодно. Но у меня дома нет чего угодно, зато по воздуху периодически летают климатические данные. От того же БДС-М, например, или от метеодатчика ПИ-ТВ-2.

Конечно, все они в конечном итоге передаются на Народный Мониторинг, откуда я имею счастье наблюдать их через браузер или апп в смартфоне. Но устоять перед идеей сделать «еще одну метеостанцию на Arduino» я не мог.

И, если честно, дело было даже не столько в технике как таковой, сколько в том, что меня упорно тянет на конструирование интерфейсов. Тянет, наверное, с такой же силой, с которой тянет петь каждого, кому медведь на ухо наступил.

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



Итак, у меня было 3.5 дюйма, 480х320 точек, показавшийся довольно простым API и семь параметров, которые нужно было показывать (в скобочках датчики, которые к этому приделаны):

1) Температура внутри и снаружи (BMP085 / DHT22)
2) Влажность внутри и снаружи (DHT22 / DHT22)
4) Атмосферное давление (BMP085)
5) Концентрация не особо полезных газов в помещении, прежде всего CO2 (MQ135)
6) Концентрация механических примесей в воздухе помещения, прежде всего PM2.5 (Sharp GP2Y1010AU0F)

Тут надо заметить, что товарищи, ознакомившиеся с характеристиками двух последних датчиков, делают вывод, что наибольший отклик достигается как раз в области CO2 и PM2.5 соответственно. Этим, собственно, и объясняется мой выбор: не очень жалко потратить две копейки и посмотреть, что получится, тогда как специализированые сенсоры стоят заметно дороже и в случае неудачи жабе всегда найдется что мне предъявить.

Понятно, что особенной точности от дешевых датчиков я не ждал — скорее предполагал соотносить собственные ощущения с их показаниями. И, кстати, все довольно очевидно: стоит закрыть окно, как одновременно с ростом температуры и духоты начинают ползти вверх значения концентрации газов от MQ135.

Впрочем, к параметрам. Показывать только лишь семь цифр мне подумалось скучным, поэтому ТЗ на прибор расширилось: я придумал для каждого параметра выводить график изменения во времени. И чтобы на одном экране можно было просматривать до четырех кривых одновременно для визуального восприятия закономерностей (если таковые вдруг найдутся) в изменениях различных показателей.

Внимательный читатель и владелец любого портативного девайса с диагональю от 3.5 дюймов, наверняка скажет, что ничего сложного в этом нет. Мол, на таком экране легко поместится и больше данных.

Но есть хитрость: для бытового прибора цифры и подписи должны быть по возможности максимально крупными, чтобы не приходилось подолгу рассматривать экран в лупу, пытаясь пронзить разумом замысел творца.

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

Главный экран с текущими показаниями датчиков:




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

Экран с графиками. Вообще-то их четыре, чтобы показывать от одного до четырех графиков, но фактически они все идентичны. Для примера экран на один и четыре графика:




Здесь для каждого параметра также отображается его максимум и минимум, чтобы понимать границы. А вот временной шкалы нет: во-первых, у прибора нет часов, а, во-вторых, для еще и шкалы просто нет места. По крайней мере, я не нашел, куда бы ее приделать с учетом того, что хотел показывать максимально четыре графика.

При этом кодом предусмотрено добавление полученных показателей в архив каждые полчаса при общей глубине архива в 48 значений, т.е. примерно на сутки. Примерно — потому что данные передаются через не защищенный от помех радиоканал 433 МГц, поэтому возможны пропуски.

А на случай этих самых пропусков также предусмотрено время жизни датчиков в те же полчаса. И если за полчаса приема данных не было, показания датчика не отображаются и не вносятся в архив.

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

Как и любом дизайнеру, пришлось «поиграть шрифтами». В процессе рисования вариантов выяснилось, что любимая мной симметрия вынужденно нарушается не только объединением индикаторов влажности в верхнем правом углу, но и гораздо более огорчительным явлением в виде отрицательных температур.

Именно, минусы портили всю картинку. Поэтому я поступил по-своему: поместил минус над значением. Математикам это, конечно, покажется варварским решением, да и причастные, конечно, будут непроизвольно вздрагивать, размышляя над тем, зачем бы метеостанции значения по модулю. Но зато получилось довольно аккуратно и цифры не пляшут.

Логика работы интерфейса следующая. Основной рабочий режим — дежурный с яркостью экрана 15% от номинала и регулярным обновлением показаний.

Прикосновение в любом месте — переход в интерактивный режим с яркостью 100%. После этого нажатия на показания выделяют их для последующего отображения на графике, а нажатие на «глаз» HAL переключает на экран с графиками.

На это есть ровно пять секунд. Потом отметки очищаются, а еще через пять минут бездействия прибор снова переходит в дежурный режим.

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

Мне показалось, что эта схема достаточно простая, чтобы не добавлять лишние кнопки, которые здесь видеть не очень-то и хочется.

Кстати, фоновые картинки при желании можно менять. Я даже подготовил несколько дополнительных вариантов.

Для колонистов:


Для хоббитов:


Для романтиков:


Для кофеманов:


По концепции весь прибор даже не столько метеостанция, сколько специализированный беспроводной дисплей. Поэтому конструкция предельно простая: внутри только экран, Arduino Pro Mini и приемник на 433 МГц с амплитудной модуляцией. Особенностей в сборке вообще никаких: экран к питанию и последовательному порту, приемник — к питанию и пину 2 контроллера. И, если не считать необходимости в совместимых внешних датчиках — это все.

Чтобы понимать простоту показываю один из вариантов корпуса с почти установленной начинкой:





Я, разумеется, не настаиваю на непременно такой конфигурации, но в моем случае она была оптимальна и по трудоемкости, и по совместимости с другими компонентами автоматики дома.

Ну а разнесение дисплея и датчиков позволяет поставить этот самый дисплей в наиболее удобном месте — как, собственно, и любую метеостанцию с выносными датчиками.

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

Суровая правда жизни заключалась в том, что коробочка, где до настоящего момента проживал БДС-М, оказалась неспособна вместить три дополнительных датчика. Поэтому я дал волю чувствам и отпечатал умопомрачительную лампу авторства Markellov.

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

Оболочка:



В сборе и на месте:



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

Начал я, правда, все равно с пластика — просто чтобы обкатать форму. Промежуточные варианты выглядели примерно таким образом:



В целом мне понравилось (небольшое недовольство решил не замечать), но вот вес, понятно, совсем смешной, не каменный. Поэтому один из корпусов использовал в качестве внутренней опалубки для изготовления бетонной «рамки».

Процесс меня потряс до глубины души — столько грязи я дома редко разводил. Результат — тоже. Хотя местами получилось очень даже ничего, оказалось, что армированная ремонтная смесь не очень-то подходит для таких изделий.

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

Бинго, — подумал я про себя, и немедленно стал выпытывать детали. Ведь отпечатать корпус и покрасить его гораздо эстетичнее, чем превращать дом в полноформатную строительную площадку. В общем, краска оказалась нечеловечески дорогая, но очень классная (для интересующихся — Rust-Oleum American Accents Stone).

Попутно я также пришел к выводу, что корпус мне все же чем-то не нравится (скорее всего, толстыми рамками по всему периметру), поэтому взял себя в руки и, после допроса с пристрастием, пришел вроде бы к финальной форме.

Было:



Стало:







Как легко видеть, от массивности не осталось и следа, но почему-то именно эта форма мне более всего по душе. На ней и остановлюсь. Пока.

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

А так как печатал прозрачным пластиком, ночью вообще космос:



Теперь о некоторых, скажем так, нюансах конструкции. Начну, пожалуй, с того же корпуса. Стремление к красоте сыграло дурную шутку: так как я желал экран вровень с поверхностью, то вырез делал максимально соответствующий внешним размерам дисплея. И спустя примерно десяток примерок таки добился своего — то ли переломил шлейф сенсорной панели, то ли нарушил контакт с платой, но теперь добиться работы сенсора очень сложно.

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

Плюс ко всему пришлось перебороть несколько особенностей самого экрана, описание которых лично я могу озаглавить как «нельзя просто так взять и...».

Итак, нельзя просто так взять и вывести на экран любой шрифт. То есть, теоретически — да. Любой шрифт Windows можно загрузить во встроенный в среду Nextion конвертер и на выходе получить довольно зубастый растр, который выглядит не лучшим образом.

Например, сравните конвертированный шрифт и оригинальный:


Поэтому пришлось пойти муторным путем. Именно — составлять числа и подписи из графических элементов, предварительно подготовленных в GIMP.

Потом я споткнулся на графиках. Оказалось, что нельзя просто так взять и скормить предназначенному для отрисовки кривых элементу Waveform произвольный набор данных. Предварительно нужно все привести к требуемому формату — от 0 до 255 и достроить промежуточные точки, если имеющихся меньше, чем ширина экрана.

Ну и потом выяснил, что и это еще не все. Оказывается, 255 логических точек не масштабируются дисплеем при изменении высоты элемента графика Waveform. Поэтому при изменении высоты элемента графика Waveform необходимо пропорционально менять масштаб величин, иначе график будет «обрезаться».

Еще нельзя использовать изображения с прозрачным фоном, что несколько ограничило полезную ширину информационных полей — чтобы не было некрасивого перекрытия в области градиента у фонового изображения. По-моему, в форуме Nextion это обсуждали и вроде бы ITEAD обещали добавить возможность работы с прозрачными изображениями, но пока что не сделали.

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

Но в целом ощущаю себя победителем. Что хотел — сделал, и коллекция домашних приборов пополнилась еще одним. К тому же, не совсем бесполезным: удобно сразу видеть, что показывают домашние датчики, не доставая смартфон и не включая компьютер.

Плюс к тому, эта вещь не зависит от интернета и таком разрезе выглядит анти-IoT устройством, поскольку все свое хранит в себе. А то представляю я себе, как бы вдруг разделал мою метеостанцию какой-нибудь хакер с нелепым чувством юмора и заставил бы ее запереть дверь.

Я бы потом скребся бы снаружи с просьбами вроде:

— Open the Pod bay doors, HAL

Правда, было бы смешно?

P.S. В изготовлении прибора руководствовался пособиями:

» Nextion HMI Solution
» FLProg + Nextion HMI. Урок 1
» FLProg + Nextion HMI. Урок 2
» FLProg + Nextion HMI. Урок 3

Также могу предложить:

» Макет интерфейса для редактирования в Nextion Editor
» Файл интерфейса для загрузки в дисплей

Код дисплейной части для Arduino
// v5: два порта для отладки Serial - Arduino, Serial1 - Nextion
// v6: история показаний
// v7: получение показаний с домашних радиодатчиков
// v8: первая полная сборка всех блоков (получение данных, вывод на главный экран, вывод истории)
// v9: переход на реальные датчики и библиотеку DHT.h Adafruit
// v10: уменьшение яркости подсветки после таймаута, исправление процедуры сдачи показаний в архив
// v11: прямое подключение датчиков CO2/PM2.5
// v12: подключение всех датчиков по радио, reDraw только если statusBoolean = true
// ToDo: график по 40 точкам, чтобы не выходить за границы, масштабирование по всем графикам (со второй страницы)


#include <DHT.h>
#include <RCSwitch.h> //   http://code.google.com/p/rc-switch/
#include <MQ135.h> // подключение библиотеки датчика CO2

#define measurePin  A1  // analog
#define ledPower  12 // digital

#define samplingTime 280
#define deltaTime 40
#define sleepTime 9680

float voMeasured = 0;
float calcVoltage = 0;
float dustDensity = 0;
float ppm = 0;


#define analogPin A0 // аналоговый выход MQ135 подключен к пину A0 Arduino

MQ135 gasSensor = MQ135(analogPin); // инициализация объекта датчика


// стартовые позиции полей



#define tempInX 10
#define tempOutX 380 // 415
#define humidityInX 320 //325
#define humidityOutX 410
#define pressureX 20 // 10
#define ppmX 20
#define pmX 370
#define Y0 0 // координата в таблице символов
#define Y1 5 // давление, влажность
#define Y2 125 // температура
#define Y3 230 // ppm&pm
#define dotY 125 // десятичная точка
#define minusY 115// минус

// знак минуса температуры снаружи
#define minusXOut 380

// знак минуса температуры внутри
#define minusXIn 10

// высота минуса
#define minusH 3 

// идентификатор картинки с минусом
#define minusPicID 2 

#define symbolID 1 // номер картинки с табицей символов

#define statusTimeOut 2200000 // время "жизни" данных датчиков (период, за который показания меняются хотя бы единожды) 900000
#define updateTimeOut 300000 // интервал обновления показателей на дисплее 60000 (300000)
#define historyTimeOut 1800000 // интервал добавления показателей в историю (1800000)
#define selectionTimeOut 5000 // время бездействия до очистки меток показателей
#define backLightTimeOut 300000 // время бездействия до уменьшения уровня подсветки
#define waveLimit 254

int parameterS[7]; // массив параметров. Последовательность: tempIn, tempOut, humidityIn, humidityOut, pressure, ppm, pm
byte statusS[7]; // подсчет количества принятых данных для определения статуса датчика (работает / не работает)
boolean statusBoolean[7]; // признаки работоспособности датчиков

/*
// массивы координат показателей на титульной странице
int coordinateX[7] = {tempInX, tempOutX, humidityInX, humidityOutX, pressureX, ppmX, pmX};
int coordinateY[7] = {Y2, Y2, Y1, Y1, Y1, Y3, Y3};
*/

// массивы историй
// Последовательность строк: tempIn, tempOut, humidityIn, humidityOut, pressure, ppm, pm
int historyArray[7][48]; // 7 строк по 48 элементов
boolean drawArray[7]; // массив для выборки параметров для построения графиков
int arrayMax; // переменные минимума и максимума в массиве значений
int arrayMin;
byte waveShift; // коррекция значений массива для приведения к диапазону waveform 0 - 255
float arrayNorm; // коэффициент масштабирования для максимального заполнения графика по высоте

byte count=0; // счетчик количества символов поля
byte arrayCounter; // счетчик для массивов
byte waveCount; // количество графиков к построению
int splitData[4]; // массив для разбиения значений на символы [единицы, десятки, сотни, тысячи]
byte thousands, hundreds, tens, ones; // для хранения частей значений
int tempIn, tempOut, humidityIn, humidityOut, pressure;
// int tempIn, tempOut, humidityIn, humidityOut, pressure, ppm, pm;
int symbolX0;
int posX; // расположение символа по X на дисплее
byte tempInStatus, tempOutStatus, humidityInStatus, humidityOutStatus, pressureStatus, ppmStatus, pmStatus; // счетчик количества обновлений показателей за период StatusTimeOut
byte symbolCount, symbolWidth;
boolean minusIn = false; // признак отрицательной температуры
boolean minusOut = false;
byte i = 0; // универсальный счетчик
unsigned long statusTime, updateTime, historyTime, selectionTime, backLightTime;
String stringToNextion;
byte historyCount;
boolean backLight; // включение подсветки на полную яркость по первому касанию после снижения яркости

int weatherData = 0;
int dht22Humidity = 0;


//CLICKER VARIABLE SECTION
byte buffer[30]; // buffer for bytes coming from HMI.
byte waveCounter = 0; // счетчик количества нажатых кнопок
// boolean drawArray[7]; // список массивов для построения графиков
boolean allClear;
byte currentPage = 0; // номер текущей страницы
byte searchTarget, searchNumber, searchCounter, pageNum, drawCounter, channelToDraw;
// int historyArray[7][48]; // 7 строк по 48 элементов
// int arrayMax; // переменные минимума и максимума в массиве значений
// int arrayMin;
// byte waveShift; // коррекция значений массива для приведения к диапазону waveform 0 - 255
// float arrayNorm; // коэффициент масштабирования для максимального заполнения графика по высоте
byte iconCounter = 0;

// int splitData[4]; // массив для разбиения значений на символы [единицы, десятки, сотни, тысячи]
// int posX; // расположение символа по X на дисплее
// byte count = 0; // счетчик количества символов поля
// String stringToNextion;
// int symbolX0;
// byte symbolCount;

// высота минуса
#define minusH 3 

// идентификатор картинки с минусом
#define minusPicID 2 

// Массивы координат для вычисления места индикатора нажатия
int axisX[5][7] = {{10, 380, 320, 410, 20, 20, 370}, // страница 0
               {0, 0, 0, 0, 0, 0, 0},
               {0, 0, 0, 0, 0, 0, 0},
               {0, 0, 0, 0, 0, 0, 0},
               {0, 0, 0, 0, 0, 0, 0}}; // страница 1-4

int axisY[5][7] = {{115, 115, 5, 5, 5, 230, 230}, // страница 0
               {145,0, 0, 0, 0, 0, 0}, // страница 1
               {63, 226, 0, 0, 0, 0, 0}, // страница 2 
               {37, 145, 253, 0, 0, 0, 0 }, // страница 3
               {25, 105, 185, 265, 0, 0, 0}}; // страница 4


byte symbolW[5][11] = {{25, 15, 30, 25, 30, 30, 25, 30, 25, 30, 5},
                       {11, 6, 12, 12, 14, 13, 12, 13, 12, 12, 3},
                       {11, 6, 12, 12, 14, 13, 12, 13, 12, 12, 3},
                       {11, 6, 12, 12, 14, 13, 12, 13, 12, 12, 3},
                       {11, 6, 12, 12, 14, 13, 12, 13, 12, 12, 3}};// ширина символов 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

// {13, 7, 15, 13, 17, 15, 13, 14, 14, 14, 5} для шрифта 25 Comfortaa Light

byte numberPic[5] = {1, 3, 3, 3, 3}; // ID изображений с цифрами

byte symbolH[5] = {60, 18, 18, 18, 18}; // высота символов по страницам 25 для шрифта 25 Comfortaa Light

// CLICKER VARIABLE SECTION ENDS    

// TREND VARIABLE SECTION

#define x2 70 // сумма квадратов временных точек

int trendArray[3][6] = {{0, 0, 0, 0, 0, 0}, // массив для расчета тренда
                        {-5, -3, -1, 1, 3, 5},
                        {0, 0, 0, 0, 0, 0}};

int sumY, sumXY;
byte trendCount;
int trend; 

// TREND VARIABLE SECTION ENDS                  

#define DHTPIN 7
#define DHTTYPE DHT22

// Setup a DHT22 instance
DHT dht(DHTPIN, DHTTYPE);




// Setup RC-Switch
RCSwitch mySwitch = RCSwitch();


void sendToNextion() {
  Serial.write(0xff);
  Serial.write(0xff);
  Serial.write(0xff);
}



// визуализация выбора и снятия выбора элемента
void drawMark(byte mark, byte markNum) {

int markX;

// признак установленных меток для сброса меток при бездействии
if (allClear == true) {
  allClear = false;
}

markX = axisX[0][markNum];

if (markNum == 1 || markNum == 3 || markNum == 6) {
  markX = 475;
} 

if (markNum == 0 || markNum == 4 || markNum == 5) {
  markX = 0;
}

if (markNum == 2) {
  markX = markX - 15;
}

    stringToNextion = String("fill ");
    stringToNextion = stringToNextion + String(markX); // String(axisX[page, type]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(axisY[0][markNum]+30); // String(axisY[page, type]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String("5");
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String("30");
    stringToNextion = stringToNextion + String(",");

if (mark == 1) {

    stringToNextion = stringToNextion + String("RED"); // ноль начинается с позиции 0, 0
    // Serial2.println("Mark set");

}

if (mark == 0) {

    stringToNextion = stringToNextion + String("BLACK"); // ноль начинается с позиции 0, 0
    // Serial2.println("Mark clear");

}

    // Serial2.println(stringToNextion);
    Serial.print(stringToNextion);
    sendToNextion();
  
}

// очистка выделения: сброс массива и счетчика графиков
void clearSelection() {

  for (byte jj = 0; jj < 7; jj++) {
   if (drawArray[jj] == true) {
    drawArray[jj] = false;
    drawMark(0, jj);
   }
  }

  allClear = true; // признак снятых меток
  waveCounter = 0;
}





void updateHistory() {
mySwitch.disableReceive();

// сдвиг массива вправо, чтобы слева были самые новые значения - для построения графика, потому что он строится в обратном порядке
for (arrayCounter = 0; arrayCounter < 7; arrayCounter++) {
  // Serial2.print("StatusBoolean for array #");// Serial2.print(arrayCounter);// Serial2.print(" = "); // Serial2.println(statusBoolean[arrayCounter]);
  if (statusBoolean[arrayCounter] == true) { // если были новые показания по выбранному каналу
    for (i = 47; i > 0; i--) {
      historyArray[arrayCounter][i] = historyArray[arrayCounter][i-1]; // сдвиг
    }
    historyArray[arrayCounter][0] = parameterS[arrayCounter]; // добавление нового
  }

// statusBoolean[arrayCounter] = false; // выключено, чтобы не сбивать отображение при перерисовке экрана

  }

for (arrayCounter = 0; arrayCounter < 7; arrayCounter++) {
 for (i = 0; i < 47; i++) {
   // Serial2.print(historyArray[arrayCounter][i]);// Serial2.print(", ");
  }
 // Serial2.println();
}

// Serial2.println();  

mySwitch.enableReceive(0);
  
}



void drawTrend(byte arrayToTrend) {

int markX;

markX = axisX[0][arrayToTrend];

// выбор координаты X для метки тренда
if (arrayToTrend == 1 || arrayToTrend == 3 || arrayToTrend == 6) {
  markX = 472;
} 

if (arrayToTrend == 0 || arrayToTrend == 4 || arrayToTrend == 5) {
  markX = 0;
}

if (arrayToTrend == 2) {
  markX = markX - 15;
}

    stringToNextion = String("xpic ");
    stringToNextion = stringToNextion + String(markX); // координата по X
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(axisY[0][arrayToTrend]); // координата по Y
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String("8"); // размер значка тренда 7x18
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String("18"); //
    stringToNextion = stringToNextion + String(",");

if (trend > 0) {


    stringToNextion = stringToNextion + String("0"); // вверх начинается с 0
    stringToNextion = stringToNextion + String(","); // 
    stringToNextion = stringToNextion + String("0"); // координата Y для вырезки всегда 0

  
}

if (trend < 0) {

    stringToNextion = stringToNextion + String("8"); // вниз начинается с 5
    stringToNextion = stringToNextion + String(","); // 
    stringToNextion = stringToNextion + String("0"); // координата Y для вырезки всегда 0
    
}

if (trend == 0) {

    stringToNextion = stringToNextion + String("16"); // без изменений начинается с 10
    stringToNextion = stringToNextion + String(","); // 
    stringToNextion = stringToNextion + String("0"); // координата Y для вырезки всегда 0
 
}

    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String("5"); // ID картинки с пиктограммами трендов
    // Serial2.println(stringToNextion);    
    Serial.print(stringToNextion);
    sendToNextion();
 
}



void splitRoutine(int input) {



input = abs(input); // на знаки разбиваются числа больше нуля, знак добавляется позже

for (count = 0; count < 4; count++) {
  splitData[count] = 0; // инициализация массива 
}

count = 0;

if (input > 9999) { // если величина за пределами измерений
  count = 5;
} else {

if (input > 999) { // до 9999
  splitData[3] = input/1000;
  input = input - splitData[3]*1000;
  count = 4;
/*
  Serial.print("SplitData Count 4:");
  Serial.println(splitData[3]);
*/
}

if (input > 99) { // до 999
  splitData[2] = input/100;
  input = input - splitData[2]*100;
  if (count == 0) {
    count = 3;
/*
  Serial.print("SplitData Count 3:");
  Serial.println(splitData[2]);
*/  
  }
}

if (input > 9) { // до 99
  splitData[1] = input/10;
  input = input - splitData[1]*10;
  if (count == 0) {
    count = 2;
/*
  Serial.print("SplitData Count 2:");
  Serial.println(splitData[1]);
*/  
  }
} 

if (input < 10) {
  splitData[0] = input;
  if (count == 0) {
    count = 1;
/*
  Serial.print("SplitData Count 1:");
  Serial.println(splitData[0]);
*/  
  }
}

}
/*
  Serial.print("Input = ");
  Serial.println(input);
  Serial.print("Count = ");
  Serial.println(count);
*/

}




void drawRoutine(byte page, int value, byte type, int drawX, int drawY) { 

mySwitch.disableReceive();  
// page - выбор шрифта; value = значение; type = вид значения для выбора координаты по осям; drawX старт по оси X, drawY - по Y
  
  boolean minusSign = false;

  splitRoutine(value);  

if (page == 0) { // размещение на нулевой странице
// приведение показателей (температура и pm25) справа к крайней правой границе с заданным отступом (например 470 = 480 - 10)
  if (type == 1) { // если температура 
    drawX = 470 - count*30 - 5; // от правой границы с отступом в 10 отсчитываем место на нужное количество символов, по 30 точек на символ + 5 на точку
    if (count == 1) {
      drawX = drawX - 30; // запас на 0, если десятичное значение температуры    
    }
  }

  if (type == 6) { // если pm25
    drawX = 470 - count*30 - 5; // от правой границы с отступом в 20 отсчитываем место на нужное количество символов, по 30 точек на символ
    if (count == 1) {
      drawX = drawX - 30;
    }
  }
}

  int posX = drawX;

 if (value < 0) {
  minusSign = true;
 }

 

 if (count < 5) { // если значение меньше 9999

  if ((count == 1) && ((type == 0) || (type  == 1) || (type  == 6))) { // если температура или pm2.5 и если модуль меньше 0, добавляется 0 с десятичной точкой
  // xpic НачалоX, НачалоY, Ширина, Высота, ВырезкаX, ВырезкаY, КартинкаВырезки

    // ноль
    stringToNextion = String("xpic ");
    stringToNextion = stringToNextion + String(drawX); // String(axisX[page, type]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(drawY); // String(axisY[page, type]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(symbolW[page][0]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(symbolH[page]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String("0"); // ноль начинается с позиции 0, 0
    stringToNextion = stringToNextion + String(","); // ноль начинается с позиции 0, 0
    stringToNextion = stringToNextion + String("0"); // ноль начинается с позиции 0, 0
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(numberPic[page]);
   // // Serial2.println("Temp leading zero");
   // // Serial2.println(stringToNextion);
    Serial.print(stringToNextion);
    sendToNextion();
    drawX = drawX + symbolW[page][0]; // смещение на ширину символа 0
      
   } // count = 1

   for (byte ii = count; ii > 0; ii--) { // индикация цифр показателя, начиная со старшей цифры

    if ((ii == 1) && ((type == 0) || (type == 1) || (type == 6))) { // вывод точки, если десятичное значение температуры 
      stringToNextion = String("xpic ");
      stringToNextion = stringToNextion + String(drawX); // String(axisX[page, type]);
      stringToNextion = stringToNextion + String(",");
      stringToNextion = stringToNextion + String(drawY); // String(axisY[page, type]);
      stringToNextion = stringToNextion + String(",");
      stringToNextion = stringToNextion + String(symbolW[page][10]);
      stringToNextion = stringToNextion + String(",");
      stringToNextion = stringToNextion + String(symbolH[page]);
      stringToNextion = stringToNextion + String(",");
      if (page == 0) {
        stringToNextion = stringToNextion + String("265"); // в шрифте нулевой страницы точка в позиции 265
      } else {
        stringToNextion = stringToNextion + String("118"); // в шрифте остальных страниц в позиции 135 для Comfortaa Light 25
      }
      stringToNextion = stringToNextion + String(","); // 
      stringToNextion = stringToNextion + String("0"); // стартовая позиция по Y для вырезки всегда 0
      stringToNextion = stringToNextion + String(",");
      stringToNextion = stringToNextion + String(numberPic[page]); 
     // // Serial2.println("Temp decimal dot");
     // // Serial2.println(stringToNextion);      
      Serial.print(stringToNextion);
      sendToNextion();
      drawX = drawX + symbolW[page][10]; // смещение на ширину символа точки
    } // ii == 1 && type == 0 || type == 1

    // печать остальных символов    
    symbolX0 = 0; // инициализация счетчика координаты символа в таблице символов
    for (symbolCount = 0; symbolCount < (splitData[ii-1]);symbolCount++) { // расчет позиции по оси X в таблице символов (расчет ширин всех символов)
      symbolX0 = symbolX0 + symbolW[page][symbolCount];
    }

    stringToNextion = String("xpic ");
    stringToNextion = stringToNextion + String(drawX); // String(axisX[page, type]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(drawY); // String(axisY[page, type]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(symbolW[page][splitData[ii-1]]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(symbolH[page]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(symbolX0); // ноль начинается с позиции 0, 0
    stringToNextion = stringToNextion + String(","); // ноль начинается с позиции 0, 0
    stringToNextion = stringToNextion + String("0"); // ноль начинается с позиции 0, 0
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(numberPic[page]);
   // // Serial2.print("Symbol: "); // Serial2.println(splitData[ii-1]);
  //  // Serial2.println(stringToNextion);    
    Serial.print(stringToNextion);
    sendToNextion();
    drawX = drawX + symbolW[page][splitData[ii-1]];
  }

  if (minusSign == true) {

    /*
    symbolX0 = 0; // инициализация расчета длины минуса
    for (byte ii = count; ii > 0; ii--) { 
      symbolX0 = symbolX0 + symbolW[page][splitData[ii-1]];
    }

    */
    
    stringToNextion = String("xpic ");
    stringToNextion = stringToNextion + String(posX); // String(axisX[page, type]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(drawY-8); // String(axisY[page, type]);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(drawX - posX);
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(minusH); // высота минуса
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(0); // ноль начинается с позиции 0, 0
    stringToNextion = stringToNextion + String(","); // ноль начинается с позиции 0, 0
    stringToNextion = stringToNextion + String("0"); // ноль начинается с позиции 0, 0
    stringToNextion = stringToNextion + String(",");
    stringToNextion = stringToNextion + String(minusPicID);
//    // Serial2.print("Symbol: "); // Serial2.println(splitData[ii-1]);
 //   // Serial2.println(stringToNextion);    
    Serial.print(stringToNextion);
    sendToNextion();
    
  }
  
 } // if count < 5 если значение меньше 9999
  mySwitch.enableReceive(0);
}         

// выбор максимума в массиве (фрагменте массива)
void getMinMax(byte arrayToMax, byte maxCount) {
  
byte getMaxCount = 0;

arrayMax = historyArray[arrayToMax][getMaxCount]; // сначала максимум равен первому элементу массива
arrayMin = historyArray[arrayToMax][getMaxCount]; // сначала минимум равен первому элементу массива

for (byte getMaxCount = 0; getMaxCount < maxCount; getMaxCount++) { // maxCount 47 для полного массива, 6 для тренда
  
  if (historyArray[arrayToMax][getMaxCount+1] > arrayMax){ 
    arrayMax = historyArray[arrayToMax][getMaxCount+1];
  }

  if (arrayMin > historyArray[arrayToMax][getMaxCount+1]){ 
    arrayMin = historyArray[arrayToMax][getMaxCount+1];
  }
  
}
 
}      

void getTrend(byte arrayToTrend) {
mySwitch.disableReceive();
getMinMax(0, 6); // вычисление минимума и максимума для сдвига выше нуля


for (trendCount = 0; trendCount < 6; trendCount++) {
  if (arrayMin < 0) {
    trendArray[0][trendCount] = historyArray[arrayToTrend][5-trendCount] + abs(arrayMin); // копирование и сдвиг массива
  } else {
    trendArray[0][trendCount] = historyArray[arrayToTrend][5-trendCount];
  }
}

sumY = 0;
sumXY = 0;

// сумма фактических значений, сумма произведений XY
for (trendCount = 0; trendCount < 7; trendCount++) {
  sumY = sumY + trendArray[0][trendCount];
  sumXY = sumXY + trendArray[0][trendCount]*trendArray[1][trendCount];
}

trend = (int) (sumY/10 + (sumXY/x2)*trendArray[1][5]) - (sumY/10 + (sumXY/x2)*trendArray[1][0])+0.5;

// Serial2.print("Trend: "); // Serial2.println(trend);

drawTrend(arrayToTrend);
mySwitch.enableReceive(0);
}

void reDraw() {
 mySwitch.disableReceive();

// Serial2.println("Redraw main page");
/* временный блок для демонстрации
 for (i = 0; i < 7; i++) {
  parameterS[i] = random(255);
  statusS[i] = 1;
 }
временный блок для демонстрации */ 

 Serial.print("page 0");
 sendToNextion();
 // Serial.print("pic 0,0,6");
 // sendToNextion();
 
 
 
  for (i = 0; i < 7; i++) {
   // Serial2.print("StatusBoolean on reDraw for item "); // Serial2.print(i); // Serial2.print(" is "); // Serial2.println(statusBoolean[i]);
   if (statusBoolean[i] == true) { // если получены данные за "период жизни"
    drawRoutine(currentPage, parameterS[i], i, axisX[currentPage][i], axisY[currentPage][i]);
    // Serial2.print("Redraw, ");// Serial2.print(i); // Serial2.print(": "); // Serial2.println(parameterS[i]);
    if (historyCount > 5) {
      getTrend(i);
    }

   }
    
  }

 mySwitch.enableReceive(0);
}




void getNorm() {

arrayNorm = 1.00; // масштаб 1:1

  arrayNorm = abs(arrayMax - arrayMin);
  
    arrayNorm = waveLimit/arrayNorm; // расчет масштабирующего коэффициента
  
}



void drawHistory(byte arrayCounter, byte waveCount){
mySwitch.disableReceive();
byte tC01 = 0;
byte tC02 = 0; 
int interPoint, lineMulti;
int justPoint;

  byte channelCount = 0; // выбор канала графика, всегда первый (ID = 0), потому что на каждый график свой объект графика с одной кривой

      getMinMax(arrayCounter, 47); // было 47 по глубине архива, но 39, чтобы не выходить за пределы видимой области экрана, уменьшенной на подписи
 //     // Serial2.print("arrayMax: "); // Serial2.println(arrayMax);
 //     // Serial2.print("arrayMin: "); // Serial2.println(arrayMin);
      getNorm();
      
      if (currentPage == 2) {
         arrayNorm = arrayNorm*0.5;      
      }

      if (currentPage == 3) {
         arrayNorm = arrayNorm*0.3;      
      }

      if (currentPage == 4) {
         arrayNorm = arrayNorm*0.2;
      }

 //     // Serial2.print("arrayNorm: "); // Serial2.println(arrayNorm);   

     
     

// первая точка
      stringToNextion = String("add "); // начало построения графика
      stringToNextion = stringToNextion + String(waveCount); // выбор графика по ID
      stringToNextion = stringToNextion + String(",");
      stringToNextion = stringToNextion + String(channelCount); // выбор канала 0, 1, 2 или 3
      stringToNextion = stringToNextion + String(",");
      if (arrayMin < 0) {
        justPoint = (int) (historyArray[arrayCounter][tC01] + abs(arrayMin))*arrayNorm + 0.5;
        stringToNextion = stringToNextion + String(justPoint); // первая точка из пары
      } else {
        justPoint = (int) historyArray[arrayCounter][tC01]*arrayNorm + 0.5;
        stringToNextion = stringToNextion + String(justPoint); // первая точка из пары
      }
      Serial.print(stringToNextion);
      // // Serial2.print("First point, original");// Serial2.println(historyArray[arrayCounter][tC01]);
      // // Serial2.print("First point: "); // Serial2.println(stringToNextion);          
      sendToNextion(); 
                
        for (tC01 = 0; tC01 < 46; tC01++) { // на месте 37 было 46
          
          lineMulti = (historyArray[arrayCounter][tC01+1] - historyArray[arrayCounter][tC01])/9; // расчет линейного коэффициента для построения прямой между точками графика
          
          if (arrayMin < 0) {
            justPoint = (int) historyArray[arrayCounter][tC01] + abs(arrayMin) + lineMulti+0.5;
            interPoint = justPoint;
          } else {
            justPoint = (int) historyArray[arrayCounter][tC01] + lineMulti + 0.5;
            interPoint = justPoint;
          }
          
          for (tC02 = 0; tC02 < 7; tC02++) { // построение промежуточных точек, линейная зависимость, в оригинале было 9
            stringToNextion = String("add "); // начало построения графика
            stringToNextion = stringToNextion + String(waveCount); // выбор страницы для графика (1, 2, 3 или 4 канала)
            stringToNextion = stringToNextion + String(",");
            stringToNextion = stringToNextion + String(channelCount); // выбор канала 0, 1, 2 или 3
            stringToNextion = stringToNextion + String(",");
            justPoint = (int) interPoint*arrayNorm;
            stringToNextion = stringToNextion + String(justPoint);
            interPoint = (int) interPoint + lineMulti;
            Serial.print(stringToNextion);   
           // // Serial2.print("Connecting point: "); // Serial2.println(stringToNextion);                               
            sendToNextion();                      
          }
          stringToNextion = String("add "); // начало построения графика
          stringToNextion = stringToNextion + String(waveCount); // выбор страницы для графика (1, 2, 3 или 4 канала)
          stringToNextion = stringToNextion + String(",");
          stringToNextion = stringToNextion + String(channelCount); // выбор канала 0, 1, 2 или 3
          stringToNextion = stringToNextion + String(",");
          if (arrayMin < 0) {
            justPoint = (int) (historyArray[arrayCounter][tC01+1] + abs(arrayMin))*arrayNorm + 0.5;
            stringToNextion = stringToNextion + String(justPoint); // последняя точка из пары
          } else {
            justPoint = (int) historyArray[arrayCounter][tC01+1]*arrayNorm + 0.5;
            stringToNextion = stringToNextion + String(justPoint); // последняя точка из пары
          }
          
          Serial.print(stringToNextion);
         // // Serial2.print("Next/Last point, original: ");// Serial2.println(historyArray[arrayCounter][tC01+1]);               
          // // Serial2.print("Next/Last point: "); // Serial2.println(stringToNextion);               
          sendToNextion();                                  
        }
    
 mySwitch.enableReceive(0);   
  }
  






void drawIcon(int where, byte what) {

//// Serial2.print("Where: "); // Serial2.println(where);
//// Serial2.print("What: "); // Serial2.println(what);

stringToNextion = String("xpic ");
stringToNextion = stringToNextion + String("0"); // String(axisX[page, type]);
stringToNextion = stringToNextion + String(",");
stringToNextion = stringToNextion + String(where-3); // String(axisY[page, type]);
stringToNextion = stringToNextion + String(",");
stringToNextion = stringToNextion + String(80); // размеры пиктограмм фиксированные
stringToNextion = stringToNextion + String(",");
stringToNextion = stringToNextion + String(30);
stringToNextion = stringToNextion + String(",");
stringToNextion = stringToNextion + String(what*80); // для начала вырезки по X = 80*порядковый номер пиктограммы
stringToNextion = stringToNextion + String(","); // 
stringToNextion = stringToNextion + String("0"); // позиция по Y всегда 0
stringToNextion = stringToNextion + String(",");
stringToNextion = stringToNextion + String(4);
//// Serial2.println(stringToNextion);
Serial.print(stringToNextion);
sendToNextion();
  
}



void toPage(byte pageNum) {
mySwitch.disableReceive();
currentPage = pageNum;

/*
// очистка страницы при переходе
stringToNextion = String("ref page");
stringToNextion = stringToNextion + String(pageNum);
Serial.print(stringToNextion);
sendToNextion();
*/

// переход на запрошенную страницу
  String stringToNextion = String("page ");
  stringToNextion = stringToNextion + String(pageNum);
 // // Serial2.print("Switch to: "); // Serial2.println(stringToNextion);
  Serial.print(stringToNextion);
  sendToNextion();



if (pageNum == 0) {
  clearSelection();
  reDraw();
}

// перестроение графиков при переходе на страницу с графиками
  if (pageNum > 0) {

    
    if (pageNum == 1 && waveCounter > 1) { // если построение отдельного графика при переходе со страницы с несколькими графиками
 //     // Serial2.print("Draw single, searchCounter-1: "); // Serial2.println(searchCounter-1);
      getMinMax(searchCounter-1, 47); // вычисление минимума и масксимума в массиве
  //    // Serial2.print("arrayMax: "); // Serial2.println(arrayMax);
  //    // Serial2.print("arrayMin: "); // Serial2.println(arrayMin);
      drawRoutine(pageNum, arrayMax, searchCounter-1, 0, 117); // 115
      drawIcon(145, searchCounter-1);
      drawRoutine(pageNum, arrayMin, searchCounter-1, 0, 185); // 175
      drawHistory(searchCounter-1, 1); // построение графика: один график на первой странице
     } else {

              // вывод пиктограмм
              iconCounter = 0;
              for (drawCounter = 0; drawCounter < 7; drawCounter++) {
                if (drawArray[drawCounter] == true) {
        //          // Serial2.print("pageNum: "); // Serial2.println(pageNum);
       //           // Serial2.print("iconCounter: "); // Serial2.println(iconCounter);
      //            // Serial2.print("drawCounter: "); // Serial2.println(drawCounter);
                  getMinMax(drawCounter, 39); // на месте 39 было 47
      //            // Serial2.print("arrayMax: "); // Serial2.println(arrayMax);
     //             // Serial2.print("arrayMin: "); // Serial2.println(arrayMin);
                  drawRoutine(pageNum, arrayMax, drawCounter, 0, axisY[pageNum][iconCounter]-22);   // -30               
                  drawIcon(axisY[pageNum][iconCounter], drawCounter);
                  drawRoutine(pageNum, arrayMin, drawCounter, 0, axisY[pageNum][iconCounter]+35); // +30
                  iconCounter++;
                }
            }      
              channelToDraw = 1; // на самом деле уже не канал, а номер объекта графика, но образно - канал
              for (drawCounter = 0; drawCounter < 7; drawCounter++) { // построение графиков "с нуля"
                if (drawArray[drawCounter] == true) {
                  drawHistory(drawCounter, channelToDraw);
           //       // Serial2.print("drawCounter / channelToDraw "); // Serial2.print(drawCounter); // Serial2.print(" | "); // Serial2.println(channelToDraw);
                  channelToDraw++;
                 }
              }
      }
    }
    
  mySwitch.enableReceive(0);
}



void setup() {

  pinMode(ledPower,OUTPUT);
 // pinMode(measurePin, INPUT);

 pinMode(4, OUTPUT);
 digitalWrite(4, HIGH);



 
  currentPage = 0;
  allClear = true;
  historyCount = 0; // счетчик минимального количества параметров для оценки тренда
  backLight = true; // признак для включения подсветки на полную яркость без дальнейшей реакции, включается при false
  
// инициализация массивов параметров и статусов
// массив параметров. Последовательность: tempIn, tempOut, humidityIn, humidityOut, pressure, ppm, pm
 for (i = 0; i < 7; i++) {
  parameterS[i] = 0;
  statusS[i] = 1;
 }

// инициализация массивов истории
for (arrayCounter = 0; arrayCounter < 7; arrayCounter++) {
 for (i = 0; i<48; i++) {
  historyArray[arrayCounter][i]=0;
 }
}
 
 statusTime = millis();
 updateTime = millis();
 historyTime = millis();
 backLightTime = millis();

 Serial.begin(9600);
  Serial.print("baud=57600");
  sendToNextion();
 Serial.end();

 Serial.begin(57600);
 Serial.print("baud=57600");
 sendToNextion();
 
 // Serial2.begin(115200);
 dht.begin();
 mySwitch.enableReceive(0);
 delay(2000);
 Serial.print("page 0");
 sendToNextion();
 // Serial2.println("Ready");
 
 reDraw();
}



void loop() {


// ПОЛУЧЕНИЕ ДАННЫХ ОТ РАДИОДАТЧИКОВ

   if (mySwitch.available()) { // проверяем датчики
    int value = mySwitch.getReceivedValue();
    if (value != 0) {
    // Serial2.print("RC-Switch: "); // Serial2.println(mySwitch.getReceivedValue());
// ВЛАЖНОСТЬ И ТЕМПЕРАТУРА СНАРУЖИ
      if (mySwitch.getReceivedValue() / 100000 == 161) {

        weatherData = mySwitch.getReceivedValue() - 16100000;
        if (weatherData > 10000) { // пришла влажность
          parameterS[3] = (weatherData - 10000)/10;
          statusS[3] = statusS[3]+1;   
          statusBoolean[3] = true;  
        }
        else { // пришла температура
          if (weatherData > 1000) { // минусовая температура
            parameterS[1] = -(weatherData - 1000);
            minusOut = true;
          }
          else { // плюсовая температура
            parameterS[1] = weatherData;
            minusOut = false;
          }
        }
          statusS[1] = statusS[1]+1;
          statusBoolean[1] = true;
      }

// ДАВЛЕНИЕ И ТЕМПЕРАТУРА ВНУТРИ
      if (mySwitch.getReceivedValue() / 10000 == 1210) {
        parameterS[4] = (mySwitch.getReceivedValue() - 12100000) / 1.33; // пришло давление
        statusS[4] = statusS[4]+1;   
        statusBoolean[4] = true;   
        // Serial2.print("Pressure: "); // Serial2.println(parameterS[4]);     
      }

      if (mySwitch.getReceivedValue() / 100000 == 131) {
        weatherData = mySwitch.getReceivedValue() - 13100000;
        if (weatherData > 1000) { // минусовая температура
          parameterS[0] = -(weatherData - 1000);
          minusIn = true;
        }
        else { // плюсовая температура
          parameterS[0] = weatherData;
          minusIn = false;
        }
          statusS[0] = statusS[0]+1;
          statusBoolean[0] = true;
      }

// ВЛАЖНОСТЬ ВНУТРИ      

      if (mySwitch.getReceivedValue() / 10000 == 1212) {
        parameterS[2] = (mySwitch.getReceivedValue() - 12120000); // влажность
        statusS[2] = statusS[2]+1;   
        statusBoolean[2] = true;   
        // Serial2.print("HumidityIn: "); // Serial2.println(parameterS[2]);     
      }
// CO2 PPM
      if (mySwitch.getReceivedValue() / 10000 == 1213) {
        parameterS[5] = (mySwitch.getReceivedValue() - 12130000); // CO2
        statusS[5] = statusS[5]+1;   
        statusBoolean[5] = true;   
        // Serial2.print("CO2 PPM: "); // Serial2.println(parameterS[5]);     
      }      
// PM2.5
      if (mySwitch.getReceivedValue() / 10000 == 1214) {
        parameterS[6] = (mySwitch.getReceivedValue() - 12140000); // PM2.5
        statusS[6] = statusS[6]+1;   
        statusBoolean[6] = true;   
        // Serial2.print("PM2.5: "); // Serial2.println(parameterS[6]);     
      } 

    }
    mySwitch.resetAvailable();
    //    mySwitch.enableReceive(0); // включение RC Switch
  }   

  
// очистка меток при бездействии
if (currentPage == 0) {
  if ((millis() - selectionTime) > selectionTimeOut) { 
    if (allClear == false) {
      clearSelection();
    }
  }
 }

  if (Serial.available())
  {
    bool moretocome = true;
    int endcount = 0;
    int bytesread = 0;
    byte inbyte;
    //bool isascii = false;

    while (moretocome)
    {
      inbyte = Serial.read();

        delay(2);
        
      if (inbyte == 0xFF)
      {
        endcount++;
        if (endcount == 3)
        {
          moretocome = false;
          // Serial2.println("");
        }
      }
      else
      {
        endcount = 0;
      }
      buffer[bytesread] = inbyte;
      bytesread++;

    }

    for (int x = 0; x < bytesread; x++)
    {
      // Serial2.print(buffer[x], HEX);
      // Serial2.print(" ");
    }

    //// Serial2.println(bytesread, DEC);
    // Serial2.println("");

  }

  if (buffer[0] == 101) { // если событие кнопка
   backLightTime = millis(); // сброс таймера для выключения подсветки пока нажимаются кнопкиы
   if (backLight == false) { // если нажата любая кнопка, а подсветка выключена
    // Serial2.println("Backlight 50%");
    Serial.print("dim=100"); // включение подсветки
    sendToNextion();
    backLight = true; // установка признака работающей подсветки
   } else { // иначе исполнение команды по кнопке
    
    if (buffer[1] == 0) { // если на нулевой странице
      selectionTime = millis();
      if (buffer[2] < 8) { // если нажата кнопка показателя
      if (waveCounter < 4) { // если выбрано менее 4 показателей
        if (drawArray[buffer[2]-1] == false) { // если показатель не выбран
          drawArray[buffer[2]-1] = true; // пометка на визуализацию массива, соответствующего нажатой кнопке
          drawMark(1, buffer[2]-1);
          waveCounter = waveCounter + 1; // увеличение счетчика графиков    
     //     // Serial2.print("Set button: "); // Serial2.println(buffer[2]);          
     //     // Serial2.print("Total buttons: "); // Serial2.println(waveCounter);
        } else {
          drawArray[buffer[2]-1] = false; // удаление пометки на визаулизацию массива
          drawMark(0, buffer[2]-1);
          if (waveCounter > 0) {
            waveCounter = waveCounter - 1;
          }
     //     // Serial2.print("Clear button: "); // Serial2.println(buffer[2]);          
     //     // Serial2.print("Total buttons: "); // Serial2.println(waveCounter);          
        }
      } else { // если уже помечено четыре массива
        if (drawArray[buffer[2]-1] == true) { // если снова нажата кнопка, нажатая ранее
          drawArray[buffer[2]-1] = false; // удаление пометки на визаулизацию массива
          drawMark(0, buffer[2]-1);
          waveCounter = waveCounter - 1; // уменьшение счетчика графиков
     //     // Serial2.print("Clear button: "); // Serial2.println(buffer[2]);          
     //     // Serial2.print("Total buttons: "); // Serial2.println(waveCounter);          
        }
      }
    } else { // если нажата кнопка перехода к графикам
        currentPage = waveCounter;
        toPage(waveCounter);    
      }
    }
    else { 
      if (currentPage > 1) { // если на странице больше одного графика, и нажата кнопка ID = 5 возврат на страницу 0
        if (buffer[2] == 5) {
          toPage(0);
        }
      }

      if (currentPage == 1) { // если страница 1 и один график, и нажата кнопка ID = 5 возврат на страницу 0
        if (buffer[2] == 5) {
          if (waveCounter == 1) {
            toPage(0);
          } else {
            toPage(waveCounter); // иначе возврат на страницу по количеству графиков
          }
        }
      }

      if (currentPage == 1) { // если на странице 1 и нажата кнопка с ID = 6 - для быстрого перехода на главный экран без возврата к графикам
        if (buffer[2] == 6) {
          toPage(0);
        }
      }      
      
      // действия по страницам

    if (currentPage > 1) { // если страница больше 1, т.е графиков больше 1
      if (buffer[2] > 5) { // если нажата кнопка с ID > 2, т.е. выбран график
   //     // Serial2.print("Buffer[2]: ");
   //     // Serial2.println(buffer[2]);
   //     // Serial2.print("Normalized button: ");
        searchTarget = buffer[2] - 5; // "нормализация" номера кнопки, одновременно счетчик для вычисления графика, который нужно отобразить: кнопка 1 - первый график - подсчет true в массиве drawarray до значения 1
        searchNumber = 0;
        searchCounter = 0;
   //     // Serial2.println(searchTarget);

        while ((searchCounter < 7) && (searchNumber < searchTarget)) {
          if (drawArray[searchCounter] == true) {
            searchNumber++;
          }
          searchCounter++;
        }
        
   //     // Serial2.print("searchCounter: "); // Serial2.println(searchCounter);
        toPage(1); // переход на страницу для отображения графика
      }
    }


      
     }
    }
  } 

buffer[0] = 0;

// счетчик каждого показателя *Status увеличивается на 1 при получении данных
// показание заносится в переменную

// Обновление статусов: если по окончании интервала счетчик на нуле, переставляем его на 9
if ((millis() - statusTime) > statusTimeOut) {

 
 for (i = 0; i < 7; i++) { // по всем семи
  // Serial2.print("Status of "); // Serial2.print(i); // Serial2.print(" is: "); // Serial2.println(statusS[i]);
    if (statusS[i] == 0) {statusBoolean[i] = false;
    // Serial2.print("As StatusS "); // Serial2.print(i); // Serial2.print(" is "); // Serial2.print(statusS[i]); // Serial2.println(" StatusBoolean set to false");
    }
    statusS[i] = 0; // сброс количества принятых данных
 }
  statusTime = millis();
  
}

if (currentPage == 0) { // обновление текущих данных только на начальном экране

 
// Обновление данных на дисплее
if ((millis() - updateTime) > updateTimeOut) {

  reDraw();
  updateTime = millis();
 }

 
 
}

// Выключение подсветки при необходимости и переход на главную страницу
if ((millis() - backLightTime) > backLightTimeOut) {
 if (backLight == true) {
  backLight = false;
  // Serial2.println("Backlight 15%");
  //Serial.print("page 0");
  // sendToNextion();
  currentPage = 0;
  clearSelection();
  reDraw();
  Serial.print("dim=15");
  sendToNextion();
 }
  
 }

// Обновление данных истории
if ((millis() - historyTime) > historyTimeOut) {
  updateHistory();

    historyCount++;
    // Serial2.print("History Q: "); // Serial2.println(historyCount);
 
  historyTime = millis();
  }

}  



Модель корпуса (осторожно — можно сломать экран)
-->


К сожалению, не доступен сервер mySQL