M5Stack PaperS3 improvements and other bug fixes (#512)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES FastEPD TactilityCore Tactility
|
||||
REQUIRES Tactility epdiy esp_lvgl_port
|
||||
PRIV_REQUIRES esp_timer
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
# EPDiy Display Driver
|
||||
|
||||
A display driver for e-paper/e-ink displays using the EPDiy library. This driver provides LVGL integration and high-level display management for EPD panels.
|
||||
@@ -0,0 +1,472 @@
|
||||
#include "EpdiyDisplay.h"
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_timer.h>
|
||||
|
||||
constexpr const char* TAG = "EpdiyDisplay";
|
||||
|
||||
bool EpdiyDisplay::s_hlInitialized = false;
|
||||
EpdiyHighlevelState EpdiyDisplay::s_hlState = {};
|
||||
|
||||
EpdiyDisplay::EpdiyDisplay(std::unique_ptr<Configuration> inConfiguration)
|
||||
: configuration(std::move(inConfiguration)) {
|
||||
check(configuration != nullptr);
|
||||
check(configuration->board != nullptr);
|
||||
check(configuration->display != nullptr);
|
||||
}
|
||||
|
||||
EpdiyDisplay::~EpdiyDisplay() {
|
||||
if (lvglDisplay != nullptr) {
|
||||
stopLvgl();
|
||||
}
|
||||
if (initialized) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
bool EpdiyDisplay::start() {
|
||||
if (initialized) {
|
||||
LOG_W(TAG, "Already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Initialize EPDiy low-level hardware
|
||||
epd_init(
|
||||
configuration->board,
|
||||
configuration->display,
|
||||
configuration->initOptions
|
||||
);
|
||||
|
||||
// Set rotation BEFORE initializing highlevel state
|
||||
epd_set_rotation(configuration->rotation);
|
||||
LOG_I(TAG, "Display rotation set to %d", configuration->rotation);
|
||||
|
||||
// Initialize the high-level API only once — epd_hl_init() sets a static flag internally
|
||||
// and there is no matching epd_hl_deinit(). Reuse the existing state on subsequent starts.
|
||||
if (!s_hlInitialized) {
|
||||
s_hlState = epd_hl_init(configuration->waveform);
|
||||
if (s_hlState.front_fb == nullptr) {
|
||||
LOG_E(TAG, "Failed to initialize EPDiy highlevel state");
|
||||
epd_deinit();
|
||||
return false;
|
||||
}
|
||||
s_hlInitialized = true;
|
||||
LOG_I(TAG, "EPDiy highlevel state initialized");
|
||||
} else {
|
||||
LOG_I(TAG, "Reusing existing EPDiy highlevel state");
|
||||
}
|
||||
|
||||
highlevelState = s_hlState;
|
||||
framebuffer = epd_hl_get_framebuffer(&highlevelState);
|
||||
|
||||
initialized = true;
|
||||
LOG_I(TAG, "EPDiy initialized successfully (%dx%d native, %dx%d rotated)", epd_width(), epd_height(), epd_rotated_display_width(), epd_rotated_display_height());
|
||||
|
||||
// Perform initial clear to ensure clean state
|
||||
LOG_I(TAG, "Performing initial screen clear...");
|
||||
clearScreen();
|
||||
LOG_I(TAG, "Screen cleared");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EpdiyDisplay::stop() {
|
||||
if (!initialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lvglDisplay != nullptr) {
|
||||
stopLvgl();
|
||||
}
|
||||
|
||||
// Power off the display
|
||||
if (powered) {
|
||||
setPowerOn(false);
|
||||
}
|
||||
|
||||
// Deinitialize EPDiy low-level hardware.
|
||||
// The HL framebuffers (s_hlState) are intentionally kept alive: epd_hl_init() has no
|
||||
// matching deinit and sets an internal already_initialized flag, so the HL state must
|
||||
// persist across stop()/start() cycles and be reused on the next start().
|
||||
epd_deinit();
|
||||
|
||||
// Clear instance references to HL state (the static s_hlState still owns the memory)
|
||||
highlevelState = {};
|
||||
framebuffer = nullptr;
|
||||
|
||||
initialized = false;
|
||||
LOG_I(TAG, "EPDiy deinitialized (HL state preserved for restart)");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void EpdiyDisplay::setPowerOn(bool turnOn) {
|
||||
if (!initialized) {
|
||||
LOG_W(TAG, "Cannot change power state - EPD not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
if (powered == turnOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (turnOn) {
|
||||
epd_poweron();
|
||||
powered = true;
|
||||
LOG_D(TAG, "EPD power on");
|
||||
} else {
|
||||
epd_poweroff();
|
||||
powered = false;
|
||||
LOG_D(TAG, "EPD power off");
|
||||
}
|
||||
}
|
||||
|
||||
// LVGL functions
|
||||
bool EpdiyDisplay::startLvgl() {
|
||||
if (lvglDisplay != nullptr) {
|
||||
LOG_W(TAG, "LVGL already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!initialized) {
|
||||
LOG_E(TAG, "EPD not initialized, call start() first");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the native display dimensions
|
||||
uint16_t width = epd_width();
|
||||
uint16_t height = epd_height();
|
||||
|
||||
LOG_I(TAG, "Creating LVGL display: %dx%d (EPDiy rotation: %d)", width, height, configuration->rotation);
|
||||
|
||||
// Create LVGL display with native dimensions
|
||||
lvglDisplay = lv_display_create(width, height);
|
||||
if (lvglDisplay == nullptr) {
|
||||
LOG_E(TAG, "Failed to create LVGL display");
|
||||
return false;
|
||||
}
|
||||
|
||||
// EPD uses 4-bit grayscale (16 levels)
|
||||
// Map to LVGL's L8 format (8-bit grayscale)
|
||||
lv_display_set_color_format(lvglDisplay, LV_COLOR_FORMAT_L8);
|
||||
auto lv_rotation = epdRotationToLvgl(configuration->rotation);
|
||||
lv_display_set_rotation(lvglDisplay, lv_rotation);
|
||||
|
||||
// Allocate LVGL draw buffer (L8 format: 1 byte per pixel)
|
||||
size_t draw_buffer_size = static_cast<size_t>(width) * height;
|
||||
|
||||
lvglDrawBuffer = static_cast<uint8_t*>(heap_caps_malloc(draw_buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
||||
if (lvglDrawBuffer == nullptr) {
|
||||
LOG_W(TAG, "PSRAM allocation failed for draw buffer, falling back to internal memory");
|
||||
lvglDrawBuffer = static_cast<uint8_t*>(heap_caps_malloc(draw_buffer_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL));
|
||||
}
|
||||
|
||||
if (lvglDrawBuffer == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate draw buffer");
|
||||
lv_display_delete(lvglDisplay);
|
||||
lvglDisplay = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pre-allocate 4-bit packed pixel buffer used in flushInternal (avoids per-flush heap allocation)
|
||||
// Row stride with odd-width padding: (width + 1) / 2 bytes per row
|
||||
size_t packed_buffer_size = static_cast<size_t>((width + 1) / 2) * static_cast<size_t>(height);
|
||||
packedBuffer = static_cast<uint8_t*>(heap_caps_malloc(packed_buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
||||
if (packedBuffer == nullptr) {
|
||||
LOG_W(TAG, "PSRAM allocation failed for packed buffer, falling back to internal memory");
|
||||
packedBuffer = static_cast<uint8_t*>(heap_caps_malloc(packed_buffer_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL));
|
||||
}
|
||||
|
||||
if (packedBuffer == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate packed pixel buffer");
|
||||
heap_caps_free(lvglDrawBuffer);
|
||||
lvglDrawBuffer = nullptr;
|
||||
lv_display_delete(lvglDisplay);
|
||||
lvglDisplay = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// For EPD, we want full refresh mode based on configuration
|
||||
lv_display_render_mode_t render_mode = configuration->fullRefresh
|
||||
? LV_DISPLAY_RENDER_MODE_FULL
|
||||
: LV_DISPLAY_RENDER_MODE_PARTIAL;
|
||||
|
||||
lv_display_set_buffers(lvglDisplay, lvglDrawBuffer, NULL, draw_buffer_size, render_mode);
|
||||
|
||||
// Set flush callback
|
||||
lv_display_set_flush_cb(lvglDisplay, flushCallback);
|
||||
lv_display_set_user_data(lvglDisplay, this);
|
||||
|
||||
// Register rotation change event callback
|
||||
lv_display_add_event_cb(lvglDisplay, rotationEventCallback, LV_EVENT_RESOLUTION_CHANGED, this);
|
||||
LOG_D(TAG, "Registered rotation change event callback");
|
||||
|
||||
// Start touch device if present
|
||||
auto touch_device = getTouchDevice();
|
||||
if (touch_device != nullptr && touch_device->supportsLvgl()) {
|
||||
LOG_D(TAG, "Starting touch device for LVGL");
|
||||
if (!touch_device->startLvgl(lvglDisplay)) {
|
||||
LOG_W(TAG, "Failed to start touch device for LVGL");
|
||||
}
|
||||
}
|
||||
|
||||
LOG_I(TAG, "LVGL display initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EpdiyDisplay::stopLvgl() {
|
||||
if (lvglDisplay == nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Stopping LVGL display");
|
||||
|
||||
// Stop touch device
|
||||
auto touch_device = getTouchDevice();
|
||||
if (touch_device != nullptr) {
|
||||
touch_device->stopLvgl();
|
||||
}
|
||||
|
||||
if (lvglDrawBuffer != nullptr) {
|
||||
heap_caps_free(lvglDrawBuffer);
|
||||
lvglDrawBuffer = nullptr;
|
||||
}
|
||||
|
||||
if (packedBuffer != nullptr) {
|
||||
heap_caps_free(packedBuffer);
|
||||
packedBuffer = nullptr;
|
||||
}
|
||||
|
||||
// Delete the LVGL display object
|
||||
lv_display_delete(lvglDisplay);
|
||||
lvglDisplay = nullptr;
|
||||
|
||||
|
||||
LOG_I(TAG, "LVGL display stopped");
|
||||
return true;
|
||||
}
|
||||
|
||||
void EpdiyDisplay::flushCallback(lv_display_t* display, const lv_area_t* area, uint8_t* pixelMap) {
|
||||
auto* instance = static_cast<EpdiyDisplay*>(lv_display_get_user_data(display));
|
||||
if (instance != nullptr) {
|
||||
uint64_t t0 = esp_timer_get_time();
|
||||
const bool isLast = lv_display_flush_is_last(display);
|
||||
instance->flushInternal(area, pixelMap, isLast);
|
||||
LOG_D(TAG, "flush took %llu us", (unsigned long long)(esp_timer_get_time() - t0));
|
||||
} else {
|
||||
LOG_W(TAG, "flush callback called with null instance");
|
||||
}
|
||||
lv_display_flush_ready(display);
|
||||
}
|
||||
|
||||
|
||||
// EPD functions
|
||||
void EpdiyDisplay::clearScreen() {
|
||||
if (!initialized) {
|
||||
LOG_E(TAG, "EPD not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!powered) {
|
||||
setPowerOn(true);
|
||||
}
|
||||
|
||||
epd_clear();
|
||||
|
||||
// Also clear the framebuffer
|
||||
epd_hl_set_all_white(&highlevelState);
|
||||
}
|
||||
|
||||
void EpdiyDisplay::clearArea(EpdRect area) {
|
||||
if (!initialized) {
|
||||
LOG_E(TAG, "EPD not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!powered) {
|
||||
setPowerOn(true);
|
||||
}
|
||||
|
||||
epd_clear_area(area);
|
||||
}
|
||||
|
||||
enum EpdDrawError EpdiyDisplay::updateScreen(enum EpdDrawMode mode, int temperature) {
|
||||
if (!initialized) {
|
||||
LOG_E(TAG, "EPD not initialized");
|
||||
return EPD_DRAW_FAILED_ALLOC;
|
||||
}
|
||||
|
||||
if (!powered) {
|
||||
setPowerOn(true);
|
||||
}
|
||||
|
||||
// Use defaults if not specified
|
||||
if (mode == MODE_UNKNOWN_WAVEFORM) {
|
||||
mode = configuration->defaultDrawMode;
|
||||
}
|
||||
if (temperature == -1) {
|
||||
temperature = configuration->defaultTemperature;
|
||||
}
|
||||
|
||||
return epd_hl_update_screen(&highlevelState, mode, temperature);
|
||||
}
|
||||
|
||||
enum EpdDrawError EpdiyDisplay::updateArea(EpdRect area, enum EpdDrawMode mode, int temperature) {
|
||||
if (!initialized) {
|
||||
LOG_E(TAG, "EPD not initialized");
|
||||
return EPD_DRAW_FAILED_ALLOC;
|
||||
}
|
||||
|
||||
if (!powered) {
|
||||
setPowerOn(true);
|
||||
}
|
||||
|
||||
// Use defaults if not specified
|
||||
if (mode == MODE_UNKNOWN_WAVEFORM) {
|
||||
mode = configuration->defaultDrawMode;
|
||||
}
|
||||
if (temperature == -1) {
|
||||
temperature = configuration->defaultTemperature;
|
||||
}
|
||||
|
||||
return epd_hl_update_area(&highlevelState, mode, temperature, area);
|
||||
}
|
||||
|
||||
void EpdiyDisplay::setAllWhite() {
|
||||
if (!initialized) {
|
||||
LOG_E(TAG, "EPD not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
epd_hl_set_all_white(&highlevelState);
|
||||
}
|
||||
|
||||
// Internal functions
|
||||
void EpdiyDisplay::flushInternal(const lv_area_t* area, uint8_t* pixelMap, bool isLast) {
|
||||
if (!initialized) {
|
||||
LOG_E(TAG, "Cannot flush - EPD not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!powered) {
|
||||
setPowerOn(true);
|
||||
}
|
||||
|
||||
const int x = area->x1;
|
||||
const int y = area->y1;
|
||||
const int width = lv_area_get_width(area);
|
||||
const int height = lv_area_get_height(area);
|
||||
|
||||
LOG_D(TAG, "Flushing area: x=%d, y=%d, w=%d, h=%d isLast=%d", x, y, width, height, (int)isLast);
|
||||
|
||||
// Convert L8 (8-bit grayscale, 0=black/255=white) to EPDiy 4-bit (0=black/15=white).
|
||||
// Pack 2 pixels per byte: lower nibble = even column, upper nibble = odd column.
|
||||
// Row stride includes one padding nibble for odd widths to keep rows aligned.
|
||||
// Threshold at 128 (matching FastEPD BB_MODE_1BPP): pixels > 127 → full white (15),
|
||||
// pixels ≤ 127 → full black (0). Maximum contrast for the Mono theme and correct for
|
||||
// MODE_DU which only drives two levels. For greyscale content / MODE_GL16, replace
|
||||
// the threshold with `src[col] >> 4` to preserve intermediate grey levels.
|
||||
const int row_stride = (width + 1) / 2;
|
||||
for (int row = 0; row < height; ++row) {
|
||||
const uint8_t* src = pixelMap + static_cast<size_t>(row) * width;
|
||||
uint8_t* dst = packedBuffer + static_cast<size_t>(row) * row_stride;
|
||||
for (int col = 0; col < width; col += 2) {
|
||||
const uint8_t p0 = (src[col] > 127) ? 15u : 0u;
|
||||
const uint8_t p1 = (col + 1 < width) ? ((src[col + 1] > 127) ? 15u : 0u) : 0u;
|
||||
dst[col / 2] = static_cast<uint8_t>((p1 << 4) | p0);
|
||||
}
|
||||
}
|
||||
|
||||
const EpdRect update_area = {
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = static_cast<uint16_t>(width),
|
||||
.height = static_cast<uint16_t>(height)
|
||||
};
|
||||
|
||||
// Write pixels into EPDiy's framebuffer (no hardware I/O, just memory)
|
||||
epd_draw_rotated_image(update_area, packedBuffer, framebuffer);
|
||||
|
||||
// Only trigger EPD hardware update on the last flush of this render cycle.
|
||||
// EPDiy's epd_prep tasks run at configMAX_PRIORITIES-1 with busy-wait loops; calling
|
||||
// epd_hl_update_area on every partial flush starves IDLE and triggers the task watchdog
|
||||
// during scroll animations. Batching to one hardware update per LVGL render cycle fixes this.
|
||||
if (isLast) {
|
||||
epd_hl_update_screen(
|
||||
&highlevelState,
|
||||
static_cast<EpdDrawMode>(configuration->defaultDrawMode | MODE_PACKING_2PPB),
|
||||
configuration->defaultTemperature
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
lv_display_rotation_t EpdiyDisplay::epdRotationToLvgl(enum EpdRotation epdRotation) {
|
||||
// Static lookup table for EPD -> LVGL rotation mapping
|
||||
// EPDiy: LANDSCAPE = 0°, PORTRAIT = 90° CW, INVERTED_LANDSCAPE = 180°, INVERTED_PORTRAIT = 270° CW
|
||||
// LVGL: 0 = 0°, 90 = 90° CW, 180 = 180°, 270 = 270° CW
|
||||
static const lv_display_rotation_t rotationMap[] = {
|
||||
LV_DISPLAY_ROTATION_0, // EPD_ROT_LANDSCAPE (0)
|
||||
LV_DISPLAY_ROTATION_270, // EPD_ROT_PORTRAIT (1) - 90° CW in EPD is 270° in LVGL
|
||||
LV_DISPLAY_ROTATION_180, // EPD_ROT_INVERTED_LANDSCAPE (2)
|
||||
LV_DISPLAY_ROTATION_90 // EPD_ROT_INVERTED_PORTRAIT (3) - 270° CW in EPD is 90° in LVGL
|
||||
};
|
||||
|
||||
// Validate input and return mapped value
|
||||
if (epdRotation >= 0 && epdRotation < 4) {
|
||||
return rotationMap[epdRotation];
|
||||
}
|
||||
|
||||
// Default to landscape if invalid
|
||||
return LV_DISPLAY_ROTATION_0;
|
||||
}
|
||||
|
||||
enum EpdRotation EpdiyDisplay::lvglRotationToEpd(lv_display_rotation_t lvglRotation) {
|
||||
// Static lookup table for LVGL -> EPD rotation mapping
|
||||
static const enum EpdRotation rotationMap[] = {
|
||||
EPD_ROT_LANDSCAPE, // LV_DISPLAY_ROTATION_0 (0)
|
||||
EPD_ROT_INVERTED_PORTRAIT, // LV_DISPLAY_ROTATION_90 (1)
|
||||
EPD_ROT_INVERTED_LANDSCAPE, // LV_DISPLAY_ROTATION_180 (2)
|
||||
EPD_ROT_PORTRAIT // LV_DISPLAY_ROTATION_270 (3)
|
||||
};
|
||||
|
||||
// Validate input and return mapped value
|
||||
if (lvglRotation >= LV_DISPLAY_ROTATION_0 && lvglRotation <= LV_DISPLAY_ROTATION_270) {
|
||||
return rotationMap[lvglRotation];
|
||||
}
|
||||
|
||||
// Default to landscape if invalid
|
||||
return EPD_ROT_LANDSCAPE;
|
||||
}
|
||||
|
||||
void EpdiyDisplay::rotationEventCallback(lv_event_t* event) {
|
||||
auto* display = static_cast<EpdiyDisplay*>(lv_event_get_user_data(event));
|
||||
if (display == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
lv_display_t* lvgl_display = static_cast<lv_display_t*>(lv_event_get_target(event));
|
||||
if (lvgl_display == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
lv_display_rotation_t rotation = lv_display_get_rotation(lvgl_display);
|
||||
display->handleRotationChange(rotation);
|
||||
}
|
||||
|
||||
void EpdiyDisplay::handleRotationChange(lv_display_rotation_t lvgl_rotation) {
|
||||
// Map LVGL rotation to EPDiy rotation using lookup table
|
||||
enum EpdRotation epd_rotation = lvglRotationToEpd(lvgl_rotation);
|
||||
|
||||
// Update EPDiy rotation
|
||||
LOG_I(TAG, "LVGL rotation changed to %d, setting EPDiy rotation to %d", lvgl_rotation, epd_rotation);
|
||||
epd_set_rotation(epd_rotation);
|
||||
|
||||
// Update configuration to keep it in sync
|
||||
configuration->rotation = epd_rotation;
|
||||
|
||||
// Log the new dimensions
|
||||
LOG_I(TAG, "Display dimensions after rotation: %dx%d", epd_rotated_display_width(), epd_rotated_display_height());
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
#include <Tactility/hal/touch/TouchDevice.h>
|
||||
|
||||
#include <epd_highlevel.h>
|
||||
#include <epdiy.h>
|
||||
#include <lvgl.h>
|
||||
#include <memory>
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
|
||||
class EpdiyDisplay final : public tt::hal::display::DisplayDevice {
|
||||
|
||||
public:
|
||||
|
||||
class Configuration {
|
||||
public:
|
||||
|
||||
Configuration(
|
||||
const EpdBoardDefinition* board,
|
||||
const EpdDisplay_t* display,
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch = nullptr,
|
||||
enum EpdInitOptions initOptions = EPD_OPTIONS_DEFAULT,
|
||||
const EpdWaveform* waveform = EPD_BUILTIN_WAVEFORM,
|
||||
int defaultTemperature = 25,
|
||||
enum EpdDrawMode defaultDrawMode = MODE_GL16,
|
||||
bool fullRefresh = false,
|
||||
enum EpdRotation rotation = EPD_ROT_LANDSCAPE
|
||||
) : board(board),
|
||||
display(display),
|
||||
touch(std::move(touch)),
|
||||
initOptions(initOptions),
|
||||
waveform(waveform),
|
||||
defaultTemperature(defaultTemperature),
|
||||
defaultDrawMode(defaultDrawMode),
|
||||
fullRefresh(fullRefresh),
|
||||
rotation(rotation) {
|
||||
check(board != nullptr);
|
||||
check(display != nullptr);
|
||||
}
|
||||
|
||||
const EpdBoardDefinition* board;
|
||||
const EpdDisplay_t* display;
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
|
||||
enum EpdInitOptions initOptions;
|
||||
const EpdWaveform* waveform;
|
||||
int defaultTemperature;
|
||||
enum EpdDrawMode defaultDrawMode;
|
||||
bool fullRefresh;
|
||||
enum EpdRotation rotation;
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
std::unique_ptr<Configuration> configuration;
|
||||
lv_display_t* _Nullable lvglDisplay = nullptr;
|
||||
EpdiyHighlevelState highlevelState = {};
|
||||
uint8_t* framebuffer = nullptr;
|
||||
uint8_t* lvglDrawBuffer = nullptr;
|
||||
uint8_t* packedBuffer = nullptr; // Pre-allocated 4-bit packed pixel buffer for flushInternal
|
||||
bool initialized = false;
|
||||
bool powered = false;
|
||||
|
||||
// epd_hl_init() sets an internal already_initialized flag and has no matching deinit.
|
||||
// We track first-time init statically and keep the HL state alive across stop()/start() cycles.
|
||||
static bool s_hlInitialized;
|
||||
static EpdiyHighlevelState s_hlState;
|
||||
|
||||
static void flushCallback(lv_display_t* display, const lv_area_t* area, uint8_t* pixelMap);
|
||||
void flushInternal(const lv_area_t* area, uint8_t* pixelMap, bool isLast);
|
||||
|
||||
static void rotationEventCallback(lv_event_t* event);
|
||||
void handleRotationChange(lv_display_rotation_t rotation);
|
||||
|
||||
// Rotation mapping helpers
|
||||
static lv_display_rotation_t epdRotationToLvgl(enum EpdRotation epdRotation);
|
||||
static enum EpdRotation lvglRotationToEpd(lv_display_rotation_t lvglRotation);
|
||||
|
||||
public:
|
||||
|
||||
explicit EpdiyDisplay(std::unique_ptr<Configuration> inConfiguration);
|
||||
|
||||
~EpdiyDisplay() override;
|
||||
|
||||
std::string getName() const override { return "EPDiy"; }
|
||||
|
||||
std::string getDescription() const override {
|
||||
return "E-Ink display powered by EPDiy library";
|
||||
}
|
||||
|
||||
// Device lifecycle
|
||||
bool start() override;
|
||||
bool stop() override;
|
||||
|
||||
// Power control
|
||||
void setPowerOn(bool turnOn) override;
|
||||
bool isPoweredOn() const override { return powered; }
|
||||
bool supportsPowerControl() const override { return true; }
|
||||
|
||||
// Touch device
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable getTouchDevice() override {
|
||||
return configuration->touch;
|
||||
}
|
||||
|
||||
// LVGL support
|
||||
bool supportsLvgl() const override { return true; }
|
||||
bool startLvgl() override;
|
||||
bool stopLvgl() override;
|
||||
lv_display_t* _Nullable getLvglDisplay() const override { return lvglDisplay; }
|
||||
|
||||
// DisplayDriver (not supported for EPD)
|
||||
bool supportsDisplayDriver() const override { return false; }
|
||||
std::shared_ptr<tt::hal::display::DisplayDriver> _Nullable getDisplayDriver() override {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// EPD specific functions
|
||||
|
||||
/**
|
||||
* Get a reference to the framebuffer
|
||||
*/
|
||||
uint8_t* getFramebuffer() {
|
||||
return epd_hl_get_framebuffer(&highlevelState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the screen by flashing it
|
||||
*/
|
||||
void clearScreen();
|
||||
|
||||
/**
|
||||
* Clear an area by flashing it
|
||||
*/
|
||||
void clearArea(EpdRect area);
|
||||
|
||||
/**
|
||||
* Manually trigger a screen update
|
||||
* @param mode The draw mode to use (defaults to configuration default)
|
||||
* @param temperature Temperature in °C (defaults to configuration default)
|
||||
*/
|
||||
enum EpdDrawError updateScreen(
|
||||
enum EpdDrawMode mode = MODE_UNKNOWN_WAVEFORM,
|
||||
int temperature = -1
|
||||
);
|
||||
|
||||
/**
|
||||
* Update a specific area of the screen
|
||||
* @param area The area to update
|
||||
* @param mode The draw mode to use (defaults to configuration default)
|
||||
* @param temperature Temperature in °C (defaults to configuration default)
|
||||
*/
|
||||
enum EpdDrawError updateArea(
|
||||
EpdRect area,
|
||||
enum EpdDrawMode mode = MODE_UNKNOWN_WAVEFORM,
|
||||
int temperature = -1
|
||||
);
|
||||
|
||||
/**
|
||||
* Set the display to all white
|
||||
*/
|
||||
void setAllWhite();
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "EpdiyDisplay.h"
|
||||
#include <epd_board.h>
|
||||
#include <epd_display.h>
|
||||
#include <memory>
|
||||
|
||||
/**
|
||||
* Helper class to create EPDiy displays with common configurations
|
||||
*/
|
||||
class EpdiyDisplayHelper {
|
||||
public:
|
||||
|
||||
/**
|
||||
* Create a display for M5Paper S3
|
||||
* @param touch Optional touch device
|
||||
* @param temperature Display temperature in °C (default: 20)
|
||||
* @param drawMode Default draw mode (default: MODE_DU)
|
||||
* @param fullRefresh Use full refresh mode (default: false for partial updates)
|
||||
* @param rotation Display rotation (default: EPD_ROT_PORTRAIT)
|
||||
*/
|
||||
static std::shared_ptr<EpdiyDisplay> createM5PaperS3Display(
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch = nullptr,
|
||||
int temperature = 20,
|
||||
enum EpdDrawMode drawMode = MODE_DU,
|
||||
bool fullRefresh = false,
|
||||
enum EpdRotation rotation = EPD_ROT_PORTRAIT
|
||||
) {
|
||||
auto config = std::make_unique<EpdiyDisplay::Configuration>(
|
||||
&epd_board_m5papers3,
|
||||
&ED047TC1,
|
||||
touch,
|
||||
static_cast<EpdInitOptions>(EPD_LUT_1K | EPD_FEED_QUEUE_32),
|
||||
static_cast<const EpdWaveform*>(EPD_BUILTIN_WAVEFORM),
|
||||
temperature,
|
||||
drawMode,
|
||||
fullRefresh,
|
||||
rotation
|
||||
);
|
||||
|
||||
return std::make_shared<EpdiyDisplay>(std::move(config));
|
||||
}
|
||||
};
|
||||
@@ -18,6 +18,7 @@ ChargeFromAdcVoltage::ChargeFromAdcVoltage(
|
||||
LOGGER.error("ADC channel config failed");
|
||||
|
||||
adc_oneshot_del_unit(adcHandle);
|
||||
adcHandle = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +30,9 @@ ChargeFromAdcVoltage::~ChargeFromAdcVoltage() {
|
||||
}
|
||||
|
||||
bool ChargeFromAdcVoltage::readBatteryVoltageOnce(uint32_t& output) const {
|
||||
if (adcHandle == nullptr) {
|
||||
return false;
|
||||
}
|
||||
int raw;
|
||||
if (adc_oneshot_read(adcHandle, configuration.adcChannel, &raw) == ESP_OK) {
|
||||
output = configuration.adcMultiplier * ((1000.f * configuration.adcRefVoltage) / 4096.f) * (float)raw;
|
||||
|
||||
@@ -34,6 +34,8 @@ public:
|
||||
|
||||
~ChargeFromAdcVoltage();
|
||||
|
||||
bool isInitialized() const { return adcHandle != nullptr; }
|
||||
|
||||
bool readBatteryVoltageSampled(uint32_t& output) const;
|
||||
|
||||
bool readBatteryVoltageOnce(uint32_t& output) const;
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
#include "FastEpdDisplay.h"
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <esp_heap_caps.h>
|
||||
#endif
|
||||
|
||||
#define TAG "FastEpdDisplay"
|
||||
|
||||
FastEpdDisplay::~FastEpdDisplay() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void FastEpdDisplay::flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) {
|
||||
auto* self = static_cast<FastEpdDisplay*>(lv_display_get_user_data(disp));
|
||||
|
||||
static uint32_t s_flush_log_counter = 0;
|
||||
|
||||
const int32_t width = area->x2 - area->x1 + 1;
|
||||
const bool grayscale4bpp = self->configuration.use4bppGrayscale;
|
||||
|
||||
// LVGL logical resolution is portrait (540x960). FastEPD PaperS3 native is landscape (960x540).
|
||||
// Keep FastEPD at rotation 0 and do the coordinate transform ourselves.
|
||||
// For a 90° clockwise transform:
|
||||
// x_native = y
|
||||
// y_native = (native_height - 1) - x
|
||||
const int native_width = self->epd.width();
|
||||
const int native_height = self->epd.height();
|
||||
|
||||
// Compute the native line range that will be affected by this flush.
|
||||
// With our mapping y_native = (native_height - 1) - x
|
||||
// So x range maps to y_native range.
|
||||
int start_line = (native_height - 1) - (int)area->x2;
|
||||
int end_line = (native_height - 1) - (int)area->x1;
|
||||
if (start_line > end_line) {
|
||||
const int tmp = start_line;
|
||||
start_line = end_line;
|
||||
end_line = tmp;
|
||||
}
|
||||
if (start_line < 0) start_line = 0;
|
||||
if (end_line >= native_height) end_line = native_height - 1;
|
||||
|
||||
for (int32_t y = area->y1; y <= area->y2; y++) {
|
||||
for (int32_t x = area->x1; x <= area->x2; x++) {
|
||||
const uint8_t gray8 = px_map[(y - area->y1) * width + (x - area->x1)];
|
||||
const uint8_t color = grayscale4bpp ? (uint8_t)(gray8 >> 4) : (uint8_t)((gray8 > 127) ? BBEP_BLACK : BBEP_WHITE);
|
||||
|
||||
const int x_native = y;
|
||||
const int y_native = (native_height - 1) - x;
|
||||
|
||||
// Be defensive: any out-of-range drawPixelFast will corrupt memory.
|
||||
if (x_native < 0 || x_native >= native_width || y_native < 0 || y_native >= native_height) {
|
||||
continue;
|
||||
}
|
||||
self->epd.drawPixelFast(x_native, y_native, color);
|
||||
}
|
||||
}
|
||||
|
||||
if (start_line <= end_line) {
|
||||
(void)self->epd.einkPower(1);
|
||||
self->flushCount++;
|
||||
const uint32_t cadence = self->configuration.fullRefreshEveryNFlushes;
|
||||
const bool requested_full = self->forceNextFullRefresh.exchange(false);
|
||||
const bool do_full = requested_full || ((cadence > 0) && (self->flushCount % cadence == 0));
|
||||
|
||||
const bool should_log = ((++s_flush_log_counter % 25U) == 0U);
|
||||
if (should_log) {
|
||||
LOG_I(TAG, "flush #%lu area=(%ld,%ld)-(%ld,%ld) lines=[%d..%d] full=%d",
|
||||
(unsigned long)self->flushCount,
|
||||
(long)area->x1, (long)area->y1, (long)area->x2, (long)area->y2,
|
||||
start_line, end_line, (int)do_full);
|
||||
}
|
||||
|
||||
if (do_full) {
|
||||
const int rc = self->epd.fullUpdate(CLEAR_FAST, true, nullptr);
|
||||
if (should_log) {
|
||||
LOG_I(TAG, "fullUpdate rc=%d", rc);
|
||||
}
|
||||
|
||||
// After a full update, keep FastEPD's previous/current buffers in sync so that
|
||||
// subsequent partial updates compute correct diffs.
|
||||
const int w = self->epd.width();
|
||||
const int h = self->epd.height();
|
||||
const size_t bytes_per_row = grayscale4bpp
|
||||
? (size_t)(w + 1) / 2
|
||||
: (size_t)(w + 7) / 8;
|
||||
const size_t plane_size = bytes_per_row * (size_t)h;
|
||||
if (self->epd.currentBuffer() && self->epd.previousBuffer()) {
|
||||
memcpy(self->epd.previousBuffer(), self->epd.currentBuffer(), plane_size);
|
||||
}
|
||||
} else {
|
||||
if (grayscale4bpp) {
|
||||
// FastEPD partialUpdate only supports 1bpp mode.
|
||||
// For 4bpp we currently do a fullUpdate. Region-based updates are tricky here because
|
||||
// we also manually rotate/transform pixels in flush_cb; a mismatched rect can refresh
|
||||
// the wrong strip of the panel (seen as "split" buttons on the final refresh).
|
||||
const int rc = self->epd.fullUpdate(CLEAR_FAST, true, nullptr);
|
||||
if (should_log) {
|
||||
LOG_I(TAG, "fullUpdate(4bpp) rc=%d", rc);
|
||||
}
|
||||
} else {
|
||||
const int rc = self->epd.partialUpdate(true, start_line, end_line);
|
||||
|
||||
// Keep FastEPD's previous/current buffers in sync after partial updates as well.
|
||||
// This avoids stale diffs where subsequent updates don't visibly apply.
|
||||
const int w = self->epd.width();
|
||||
const int h = self->epd.height();
|
||||
const size_t bytes_per_row = (size_t)(w + 7) / 8;
|
||||
const size_t plane_size = bytes_per_row * (size_t)h;
|
||||
if (rc == BBEP_SUCCESS && self->epd.currentBuffer() && self->epd.previousBuffer()) {
|
||||
memcpy(self->epd.previousBuffer(), self->epd.currentBuffer(), plane_size);
|
||||
}
|
||||
|
||||
if (should_log) {
|
||||
LOG_I(TAG, "partialUpdate rc=%d", rc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lv_display_flush_ready(disp);
|
||||
}
|
||||
|
||||
bool FastEpdDisplay::start() {
|
||||
if (initialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const int rc = epd.initPanel(BB_PANEL_M5PAPERS3, configuration.busSpeedHz);
|
||||
if (rc != BBEP_SUCCESS) {
|
||||
LOG_E(TAG, "FastEPD initPanel failed rc=%d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "FastEPD native size %dx%d", epd.width(), epd.height());
|
||||
|
||||
const int desired_mode = configuration.use4bppGrayscale ? BB_MODE_4BPP : BB_MODE_1BPP;
|
||||
if (epd.setMode(desired_mode) != BBEP_SUCCESS) {
|
||||
LOG_E(TAG, "FastEPD setMode(%d) failed", desired_mode);
|
||||
epd.deInit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep FastEPD at rotation 0. LVGL-to-native mapping is handled in flush_cb.
|
||||
|
||||
// Ensure previous/current buffers are in sync and the panel starts from a known state.
|
||||
if (epd.einkPower(1) != BBEP_SUCCESS) {
|
||||
LOG_W(TAG, "FastEPD einkPower(1) failed");
|
||||
} else {
|
||||
epd.fillScreen(configuration.use4bppGrayscale ? 0x0F : BBEP_WHITE);
|
||||
|
||||
const int native_width = epd.width();
|
||||
const int native_height = epd.height();
|
||||
const size_t bytes_per_row = configuration.use4bppGrayscale
|
||||
? (size_t)(native_width + 1) / 2
|
||||
: (size_t)(native_width + 7) / 8;
|
||||
const size_t plane_size = bytes_per_row * (size_t)native_height;
|
||||
|
||||
if (epd.currentBuffer() && epd.previousBuffer()) {
|
||||
memcpy(epd.previousBuffer(), epd.currentBuffer(), plane_size);
|
||||
}
|
||||
|
||||
if (epd.fullUpdate(CLEAR_FAST, true, nullptr) != BBEP_SUCCESS) {
|
||||
LOG_W(TAG, "FastEPD fullUpdate failed");
|
||||
}
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FastEpdDisplay::stop() {
|
||||
if (lvglDisplay) {
|
||||
stopLvgl();
|
||||
}
|
||||
|
||||
if (initialized) {
|
||||
epd.deInit();
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FastEpdDisplay::startLvgl() {
|
||||
if (lvglDisplay != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
lvglDisplay = lv_display_create(configuration.horizontalResolution, configuration.verticalResolution);
|
||||
if (lvglDisplay == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lv_display_set_color_format(lvglDisplay, LV_COLOR_FORMAT_L8);
|
||||
|
||||
if (lv_display_get_rotation(lvglDisplay) != LV_DISPLAY_ROTATION_0) {
|
||||
lv_display_set_rotation(lvglDisplay, LV_DISPLAY_ROTATION_0);
|
||||
}
|
||||
|
||||
const uint32_t pixel_count = (uint32_t)(configuration.horizontalResolution * configuration.verticalResolution / 10);
|
||||
const uint32_t buf_size = pixel_count * (uint32_t)lv_color_format_get_size(LV_COLOR_FORMAT_L8);
|
||||
lvglBufSize = buf_size;
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
lvglBuf1 = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
#else
|
||||
lvglBuf1 = malloc(buf_size);
|
||||
#endif
|
||||
|
||||
if (lvglBuf1 == nullptr) {
|
||||
lv_display_delete(lvglDisplay);
|
||||
lvglDisplay = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
lvglBuf2 = nullptr;
|
||||
lv_display_set_buffers(lvglDisplay, lvglBuf1, lvglBuf2, buf_size, LV_DISPLAY_RENDER_MODE_PARTIAL);
|
||||
|
||||
lv_display_set_user_data(lvglDisplay, this);
|
||||
lv_display_set_flush_cb(lvglDisplay, FastEpdDisplay::flush_cb);
|
||||
|
||||
if (configuration.touch && configuration.touch->supportsLvgl()) {
|
||||
configuration.touch->startLvgl(lvglDisplay);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FastEpdDisplay::stopLvgl() {
|
||||
if (lvglDisplay) {
|
||||
if (configuration.touch) {
|
||||
configuration.touch->stopLvgl();
|
||||
}
|
||||
|
||||
lv_display_delete(lvglDisplay);
|
||||
lvglDisplay = nullptr;
|
||||
}
|
||||
|
||||
if (lvglBuf1 != nullptr) {
|
||||
free(lvglBuf1);
|
||||
lvglBuf1 = nullptr;
|
||||
}
|
||||
|
||||
if (lvglBuf2 != nullptr) {
|
||||
free(lvglBuf2);
|
||||
lvglBuf2 = nullptr;
|
||||
}
|
||||
|
||||
lvglBufSize = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/Lock.h>
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
#include <Tactility/hal/touch/TouchDevice.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <FastEPD.h>
|
||||
#include <lvgl.h>
|
||||
|
||||
class FastEpdDisplay final : public tt::hal::display::DisplayDevice {
|
||||
public:
|
||||
struct Configuration final {
|
||||
int horizontalResolution;
|
||||
int verticalResolution;
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
|
||||
uint32_t busSpeedHz = 20000000;
|
||||
int rotationDegrees = 90;
|
||||
bool use4bppGrayscale = false;
|
||||
uint32_t fullRefreshEveryNFlushes = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
Configuration configuration;
|
||||
std::shared_ptr<tt::Lock> lock;
|
||||
|
||||
lv_display_t* lvglDisplay = nullptr;
|
||||
void* lvglBuf1 = nullptr;
|
||||
void* lvglBuf2 = nullptr;
|
||||
uint32_t lvglBufSize = 0;
|
||||
|
||||
FASTEPD epd;
|
||||
bool initialized = false;
|
||||
uint32_t flushCount = 0;
|
||||
std::atomic_bool forceNextFullRefresh{false};
|
||||
|
||||
static void flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map);
|
||||
|
||||
public:
|
||||
FastEpdDisplay(Configuration configuration, std::shared_ptr<tt::Lock> lock)
|
||||
: configuration(std::move(configuration)), lock(std::move(lock)) {}
|
||||
|
||||
~FastEpdDisplay() override;
|
||||
|
||||
void requestFullRefresh() override { forceNextFullRefresh.store(true); }
|
||||
|
||||
std::string getName() const override { return "FastEpdDisplay"; }
|
||||
std::string getDescription() const override { return "FastEPD (bitbank2) E-Ink display driver"; }
|
||||
|
||||
bool start() override;
|
||||
bool stop() override;
|
||||
|
||||
bool supportsLvgl() const override { return true; }
|
||||
bool startLvgl() override;
|
||||
bool stopLvgl() override;
|
||||
|
||||
lv_display_t* getLvglDisplay() const override { return lvglDisplay; }
|
||||
|
||||
bool supportsDisplayDriver() const override { return false; }
|
||||
std::shared_ptr<tt::hal::display::DisplayDriver> getDisplayDriver() override { return nullptr; }
|
||||
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> getTouchDevice() override {
|
||||
return configuration.touch;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user