Andrea Palisca
Published © MIT

Coffee Machine Controller Using Particle Photon

Digital temperature control of Gaggia Evolution espresso machine with preinfusion functionality using Photon, SSRs, OLED, and a thermistor.

AdvancedFull instructions provided920
Coffee Machine Controller Using Particle Photon

Things used in this project

Hardware components

Photon
Particle Photon
×1
Graphic OLED, 128 x 128
Graphic OLED, 128 x 128
×1
Thermistor NTC 3950 100K ohm
×1
Solid State Relay, 25 A
Solid State Relay, 25 A
×2
Rotary Encoder with Push-Button
Rotary Encoder with Push-Button
×1

Software apps and online services

Particle Build Web IDE
Particle Build Web IDE
Google Sheets
Google Sheets

Story

Read more

Schematics

G Revolution v0.1

Code

G Revolution v0.1 sketch

Arduino
// G Revolution v0.1
// (c) Andrea Palisca - February 2020
// MIT License - please keep attribution
// Used Adafruit GFX Library

// OLED pins
#define cs   A2
#define sclk A3
#define mosi A5
#define rst  D1
#define dc   D0

// Color definitions
#define	BLACK           0x0000
#define	BLUE            0x001F
#define	RED             0xF800
#define	GREEN           0x07E0
#define CYAN            0x07FF
#define MAGENTA         0xF81F
#define YELLOW          0xFFE0  
#define WHITE           0xFFFF

#include "Adafruit_mfGFX/Adafruit_mfGFX.h"
#include "Adafruit_SSD1351_Photon.h"
#include "math.h"

Adafruit_SSD1351 tft = Adafruit_SSD1351(cs, dc, rst);

void doEncoderA();                                  // encoder variables
void doEncoderB();
bool A_set = false;
bool B_set = false;
int prevPos = 0;
int encoderPos = 0;
int encoderPosPrevious = 0;
bool encoder_button_state = 0;

void debouncedoPower();
void doPower();
void debouncedoEncoderButton();
void doEncoderButton();
long debouncing_time = 300;                          //Debouncing Time in Milliseconds
volatile unsigned long last_micros;

int encoderA = A1;                                  // Pin Names
int encoderB = A0;
int photocell = A6;
int thermistor = A7;
int heaterpin = D2;
int pumppin = D3;
int power_led = D4;
int encoder_button = D5;
int power_button = D6;
int thermistor_power = D7;

int photocell_value = 0;
int thermistor_value = 0;
enum State { OFF, HEATING, COOLING, READY, FLUSHING, INFUSING, BREWING, SETTINGS };
enum Mode { COFFEE, STEAM };
State state = OFF;
Mode mode = COFFEE;

unsigned int nextSampleTime = millis();
unsigned int lastSampleTime = 0;
unsigned int SAMPLE_INTERVAL = 2000;
unsigned int heaterwaittimer = 0;
unsigned int brewingtimer = 0;
unsigned int flushingtimer = 0;
unsigned int infusingtimer = 0;
unsigned int infusingpumptimer = 0;
unsigned int pulsetimer = 0;
unsigned int displaytimer = 0;
float boilertemp = 0;
float watertemp = 0;
uint8_t i;
bool heaterstate = 0;
bool pumpstate = 0;
bool flushcomplete = 0;
bool infusingpumpcomplete = 0;

// Thermistor Example #3 from the Adafruit Learning System guide on Thermistors 
// https://learn.adafruit.com/thermistor/overview by Limor Fried, Adafruit Industries
// MIT License - please keep attribution and consider buying parts from Adafruit

#define THERMISTORPIN A7                // which analog pin to connect  
#define THERMISTORNOMINAL 100000        // resistance at 25 degrees C
#define TEMPERATURENOMINAL 25           // temp. for nominal resistance (almost always 25 C)
#define NUMSAMPLES 5                    // how many samples to take and average, more takes longer but is more 'smooth'
#define BCOEFFICIENT 3950               // The beta coefficient of the thermistor (usually 3000-4000)
#define SERIESRESISTOR 46600            // the value of the 'other' resistor.... try 48800?

float COFFEETEMPLOW = 104.0;    // was 106, was 108, tried 110 too high,
float COFFEETEMPSTART = COFFEETEMPLOW - 2;  // WAS 104
float COFFEETEMPHIGH = COFFEETEMPLOW + 9;    // WAS 115

float STEAMTEMPLOW = 130.0;   
float STEAMTEMPSTART = STEAMTEMPLOW - 5; // 125.0; 
float STEAMTEMPHIGH = STEAMTEMPLOW + 15; // 145.0;  

float TEMPSTART = 0; 
float TEMPHIGH = 0;   
float TEMPLOW = 0;   
int HEATERPULSEDURATION = 0;
int FLUSHDURATION = 4000;
int INFUSEPUMPDURATION = 2500;
int INFUSEDURATION = 12000;

int samples[NUMSAMPLES];
int row = 1;

// Timer heaterpulse(HEATERPULSEDURATION, heateroff, true);
// Timer longheaterpulse(HEATERPULSEDURATION*2.5, heateroff, true);


void setup(void) {
    
    pinMode (encoderA, INPUT_PULLUP);               // Pins
    pinMode (encoderB, INPUT_PULLUP);
    attachInterrupt(encoderA, doEncoderA, CHANGE);
    attachInterrupt(encoderB, doEncoderB, CHANGE);
    pinMode(encoder_button, INPUT_PULLUP);
    attachInterrupt(encoder_button, debouncedoEncoderButton, FALLING);
    pinMode (photocell, INPUT);
    pinMode (thermistor, INPUT);
    pinMode(power_button,INPUT_PULLUP);
    attachInterrupt(power_button, debouncedoPower, FALLING);
    pinMode(power_led, OUTPUT);
    pinMode(heaterpin, OUTPUT);
    pinMode(pumppin, OUTPUT);
    pinMode (thermistor_power, OUTPUT);
    
    digitalWrite(thermistor_power,HIGH);
    digitalWrite(pumppin,LOW);
    digitalWrite(heaterpin,LOW);
    digitalWrite(power_led, LOW);
    
    setMode(COFFEE);
    
//    Serial.begin (9600);
//    Serial.print ("Starting...");
//    debugPrint("debug text");             // HOW TO DEBUG TO OLED
  
    tft.begin();
    // tft.setRotation(2);
    tft.fillScreen(BLACK);
    tft.fillRect(0, 0, 128, 9, WHITE);
    tft.setCursor(1, 1);
    tft.setTextColor(BLACK, WHITE);
    tft.setTextSize(1);
    tft.println("  G REVOLUTION v0.1  ");
    tft.drawFastHLine(0, 53, 128, WHITE);
    tft.drawFastHLine(0, 75, 128, WHITE);
    refreshDisplayHeader();
}


void loop() {
    measureTemp();
    switch (state) {
        case OFF:
            pump(0);                        // pump and heater off
            heater(0);
		break;
        case HEATING:                   
            pump(0);  
            if ( boilertemp < TEMPSTART ) {  // go to temp, when at temp go to READY state
                heater(1);
            }
            else {
                heater(0);
                heaterwaittimer = millis() + 3000;
                state = READY;
                refreshDisplayHeader(); refreshDisplay();
            }
		break;
		case COOLING:                   
            pump(0);
            heater(0);
            if ( boilertemp < TEMPHIGH ) {  
                state = READY;
                refreshDisplayHeader(); refreshDisplay();
            }
		break;
		case READY:                         
            pump(0);                      
            keepTemp();
		break;
		case FLUSHING:                         
            keepTemp();
            if ( flushcomplete == 0 ) {
                if ( pumpstate == 0 ) {
                    flushingtimer = millis() + FLUSHDURATION;
                    pump(1);
                }
                else {
                    if ( millis() > flushingtimer ) {
                        pump(0);
                        flushcomplete = 1;
                        refreshDisplayHeader(); refreshDisplay();
                    }
                }
            }
		break;
		case INFUSING:                         
            keepTemp();
            if ( infusingpumpcomplete == 0 ) {
                if ( pumpstate == 0 ) {
                    infusingpumptimer = millis() + INFUSEPUMPDURATION;
                    pump(1);
                }
                else {
                    if ( millis() > infusingpumptimer ) {
                        pump(0);
                        infusingpumpcomplete = 1;
                        infusingtimer = millis() + INFUSEDURATION;
                    }
                }
            }
            else {
                if ( millis() > infusingtimer ) {
                    state = BREWING;
                    refreshDisplayHeader(); refreshDisplay();
                }
            }
		break;
		case BREWING:                       // if temp not at max, turn on heater, wait then start pump
		    if ( boilertemp < TEMPHIGH ) {
                heater(1);
                if (pumpstate == 0) {
                    refreshDisplayHeader(); refreshDisplay();
                    delay(1250);
                }
                pump(1);
            }
            else {
                heater(0);
                pump(1);
            }
		break;
		case SETTINGS:
		    pump(0);
            heater(0);
		break;
    }
    
    checkLogData();
    if ( millis() > displaytimer ) {
        refreshDisplay();
        displaytimer = millis() + 500;
    }
    measureTempWater();
    debugPrint( String(millis()/1000,DEC) );
    checkEncoder();
}


void heater(bool power) {
    if (power == 1) {
        if (heaterstate == 0) {
            digitalWrite(heaterpin,HIGH);
            heaterstate = 1;
            logData();
        }
    }
    if (power == 0) {
        if (heaterstate == 1) {
            digitalWrite(heaterpin,LOW);
            heaterstate = 0;
            logData();
        }
    }
}


void pulseheater(unsigned int duration) {
    if (heaterstate == 0) {
        pulsetimer = millis() + duration;
        heater(1);
        if ( HEATERPULSEDURATION > 2500) {
            heaterwaittimer = millis() + HEATERPULSEDURATION + 2000;
        }
        else {
            heaterwaittimer = millis() + HEATERPULSEDURATION + 3500;
        }
    }
}

void checkpulseheater() {
    if (millis() > pulsetimer) {
        heater(0);
        HEATERPULSEDURATION = 0;
        refreshDisplay();
    }
}



void pump(bool ppower) {
    if (ppower == 1) {
        if (pumpstate == 0) {
            digitalWrite(pumppin,HIGH);
            pumpstate = 1;
            brewingtimer = millis();
        }
    }
    if (ppower == 0) {
        if (pumpstate == 1) {
            digitalWrite(pumppin,LOW);
            pumpstate = 0;
        }
    }
}

void checkLogData(void) {
    // every interval: read tempearture, read photocell, convert to boolean, send to GSheet
    if ( millis() > nextSampleTime) {
        if ( millis() - lastSampleTime > 1000) {                // don't log if log happened less than a second ago
            nextSampleTime = millis() + SAMPLE_INTERVAL;
            logData();
        }
        // debugPrint("logging data...");
        // delay(100);
        // debugPrint("                ");
    }
}


void logData() {
    
    float milli = millis();
    milli = milli/1000;
    
    char data[256];
	snprintf(data, sizeof(data), "{\"milli\":%f, \"temp\":%f, \"heater\":%d, \"pump\":%d, \"water\":%f}", milli, boilertemp, heaterstate, pumpstate, watertemp);

    Particle.publish("logData", data, PRIVATE);
    lastSampleTime = millis();
}


float measureTemp(void) {
    uint8_t i;
    float average;

    average = 0;                                  // take N samples in a row, with a slight delay
    for (i=0; i< NUMSAMPLES; i++) {               
        average += analogRead(THERMISTORPIN);
        delay(10);
    }
    average /= NUMSAMPLES;                        // average all the samples out
  
    average = 4095 / average - 1;                 // convert the value to resistance
    average = SERIESRESISTOR / average;
  
    float steinhart;
    steinhart = average / THERMISTORNOMINAL;      // (R/Ro)
    steinhart = log(steinhart);                   // ln(R/Ro)
    steinhart /= BCOEFFICIENT;                    // 1/B * ln(R/Ro)
    steinhart += 1.0 / (TEMPERATURENOMINAL + 273.15); // + (1/To)
    steinhart = 1.0 / steinhart;                  // Invert
    steinhart -= 273.15;                          // convert to C

    boilertemp = steinhart;
  
    return steinhart;
}

float measureTempWater(void) {
    uint8_t i;
    float average;

    average = 0;                                  // take N samples in a row, with a slight delay
    for (i=0; i< NUMSAMPLES; i++) {               
        average += analogRead(photocell);
        delay(10);
    }
    average /= NUMSAMPLES;                        // average all the samples out
  
    average = 4095 / average - 1;                 // convert the value to resistance
    average = SERIESRESISTOR / average;
  
    float steinhart;
    steinhart = average / THERMISTORNOMINAL;      // (R/Ro)
    steinhart = log(steinhart);                   // ln(R/Ro)
    steinhart /= BCOEFFICIENT;                    // 1/B * ln(R/Ro)
    steinhart += 1.0 / (TEMPERATURENOMINAL + 273.15); // + (1/To)
    steinhart = 1.0 / steinhart;                  // Invert
    steinhart -= 273.15;                          // convert to C

    watertemp = steinhart;
  
    return steinhart;
}


void keepTemp(void) {
    if ( boilertemp > TEMPHIGH ) {
        heater(0);
        state = COOLING;
        refreshDisplayHeader(); refreshDisplay();
    }
    if ( millis() > heaterwaittimer ) {
        if ( boilertemp < TEMPLOW ) {
            if (heaterstate==0) {
                HEATERPULSEDURATION = (TEMPLOW - boilertemp)*1000;
                if ( HEATERPULSEDURATION < 1000) { 
                    HEATERPULSEDURATION = 1000;
                }
                refreshDisplay();
                pulseheater(HEATERPULSEDURATION);
            }
        }
    }
    checkpulseheater();
}

//  ===== DISPLAY ROUTINES =====

void refreshDisplayHeader() {
    noInterrupts();
    //tft.fillScreen(BLACK);
    tft.setCursor(0, 13);
    tft.setTextColor(WHITE,BLACK);
    tft.setTextSize(2);
    switch (mode) {
        case COFFEE:
            tft.print("COFFEE   ");
        break;
        case STEAM:
            tft.setTextColor(CYAN,BLACK);
            tft.print("STEAM    ");
        break;
    }
    tft.setCursor(0, 29);
    tft.setTextSize(3);
    switch (state) {
        case OFF:
            tft.setTextColor(WHITE,BLACK);
            tft.print("OFF    ");
        break;
        case HEATING:
            tft.setTextColor(RED,BLACK);
            tft.print("HEATING");
        break;
        case COOLING:
            tft.setTextColor(BLUE,BLACK);
            tft.print("COOLING");
        break;
        case READY:
            tft.setTextColor(GREEN,BLACK);
            tft.print("READY  ");
        break;
        case FLUSHING:
            tft.setTextColor(YELLOW,BLACK);
            tft.print("FLUSH  ");
        break;
        case INFUSING:
            tft.setTextColor(CYAN,BLACK);
            tft.print("INFUSE ");
        break;
        case BREWING:
            tft.setTextColor(BLUE,BLACK);
            tft.print("BREWING");
        break;
        case SETTINGS:
            tft.setTextColor(BLUE,BLACK);
            tft.print("CONFIG ");
        break;
    }
    tft.fillRect(0, 76, 128, 45, BLACK);
    interrupts();
}

void refreshDisplay() {
    noInterrupts();
//  ======== Temperature ========  
    if ( state != SETTINGS ) {
        tft.setTextColor(WHITE,BLACK);
        tft.setCursor(0, 57);
        tft.setTextSize(2);
        if (heaterstate) { tft.setTextColor(RED,BLACK); }
        tft.print(boilertemp,1);
        tft.print(" C ");
        tft.setCursor(95, 64);
        tft.setTextSize(1);
        tft.setTextColor(BLUE,BLACK);
        if ( mode == STEAM ) { tft.setTextColor(CYAN,BLACK); }
        tft.print(TEMPLOW,1);
    }
    
//  ======== Temperature ========      
    tft.setCursor(0, 80);
    tft.setTextSize(1);
    tft.setTextColor(WHITE,BLACK);
    
    switch (state) {
        case OFF:
            tft.setTextColor(GREEN,BLACK);
            tft.print("Press for Settings");
        break;
        case HEATING:
            tft.setTextColor(RED,BLACK);
            tft.print("Please wait...");
        break;
        case COOLING:
            tft.setTextColor(BLUE,BLACK);
            tft.print("Please wait...");
        break;
        case READY:
            tft.print("Pulse: ");
            tft.print(String(HEATERPULSEDURATION,DEC));
            tft.print(" ms   ");
            tft.setCursor(0, 95);
            switch (mode) {
                case COFFEE:
                    tft.setTextColor(YELLOW,BLACK);
                    tft.print("Press to Flush");
                break;
                case STEAM:
                    tft.setTextColor(BLUE,BLACK);
                    tft.print("Use valve for Steam");
                break;
            }
        break;
        case FLUSHING:
            if ( flushcomplete == 0 ) {
                tft.setCursor(53, 80);
                tft.setTextSize(4);
                tft.setTextColor(YELLOW,BLACK);
                if ( flushingtimer > millis() ) {
                    tft.print(String((flushingtimer-millis())/1000,DEC));
                }
                else {
                    tft.print("  ");
                }
            }
            else {
                tft.setTextColor(BLUE,BLACK);
                tft.print("Press to Brew");
            }
        break;
        case INFUSING:
            tft.setTextColor(CYAN,BLACK);
            if ( infusingpumpcomplete == 0 ) {
                tft.print("Soaking...");
                tft.setCursor(53, 90);
                tft.setTextSize(4);
                if ( infusingpumptimer > millis() ) {
                    tft.print(String((infusingpumptimer-millis())/1000,DEC));
                }
                else {
                    tft.print("  ");
                }
            }
            else {
                tft.print("Waiting...");
                tft.setCursor(53, 90);
                tft.setTextSize(4);
                if ( infusingtimer > millis() ) {
                    tft.print(String((infusingtimer-millis())/1000,DEC));
                    tft.print(" ");
                }
                else {
                    tft.print("  ");
                }
            }
        break;
        case BREWING:
            if ( pumpstate == 1 ) {
                tft.print("Brewing timer: ");
                tft.setCursor(0, 90);
                tft.setTextSize(2);
                tft.print(String((millis()-brewingtimer)/1000,DEC));
                tft.print(" seconds  ");
            }
        break;
        case SETTINGS:
            if ( mode == COFFEE ) {
		        if ( row == 1 ) { tft.setTextColor(BLACK,WHITE); } else { tft.setTextColor(WHITE,BLACK); }
                tft.setCursor(0, 68);
                tft.setTextSize(1);
                tft.print("Temperature: ");
                tft.print(COFFEETEMPLOW,1);
                tft.print(" C");
                if ( row == 2 ) { tft.setTextColor(BLACK,WHITE); } else { tft.setTextColor(WHITE,BLACK); }
                tft.setCursor(0, 77);
                tft.print("Infuse Time: ");
                tft.print(String(INFUSEDURATION/1000,DEC));
                tft.print(" s");
		    }
		    if ( mode == STEAM ) {
		        if ( row == 3 ) { tft.setTextColor(BLACK,WHITE); } else { tft.setTextColor(WHITE,BLACK); }
                tft.setCursor(0, 68);
                tft.setTextSize(1);
                tft.print("Temperature: ");
                tft.print(STEAMTEMPLOW,1);
                tft.print(" C");
		    }
		break;
    }
    //debugPrint( String(millis()/1000,DEC) );
    interrupts();
}

int debugPrint( String s ) {
    noInterrupts();
    tft.setTextColor(BLUE,BLACK);
    tft.setTextSize(1);
    tft.setCursor(0, 120);
    tft.print(s);
    for (int i = 0; i <= s.length(); i++) {
        tft.print(" ");
    }
    interrupts();
}

 
//  ===== INPUT ROUTINES =====

void debouncedoPower() {
    if((long)(micros() - last_micros) >= debouncing_time * 500) {
        doPower();
        last_micros = micros();
    }
}

void doPower() {                        
    if ( state == OFF) {
        state = HEATING;
        digitalWrite(power_led, HIGH);
    }
    else {
        state = OFF;
        digitalWrite(power_led, LOW);
    }
    refreshDisplayHeader();
}


void debouncedoEncoderButton() {
    if((long)(micros() - last_micros) >= debouncing_time * 1000) {
        doEncoderButton();
        last_micros = micros();
    }
}

void doEncoderButton() {                        
    switch (state) {
        case OFF:
            row = 1;
            setMode(COFFEE);
            state = SETTINGS;
            refreshDisplayHeader();
            tft.fillRect(0, 57, 128, 45, BLACK);
        break;
        case HEATING:
        break;
        case COOLING:
        break;
        case READY:
            if (mode == COFFEE) {
                flushcomplete = 0;
                state = FLUSHING;
                refreshDisplayHeader();
            }
        break;
        case FLUSHING:
            if ( flushcomplete == 1 ) {
                infusingpumpcomplete = 0;
                state = INFUSING;
                refreshDisplayHeader();
            }
        break;
        case BREWING:
            state = HEATING;
            refreshDisplayHeader();
        break;
        case SETTINGS:
            switch ( row ) {
                case 1:
                    setMode(COFFEE);
                    row = 2;
                break;
                case 2:
                    setMode(STEAM);
                    refreshDisplayHeader();
                    row = 3;
                break;
                case 3:
                    setMode(COFFEE);
                    tft.fillRect(0, 57, 128, 45, BLACK);
                    tft.drawFastHLine(0, 75, 128, WHITE);
                    state = OFF;
                    refreshDisplayHeader();
                break;
            }
        break;
    }
}

void checkEncoder() {
    switch (state) {
        case OFF:
            if ( encoderPos != encoderPosPrevious) {
                switch (mode) {
                    case COFFEE:
                        setMode(STEAM);
                    break;
                    case STEAM:
                        setMode(COFFEE);
                    break;
                }
                refreshDisplayHeader(); refreshDisplay();
            }
            encoderPosPrevious = encoderPos;
        break;
        case READY:
            if ( encoderPos != encoderPosPrevious) {
                switch (mode) {
                    case COFFEE:
                        setMode(STEAM);
                        state = HEATING;
                    break;
                    case STEAM:
                        setMode(COFFEE);
                        state = COOLING;
                    break;
                }
                refreshDisplayHeader(); refreshDisplay();
            }
            encoderPosPrevious = encoderPos; 
        break;
        case SETTINGS:
            if ( encoderPos > encoderPosPrevious) {
                switch ( row ) {
                    case 1:
                        COFFEETEMPLOW = COFFEETEMPLOW + 1;
                        COFFEETEMPSTART = COFFEETEMPLOW - 2;
                        COFFEETEMPHIGH = COFFEETEMPLOW + 9;
                    break;
                    case 2:
                        INFUSEDURATION = INFUSEDURATION + 1000;
                    break;
                    case 3:
                        // STEAMTEMPLOW =+ 1;
                    break;
                }
            }
            if ( encoderPos < encoderPosPrevious) {
                switch ( row ) {
                    case 1:
                        COFFEETEMPLOW = COFFEETEMPLOW - 1;
                        COFFEETEMPSTART = COFFEETEMPLOW - 2;
                        COFFEETEMPHIGH = COFFEETEMPLOW + 9;
                    break;
                    case 2:
                        INFUSEDURATION = INFUSEDURATION - 1000;
                    break;
                    case 3:
                        // STEAMTEMPLOW =- 1;
                    break;
                }
            }
            encoderPosPrevious = encoderPos; 
        break;
    }
}


void setMode(int x) {
    switch (x) {
        case COFFEE:
            mode = COFFEE;
            TEMPSTART = COFFEETEMPSTART;
            TEMPLOW = COFFEETEMPLOW;
            TEMPHIGH = COFFEETEMPHIGH;
        break;
        case STEAM:
            mode = STEAM;
            TEMPSTART = STEAMTEMPSTART;
            TEMPLOW = STEAMTEMPLOW;
            TEMPHIGH = STEAMTEMPHIGH;
        break;
    }
}


void doEncoderA() {                        
    if ( digitalRead(encoderA) != A_set ) {                     // debounce once more
        A_set = !A_set;
        if ( A_set && !B_set )                                  // adjust counter + if A leads B
            encoderPos = encoderPos + 1;
    }
}

void doEncoderB() {                                             // Interrupt on B changing state, same as A above
    if ( digitalRead(encoderB) != B_set ) {
        B_set = !B_set;
        if ( B_set && !A_set )                                  //  adjust counter - 1 if B leads A
            encoderPos = encoderPos - 1;
    }
}

Credits

Andrea Palisca

Andrea Palisca

1 project • 0 followers
Thanks to Adafruit and Limor Fried, Adafruit Industries.

Comments

Add projectSign up / Login