mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 10:55:06 +00:00
- CL-32 features added: - Hardware revision detection - v0.3 and v0.4 keyboard - v0.4 power supply - Inlined epd library (was a module) and made changes to the device definition (don't do full update after 5 small updates) - Statusbar: add support for inverted colors - e-paper driver improvements and warning fixes - Implemented gpio_controller_get_level() - Keyboard support improvements (unicode for tca drivers)
376 lines
11 KiB
C
376 lines
11 KiB
C
#include "epaper.h"
|
|
#include "epaper_panel.h"
|
|
#include "epaper_spi.h"
|
|
#include "esp_log.h"
|
|
#include "esp_heap_caps.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static const char *TAG = "epaper";
|
|
|
|
/*=============================================================================
|
|
* Device Structure
|
|
*============================================================================*/
|
|
|
|
struct epd_device {
|
|
epd_config_t config;
|
|
epd_spi_t spi;
|
|
const epd_panel_desc_t *panel;
|
|
const epd_controller_ops_t *ops;
|
|
uint16_t width;
|
|
uint16_t height;
|
|
uint32_t buffer_size;
|
|
uint8_t *framebuffer;
|
|
bool initialized;
|
|
bool partial_ready;
|
|
uint32_t partial_count;
|
|
};
|
|
|
|
/*=============================================================================
|
|
* Device Access Functions (used by controllers)
|
|
*============================================================================*/
|
|
|
|
epd_spi_t* epd_get_spi(epd_device_t *dev)
|
|
{
|
|
return dev ? &dev->spi : NULL;
|
|
}
|
|
|
|
uint16_t epd_get_width(epd_device_t *dev)
|
|
{
|
|
return dev ? dev->width : 0;
|
|
}
|
|
|
|
uint16_t epd_get_height(epd_device_t *dev)
|
|
{
|
|
return dev ? dev->height : 0;
|
|
}
|
|
|
|
const epd_panel_desc_t* epd_get_panel(epd_device_t *dev)
|
|
{
|
|
return dev ? dev->panel : NULL;
|
|
}
|
|
|
|
bool epd_is_partial_ready(epd_device_t *dev)
|
|
{
|
|
return dev ? dev->partial_ready : false;
|
|
}
|
|
|
|
/*=============================================================================
|
|
* Initialization
|
|
*============================================================================*/
|
|
|
|
esp_err_t epd_init(const epd_config_t *config, epd_handle_t *handle)
|
|
{
|
|
if (!config || !handle) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
// Get panel descriptor from registry
|
|
const epd_panel_desc_t *panel = epd_get_panel_desc(config->panel.type);
|
|
if (!panel) {
|
|
ESP_LOGE(TAG, "Unknown panel type: %d", config->panel.type);
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
|
|
// Get controller operations
|
|
const epd_controller_ops_t *ops = epd_get_controller_ops(panel->ctrl);
|
|
if (!ops) {
|
|
ESP_LOGE(TAG, "Unknown controller type: %d", panel->ctrl);
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
|
|
// Allocate device
|
|
epd_device_t *dev = calloc(1, sizeof(epd_device_t));
|
|
if (!dev) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
// Copy config and set references
|
|
memcpy(&dev->config, config, sizeof(epd_config_t));
|
|
dev->panel = panel;
|
|
dev->ops = ops;
|
|
|
|
// Set dimensions (allow override from config)
|
|
dev->width = config->panel.width > 0 ? config->panel.width : panel->width;
|
|
dev->height = config->panel.height > 0 ? config->panel.height : panel->height;
|
|
|
|
// Calculate buffer size using panel's bits_per_pixel
|
|
dev->buffer_size = epd_calc_buffer_size(dev->width, dev->height, panel->bits_per_pixel);
|
|
|
|
// Allocate framebuffer (prefer PSRAM for large buffers)
|
|
if (dev->buffer_size > 32768) {
|
|
dev->framebuffer = heap_caps_malloc(dev->buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
|
} else {
|
|
dev->framebuffer = heap_caps_malloc(dev->buffer_size, MALLOC_CAP_DMA);
|
|
}
|
|
|
|
if (!dev->framebuffer) {
|
|
ESP_LOGE(TAG, "Failed to allocate framebuffer (%lu bytes)", dev->buffer_size);
|
|
free(dev);
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Framebuffer allocated: %lu bytes", dev->buffer_size);
|
|
memset(dev->framebuffer, 0xFF, dev->buffer_size); // White
|
|
|
|
// Initialize SPI
|
|
esp_err_t ret = epd_spi_init(&dev->spi, &config->pins, &config->spi);
|
|
if (ret != ESP_OK) {
|
|
free(dev->framebuffer);
|
|
free(dev);
|
|
return ret;
|
|
}
|
|
|
|
// Initialize panel via controller
|
|
ret = dev->ops->init(dev);
|
|
if (ret != ESP_OK) {
|
|
epd_spi_deinit(&dev->spi);
|
|
free(dev->framebuffer);
|
|
free(dev);
|
|
return ret;
|
|
}
|
|
|
|
dev->initialized = true;
|
|
*handle = dev;
|
|
|
|
ESP_LOGI(TAG, "Initialized %s (%dx%d)", panel->name, dev->width, dev->height);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t epd_deinit(epd_handle_t handle)
|
|
{
|
|
if (!handle) return ESP_ERR_INVALID_ARG;
|
|
|
|
epd_device_t *dev = handle;
|
|
|
|
if (dev->ops && dev->ops->sleep) {
|
|
dev->ops->sleep(dev);
|
|
}
|
|
|
|
epd_spi_deinit(&dev->spi);
|
|
|
|
if (dev->framebuffer) {
|
|
free(dev->framebuffer);
|
|
}
|
|
|
|
free(dev);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/*=============================================================================
|
|
* Info & Status
|
|
*============================================================================*/
|
|
|
|
esp_err_t epd_get_info(epd_handle_t handle, epd_panel_info_t *info)
|
|
{
|
|
if (!handle || !info) return ESP_ERR_INVALID_ARG;
|
|
|
|
epd_device_t *dev = handle;
|
|
info->width = dev->width;
|
|
info->height = dev->height;
|
|
info->buffer_size = dev->buffer_size;
|
|
info->color_mode = dev->panel->color_mode;
|
|
return ESP_OK;
|
|
}
|
|
|
|
bool epd_is_busy(epd_handle_t handle)
|
|
{
|
|
if (!handle) return false;
|
|
epd_device_t *dev = handle;
|
|
return epd_spi_is_busy(&dev->spi) != 0;
|
|
}
|
|
|
|
esp_err_t epd_wait_busy(epd_handle_t handle, uint32_t timeout_ms)
|
|
{
|
|
if (!handle) return ESP_ERR_INVALID_ARG;
|
|
epd_device_t *dev = handle;
|
|
epd_spi_wait_busy(&dev->spi, timeout_ms);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/*=============================================================================
|
|
* Display Operations
|
|
*============================================================================*/
|
|
|
|
esp_err_t epd_clear(epd_handle_t handle)
|
|
{
|
|
return epd_fill(handle, 0xFF);
|
|
}
|
|
|
|
esp_err_t epd_fill(epd_handle_t handle, uint8_t color)
|
|
{
|
|
if (!handle) return ESP_ERR_INVALID_ARG;
|
|
|
|
epd_device_t *dev = handle;
|
|
memset(dev->framebuffer, color, dev->buffer_size);
|
|
|
|
return epd_update(handle, dev->framebuffer, EPD_UPDATE_FULL);
|
|
}
|
|
|
|
esp_err_t epd_update(epd_handle_t handle, const uint8_t *buffer, epd_update_mode_t mode)
|
|
{
|
|
if (!handle || !buffer) return ESP_ERR_INVALID_ARG;
|
|
|
|
epd_device_t *dev = handle;
|
|
esp_err_t ret;
|
|
|
|
// Check capabilities and fallback if needed
|
|
if (mode == EPD_UPDATE_PARTIAL && !epd_panel_has_cap(dev->panel, EPD_CAP_PARTIAL)) {
|
|
ESP_LOGD(TAG, "Partial not supported, using full refresh");
|
|
mode = EPD_UPDATE_FULL;
|
|
}
|
|
if (mode == EPD_UPDATE_FAST && !epd_panel_has_cap(dev->panel, EPD_CAP_FAST)) {
|
|
ESP_LOGD(TAG, "Fast not supported, using full refresh");
|
|
mode = EPD_UPDATE_FULL;
|
|
}
|
|
|
|
// Periodic full refresh: repeated partial writes on the same GDDRAM base
|
|
// image accumulate ghosting, so every Nth partial refresh the whole panel.
|
|
// The conversion redraws the identical current buffer, so the base image
|
|
// stays valid and subsequent partials can continue immediately.
|
|
bool converted_to_full = false;
|
|
if (mode == EPD_UPDATE_PARTIAL && dev->partial_ready && dev->panel->full_refresh_interval > 0) {
|
|
if (++dev->partial_count >= dev->panel->full_refresh_interval) {
|
|
dev->partial_count = 0;
|
|
mode = EPD_UPDATE_FULL;
|
|
converted_to_full = true;
|
|
ESP_LOGI(TAG, "Periodic full refresh after %d partial updates", dev->panel->full_refresh_interval);
|
|
}
|
|
}
|
|
|
|
// Copy to framebuffer (callers may pass the internal framebuffer itself)
|
|
if (buffer != dev->framebuffer) {
|
|
memcpy(dev->framebuffer, buffer, dev->buffer_size);
|
|
}
|
|
|
|
// For partial mode on first call, do a full refresh to set base image
|
|
if (mode == EPD_UPDATE_PARTIAL && !dev->partial_ready) {
|
|
if (!dev->initialized) {
|
|
ret = dev->ops->init(dev);
|
|
if (ret != ESP_OK) return ret;
|
|
dev->initialized = true;
|
|
}
|
|
|
|
ret = dev->ops->write_ram(dev, buffer, dev->buffer_size);
|
|
if (ret != ESP_OK) return ret;
|
|
|
|
ret = dev->ops->update(dev, EPD_UPDATE_FULL);
|
|
if (ret != ESP_OK) return ret;
|
|
|
|
dev->partial_ready = true;
|
|
ESP_LOGI(TAG, "Partial mode ready");
|
|
return ESP_OK;
|
|
}
|
|
|
|
// Full refresh resets partial mode (unless it was the periodic conversion,
|
|
// which refreshed the same base image and keeps partial mode valid)
|
|
if (mode == EPD_UPDATE_FULL && !converted_to_full) {
|
|
dev->partial_ready = false;
|
|
if (!dev->initialized) {
|
|
ret = dev->ops->init(dev);
|
|
if (ret != ESP_OK) return ret;
|
|
dev->initialized = true;
|
|
}
|
|
}
|
|
|
|
// Write RAM and update
|
|
ret = dev->ops->write_ram(dev, buffer, dev->buffer_size);
|
|
if (ret != ESP_OK) return ret;
|
|
|
|
return dev->ops->update(dev, mode);
|
|
}
|
|
|
|
esp_err_t epd_update_partial(epd_handle_t handle, uint16_t x, uint16_t y,
|
|
uint16_t w, uint16_t h, const uint8_t *buffer)
|
|
{
|
|
if (!handle || !buffer) return ESP_ERR_INVALID_ARG;
|
|
|
|
epd_device_t *dev = handle;
|
|
|
|
if (!epd_panel_has_cap(dev->panel, EPD_CAP_PARTIAL)) {
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
|
|
if (!dev->ops->write_ram_partial) {
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
|
|
esp_err_t ret = dev->ops->write_ram_partial(dev, x, y, w, h, buffer);
|
|
if (ret != ESP_OK) return ret;
|
|
|
|
return dev->ops->update(dev, EPD_UPDATE_PARTIAL);
|
|
}
|
|
|
|
esp_err_t epd_set_base_image(epd_handle_t handle, const uint8_t *buffer)
|
|
{
|
|
if (!handle || !buffer) return ESP_ERR_INVALID_ARG;
|
|
|
|
epd_device_t *dev = handle;
|
|
memcpy(dev->framebuffer, buffer, dev->buffer_size);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/*=============================================================================
|
|
* Power Management
|
|
*============================================================================*/
|
|
|
|
esp_err_t epd_sleep(epd_handle_t handle)
|
|
{
|
|
if (!handle) return ESP_ERR_INVALID_ARG;
|
|
|
|
epd_device_t *dev = handle;
|
|
if (dev->ops->sleep) {
|
|
// Deep sleep clears GDDRAM, so the base image for partial updates is
|
|
// lost; the next partial update must re-establish it with a full refresh.
|
|
dev->partial_ready = false;
|
|
dev->partial_count = 0;
|
|
dev->initialized = false;
|
|
return dev->ops->sleep(dev);
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t epd_wake(epd_handle_t handle)
|
|
{
|
|
if (!handle) return ESP_ERR_INVALID_ARG;
|
|
|
|
epd_device_t *dev = handle;
|
|
if (dev->ops->wake) {
|
|
esp_err_t ret = dev->ops->wake(dev);
|
|
if (ret == ESP_OK) {
|
|
dev->initialized = true;
|
|
// Wake re-initializes the panel and clears GDDRAM, so the next
|
|
// partial update must re-establish the base with a full refresh.
|
|
dev->partial_ready = false;
|
|
dev->partial_count = 0;
|
|
}
|
|
return ret;
|
|
}
|
|
return dev->ops->init(dev);
|
|
}
|
|
|
|
/*=============================================================================
|
|
* Framebuffer Access
|
|
*============================================================================*/
|
|
|
|
uint8_t* epd_get_framebuffer(epd_handle_t handle)
|
|
{
|
|
if (!handle) return NULL;
|
|
return handle->framebuffer;
|
|
}
|
|
|
|
esp_err_t epd_flush_framebuffer(epd_handle_t handle, epd_update_mode_t mode)
|
|
{
|
|
if (!handle) return ESP_ERR_INVALID_ARG;
|
|
return epd_update(handle, handle->framebuffer, mode);
|
|
}
|
|
|
|
esp_err_t epd_lvgl_flush(epd_handle_t handle, const void *area, const uint8_t *px_map)
|
|
{
|
|
if (!handle || !px_map) return ESP_ERR_INVALID_ARG;
|
|
|
|
epd_device_t *dev = handle;
|
|
memcpy(dev->framebuffer, px_map, dev->buffer_size);
|
|
return ESP_OK;
|
|
}
|