Общие сведения
В этом проекте мы соберём систему автоматизированной нормализации кислотности раствора и настроим удалённое управление.
Видео
Нам понадобится
Аппаратная часть
- Микроконтроллер 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 pH
Про установку библиотек в Arduino IDE можно узнать по ссылке: https://wiki.iarduino.ru/page/Installing_libraries/
Подключение
Установим преобразователь UART на Piranha Set ESP32. Подключим дисплей к I2C колодке
Соединим блоки Насоса и pH сенсора проводами и подключим к контроллеру
Подключите питание RS-485 при помощи Блока питаия и коннектора. Включите блок питания в розетку, затем подключите контроллер к ПК при помощи USB шнура
Создание и настройка панели на iocontrol.ru
- Создаём или входим в учётную запись на сайте iocontrol
- нажимаем "Создать панель". Придумываем название панели, например "myNewPanel" (название панели должно быть уникальным на сайте. Если панель с таким именем уже есть, сайт уведомит об этом). Нажимаем Создать.
- Создаём следующие переменные:
Название | Тип | Вид |
---|---|---|
PUMP_SECONDS | Целочисленная | Ввод/Вывод значения |
NORM_INTERVAL | Целочисленная | Ввод/Вывод значения |
TARGET_PH | С плавающей точкой | Ввод/Вывод значения |
CURRENT_PH | С плавающей точкой | Вывод значения |
Скетч проекта
// 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_pH.h> // 1.2.3 #include <LiquidCrystal_I2C.h> // 1.1.4 #include <SPIFFS.h> #include <iocontrol.h> #include <list> // для возможных ошибок #define SETTINGS\ X(PUMP_TIME, "HACOC (CEK):")\ X(INTERVAL, "")\ constexpr float PH_HYST = 0.1; // данные для подключения const char* ssid = "имя точки доступа"; const char* password = "пароль точки доступа"; // название панели const char* my_panel_name = "Название вашей панели"; // название online переменных const char* io_pump_sec = "PUMP_SECONDS"; const char* io_norm_interval = "NORM_INTERVAL"; const char* io_target_ph = "TARGET_PH"; const char* io_current_ph = "CURRENT_PH"; // объекты для подключения к панели iocontrol WiFiClient client; iocontrol mypanel(my_panel_name, client); // пользовательский тип настроек typedef uint16_t setting_t; // максимальный допустимый уровень pH constexpr float MAX_PH_ALLOWED = 8.0; // минимальный допустимый уровень pH constexpr float MIN_PH_ALLOWED = 3.0; // время работы насосов по умолчанию (сек) constexpr setting_t DEFAULT_PUMP_TIME = 10; // интервал нормализации (мин) constexpr setting_t DEFAULT_INTERVAL = 30; // вывод управления Modbus constexpr unsigned MB_DE = 18; // глобальные переменные текущего и целевого pH float g_current_ph = 0.0; float g_target_ph = 7.1; // переменная удалённого pH float online_target_ph = g_target_ph; // объекты оборудования LiquidCrystal_I2C disp(0x27, 20, 4); ModbusClient modbus(Serial2, MB_DE); iarduino_MB_Pump pump(modbus); iarduino_MB_pH ph_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, ERROR_DISP } state_t; state_t menu_state = MAIN; // интервал ротации настроек на дисплее constexpr unsigned long ROTATE_INT = 5000; // возможные настройки (см. макрос 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_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) 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; } } 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_ph = mypanel.readFloat(io_target_ph); online_settings[PUMP_TIME] = mypanel.readInt(io_pump_sec); online_settings[INTERVAL] = mypanel.readInt(io_norm_interval); } checkForZeroes(); mypanel.write(io_current_ph, g_current_ph); status = mypanel.writeUpdate(); } void checkForZeroes() { if (online_target_ph < 0.01f) { online_target_ph = g_target_ph; mypanel.write(io_target_ph, g_target_ph); } if (online_settings[PUMP_TIME] == 0) { online_settings[PUMP_TIME] = settings[PUMP_TIME]; mypanel.write(io_pump_sec, settings[PUMP_TIME]); } if (online_settings[INTERVAL] == 0) { online_settings[INTERVAL] = settings[INTERVAL]; mypanel.write(io_norm_interval, settings[INTERVAL]); } } // инициализация памяти 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 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 printIntervalWord() { disp.write(ee); disp.print("HTEPBA"); disp.write(ll); disp.print(": "); } // инициализация Modbus void initModbus() { Serial2.begin(9600); while (!Serial2) ; modbus.begin(); pump.enableWDT(3000); pump.begin(); ph_sensor.begin(); //g_current_ph = ph_sensor.getPH(); } // обработка сохранения настроек 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_ph - online_target_ph) > 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_ph = online_target_ph; } // сохранение настроек 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_ph, 2); g_file.flush(); g_file.close(); Serial.println("Настройки сохранены"); return true; } catch (...) { Serial.println("Исключение при сохранении файла"); return false; } } // инициализация настроек void initSettings() { settings[PUMP_TIME] = DEFAULT_PUMP_TIME; settings[INTERVAL] = DEFAULT_INTERVAL; } // декларация функции парсинга настроек (для линкера) 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("Не получилось открыть файл"); for (size_t i = 0; i < N_SETTING; i++) online_settings[i] = settings[i]; return; } parseFile(g_file); g_file.close(); } catch (...) { g_file.close(); disp.setCursor(0,0); disp.print("Error reading file"); 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_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_ph = loaded_setting.toFloat(); Serial.println(g_target_ph); 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 = 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_ph, 2); disp.setCursor(0,1); printTargetWord(); disp.print(g_target_ph, 1); } else if (menu_state == SECOND_PAGE) { disp.setCursor(0,0); printIntervalWord(); disp.print(settings[INTERVAL]); disp.setCursor(0,1); disp.print("HACOC: "); disp.print(settings[PUMP_TIME]); } 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; unsigned long pump_millis = 0; bool normalization_done = true; bool low_ph = false; bool hi_ph = 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(ph_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(); updatePH(); checkPH(); checkNormalizationTime(); if (!normalization_done && (low_ph || hi_ph)) normalize(); } // проверка pH void checkPH() { if (g_current_ph < 0.01f) return; if (g_current_ph + PH_HYST < g_target_ph) low_ph = true; else if (g_current_ph - PH_HYST > g_target_ph) hi_ph = true; else { low_ph = false; hi_ph = false; } } // обновление текущего pH void updatePH() { g_current_ph = ph_sensor.getPH(); } // нормализация void normalize() { if (low_ph) pump.setTimeOn(PUMP_A, float(settings[PUMP_TIME])); else if (hi_ph) { pump.setTimeOn(PUMP_B, float(settings[PUMP_TIME])); } low_ph = false; hi_ph = false; normalization_done = true; } unsigned long normalize_millis = 0; // проверка времени нормализации void checkNormalizationTime() { if (millis() - normalize_millis > settings[INTERVAL]*1000*60) { normalize_millis = millis(); 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"); }
Обсуждение