first commit
This commit is contained in:
816
grunt_stand.ino
Normal file
816
grunt_stand.ino
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user