first commit

This commit is contained in:
2026-01-26 16:57:54 +03:00
commit 31720fff95

816
grunt_stand.ino Normal file
View File

@@ -0,0 +1,816 @@
#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 = 30000; // 30 секунд нагрева
const unsigned long HEAT_OFF_TIME = 20000; // 20 секунд паузы
unsigned long last_cycle_change = 0; // Время последнего переключения
bool heat_cycle_on = false; // Текущее состояние цикла (вкл/выкл)
// Для постоянного расчета W с фильтрацией
float current_W = 0.0; // Текущее значение влажности (сырое)
float filtered_W = 0.0; // Отфильтрованное значение W для отображения
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
filtered_W = w_filter.update(current_W); // Фильтруем для отображения
// Обновляем максимальное значение W
if (filtered_W > max_W) {
max_W = filtered_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_filter.reset(); // Сбрасываем фильтр 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 && press_duration <= MAX_CAL_TIME) {
// Калибровка 8-10 секунд
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 m_buf[6];
// Форматируем максимальный W
if (max_W >= 100) {
strcpy(m_buf, ">99");
} else {
dtostrf(max_W, 5, 1, m_buf);
}
// Формат: "Wmax: XX.X%"
snprintf(line2, sizeof(line2), "W:%s%%", m_buf);
printCentered(1, 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);
w_filter.reset();
// Если был режим нагрева - выходим из него
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() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Calibration ");
// Если был режим нагрева - выходим из него
if (heating_active) {
digitalWrite(HEAT_PIN, LOW);
heating_active = false;
heat_cycle_on = false;
}
// Шаг 1: Clear 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);
// Сообщение о тарировании
lcd.setCursor(0, 1);
lcd.print("Taring... ");
// Тарируем весы (устанавливаем ноль)
delay(500); // Даем время на стабилизацию
scale.tare();
// Шаг 2: Set 100g
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г
lcd.setCursor(0, 1);
lcd.print("saving results ");
// Измеряем среднее значение 100г (15 измерений)
delay(1000); // Даем время на стабилизацию
float weight_sum = 0;
for (int i = 0; i < 15; i++) {
float reading = scale.get_units(1);
weight_sum += reading;
delay(100);
}
float weight_average = weight_sum / 15;
// Вычисляем коэффициент калибровки
// После tare() шкала обнулена, поэтому weight_average - это разница веса напрямую
// Проверяем, что груз действительно поставлен (вес больше 10 единиц)
if (abs(weight_average) > 10) {
calibration_factor = weight_average / CALIBRATION_WEIGHT;
// Проверяем, что коэффициент положительный
if (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);
// Устанавливаем новый коэффициент
scale.set_scale(calibration_factor);
// Сообщение об успешной калибровке
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Calibration ");
lcd.setCursor(0, 1);
lcd.print("OK! ");
delay(1000);
}
} else {
// Сообщение о неудачной калибровке (груз не поставлен или слишком легкий)
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;
}