#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.7 Production Release (LOCKED)
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
- NEW Settings Menu
- NEW Progress bar during grinding
- NEW Customisable Descale Reminders with built in Coffee Counter
- Full Automated Calibration Mode
- Adjustable Calibration Duration (Defaults to optimum 10 second)
- Improved manual calibration overide
- Aeropress Calibration Function
- Simplified output display - No longer time dependent.
- 'Hold to Start' converted to 'Single Click' to Start
- Confirmation of output screen added
- Grind value saved on confirmation screen rather than after grinding (allows change to next grind before reboot without active grinding)
- Cancel Grinding function added before and during grind
- Independent processing timing code for timing the grind - eliminated time drift
- Relay soft start function added
- 1/2 second grind changed to 1 GRAM grind.
- Fixed intermittent responsiveness rotary encoder
- Improved accelerated encoder function for value setting
- Freerun grind output monitoring
- TG and AP mode require a valid calibration before being allowed to run
- Main screen live temp display
- Main screen mode indicator
- Added visual moving graphic on line 2 in TG mode
- Add <Return to Home> 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 screen 1
- Update Totals Screen to show Total Grinds, Total until descale required and Total 18g shots
- Removed flicker in AP/FR mode
- Error handling for new chips without previously saved EEPROM values
- Bug fix - Correct screen initialised
- Fix several other small bugs
==============================================================
** Top Button = Settings Menu **
** Green Button = Mode selector **
** White Button = Cancel in all screens other than main menu where it acts as 1 Gram Grind Button **
** Black Button = Hard Reset (Should no longer be required in general use) **
==============================================================
*/
DHT11 dht11(7);
int grindMode = 0;
bool greenButtonWasPressed = false;
const float versionNumber = 32.7;
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;
unsigned long lastCaterpillarMove = 0;
unsigned long lastCaterpillarRun = 0;
const unsigned long caterpillarMoveInterval = 120;
const unsigned long caterpillarRunInterval = 5500;
bool caterpillarActive = false;
int caterpillarPos = 0;
// Main screen temperature refresh
unsigned long lastTempUpdate = 0;
const unsigned long tempUpdateInterval = 500;
float aeropressGrams = 15.0;
#define descaleIntervalMemory 50
#define descaleCounterMemory 60
int descaleInterval = 100; // 0 = Off
int descaleCoffeeCount = 0;
#define apMemory 40
#define LCD_COLS 20
#define LCD_ROWS 4
#define EEPROM_ADDRESS 0
#define grindTimeMemory 10
#define gpsMemory 20
#define finalDrinkTempSave 30
bool knobWasRotatedWhilePressed = false;
LiquidCrystal_I2C lcd(i2cAddr, LCD_COLS, LCD_ROWS);
#define clk A0
#define dt A1
#define sw 11
Versatile_RotaryEncoder *versatile_encoder;
void createProgressChars() {
byte block1[8] = {
B10000,
B10000,
B10000,
B10000,
B10000,
B10000,
B10000,
B10000
};
byte block2[8] = {
B11000,
B11000,
B11000,
B11000,
B11000,
B11000,
B11000,
B11000
};
byte block3[8] = {
B11100,
B11100,
B11100,
B11100,
B11100,
B11100,
B11100,
B11100
};
byte block4[8] = {
B11110,
B11110,
B11110,
B11110,
B11110,
B11110,
B11110,
B11110
};
lcd.createChar(1, block1);
lcd.createChar(2, block2);
lcd.createChar(3, block3);
lcd.createChar(4, block4);
byte copyrightChar[8] = {
B01110,
B10001,
B10101,
B10111,
B10101,
B10001,
B01110,
B00000
};
lcd.createChar(0, copyrightChar);
}
void setup() {
Wire.begin();
Wire.setClock(400000L);
randomSeed(analogRead(A2));
EEPROM.get(apMemory, aeropressGrams);
if (isnan(aeropressGrams) || aeropressGrams < 1.0 || aeropressGrams > 50.0) {
aeropressGrams = 15.0;
EEPROM.put(apMemory, aeropressGrams);
}
// safety default
if (aeropressGrams <= 0 || aeropressGrams > 50) {
aeropressGrams = 15.0;
}
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(20,4);
lcd.backlight();
lcd.setBacklight(255);
createProgressChars();
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);
pinMode(settingsButton, INPUT_PULLUP);
EEPROM.get(EEPROM_ADDRESS, totalGramsStored);
EEPROM.get(grindTimeMemory, grinding);
EEPROM.get(gpsMemory, gramsPerSecond);
EEPROM.get(finalDrinkTempSave, stopMilkC);
EEPROM.get(descaleIntervalMemory, descaleInterval);
EEPROM.get(descaleCounterMemory, descaleCoffeeCount);
//=============
if (isnan(totalGramsStored) || totalGramsStored < 0 || totalGramsStored > 1000000) {
totalGramsStored = 0.0;
EEPROM.put(EEPROM_ADDRESS, totalGramsStored);
}
if (isnan(gramsPerSecond) || gramsPerSecond < 0 || gramsPerSecond > 20.0) {
gramsPerSecond = 0.0;
EEPROM.put(gpsMemory, gramsPerSecond);
}
if (isnan(grinding) || grinding < 0 || grinding > 120.0) {
grinding = 0.0;
EEPROM.put(grindTimeMemory, grinding);
}
//=============
if (descaleInterval != 0 &&
descaleInterval != 5 &&
(descaleInterval < 100 || descaleInterval > 500 || descaleInterval % 50 != 0)) {
descaleInterval = 100;
EEPROM.put(descaleIntervalMemory, descaleInterval);
}
if (descaleCoffeeCount < 0 || descaleCoffeeCount > 10000) {
descaleCoffeeCount = 0;
EEPROM.put(descaleCounterMemory, descaleCoffeeCount);
}
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();
// --- LINE 1 ---
lcd.setCursor(2, 1);
lcd.write(byte(0));
lcd.print(" ElectroKev.com");
// --- LINE 3 ---
lcd.setCursor(0, 3);
//lcd.print(temperature);
//lcd.print("C");
lcd.setCursor(8, 3);
lcd.print("v");
lcd.print(versionNumber, 1);
lcd.setCursor(17, 3);
//lcd.print(humidity);
//lcd.print("%");
delay(3000);
// --- SECOND SCREEN ---
lcd.clear();
lcd.setCursor(7, 0);
lcd.print("Totals:");
// -------- GROUNDS --------
lcd.setCursor(0, 1);
lcd.print("Grounds:");
char buffer1[10];
sprintf(buffer1, "%ld g", (long)totalGramsStored);
lcd.setCursor(20 - strlen(buffer1), 1);
lcd.print(buffer1);
// -------- DESCALE --------
lcd.setCursor(0, 2);
if (descaleInterval == 0) {
char offText[] = "Off";
lcd.print("Descale:");
lcd.setCursor(20 - strlen(offText), 2);
lcd.print(offText);
} else {
int coffeesRemaining = descaleInterval - descaleCoffeeCount;
if (coffeesRemaining < 0) coffeesRemaining = 0;
lcd.print("Until Descale:");
char buffer2[6];
sprintf(buffer2, "%d", coffeesRemaining);
lcd.setCursor(20 - strlen(buffer2), 2);
lcd.print(buffer2);
}
// -------- TOTAL COFFEES --------
lcd.setCursor(0, 3);
lcd.print("Total Coffees:");
int total18Coffee = totalGramsStored / 18;
char buffer3[6];
sprintf(buffer3, "%d", total18Coffee);
lcd.setCursor(20 - strlen(buffer3), 3);
lcd.print(buffer3);
delay(5000);
updateScreen();
}
void loop() {
if (versatile_encoder->ReadEncoder()) {
if (grindMode == 0) {
updateGramsOnly();
} else {
//updateScreen();
}
}
if (millis() - lastTempUpdate >= tempUpdateInterval) {
lastTempUpdate = millis();
updateMainTempOnly();
}
updateSleepingDots();
if (digitalRead(buttonPin) == LOW && !greenButtonWasPressed) {
greenButtonWasPressed = true;
greenButtonPressed();
}
if (digitalRead(buttonPin) == HIGH) {
greenButtonWasPressed = false;
}
if (digitalRead(clearEEPROMButtonPin) == LOW) {
masterReset();
}
if (digitalRead(instantGrindButtonPin) == LOW) {
oneGramGrind();
}
if (digitalRead(settingsButton) == LOW) {
settingsMenu();
}
//updateModeIndicator();
}
void updateGramsOnly() {
lcd.setCursor(7, 0);
lcd.print(" "); // clears old value only
lcd.setCursor(7, 0);
lcd.print(totalGrams, 1);
lcd.print(" g");
}
void updateSleepingDots() {
if (grindMode != 0) {
return;
}
unsigned long now = millis();
const unsigned long frameInterval = 250;
const unsigned long runInterval = 4500;
static bool active = false;
static int frame = 0;
static unsigned long lastFrame = 0;
static unsigned long lastRun = 0;
// Shifted right by 1
const int positions[7] = {8, 9, 10, 11, 10, 9, 8};
if (!active && now - lastRun >= runInterval) {
active = true;
frame = 0;
lastRun = now;
lastFrame = now;
// Clear the area used
lcd.setCursor(8, 1);
lcd.print(" ");
}
if (!active) {
return;
}
if (now - lastFrame < frameInterval) {
return;
}
lastFrame = now;
// Clear previous
lcd.setCursor(8, 1);
lcd.print(" ");
// Draw dot
lcd.setCursor(positions[frame], 1);
lcd.print(".");
frame++;
if (frame >= 7) {
active = false;
lcd.setCursor(8, 1);
lcd.print(" ");
}
}
void setAeropressGrams() {
lcd.clear();
while (digitalRead(settingsButton) == LOW) {
delay(10);
}
float apValue = aeropressGrams;
float lastDisplayed = -1;
static int lastClkState = HIGH;
unsigned long lastTurnTime = 0;
lcd.setCursor(1, 0);
lcd.print("Set Aeropress Dose");
lcd.setCursor(2, 3);
lcd.print("<Press to save>");
while (digitalRead(sw) == HIGH) {
// --- WHITE BUTTON CANCEL ---
if (digitalRead(instantGrindButtonPin) == LOW) {
lcd.clear();
lcd.setCursor(7, 1);
lcd.print("Cancel");
delay(800);
while (digitalRead(instantGrindButtonPin) == LOW) {
delay(10);
}
updateScreen();
return;
}
int clkState = digitalRead(clk);
if (clkState != lastClkState && clkState == LOW) {
unsigned long now = millis();
unsigned long turnGap = now - lastTurnTime;
lastTurnTime = now;
float stepSize = 0.1;
if (turnGap < 80) {
stepSize = 1.0;
} else if (turnGap < 180) {
stepSize = 0.5;
}
if (digitalRead(dt) != clkState) {
apValue += stepSize;
} else {
apValue -= stepSize;
}
if (apValue < 1.0) apValue = 1.0;
if (apValue > 50.0) apValue = 50.0;
delay(40);
}
lastClkState = clkState;
if (apValue != lastDisplayed) {
lcd.setCursor(4, 2);
lcd.print("Dose: ");
lcd.setCursor(10, 2);
lcd.print(apValue, 1);
lcd.print(" g");
lastDisplayed = apValue;
}
}
// Save
aeropressGrams = apValue;
EEPROM.put(apMemory, aeropressGrams);
lcd.clear();
lcd.setCursor(8, 1);
lcd.print("Saved");
lcd.setCursor(7, 2);
lcd.print(aeropressGrams, 1);
lcd.print(" g");
delay(1200);
while (digitalRead(sw) == LOW) {
delay(10);
}
updateScreen();
}
void settingsMenu() {
lcd.clear();
while (digitalRead(settingsButton) == LOW) {
delay(10);
}
const int menuCount = 5;
const char* menuItems[menuCount] = {
"Calibration",
"Edit Total Grams",
"Set Aeropress Dose",
"Descale Reminder",
"Cancel"
};
int menuItem = 0;
int topItem = 0;
int lastMenuItem = -1;
int lastTopItem = -1;
static int lastClkState = HIGH;
while (digitalRead(sw) == HIGH) {
if (digitalRead(instantGrindButtonPin) == LOW) {
lcd.clear();
lcd.setCursor(7, 1);
lcd.print("Cancel");
delay(700);
while (digitalRead(instantGrindButtonPin) == LOW) {
delay(10);
}
updateScreen();
return;
}
int clkState = digitalRead(clk);
if (clkState != lastClkState && clkState == LOW) {
if (digitalRead(dt) != clkState) {
menuItem++;
} else {
menuItem--;
}
if (menuItem >= menuCount) menuItem = 0;
if (menuItem < 0) menuItem = menuCount - 1;
if (menuItem < topItem) {
topItem = menuItem;
}
if (menuItem > topItem + 2) {
topItem = menuItem - 2;
}
if (menuItem == 0) {
topItem = 0;
}
delay(120);
}
lastClkState = clkState;
if (menuItem != lastMenuItem || topItem != lastTopItem) {
lcd.clear();
lcd.setCursor(6, 0);
lcd.print("Settings");
for (int i = 0; i < 3; i++) {
int itemIndex = topItem + i;
if (itemIndex < menuCount) {
lcd.setCursor(0, i + 1);
if (itemIndex == menuItem) {
lcd.print(">");
} else {
lcd.print(" ");
}
lcd.print(menuItems[itemIndex]);
}
}
lastMenuItem = menuItem;
lastTopItem = topItem;
}
}
while (digitalRead(sw) == LOW) {
delay(10);
}
if (menuItem == 0) {
calibrationMode();
}
if (menuItem == 1) {
editTotalGrams();
}
if (menuItem == 2) {
setAeropressGrams();
}
if (menuItem == 3) {
setDescaleReminder();
}
if (menuItem == 4) {
updateScreen();
}
}
void setDescaleReminder() {
lcd.clear();
while (digitalRead(settingsButton) == LOW) {
delay(10);
}
int tempInterval = descaleInterval;
int lastDisplayed = -1;
static int lastClkState = HIGH;
lcd.setCursor(1, 0);
lcd.print("Descale Reminder");
lcd.setCursor(0, 3);
lcd.print("<Press knob to save>");
while (digitalRead(sw) == HIGH) {
if (digitalRead(instantGrindButtonPin) == LOW) {
lcd.clear();
lcd.setCursor(7, 1);
lcd.print("Cancel");
delay(800);
while (digitalRead(instantGrindButtonPin) == LOW) {
delay(10);
}
updateScreen();
return;
}
int clkState = digitalRead(clk);
if (clkState != lastClkState && clkState == LOW) {
if (digitalRead(dt) != clkState) {
// Rotate forward
if (tempInterval == 0) {
tempInterval = 5;
} else if (tempInterval == 5) {
tempInterval = 100;
} else {
tempInterval += 50;
}
} else {
// Rotate backward
if (tempInterval == 100) {
tempInterval = 5;
} else if (tempInterval == 5) {
tempInterval = 0;
} else if (tempInterval > 100) {
tempInterval -= 50;
}
}
if (tempInterval > 500) {
tempInterval = 500;
}
delay(120);
}
lastClkState = clkState;
if (tempInterval != lastDisplayed) {
lcd.setCursor(1, 1);
lcd.print("Every: ");
lcd.setCursor(7, 1);
if (tempInterval == 0) {
lcd.print("Off ");
} else {
lcd.print(tempInterval);
lcd.print(" ");
lcd.setCursor(10,1);
lcd.print(" coffees");
}
lastDisplayed = tempInterval;
}
}
descaleInterval = tempInterval;
EEPROM.put(descaleIntervalMemory, descaleInterval);
lcd.clear();
lcd.setCursor(2, 0);
lcd.print("Descale Interval");
lcd.setCursor(7,1);
lcd.print("Saved");
if (descaleInterval == 0) {
lcd.setCursor(6,2);
lcd.print("* Off *");
} else {
lcd.setCursor(4, 2);
lcd.print(descaleInterval);
lcd.print(" coffees");
}
delay(2000);
while (digitalRead(sw) == LOW) {
delay(10);
}
updateScreen();
}
void addCoffeeToDescaleCounter() {
if (descaleInterval == 0) {
return;
}
descaleCoffeeCount++;
EEPROM.put(descaleCounterMemory, descaleCoffeeCount);
if (descaleCoffeeCount >= descaleInterval) {
showDescaleReminder();
}
}
void showDescaleReminder() {
lcd.clear();
lcd.setCursor(2, 0);
lcd.print("Descale Reminder");
lcd.setCursor(10, 1);
lcd.print(descaleCoffeeCount);
lcd.setCursor(2,2);
lcd.print("Coffees Reached");
lcd.setCursor(2, 3);
lcd.print("<Press to reset>");
while (digitalRead(sw) == HIGH) {
delay(10);
}
while (digitalRead(sw) == LOW) {
delay(10);
}
descaleCoffeeCount = 0;
EEPROM.put(descaleCounterMemory, descaleCoffeeCount);
lcd.clear();
lcd.setCursor(8, 1);
lcd.print("Reset");
lcd.setCursor(1,3);
lcd.print("<Press to Continue>");
delay(1000);
}
void calibrationMode() {
lcd.clear();
int calibrationSeconds = 10;
int lastDisplayedSeconds = -1;
static int lastClkState = HIGH;
unsigned long lastTurnTime = 0;
lcd.setCursor(2, 0);
lcd.print("Calibration Time");
lcd.setCursor(0, 3);
lcd.print("<Press knob to set>");
// --- SELECT CALIBRATION DURATION ---
while (digitalRead(sw) == HIGH) {
if (digitalRead(instantGrindButtonPin) == LOW) {
lcd.clear();
lcd.setCursor(7, 1);
lcd.print("Cancel");
delay(800);
while (digitalRead(instantGrindButtonPin) == LOW) {
delay(10);
}
updateScreen();
return;
}
int clkState = digitalRead(clk);
if (clkState != lastClkState && clkState == LOW) {
unsigned long now = millis();
unsigned long turnGap = now - lastTurnTime;
lastTurnTime = now;
int stepSize = 1;
if (turnGap < 80) {
stepSize = 10;
} else if (turnGap < 180) {
stepSize = 5;
}
if (digitalRead(dt) != clkState) {
calibrationSeconds += stepSize;
} else {
calibrationSeconds -= stepSize;
}
if (calibrationSeconds < 1) calibrationSeconds = 1;
if (calibrationSeconds > 60) calibrationSeconds = 60;
delay(40);
}
lastClkState = clkState;
if (calibrationSeconds != lastDisplayedSeconds) {
lcd.setCursor(4, 2);
lcd.print("Time: ");
lcd.setCursor(10, 2);
lcd.print(calibrationSeconds);
lcd.print(" sec");
lastDisplayedSeconds = calibrationSeconds;
}
}
while (digitalRead(sw) == LOW) {
delay(10);
}
lcd.clear();
lcd.setCursor(5, 0);
lcd.print("Calibration");
lcd.setCursor(2, 1);
lcd.print(calibrationSeconds);
lcd.print(" sec test grind");
lcd.setCursor(1, 3);
lcd.print("<Press Knob Start>");
while (digitalRead(sw) == HIGH) {
if (digitalRead(instantGrindButtonPin) == LOW) {
lcd.clear();
lcd.setCursor(7, 1);
lcd.print("Cancel");
delay(800);
while (digitalRead(instantGrindButtonPin) == LOW) {
delay(10);
}
updateScreen();
return;
}
delay(10);
}
while (digitalRead(sw) == LOW) {
delay(10);
}
lcd.clear();
lcd.setCursor(4, 1);
lcd.print("Preparing...");
delay(1500);
lcd.clear();
digitalWrite(ledPin, HIGH);
digitalWrite(relayPin, HIGH);
for (int i = calibrationSeconds; i > 0; i--) {
lcd.setCursor(0, 1);
lcd.print("Starting Calibration");
lcd.setCursor(7, 2);
lcd.print(" ");
lcd.setCursor(7, 2);
lcd.print(i);
lcd.print(" sec");
delay(1000);
}
digitalWrite(ledPin, LOW);
digitalWrite(relayPin, LOW);
float measuredGrams = calibrationSeconds * gramsPerSecond;
if (measuredGrams <= 0 || isnan(measuredGrams)) {
measuredGrams = 10.0;
}
float lastDisplayed = -1;
lastTurnTime = 0;
lcd.clear();
lcd.setCursor(4, 0);
lcd.print("Enter Weight:");
lcd.setCursor(0, 3);
lcd.print("<Press knob to save>");
while (digitalRead(sw) == HIGH) {
if (digitalRead(instantGrindButtonPin) == LOW) {
lcd.clear();
lcd.setCursor(7, 1);
lcd.print("Cancel");
delay(800);
while (digitalRead(instantGrindButtonPin) == LOW) {
delay(10);
}
updateScreen();
return;
}
int clkState = digitalRead(clk);
if (clkState != lastClkState && clkState == LOW) {
unsigned long now = millis();
unsigned long turnGap = now - lastTurnTime;
lastTurnTime = now;
float stepSize = 0.1;
if (turnGap < 80) {
stepSize = 1.0;
} else if (turnGap < 180) {
stepSize = 0.5;
}
if (digitalRead(dt) != clkState) {
measuredGrams += stepSize;
} else {
measuredGrams -= stepSize;
}
if (measuredGrams < 0) {
measuredGrams = 0;
}
delay(40);
}
lastClkState = clkState;
if (measuredGrams != lastDisplayed) {
lcd.setCursor(3, 2);
lcd.print("Weight:");
lcd.setCursor(10, 2);
lcd.print(measuredGrams, 1);
lcd.print(" ");
lcd.setCursor(15,2);
lcd.print("g");
lastDisplayed = measuredGrams;
}
}
gramsPerSecond = measuredGrams / calibrationSeconds;
EEPROM.put(gpsMemory, gramsPerSecond);
lcd.clear();
lcd.setCursor(1, 1);
lcd.print("Calibration Saved");
lcd.setCursor(6, 2);
lcd.print(gramsPerSecond, 2);
lcd.print(" g/s");
delay(2000);
while (digitalRead(sw) == LOW) {
delay(10);
}
updateScreen();
}
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;
lcd.setCursor(2, 0);
lcd.print("Set Total Grams:");
lcd.setCursor(0, 3);
lcd.print("<Press knob to save>");
while (digitalRead(sw) == HIGH) {
// White button cancels without saving
if (digitalRead(instantGrindButtonPin) == LOW) {
lcd.clear();
lcd.setCursor(7, 1);
lcd.print("Cancel");
delay(800);
while (digitalRead(instantGrindButtonPin) == LOW) {
delay(10);
}
updateScreen();
return;
}
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;
if (editedTotal != lastDisplayed) {
lcd.setCursor(3, 2);
lcd.print("Total: ");
lcd.setCursor(10, 2);
lcd.print(editedTotal, 0);
lcd.print(" g");
lastDisplayed = editedTotal;
}
}
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);
while (digitalRead(sw) == LOW) {
delay(10);
}
updateScreen();
}
void updateModeIndicator() {
lcd.setCursor(11, 3);
lcd.print("Mode:");
lcd.setCursor(18,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();
// Calculate grams from current settings
totalGrams = gramsPerSecond * grinding;
if (grindMode == 1) {
// --- AEROPRESS MODE ---
lcd.setCursor(0,0);
lcd.print(" ");
lcd.setCursor(0,1);
lcd.print("Aeropress ");
lcd.print(aeropressGrams, 0);
lcd.print("g ");
} else if (grindMode == 2) {
// --- FREE RUN MODE ---
lcd.setCursor(0,0);
lcd.print(" ");
lcd.setCursor(0,1);
lcd.print("Free Run ");
} else {
// --- TIMED GRIND MODE (NOW GRAMS BASED) ---
lcd.setCursor(0, 0);
lcd.print("Grams: ");
//lcd.setCursor(7, 1);
lcd.print(totalGrams, 1);
lcd.print(" g");
}
// Always show calibration (g/s)
lcd.setCursor(11,2);
lcd.print("g/s: ");
lcd.setCursor(16,2);
lcd.print(gramsPerSecond, 2);
// Bottom right prompt
lcd.setCursor(16, 0);
lcd.print("<GO>");
// Mode indicator (TG / AP / FR)
updateModeIndicator();
// Update speed tracking for fast scroll
grindSpeed = millis();
}
void handleRotate(int8_t rotation) {
// Only allow adjustment in Timed Grind mode
if (grindMode != 0) {
return;
}
// Do nothing if calibration is not set
if (gramsPerSecond <= 0) {
return;
}
static unsigned long lastRotateTime = 0;
unsigned long now = millis();
unsigned long turnGap = now - lastRotateTime;
lastRotateTime = now;
// Current grams required
totalGrams = gramsPerSecond * grinding;
float stepSize = 0.1;
if (turnGap < 60) {
stepSize = 2.5;
}
else if (turnGap < 120) {
stepSize = 1.0;
}
else if (turnGap < 220) {
stepSize = 0.5;
}
else {
stepSize = 0.1;
}
if (rotation > 0) {
totalGrams -= stepSize;
}
else {
totalGrams += stepSize;
}
// Limit range
if (totalGrams < 0.1) {
totalGrams = 0.1;
}
if (totalGrams > 100.0) {
totalGrams = 100.0;
}
// Convert grams back into grind time
grinding = totalGrams / gramsPerSecond;
}
void handleLongPress() {
//startGrinding();
}
void handlePressRotate(int8_t rotation) {
knobWasRotatedWhilePressed = true;
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;
}
}
updateGpsOnly();
}
void handlePressRelease() {
if (!knobWasRotatedWhilePressed) {
if (gramsPerSecond <= 0 && grindMode != 2) {
lcd.clear();
lcd.setCursor(3, 1);
lcd.print("Calibrate First");
delay(2000);
updateScreen();
return;
}
confirmStartGrinding();
}
}
void handleLongPressRelease() {
}
void handlePress() {
knobWasRotatedWhilePressed = false;
}
void handlePressRotateRelease() {
EEPROM.put(gpsMemory, gramsPerSecond);
calibrateScreen();
knobWasRotatedWhilePressed = false;
updateScreen();
}
void updateGpsOnly() {
lcd.setCursor(16, 2);
lcd.print(" ");
lcd.setCursor(16, 2);
lcd.print(gramsPerSecond, 2);
}
void saveSettings() {
EEPROM.put(grindTimeMemory, grinding);
}
void confirmStartGrinding() {
lcd.clear();
float targetGrams = gramsPerSecond * grinding;
// Only save the timed grind setting in Timed Grind mode
if (grindMode == 0) {
saveSettings();
}
lcd.setCursor(0, 0);
lcd.print("Start Grind?");
lcd.setCursor(0, 1);
if (grindMode == 1) {
lcd.print("Aeropress Mode");
}
else if (grindMode == 2) {
lcd.print("Free Run Mode");
}
else {
lcd.setCursor(13, 0);
lcd.print(targetGrams, 1);
lcd.print(" g");
}
lcd.setCursor(1, 2);
lcd.print("<Click to Start>");
lcd.setCursor(1, 3);
lcd.print("<White to Cancel>");
// Wait for knob release after initial press
while (digitalRead(sw) == LOW) {
delay(10);
}
while (true) {
// Confirm with knob press
if (digitalRead(sw) == LOW) {
while (digitalRead(sw) == LOW) {
delay(10);
}
lcd.clear();
startGrinding();
return;
}
// Cancel with white button
if (digitalRead(instantGrindButtonPin) == LOW) {
lcd.clear();
lcd.setCursor(7, 1);
lcd.print("Cancel");
delay(700);
while (digitalRead(instantGrindButtonPin) == LOW) {
delay(10);
}
updateScreen();
return;
}
delay(10);
}
}
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(8, 2);
lcd.print(aeropressGrams, 1);
lcd.print(" g");
}
if (grindMode == 2) {
lcd.setCursor(4, 1);
lcd.print("Free Run Mode");
lcd.setCursor(1, 2);
lcd.print("<Click to start>");
}
delay(1000);
updateScreen();
}
void calibrateScreen() {
lcd.clear();
lcd.setCursor(0,1);
lcd.print("Manual Calibration");
lcd.setCursor(0,2);
lcd.print("Saved: ");
lcd.print(gramsPerSecond * 10 / 10,2);
lcd.print(" g/s");
delay(2000);
}
void oneGramGrind() {
if (gramsPerSecond <= 0) {
lcd.clear();
lcd.setCursor(3, 1);
lcd.print("Calibrate First");
delay(1200);
updateScreen();
return;
}
float targetGrams = 1.0;
unsigned long grindDuration = (targetGrams / gramsPerSecond) * 1000;
lcd.clear();
lcd.setCursor(3, 1);
lcd.print("Dispensing 1g");
// Wait for white button release before starting
while (digitalRead(instantGrindButtonPin) == LOW) {
delay(10);
}
digitalWrite(ledPin, HIGH);
digitalWrite(relayPin, HIGH);
unsigned long startTime = millis();
while (millis() - startTime < grindDuration) {
// just wait
}
digitalWrite(ledPin, LOW);
digitalWrite(relayPin, LOW);
totalGramsStored += 1.0;
EEPROM.put(EEPROM_ADDRESS, totalGramsStored);
lcd.clear();
lcd.setCursor(6, 1);
lcd.print("1g Added");
delay(800);
updateScreen();
}
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(6, 2);
lcd.print("1 Second");
delay(1000);
digitalWrite(ledPin, LOW);
digitalWrite(relayPin, LOW);
}
else {
pauseButtonStartTime = millis();
}
updateScreen();
}
void startGrinding() {
if (!countdownStarted) {
countdownStarted = true;
// --- STARTING DELAY ---
lcd.clear();
lcd.setCursor(4, 1);
lcd.print("Preparing...");
delay(1500);
bool tempAPMode = (grindMode == 1);
bool freeRunMode = (grindMode == 2);
float originalGrinding = grinding;
float targetGrams = gramsPerSecond * grinding;
// --- AEROPRESS MODE ---
if (tempAPMode && gramsPerSecond > 0) {
grinding = aeropressGrams / gramsPerSecond;
targetGrams = aeropressGrams;
lcd.clear();
lcd.setCursor(1, 1);
lcd.print("Aeropress ");
lcd.print(aeropressGrams, 1);
lcd.print("g");
delay(1000);
}
// --- FREE RUN MODE ---
if (freeRunMode) {
lcd.clear();
lcd.setCursor(4, 0);
lcd.print("FREE RUNNING");
lcd.setCursor(1, 3);
lcd.print("Press knob to stop");
while (digitalRead(sw) == LOW) {
delay(10);
}
digitalWrite(ledPin, HIGH);
digitalWrite(relayPin, HIGH);
unsigned long startTime = millis();
unsigned long lastDisplayUpdate = 0;
// Chasing bar variables
int chasePosition = 0;
bool chaseFilled = true;
unsigned long lastChaseUpdate = 0;
float gramsDispensed = 0;
while (digitalRead(sw) == HIGH) {
unsigned long elapsed = millis() - startTime;
float secondsElapsed = elapsed / 1000.0;
gramsDispensed = gramsPerSecond * secondsElapsed;
// --- GRAMS DISPLAY ---
if (millis() - lastDisplayUpdate > 120) {
lastDisplayUpdate = millis();
lcd.setCursor(2, 1);
lcd.print("Output:");
lcd.setCursor(10, 1);
lcd.print(gramsDispensed, 1);
lcd.print(" g");
}
// --- CHASING BAR ---
if (millis() - lastChaseUpdate > 80) {
lastChaseUpdate = millis();
lcd.setCursor(0, 2);
for (int i = 0; i < 20; i++) {
if (chaseFilled) {
if (i <= chasePosition) {
lcd.print((char)255);
} else {
lcd.print(" ");
}
} else {
if (i <= chasePosition) {
lcd.print(" ");
} else {
lcd.print((char)255);
}
}
}
chasePosition++;
if (chasePosition >= 20) {
chasePosition = 0;
chaseFilled = !chaseFilled;
}
}
delay(10);
}
digitalWrite(ledPin, LOW);
digitalWrite(relayPin, LOW);
unsigned long runTime = millis() - startTime;
float secondsRun = runTime / 1000.0;
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;
addCoffeeToDescaleCounter();
updateScreen();
return;
}
// --- TIMED GRIND (UNCHANGED) ---
digitalWrite(ledPin, HIGH);
digitalWrite(relayPin, HIGH);
lcd.clear();
unsigned long grindStartTime = millis();
unsigned long grindDuration = grinding * 1000;
unsigned long lastDisplayUpdate = 0;
while (millis() - grindStartTime < grindDuration) {
unsigned long elapsed = millis() - grindStartTime;
float secondsElapsed = elapsed / 1000.0;
float gramsElapsed = gramsPerSecond * secondsElapsed;
float gramsRemaining = targetGrams - gramsElapsed;
if (gramsRemaining < 0.05) {
gramsRemaining = 0;
}
if (millis() - lastDisplayUpdate >= 120) {
lastDisplayUpdate = millis();
lcd.setCursor(3, 1);
lcd.print("Grinding ");
lcd.print(gramsRemaining, 1);
lcd.print(" g ");
float progress = secondsElapsed / grinding;
if (progress < 0) progress = 0;
if (progress > 1) progress = 1;
int totalPixels = progress * 100;
int fullBlocks = totalPixels / 5;
int partialBlock = totalPixels % 5;
lcd.setCursor(0, 2);
for (int b = 0; b < 20; b++) {
if (b < fullBlocks) {
lcd.print((char)255);
}
else if (b == fullBlocks && partialBlock > 0) {
lcd.write(byte(partialBlock));
}
else {
lcd.print(" ");
}
}
lcd.setCursor(1, 3);
lcd.print("<White to Cancel>");
}
if (digitalRead(instantGrindButtonPin) == LOW) {
digitalWrite(ledPin, LOW);
digitalWrite(relayPin, LOW);
lcd.clear();
lcd.setCursor(3, 1);
lcd.print("Grind Cancelled");
delay(2000);
while (digitalRead(instantGrindButtonPin) == LOW) {
delay(10);
}
countdownStarted = false;
isPaused = false;
grinding = originalGrinding;
totalGrams = gramsPerSecond * grinding;
updateScreen();
return;
}
}
digitalWrite(ledPin, LOW);
digitalWrite(relayPin, LOW);
if (tempAPMode) {
totalGramsStored += aeropressGrams;
} else {
totalGramsStored += totalGrams;
}
EEPROM.put(EEPROM_ADDRESS, totalGramsStored);
lcd.clear();
lcd.setCursor(2, 0);
lcd.print("Grams Dispensed");
lcd.setCursor(7, 1);
if (tempAPMode) {
lcd.print(aeropressGrams, 1);
} else {
lcd.print(totalGrams, 1);
}
lcd.print(" g");
lcd.setCursor(5, 2);
lcd.print("in ");
lcd.print(grinding, 1);
lcd.print(" sec");
lcd.setCursor(2, 3);
lcd.print("<Return to Home>");
grinding = originalGrinding;
totalGrams = gramsPerSecond * grinding;
countdownStarted = false;
isPaused = false;
addCoffeeToDescaleCounter();
}
while (digitalRead(sw) == HIGH) {
}
while (digitalRead(sw) == LOW) {
delay(10);
}
updateScreen();
}