// smart_pump.ino - Исправленная версия
#include <U8g2lib.h>
#include <HX711.h>
#include <Preferences.h>
#include <WebServer.h>
#include "wifi_config.h"
#include "button.h"
#include "relays.h"
#include "display.h"
#include "servo_controller.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; // EMPTY_WEIGHT + 500 мл
float FULL_WEIGHT = 0; // EMPTY_WEIGHT + 1700 мл
#define CUP_VOLUME 250
#define WEIGHT_HYST 20 // вес воды для гистерезиса
#define NO_FLOW_THRESHOLD_MS 5000 // 5 секунд для детекции "нет потока"
#define FILL_TIMEOUT_MS 120000 // 2 минуты на максимальный налив
/* ================= ГЛОБАЛЬНЫЕ ОБЪЕКТЫ ================= */
U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(U8G2_R0, U8X8_PIN_NONE);
DisplayManager displayManager(display);
ServoController servoController;
HX711 scale;
Button button(PIN_BUTTON);
Relays relays(PIN_PUMP_RELAY, PIN_POWER_RELAY);
WebServer server(80); // ← ОБЪЕКТ server, НЕ sserver!
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;
#define CALIBRATION_CLICK_COUNT 3
#define CALIBRATION_CLICK_TIMEOUT 2000
bool needsCalibration = true; // По умолчанию нужна калибровка
bool calibrationInProgress = false;
uint8_t calibrationClicks = 0;
unsigned long lastCalibrationClickTime = 0;
unsigned long calibrationSuccessTime = 0;
bool showCalibrationSuccess = false;
/* ================= БУЗЗЕР ================= */
void buzzerShort(uint8_t n) {
for (uint8_t i = 0; i < n; i++) {
digitalWrite(BUZZER_PIN, HIGH);
delay(80);
digitalWrite(BUZZER_PIN, LOW);
delay(120);
}
}
void buzzerLong(uint8_t n) {
for (uint8_t i = 0; i < n; i++) {
digitalWrite(BUZZER_PIN, HIGH);
delay(1000);
digitalWrite(BUZZER_PIN, LOW);
delay(200);
}
}
/* ================= УТИЛИТЫ ================= */
bool isKettlePresent() {
if (EMPTY_WEIGHT <= 0) return false; // Если не откалибровано
return currentWeight >= (EMPTY_WEIGHT - WEIGHT_HYST);
}
bool isMinWaterReached() {
if (MIN_WEIGHT <= 0) return false; // Если не откалибровано
return currentWeight >= (MIN_WEIGHT - WEIGHT_HYST);
}
float getWaterVolume() {
if (EMPTY_WEIGHT <= 0) return 0; // Если не откалибровано
float vol = currentWeight - EMPTY_WEIGHT;
return (vol < 0) ? 0 : vol;
}
float getTargetWaterVolume() {
if (EMPTY_WEIGHT <= 0) return 0; // Если не откалибровано
float vol = targetWeight - EMPTY_WEIGHT;
return (vol < 0) ? 0 : vol;
}
bool getPowerRelayState() {
return relays.getPowerState();
}
/* ================= СОХРАНЕНИЕ И ЗАГРУЗКА ДАННЫХ ================= */
void saveCalibrationData(float emptyWeight) {
preferences.begin("calibration", false);
preferences.putFloat("empty_weight", emptyWeight);
float minWeight = emptyWeight + 500;
float fullWeight = emptyWeight + 1700;
preferences.putFloat("min_weight", minWeight);
preferences.putFloat("full_weight", fullWeight);
preferences.putBool("calibrated", true);
preferences.end();
Serial.print("[CALIBRATION] Saved: empty=");
Serial.print(emptyWeight);
Serial.print(", min=");
Serial.print(minWeight);
Serial.print(", full=");
Serial.println(fullWeight);
}
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);
bool calibrated = preferences.getBool("calibrated", false);
preferences.end();
needsCalibration = !calibrated;
if (!needsCalibration) {
Serial.print("[CALIBRATION] Loaded: empty=");
Serial.print(EMPTY_WEIGHT);
Serial.print(", min=");
Serial.print(MIN_WEIGHT);
Serial.print(", full=");
Serial.println(FULL_WEIGHT);
} else {
Serial.println("[CALIBRATION] No calibration data found");
}
}
void resetCalibrationData() {
preferences.begin("calibration", false);
preferences.clear();
preferences.end();
EMPTY_WEIGHT = 0;
MIN_WEIGHT = 0;
FULL_WEIGHT = 0;
needsCalibration = true;
Serial.println("[CALIBRATION] All calibration data reset");
}
void startCalibration() {
// Проверяем, что трубка отведена
if (!servoController.isRetracted()) {
Serial.println("[CAL] Cannot start calibration - tube not retracted");
buzzerShort(2);
return;
}
calibrationInProgress = true;
calibrationClicks = 0;
lastCalibrationClickTime = millis();
showCalibrationSuccess = false;
Serial.println("Starting calibration process");
buzzerShort(1);
// Гарантируем, что все реле выключены
relays.setPump(false);
relays.setPower(false);
// Возвращаем систему в состояние инициализации для калибровки
currentState = ST_INIT;
// Активируем режим калибровки на дисплее
displayManager.setCalibrationMode(true);
Serial.println("[CAL] Place empty kettle on platform");
Serial.println("[CAL] Press button 3 times within 2 seconds");
}
void handleCalibration() {
ButtonAction action = button.update();
if (action == BTN_SINGLE_CLICK) {
unsigned long now = millis();
if (now - lastCalibrationClickTime > CALIBRATION_CLICK_TIMEOUT) {
calibrationClicks = 0;
Serial.println("[CAL] Reset click counter");
}
calibrationClicks++;
lastCalibrationClickTime = now;
Serial.print("[CAL] Click #");
Serial.println(calibrationClicks);
buzzerShort(1);
if (calibrationClicks >= CALIBRATION_CLICK_COUNT) {
completeCalibration();
}
}
if (action == BTN_LONG_PRESS) {
Serial.println("[CAL] Calibration cancelled");
calibrationInProgress = false;
displayManager.setCalibrationMode(false);
buzzerShort(2);
currentState = ST_IDLE;
}
}
void completeCalibration() {
// Проверяем, что трубка отведена
if (!servoController.isRetracted()) {
Serial.println("[CAL] Error: Tube not retracted during calibration");
servoController.retractTube();
delay(1000);
}
if (currentWeight <= 0) {
Serial.println("[CAL] Error: Invalid weight for calibration");
buzzerShort(2);
return;
}
float emptyWeight = currentWeight;
saveCalibrationData(emptyWeight);
EMPTY_WEIGHT = emptyWeight;
MIN_WEIGHT = emptyWeight + 500;
FULL_WEIGHT = emptyWeight + 1700;
calibrationInProgress = false;
needsCalibration = false;
showCalibrationSuccess = true;
calibrationSuccessTime = millis();
Serial.println("Calibration completed successfully!");
Serial.print("Empty weight: ");
Serial.print(EMPTY_WEIGHT);
Serial.print(", Min weight: ");
Serial.print(MIN_WEIGHT);
Serial.print(", Full weight: ");
Serial.println(FULL_WEIGHT);
// Три коротких сигнала успеха
buzzerShort(3);
displayManager.setCalibrationMode(false);
displayManager.setCalibrationSuccessMode(true);
// После калибровки запускаем Wi-Fi
wifiConfig.begin();
// Проверяем наличие чайника после калибровки
kettlePresent = isKettlePresent();
if (kettlePresent) {
Serial.println("[CAL] Kettle detected after calibration");
} else {
Serial.println("[CAL] No kettle after calibration");
}
}
/* ================= ОШИБКА ================= */
void setError(ErrorCode err) {
currentState = ST_ERROR;
lastError = err;
// Останавливаем помпу
relays.setPump(false);
// Возвращаем трубку при ошибке
if (!servoController.isRetracted()) {
servoController.retractTube();
}
// Останавливаем питание чайника
relays.setPower(false);
buzzerLong(3);
Serial.print("ERROR: ");
switch (err) {
case ERR_HX711_TIMEOUT: Serial.println("Weight sensor timeout"); break;
case ERR_NO_FLOW: Serial.println("No water flow"); break;
case ERR_FILL_TIMEOUT: Serial.println("Filling timeout"); break;
default: break;
}
}
/* ================= ВЕС ================= */
void updateWeight() {
static unsigned long lastRead = 0;
static unsigned long hxTimeout = 0;
if (millis() - lastRead < 200) return;
lastRead = millis();
if (scale.is_ready()) {
hxTimeout = 0;
float w = scale.get_units(1);
if (abs(w - currentWeight) > 0.1) {
currentWeight = w;
lastWeightChangeTime = millis();
}
} else {
if (hxTimeout == 0) hxTimeout = millis();
if (millis() - hxTimeout > 2000) {
setError(ERR_HX711_TIMEOUT);
}
}
}
/* ================= УПРАВЛЕНИЕ ПИТАНИЕМ ЧАЙНИКА ================= */
void updatePowerRelayControl() {
if (MIN_WEIGHT <= 0) {
relays.updatePowerControl(false);
return;
}
bool shouldBeOn = isMinWaterReached();
relays.updatePowerControl(shouldBeOn);
}
/* ================= HTTP API ДЛЯ HOME ASSISTANT ================= */
void setupAPI() {
// Статус системы - ИСПРАВЛЕНА ОШИБКА: server.on, НЕ sserver.on!
server.on("/api/status", HTTP_GET, []() {
String json = "{";
json += "\"state\":\"" + String(currentState == ST_IDLE ? "idle" :
currentState == ST_FILLING ? "filling" :
currentState == ST_ERROR ? "error" : "init") + "\",";
json += "\"weight\":" + String(currentWeight, 1) + ",";
float waterVolume = getWaterVolume();
int cups = displayManager.mlToCups(waterVolume);
json += "\"water_volume\":" + String(waterVolume, 0) + ",";
json += "\"water_cups\":" + String(cups) + ",";
json += "\"kettle_present\":" + String(isKettlePresent() ? "true" : "false") + ",";
json += "\"power_on\":" + String(getPowerRelayState() ? "true" : "false") + ",";
json += "\"available_volume\":" + String(FULL_WEIGHT - currentWeight, 0) + ",";
json += "\"available_cups\":" + String(int((FULL_WEIGHT - currentWeight) / CUP_VOLUME)) + ",";
// Текстовое описание для голосового помощника
String statusText = "";
if (!isKettlePresent()) {
statusText = "чайник отсутствует";
} else if (waterVolume == 0) {
statusText = "чайник пустой";
} else if (waterVolume < 500) {
statusText = "меньше двух кружек";
} else {
statusText = String(cups) + " кружек";
if (cups == 1) statusText = "одна кружка";
else if (cups >= 2 && cups <= 4) statusText += " кружки";
else statusText += " кружек";
}
json += "\"status_text\":\"" + statusText + "\"";
json += "}";
server.send(200, "application/json", json);
});
// Команды управления
server.on("/api/command", HTTP_POST, []() {
if (server.hasArg("plain")) {
String command = server.arg("plain");
command.trim();
if (command == "fill_cup") {
handleSingleClick();
server.send(200, "text/plain", "OK - fill cup");
} else if (command == "fill_2_cups") {
handleMultipleCups(2);
server.send(200, "text/plain", "OK - fill 2 cups");
} else if (command == "fill_3_cups") {
handleMultipleCups(3);
server.send(200, "text/plain", "OK - fill 3 cups");
} else if (command == "fill_4_cups") {
handleMultipleCups(4);
server.send(200, "text/plain", "OK - fill 4 cups");
} else if (command == "fill_5_cups") {
handleMultipleCups(5);
server.send(200, "text/plain", "OK - fill 5 cups");
} else if (command == "fill_6_cups") {
handleMultipleCups(6);
server.send(200, "text/plain", "OK - fill 6 cups");
} else if (command == "fill_full") {
handleDoubleClick();
server.send(200, "text/plain", "OK - fill full");
} else if (command == "stop") {
handleLongPress();
server.send(200, "text/plain", "OK - stop");
} else if (command == "calibrate") {
startCalibration();
server.send(200, "text/plain", "OK - calibrate");
} else {
server.send(400, "text/plain", "Unknown command");
}
} else {
server.send(400, "text/plain", "No command");
}
});
server.begin();
Serial.println("HTTP API server started");
}
/* ================= ОБРАБОТКА КНОПКИ ================= */
void handleButtonAction(ButtonAction action) {
Serial.print("[MAIN] Button action: ");
switch (action) {
case BTN_SINGLE_CLICK:
Serial.println("SINGLE CLICK");
handleSingleClick();
break;
case BTN_DOUBLE_CLICK:
Serial.println("DOUBLE CLICK");
handleDoubleClick();
break;
case BTN_TRIPLE_CLICK:
Serial.println("TRIPLE CLICK");
handleTripleClick();
break;
case BTN_LONG_PRESS:
Serial.println("LONG PRESS");
handleLongPress();
break;
case BTN_VERY_LONG_PRESS:
Serial.println("VERY LONG PRESS");
handleVeryLongPress();
break;
default:
break;
}
}
void handleFilling() {
static enum FillingStage {
STAGE_MOVING_TUBE, // Движение трубки к чайнику
STAGE_READY_TO_FILL, // Трубка на месте, можно начинать налив
STAGE_FILLING, // Идет налив воды
STAGE_COMPLETING // Завершение налива
} currentStage = STAGE_MOVING_TUBE;
static unsigned long stageStartTime = 0;
switch (currentStage) {
case STAGE_MOVING_TUBE:
// Ждем, пока трубка переместится над чайником
if (servoController.isOverKettle()) {
Serial.println("[MAIN] Tube in position, starting pump");
currentStage = STAGE_READY_TO_FILL;
stageStartTime = millis();
} else if (servoController.isMoving()) {
// Сервопривод еще двигается - ждем
return;
} else {
// Ошибка - сервопривод не двигается
Serial.println("[MAIN] ERROR: Tube movement failed");
setError(ERR_NO_FLOW);
return;
}
break;
case STAGE_READY_TO_FILL:
// Небольшая пауза после движения сервы (500 мс)
if (millis() - stageStartTime >= 500) {
Serial.println("[MAIN] Starting water flow");
relays.setPump(true);
currentStage = STAGE_FILLING;
stageStartTime = millis();
}
break;
case STAGE_FILLING:
// Существующая логика налива с проверками
if (millis() - fillStartTime > FILL_TIMEOUT_MS) {
setError(ERR_FILL_TIMEOUT);
return;
}
float currentWater = getWaterVolume();
static float lastWater = 0;
if (abs(currentWater - lastWater) > 5) {
lastWater = currentWater;
lastWeightChangeTime = millis();
}
if (millis() - lastWeightChangeTime > NO_FLOW_THRESHOLD_MS &&
currentWeight < targetWeight - 50) {
setError(ERR_NO_FLOW);
return;
}
if (currentWeight >= targetWeight - WEIGHT_HYST) {
// Достигнут целевой вес - останавливаем помпу
relays.setPump(false);
currentStage = STAGE_COMPLETING;
stageStartTime = millis();
Serial.println("[MAIN] Target weight reached, stopping pump");
}
break;
case STAGE_COMPLETING:
// Пауза после остановки помпы (1 секунда)
if (millis() - stageStartTime >= 1000) {
// Возвращаем трубку в исходное положение
servoController.retractTube();
// Ждем, пока трубка вернется
if (servoController.isRetracted()) {
// Процесс завершен
currentState = ST_IDLE;
currentStage = STAGE_MOVING_TUBE; // Сброс для следующего раза
buzzerShort(2);
Serial.println("[MAIN] Filling completed successfully");
}
}
break;
}
}
void handleSingleClick() {
if (currentState == ST_ERROR) {
Serial.println("[MAIN] Ignoring button - system error");
return;
}
if (needsCalibration) {
Serial.println("[MAIN] Ignoring button - needs calibration");
buzzerShort(2);
return;
}
if (!isKettlePresent()) {
Serial.println("[MAIN] Ignoring button - no kettle");
buzzerShort(2);
return;
}
if (currentState != ST_IDLE) {
Serial.println("[MAIN] Ignoring click - not idle");
return;
}
// Проверяем, что трубка отведена
if (!servoController.isRetracted()) {
Serial.println("[MAIN] Cannot start - tube not retracted");
buzzerShort(2);
return;
}
float add = 0;
if (currentWeight < MIN_WEIGHT - WEIGHT_HYST) {
add = MIN_WEIGHT - currentWeight;
Serial.println("[MAIN] Single click: fill to minimum (500ml)");
} else {
add = CUP_VOLUME;
Serial.println("[MAIN] Single click: add one cup (+250ml)");
}
if (add > WEIGHT_HYST && currentWeight + add <= FULL_WEIGHT + WEIGHT_HYST) {
// Начинаем процесс налива
Serial.println("[MAIN] Starting filling sequence...");
// 1. Перемещаем трубку над чайником
servoController.moveToKettle();
// 2. Устанавливаем целевой вес и параметры налива
targetWeight = currentWeight + add;
fillStartVolume = getWaterVolume();
// 3. Устанавливаем состояние "подготовка к наливу"
currentState = ST_FILLING;
// 4. Начинаем отсчет времени
fillStartTime = millis();
lastWeightChangeTime = millis();
// 5. Звуковой сигнал начала процесса
buzzerShort(1);
Serial.print("[MAIN] Waiting for tube to move... Target: ");
Serial.print(targetWeight, 0);
Serial.println("g");
} else {
Serial.println("[MAIN] Cannot fill - weight constraints");
buzzerShort(2);
}
}
void handleDoubleClick() {
if (currentState == ST_ERROR) {
Serial.println("[MAIN] Ignoring button - system error");
return;
}
if (needsCalibration) {
Serial.println("[MAIN] Ignoring button - needs calibration");
buzzerShort(2);
return;
}
if (!isKettlePresent()) {
Serial.println("[MAIN] Ignoring button - no kettle");
buzzerShort(2);
return;
}
if (currentState != ST_IDLE) {
Serial.println("[MAIN] Ignoring double click - not idle");
return;
}
// Проверяем, что трубка отведена
if (!servoController.isRetracted()) {
Serial.println("[MAIN] Cannot start - tube not retracted");
buzzerShort(2);
return;
}
float add = FULL_WEIGHT - currentWeight;
// Проверяем, можно ли налить (добавка должна быть больше гистерезиса)
if (add > WEIGHT_HYST && currentWeight + add <= FULL_WEIGHT + WEIGHT_HYST) {
// Начинаем процесс налива до полного
Serial.println("[MAIN] Starting full filling sequence...");
// 1. Перемещаем трубку над чайником
servoController.moveToKettle();
// 2. Устанавливаем целевой вес (полный чайник)
targetWeight = FULL_WEIGHT;
fillStartVolume = getWaterVolume();
// 3. Устанавливаем состояние "подготовка к наливу"
currentState = ST_FILLING;
// 4. Начинаем отсчет времени
fillStartTime = millis();
lastWeightChangeTime = millis();
// 5. Звуковой сигнал начала процесса
buzzerShort(1);
Serial.print("[MAIN] Waiting for tube to move... ");
Serial.print("Current: ");
Serial.print(currentWeight, 0);
Serial.print("g, Target: ");
Serial.print(targetWeight, 0);
Serial.println("g (full)");
// 6. Рассчитываем сколько кружек будет налито
float waterToAdd = targetWeight - currentWeight;
int cupsToAdd = round(waterToAdd / CUP_VOLUME);
Serial.print("[MAIN] Will add: ");
Serial.print(waterToAdd, 0);
Serial.print("ml (");
Serial.print(cupsToAdd);
Serial.println(" cups)");
} else {
// Нельзя налить до полного - уже полный или ошибка веса
if (add <= WEIGHT_HYST) {
Serial.println("[MAIN] Teapot is already full or almost full");
buzzerShort(2);
// Моргаем дисплеем для индикации
for (int i = 0; i < 3; i++) {
display.clearBuffer();
display.sendBuffer();
delay(200);
displayManager.update(currentState, lastError, kettlePresent,
currentWeight, targetWeight, fillStartVolume,
relays.getPowerState(), EMPTY_WEIGHT, MIN_WEIGHT, FULL_WEIGHT);
delay(200);
}
} else {
Serial.println("[MAIN] Cannot fill to max - weight error");
buzzerShort(2);
}
}
}
void handleMultipleCups(int cups) {
if (currentState == ST_ERROR) {
Serial.println("[MAIN] Ignoring command - system error");
return;
}
if (needsCalibration) {
Serial.println("[MAIN] Ignoring command - needs calibration");
buzzerShort(2);
return;
}
if (!isKettlePresent()) {
Serial.println("[MAIN] Ignoring command - no kettle");
buzzerShort(2);
return;
}
if (currentState != ST_IDLE) {
Serial.println("[MAIN] Ignoring command - not idle");
return;
}
// Проверяем, что трубка отведена
if (!servoController.isRetracted()) {
Serial.println("[MAIN] Cannot start - tube not retracted");
buzzerShort(2);
return;
}
if (cups < 1 || cups > 6) {
Serial.println("[MAIN] Invalid cup count (1-6 only)");
buzzerShort(2);
return;
}
float add = cups * CUP_VOLUME;
float maxPossibleAdd = FULL_WEIGHT - currentWeight;
// Интеллектуальная корректировка
bool adjusted = false;
if (add > maxPossibleAdd + WEIGHT_HYST) {
// Рассчитываем сколько кружек реально можно налить
int maxPossibleCups = floor(maxPossibleAdd / CUP_VOLUME);
if (maxPossibleCups <= 0) {
Serial.println("[MAIN] Teapot is already full");
buzzerShort(2);
return;
}
// Корректируем до максимально возможного
cups = maxPossibleCups;
add = cups * CUP_VOLUME;
adjusted = true;
Serial.print("[MAIN] Adjusting from ");
Serial.print(cups);
Serial.print(" to ");
Serial.print(maxPossibleCups);
Serial.println(" cups");
}
if (add > WEIGHT_HYST && currentWeight + add <= FULL_WEIGHT + WEIGHT_HYST) {
// Начинаем процесс налива
Serial.println("[MAIN] Starting multi-cup filling sequence...");
// 1. Перемещаем трубку над чайником
servoController.moveToKettle();
// 2. Устанавливаем целевой вес
targetWeight = currentWeight + add;
fillStartVolume = getWaterVolume();
// 3. Устанавливаем состояние "подготовка к наливу"
currentState = ST_FILLING;
// 4. Начинаем отсчет времени
fillStartTime = millis();
lastWeightChangeTime = millis();
// 5. Звуковой сигнал начала процесса
buzzerShort(1);
Serial.print("[MAIN] Waiting for tube to move... ");
Serial.print("Will fill ");
if (adjusted) {
Serial.print("adjusted ");
}
Serial.print(cups);
Serial.print(" cup");
if (cups != 1) Serial.print("s");
Serial.print(" (");
Serial.print(add, 0);
Serial.print("ml) to ");
Serial.print(targetWeight, 0);
Serial.println("g");
} else {
Serial.println("[MAIN] Cannot fill - weight constraints");
buzzerShort(2);
}
}
void handleTripleClick() {
// Проверяем, что трубка отведена перед началом калибровки
if (!servoController.isRetracted()) {
Serial.println("[MAIN] Cannot calibrate - tube not retracted");
buzzerShort(2);
return;
}
if (!needsCalibration && currentState == ST_IDLE) {
Serial.println("[MAIN] Starting calibration via triple click");
startCalibration();
} else {
Serial.println("[MAIN] Triple click ignored - wrong state");
}
}
void handleLongPress() {
if (currentState == ST_FILLING) {
// Немедленная остановка помпы
relays.setPump(false);
// Возвращаем трубку в исходное положение
if (!servoController.isRetracted()) {
servoController.retractTube();
}
currentState = ST_IDLE;
buzzerShort(3);
Serial.println("[MAIN] Manual stop (long press) - tube returning");
} else {
Serial.println("[MAIN] Long press ignored - not filling");
}
}
void handleVeryLongPress() {
Serial.println("[MAIN] Calibration and Wi-Fi RESET triggered");
buzzerLong(1);
delay(500);
buzzerLong(1);
delay(500);
buzzerLong(1);
resetCalibrationData();
wifiConfig.resetSettings();
Serial.println("[MAIN] Restarting system for new calibration...");
delay(1000);
ESP.restart();
}
void handleFilling() {
if (millis() - fillStartTime > FILL_TIMEOUT_MS) {
setError(ERR_FILL_TIMEOUT);
return;
}
float currentWater = getWaterVolume();
static float lastWater = 0;
if (abs(currentWater - lastWater) > 5) {
lastWater = currentWater;
lastWeightChangeTime = millis();
}
if (millis() - lastWeightChangeTime > NO_FLOW_THRESHOLD_MS &&
currentWeight < targetWeight - 50) {
setError(ERR_NO_FLOW);
return;
}
if (currentWeight >= targetWeight - WEIGHT_HYST) {
relays.setPump(false);
currentState = ST_IDLE;
buzzerShort(2);
Serial.println("Filling completed");
}
}
/* ================= SETUP ================= */
void setup() {
Serial.begin(115200);
Serial.println("Smart Kettle Pump - Starting...");
relays.begin();
pinMode(BUZZER_PIN, OUTPUT);
displayManager.begin();
servoController.begin(PIN_SERVO);
loadCalibrationData();
scale.begin(PIN_HX711_DT, PIN_HX711_SCK);
scale.set_scale(0.42);
unsigned long startTime = millis();
while (!scale.is_ready() && (millis() - startTime < 3000)) {
delay(10);
}
if (scale.is_ready()) {
currentWeight = scale.get_units(5);
if (needsCalibration) {
currentState = ST_INIT;
displayManager.update(currentState, lastError, false,
currentWeight, 0, 0, false,
EMPTY_WEIGHT, MIN_WEIGHT, FULL_WEIGHT);
delay(1000);
startCalibration();
} else {
currentState = ST_IDLE;
kettlePresent = isKettlePresent();
bool shouldPowerBeOn = isMinWaterReached();
relays.setPower(shouldPowerBeOn);
Serial.println("System initialized successfully");
Serial.print("Calibration data: empty=");
Serial.print(EMPTY_WEIGHT);
Serial.print(", min=");
Serial.print(MIN_WEIGHT);
Serial.print(", full=");
Serial.println(FULL_WEIGHT);
Serial.print("Current weight: ");
Serial.print(currentWeight, 1);
Serial.println(" g");
Serial.println("[SERVO] System ready - tube is retracted");
buzzerShort(1);
wifiConfig.begin();
setupAPI();
Serial.print("Wi-Fi Status: ");
if (wifiConfig.isConnected()) {
Serial.print("Connected to ");
Serial.print(WiFi.SSID());
Serial.print(" IP: ");
Serial.println(WiFi.localIP());
Serial.print("API доступен по: http://");
Serial.print(WiFi.localIP());
Serial.println("/api/status");
} else {
Serial.println("Access Point Mode");
Serial.print("AP IP: ");
Serial.println(wifiConfig.getIP());
}
}
} else {
setError(ERR_HX711_TIMEOUT);
}
}
/* ================= LOOP ================= */
void loop() {
updateWeight();
if (calibrationInProgress) {
handleCalibration();
displayManager.update(currentState, lastError, false,
currentWeight, 0, 0, false,
EMPTY_WEIGHT, MIN_WEIGHT, FULL_WEIGHT);
delay(50);
return;
}
if (showCalibrationSuccess) {
if (millis() - calibrationSuccessTime < 5000) {
displayManager.update(ST_INIT, lastError, false,
currentWeight, 0, 0, false,
EMPTY_WEIGHT, MIN_WEIGHT, FULL_WEIGHT);
} else {
showCalibrationSuccess = false;
displayManager.setCalibrationSuccessMode(false);
currentState = ST_IDLE;
kettlePresent = isKettlePresent();
bool shouldPowerBeOn = isMinWaterReached();
relays.setPower(shouldPowerBeOn);
Serial.println("Calibration complete, entering normal mode");
}
delay(50);
return;
}
kettlePresent = isKettlePresent();
updatePowerRelayControl();
ButtonAction action = button.update();
if (action != BTN_NONE) {
handleButtonAction(action);
}
server.handleClient();
if (currentState == ST_ERROR) {
static unsigned long lastErrorBeep = 0;
if (millis() - lastErrorBeep > 5000) {
buzzerShort(1);
lastErrorBeep = millis();
}
displayManager.update(currentState, lastError, kettlePresent,
currentWeight, targetWeight, fillStartVolume,
relays.getPowerState(), EMPTY_WEIGHT, MIN_WEIGHT, FULL_WEIGHT);
delay(50);
return;
}
servoController.update();
if (currentState == ST_FILLING) {
handleFilling();
}
if (currentState == ST_INIT) {
displayManager.update(currentState, lastError, kettlePresent,
currentWeight, targetWeight, fillStartVolume,
relays.getPowerState(), EMPTY_WEIGHT, MIN_WEIGHT, FULL_WEIGHT);
delay(100);
return;
}
displayManager.update(currentState, lastError, kettlePresent,
currentWeight, targetWeight, fillStartVolume,
relays.getPowerState(), EMPTY_WEIGHT, MIN_WEIGHT, FULL_WEIGHT);
delay(50);
}