#include #include #include // LCD Keypad Shield 1602 pins LiquidCrystal lcd(8, 9, 4, 5, 6, 7); // HX711 pins const int LOADCELL_DOUT_PIN = A3; const int LOADCELL_SCK_PIN = A2; // Button pin const int BUTTON_PIN = 13; // Heat control pin const int HEAT_PIN = 3; // D3 для управления нагревом // LCD Backlight pin const int BACKLIGHT_PIN = 10; // D10 для подсветки LCD // Voltage measurement pin const int VOLTAGE_PIN = A1; // Пин для измерения напряжения const float VOLTAGE_DIVIDER = 5.0; // Делитель 1:10 (16V -> 1.6V на пине) const float VREF = 5.0; // Опорное напряжение Arduino (5V) // Calibration float calibration_factor = 1.0; const float CALIBRATION_WEIGHT = 100.0; // EEPROM const int EEPROM_CALIBRATION_ADDR = 0; // Весовые переменные float current_weight = 0.0; float displayed_weight = 0.0; float m = 0.0; // Масса пустой кюветы float m1 = 0.0; // Масса заполненной кюветы float m0 = 0.0; // Текущая масса во время нагрева // Режимы работы enum SystemState { STATE_READY, STATE_EMPTY_CUVETTE, STATE_SAVING_EMPTY, STATE_FILLED_CUVETTE, STATE_SAVING_FILLED, STATE_HEATING, STATE_HEATING_COMPLETE }; SystemState current_state = STATE_READY; bool need_display_clear = true; // Таймеры для нагрева unsigned long heat_start_time = 0; const unsigned long HEAT_DURATION = 900000; // 15 минут = 900000 мс bool heating_active = false; // Циклический нагрев const unsigned long HEAT_ON_TIME = 20000; // 15 секунд нагрева const unsigned long HEAT_OFF_TIME = 30000; // 30 секунд паузы unsigned long last_cycle_change = 0; // Время последнего переключения bool heat_cycle_on = false; // Текущее состояние цикла (вкл/выкл) // Для расчета W (без фильтрации) float current_W = 0.0; // Текущее значение влажности float max_W = 0.0; // Максимальное значение W за время нагрева unsigned long last_W_update = 0; const unsigned long W_UPDATE_INTERVAL = 500; // Обновлять W каждые 500 мс // Button timing bool button_pressed = false; unsigned long button_press_time = 0; const unsigned long MIN_TARE_TIME = 4000; // 4 секунды для тарирования const unsigned long MAX_TARE_TIME = 6000; // 6 секунд максимум для тарирования const unsigned long MIN_CAL_TIME = 8000; // 8 секунд для калибровки const unsigned long MAX_CAL_TIME = 10000; // 10 секунд максимум для калибровки bool showing_countdown = false; // Флаг отображения отсчета // Scale HX711 scale; // === ФИЛЬТРЫ === // МЕДИАННЫЙ ФИЛЬТР class MedianFilter { private: float buffer[7]; int index = 0; bool filled = false; public: float update(float new_val) { buffer[index] = new_val; index = (index + 1) % 7; if (index == 0) filled = true; float temp[7]; int count = filled ? 7 : index; for (int i = 0; i < count; i++) temp[i] = buffer[i]; for (int i = 0; i < count - 1; i++) { for (int j = i + 1; j < count; j++) { if (temp[i] > temp[j]) { float t = temp[i]; temp[i] = temp[j]; temp[j] = t; } } } return temp[count / 2]; } void reset() { index = 0; filled = false; } }; MedianFilter median_filter; // ФИЛЬТР ВЫВОДА W (с дедбэндом для уменьшения дрейфа) class WFilter { private: float filtered_value = 0.0; float displayed_value = 0.0; const float ALPHA = 0.15; // Коэффициент EMA (меньше = больше сглаживание) const float DEADBAND = 0.01; // Игнорировать изменения меньше 0.3% bool initialized = false; public: float update(float raw_value) { if (!initialized) { filtered_value = raw_value; displayed_value = raw_value; initialized = true; return displayed_value; } // Экспоненциальное сглаживание (EMA) filtered_value = ALPHA * raw_value + (1.0 - ALPHA) * filtered_value; // Дедбэнд: обновляем отображаемое значение только если изменение значительное if (abs(filtered_value - displayed_value) >= DEADBAND) { displayed_value = filtered_value; } return displayed_value; } float getValue() { return displayed_value; } void reset() { filtered_value = 0.0; displayed_value = 0.0; initialized = false; } void setImmediate(float value) { filtered_value = value; displayed_value = value; initialized = true; } }; WFilter w_filter; // ФИЛЬТР ВЫВОДА веса class DisplayFilter { private: float target_value = 0.0; float current_display = 0.0; const float SMOOTH_SPEED = 0.5; public: float update(float new_value) { target_value = new_value; float diff = target_value - current_display; if (abs(diff) < 0.01) { current_display = target_value; } else { current_display += diff * SMOOTH_SPEED; } return current_display; } void reset() { current_display = 0.0; target_value = 0.0; } void setImmediate(float value) { current_display = value; target_value = value; } }; DisplayFilter display_filter; // EEPROM функции void saveCalibrationToEEPROM(float factor) { EEPROM.put(EEPROM_CALIBRATION_ADDR, factor); } float loadCalibrationFromEEPROM() { float factor = 1.0; EEPROM.get(EEPROM_CALIBRATION_ADDR, factor); if (isnan(factor) || factor < 0.001 || factor > 10000.0) { factor = 1.0; } return factor; } // Функция для измерения среднего значения float measureAverageWeight(int samples = 10) { float sum = 0; for (int i = 0; i < samples; i++) { float raw = scale.get_units(1); float filtered = median_filter.update(raw); sum += filtered; delay(100); } return sum / samples; } // Функция центрированного вывода текста void printCentered(int row, String text) { int spaces = (16 - text.length()) / 2; lcd.setCursor(spaces, row); lcd.print(text); } // Функция центрированного вывода веса void printWeightCentered(int row, float weight) { char buffer[16]; if (weight >= 100.0) { strcpy(buffer, "cuvette:>99.99g"); } else if (weight <= -100.0) { strcpy(buffer, "cuvette:<-99.99g"); } else { // Форматируем число с 2 знаками после запятой char num_buf[8]; dtostrf(fabs(weight), 0, 2, num_buf); // Без пробелов, 2 знака после запятой // Формируем полную строку if (weight < 0) { snprintf(buffer, sizeof(buffer), "cuvette:-%sg", num_buf); } else { snprintf(buffer, sizeof(buffer), "cuvette:%sg", num_buf); } } // Центрируем int text_len = strlen(buffer); int spaces = (16 - text_len) / 2; lcd.setCursor(spaces, row); lcd.print(buffer); } // Функция расчета W float calculateW() { if (m0 > m && m1 > m) { return 100.0 * (m1 - m0) / (m0 - m); } return 0.0; } // Функция измерения напряжения (сырое значение) float readVoltageRaw() { int raw = analogRead(VOLTAGE_PIN); float voltage_on_pin = (raw / 1023.0) * VREF; // Напряжение на пине (0-5V) float actual_voltage = voltage_on_pin * VOLTAGE_DIVIDER; // Реальное напряжение с учетом делителя return actual_voltage; } // Фильтрованное напряжение (EMA) float filtered_voltage = 0.0; bool voltage_initialized = false; const float VOLTAGE_ALPHA = 0.1; // Коэффициент EMA для напряжения (меньше = больше сглаживание) // Функция обновления и получения отфильтрованного напряжения float updateVoltage() { float raw_voltage = readVoltageRaw(); if (!voltage_initialized) { filtered_voltage = raw_voltage; voltage_initialized = true; } else { filtered_voltage = VOLTAGE_ALPHA * raw_voltage + (1.0 - VOLTAGE_ALPHA) * filtered_voltage; } return filtered_voltage; } void setup() { Serial.begin(9600); delay(500); // LCD lcd.begin(16, 2); // Настройка пина нагрева pinMode(HEAT_PIN, OUTPUT); digitalWrite(HEAT_PIN, LOW); // Яркость подсветки pinMode(BACKLIGHT_PIN, OUTPUT); analogWrite(BACKLIGHT_PIN, 180); // Button pinMode(BUTTON_PIN, INPUT); // HX711 scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN); scale.set_gain(128); // Быстрое тарирование scale.tare(); // Загрузка калибровки calibration_factor = loadCalibrationFromEEPROM(); scale.set_scale(calibration_factor); // Стартовое сообщение displayReadyScreen(); delay(1000); } void loop() { // Обработка кнопки handleButton(); // Обновление веса и напряжения static unsigned long last_weight_update = 0; if (millis() - last_weight_update > 100) { float raw = scale.get_units(1); float median = median_filter.update(raw); current_weight = median; displayed_weight = display_filter.update(current_weight); // Обновляем отфильтрованное напряжение updateVoltage(); // В режиме нагрева обновляем W только когда индуктор ВЫКЛЮЧЕН // (чтобы избежать влияния электромагнитных помех на измерения) if (current_state == STATE_HEATING && !heat_cycle_on) { m0 = current_weight; // Обновляем текущую массу только при выключенном индукторе // Обновляем W каждые W_UPDATE_INTERVAL мс if (millis() - last_W_update > W_UPDATE_INTERVAL) { current_W = calculateW(); // Вычисляем W напрямую (без фильтра) // DEBUG: выводим все значения для диагностики Serial.print("m="); Serial.print(m, 4); Serial.print(" m1="); Serial.print(m1, 4); Serial.print(" m0="); Serial.print(m0, 4); Serial.print(" W="); Serial.print(current_W, 4); Serial.print(" max_W="); Serial.println(max_W, 4); // Обновляем максимальное значение W if (current_W > max_W) { max_W = current_W; } last_W_update = millis(); } } // Обновление дисплея в зависимости от состояния updateDisplay(); last_weight_update = millis(); } // Управление нагревом if (current_state == STATE_HEATING) { manageHeating(); } delay(10); } void displayReadyScreen() { lcd.clear(); printCentered(0, "Gruntometriya"); printCentered(1, "ready"); } void updateDisplay() { if (need_display_clear) { lcd.clear(); need_display_clear = false; } switch (current_state) { case STATE_READY: // Показываем центрированный текст printCentered(0, "Gruntometriya"); // Вторая строка: "ready" и напряжение { char ready_line[17]; char v_buf[5]; dtostrf(filtered_voltage, 4, 1, v_buf); snprintf(ready_line, sizeof(ready_line), "ready %sV", v_buf); lcd.setCursor(0, 1); lcd.print(ready_line); } // Показываем отсчет в правом нижнем углу если нужно if (showing_countdown) { unsigned long hold_time = millis() - button_press_time; int seconds = hold_time / 1000; // Отображаем отсчет в правом нижнем углу (позиция 13-15) lcd.setCursor(13, 1); if (seconds < 10) { lcd.print(" "); lcd.setCursor(15, 1); lcd.print(seconds); } else { lcd.setCursor(14, 1); lcd.print(seconds); } } break; case STATE_EMPTY_CUVETTE: printCentered(0, "hang an empty"); // Во второй строке показываем текущий вес кюветы printWeightCentered(1, displayed_weight); break; case STATE_SAVING_EMPTY: printCentered(0, "saving value"); printCentered(1, "measuring..."); // Измерение и сохранение массы пустой кюветы m = measureAverageWeight(10); delay(500); current_state = STATE_FILLED_CUVETTE; need_display_clear = true; break; case STATE_FILLED_CUVETTE: printCentered(0, "Hang the filled"); // Во второй строке показываем текущий вес заполненной кюветы printWeightCentered(1, displayed_weight); break; case STATE_SAVING_FILLED: printCentered(0, "saving value"); printCentered(1, "measuring..."); // Измерение и сохранение массы заполненной кюветы m1 = measureAverageWeight(10); delay(500); current_state = STATE_HEATING; need_display_clear = true; heat_start_time = millis(); heating_active = true; heat_cycle_on = true; last_cycle_change = millis(); last_W_update = millis(); // Фильтр W удалён - используем прямое значение max_W = 0.0; // Сбрасываем максимальное W digitalWrite(HEAT_PIN, HIGH); break; case STATE_HEATING: displayHeating(); break; case STATE_HEATING_COMPLETE: displayHeatingComplete(); break; } } void handleButton() { int button_state = digitalRead(BUTTON_PIN); if (button_state == 0) { // Кнопка нажата if (!button_pressed) { button_pressed = true; button_press_time = millis(); showing_countdown = false; } unsigned long hold_time = millis() - button_press_time; // Показываем отсчет начиная с 1 секунды (только в режиме READY) if (current_state == STATE_READY && hold_time >= 1000 && !showing_countdown) { showing_countdown = true; need_display_clear = true; // Обновляем дисплей для показа отсчета } // Обновляем отсчет каждую секунду (только в режиме READY) if (current_state == STATE_READY && showing_countdown) { // Обновляем отсчет когда меняется секунда static int last_seconds = -1; int seconds = hold_time / 1000; if (seconds != last_seconds && seconds <= 10) { last_seconds = seconds; need_display_clear = true; // Запрашиваем обновление дисплея } } } else { // Кнопка отпущена if (button_pressed) { unsigned long press_duration = millis() - button_press_time; button_pressed = false; // Скрываем отсчет if (showing_countdown) { showing_countdown = false; need_display_clear = true; // Запрашиваем обновление экрана } // Определяем действие по времени нажатия (только в режиме READY) if (current_state == STATE_READY) { if (press_duration >= MIN_TARE_TIME && press_duration <= MAX_TARE_TIME) { // Тарирование 4-6 секунд performTaring(); } else if (press_duration >= MIN_CAL_TIME) { // Калибровка 8+ секунд performCalibration(); } else if (press_duration < 1000) { // Короткое нажатие processShortPress(); } } else { // Короткое нажатие в других состояниях if (press_duration < 1000) { processShortPress(); } } } } } void processShortPress() { switch (current_state) { case STATE_READY: current_state = STATE_EMPTY_CUVETTE; need_display_clear = true; break; case STATE_EMPTY_CUVETTE: current_state = STATE_SAVING_EMPTY; need_display_clear = true; break; case STATE_SAVING_EMPTY: // Сохранение уже произошло, состояние изменится автоматически break; case STATE_FILLED_CUVETTE: current_state = STATE_SAVING_FILLED; need_display_clear = true; break; case STATE_SAVING_FILLED: // Сохранение уже произошло, состояние изменится автоматически break; case STATE_HEATING: // Прерывание нагрева - max_W уже содержит максимальное значение digitalWrite(HEAT_PIN, LOW); heating_active = false; heat_cycle_on = false; current_state = STATE_HEATING_COMPLETE; need_display_clear = true; break; case STATE_HEATING_COMPLETE: // Возврат к началу current_state = STATE_READY; need_display_clear = true; break; } } void displayHeating() { // Первая строка: время, напряжение unsigned long elapsed = millis() - heat_start_time; unsigned long remaining = HEAT_DURATION - elapsed; int minutes_remaining = (remaining / 60000) + 1; // +1 чтобы показывать целые минуты if (minutes_remaining < 0) minutes_remaining = 0; char line1[17]; char v_buf[5]; dtostrf(filtered_voltage, 4, 1, v_buf); // Формат: "Heat 15m 16.0V" snprintf(line1, sizeof(line1), "Heat %2dm %sV", minutes_remaining, v_buf); lcd.setCursor(0, 0); lcd.print(line1); // Вторая строка: масса (слева) и W (справа) char line2[17]; char w_buf[6]; char mass_buf[7]; // Форматируем максимальный W if (max_W >= 100) { strcpy(w_buf, ">99"); } else { dtostrf(max_W, 4, 1, w_buf); } // Форматируем массу m0 (показываем когда индуктор выключен) if (!heat_cycle_on) { // Индуктор выключен - показываем текущую массу if (m0 >= 100.0) { strcpy(mass_buf, ">99g"); } else if (m0 <= -10.0) { strcpy(mass_buf, "<-9g"); } else { dtostrf(m0, 5, 2, mass_buf); strcat(mass_buf, "g"); } // Формат: "XX.XXg W:XX.X%" snprintf(line2, sizeof(line2), "%-8s %s%%", mass_buf, w_buf); } else { // Индуктор включен - показываем только W по центру snprintf(line2, sizeof(line2), " * %s%%", w_buf); } lcd.setCursor(0, 1); lcd.print(line2); } void displayHeatingComplete() { // Первая строка printCentered(0, "end of heating"); // Вторая строка с максимальным W (это и есть результат измерения влажности) String w_text = "W: "; if (max_W >= 100) { w_text += ">99%"; } else if (max_W >= 0) { w_text += String(max_W, 1); w_text += "%"; } else { w_text += "0.0%"; // Если max_W отрицательный, показываем 0 } printCentered(1, w_text); } void manageHeating() { if (!heating_active) return; unsigned long elapsed = millis() - heat_start_time; unsigned long cycle_elapsed = millis() - last_cycle_change; // Управление циклическим нагревом (30 сек ON / 20 сек OFF) if (heat_cycle_on) { // Если прошло 30 секунд нагрева - выключаем if (cycle_elapsed >= HEAT_ON_TIME) { heat_cycle_on = false; last_cycle_change = millis(); digitalWrite(HEAT_PIN, LOW); } } else { // Если прошло 20 секунд паузы - включаем if (cycle_elapsed >= HEAT_OFF_TIME) { heat_cycle_on = true; last_cycle_change = millis(); digitalWrite(HEAT_PIN, HIGH); } } // Проверка завершения общего времени нагрева (15 минут) if (elapsed >= HEAT_DURATION) { // max_W уже содержит максимальное значение W за время нагрева // Гарантированно выключаем нагрев digitalWrite(HEAT_PIN, LOW); heat_cycle_on = false; heating_active = false; current_state = STATE_HEATING_COMPLETE; need_display_clear = true; } } void performTaring() { lcd.clear(); printCentered(0, "Auto-Taring..."); scale.tare(); median_filter.reset(); display_filter.setImmediate(0.0); // Если был режим нагрева - выходим из него if (heating_active) { digitalWrite(HEAT_PIN, LOW); heating_active = false; heat_cycle_on = false; } lcd.setCursor(0, 1); lcd.print("Done!"); delay(500); lcd.clear(); need_display_clear = true; // Возвращаемся в состояние READY current_state = STATE_READY; } void performCalibration() { calibrationProcedure(); } void calibrationProcedure() { Serial.println("=== CALIBRATION START ==="); Serial.print("Old calibration_factor: "); Serial.println(calibration_factor, 6); lcd.clear(); lcd.setCursor(0, 0); lcd.print("Calibration "); // Если был режим нагрева - выходим из него if (heating_active) { digitalWrite(HEAT_PIN, LOW); heating_active = false; heat_cycle_on = false; Serial.println("Heating was active, turned off"); } // Шаг 1: Clear platform Serial.println("Step 1: Waiting for empty platform..."); lcd.setCursor(0, 1); lcd.print("Clear platform "); // Ждем нажатия кнопки while (digitalRead(BUTTON_PIN) == 1) { delay(10); } while (digitalRead(BUTTON_PIN) == 0) { delay(10); } delay(100); // Сообщение о тарировании Serial.println("Step 2: Taring..."); lcd.setCursor(0, 1); lcd.print("Taring... "); // ВАЖНО: Устанавливаем scale=1.0 ПЕРЕД тарированием, // чтобы получать сырые значения и избежать зацикливания калибровки scale.set_scale(1.0); Serial.println("Scale set to 1.0 for raw measurements"); // Тарируем весы (устанавливаем ноль) delay(500); // Даем время на стабилизацию scale.tare(); Serial.println("Tare complete"); // Шаг 2: Set 100g Serial.println("Step 3: Waiting for 100g weight..."); lcd.setCursor(0, 1); lcd.print("Set 100g "); // Ждем нажатия кнопки while (digitalRead(BUTTON_PIN) == 1) { delay(10); } while (digitalRead(BUTTON_PIN) == 0) { delay(10); } delay(100); // Сообщение о сохранении результатов 100г Serial.println("Step 4: Measuring 100g raw values (15 samples)..."); lcd.setCursor(0, 1); lcd.print("saving results "); // Измеряем среднее СЫРОЕ значение 100г (15 измерений) // scale=1.0, поэтому get_units() возвращает сырые значения delay(1000); // Даем время на стабилизацию float weight_sum = 0; for (int i = 0; i < 15; i++) { float reading = scale.get_units(1); // При scale=1.0 это сырое значение weight_sum += reading; Serial.print(" Raw sample "); Serial.print(i + 1); Serial.print(": "); Serial.println(reading, 2); delay(100); } float weight_average = weight_sum / 15; Serial.print("Raw weight sum: "); Serial.println(weight_sum, 2); Serial.print("Raw weight average: "); Serial.println(weight_average, 2); Serial.print("CALIBRATION_WEIGHT: "); Serial.println(CALIBRATION_WEIGHT, 2); // Вычисляем коэффициент калибровки // scale=1.0 и tare() выполнен, поэтому weight_average - это СЫРОЕ значение АЦП // new_factor = raw_value / known_weight // Проверяем, что груз поставлен (сырое значение больше 1000 единиц АЦП) if (abs(weight_average) > 1000) { calibration_factor = weight_average / CALIBRATION_WEIGHT; Serial.print("New calibration_factor: "); Serial.println(calibration_factor, 6); // Проверяем, что коэффициент положительный if (calibration_factor <= 0) { // Ошибка: отрицательный или нулевой коэффициент - неправильная калибровка // (датчик подключен неправильно или вес убрали вместо добавления) Serial.println("ERROR: calibration_factor <= 0!"); lcd.clear(); lcd.setCursor(0, 0); lcd.print("Calibration "); lcd.setCursor(0, 1); lcd.print("ERROR: coef<0! "); delay(2000); } else { // Сохраняем коэффициент в EEPROM saveCalibrationToEEPROM(calibration_factor); Serial.println("Calibration factor saved to EEPROM"); // Устанавливаем новый коэффициент scale.set_scale(calibration_factor); Serial.println("Scale updated with new factor"); // Сообщение об успешной калибровке Serial.println("=== CALIBRATION OK ==="); lcd.clear(); lcd.setCursor(0, 0); lcd.print("Calibration "); lcd.setCursor(0, 1); lcd.print("OK! "); delay(1000); } } else { // Сообщение о неудачной калибровке (груз не поставлен или слишком легкий) Serial.print("ERROR: abs(raw_weight_average) = "); Serial.print(abs(weight_average), 2); Serial.println(" <= 1000, raw ADC value too low!"); Serial.println("Check: is 100g weight placed? Is HX711 connected?"); Serial.println("=== CALIBRATION FAILED ==="); lcd.clear(); lcd.setCursor(0, 0); lcd.print("Calibration "); lcd.setCursor(0, 1); lcd.print("Failed! "); delay(1000); } lcd.clear(); // Возврат в режим READY current_state = STATE_READY; need_display_clear = true; Serial.println("Returned to READY state"); }