2026-08-07 19:30:22 +02:00

354 lines
13 KiB
C++

// SPDX-License-Identifier: Apache-2.0
#include <drivers/esp_epaper.h>
#include <esp_epaper_module.h>
#include "esp_epaper_rotate.h"
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/esp32_spi.h>
#include <tactility/drivers/spi_controller.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <epaper.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <cstdlib>
#include <cstring>
constexpr auto* TAG = "esp_epaper";
#define GET_CONFIG(device) (static_cast<const EspEpaperConfig*>((device)->config))
/** Width/height overrides above this are rejected as nonsense. */
constexpr uint16_t MAX_PANEL_DIMENSION = 2048;
struct EspEpaperInternal {
/** Opaque esp_epaper device, owns the panel's pins and SPI device. */
epd_handle_t epd;
epd_panel_info_t info;
/** Scratch buffer in native panel layout, for rotated frames (rotation != 0). */
uint8_t* rotate_buffer;
/** Serializes panel/SPI access between draw_bitmap and power state changes. */
SemaphoreHandle_t panel_mutex;
/** disp_on_off state; the panel is in deep sleep while false. */
bool display_on;
};
static uint16_t esp_epaper_get_display_width(const EspEpaperInternal* internal, uint8_t rotation) {
return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.height : internal->info.width;
}
static uint16_t esp_epaper_get_display_height(const EspEpaperInternal* internal, uint8_t rotation) {
return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.width : internal->info.height;
}
static bool resolve_panel_type(const char* name, epd_panel_type_t* out_type) {
struct PanelMapping {
const char* name;
epd_panel_type_t type;
};
static constexpr PanelMapping kPanelMappings[] = {
{ "gdey0154d67", EPD_PANEL_GDEY0154D67 },
{ "gdep073e01", EPD_PANEL_GDEP073E01 },
{ "gdey037f51", EPD_PANEL_GDEY037F51 },
{ "gdey029t71h", EPD_PANEL_GDEY029T71H },
{ "ssd16xx-154", EPD_PANEL_SSD16XX_154 },
{ "ssd16xx-213", EPD_PANEL_SSD16XX_213 },
{ "ssd16xx-266", EPD_PANEL_SSD16XX_266 },
{ "ssd16xx-270", EPD_PANEL_SSD16XX_270 },
{ "ssd16xx-290", EPD_PANEL_SSD16XX_290 },
{ "ssd16xx-370", EPD_PANEL_SSD16XX_370 },
{ "ssd16xx-420", EPD_PANEL_SSD16XX_420 },
};
for (const auto& mapping : kPanelMappings) {
if (std::strcmp(name, mapping.name) == 0) {
*out_type = mapping.type;
return true;
}
}
return false;
}
static error_t esp_epaper_reset(Device* device) {
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
// epd_wake() toggles the reset pin and re-runs the full init sequence.
const esp_err_t ret = epd_wake(internal->epd);
// epd_wake() re-inits the panel, so it is awake (and drawable) again.
if (ret == ESP_OK) {
internal->display_on = true;
}
xSemaphoreGive(internal->panel_mutex);
return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t esp_epaper_init(Device* device) {
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
const esp_err_t ret = epd_wake(internal->epd);
if (ret == ESP_OK) {
internal->display_on = true;
}
xSemaphoreGive(internal->panel_mutex);
return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
// LVGL only ever calls this with the full frame: DISPLAY_COLOR_FORMAT_MONOCHROME forces
// LV_DISPLAY_RENDER_MODE_FULL in the generic kernel LVGL bridge (lvgl_display.c), and FULL mode
// only presents (calls draw_bitmap) once per render cycle, with the complete 0,0..hres,vres rect.
// hres/vres are the rotated (display) resolution reported by get_resolution_*; color_data is
// row-major, MSB-first 1bpp (LVGL's LV_COLOR_FORMAT_I1 with the palette header already stripped by
// the caller); bit 1 = white, bit 0 = black. epd_update() expects exactly that layout and polarity.
static error_t esp_epaper_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device);
const uint16_t display_width = esp_epaper_get_display_width(internal, config->rotation);
const uint16_t display_height = esp_epaper_get_display_height(internal, config->rotation);
if (x_start != 0 || y_start != 0 || x_end != display_width || y_end != display_height) {
LOG_W(TAG, "draw_bitmap: only full-frame draws are supported (got %ld,%ld..%ld,%ld)", (long)x_start, (long)y_start, (long)x_end, (long)y_end);
return ERROR_NOT_SUPPORTED;
}
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
if (!internal->display_on) {
// Display is off (deep sleep). Drop the frame; the mismatch is fine because the next
// power-on triggers a full refresh of the current render anyway.
xSemaphoreGive(internal->panel_mutex);
return ERROR_NONE;
}
const uint8_t* source;
if (config->rotation == 0) {
source = static_cast<const uint8_t*>(color_data);
} else {
memset(internal->rotate_buffer, 0, internal->info.buffer_size);
esp_epaper_rotate_frame(static_cast<const uint8_t*>(color_data), internal->rotate_buffer,
internal->info.width, internal->info.height, config->rotation);
source = internal->rotate_buffer;
}
const esp_err_t ret = epd_update(internal->epd, source, config->update_mode);
xSemaphoreGive(internal->panel_mutex);
if (ret != ESP_OK) {
LOG_E(TAG, "epd_update failed: %s", esp_err_to_name(ret));
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
static error_t esp_epaper_disp_on_off(Device* device, bool on_off) {
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
if (on_off == internal->display_on) {
xSemaphoreGive(internal->panel_mutex);
return ERROR_NONE;
}
bool ok = true;
if (on_off) {
if (epd_wake(internal->epd) != ESP_OK) {
LOG_E(TAG, "epd_wake failed");
ok = false;
}
} else {
if (epd_sleep(internal->epd) != ESP_OK) {
LOG_E(TAG, "epd_sleep failed");
ok = false;
}
}
if (ok) {
internal->display_on = on_off;
}
xSemaphoreGive(internal->panel_mutex);
return ok ? ERROR_NONE : ERROR_RESOURCE;
}
static DisplayColorFormat esp_epaper_get_color_format(Device*) {
return DISPLAY_COLOR_FORMAT_MONOCHROME;
}
static uint16_t esp_epaper_get_resolution_x(Device* device) {
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
return esp_epaper_get_display_width(internal, GET_CONFIG(device)->rotation);
}
static uint16_t esp_epaper_get_resolution_y(Device* device) {
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
return esp_epaper_get_display_height(internal, GET_CONFIG(device)->rotation);
}
static void esp_epaper_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
*out_buffer = nullptr;
}
static uint8_t esp_epaper_get_frame_buffer_count(Device*) {
return 0;
}
static const DisplayApi esp_epaper_display_api = {
.capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH,
.reset = esp_epaper_reset,
.init = esp_epaper_init,
.draw_bitmap = esp_epaper_draw_bitmap,
.mirror = nullptr,
.swap_xy = nullptr,
.get_swap_xy = nullptr,
.get_mirror_x = nullptr,
.get_mirror_y = nullptr,
.set_gap = nullptr,
.get_gap_x = nullptr,
.get_gap_y = nullptr,
.invert_color = nullptr,
.disp_on_off = esp_epaper_disp_on_off,
.disp_sleep = nullptr,
.get_color_format = esp_epaper_get_color_format,
.get_resolution_x = esp_epaper_get_resolution_x,
.get_resolution_y = esp_epaper_get_resolution_y,
.get_frame_buffer = esp_epaper_get_frame_buffer,
.get_frame_buffer_count = esp_epaper_get_frame_buffer_count,
.get_backlight = nullptr,
.has_capability = nullptr,
};
static void free_internal(EspEpaperInternal* internal) {
if (internal->epd != nullptr) {
epd_deinit(internal->epd);
}
if (internal->rotate_buffer != nullptr) {
free(internal->rotate_buffer);
}
if (internal->panel_mutex != nullptr) {
vSemaphoreDelete(internal->panel_mutex);
}
free(internal);
}
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
const auto* spi_config = static_cast<const Esp32SpiConfig*>(parent->config);
const auto* config = GET_CONFIG(device);
epd_panel_type_t panel_type;
if (!resolve_panel_type(config->panel_type, &panel_type)) {
LOG_E(TAG, "Unknown panel type: %s", config->panel_type);
return ERROR_NOT_SUPPORTED;
}
if (config->clock_speed_hz == 0) {
LOG_E(TAG, "Invalid clock_speed_hz (0)");
return ERROR_NOT_SUPPORTED;
}
if (config->update_mode > EPD_UPDATE_PARTIAL) {
LOG_E(TAG, "Invalid update_mode %d (expected %d-%d)", (int)config->update_mode, (int)EPD_UPDATE_FULL, (int)EPD_UPDATE_PARTIAL);
return ERROR_NOT_SUPPORTED;
}
if (config->width > MAX_PANEL_DIMENSION || config->height > MAX_PANEL_DIMENSION) {
LOG_E(TAG, "Invalid panel size %ux%u (maximum %ux%u)", (unsigned)config->width, (unsigned)config->height, (unsigned)MAX_PANEL_DIMENSION, (unsigned)MAX_PANEL_DIMENSION);
return ERROR_NOT_SUPPORTED;
}
epd_config_t epd_config = EPD_CONFIG_DEFAULT();
epd_config.pins.busy = config->pin_busy.pin;
epd_config.pins.rst = config->pin_reset.pin;
epd_config.pins.dc = config->pin_dc.pin;
epd_config.pins.cs = config->pin_cs.pin;
epd_config.pins.sck = spi_config->pin_sclk.pin;
epd_config.pins.mosi = spi_config->pin_mosi.pin;
epd_config.spi.host = spi_config->host;
epd_config.spi.speed_hz = config->clock_speed_hz;
epd_config.panel.type = panel_type;
epd_config.panel.width = config->width;
epd_config.panel.height = config->height;
epd_handle_t epd = nullptr;
if (epd_init(&epd_config, &epd) != ESP_OK) {
LOG_E(TAG, "epd_init failed");
return ERROR_RESOURCE;
}
auto* internal = static_cast<EspEpaperInternal*>(calloc(1, sizeof(EspEpaperInternal)));
if (internal == nullptr) {
epd_deinit(epd);
return ERROR_OUT_OF_MEMORY;
}
internal->epd = epd;
internal->panel_mutex = xSemaphoreCreateMutex();
if (internal->panel_mutex == nullptr) {
free_internal(internal);
return ERROR_OUT_OF_MEMORY;
}
if (epd_get_info(epd, &internal->info) != ESP_OK) {
LOG_E(TAG, "epd_get_info failed");
free_internal(internal);
return ERROR_RESOURCE;
}
if (internal->info.color_mode != EPD_COLOR_BW) {
LOG_E(TAG, "Panel color mode %d is not supported by the kernel display bridge (monochrome only)", internal->info.color_mode);
free_internal(internal);
return ERROR_NOT_SUPPORTED;
}
if (config->rotation > 3) {
LOG_E(TAG, "Invalid rotation %u (expected 0-3)", config->rotation);
free_internal(internal);
return ERROR_NOT_SUPPORTED;
}
if (config->rotation != 0) {
internal->rotate_buffer = static_cast<uint8_t*>(malloc(internal->info.buffer_size));
if (internal->rotate_buffer == nullptr) {
LOG_E(TAG, "Failed to allocate %lu-byte rotation buffer", internal->info.buffer_size);
free_internal(internal);
return ERROR_OUT_OF_MEMORY;
}
}
internal->display_on = true;
device_set_driver_data(device, internal);
LOG_I(TAG, "Started %ux%u panel (buffer %lu bytes, rotation %u)", internal->info.width, internal->info.height, internal->info.buffer_size, config->rotation);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
if (internal->display_on) {
// Leave the panel in deep sleep.
epd_sleep(internal->epd);
internal->display_on = false;
}
xSemaphoreGive(internal->panel_mutex);
free_internal(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
Driver esp_epaper_driver = {
.name = "esp_epaper",
.compatible = (const char*[]) { "tuanpmt,esp-epaper", nullptr },
.start_device = start,
.stop_device = stop,
.api = &esp_epaper_display_api,
.device_type = &DISPLAY_TYPE,
.owner = &esp_epaper_module,
.internal = nullptr
};