From be4abb9289b9755e3f64d83ac06f15edd512586f Mon Sep 17 00:00:00 2001 From: Gabriel Lima Date: Fri, 3 Apr 2026 16:13:07 -0300 Subject: [PATCH] First version with generated code --- CMakeLists.txt | 21 +- gfx_canvas.cpp | 574 +++++++++++++++++++++++++++++++++++++++++++ gfx_canvas.h | 176 +++++++++++++ gfx_font_5x7.h | 123 ++++++++++ linux_fb_display.cpp | 214 ++++++++++++++++ linux_fb_display.h | 58 +++++ main.cpp | 139 ++++++++++- 7 files changed, 1295 insertions(+), 10 deletions(-) create mode 100644 gfx_canvas.cpp create mode 100644 gfx_canvas.h create mode 100644 gfx_font_5x7.h create mode 100644 linux_fb_display.cpp create mode 100644 linux_fb_display.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 31b2238..1866a2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,13 +38,29 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS ON) # This enables GNU extensions (gnu++23 instead of c++23) # Add compile options -add_compile_options(-Wall -Wextra -pedantic) +add_compile_options(-fno-exceptions -fno-rtti -Wall -Wextra -Wpedantic) # Build with debug symbols by default if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) endif() +# -------------------------------------------------------------------------- +# Graphics library (platform-independent core) +# -------------------------------------------------------------------------- +add_library(gfx_core STATIC + gfx_canvas.cpp +) +target_include_directories(gfx_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# -------------------------------------------------------------------------- +# Linux framebuffer HAL +# -------------------------------------------------------------------------- +add_library(gfx_linux_fb STATIC + linux_fb_display.cpp +) +target_link_libraries(gfx_linux_fb PUBLIC gfx_core) + # Use project name for executable add_executable(${PROJECT_NAME} main.cpp @@ -57,8 +73,7 @@ add_executable(${PROJECT_NAME} # another_file.cpp # ) -# If you need to link libraries (example: pthread) -# target_link_libraries(${PROJECT_NAME} PRIVATE pthread) +target_link_libraries(${PROJECT_NAME} PRIVATE gfx_linux_fb) # Install rule (optional) install(TARGETS ${PROJECT_NAME} DESTINATION bin) \ No newline at end of file diff --git a/gfx_canvas.cpp b/gfx_canvas.cpp new file mode 100644 index 0000000..3f14891 --- /dev/null +++ b/gfx_canvas.cpp @@ -0,0 +1,574 @@ +// gfx_canvas.cpp — Drawing algorithm implementations. + +#include "gfx_canvas.h" +#include + +namespace gfx { + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +static void swap16(int16_t& a, int16_t& b) { + int16_t t = a; a = b; b = t; +} + +static int16_t abs16(int16_t v) { + return v < 0 ? -v : v; +} + +static int16_t min16(int16_t a, int16_t b) { return a < b ? a : b; } +static int16_t max16(int16_t a, int16_t b) { return a > b ? a : b; } + +// --------------------------------------------------------------------------- +// Construction +// --------------------------------------------------------------------------- +GfxCanvas::GfxCanvas(int16_t width, int16_t height) + : raw_width_(width), raw_height_(height) {} + +// --------------------------------------------------------------------------- +// Geometry (accounts for rotation) +// --------------------------------------------------------------------------- +int16_t GfxCanvas::width() const { + return (rotation_ == Rotation::deg_90 || rotation_ == Rotation::deg_270) + ? raw_height_ : raw_width_; +} + +int16_t GfxCanvas::height() const { + return (rotation_ == Rotation::deg_90 || rotation_ == Rotation::deg_270) + ? raw_width_ : raw_height_; +} + +void GfxCanvas::setRotation(Rotation r) { rotation_ = r; } + +// --------------------------------------------------------------------------- +// drawPixel — bounds check + rotation → drawPixelImpl +// --------------------------------------------------------------------------- +void GfxCanvas::drawPixel(int16_t x, int16_t y, Color color) { + if (x < 0 || x >= width() || y < 0 || y >= height()) return; + + int16_t px = x, py = y; + switch (rotation_) { + case Rotation::deg_0: break; + case Rotation::deg_90: px = raw_height_ - 1 - y; py = x; break; + case Rotation::deg_180: px = raw_width_ - 1 - x; py = raw_height_ - 1 - y; break; + case Rotation::deg_270: px = y; py = raw_width_ - 1 - x; break; + } + + drawPixelImpl(px, py, color); +} + +// --------------------------------------------------------------------------- +// Horizontal line — clip + rotation → physical HLine or VLine +// --------------------------------------------------------------------------- +void GfxCanvas::drawHLine(int16_t x, int16_t y, int16_t w, Color color) { + if (w <= 0 || y < 0 || y >= height()) return; + if (x < 0) { w += x; x = 0; } + if (x + w > width()) w = width() - x; + if (w <= 0) return; + + switch (rotation_) { + case Rotation::deg_0: + drawHLineImpl(x, y, w, color); + break; + case Rotation::deg_90: + drawVLineImpl(raw_height_ - 1 - y, x, w, color); + break; + case Rotation::deg_180: + drawHLineImpl(raw_width_ - x - w, raw_height_ - 1 - y, w, color); + break; + case Rotation::deg_270: + drawVLineImpl(y, raw_width_ - x - w, w, color); + break; + } +} + +// --------------------------------------------------------------------------- +// Vertical line — clip + rotation → physical VLine or HLine +// --------------------------------------------------------------------------- +void GfxCanvas::drawVLine(int16_t x, int16_t y, int16_t h, Color color) { + if (h <= 0 || x < 0 || x >= width()) return; + if (y < 0) { h += y; y = 0; } + if (y + h > height()) h = height() - y; + if (h <= 0) return; + + switch (rotation_) { + case Rotation::deg_0: + drawVLineImpl(x, y, h, color); + break; + case Rotation::deg_90: + drawHLineImpl(raw_height_ - y - h, x, h, color); + break; + case Rotation::deg_180: + drawVLineImpl(raw_width_ - 1 - x, raw_height_ - y - h, h, color); + break; + case Rotation::deg_270: + drawHLineImpl(y, raw_width_ - 1 - x, h, color); + break; + } +} + +// --------------------------------------------------------------------------- +// Arbitrary line — Bresenham +// --------------------------------------------------------------------------- +void GfxCanvas::drawLine(int16_t x0, int16_t y0, + int16_t x1, int16_t y1, Color color) { + if (x0 == x1) { + if (y0 > y1) swap16(y0, y1); + drawVLine(x0, y0, y1 - y0 + 1, color); + return; + } + if (y0 == y1) { + if (x0 > x1) swap16(x0, x1); + drawHLine(x0, y0, x1 - x0 + 1, color); + return; + } + + bool steep = abs16(y1 - y0) > abs16(x1 - x0); + if (steep) { swap16(x0, y0); swap16(x1, y1); } + if (x0 > x1) { swap16(x0, x1); swap16(y0, y1); } + + int16_t dx = x1 - x0; + int16_t dy = abs16(y1 - y0); + int16_t err = dx / 2; + int16_t ystep = (y0 < y1) ? 1 : -1; + int16_t y = y0; + + for (int16_t x = x0; x <= x1; ++x) { + if (steep) drawPixel(y, x, color); + else drawPixel(x, y, color); + err -= dy; + if (err < 0) { + y += ystep; + err += dx; + } + } +} + +// --------------------------------------------------------------------------- +// Rectangles +// --------------------------------------------------------------------------- +void GfxCanvas::drawRect(int16_t x, int16_t y, + int16_t w, int16_t h, Color color) { + if (w <= 0 || h <= 0) return; + drawHLine(x, y, w, color); + drawHLine(x, y + h - 1, w, color); + drawVLine(x, y + 1, h - 2, color); + drawVLine(x + w - 1, y + 1, h - 2, color); +} + +void GfxCanvas::fillRect(int16_t x, int16_t y, + int16_t w, int16_t h, Color color) { + if (w <= 0 || h <= 0) return; + + // Clip to screen bounds. + if (x < 0) { w += x; x = 0; } + if (y < 0) { h += y; y = 0; } + if (x + w > width()) w = width() - x; + if (y + h > height()) h = height() - y; + if (w <= 0 || h <= 0) return; + + // Transform the rectangle to physical coordinates. + // After rotation, an axis-aligned rect stays axis-aligned. + int16_t px = x, py = y, pw = w, ph = h; + + switch (rotation_) { + case Rotation::deg_0: + break; + case Rotation::deg_90: + px = raw_height_ - y - h; + py = x; + pw = h; + ph = w; + break; + case Rotation::deg_180: + px = raw_width_ - x - w; + py = raw_height_ - y - h; + break; + case Rotation::deg_270: + px = y; + py = raw_width_ - x - w; + pw = h; + ph = w; + break; + } + + fillRectImpl(px, py, pw, ph, color); +} + +// --------------------------------------------------------------------------- +// Circles — midpoint algorithm +// --------------------------------------------------------------------------- +void GfxCanvas::drawCircle(int16_t cx, int16_t cy, + int16_t r, Color color) { + if (r <= 0) { drawPixel(cx, cy, color); return; } + drawCircleQuadrants(cx, cy, r, 0x0F, color); +} + +// Draws selected quadrant arcs using the midpoint circle algorithm. +// bit 0 (0x01): top-right +// bit 1 (0x02): top-left +// bit 2 (0x04): bottom-left +// bit 3 (0x08): bottom-right +void GfxCanvas::drawCircleQuadrants(int16_t cx, int16_t cy, int16_t r, + uint8_t corners, Color color) { + int16_t x = 0, y = r; + int16_t d = 1 - r; + + while (x <= y) { + if (corners & 0x01) { drawPixel(cx + x, cy - y, color); + drawPixel(cx + y, cy - x, color); } + if (corners & 0x02) { drawPixel(cx - y, cy - x, color); + drawPixel(cx - x, cy - y, color); } + if (corners & 0x04) { drawPixel(cx - x, cy + y, color); + drawPixel(cx - y, cy + x, color); } + if (corners & 0x08) { drawPixel(cx + y, cy + x, color); + drawPixel(cx + x, cy + y, color); } + + if (d < 0) { + d += 2 * x + 3; + } else { + d += 2 * (x - y) + 5; + --y; + } + ++x; + } +} + +void GfxCanvas::fillCircle(int16_t cx, int16_t cy, + int16_t r, Color color) { + if (r <= 0) { drawPixel(cx, cy, color); return; } + drawVLine(cx, cy - r, 2 * r + 1, color); + fillCircleSpans(cx, cy, r, 0x03, 0, color); +} + +// Fills horizontal spans for circle quadrants. +// bit 0: right half (cx to cx+x) +// bit 1: left half (cx-x to cx) +// delta: extra width added to each span (used by fillRoundRect). +void GfxCanvas::fillCircleSpans(int16_t cx, int16_t cy, int16_t r, + uint8_t corners, int16_t delta, + Color color) { + int16_t x = 0, y = r; + int16_t d = 1 - r; + int16_t prev_y = -1; // track to avoid duplicate spans + + while (x <= y) { + if (d >= 0) { + if (corners & 0x01) { + drawHLine(cx - x, cy + y, 2 * x + 1 + delta, color); + drawHLine(cx - x, cy - y, 2 * x + 1 + delta, color); + } + if (corners & 0x02) { + drawHLine(cx - x, cy + y, 2 * x + 1 + delta, color); + drawHLine(cx - x, cy - y, 2 * x + 1 + delta, color); + } + d += 2 * (x - y) + 5; + --y; + } else { + d += 2 * x + 3; + } + + if (x != prev_y) { + if (corners & 0x01) { + drawHLine(cx - y, cy + x, 2 * y + 1 + delta, color); + drawHLine(cx - y, cy - x, 2 * y + 1 + delta, color); + } + if (corners & 0x02) { + drawHLine(cx - y, cy + x, 2 * y + 1 + delta, color); + drawHLine(cx - y, cy - x, 2 * y + 1 + delta, color); + } + } + prev_y = y; + ++x; + } +} + +// --------------------------------------------------------------------------- +// Rounded rectangles +// --------------------------------------------------------------------------- +void GfxCanvas::drawRoundRect(int16_t x, int16_t y, + int16_t w, int16_t h, + int16_t r, Color color) { + if (w <= 0 || h <= 0) return; + if (r <= 0) { drawRect(x, y, w, h, color); return; } + + int16_t max_r = min16(w, h) / 2; + if (r > max_r) r = max_r; + + // Straight edges (between the arc endpoints). + drawHLine(x + r, y, w - 2 * r, color); // top + drawHLine(x + r, y + h - 1, w - 2 * r, color); // bottom + drawVLine(x, y + r, h - 2 * r, color); // left + drawVLine(x + w - 1, y + r, h - 2 * r, color); // right + + // Four corner arcs. + drawCircleQuadrants(x + w - 1 - r, y + r, r, 0x01, color); // top-right + drawCircleQuadrants(x + r, y + r, r, 0x02, color); // top-left + drawCircleQuadrants(x + r, y + h - 1 - r, r, 0x04, color); // bottom-left + drawCircleQuadrants(x + w - 1 - r, y + h - 1 - r, r, 0x08, color);// bottom-right +} + +void GfxCanvas::fillRoundRect(int16_t x, int16_t y, + int16_t w, int16_t h, + int16_t r, Color color) { + if (w <= 0 || h <= 0) return; + if (r <= 0) { fillRect(x, y, w, h, color); return; } + + int16_t max_r = min16(w, h) / 2; + if (r > max_r) r = max_r; + + // Central rectangle. + fillRect(x + r, y, w - 2 * r, h, color); + + // Side rectangles. + fillRect(x, y + r, r, h - 2 * r, color); + fillRect(x + w - r, y + r, r, h - 2 * r, color); + + // Four filled corners using spans. + int16_t cx_l = x + r; + int16_t cx_r = x + w - 1 - r; + int16_t cy_t = y + r; + int16_t cy_b = y + h - 1 - r; + + int16_t xi = 0, yi = r; + int16_t d = 1 - r; + + while (xi <= yi) { + // Top corners. + drawHLine(cx_l - xi, cy_t - yi, xi + (cx_r - cx_l) + xi + 1, color); + drawHLine(cx_l - yi, cy_t - xi, yi + (cx_r - cx_l) + yi + 1, color); + // Bottom corners. + drawHLine(cx_l - xi, cy_b + yi, xi + (cx_r - cx_l) + xi + 1, color); + drawHLine(cx_l - yi, cy_b + xi, yi + (cx_r - cx_l) + yi + 1, color); + + if (d < 0) { + d += 2 * xi + 3; + } else { + d += 2 * (xi - yi) + 5; + --yi; + } + ++xi; + } +} + +// --------------------------------------------------------------------------- +// Triangles +// --------------------------------------------------------------------------- +void GfxCanvas::drawTriangle(int16_t x0, int16_t y0, + int16_t x1, int16_t y1, + int16_t x2, int16_t y2, Color color) { + drawLine(x0, y0, x1, y1, color); + drawLine(x1, y1, x2, y2, color); + drawLine(x2, y2, x0, y0, color); +} + +void GfxCanvas::fillTriangle(int16_t x0, int16_t y0, + int16_t x1, int16_t y1, + int16_t x2, int16_t y2, Color color) { + // Sort by y-coordinate ascending. + if (y0 > y1) { swap16(x0, x1); swap16(y0, y1); } + if (y1 > y2) { swap16(x1, x2); swap16(y1, y2); } + if (y0 > y1) { swap16(x0, x1); swap16(y0, y1); } + + // Degenerate: all on same row. + if (y0 == y2) { + int16_t a = min16(min16(x0, x1), x2); + int16_t b = max16(max16(x0, x1), x2); + drawHLine(a, y0, b - a + 1, color); + return; + } + + // Edge slopes using fixed-point (16.16) arithmetic. + // Walk from top to bottom, interpolating left and right x boundaries. + int32_t dx02 = x2 - x0, dy02 = y2 - y0; + int32_t dx01 = x1 - x0, dy01 = y1 - y0; + int32_t dx12 = x2 - x1, dy12 = y2 - y1; + + // Accumulator for edge 0→2 (spans the full height). + int32_t sa = 0; + // Accumulator for edge 0→1 (top half) then 1→2 (bottom half). + int32_t sb = 0; + + // Top half: y0 → y1. + int16_t last_y = (y1 == y2) ? y1 : y1 - 1; + for (int16_t y = y0; y <= last_y; ++y) { + int16_t a = x0 + static_cast(sa / dy02); + int16_t b = x0 + static_cast(sb / dy01); + sa += dx02; + sb += dx01; + if (a > b) swap16(a, b); + drawHLine(a, y, b - a + 1, color); + } + + // Bottom half: y1 → y2. + sb = dx12 * (last_y + 1 - y1); + for (int16_t y = last_y + 1; y <= y2; ++y) { + int16_t a = x0 + static_cast(sa / dy02); + int16_t b = x1 + static_cast(sb / dy12); + sa += dx02; + sb += dx12; + if (a > b) swap16(a, b); + drawHLine(a, y, b - a + 1, color); + } +} + +// --------------------------------------------------------------------------- +// Screen +// --------------------------------------------------------------------------- +void GfxCanvas::fillScreen(Color color) { + fillRect(0, 0, width(), height(), color); +} + +// --------------------------------------------------------------------------- +// Text — configuration +// --------------------------------------------------------------------------- +void GfxCanvas::setFont(const Font& font) { font_ = &font; } + +void GfxCanvas::setCursor(int16_t x, int16_t y) { + cursor_x_ = x; + cursor_y_ = y; +} + +void GfxCanvas::setTextColor(Color fg) { + text_fg_ = fg; + text_bg_opaque_ = false; +} + +void GfxCanvas::setTextColor(Color fg, Color bg) { + text_fg_ = fg; + text_bg_ = bg; + text_bg_opaque_ = true; +} + +void GfxCanvas::setTextWrap(bool wrap) { text_wrap_ = wrap; } + +// --------------------------------------------------------------------------- +// Text — character rendering (column-major font encoding) +// --------------------------------------------------------------------------- +void GfxCanvas::drawChar(int16_t x, int16_t y, char c, + Color fg, Color bg) const { + if (!font_) return; + if (c < static_cast(font_->first_char) || + c > static_cast(font_->last_char)) + return; + + int idx = c - font_->first_char; + const uint8_t* glyph = font_->bitmap + idx * font_->glyph_width; + + // Column-major: each byte is one column, bit 0 = top row. + for (int16_t col = 0; col < font_->glyph_width; ++col) { + uint8_t column_bits = glyph[col]; + for (int16_t row = 0; row < font_->glyph_height; ++row) { + if (column_bits & (1 << row)) { + const_cast(this)->drawPixel( + x + col, y + row, fg); + } else if (text_bg_opaque_) { + const_cast(this)->drawPixel( + x + col, y + row, bg); + } + } + } +} + +// --------------------------------------------------------------------------- +// Text — string rendering +// --------------------------------------------------------------------------- +void GfxCanvas::drawString(int16_t x, int16_t y, + const char* str, Color color) { + if (!font_ || !str) return; + while (*str) { + drawChar(x, y, *str, color, text_bg_); + x += font_->x_advance; + ++str; + } +} + +void GfxCanvas::print(char c) { + if (!font_) return; + if (c == '\n') { + cursor_x_ = 0; + cursor_y_ += font_->y_advance; + return; + } + + if (text_wrap_ && (cursor_x_ + font_->x_advance > width())) { + cursor_x_ = 0; + cursor_y_ += font_->y_advance; + } + + drawChar(cursor_x_, cursor_y_, c, text_fg_, text_bg_); + cursor_x_ += font_->x_advance; +} + +void GfxCanvas::print(const char* str) { + if (!str) return; + while (*str) { + print(*str); + ++str; + } +} + +// --------------------------------------------------------------------------- +// Text — measurement +// --------------------------------------------------------------------------- +void GfxCanvas::getTextBounds(const char* str, int16_t cx, int16_t cy, + int16_t& bx, int16_t& by, + uint16_t& bw, uint16_t& bh) const { + bx = cx; + by = cy; + bw = 0; + bh = 0; + if (!font_ || !str) return; + + int16_t x = cx; + int16_t min_x = cx, max_x = cx; + int16_t min_y = cy, max_y = cy; + + while (*str) { + if (*str == '\n') { + x = cx; + cy += font_->y_advance; + } else { + int16_t gx = x; + int16_t gy = cy; + int16_t gx2 = gx + font_->glyph_width - 1; + int16_t gy2 = gy + font_->glyph_height - 1; + + if (gx < min_x) min_x = gx; + if (gx2 > max_x) max_x = gx2; + if (gy < min_y) min_y = gy; + if (gy2 > max_y) max_y = gy2; + + x += font_->x_advance; + } + ++str; + } + + if (max_x >= min_x) { + bx = min_x; + by = min_y; + bw = static_cast(max_x - min_x + 1); + bh = static_cast(max_y - min_y + 1); + } +} + +// --------------------------------------------------------------------------- +// Default Impl methods — loop over drawPixelImpl +// --------------------------------------------------------------------------- +void GfxCanvas::drawHLineImpl(int16_t x, int16_t y, int16_t w, Color color) { + for (int16_t i = 0; i < w; ++i) + drawPixelImpl(x + i, y, color); +} + +void GfxCanvas::drawVLineImpl(int16_t x, int16_t y, int16_t h, Color color) { + for (int16_t i = 0; i < h; ++i) + drawPixelImpl(x, y + i, color); +} + +void GfxCanvas::fillRectImpl(int16_t x, int16_t y, + int16_t w, int16_t h, Color color) { + for (int16_t row = 0; row < h; ++row) + drawHLineImpl(x, y + row, w, color); +} + +} // namespace gfx diff --git a/gfx_canvas.h b/gfx_canvas.h new file mode 100644 index 0000000..aef40d9 --- /dev/null +++ b/gfx_canvas.h @@ -0,0 +1,176 @@ +// gfx_canvas.h — Minimal embedded graphics library. +// +// Zero dynamic allocation. No exceptions. C++23 compatible. +// Compatible with glibc and newlib. +// +// Color model: XRGB8888 (uint32_t) everywhere in the public API. +// The HAL subclass converts to whatever the hardware needs. + +#ifndef GFX_CANVAS_H +#define GFX_CANVAS_H + +#include +#include + +namespace gfx { + +// --------------------------------------------------------------------------- +// Color +// --------------------------------------------------------------------------- +using Color = uint32_t; // 0x00RRGGBB + +namespace colors { + inline constexpr Color black = 0x00000000; + inline constexpr Color white = 0x00FFFFFF; + inline constexpr Color red = 0x00FF0000; + inline constexpr Color green = 0x0000FF00; + inline constexpr Color blue = 0x000000FF; + inline constexpr Color yellow = 0x00FFFF00; + inline constexpr Color cyan = 0x0000FFFF; + inline constexpr Color magenta = 0x00FF00FF; +} // namespace colors + +constexpr Color rgb(uint8_t r, uint8_t g, uint8_t b) { + return (static_cast(r) << 16) | + (static_cast(g) << 8) | + static_cast(b); +} + +// --------------------------------------------------------------------------- +// Font — fixed-width, column-major encoding +// +// Each glyph occupies glyph_width consecutive bytes. +// Each byte is one column, bit 0 = topmost row, bit (glyph_height-1) = bottom. +// This is the standard encoding used by SSD1306 fonts, u8g2 legacy fonts, +// and the classic Adafruit 5x7 glcdfont. +// --------------------------------------------------------------------------- +struct Font { + const uint8_t* bitmap; // glyph data, column-major + uint8_t glyph_width; // pixels per glyph (all glyphs same width) + uint8_t glyph_height; // pixels tall + uint8_t first_char; // first codepoint (usually 0x20) + uint8_t last_char; // last codepoint (usually 0x7E) + uint8_t x_advance; // horizontal cursor step (width + spacing) + uint8_t y_advance; // vertical line step (height + spacing) +}; + +// --------------------------------------------------------------------------- +// Rotation +// --------------------------------------------------------------------------- +enum class Rotation : uint8_t { + deg_0 = 0, + deg_90 = 1, + deg_180 = 2, + deg_270 = 3, +}; + +// --------------------------------------------------------------------------- +// GfxCanvas — abstract base class +// --------------------------------------------------------------------------- +class GfxCanvas { +public: + GfxCanvas(int16_t width, int16_t height); + virtual ~GfxCanvas() = default; + + GfxCanvas(const GfxCanvas&) = delete; + GfxCanvas& operator=(const GfxCanvas&) = delete; + GfxCanvas(GfxCanvas&&) = delete; + GfxCanvas& operator=(GfxCanvas&&) = delete; + + // --- Pixel ---------------------------------------------------------- + void drawPixel(int16_t x, int16_t y, Color color); + + // --- Lines ---------------------------------------------------------- + void drawHLine(int16_t x, int16_t y, int16_t w, Color color); + void drawVLine(int16_t x, int16_t y, int16_t h, Color color); + void drawLine(int16_t x0, int16_t y0, + int16_t x1, int16_t y1, Color color); + + // --- Rectangles ----------------------------------------------------- + void drawRect(int16_t x, int16_t y, + int16_t w, int16_t h, Color color); + void fillRect(int16_t x, int16_t y, + int16_t w, int16_t h, Color color); + + // --- Circles -------------------------------------------------------- + void drawCircle(int16_t cx, int16_t cy, int16_t r, Color color); + void fillCircle(int16_t cx, int16_t cy, int16_t r, Color color); + + // --- Rounded rectangles --------------------------------------------- + void drawRoundRect(int16_t x, int16_t y, + int16_t w, int16_t h, + int16_t r, Color color); + void fillRoundRect(int16_t x, int16_t y, + int16_t w, int16_t h, + int16_t r, Color color); + + // --- Triangles ------------------------------------------------------ + void drawTriangle(int16_t x0, int16_t y0, + int16_t x1, int16_t y1, + int16_t x2, int16_t y2, Color color); + void fillTriangle(int16_t x0, int16_t y0, + int16_t x1, int16_t y1, + int16_t x2, int16_t y2, Color color); + + // --- Screen --------------------------------------------------------- + void fillScreen(Color color); + + // --- Text ----------------------------------------------------------- + void setFont(const Font& font); + void setCursor(int16_t x, int16_t y); + void setTextColor(Color fg); + void setTextColor(Color fg, Color bg); + void setTextWrap(bool wrap); + + void drawChar(int16_t x, int16_t y, char c, Color fg, Color bg) const; + void drawString(int16_t x, int16_t y, const char* str, Color color); + void print(const char* str); + void print(char c); + + void getTextBounds(const char* str, int16_t cx, int16_t cy, + int16_t& bx, int16_t& by, + uint16_t& bw, uint16_t& bh) const; + + // --- Rotation ------------------------------------------------------- + void setRotation(Rotation r); + Rotation rotation() const { return rotation_; } + + // --- Geometry ------------------------------------------------------- + int16_t width() const; + int16_t height() const; + int16_t rawWidth() const { return raw_width_; } + int16_t rawHeight() const { return raw_height_; } + +protected: + // === HAL — subclass must implement drawPixelImpl =================== + virtual void drawPixelImpl(int16_t x, int16_t y, Color color) = 0; + + // === HAL — optional fast-path overrides ============================ + virtual void drawHLineImpl(int16_t x, int16_t y, int16_t w, Color color); + virtual void drawVLineImpl(int16_t x, int16_t y, int16_t h, Color color); + virtual void fillRectImpl(int16_t x, int16_t y, + int16_t w, int16_t h, Color color); + +private: + int16_t raw_width_; + int16_t raw_height_; + int16_t cursor_x_ = 0; + int16_t cursor_y_ = 0; + Color text_fg_ = colors::white; + Color text_bg_ = colors::black; + bool text_bg_opaque_ = false; + bool text_wrap_ = true; + Rotation rotation_ = Rotation::deg_0; + + const Font* font_ = nullptr; + + // Internal helpers for circle quadrant drawing. + void drawCircleQuadrants(int16_t cx, int16_t cy, int16_t r, + uint8_t corners, Color color); + void fillCircleSpans(int16_t cx, int16_t cy, int16_t r, + uint8_t corners, int16_t delta, Color color); +}; + +} // namespace gfx + +#endif // GFX_CANVAS_H diff --git a/gfx_font_5x7.h b/gfx_font_5x7.h new file mode 100644 index 0000000..ba72326 --- /dev/null +++ b/gfx_font_5x7.h @@ -0,0 +1,123 @@ +// gfx_font_5x7.h — Built-in 5x7 monospace bitmap font. +// +// Column-major encoding: 5 bytes per glyph, each byte is one column, +// bit 0 = topmost row. Covers printable ASCII 0x20–0x7E (95 glyphs). + +#ifndef GFX_FONT_5X7_H +#define GFX_FONT_5X7_H + +#include "gfx_canvas.h" + +namespace gfx { + +inline constexpr uint8_t font_5x7_bitmap[] = { + 0x00,0x00,0x00,0x00,0x00, // 0x20 ' ' + 0x00,0x00,0x5F,0x00,0x00, // 0x21 '!' + 0x00,0x07,0x00,0x07,0x00, // 0x22 '"' + 0x14,0x7F,0x14,0x7F,0x14, // 0x23 '#' + 0x24,0x2A,0x7F,0x2A,0x12, // 0x24 '$' + 0x23,0x13,0x08,0x64,0x62, // 0x25 '%' + 0x36,0x49,0x55,0x22,0x50, // 0x26 '&' + 0x00,0x05,0x03,0x00,0x00, // 0x27 ''' + 0x00,0x1C,0x22,0x41,0x00, // 0x28 '(' + 0x00,0x41,0x22,0x1C,0x00, // 0x29 ')' + 0x14,0x08,0x3E,0x08,0x14, // 0x2A '*' + 0x08,0x08,0x3E,0x08,0x08, // 0x2B '+' + 0x00,0x50,0x30,0x00,0x00, // 0x2C ',' + 0x08,0x08,0x08,0x08,0x08, // 0x2D '-' + 0x00,0x60,0x60,0x00,0x00, // 0x2E '.' + 0x20,0x10,0x08,0x04,0x02, // 0x2F '/' + 0x3E,0x51,0x49,0x45,0x3E, // 0x30 '0' + 0x00,0x42,0x7F,0x40,0x00, // 0x31 '1' + 0x42,0x61,0x51,0x49,0x46, // 0x32 '2' + 0x21,0x41,0x45,0x4B,0x31, // 0x33 '3' + 0x18,0x14,0x12,0x7F,0x10, // 0x34 '4' + 0x27,0x45,0x45,0x45,0x39, // 0x35 '5' + 0x3C,0x4A,0x49,0x49,0x30, // 0x36 '6' + 0x01,0x71,0x09,0x05,0x03, // 0x37 '7' + 0x36,0x49,0x49,0x49,0x36, // 0x38 '8' + 0x06,0x49,0x49,0x29,0x1E, // 0x39 '9' + 0x00,0x36,0x36,0x00,0x00, // 0x3A ':' + 0x00,0x56,0x36,0x00,0x00, // 0x3B ';' + 0x08,0x14,0x22,0x41,0x00, // 0x3C '<' + 0x14,0x14,0x14,0x14,0x14, // 0x3D '=' + 0x00,0x41,0x22,0x14,0x08, // 0x3E '>' + 0x02,0x01,0x51,0x09,0x06, // 0x3F '?' + 0x32,0x49,0x79,0x41,0x3E, // 0x40 '@' + 0x7E,0x11,0x11,0x11,0x7E, // 0x41 'A' + 0x7F,0x49,0x49,0x49,0x36, // 0x42 'B' + 0x3E,0x41,0x41,0x41,0x22, // 0x43 'C' + 0x7F,0x41,0x41,0x22,0x1C, // 0x44 'D' + 0x7F,0x49,0x49,0x49,0x41, // 0x45 'E' + 0x7F,0x09,0x09,0x09,0x01, // 0x46 'F' + 0x3E,0x41,0x49,0x49,0x7A, // 0x47 'G' + 0x7F,0x08,0x08,0x08,0x7F, // 0x48 'H' + 0x00,0x41,0x7F,0x41,0x00, // 0x49 'I' + 0x20,0x40,0x41,0x3F,0x01, // 0x4A 'J' + 0x7F,0x08,0x14,0x22,0x41, // 0x4B 'K' + 0x7F,0x40,0x40,0x40,0x40, // 0x4C 'L' + 0x7F,0x02,0x0C,0x02,0x7F, // 0x4D 'M' + 0x7F,0x04,0x08,0x10,0x7F, // 0x4E 'N' + 0x3E,0x41,0x41,0x41,0x3E, // 0x4F 'O' + 0x7F,0x09,0x09,0x09,0x06, // 0x50 'P' + 0x3E,0x41,0x51,0x21,0x5E, // 0x51 'Q' + 0x7F,0x09,0x19,0x29,0x46, // 0x52 'R' + 0x46,0x49,0x49,0x49,0x31, // 0x53 'S' + 0x01,0x01,0x7F,0x01,0x01, // 0x54 'T' + 0x3F,0x40,0x40,0x40,0x3F, // 0x55 'U' + 0x1F,0x20,0x40,0x20,0x1F, // 0x56 'V' + 0x3F,0x40,0x38,0x40,0x3F, // 0x57 'W' + 0x63,0x14,0x08,0x14,0x63, // 0x58 'X' + 0x07,0x08,0x70,0x08,0x07, // 0x59 'Y' + 0x61,0x51,0x49,0x45,0x43, // 0x5A 'Z' + 0x00,0x7F,0x41,0x41,0x00, // 0x5B '[' + 0x02,0x04,0x08,0x10,0x20, // 0x5C '\' + 0x00,0x41,0x41,0x7F,0x00, // 0x5D ']' + 0x04,0x02,0x01,0x02,0x04, // 0x5E '^' + 0x40,0x40,0x40,0x40,0x40, // 0x5F '_' + 0x00,0x01,0x02,0x04,0x00, // 0x60 '`' + 0x20,0x54,0x54,0x54,0x78, // 0x61 'a' + 0x7F,0x48,0x44,0x44,0x38, // 0x62 'b' + 0x38,0x44,0x44,0x44,0x20, // 0x63 'c' + 0x38,0x44,0x44,0x48,0x7F, // 0x64 'd' + 0x38,0x54,0x54,0x54,0x18, // 0x65 'e' + 0x08,0x7E,0x09,0x01,0x02, // 0x66 'f' + 0x0C,0x52,0x52,0x52,0x3E, // 0x67 'g' + 0x7F,0x08,0x04,0x04,0x78, // 0x68 'h' + 0x00,0x44,0x7D,0x40,0x00, // 0x69 'i' + 0x20,0x40,0x44,0x3D,0x00, // 0x6A 'j' + 0x7F,0x10,0x28,0x44,0x00, // 0x6B 'k' + 0x00,0x41,0x7F,0x40,0x00, // 0x6C 'l' + 0x7C,0x04,0x18,0x04,0x78, // 0x6D 'm' + 0x7C,0x08,0x04,0x04,0x78, // 0x6E 'n' + 0x38,0x44,0x44,0x44,0x38, // 0x6F 'o' + 0x7C,0x14,0x14,0x14,0x08, // 0x70 'p' + 0x08,0x14,0x14,0x18,0x7C, // 0x71 'q' + 0x7C,0x08,0x04,0x04,0x08, // 0x72 'r' + 0x48,0x54,0x54,0x54,0x20, // 0x73 's' + 0x04,0x3F,0x44,0x40,0x20, // 0x74 't' + 0x3C,0x40,0x40,0x20,0x7C, // 0x75 'u' + 0x1C,0x20,0x40,0x20,0x1C, // 0x76 'v' + 0x3C,0x40,0x30,0x40,0x3C, // 0x77 'w' + 0x44,0x28,0x10,0x28,0x44, // 0x78 'x' + 0x0C,0x50,0x50,0x50,0x3C, // 0x79 'y' + 0x44,0x64,0x54,0x4C,0x44, // 0x7A 'z' + 0x00,0x08,0x36,0x41,0x00, // 0x7B '{' + 0x00,0x00,0x7F,0x00,0x00, // 0x7C '|' + 0x00,0x41,0x36,0x08,0x00, // 0x7D '}' + 0x10,0x08,0x08,0x10,0x08, // 0x7E '~' +}; + +inline constexpr Font font_5x7 = { + .bitmap = font_5x7_bitmap, + .glyph_width = 5, + .glyph_height = 7, + .first_char = 0x20, + .last_char = 0x7E, + .x_advance = 6, // 5px glyph + 1px spacing + .y_advance = 9, // 7px glyph + 2px spacing +}; + +} // namespace gfx + +#endif // GFX_FONT_5X7_H diff --git a/linux_fb_display.cpp b/linux_fb_display.cpp new file mode 100644 index 0000000..53dc932 --- /dev/null +++ b/linux_fb_display.cpp @@ -0,0 +1,214 @@ +// linux_fb_display.cpp — Linux framebuffer HAL implementation. + +#include "linux_fb_display.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace gfx { + +// --------------------------------------------------------------------------- +// Construction / destruction +// --------------------------------------------------------------------------- +LinuxFBDisplay::LinuxFBDisplay(int16_t width, int16_t height) + : GfxCanvas(width, height) {} + +LinuxFBDisplay::~LinuxFBDisplay() { close(); } + +// --------------------------------------------------------------------------- +// Open / close +// --------------------------------------------------------------------------- +bool LinuxFBDisplay::open(const char* device) { + fd_ = ::open(device, O_RDWR); + if (fd_ < 0) { + std::perror("LinuxFBDisplay::open"); + return false; + } + + if (ioctl(fd_, FBIOGET_VSCREENINFO, &vinfo_) < 0) { + std::perror("FBIOGET_VSCREENINFO"); + close(); + return false; + } + if (ioctl(fd_, FBIOGET_FSCREENINFO, &finfo_) < 0) { + std::perror("FBIOGET_FSCREENINFO"); + close(); + return false; + } + + bpp_ = vinfo_.bits_per_pixel; + line_len_ = finfo_.line_length; + map_size_ = finfo_.smem_len; + + buf_ = static_cast( + mmap(nullptr, map_size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0)); + if (buf_ == MAP_FAILED) { + std::perror("mmap"); + buf_ = nullptr; + close(); + return false; + } + + return true; +} + +void LinuxFBDisplay::close() { + if (buf_) { + munmap(buf_, map_size_); + buf_ = nullptr; + } + if (fd_ >= 0) { + ::close(fd_); + fd_ = -1; + } +} + +// --------------------------------------------------------------------------- +// Pixel format conversion +// --------------------------------------------------------------------------- +void LinuxFBDisplay::writePixel(uint8_t* dst, Color color) const { + if (bpp_ == 32) { + // XRGB8888 — write directly. + std::memcpy(dst, &color, 4); + } else if (bpp_ == 16) { + // RGB565 + uint16_t r = (color >> 19) & 0x1F; + uint16_t g = (color >> 10) & 0x3F; + uint16_t b = (color >> 3) & 0x1F; + uint16_t pixel = (r << 11) | (g << 5) | b; + std::memcpy(dst, &pixel, 2); + } else if (bpp_ == 8) { + // Grayscale — simple luminance approximation. + uint8_t r = (color >> 16) & 0xFF; + uint8_t g = (color >> 8) & 0xFF; + uint8_t b = color & 0xFF; + dst[0] = static_cast((r * 77 + g * 150 + b * 29) >> 8); + } +} + +// --------------------------------------------------------------------------- +// drawPixelImpl +// --------------------------------------------------------------------------- +void LinuxFBDisplay::drawPixelImpl(int16_t x, int16_t y, Color color) { + if (!buf_) return; + uint32_t offset = static_cast(y) * line_len_ + + static_cast(x) * (bpp_ / 8); + writePixel(buf_ + offset, color); +} + +// --------------------------------------------------------------------------- +// Accelerated horizontal line — memset for monochrome, loop for color +// --------------------------------------------------------------------------- +void LinuxFBDisplay::drawHLineImpl(int16_t x, int16_t y, + int16_t w, Color color) { + if (!buf_ || w <= 0) return; + + uint32_t bytes_pp = bpp_ / 8; + uint8_t* row = buf_ + static_cast(y) * line_len_ + + static_cast(x) * bytes_pp; + + if (bpp_ == 32) { + // For XRGB8888, use 32-bit fill. + auto* p = reinterpret_cast(row); + for (int16_t i = 0; i < w; ++i) + p[i] = color; + } else if (bpp_ == 16) { + uint16_t r = (color >> 19) & 0x1F; + uint16_t g = (color >> 10) & 0x3F; + uint16_t b = (color >> 3) & 0x1F; + uint16_t pixel = (r << 11) | (g << 5) | b; + auto* p = reinterpret_cast(row); + for (int16_t i = 0; i < w; ++i) + p[i] = pixel; + } else if (bpp_ == 8) { + uint8_t r_ch = (color >> 16) & 0xFF; + uint8_t g_ch = (color >> 8) & 0xFF; + uint8_t b_ch = color & 0xFF; + uint8_t gray = static_cast( + (r_ch * 77 + g_ch * 150 + b_ch * 29) >> 8); + std::memset(row, gray, static_cast(w)); + } +} + +// --------------------------------------------------------------------------- +// Accelerated vertical line +// --------------------------------------------------------------------------- +void LinuxFBDisplay::drawVLineImpl(int16_t x, int16_t y, + int16_t h, Color color) { + if (!buf_ || h <= 0) return; + + uint32_t bytes_pp = bpp_ / 8; + uint8_t* p = buf_ + static_cast(y) * line_len_ + + static_cast(x) * bytes_pp; + + // Pre-encode the pixel once. + uint8_t pixel_buf[4]; + writePixel(pixel_buf, color); + + for (int16_t i = 0; i < h; ++i) { + std::memcpy(p, pixel_buf, bytes_pp); + p += line_len_; + } +} + +// --------------------------------------------------------------------------- +// Accelerated filled rectangle +// --------------------------------------------------------------------------- +void LinuxFBDisplay::fillRectImpl(int16_t x, int16_t y, + int16_t w, int16_t h, Color color) { + if (!buf_ || w <= 0 || h <= 0) return; + + // For 32-bpp with color == black or white, we can use memset for the + // entire rectangle. Otherwise, fill the first row with drawHLineImpl + // and memcpy it to subsequent rows. + + uint32_t bytes_pp = bpp_ / 8; + uint32_t row_bytes = static_cast(w) * bytes_pp; + uint8_t* first_row = buf_ + static_cast(y) * line_len_ + + static_cast(x) * bytes_pp; + + // Optimized path: if all bytes of the pixel are the same value, + // the entire rect can be filled with memset. + bool can_memset = false; + uint8_t fill_byte = 0; + + if (bpp_ == 32) { + // 0x00000000 → all bytes 0x00. 0x00FFFFFF → not uniform. + // Only pure black (all 0x00) qualifies. + if (color == 0x00000000) { can_memset = true; fill_byte = 0x00; } + } else if (bpp_ == 8) { + uint8_t r_ch = (color >> 16) & 0xFF; + uint8_t g_ch = (color >> 8) & 0xFF; + uint8_t b_ch = color & 0xFF; + fill_byte = static_cast( + (r_ch * 77 + g_ch * 150 + b_ch * 29) >> 8); + can_memset = true; + } + + if (can_memset) { + uint8_t* row = first_row; + for (int16_t r = 0; r < h; ++r) { + std::memset(row, fill_byte, row_bytes); + row += line_len_; + } + return; + } + + // General path: fill first row with pixel values, then memcpy. + drawHLineImpl(x, y, w, color); + + uint8_t* src = first_row; + uint8_t* dst = first_row + line_len_; + for (int16_t r = 1; r < h; ++r) { + std::memcpy(dst, src, row_bytes); + dst += line_len_; + } +} + +} // namespace gfx diff --git a/linux_fb_display.h b/linux_fb_display.h new file mode 100644 index 0000000..c746e06 --- /dev/null +++ b/linux_fb_display.h @@ -0,0 +1,58 @@ +// linux_fb_display.h — GfxCanvas subclass for Linux framebuffer devices. +// +// Supports XRGB8888 (32 bpp), RGB565 (16 bpp), and 8-bit grayscale. +// Uses mmap for zero-copy access to the kernel framebuffer. + +#ifndef LINUX_FB_DISPLAY_H +#define LINUX_FB_DISPLAY_H + +#include "gfx_canvas.h" + +#include +#include + +namespace gfx { + +class LinuxFBDisplay : public GfxCanvas { +public: + // Construct with the physical panel dimensions. + LinuxFBDisplay(int16_t width, int16_t height); + ~LinuxFBDisplay() override; + + // Open the framebuffer device. Returns true on success. + [[nodiscard]] bool open(const char* device); + + // Release the mmap and close the file descriptor. + void close(); + + // Is the framebuffer currently open and mapped? + bool isOpen() const { return buf_ != nullptr; } + +protected: + // Mandatory HAL: single pixel write. + void drawPixelImpl(int16_t x, int16_t y, Color color) override; + + // Accelerated HAL overrides. + void drawHLineImpl(int16_t x, int16_t y, int16_t w, Color color) override; + void drawVLineImpl(int16_t x, int16_t y, int16_t h, Color color) override; + void fillRectImpl(int16_t x, int16_t y, + int16_t w, int16_t h, Color color) override; + +private: + int fd_ = -1; + uint8_t* buf_ = nullptr; + size_t map_size_ = 0; + uint32_t bpp_ = 0; + uint32_t line_len_ = 0; + + fb_var_screeninfo vinfo_{}; + fb_fix_screeninfo finfo_{}; + + // Convert XRGB8888 color to the native pixel value for this fb. + // Returns the value and its byte width. + void writePixel(uint8_t* dst, Color color) const; +}; + +} // namespace gfx + +#endif // LINUX_FB_DISPLAY_H diff --git a/main.cpp b/main.cpp index 03ae8f3..d263d57 100644 --- a/main.cpp +++ b/main.cpp @@ -1,9 +1,134 @@ -#include +// main.cpp — Info display for SSD1306 128x64 using gfx::LinuxFBDisplay. +// +// Build: see CMakeLists.txt +// Run: ./gfx_info_display # defaults to /dev/fb1 +// ./gfx_info_display /dev/fb0 # override -int main(int argc, char** argv) -{ - (void)argc; - (void)argv; - std::cout << "Hello RPI" << std::endl; - return 0; +#include "gfx_canvas.h" +#include "gfx_font_5x7.h" +#include "linux_fb_display.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- +constexpr int16_t SCREEN_W = 128; +constexpr int16_t SCREEN_H = 64; + +constexpr const char* ETH_IFACE = "eth0"; +constexpr const char* WLAN_IFACE = "wlan0"; + +// --------------------------------------------------------------------------- +// Network helper +// --------------------------------------------------------------------------- +static std::string get_ip(const char* iface_name) { + struct ifaddrs* ifaddr = nullptr; + if (getifaddrs(&ifaddr) == -1) return "err"; + + std::string result = "down"; + for (auto* ifa = ifaddr; ifa; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) continue; + if (ifa->ifa_addr->sa_family != AF_INET) continue; + if (std::strcmp(ifa->ifa_name, iface_name) != 0) continue; + + char buf[INET_ADDRSTRLEN]; + auto* sa = reinterpret_cast(ifa->ifa_addr); + inet_ntop(AF_INET, &sa->sin_addr, buf, sizeof(buf)); + result = buf; + break; + } + + freeifaddrs(ifaddr); + return result; +} + +// --------------------------------------------------------------------------- +// Text centering helper +// --------------------------------------------------------------------------- +static void draw_centered(gfx::GfxCanvas& disp, int16_t y, const char* text) { + int16_t bx, by; + uint16_t bw, bh; + disp.getTextBounds(text, 0, 0, bx, by, bw, bh); + disp.drawString((SCREEN_W - static_cast(bw)) / 2, y, text, + gfx::colors::white); +} + +// --------------------------------------------------------------------------- +// Signal handling +// --------------------------------------------------------------------------- +static volatile sig_atomic_t g_running = 1; + +static void signal_handler(int) { g_running = 0; } + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- +int main(int argc, char* argv[]) { + const char* fb_device = "/dev/fb0"; + if (argc > 1) fb_device = argv[1]; + + gfx::LinuxFBDisplay display(SCREEN_W, SCREEN_H); + if (!display.open(fb_device)) return EXIT_FAILURE; + + display.setFont(gfx::font_5x7); + display.setTextColor(gfx::colors::white); + display.setTextWrap(false); + + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + char line_buf[64]; + + while (g_running) { + display.fillScreen(gfx::colors::black); + + // --- Network info --- + std::string eth_ip = get_ip(ETH_IFACE); + std::string wlan_ip = get_ip(WLAN_IFACE); + + std::snprintf(line_buf, sizeof(line_buf), "ETH: %s", eth_ip.c_str()); + display.setCursor(0, 1); + display.print(line_buf); + + std::snprintf(line_buf, sizeof(line_buf), "WLAN: %s", wlan_ip.c_str()); + display.setCursor(0, 11); + display.print(line_buf); + + // --- Separator --- + display.drawHLine(0, 23, SCREEN_W, gfx::colors::white); + + // --- Date and time --- + std::time_t now = std::time(nullptr); + std::tm* tm = std::localtime(&now); + + std::snprintf(line_buf, sizeof(line_buf), "%04d-%02d-%02d", + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); + draw_centered(display, 29, line_buf); + + std::snprintf(line_buf, sizeof(line_buf), "%02d:%02d:%02d", + tm->tm_hour, tm->tm_min, tm->tm_sec); + draw_centered(display, 41, line_buf); + + // --- Sleep aligned to next whole second --- + struct timespec ts{}; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += 1; + ts.tv_nsec = 0; + clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &ts, nullptr); + } + + display.fillScreen(gfx::colors::black); + std::printf("Exiting.\n"); + return EXIT_SUCCESS; }