Arduino

технологии

Контроллер для зимнего курятника

Введение

Winter chicken coop

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

Для решения был выбран наиболее просто и часто использующийся вариант: разместить в курятнике инфракрасную лампу (по сути обычную лампу накаливания красного цвета на 100Вт). Так как курятник был хорошо утеплен, такой лампы вполне достаточно для обогрева всего курятника (курятник имеет размер 120 см по каждой стороне).

Процесс постройки утепленного курятника можно посмотреть тут.

Однако включать и выключать лампочку вручную не очень удобно, особенно в 5 часов утра. Также хочется следить за ночной температурой в курятнике, без необходимости вставать ночью и идти смотреть на термометр. Для всего этого был разработан контроллер на Arduino Uno:

Winter chicken coop controller

Его возможности:

  • Автоматически включать/выключать обогрев курятника в определенное время 1 или 2 раза в сутки
  • Включать обогрев только при температуре ниже заданного порогового значения
  • Ручной режим управление обогревом
  • Отображения текущей температуры и влажности
  • Просмотр изменения значений температуры и влажности за последние сутки
  • Все управление делается через web-интерфейс, что позволяет делать это удаленно

Теперь расскажу чуть подробнее. Сердцем контроллера является плата Arduino Uno с ethernet контроллером w5100. Список компонентов ограничивается твердотельным реле для управления лампой накаливания, датчиком температуры/влажности dht22 и модулем часов реального времени RTC.

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

Кроме вышеописанного модуля Arduino Uno и компонентов понадобятся:

  • Красная лампа на 100Вт или любая другая подходящая лампа, которая способна выполнять роль небольшого обогревателя
  • Корпус (отлично подходит обычная распределительная коробка, способная вместить все компоненты)
  • Батарейка CR2032 для модуля часов
  • Несколько Wago-коннекторов для удобного соединения проводов, но не обязательно, можно обойтись пайкой
  • Инструменты: паяльник и обжимные клещи для rj-45, однако можно обойтись и скрутками и готовым патч-кордом :)

Сборка

Собираем все по следующей схеме:

Chicken coop controler schema

Код я выложил на github: https://github.com/arduinotech/chicken_uno_ethernet/.

Так как я постоянно дорабатываю код, дам рекомендации по текущей версии кода (main.ino):

#include <Arduino.h>
#include <SPI.h>
#include <avr/pgmspace.h>

#include "WebServer.h"
#include "Config.h"
#include "hardware/SensorDHT22.h"
#include "hardware/Lamp.h"
#include "hardware/Clock.h"
#include "StringParser.h"
#include "SettingsStorage.h"

// TODO remove for production
#include "MemoryFree.h"

#define DEBUG_MEM(text) Serial.print(F("freeMemory() = ")); Serial.print(freeMemory()); Serial.print(F(" - ")); Serial.println(text);

WebServer webServer(PORT);
SensorDHT22 dht(DHT22_PIN);
Lamp lamp(LAMP_PIN);
Clock clock;
SettingsStorage settingsStorage;

LogEvent logEvents[LOG_SIZE];
uint8_t logEventsCount = 0;

void lampOnOrOffIfNeed()
{
    if (settingsStorage.getManual()) {
        if (settingsStorage.getLamp() && !lamp.isOn()) {
            lamp.on();
        } else if (!settingsStorage.getLamp() && lamp.isOn()) {
            lamp.off();
        }
        return;
    }

    if ((settingsStorage.getTempToOn() != String("")) && (dht.getTemp() > settingsStorage.getTempToOn().toInt())) {
        if (lamp.isOn()) {
            lamp.off();
        }
        return;
    }

    uint16_t currentTime = clock.getCurrentTimeInMinutes();
    if ((settingsStorage.getTimeToOn1() != String("")) && (settingsStorage.getTimeToOff1() != String(""))) {
        uint16_t timeToOn = Clock::stringTimeToMinutes(settingsStorage.getTimeToOn1());
        uint16_t timeToOff = Clock::stringTimeToMinutes(settingsStorage.getTimeToOff1());
        if ((timeToOn <= currentTime) && (currentTime < timeToOff)) {
            if (!lamp.isOn()) {
                lamp.on();
            }
            return;
        }
    }

    if ((settingsStorage.getTimeToOn2() != String("")) && (settingsStorage.getTimeToOff2() != String(""))) {
        uint16_t timeToOn = Clock::stringTimeToMinutes(settingsStorage.getTimeToOn2());
        uint16_t timeToOff = Clock::stringTimeToMinutes(settingsStorage.getTimeToOff2());
        if ((timeToOn <= currentTime) && (currentTime < timeToOff)) {
            if (!lamp.isOn()) {
                lamp.on();
            }
            return;
        }
    }

    if (lamp.isOn()) {
        lamp.off();
    }
}

HtmlParams processor(String url)
{
    DEBUG_MEM(F("processor begin"))

    Serial.print(F("Request: "));
    Serial.println(url);

    if (String(F("/")) == url) {
        return {clock.getCurrentDateTime(),
                dht.getTemp(),
                dht.getHumi(),
                lamp.isOn(),
                settingsStorage.getManual(),
                settingsStorage.getTimeToOn1(),
                settingsStorage.getTimeToOff1(),
                settingsStorage.getTimeToOn2(),
                settingsStorage.getTimeToOff2(),
                settingsStorage.getTempToOn(),
                logEvents,
                logEventsCount};
    }

    if (!settingsStorage.getManual() && (url.indexOf(F("m=on")) != -1)) {
        settingsStorage.setManual(true);
    }

    if (settingsStorage.getManual() && (url.indexOf(F("m=on")) == -1) && (url.indexOf(F("s=")) != -1)) {
        settingsStorage.setManual(false);
        settingsStorage.setLamp(false);
    }

    if (settingsStorage.getManual() && !lamp.isOn() && (url.indexOf(F("l=on")) != -1)) {
        lamp.on();
        settingsStorage.setLamp(true);
    }

    if (settingsStorage.getManual() && (url.indexOf(F("m=on")) != -1) && (url.indexOf(F("l=on")) == -1) && (url.indexOf(F("f=")) == -1)) {
        lamp.off();
        settingsStorage.setLamp(false);
    }

    int pos;

    pos = url.indexOf(F("n="));
    if (pos != -1) {
        settingsStorage.setTimeToOn1(StringParser::parseTime(pos, url));
    }

    pos = url.indexOf(F("f="));
    if (pos != -1) {
        settingsStorage.setTimeToOff1(StringParser::parseTime(pos, url));
    }

    pos = url.indexOf(F("o="));
    if (pos != -1) {
        settingsStorage.setTimeToOn2(StringParser::parseTime(pos, url));
    }

    pos = url.indexOf(F("g="));
    if (pos != -1) {
        settingsStorage.setTimeToOff2(StringParser::parseTime(pos, url));
    }

    pos = url.indexOf(F("t="));
    if (pos != -1) {
        settingsStorage.setTempToOn(StringParser::parseTemp(pos, url));
    }

    lampOnOrOffIfNeed();

    DEBUG_MEM(F("processor end"))
    return {clock.getCurrentDateTime(),
            dht.getTemp(),
            dht.getHumi(),
            lamp.isOn(),
            settingsStorage.getManual(),
            settingsStorage.getTimeToOn1(),
            settingsStorage.getTimeToOff1(),
            settingsStorage.getTimeToOn2(),
            settingsStorage.getTimeToOff2(),
            settingsStorage.getTempToOn(),
            logEvents,
            logEventsCount};
}

void setup()
{
    Serial.begin(115200);

    DEBUG_MEM(F("setup begin"))

    // hardware
    dht.init();
    lamp.init();
    clock.init();

    settingsStorage.init();

    if (settingsStorage.getManual() && settingsStorage.getLamp()) {
        lamp.on();
    }

    DEBUG_MEM(F("setup hardware inited"))

    // network
    byte mac[] = MAC;
    byte ip[] = IP;
    webServer.init(mac, ip, processor);

    DEBUG_MEM(F("setup end"))
}

void loop()
{
    uint32_t now = millis();
    static uint32_t lampOnOffIfNeedLastCall = 0;
    static uint8_t lastHourSaveData = 1;

    if (lampOnOffIfNeedLastCall > now) {
        lampOnOffIfNeedLastCall = 0;
    }

    if ((now - lampOnOffIfNeedLastCall) > CHECK_INTERVAL) {
        lampOnOrOffIfNeed();
        uint8_t hour = clock.getCurrentHour();
        if ((hour % 2 == 0) && (hour != lastHourSaveData)) {
            LogEvent newEvent = {clock.getCurrentUnixtime(), dht.getTemp(), dht.getHumi(), lamp.isOn()};
            if (logEventsCount < LOG_SIZE) {
                logEvents[logEventsCount] = newEvent;
                logEventsCount++;
            } else {
                for (int i = 0; i < LOG_SIZE - 1; i++) {
                    logEvents[i] = logEvents[i + 1];
                }
                logEvents[LOG_SIZE - 1] = newEvent;
            }
            lastHourSaveData = hour;
        }
    }

    webServer.listening();
}

Код написан в ООП-стиле, для удобства работа с каждым компонентом реализована в отдельном класса.

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

В отдельном файле Config.h можно задать ip-адрес и порт, по которым будет доступно управление контроллером через браузер.

Стоит обратить внимание, что библиотека реализации web-сервера расходует довольно много оперативной памяти, поэтому в коде присутствует вывод количества свободной памяти в отладочный сериальный порт. Этот код можно убрать, однако если вы планируете как-то самостоятельно дорабатывать функционал, рекомендую его оставить (подключается класс работы с памятью на строчек 14 (MemoryFree.h), на строчке 16 определен макрос для короткого вывода памяти (пример вызова: DEBUG_MEM(F("processor begin"))), ну и в определенных места кода разбросаны вызова этого макроса, например в строчке 75.

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

Настройки сохраняются в энергонезависимой памяти EEPROM, чтобы при внеплановом отключения электричества они не "слетели", поэтому при первом запуске настройки будут содержать некорректные значения - требуется просто 1 раз сохранить корректные настройки. Либо в файле SettingsStorage.cpp в функции init() раскомментировать блок начальных установок значений, скомпилировать и прошить контроллер, после чего закомментировать этот блок и прошить код повторно.

Управление модулем

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

  • Текущее время для проверки корректности работы модуля часов
  • Текущую температуру и влажность
  • Первый интервал включения и выключения лампы накаливания
  • Второй необязательный интервал включения и выключения лампы накаливания (можно не задавать)
  • Граничное значение температуры (проверяется только при включении лампы, если значение текущей температуры больше этого значения, то лампа включена не будет), также можно не задавать
  • Переход в ручной режим включения/выключения лампы