// Pinout configuration const int grinderPin = 9, pumpPin = 8, boilerPin = 6, powderPin = 7, wheelPin = 10, invertWheelPin = 11, powderSensorPin = 2, vaporSensorPin = 5, tempSensorPin = A7, wheelStartSensorPin = 4, wheelEndSensorPin = 3, redLED = 13, greenLED = 12; // Global string String readString; // Thermistor config // WARNING! while the old coffee machine used a PTC thermistor, this was replaced with an NTC one as it was more readily available // Code must be adjusted accordingly if a PTC resistor is used once more! int Vo; float R1 = 10000; // <-- change this value to the resistance of the fixed resistor (so don't change PLS!) float logR2, R2, T, Tc, Tp, Tstart, Told; float c1 = 8.5e-04, c2 = 1.85e-04, c3 = 2e-07; // Here's where you can perform actual calibration // Variable to store desired Max boiler Temp int desiredTemp = 70; // Variables to Store errors: int unrecoverableErr = 0; int warning = 0; int warned = 0; // support variable needed to prevent arduino from making second coffee after refill. this is a shitty hack and resets the board. // Variables for cleaning and other maintenance int dry = 0; int noCoffee = 0; // Milliseconds to delay between each cycle const int milliseconds = 10; int timeratio = 1; // pumpRatio is = seconds to pump water for and will be updated over serial int pumpRatio = 15; // warning: there is usually ~5s dead time from pump startup to water flowing out of nozzle int pumpStatus = 0; // support variable to enable pumping while heating, added feb 24 // This next variable is used to get current "time" in ms and break out of while cycles that need a time limit safeguard unsigned long startTime; unsigned long pMillis; //_____________________________________________________________________________ void(* resetFunc) (void) = 0; // declare reset fuction at address 0 //_____________________________________________________________________________ void setup() { // put your setup code here, to run once: // first we set pins as I/O and initialize outputs LOW pinMode(boilerPin, OUTPUT); pinMode(pumpPin, OUTPUT); pinMode(grinderPin, OUTPUT); pinMode(powderPin, OUTPUT); pinMode(wheelPin, OUTPUT); pinMode(invertWheelPin, OUTPUT); pinMode (redLED, OUTPUT); pinMode (greenLED, OUTPUT); pinMode(powderSensorPin, INPUT); pinMode(vaporSensorPin, INPUT); pinMode(tempSensorPin, INPUT); pinMode(wheelStartSensorPin, INPUT); pinMode(wheelEndSensorPin, INPUT); digitalWrite(grinderPin, LOW); digitalWrite(pumpPin, LOW); digitalWrite(boilerPin, LOW); digitalWrite(powderPin, LOW); digitalWrite(wheelPin, LOW); digitalWrite(invertWheelPin, LOW); digitalWrite(redLED, LOW); digitalWrite(greenLED, LOW); // timeratio easily allows to determine how many cycles are required to make 1s pass (ms * ratio = 1s) timeratio = 1000/milliseconds; // initialize serial: Serial.begin(9600); delay(100); Serial.write("Arduino running :)\n"); } //_____________________________________________________________________________ void loop() { // put your main code here, to run repeatedly: digitalWrite(greenLED, HIGH); // Check if unrecoverable error has occurred: if (unrecoverableErr == 1) { //startTime = millis(); delay(100); Serial.write("Error has occurred.\n"); delay(100); Serial.write("Check machine and restart\n"); digitalWrite(greenLED, LOW); while (true){ // lock the machine in a loop while blinking the RED LED to indicate an error digitalWrite(redLED, HIGH); delay(100); digitalWrite(redLED, LOW); delay(900); if (Serial.available() > 0) { warning = 0; warned = 0; unrecoverableErr = 0; // added on 19/11/24 to reset unrecoverable error when serial com is received break; } // reset arduino after 30s -> removed reset on 19/11/24, LED should stay blinking and machine locked until reboot or serialcom received //if (millis() - startTime == 30000) { // break; //} } //resetFunc(); //call reset. } // check if data has been sent on serial from ESP or PC: if (Serial.available() > 0) { // read the incoming data: (we only care about the first few characters, I've chosen 4) readString = ""; serialRead(); String incomingData = readString.substring(0,4); //if (incomingData == "read") { //digitalWrite(greenLED, HIGH); //} if (incomingData == "rist") { // set parameters for ristretto: delay(100); Serial.write("Ristretto\n"); pumpRatio = 10; // updated from 15 to 10s after fixing restriction 19/11/24 } if (incomingData == "espr") { // set parameters for espresso: delay(100); Serial.write("Espresso\n"); pumpRatio = 15; // updated from 20 to 15s after fixing restriction 19/11/24 } if (incomingData == "long") { // set parameters for lungo: delay(100); Serial.write("Lungo\n"); pumpRatio = 20; // updated from 25 to 20s after fixing restriction 19/11/24 } if (incomingData == "amer") { // set parameters for lungo: delay(100); Serial.write("Americano\n"); pumpRatio = 60; } // check if the incoming data is "make": if (incomingData == "make") { // run the code to make coffee: delay(100); Serial.write("Making some coffee!\n"); makeCoffee(); } if (incomingData == "dryr") { // run the code to perform a dry run (no powder, nor water): delay(100); Serial.write("DryRun\n"); dry = 1; makeCoffee(); } if (incomingData == "noco") { // run the code to perform a wet run (water, but no powder): delay(100); Serial.write("NoCo\n"); pumpRatio = 10; noCoffee = 1; makeCoffee(); } if (incomingData == "grin") { // only grind Grind(); } if (incomingData == "pump") { // run the code to just pump water: pumpRatio = 15; Pump(); } if (incomingData == "pres") { // only press Press(); } if (incomingData == "unpr") { // only unpress unPress(); } if (incomingData == "heat") { // only heat desiredTemp = 80; Heat(); } if (incomingData == "drop") { // only drop Drop(); } if (incomingData == "clea") { // clean machine delay(100); Serial.write("s-clean\n"); dry = 1; makeCoffee(); noCoffee = 1; makeCoffee(); delay(100); Serial.write("S-cleaning done.\n"); } if (incomingData == "f-un") { Serial.write("F-Unpress\n"); delay(1000); digitalWrite(invertWheelPin, HIGH); delay(5000); digitalWrite(invertWheelPin, LOW); } if (incomingData == "f-pr") { Serial.write("F-Press\n"); delay(1000); digitalWrite(wheelPin, HIGH); delay(5000); digitalWrite(wheelPin, LOW); } } // making steam: while (digitalRead(vaporSensorPin) == HIGH) { desiredTemp = 100; Serial.write("Vapor heating\n"); delay(500); Heat(); // Heat the boiler to vapor temperature //delay(20000); // no idea why if had a 20s delay originally, I attempted to remove it to see how it goes on 19/11/24 delay(200); // assuming 20000 was a type error, 200ms seems sensible if (unrecoverableErr == 1) { break; } } delay(milliseconds); } //_____________________________________________________________________________ // this is how we read the input void serialRead() { while (Serial.available()) { delay(10); if (Serial.available() > 0) { char c = Serial.read(); readString += c;} } } //_____________________________________________________________________________ void Grind() { digitalWrite(greenLED, LOW); delay(100); Serial.write("grinding...\n"); digitalWrite(grinderPin, HIGH); startTime = millis(); while (true) { delay(milliseconds); if (digitalRead(powderSensorPin) == HIGH) { digitalWrite(grinderPin, LOW); delay(100); Serial.write("Grinding Done\n"); break; } if (millis() - startTime > 15000) { Serial.write("Warning, grinding took too long!\n"); digitalWrite(grinderPin, LOW); delay(100); Serial.write("Out of Coffee?\n"); warning = 1; warned = 1; break; } } digitalWrite(greenLED, HIGH); } //_____________________________________________________________________________ void Drop() { digitalWrite(greenLED, LOW); delay(100); Serial.write("dropping...\n"); digitalWrite(powderPin, HIGH); delay(1000); digitalWrite(powderPin, LOW); delay(100); Serial.write("Dropped\n"); digitalWrite(greenLED, HIGH); } //_____________________________________________________________________________ void Heat() { digitalWrite(greenLED, LOW); digitalWrite(redLED, HIGH); startTime = millis(); pMillis = startTime; // value to store delay(100); Serial.write("Heating to "); delay(100); Serial.print(desiredTemp); delay(100); // initialize variables at safe values Tc = 0; // current temperature Tp = -10; // temperature at previous cycle Tstart = - 100; // temperature at start of Heat() function // monitor temperature and adjust boilerPin as needed: while (true) { // read temperature from tempSensorPin: Vo = analogRead(tempSensorPin); R2 = R1 * (1023.0 / (float)Vo - 1.0); logR2 = log(R2); T = (1.0 / (c1 + c2*logR2 + c3*logR2*logR2*logR2)); // compute temperature from NTC Tc = (T - 273.15); // convert from Kelvin to Celsius for readability if (millis() - startTime > 500 && millis() - startTime < 1500) { Tstart = Tc; // support variable to store temp at beginning, so that we can be sure it's increasing delay(1001); // make sure we only set Tstart once! } // check if temperature is within the acceptable range and break out of the loop without error if done heating: if (Tc > desiredTemp && pumpStatus == 0) { delay(100); Serial.write("reached desired temp\n"); delay(100); Serial.print(Tc); break; // temperature is within range, break out of the loop: } if (Tc < -100) { // break the loop if temperature < -100, can only happen if cable gets unplugged delay(100); Serial.write("u-Thermocouple: unplugged or failed\n"); unrecoverableErr = 1; break; } if (millis() - startTime > 20000 && Tc - Tstart < 1 && pumpStatus == 0) { // break the loop if temperature is not increasing delay(100); Serial.write("p-Thermocouple: not detecting heating, positioning or relay fault\n"); unrecoverableErr = 1; break; } if (millis() - startTime > 90000 && pumpStatus == 0) { // break out of the loop after 60s if the boiler is not yet hot delay(100); Serial.write("h-taking too long, continuing...\n"); break; } // actual heater logic follows: // check aprox. derivative every second and shut off heater if temperature is increasing too quickly! // conversely, heater is turned on if temperature is not incresing quickly enough if (millis() > (pMillis + 1000)){ // initially run the heater on fully. eg: if we set the number to 30 and start with // Tambient = 20C and desiredTemp = 90 the heater will stay fully on until 90-30 = 60C //if (Tc < (desiredTemp - 30)) { // old if with variable temp // new if with hard-coded minimum pulsing enable temperature of 70C if (Tc < 70 || Tc < (desiredTemp - 20)) { digitalWrite(boilerPin, HIGH); } // change this value for proportional behaviour: lower values will cycle the relay more often. // Cycling often reduces temperature undershoot, but will shorten contact lifespan else if ((Tc - Tp) < 0.6) { digitalWrite(boilerPin, HIGH); } //else if ((Tc - Tp) > 1) { // digitalWrite(boilerPin, LOW); //} else { digitalWrite(boilerPin, LOW); // delay(1000); // extra 1s delay to keep boiler off for 2s total (delay + millis()) } // this next if lets us call us for pumping while still monitoring heat if (pumpStatus == 1 && ((millis() - startTime)/1000) < pumpRatio) { digitalWrite(pumpPin, HIGH); } else if (pumpStatus == 1 && ((millis() - startTime)/1000) > pumpRatio) { digitalWrite(pumpPin, LOW); break; } else { //digitalWrite(pumpPin, LOW); //pumpStatus = 0; } Tp = Tc; pMillis = millis(); } delay(milliseconds); } digitalWrite(boilerPin, LOW); digitalWrite(redLED, LOW); digitalWrite(greenLED, HIGH); } //_____________________________________________________________________________ void Press() { digitalWrite(greenLED, LOW); delay(100); Serial.write("pressing...\n"); // Slightly move the press in the opposite direction to make sure the motor's commutator engages digitalWrite(invertWheelPin, HIGH); delay(100); digitalWrite(invertWheelPin, LOW); digitalWrite(wheelPin, HIGH); startTime = millis(); while (true) { delay(milliseconds); if (digitalRead(wheelEndSensorPin) == HIGH) { delay(90); // extra delay added to compensate for slightly incorrect endstop placement after repair digitalWrite(wheelPin, LOW); delay(100); Serial.write("Pressed\n"); break; } if (millis() - startTime > 20000) { // changed from 15s to 20s to account for motor slowdown over the years delay(100); Serial.write("p-end of pressing not detected\n"); digitalWrite(wheelPin, LOW); unrecoverableErr = 1; //temporarily disabled because of failing sensor(s) break; } } digitalWrite(greenLED, HIGH); } //_____________________________________________________________________________ void unPress() { digitalWrite(greenLED, LOW); delay(100); Serial.write("unPressing...\n"); // Slightly move the press in the opposite direction to make sure the motor's commutator engages digitalWrite(wheelPin, HIGH); delay(100); digitalWrite(wheelPin, LOW); digitalWrite(invertWheelPin, HIGH); startTime = millis(); while (true) { delay(milliseconds); if (digitalRead(wheelStartSensorPin) == HIGH) { delay(1000); digitalWrite(invertWheelPin, LOW); delay(100); Serial.write("UnPressed\n"); break; } if (millis() - startTime > 20000) { // changed from 15s to 20s to account for motor slowdown over the years delay(100); Serial.write("u-end of unPressing not detected\n"); digitalWrite(invertWheelPin, LOW); //unrecoverableErr = 1; //temporarily disabled because of failing sensor(s) break; } } digitalWrite(greenLED, HIGH); } //_____________________________________________________________________________ void Pump() { digitalWrite(greenLED, LOW); delay(100); Serial.write("pumping Water...\n"); pumpStatus = 1; // set pumpStatus variable to 1 to tell Heat() function to pump; this is so that Proportional heating control can still function while pumping, preventing overheat Heat(); // call Heat function with current parameters pumpStatus = 0; // set pumpStatus variable to 0 // OLD PUMPING LOGIC BEFORE MOVING INSIDE Heat() //digitalWrite(pumpPin, HIGH); //while (pumpRatio > 1) { // pumpRatio--; // delay(1000); //} //digitalWrite(pumpPin, LOW); delay(100); Serial.write("Pumping water done\n"); digitalWrite(greenLED, HIGH); } //_____________________________________________________________________________ void makeCoffee() { // code to make coffee goes here... // It would be much smarter to break down complicated functions into subroutines: press/unpress etc... // Hence that's what we'll have done by the time the first release goes public ;) // makeCoffee() can still run all the same, but clean() can use the same press/unpress code! // care must be taken to place these functions earlier in the code, or the compiler will rightfully freak out while (true) { desiredTemp = 80; Heat(); // First we pre-heat the boiler if (unrecoverableErr == 1) { break; } if (dry == 0 && noCoffee == 0) { Grind(); // Grinding some nice roasted coffee beans! } // If grinder won't stop, coffee probably needs to be refilled. if (warning == 1) { delay(100); Serial.write("refill coffee and send cont command\n"); while (true) { // check if data has been sent on serial from ESP or PC: if (Serial.available() > 0) { // read the incoming data: (we only care about the first few characters, I've chosen 4) readString = ""; serialRead(); String incomingData = readString.substring(0,4); if (incomingData == "cont") { // coffee refilled, making is allowed to continue Serial.write("Refilled\n"); warning = 0; Grind(); // Grinding some nice roasted coffee beans! if (warning == 1) { unrecoverableErr = 1; Serial.write("g-failure: second grinding failure, check grinder!\n"); } break; } } } } // Stop if grinding failed a second time if (unrecoverableErr == 1) { break; } delay(1000); // this delay is mandatory to prevent powder spillage caused by grinder inertia. // Slightly move the press in the opposite direction to make sure the press sensor detects contact digitalWrite(wheelPin, HIGH); delay(1000); digitalWrite(wheelPin, LOW); unPress(); // unpressing the press to reset the machine if (unrecoverableErr == 1) { break; } Drop(); // Dropping the powder in the press delay(1000); Press(); // Self explainatory if (unrecoverableErr == 1) { break; } desiredTemp = 90; Heat(); // First we pre-heat the boiler if (unrecoverableErr == 1) { break; } if (dry == 0) { //digitalWrite(boilerPin, HIGH); delay(100); desiredTemp = 95; // temperature to try and maintain while making coffee Pump(); // Pump ratio (in seconds) will be read from serial input in final release digitalWrite(boilerPin, LOW); } // Slightly move the press back to create a way for vapor to escape (added 19/11/24) digitalWrite(invertWheelPin, HIGH); delay(3000); digitalWrite(invertWheelPin, LOW); delay(3000); // Allow pressure to wane unPress(); // unpressing the press to reset the machine if (unrecoverableErr == 1) { break; } delay(100); Serial.write("Complete!\n"); delay(1000); dry = 0; noCoffee = 0; if (warned == 1) { resetFunc(); //call reset. I am ashamed of this, but it works. } break; } }