В сети много статей, посвященных освещению аквариума. Чтобы исключить влияние человеческого фактора мы сделаем устройство, обеспечивающее автоматическое включение и выключение света.
Т.к. наш материал учебный, некоторые вещи мы сделаем не оптимально для того, чтобы разобрать новые для нас участки кода.
Добавим ручное включение/выключение. Может пригодиться. Алгоритм простой: после ручного вкл/выкл устройство возвращается в автоматический режим по расписанию. Т.е. если ночью включили свет, вечером он отключится по расписанию.
Я использую ESP32. Для этого контроллера задача слишком простая. Пусть ЕСПешка делает ещё что-нибудь полезное, в свободное от включения и выключение света время. На видео видно мигание синего светодиода. Это ещё одна полезная функция. После включения освещения включается периодическое мигание светодиода, что говорит о том, что рыбок ещё не кормили. В этом режиме устройство включения (может быть простая кнопка) при однократном “нажатии” выключает режим индикации. Для подтверждения отключения индикации светодиод мигает 5 раз.
Почему я пишу “устройство включения”? 🙂 У меня аквариум стоит рядом с кроватью и искать в темноте кнопку мне показалось неудобным. Поэтому я использовал инфракрасный аналоговый датчик препятствия. Для включения / выключения достаточно провести рукой над устройством.
Но и это не задача для ESP. Я добавил датчик измерения атмосферного давления BMP-180. Знать величину атмосферного давления интересно, но значительно более информативна динамика изменения давления во времени. Хранить данные будем в структурах-контейнерах pair. Эти структуры позволяют хранить пары разнородных значений – то, что нам и надо. Мы будем сохранять время измерения и величину давления. Для сохранения 100 последних значений сохраненных в pair, будем использовать класс-контейнер list из стандартной библиотеки. С помощью класса list организуем стек по принципу FIFO (first-in, first-out) первым вошел – первым вышел.
График атмосферного давления будет выглядеть так, как показано на картинке. Для построения графика используем замечательную библиотеку google charts.
Датчик температуры находится в составе модуля BMP-180. Выведем и его значения.
Подключение к локальной сети и привязка по времени
ESP32 содержит “на борту” модуль WIFI, что позволяет нам подключиться к локальной сети. Для подключения используем библиотеку WiFi. Подключение очень простое:
Serial.print("Connecting to "); Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected..!");
Serial.print("Got IP: "); Serial.println(WiFi.localIP());
Если мы правильно указали логин и пароль (ssid и password) наша плата будет подключена к wifi.
Для привязки к реальному времени используем NTP сервер. Можете проверить ваши часы на странице сервера. Если нет wifi, можно использовать модуль часов, но это значительно сложнее и дороже. Я использовал библиотеку NTPClient. Подключив библиотеку мы можем получать по запросу текущее время.
#include <NTPClient.h>
#include <time.h>
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", 10800, 600000); //инициализация
timeClient.begin(); //запуск в setup()
timeClient.update(); //обновление данных
timeClient.getMinutes(); //получение минут
timeClient.getHours(); //получение часов
Вот так просто можно получать значения точного времени.
Включение и выключение освещения
Для управления светом я использую немого модифицированный наш класс SoftLed. Основное управление выполняет класс Akvalight.
class Akvalight : public timer {
protected:
int pin;
NTPClient *timeClient; //часы реального времени
Blink *blink; //светодиодный индикатор
SoftLed *softLed; //управление светом
uint16_t timeOn; //минуты включения
uint16_t timeOff; //минуты выключения
bool status; //состояние
unsigned long int interval; //период между опросами часов мс.
public:
Akvalight(int, NTPClient*, Blink*);
~Akvalight();
void cycle();
bool getStatus();
String getStat();
bool ledToggle();
};
На что стоит обратить внимание: управление ШИМ у ESP32 отличается от Arduino. По этому на Arduino данный код не заработает. В конструкторе класса первая переменная обозначена как int pin, но в данной программе я передаю номер канала. В конструкторе присваиваются числовые значения. Это сделано исключительно для наглядности.
#include "akvalight.h"
#include "softLed.h"
#include "timer.h"
//*********************************
Akvalight::Akvalight(int pin, NTPClient *timeClient, Blink *blink):timer(){
softLed = new SoftLed(pin, 255, false, 50);
this->blink = blink;
this->timeClient = timeClient;
interval = 2000;
timeOn = 8*60 + 0; //время включения 8:00
timeOff = 22*60 + 00; //время включения 22:00
status = false;
timeClient->update();
}
//*********************************
Akvalight::~Akvalight(){
delete softLed;
}
//*********************************
bool Akvalight::ledToggle(){
return softLed->ledToggle(); //переключение света на противоположное
}
//*********************************************
void Akvalight::cycle(){
if(!this->getTimer()){
this->setTimer(interval);
uint16_t TimeNow = timeClient->getMinutes() + timeClient->getHours() * 60;
if(TimeNow >= timeOn && TimeNow < timeOff){
if(!status){
status = true;
softLed->ledOn(); //включение света
blink->blinkOn(); //включение индикатора кормёжки
}
} else {
if(status){
status = false;
softLed->ledOff(); //выключение света
}
}
}
softLed -> cycle();
}
//******************************************
bool Akvalight::getStatus(){
return status;
}
//******************************************
String Akvalight::getStat(){
return softLed->getStat()? "Включен" : "Выключен";
}
Индикатор
Индикатор представляет собой светодиод, подключенный к 33 пину. Очень просто :-). Однако, для примера использования наследования, написал класс Blink, являющегося наследником класса SoftLed.
class Blink : public SoftLed {
bool statBlink; //включение режима мигания
uint8_t nt; //номер цикла
timer *TimerBlink;
protected:
uint16_t timePer; //период мигания
uint8_t nBlink; //число миганий в одном периоде
public:
Blink(int pin, uint16_t, uint8_t);
~Blink();
void blinkOn(){statBlink = true;}
void blinkOff(){statBlink = false;}
bool getStat(){return statBlink;}
void cycle();
bool action() override;
void setNblink(uint8_t);
};
Класс управляет включением и выключением режимом индикации.
#include "blink.h"
#include "softLed.h"
#include "timer.h"
Blink::Blink(int pin, uint16_t tP, uint8_t nB):SoftLed(pin, 5, false, 30){
statBlink=false;
TimerBlink = new timer(tP); //период миганий
timePer = tP;
nBlink = nB * 2; //число периодов включен и выключен.
nt = 0;
ledOff();
}
//*****************************
Blink::~Blink(){
delete TimerBlink;
}
//*****************************
void Blink::cycle(){
if(statBlink && !TimerBlink->getTimer()){
TimerBlink->setTimer(timePer);
nt = nBlink-1;
ledOn();
}
SoftLed::cycle();
}
//*****************************
void Blink::setNblink(uint8_t n){
ledOn();
nt = n * 2 - 1;
}
//*****************************
bool Blink::action(){
if(nt){
ledToggle();
nt--;
}
// if(!nt)statBlink = false;
return nt;
}
//****************************
Данный класс позволяет включить режим постоянного мигания или включения разового блока, состоящего из заданного числа миганий.
Измерение атмосферного давления
Класс Pressure, обращаясь к датчику BMP-180, получает значения давления и температуры и сохраняет их в контейнере arP.
class Pressure : public timer{
NTPClient *tC;
Adafruit_BMP085 *bmp;
float pressure;
float temp;
uint16_t n;
std::list<std::pair<String, float>> arP;
const char *dayName[7]={"Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"};
public:
Pressure(NTPClient*, Adafruit_BMP085 *bmp);
float readTemperature();
float readPressure();
bool cycle();
std::list<std::pair<String, float>>& getArray();
friend void getStat();
};
Для построения графика используется функция getArray(), возвращающая ссылку на массив значений arP. При возвращении ссылки не производится копирования массива в промежуточную переменную, что экономит память и время работы.
#include "Pressure.h"
const int ARP_SIZE = 100;
const int TIME_PRESSURE = 60000;
//***************************************
Pressure::Pressure(NTPClient* tC, Adafruit_BMP085 *bmp): timer(), tC(tC), bmp(bmp) {
pressure = 0;
temp = 0;
}
//***************************************
bool Pressure::cycle(){
if(!getTimer()){
setTimer(TIME_PRESSURE); //массив обновляется раз в TIME_PRESSURE миллисекунд
temp = bmp->readTemperature();//sensors.getTempC(sensor2);
pressure = bmp->readPressure() * 0.00750063755419211;
tC->update();
uint8_t min = tC->getMinutes();
uint8_t hour = tC->getHours();
String sTime = (String)dayName[tC->getDay()] + " " + (String)hour + ":" + (String)min;
if(arP.size() >= ARP_SIZE){
arP.pop_front(); //если массив полон, удаляем первое значение
}
arP.push_back({sTime, pressure}); //добавляем в массив новое значение
}
return true;
}
//***************************************
float Pressure::readTemperature(){return temp;}
//***************************************
float Pressure::readPressure(){return pressure;}
//***************************************
std::list<std::pair<String, float>>& Pressure::getArray(){
return arP;
}
Я обновляю массив значений раз в час: if(min == 0 && seconds == 0 || arP.size() == 0). Это можно было реализовать и другими методами.
WEB сервер
Для отображения значений температуры, давления и построения графика изменения атмосферного давления мы используем библиотеку WebServer.
#include <WebServer.h>
WebServer server(80);
//setup()
server.on("/", handle_OnConnect);
server.on("/getstat", getStat);
server.onNotFound(handle_NotFound);
server.begin();
//loop()
server.handleClient();
//вывод HTML страницы
server.send(200, "text/html", SendHTML());
//асинхронная передача динамических данных
server.send(200, "application/json", output);
Описание того, как происходит генерация страницы и вывод данных заслуживает отдельного поста. Для построения страницы используются:
- Axios – асинхронная передача данных
- Vue – реактивное построение страницы
- Google cherts – построение графика
- HTML + CSS – как без них?
«Кнопка»
То, что я использовал аналоговый датчик препятствия – одно из возможных решений. Было интересно посмотреть, как ESP работает с аналоговым входом. В сети не раз встречал нарекания.
class IRButton {
private:
uint8_t pin;
bool stat;
int aValue;
int aValueOld; //предыдущее значение
float alfa; //коэффициент сглаживания
timer *Timer;
uint16_t dT; //частота опроса датчика
Akvalight *akvalight;
Blink *blink;
int filter(int);
protected:
public:
IRButton(uint8_t, uint16_t, Akvalight*, Blink *blink);
~IRButton();
bool cycle();
};
Этот модуль обрабатывает аналоговые значения полученные с датчика. Для подавления помех включено простейшее экспоненциальное сглаживание.
IRButton::IRButton(uint8_t pin, uint16_t dT, Akvalight *akvalight, Blink *blink){
this->pin = pin;
this->blink = blink;
stat = 0;
alfa = 0.3;
this->dT = dT;
pinMode(pin, INPUT);
aValueOld = 3000;
aValue = 3000;
Timer = new timer();
this->akvalight = akvalight;
}
//*******************************
IRButton::~IRButton(){
delete Timer;
}
//*******************************
int IRButton::filter(int av){
return aValueOld = alfa*av + (1-alfa) * aValueOld;
}
//*******************************
bool IRButton::cycle(){
aValue = filter(analogRead(pin)); //получение значений с датчика
if(!Timer->getTimer()){
Timer->setTimer(dT);
if(!stat && aValue < 1000)stat = true;
else if(stat && aValue > 1000){
if(blink->getStat()){
blink->blinkOff(); //отключение индикатора кормёжки
blink->setNblink(5); //включение 5-и миганий
} else {
blink->setNblink(1 + akvalight -> ledToggle()); //переключение состояния освещения
}
stat = false;
}
}
return stat;
}
Основная программа. Цикл loop
void loop(void) {
server.handleClient();
blink->cycle();
pressure->cycle();
akvalight->cycle();
irbutton->cycle();
}
Собственно говоря рассказывать то и нечего… Мы проверяем наличие соединения на 80 порту и если есть, то обрабатываем его, а потом, по очереди запускаем обработку наших модулей. Всё…
Я не уверен, что кто-нибудь дочитал до конца, но если это случилось, то я рад. 🙂 Задавайте вопросы. По ним я пойму, стоит ли разбирать эту тему дальше.