#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
Release Notes:
- Change Milk temp to Sensor Temp
- Main screen live temp display
- Add USB protection for pull up resistor in temperature mode
- Adjust temperature calculation algorithm for 10k (Variable) + 330 Ohm (Fixed) Sensor
- Remove the double 'gg' when on live grinding screen
- Update welcome screens
- NEW Aeropress Mode. Hold top sensor whilst long press for 15g set output without changing settings
- Temporary Aeropress Mode Main screen indicator
- Fix several other small bugs
*/
DHT11 dht11(7);
const int versionNumber = 32;
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 = 14000.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(0,0);
lcd.print("*");
lcd.setCursor(19,0);
lcd.print("*");
lcd.setCursor(0, 1);
lcd.print(" ElectroKev.com");
lcd.setCursor(0, 2);
lcd.print(" Coffee Time!");
lcd.setCursor(0,3);
lcd.print(temperature);
lcd.print("c");
lcd.setCursor(17,3);
lcd.print(humidity);
lcd.print("%");
delay(2000);
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Grinder Project V");
lcd.print(versionNumber);
lcd.setCursor(0,1);
lcd.print("George Crawford &");
lcd.setCursor(0,2);
lcd.print("Kevin Davy");
lcd.setCursor(0,3);
lcd.print("ElectroKev.com 2026");
delay(2000);
lcd.clear();
lcd.setCursor(3, 1);
lcd.print("All Time Total:");
lcd.setCursor(8,2);
lcd.print(totalGramsStored,0);
lcd.print(" g");
delay(2000);
updateScreen();
}
void loop() {
if (versatile_encoder->ReadEncoder()) {
updateScreen();
}
if (millis() - lastTempUpdate >= tempUpdateInterval) {
lastTempUpdate = millis();
updateMainTempOnly();
}
if (digitalRead(buttonPin) == LOW) {
greenButtonPressed();
}
if (digitalRead(clearEEPROMButtonPin) == LOW) {
masterReset();
}
if (digitalRead(instantGrindButtonPin) == LOW) {
oneSecondGrind();
}
if (digitalRead(settingsButton) == LOW) {
lcd.setCursor(11,3);
lcd.print("AP");
} else {
lcd.setCursor(11,3);
lcd.print(" ");
}
}
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.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 ");
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();
}
void handleRotate(int8_t rotation) {
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() {
lcd.clear();
showTemperature = !showTemperature;
while (showTemperature) {
if (digitalRead(settingsButton) == LOW) {
drinkTemperature();
lcd.clear();
}
tempCelsius = readSensorTemperature();
if (tempCelsius >= 0 && tempCelsius <= 200) {
if (tempCelsius >= 0 && tempCelsius <= heatMilkC) {
offBackFlash = false;
lcd.setCursor(0,0);
lcd.print(" ** Aerate Milk ** ");
}
if (tempCelsius >= heatMilkC && tempCelsius <= stopMilkC) {
offBackFlash = false;
lcd.setCursor(0,0);
lcd.print(" ** Heat Milk ** ");
}
if (tempCelsius >= stopMilkC) {
lcd.setCursor(0,0);
lcd.print(" ** STOP - DONE ** ");
if (!offBackFlash) {
for (int backFlash = 0; backFlash < 6; backFlash++) {
lcd.setBacklight(0);
delay(100);
lcd.setBacklight(255);
delay(100);
offBackFlash = true;
}
}
}
lcd.setCursor(5,1);
lcd.print("Sensor Temp: ");
lcd.setCursor(3,2);
lcd.print(tempCelsius * 10 / 10,1);
lcd.print(" C -> ");
lcd.setCursor(14,2);
lcd.print(stopMilkC);
lcd.print(" C");
lcd.setCursor(1,3);
lcd.print("<Press Menu Knob>");
delay(100);
}
else {
lcd.setCursor(0,0);
lcd.print("<Connect Temp Sensor>");
lcd.setCursor(0,2);
lcd.print(" ? ");
}
if (digitalRead(sw) == LOW) {
showTemperature = false;
break;
}
}
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;
// Detect temporary aeropress 15g mode
bool temp15gMode = (digitalRead(settingsButton) == LOW);
// Store original values
float originalGrinding = grinding;
float targetGrams = gramsPerSecond * grinding;
// Apply temporary override
if (temp15gMode && gramsPerSecond > 0) {
grinding = 15.0 / gramsPerSecond;
targetGrams = 15.0;
lcd.clear();
lcd.setCursor(1, 1);
lcd.print("Aeropress 15g Mode");
delay(800);
}
digitalWrite(ledPin, HIGH);
digitalWrite(relayPin, HIGH);
if (!temp15gMode) {
saveSettings(); // only save normal grinding time
}
lcd.clear();
// MAIN GRINDING LOOP ***************************
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");
// CHECK FOR 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);
}
}
// END OF MAIN GRINDING LOOP *********************
digitalWrite(ledPin, LOW);
digitalWrite(relayPin, LOW);
// Add correct grams to total
if (temp15gMode) {
totalGramsStored += 15.0;
} else {
totalGramsStored += totalGrams;
}
EEPROM.put(0, 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>");
// Restore original values
grinding = originalGrinding;
totalGrams = gramsPerSecond * grinding;
countdownStarted = false;
isPaused = false;
}
while (digitalRead(sw) == HIGH) {
// Wait for encoder 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);
}