Общие сведения
В этом проекте мы соберём систему автоматизированной нормализации электропроводности раствора и настроим удалённое управление.
Видео
Редактируется...
Нам понадобится
Аппаратная часть
- Микроконтроллер Piranha Set ESP32
- Устройство преобразования UART-RS485
- Дисплей 1602 и I2C конвертер для связи с ним
- Блок, поддерживающий три перистальтических насоса, с подключением по протоколу RS485
- Датчик, предназначенный для измерения минерализации жидкости
- Блок питания
- Коннектор power jack Мама с клемником
- Провода для подключения устройств: USB, UTP, Макетные провода
Программная часть
- Arduino IDE 1.8.19
- ESP32 core (устанавливается в Arduino IDE, пункт меню Инструменты->Плата->Менеджер плат)
- Библиотека LiquidCrystal_I2C (Устанавливается в 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 шнура
Создание и настройка панели на iocontrol.ru
- Создаём или входим в учётную запись на сайте iocontrol
- нажимаем "Создать панель". Придумываем название панели, например "myNewPanel" (название панели должно быть уникальным на сайте. Если панель с таким именем уже есть, сайт уведомит об этом). Нажимаем Создать.
- Создаём следующие переменные:
Название | Тип | Вид |
---|---|---|
PUMP_A_SECONDS | Целочисленная | Ввод/Вывод значения |
PUMP_B_SECONDS | Целочисленная | Ввод/Вывод значения |
PUMP_C_SECONDS | Целочисленная | Ввод/Вывод значения |
NORM_INTERVAL | Целочисленная | Ввод/Вывод значения |
PAUSE | Целочисленная | Ввод/Вывод значения |
TARGET_EC | С плавающей точкой | Ввод/Вывод значения |
CURRENT_EC | С плавающей точкой | Вывод значения |
Скетч проекта
// esp core - 2.0.11 // arduino ide - 1.8.19 #include <WiFi.h> #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 <SPIFFS.h> #include <iocontrol.h> #include <list> // для возможных ошибок // X макрос возможных настроек // (индекс, строка для вывода на экран, строка названия в iocontrol) #define SETTINGS\ X(PUMP_A_TIME, "HACOC A (CEK):", "PUMP_A_SECONDS")\ X(PUMP_B_TIME, "HACOC B (CEK):", "PUMP_B_SECONDS")\ X(PUMP_C_TIME, "HACOC C (CEK):", "PUMP_C_SECONDS")\ X(INTERVAL, "", "NORM_INTERVAL")\ X(PAUSE, "", "PAUSE")\ // гистересис constexpr float EC_HYST = 0.1; // данные для подключения const char* ssid = "имя точки доступа"; const char* password = "пароль точки доступа"; // название панели const char* my_panel_name = "Название вашей панели"; // название online переменных const char* io_target_ec = "TARGET_EC"; const char* io_current_ec = "CURRENT_EC"; // интервал ротации настроек на дисплее constexpr unsigned long ROTATE_INT = 5000; // объекты для подключения к панели iocontrol WiFiClient client; iocontrol mypanel(my_panel_name, client); // пользовательский тип настроек 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 DEFAULT_PAUSE = 1; // вывод управления Modbus constexpr unsigned MB_DE = 18; // глобальные переменные кнопок 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; // переменная удалённого ЕС float online_target_ec = g_target_ec; // объекты оборудования 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; // состояния дисплея typedef enum { MAIN, SECOND_PAGE, THIRD_PAGE, FOURTH_PAGE, ERROR_DISP } state_t; state_t menu_state = MAIN; // возможные насройки (см. макрос SETTINGS) typedef enum { #define X(INDEX, STRING, IOC_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, IOC_STRING) \ case INDEX: s = static_cast<settings_state_t>(INDEX + 1); s == N_SETTING ? s = PUMP_A_TIME : 0; return s; SETTINGS #undef X } } // массивы настроек setting_t settings[N_SETTING]{0}; setting_t online_settings[N_SETTING]{0}; const char* settings_strings[N_SETTING] = { #define X(INDEX, STRING, IOC_STRING) STRING, SETTINGS #undef X }; const char* online_var_names[N_SETTING] = { #define X(INDEX, STRING, IOC_STRING) IOC_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(); initOnline(); initMemory(); initModbus(); initSettings(); handleSettingsFile(); } void loop() { handleOnline(); handleDisplay(); 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; } } // подключение и инициализация iocontrol void initOnline() { WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); disp.print("."); } Serial.println(); mypanel.begin(); } // обработка онлайн переменных void handleOnline() { int status = mypanel.readUpdate(); if (status == OK) { online_target_ec = mypanel.readFloat(io_target_ec); for (size_t i = 0; i < N_SETTING; i++) { online_settings[i] = mypanel.readInt(online_var_names[i]); } } checkForZeroes(); mypanel.write(io_current_ec, g_current_ec); status = mypanel.writeUpdate(); } // проверака на неиницализированные переменные в панели void checkForZeroes() { if (online_target_ec < 0.01f) { online_target_ec = g_target_ec; mypanel.write(io_target_ec, g_target_ec); } for (size_t i = 0; i < N_SETTING; i++) { if (online_settings[i] == 0) { online_settings[i] = settings[i]; mypanel.write(online_var_names[i], settings[i]); } } } // инициализация памяти 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 printIntervalWord() { disp.write(ee); disp.print("HTEPBA"); disp.write(ll); disp.print(": "); } // вывод на дисплей "ПАУЗА:" void printPauseWord() { disp.write(pp); disp.print("A"); disp.write(uu); disp.print("3A: "); } // инициализация дисплея 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(": "); } // инициализация Modbus void initModbus() { Serial2.begin(9600); while (!Serial2) ; modbus.begin(); pump.enableWDT(3000); pump.begin(); ec_sensor.begin(); g_current_ec = ec_sensor.getEC(); } // обработка сохранения настроек void handleSettingsSave() { bool save_settings = false; for (size_t i = 0; i < N_SETTING; i++) if (settings[i] != online_settings[i]) save_settings = true; if (abs(g_target_ec - online_target_ec) > 0.09) save_settings = true; if (save_settings) { updateSettings(); saveSettings(); } } // обновление настроек void updateSettings() { for (size_t i = 0; i < N_SETTING; i++) settings[i] = online_settings[i]; g_target_ec = online_target_ec; } // сохранение настроек 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 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; for (size_t i = 0; i < N_SETTING; i++) online_settings[i] = settings[i]; } // декларация функции парсинга настроек (для линкера) 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); //loaded_setting.trim(); 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); //loaded_setting.trim(); g_target_ec = loaded_setting.toFloat(); Serial.println(g_target_ec); i = 0; memset(buf, '\0', 256); continue; } buf[i++] = (byte)b; } } state_t last_state = MAIN; void handleDisplay() { static unsigned long rotateMillis = 0; if (millis() - rotateMillis > ROTATE_INT) { rotateMillis = millis(); if (menu_state == MAIN) { menu_state = SECOND_PAGE; } else if (menu_state == SECOND_PAGE) { menu_state = THIRD_PAGE; } else if (menu_state == THIRD_PAGE) { menu_state = FOURTH_PAGE; } else if (menu_state == FOURTH_PAGE) { menu_state = MAIN; } } 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 == SECOND_PAGE) { // print pump A setting disp.setCursor(0,0); disp.print("HACOC A: "); disp.print(settings[PUMP_A_TIME]); // print pump B setting disp.setCursor(0,1); disp.print("HACOC B: "); disp.print(settings[PUMP_B_TIME]); } else if (menu_state == THIRD_PAGE) { // print pump C setting disp.setCursor(0,0); disp.print("HACOC C: "); disp.print(settings[PUMP_C_TIME]); // print interval setting disp.setCursor(0,1); printIntervalWord(); disp.print(settings[INTERVAL]); } else if (menu_state == FOURTH_PAGE) { // print pause setting disp.setCursor(0,0); printPauseWord(); disp.print(settings[PAUSE]); } else if (menu_state == ERROR_DISP) { disp.setCursor(0,0); g_current_error_func(); } if (WiFi.status() != WL_CONNECTED) { disp.setCursor(15,0); disp.print("X"); } else { disp.setCursor(15,0); disp.print("W"); } } // работа установки 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() { // handle sensor error if (g_current_ec == -100.0) g_errors.push_back(Error(printSensorError, SENSOR_ERROR)); bool sensor_present = true; bool sensor_ok = true; // handle bad ec 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)); // handle normal ec 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 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"); }
Обсуждение