Internal functions
| 346 | |
| 347 | // Internal functions |
| 348 | void EpdiyDisplay::flushInternal(const lv_area_t* area, uint8_t* pixelMap, bool isLast) { |
| 349 | if (!initialized) { |
| 350 | LOG_E(TAG, "Cannot flush - EPD not initialized"); |
| 351 | return; |
| 352 | } |
| 353 | |
| 354 | if (!powered) { |
| 355 | setPowerOn(true); |
| 356 | } |
| 357 | |
| 358 | const int x = area->x1; |
| 359 | const int y = area->y1; |
| 360 | const int width = lv_area_get_width(area); |
| 361 | const int height = lv_area_get_height(area); |
| 362 | |
| 363 | LOG_D(TAG, "Flushing area: x=%d, y=%d, w=%d, h=%d isLast=%d", x, y, width, height, (int)isLast); |
| 364 | |
| 365 | // Convert L8 (8-bit grayscale, 0=black/255=white) to EPDiy 4-bit (0=black/15=white). |
| 366 | // Pack 2 pixels per byte: lower nibble = even column, upper nibble = odd column. |
| 367 | // Row stride includes one padding nibble for odd widths to keep rows aligned. |
| 368 | // Threshold at 128 (matching FastEPD BB_MODE_1BPP): pixels > 127 → full white (15), |
| 369 | // pixels ≤ 127 → full black (0). Maximum contrast for the Mono theme and correct for |
| 370 | // MODE_DU which only drives two levels. For greyscale content / MODE_GL16, replace |
| 371 | // the threshold with `src[col] >> 4` to preserve intermediate grey levels. |
| 372 | const int row_stride = (width + 1) / 2; |
| 373 | for (int row = 0; row < height; ++row) { |
| 374 | const uint8_t* src = pixelMap + static_cast<size_t>(row) * width; |
| 375 | uint8_t* dst = packedBuffer + static_cast<size_t>(row) * row_stride; |
| 376 | for (int col = 0; col < width; col += 2) { |
| 377 | const uint8_t p0 = (src[col] > 127) ? 15u : 0u; |
| 378 | const uint8_t p1 = (col + 1 < width) ? ((src[col + 1] > 127) ? 15u : 0u) : 0u; |
| 379 | dst[col / 2] = static_cast<uint8_t>((p1 << 4) | p0); |
| 380 | } |
| 381 | } |
| 382 | |
| 383 | const EpdRect update_area = { |
| 384 | .x = x, |
| 385 | .y = y, |
| 386 | .width = static_cast<uint16_t>(width), |
| 387 | .height = static_cast<uint16_t>(height) |
| 388 | }; |
| 389 | |
| 390 | // Write pixels into EPDiy's framebuffer (no hardware I/O, just memory) |
| 391 | epd_draw_rotated_image(update_area, packedBuffer, framebuffer); |
| 392 | |
| 393 | // Only trigger EPD hardware update on the last flush of this render cycle. |
| 394 | // EPDiy's epd_prep tasks run at configMAX_PRIORITIES-1 with busy-wait loops; calling |
| 395 | // epd_hl_update_area on every partial flush starves IDLE and triggers the task watchdog |
| 396 | // during scroll animations. Batching to one hardware update per LVGL render cycle fixes this. |
| 397 | if (isLast) { |
| 398 | epd_hl_update_screen( |
| 399 | &highlevelState, |
| 400 | static_cast<EpdDrawMode>(configuration->defaultDrawMode | MODE_PACKING_2PPB), |
| 401 | configuration->defaultTemperature |
| 402 | ); |
| 403 | } |
| 404 | } |
| 405 |