Программируемое реле на Ардуино

в 0:52, , рубрики: arduino, программирование микроконтроллеров, Программируемое реле на Ардуино, метки:

Идея банальна, понадобился контроллер для управления нагрузкой в доме:
1. Котел отопления.
2. Накопительный бойлер для водоснабжения.
3. Насос в скважине.

Читал массу увлекательных статей на тему ХХ на Ардуино, читая которые четко фиксировал в голове мысль «хочу Ардуино». Прикинув стоимость компонентов и готовых решений, посчитал явную выгоду от внедрения Ардуино.

image

Итак, программа минимум:

1. 4 реле, часы (RTC), ЖК экран;
2. Режимы работы каждого реле: включено, выключено, суточный таймер, одноразовое включение;
3. Кнопки управления для настройки времени и режимов реле;

В доме установлен двухтарифный счетчик, поэтому бойлер нагревает воду с 23 до 7 утра. Аналогично отопление: два из трех тэнов, по моей задумке будут включаться ночью. Управление температурой пока остается родное на штатном пульте. Одноразовое включение в качестве резерва пойдет на насос, программируем включение, например, на набор емкости или прокачку скважины, после чего реле переходит в режим выключено. Основная особенность: изготовлено законченное устройство, управляемое кнопками, и не требующее подключения к ПК.

Конечно, хотелось в перспективе все повесить на контроллер, так как для отопления целесообразно сделать 3 режима работы: день с 7 до 23 в целях экономии, ночь, разогрев к утреннему отключению с 5..6 до 7. Но пока реализована программа минимум.

Аппаратная часть:

При изготовлении была задача получить как можно более дешевое изделие, поэтому максимально присутствует колхоз. На Али были заказаны стартовый комплект для arduino Uno R3, 4 релейный модуль, жк-экран I2C 20*4, часы RTC DS1307 I2C, цифровой датчик температуры и влажности Dht21.

Поскольку все это видел первый раз пришлось осваивать. Общие понятия почерпнул с помощью гугла из:
http://habrahabr.ru/company/masterkit/blog/257747/
http://arduino.ru/Reference

Красивую схему подключения сделать не могу, не в чем. Во Fritzing к примеру, из компонентов только сам микроконтроллер.

Подключение реле и кнопок проблем не вызвало, единственно включил подтягивающие резисторы. Это есть в руководстве. В подключении ЖК экрана помогла ссылка https://arduino-info.wikispaces.com/LCD-Blue-I2C#v3. Потребовалось регулировка подстроечным резистором, «из коробки» экран не горел совсем, чем вызвал у меня легкое замешательство.

Часы потребовали только батарейку, подключил по типовой схеме http://zelectro.cc/RTC_DS1307_arduino

Синхронизацию часов с компьютером делать не стал. При запуске производится проверка, если дата меньше 2000 года или больше 2100, выводится меню настройки часов.

Подключение нескольких кнопок к аналоговому входу описано по ссылке http://arduino.net.ua/Arduino_articles/Arduino_proekty/Podkljuchenie%20knopok%20k%20odnomu%20analogovomu%20vhodu/, там же в комментариях описано как включить подтягивающие резисторы «pinMode (A2, INPUT_PULLUP);»

Управление классическое, «мониторное»: кнопки «меню», "+","-", «set».

Процесс сборки (с фото)

Взял монтажный щиток на 6 автоматов:

image

Поставил монтажные стойки под модули:

image

Прикрутил реле, часы, контроллер:

image

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

Блок питания взял от какого-то роутера Dlink, 5В 2А, ломать голову не стал запаял прямо на него USB провод:

image

Вырезал из пластика панель для крепления экрана:

image

Установил экран. Закрепил монтажными стойками, под клавиатурой — винт. Стойки подобраны по высоте с расчетом, что в них будет упираться крышка щитка, придавая жесткость конструкции. Втулка на блоке реле предотвращает продавливание платы вниз при нажатии кнопок.

image

Кнопки изначально планировал подключить к цифровым входам, но внезапно нашел модуль клавиатуры от монитора, который подошел как родной (схема кнопок от монитора).

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

Фото готового устройства:
image
image

Мигающий светодиод также присутствует.

В данный момент реле висит «на соплях», управляет бойлером, окончательный монтаж будет произведен после установки проводки и контакторов для котла отопления. Фазу с колодки тоже надо убрать, конечно, при монтаже проводки. Сейчас некогда, надо делать наружные работы по дому. Затраты на детали составили около 2 тысяч рублей.

Программная часть:

Программная часть далась нелегко: 90% времени ушло на написание меню, годный код удалось осилить только с третьей версией прошивки.

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

Второй подход был переводом кода на принципы ООП. За основу был взят определенный класс TMenu, от которого наследовались непосредственно элементы меню.

Вкратце. Указателю CurrentMenu присваивается адрес текущего элемента меню Основные элементы класса это бит ItemIsValue, который определяет является ли текущий элемент подменю или изменяемым значением и функции OnKey(), Increment(), Decrement() и Print(). Также класс меню содержит указатель на родительское меню и массив указателей. В общем использование наследования позволяло сделать произвольное многоуровневое меню, в принципе можно сказать, что это динамическое меню, только в в данной реализации оно формируется один раз при инициализации. Во всяком случае код легко редактируется, добавляются элементы меню. Жестокая реальность поставила меня на место. В UNO R3 на всю эту роскошь не хватает памяти.

Третий подход — урезка второго. Главное отличие одно — конкретный объект класса меню содержит либо вложенные меню, либо переменные — редактируемые значения, тип которых задан классом.

Итак, Определен класс:

class TMenu
{
  public:
    byte _ItemsCount;
    TMenu *Parent;
    String *MenuName;
    boolean ItemIsValue;
    byte CurrentItem;

    TMenu **Items;
    String *ItemsName;
    byte ItemsCount(void) ;
    bool AddItem(TMenu *NewItem);

    virtual void Print(void);
    void OnKey(byte KeyNum);
    void ChangeItem(byte value);

    virtual void Increment(void);
    virtual void Decrement(void);

    virtual void OnSet(void);
    DateTime CheckDateTime(DateTime OldDate, int Increment, byte DatePart);
};

Класс содержит:
— число элементов меню (подменю или переменная), указатель на родительское меню (если указатель равен 0, то достигнут верх);
MenuName имя меню;
ItemIsValue описан выше
— номер позиции курсора в меню (CurrentItem);
— указатель на массив указателей Items. Адреса подменю. Если меню содержит редактируемые элементы, это значение равно 0;
— функция Print() вызывается из цикла loop от имени текущего меню «CurrentMenu->Print();» таким образом отрисовывается экран с нужным текстом.
— функция OnKey(byte KeyNum) также вызывается из цикла loop в блоке подавления дребезга контактов, он же декодер клавиатуры от монитора.
— функции ChangeItem(byte value), virtual void Increment(void), virtual void Decrement(void) вызываются из OnKey() и обрабатывают кнопки "+" и "-". ChangeItem() — это переборка элементов меню, Increment() и Decrement() — полиморфные, переборка значений текущей переменной.
— функция CheckDateTime(DateTime OldDate, int Increment, byte DatePart) проверяет введенную дату и время. Распознается вискозный год и количество дней в месяце 28/29, 30, 31. Исходя из логики в функцию передается текущая дата, +1 или -1 и индекс части даты/времени (0 — год, 5 — секунды)

Навигация по меню реализована присвоением адреса объекта указателю CurrentMenu:
— CurrentMenu = CurrentMenu->Items[CurrentMenu->CurrentItem]; вход в выбранное меню
— CurrentMenu = CurrentMenu->Parent; переход в предыдущее меню

Логика работы:

Цикл loop непрерывно опрашивает клавиатуру, проверяет настройки реле и мигает светодиодом.

Клавиатура опрашивается в качестве рудимента и по цифровым входам 2-6 (menu,-,+,set), к этим кодам пересчитываются значения аналоговых портов.
— при нажатии на кнопку «menu» вне меню происходит вызов меню, в противном случае переход на меню вверх;
— при нажатии "+" или "-" происходит циклическая переборка элементов меню или циклическое изменение текущего параметра. При нажатии кнопки "'set" вход в выбранное меню либо сохранение значения переменной во флеш с одновременным выбором следующего значения.

Дребезг подавляется программно, каждой кнопке присваивается счетчик нажатия и отпускания, который увеличивается в случае нажатия или отпускания. Опрос проводится 3 раза с интервалом 15 мс. Счетчик нажатия или отпускания увеличивается на 1 либо сбрасывается. Таким образом распознается дребезг как нажатия так и отпускания. Состояние отпускания фиксируется для однократного срабатывания при удержании кнопки.

В настройках реле проверяется режим работы, в режиме «Daily» вводится и проверяется только время, с точностью до минут. Правильно распознается время включения больше времени выключения, например, включение в 23 и выключение в 7. В режиме «Оnce» задается дата и время. Для удобства настройки планирую подключить пятую кнопку и задать на нее функцию установки текущей даты и времени в режиме редактирования.

Это вкратце. Небольшие функции классов объявлены, как правило при объявлении класса, заголовочные файлы и библиотеки не используются. Код и так небольшой.

Код программы

#include <EEPROM.h>

#include <DHT.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <RTClib.h>
#define LEFT 0
#define CENTER 1
#define RIGHT 2

#define RelayModesCount 4
#define KeyFirst 2
#define KeyLast 6

LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);
RTC_DS1307 RTC; // RTC Modul
DHT dht(7, DHT21); // pin, type
volatile boolean Blinker = true;
volatile long BlinkerTime;
volatile byte ButtonPress[8];
const String RelayModeNames[] = {«OFF», «ON», «Once», «Daily»};

int aKey1 = 0;
int aKey2 = 0;

DateTime NowDate;

boolean DoBlink(void)
{
boolean Result = false;
long NBlinkerTime = millis();
if (Blinker)
{
if (NBlinkerTime — BlinkerTime > 200)
{
digitalWrite(8, HIGH);
BlinkerTime = NBlinkerTime;
Blinker = false;
Result = true;
}
}
else
{
if (NBlinkerTime — BlinkerTime > 300 )
{
digitalWrite(8, LOW);
BlinkerTime = NBlinkerTime;
Blinker = true;
}

}
return Result;
}
String BlinkString(String string, byte Cur, byte ItemsCount)
{
String result = string;
byte len = string.length();
if (!Blinker && Cur == ItemsCount)
{
for (byte i = 0; i < len; i++) result.setCharAt(i, ' ');
}
return result;
}

/********************************************************************************************************/
/********************************** Объявление классов *********************************************/
/********************************************************************************************************/

class TMenu
{
public:
byte _ItemsCount;
TMenu *Parent;
String *MenuName;
boolean ItemIsValue;
byte CurrentItem;

TMenu **Items;
String *ItemsName;
// byte ItemsCount(void);
byte ItemsCount(void) {
return _ItemsCount;
};
bool AddItem(TMenu *NewItem);

virtual void Print(void);
void OnKey(byte KeyNum);
void ChangeItem(byte value);

virtual void Increment(void);
virtual void Decrement(void);

virtual void OnSet(void);
DateTime CheckDateTime(DateTime OldDate, int Increment, byte DatePart);
};

class TNoMenu: public TMenu
{
public:
void Print(void);
TNoMenu(TMenu *ParentMenu) {
MenuName = 0;
CurrentItem = 0;
_ItemsCount = 0;
Parent = ParentMenu;
Items = 0;
ItemsName = 0;
ItemIsValue = false;
};
void Increment(void) {};
void Decrement(void) {};
void OnSet(void) {};

};

class TSelectMenu: public TMenu
{
public:
void Print(void);
TSelectMenu(TMenu *ParentMenu, String NewName) {
MenuName = new String(NewName);
CurrentItem = 0;
_ItemsCount = 0;
Parent = ParentMenu;
Items = 0;
ItemsName = 0;
ItemIsValue = false;
};
void Increment(void) {};
void Decrement(void) {};
void OnSet(void) {};
};

class TTimeMenu: public TMenu
{
public:
void Print(void);
DateTime *SetDateTime;
long OldDateTime;
TTimeMenu(TMenu *ParentMenu, String NewName, DateTime *ParamDate) {
MenuName = new String(NewName);
CurrentItem = 0; _ItemsCount = 6; Parent = ParentMenu; Items = 0; ItemsName = 0;
ItemIsValue = true; OldDateTime = millis();
SetDateTime = ParamDate;
};
void Increment(void) {
*SetDateTime = CheckDateTime (*SetDateTime, 1, CurrentItem);
};
void Decrement(void) {
*SetDateTime = CheckDateTime (*SetDateTime, -1, CurrentItem);
};
void OnSet(void) {
RTC.adjust(*SetDateTime);
};
void SecondTimer(void) {
long TmpDateTime = millis(); if (TmpDateTime — OldDateTime > 1000) {
OldDateTime = TmpDateTime;
*SetDateTime = *SetDateTime + 1;
};
};
};
class TRelayMenu: public TMenu
{
public:
byte RelayNumber;
byte RelayMode;
// byte Shedule=0;
boolean OnceBit;
DateTime RelayOn;
DateTime RelayOff;
TRelayMenu(TMenu *ParentMenu, byte NewNumber, String NewName) {
MenuName = new String(NewName);
CurrentItem = 0; _ItemsCount = 11; Parent = ParentMenu; Items = 0; ItemsName = 0; ItemIsValue = true, OnceBit = false;
RelayNumber = NewNumber;
RelayMode = 0;
RelayOn = DateTime(2015, 1, 1, 23, 00, 00);
RelayOff = DateTime(2015, 1, 1, 07, 00, 00);
};
void Print(void);
void Increment(void) {
if (!CurrentItem) {
RelayMode++;
if ( RelayMode >= RelayModesCount) RelayMode = 0;
}
else if (CurrentItem < 6) RelayOn = CheckDateTime (RelayOn, 1, CurrentItem — 1);
else RelayOff = CheckDateTime (RelayOff, 1, CurrentItem — 6);
};
void Decrement(void) {
if (!CurrentItem) {
RelayMode--;
if ( RelayMode > 127) RelayMode = RelayModesCount — 1;
}
else if (CurrentItem < 6) RelayOn = CheckDateTime (RelayOn, -1, CurrentItem — 1);
else RelayOff = CheckDateTime (RelayOff, -1, CurrentItem — 6);
};

boolean CheckDaily(void);

void OnSet(void) {
///// здесь надо записать реле в память

byte p_address = RelayNumber * 16;
EEPROM.write(p_address, RelayMode);

EEPROM.write(p_address + 1, byte(RelayOn.year() — 2000));
EEPROM.write(p_address + 2, byte(RelayOn.month() ));
EEPROM.write(p_address + 3, byte(RelayOn.day() ));
EEPROM.write(p_address + 4, byte(RelayOn.hour() ));
EEPROM.write(p_address + 5, byte(RelayOn.minute() ));

EEPROM.write(p_address + 6, byte(RelayOff.year() — 2000));
EEPROM.write(p_address + 7, byte(RelayOff.month() ));
EEPROM.write(p_address + 8, byte(RelayOff.day() ));
EEPROM.write(p_address + 9, byte(RelayOff.hour() ));
EEPROM.write(p_address + 10, byte(RelayOff.minute() ));
};
};

/********************************************************************************************************/
/******************************** Конец объявления классов ******************************************/
/********************************************************************************************************/

TMenu *CurrentMenu = 0;
TNoMenu *NoMenu = 0;
TSelectMenu *SelectMenu;
TTimeMenu *TimeMenu;

TRelayMenu *RelayMenu[4];

/********************************************************************************************************************************************/
/********************************************************************************************************************************************/
/********************************************************************************************************************************************/
void setup()
{
NoMenu = new TNoMenu(0);
SelectMenu = new TSelectMenu (NoMenu, «NoMenu»);
TimeMenu = new TTimeMenu(SelectMenu, «Time Setup», &NowDate);

SelectMenu->AddItem(TimeMenu);

byte p_address;
DateTime DTFlesh;
for (int i = 0; i < 4; i++)
{
// здесь надо добавить загрузку параметров из флеша
RelayMenu[i] = new TRelayMenu (SelectMenu, i, «Relay » + String(i + 1));
SelectMenu->AddItem(RelayMenu[i]);

p_address = i * 16;

RelayMenu[i]->RelayMode = EEPROM.read(p_address);

DTFlesh = DateTime(int(EEPROM.read(p_address + 1) + 2000), EEPROM.read(p_address + 2), EEPROM.read(p_address + 3), EEPROM.read(p_address + 4), EEPROM.read(p_address + 5), 0 );
RelayMenu[i]->RelayOn = RelayMenu[i]->CheckDateTime(DTFlesh, 0, 0);

DTFlesh = DateTime(int(EEPROM.read(p_address + 6) + 2000), EEPROM.read(p_address + 7), EEPROM.read(p_address + 8), EEPROM.read(p_address + 9), EEPROM.read(p_address + 10), 0 );
RelayMenu[i]->RelayOff = RelayMenu[i]->CheckDateTime(DTFlesh, 0, 0);
}

for (byte i = KeyFirst; i < KeyLast; i++)
{
pinMode(i, INPUT); //Keypad 2-«menu» 3-"-" 4-"+" 5-«SET»
digitalWrite(i, HIGH); //setup Resistor input2Vcc
ButtonPress[i] = true;
}
pinMode(8, OUTPUT); //LED
pinMode(9, OUTPUT);
for (byte i = 10; i < 14; i++)
{
pinMode(i, OUTPUT); // relay i
digitalWrite(i, HIGH);
}

pinMode (A2, INPUT_PULLUP);
pinMode (A3, INPUT_PULLUP);

Serial.begin(9600); // Used to type in characters
digitalWrite(8, LOW);
digitalWrite(9, HIGH);

lcd.begin(20, 4); // initialize the lcd for 20 chars 4 lines and turn on backlight
RTC.begin();

lcd.noBacklight();
delay(150);
lcd.backlight();

NowDate = RTC.now();
//проверка времени
if ( NowDate.year() > 2000 && NowDate.year() < 2114 &&
NowDate.month() > 0 && NowDate.month() < 13 &&
NowDate.day() > 0 && NowDate.day() < 32 &&
NowDate.hour() >= 0 && NowDate.hour() < 24 &&
NowDate.minute() >= 0 && NowDate.minute() < 60 &&
NowDate.second() >= 0 && NowDate.second() < 60 )
{
CurrentMenu = NoMenu;
}
else
{
lcd.setCursor(2, 1);
lcd.print(«Clock Failure!»);
delay(700);
RTC.adjust(DateTime(2015, 1, 1, 00, 00, 00));
CurrentMenu = TimeMenu;
}

}

void loop()
{
/********* KEYPAD BUNCLE, 5 keys, from 2 to 6 *********/
byte NButtonPress[8] = {0, 0, 0, 0, 0, 0, 0, 0};
byte NButtonRelease[8] = {0, 0, 0, 0, 0, 0, 0, 0};
const byte ButtonTry = 3;

aKey1 = analogRead (2);
aKey2 = analogRead (3);
byte aKeyNum=0;

/**************** check for key pressed or released ****************/
for (byte i = 0; i < 3; i++)
{
delay(15);
if (aKey1< 64) aKeyNum=2;//AnalogKey 1 = Dig2
else if (aKey1<128) aKeyNum=6;//Analog key 3 = D4
else if (aKey1<256) aKeyNum=4;//key 5=d6
else if (aKey2< 64) aKeyNum=1;//key 6 = menu
else if (aKey2<128) aKeyNum=3;//analogkey 2 = D3
else if (aKey2<256) aKeyNum=5;//key 4 =d5
else aKeyNum=0; // no key

for (byte j = KeyFirst; j < KeyLast; j++) // Read ports 2...6
{
if (digitalRead(j) == LOW || aKeyNum==j)
{
NButtonPress[j]++;
NButtonRelease[j] = 0;
}
else
{
NButtonPress[j] = 0;
NButtonRelease[j]++;
delay(5);
}
}

}
/*************** Do key process ******************/
// byte m;

for (byte j = KeyFirst; j < KeyLast; j++)
{
if (NButtonPress[j] >= ButtonTry && ButtonPress[j] == false)
{
ButtonPress[j] = true;
CurrentMenu->OnKey(j);
}
else
{
if (NButtonRelease[j] >= ButtonTry && ButtonPress[j] == true)
{
ButtonPress[j] = false;
}
}
}
/***************** Relay Check *********************/

CurrentMenu->Print();
DoBlink();
}

void LcdPrint(byte string, String str, byte Align)
{
byte StrTrim1;
byte StrTrim2;
lcd.setCursor(0, string); //Start at character 0 on line 0
switch (Align)
{
case RIGHT:

break;

case CENTER:
StrTrim1 = byte((20 — str.length()) / 2);
StrTrim2 = 20 — str.length() — StrTrim1;
for (byte k = 0; k < StrTrim1; k++) lcd.print(" ");
lcd.print(str);
for (byte k = 0; k < StrTrim2; k++) lcd.print(" ");
break;

default:
lcd.print(str);
StrTrim1 = 20 — str.length();
for (byte k = 0; k < StrTrim1; k++) lcd.print(" ");
}
}

void TNoMenu::Print(void)
{
NowDate = RTC.now();
String Ddate;
Ddate = " R1-" + RelayModeNames[RelayMenu[0]->RelayMode] + " R2-" + RelayModeNames[RelayMenu[1]->RelayMode];
LcdPrint(0, Ddate, CENTER);
Ddate = " R3-" + RelayModeNames[RelayMenu[2]->RelayMode] + " R4-" + RelayModeNames[RelayMenu[3]->RelayMode];
LcdPrint(1, Ddate, CENTER);
Ddate = String (NowDate.year()) + "/" + String(NowDate.month()) + "/" + String(NowDate.day()) + " " + String (NowDate.hour()) + ":" + String(NowDate.minute()) + ":" + String(NowDate.second());
LcdPrint(2, Ddate, CENTER);
Ddate = «Temp » + String (int(dht.readTemperature())) + «C, Hum » + String(int(dht.readHumidity())) + "%";
LcdPrint(3, Ddate, CENTER);

RelayCheck();
}

void TTimeMenu::Print(void)
{
SecondTimer();
String Ddate = BlinkString(String((*SetDateTime).year()), CurrentItem, 0) + "/" +
BlinkString(String( (*SetDateTime).month()), CurrentItem, 1) + "/" +
BlinkString(String((*SetDateTime).day()), CurrentItem, 2) + " ";
LcdPrint(1, Ddate, CENTER);
Ddate = BlinkString(String ((*SetDateTime).hour()), CurrentItem, 3) + ":" +
BlinkString(String((*SetDateTime).minute()), CurrentItem, 4) + ":" +
BlinkString(String((*SetDateTime).second()), CurrentItem, 5);
LcdPrint(2, Ddate, CENTER);

LcdPrint(3, " ", CENTER);
RelayCheck();
}

void TMenu::OnKey(byte KeyNum)
{
switch (KeyNum)
{
case 3: // — if (ItemIsValue) Decrement();
else ChangeItem(-1);
break;
case 4: // +
if (ItemIsValue) Increment();
else ChangeItem(1);
break;
case 5: // SET
if (ItemIsValue)
{
OnSet();
ChangeItem(+1);
}
else // вход в подменю
{
if (Items && ItemsCount())
{
if (CurrentMenu->ItemsCount())
{
CurrentMenu = CurrentMenu->Items[CurrentMenu->CurrentItem];
CurrentMenu->CurrentItem = 0;
}
}
}
break;
default: // 2 -menu
if (Parent) CurrentMenu = CurrentMenu->Parent; //(TMenu *) &NoMenu;
else
{
CurrentMenu = SelectMenu;
CurrentMenu->CurrentItem = 0;
}
}
}

void TMenu::ChangeItem(byte value)
{
CurrentItem += value;
if (CurrentItem > 128) CurrentItem = ItemsCount() — 1;
else if (CurrentItem > ItemsCount() — 1) CurrentItem = 0;
}

boolean TMenu::AddItem(TMenu *NewItem)
{
if (!Items) Items = new TMenu *[_ItemsCount = 1];
else Items = (TMenu **)realloc((void *)Items, (_ItemsCount = _ItemsCount + 1) * sizeof(void *));
Items[_ItemsCount — 1] = NewItem;
}

DateTime TMenu::CheckDateTime(DateTime OldDate, int Increment, byte DatePart)
{
int DTmin[6] = {2000, 1, 1, 0, 0, 0};
int DTmax[6] = {2199, 12, 31, 23, 59, 59};

int DT[6];
int diff;

DT[0] = OldDate.year();
DT[1] = OldDate.month();
DT[2] = OldDate.day();
DT[3] = OldDate.hour();
DT[4] = OldDate.minute();
DT[5] = OldDate.second();
DT[DatePart] = DT[DatePart] + Increment;

if (DT[1] == 1 || DT[1] == 3 || DT[1] == 5 || DT[1] == 7 || DT[1] == 8 || DT[1] == 10 || DT[1] == 12) DTmax[2] = 31;
else if (DT[1] == 2)
{
if ((DT[0] % 4 == 0 && DT[0] % 100 != 0) || (DT[0] % 400 == 0)) DTmax[2] = 29;
else DTmax[2] = 28;
}
else DTmax[2] = 30;

for (byte i = 0; i < 6; i++)
{
if (DT[i] > DTmax[i]) DT[i] = DTmin[i];
else if (DT[i] < DTmin[i]) DT[i] = DTmax[i];
}

return DateTime(DT[0], DT[1], DT[2], DT[3], DT[4], DT[5]);

}

void TSelectMenu::Print(void)
{
NowDate = RTC.now();
byte shift = 0;
if (CurrentItem > 3) shift = CurrentItem — 3;
for (byte i = 0; i < 4; i++)
{
if ((CurrentItem — shift) == i) //&&Blinker)
{
LcdPrint(i, ">> " + * (Items[i + shift]->MenuName) + " <<", CENTER);
}
else LcdPrint(i, *(Items[i + shift]->MenuName), CENTER);
}
RelayCheck();
}

void TRelayMenu::Print(void)
{

String DData;
NowDate = RTC.now();
LcdPrint(0, (*MenuName) + "[" + BlinkString(RelayModeNames[RelayMode], CurrentItem, 0) + "]", CENTER);
DData = «On:»;
switch (RelayMode)
{
case 3: //Daily
// DData = DData + " ";
if (CurrentItem > 0 && CurrentItem < 4) CurrentItem = 4;
break;
default:
DData = DData + BlinkString(String(RelayOn.year(), DEC), CurrentItem, 1) + "/" + BlinkString(String( RelayOn.month(), DEC), CurrentItem, 2) +
"/" + BlinkString(String( RelayOn.day(), DEC), CurrentItem, 3);
}
DData = DData + " " + BlinkString(String (RelayOn.hour(), DEC), CurrentItem, 4) + ":" + BlinkString(String(RelayOn.minute(), DEC), CurrentItem, 5);
LcdPrint(1, DData, CENTER);
DData = «Off:»;
switch (RelayMode)
{
case 3: //Daily
// DData = DData + " ";
if (CurrentItem > 5 && CurrentItem < 9) CurrentItem = 9;
break;
default:
DData = DData + BlinkString(String(RelayOff.year(), DEC), CurrentItem, 6) + "/" + BlinkString(String( RelayOff.month(), DEC), CurrentItem, 7) +
"/" + BlinkString(String( RelayOff.day(), DEC), CurrentItem, 8);
}
DData = DData + " " + BlinkString(String (RelayOff.hour(), DEC), CurrentItem, 9) + ":" + BlinkString(String(RelayOff.minute(), DEC), CurrentItem, 10);
LcdPrint(2, DData, CENTER);
LcdPrint(3, " ", CENTER);
}

boolean TRelayMenu::CheckDaily(void)
{
int TimeOn = 60 * int(RelayOn.hour()) + int(RelayOn.minute());
int TimeOff = 60 * int(RelayOff.hour()) + int(RelayOff.minute());
int NowTime = 60 * int(NowDate.hour()) + int(NowDate.minute());
boolean result; // true = время включения больше времени выключения
if ( TimeOn > TimeOff )
{
if (NowTime <= TimeOff || NowTime >= TimeOn ) result = true;
else result = false;
}
else
{
if (NowTime <= TimeOff && NowTime >= TimeOn ) result = true;
else result = false;
};
return result;

}

void RelayCheck (void)
{
boolean OnceBitCheck;
for (byte i = 0; i < 4; i++)
{
switch (RelayMenu[i]->RelayMode)
{
case 1: //relay 0n
digitalWrite(i + 10, LOW);

break;
case 2: //Once;
OnceBitCheck = (NowDate.unixtime() > RelayMenu[i]->RelayOn.unixtime() && NowDate.unixtime()<RelayMenu[i]->RelayOff.unixtime());

if (OnceBitCheck) RelayMenu[i]->OnceBit = true;
else if (RelayMenu[i]->OnceBit)
{
RelayMenu[i]->RelayMode = 0;
byte p_address = RelayMenu[i]->RelayNumber * 16;
EEPROM.write(p_address, RelayMenu[i]->RelayMode);
}
digitalWrite(i + 10, !OnceBitCheck);
break;
case 3: //Daily
digitalWrite(i + 10, !(RelayMenu[i]->CheckDaily()));
break;
default: //relay 0ff
digitalWrite(i + 10, HIGH);
}
}
}

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

Были опасения в точности хода часов, но пока существенного отклонения не заметил.

Благодарю за внимание.

Автор: viacheslav77

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js