diff --git a/barista/Makefile b/barista/Makefile index 5a9da64..84cde25 100644 --- a/barista/Makefile +++ b/barista/Makefile @@ -40,6 +40,9 @@ test: ntc.test overheat.test led.test %.test: test/test_%.o test/compat.o %.o gcc $^ -o $@ $(LIBS) +thermostat.test: test/test_thermostat.o test/compat.o thermostat.o overheat.o heater.o + gcc $^ -o $@ $(LIBS) + clean: rm -f $(TESTS) test/*.o *.o diff --git a/barista/barista.ino b/barista/barista.ino index 4e07f17..a96e579 100644 --- a/barista/barista.ino +++ b/barista/barista.ino @@ -15,7 +15,7 @@ enum logic { }; #define DEBOUNCE_TIME 20L /* ms */ -#define TEMP_HOT 70.0f /* C */ +#define TEMP_HOT 75.0f /* C */ #define LED_MIN_VALUE 0 @@ -198,7 +198,7 @@ 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 */ +unsigned long max_idle_time = 300000UL; /* 300 seconds */ void proc_machine(struct state *st) { @@ -209,15 +209,6 @@ void proc_machine(struct state *st) 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; @@ -352,43 +343,55 @@ output_leds(const struct state *st) } void -output_heater(const struct state *st) +output_relays(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); + thermostat_set(&st->thermostat, TEMP_HOT+5.0); 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); + float T = st->ntc_T; + float dT_dt = overheat_speed(&st->overheat); + float u = thermostat_state(&st->thermostat, T, dT_dt); + heater_on(&st->heater, t, u); /* Then switch relays accordingly */ - if (heater_state(&st->heater, t)) + int heater_relay_st = heater_state(&st->heater, t); + if (heater_relay_st) relay(PIN_HEAT, ON); else relay(PIN_HEAT, OFF); -} -void -output_pump(const struct state *st) -{ + int pump_relay_st = 0; if (st->mstate == BREWING_HOT || st->mstate == BREWING_COLD) + pump_relay_st = 1; + + if (pump_relay_st) relay(PIN_PUMP, ON); else relay(PIN_PUMP, OFF); + + Serial.print(t); + Serial.print(" "); + Serial.print(st->mstate); + Serial.print(" "); + Serial.print(T); + Serial.print(" "); + Serial.print(u); + Serial.print(" "); + Serial.print(heater_relay_st); + Serial.print(" "); + Serial.println(pump_relay_st); } void do_output(const struct state *st) { output_leds(st); - output_heater(st); - output_pump(st); + output_relays(st); } void setup() diff --git a/barista/heater.c b/barista/heater.c index cef5791..3039d24 100644 --- a/barista/heater.c +++ b/barista/heater.c @@ -3,25 +3,34 @@ #include "heater.h" -#define HEATER_T_ON 500UL /* ms */ -#define HEATER_T_OFF 5000UL /* ms */ +#define HEATER_MIN 500UL /* ms */ +#define HEATER_MAX 2000UL /* ms */ +#define HEATER_PERIOD 5000UL /* ms */ void -heater_on(struct heater *h, unsigned long t_ms) +heater_on(struct heater *h, unsigned long t_ms, float duty) { - if (h->st == HEATER_ON) - return; + unsigned long dt_on = duty * HEATER_MAX; - h->next_t = t_ms + HEATER_T_ON; - h->st = HEATER_ON; - h->turn_on = 1; + if (dt_on < HEATER_MIN) + dt_on = 0; + else if (dt_on > HEATER_MAX) + dt_on = HEATER_MAX; + + h->next_on_dt = dt_on; + + if (h->st == HEATER_OFF) { + h->st = HEATER_ON; + h->t0_on = t_ms; + h->t0_off = t_ms + h->next_on_dt; + h->cycle = CYCLE_ON; + } } void heater_off(struct heater *h) { h->st = HEATER_OFF; - h->turn_on = 0; } int @@ -33,15 +42,29 @@ heater_state(struct heater *h, unsigned long t_ms) /* Switch state if current time exceeds time limit * in the current state */ /* FIXME: Integer overflow can cause the heater to turn on forever */ - if (t_ms > h->next_t) { - if (h->turn_on) { - h->turn_on = 0; - h->next_t = t_ms + HEATER_T_OFF; - } else { - h->turn_on = 1; - h->next_t = t_ms + HEATER_T_ON; + while (1) { + int changed = 0; + if (h->cycle == CYCLE_ON) { + if (t_ms >= h->t0_off) { + h->cycle = CYCLE_OFF; + h->t0_on += HEATER_PERIOD; + changed = 1; + } + } else if (h->cycle == CYCLE_OFF) { + if (t_ms >= h->t0_on) { + /* Compute current cycle t0_off */ + h->cycle = CYCLE_ON; + h->t0_off = h->t0_on + h->next_on_dt; + changed = 1; + } } + + if (!changed) + break; } - return h->turn_on; + if (h->cycle == CYCLE_ON) + return 1; + + return 0; } diff --git a/barista/heater.h b/barista/heater.h index cf0aa16..e9f889d 100644 --- a/barista/heater.h +++ b/barista/heater.h @@ -13,13 +13,22 @@ enum heater_state { HEATER_ON, }; -struct heater { - enum heater_state st; - int turn_on; - unsigned long next_t; /* in ms */ +enum heater_cycle { + CYCLE_ON = 0, + CYCLE_OFF, }; -void heater_on(struct heater *h, unsigned long t_ms); +struct heater { + enum heater_state st; + enum heater_cycle cycle; + unsigned long t0_on; /* current cycle time on */ + unsigned long t0_off; /* current cycle time off */ + + /* Next cycle */ + unsigned long next_on_dt; /* in ms */ +}; + +void heater_on(struct heater *h, unsigned long t_ms, float duty); void heater_off(struct heater *h); int heater_state(struct heater *h, unsigned long t_ms); diff --git a/barista/overheat.c b/barista/overheat.c index e4b0d4f..b309568 100644 --- a/barista/overheat.c +++ b/barista/overheat.c @@ -18,22 +18,25 @@ overheat_input(struct overheat *o, unsigned long t_ms, float temp) if (delta < OVH_INTERVAL) return; - o->temp[o->next++] = temp; + /* If already go n samples, recompute delta and speed */ + if (o->n == OVH_NSAMPLES) { + float last_T = o->temp[o->next]; + float last_t = o->t[o->next]; + float dt = (float) (t_ms - last_t) * 1e-3; + o->delta = temp - last_T; + o->speed = o->delta / dt; + } + + /* Add the new sample */ + o->temp[o->next] = temp; + o->t[o->next] = t_ms; + o->next++; if (o->next >= OVH_NSAMPLES) o->next = 0; if (o->n < OVH_NSAMPLES) o->n++; - /* Recompute state */ - float tmin = 10000.0, tmax = 0.0; - for (int i = 0; i < o->n; i++) { - if (o->temp[i] < tmin) - tmin = o->temp[i]; - if (o->temp[i] > tmax) - tmax = o->temp[i]; - } - - o->delta = tmax - tmin; + o->last_time = t_ms; } float @@ -42,10 +45,16 @@ overheat_delta(struct overheat *o) return o->delta; } +float +overheat_speed(struct overheat *o) +{ + return o->speed; +} + int overheat_panic(struct overheat *o) { - if (o->delta > OVH_THRESHOLD) + if (o->speed > OVH_THRESHOLD) return 1; else return 0; diff --git a/barista/overheat.h b/barista/overheat.h index 3f4ef0e..9003fac 100644 --- a/barista/overheat.h +++ b/barista/overheat.h @@ -10,19 +10,22 @@ extern "C" { #define OVH_NSAMPLES 20 #define OVH_INTERVAL 200 /* ms */ -#define OVH_THRESHOLD 2.0 /* °C */ +#define OVH_THRESHOLD 2.0 /* °C / s */ struct overheat { unsigned long last_time; float temp[OVH_NSAMPLES]; + long unsigned t[OVH_NSAMPLES]; int next; int n; float delta; + float speed; }; void overheat_init(struct overheat *o); void overheat_input(struct overheat *o, unsigned long t_ms, float temp); float overheat_delta(struct overheat *o); +float overheat_speed(struct overheat *o); int overheat_panic(struct overheat *o); #ifdef __cplusplus diff --git a/barista/test/test_thermostat.c b/barista/test/test_thermostat.c new file mode 100644 index 0000000..c7e94d4 --- /dev/null +++ b/barista/test/test_thermostat.c @@ -0,0 +1,49 @@ +/* Copyright (c) 2025 Rodrigo Arias Mallo + * SPDX-License-Identifier: GPL-3.0-or-later */ + +#include +#include "overheat.h" +#include "thermostat.h" +#include "heater.h" + +/* Read a CSV from the stdin in the format + * + * skipping the first row (header). + * + * Outputs overheat state. */ + +#define MAX_LINE 1024 + +int main(void) +{ + char buf[MAX_LINE]; + fgets(buf, MAX_LINE, stdin); + + float t, temp; + int st; + + struct overheat ovh = { 0 }; + struct thermostat th = { 0 }; + struct heater heater = { 0 }; + + overheat_init(&ovh); + + for (int i = 0; scanf("%f %d %f", &t, &st, &temp) == 3; i++) { + if (i == 0) + thermostat_set(&th, 60.0); + + unsigned long t_ms = t * 1000; + + overheat_input(&ovh, t_ms, temp); + float delta = overheat_delta(&ovh); + float speed = overheat_speed(&ovh); + float u = thermostat_state(&th, temp, speed); + heater_on(&heater, t_ms, u); + int h = heater_state(&heater, t_ms); + int panic = overheat_panic(&ovh); + + printf("%8.3f %2d %6.2f %6.1f %8.3f %8.3f %d %s\n", t, st, temp, delta, speed, u, h, panic ? "PANIC" : "OK"); + } + + return 0; +} diff --git a/barista/thermostat.c b/barista/thermostat.c index 0438f50..5029e2d 100644 --- a/barista/thermostat.c +++ b/barista/thermostat.c @@ -3,9 +3,12 @@ #include "thermostat.h" +#define TEMP_MIN 35.0 /* °C */ #define DELTA_LOW 1.0 #define DELTA_HIGH 1.0 +#define T_ERR_MIN 35.0 /* °C */ + void thermostat_set(struct thermostat *th, float temp_target) { @@ -26,16 +29,36 @@ thermostat_off(struct thermostat *th) th->on = 0; } -int -thermostat_state(struct thermostat *th, float temp) +static float +pid(float T0, float T, float dT_dt) +{ + float err_min = 2.0; + + /* The rate of change of error is the same as the temperature, as they + * are only offset by a mostly constant value */ + float derr_dt = dT_dt; + + if ((T0 - T) < err_min) + return 0.0; + + float Kp = 1.0 / 20.0; + float Kd = - 1.0 / 3.0; + float u = Kp * (T0 - T) + Kd * dT_dt; + + if (u < 0.0) + u = 0.0; + else if (u > 1.0) + u = 1.0; + + return u; +} + +/* Return a value in [0, 1] to set the heater duty cycle */ +float +thermostat_state(struct thermostat *th, float T, float dT_dt) { if (th->st == THERMOSTAT_OFF) - return 0; + return 0.0; - if (th->on && temp > th->temp_max) - th->on = 0; - else if (!th->on && temp < th->temp_min) - th->on = 1; - - return th->on; + return pid(th->temp_target, T, dT_dt); } diff --git a/barista/thermostat.h b/barista/thermostat.h index c0d89d7..9a1f66a 100644 --- a/barista/thermostat.h +++ b/barista/thermostat.h @@ -23,7 +23,7 @@ struct thermostat { void thermostat_set(struct thermostat *th, float temp_target); void thermostat_off(struct thermostat *th); -int thermostat_state(struct thermostat *th, float temp); +float thermostat_state(struct thermostat *th, float T, float dT_dt); #ifdef __cplusplus }