883 lines
27 KiB
C++
883 lines
27 KiB
C++
#include <LiquidCrystal.h>
|
||
#include <HX711.h>
|
||
#include <EEPROM.h>
|
||
|
||
// 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");
|
||
} |