#include #include #include #include // Include the GyverOLED 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; // 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}, }; // Select active profile (constant at this point) const int activeProfileIndex = 0; // Index of the active profile, starting from 0 Profile activeProfile = profiles[activeProfileIndex]; // Global variables for current phase, setpoint, and transition state int currentPhase; bool isInTransition = false; // 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; 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 oled.setScale(2); // Set text scale to 2 for better visibility oled.print("Starting..."); oled.update(); oled.setScale(1); // Set text scale back to 1 for more detailed information calculateTotalTime(); // Begin the first phase phaseStartTime = totalStartTime = millis(); myPID.SetMode(AUTOMATIC); myPID.SetOutputLimits(0, 1); // SSR is either ON or OFF digitalWrite(ssrPin, HIGH); // Start with heater on ssrLastSwitchTime = millis(); } void loop() { unsigned long currentTime = millis(); totalElapsedTime = (currentTime - totalStartTime) / 1000; // Total elapsed time in seconds getPhaseAndTemperature(totalElapsedTime); Input = (int) thermocouple.readCelsius(); // Cast to integer for display and control 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 = currentTime; } // Display all phases and highlight the current one printPhases(currentPhase, currentTime - phaseStartTime, currentTime); oled.update(); delay(1000); // Update every second } 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; int timeInTransition = elapsedSeconds - accumulatedTime; Setpoint = previousTemp + ((targetTemp > previousTemp ? (double)timeInTransition / 60 : -(double)timeInTransition / 60) / (double)activeProfile.transitionMinutesPerDegree); 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(int currentPhase, unsigned long phaseElapsedTime, unsigned long currentTime) { 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) - (phaseElapsedTime / 1000); formatTime(timeRemaining, timeBuffer); oled.print(i + 1); oled.print(". "); oled.print((int)Input); 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); } }