Colin V32.9 Code (2026)

#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.9 Final 
Release Notes:

V32.9===========================================================
NEW: Last grind value stored
NEW: Quick Caibration menu option for last saved grind
NEW: Calibration error margin shown as a percentage
Aeropress/V60 mode no longer counts towards descale count
Aeropress Mode Changed to Aeropress/V60 Mode
Descale interval can now be configured +/- resetting the counter. 
Grinding screen modified to reflect 'Remaining Grams'
DHT11 Library removed so RAM not exceeded/improve stability
Temp sensor algorithn calibrated for accuracy in steam mode to 140c
Calibration Bug fixed between grind modes


V32.7===========================================================
- 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.9;
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

float lastGrindGrams = 0.0;
float lastGrindSeconds = 0.0;
bool hasLastGrind = false;

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(2, 0);
  lcd.print("Set AP/V60 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 = 6;

const char* menuItems[menuCount] = {
  "Calibration",
  "Quick Calibration",
  "Edit Total Grams",
  "Set AP/V60 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) {
  quickCalibrationMode();
}

if (menuItem == 2) {
  editTotalGrams();
}

if (menuItem == 3) {
  setAeropressGrams();
}

if (menuItem == 4) {
  setDescaleReminder();
}

if (menuItem == 5) {
  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) {
        if (tempInterval == 0) {
          tempInterval = 5;
        } else if (tempInterval == 5) {
          tempInterval = 100;
        } else {
          tempInterval += 50;
        }
      } else {
        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(8, 1);

      if (tempInterval == 0) {
        lcd.print("Off");
      } else {
        lcd.print(tempInterval);
        lcd.print(" coffees");
      }

      lastDisplayed = tempInterval;
    }
  }

  while (digitalRead(sw) == LOW) {
    delay(10);
  }

  // Save interval first
  descaleInterval = tempInterval;
  EEPROM.put(descaleIntervalMemory, descaleInterval);

  // Ask whether to reset counter
  bool resetCounter = false;
  bool lastResetDisplayed = !resetCounter;

  lcd.clear();
  lcd.setCursor(2, 0);
  lcd.print("Reset Counter?");

  lcd.setCursor(0, 3);
  lcd.print("<Press knob save>");

  lastClkState = HIGH;

  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) {
      resetCounter = !resetCounter;
      delay(160);
    }

    lastClkState = clkState;

    if (resetCounter != lastResetDisplayed) {
      lcd.setCursor(4, 2);
      lcd.print("Choice:       ");

      lcd.setCursor(12, 2);
      if (resetCounter) {
        lcd.print("Yes");
      } else {
        lcd.print("No ");
      }

      lastResetDisplayed = resetCounter;
    }
  }

  while (digitalRead(sw) == LOW) {
    delay(10);
  }

  if (resetCounter) {
    descaleCoffeeCount = 0;
    EEPROM.put(descaleCounterMemory, descaleCoffeeCount);
  }

  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");
  }

  if (resetCounter) {
    lcd.setCursor(3, 3);
    lcd.print("Counter Reset");
  } else {
    lcd.setCursor(3, 3);
    lcd.print("Counter Kept");
  }

  delay(2000);

  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 quickCalibrationMode() {
  lcd.clear();

  if (!hasLastGrind || lastGrindSeconds <= 0) {
    lcd.setCursor(2, 1);
    lcd.print("No Last Grind");
    lcd.setCursor(1, 2);
    lcd.print("Do a grind first");
    delay(1800);
    updateScreen();
    return;
  }

  float expectedGrams = lastGrindGrams;
  float actualGrams = expectedGrams;
  float lastDisplayed = -1;

  static int lastClkState = HIGH;
  unsigned long lastTurnTime = 0;

  lcd.setCursor(1, 0);
  lcd.print("Quick Calibration");

  lcd.setCursor(0, 1);
  lcd.print("Last:");
  lcd.setCursor(6, 1);
  lcd.print(expectedGrams, 1);
  lcd.print("g");

  lcd.setCursor(12, 1);
  lcd.print("in ");
  lcd.print(lastGrindSeconds, 1);
  lcd.print("s");

  lcd.setCursor(0, 3);
  lcd.print("<Press knob 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) {
        actualGrams += stepSize;
      } else {
        actualGrams -= stepSize;
      }

      if (actualGrams < 0.1) {
        actualGrams = 0.1;
      }

      delay(40);
    }

    lastClkState = clkState;

    if (actualGrams != lastDisplayed) {
      lcd.setCursor(0, 2);
      lcd.print("Actual:             ");
      lcd.setCursor(8, 2);
      lcd.print(actualGrams, 1);
      lcd.print(" g");

      lastDisplayed = actualGrams;
    }
  }

  // --- CALCULATION ---
  float errorPercent = 0;
  if (expectedGrams > 0.01) {
    errorPercent = ((actualGrams - expectedGrams) / expectedGrams) * 100.0;
  }

  gramsPerSecond = actualGrams / lastGrindSeconds;
  EEPROM.put(gpsMemory, gramsPerSecond);

  // --- RESULT SCREEN ---
  lcd.clear();

  lcd.setCursor(1, 0);
  lcd.print("Calibration Saved");

  lcd.setCursor(6, 1);
  lcd.print(gramsPerSecond, 2);
  lcd.print(" g/s");

  lcd.setCursor(4, 2);
  lcd.print("Error: ");
  if (errorPercent >= 0) {
    lcd.print("+");
  }
  lcd.print(errorPercent, 1);
  lcd.print("%");

  lcd.setCursor(4, 3);
  lcd.print("<Press Knob>");

  // --- WAIT FOR RELEASE (from save press) ---
  while (digitalRead(sw) == LOW) {
    delay(10);
  }

  // --- WAIT FOR NEW PRESS ---
  while (digitalRead(sw) == HIGH) {
    delay(10);
  }

  // --- WAIT FOR RELEASE AGAIN ---
  while (digitalRead(sw) == LOW) {
    delay(10);
  }

  updateScreen();
}

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;

  if (calculatedTemp > 100.0) {
    calculatedTemp = 100.0 + ((calculatedTemp - 100.0) * 1.4);
  }

  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/V60 ");

  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/V60 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(1, 1);
  lcd.print("Aeropress/V60 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/V60 ");
      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(5,0);
lcd.print("Grinding:");
lcd.setCursor(2, 1);
lcd.print("Remaining ");
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;
/*
if (tempAPMode) {
  lastGrindGrams = aeropressGrams;
} else {
  lastGrindGrams = totalGrams;
}

lastGrindSeconds = grinding;
hasLastGrind = true;*/

if (tempAPMode) {
  lastGrindGrams = aeropressGrams;
  lastGrindSeconds = aeropressGrams / gramsPerSecond;
} else {
  lastGrindGrams = totalGrams;
  lastGrindSeconds = grinding;
}

hasLastGrind = true;

if (!tempAPMode) {
  addCoffeeToDescaleCounter();
}
  }

  while (digitalRead(sw) == HIGH) {
  }

  while (digitalRead(sw) == LOW) {
    delay(10);
  }

  updateScreen();
}