ESP-IDF v5.3.2 firmware for the CrowPanel 2.1" rotary display. Builds clean;
nothing has been run on hardware yet.
Hardware verified against Elecrow's own source for this board and
cross-checked against their ESPHome config, per PRD 8.1. Three corrections to
the PRD's pinout, all documented in docs/HARDWARE.md:
- touch is CST826 at 0x15, not CST816 (chip ID reads 0x11, not 0xB4)
- the ST7701S needs a 3-wire SPI bus on GPIO 16/2/1 that the PRD omitted;
without it the init sequence cannot be sent and the panel stays black
- the backlight is GPIO6
Two further departures from the PRD, both forced by facts on the ground:
- Transport is MQTT, not HTTP. zigbee2mqtt has no REST endpoint for live
sensor state. Subscribing to the broker also gets retained topics, so
every room's last reading arrives on connect rather than after a poll.
- The furnace is 2-wire R/W with no C, not 3-wire. The board needs its own
5V supply. The thermostat's 5 CPH setting is enforced as a minimum
12-minute period between heat-call starts, which a minimum off-time alone
does not achieve.
All of PRD section 5 is implemented in the relay layer itself: fail-open,
watchdog-gated output, max on-time, min off-time, cycle-rate limit, startup
lockout, and the stale-data fallback ladder down to open-loop freeze
protection. The one gap that cannot be closed in software — the PCF8574 not
resetting when the ESP32 does — is documented rather than papered over.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
188 lines
4.9 KiB
C
188 lines
4.9 KiB
C
#include "lvgl_port.h"
|
|
|
|
#include "cst826.h"
|
|
#include "display.h"
|
|
#include "esp_check.h"
|
|
#include "esp_log.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/semphr.h"
|
|
#include "freertos/task.h"
|
|
|
|
static const char *TAG = "lvgl";
|
|
|
|
#define TICK_PERIOD_MS 2
|
|
#define TASK_STACK 8192
|
|
#define TASK_PRIO 3
|
|
#define TASK_MAX_DELAY_MS 20
|
|
#define TASK_MIN_DELAY_MS 2
|
|
|
|
static SemaphoreHandle_t s_lock;
|
|
static lv_display_t *s_disp;
|
|
static lv_indev_t *s_touch_indev;
|
|
|
|
/* ---------------------------------------------------------------- tick -- */
|
|
|
|
static void tick_cb(void *arg)
|
|
{
|
|
(void)arg;
|
|
lv_tick_inc(TICK_PERIOD_MS);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- flush -- */
|
|
|
|
static void flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
|
|
{
|
|
(void)area;
|
|
|
|
/*
|
|
* px_map is one of the panel's own framebuffers, so this does not copy —
|
|
* the RGB driver just switches which buffer is being scanned out and waits
|
|
* for the switch to take effect.
|
|
*/
|
|
esp_lcd_panel_handle_t panel = display_panel();
|
|
esp_lcd_panel_draw_bitmap(panel, 0, 0, DISPLAY_H_RES, DISPLAY_V_RES, px_map);
|
|
|
|
lv_display_flush_ready(disp);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- input -- */
|
|
|
|
static void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data)
|
|
{
|
|
(void)indev;
|
|
|
|
cst826_touch_t t;
|
|
cst826_read(&t);
|
|
|
|
if (t.pressed) {
|
|
data->point.x = t.x;
|
|
data->point.y = t.y;
|
|
data->state = LV_INDEV_STATE_PRESSED;
|
|
} else {
|
|
data->state = LV_INDEV_STATE_RELEASED;
|
|
}
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- task -- */
|
|
|
|
static void lvgl_task(void *arg)
|
|
{
|
|
(void)arg;
|
|
|
|
ESP_LOGI(TAG, "service task running");
|
|
|
|
while (1) {
|
|
uint32_t next = TASK_MAX_DELAY_MS;
|
|
|
|
if (lvgl_port_lock(-1)) {
|
|
next = lv_timer_handler();
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
if (next > TASK_MAX_DELAY_MS) {
|
|
next = TASK_MAX_DELAY_MS;
|
|
} else if (next < TASK_MIN_DELAY_MS) {
|
|
next = TASK_MIN_DELAY_MS;
|
|
}
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(next));
|
|
}
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- init -- */
|
|
|
|
esp_err_t lvgl_port_init(void)
|
|
{
|
|
if (s_disp) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
void *fb0 = NULL, *fb1 = NULL;
|
|
esp_err_t err = display_get_framebuffers(&fb0, &fb1);
|
|
if (err != ESP_OK || !fb0 || !fb1) {
|
|
ESP_LOGE(TAG, "no framebuffers — display_init() must run first");
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
s_lock = xSemaphoreCreateRecursiveMutex();
|
|
if (!s_lock) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
/*
|
|
* PRD 8.2: lv_init() before creating any LVGL object. Calling
|
|
* lv_display_create() first faults with LoadProhibited inside the TLSF
|
|
* allocator, which is a confusing way to discover an ordering bug.
|
|
*/
|
|
if (!lv_is_initialized()) {
|
|
lv_init();
|
|
}
|
|
|
|
s_disp = lv_display_create(DISPLAY_H_RES, DISPLAY_V_RES);
|
|
if (!s_disp) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
lv_display_set_color_format(s_disp, LV_COLOR_FORMAT_RGB565);
|
|
lv_display_set_flush_cb(s_disp, flush_cb);
|
|
|
|
size_t fb_bytes = (size_t)DISPLAY_H_RES * DISPLAY_V_RES * 2;
|
|
lv_display_set_buffers(s_disp, fb0, fb1, fb_bytes,
|
|
LV_DISPLAY_RENDER_MODE_FULL);
|
|
|
|
/* Touch, if the controller answered. The thermostat is fully usable from
|
|
* the encoder alone, so a missing touch panel is not fatal. */
|
|
if (cst826_is_ready()) {
|
|
s_touch_indev = lv_indev_create();
|
|
lv_indev_set_type(s_touch_indev, LV_INDEV_TYPE_POINTER);
|
|
lv_indev_set_read_cb(s_touch_indev, touch_read_cb);
|
|
lv_indev_set_display(s_touch_indev, s_disp);
|
|
} else {
|
|
ESP_LOGW(TAG, "touch unavailable — encoder only");
|
|
}
|
|
|
|
const esp_timer_create_args_t tick_args = {
|
|
.callback = tick_cb,
|
|
.name = "lv_tick",
|
|
};
|
|
esp_timer_handle_t tick_timer;
|
|
ESP_RETURN_ON_ERROR(esp_timer_create(&tick_args, &tick_timer),
|
|
TAG, "tick timer");
|
|
ESP_RETURN_ON_ERROR(
|
|
esp_timer_start_periodic(tick_timer, TICK_PERIOD_MS * 1000),
|
|
TAG, "tick start");
|
|
|
|
if (xTaskCreate(lvgl_task, "lvgl", TASK_STACK, NULL, TASK_PRIO, NULL)
|
|
!= pdPASS) {
|
|
ESP_LOGE(TAG, "task create failed");
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "up: %dx%d, full-refresh double buffered",
|
|
DISPLAY_H_RES, DISPLAY_V_RES);
|
|
return ESP_OK;
|
|
}
|
|
|
|
bool lvgl_port_lock(int timeout_ms)
|
|
{
|
|
if (!s_lock) {
|
|
return false;
|
|
}
|
|
TickType_t wait = (timeout_ms < 0) ? portMAX_DELAY
|
|
: pdMS_TO_TICKS(timeout_ms);
|
|
return xSemaphoreTakeRecursive(s_lock, wait) == pdTRUE;
|
|
}
|
|
|
|
void lvgl_port_unlock(void)
|
|
{
|
|
if (s_lock) {
|
|
xSemaphoreGiveRecursive(s_lock);
|
|
}
|
|
}
|
|
|
|
lv_display_t *lvgl_port_display(void)
|
|
{
|
|
return s_disp;
|
|
}
|