#include "os_gfx.h" #include "os_font_5x7.h" #include "os_font_3x5.h" #include #include #include #include "esp_timer.h" static char s_toast_msg[32] = {0}; static uint64_t s_toast_expire_us = 0; esp_err_t sys_gfx_init(void) { return oled_driver_init(OLED_PIN_SDA, OLED_PIN_SCL, OLED_COLUMN_OFFSET); } void sys_gfx_clear(void) { oled_driver_clear(); } void sys_gfx_clear_viewport(void) { for (int16_t y = VIEWPORT_Y_TOP; y < OLED_HEIGHT; y++) { for (int16_t x = 0; x < OLED_WIDTH; x++) { oled_driver_draw_pixel(x, y, COLOR_BLACK); } } } esp_err_t sys_gfx_flush(void) { return oled_driver_flush(); } void sys_gfx_draw_pixel(int16_t x, int16_t y, uint8_t color) { oled_driver_draw_pixel(x, y, color); } void sys_gfx_draw_line(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color) { int16_t dx = abs(x1 - x0); int16_t dy = abs(y1 - y0); int16_t sx = (x0 < x1) ? 1 : -1; int16_t sy = (y0 < y1) ? 1 : -1; int16_t err = dx - dy; while (1) { oled_driver_draw_pixel(x0, y0, color); if (x0 == x1 && y0 == y1) break; int16_t e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } void sys_gfx_draw_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) { if (w <= 0 || h <= 0) return; sys_gfx_draw_line(x, y, x + w - 1, y, color); sys_gfx_draw_line(x, y + h - 1, x + w - 1, y + h - 1, color); sys_gfx_draw_line(x, y, x, y + h - 1, color); sys_gfx_draw_line(x + w - 1, y, x + w - 1, y + h - 1, color); } void sys_gfx_fill_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) { for (int16_t i = x; i < x + w; i++) { for (int16_t j = y; j < y + h; j++) { oled_driver_draw_pixel(i, j, color); } } } void sys_gfx_draw_char_5x7(int16_t x, int16_t y, char c, uint8_t color) { if (c < 32 || c > 127) c = '?'; const uint8_t *char_data = font_5x7[c - 32]; for (uint8_t col = 0; col < 5; col++) { uint8_t line = char_data[col]; for (uint8_t row = 0; row < 7; row++) { if (line & (1 << row)) { oled_driver_draw_pixel(x + col, y + row, color); } } } } void sys_gfx_draw_str_5x7(int16_t x, int16_t y, const char *str, uint8_t color) { if (!str) return; int16_t cur_x = x; int16_t cur_y = y; while (*str) { if (*str == '\n') { cur_x = x; cur_y += 8; } else { sys_gfx_draw_char_5x7(cur_x, cur_y, *str, color); cur_x += 6; } str++; } } void sys_gfx_draw_char_3x5(int16_t x, int16_t y, char c, uint8_t color) { if (c < 32 || c > 127) c = '?'; const uint8_t *char_data = font_3x5[c - 32]; for (uint8_t col = 0; col < 3; col++) { uint8_t line = char_data[col]; for (uint8_t row = 0; row < 5; row++) { if (line & (1 << row)) { oled_driver_draw_pixel(x + col, y + row, color); } } } } void sys_gfx_draw_str_3x5(int16_t x, int16_t y, const char *str, uint8_t color) { if (!str) return; int16_t cur_x = x; int16_t cur_y = y; while (*str) { if (*str == '\n') { cur_x = x; cur_y += 6; } else { sys_gfx_draw_char_3x5(cur_x, cur_y, *str, color); cur_x += 4; } str++; } } void sys_gfx_draw_bitmap(int16_t x, int16_t y, int16_t w, int16_t h, const uint8_t *bitmap, uint8_t color) { if (!bitmap) return; int16_t byte_width = (w + 7) / 8; for (int16_t j = 0; j < h; j++) { for (int16_t i = 0; i < w; i++) { if (bitmap[j * byte_width + i / 8] & (128 >> (i & 7))) { oled_driver_draw_pixel(x + i, y + j, color); } } } } /* UI Framework: Micro Status Bar (Top 8 pixels) */ void sys_gfx_draw_status_bar(const sys_status_t *status) { if (!status || !status->status_bar_visible) return; sys_gfx_fill_rect(0, 0, OLED_WIDTH, 7, COLOR_BLACK); sys_gfx_draw_line(0, 7, OLED_WIDTH - 1, 7, COLOR_WHITE); if (status->active_app_name) { char short_name[7]; snprintf(short_name, sizeof(short_name), "%.5s", status->active_app_name); sys_gfx_draw_str_3x5(1, 1, short_name, COLOR_WHITE); } if (status->wifi_state == WIFI_STATE_CONNECTED) { sys_gfx_draw_line(43, 4, 43, 5, COLOR_WHITE); sys_gfx_draw_line(45, 3, 45, 5, COLOR_WHITE); sys_gfx_draw_line(47, 1, 47, 5, COLOR_WHITE); } else if (status->wifi_state == WIFI_STATE_CONNECTING) { uint32_t t = (uint32_t)(esp_timer_get_time() / 500000ULL); if (t % 2) sys_gfx_draw_pixel(45, 3, COLOR_WHITE); } if (status->ble_state != BLE_STATE_OFF) { sys_gfx_draw_pixel(52, 1, COLOR_WHITE); sys_gfx_draw_pixel(52, 3, COLOR_WHITE); sys_gfx_draw_pixel(52, 5, COLOR_WHITE); } sys_gfx_draw_rect(58, 1, 11, 5, COLOR_WHITE); sys_gfx_draw_pixel(69, 2, COLOR_WHITE); sys_gfx_draw_pixel(69, 3, COLOR_WHITE); if (status->is_charging) { sys_gfx_draw_pixel(63, 2, COLOR_WHITE); sys_gfx_draw_pixel(62, 3, COLOR_WHITE); sys_gfx_draw_pixel(64, 3, COLOR_WHITE); sys_gfx_draw_pixel(63, 4, COLOR_WHITE); } else { uint8_t fill_w = (status->battery_pct * 7) / 100; if (fill_w > 7) fill_w = 7; sys_gfx_fill_rect(60, 2, fill_w, 3, COLOR_WHITE); } } /* UI Framework: Action Bar (rows 32-39) */ void sys_gfx_draw_action_bar(const char *left_hint, const char *right_hint) { sys_gfx_draw_line(0, 32, OLED_WIDTH - 1, 32, COLOR_WHITE); if (left_hint) { sys_gfx_draw_str_3x5(1, 34, left_hint, COLOR_WHITE); } if (right_hint) { int16_t str_len = (int16_t)strlen(right_hint); /* 3x5 font: 3px wide + 1px gap per char, no trailing gap on last char */ int16_t str_w = (str_len > 0) ? ((str_len - 1) * 4 + 3) : 0; int16_t rx = OLED_WIDTH - 1 - str_w; sys_gfx_draw_str_3x5(rx, 34, right_hint, COLOR_WHITE); } } /* UI Framework: Scrollable List Menu (Always centers highlighted entry) */ void sys_gfx_draw_menu(int16_t y_start, const char *items[], size_t count, size_t selected_idx) { if (!items || count == 0) return; int16_t y_center = y_start + 9; int16_t y_top = y_start; int16_t y_bot = y_start + 18; if (count == 1) { sys_gfx_fill_rect(0, y_center - 1, OLED_WIDTH - 4, 9, COLOR_WHITE); sys_gfx_draw_char_3x5(2, y_center + 1, '>', COLOR_BLACK); sys_gfx_draw_str_5x7(8, y_center, items[0], COLOR_BLACK); return; } // Top item: previous item in list (cyclically) size_t prev_idx = (selected_idx + count - 1) % count; sys_gfx_draw_str_5x7(8, y_top, items[prev_idx], COLOR_WHITE); // Center item: currently highlighted entry sys_gfx_fill_rect(0, y_center - 1, OLED_WIDTH - 4, 9, COLOR_WHITE); sys_gfx_draw_char_3x5(2, y_center + 1, '>', COLOR_BLACK); sys_gfx_draw_str_5x7(8, y_center, items[selected_idx], COLOR_BLACK); // Bottom item: next item in list (cyclically) size_t next_idx = (selected_idx + 1) % count; sys_gfx_draw_str_5x7(8, y_bot, items[next_idx], COLOR_WHITE); // Right-hand vertical scroll indicator if (count > 1) { sys_gfx_draw_line(OLED_WIDTH - 1, y_start, OLED_WIDTH - 1, y_start + 26, COLOR_WHITE); int16_t thumb_h = 27 / (int16_t)count; if (thumb_h < 3) thumb_h = 3; int16_t thumb_y = y_start + (int16_t)((selected_idx * (27 - thumb_h)) / (count - 1)); sys_gfx_fill_rect(OLED_WIDTH - 2, thumb_y, 2, thumb_h, COLOR_WHITE); } } /* UI Framework: Progress Bar */ void sys_gfx_draw_progress_bar(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t pct) { if (pct > 100) pct = 100; sys_gfx_draw_rect(x, y, w, h, COLOR_WHITE); int16_t inner_w = ((w - 2) * pct) / 100; if (inner_w > 0 && h > 2) { sys_gfx_fill_rect(x + 1, y + 1, inner_w, h - 2, COLOR_WHITE); } } /* UI Framework: Sparkline */ void sys_gfx_draw_sparkline(int16_t x, int16_t y, int16_t w, int16_t h, const uint8_t *data, size_t count, uint8_t max_val) { if (!data || count < 2 || max_val == 0) return; sys_gfx_draw_rect(x, y, w, h, COLOR_WHITE); size_t points = (count < (size_t)(w - 2)) ? count : (size_t)(w - 2); int16_t prev_px = x + 1; int16_t prev_py = y + h - 2 - ((data[0] * (h - 3)) / max_val); for (size_t i = 1; i < points; i++) { int16_t px = x + 1 + i; int16_t val = (data[i] > max_val) ? max_val : data[i]; int16_t py = y + h - 2 - ((val * (h - 3)) / max_val); sys_gfx_draw_line(prev_px, prev_py, px, py, COLOR_WHITE); prev_px = px; prev_py = py; } } /* Toast Notifications */ void sys_gfx_show_toast(const char *msg, uint32_t duration_ms) { if (!msg) return; snprintf(s_toast_msg, sizeof(s_toast_msg), "%s", msg); s_toast_expire_us = esp_timer_get_time() + ((uint64_t)duration_ms * 1000ULL); } void sys_gfx_render_toast_if_active(void) { if (s_toast_expire_us == 0) return; uint64_t now = esp_timer_get_time(); if (now > s_toast_expire_us) { s_toast_expire_us = 0; return; } int16_t box_h = 13; int16_t box_y = (OLED_HEIGHT - box_h) / 2; sys_gfx_fill_rect(2, box_y, OLED_WIDTH - 4, box_h, COLOR_BLACK); sys_gfx_draw_rect(2, box_y, OLED_WIDTH - 4, box_h, COLOR_WHITE); sys_gfx_draw_str_5x7(6, box_y + 3, s_toast_msg, COLOR_WHITE); }