Coffee grinder timer

Hi folks,

My coffee grinder timer project is done and I thought I'd post some info about it.

First, some background on the motivation. When making espresso, to keep things fresh and consistent, people generally grind the coffee right before "pulling a shot," and try to use the same amount of coffee grounds every time. A good coffee grinder should output grounds at a consistent rate, so a precisely timed grind can produces a consistent quantity of ground coffee. 28 seconds works for me for most roasts I get, whether Caffe Trieste, Blue Bottle, or Graffeo.

The timer mounts to the left side of the coffee grinder with Velcro tape. The back has a power input socket and main switch, a controlled output outlet (for the grinder, turned "on"), the USB port and *duino power switch. On the front, a scroll wheel adjusts a displayed time, and two buttons can trigger a timed grind (countdown) or free run (count up).

The buttons light up when the grinder is triggered, depending on which was clicked, and the display shows a "chase" animation cycle while it counts up or down.

This started as a learning project to figure out techniques I need for a bunch of projects, in no particular order:

  • More complicated Arduino programming, esp. modes and fast execution-- loop() here runs at ~2500Hz, and the program has all the "polish" I could think of,
  • EAGLE CAD-- I know enough now to be dangerous,
  • Homemade PCB fabrication-- won't solder more than three components to perf/strip/protoboard ever again,
  • How to "case up" and really finish a project-- still not sure how, but at least I finished this one, and nobody has to know there's a breadboard inside...:slight_smile:

This uses an iDuino from Fundamental Logic, chosen for its built-in USB and breadboard factor. The iDuino can be powered from an internal DC source or from its USB port, depending on the switch on the rear.

I originally wanted to make a single board that the iDuino would slot into, but I couldn't figure out the layout, and it worked well separating the circuit into three sections: control, relay, and *duino. I put female headers on the relay and control boards, with the iDuino on a little breadboard so it will be easy to change things around later.


/* Grinder Timer V2

Replacement of my first grinder timer.
Uses a scroll wheel to select time, a 2-digit display, and buttons for triggering timed countdown
or free count up.

by Jeff Saltzman

Interface with a scroll controller from a Panasonic VCR remote and a 2-digit 7-segment LED display from a Sony DigiBeta deck.
This will become the next version of the coffee grinder timer with the addition of two buttons and an output to a relay or triac

v.1: 11/12/2008: Just starting: first task: count
second task: mess w/LED... that'll eat a lot of pins...:o
11/15/2008: Counting 0-99 on the display! Will keep code verbose for now
Got the scroller working-- simple! Adding different modes...
11/22/2008: Get anlog pin to switch multiple inputs
Build complete countDown program
Program enhancements:
Chase and time remaining display during countDown
No scroll during override
Max override time
Flash max time when max time reached
Flash ct when countDown complete
Do not show leading 0 when count time is single digits
Chase and time elapsed display during override
Lit buttons for countDown grinding versus override
Store delay in EEPROM so it will have the same count as last time
11/22/2008: Test with triac output: shuffle pins go use 13 for the trigger
11/30/2008: Fixing code thanks to suggestions in the forum:
Set multiPin's pullup resistor so the "extra" resistor is not needed per http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1226896251/3#3
12/13/2008: Major revision: use shift registers for displaying the digits
12/14/2008: Add proper third override mode: dial, count down, override (count up)
Check button lights w/logic of registers: add 128 to byte for button light
Check multi-button input: cancel mode when hitting other button
12/30/2008: Build display+button board
Refine display+button board EAGLE layout
Etch PCB, clean PCB, drill PCB
Build board
12/31/2008: Troubleshoot board and program
Infinite looping when counting down from >32s
HW Testing:
Segment 1D not lit
Chase animation incorrect
Button 2 lights?
Button 1 accidentally triggers
No button 2 trigger
1/1/2009: Build relay circuit onto outlet
Capacitor squeal-- replace
Replace M with F headers on display board
Build power circuit-- swich & reg >> BB
Print cutting guides
Cut box
Make standoffs for control board
Mount everything

  • FINAL *
    1/2/2009: GRIND ME SOME COFFEE!

All code released under
Creative Commons Attribution-Noncommercial-Share Alike 3.0

*/

#include <EEPROM.h>

// Shift register control pins
int latchPin = 8;
int clockPin = 12;
int dataPin = 11;

// Input button pins
int btn1Pin = 6;
int btn2Pin = 5;
int btnUpPin = 4;
int btnDnPin = 3;

// Button states
int btn1Val = 0; // Value of button 1
int btn2Val = 0; // Value of button 2
int upVal = 0; // Value of up scroller
int dnVal = 0; // Value of down scroller

int btn1Last = HIGH; // Last value for button 1
int btn2Last = HIGH; // Last value for button 2
int upLast = 0; // Last value for scroll up
int dnLast = 0; // Last value for scroll down

// Trigger pin
int triggerPin = 2; // Pin to trigger output relay
int triggerVal = 0; // Value of the trigger pin

// Vars for debouncing
long time1 = 0; // The last time button 1 was toggled
long time2 = 0; // The last time button 2 was toggled
long timeUp = 0; // The last time up button was toggled
long timeDn = 0; // The last time down button was toggled
long debounce = 50; // The debounce time for the buttons
long chaseTime = 0; // Time of last chase step

// Timing the grind...
long grindStartTime = 0; // The time the countDown or override started
long grindTimeMax = 60; // Longest the grinder can run
int grindTimeMin = 3; // Shortest duration for a timed grind

// Program vars
int mode = 0; // 0: dialing, 1: counting down, 2: counting up, 3: setup
long ct = 28; // Count value: the desired time
int ctRead = 0; // Count value read from the EEPROM
long longClick = 3000; // ms duration to click and hold button 1 for setup mode
int chaseDelay = 42; // ms duration of each chase segment

// Program status vars
int countDown = 0; // Countdown value
int countUp = 0; // Count up value
boolean setupMax = false; // Whether setting max time or not (not == setting min)
int chaseVal = 0; // 0-7: chase position

int i; // For keeping track of things
int j; // For keeping track of...

long freqCounter = 0; // Timing test counter
long freqTime = 0; // Last time frequency was reported

// Data for digits 0-8 to shift gfedcba: 7 BITS ONLY
//byte digs[] = {
// B0111111, B0000110, B1011011, B1001111, B1100110, B1101101, B1111101, B0000111, B1111111, B1101111 };
// old: gfedcba
// new1: cedgafb
// new1: bfagdec
byte digs1[] = {
B1110111, B1000001, B0111101, B1011101, B1001011, B1011110, B1111110, B1000101, B1111111, B1011111 };
byte digs2[] = {
B1110111, B1000001, B1011110, B1011101, B1101001, B0111101, B0111111, B1010001, B1111111, B1111101 };

// Chase pattern, 2-segments clockwise
byte ch1[] = {
B1000001, B1010000, B0010000, B0000000, B0000000, B0000000, B0000100, B0000101 };
byte ch2[] = {
B0000000, B0000000, B0000100, B0000110, B0100010, B0110000, B0100000, B0000000 };

// Data to display
byte D1; // First digit to display
byte D2; // Second digit to display

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

void setup() {

Serial.begin(9600);

// Set trigger output pin
pinMode(triggerPin, OUTPUT);
digitalWrite(triggerPin, LOW);

// Set shift register output pins
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);

// Set input buttons
pinMode(btn1Pin, INPUT);
pinMode(btn2Pin, INPUT);
pinMode(btnUpPin, INPUT);
pinMode(btnDnPin, INPUT);

// Turn on pullup resistors for the pins
digitalWrite(btn1Pin, HIGH);
digitalWrite(btn2Pin, HIGH);
digitalWrite(btnUpPin, HIGH);
digitalWrite(btnDnPin, HIGH);

// Get ct from the EEPROM
ctRead = EEPROM.read(0);
delay(100);
ct = ctRead;

}

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

void loop() {

// Frequency counter: how many times/sec does the loop run?
freqCounter += 1;
if ((millis()-freqTime) > 5000) {
Serial.print(freqCounter/5);
Serial.println(" Hz");
freqCounter = 0;
freqTime = millis();
}

// Read control button inputs
btn1Val = digitalRead(btn1Pin);
btn2Val = digitalRead(btn2Pin);
upVal = digitalRead(btnUpPin);
dnVal = digitalRead(btnDnPin);

// Debounce and process button 1: countdown OR setup
if (btn1Val == 1 && btn1Last == 0 && millis() - time1 > debounce) {
if (mode == 0) { // Button 1 switching from mode 0 (dialing) to mode 1 (timed grind)
Serial.println("B1: Mode 1: countdown");
mode = 1;
runGrinder(true);
grindStartTime = millis();
EEPROM.write(0, int(ct)); // Write ct to the EEPROM
delay(20);
}
else if (mode == 1 || mode == 2) { // Button 1 switching from modes 1 or 2 to mode 0 (dialing)
Serial.println("B1: Mode 0: wait");
mode = 0;
runGrinder(false);
}
time1 = millis();
}
btn1Last = btn1Val;

// Debounce and process button 2: override
if (btn2Val == 1 && btn2Last == 0 && millis() - time2 > debounce) {
if (mode == 0) { // Button 2 switching from mode 0 (dialing) to mode 2 (count up/override)
Serial.println("B2: Mode 2: count up/override");
mode = 2;
runGrinder(true);
grindStartTime = millis();
}
else if (mode == 2 || mode == 1) { // Button 2 switching off the relay and not chasing
Serial.println("B2: Mode 0");
mode = 0;
runGrinder(false);
}
time2 = millis();
}
btn2Last = btn2Val;

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
//-=-= MODES =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

if (mode == 0) {
// Debounce and process up and down buttons: scroll
if (upVal == 1 && upLast == 0 && millis() - timeUp > debounce) {
if ((millis()-timeUp) < 120) ct+=2;
else ct+=1;
Serial.print("UP: ");Serial.print(ct);Serial.print(" (");Serial.print(millis()-timeUp);Serial.println(")");
timeUp = millis();
}
upLast = upVal;

if (dnVal == 1 && dnLast == 0 && millis() - timeDn > debounce) {
if ((millis()-timeDn) < 120) ct-=2;
else ct-=1;
Serial.print("DN: ");Serial.print(ct);Serial.print(" (");Serial.print(millis()-timeDn);Serial.println(")");
timeDn = millis();
}
dnLast = dnVal;

ct = max(min(ct, grindTimeMax), grindTimeMin); // Limit count value
// Show the chosen count
displayNumber(ct, 0, 0);
}

// Countdown mode
else if (mode == 1) {
if ( (millis() - grindStartTime) < ct*1000 ) {
countDown = ct - int( (millis() - grindStartTime)/1000 ) ;
if ( ((millis() - grindStartTime) - ((ct-countDown)*1000)) > 500 ) {
displayChase(chaseVal, 1, 0);
if ( millis() - chaseTime > chaseDelay ) {
chaseVal++;
if (chaseVal == 8) chaseVal = 0;
chaseTime = millis();
}
}
else {
displayNumber(countDown, 1, 0);
chaseVal = 3;
}
}
// Stop running if countdown gets to 0
else {
Serial.println("Countdown done");
mode = 0;
runGrinder(false);
countDown = ct;
// Flash that count time has been reached
for (j=0; j<3; j++) {
clearDisplay();
delay(150);
displayNumber(ct, 1, 0);
delay(300);
}
}
}

// Countup mode
else if (mode == 2) {
// Display chase and the override duration
countUp = int((millis()-grindStartTime)/1000);
// Display chase if it's not time to show the countup time
if ( ((millis() - grindStartTime) - long(countUp)1000) < 800 ) {
displayChase(chaseVal, 0, 1);
if ( millis() - chaseTime > chaseDelay ) {
chaseVal++;
if (chaseVal == 8) chaseVal = 0;
chaseTime = millis();
}
}
else {
displayNumber(1+countUp, 0, 1);
chaseVal = 3;
}
// Stop grinding if max time is reached
if ( (millis() - grindStartTime ) > grindTimeMax
1000 ) {
mode = 0;
runGrinder(false);
// Flash that max time has been reached
for (j=0; j<6; j++) {
clearDisplay();
delay(150);
displayNumber(grindTimeMax, 0, 1);
delay(300);
}
}
}

// Setup min/max mode
else if (mode == 3) {
// Debounce and process up and down buttons: scroll

if (upVal == 1 && upLast == 0 && millis() - timeUp > debounce) {
if (setupMax == false) grindTimeMin = max(2, grindTimeMin+1);
else grindTimeMax = min(99, grindTimeMax+1);
timeUp = millis();
}
upLast = upVal;
if (dnVal == 1 && dnLast == 0 && millis() - timeDn > debounce) {
if (setupMax == false) grindTimeMin = max(2, grindTimeMin-1);
else grindTimeMax = max(grindTimeMin, min(99, grindTimeMax-1));
timeDn = millis();
}
dnLast = dnVal;
// Show the chosen count
if (setupMax == false) displayNumber(grindTimeMin, 1, 1);
else displayNumber(grindTimeMax, 1, 1);
}

}

//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
//-=-=-=- Program functions -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

// Turn the grinder/power on or off
void runGrinder(boolean grinderState) {
digitalWrite(triggerPin, grinderState);
}

// Write two digits to the 14-segment display
void displayNumber(int number, boolean btn1, boolean btn2) {
number = max(0, min(number, 99));
if (number < 10) D1 = 0;
else D1 = digs2[number/10]; // First digit: tens
D1 = D1 << 1;
D2 = digs1[number%10]; // Second digit: ones
// Flip extra bits to turn on button LEDs
if (btn1) D1=D1+B00000001;
if (btn2) D2=D2+B10000000;
// Write the bytes
digitalWrite(latchPin, LOW); // Unlatch the shift registers
shiftOut(D1); // Send first digit to 595
shiftOut(D2); // Send second digit to 595: first gets shifted left
digitalWrite(latchPin, HIGH); // Re-latch the shift registers
}

// Display a frame of the chase cycle
void displayChase(int x, boolean btn1, boolean btn2) {
x = max(0, min(x, 7));
D1 = ch2[x]; // First digit
D1 = D1 << 1;
D2 = ch1[x]; // Second digit
// Flip extra bits to turn on button LEDs
if (btn1) D1=D1+B00000001;
if (btn2) D2=D2+B10000000;
// Write the bytes
digitalWrite(latchPin, LOW); // Unlatch the shift registers
shiftOut(D1); // Send first digit to 595
shiftOut(D2); // Send second digit to 595: first gets shifted left
digitalWrite(latchPin, HIGH); // Re-latch the shift registers
}

// Clear the display: turn off all segments
void clearDisplay() {
digitalWrite(latchPin, LOW); // Unlatch the shift registers
shiftOut(0); shiftOut(0);
digitalWrite(latchPin, HIGH); // Re-latch the shift registers
}

// Write a byte to the shift register
void shiftOut(byte d) {
digitalWrite(dataPin, LOW);
for (int i=7; i>=0; i--) { // Step through the bits
digitalWrite(clockPin, LOW);
digitalWrite(dataPin, (d & (1<<i)) ); // Pin HIGH if the bit matches the byte's bit
digitalWrite(clockPin, HIGH);
digitalWrite(dataPin, LOW);
}
digitalWrite(clockPin, LOW);
}

Good stuff! Better coffee through the application of Arduino!