#define useEEPROM 1 #include #include #include #include #include "GyverEncoder.h" #if useEEPROM #include #endif // 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; 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 = 2; // 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[] = { {"Chickpeas", 3, {{47, 120}, {53, 120}, {65, 180}, {72, 90}, {90, 90}}, 5}, {"Lentils", 3, {{47, 120}, {53, 120}, {65, 180}, {72, 90}, {90, 15}}, 5}, {"Lentils: Gradual", 10, {{48, 120}, {80, 60}}, 2}, {"Wheat", 3, {{47, 60}, {53, 60}, {65, 20}, {72, 20}, {85, 20}}, 5}, {"Veggies Sous Vide", 2, {{55, 30}, {85, 120}}, 2}, {"Test", 1, {{49, 1}, {51, 1}}, 2}, }; // 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 = 1, Ki = 2, Kd = 0.25; PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); // Timing variables long phaseStartTime; long totalStartTime; long ssrLastSwitchTime; long totalElapsedTime; long totalProcessTime; long finishTime = 0; long currentTime = 0; long lastEEPROMWriteTime = 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 readEEPROM(); myPID.SetMode(AUTOMATIC); myPID.SetOutputLimits(0, 1); } void loop() { enc1.tick(); if (inSelectionMode) { handleProfileSelection(); } else { handleExecution(); } } void handleExecution() { currentTime = millis(); totalElapsedTime = (currentTime - totalStartTime) / 1000; // Total elapsed time in seconds if ((currentTime - lastEEPROMWriteTime) >= (long)10*60*1000) { writeEEPROM(); } getPhaseAndTemperature(); Input = thermocouple.readCelsius(); if (isnan(Input)) { Input = 0; } myPID.Compute(); // Serial.print(Input); // Serial.print(" "); // Serial.print(Output); // Serial.print(" "); // Serial.println(Setpoint); // 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(); if (enc1.isClick()) { Serial.println("clicked! resetting..."); activeProfileIndex = 100; activeProfile = profiles[activeProfileIndex]; inSelectionMode = true; writeEEPROM(); oled.clear(); displaySelection(); return; } 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]; inSelectionMode = false; // Switch to execution mode phaseStartTime = totalStartTime = millis(); // Start the timer totalElapsedTime = 0; myPID.SetMode(AUTOMATIC); myPID.SetOutputLimits(0, 1); // SSR is either ON or OFF // digitalWrite(ssrPin, HIGH); // Start with heater on ssrLastSwitchTime = millis(); calculateTotalTime(); getPhaseAndTemperature(); writeEEPROM(); oled.clear(); } } 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 activeProfile = profiles[activeProfileIndex]; 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() { 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 (totalElapsedTime < accumulatedTime + transitionDuration) { isInTransition = true; currentPhase = i - 1; // Keep currentPhase as the previous phase int timeInTransition = totalElapsedTime - accumulatedTime; Setpoint = previousTemp + (double)timeInTransition / (60 * activeProfile.transitionMinutesPerDegree) * (targetTemp > previousTemp ? 1 : -1); phaseStartTime = totalStartTime + accumulatedTime * 1000; // Set phase start time return; } accumulatedTime += transitionDuration; } // Check if we're within the current phase if (totalElapsedTime < accumulatedTime + phaseDuration) { isInTransition = false; currentPhase = i; Setpoint = activeProfile.phases[i].temperature; phaseStartTime = totalStartTime + accumulatedTime * 1000; // Set phase start time 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 phaseStartTime = totalStartTime + accumulatedTime * 1000; // Set phase start time to the end } 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(Input,0); oled.print("c"); oled.print("->"); oled.print((int)Setpoint); oled.print("c "); } else { oled.setCursor(0, 0); oled.print(activeProfile.name); oled.print(" "); } // Display the totals and current state on the OLED oled.setCursor(0, 1); oled.print(" "); oled.setCursor(18, 1); formatTime(totalElapsedTime, timeBuffer); oled.print(timeBuffer); oled.print(" / "); formatTime(totalProcessTime, timeBuffer); oled.print(timeBuffer); oled.print(" "); for (int i = 0; i < activeProfile.numPhases; i++) { if (i == currentPhase) { oled.invertText(true); // Invert text for the current phase oled.setCursor(0, i + 2); // Set cursor to the row corresponding to the phase 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(Setpoint, 0); // Print without decimals } else { oled.print(Setpoint, 1); // Print with 1 decimal place } oled.print("c "); if (!isInTransition) { oled.print(timeBuffer); } oled.print(" "); } 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.print(" "); } } oled.invertText(false); // Ensure text inversion is off after the loop oled.setCursor(110,7); sprintf(timeBuffer, "%3d", (int)(Output*100)); oled.print(timeBuffer); oled.update(); } 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); } } void writeEEPROM() { #if useEEPROM lastEEPROMWriteTime = millis(); EEPROM.put(0, activeProfileIndex); // Store the active profile index EEPROM.put(4, totalElapsedTime); // Store the total elapsed time Serial.print("EEPROM written: "); Serial.print(activeProfileIndex); Serial.print(" "); Serial.println(totalElapsedTime); #endif } void readEEPROM() { #if useEEPROM long time = 0; EEPROM.get(0, activeProfileIndex); // Store the active profile index EEPROM.get(4, time); // Store the total elapsed time if (activeProfileIndex != 100) { totalStartTime = millis() - time*1000; calculateTotalTime(); getPhaseAndTemperature(); inSelectionMode = false; } else { oled.clear(); displaySelection(); } Serial.print("EEPROM read: "); Serial.print(activeProfileIndex); Serial.print(" "); Serial.println(time); #endif }