Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ else()
set(RMAKER_PATH ${CMAKE_CURRENT_LIST_DIR}/../..)
endif(DEFINED ENV{RMAKER_PATH})

# Add RainMaker components and other common application components
set(EXTRA_COMPONENT_DIRS ${RMAKER_PATH}/components/esp-insights/components ${RMAKER_PATH}/components ${RMAKER_PATH}/examples/common ${CMAKE_CURRENT_LIST_DIR}/components)
# RainMaker components + example helpers (app_wifi, app_insights, …)
set(EXTRA_COMPONENT_DIRS ${RMAKER_PATH}/components ${RMAKER_PATH}/examples/common ${CMAKE_CURRENT_LIST_DIR}/components)

set(PROJECT_VER "1.0")
# Firmware version: root VERSION file (see README)
file(STRINGS "${CMAKE_CURRENT_LIST_DIR}/VERSION" PROJECT_VER LIMIT_COUNT 1)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_LIST_DIR}/VERSION")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(switch)
project(espresso)
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# project subdirectory.
#

PROJECT_NAME := switch
PROJECT_VER := 1.0
PROJECT_NAME := espresso
PROJECT_VER := $(shell sed -n '1p' "$(dir $(abspath $(lastword $(MAKEFILE_LIST))))VERSION" | tr -d '\r')

# Add RainMaker components and other common application components
EXTRA_COMPONENT_DIRS += $(PROJECT_PATH)/../../components $(PROJECT_PATH)/../common
Expand Down
121 changes: 115 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ This project implements a PID/P controller to effectively control the temperatur
Another feature implemented is the control of the water pump and pre-infusion settings. Combined, these functionalities allow for
improved espresso extraction and consistency.

March 2024
https://github.com/raffarost/espresso
**Firmware version** is defined in the repository root file [`VERSION`](VERSION); CMake and the legacy `Makefile` set `PROJECT_VER` from it, and the running image reports it via ESP-IDF (`esp_app_get_description()->version`).

April 2026 — https://github.com/raffarost/espresso

Raffael Rostagno
raffael.rostagno@gmail.com
Expand Down Expand Up @@ -107,10 +108,118 @@ To calibrate the PID controller, the following symbols can be optimized for each

To calibrate the P controller, the following vectors can be changed:

```c
/* delta °C: -10 0 0.5 1 2 4 10 25 50 70 */
static float deltaBkp[BKP_NUM] = {-10, 0, 0.5, 1, 2, 4, 10, 25, 50, 70};
static float controlSet[BKP_NUM] = { 0, 0, 5, 5, 8, 20, 30, 50, 80, 100};
```

The first vector is the temperature difference between setpoint and actual reading (°C).
The second vector is the driver power value (0–100) applied for each (interpolated) delta.

##### TRIAC driver hardware constraint

The dimmer driver uses phase-angle control with a **fixed gate pulse width of 4 timer steps**.
For driver values 1–4, the gate pulse extends beyond the AC half-cycle boundary, re-latching
the TRIAC at the start of the next half-cycle and delivering ~25 % average power regardless
of the intended setting.

**Minimum safe value in `controlSet[]` is 5. Never use values 1–4.**
Use 0 (heater fully off) or ≥ 5.

##### Actual power delivery vs. driver value

Phase-angle control is highly nonlinear. The driver value does **not** map linearly to
delivered power — most of the useful range is concentrated above 20.

| Driver value | Approx. actual power (% of rated) |
|---|---|
| 5 | ~0.1 % |
| 10 | ~0.6 % |
| 15 | ~2 % |
| 20 | ~5 % |
| 25 | ~9 % |
| 30 | ~15 % |
| 40 | ~31 % |
| 50 | ~50 % |
| 60 | ~69 % |
| 80 | ~95 % |
| 99 | ~100 % |

Values below ~15 deliver negligible heat and are only useful in `controlSet[]` as a defined
floor to avoid the gate-overflow bug (see above). Practical maintenance and warmup
calibration should use values in the 15–99 range.

#### Adaptive warmup (`ADAPTIVE_WARMUP_ENABLE`)

When `CONTROL_TYPE == LOOKUP`, the adaptive warmup strategy is enabled automatically. It replaces the old fixed-duty power toggle with a self-tuning mechanism that adjusts heater duty during the final approach to setpoint.

##### Dither step table

Within `DELTA_PWR_TOGGLE` (10 °C below setpoint), the heater is pulsed at a duty cycle determined by the current *step index* (0–4):

| Step | Duty | Period / on-count |
|------|-------|-------------------|
| 0 | 100 % | 1 / 1 |
| 1 | 75 % | 4 / 3 |
| 2 | 50 % | 2 / 1 — **default** |
| 3 | 25 % | 4 / 1 |

Within `TEMP_DELTA` (2 °C) of setpoint, the step is overridden to `POWER_NEAR_SETPOINT` (default: step 3, 33 %) regardless of the learned index, to prevent overshoot and overly aggressive fighting between the stall and overshoot detectors near the target.

The current duty is reported in the RainMaker UI as **Power factor (dither)**. The step index is persisted to NVS (`dithStep`) and restored on each reboot, so the machine retains its seasonal calibration across power cycles.

##### Overshoot detection

After the approach phase, if the temperature exceeds `tempSetpoint + TEMP_DELTA`, an overshoot is latched. The peak excursion above setpoint is tracked until the temperature drops back to setpoint, then the step is incremented (less power next warmup):

- Peak < 5 °C → `step + 1`
- Peak ≥ 5 °C (`OVERSHOOT_SEVERE_PEAK_C`) → `step + 2`

The step is clamped at 4 (25 %). The NVS value is written after the commit.

##### Warmup stall detection

During the approach window, the detector tracks the best (smallest) delta seen since entering the window. A 30-second timer (`DITHER_STALL_SEC`) is reset every time a new delta minimum is recorded. If the timer expires — meaning temperature has not improved in 30 s — a **warmup stall** is declared:

- `step - 1` (more heater power) is applied immediately in RAM.
- The NVS write is deferred until the end of the warmup cycle (overshoot settle or soft-settle).
- The **Status** diagnostic field shows `"Warmup stall"` while the stall is active, clearing automatically once the setpoint is reached.

The stall detector is inhibited while `temp_stuck_diag` is active (frozen sensor) to avoid false step adjustments based on stale readings.

##### Self-correcting behaviour

The two detectors naturally balance each other:

- Stall fires → `step - 1` → more power → may cause overshoot → `step + 1` → net 0 (learning discarded)
- Stall fires → `step - 1` → severe overshoot → `step + 2` → net +1 (learned: needs less power overall)

The step is reset to `DITHER_STEP_DEFAULT` (50 %) via the **Reset power factor** button in the RainMaker UI.

#### Pump heat buffer calibration

When the pump is active (pre-infusion, brew, or flush), cold water entering the boiler causes a
temperature drop. To compensate, a time-indexed power profile is applied instead of the idle
temperature controller:

```
static float deltaBkp[BKP_NUM] = {-10, 0, 0.5, 1, 2, 4, 10, 25, 50, 100};
static float controlSet[BKP_NUM] = { 0, 0, 1, 1, 1, 1, 1, 80, 100, 100};
static int pumpOnHeatBuff[PUMP_ON_HEAT_BUFF_LEN] = {
100, 100, 100, 100, 80, 80, 50, 50, 40, 40, 40, 50, 50, 50, 50, 70, 70, 70, 70, 70
};
```

The first vector corresponds to the temperature difference between target and actual reading.
The second vector is the power factor (0 to 100%) to apply for each (interpolated) delta.
Each entry is the heater power (0–100%) applied at the corresponding second of pump-on time.
Index 0 is the first second, index 1 the second, and so on up to `PUMP_ON_HEAT_BUFF_LEN - 1`.
The vector length must be at least as long as the maximum configured brew time and flush time.

##### Extraction flow dynamics

The power profile follows the natural resistance the coffee puck offers to water flow during extraction:

- **Phase 1 (~4 s)** — Free-flow: water saturates the dry puck with little resistance. Flow rate is high and the boiler cools quickly, so high heater power is needed.
- **Phase 2 (~4 s)** — Puck compressing: swelling grounds start restricting flow. Less water moves through, so less compensation is required.
- **Phase 3 (~6 s)** — Maximum compression: the puck is fully saturated and tightly packed. Flow is most restricted and the boiler loses less heat, so power demand is at its lowest.
- **Phase 4 (~6 s+)** — Puck deterioration: channels begin to form as the grounds break down, flow gradually recovers, and power demand rises again.

This is a general profile — it can vary significantly depending on basket type (single vs. double) and dimensions, coffee dose, grind size, and temperature sensor position within the machine.
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.0.23
29 changes: 19 additions & 10 deletions components/esp32-triac-dimmer-driver/esp32-triac-dimmer-driver.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@

#include "esp32-triac-dimmer-driver.h"

static void isr_ext(void *arg);
static bool onTimerISR(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx);

static const char *TAG = "Esp32idfDimmer";

int pulseWidth = 4;
Expand All @@ -16,8 +19,6 @@ static int toggleReload = 25;
volatile bool _initDone = false;
volatile int _steps = 0;

static unsigned long long lastEdgeTime = 0;

static dimmertyp *dimmer[ALL_DIMMERS];
volatile bool firstSetup = false;
volatile uint16_t dimPower[ALL_DIMMERS];
Expand Down Expand Up @@ -62,13 +63,13 @@ dimmertyp *createDimmer(gpio_num_t user_dimmer_pin, gpio_num_t zc_dimmer_pin)
return dimmer[current_dim - 1];
}

#define TIMER_BASE_CLK 1 * 1000 * 1000, // 1MHz, 1 tick = 1us
#define TIMER_BASE_CLK (1000000u) /* 1 MHz, 1 tick = 1 us */

/**
* @brief Configure the timer alarm
*/
void config_alarm(gptimer_handle_t *timer, int ACfreq)
{
void config_alarm(gptimer_handle_t timer, int ACfreq)
{
/*self regulation 50/60 Hz*/
double m_calculated_interval = (1 / (double)(ACfreq * 2)) / 100;
ESP_LOGI(TAG, "Interval between wave calculated for frequency : %3dHz = %5f", ACfreq, m_calculated_interval);
Expand All @@ -81,13 +82,13 @@ void config_alarm(gptimer_handle_t *timer, int ACfreq)
.flags.auto_reload_on_alarm = true, // enable auto-reload
};
ESP_LOGI(TAG, "Timer configuration - set alarm action");
ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer, &alarm_config));
ESP_ERROR_CHECK(gptimer_set_alarm_action(timer, &alarm_config));

gptimer_event_callbacks_t cbs = {
.on_alarm = onTimerISR, // register user callback
};
ESP_LOGI(TAG, "Timer configuration - register event callbacks");
ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer, &cbs, NULL));
ESP_ERROR_CHECK(gptimer_register_event_callbacks(timer, &cbs, NULL));

ESP_LOGI(TAG, "Timer configuration - configuration completed");
}
Expand Down Expand Up @@ -155,6 +156,7 @@ void ext_int_init(dimmertyp *ptr)
ESP_LOGI(TAG, "Triac command configuration");

gpio_set_direction(dimOutPin[ptr->current_num], GPIO_MODE_OUTPUT);
gpio_set_level(dimOutPin[ptr->current_num], 0);
ESP_LOGI(TAG, "Triac command configuration - completed");
}

Expand Down Expand Up @@ -324,14 +326,19 @@ static int k;
#if DEBUG_ISR_TIMER == ISR_DEBUG_ON
static int counter = 0;
#endif
/* Execution on timer event */
static void IRAM_ATTR onTimerISR(void *para)
/* Execution on timer event (GPTimer ISR contract: IDF 5.x+) */
static bool IRAM_ATTR onTimerISR(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx)
{
(void)timer;
(void)edata;
(void)user_ctx;
BaseType_t hp_task_awoken = pdFALSE;

/**********************************/
#if DEBUG_ISR_TIMER == ISR_DEBUG_ON
counter++;
uint32_t info = (uint32_t)counter;
xQueueSendFromISR(timer_event_queue, &info, NULL);
xQueueSendFromISR(timer_event_queue, &info, &hp_task_awoken);
#endif

toggleCounter++;
Expand Down Expand Up @@ -391,4 +398,6 @@ static void IRAM_ATTR onTimerISR(void *para)
}
if (toggleCounter >= toggleReload)
toggleCounter = 1;

return hp_task_awoken == pdTRUE;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "driver/periph_ctrl.h"
#include "driver/gptimer.h"
#include "freertos/task.h"
#include "math.h"
Expand Down Expand Up @@ -80,9 +79,6 @@ void port_init(dimmertyp *ptr);
void config_timer(int freq);
void ext_int_init(dimmertyp *ptr);

static void IRAM_ATTR isr_ext(void* arg);
static void IRAM_ATTR onTimerISR(void* arg);

extern unsigned long long getAbsTime1us(void);

#endif
Loading