Three bugs and two UX defects found on review, plus hardening: - The control loop fed the task watchdog then slept exactly the watchdog period (both 10s), tripping the TWDT fractionally late on every single iteration. Sleep is now sliced 1s with a reset per slice. Also set CONFIG_ESP_TASK_WDT_PANIC=y - the sdkconfig comment claimed a hung task panics the chip, but nothing actually made that true. - The LVGL flush called draw_bitmap (which only queues the framebuffer switch) and released LVGL immediately, letting it render the next frame into the buffer still being scanned out - the exact PRD 8.2 race the double-buffer design exists to prevent. display_present_frame() now blocks on a vsync handshake until the swap has latched. - WiFi reconnect slept up to 30s inside the system event handler, stalling all queued WiFi/IP events. Moved to an esp_timer. - The setpoint label, arc, and mode/room lines rendered the control loop's copy of state, which refreshes every 10s - every knob detent looked dead for up to 10 seconds. The UI now reads settings live; when the fallback ladder clamps the setpoint it shows both values. Room-select focus now shows which room is being selected. - z2m ISO_8601_local timestamps carry a UTC offset the parser ignored, which would shift apparent reading age by the local offset (5-6h in the US) and permanently trip the fallback ladder. Offset is now honoured. - Suppressed the cosmetic "control loop stopped" fault in the window between relay_init and the control task's first call-in at boot. - Explicit pullups on the encoder A/B lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
188 lines
5.7 KiB
C
188 lines
5.7 KiB
C
#include "encoder.h"
|
|
|
|
#include "driver/gpio.h"
|
|
#include "driver/pulse_cnt.h"
|
|
#include "esp_check.h"
|
|
#include "esp_log.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "pcf8574.h"
|
|
|
|
static const char *TAG = "encoder";
|
|
|
|
/*
|
|
* Full quadrature decode counts 4 edges per mechanical detent on an EC28A.
|
|
* PCNT counts in raw edges; we divide.
|
|
*/
|
|
#define COUNTS_PER_DETENT 4
|
|
|
|
#define PCNT_HIGH_LIMIT 1000
|
|
#define PCNT_LOW_LIMIT (-1000)
|
|
#define GLITCH_FILTER_NS 1000
|
|
|
|
#define BUTTON_POLL_MS 20
|
|
#define BUTTON_DEBOUNCE_N 3
|
|
#define LONG_PRESS_MS 800
|
|
|
|
static pcnt_unit_handle_t s_unit;
|
|
static int s_residual; /* raw counts not yet worth a detent */
|
|
|
|
static volatile encoder_button_event_t s_btn_event;
|
|
static volatile bool s_btn_down;
|
|
|
|
static void button_task(void *arg)
|
|
{
|
|
(void)arg;
|
|
|
|
int stable = 0;
|
|
bool debounced = false; /* true = pressed */
|
|
int64_t pressed_at = 0;
|
|
bool long_fired = false;
|
|
|
|
while (1) {
|
|
bool level = true;
|
|
/* P5 is INPUT_PULLUP: low means pressed. */
|
|
bool pressed = (pcf8574_read(PCF_PIN_ENCODER_SW, &level) == ESP_OK)
|
|
&& !level;
|
|
|
|
if (pressed == debounced) {
|
|
stable = 0;
|
|
} else if (++stable >= BUTTON_DEBOUNCE_N) {
|
|
stable = 0;
|
|
debounced = pressed;
|
|
s_btn_down = pressed;
|
|
|
|
if (pressed) {
|
|
pressed_at = esp_timer_get_time();
|
|
long_fired = false;
|
|
} else if (!long_fired) {
|
|
/* Click reported on release, so a long press never also
|
|
* produces a click. */
|
|
s_btn_event = ENCODER_BTN_CLICK;
|
|
}
|
|
}
|
|
|
|
if (debounced && !long_fired &&
|
|
(esp_timer_get_time() - pressed_at) > (LONG_PRESS_MS * 1000)) {
|
|
long_fired = true;
|
|
s_btn_event = ENCODER_BTN_LONG_PRESS;
|
|
}
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(BUTTON_POLL_MS));
|
|
}
|
|
}
|
|
|
|
esp_err_t encoder_init(void)
|
|
{
|
|
pcnt_unit_config_t unit_cfg = {
|
|
.high_limit = PCNT_HIGH_LIMIT,
|
|
.low_limit = PCNT_LOW_LIMIT,
|
|
};
|
|
ESP_RETURN_ON_ERROR(pcnt_new_unit(&unit_cfg, &s_unit), TAG, "pcnt unit");
|
|
|
|
pcnt_glitch_filter_config_t filter = {
|
|
.max_glitch_ns = GLITCH_FILTER_NS,
|
|
};
|
|
ESP_RETURN_ON_ERROR(pcnt_unit_set_glitch_filter(s_unit, &filter),
|
|
TAG, "glitch filter");
|
|
|
|
/* Two channels, each watching one edge and using the other line as the
|
|
* direction control — the standard full-quadrature setup. */
|
|
pcnt_chan_config_t chan_a_cfg = {
|
|
.edge_gpio_num = ENCODER_PIN_A,
|
|
.level_gpio_num = ENCODER_PIN_B,
|
|
};
|
|
pcnt_channel_handle_t chan_a = NULL;
|
|
ESP_RETURN_ON_ERROR(pcnt_new_channel(s_unit, &chan_a_cfg, &chan_a),
|
|
TAG, "chan a");
|
|
|
|
pcnt_chan_config_t chan_b_cfg = {
|
|
.edge_gpio_num = ENCODER_PIN_B,
|
|
.level_gpio_num = ENCODER_PIN_A,
|
|
};
|
|
pcnt_channel_handle_t chan_b = NULL;
|
|
ESP_RETURN_ON_ERROR(pcnt_new_channel(s_unit, &chan_b_cfg, &chan_b),
|
|
TAG, "chan b");
|
|
|
|
ESP_RETURN_ON_ERROR(
|
|
pcnt_channel_set_edge_action(chan_a,
|
|
PCNT_CHANNEL_EDGE_ACTION_DECREASE,
|
|
PCNT_CHANNEL_EDGE_ACTION_INCREASE),
|
|
TAG, "chan a edge");
|
|
ESP_RETURN_ON_ERROR(
|
|
pcnt_channel_set_level_action(chan_a,
|
|
PCNT_CHANNEL_LEVEL_ACTION_KEEP,
|
|
PCNT_CHANNEL_LEVEL_ACTION_INVERSE),
|
|
TAG, "chan a level");
|
|
ESP_RETURN_ON_ERROR(
|
|
pcnt_channel_set_edge_action(chan_b,
|
|
PCNT_CHANNEL_EDGE_ACTION_INCREASE,
|
|
PCNT_CHANNEL_EDGE_ACTION_DECREASE),
|
|
TAG, "chan b edge");
|
|
ESP_RETURN_ON_ERROR(
|
|
pcnt_channel_set_level_action(chan_b,
|
|
PCNT_CHANNEL_LEVEL_ACTION_KEEP,
|
|
PCNT_CHANNEL_LEVEL_ACTION_INVERSE),
|
|
TAG, "chan b level");
|
|
|
|
/*
|
|
* The vendor's Arduino example uses bare INPUT, implying external pullups
|
|
* on the board, but their ESPHome config enables internal ones as well.
|
|
* Enabling them here costs nothing and guards against a floating line
|
|
* reading as phantom rotation. (Done after channel creation so the PCNT
|
|
* driver's own GPIO setup cannot undo it.)
|
|
*/
|
|
gpio_pullup_en(ENCODER_PIN_A);
|
|
gpio_pullup_en(ENCODER_PIN_B);
|
|
|
|
ESP_RETURN_ON_ERROR(pcnt_unit_enable(s_unit), TAG, "enable");
|
|
ESP_RETURN_ON_ERROR(pcnt_unit_clear_count(s_unit), TAG, "clear");
|
|
ESP_RETURN_ON_ERROR(pcnt_unit_start(s_unit), TAG, "start");
|
|
|
|
if (xTaskCreate(button_task, "enc_btn", 3072, NULL, 4, NULL) != pdPASS) {
|
|
ESP_LOGE(TAG, "button task create failed");
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "ready: A=%d B=%d, button on expander P%d",
|
|
ENCODER_PIN_A, ENCODER_PIN_B, PCF_PIN_ENCODER_SW);
|
|
return ESP_OK;
|
|
}
|
|
|
|
int encoder_take_delta(void)
|
|
{
|
|
if (!s_unit) {
|
|
return 0;
|
|
}
|
|
|
|
int raw = 0;
|
|
if (pcnt_unit_get_count(s_unit, &raw) != ESP_OK) {
|
|
return 0;
|
|
}
|
|
pcnt_unit_clear_count(s_unit);
|
|
|
|
/*
|
|
* Carry the remainder rather than discarding it. Dropping it would lose a
|
|
* detent whenever a turn straddles two polls, which shows up as a knob
|
|
* that occasionally ignores you.
|
|
*/
|
|
s_residual += raw;
|
|
int detents = s_residual / COUNTS_PER_DETENT;
|
|
s_residual -= detents * COUNTS_PER_DETENT;
|
|
|
|
return detents;
|
|
}
|
|
|
|
encoder_button_event_t encoder_take_button_event(void)
|
|
{
|
|
encoder_button_event_t e = s_btn_event;
|
|
s_btn_event = ENCODER_BTN_NONE;
|
|
return e;
|
|
}
|
|
|
|
bool encoder_button_is_down(void)
|
|
{
|
|
return s_btn_down;
|
|
}
|