// 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;
}
}
Comments
Please log in or sign up to comment.