diff --git a/zephcore/helpers/ui/display.c b/zephcore/helpers/ui/display.c index e394e07..63ddc6d 100644 --- a/zephcore/helpers/ui/display.c +++ b/zephcore/helpers/ui/display.c @@ -24,6 +24,7 @@ #include #include #include +#include #include LOG_MODULE_REGISTER(zephcore_display, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); @@ -58,6 +59,31 @@ static const struct device *backlight_reg; static bool backlight_on; +/* EPD frame change detection (Arduino-style): + * hash draw calls across a frame and skip hardware flush if unchanged. */ +static uint32_t epd_frame_hash; +static uint32_t epd_last_frame_hash = UINT_MAX; + +static inline void epd_hash_bytes(const void *data, size_t len) +{ + if (!is_epd || !data || len == 0) { + return; + } + + const uint8_t *p = (const uint8_t *)data; + + for (size_t i = 0; i < len; i++) { + /* FNV-1a */ + epd_frame_hash ^= p[i]; + epd_frame_hash *= 16777619u; + } +} + +static inline void epd_hash_u32(uint32_t v) +{ + epd_hash_bytes(&v, sizeof(v)); +} + static inline void backlight_set(bool on) { if (backlight_reg && device_is_ready(backlight_reg) && on != backlight_on) { @@ -336,6 +362,9 @@ void mc_display_clear(void) return; } + if (is_epd) { + epd_frame_hash = 2166136261u; + } cfb_framebuffer_clear(disp_dev, false); } @@ -349,6 +378,12 @@ void mc_display_text(int x, int y, const char *text, bool invert) cfb_framebuffer_invert(disp_dev); } + if (is_epd) { + epd_hash_u32((uint32_t)x); + epd_hash_u32((uint32_t)y); + epd_hash_u32(invert ? 1u : 0u); + epd_hash_bytes(text, strlen(text)); + } cfb_print(disp_dev, text, x + DISP_INSET, y + DISP_INSET); if (invert) { @@ -365,6 +400,12 @@ void mc_display_fill_rect(int x, int y, int w, int h) /* CFB doesn't have a native fill_rect, so we draw line by line */ const int row_clamp = (int)disp_height - DISP_INSET; + if (is_epd) { + epd_hash_u32((uint32_t)x); + epd_hash_u32((uint32_t)y); + epd_hash_u32((uint32_t)w); + epd_hash_u32((uint32_t)h); + } for (int row = y + DISP_INSET; row < y + h + DISP_INSET && row < row_clamp; row++) { struct cfb_position start = { .x = x + DISP_INSET, .y = row }; struct cfb_position end = { .x = x + w - 1 + DISP_INSET, .y = row }; @@ -380,6 +421,12 @@ void mc_display_hline(int x, int y, int w) struct cfb_position start = { .x = x + DISP_INSET, .y = y + DISP_INSET }; struct cfb_position end = { .x = x + w - 1 + DISP_INSET, .y = y + DISP_INSET }; + + if (is_epd) { + epd_hash_u32((uint32_t)x); + epd_hash_u32((uint32_t)y); + epd_hash_u32((uint32_t)w); + } cfb_draw_line(disp_dev, &start, &end); } @@ -393,6 +440,15 @@ void mc_display_xbm(int x, int y, const uint8_t *data, int w, int h) * This matches the Arduino MeshCore logo data from icons.h. * Each row is padded to byte boundary: bytes_per_row = (w+7)/8 */ int bytes_per_row = (w + 7) / 8; + size_t bitmap_len = (size_t)bytes_per_row * (size_t)h; + + if (is_epd) { + epd_hash_u32((uint32_t)x); + epd_hash_u32((uint32_t)y); + epd_hash_u32((uint32_t)w); + epd_hash_u32((uint32_t)h); + epd_hash_bytes(data, bitmap_len); + } for (int row = 0; row < h; row++) { for (int col = 0; col < w; col++) { @@ -421,7 +477,13 @@ void mc_display_finalize(void) return; } + if (is_epd && epd_frame_hash == epd_last_frame_hash) { + return; + } cfb_framebuffer_finalize(disp_dev); + if (is_epd) { + epd_last_frame_hash = epd_frame_hash; + } } void mc_display_reset_auto_off(void) @@ -439,6 +501,24 @@ void mc_display_reset_auto_off(void) #endif } +void mc_display_epd_full_reset(void) +{ + if (!disp_initialized || !is_epd) { + return; + } + + /* Force the SSD16xx path back through a full-refresh cycle before + * entering steady-state partial updates for page rendering. */ + display_blanking_on(disp_dev); + display_blanking_off(disp_dev); + epd_last_frame_hash = UINT_MAX; + + /* After splash handoff, keep frontlight off; next user interaction + * wakes it via mc_display_on(). */ + backlight_set(false); + disp_on = false; +} + const struct device *mc_display_get_device(void) { return disp_initialized ? disp_dev : NULL; diff --git a/zephcore/helpers/ui/display.h b/zephcore/helpers/ui/display.h index cbdab40..8fce1e2 100644 --- a/zephcore/helpers/ui/display.h +++ b/zephcore/helpers/ui/display.h @@ -1,151 +1,157 @@ -/* - * ZephCore - Display Abstraction (CFB) - * Copyright (c) 2025 ZephCore - * SPDX-License-Identifier: Apache-2.0 - * - * Wraps Zephyr's Character Framebuffer (CFB) subsystem with: - * - Auto-detection from devicetree (any Zephyr-supported display) - * - Runtime resolution query (supports any size, not just 128x64) - * - Auto-off timer via k_work_delayable - * - Simple text/rect drawing API for UI pages - * - * All functions prefixed mc_display_ to avoid collision with - * Zephyr's display_* namespace in . - */ - -#ifndef ZEPHCORE_DISPLAY_H -#define ZEPHCORE_DISPLAY_H - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * Initialize the display from devicetree. - * Detects any Zephyr-supported display via: - * 1. "zephyr,display" chosen node (standard) - * 2. Legacy nodelabels: sh1106, ssd1306 (backwards compat) - * - * Queries actual resolution from driver — no hardcoded dimensions. - * - * @return 0 on success, negative errno on failure, -ENODEV if no display - */ -int mc_display_init(void); - -/** - * Get display width in pixels (queried from hardware at init). - * Returns 0 if display not initialized. - */ -uint16_t mc_display_width(void); - -/** - * Get display height in pixels (queried from hardware at init). - * Returns 0 if display not initialized. - */ -uint16_t mc_display_height(void); - -/** - * Get active font width in pixels. - * Returns 0 if display not initialized. - */ -uint8_t mc_display_font_width(void); - -/** - * Get active font height in pixels. - * Returns 0 if display not initialized. - */ -uint8_t mc_display_font_height(void); - -/** - * Turn display on (wake from blanking). - * Resets the auto-off timer. - */ -void mc_display_on(void); - -/** - * Turn display off (blanking). - */ -void mc_display_off(void); - -/** - * @return true if the display is currently on - */ -bool mc_display_is_on(void); - -/** - * @return true if the display is an e-paper (EPD) type. - * EPD displays have slow refresh (~2s) and use zero power when static, - * so callers should use longer update intervals and skip blanking. - */ -bool mc_display_is_epd(void); - -/** - * Clear the framebuffer (fill with black). - * Call before rendering a new frame. - */ -void mc_display_clear(void); - -/** - * Draw text at position. - * - * @param x X position in pixels - * @param y Y position in pixels - * @param text Null-terminated string - * @param invert If true, draw black text on white background - */ -void mc_display_text(int x, int y, const char *text, bool invert); - -/** - * Draw a filled rectangle. - * - * @param x Top-left X - * @param y Top-left Y - * @param w Width - * @param h Height - */ -void mc_display_fill_rect(int x, int y, int w, int h); - -/** - * Draw a horizontal line. - */ -void mc_display_hline(int x, int y, int w); - -/** - * Draw a monochrome bitmap (Adafruit/Arduino format). - * MSB first, row-major, 1=foreground. - * Compatible with Arduino's drawBitmap() and MeshCore icons.h data. - * - * @param x Top-left X position - * @param y Top-left Y position - * @param data Bitmap data (MSB first, row-major) - * @param w Width in pixels - * @param h Height in pixels - */ -void mc_display_xbm(int x, int y, const uint8_t *data, int w, int h); - -/** - * Flush the framebuffer to the display hardware. - * Call after all drawing operations for a frame are complete. - */ -void mc_display_finalize(void); - -/** - * Reset the auto-off timer (called on user interaction). - */ -void mc_display_reset_auto_off(void); - -/** - * Get the raw display device pointer. - * Used by easter egg (Doom) to bypass CFB and write directly. - * Returns NULL if display not initialized. - */ -const struct device *mc_display_get_device(void); - -#ifdef __cplusplus -} -#endif - -#endif /* ZEPHCORE_DISPLAY_H */ +/* + * ZephCore - Display Abstraction (CFB) + * Copyright (c) 2025 ZephCore + * SPDX-License-Identifier: Apache-2.0 + * + * Wraps Zephyr's Character Framebuffer (CFB) subsystem with: + * - Auto-detection from devicetree (any Zephyr-supported display) + * - Runtime resolution query (supports any size, not just 128x64) + * - Auto-off timer via k_work_delayable + * - Simple text/rect drawing API for UI pages + * + * All functions prefixed mc_display_ to avoid collision with + * Zephyr's display_* namespace in . + */ + +#ifndef ZEPHCORE_DISPLAY_H +#define ZEPHCORE_DISPLAY_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Initialize the display from devicetree. + * Detects any Zephyr-supported display via: + * 1. "zephyr,display" chosen node (standard) + * 2. Legacy nodelabels: sh1106, ssd1306 (backwards compat) + * + * Queries actual resolution from driver — no hardcoded dimensions. + * + * @return 0 on success, negative errno on failure, -ENODEV if no display + */ +int mc_display_init(void); + +/** + * Get display width in pixels (queried from hardware at init). + * Returns 0 if display not initialized. + */ +uint16_t mc_display_width(void); + +/** + * Get display height in pixels (queried from hardware at init). + * Returns 0 if display not initialized. + */ +uint16_t mc_display_height(void); + +/** + * Get active font width in pixels. + * Returns 0 if display not initialized. + */ +uint8_t mc_display_font_width(void); + +/** + * Get active font height in pixels. + * Returns 0 if display not initialized. + */ +uint8_t mc_display_font_height(void); + +/** + * Turn display on (wake from blanking). + * Resets the auto-off timer. + */ +void mc_display_on(void); + +/** + * Turn display off (blanking). + */ +void mc_display_off(void); + +/** + * @return true if the display is currently on + */ +bool mc_display_is_on(void); + +/** + * @return true if the display is an e-paper (EPD) type. + * EPD displays have slow refresh (~2s) and use zero power when static, + * so callers should use longer update intervals and skip blanking. + */ +bool mc_display_is_epd(void); + +/** + * Clear the framebuffer (fill with black). + * Call before rendering a new frame. + */ +void mc_display_clear(void); + +/** + * Draw text at position. + * + * @param x X position in pixels + * @param y Y position in pixels + * @param text Null-terminated string + * @param invert If true, draw black text on white background + */ +void mc_display_text(int x, int y, const char *text, bool invert); + +/** + * Draw a filled rectangle. + * + * @param x Top-left X + * @param y Top-left Y + * @param w Width + * @param h Height + */ +void mc_display_fill_rect(int x, int y, int w, int h); + +/** + * Draw a horizontal line. + */ +void mc_display_hline(int x, int y, int w); + +/** + * Draw a monochrome bitmap (Adafruit/Arduino format). + * MSB first, row-major, 1=foreground. + * Compatible with Arduino's drawBitmap() and MeshCore icons.h data. + * + * @param x Top-left X position + * @param y Top-left Y position + * @param data Bitmap data (MSB first, row-major) + * @param w Width in pixels + * @param h Height in pixels + */ +void mc_display_xbm(int x, int y, const uint8_t *data, int w, int h); + +/** + * Flush the framebuffer to the display hardware. + * Call after all drawing operations for a frame are complete. + */ +void mc_display_finalize(void); + +/** + * Reset the auto-off timer (called on user interaction). + */ +void mc_display_reset_auto_off(void); + +/** + * EPD-only: force a full panel reset cycle before normal page rendering. + * No-op on non-EPD displays or when display is not initialized. + */ +void mc_display_epd_full_reset(void); + +/** + * Get the raw display device pointer. + * Used by easter egg (Doom) to bypass CFB and write directly. + * Returns NULL if display not initialized. + */ +const struct device *mc_display_get_device(void); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHCORE_DISPLAY_H */ diff --git a/zephcore/helpers/ui/ui_task.c b/zephcore/helpers/ui/ui_task.c index 0c38d52..718366a 100644 --- a/zephcore/helpers/ui/ui_task.c +++ b/zephcore/helpers/ui/ui_task.c @@ -245,6 +245,8 @@ static void splash_work_handler(struct k_work *work) splash_active = false; #ifdef CONFIG_ZEPHCORE_UI_DISPLAY + mc_display_epd_full_reset(); + /* Transition from splash to home page (first active page for this role) */ #ifdef ZEPHCORE_REPEATER ui_pages_set(UI_PAGE_STATUS); @@ -741,6 +743,8 @@ static void ui_input_cb(struct input_event *evt, void *user_data) k_work_cancel_delayable(&splash_work); splash_active = false; #ifdef CONFIG_ZEPHCORE_UI_DISPLAY + mc_display_epd_full_reset(); + #ifdef ZEPHCORE_REPEATER ui_pages_set(UI_PAGE_STATUS); #else