КОРЗИНА
магазина
8 (499) 500-14-56 | ПН. - ПТ. 12:00-18:00
ЛЕСНОРЯДСКИЙ ПЕРЕУЛОК, 18С2, БЦ "ДМ-ПРЕСС"

Автоматизированная нормализация EC своими руками

Общие сведения

В этом проекте мы соберём систему автоматизированной нормализации электропроводности раствора.

Видео

Редактируется...

Нам понадобится

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

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

  • Arduino IDE 1.8.19
  • ESP32 core (устанавливается в Arduino IDE, пункт меню Инструменты->Плата->Менеджер плат)
  • Библиотека LiquidCrystal_I2C (Устанавливается в Arduino IDE, пункт меню "Скетч"->"Подключить <иблиотеку"->"Менеджер библиотек" или на клавиатуре <CTRL>+<SHIFT>+<L>)
  • Библиотека Button2 (Устанавливается в Arduino IDE, пункт меню "Скетч"->"Подключить библиотеку"->"Менеджер библиотек" или на клавиатуре <CTRL>+<SHIFT>+<L>)
  • Библиотека iarduino Modbus
  • Библиотека iarduino MB Pump
  • Библиотека iarduino MB TDS

Про установку библиотек в Arduino IDE можно узнать по ссылке: https://wiki.iarduino.ru/page/Installing_libraries/

Подключение

Установим модули кнопок, преобразователь UART на Piranha Set ESP32. Подключим дисплей к I2C колодке

Соединим блоки Насоса и TDS сенсора проводами.

Подключите питание RS-485 при помощи Блока питаия и коннектора. Включите блок питания в розетку, затем подключите контроллер к ПК при помощи USB шнура

Скетч проекта

// esp core - 2.0.11
// arduino ide - 1.8.19
#include <iarduino_Modbus.h> // 1.0.1
#include <iarduino_MB_Pump.h> // 1.0.3
#include <iarduino_MB_TDS.h> // 1.1.3
#include <LiquidCrystal_I2C.h> // 1.1.4
#include <Button2.h> // 2.2.4
#include <SPIFFS.h>
#include <list> // для возможных ошибок

// макрос возможных настроек
#define SETTINGS\
    X(PUMP_A_TIME, "HACOC A (CEK):")\
    X(PUMP_B_TIME, "HACOC B (CEK):")\
    X(PUMP_C_TIME, "HACOC C (CEK):")\
    X(INTERVAL, "")\
    X(PAUSE, "")\

// гистерезис
constexpr float EC_HYST = 0.1;

// пользовательский тип настроек
typedef uint16_t setting_t;

// максимальный допустимый уровень EC
constexpr float MAX_EC_ALLOWED = 5.0;
// минимальный допустимый уровень EC
constexpr float MIN_EC_ALLOWED = 0.1;
// время работы насосов по умолчанию (сек)
constexpr setting_t DEFAULT_PUMP_TIME = 10;
// интервал нормализации (мин)
constexpr setting_t DEFAULT_INTERVAL = 30;
// дельта при смене интервала (настройки)
constexpr setting_t INTERVAL_DELTA = 5;
// пауза между включением насосов по умолчанию (сек)
constexpr setting_t DEFAULT_PAUSE = 1;
// максимальное время работы насосов (сек)
constexpr setting_t MAX_PUMP_TIME = 90;
// после какого значения сменить дельту (настройки)
constexpr setting_t PUMP_TIME_ACCEL = 10;
// максимальная дельта для насосов (настройки)
constexpr setting_t PUMP_TIME_HIGH_DELTA = 10;
// максимальная пауза
constexpr setting_t MAX_ALLOWED_PAUSE = 10;
// максимальный интервал нормализации (мин)
constexpr setting_t MAX_ALLOWED_INTERVAL = 90;
// минимальный интервал нормализации (мин)
constexpr setting_t MIN_ALLOWED_ITERVAL = 5;
// через сколько миллисекунд сохранять настройки после последнего нажатия на кнопку
constexpr unsigned USER_INTERACTED_DELAY = 2000;
// выход из настроек через (миллисекунды)
constexpr unsigned SETTINGS_TIMEOUT = 10000;
// вывод левой кнопки
constexpr unsigned BUTTON_LEFT = 13;
// вывод правой кнопки
constexpr unsigned BUTTON_RIGHT = 33;
// вывод управления Modbus
constexpr unsigned MB_DE = 18;
// время зажатия для входа в настройки (миллисекунды)
constexpr unsigned HOLD_TIME = 1000;
// время повтора при зажатии кнопки
constexpr unsigned REPEAT_TIME = 200;

// глобальные переменные кнопок
bool g_left_pressed = false;
bool g_right_pressed = false;
bool g_left_released = false;
bool g_right_released = false;
bool g_left_holding = false;
bool g_right_holding = false;

// глобальные переменные текущего и целевого ЕС
float g_current_ec = 0.0;
float g_target_ec = 1.2;

// объекты оборудования
LiquidCrystal_I2C disp(0x27, 20, 4);
ModbusClient modbus(Serial2, MB_DE);
iarduino_MB_Pump pump(modbus);
iarduino_MB_TDS ec_sensor(modbus);

using namespace std;

// возможные ошибки
typedef enum {
        NO_PUMP,
        NO_SENSOR,
        SENSOR_ERROR,
        BAD_SOLUTION
} rig_error_t;

// определение типа указателя на функцию c++
typedef function<void()> func_ptr;

// структура ошибки
struct Error {
        Error(func_ptr a, rig_error_t e): action(a), what(e) {} // конструктор
        func_ptr action; // функция ошибки
        rig_error_t what; // тип ошибки
    // переопределение оператора "меньше" для сортировки списка (list::sort())
        const bool operator<(Error const& e) const
        {
                return (what < e.what);
        }
    // переопределение оператора сравнения для удаления из списка (list::unique())
        const bool operator==(Error const& e) const
        {
                return (what == e.what);
        }
};

// список ошибок
list<Error> g_errors;

// объект файла настроек
fs::File g_file;

// объекты кнопок
Button2 leftButton;
Button2 rightButton;

// состояние меню
typedef enum {
    MAIN,
    CHANGE_TARGET,
    SETTINGS_MENU,
    ERROR_DISP
} state_t;

state_t menu_state = MAIN;

// возможные насройки (см. макрос SETTINGS)
typedef enum {
#define X(INDEX, STRING) INDEX,
    SETTINGS
#undef X
    N_SETTING
} settings_state_t;

// переопределение оператора для смены текущей настройки
settings_state_t& operator++(settings_state_t& s)
{
    switch (s) {
        default:
#define X(INDEX, STRING) \
        case INDEX: s = static_cast<settings_state_t>(INDEX + 1); s == N_SETTING ? s = PUMP_A_TIME : 0; return s;
                SETTINGS
#undef X
    }
}

// первая настройка
settings_state_t current_item = PUMP_A_TIME;

// массив настроек
setting_t settings[N_SETTING]{0};

// возможные строки настроек (см. макрос SETTINGS)
const char* settings_strings[N_SETTING] = {
#define X(INDEX, STRING) STRING,
    SETTINGS
#undef X
};

// название файла настроек
const char* settings_file = "/settings.cfg";

// декларация функции (для линкера)
void handleInput();

// определения пользовательских символов дисплея
#define sh 0
#define ts 1
#define yi 2
#define uu 3
#define ee 4
#define ll 5
#define pp 6
#define ff 7

uint8_t SH[8] = {0x15, 0x15, 0x15, 0x15, 0x15, 0x1f, 0x01, 0x00};
uint8_t TS[8] = {0x12, 0x12, 0x12, 0x12, 0x12, 0x1f, 0x01, 0x00};
uint8_t YI[8] = {0x04, 0x11, 0x11, 0x13, 0x15, 0x19, 0x11, 0x00};
uint8_t UU[8] = {0x11, 0x11, 0x11, 0x0f, 0x01, 0x01, 0x1e, 0x00};
uint8_t EE[8] = {0x11, 0x11, 0x11, 0x13, 0x15, 0x19, 0x11, 0x00};
uint8_t LL[8] = {0x07, 0x09, 0x09, 0x09, 0x09, 0x09, 0x11, 0x00};
uint8_t PP[8] = {0x1f, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x00};
uint8_t FF[8] = {0x0e, 0x15, 0x15, 0x15, 0x0e, 0x04, 0x04, 0x00};

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

    initDisplay();
    initMemory();
    initButtons();
    initModbus();
    initSettings();
    handleSettingsFile();
}

void loop()
{
    handleInput();
    handleDisplay();
    handleMenu();
    handleRig();
    handleErrors();
    handleSettingsSave();
}

// текущая функция для ошибки
func_ptr g_current_error_func;

// обработка ошибок
void handleErrors()
{
    static unsigned count = 0;
    static unsigned index = 0;

    if (g_errors.empty()) {
        if (menu_state == ERROR_DISP)
            menu_state = MAIN;
        return;
    }

    menu_state = ERROR_DISP;

    if (count++ % 500)
        return;

    Serial.println("Проверка ошибок");
    g_errors.sort();
    g_errors.unique();
    if (g_errors.size()) {
        disp.clear();
        disp.setCursor(0,0);
        auto it = next(g_errors.begin(), index++ % g_errors.size());
        g_current_error_func = (*it).action;
    }
}

// инициализация памяти
void initMemory()
{
    if (!SPIFFS.begin()) {
        Serial.println("Форматируем память, нужно подождать...");
        disp.setCursor(0,0);
        printFormattingMemory();
        SPIFFS.format();
    }

    if (!SPIFFS.begin()) {
        Serial.println("Не получилось отформатировать, остановка.");
        disp.setCursor(0,1);
        printFormatError();
        while(true)
            ;
    }
}

// вывод на дисплей "ФОРМАТИРУЕМ ПАМЯТЬ"
void printFormattingMemory()
{
    disp.write(ff);disp.print("OPMAT");disp.write(ee);disp.write(uu);disp.print("EM ");
    disp.write(pp);disp.print("AMATb...");
}

// вывод на дисплей "Error. ОСТАНОВКА."
void printFormatError()
{
    disp.print("Error. "); disp.print("OCTAHOBKA.");
}

// инициализация кнопок
void initButtons()
{
    leftButton.begin(BUTTON_LEFT, INPUT, false);
    rightButton.begin(BUTTON_RIGHT, INPUT, false);

    leftButton.setReleasedHandler(released);
    leftButton.setLongClickTime(HOLD_TIME);
    leftButton.setLongClickDetectedHandler(longClickDetected);

    rightButton.setReleasedHandler(released);
    rightButton.setLongClickTime(HOLD_TIME);
    rightButton.setLongClickDetectedHandler(longClickDetected);
}

// инициализация дисплея
void initDisplay()
{
    disp.init();
    disp.backlight();
    disp.noBlink();
    disp.print("CTAPTyEM....");
    disp.createChar(0, SH);
    disp.createChar(1, TS); 
    disp.createChar(2, YI);
    disp.createChar(3, UU);
    disp.createChar(4, EE);
    disp.createChar(5, LL);
    disp.createChar(6, PP);
    disp.createChar(7, FF);
    delay(500);

}

// вывод на дисплей "ТЕКУЩИЙ:"
void printCurrentWord()
{
    disp.print("TEK");
    disp.write(uu);
    disp.write(sh);
    disp.write(ee);
    disp.write(yi);
    disp.print(": ");
}

// вывод на дисплей "ЦЕЛЕВОЙ:"
void printTargetWord()
{
    disp.write(ts);
    disp.print("E");
    disp.write(ll);
    disp.print("E");
    disp.print("BO");
    disp.write(yi);
    disp.print(": ");
}

// вывод на дисплей "НАСТРОЙКИ:"
void printSettingsWord()
{
    disp.print("HACTPO");
    disp.write(yi);
    disp.print("K");
    disp.write(ee);
    disp.print(":");
}

// инициализация Modbus
void initModbus()
{
    Serial2.begin(9600);
    while (!Serial2)
        ;
    modbus.begin();
    pump.enableWDT(3000);
    pump.begin();
    ec_sensor.begin();
    g_current_ec = ec_sensor.getEC();
}

unsigned long submenu_millis = 0;
bool g_already_in_settings = false;

// обработка состояний меню
void handleMenu()
{
    if (menu_state == MAIN) {
        handleMainMenu();
    }
    else if (menu_state == CHANGE_TARGET) {
        handleChangeTargetMenu();
        exitOnTimeOut();
    }
    else if (menu_state == SETTINGS_MENU) {
        handleSettingsMenu();
        exitOnTimeOut();
    }
}

// обработка главного меню
void handleMainMenu()
{
    if (!isLeftReleased() && !isRightReleased() && g_already_in_settings) {
        return;
    }

    g_already_in_settings = false;

    if (isLeftPressed() || isRightPressed()) {
        menu_state = CHANGE_TARGET;
        submenu_millis = millis();
    }

    if (areBothHolding()) {
        menu_state = SETTINGS_MENU;
        submenu_millis = millis();
    }
}

// обработка меню смены целевого уровня
void handleChangeTargetMenu()
{
    if (isLeftPressed() || isLeftRepeating()) {
        submenu_millis = millis();
        incrementTargetLevel();
    }
    if (isRightPressed() || isRightRepeating()) {
        submenu_millis = millis();
        decrementTargetLevel();
    }

    if (areBothHolding()) {
        menu_state = SETTINGS_MENU;
    }
}

// обработка меню настроек
void handleSettingsMenu()
{
    if (!isLeftReleased() && !isRightReleased() && !g_already_in_settings) {
        return;
    }

    g_already_in_settings = true;

    if (isLeftPressed() || isLeftRepeating()) {
        submenu_millis = millis();
        changeItem();
    }
    if (isRightPressed() || isRightRepeating()) {
        submenu_millis = millis();
        selectItem();
    }

    if (areBothHolding()) {
        menu_state = MAIN;
    }
}

// обработка сохранения настроек
void handleSettingsSave()
{
    static bool saved = true;
    static unsigned long last_input_millis = 0;
    static state_t last_state = menu_state;

    if (last_state != menu_state) {
        last_input_millis = millis();
        last_state = menu_state;
        saved = false;
    }

    if (saved)
        return;

    if (millis() - last_input_millis > USER_INTERACTED_DELAY) {
        saved = saveSettings();
    }
}

// сохранение настроек
bool saveSettings()
{
    try {
        g_file = SPIFFS.open(settings_file, "w");

        if (!g_file) {
            Serial.println("Ошибка открытия файла");
            return false;
        }

        for (auto& s:settings) {
            g_file.print(s);
            g_file.print('\n');
        }

        g_file.print(g_target_ec, 2);
        g_file.flush();
        g_file.close();
        Serial.println("Настройки сохранены");

        return true;
    }
    catch (...) {
        Serial.println("Исключение при сохранении файла");
        return false;
    }
}

// выход по истечении времени на главный экран
void exitOnTimeOut()
{
    if (millis() - submenu_millis > SETTINGS_TIMEOUT) {
        menu_state = MAIN;
    }
}

// выбор предмета настройки
void selectItem()
{
    ++current_item;
    if (current_item > N_SETTING)
        current_item = PUMP_A_TIME;
}

// изменение предмета настройки
void changeItem()
{
    switch (current_item) {
        default: break;
        case PUMP_A_TIME: changePumpTime(PUMP_A_TIME); break;
        case PUMP_B_TIME: changePumpTime(PUMP_B_TIME); break;
        case PUMP_C_TIME: changePumpTime(PUMP_C_TIME); break;
        case PAUSE: changePause(); break;
        case INTERVAL: changeInterval(); break;
    }
}

// приращение целевого уровня
void incrementTargetLevel()
{
    g_target_ec += 0.1;
    if (g_target_ec > MAX_EC_ALLOWED)
        g_target_ec = MAX_EC_ALLOWED;
}

// уменьшение целевого уровня
void decrementTargetLevel()
{
    g_target_ec -= 0.1;
    if (g_target_ec < MIN_EC_ALLOWED)
        g_target_ec = MIN_EC_ALLOWED;
}

// изменение времени работы насоса
void changePumpTime(setting_t s)
{
    static setting_t delta = 1;
    setting_t tmp = settings[s];

    if (tmp + delta > PUMP_TIME_ACCEL)
        delta = PUMP_TIME_HIGH_DELTA;

    if (tmp + delta > MAX_PUMP_TIME) {
        tmp = delta = 1;
        goto exit;
    }
    tmp += delta;
exit:
    settings[s] = tmp;
}

// изменение паузы
void changePause()
{
    setting_t tmp = settings[PAUSE];
    tmp++;
    if (tmp > MAX_ALLOWED_PAUSE)
        tmp = 0;
    settings[PAUSE] = tmp;
}

// изменение интервала нормализации
void changeInterval()
{
    setting_t tmp = settings[INTERVAL];
    tmp += INTERVAL_DELTA;
    if (tmp > MAX_ALLOWED_INTERVAL)
        tmp = MIN_ALLOWED_ITERVAL;
    settings[INTERVAL] = tmp;
}

// инициализация настроек
void initSettings()
{
    settings[PUMP_A_TIME] = DEFAULT_PUMP_TIME;
    settings[PUMP_B_TIME] = DEFAULT_PUMP_TIME;
    settings[PUMP_C_TIME] = DEFAULT_PUMP_TIME;
    settings[INTERVAL] = DEFAULT_INTERVAL;
    settings[PAUSE] = DEFAULT_PAUSE;
}

// декларация функции парсинга настроек (для линкера)
void parseFile(fs::File&);

// обработка загрузки файла
void handleSettingsFile() try
{
    if (!SPIFFS.exists(settings_file)) {
        Serial.println("Нет файла настроек");
        return;
    }

    g_file = SPIFFS.open(settings_file, "r");

    if (!g_file) {
        Serial.println("Не получилось открыть файл");
        return;
    }

    parseFile(g_file);

    g_file.close();
}
catch (...)
{
    g_file.close();
    Serial.println("Проблемы парсинга файла");
}

// парсинг файла настроек
void parseFile(fs::File& file)
{
    int b = 0;
    char buf[256];
    String loaded_setting = "";

    while (b != EOF) {
        static int i = 0;
        static settings_state_t index = PUMP_A_TIME;
        b = file.read();
        if (b == '\n') {
            loaded_setting = String(buf);
            settings[index] = static_cast<settings_state_t>(loaded_setting.toInt());
            Serial.println(settings[index]);
            ++index;
            i = 0;
            memset(buf, '\0', 256);
            continue;
        }
        else if (b == EOF) {
            loaded_setting = String(buf);
            g_target_ec = loaded_setting.toFloat();
            Serial.println(g_target_ec);
            i = 0;
            memset(buf, '\0', 256);
            continue;
        }
        buf[i++] = (byte)b;
    }
}

// обработка ввода
void handleInput()
{
    leftButton.loop();
    rightButton.loop();
}

// функции обработки кнопок
bool isLeftPressed()
{
    bool tmp = g_left_pressed;
    g_left_pressed = false;
    return tmp;
}

bool isRightPressed()
{
    bool tmp = g_right_pressed;
    g_right_pressed = false;
    return tmp;
}

bool isLeftRepeating()
{
    if (digitalRead(BUTTON_LEFT) == LOW) {
        g_left_holding = false;
    }

    if (g_left_holding && repeatInterval())
        return true;
    else
        return false;
}

bool isRightRepeating()
{
    if (digitalRead(BUTTON_RIGHT) == LOW) {
        g_right_holding = false;
    }

    if (g_right_holding && repeatInterval())
        return true;
    else
        return false;
}

bool isLeftHolding()
{
    return g_left_holding;
}

bool isRightHolding()
{
    return g_left_holding;
}

bool isLeftReleased()
{
    return !leftButton.isPressed();
}

bool isRightReleased()
{
    return !rightButton.isPressed();
}

bool areBothHolding()
{
    if (g_right_holding && g_left_holding && digitalRead(BUTTON_LEFT) && digitalRead(BUTTON_RIGHT))
        return true;
    else
        return false;
}

unsigned long repeat_millis = 0;

bool repeatInterval()
{
    if (millis() - repeat_millis > REPEAT_TIME) {
        repeat_millis = millis();
        return true;
    }
    else
        return false;
}

state_t last_state = MAIN;

// обработка дисплея
void handleDisplay()
{
    if (last_state != menu_state) {
        disp.clear();
        last_state = menu_state;
    }

    if (menu_state == MAIN) {
        disp.setCursor(0,0);
        printCurrentWord();
        disp.print(g_current_ec, 2);
        disp.setCursor(0,1);
        printTargetWord();
        disp.print(g_target_ec, 1);
    }
    else if (menu_state == CHANGE_TARGET) {
        disp.setCursor(0,0);
        printTargetWord();
        disp.print(g_target_ec, 1);
    }
    else if (menu_state == SETTINGS_MENU) {
        disp.setCursor(0,0);
        printSettingsWord();
        disp.setCursor(0,1);
        printCurrentSetting();
        setting_t curr_setting = settings[current_item];
        String disp_setting = curr_setting >= 10 ? String(curr_setting) : " " + String(curr_setting);
        disp.print(disp_setting);
        disp.print("         ");
    }
    else if (menu_state == ERROR_DISP) {
        disp.setCursor(0,0);
        g_current_error_func();
    }
}

// вывод текущей строки настроек
void printCurrentSetting()
{
    switch (current_item) {
        default: disp.print(settings_strings[current_item]); return;
        case INTERVAL: disp.write(ee); disp.print("HTEPBA"); disp.write(ll); disp.print("(M"); disp.write(ee); disp.print("H):"); return;
        case PAUSE: disp.write(pp); disp.write('A'); disp.write(uu); disp.print("3A(CEK):"); return;
    }
}

// работа установки
constexpr unsigned long RIG_UPDATE_INTERVAL = 1000;
unsigned long rig_update_millis = 0;

// возможные состояния насосов
typedef enum {
    A_ON,
    B_WAIT,
    B_ON,
    C_WAIT,
    C_ON
} pump_state_t;

pump_state_t pump_state;
unsigned long pump_millis = 0;
bool normalization_done = true;
bool pump_a_status = false;
bool pump_b_status = false;
bool pump_c_status = false;
bool low_ec = false;

// обработка Modbus
void handleModbus()
{
    pump.resetWDT();

    checkForAbsentDevices();
}

// проверка устройств на отсутствие
void checkForAbsentDevices()
{
    static unsigned count = 0;
    if (count++ % 101)
        return;

    Serial.print("Проверяем устройства Modbus");
    
    if (modbus.checkID(pump.getID() == DEVICE_MB_ABSENT))
        g_errors.push_back(Error(printNoPump, NO_PUMP));

    if (modbus.checkID(ec_sensor.getID()) == DEVICE_MB_ABSENT)
        g_errors.push_back(Error(printNoSensor, NO_SENSOR));
}

// обработка установки
void handleRig()
{
    if (millis() - rig_update_millis < RIG_UPDATE_INTERVAL)
        return;

    rig_update_millis = millis();

    handleModbus();
    updateEC();
    checkEC();
    checkNormalizationTime();
    if (!normalization_done && low_ec)
        normalize();
}

// проверка ЕС
void checkEC()
{
    // проверка и создание ошибки сенсора
    if (g_current_ec == -100.0)
        g_errors.push_back(Error(printSensorError, SENSOR_ERROR));

    bool sensor_present = true;
    bool sensor_ok = true;

    // проверка наличия оборудования
    for (auto& e:g_errors) {
        if (e.what == SENSOR_ERROR)
            sensor_ok = false;
        if (e.what == NO_SENSOR)
            sensor_present = false;
    }

    // проверка максимально и минимально возможных ЕС
    if ((g_current_ec < MIN_EC_ALLOWED || g_current_ec > MAX_EC_ALLOWED) && sensor_present && sensor_ok)
        g_errors.push_back(Error(printBadSolution, BAD_SOLUTION));


    // установка флага низкого ЕС
    if (g_current_ec + EC_HYST < g_target_ec)
        low_ec = true;
    else
        low_ec = false;
}

// обновление текущего ЕС
void updateEC()
{
    g_current_ec = ec_sensor.getEC();
}

// нормализация
void normalize()
{
    // включаем насос A
    if (pump_state == A_ON && !pump_a_status) {
        pump_millis = millis();
        pump_a_status = true;
        pump.setTimeOn(PUMP_A, float(settings[PUMP_A_TIME]));
    }

    // насос А отработал заданное время
    // переключаем статус нормализации на ожидание насоса В
    if (pump_state == A_ON && millis() - pump_millis > settings[PUMP_A_TIME]*1000)  {
        pump_millis = millis();
        pump_state = B_WAIT;
    }

    // время ожидания насоса B закончилось
    // переключаем статус на работу насоса B
    if (pump_state == B_WAIT && millis() - pump_millis > settings[PAUSE]*1000) {
        pump_millis = millis();
        pump_state = B_ON;
    }

    // включаем насос B
    if (pump_state == B_ON && !pump_b_status) {
        pump_millis = millis();
        pump_b_status = true;
        pump.setTimeOn(PUMP_B, float(settings[PUMP_B_TIME]));
    }

    // насос B отработал заданное время
    // переключаем статус нормализации на ожидание насоса C
    if (pump_state == B_ON && millis() - pump_millis > settings[PUMP_B_TIME]*1000) {
        pump_millis = millis();
        pump_state = C_WAIT;
    }

    // время ожидания насоса C закончилось
    // переключаем статус на работу насоса C
    if (pump_state == C_WAIT && millis() - pump_millis > settings[PAUSE]*1000) {
        pump_millis = millis();
        pump_state = C_ON;
    }

    // включаем насос C
    if (pump_state == C_ON && !pump_c_status) {
        pump_millis = millis();
        pump_c_status = true;
        pump.setTimeOn(PUMP_C, float(settings[PUMP_C_TIME]));
    }

    // насос C отработал заданное время
    // устанавливаем флаги в исходное значение
    if (pump_state == C_ON && millis() - pump_millis > settings[PUMP_C_TIME]*1000) {
        pump_a_status = false;
        pump_b_status = false;
        pump_c_status = false;
        normalization_done = true;
        low_ec = false;
    }
}

// проверка времени нормализации
void checkNormalizationTime()
{
    static unsigned long normalize_millis = 0;

    if (millis() - normalize_millis > settings[INTERVAL]*1000*60) {
        normalize_millis = millis();
        pump_state = A_ON;
        normalization_done = false;
    }
}

void longClickDetected(Button2& btn)
{
    if (&btn == &leftButton)
        g_left_holding = true;
    if (&btn == &rightButton)
        g_right_holding = true;
}

void released(Button2& btn)
{
    if (&btn == &leftButton) {
        if (g_left_holding)
            g_left_holding = false;
        else
            g_left_pressed = true;
    }
    if (&btn == &rightButton) {
        if (g_right_holding)
            g_right_holding = false;
        else
            g_right_pressed = true;
    }
}

void printNoSensor()
{
    disp.print("HET CEHCOPA");
}

void printNoPump()
{
    disp.print("HET HACOCA");
}

// сенсор неисправен
void printSensorError()
{
    disp.print("CEHCOP HE");disp.write(ee);disp.write('C');disp.write(pp);disp.print("PABEH");
}

// плохой раствор
void printBadSolution()
{
    disp.write(pp);disp.write(ll);disp.print("OXO");disp.write(yi);disp.print(" PACTBOP");
}

Ссылки




Обсуждение

Гарантии и возврат Используя сайт Вы соглашаетесь с условями