Files
2026-08-25 15:19:11 +02:00

387 lines
12 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 <zephyr/drivers/display.h>.
*/
#ifndef ZEPHCORE_DISPLAY_H
#define ZEPHCORE_DISPLAY_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <zephyr/devicetree.h>
#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 */