Давайте представим, что после стычки с Дэйвом Боуменом остатков некогда могучего интеллекта HAL 9000 хватило, чтобы вернуть Discovery One к Земле и под лозунгом «смерть всем человекам» разбить его о планету, чтобы впредь неповадно было так поступать с компьютерами.
Разразившийся катаклизм, конечно, уничтожил все разумное человечество, и остались только особи вроде меня — с молотком в одной руке и Arduino — в другой. Поэтому неудивительно, что найденный в горе еще дымящихся остатков космического корабля таинственный прибор означенный представитель человечества может применить разве что в метеостанции. Ну и поделом ему!
Если же отбросить романтику, то суровая правда жизни заключалась в том, что небезызвестная ITEAD предложила на тест дисплей Nextion HMI NX4832T035.
Так как здесь уже неоднократно описывали изделия Nextion, то конспективно о его возможностях. Это экран с собственным простым API и сенсорной панелью, чтобы изготавливать блоки управления с графическими меню без особых хлопот.
Для этой цели у Nextion есть простая система команд для вывода изображений (и их частей), текстовых элементов, полей ввода, графиков. Плюс к тому — некое подобие скриптового языка для выполнения действий, скажем, по нажатию на экранные кнопки или по таймеру. И заодно порт для получения внешних команд от микроконтроллеров или им подобных и возврата данных о прикосновениях к экрану, чтобы знать, до какой кнопки добрались шаловливые руки владельца, и в каком состоянии эта кнопка (нажата, отпущена) в текущий момент.
Грубо говоря, здесь не нужно мучиться и собирать массивы точек или подключать тучу всяких библиотек. Одна строчка — и на экране поменялся фон. Нажали — открылось меню. Подключили Arduino — строим графики по данным, которые поступают через последовательный порт.
То есть, судя о описанию, дисплей настолько умен, что может показывать и реагировать (есть сенсорная панель) на что угодно. Но у меня дома нет чего угодно, зато по воздуху периодически летают климатические данные. От того же БДС-М, например, или от метеодатчика ПИ-ТВ-2.
Конечно, все они в конечном итоге передаются на Народный Мониторинг, откуда я имею счастье наблюдать их через браузер или апп в смартфоне. Но устоять перед идеей сделать «еще одну метеостанцию на 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