hot-fermentation/hot_fermentation.ino

329 lines
9.6 KiB
Arduino
Raw Normal View History

2024-08-29 12:37:31 +07:00
#include <max6675.h>
#include <Wire.h>
#include <PID_v1.h>
2024-08-31 01:38:49 +07:00
#include <GyverOLED.h>
#include "GyverEncoder.h" // Include the GyverEncoder library
#include <EEPROM.h>
2024-08-29 12:37:31 +07:00
// MAX6675 configuration
int max_SO = 12;
int max_CS = 10;
int max_SCK = 13;
MAX6675 thermocouple(max_SCK, max_CS, max_SO);
2024-08-29 23:01:32 +07:00
// OLED configuration
// GyverOLED<SSD1306_128x64> oled;
GyverOLED<SSD1306_128x64, OLED_NO_BUFFER> oled;
2024-08-29 12:37:31 +07:00
2024-08-31 01:38:49 +07:00
// Encoder configuration
#define CLK 5
#define DT 6
#define SW 7
Encoder enc1(CLK, DT, SW, TYPE2);
2024-08-29 12:37:31 +07:00
// SSR pin configuration
const int ssrPin = 7;
2024-08-29 23:58:25 +07:00
// Profile structure definition
struct Phase {
int temperature;
int duration; // in minutes
};
struct Profile {
const char* name;
2024-08-30 02:45:48 +07:00
int transitionMinutesPerDegree;
2024-08-30 00:07:12 +07:00
Phase phases[6]; // Maximum of 6 phases per profile
2024-08-29 23:58:25 +07:00
int numPhases;
};
// Profiles definition
Profile profiles[] = {
2024-08-30 02:45:48 +07:00
{"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},
2024-08-29 23:58:25 +07:00
};
2024-08-31 01:38:49 +07:00
// 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
2024-08-29 12:37:31 +07:00
2024-08-30 02:45:48 +07:00
// Global variables for current phase, setpoint, and transition state
int currentPhase;
bool isInTransition = false;
2024-08-31 01:38:49 +07:00
bool inSelectionMode = true;
2024-08-30 02:45:48 +07:00
2024-08-29 12:37:31 +07:00
// 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
2024-08-31 12:47:54 +07:00
long phaseStartTime;
long totalStartTime;
long ssrLastSwitchTime;
long totalElapsedTime;
long totalProcessTime;
long finishTime = 0;
long currentTime = 0;
long lastEEPROMWriteTime = 0;
bool isComplete = false;
2024-08-29 12:37:31 +07:00
const int ssrSwitchInterval = 1000; // SSR switching interval in milliseconds
2024-08-29 23:58:25 +07:00
// Buffer for formatted time strings
char timeBuffer[10];
2024-08-29 12:37:31 +07:00
void setup() {
pinMode(ssrPin, OUTPUT);
Serial.begin(9600);
2024-08-29 23:01:32 +07:00
oled.init(); // Initialize the OLED
oled.clear(); // Clear the display
2024-08-31 01:38:49 +07:00
displaySelection();
2024-08-29 12:37:31 +07:00
}
void loop() {
2024-08-31 12:47:54 +07:00
enc1.tick();
2024-08-31 01:38:49 +07:00
if (inSelectionMode) {
2024-08-31 12:47:54 +07:00
handleProfileSelection();
2024-08-31 01:38:49 +07:00
} else {
2024-08-31 12:47:54 +07:00
handleExecution();
}
}
2024-08-31 01:38:49 +07:00
2024-08-31 12:47:54 +07:00
void handleExecution() {
currentTime = millis();
totalElapsedTime = (currentTime - totalStartTime) / 1000; // Total elapsed time in seconds
2024-08-31 01:38:49 +07:00
if ((currentTime - lastEEPROMWriteTime) >= (unsigned int) 10*60*1000) {
writeEEPROM();
}
2024-08-31 12:47:54 +07:00
getPhaseAndTemperature();
2024-08-31 01:38:49 +07:00
2024-08-31 12:47:54 +07:00
Input = thermocouple.readCelsius();
if (isnan(Input)) {
Input = 0;
}
myPID.Compute();
2024-08-31 01:38:49 +07:00
2024-08-31 12:47:54 +07:00
// Switch SSR based on PID output and interval control
if (currentTime - ssrLastSwitchTime > ssrSwitchInterval) {
digitalWrite(ssrPin, Output > 0.5 ? HIGH : LOW);
ssrLastSwitchTime = currentTime;
}
2024-08-30 02:45:48 +07:00
2024-08-31 12:47:54 +07:00
if (isComplete && currentPhase >= activeProfile.numPhases && !finishTime) {
finishTime = currentTime;
}
2024-08-29 12:37:31 +07:00
2024-08-31 12:47:54 +07:00
// Display all phases and highlight the current one
printPhases();
2024-08-30 02:45:48 +07:00
2024-08-31 12:47:54 +07:00
oled.update();
delay(1000); // Update every second
2024-08-31 01:38:49 +07:00
}
void handleProfileSelection() {
2024-08-29 12:37:31 +07:00
2024-08-31 01:38:49 +07:00
bool click = enc1.isClick();
bool turn = enc1.isTurn();
if (!click && !turn) {
return;
2024-08-30 02:45:48 +07:00
}
2024-08-31 01:38:49 +07:00
// 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]));
}
2024-08-30 02:45:48 +07:00
2024-08-31 01:38:49 +07:00
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
totalElapsedTime = 0;
2024-08-31 01:38:49 +07:00
myPID.SetMode(AUTOMATIC);
myPID.SetOutputLimits(0, 1); // SSR is either ON or OFF
// digitalWrite(ssrPin, HIGH); // Start with heater on
2024-08-31 01:38:49 +07:00
ssrLastSwitchTime = millis();
writeEEPROM();
2024-08-31 01:38:49 +07:00
}
}
2024-08-30 02:45:48 +07:00
2024-08-31 01:38:49 +07:00
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();
2024-08-30 02:45:48 +07:00
}
void calculateTotalTime() {
// Calculate total process time, including transitions
2024-08-29 23:58:25 +07:00
totalProcessTime = 0;
for (int i = 0; i < activeProfile.numPhases; i++) {
totalProcessTime += activeProfile.phases[i].duration * 60;
2024-08-30 02:45:48 +07:00
if (i < activeProfile.numPhases - 1) {
int tempDiff = abs(activeProfile.phases[i + 1].temperature - activeProfile.phases[i].temperature);
totalProcessTime += tempDiff * 60 * activeProfile.transitionMinutesPerDegree;
}
2024-08-29 12:37:31 +07:00
}
2024-08-30 02:45:48 +07:00
}
2024-08-29 12:37:31 +07:00
2024-08-31 12:47:54 +07:00
void getPhaseAndTemperature() {
long accumulatedTime = 0;
2024-08-30 02:45:48 +07:00
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;
2024-08-31 12:47:54 +07:00
if (totalElapsedTime < accumulatedTime + transitionDuration) {
2024-08-30 02:45:48 +07:00
isInTransition = true;
2024-08-31 01:38:49 +07:00
currentPhase = i - 1; // Keep currentPhase as the previous phase
2024-08-31 12:47:54 +07:00
int timeInTransition = totalElapsedTime - accumulatedTime;
2024-08-31 01:38:49 +07:00
Setpoint = previousTemp + (double)timeInTransition / (60 * activeProfile.transitionMinutesPerDegree) * (targetTemp > previousTemp ? 1 : -1);
2024-08-31 12:47:54 +07:00
phaseStartTime = totalStartTime + accumulatedTime * 1000; // Set phase start time
2024-08-30 02:45:48 +07:00
return;
}
accumulatedTime += transitionDuration;
2024-08-29 12:37:31 +07:00
}
2024-08-30 02:45:48 +07:00
// Check if we're within the current phase
2024-08-31 12:47:54 +07:00
if (totalElapsedTime < accumulatedTime + phaseDuration) {
2024-08-30 02:45:48 +07:00
isInTransition = false;
currentPhase = i;
Setpoint = activeProfile.phases[i].temperature;
2024-08-31 12:47:54 +07:00
phaseStartTime = totalStartTime + accumulatedTime * 1000; // Set phase start time
2024-08-30 02:45:48 +07:00
return;
}
2024-08-29 23:58:25 +07:00
2024-08-30 02:45:48 +07:00
accumulatedTime += phaseDuration;
}
2024-08-29 12:37:31 +07:00
2024-08-30 02:45:48 +07:00
// 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
2024-08-31 12:47:54 +07:00
phaseStartTime = totalStartTime + accumulatedTime * 1000; // Set phase start time to the end
2024-08-29 12:37:31 +07:00
}
2024-08-31 12:47:54 +07:00
2024-08-31 01:46:02 +07:00
void printPhases() {
2024-08-29 23:58:25 +07:00
oled.clear();
if (isComplete) {
// Show completion time and current temperature instead of the title
oled.setCursor(0, 0);
oled.invertText(true);
2024-08-30 02:45:48 +07:00
oled.print("Done!");
oled.invertText(false);
oled.print(" ");
formatTime((currentTime - finishTime) / 1000, timeBuffer); // Time since completion
oled.print(timeBuffer);
oled.print(" ");
oled.print(Input,1);
oled.print("c");
oled.print("->");
oled.print((int)Setpoint);
oled.print("c");
} else {
oled.setCursor(0, 0);
oled.print(activeProfile.name);
}
2024-08-29 23:58:25 +07:00
// 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);
2024-08-31 01:46:02 +07:00
2024-08-29 23:58:25 +07:00
for (int i = 0; i < activeProfile.numPhases; i++) {
if (i == currentPhase && !isComplete) {
2024-08-29 23:58:25 +07:00
oled.invertText(true); // Invert text for the current phase
oled.setCursor(0, i + 2); // Set cursor to the row corresponding to the phase
2024-08-31 12:47:54 +07:00
long timeRemaining = (activeProfile.phases[i].duration * 60) - ((currentTime - phaseStartTime) / 1000);
2024-08-29 23:58:25 +07:00
formatTime(timeRemaining, timeBuffer);
oled.print(i + 1);
oled.print(". ");
2024-08-31 01:46:02 +07:00
oled.print(Input, 1);
2024-08-30 00:07:12 +07:00
oled.print("c ");
2024-08-30 02:45:48 +07:00
if (fabs(Setpoint - round(Setpoint)) < 0.05) {
oled.print((int)Setpoint); // Print without decimals
} else {
oled.print(Setpoint, 1); // Print with 1 decimal place
}
2024-08-30 00:07:12 +07:00
oled.print("c ");
2024-08-29 23:58:25 +07:00
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);
2024-08-30 00:07:12 +07:00
oled.print("c ");
2024-08-29 23:58:25 +07:00
oled.print(timeBuffer);
}
}
oled.invertText(false); // Ensure text inversion is off after the loop
}
void formatTime(long seconds, char* buffer) {
2024-08-30 00:07:12 +07:00
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);
}
2024-08-29 23:01:32 +07:00
}
void writeEEPROM() {
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);
}