#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include <DHT11.h>
#include <Versatile_RotaryEncoder.h>
/* Colin the Grinder Project
© 2023-2026 George Crawford and Kevin Davy
ElectroKev.com
V32.3
Release Notes:
- NEW MODE SYSTEM:
- AEROPRESS MODE (AP)
- FREERUN MODE (FR)
- TIMED GRIND MODE (TG)
- Prohibit grind settings changes unless in Timed Grind (TG) Mode
- NEW Edit total grams memory for total grinds correction
- NEW Live sensor temperature on main screen
- Freerun grind output monitoring
- Main screen live temp display
- Mode Main screen indicator
- Change Milk temp text to Sensor Temp
- Add <press to continue> after grinding in TG mode
- Add USB protection for pull up resistor in temperature mode
- Optimise temperature calculation algorithm for 10k (Variable) + 330 Ohm (Fixed) Sensor
- Remove the double 'gg' bug when on live grinding screen
- Update welcome screens
- Fix several other small bugs
*/
DHT11 dht11(7);
int grindMode = 0;
bool greenButtonWasPressed = false;
const float versionNumber = 32.3;
const int clearEEPROMButtonPin = 2;
const int clearHoldDuration = 3000;
// Define the pin for the thermistor
const int thermistorPin = A3;
// Define the value of the resistor
const float resistorValue = 12500.0;
// Define the parameters for the Steinhart-Hart equation
// NEVER EVER EVER EVER EVER CHANGE THE VALUES BELOW
const float A = 0.001129148;
const float B = 0.000195125;
const float C = 0.0000000996741;
// higher the co e the lower the temp
// lower the co e the highewr the temp
float adjustStopMilkC;
const int buttonPin = 3;
const int settingsButton = 9;
const int instantGrindButtonPin = 6;
const int ledPin = 4;
const int relayPin = 5;
const int powerLightPin = 10;
const uint8_t i2cAddr = 0x27;
const int pauseHoldDuration = 1;
bool offBackFlash = false;
bool countdownStarted = false;
bool isPaused = false;
bool showTemperature = false;
unsigned long pauseButtonStartTime = 0;
unsigned long clearButtonStartTime = 0;
float totalGramsStored;
float grinding;
float totalGrams;
float gramsPerSecond;
int heatMilkC = 35;
int stopMilkC = 63;
float grindSpeed;
float grindSpeedExceeded = 20;
float gpsSpeedExceeded = 35;
float tempCelsius;
// Main screen temperature refresh
unsigned long lastTempUpdate = 0;
const unsigned long tempUpdateInterval = 500;
#define LCD_COLS 20
#define LCD_ROWS 4
#define EEPROM_ADDRESS 0
#define grindTimeMemory 10
#define gpsMemory 20
#define finalDrinkTempSave 30
LiquidCrystal_I2C lcd(i2cAddr, LCD_COLS, LCD_ROWS);
#define clk A0
#define dt A1
#define sw 11
Versatile_RotaryEncoder *versatile_encoder;
void setup() {
Wire.begin();
Wire.setClock(400000L);
versatile_encoder = new Versatile_RotaryEncoder(clk, dt, sw);
versatile_encoder->setHandleRotate(handleRotate);
versatile_encoder->setHandlePress(handlePress);
versatile_encoder->setHandlePressRelease(handlePressRelease);
versatile_encoder->setHandleLongPress(handleLongPress);
versatile_encoder->setHandlePressRotate(handlePressRotate);
versatile_encoder->setHandlePressRotateRelease(handlePressRotateRelease);
lcd.init();
lcd.begin(16, 2);
lcd.backlight();
lcd.setBacklight(255);
pinMode(buttonPin, INPUT_PULLUP);
pinMode(sw, INPUT_PULLUP);
pinMode(instantGrindButtonPin, INPUT_PULLUP);
pinMode(ledPin, OUTPUT);
pinMode(relayPin, OUTPUT);
pinMode(powerLightPin, OUTPUT);
pinMode(clearEEPROMButtonPin, INPUT_PULLUP);
EEPROM.get(EEPROM_ADDRESS, totalGramsStored);
EEPROM.get(grindTimeMemory, grinding);
EEPROM.get(gpsMemory, gramsPerSecond);
EEPROM.get(finalDrinkTempSave, stopMilkC);
analogWrite(powerLightPin, 10);
// change this to Non Pullup for built in USB protection - Not done yet as fails no sensor check.
pinMode(thermistorPin, INPUT_PULLUP);
int humidity = dht11.readHumidity();
delay(500);
int temperature = dht11.readTemperature();
lcd.init();
lcd.clear();
lcd.setCursor(3, 0);
lcd.print("ElectroKev.com");
lcd.setCursor(4, 1);
lcd.print("Coffee Time!");
lcd.setCursor(8,2);
lcd.print("v");
lcd.print(versionNumber,1);
lcd.setCursor(0,3);
lcd.print(temperature);
lcd.print("c");
lcd.setCursor(17,3);
lcd.print(humidity);
lcd.print("%");
delay(3000);
lcd.clear();
lcd.setCursor(0,0);
lcd.print("George Crawford &");
lcd.setCursor(0,1);
lcd.print("Kevin Davy");
lcd.setCursor(0,2);
lcd.print("Total Ground:");
lcd.setCursor(0,3);
lcd.print(totalGramsStored,0);
lcd.print(" g");
delay(3000);
updateScreen();
}
void loop() {
if (versatile_encoder->ReadEncoder()) {
updateScreen();
}
if (millis() - lastTempUpdate >= tempUpdateInterval) {
lastTempUpdate = millis();
updateMainTempOnly();
}
if (digitalRead(buttonPin) == LOW && !greenButtonWasPressed) {
greenButtonWasPressed = true;
greenButtonPressed();
}
if (digitalRead(buttonPin) == HIGH) {
greenButtonWasPressed = false;
}
if (digitalRead(clearEEPROMButtonPin) == LOW) {
masterReset();
}
if (digitalRead(instantGrindButtonPin) == LOW) {
oneSecondGrind();
}
if (digitalRead(settingsButton) == LOW) {
editTotalGrams();
}
updateModeIndicator();
}
void editTotalGrams() {
lcd.clear();
// Wait for settings button release
while (digitalRead(settingsButton) == LOW) {
delay(10);
}
float editedTotal = totalGramsStored;
float lastDisplayed = -1;
static int lastClkState = HIGH;
unsigned long lastTurnTime = 0;
// Header
lcd.setCursor(2, 0);
lcd.print("Set Total Grams:");
// Instruction (full 20 chars)
lcd.setCursor(0, 3);
lcd.print("<Press knob to save>");
while (digitalRead(sw) == HIGH) {
int clkState = digitalRead(clk);
if (clkState != lastClkState && clkState == LOW) {
unsigned long now = millis();
unsigned long turnGap = now - lastTurnTime;
lastTurnTime = now;
float stepSize = 10;
if (turnGap < 80) {
stepSize = 500;
} else if (turnGap < 180) {
stepSize = 100;
}
if (digitalRead(dt) != clkState) {
editedTotal += stepSize;
} else {
editedTotal -= stepSize;
}
if (editedTotal < 0) {
editedTotal = 0;
}
delay(40);
}
lastClkState = clkState;
// Only update display if value changed
if (editedTotal != lastDisplayed) {
lcd.setCursor(3, 2);
lcd.print("Total: ");
lcd.setCursor(10, 2);
lcd.print(editedTotal, 0);
lcd.print(" g");
lastDisplayed = editedTotal;
}
}
// Save value
totalGramsStored = editedTotal;
EEPROM.put(EEPROM_ADDRESS, totalGramsStored);
lcd.clear();
lcd.setCursor(8, 1);
lcd.print("Saved");
lcd.setCursor(7, 2);
lcd.print(totalGramsStored, 0);
lcd.print(" g");
delay(1200);
// Wait for knob release
while (digitalRead(sw) == LOW) {
delay(10);
}
updateScreen();
}
void updateModeIndicator() {
lcd.setCursor(11, 3);
if (grindMode == 0) {
lcd.print("TG"); // Timed Grind
}
else if (grindMode == 1) {
lcd.print("AP"); // Aeropress
}
else if (grindMode == 2) {
lcd.print("FR"); // Free Run
}
}
float readSensorTemperature() {
int rawADC = analogRead(thermistorPin);
if (rawADC <= 1 || rawADC >= 1022) {
return -999;
}
float resistance = resistorValue / ((1023.0 / rawADC) - 1.0);
if (resistance <= 0 || isnan(resistance) || isinf(resistance)) {
return -999;
}
float tempKelvin = 1.0 / (A + B * log(resistance) + C * pow(log(resistance), 3));
float tempCelsiusStretch = tempKelvin - 273.15;
float inputValue = tempCelsiusStretch;
float calculatedTemp = -999;
if (inputValue < 51) {
float inputMin = 39.3;
float inputMax = 51.0;
float outputMin = 4.0;
float outputMax = 35.0;
calculatedTemp = (inputValue - inputMin) * (outputMax - outputMin) / (inputMax - inputMin) + outputMin;
}
if (inputValue >= 51) {
float inputMin = 51.1;
float inputMax = 61.0;
float outputMin = 35.1;
float outputMax = 54.0;
calculatedTemp = (inputValue - inputMin) * (outputMax - outputMin) / (inputMax - inputMin) + outputMin;
}
return calculatedTemp;
}
void updateMainTempOnly() {
float mainTemp = readSensorTemperature();
lcd.setCursor(0, 3);
if (mainTemp >= 0 && mainTemp <= 200) {
lcd.print(" ");
lcd.setCursor(0, 3);
lcd.print(mainTemp, 1);
lcd.setCursor(6,3);
lcd.print("C");
} else {
lcd.print("-----");
}
}
void updateScreen() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Time: ");
lcd.setCursor(8,0);
lcd.print(grinding,1);
lcd.print(" Seconds");
totalGrams = gramsPerSecond * grinding;
/*
lcd.setCursor(0, 1);
lcd.print("Grams: ");
lcd.setCursor(8,1);
lcd.print(totalGrams, 1);
lcd.print(" g ");
*/
if (grindMode == 1) {
// Aeropress mode
lcd.setCursor(0,0);
lcd.print(" ");
lcd.setCursor(0,1);
lcd.print("Aeropress Mode 15g ");
} else if (grindMode == 2) {
// Free run mode
lcd.setCursor(0,0);
lcd.print(" ");
lcd.setCursor(0,1);
lcd.print("Free Run Mode ");
} else {
// Timed grind mode
lcd.setCursor(0, 0);
lcd.print("Time: ");
lcd.setCursor(8,0);
lcd.print(grinding,1);
lcd.print(" Seconds");
lcd.setCursor(0, 1);
lcd.print("Grams: ");
lcd.setCursor(8,1);
lcd.print(totalGrams, 1);
lcd.print(" g ");
}
lcd.setCursor(0,2);
lcd.print("g/s: ");
lcd.setCursor(8,2);
lcd.print(gramsPerSecond * 10 / 10,2);
lcd.setCursor(14, 3);
lcd.print("<HOLD>");
grindSpeed = millis();
updateModeIndicator();
}
void handleRotate(int8_t rotation) {
// Only allow grind time adjustment in Timed Grind mode
if (grindMode != 0) {
return;
}
if (rotation > 0) {
if (grinding > 0.1) {
grinding = grinding - 0.1;
if (grindSpeedExceeded > millis() - grindSpeed) {
grinding = grinding - 1.5;
}
}
}
else if (grinding < 50) {
grinding = grinding + 0.1;
if (grindSpeedExceeded > millis() - grindSpeed) {
grinding = grinding + 1.5;
}
}
}
void handleLongPress() {
startGrinding();
}
void handlePressRotate(int8_t rotation) {
if (rotation > 0) {
if (gramsPerSecond >= 0.1) {
gramsPerSecond = gramsPerSecond - 0.01;
if (gpsSpeedExceeded > millis() - grindSpeed) {
gramsPerSecond = gramsPerSecond - 0.2;
}
}
}
else {
gramsPerSecond = gramsPerSecond + 0.01;
if (gpsSpeedExceeded > millis() - grindSpeed) {
gramsPerSecond = gramsPerSecond + 0.2;
}
}
}
void handlePressRelease() {
}
void handleLongPressRelease() {
}
void handlePress() {
}
void handlePressRotateRelease() {
EEPROM.put(gpsMemory, gramsPerSecond);
calibrateScreen();
}
void saveSettings() {
EEPROM.put(grindTimeMemory, grinding);
}
void greenButtonPressed() {
grindMode++;
if (grindMode > 2) {
grindMode = 0;
}
lcd.clear();
if (grindMode == 0) {
lcd.setCursor(2, 1);
lcd.print("Timed Grind Mode");
}
if (grindMode == 1) {
lcd.setCursor(3, 1);
lcd.print("Aeropress Mode");
lcd.setCursor(9, 2);
lcd.print("15 g");
}
if (grindMode == 2) {
lcd.setCursor(4, 1);
lcd.print("Free Run Mode");
lcd.setCursor(1, 2);
lcd.print("Hold knob to start");
}
delay(1000);
updateScreen();
}
void calibrateScreen() {
lcd.clear();
lcd.setCursor(7,1);
lcd.print("Saved:");
lcd.setCursor(7,2);
lcd.print(gramsPerSecond * 10 / 10,2);
lcd.print(" g/s");
delay(1000);
}
void masterReset() {
if (digitalRead(clearEEPROMButtonPin) == LOW) {
if (clearButtonStartTime == 0) {
clearButtonStartTime = millis();
}
if (millis() - clearButtonStartTime >= clearHoldDuration) {
totalGramsStored = 0.0;
for (int i = 0; i < EEPROM.length(); ++i) {
EEPROM.write(i, 1);
}
gramsPerSecond = 0;
grinding = 0;
stopMilkC = 55;
EEPROM.put(finalDrinkTempSave, stopMilkC);
lcd.clear();
lcd.setCursor(3, 1);
lcd.print("Memory Cleared");
delay(2000);
updateScreen();
}
}
else {
clearButtonStartTime = 0;
}
}
void oneSecondGrind() {
if (millis() - pauseButtonStartTime >= pauseHoldDuration) {
digitalWrite(ledPin, HIGH);
digitalWrite(relayPin, HIGH);
lcd.clear();
lcd.setCursor(2, 1);
lcd.print("Immediate Grind");
lcd.setCursor(5, 2);
lcd.print("1/2 Second");
delay(500);
digitalWrite(ledPin, LOW);
digitalWrite(relayPin, LOW);
}
else {
pauseButtonStartTime = millis();
}
updateScreen();
}
void startGrinding() {
if (!countdownStarted) {
countdownStarted = true;
bool temp15gMode = (grindMode == 1);
bool freeRunMode = (grindMode == 2);
float originalGrinding = grinding;
float targetGrams = gramsPerSecond * grinding;
// --- AEROPRESS MODE ---
if (temp15gMode && gramsPerSecond > 0) {
grinding = 15.0 / gramsPerSecond;
targetGrams = 15.0;
lcd.clear();
lcd.setCursor(1, 1);
lcd.print("Aeropress 15g Mode");
delay(800);
}
// --- FREE RUN MODE ---
if (freeRunMode) {
lcd.clear();
lcd.setCursor(4, 1);
lcd.print("FREE RUNNING");
lcd.setCursor(1, 3);
lcd.print("Press knob to stop");
// Wait for release of initial press
while (digitalRead(sw) == LOW) {
delay(10);
}
digitalWrite(ledPin, HIGH);
digitalWrite(relayPin, HIGH);
unsigned long startTime = millis();
// Wait for next press to stop
while (digitalRead(sw) == HIGH) {
delay(10);
}
digitalWrite(ledPin, LOW);
digitalWrite(relayPin, LOW);
unsigned long runTime = millis() - startTime;
float secondsRun = runTime / 1000.0;
float gramsDispensed = gramsPerSecond * secondsRun;
totalGramsStored += gramsDispensed;
EEPROM.put(EEPROM_ADDRESS, totalGramsStored);
lcd.clear();
lcd.setCursor(2, 1);
lcd.print("Free Run Output");
lcd.setCursor(8, 2);
lcd.print(gramsDispensed, 1);
lcd.print(" g");
delay(2500);
countdownStarted = false;
isPaused = false;
updateScreen();
return;
}
// --- NORMAL / TIMED GRIND ---
digitalWrite(ledPin, HIGH);
digitalWrite(relayPin, HIGH);
if (!temp15gMode) {
saveSettings();
}
lcd.clear();
for (float i = grinding; i >= 0; i -= 0.1) {
delay(62);
lcd.setCursor(6, 1);
lcd.print("Grinding ");
lcd.setCursor(3, 2);
lcd.print(i, 1);
lcd.print(" sec ");
float gramsElapsed = gramsPerSecond * (grinding - i);
lcd.print(gramsElapsed, 1);
lcd.setCursor(17, 2);
lcd.print("g");
// --- PAUSE FUNCTION ---
if (digitalRead(instantGrindButtonPin) == LOW) {
if (millis() - pauseButtonStartTime >= pauseHoldDuration) {
isPaused = !isPaused;
while (digitalRead(instantGrindButtonPin) == LOW) {}
}
} else {
pauseButtonStartTime = millis();
}
if (isPaused) {
lcd.clear();
lcd.setCursor(7, 1);
lcd.print("PAUSED");
digitalWrite(ledPin, LOW);
digitalWrite(relayPin, LOW);
while (isPaused) {
if (digitalRead(instantGrindButtonPin) == LOW) {
if (millis() - pauseButtonStartTime >= pauseHoldDuration) {
isPaused = !isPaused;
while (digitalRead(instantGrindButtonPin) == LOW) {}
}
} else {
pauseButtonStartTime = millis();
}
}
lcd.clear();
} else {
digitalWrite(ledPin, HIGH);
digitalWrite(relayPin, HIGH);
}
}
// --- FINISH TIMED GRIND ---
digitalWrite(ledPin, LOW);
digitalWrite(relayPin, LOW);
if (temp15gMode) {
totalGramsStored += 15.0;
} else {
totalGramsStored += totalGrams;
}
EEPROM.put(EEPROM_ADDRESS, totalGramsStored);
lcd.clear();
lcd.setCursor(3, 1);
lcd.print("Grams Dispensed");
lcd.setCursor(7, 2);
if (temp15gMode) {
lcd.print(15.0, 1);
} else {
lcd.print(totalGrams, 1);
}
lcd.print(" g");
lcd.setCursor(1, 3);
lcd.print("<Press Menu Knob>");
grinding = originalGrinding;
totalGrams = gramsPerSecond * grinding;
countdownStarted = false;
isPaused = false;
}
while (digitalRead(sw) == HIGH) {
// wait for press
}
}
void drinkTemperature() {
lcd.clear();
while (digitalRead(settingsButton) == LOW) {
delay(50);
lcd.setCursor(2,0);
lcd.print("Milk Temperature");
lcd.setCursor(8,2);
lcd.print(stopMilkC);
lcd.print(" C");
if (digitalRead(buttonPin) == LOW) {
lcd.setCursor(8,2);
stopMilkC = stopMilkC - 1;
lcd.print(stopMilkC);
lcd.print(" C");
}
if (digitalRead(instantGrindButtonPin) == LOW) {
lcd.setCursor(8,2);
stopMilkC = stopMilkC + 1;
lcd.print(stopMilkC);
lcd.print(" C");
}
}
EEPROM.put(finalDrinkTempSave, stopMilkC);
}