// smart_pump.ino - ПОЛНОСТЬЮ ИСПРАВЛЕННАЯ ВЕРСИЯ
#include <U8g2lib.h>
#include <HX711.h>
#include <Preferences.h>
#include "button.h"
#include "relays.h"
#include "display.h"
#include "servo_controller.h"
#include "tuya_cloud.h"
/* ================= ПИНЫ ================= */
#define PIN_PUMP_RELAY 26
#define PIN_POWER_RELAY 25
#define PIN_BUTTON 27
#define BUZZER_PIN 33
#define PIN_SERVO 32
#define PIN_HX711_DT 16
#define PIN_HX711_SCK 4
/* ================= ВЕСА ================= */
float EMPTY_WEIGHT = 0;
float MIN_WEIGHT = 0;
float FULL_WEIGHT = 0;
#define CUP_VOLUME 250
#define WEIGHT_HYST 30
#define NO_FLOW_THRESHOLD_MS 7000
#define FILL_TIMEOUT_MS 90000
/* ================= ЗВУКОВЫЕ СИГНАЛЫ ================= */
#define BEEP_SHORT_DURATION 80
#define BEEP_LONG_DURATION 1000
#define BEEP_PAUSE_SHORT 120
#define BEEP_PAUSE_LONG 200
#define ERROR_BEEP_INTERVAL 5000 // 5 секунд между повторными сигналами
/* ================= ВРЕМЕННЫЕ КОНСТАНТЫ ================= */
#define SERVO_MOVE_TIMEOUT 8000
#define PUMP_START_DELAY 500 // Задержка перед включением насоса
#define DRIP_DELAY_MS 2000 // Время на стекание воды
#define CALIBRATION_TIMEOUT 30000 // Таймаут калибровки
#define CALIBRATION_SUCCESS_SHOW 5000 // Время показа успеха калибровки
#define CALIBRATION_REMINDER_SHOW 3000 // Время показа напоминания о калибровке
#define ERROR_AUTO_RESET_DELAY 30000 // Время автосброса ошибки
#define TUYA_UPDATE_INTERVAL 5000 // Интервал отправки в Tuya
#define STATUS_LOG_INTERVAL 10000 // Интервал логирования состояния
#define WEIGHT_UPDATE_INTERVAL 200 // Интервал обновления веса
#define DISPLAY_UPDATE_DELAY 10 // Задержка в loop
#define POWER_TOGGLE_MIN_INTERVAL 10000 // Мин. интервал между переключениями реле питания
/* ================= ГЛОБАЛЬНЫЕ ОБЪЕКТЫ ================= */
U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(U8G2_R0, U8X8_PIN_NONE);
DisplayManager displayManager(display);
HX711 scale;
Button button(PIN_BUTTON);
Relays relays(PIN_PUMP_RELAY, PIN_POWER_RELAY);
ServoController servoController;
TuyaCloud tuyaCloud;
SystemState currentState = ST_INIT;
ErrorCode lastError = ERR_NONE;
Preferences preferences;
float currentWeight = 0;
float targetWeight = 0;
float fillStartVolume = 0;
bool kettlePresent = false;
unsigned long fillStartTime = 0;
unsigned long lastWeightChangeTime = 0;
bool calibrationInProgress = false;
uint8_t calibrationClicks = 0;
unsigned long lastCalibrationClickTime = 0;
unsigned long calibrationSuccessTime = 0;
bool showCalibrationSuccess = false;
/* ================= НОВЫЕ ФЛАГИ ДЛЯ НАПОМИНАНИЙ ================= */
bool showCalibrationReminder = false;
unsigned long calibrationReminderTime = 0;
/* ================= УПРАВЛЕНИЕ ОШИБКАМИ ================= */
unsigned long lastErrorBeepTime = 0;
bool errorBeepActive = false;
unsigned long errorStartTime = 0;
bool noFlowErrorTimerValid = false; // Флаг валидности таймера для ERR_NO_FLOW
/* ================= БУЗЗЕР ================= */
void buzzerShort(uint8_t n) {
for (uint8_t i = 0; i < n; i++) {
digitalWrite(BUZZER_PIN, HIGH);
delay(BEEP_SHORT_DURATION);
digitalWrite(BUZZER_PIN, LOW);
delay(BEEP_PAUSE_SHORT);
}
}
void buzzerLong(uint8_t n) {
for (uint8_t i = 0; i < n; i++) {
digitalWrite(BUZZER_PIN, HIGH);
delay(BEEP_LONG_DURATION);
digitalWrite(BUZZER_PIN, LOW);
delay(BEEP_PAUSE_LONG);
}
}
/* ================= УТИЛИТЫ ================= */
bool isKettlePresent() {
// Если калибровка не выполнена, любой вес > 50г считаем чайником
if (EMPTY_WEIGHT <= 0) {
return (currentWeight > 50.0f);
}
// С калибровкой: вес должен быть близок к пустому чайнику или больше
return (currentWeight >= EMPTY_WEIGHT - 50);
}
float getWaterVolume() {
// Без калибровки возвращаем 0
if (EMPTY_WEIGHT <= 0) return 0;
float waterGrams = currentWeight - EMPTY_WEIGHT;
if (waterGrams < 20.0f) waterGrams = 0;
return waterGrams;
}
bool isMinWaterReached() {
if (MIN_WEIGHT <= 0) return false;
return currentWeight >= (MIN_WEIGHT - WEIGHT_HYST);
}
/* ================= СОХРАНЕНИЕ ДАННЫХ ================= */
void saveCalibrationData(float emptyWeight) {
EMPTY_WEIGHT = emptyWeight;
MIN_WEIGHT = emptyWeight + 500.0f;
FULL_WEIGHT = emptyWeight + 1700.0f;
preferences.begin("calibration", false);
preferences.putFloat("empty_weight", emptyWeight);
preferences.putFloat("min_weight", MIN_WEIGHT);
preferences.putFloat("full_weight", FULL_WEIGHT);
preferences.putBool("calibrated", true);
preferences.end();
Serial.print("[CAL] Saved calibration: ");
Serial.print("Empty="); Serial.print(emptyWeight, 1); Serial.print("g, ");
Serial.print("Min="); Serial.print(MIN_WEIGHT, 1); Serial.print("g, ");
Serial.print("Full="); Serial.print(FULL_WEIGHT, 1); Serial.println("g");
}
void loadCalibrationData() {
preferences.begin("calibration", true);
EMPTY_WEIGHT = preferences.getFloat("empty_weight", 0);
MIN_WEIGHT = preferences.getFloat("min_weight", 0);
FULL_WEIGHT = preferences.getFloat("full_weight", 0);
preferences.end();
if (EMPTY_WEIGHT > 0) {
Serial.print("[CAL] Loaded - Empty: "); Serial.print(EMPTY_WEIGHT, 1);
Serial.print("g, Min: "); Serial.print(MIN_WEIGHT, 1);
Serial.print("g, Full: "); Serial.print(FULL_WEIGHT, 1); Serial.println("g");
}
}
void resetCalibrationData() {
preferences.begin("calibration", false);
preferences.clear();
preferences.end();
EMPTY_WEIGHT = MIN_WEIGHT = FULL_WEIGHT = 0;
Serial.println("[CAL] Calibration data reset");
}
/* ================= ОБРАБОТКА ОШИБОК ================= */
void setError(ErrorCode err) {
// Если уже в ошибке, не обрабатываем повторно
if (currentState == ST_ERROR) return;
currentState = ST_ERROR;
lastError = err;
errorStartTime = millis();
Serial.print("[ERROR] System error: ");
switch (err) {
case ERR_HX711_TIMEOUT: Serial.println("HX711 timeout - weight sensor not responding"); break;
case ERR_NO_FLOW: Serial.println("No water flow detected"); break;
case ERR_FILL_TIMEOUT: Serial.println("Filling timeout - taking too long"); break;
case ERR_SERVO_JAM: Serial.println("Servo jam detected"); break;
default: Serial.println("Unknown error"); break;
}
// Аварийное отключение всего
relays.setPump(false);
relays.setPower(false);
// Безопасный возврат трубки
if (err == ERR_SERVO_JAM) {
Serial.println("[ERROR] Attempting servo recovery...");
servoController.emergencyReturn();
unsigned long start = millis();
while (millis() - start < 1000) {
servoController.update();
delay(10);
}
if (servoController.isInError()) {
servoController.forcePosition(0);
}
} else {
if (!servoController.isRetracted()) {
Serial.println("[ERROR] Retracting tube for safety");
servoController.retractTube();
}
}
// ЕДИНЫЙ ЗВУКОВОЙ СИГНАЛ для ВСЕХ аварийных ошибок
buzzerLong(3); // 3 длинных сигнала
lastErrorBeepTime = millis();
errorBeepActive = true;
// Отправка в Tuya
const char* errorMsg = "";
switch (err) {
case ERR_HX711_TIMEOUT: errorMsg = "Weight sensor error"; break;
case ERR_NO_FLOW: errorMsg = "No water flow"; break;
case ERR_FILL_TIMEOUT: errorMsg = "Filling timeout"; break;
case ERR_SERVO_JAM: errorMsg = "Servo jammed"; break;
default: errorMsg = "Unknown error"; break;
}
tuyaCloud.sendError(errorMsg);
}
void exitError() {
if (currentState == ST_ERROR) {
Serial.println("[MAIN] Exiting error state");
currentState = ST_IDLE;
lastError = ERR_NONE;
errorBeepActive = false;
noFlowErrorTimerValid = false;
buzzerShort(1); // Сигнал восстановления
}
}
/* ================= ВЕС ================= */
void updateWeight() {
static unsigned long lastRead = 0;
if (millis() - lastRead < WEIGHT_UPDATE_INTERVAL) return;
lastRead = millis();
if (scale.is_ready()) {
float w = scale.get_units(1);
if (abs(w - currentWeight) > 0.1) {
currentWeight = w;
lastWeightChangeTime = millis();
}
} else {
static int timeoutCount = 0;
timeoutCount++;
if (timeoutCount > 10 && currentState != ST_ERROR) {
setError(ERR_HX711_TIMEOUT);
}
}
}
/* ================= УПРАВЛЕНИЕ ПИТАНИЕМ ================= */
void updatePowerRelayControl() {
static unsigned long lastPowerToggleTime = 0;
static bool lastPowerState = false;
bool shouldBeOn = isMinWaterReached();
// Защита от частых переключений
if (shouldBeOn != lastPowerState) {
if (millis() - lastPowerToggleTime > POWER_TOGGLE_MIN_INTERVAL) {
relays.setPower(shouldBeOn);
lastPowerState = shouldBeOn;
lastPowerToggleTime = millis();
}
} else {
// Если состояние не меняется, просто обновляем с обычным интервалом
relays.updatePowerControl(shouldBeOn);
}
}
/* ================= ОБРАБОТКА КНОПКИ ================= */
void emergencyStopFilling() {
if (currentState != ST_FILLING) return;
Serial.println("[MAIN] Emergency stop requested");
// 1. Немедленно выключаем насос
relays.setPump(false);
// 2. Даем воде стечь
Serial.println("[MAIN] Waiting for dripping...");
delay(DRIP_DELAY_MS);
// 3. Возвращаем трубку
servoController.retractTube();
// 4. Ждем завершения возврата (но не бесконечно)
unsigned long retractStart = millis();
while (!servoController.isRetracted() && millis() - retractStart < 5000) {
servoController.update();
delay(10);
}
// 5. Переходим в IDLE
currentState = ST_IDLE;
buzzerShort(3); // 3 коротких сигнала - ручная остановка
Serial.println("[MAIN] Emergency stop complete");
}
void handleSingleClick() {
// 1. ПРОВЕРКА НАЛИЧИЯ ЧАЙНИКА
if (!isKettlePresent()) {
Serial.println("[BTN] Cannot fill: no kettle detected");
buzzerShort(2);
return;
}
// 2. ПРОВЕРКА КАЛИБРОВКИ
if (EMPTY_WEIGHT <= 0) {
Serial.println("[BTN] Cannot fill: calibration required");
Serial.println("[BTN] Press button 3 times to calibrate");
// Просто звуковой сигнал, экран калибровки уже активен
buzzerShort(2);
return;
}
// 3. ПРОВЕРКА ПОЗИЦИИ ТРУБКИ
if (!servoController.isRetracted()) {
Serial.println("[BTN] Cannot fill: tube not retracted");
buzzerShort(2);
return;
}
// 4. ПОЛУЧАЕМ ТЕКУЩИЙ ОБЪЕМ ВОДЫ
float currentWater = getWaterVolume();
Serial.print("[BTN] Current water volume: ");
Serial.print(currentWater);
Serial.println("ml");
// 5. ОПРЕДЕЛЯЕМ, СКОЛЬКО ДОЛИВАТЬ
float add;
float targetWater;
if (currentWater < 500.0f - WEIGHT_HYST) {
add = 500.0f - currentWater;
targetWater = 500.0f;
Serial.print("[BTN] Filling to min level (+");
Serial.print(add);
Serial.println("ml)");
} else {
add = CUP_VOLUME;
targetWater = currentWater + CUP_VOLUME;
Serial.println("[BTN] Adding one cup (250ml)");
}
// 6. ПРОВЕРКА НА МИНИМАЛЬНОЕ ДОБАВЛЕНИЕ
if (add < 10.0f) {
Serial.println("[BTN] Already at or above target");
buzzerShort(1);
return;
}
// 7. ПРОВЕРКА НА ПРЕВЫШЕНИЕ МАКСИМУМА
if (targetWater > 1700.0f + WEIGHT_HYST) {
Serial.println("[BTN] Would exceed max capacity (1700ml)");
float maxAdd = 1700.0f - currentWater;
if (maxAdd > 10.0f) {
Serial.print("[BTN] Use double click to fill to max (+");
Serial.print(maxAdd);
Serial.println("ml)");
}
buzzerShort(2);
return;
}
// 8. ЗАЩИТА ОТ СЛИШКОМ БОЛЬШОГО ДОБАВЛЕНИЯ
if (add > 1000.0f) {
Serial.println("[BTN] ERROR: Attempting to add too much water");
buzzerShort(3);
return;
}
// 9. РАССЧИТЫВАЕМ ЦЕЛЕВОЙ ВЕС
float newTotalWeight = EMPTY_WEIGHT + targetWater;
Serial.print("[BTN] Target weight: ");
Serial.print(newTotalWeight);
Serial.print("g (water: ");
Serial.print(targetWater);
Serial.println("ml)");
// 10. ЗАПУСКАЕМ НАЛИВ
startFillingSequence(newTotalWeight);
}
void handleDoubleClick() {
// 1. ПРОВЕРКА НАЛИЧИЯ ЧАЙНИКА
if (!isKettlePresent()) {
Serial.println("[BTN] Cannot fill: no kettle detected");
if (currentWeight > 50.0f && EMPTY_WEIGHT <= 0) {
Serial.println("[BTN] Kettle detected but calibration required");
Serial.println("[BTN] Press button 3 times to calibrate");
showCalibrationReminder = true;
calibrationReminderTime = millis();
}
buzzerShort(2);
return;
}
// 2. ПРОВЕРКА КАЛИБРОВКИ
if (EMPTY_WEIGHT <= 0) {
Serial.println("[BTN] Cannot fill: calibration required");
Serial.println("[BTN] Press button 3 times to calibrate");
showCalibrationReminder = true;
calibrationReminderTime = millis();
buzzerShort(2);
return;
}
// 3. ПРОВЕРКА ПОЗИЦИИ ТРУБКИ
if (!servoController.isRetracted()) {
Serial.println("[BTN] Cannot fill: tube not retracted");
buzzerShort(2);
return;
}
// 4. ПОЛУЧАЕМ ТЕКУЩИЙ ОБЪЕМ
float currentWater = getWaterVolume();
float maxWater = 1700.0f;
float add = maxWater - currentWater;
// 5. ПРОВЕРКИ
if (add < 10.0f) {
Serial.println("[BTN] Already full or above");
buzzerShort(1);
return;
}
if (add > CUP_VOLUME * 8) {
Serial.println("[BTN] Too much to fill at once");
buzzerShort(2);
return;
}
Serial.print("[BTN] Filling to full (+");
Serial.print(add);
Serial.println("ml)");
// 6. ЗАПУСКАЕМ НАЛИВ
startFillingSequence(EMPTY_WEIGHT + maxWater);
}
void handleButtonAction(ButtonAction action) {
// В состоянии ошибки обрабатываем только VERY_LONG_PRESS для сброса
if (currentState == ST_ERROR) {
if (action == BTN_VERY_LONG_PRESS) {
Serial.println("[MAIN] System reset requested from error state");
resetCalibrationData();
buzzerLong(3);
delay(1000);
ESP.restart();
}
return;
}
// Защита от двойной обработки
static bool veryLongPressHandled = false;
switch (action) {
case BTN_SINGLE_CLICK:
if (!veryLongPressHandled) handleSingleClick();
break;
case BTN_DOUBLE_CLICK:
if (!veryLongPressHandled) handleDoubleClick();
break;
case BTN_TRIPLE_CLICK:
if (!veryLongPressHandled) startCalibration();
break;
case BTN_LONG_PRESS:
if (!veryLongPressHandled && currentState == ST_FILLING) {
emergencyStopFilling();
}
break;
case BTN_VERY_LONG_PRESS:
veryLongPressHandled = true;
Serial.println("[MAIN] Factory reset requested");
resetCalibrationData();
buzzerLong(3);
delay(1000);
ESP.restart();
break;
default:
break;
}
if (action != BTN_VERY_LONG_PRESS) {
veryLongPressHandled = false;
}
}
void startFillingSequence(float target) {
Serial.print("[FILL] Starting filling sequence to ");
Serial.print(target);
Serial.println("g");
servoController.moveToKettle();
targetWeight = target;
fillStartVolume = getWaterVolume();
currentState = ST_FILLING;
fillStartTime = millis();
lastWeightChangeTime = millis();
buzzerShort(1);
}
/* ================= КАЛИБРОВКА ================= */
void startCalibration() {
Serial.println("[CAL] Starting calibration");
if (!servoController.isRetracted()) {
servoController.retractTube();
delay(1000);
}
calibrationInProgress = true;
calibrationClicks = 0;
lastCalibrationClickTime = millis();
showCalibrationSuccess = false;
showCalibrationReminder = false; // Сбрасываем напоминание
noFlowErrorTimerValid = false;
relays.setPump(false);
relays.setPower(false);
currentState = ST_INIT;
displayManager.setCalibrationMode(true);
Serial.println("[CAL] Place EMPTY kettle on scale");
Serial.println("[CAL] Then press button 3 times");
buzzerShort(1);
}
void handleCalibration() {
ButtonAction action = button.update();
if (action == BTN_SINGLE_CLICK) {
calibrationClicks++;
lastCalibrationClickTime = millis();
Serial.print("[CAL] Click #"); Serial.println(calibrationClicks);
buzzerShort(1);
if (calibrationClicks >= 3) {
delay(1000); // Стабилизация
float emptyWeight = scale.get_units(10);
Serial.print("[CAL] Empty kettle weight: ");
Serial.print(emptyWeight, 1);
Serial.println("g");
if (emptyWeight < 50.0f || emptyWeight > 3000.0f) {
Serial.println("[CAL] ERROR: Unreasonable weight");
buzzerShort(3);
calibrationClicks = 0;
lastCalibrationClickTime = millis(); // Сбрасываем таймер
return;
}
saveCalibrationData(emptyWeight);
calibrationInProgress = false;
showCalibrationSuccess = true;
calibrationSuccessTime = millis();
Serial.println("[CAL] Calibration complete!");
buzzerShort(2);
displayManager.setCalibrationMode(false);
displayManager.setCalibrationSuccessMode(true);
}
}
// Отмена по длинному нажатию
if (action == BTN_LONG_PRESS) {
Serial.println("[CAL] Cancelled");
calibrationInProgress = false;
displayManager.setCalibrationMode(false);
buzzerShort(2);
currentState = ST_IDLE;
return;
}
// Таймаут - НО НЕ ВЫХОДИМ ИЗ РЕЖИМА КАЛИБРОВКИ!
if (calibrationInProgress && millis() - lastCalibrationClickTime > CALIBRATION_TIMEOUT) {
// Просто сбрасываем счетчик кликов, но остаемся в режиме калибровки
Serial.println("[CAL] Click timeout - resetting counter");
calibrationClicks = 0;
lastCalibrationClickTime = millis();
// Даем звуковой сигнал, чтобы пользователь знал
buzzerShort(2); // Два коротких - сигнал сброса
// Обновляем экран, чтобы показать, что нужно начать заново
// Экран калибровки все еще активен
}
}
/* ================= ОБРАБОТКА НАЛИВА ================= */
void handleFilling() {
static enum {
MOVING,
READY,
FILLING,
DRIPPING,
COMPLETING
} stage = MOVING;
static unsigned long stageStartTime = 0;
static unsigned long servoMoveStartTime = 0;
static bool tubeRetractionStarted = false;
switch (stage) {
case MOVING:
// Просто ждем, пока серво достигнет цели
if (servoController.isOverKettle()) {
stage = READY;
stageStartTime = millis();
servoMoveStartTime = 0;
Serial.println("[FILL] Tube positioned over kettle");
} else if (servoMoveStartTime == 0) {
// Серво уже должно двигаться с момента startFillingSequence
servoMoveStartTime = millis();
Serial.println("[FILL] Waiting for servo to reach position...");
} else {
// Проверяем только очень долгий таймаут (8 секунд)
if (millis() - servoMoveStartTime > 8000) {
if (servoController.isInError()) {
setError(ERR_SERVO_JAM);
return;
} else {
// Все еще ждем, просто логируем
static unsigned long lastWarnLog = 0;
if (millis() - lastWarnLog > 2000) {
lastWarnLog = millis();
Serial.println("[FILL] Still waiting for servo...");
}
}
}
}
break;
case DRIPPING:
if (millis() - stageStartTime >= DRIP_DELAY_MS) {
stage = COMPLETING;
stageStartTime = millis();
tubeRetractionStarted = false;
Serial.println("[FILL] Dripping completed");
}
break;
case COMPLETING:
if (!tubeRetractionStarted) {
servoController.retractTube();
tubeRetractionStarted = true;
Serial.println("[FILL] Starting tube retraction");
}
if (servoController.isRetracted()) {
currentState = ST_IDLE;
stage = MOVING;
stageStartTime = 0;
servoMoveStartTime = 0;
tubeRetractionStarted = false;
Serial.println("[FILL] Filling completed successfully");
buzzerShort(2);
}
// Проверка таймаута возврата
if (tubeRetractionStarted && millis() - stageStartTime > 5000) {
Serial.println("[FILL] WARNING: Tube retraction taking too long, forcing complete");
currentState = ST_IDLE;
stage = MOVING;
stageStartTime = 0;
servoMoveStartTime = 0;
tubeRetractionStarted = false;
buzzerShort(3);
}
break;
}
}
/* ================= ОБРАБОТКА КОМАНД TUYA ================= */
void handleTuyaCommand(TuyaCommand cmd, uint8_t param) {
Serial.print("[TUYA] Command received: "); Serial.println(cmd);
switch (cmd) {
case TUYA_CMD_FILL_CUP:
handleSingleClick();
break;
case TUYA_CMD_FILL_TWO_CUPS:
startFillingSequence(currentWeight + (param > 0 ? param * CUP_VOLUME : 2 * CUP_VOLUME));
break;
case TUYA_CMD_FILL_FULL:
handleDoubleClick();
break;
case TUYA_CMD_STOP:
if (currentState == ST_FILLING) {
emergencyStopFilling();
}
break;
case TUYA_CMD_CALIBRATE:
startCalibration();
break;
default:
Serial.println("[TUYA] Unknown command");
break;
}
}
/* ================= SETUP ================= */
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("=======================================");
Serial.println(" SMART PUMP STARTING");
Serial.println("=======================================");
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
relays.begin();
servoController.begin(PIN_SERVO);
displayManager.begin();
displayManager.setCalibrationPointers(&calibrationClicks, &lastCalibrationClickTime);
loadCalibrationData();
Serial.println("[MAIN] Initializing weight sensor...");
scale.begin(PIN_HX711_DT, PIN_HX711_SCK);
scale.set_scale(0.42f);
delay(500);
if (scale.is_ready()) {
currentWeight = scale.get_units(5);
Serial.print("[MAIN] Current weight: ");
Serial.print(currentWeight, 1);
Serial.println("g");
if (EMPTY_WEIGHT > 0) {
// Калибровка есть - нормальный режим
currentState = ST_IDLE;
kettlePresent = isKettlePresent();
Serial.print("[MAIN] Empty weight (calibrated): ");
Serial.print(EMPTY_WEIGHT, 1);
Serial.println("g");
Serial.print("[MAIN] Water volume: ");
Serial.print(getWaterVolume(), 0);
Serial.println("ml");
relays.updatePowerControl(isMinWaterReached());
buzzerShort(1); // 1 короткий - успешная инициализация
} else {
// Калибровки нет - АВТОМАТИЧЕСКИ ЗАПУСКАЕМ КАЛИБРОВКУ
Serial.println("[MAIN] No calibration found - starting calibration mode");
Serial.println("[MAIN] Place EMPTY kettle on scale and press button 3 times");
// Сбрасываем серво в исходное положение
if (!servoController.isRetracted()) {
servoController.retractTube();
delay(1000);
}
// Запускаем режим калибровки
calibrationInProgress = true;
calibrationClicks = 0;
lastCalibrationClickTime = millis();
showCalibrationSuccess = false;
showCalibrationReminder = false; // Не используем напоминание, сразу калибровка
relays.setPump(false);
relays.setPower(false);
currentState = ST_INIT; // Будет показан экран калибровки через DisplayManager
displayManager.setCalibrationMode(true); // Включаем режим калибровки в дисплее
// Звуковой сигнал о входе в режим калибровки
buzzerShort(2); // 2 коротких сигнала
}
} else {
Serial.println("[MAIN] ERROR: Weight sensor not responding");
setError(ERR_HX711_TIMEOUT);
}
tuyaCloud.begin();
tuyaCloud.setCommandCallback(handleTuyaCommand);
Serial.println("[MAIN] Setup complete");
Serial.println("=======================================");
}
/* ================= LOOP ================= */
void loop() {
// 1. Обновление веса
updateWeight();
// 2. Обновление сервопривода
servoController.update();
// 3. Проверка ошибок сервопривода
if (servoController.isInError() && currentState != ST_ERROR) {
Serial.println("[MAIN] Servo error detected in main loop");
setError(ERR_SERVO_JAM);
}
// 4. Обработка режимов
if (calibrationInProgress) {
// В режиме калибровки handleCalibration() сам обновляет дисплей
handleCalibration();
}
else if (showCalibrationSuccess) {
// Показываем экран успешной калибровки
if (millis() - calibrationSuccessTime < CALIBRATION_SUCCESS_SHOW) {
displayManager.update(ST_INIT, lastError, false, currentWeight, 0, 0, false, EMPTY_WEIGHT);
} else {
showCalibrationSuccess = false;
displayManager.setCalibrationSuccessMode(false);
currentState = ST_IDLE;
if (scale.is_ready()) {
currentWeight = scale.get_units(3);
}
kettlePresent = isKettlePresent();
relays.updatePowerControl(isMinWaterReached());
Serial.println("[MAIN] Calibration complete, entering normal mode");
}
}
else {
// Основной рабочий режим
kettlePresent = isKettlePresent();
updatePowerRelayControl();
ButtonAction action = button.update();
if (action != BTN_NONE) {
handleButtonAction(action);
}
if (currentState == ST_FILLING) {
handleFilling();
}
// 5. Обработка состояния ошибки
if (currentState == ST_ERROR) {
// Повторяющиеся ОДИНАРНЫЕ сигналы каждые 5 секунд
if (errorBeepActive && millis() - lastErrorBeepTime > ERROR_BEEP_INTERVAL) {
buzzerShort(1);
lastErrorBeepTime = millis();
}
// Автоматическое восстановление после некоторых ошибок
if (lastError == ERR_HX711_TIMEOUT && scale.is_ready()) {
float testWeight = scale.get_units(1);
if (testWeight > 0) {
Serial.println("[MAIN] Weight sensor recovered, resetting error");
exitError();
currentWeight = testWeight;
}
}
// Автоматическое восстановление после ERR_NO_FLOW
if (lastError == ERR_NO_FLOW && noFlowErrorTimerValid) {
static unsigned long noFlowErrorTime = 0;
if (noFlowErrorTime == 0) {
noFlowErrorTime = millis();
}
if (millis() - noFlowErrorTime > ERROR_AUTO_RESET_DELAY) {
Serial.println("[MAIN] Auto-reset after no-flow error");
exitError();
noFlowErrorTime = 0;
}
}
}
}
// 6. Обновление Tuya Cloud
tuyaCloud.loop();
// 7. Периодическая отправка состояния в Tuya
static unsigned long lastTuyaUpdate = 0;
if (millis() - lastTuyaUpdate > TUYA_UPDATE_INTERVAL) {
lastTuyaUpdate = millis();
TuyaDeviceState tuyaState;
switch (currentState) {
case ST_IDLE: tuyaState = TUYA_STATE_IDLE; break;
case ST_FILLING: tuyaState = TUYA_STATE_FILLING; break;
case ST_ERROR: tuyaState = TUYA_STATE_ERROR; break;
case ST_INIT: tuyaState = TUYA_STATE_CALIBRATING; break;
default: tuyaState = TUYA_STATE_IDLE; break;
}
float waterVolume = getWaterVolume();
int cups = DisplayManager::mlToCups(waterVolume);
tuyaCloud.sendState(tuyaState, waterVolume, cups,
kettlePresent, relays.getPowerState(),
relays.getPumpState());
// Логирование состояния
static unsigned long lastStateLog = 0;
if (millis() - lastStateLog > STATUS_LOG_INTERVAL) {
lastStateLog = millis();
Serial.print("[STATUS] State: ");
switch (currentState) {
case ST_IDLE: Serial.print("IDLE"); break;
case ST_FILLING: Serial.print("FILLING"); break;
case ST_ERROR: Serial.print("ERROR"); break;
case ST_INIT: Serial.print("INIT"); break;
}
Serial.print(", Weight: "); Serial.print(currentWeight);
Serial.print("g, Water: "); Serial.print(waterVolume);
Serial.print("ml, Cups: "); Serial.print(cups);
Serial.print(", Kettle: "); Serial.print(kettlePresent ? "YES" : "NO");
Serial.print(", Power: "); Serial.print(relays.getPowerState() ? "ON" : "OFF");
Serial.print(", Pump: "); Serial.println(relays.getPumpState() ? "ON" : "OFF");
}
}
// 8. Обновление дисплея
displayManager.update(currentState, lastError, kettlePresent,
currentWeight, targetWeight, fillStartVolume,
relays.getPowerState(), EMPTY_WEIGHT);
static unsigned long lastServoLog = 0;
if (millis() - lastServoLog > 5000) {
lastServoLog = millis();
Serial.print("[SERVO] State: ");
if (servoController.isOverKettle()) Serial.print("OVER_KETTLE");
else if (servoController.isRetracted()) Serial.print("RETRACTED");
else if (servoController.isMoving()) Serial.print("MOVING");
else if (servoController.isInError()) Serial.print("ERROR");
else Serial.print("IDLE");
Serial.print(", Angle: ");
Serial.print(servoController.getCurrentAngle());
Serial.println("°");
}
delay(DISPLAY_UPDATE_DELAY);
}