coffee/barista/barista.ino
2025-11-02 19:24:02 +01:00

427 lines
9.3 KiB
C++

/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later */
#include "ntc.h"
#include "overheat.h"
#include "led.h"
#include "heater.h"
#include "thermostat.h"
#include "pinout.h"
#include <avr/wdt.h>
enum logic {
ON = 1,
OFF = 0,
};
#define DEBOUNCE_TIME 20L /* ms */
#define TEMP_HOT 70.0f /* C */
#define LED_MIN_VALUE 0
enum machine_state {
SLEEPING = 0,
DEBOUNCE,
HEATING,
HOT,
BREWING_HOT,
BREWING_COLD,
COOLING,
PANIC_OVERHEAT,
};
//int state = SLEEPING;
enum btn_index {
BTN_ON = 0,
BTN_HOT,
MAX_BTN,
};
enum btn_state {
RESTING = 0,
PRESSING,
PRESSED,
RELEASING,
RELEASED,
};
enum buzz_state {
BUZZ_OFF = 0,
BUZZ_HEY,
BUZZ_ACTIVE,
};
int button_pin[MAX_BTN] = {
[BTN_ON] = PIN_POWER_ON,
[BTN_HOT] = PIN_HOT,
};
struct btn {
enum btn_state state;
unsigned long press_t0;
unsigned long release_t0;
};
struct input {
unsigned long t_ms;
int ntc_V;
enum logic btn[MAX_BTN];
} g_in;
#define MAX_SAMPLES 16
struct state {
enum machine_state mstate;
unsigned long brewing_t0;
unsigned long cooling_t0;
unsigned long heating_t0;
unsigned long hot_t0;
int ntc_i; /* Next available place */
int ntc_n; /* Samples in array */
float ntc_R;
float ntc_last_T;
float ntc_array_T[MAX_SAMPLES];
float ntc_T; /* average */
struct btn btn[MAX_BTN];
enum buzz_state buzz_state;
unsigned long buzz_t0;
struct overheat overheat;
unsigned long overheat_t0;
struct led red_led;
struct led green_led;
struct heater heater;
struct thermostat thermostat;
} g_st;
int read_input(int pin)
{
return !digitalRead(pin);
}
void relay(int pin, enum logic st)
{
/* Relays are active low */
if (st == ON)
digitalWrite(pin, 0);
else
digitalWrite(pin, 1);
}
void setled(int pin, enum logic st)
{
/* LEDs are active high */
if (st == ON)
digitalWrite(pin, 1);
else
digitalWrite(pin, 0);
}
void do_input(struct input *input)
{
input->t_ms = millis();
/* Read buttons */
for (int i = 0; i < MAX_BTN; i++)
input->btn[i] = read_input(button_pin[i]);
/* Read temperature sensor */
input->ntc_V = analogRead(PIN_NTC);
}
void proc_ntc(struct state *state, const struct input *input)
{
state->ntc_R = ntc_resistance(input->ntc_V);
state->ntc_last_T = ntc_temp(state->ntc_R);
state->ntc_array_T[state->ntc_i++] = state->ntc_last_T;
if (state->ntc_i >= MAX_SAMPLES)
state->ntc_i = 0;
if (state->ntc_n < MAX_SAMPLES)
state->ntc_n++;
float avg = 0;
for (int i = 0; i < state->ntc_n; i++)
avg += state->ntc_array_T[i];
state->ntc_T = avg / state->ntc_n;
overheat_input(&state->overheat, millis(), state->ntc_T);
}
void proc_buttons(struct state *state, const struct input *input)
{
for (int i = 0; i < MAX_BTN; i++) {
struct btn *btn = &state->btn[i];
int v = input->btn[i];
if (btn->state == RESTING) {
if (v == ON) {
btn->state = PRESSING;
btn->press_t0 = millis();
}
} else if (btn->state == PRESSING) {
if (v != ON) {
btn->state = RESTING;
} else if (millis() - btn->press_t0 > DEBOUNCE_TIME) {
btn->state = PRESSED;
}
} else if (btn->state == PRESSED) {
if (v != ON) {
btn->state = RELEASING;
btn->release_t0 = millis();
}
} else if (btn->state == RELEASING) {
if (v == ON) {
btn->state = PRESSED;
} else if (millis() - btn->release_t0 > DEBOUNCE_TIME) {
btn->state = RELEASED;
}
} else if (btn->state == RELEASED) {
btn->state = RESTING;
}
}
}
/* In PANIC_OVERHEAT state wait at least TIME_OVERHEAT_COOL and until the
* temperature goes below TIME_OVERHEAT_TEMP before doing anything. */
#define TIME_OVERHEAT_COOL 10000 /* ms */
#define TIME_OVERHEAT_TEMP 40.0 /* °C */
int red_min = 50;
int red_state = red_min;
unsigned long brewing_max_time = 30000UL; /* 30 seconds */
unsigned long cooling_time = 3000UL; /* 3 seconds */
unsigned long overheat_time = 10000UL; /* 10 seconds */
unsigned long max_heating_time = 60000UL; /* 60 seconds */
unsigned long max_idle_time = 120000UL; /* 120 seconds */
void proc_machine(struct state *st)
{
static unsigned long last_print_t = 0;
float temp = st->ntc_T;
int on = (st->btn[BTN_ON].state == RELEASED);
int brew_hot = (st->btn[BTN_HOT].state == PRESSED);
unsigned long t = millis();
if (t - last_print_t > 100) {
Serial.print(t);
Serial.print(" ");
Serial.print(st->mstate);
Serial.print(" ");
Serial.println(temp);
last_print_t = t;
}
/* If the machine is overheating */
if (overheat_panic(&st->overheat)) {
st->mstate = PANIC_OVERHEAT;
st->overheat_t0 = millis();
Serial.println("PANIC OVERHEATING");
}
/* Pressing ON cancels any operation */
if (st->mstate != SLEEPING && on) {
st->mstate = SLEEPING;
st->buzz_state = BUZZ_OFF;
return;
}
if (st->mstate == SLEEPING) {
/* Allow brewing cold at without turning the heater */
/* FIXME: Use the cold button instead */
if (brew_hot) {
st->mstate = BREWING_COLD;
Serial.println("brewing cold");
} else if (on) {
st->mstate = HEATING;
st->heating_t0 = millis();
Serial.println("heating");
}
} else if (st->mstate == HEATING) {
if (temp > TEMP_HOT) {
st->mstate = HOT;
st->hot_t0 = millis();
st->buzz_state = BUZZ_HEY;
Serial.println("hot");
} else if (millis() - st->heating_t0 > max_heating_time) {
/* TODO: Add alarm state */
st->mstate = SLEEPING;
Serial.println("cannot heat, going to sleep");
}
} else if (st->mstate == HOT) {
if (brew_hot) {
st->mstate = BREWING_HOT;
st->brewing_t0 = millis();
Serial.println("brewing");
} else if (millis() - st->hot_t0 > max_idle_time) {
st->mstate = SLEEPING;
Serial.println("idle timeout, going to sleep");
}
} else if (st->mstate == BREWING_HOT) {
/* Stop brewing if no longer pressing the brew button or we
* exceed the brew max time */
if (!brew_hot || millis() - st->brewing_t0 > brewing_max_time) {
st->mstate = COOLING;
st->cooling_t0 = millis();
Serial.println("cooling");
}
} else if (st->mstate == BREWING_COLD) {
/* FIXME: Use cold button instead */
if (!brew_hot) {
st->mstate = SLEEPING;
Serial.println("going back to sleeping after cold brewing");
}
} else if (st->mstate == COOLING) {
/* TODO: Wait a bit and go back to heating */
if (millis() - st->cooling_t0 > cooling_time) {
st->mstate = HEATING;
st->heating_t0 = millis();
Serial.println("heating");
}
} else if (st->mstate == PANIC_OVERHEAT) {
/* Wait until it cools down and enough time has passed */
if (st->ntc_T < TIME_OVERHEAT_TEMP &&
millis() - st->overheat_t0 > TIME_OVERHEAT_COOL) {
st->mstate = SLEEPING;
Serial.println("sleeping");
}
}
}
void
proc_buzz(struct state *st)
{
if (st->buzz_state == BUZZ_HEY) {
tone(PIN_BUZZ, 1500);
st->buzz_state = BUZZ_ACTIVE;
st->buzz_t0 = millis();
} else if (st->buzz_state == BUZZ_ACTIVE) {
if (millis() - st->buzz_t0 > 20) {
st->buzz_state = BUZZ_OFF;
noTone(PIN_BUZZ);
}
} else {
noTone(PIN_BUZZ);
}
}
void do_proc(struct state *st, const struct input *input)
{
proc_ntc(st, input);
proc_buttons(st, input);
proc_machine(st);
proc_buzz(st);
}
void
output_leds(const struct state *st)
{
unsigned long t = millis();
if (st->mstate == SLEEPING) {
led_off(&st->red_led);
led_off(&st->green_led);
} else if (st->mstate == HEATING) {
led_off(&st->red_led);
led_pattern(&st->green_led, t, 1000UL, "000123456789abcdefff");
} else if (st->mstate == COOLING) {
led_off(&st->red_led);
led_pattern(&st->green_led, t, 1000UL, "fffedcba987654321000");
} else if (st->mstate == HOT) {
led_off(&st->red_led);
led_on(&st->green_led);
} else if (st->mstate == PANIC_OVERHEAT) {
led_pattern(&st->red_led, t, 3000UL, "f0f0f0000000");
led_pattern(&st->green_led, t, 3000UL, "000000f0f0f0");
} else if (st->mstate == BREWING_HOT || st->mstate == BREWING_COLD) {
led_off(&st->red_led);
led_pattern(&st->green_led, millis(), 2000UL, "0123456789abcdefedcba9876543210");
} else {
led_off(&st->red_led);
led_off(&st->green_led);
}
analogWrite(PIN_LED_RED, led_level(&st->red_led, t));
analogWrite(PIN_LED_GREEN, led_level(&st->green_led, t));
}
void
output_heater(const struct state *st)
{
unsigned long t = millis();
/* First configure thermostate */
if (st->mstate == HEATING || st->mstate == HOT || st->mstate == BREWING_HOT)
thermostat_set(&st->thermostat, TEMP_HOT);
else
thermostat_off(&st->thermostat);
/* Then update heater state from thermostate */
if (thermostat_state(&st->thermostat, st->ntc_T))
heater_on(&st->heater, t);
else
heater_off(&st->heater);
/* Then switch relays accordingly */
if (heater_state(&st->heater, t))
relay(PIN_HEAT, ON);
else
relay(PIN_HEAT, OFF);
}
void
output_pump(const struct state *st)
{
if (st->mstate == BREWING_HOT || st->mstate == BREWING_COLD)
relay(PIN_PUMP, ON);
else
relay(PIN_PUMP, OFF);
}
void do_output(const struct state *st)
{
output_leds(st);
output_heater(st);
output_pump(st);
}
void setup()
{
wdt_disable();
Serial.begin(9600);
Serial.println("Booting");
pinMode(PIN_POWER_ON, INPUT);
pinMode(PIN_HOT, INPUT);
pinMode(PIN_LED_RED, OUTPUT);
pinMode(PIN_LED_GREEN, OUTPUT);
pinMode(PIN_HEAT, OUTPUT);
pinMode(PIN_PUMP, OUTPUT);
/* Turn all relays off */
relay(PIN_HEAT, OFF);
relay(PIN_PUMP, OFF);
overheat_init(&g_st.overheat);
Serial.println("Ready");
wdt_enable(WDTO_250MS);
}
void loop()
{
do_input(&g_in);
do_proc(&g_st, &g_in);
do_output(&g_st);
delay(5);
wdt_reset();
}