Go Down

Topic: Coffee grinder timer (Read 1 time) previous topic - next topic

salsaman

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...:)

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.


My Arduino blog: http://jmsarduino.blogspot.com
Comprehensive (?) Arduino-compatible board list: http://tinyurl.com/allarduinos

salsaman

/* 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;

My Arduino blog: http://jmsarduino.blogspot.com
Comprehensive (?) Arduino-compatible board list: http://tinyurl.com/allarduinos

salsaman


   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
  • ;         // First digit
     D1 = D1 << 1;
     D2 = ch1
  • ;         // 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);
    }
My Arduino blog: http://jmsarduino.blogspot.com
Comprehensive (?) Arduino-compatible board list: http://tinyurl.com/allarduinos

Anachrocomputer

Good stuff!  Better coffee through the application of Arduino!

Go Up