#include "EpdiyDisplay.h" #include #include #include #include constexpr const char* TAG = "EpdiyDisplay"; bool EpdiyDisplay::s_hlInitialized = false; EpdiyHighlevelState EpdiyDisplay::s_hlState = {}; EpdiyDisplay::EpdiyDisplay(std::unique_ptr 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(width) * height; lvglDrawBuffer = static_cast(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(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((width + 1) / 2) * static_cast(height); packedBuffer = static_cast(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(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(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(row) * width; uint8_t* dst = packedBuffer + static_cast(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((p1 << 4) | p0); } } const EpdRect update_area = { .x = x, .y = y, .width = static_cast(width), .height = static_cast(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(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(lv_event_get_user_data(event)); if (display == nullptr) { return; } lv_display_t* lvgl_display = static_cast(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()); }