 53e3d47a04
			
		
	
	
		53e3d47a04
		
	
	
	
	
		
			
			Prevents a hang from keeping the heater or pump relays always on, which could cause the thermal fuses to burn or the pump to empty the deposit and then burn as well.
		
			
				
	
	
		
			359 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			359 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
 | |
|  * SPDX-License-Identifier: GPL-3.0-or-later */
 | |
| 
 | |
| #include "ntc.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,
 | |
| 	COOLING,
 | |
| };
 | |
| 
 | |
| //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 */
 | |
| 	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;
 | |
| } 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;
 | |
| 
 | |
| 	float avg = 0;
 | |
| 	for (int i = 0; i < MAX_SAMPLES; i++)
 | |
| 		avg += state->ntc_array_T[i];
 | |
| 
 | |
| 	state->ntc_T = avg / MAX_SAMPLES;
 | |
| }
 | |
| 
 | |
| 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;
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| int red_min = 50;
 | |
| int red_state = red_min;
 | |
| unsigned long brewing_time     = 3000UL; /* 3 seconds */
 | |
| unsigned long cooling_time     = 3000UL; /* 3 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 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");
 | |
| 
 | |
| 	/* Pressing ON cancels any operation */
 | |
| 	if (st->mstate != SLEEPING && on) {
 | |
| 		st->mstate = SLEEPING;
 | |
| 		st->buzz_state = BUZZ_OFF;
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	if (st->mstate == SLEEPING) {
 | |
| 		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 (hot) {
 | |
| 			st->mstate = BREWING;
 | |
| 			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) {
 | |
| 		if (millis() - st->brewing_t0 > brewing_time) {
 | |
| 			st->mstate = COOLING;
 | |
| 			st->cooling_t0 = millis();
 | |
| 			Serial.println("cooling");
 | |
| 		}
 | |
| 	} 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");
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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 += 3;
 | |
| 	} else if (st->mstate == HOT) {
 | |
| 		setled(PIN_LED_RED, 0);
 | |
| 		setled(PIN_LED_GREEN, 1);
 | |
| 		r = 0;
 | |
| 	} else if (st->mstate == BREWING) {
 | |
| 		setled(PIN_LED_RED, 0);
 | |
| 		analogWrite(PIN_LED_GREEN, g);
 | |
| 		if (g >= 255)
 | |
| 			g = 0;
 | |
| 		else
 | |
| 			g += 3;
 | |
| 	} 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) {
 | |
| 		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)
 | |
| 		relay(PIN_PUMP, ON);
 | |
| 	else
 | |
| 		relay(PIN_PUMP, OFF);
 | |
| }
 | |
| 
 | |
| void do_output(const struct state *st)
 | |
| {
 | |
| 	output_leds(st);
 | |
| 	output_heater(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);
 | |
| 
 | |
| 	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();
 | |
| }
 |