#include #include #include #include #include "GyverEncoder.h" // Include the GyverEncoder library // MAX6675 configuration int max_SO = 12; int max_CS = 10; int max_SCK = 13; MAX6675 thermocouple(max_SCK, max_CS, max_SO); // OLED configuration GyverOLED oled; // Encoder configuration #define CLK 5 #define DT 6 #define SW 7 Encoder enc1(CLK, DT, SW, TYPE2); // SSR pin configuration const int ssrPin = 7; // Profile structure definition struct Phase { int temperature; int duration; // in minutes }; struct Profile { const char* name; int transitionMinutesPerDegree; Phase phases[6]; // Maximum of 6 phases per profile int numPhases; }; // Profiles definition Profile profiles[] = { {"Test", 0, {{49, 1}, {51, 1}}, 2}, {"Пшеница", 1, {{47, 40}, {55, 40}, {65, 20}, {72, 20}, {85, 20}}, 5}, {"Veggies Sous Vide", 0, {{85, 120}}, 1}, {"Фитаза/Протеаза", 2, {{47, 120}, {53, 120}, {65, 150}, {72, 60}, {90, 105}, {50, 60}}, 6}, }; // Global variables for profile selection and execution int activeProfileIndex = 100; // 100 indicates no profile selected yet int selectedProfileIndex = 0; // Index of the currently selected profile in the selection mode Profile activeProfile; // The active profile once selected // Global variables for current phase, setpoint, and transition state int currentPhase; bool isInTransition = false; bool inSelectionMode = true; // PID Control variables double Setpoint, Input, Output; double Kp = 2, Ki = 0.5, Kd = 0.25; PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); // Timing variables unsigned long phaseStartTime; unsigned long totalStartTime; unsigned long ssrLastSwitchTime; unsigned long totalElapsedTime; unsigned long totalProcessTime; unsigned long finishTime = 0; unsigned long currentTime = 0; bool isComplete = false; const int ssrSwitchInterval = 1000; // SSR switching interval in milliseconds // Buffer for formatted time strings char timeBuffer[10]; void setup() { pinMode(ssrPin, OUTPUT); Serial.begin(9600); oled.init(); // Initialize the OLED oled.clear(); // Clear the display displaySelection(); } void loop() { enc1.tick(); // Mandatory encoder update function if (inSelectionMode) { handleProfileSelection(); // Handle profile selection mode } else { currentTime = millis(); totalElapsedTime = (currentTime - totalStartTime) / 1000; // Total elapsed time in seconds getPhaseAndTemperature(totalElapsedTime); Input = thermocouple.readCelsius(); if (isnan(Input)) { Input = 0; } myPID.Compute(); // Switch SSR based on PID output and interval control if (currentTime - ssrLastSwitchTime > ssrSwitchInterval) { digitalWrite(ssrPin, Output > 0.5 ? HIGH : LOW); ssrLastSwitchTime = currentTime; } if (isComplete && currentPhase >= activeProfile.numPhases && !finishTime) { finishTime = currentTime; } // Display all phases and highlight the current one printPhases(); oled.update(); delay(1000); // Update every second } } void handleProfileSelection() { bool click = enc1.isClick(); bool turn = enc1.isTurn(); if (!click && !turn) { return; } // Handle encoder input for selecting the profile if (enc1.isRight()) { selectedProfileIndex = (selectedProfileIndex + 1) % (sizeof(profiles) / sizeof(profiles[0])); } else if (enc1.isLeft()) { selectedProfileIndex = (selectedProfileIndex - 1 + (sizeof(profiles) / sizeof(profiles[0]))) % (sizeof(profiles) / sizeof(profiles[0])); } displaySelection(); // Start the selected profile on button press if (click) { activeProfileIndex = selectedProfileIndex; activeProfile = profiles[activeProfileIndex]; calculateTotalTime(); inSelectionMode = false; // Switch to execution mode phaseStartTime = totalStartTime = millis(); // Start the timer myPID.SetMode(AUTOMATIC); myPID.SetOutputLimits(0, 1); // SSR is either ON or OFF digitalWrite(ssrPin, HIGH); // Start with heater on ssrLastSwitchTime = millis(); } } void displaySelection() { oled.clear(); // Display all profiles with the selected one highlighted for (int i = 0; i < (int) (sizeof(profiles) / sizeof(profiles[0])); i++) { if (i == selectedProfileIndex) { oled.invertText(true); // Highlight the selected profile } oled.setCursor(0, i); // Set cursor to the correct row oled.print(profiles[i].name); oled.invertText(false); // Reset text inversion for other profiles } oled.update(); } void calculateTotalTime() { // Calculate total process time, including transitions totalProcessTime = 0; for (int i = 0; i < activeProfile.numPhases; i++) { totalProcessTime += activeProfile.phases[i].duration * 60; if (i < activeProfile.numPhases - 1) { int tempDiff = abs(activeProfile.phases[i + 1].temperature - activeProfile.phases[i].temperature); totalProcessTime += tempDiff * 60 * activeProfile.transitionMinutesPerDegree; } } } void getPhaseAndTemperature(unsigned long elapsedSeconds) { unsigned long accumulatedTime = 0; for (int i = 0; i < activeProfile.numPhases; i++) { int phaseDuration = activeProfile.phases[i].duration * 60; // Check for transition if (i > 0) { int previousTemp = activeProfile.phases[i - 1].temperature; int targetTemp = activeProfile.phases[i].temperature; int tempDiff = abs(targetTemp - previousTemp); int transitionDuration = tempDiff * 60 * activeProfile.transitionMinutesPerDegree; if (elapsedSeconds < accumulatedTime + transitionDuration) { isInTransition = true; currentPhase = i - 1; // Keep currentPhase as the previous phase int timeInTransition = elapsedSeconds - accumulatedTime; Setpoint = previousTemp + (double)timeInTransition / (60 * activeProfile.transitionMinutesPerDegree) * (targetTemp > previousTemp ? 1 : -1); return; } accumulatedTime += transitionDuration; } // Check if we're within the current phase if (elapsedSeconds < accumulatedTime + phaseDuration) { isInTransition = false; currentPhase = i; Setpoint = activeProfile.phases[i].temperature; return; } accumulatedTime += phaseDuration; } // If the elapsed time exceeds the total duration, indicate completion currentPhase = activeProfile.numPhases; // Indicate completion Setpoint = 45; // Default to 45°C after completion isInTransition = false; isComplete = true; // Mark the process as complete } void printPhases() { oled.clear(); if (isComplete) { // Show completion time and current temperature instead of the title oled.setCursor(0, 0); oled.invertText(true); oled.print("Done!"); oled.invertText(false); oled.print(" "); formatTime((currentTime - finishTime) / 1000, timeBuffer); // Time since completion oled.print(timeBuffer); oled.print(" "); oled.print((int)Input); oled.print("c"); } else { oled.setCursor(0, 0); oled.print(activeProfile.name); } // Display the totals and current state on the OLED oled.setCursor(18, 1); formatTime(totalElapsedTime, timeBuffer); oled.print(timeBuffer); oled.print(" / "); formatTime(totalProcessTime, timeBuffer); oled.print(timeBuffer); for (int i = 0; i < activeProfile.numPhases; i++) { if (i == currentPhase && !isComplete) { oled.invertText(true); // Invert text for the current phase oled.setCursor(0, i + 2); // Set cursor to the row corresponding to the phase unsigned long timeRemaining = (activeProfile.phases[i].duration * 60) - ((currentTime - phaseStartTime) / 1000); formatTime(timeRemaining, timeBuffer); oled.print(i + 1); oled.print(". "); oled.print(Input, 1); oled.print("c "); if (fabs(Setpoint - round(Setpoint)) < 0.05) { oled.print((int)Setpoint); // Print without decimals } else { oled.print(Setpoint, 1); // Print with 1 decimal place } oled.print("c "); oled.print(timeBuffer); } else { oled.invertText(false); // Normal text for other phases oled.setCursor(0, i + 2); // Set cursor to the row corresponding to the phase formatTime(activeProfile.phases[i].duration * 60, timeBuffer); oled.print(i + 1); oled.print(". "); oled.print(activeProfile.phases[i].temperature); oled.print("c "); oled.print(timeBuffer); } } oled.invertText(false); // Ensure text inversion is off after the loop } void formatTime(long seconds, char* buffer) { long hours = seconds / 3600; long mins = (seconds % 3600) / 60; int secs = seconds % 60; buffer[0] = '\0'; // Ensure the buffer is empty if (hours > 0) { sprintf(buffer + strlen(buffer), "%ldh", hours); } if (mins > 0) { sprintf(buffer + strlen(buffer), "%ldm", mins); } if (secs > 0) { sprintf(buffer + strlen(buffer), "%ds", secs); } }