/* * ZephCore - Display Abstraction (CFB) * Copyright (c) 2025 ZephCore * SPDX-License-Identifier: MIT * * 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 #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); /* Color overlay support is compiled only when the devicetree points at a raw * RGB565 TFT (the runtime probe still verifies pixel format and readiness). * Boards without one get constant-false / mono-fallback inlines so every * color code path — including the ~3.8 KB overlay op queue in display.c — * is dropped at compile time. * * Two ways to name the panel, in priority order: * * 1. chosen { zephcore,color-tft = <&some_panel>; } * 2. the `tft` nodelabel on the panel node * * (1) exists for boards whose panel node lives in an upstream Zephyr DTS: * an overlay cannot add a nodelabel to an existing node, but it can always * set a chosen property. (2) is kept because every in-tree board that had * color before this indirection names its panel `tft:` — they need no edit. */ #if DT_HAS_CHOSEN(zephcore_color_tft) #define MC_DISPLAY_COLOR_NODE DT_CHOSEN(zephcore_color_tft) #elif DT_NODE_EXISTS(DT_NODELABEL(tft)) #define MC_DISPLAY_COLOR_NODE DT_NODELABEL(tft) #else #define MC_DISPLAY_COLOR_NODE DT_INVALID_NODE #endif #define MC_DISPLAY_COLOR_PANEL DT_NODE_EXISTS(MC_DISPLAY_COLOR_NODE) /* * The panel node mc_display_init() will bind to, resolved at compile time in * the same priority order the runtime lookup uses. Only needed to ask * compile-time questions about the panel; the device handle itself still * comes from the runtime lookup. */ #if DT_HAS_CHOSEN(zephyr_display) #define MC_DISPLAY_NODE DT_CHOSEN(zephyr_display) #elif DT_NODE_EXISTS(DT_NODELABEL(sh1106)) #define MC_DISPLAY_NODE DT_NODELABEL(sh1106) #elif DT_NODE_EXISTS(DT_NODELABEL(ssd1306)) #define MC_DISPLAY_NODE DT_NODELABEL(ssd1306) #else #define MC_DISPLAY_NODE DT_INVALID_NODE #endif /* * 180-degree rotation support, decided at compile time from the panel. * * Deliberately an allow-list of the two families where the rotation is a * hardware remap the driver actually implements: display_ssd1306.c flips * SEGMENT_MAP + COM_OUTPUT_SCAN (two bytes on the wire, framebuffer * untouched, no per-frame cost), and that driver backs both solomon,ssd1306 * and sinowealth,sh1106. * * Other panel families are excluded on purpose rather than probed: * - st7735r/st7789v (our mono-tft boards) return -ENOTSUP upstream for * anything but NORMAL, so a probe would just fail; * - ssd16xx e-paper *accepts* ROTATED_180 but implements it by flipping * the RAM entry mode only, which reverses byte order without reversing * bit order inside each byte — the 8 pixels a byte spans stay in their * original order. It would report success and render wrong, which is * worse than reporting unsupported. * * The geometry test on top of the family check matters as much as the family * check itself. Both remaps reverse the controller's *entire* addressable * range, not the part a given panel happens to use, so the flip only lands * back on the glass when the visible window is centred in that range: * * - Vertically, that means the panel uses the full multiplex height from * page 0. Boards that window a small panel into a larger controller do * not: lilygo_timpulse_plus is a 64x32 glass on a 128x64 SSD1306 driven * at page-offset 4 with multiplex-ratio 63, so its content sits on COM * 32..63. Reversing the COM scan moves it to COM 31..0 — off the bonded * region entirely, i.e. a blank screen. `page-offset == 0` and * `height == multiplex-ratio + 1` is exactly the "uses the whole * controller" condition, and it excludes that board. * - Horizontally the surviving boards are already centred: the SSD1306 * ones are a full 128 columns at segment-offset 0, and the SH1106 ones * are 128 columns at segment-offset 2 in 132 columns of RAM — the * standard 2-either-side layout these modules ship with. */ #if DT_NODE_HAS_COMPAT(MC_DISPLAY_NODE, solomon_ssd1306) || \ DT_NODE_HAS_COMPAT(MC_DISPLAY_NODE, sinowealth_sh1106) #define MC_DISPLAY_ROTATE_SUPPORTED \ (DT_PROP_OR(MC_DISPLAY_NODE, page_offset, 1) == 0 && \ DT_PROP_OR(MC_DISPLAY_NODE, height, 0) == \ DT_PROP_OR(MC_DISPLAY_NODE, multiplex_ratio, 0) + 1) #else #define MC_DISPLAY_ROTATE_SUPPORTED 0 #endif /** * Rotate the panel 180 degrees, for cases that mount the screen upside down * (e.g. the Meshnology N37E kit for the Wio Tracker L1). * * Takes effect on the next frame; the caller does not need to redraw. On * panels outside MC_DISPLAY_ROTATE_SUPPORTED this is a no-op that reports * the failure instead of pretending to have rotated. * * @param rotated true for 180 degrees, false for the panel's native orientation * @return 0 on success, -ENOTSUP if the panel cannot rotate, -ENODEV if no * display was initialized, or the driver's negative errno */ int mc_display_set_rotated(bool rotated); /** * @return true if the panel is currently rotated 180 degrees. */ bool mc_display_is_rotated(void); /** * @return true when a raw RGB565-capable color panel is available for * optional color overlays. Monochrome displays return false. */ #if MC_DISPLAY_COLOR_PANEL bool mc_display_has_color(void); #else static inline bool mc_display_has_color(void) { return false; } #endif /** * Glyph cell size of the color overlay renderer in pixels. The 6x8 font * is upscaled 1.5x under CONFIG_ZEPHCORE_DISPLAY_LARGE_FONT, so this can * differ from the CFB font metrics — layout math for color pages must use * these, not mc_display_font_width/height(). On monochrome builds * mc_display_color_text() falls back to the CFB path, so these fall back * to the CFB metrics too. */ #if MC_DISPLAY_COLOR_PANEL uint8_t mc_display_color_font_width(void); uint8_t mc_display_color_font_height(void); #else static inline uint8_t mc_display_color_font_width(void) { return mc_display_font_width(); } static inline uint8_t mc_display_color_font_height(void) { return mc_display_font_height(); } #endif /** * 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); /* Common RGB565 colors for optional color-capable pages. */ #define MC_COLOR_BLACK 0x0000 #define MC_COLOR_WHITE 0xffff #define MC_COLOR_GREEN 0x07e0 #define MC_COLOR_CYAN 0x07ff #define MC_COLOR_YELLOW 0xffe0 #define MC_COLOR_ORANGE 0xfd20 #define MC_COLOR_RED 0xf800 #define MC_COLOR_BLUE 0x001f #define MC_COLOR_GRAY 0x8410 /* true mid-gray — small-TFT gamma may crush it near-black */ #define MC_COLOR_LIGHT_GRAY 0xef7d /* ~93% white — reads as soft white on small TFTs */ /** * Draw text using RGB565 color when supported. On non-color displays this * falls back to mc_display_text(..., invert=false). * * Color overlays are flushed after the normal CFB frame in mc_display_finalize(). */ #if MC_DISPLAY_COLOR_PANEL void mc_display_color_text(int x, int y, const char *text, uint16_t color); #else static inline void mc_display_color_text(int x, int y, const char *text, uint16_t color) { (void)color; mc_display_text(x, y, text, false); } #endif /** * 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 filled rectangle using RGB565 color when supported. On non-color * displays this falls back to mc_display_fill_rect(). */ #if MC_DISPLAY_COLOR_PANEL void mc_display_color_fill_rect(int x, int y, int w, int h, uint16_t color); #else static inline void mc_display_color_fill_rect(int x, int y, int w, int h, uint16_t color) { (void)color; mc_display_fill_rect(x, y, w, h); } #endif /** * Draw a horizontal line. */ void mc_display_hline(int x, int y, int w); /** * Invert a rectangular region of the framebuffer. * Pixels that are on (white) become off (black) and vice versa. * Used to create clean dark-background modal overlays. */ void mc_display_invert_rect(int x, int y, int w, int h); /** * 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); /** * ZephCore logo bitmap (128 × 13 px, MSB-first, row-major). * Shared by both UI variants' splash screens. Defined in ui_common.c. */ #define ZEPHCORE_LOGO_W 128 #define ZEPHCORE_LOGO_H 13 extern const uint8_t zephcore_logo[]; /** * 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); /** * Override the auto-off timeout (0 = revert to Kconfig default). * Call from the UI layer when the user changes the screen-off duration * so the Kconfig-driven timer and the UI timer stay in sync. */ void mc_display_set_auto_off_ms(uint32_t ms); /** * 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); /** * Convert UTF-8 text to the display charset for rendering. * Passes ASCII unchanged, converts Latin-1 (U+00A0-U+00FF) to its native * code points, maps 32 Latin-2 letters (Hungarian/Czech/Slovak/Polish/...) * into font slots 128-159, folds the rest of Latin Extended-A to base ASCII * letters, and strips everything else (emojis, CJK). Bytes that are not * valid UTF-8 pass through unchanged, so already-converted text survives a * second pass. */ void utf8_to_display(char *dst, const char *src, size_t dst_size); /** * utf8_to_display() + leading-space trim (names are sometimes space-padded * to game sort order). */ void utf8_to_latin1(char *dst, const char *src, size_t dst_size); #ifdef __cplusplus } #endif #endif /* ZEPHCORE_DISPLAY_H */