#include #include #include #include #include #include #include #include "config.h" #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_ADDR 0x3C #define ENCODER_SW D5 #define ENCODER_CLK D6 #define ENCODER_DT D7 #define RELAY_PIN D8 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, "pool.ntp.org", 3600, 60000); float curTemp = 0; float targetTemp = 60; bool editMode = false; bool heatingEnabled = false; bool relayOn = false; bool lastSw = HIGH; unsigned long lastDebounce = 0; bool wifiConnected = false; String currentTime = "--:--"; String weatherTemp = "--°C"; // Add encoder variables for interrupt handling volatile bool encoderDirty = false; // Flag for UI updates volatile int encoderValue = 60; // Encoder counter for temperature void IRAM_ATTR encoderISR() { static bool lastDT = HIGH; bool dt = digitalRead(ENCODER_DT); if (dt != lastDT) { // State changed bool clk = digitalRead(ENCODER_CLK); // Gray code decoding if (editMode) { encoderValue += (dt == clk) ? +1 : -1; encoderValue = constrain(encoderValue, 30*2, 95*2); // *2 for 0.5°C precision targetTemp = encoderValue / 2.0; // Convert back to temperature (0.5°C steps) encoderDirty = true; } lastDT = dt; } } void setup() { Serial.begin(115200); Wire.begin(D2, D1); // Setup encoder pins and interrupt pinMode(ENCODER_CLK, INPUT_PULLUP); pinMode(ENCODER_DT, INPUT_PULLUP); pinMode(ENCODER_SW, INPUT_PULLUP); pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, LOW); // Attach interrupt to encoder pin attachInterrupt(digitalPinToInterrupt(ENCODER_DT), encoderISR, CHANGE); // Convert target temp to encoder value encoderValue = targetTemp * 2; if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { Serial.println("Display error"); while (1); } // Add display initialization confirmation display.clearDisplay(); display.setTextSize(1); display.setTextColor(WHITE); display.setCursor(0,0); display.println("Starting..."); display.display(); delay(1000); WiFi.begin(WIFI_SSID, WIFI_PASS); // Give WiFi a single quick check without waiting delay(1000); if (WiFi.status() == WL_CONNECTED) { Serial.println("WiFi connected"); wifiConnected = true; // Only try to fetch time and weather if WiFi is connected timeClient.begin(); timeClient.update(); currentTime = timeClient.getFormattedTime(); HTTPClient http; WiFiClient wifiClient; http.begin(wifiClient, "http://api.weatherapi.com/v1/current.json?key=" API_KEY "&q=London&aqi=no"); int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); Serial.println("Weather API response: " + payload); // Just set a placeholder instead of using raw JSON weatherTemp = "20°C"; // Static placeholder until parsing is added } http.end(); } else { Serial.println("Connect failed"); } // Add another debug message on display display.clearDisplay(); display.setCursor(0,0); display.println("Init done"); display.println("WiFi: " + String(wifiConnected ? "OK" : "Fail")); display.display(); delay(2000); } float readC() { const float R_SER=20000, R0=100000, B=3950, T0=298.15; int a = analogRead(A0); float v = a * 3.3 / 1023.0; if (v > 3.2) return NAN; float r = R_SER / (3.3 / v - 1); float invT = 1/T0 + log(r/R0)/B; return 1/invT - 273.15; } void drawUI() { display.clearDisplay(); display.setTextColor(WHITE); // Ensure text color is set display.setTextSize(1); display.setCursor(0, 0); display.print(currentTime); display.setCursor(70, 0); display.print(weatherTemp); display.setTextSize(2); display.setCursor(0, 16); display.printf("%.1f", curTemp); display.write(0xF8); display.print("C"); // Show target temperature, blinking if in edit mode display.setTextSize(1); display.setCursor(0, 40); if (editMode && (millis() / 300 % 2)) { // Blink effect when editing display.printf("Set: %.1f°C", targetTemp); } else { display.printf("Set: %.1f°C", targetTemp); } // Show heating status if (!heatingEnabled) { display.setCursor(0, 50); display.print("Click to start heating"); } else if (relayOn) { display.setCursor(100, 32); display.print("~^~"); // Heating indicator } display.display(); } void loop() { // Check encoder button bool sw = digitalRead(ENCODER_SW); if (sw == LOW && lastSw == HIGH && millis() - lastDebounce > 200) { if (editMode) { // Exiting edit mode - enable heating editMode = false; heatingEnabled = true; Serial.println("Target set, heating enabled"); } else { // Entering edit mode - disable heating during adjustment editMode = true; heatingEnabled = false; relayOn = false; digitalWrite(RELAY_PIN, LOW); Serial.println("Entering edit mode, heating disabled"); } lastDebounce = millis(); } lastSw = sw; curTemp = readC(); // TPO heating control - only active when heating is enabled static unsigned long tpoStart = 0; if (heatingEnabled) { float error = targetTemp - curTemp; float duty = constrain(error * 0.25, 0.4, 1); // Simple proportional control // 10-second TPO cycle unsigned long currentMillis = millis(); unsigned long cycleTime = (currentMillis - tpoStart) % 10000; relayOn = cycleTime < (duty * 10000); digitalWrite(RELAY_PIN, relayOn); } else { // Make sure relay is off when heating disabled relayOn = false; digitalWrite(RELAY_PIN, LOW); } // Update time occasionally if WiFi is connected static unsigned long lastTimeUpdate = 0; if (wifiConnected && millis() - lastTimeUpdate > 60000) { // Update every minute timeClient.update(); currentTime = timeClient.getFormattedTime(); lastTimeUpdate = millis(); } // Add serial debugging for temperature readings static unsigned long lastDebugPrint = 0; if (millis() - lastDebugPrint > 3000) { // Print debug info every 3 seconds Serial.print("Current temp: "); Serial.print(curTemp); Serial.print(", Target: "); Serial.print(targetTemp); Serial.print(", Relay: "); Serial.println(relayOn ? "ON" : "OFF"); lastDebugPrint = millis(); } // Only draw UI when needed static unsigned long lastDisplayUpdate = 0; if (encoderDirty || millis() - lastDisplayUpdate > 500) { drawUI(); encoderDirty = false; lastDisplayUpdate = millis(); } // No need for delay since we have interrupt-based encoder reading yield(); }