Конечные автоматы, ATmega8 и бочка в теплице

Тема в разделе "Глядите, что я сделал", создана пользователем DetSimen, 23 окт 2018.

  1. DetSimen

    DetSimen Guest

    Анажды, этой весной, я построил на даче вторую теплицу для огурцов и почти сразу же, в полный рост, встала проблема её автоматизации. Канешна, чтоб полностью запустить весь технологический процесс выращивания без участия человека, нада много денег, времени и желания, а у меня совсем нету первого, и всего по чучуть второго и третьего, ведь профессиональнее всего я умею только лечь и лежать на любимом диванчике с пивом (или еще с чем покрепче по вечерам). Через некоторое время жена, увидев отсутствие у меня трудовой эрекции на теплицу, закатила скандальчик, потом еще один, потом устроила истерику с битьем меня шваброй поперек хребта, так что пришлось мне, кряхтя, принять вертикальное положение и, замазав вмятины на спине зеленкой, идти смотреть, что можно сделать с теплицей при полном ацуцтвии денег. Посмотрел. В голове, мыслеблоком, начала вертеца фраза из рекламы какого-то особо мерзкого на мой вкус кофя, типа "Начинать нада с малого" и я решил сделать сначала это малое, автоматизировать наполнение бочки водой. В теплице, унутре, стоит двухсотлитровая бочка, к которой подведен внешний краник с водой. За день вода в бочке хорошо прогревается от солнца и вечером тот, кто не лежит на диванчике, берет лейку и начинает поливать из нее тепленькой водичкой закуску того, кто охренел и лежит. По окончании полива, тот, кто поливал, открывает краник и стоит ждёт, пока наполнится бочка, попутно сам наполняясь злобой на того кто лежит, за напрасно потраченное время. Сразу краник открыть нельзя, потому что вода в трубах идёт ледяная, а закуску надо поливать именно тепленькой. Не знаю, так ли это на самом деле, может это всего лишь женские байки, возведенные в жоска детерминированный ритуал, но спорить с аццкой сотоной, а потом покупать новые швабры взамен сломанных, и лечить синяки на хребте, я не хочу, примем это как данность.
    Итак, алгоритм вырисовывается очень простой, следить за даччиком уровня, и когда уровень воды в бочке падает, открывать китайский клапан для её наполнения. Как только бочка наполница, клапан надо закрыть. Ах, да, бочка должна наполняться не сразу после падения уровня, а через время, которого точно хватит для поливания всех огурцов из лейки (водичка должна быть тёплой, помним?). То есть, минут 15 должно хватить. Итого, алгоритм чуть усложнился, после срабатывания даччика уровня, должно пройти 15 минут, прежде чем включица наполняющий клапан. Акей. С прошлой теплицы у мня какрас остался поплавковый даччик уровня, вот такой

    https://ru.aliexpress.com/store/pro...?spm=a2g0v.12010615.8148356.11.29552d18mcaGyU

    и нормально закрытый китайский клапан для воды, на 12 Вольт, примерна вот такой

    https://ru.aliexpress.com/item/AC22...expid=73e66589-d61d-455b-a3b3-197f2479d271-22

    к которому была собрана схема, вот такая


    valve12V.jpg


    Ну и пришла, наканец, пора писать машинные слова, чтоб всё это работало вместе. Из дополнений, хотелось бы поставить светодиодик какой-нить, который бы сигнализировал о нормальной работе или ошибке, и кнопочку, которой можно вручную включить воду, когда всё полил и уходишь. Тоись, после полива нажал кнопку, бочка пошла наливаца, не нажал, система ждёт 15 минут и сама наливает до уровня. Еще бы надо предусмотреть красный светодиод который бы сигнализировал об ошибке. Какие могут быть ошибки? Да мало ли, воды нет в линии, бочка прохудилась или её украла какая-нить скатина. В общем, если через определенное время (да те же 15 минут) датчик уровня не покажет, что бочка наполнена, то надо в любом случае выключить клапан наполнения, ну и желательно включить красенький светодиод.
     
    Feofan, SergeiL, Andrey12 и 4 другим нравится это.
  2. DetSimen

    DetSimen Guest

    Итак, аборудование. Входные устройства:

    Цифровой датчик уровня, активный уровень - ниский, то есть если переключился в ноль - бочку пора наполнять.

    Кнопка ручного наполнения, активный уровень низкий, замкнули на землю - пошла вода.

    Выходные устройства:

    Схема управления электроклапаном, активный уровень высокий, пришла 1 - клапан открылся.

    Зеленый светодиод, активный уровень высокий. Для большей информативности, гореть или мигать он будет в разных режимах по разному. В режиме ожидания коротко мигать с периодом 5 секунд, показывая, что ничего не зависло. В режиме ожидания 15 минут до включения клапана пусь мигает по полсекунды и постоянно горит, когда клапан открыт и льёца вода.

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

    Для экономии дырок на корпусе я взял один трехцветный светодиод.


    Программа.

    Так как на бесконечные автоматы денег у меня нет, реализовывать вот это вот всё я буду методом конечных автоматов. Состояний у устройства немного, можно перечислить их все.

    1. Unknown - неизвестное состояние, в нем устройство находится при первоначальной загрузке. Из этого состояния можно только выйти, например, при инициализации, а если устройство входит в это состояние, то это ошибка.

    2. Normal - нормальное состояние в котором устройство проводит большую часть времени. Клапан закрыт, ждём сигнала с даччика уровня, зеленый светодиод коротко мигает с периодом 5 секунд.

    3. WaterWait - состояние ожидания, когда уровень упал, но устройство ждёт 15 минут до включения воды. Зеленый светодиод мигает с перриодом полсекунды. Если в этом состоянии нажать кнопку ручного включения, устройство перейдет в состояние

    4. WaterFill - наполнение. Клапан открыт, вода наливается, зеленый светодиод горит постоянно. Ждет либо сигнала, что уровень полон, либо таймаута в 15 минут. Если случился таймаут, устройство плавно переходит в состояние

    5. Error - ошибка. Зеленый светодиод не горит, красный светодиод горит, устройство остановлено.


    осталось только формализовать

    enum class enumAppState: int8_t (Unknown=-1, Normal, WaterWait, WaterFill, Error); // Application States

    теперь нам нужна переменная, которая будет хранить текущее состояние

    enumAppState AppState;

    и функция, которая будет заведовать переходом из одного состояния в другое.

    void setAppState(enumAppState AppState);

    еще, нам нужны два таймера, один для мигания зеленым светодиодом, второй для отсчета времени до наполнения и во время наполнения. Можно уже нарисовать интерфейсную часть. Сразу говорю, все классы, используемые здесь, это не псевдокод, они реально существуют и живут пока вот в этом загончике https://github.com/DetSimen/UsefulClasses это необходимый минимум, выжатый из моей библиотеки, чтоб этот пример работал, их можно скопировать к себе в папку с праэктом, а можно сделать библиотеку, как угодно. Если надо, я подскажу как, када буду трезвый.
     
    Последнее редактирование модератором: 24 окт 2018
    SergeiL, parovoZZ и ИгорьК нравится это.
  3. DetSimen

    DetSimen Guest

    Код (C++):
    #include "Arduino.h"
    #include "Generics.h"
    #include "TTimerList.h"
    #include "DEF_Message.h"
    #include <Messages.h>
    #include "TButton.h"
    #include "TDevice.h"
    #include "TLed.h"
    #include "DSensor.h"


    #define ACTIVE_LOW  true        // инверсия
    #define ACTIVE_HIGH false        // нет инверсии

    #define WAIT_MINUTES 15              // таймаут наполнения/ожидания бочки

    enum class enumAppState : uint8_t { Unknown=0, Normal=1, WaterWait=2, WaterFill=3, Error=4 };

    void SetAppState(enumAppState newstate);   // самая главная функция смены состояния программы

    enumAppState AppState = enumAppState::Unknown;  // текущее состояние программы



    extern TTimerList TimerList;        // список таймеров, нам нужны будут два

    TMessageList MessageList(10);        // очередь сообщений на 10 штук


    // выходные устройства -------------------

    // зилёный светодиод, управляется +5 Вольтами на ноге 13
    //
    TLed ledGreen(13, ACTIVE_HIGH);  

    // красенький светодиод ошибки, управляется +5 на ноге 12
    //
    TLed ledError(12, ACTIVE_HIGH);  

    // клапан открытия воды, вернее, схема управления им, управляется +5В на ноге 10
    //
    TDevice waterGate(10, ACTIVE_HIGH);  


    // ---------------------------------------



    // вводные устройства  -------------------
    //
    // кнопка мануального включения насоса, замыкается на землю
    //
    TButton btnManual(A1, ACTIVE_LOW);  

    //
    // датчик уровня воды, замыкается на землю
    //
    TDSensor LevelSensor(A0, ACTIVE_LOW);

    // ---------------------------------------

    //  таймеры

    // таймер таймаута в 15 минут
    //
    THandle hWaitTimer=INVALID_HANDLE;              

    // таймер зеленого светодиода, в разных режимах настроен по разному
    //
    THandle hGreenLedTimer = INVALID_HANDLE;  


    вот. с интерфесной частью закончили, падём дальше

    главная функция конечного автомата - переход из одного состояния в другое


    void SetAppState(enumAppState newstate)
    {
        if (AppState == newstate) return;       // если мы уже в этом состоянии - выход
        AppState = newstate;            // запомним новое состояние


        switch (AppState)
        {
        case enumAppState::Unknown:        // если мы ПЕРЕШЛИ в это состояние, это ошибка
            SetAppState(enumAppState::Error);
            break;

        case enumAppState::Normal:  // в нормальном состоянии

            ledError.Off();     // светодиод ошибки выключен
            waterGate.Off();    // клапан выключен
            if (hGreenLedTimer == INVALID_HANDLE) hGreenLedTimer = TimerList.Add(tmrGreenLed, 500);
            TimerList.Reset(hGreenLedTimer); // работает таймер зеленого светодиода
            TimerList.Stop(hWaitTimer);     // таймер таймаута/наполнения - отключен
            break;

        case enumAppState::WaterWait:     // режим ожидания 15 минут, до наполнения бочки
            TimerList.SetNewInterval(hGreenLedTimer, 250); // зеленый светодиод мигает с периодом полсекунды

            if (hWaitTimer == INVALID_HANDLE) hWaitTimer = TimerList.AddMinutes(tmrTimeout, WAIT_MINUTES);
            TimerList.Reset(hWaitTimer); // включается таймер таймаута
            break;

        case enumAppState::WaterFill:    // режим наполнения бочки      

            if (hWaitTimer == INVALID_HANDLE) hWaitTimer = TimerList.AddMinutes(tmrTimeout, WAIT_MINUTES);
            TimerList.Reset(hWaitTimer);   // включается таймер таймаута
            TimerList.Stop(hGreenLedTimer);// останавливается таймер зеленого светодиода
            ledGreen.On();    // мигать не надо, он просто горит
            waterGate.On(); // включаеца клапан наполнения воды в бочке
            break;

        case enumAppState::Error:       // если ошибка
            TimerList.Stop();    // останавливаем все таймеры
            waterGate.Off();    // выключаем клапан наполнения
            ledGreen.Off();        // выключаем зеленый светлодиот
            ledError.On();        // включаем красный
            cli();            // входим в клинч
            abort();        // выход только по Reset
            break;
        default:
            break;
        }
    }

    сопсно, это и есть основополагающий лагаритм работы программы, остальные функции весьма вспомогательны.

    функции отклика на события таймеров


    // зеленый светодиод, в нормальном режиме просто попискивает светом, 200 мс горит, 4700 мс не горит
    // в ждущем - мигает с периодом полсекунды, см. выше case enumAppState::WaterWait:

    void tmrGreenLed(void) {
        ledGreen.Toggle();
        if (AppState==enumAppState::Normal)  TimerList.SetNewInterval(hGreenLedTimer, ledGreen.isOn() ? 200 : 4700);
    }

    // таймер таймаута, когда кончица 15 минут в любом режиме, просто отправит в очередь сапщение msg_Timeout

    void tmrTimeout(void) {
        SendMessage(msg_Timeout);
    }

    всеми любимая setup()

    void setup() {
        Serial.begin(115200);                // на всякий случай, настроим Serial  
        Serial << F("Program started...") << eoln;    // хоть он и не нужен

    // настроим сенсоры

        LevelSensor.ReadInterval = 150;    // читать LevelSensor не чаще чем раз в четверть секунды
        btnManual.ReadInterval = 50;    // читать кнопку не чаще чем раз в 50 миллисекунд
       
        SetAppState(enumAppState::Normal); // сменить состояние c Unknown на Normal

        // и все
        // рабочий процесс запущен
    }

    и loop(), где вертица бесконечная карусель очереди сапщений

    void loop() {


        LevelSensor.Read();   // здесь читаем сенсоры с частотой ReadInterval каждого
        btnManual.Read();


        if (!MessageList.Availiable()) return;  // если сообщений нет - выходим

        TMessage msg = MessageList.GetMessage(); // иначе берем сообщение


        switch (msg.Message)   // и давай им крутить да размахивать
        {
        enumSensorState astate;


        case msg_SensorStateChanged:            // это прислал датчик уровня

            if (msg.ClassID != LevelSensor.getID()) break;

            astate = enumSensorState(msg.LoParam);

            // если бочка наполнилась - переходим в Normal,
            // если опустела - в WaterWait

            if (astate == enumSensorState::NotActive) SetAppState(enumAppState::Normal);
            else
            if (astate == enumSensorState::Active) SetAppState(enumAppState::WaterWait);
           
            break;

        case msg_Timeout:    // это прислал 15-ти минутный таймер
           
            // если мы были в состоянии WaterWait - переходим к наполнению бочки, в WaterFill
            // а если были в WaterFill, то бочка за 15 минут не наполнилась, это ошибка, или воды нету в магистрали,
            // или бочку с даччиком спёрли. переходим в состояние Error

            if (AppState == enumAppState::WaterWait) SetAppState(enumAppState::WaterFill);
            else
                 if (AppState == enumAppState::WaterFill) SetAppState(enumAppState::Error);
            break;

        case msg_KeyPress:    // это прислала кнопка

            // если мы были в режиме WaterWait - переходим к наполнению (WaterFill)
            // а если в WaterFill, то переходим в Normal (всё отключаем и ждём)
       
            if (AppState == enumAppState::WaterWait) {
                SetAppState(enumAppState::WaterFill);
                break;
            }
           
            if (AppState == enumAppState::WaterFill) {
                SetAppState(enumAppState::Normal);
            }

            break;

        case msg_LongKey:  // это длинное нажатие (дольше 750мс держим), присылает кнопка
           
            // просто, мимо всех даччиков включить или выключить наполнение бочки

            if (AppState!=enumAppState::WaterFill)
                 SetAppState(enumAppState::WaterFill);
            else
                 SetAppState(enumAppState::Normal);

            break;

        default:    // сюда попадаем, если сообщение не обрабатывается в данном контексте
            break;
        }
    }
     
     
    SergeiL, ИгорьК и DIYMan нравится это.
  4. DetSimen

    DetSimen Guest

    устройство было собрано на Atmega8, так как

    Program size: 5 222 bytes (used 73% of a 7 168 byte maximum) (31,69 secs)
    Minimum Memory Usage: 377 bytes (37% of a 1024 byte maximum)

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

    На след. сезон будем городить автополив.
     
    Airbus и ИгорьК нравится это.
  5. DetSimen

    DetSimen Guest

    полный текст для копирования в свой проект, без воды и рассуждений
    Код (C++):
    #include "Arduino.h"
    #include "Generics.h"
    #include "TTimerList.h"
    #include "DEF_Message.h"
    #include <Messages.h>
    #include "TButton.h"
    #include "TDevice.h"
    #include "TLed.h"
    #include "DSensor.h"


    #define ACTIVE_LOW  true
    #define ACTIVE_HIGH false

    #define WAIT_MINUTES 15            // таймаут наполнения бочки

    enum class enumAppState : uint8_t { Unknown=0, Normal=1, WaterWait=2, WaterFill=3, Error=4 };

    void SetAppState(enumAppState newstate);

    enumAppState AppState = enumAppState::Unknown;



    extern TTimerList TimerList;

    TMessageList MessageList(10);


    // выходные устройства -------------------

    TLed ledGreen(13, ACTIVE_HIGH);
    TLed ledError(12, ACTIVE_HIGH);

    TDevice waterGate(10, ACTIVE_HIGH);


    // ---------------------------------------



    // вводные устройства  -------------------

    TButton btnManual(A1, ACTIVE_LOW);   // кнопка мануального включения насоса

    TDSensor LevelSensor(A0, ACTIVE_LOW);  // датчик уровня воды

    // ---------------------------------------


    THandle hWaitTimer=INVALID_HANDLE;            // таймер таймаута
    THandle hGreenLedTimer = INVALID_HANDLE;    // таймер зеленого светодиода


    void tmrGreenLed(void) {
        ledGreen.Toggle();
        if (AppState==enumAppState::Normal)  TimerList.SetNewInterval(hGreenLedTimer, ledGreen.isOn() ? 200 : 4700);
    }

    void tmrTimeout(void) {
        SendMessage(msg_Timeout);
    }



    void setup() {
        Serial.begin(115200);
        Serial << F("Program started...") << eoln;

        LevelSensor.ReadInterval = 150;    // читать LevelSensor не чаще чем раз в четверть секунды
    //    LevelSensor.onStateChanged = levelChanged;

        btnManual.ReadInterval = 100;    // читать кнопку раз в 100 миллисекунд
       
        SetAppState(enumAppState::Normal);

        digitalWrite(10, LOW);

    }


    void loop() {


        LevelSensor.Read();
        btnManual.Read();


        if (!MessageList.Availiable()) return;

        TMessage msg = MessageList.GetMessage();


        switch (msg.Message)
        {
        enumSensorState astate;


        case msg_SensorStateChanged:
            if (msg.ClassID != LevelSensor.getID()) break;

            astate = enumSensorState(msg.LoParam);

            if (astate == enumSensorState::NotActive) SetAppState(enumAppState::Normal);
            else
            if (astate == enumSensorState::Active) SetAppState(enumAppState::WaterWait);
           
            break;

        case msg_Timeout:
           
            if (AppState == enumAppState::WaterWait) SetAppState(enumAppState::WaterFill);
            else
                 if (AppState == enumAppState::WaterFill) SetAppState(enumAppState::Error);
            break;

        case msg_KeyPress:
       
            if (AppState == enumAppState::WaterWait) {
                SetAppState(enumAppState::WaterFill);
                break;
            }
           
            if (AppState == enumAppState::WaterFill) {
                SetAppState(enumAppState::Normal);
            }

            break;

        case msg_LongKey:
           
            if (AppState!=enumAppState::WaterFill)
                 SetAppState(enumAppState::WaterFill);
            else
                 SetAppState(enumAppState::Normal);

            break;

        default:
           
            break;
        }
    }

    void SetAppState(enumAppState newstate)
    {
        if (AppState == newstate) return;
        AppState = newstate;


        switch (AppState)
        {
        case enumAppState::Unknown:
            SetAppState(enumAppState::Error);
            break;

        case enumAppState::Normal:  // в нормальном состоянии мы просто ждем сигнала с даччика уровня, попутно мигая светодиодиком, что всё намайна

            ledError.Off();
            waterGate.Off();
            if (hGreenLedTimer == INVALID_HANDLE) hGreenLedTimer = TimerList.Add(tmrGreenLed, 500);
            TimerList.Reset(hGreenLedTimer);
            TimerList.Stop(hWaitTimer);
            break;

        case enumAppState::WaterWait:
            TimerList.SetNewInterval(hGreenLedTimer, 250);
            if (hWaitTimer == INVALID_HANDLE) hWaitTimer = TimerList.AddMinutes(tmrTimeout, WAIT_MINUTES);
            TimerList.Reset(hWaitTimer);
            break;

        case enumAppState::WaterFill:
            if (hWaitTimer == INVALID_HANDLE) hWaitTimer = TimerList.AddMinutes(tmrTimeout, WAIT_MINUTES);
            TimerList.Reset(hWaitTimer);
            TimerList.Stop(hGreenLedTimer);
            ledGreen.Off();
            waterGate.On();

            break;
        case enumAppState::Error:
            TimerList.Stop();
            waterGate.Off();
            ledGreen.Off();
            ledError.On();
            cli();
            abort();
            break;
        default:
            break;
        }
    }
     
     
    Airbus и ИгорьК нравится это.
  6. parovoZZ

    parovoZZ Гуру

     
  7. DetSimen

    DetSimen Guest

    Не могу с этим не согласица.
     
  8. ИгорьК

    ИгорьК Гуру

    Дет! Мой алгаритм для экономии на дадчегах.

    Стемнело - читатель темна открыл кран. Патамушта в тимноте нихто неполивает уже.
    В бочке стаит афигеннае устройства контроля перелива - хрень от унитаза.
    Через час читатель тимна закрыл кран.

    Профит на экономии датчегов уровня и провадах.
    Убыток разарение! на пакупке фотарезистора.
     
    Последнее редактирование: 23 окт 2018
    DetSimen нравится это.
  9. DetSimen

    DetSimen Guest

    Некошерно. К ей же еще автополив прикручивать потом, с насосиком. Это ж только начало.
     
  10. ИгорьК

    ИгорьК Гуру

    Дело хозяинское. Автополиф кагбы никак не связан с заполнением бочки.
    Да более того. Поливать нада на закате. Или расвете.

    Смотрящий за сонцем уловил сумерки и аткрыл кран полива на время. Потом закрыл ево и открыл кран наполнения.

    Ни тебе датчиков уровня, ни чисов на микрасхемах. Все дешева и проста.
     
    DetSimen нравится это.
  11. b707

    b707 Гуру

    а если на датчик бабочка села? или птичка накакала?
     
  12. parovoZZ

    parovoZZ Гуру

    а если кирпич упадет?
     
    ИгорьК нравится это.
  13. ИгорьК

    ИгорьК Гуру

    Это более вероятно.
     
  14. ИгорьК

    ИгорьК Гуру

    Если у деда там много какают, надо ставить датчегов на один больше чем какающих.
     
  15. b707

    b707 Гуру

    "кирпич просто так не падает" (с) а бабочка - божья тварь, летает где вздумается
     
  16. Andrey12

    Andrey12 Гик

    А почему не так управлять? Деталек меньше.
    Интересуюсь потому как пытаюсь принести свет перепелками в клетку в виде светодиодной ленты. У себя пока сделал как на схеме ниже, в брудере такая схема успешно работает. Но может чего не знаю?
    [​IMG]
     
  17. DetSimen

    DetSimen Guest

    А у мня на тот момент N-канальные logic-level полевики все покончались. :)
    А NDP6020P и по сейчас хоть анусом ешь. :)
     
  18. Andrey12

    Andrey12 Гик

    Ну если противопоказаний нет то сэкономлю на биполярнике :)
     
  19. DetSimen

    DetSimen Guest

    собирай по левой схеме, только полевик возьми не IRL, а IRLML накойнить, IRL от +5 Вольт всёже плохо открываются
     
  20. Йа тащусь от IRLML6346, для фсяких шимоф, самое то. Только на них сильно мощную нагрузку не повесить(