409 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			409 lines
		
	
	
		
			8.5 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 "pinout.h"
 | |
| #include <avr/wdt.h>
 | |
| 
 | |
| enum logic {
 | |
| 	ON = 1,
 | |
| 	OFF = 0,
 | |
| };
 | |
| 
 | |
| #define DEBOUNCE_TIME 20L /* ms */
 | |
| #define TEMP_MIN 40.0f /* C */
 | |
| #define TEMP_MAX 50.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;
 | |
| } 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 =  3000UL; /* 3 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    = 10000UL; /* 10 seconds */
 | |
| 
 | |
| void proc_machine(struct state *st)
 | |
| {
 | |
| 	float temp = st->ntc_T;
 | |
| 	int on = (st->btn[BTN_ON].state == RELEASED);
 | |
| 	int brew_hot = (st->btn[BTN_HOT].state == PRESSED);
 | |
| 
 | |
| 	Serial.print("t=");
 | |
| 	Serial.print(millis());
 | |
| 	Serial.print(" state=");
 | |
| 	Serial.print(st->mstate);
 | |
| 	Serial.print(" on=");
 | |
| 	Serial.print(on);
 | |
| 	Serial.print(" temp=");
 | |
| 	Serial.print(temp);
 | |
| 	Serial.println(" C");
 | |
| 
 | |
| 	/* 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_MAX) {
 | |
| 			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)
 | |
| {
 | |
| 	static int r = 0;
 | |
| 	static int g = 0;
 | |
| 
 | |
| 	if (st->mstate == HEATING || st->mstate == COOLING) {
 | |
| 		analogWrite(PIN_LED_RED, r);
 | |
| 		setled(PIN_LED_GREEN, 0);
 | |
| 		if (r >= 255)
 | |
| 			r = 0;
 | |
| 		else
 | |
| 			r += 5;
 | |
| 	} else if (st->mstate == HOT) {
 | |
| 		setled(PIN_LED_RED, 0);
 | |
| 		setled(PIN_LED_GREEN, 1);
 | |
| 		r = 0;
 | |
| 	} else if (st->mstate == PANIC_OVERHEAT) {
 | |
| 		setled(PIN_LED_RED, 1);
 | |
| 		setled(PIN_LED_GREEN, 0);
 | |
| 	} else if (st->mstate == BREWING_HOT || st->mstate == BREWING_COLD) {
 | |
| 		setled(PIN_LED_RED, 0);
 | |
| 		analogWrite(PIN_LED_GREEN, g);
 | |
| 		if (g >= 255)
 | |
| 			g = 0;
 | |
| 		else
 | |
| 			g += 5;
 | |
| 	} else {
 | |
| 		setled(PIN_LED_RED, 0);
 | |
| 		setled(PIN_LED_GREEN, 0);
 | |
| 		r = 0;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void
 | |
| output_heater(const struct state *st)
 | |
| {
 | |
| 	if (st->mstate == HEATING || st->mstate == HOT || st->mstate == BREWING_HOT) {
 | |
| 		if (st->ntc_T < TEMP_MIN)
 | |
| 			relay(PIN_HEAT, ON);
 | |
| 		else if (st->ntc_T > TEMP_MAX)
 | |
| 			relay(PIN_HEAT, OFF);
 | |
| 	} 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();
 | |
| }
 | 
