Initial Demo OS

This commit is contained in:
dark98
2026-09-03 00:46:04 +01:00
commit 1491821e96
45 changed files with 2908 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
idf_component_register(
SRCS
"oled_driver.c"
"sys_gfx.c"
INCLUDE_DIRS
"include"
REQUIRES
sys_core
driver
esp_driver_gpio
esp_driver_i2c
freertos
esp_timer
log
)
+104
View File
@@ -0,0 +1,104 @@
#ifndef OS_FONT_3X5_H
#define OS_FONT_3X5_H
#include <stdint.h>
static const uint8_t font_3x5[][3] = {
{0x00, 0x00, 0x00}, // ' ' (32)
{0x00, 0x17, 0x00}, // '!'
{0x03, 0x00, 0x03}, // '"'
{0x1F, 0x0A, 0x1F}, // '#'
{0x12, 0x1F, 0x09}, // '$'
{0x19, 0x04, 0x13}, // '%'
{0x1A, 0x15, 0x0A}, // '&'
{0x00, 0x03, 0x00}, // '''
{0x00, 0x0E, 0x11}, // '('
{0x11, 0x0E, 0x00}, // ')'
{0x0A, 0x04, 0x0A}, // '*'
{0x04, 0x0E, 0x04}, // '+'
{0x10, 0x08, 0x00}, // ','
{0x04, 0x04, 0x04}, // '-'
{0x10, 0x00, 0x00}, // '.'
{0x10, 0x08, 0x06}, // '/'
{0x1F, 0x11, 0x1F}, // '0'
{0x12, 0x1F, 0x10}, // '1'
{0x1D, 0x15, 0x17}, // '2'
{0x15, 0x15, 0x1F}, // '3'
{0x07, 0x04, 0x1F}, // '4'
{0x17, 0x15, 0x1D}, // '5'
{0x1F, 0x15, 0x1D}, // '6'
{0x01, 0x01, 0x1F}, // '7'
{0x1F, 0x15, 0x1F}, // '8'
{0x17, 0x15, 0x1F}, // '9'
{0x00, 0x0A, 0x00}, // ':'
{0x10, 0x0A, 0x00}, // ';'
{0x04, 0x0A, 0x11}, // '<'
{0x0A, 0x0A, 0x0A}, // '='
{0x11, 0x0A, 0x04}, // '>'
{0x01, 0x15, 0x02}, // '?'
{0x0E, 0x15, 0x16}, // '@'
{0x1E, 0x05, 0x1E}, // 'A'
{0x1F, 0x15, 0x0A}, // 'B'
{0x0E, 0x11, 0x11}, // 'C'
{0x1F, 0x11, 0x0E}, // 'D'
{0x1F, 0x15, 0x11}, // 'E'
{0x1F, 0x05, 0x01}, // 'F'
{0x0E, 0x11, 0x1D}, // 'G'
{0x1F, 0x04, 0x1F}, // 'H'
{0x11, 0x1F, 0x11}, // 'I'
{0x08, 0x10, 0x0F}, // 'J'
{0x1F, 0x04, 0x1B}, // 'K'
{0x1F, 0x10, 0x10}, // 'L'
{0x1F, 0x02, 0x1F}, // 'M'
{0x1F, 0x02, 0x1F}, // 'N'
{0x0E, 0x11, 0x0E}, // 'O'
{0x1F, 0x05, 0x02}, // 'P'
{0x0E, 0x11, 0x1E}, // 'Q'
{0x1F, 0x05, 0x1A}, // 'R'
{0x12, 0x15, 0x09}, // 'S'
{0x01, 0x1F, 0x01}, // 'T'
{0x0F, 0x10, 0x0F}, // 'U'
{0x07, 0x18, 0x07}, // 'V'
{0x1F, 0x08, 0x1F}, // 'W'
{0x1B, 0x04, 0x1B}, // 'X'
{0x03, 0x1C, 0x03}, // 'Y'
{0x19, 0x15, 0x13}, // 'Z'
{0x00, 0x1F, 0x11}, // '['
{0x03, 0x04, 0x18}, // '\'
{0x11, 0x1F, 0x00}, // ']'
{0x02, 0x01, 0x02}, // '^'
{0x10, 0x10, 0x10}, // '_'
{0x00, 0x01, 0x02}, // '`'
{0x0C, 0x14, 0x1C}, // 'a'
{0x1F, 0x14, 0x08}, // 'b'
{0x0C, 0x14, 0x14}, // 'c'
{0x08, 0x14, 0x1F}, // 'd'
{0x0C, 0x16, 0x14}, // 'e'
{0x04, 0x1F, 0x05}, // 'f'
{0x18, 0x14, 0x1E}, // 'g'
{0x1F, 0x04, 0x18}, // 'h'
{0x00, 0x17, 0x00}, // 'i'
{0x10, 0x17, 0x00}, // 'j'
{0x1F, 0x08, 0x14}, // 'k'
{0x00, 0x1F, 0x00}, // 'l'
{0x1C, 0x08, 0x1C}, // 'm'
{0x1C, 0x04, 0x18}, // 'n'
{0x0C, 0x12, 0x0C}, // 'o'
{0x1C, 0x0A, 0x04}, // 'p'
{0x04, 0x0A, 0x1C}, // 'q'
{0x1C, 0x04, 0x08}, // 'r'
{0x12, 0x16, 0x0C}, // 's'
{0x04, 0x1E, 0x04}, // 't'
{0x0E, 0x10, 0x0E}, // 'u'
{0x06, 0x18, 0x06}, // 'v'
{0x1E, 0x08, 0x1E}, // 'w'
{0x12, 0x0C, 0x12}, // 'x'
{0x16, 0x10, 0x0E}, // 'y'
{0x12, 0x16, 0x1A}, // 'z'
{0x04, 0x1B, 0x11}, // '{'
{0x00, 0x1F, 0x00}, // '|'
{0x11, 0x1B, 0x04}, // '}'
{0x08, 0x04, 0x08} // '~'
};
#endif // OS_FONT_3X5_H
+104
View File
@@ -0,0 +1,104 @@
#ifndef OS_FONT_5X7_H
#define OS_FONT_5X7_H
#include <stdint.h>
static const uint8_t font_5x7[][5] = {
{0x00, 0x00, 0x00, 0x00, 0x00}, // ' ' (32)
{0x00, 0x00, 0x5F, 0x00, 0x00}, // '!'
{0x00, 0x07, 0x00, 0x07, 0x00}, // '"'
{0x14, 0x7F, 0x14, 0x7F, 0x14}, // '#'
{0x24, 0x2A, 0x7F, 0x2A, 0x12}, // '$'
{0x23, 0x13, 0x08, 0x64, 0x62}, // '%'
{0x36, 0x49, 0x55, 0x22, 0x50}, // '&'
{0x00, 0x05, 0x03, 0x00, 0x00}, // '''
{0x00, 0x1C, 0x22, 0x41, 0x00}, // '('
{0x00, 0x41, 0x22, 0x1C, 0x00}, // ')'
{0x14, 0x08, 0x3E, 0x08, 0x14}, // '*'
{0x08, 0x08, 0x3E, 0x08, 0x08}, // '+'
{0x00, 0x50, 0x30, 0x00, 0x00}, // ','
{0x08, 0x08, 0x08, 0x08, 0x08}, // '-'
{0x00, 0x60, 0x60, 0x00, 0x00}, // '.'
{0x20, 0x10, 0x08, 0x04, 0x02}, // '/'
{0x3E, 0x51, 0x49, 0x45, 0x3E}, // '0'
{0x00, 0x42, 0x7F, 0x40, 0x00}, // '1'
{0x42, 0x61, 0x51, 0x49, 0x46}, // '2'
{0x21, 0x41, 0x45, 0x4B, 0x31}, // '3'
{0x18, 0x14, 0x12, 0x7F, 0x10}, // '4'
{0x27, 0x45, 0x45, 0x45, 0x39}, // '5'
{0x3C, 0x4A, 0x49, 0x49, 0x30}, // '6'
{0x01, 0x71, 0x09, 0x05, 0x03}, // '7'
{0x36, 0x49, 0x49, 0x49, 0x36}, // '8'
{0x06, 0x49, 0x49, 0x29, 0x1E}, // '9'
{0x00, 0x36, 0x36, 0x00, 0x00}, // ':'
{0x00, 0x56, 0x36, 0x00, 0x00}, // ';'
{0x08, 0x14, 0x22, 0x41, 0x00}, // '<'
{0x14, 0x14, 0x14, 0x14, 0x14}, // '='
{0x00, 0x41, 0x22, 0x14, 0x08}, // '>'
{0x02, 0x01, 0x51, 0x09, 0x06}, // '?'
{0x32, 0x49, 0x79, 0x41, 0x3E}, // '@'
{0x7E, 0x11, 0x11, 0x11, 0x7E}, // 'A'
{0x7F, 0x49, 0x49, 0x49, 0x36}, // 'B'
{0x3E, 0x41, 0x41, 0x41, 0x22}, // 'C'
{0x7F, 0x41, 0x41, 0x22, 0x1C}, // 'D'
{0x7F, 0x49, 0x49, 0x49, 0x41}, // 'E'
{0x7F, 0x09, 0x09, 0x09, 0x01}, // 'F'
{0x3E, 0x41, 0x49, 0x49, 0x7A}, // 'G'
{0x7F, 0x08, 0x08, 0x08, 0x7F}, // 'H'
{0x00, 0x41, 0x7F, 0x41, 0x00}, // 'I'
{0x20, 0x40, 0x41, 0x3F, 0x01}, // 'J'
{0x7F, 0x08, 0x14, 0x22, 0x41}, // 'K'
{0x7F, 0x40, 0x40, 0x40, 0x40}, // 'L'
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, // 'M'
{0x7F, 0x04, 0x08, 0x10, 0x7F}, // 'N'
{0x3E, 0x41, 0x41, 0x41, 0x3E}, // 'O'
{0x7F, 0x09, 0x09, 0x09, 0x06}, // 'P'
{0x3E, 0x41, 0x51, 0x21, 0x5E}, // 'Q'
{0x7F, 0x09, 0x19, 0x29, 0x46}, // 'R'
{0x46, 0x49, 0x49, 0x49, 0x31}, // 'S'
{0x01, 0x01, 0x7F, 0x01, 0x01}, // 'T'
{0x3F, 0x40, 0x40, 0x40, 0x3F}, // 'U'
{0x1F, 0x20, 0x40, 0x20, 0x1F}, // 'V'
{0x3F, 0x40, 0x38, 0x40, 0x3F}, // 'W'
{0x63, 0x14, 0x08, 0x14, 0x63}, // 'X'
{0x07, 0x08, 0x70, 0x08, 0x07}, // 'Y'
{0x61, 0x51, 0x49, 0x45, 0x43}, // 'Z'
{0x00, 0x7F, 0x41, 0x41, 0x00}, // '['
{0x02, 0x04, 0x08, 0x10, 0x20}, // '\'
{0x00, 0x41, 0x41, 0x7F, 0x00}, // ']'
{0x04, 0x02, 0x01, 0x02, 0x04}, // '^'
{0x40, 0x40, 0x40, 0x40, 0x40}, // '_'
{0x00, 0x01, 0x02, 0x04, 0x00}, // '`'
{0x20, 0x54, 0x54, 0x54, 0x78}, // 'a'
{0x7F, 0x48, 0x44, 0x44, 0x38}, // 'b'
{0x38, 0x44, 0x44, 0x44, 0x20}, // 'c'
{0x38, 0x44, 0x44, 0x48, 0x7F}, // 'd'
{0x38, 0x54, 0x54, 0x54, 0x18}, // 'e'
{0x08, 0x7E, 0x09, 0x01, 0x02}, // 'f'
{0x0C, 0x52, 0x52, 0x52, 0x3E}, // 'g'
{0x7F, 0x08, 0x04, 0x04, 0x78}, // 'h'
{0x00, 0x44, 0x7D, 0x40, 0x00}, // 'i'
{0x20, 0x40, 0x44, 0x3D, 0x00}, // 'j'
{0x7F, 0x10, 0x28, 0x44, 0x00}, // 'k'
{0x00, 0x41, 0x7F, 0x40, 0x00}, // 'l'
{0x7C, 0x04, 0x18, 0x04, 0x78}, // 'm'
{0x7C, 0x08, 0x04, 0x04, 0x78}, // 'n'
{0x38, 0x44, 0x44, 0x44, 0x38}, // 'o'
{0x7C, 0x14, 0x14, 0x14, 0x08}, // 'p'
{0x08, 0x14, 0x14, 0x18, 0x7C}, // 'q'
{0x7C, 0x08, 0x04, 0x04, 0x08}, // 'r'
{0x48, 0x54, 0x54, 0x54, 0x20}, // 's'
{0x04, 0x3F, 0x44, 0x40, 0x20}, // 't'
{0x3C, 0x40, 0x40, 0x20, 0x7C}, // 'u'
{0x1C, 0x20, 0x40, 0x20, 0x1C}, // 'v'
{0x3C, 0x40, 0x30, 0x40, 0x3C}, // 'w'
{0x44, 0x28, 0x10, 0x28, 0x44}, // 'x'
{0x0C, 0x50, 0x50, 0x50, 0x3C}, // 'y'
{0x44, 0x64, 0x54, 0x4C, 0x44}, // 'z'
{0x00, 0x08, 0x36, 0x41, 0x00}, // '{'
{0x00, 0x00, 0x7F, 0x00, 0x00}, // '|'
{0x00, 0x41, 0x36, 0x08, 0x00}, // '}'
{0x08, 0x08, 0x2A, 0x1C, 0x08} // '~'
};
#endif // OS_FONT_5X7_H
+48
View File
@@ -0,0 +1,48 @@
#ifndef OS_GFX_H
#define OS_GFX_H
#include <stdint.h>
#include <stdbool.h>
#include "os_oled.h"
#include "os_status.h"
#ifdef __cplusplus
extern "C" {
#endif
#define STATUS_BAR_HEIGHT 8
#define ACTION_BAR_HEIGHT 7
#define VIEWPORT_Y_TOP 8
#define VIEWPORT_HEIGHT 32
#define VIEWPORT_FULL_HEIGHT 40
esp_err_t sys_gfx_init(void);
void sys_gfx_clear(void);
void sys_gfx_clear_viewport(void);
esp_err_t sys_gfx_flush(void);
void sys_gfx_draw_pixel(int16_t x, int16_t y, uint8_t color);
void sys_gfx_draw_line(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color);
void sys_gfx_draw_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color);
void sys_gfx_fill_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color);
void sys_gfx_draw_bitmap(int16_t x, int16_t y, int16_t w, int16_t h, const uint8_t *bitmap, uint8_t color);
void sys_gfx_draw_char_5x7(int16_t x, int16_t y, char c, uint8_t color);
void sys_gfx_draw_str_5x7(int16_t x, int16_t y, const char *str, uint8_t color);
void sys_gfx_draw_char_3x5(int16_t x, int16_t y, char c, uint8_t color);
void sys_gfx_draw_str_3x5(int16_t x, int16_t y, const char *str, uint8_t color);
void sys_gfx_draw_status_bar(const sys_status_t *status);
void sys_gfx_draw_action_bar(const char *left_hint, const char *right_hint);
void sys_gfx_draw_menu(int16_t y_start, const char *items[], size_t count, size_t selected_idx);
void sys_gfx_draw_progress_bar(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t pct);
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);
void sys_gfx_show_toast(const char *msg, uint32_t duration_ms);
void sys_gfx_render_toast_if_active(void);
#ifdef __cplusplus
}
#endif
#endif // OS_GFX_H
+46
View File
@@ -0,0 +1,46 @@
#ifndef OS_OLED_H
#define OS_OLED_H
#include <stdint.h>
#include <stdbool.h>
#include "driver/gpio.h"
#include "driver/i2c_master.h"
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
#define OLED_I2C_PORT 0 /* i2c_port_num_t */
#define OLED_I2C_ADDR 0x3C
#define OLED_PIN_SDA GPIO_NUM_5
#define OLED_PIN_SCL GPIO_NUM_6
#define OLED_I2C_FREQ_HZ 400000
#define OLED_WIDTH 72
#define OLED_HEIGHT 40
#define OLED_PAGES 5
#define OLED_BUFFER_SIZE (OLED_WIDTH * OLED_PAGES)
#ifndef OLED_COLUMN_OFFSET
#define OLED_COLUMN_OFFSET 28
#endif
#define COLOR_BLACK 0
#define COLOR_WHITE 1
#define COLOR_INVERT 2
esp_err_t oled_driver_init(gpio_num_t sda, gpio_num_t scl, uint8_t col_offset);
uint8_t *oled_driver_get_buffer(void);
void oled_driver_clear(void);
void oled_driver_fill(void);
esp_err_t oled_driver_flush(void);
esp_err_t oled_driver_set_contrast(uint8_t contrast);
esp_err_t oled_driver_set_invert(bool invert);
void oled_driver_draw_pixel(int16_t x, int16_t y, uint8_t color);
#ifdef __cplusplus
}
#endif
#endif // OS_OLED_H
+159
View File
@@ -0,0 +1,159 @@
#include "os_oled.h"
#include <string.h>
#include "esp_log.h"
#include "driver/i2c_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "OLED_DRV";
static uint8_t s_frame_buffer[OLED_BUFFER_SIZE];
static uint8_t s_column_offset = OLED_COLUMN_OFFSET;
static i2c_master_bus_handle_t s_i2c_bus = NULL;
static i2c_master_dev_handle_t s_i2c_dev = NULL;
/* Send a single command byte to the OLED */
static esp_err_t oled_send_cmd(uint8_t cmd)
{
uint8_t buf[2] = {0x00, cmd}; /* 0x00 = Co=0, D/C#=0 (command) */
return i2c_master_transmit(s_i2c_dev, buf, sizeof(buf), 50);
}
/* Send multiple command bytes in one command transaction */
static esp_err_t oled_send_cmds(const uint8_t *cmds, size_t len)
{
uint8_t buf[1 + 64];
if (len > sizeof(buf) - 1) return ESP_ERR_INVALID_SIZE;
buf[0] = 0x00; /* Co=0, D/C#=0: remaining bytes are commands */
memcpy(&buf[1], cmds, len);
return i2c_master_transmit(s_i2c_dev, buf, 1 + len, 100);
}
esp_err_t oled_driver_init(gpio_num_t sda, gpio_num_t scl, uint8_t col_offset)
{
s_column_offset = col_offset;
/* --- I2C bus --- */
i2c_master_bus_config_t bus_cfg = {
.i2c_port = OLED_I2C_PORT,
.sda_io_num = sda,
.scl_io_num = scl,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
esp_err_t err = i2c_new_master_bus(&bus_cfg, &s_i2c_bus);
if (err != ESP_OK) {
ESP_LOGE(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err));
return err;
}
/* --- OLED device on the bus --- */
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = OLED_I2C_ADDR,
.scl_speed_hz = OLED_I2C_FREQ_HZ,
};
err = i2c_master_bus_add_device(s_i2c_bus, &dev_cfg, &s_i2c_dev);
if (err != ESP_OK) {
ESP_LOGE(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err));
return err;
}
vTaskDelay(pdMS_TO_TICKS(50));
/* SH1106 / SSD1306 init sequence for 72x40 */
const uint8_t init_cmds[] = {
0xAE, /* Display OFF */
0xD5, 0x80, /* Clock divide / Osc freq */
0xA8, 0x27, /* Multiplex Ratio (40 - 1 = 39 = 0x27) */
0xD3, 0x00, /* Display Offset = 0 */
0x40, /* Start Line = 0 */
0x8D, 0x14, /* Charge Pump enable */
0x20, 0x02, /* Memory Addressing Mode: Page mode */
0xA1, /* Segment Re-map */
0xC8, /* COM Output Scan Direction */
0xDA, 0x12, /* COM Pins hardware config */
0x81, 0xAF, /* Contrast */
0xD9, 0xF1, /* Pre-charge Period */
0xDB, 0x20, /* VCOMH Deselect Level */
0xA4, /* Resume to RAM */
0xA6, /* Normal display */
0xAF /* Display ON */
};
err = oled_send_cmds(init_cmds, sizeof(init_cmds));
if (err != ESP_OK) {
ESP_LOGE(TAG, "OLED initialization failed: %s", esp_err_to_name(err));
return err;
}
oled_driver_clear();
oled_driver_flush();
ESP_LOGI(TAG, "OLED driver initialized (72x40, col_offset=%d)", s_column_offset);
return ESP_OK;
}
uint8_t *oled_driver_get_buffer(void)
{
return s_frame_buffer;
}
void oled_driver_clear(void)
{
memset(s_frame_buffer, 0x00, sizeof(s_frame_buffer));
}
void oled_driver_fill(void)
{
memset(s_frame_buffer, 0xFF, sizeof(s_frame_buffer));
}
esp_err_t oled_driver_flush(void)
{
for (uint8_t page = 0; page < OLED_PAGES; page++) {
/* Set page address + column start */
const uint8_t page_cmds[] = {
(uint8_t)(0xB0 | page),
(uint8_t)(0x00 | (s_column_offset & 0x0F)),
(uint8_t)(0x10 | ((s_column_offset >> 4) & 0x0F))
};
esp_err_t err = oled_send_cmds(page_cmds, sizeof(page_cmds));
if (err != ESP_OK) return err;
/* Send pixel data: [0x40, pixel0, pixel1, ...] */
uint8_t data_buf[1 + OLED_WIDTH];
data_buf[0] = 0x40; /* Co=0, D/C#=1 (data) */
memcpy(&data_buf[1], &s_frame_buffer[page * OLED_WIDTH], OLED_WIDTH);
err = i2c_master_transmit(s_i2c_dev, data_buf, sizeof(data_buf), 50);
if (err != ESP_OK) return err;
}
return ESP_OK;
}
esp_err_t oled_driver_set_contrast(uint8_t contrast)
{
const uint8_t cmds[] = {0x81, contrast};
return oled_send_cmds(cmds, sizeof(cmds));
}
esp_err_t oled_driver_set_invert(bool invert)
{
return oled_send_cmd(invert ? 0xA7 : 0xA6);
}
void oled_driver_draw_pixel(int16_t x, int16_t y, uint8_t color)
{
if (x < 0 || x >= OLED_WIDTH || y < 0 || y >= OLED_HEIGHT) return;
uint16_t index = (y / 8) * OLED_WIDTH + x;
uint8_t bit = y % 8;
switch (color) {
case COLOR_WHITE: s_frame_buffer[index] |= (1 << bit); break;
case COLOR_BLACK: s_frame_buffer[index] &= ~(1 << bit); break;
case COLOR_INVERT: s_frame_buffer[index] ^= (1 << bit); break;
default: break;
}
}
+305
View File
@@ -0,0 +1,305 @@
#include "os_gfx.h"
#include "os_font_5x7.h"
#include "os_font_3x5.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#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 */
void sys_gfx_draw_menu(int16_t y_start, const char *items[], size_t count, size_t selected_idx)
{
if (!items || count == 0) return;
size_t visible_count = 3;
size_t top_idx = 0;
if (selected_idx >= visible_count) {
top_idx = selected_idx - visible_count + 1;
}
for (size_t i = 0; i < visible_count && (top_idx + i) < count; i++) {
size_t item_idx = top_idx + i;
int16_t y = y_start + (i * 9);
if (item_idx == selected_idx) {
sys_gfx_fill_rect(0, y - 1, OLED_WIDTH - 3, 9, COLOR_WHITE);
sys_gfx_draw_char_3x5(2, y + 1, '>', COLOR_BLACK);
sys_gfx_draw_str_5x7(8, y, items[item_idx], COLOR_BLACK);
} else {
sys_gfx_draw_str_5x7(8, y, items[item_idx], COLOR_WHITE);
}
}
if (count > visible_count) {
sys_gfx_draw_line(OLED_WIDTH - 1, y_start, OLED_WIDTH - 1, y_start + 26, COLOR_WHITE);
int16_t thumb_h = 27 / count;
if (thumb_h < 3) thumb_h = 3;
int16_t thumb_y = y_start + (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);
}