Compare commits
4 Commits
402e1f4e43
...
8502ee3c5c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8502ee3c5c | ||
|
|
678f16111b | ||
|
|
3ea6ff0e14 | ||
|
|
d59dec50a8 |
3
barista/.gitignore
vendored
3
barista/.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
build/
|
build/
|
||||||
test_ntc
|
misc/
|
||||||
|
*.test
|
||||||
*.o
|
*.o
|
||||||
|
|||||||
@ -16,6 +16,10 @@ SRC=barista.ino \
|
|||||||
overheat.c \
|
overheat.c \
|
||||||
overheat.h
|
overheat.h
|
||||||
|
|
||||||
|
TESTS=ntc.test \
|
||||||
|
overheat.test \
|
||||||
|
led.test
|
||||||
|
|
||||||
# For host test programs
|
# For host test programs
|
||||||
CPPFLAGS=-I.
|
CPPFLAGS=-I.
|
||||||
LIBS=-lm
|
LIBS=-lm
|
||||||
@ -31,15 +35,15 @@ upload: $(HEX)
|
|||||||
serial:
|
serial:
|
||||||
picocom -b 9600 --lower-rts --lower-dtr /dev/ttyUSB0 --imap lfcrlf
|
picocom -b 9600 --lower-rts --lower-dtr /dev/ttyUSB0 --imap lfcrlf
|
||||||
|
|
||||||
test: test_ntc test_overheat
|
test: ntc.test overheat.test led.test
|
||||||
|
|
||||||
test_ntc: test/test_ntc.o ntc.o
|
%.test: test/test_%.o test/compat.o %.o
|
||||||
gcc $^ -o $@ $(LIBS)
|
gcc $^ -o $@ $(LIBS)
|
||||||
|
|
||||||
test_overheat: test/test_overheat.o overheat.o
|
thermostat.test: test/test_thermostat.o test/compat.o thermostat.o overheat.o heater.o
|
||||||
gcc $^ -o $@ $(LIBS)
|
gcc $^ -o $@ $(LIBS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f test/test_ntc.o ntc.o
|
rm -f $(TESTS) test/*.o *.o
|
||||||
|
|
||||||
.PHONY: test all clean
|
.PHONY: test all clean
|
||||||
|
|||||||
@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
#include "ntc.h"
|
#include "ntc.h"
|
||||||
#include "overheat.h"
|
#include "overheat.h"
|
||||||
|
#include "led.h"
|
||||||
|
#include "heater.h"
|
||||||
|
#include "thermostat.h"
|
||||||
#include "pinout.h"
|
#include "pinout.h"
|
||||||
#include <avr/wdt.h>
|
#include <avr/wdt.h>
|
||||||
|
|
||||||
@ -12,8 +15,7 @@ enum logic {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#define DEBOUNCE_TIME 20L /* ms */
|
#define DEBOUNCE_TIME 20L /* ms */
|
||||||
#define TEMP_MIN 40.0f /* C */
|
#define TEMP_HOT 75.0f /* C */
|
||||||
#define TEMP_MAX 50.0f /* C */
|
|
||||||
|
|
||||||
#define LED_MIN_VALUE 0
|
#define LED_MIN_VALUE 0
|
||||||
|
|
||||||
@ -50,7 +52,6 @@ enum buzz_state {
|
|||||||
BUZZ_ACTIVE,
|
BUZZ_ACTIVE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
int button_pin[MAX_BTN] = {
|
int button_pin[MAX_BTN] = {
|
||||||
[BTN_ON] = PIN_POWER_ON,
|
[BTN_ON] = PIN_POWER_ON,
|
||||||
[BTN_HOT] = PIN_HOT,
|
[BTN_HOT] = PIN_HOT,
|
||||||
@ -90,6 +91,12 @@ struct state {
|
|||||||
|
|
||||||
struct overheat overheat;
|
struct overheat overheat;
|
||||||
unsigned long overheat_t0;
|
unsigned long overheat_t0;
|
||||||
|
|
||||||
|
struct led red_led;
|
||||||
|
struct led green_led;
|
||||||
|
|
||||||
|
struct heater heater;
|
||||||
|
struct thermostat thermostat;
|
||||||
} g_st;
|
} g_st;
|
||||||
|
|
||||||
int read_input(int pin)
|
int read_input(int pin)
|
||||||
@ -187,27 +194,20 @@ void proc_buttons(struct state *state, const struct input *input)
|
|||||||
|
|
||||||
int red_min = 50;
|
int red_min = 50;
|
||||||
int red_state = red_min;
|
int red_state = red_min;
|
||||||
unsigned long brewing_max_time = 3000UL; /* 3 seconds */
|
unsigned long brewing_max_time = 30000UL; /* 30 seconds */
|
||||||
unsigned long cooling_time = 3000UL; /* 3 seconds */
|
unsigned long cooling_time = 3000UL; /* 3 seconds */
|
||||||
unsigned long overheat_time = 10000UL; /* 10 seconds */
|
unsigned long overheat_time = 10000UL; /* 10 seconds */
|
||||||
unsigned long max_heating_time = 60000UL; /* 60 seconds */
|
unsigned long max_heating_time = 60000UL; /* 60 seconds */
|
||||||
unsigned long max_idle_time = 10000UL; /* 10 seconds */
|
unsigned long max_idle_time = 300000UL; /* 300 seconds */
|
||||||
|
|
||||||
void proc_machine(struct state *st)
|
void proc_machine(struct state *st)
|
||||||
{
|
{
|
||||||
|
static unsigned long last_print_t = 0;
|
||||||
|
|
||||||
float temp = st->ntc_T;
|
float temp = st->ntc_T;
|
||||||
int on = (st->btn[BTN_ON].state == RELEASED);
|
int on = (st->btn[BTN_ON].state == RELEASED);
|
||||||
int brew_hot = (st->btn[BTN_HOT].state == PRESSED);
|
int brew_hot = (st->btn[BTN_HOT].state == PRESSED);
|
||||||
|
unsigned long t = millis();
|
||||||
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 the machine is overheating */
|
||||||
if (overheat_panic(&st->overheat)) {
|
if (overheat_panic(&st->overheat)) {
|
||||||
@ -235,7 +235,7 @@ void proc_machine(struct state *st)
|
|||||||
Serial.println("heating");
|
Serial.println("heating");
|
||||||
}
|
}
|
||||||
} else if (st->mstate == HEATING) {
|
} else if (st->mstate == HEATING) {
|
||||||
if (temp > TEMP_MAX) {
|
if (temp > TEMP_HOT) {
|
||||||
st->mstate = HOT;
|
st->mstate = HOT;
|
||||||
st->hot_t0 = millis();
|
st->hot_t0 = millis();
|
||||||
st->buzz_state = BUZZ_HEY;
|
st->buzz_state = BUZZ_HEY;
|
||||||
@ -313,64 +313,85 @@ void do_proc(struct state *st, const struct input *input)
|
|||||||
void
|
void
|
||||||
output_leds(const struct state *st)
|
output_leds(const struct state *st)
|
||||||
{
|
{
|
||||||
static int r = 0;
|
unsigned long t = millis();
|
||||||
static int g = 0;
|
|
||||||
|
|
||||||
if (st->mstate == HEATING || st->mstate == COOLING) {
|
if (st->mstate == SLEEPING) {
|
||||||
analogWrite(PIN_LED_RED, r);
|
led_off(&st->red_led);
|
||||||
setled(PIN_LED_GREEN, 0);
|
led_off(&st->green_led);
|
||||||
if (r >= 255)
|
} else if (st->mstate == HEATING) {
|
||||||
r = 0;
|
led_off(&st->red_led);
|
||||||
else
|
led_pattern(&st->green_led, t, 1000UL, "000123456789abcdefff");
|
||||||
r += 5;
|
} else if (st->mstate == COOLING) {
|
||||||
|
led_off(&st->red_led);
|
||||||
|
led_pattern(&st->green_led, t, 1000UL, "fffedcba987654321000");
|
||||||
} else if (st->mstate == HOT) {
|
} else if (st->mstate == HOT) {
|
||||||
setled(PIN_LED_RED, 0);
|
led_off(&st->red_led);
|
||||||
setled(PIN_LED_GREEN, 1);
|
led_on(&st->green_led);
|
||||||
r = 0;
|
|
||||||
} else if (st->mstate == PANIC_OVERHEAT) {
|
} else if (st->mstate == PANIC_OVERHEAT) {
|
||||||
setled(PIN_LED_RED, 1);
|
led_pattern(&st->red_led, t, 3000UL, "f0f0f0000000");
|
||||||
setled(PIN_LED_GREEN, 0);
|
led_pattern(&st->green_led, t, 3000UL, "000000f0f0f0");
|
||||||
} else if (st->mstate == BREWING_HOT || st->mstate == BREWING_COLD) {
|
} else if (st->mstate == BREWING_HOT || st->mstate == BREWING_COLD) {
|
||||||
setled(PIN_LED_RED, 0);
|
led_off(&st->red_led);
|
||||||
analogWrite(PIN_LED_GREEN, g);
|
led_pattern(&st->green_led, millis(), 2000UL, "0123456789abcdefedcba9876543210");
|
||||||
if (g >= 255)
|
|
||||||
g = 0;
|
|
||||||
else
|
|
||||||
g += 5;
|
|
||||||
} else {
|
} else {
|
||||||
setled(PIN_LED_RED, 0);
|
led_off(&st->red_led);
|
||||||
setled(PIN_LED_GREEN, 0);
|
led_off(&st->green_led);
|
||||||
r = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
analogWrite(PIN_LED_RED, led_level(&st->red_led, t));
|
||||||
|
analogWrite(PIN_LED_GREEN, led_level(&st->green_led, t));
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
output_heater(const struct state *st)
|
output_relays(const struct state *st)
|
||||||
{
|
{
|
||||||
if (st->mstate == HEATING || st->mstate == HOT || st->mstate == BREWING_HOT) {
|
unsigned long t = millis();
|
||||||
if (st->ntc_T < TEMP_MIN)
|
|
||||||
relay(PIN_HEAT, ON);
|
/* First configure thermostate */
|
||||||
else if (st->ntc_T > TEMP_MAX)
|
if (st->mstate == HEATING || st->mstate == HOT || st->mstate == BREWING_HOT)
|
||||||
relay(PIN_HEAT, OFF);
|
thermostat_set(&st->thermostat, TEMP_HOT+5.0);
|
||||||
} else {
|
else
|
||||||
|
thermostat_off(&st->thermostat);
|
||||||
|
|
||||||
|
/* Then update heater state from thermostate */
|
||||||
|
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 */
|
||||||
|
int heater_relay_st = heater_state(&st->heater, t);
|
||||||
|
if (heater_relay_st)
|
||||||
|
relay(PIN_HEAT, ON);
|
||||||
|
else
|
||||||
relay(PIN_HEAT, OFF);
|
relay(PIN_HEAT, OFF);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
int pump_relay_st = 0;
|
||||||
output_pump(const struct state *st)
|
|
||||||
{
|
|
||||||
if (st->mstate == BREWING_HOT || st->mstate == BREWING_COLD)
|
if (st->mstate == BREWING_HOT || st->mstate == BREWING_COLD)
|
||||||
|
pump_relay_st = 1;
|
||||||
|
|
||||||
|
if (pump_relay_st)
|
||||||
relay(PIN_PUMP, ON);
|
relay(PIN_PUMP, ON);
|
||||||
else
|
else
|
||||||
relay(PIN_PUMP, OFF);
|
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)
|
void do_output(const struct state *st)
|
||||||
{
|
{
|
||||||
output_leds(st);
|
output_leds(st);
|
||||||
output_heater(st);
|
output_relays(st);
|
||||||
output_pump(st);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
|
|||||||
70
barista/heater.c
Normal file
70
barista/heater.c
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||||
|
|
||||||
|
#include "heater.h"
|
||||||
|
|
||||||
|
#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, float duty)
|
||||||
|
{
|
||||||
|
unsigned long dt_on = duty * HEATER_MAX;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
heater_state(struct heater *h, unsigned long t_ms)
|
||||||
|
{
|
||||||
|
if (h->st == HEATER_OFF)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
/* Switch state if current time exceeds time limit
|
||||||
|
* in the current state */
|
||||||
|
/* FIXME: Integer overflow can cause the heater to turn on forever */
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h->cycle == CYCLE_ON)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
39
barista/heater.h
Normal file
39
barista/heater.h
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||||
|
|
||||||
|
#ifndef BARISTA_HEATER_H
|
||||||
|
#define BARISTA_HEATER_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum heater_state {
|
||||||
|
HEATER_OFF = 0,
|
||||||
|
HEATER_ON,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum heater_cycle {
|
||||||
|
CYCLE_ON = 0,
|
||||||
|
CYCLE_OFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* BARISTA_HEATER_H */
|
||||||
71
barista/led.c
Normal file
71
barista/led.c
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||||
|
|
||||||
|
#include "led.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void led_on(struct led *led)
|
||||||
|
{
|
||||||
|
led->mode = LED_ON;
|
||||||
|
}
|
||||||
|
|
||||||
|
void led_off(struct led *led)
|
||||||
|
{
|
||||||
|
led->mode = LED_OFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
void led_pattern(struct led *led, unsigned long t_ms, unsigned long period_ms, const char *pattern)
|
||||||
|
{
|
||||||
|
int n = strlen(pattern);
|
||||||
|
unsigned long step_ms = period_ms / n;
|
||||||
|
|
||||||
|
/* Don't change the current state */
|
||||||
|
if (led->mode == LED_PATTERN &&
|
||||||
|
led->step_ms == step_ms &&
|
||||||
|
led->pattern == pattern)
|
||||||
|
return;
|
||||||
|
|
||||||
|
led->mode = LED_PATTERN;
|
||||||
|
led->pat_i = 0;
|
||||||
|
led->pat_n = n;
|
||||||
|
led->pattern = pattern;
|
||||||
|
led->step_ms = step_ms;
|
||||||
|
led->t_ms = t_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return led level brightness in [0, 255] at current time */
|
||||||
|
int led_level(struct led *led, unsigned long t_ms)
|
||||||
|
{
|
||||||
|
if (led->mode == LED_OFF)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (led->mode == LED_ON)
|
||||||
|
return 255;
|
||||||
|
|
||||||
|
if (led->mode == LED_PATTERN) {
|
||||||
|
while (led->t_ms + led->step_ms < t_ms) {
|
||||||
|
led->t_ms += led->step_ms;
|
||||||
|
led->pat_i++;
|
||||||
|
if (led->pat_i >= led->pat_n)
|
||||||
|
led->pat_i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int c = led->pattern[led->pat_i];
|
||||||
|
int level;
|
||||||
|
|
||||||
|
if (c >= '0' && c <= '9')
|
||||||
|
level = 17 * (c - '0');
|
||||||
|
else
|
||||||
|
level = 17 * (10 + (c - 'a'));
|
||||||
|
|
||||||
|
if (level < 0)
|
||||||
|
level = 0;
|
||||||
|
else if (level > 255)
|
||||||
|
level = 255;
|
||||||
|
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unknown mode, turn off */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
35
barista/led.h
Normal file
35
barista/led.h
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||||
|
|
||||||
|
#ifndef BARISTA_LED_H
|
||||||
|
#define BARISTA_LED_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum led_mode {
|
||||||
|
LED_OFF = 0,
|
||||||
|
LED_ON,
|
||||||
|
LED_PATTERN,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct led {
|
||||||
|
enum led_mode mode;
|
||||||
|
const char *pattern;
|
||||||
|
int pat_i;
|
||||||
|
int pat_n;
|
||||||
|
unsigned long step_ms;
|
||||||
|
unsigned long t_ms;
|
||||||
|
};
|
||||||
|
|
||||||
|
void led_on(struct led *led);
|
||||||
|
void led_off(struct led *led);
|
||||||
|
void led_pattern(struct led *led, unsigned long t_ms, unsigned long period_ms, const char *pattern);
|
||||||
|
int led_level(struct led *led, unsigned long t_ms);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* BARISTA_LED_H */
|
||||||
@ -18,22 +18,25 @@ overheat_input(struct overheat *o, unsigned long t_ms, float temp)
|
|||||||
if (delta < OVH_INTERVAL)
|
if (delta < OVH_INTERVAL)
|
||||||
return;
|
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)
|
if (o->next >= OVH_NSAMPLES)
|
||||||
o->next = 0;
|
o->next = 0;
|
||||||
if (o->n < OVH_NSAMPLES)
|
if (o->n < OVH_NSAMPLES)
|
||||||
o->n++;
|
o->n++;
|
||||||
|
|
||||||
/* Recompute state */
|
o->last_time = t_ms;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float
|
float
|
||||||
@ -42,10 +45,16 @@ overheat_delta(struct overheat *o)
|
|||||||
return o->delta;
|
return o->delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float
|
||||||
|
overheat_speed(struct overheat *o)
|
||||||
|
{
|
||||||
|
return o->speed;
|
||||||
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
overheat_panic(struct overheat *o)
|
overheat_panic(struct overheat *o)
|
||||||
{
|
{
|
||||||
if (o->delta > OVH_THRESHOLD)
|
if (o->speed > OVH_THRESHOLD)
|
||||||
return 1;
|
return 1;
|
||||||
else
|
else
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@ -10,19 +10,22 @@ extern "C" {
|
|||||||
|
|
||||||
#define OVH_NSAMPLES 20
|
#define OVH_NSAMPLES 20
|
||||||
#define OVH_INTERVAL 200 /* ms */
|
#define OVH_INTERVAL 200 /* ms */
|
||||||
#define OVH_THRESHOLD 2.0 /* °C */
|
#define OVH_THRESHOLD 2.0 /* °C / s */
|
||||||
|
|
||||||
struct overheat {
|
struct overheat {
|
||||||
unsigned long last_time;
|
unsigned long last_time;
|
||||||
float temp[OVH_NSAMPLES];
|
float temp[OVH_NSAMPLES];
|
||||||
|
long unsigned t[OVH_NSAMPLES];
|
||||||
int next;
|
int next;
|
||||||
int n;
|
int n;
|
||||||
float delta;
|
float delta;
|
||||||
|
float speed;
|
||||||
};
|
};
|
||||||
|
|
||||||
void overheat_init(struct overheat *o);
|
void overheat_init(struct overheat *o);
|
||||||
void overheat_input(struct overheat *o, unsigned long t_ms, float temp);
|
void overheat_input(struct overheat *o, unsigned long t_ms, float temp);
|
||||||
float overheat_delta(struct overheat *o);
|
float overheat_delta(struct overheat *o);
|
||||||
|
float overheat_speed(struct overheat *o);
|
||||||
int overheat_panic(struct overheat *o);
|
int overheat_panic(struct overheat *o);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
13
barista/test/compat.c
Normal file
13
barista/test/compat.c
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||||
|
|
||||||
|
#include "compat.h"
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
unsigned long millis(void)
|
||||||
|
{
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
|
||||||
|
return ts.tv_sec * 1000UL + ts.tv_nsec / 1000000UL;
|
||||||
|
}
|
||||||
17
barista/test/compat.h
Normal file
17
barista/test/compat.h
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||||
|
|
||||||
|
#ifndef BARISTA_COMPAT_H
|
||||||
|
#define BARISTA_COMPAT_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
unsigned long millis(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* BARISTA_COMPAT_H */
|
||||||
42
barista/test/test_led.c
Normal file
42
barista/test/test_led.c
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "led.h"
|
||||||
|
#include "compat.h"
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
struct led led;
|
||||||
|
unsigned long t0 = millis();
|
||||||
|
unsigned long step = 1000UL;
|
||||||
|
|
||||||
|
led_pattern(&led, t0, step, "000123456789abcdefff");
|
||||||
|
|
||||||
|
int last_level = -1;
|
||||||
|
|
||||||
|
unsigned long last_t = millis();
|
||||||
|
while (1) {
|
||||||
|
unsigned long t = millis();
|
||||||
|
|
||||||
|
if (t - t0 >= 10000UL)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (t - last_t < 50UL)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int level = led_level(&led, millis());
|
||||||
|
printf("|");
|
||||||
|
for (int i = 0; i < 256; i+=4) {
|
||||||
|
if (i < level)
|
||||||
|
printf("=");
|
||||||
|
else
|
||||||
|
printf(" ");
|
||||||
|
}
|
||||||
|
printf("|\n");
|
||||||
|
last_level = level;
|
||||||
|
last_t = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
49
barista/test/test_thermostat.c
Normal file
49
barista/test/test_thermostat.c
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "overheat.h"
|
||||||
|
#include "thermostat.h"
|
||||||
|
#include "heater.h"
|
||||||
|
|
||||||
|
/* Read a CSV from the stdin in the format
|
||||||
|
* <time_in_seconds> <state> <temp_in_C>
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
64
barista/thermostat.c
Normal file
64
barista/thermostat.c
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||||
|
|
||||||
|
#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)
|
||||||
|
{
|
||||||
|
if (th->st == THERMOSTAT_ON && th->temp_target == temp_target)
|
||||||
|
return;
|
||||||
|
|
||||||
|
th->st = THERMOSTAT_ON;
|
||||||
|
th->temp_target = temp_target;
|
||||||
|
th->temp_min = temp_target - DELTA_LOW;
|
||||||
|
th->temp_max = temp_target + DELTA_HIGH;
|
||||||
|
th->on = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
thermostat_off(struct thermostat *th)
|
||||||
|
{
|
||||||
|
th->st = THERMOSTAT_OFF;
|
||||||
|
th->on = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.0;
|
||||||
|
|
||||||
|
return pid(th->temp_target, T, dT_dt);
|
||||||
|
}
|
||||||
32
barista/thermostat.h
Normal file
32
barista/thermostat.h
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/* Copyright (c) 2025 Rodrigo Arias Mallo <rodarima@gmail.com>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||||
|
|
||||||
|
#ifndef BARISTA_THERMOSTAT_H
|
||||||
|
#define BARISTA_THERMOSTAT_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum thermostat_state {
|
||||||
|
THERMOSTAT_OFF = 0,
|
||||||
|
THERMOSTAT_ON,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct thermostat {
|
||||||
|
enum thermostat_state st;
|
||||||
|
float temp_target;
|
||||||
|
float temp_min;
|
||||||
|
float temp_max;
|
||||||
|
int on;
|
||||||
|
};
|
||||||
|
|
||||||
|
void thermostat_set(struct thermostat *th, float temp_target);
|
||||||
|
void thermostat_off(struct thermostat *th);
|
||||||
|
float thermostat_state(struct thermostat *th, float T, float dT_dt);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* BARISTA_THERMOSTAT_H */
|
||||||
Loading…
x
Reference in New Issue
Block a user