#define useEEPROM 1 // #define TempSensorMax #define TempSensorDallas // #define PlotValues #include #include #include "GyverEncoder.h" #if useEEPROM #include #endif #ifdef TempSensorMax #include // MAX6675 configuration int max_SO = 12; int max_CS = 10; int max_SCK = 13; MAX6675 thermocouple(max_SCK, max_CS, max_SO); #endif #ifdef TempSensorDallas #include #include // Data wire is plugged into port 2 on the Arduino #define ONE_WIRE_BUS 9 // Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs) OneWire oneWire(ONE_WIRE_BUS); // Pass our oneWire reference to Dallas Temperature. DallasTemperature sensors(&oneWire); #endif // 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; const int activeBuzzerPin = 8; // 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", 1, {{46, 120}, {55, 60}, {65, 150}, {70, 60}, {90, 105}}, 5}, {"Lentils", 3, {{46, 120}, {53, 120}, {65, 180}, {72, 90}, {90, 15}}, 5}, {"Gradual", 10, {{46, 120}, {80, 60}}, 2}, {"Long gradual", 10, {{46, 120}, {80, 60}, {90, 15}}, 3}, {"Wheat", 1, {{46, 60}, {53, 60}, {65, 20}, {72, 20}, {85, 20}}, 5}, {"SV1", 1, {{55, 30}, {85, 120}}, 2}, {"SV2", 0, {{46, 45}}, 1}, {"Yoghurt maker", 0, {{40, 300}, {40, 300}, {30, 300}}, 3}, }; // 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 Kp = 0.5, Ki = 1, Kd = 0.05; // PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); #include "GyverPID.h" double Setpoint, Input, Output; GyverPID regulator(25, 0, 0, 1000); bool temperatureSensorError = false; // Timing variables long phaseStartTime; long totalStartTime; long ssrLastSwitchTime; long totalElapsedTime; long totalProcessTime; long finishTime = 0; long currentTime = 0; long lastEEPROMWriteTime = 0; bool isClick = false; bool isLeft = false; bool isRight = false; bool isComplete = false; const int ssrSwitchInterval = 1000; // SSR switching interval in milliseconds // Buffer for formatted time strings char timeBuffer[10]; uint32_t timerExecution = 0; #define T_PERIOD_EXEC 1000 // period of Execution processing uint32_t timerSSR = 0; #define T_PERIOD_SSR 500 // period of heater handling uint32_t timerTemp = 0; #define T_PERIOD_TEMP 1000 // period of dallas sensor requesting uint32_t timerChecks= 0; #define T_PERIOD_Checks 1000 // period of additional checks bool boolLastCompletedState = false; float failedReadingLastValue = 0; int failedReadingCount = 0; #define PARTS 8 void setup() { pinMode(ssrPin, OUTPUT); pinMode(activeBuzzerPin, OUTPUT); Serial.begin(9600); oled.init(); // Initialize the OLED oled.clear(); // Clear the display readEEPROM(); regulator.setDirection(NORMAL); // направление регулирования (NORMAL/REVERSE). ПО УМОЛЧАНИЮ СТОИТ NORMAL regulator.setLimits(0, 100); // пределы (ставим для 8 битного ШИМ). ПО УМОЛЧАНИЮ СТОЯТ 0 И 255 regulator.setpoint = 0; #ifdef TempSensorDallas sensors.begin(); DeviceAddress tempDeviceAddress; sensors.getAddress(tempDeviceAddress, 0); sensors.setResolution(tempDeviceAddress, 12); sensors.setWaitForConversion(false); #endif } void loop() { enc1.tick(); isLeft = enc1.isLeft(); isRight = enc1.isRight(); isClick = enc1.isClick(); if (isLeft || isRight || isClick) { handleEncoder(); } handleTemperatureSensor(); handleExecution(); handleHeaterAdv(); handleAdditionalChecks(); } void handleAdditionalChecks() { long time = millis(); if (time - timerChecks < T_PERIOD_Checks) { return; } timerChecks = time; if (isComplete && !boolLastCompletedState) { boolLastCompletedState = true; for(int i=0; i<10; i++) { digitalWrite(activeBuzzerPin, HIGH); delay(100); digitalWrite(activeBuzzerPin, LOW); delay(100); } } } void handleTemperatureSensor() { long time = millis(); static int currentPart = 0; if (time - timerTemp < T_PERIOD_TEMP) { return; } timerTemp = time; #ifdef TempSensorDallas Input = (double) sensors.getTempCByIndex(0); sensors.requestTemperatures(); // Send the command to get temperatures #endif #ifdef TempSensorMax // to go into the read temp function, rename from dallas temp Input = thermocouple.readCelsius(); #endif if (isnan(Input) || Input < 20 || Input > 95) { failedReadingLastValue = (float) Input; failedReadingCount++; temperatureSensorError = true; } else { temperatureSensorError = false; } } void handleHeaterSimple() { long time = millis(); static int currentPart = 0; if (time - timerSSR < T_PERIOD_SSR) { return; } if (inSelectionMode) { digitalWrite(ssrPin, LOW); return; } timerSSR = time; int current = Output * PARTS; oled.setCursor(104, 7); char symbol; for (int i = 0; i < PARTS; i++) { symbol = current > i ? '=' : '-'; if (currentPart == i) { oled.invertText(true); } oled.print(symbol); oled.invertText(false); } digitalWrite(ssrPin, currentPart < current ? HIGH : LOW); currentPart++; if (currentPart >= PARTS) { currentPart = 0; } } void handleHeaterAdv() { long time = millis(); static int currentPart = 0; static bool states[PARTS]; if (time - timerSSR < T_PERIOD_SSR) { return; } if (inSelectionMode) { digitalWrite(ssrPin, LOW); return; } timerSSR = time; int current = Output/100 * PARTS; int last = 0; for (int i = 1; i < PARTS; i++) { last += states[i] ? 1 : 0; if (i) { states[i-1] = states[i]; } } bool next = current >= last ? true : false; if (Output < 1) next = false; states[PARTS-1] = next; oled.setCursor(128-6*PARTS, 7); sprintf(timeBuffer, "%3d", (int)(Output)); char symbol; for (int i = 0; i < PARTS; i++) { int index = i - (PARTS - 3); symbol = index < 0 ? ' ' : timeBuffer[index]; oled.invertText(states[i]); oled.print(symbol); oled.invertText(false); } if (temperatureSensorError) { next = 0; } digitalWrite(ssrPin, next); currentPart++; if (currentPart >= PARTS) { currentPart = 0; } #ifdef PlotValues Serial.print(Setpoint); Serial.print(","); Serial.print(Input); Serial.print(","); Serial.print(Output); Serial.println(",0,100"); #endif } void handleEncoder() { if (inSelectionMode) { handleProfileSelection(); } else { handleExecutionSelection(); } } void handleExecutionSelection() { if (isClick) { activeProfileIndex = 100; activeProfile = profiles[activeProfileIndex]; inSelectionMode = true; writeEEPROM(); oled.clear(); displaySelection(); return; } } void handleExecution() { currentTime = millis(); if (inSelectionMode || (currentTime - timerExecution < T_PERIOD_EXEC)) { // таймер на millis() return; } timerExecution = currentTime; totalElapsedTime = (currentTime - totalStartTime) / 1000; // Total elapsed time in seconds if ((currentTime - lastEEPROMWriteTime) >= (long)10*60*1000) { writeEEPROM(); } getPhaseAndTemperature(); regulator.input = (float)Input; regulator.setpoint = Setpoint; regulator.getResultNow(); Output = regulator.output; if (isComplete && currentPhase >= activeProfile.numPhases && !finishTime) { finishTime = currentTime; } // Display all phases and highlight the current one printPhases(); } void startExecution() { activeProfile = profiles[activeProfileIndex]; inSelectionMode = false; // Switch to execution mode phaseStartTime = totalStartTime = millis(); // Start the timer totalElapsedTime = 0; regulator.setpoint = 0; // digitalWrite(ssrPin, HIGH); // Start with heater on ssrLastSwitchTime = millis(); calculateTotalTime(); getPhaseAndTemperature(); writeEEPROM(); oled.clear(); boolLastCompletedState = isComplete = false; } void handleProfileSelection() { if (!isClick && !isLeft && !isRight) { return; } // Handle encoder input for selecting the profile if (isRight) { selectedProfileIndex = (selectedProfileIndex + 1) % (sizeof(profiles) / sizeof(profiles[0])); } else if (isLeft) { selectedProfileIndex = (selectedProfileIndex - 1 + (sizeof(profiles) / sizeof(profiles[0]))) % (sizeof(profiles) / sizeof(profiles[0])); } displaySelection(); // Start the selected profile on button press if (isClick) { activeProfileIndex = selectedProfileIndex; startExecution(); } } 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(" "); } } if (temperatureSensorError) { oled.invertText(true); oled.setCursor(0, 0); oled.setScale(2); oled.print("T = "); oled.print(Input); oled.print(" "); oled.setScale(1); digitalWrite(activeBuzzerPin, HIGH); delay(5); digitalWrite(activeBuzzerPin, LOW); } if (failedReadingCount) { oled.setCursor(0, 7); oled.print(failedReadingCount); oled.print(" ("); oled.print(failedReadingLastValue); 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 }