#include #include #include #include #include #include #include #include #include #include "config.h" Adafruit_ST7735 tft(&SPI, TFT_CS, TFT_DC, TFT_RST); Servo valveServo; Preferences preferences; OneWire externalWire(EXTERNAL_BUS); OneWire internalWire(INTERNAL_BUS); DallasTemperature internalSensor(&internalWire); DallasTemperature externalSensor(&externalWire); ezButton buttonUp(BUTTON_UP); ezButton buttonDown(BUTTON_DOWN); ezButton buttonSelect(BUTTON_SELECT); ezButton buttonExit(BUTTON_EXIT); enum class MenuItem : uint8_t { MinInternal, MaxExternal, Hysteresis, Count }; struct Settings { float minInternal = DEFAULT_MIN_INTERNAL_C; float maxExternal = DEFAULT_MAX_EXTERNAL_C; float hysteresis = DEFAULT_HYSTERESIS_C; }; Settings settings; Settings settingsBeforeEdit; float tempInternal = DEVICE_DISCONNECTED_C; float tempExternal = DEVICE_DISCONNECTED_C; bool internalSensorOk = false; bool externalSensorOk = false; bool sensorConversionInProgress = false; bool editing = false; bool displayDirty = true; bool settingsStorageOk = true; MenuItem selectedItem = MenuItem::MinInternal; uint8_t valvePercent = 0; unsigned long conversionStartedAt = 0; unsigned long lastSensorRequestAt = 0; uint16_t sensorConversionMs = 750; constexpr float MIN_SETTING_C = -40.0F; constexpr float MAX_SETTING_C = 120.0F; constexpr float MAX_HYSTERESIS_C = 20.0F; constexpr float SETTING_STEP_C = 0.5F; bool isValidTemperature(float temperature) { return temperature != DEVICE_DISCONNECTED_C && isfinite(temperature) && temperature >= -55.0F && temperature <= 125.0F; } float sanitizeSetting(float value, float fallback, float minimum, float maximum) { if (!isfinite(value)) { return fallback; } return constrain(value, minimum, maximum); } void loadSettings() { // Tryb read-write tworzy przestrzen nazw przy pierwszym uruchomieniu. if (!preferences.begin("thermostat", false)) { settingsStorageOk = false; return; } settings.minInternal = preferences.getFloat("minInternal", DEFAULT_MIN_INTERNAL_C); settings.maxExternal = preferences.getFloat("maxExternal", DEFAULT_MAX_EXTERNAL_C); settings.hysteresis = preferences.getFloat("hysteresis", DEFAULT_HYSTERESIS_C); preferences.end(); settings.minInternal = sanitizeSetting(settings.minInternal, DEFAULT_MIN_INTERNAL_C, MIN_SETTING_C, MAX_SETTING_C); settings.maxExternal = sanitizeSetting(settings.maxExternal, DEFAULT_MAX_EXTERNAL_C, MIN_SETTING_C, MAX_SETTING_C); settings.hysteresis = sanitizeSetting( settings.hysteresis, DEFAULT_HYSTERESIS_C, 0.0F, MAX_HYSTERESIS_C); } bool saveSettings() { if (!preferences.begin("thermostat", false)) { return false; } bool saved = true; saved &= preferences.putFloat("minInternal", settings.minInternal) == sizeof(settings.minInternal); saved &= preferences.putFloat("maxExternal", settings.maxExternal) == sizeof(settings.maxExternal); saved &= preferences.putFloat("hysteresis", settings.hysteresis) == sizeof(settings.hysteresis); preferences.end(); return saved; } void setValvePercent(uint8_t targetPercent) { targetPercent = constrain(targetPercent, 0, 100); if (targetPercent == valvePercent) { return; } valvePercent = targetPercent; const int pulseWidth = map(valvePercent, 0, 100, SERVO_MIN_US, SERVO_MAX_US); valveServo.writeMicroseconds(pulseWidth); displayDirty = true; Serial.print("Valve position: "); Serial.print(valvePercent); Serial.println("%"); } void updateValveControl() { if (!internalSensorOk || !externalSensorOk) { setValvePercent(0); return; } if (tempInternal < settings.minInternal) { setValvePercent(0); return; } // Po zamknieciu zawor otwiera sie dopiero po wyjsciu ponad histereze. if (valvePercent == 0 && tempInternal < settings.minInternal + settings.hysteresis) { return; } if (tempExternal > settings.maxExternal) { setValvePercent(100); return; } // Z pozycji 100% wraca dopiero po spadku ponizej progu z histereza. if (valvePercent == 100 && tempExternal > settings.maxExternal - settings.hysteresis) { return; } setValvePercent(70); } void requestTemperatures() { internalSensor.requestTemperatures(); externalSensor.requestTemperatures(); conversionStartedAt = millis(); lastSensorRequestAt = conversionStartedAt; sensorConversionInProgress = true; } void serviceSensors() { const unsigned long now = millis(); if (sensorConversionInProgress && now - conversionStartedAt >= sensorConversionMs) { tempInternal = internalSensor.getTempCByIndex(0); tempExternal = externalSensor.getTempCByIndex(0); internalSensorOk = isValidTemperature(tempInternal); externalSensorOk = isValidTemperature(tempExternal); sensorConversionInProgress = false; displayDirty = true; Serial.print("Internal: "); internalSensorOk ? Serial.print(tempInternal, 1) : Serial.print("ERROR"); Serial.print(" C, external: "); externalSensorOk ? Serial.print(tempExternal, 1) : Serial.print("ERROR"); Serial.println(" C"); updateValveControl(); } if (!sensorConversionInProgress && now - lastSensorRequestAt >= SENSOR_INTERVAL_MS) { requestTemperatures(); } } void setupSensors() { internalSensor.begin(); externalSensor.begin(); internalSensor.setResolution(12); externalSensor.setResolution(12); internalSensor.setWaitForConversion(false); externalSensor.setWaitForConversion(false); sensorConversionMs = internalSensor.millisToWaitForConversion(); const uint16_t externalConversionMs = externalSensor.millisToWaitForConversion(); if (externalConversionMs > sensorConversionMs) { sensorConversionMs = externalConversionMs; } sensorConversionMs += 25; Serial.print("Internal sensors found: "); Serial.println(internalSensor.getDeviceCount()); Serial.print("External sensors found: "); Serial.println(externalSensor.getDeviceCount()); requestTemperatures(); } void setupDisplay() { pinMode(TFT_BL, OUTPUT); digitalWrite(TFT_BL, TFT_BL_ON); SPI.begin(TFT_SCLK, -1, TFT_MOSI, TFT_CS); tft.initR(INITR_BLACKTAB); tft.setRotation(TFT_ROTATION); tft.fillScreen(ST77XX_BLACK); tft.setTextWrap(false); } void setupButtons() { buttonUp.setDebounceTime(BUTTON_DEBOUNCE_MS); buttonDown.setDebounceTime(BUTTON_DEBOUNCE_MS); buttonSelect.setDebounceTime(BUTTON_DEBOUNCE_MS); buttonExit.setDebounceTime(BUTTON_DEBOUNCE_MS); } void setupServo() { valveServo.setPeriodHertz(50); valveServo.attach(SERVO_PIN, SERVO_MIN_US, SERVO_MAX_US); valveServo.writeMicroseconds(SERVO_MIN_US); valvePercent = 0; } void printTemperature(float temperature, bool valid) { if (valid) { tft.print(temperature, 1); tft.print(" C"); } else { tft.print("---.- C"); } } void drawSettingRow(int16_t y, MenuItem item, const char* label, float value) { const bool selected = selectedItem == item; const uint16_t background = selected ? (editing ? ST77XX_RED : ST77XX_BLUE) : ST77XX_BLACK; const uint16_t foreground = selected ? ST77XX_WHITE : ST77XX_CYAN; tft.fillRect(0, y, tft.width(), 18, background); tft.setTextColor(foreground, background); tft.setCursor(2, y + 5); tft.print(label); tft.setCursor(72, y + 5); tft.print(value, 1); tft.print("C"); } void drawDisplay() { tft.fillScreen(ST77XX_BLACK); tft.setTextSize(1); tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK); tft.setCursor(2, 3); tft.print("WEW:"); tft.setTextSize(2); tft.setCursor(35, 1); printTemperature(tempInternal, internalSensorOk); tft.setTextSize(1); tft.setCursor(2, 24); tft.print("ZEW:"); tft.setTextSize(2); tft.setCursor(35, 22); printTemperature(tempExternal, externalSensorOk); tft.setTextSize(1); drawSettingRow(47, MenuItem::MinInternal, "Min wew", settings.minInternal); drawSettingRow(67, MenuItem::MaxExternal, "Max zew", settings.maxExternal); drawSettingRow(87, MenuItem::Hysteresis, "Hister.", settings.hysteresis); tft.setTextColor(ST77XX_GREEN, ST77XX_BLACK); tft.setTextSize(2); tft.setCursor(2, 110); tft.print("ZAWOR:"); tft.setCursor(80, 110); tft.print(valvePercent); tft.print("%"); tft.setTextSize(1); tft.setTextColor(settingsStorageOk ? ST77XX_WHITE : ST77XX_RED, ST77XX_BLACK); tft.setCursor(2, 137); if (!settingsStorageOk) { tft.print("BLAD ZAPISU"); } else { tft.print(editing ? "UP/DN zmien SEL zap" : "UP/DN wybor SEL edit"); } tft.setCursor(2, 150); tft.print(editing ? "EXIT anuluj" : "Blad czujnika = 0%"); displayDirty = false; } float& selectedSetting() { switch (selectedItem) { case MenuItem::MinInternal: return settings.minInternal; case MenuItem::MaxExternal: return settings.maxExternal; case MenuItem::Hysteresis: return settings.hysteresis; default: return settings.minInternal; } } void changeSelectedSetting(float delta) { float& value = selectedSetting(); const float maximum = selectedItem == MenuItem::Hysteresis ? MAX_HYSTERESIS_C : MAX_SETTING_C; const float minimum = selectedItem == MenuItem::Hysteresis ? 0.0F : MIN_SETTING_C; value = sanitizeSetting(value + delta, value, minimum, maximum); displayDirty = true; } void moveSelection(int8_t direction) { int8_t selected = static_cast(selectedItem); const int8_t count = static_cast(MenuItem::Count); selected = (selected + direction + count) % count; selectedItem = static_cast(selected); displayDirty = true; } void serviceButtons() { buttonUp.loop(); buttonDown.loop(); buttonSelect.loop(); buttonExit.loop(); if (buttonUp.isPressed()) { editing ? changeSelectedSetting(SETTING_STEP_C) : moveSelection(-1); } if (buttonDown.isPressed()) { editing ? changeSelectedSetting(-SETTING_STEP_C) : moveSelection(1); } if (buttonSelect.isPressed()) { if (editing) { settingsStorageOk = saveSettings(); if (settingsStorageOk) { editing = false; updateValveControl(); } } else { settingsBeforeEdit = settings; editing = true; } displayDirty = true; } if (buttonExit.isPressed() && editing) { settings = settingsBeforeEdit; editing = false; displayDirty = true; updateValveControl(); } } void setup() { Serial.begin(115200); delay(100); loadSettings(); setupDisplay(); setupButtons(); setupServo(); setupSensors(); drawDisplay(); } void loop() { serviceButtons(); serviceSensors(); if (displayDirty) { drawDisplay(); } }