Автоматическое включение и выключение подсветки аквариума

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

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

К сожалению, камера телефона подстраивается автоматически. Поэтому не видно, что свет включается и выключается очень плавно.

Добавим ручное включение/выключение. Может пригодиться. Алгоритм простой: после ручного вкл/выкл устройство возвращается в автоматический режим по расписанию. Т.е. если ночью включили свет, вечером он отключится по расписанию.

Я использую 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 порту и если есть, то обрабатываем его, а потом, по очереди запускаем обработку наших модулей. Всё…

Я не уверен, что кто-нибудь дочитал до конца, но если это случилось, то я рад. 🙂 Задавайте вопросы. По ним я пойму, стоит ли разбирать эту тему дальше.