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
+14
View File
@@ -0,0 +1,14 @@
idf_component_register(
SRCS
"sys_app.c"
INCLUDE_DIRS
"include"
REQUIRES
sys_core
sys_input
sys_gfx
freertos
esp_timer
log
)
+38
View File
@@ -0,0 +1,38 @@
#ifndef OS_APP_H
#define OS_APP_H
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#include "os_input.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
const char *id;
const char *name;
void (*init)(void);
void (*enter)(void);
void (*update)(uint32_t delta_ms);
void (*handle_input)(input_action_t action);
void (*exit)(void);
} sys_app_t;
esp_err_t sys_app_init(void);
esp_err_t sys_app_register(const sys_app_t *app);
esp_err_t sys_app_launch_by_id(const char *id);
esp_err_t sys_app_launch_by_index(size_t index);
void sys_app_launch_launcher(void);
const sys_app_t *sys_app_get_current(void);
void sys_app_update(uint32_t delta_ms);
void sys_app_dispatch_input(input_action_t action);
size_t sys_app_get_count(void);
const sys_app_t *sys_app_get_by_index(size_t index);
#ifdef __cplusplus
}
#endif
#endif // OS_APP_H
+157
View File
@@ -0,0 +1,157 @@
#include "os_app.h"
#include "os_gfx.h"
#include "os_status.h"
#include "os_event.h"
#include <string.h>
#include "esp_log.h"
static const char *TAG = "SYS_APP";
#define MAX_APPS 16
static const sys_app_t *s_registry[MAX_APPS];
static size_t s_app_count = 0;
static const sys_app_t *s_current_app = NULL;
static bool s_in_launcher = true;
static size_t s_launcher_selected = 0;
static void launcher_update(uint32_t delta_ms)
{
sys_status_t status;
sys_status_get(&status);
sys_gfx_draw_status_bar(&status);
sys_gfx_draw_str_3x5(2, 9, "--- APPS ---", COLOR_WHITE);
const char *names[MAX_APPS];
for (size_t i = 0; i < s_app_count; i++) {
names[i] = s_registry[i]->name;
}
sys_gfx_draw_menu(15, names, s_app_count, s_launcher_selected);
}
static void launcher_handle_input(input_action_t action)
{
if (s_app_count == 0) return;
if (action == INPUT_ACTION_CLICK) {
s_launcher_selected = (s_launcher_selected + 1) % s_app_count;
} else if (action == INPUT_ACTION_DOUBLE_CLICK) {
if (s_launcher_selected == 0) {
s_launcher_selected = s_app_count - 1;
} else {
s_launcher_selected--;
}
} else if (action == INPUT_ACTION_LONG_PRESS) {
sys_app_launch_by_index(s_launcher_selected);
}
}
esp_err_t sys_app_init(void)
{
s_app_count = 0;
s_current_app = NULL;
s_in_launcher = true;
s_launcher_selected = 0;
sys_status_set_active_app_name("Launcher");
ESP_LOGI(TAG, "App Manager initialized");
return ESP_OK;
}
esp_err_t sys_app_register(const sys_app_t *app)
{
if (!app || !app->id || !app->name) return ESP_ERR_INVALID_ARG;
if (s_app_count >= MAX_APPS) return ESP_ERR_NO_MEM;
s_registry[s_app_count++] = app;
if (app->init) {
app->init();
}
ESP_LOGI(TAG, "Registered app: [%s] '%s'", app->id, app->name);
return ESP_OK;
}
esp_err_t sys_app_launch_by_id(const char *id)
{
if (!id) return ESP_ERR_INVALID_ARG;
for (size_t i = 0; i < s_app_count; i++) {
if (strcmp(s_registry[i]->id, id) == 0) {
return sys_app_launch_by_index(i);
}
}
ESP_LOGW(TAG, "App not found: %s", id);
return ESP_ERR_NOT_FOUND;
}
esp_err_t sys_app_launch_by_index(size_t index)
{
if (index >= s_app_count) return ESP_ERR_INVALID_ARG;
if (s_current_app && s_current_app->exit) {
s_current_app->exit();
}
s_current_app = s_registry[index];
s_in_launcher = false;
sys_status_set_active_app_name(s_current_app->name);
if (s_current_app->enter) {
s_current_app->enter();
}
sys_event_post(SYS_EVENT_APP_SWITCH, (int32_t)index, (void *)s_current_app->id);
sys_gfx_show_toast(s_current_app->name, 800);
ESP_LOGI(TAG, "Switched to app: %s", s_current_app->name);
return ESP_OK;
}
void sys_app_launch_launcher(void)
{
if (s_current_app && s_current_app->exit) {
s_current_app->exit();
}
s_current_app = NULL;
s_in_launcher = true;
sys_status_set_active_app_name("Launcher");
sys_gfx_show_toast("Launcher", 600);
ESP_LOGI(TAG, "Returned to App Launcher");
}
const sys_app_t *sys_app_get_current(void)
{
return s_current_app;
}
void sys_app_update(uint32_t delta_ms)
{
if (s_in_launcher) {
launcher_update(delta_ms);
} else if (s_current_app && s_current_app->update) {
s_current_app->update(delta_ms);
}
}
void sys_app_dispatch_input(input_action_t action)
{
if (action == INPUT_ACTION_HOLD_HOME) {
sys_app_launch_launcher();
return;
}
if (s_in_launcher) {
launcher_handle_input(action);
} else if (s_current_app && s_current_app->handle_input) {
s_current_app->handle_input(action);
}
}
size_t sys_app_get_count(void)
{
return s_app_count;
}
const sys_app_t *sys_app_get_by_index(size_t index)
{
if (index >= s_app_count) return NULL;
return s_registry[index];
}
+16
View File
@@ -0,0 +1,16 @@
idf_component_register(
SRCS
"sys_cli.c"
INCLUDE_DIRS
"include"
REQUIRES
sys_core
sys_input
sys_app
sys_gfx
console
freertos
esp_system
log
)
+17
View File
@@ -0,0 +1,17 @@
#ifndef OS_CLI_H
#define OS_CLI_H
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
esp_err_t sys_cli_init(void);
#ifdef __cplusplus
}
#endif
#endif // OS_CLI_H
+189
View File
@@ -0,0 +1,189 @@
#include "os_cli.h"
#include "os_app.h"
#include "os_input.h"
#include "os_gfx.h"
#include "os_oled.h"
#include "os_status.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "esp_console.h"
#include "esp_system.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "SYS_CLI";
static int cmd_ps(int argc, char **argv)
{
printf("\n=== Running FreeRTOS Tasks ===\n");
char task_buf[512];
vTaskList(task_buf);
printf("TaskName\tStatus\tPrio\tStack\tNum\n");
printf("%s\n", task_buf);
return 0;
}
static int cmd_free(int argc, char **argv)
{
printf("\n=== Memory Allocation ===\n");
printf("Current Free Heap: %lu bytes\n", (unsigned long)esp_get_free_heap_size());
printf("Minimum Free Heap: %lu bytes\n", (unsigned long)esp_get_minimum_free_heap_size());
return 0;
}
static int cmd_app(int argc, char **argv)
{
if (argc < 2) {
printf("Usage: app <list | launch <id> | home>\n");
return 1;
}
if (strcmp(argv[1], "list") == 0) {
size_t count = sys_app_get_count();
printf("\n=== Registered Apps (%u) ===\n", (unsigned int)count);
for (size_t i = 0; i < count; i++) {
const sys_app_t *app = sys_app_get_by_index(i);
printf(" [%u] %-12s (ID: %s)\n", (unsigned int)i, app->name, app->id);
}
return 0;
} else if (strcmp(argv[1], "launch") == 0 && argc >= 3) {
esp_err_t err = sys_app_launch_by_id(argv[2]);
if (err == ESP_OK) {
printf("Launched app: %s\n", argv[2]);
} else {
printf("Failed to launch app: %s\n", argv[2]);
}
return 0;
} else if (strcmp(argv[1], "home") == 0) {
sys_app_launch_launcher();
printf("Switched to Home Launcher\n");
return 0;
}
printf("Unknown app sub-command\n");
return 1;
}
static int cmd_input(int argc, char **argv)
{
if (argc < 2) {
printf("Usage: input <click | double | long | home>\n");
return 1;
}
if (strcmp(argv[1], "click") == 0) {
sys_input_inject(INPUT_ACTION_CLICK);
} else if (strcmp(argv[1], "double") == 0) {
sys_input_inject(INPUT_ACTION_DOUBLE_CLICK);
} else if (strcmp(argv[1], "long") == 0) {
sys_input_inject(INPUT_ACTION_LONG_PRESS);
} else if (strcmp(argv[1], "home") == 0) {
sys_input_inject(INPUT_ACTION_HOLD_HOME);
} else {
printf("Invalid action\n");
return 1;
}
printf("Injected action: %s\n", argv[1]);
return 0;
}
static int cmd_reboot(int argc, char **argv)
{
printf("Restarting system...\n");
esp_restart();
return 0;
}
static int cmd_contrast(int argc, char **argv)
{
if (argc < 2) {
printf("Usage: contrast <0-255>\n");
return 1;
}
int val = atoi(argv[1]);
if (val < 0 || val > 255) {
printf("Value must be 0 to 255\n");
return 1;
}
oled_driver_set_contrast((uint8_t)val);
printf("Contrast set to %d\n", val);
return 0;
}
esp_err_t sys_cli_init(void)
{
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
repl_config.prompt = "esp32c3-os> ";
repl_config.max_cmdline_args = 8;
repl_config.max_cmdline_length = 256;
esp_console_dev_usb_serial_jtag_config_t hw_config =
ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT();
esp_console_repl_t *repl = NULL;
esp_err_t err = esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &repl);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to create USB Serial/JTAG REPL: %s", esp_err_to_name(err));
return err;
}
esp_console_register_help_command();
const esp_console_cmd_t ps_cmd = {
.command = "ps",
.help = "List active FreeRTOS tasks and memory",
.hint = NULL,
.func = &cmd_ps,
};
esp_console_cmd_register(&ps_cmd);
const esp_console_cmd_t free_cmd = {
.command = "free",
.help = "Display system heap status",
.hint = NULL,
.func = &cmd_free,
};
esp_console_cmd_register(&free_cmd);
const esp_console_cmd_t app_cmd = {
.command = "app",
.help = "App management (list, launch <id>, home)",
.hint = "<list|launch|home>",
.func = &cmd_app,
};
esp_console_cmd_register(&app_cmd);
const esp_console_cmd_t input_cmd = {
.command = "input",
.help = "Simulate button input (click, double, long, home)",
.hint = "<click|double|long|home>",
.func = &cmd_input,
};
esp_console_cmd_register(&input_cmd);
const esp_console_cmd_t contrast_cmd = {
.command = "contrast",
.help = "Set OLED contrast (0-255)",
.hint = "<value>",
.func = &cmd_contrast,
};
esp_console_cmd_register(&contrast_cmd);
const esp_console_cmd_t reboot_cmd = {
.command = "reboot",
.help = "Reboot the MCU",
.hint = NULL,
.func = &cmd_reboot,
};
esp_console_cmd_register(&reboot_cmd);
err = esp_console_start_repl(repl);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start USB Serial/JTAG REPL: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "CLI shell initialized");
return ESP_OK;
}
+12
View File
@@ -0,0 +1,12 @@
idf_component_register(
SRCS
"sys_event.c"
"sys_status.c"
INCLUDE_DIRS
"include"
REQUIRES
freertos
esp_timer
esp_system
log
)
+52
View File
@@ -0,0 +1,52 @@
#ifndef OS_EVENT_H
#define OS_EVENT_H
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* System Event IDs */
typedef enum {
SYS_EVENT_INPUT_SHORT_PRESS = 1, /* Single click (e.g. Next / Increment) */
SYS_EVENT_INPUT_DOUBLE_CLICK, /* Double click (e.g. Back / Cancel) */
SYS_EVENT_INPUT_LONG_PRESS, /* Hold 400ms (e.g. Select / Confirm) */
SYS_EVENT_INPUT_EXTRA_LONG_PRESS, /* Hold 1500ms (e.g. Global Home / Launcher) */
SYS_EVENT_APP_SWITCH, /* Switched to a new app */
SYS_EVENT_TICK_1S, /* 1 Hz system heartbeat */
SYS_EVENT_WIFI_STATUS_CHANGED, /* WiFi connection state changed */
SYS_EVENT_BATTERY_STATUS_CHANGED, /* Battery level / charging changed */
SYS_EVENT_CUSTOM
} sys_event_id_t;
typedef struct {
sys_event_id_t id;
int32_t data;
void *ptr_data;
} sys_event_t;
typedef void (*sys_event_cb_t)(const sys_event_t *event, void *user_arg);
/**
* @brief Initialize the global system event bus.
*/
esp_err_t sys_event_init(void);
/**
* @brief Post an event to the global event bus.
*/
esp_err_t sys_event_post(sys_event_id_t id, int32_t data, void *ptr_data);
/**
* @brief Subscribe to system events.
*/
esp_err_t sys_event_subscribe(sys_event_cb_t callback, void *user_arg);
#ifdef __cplusplus
}
#endif
#endif // OS_EVENT_H
+51
View File
@@ -0,0 +1,51 @@
#ifndef OS_STATUS_H
#define OS_STATUS_H
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
WIFI_STATE_OFF = 0,
WIFI_STATE_CONNECTING,
WIFI_STATE_CONNECTED,
WIFI_STATE_AP_MODE,
} wifi_state_t;
typedef enum {
BLE_STATE_OFF = 0,
BLE_STATE_ADVERTISING,
BLE_STATE_CONNECTED,
} ble_state_t;
typedef struct {
uint32_t uptime_sec;
uint32_t free_heap;
uint32_t min_free_heap;
uint8_t cpu_usage_pct;
wifi_state_t wifi_state;
int8_t wifi_rssi;
ble_state_t ble_state;
bool is_charging;
uint8_t battery_pct;
bool status_bar_visible;
const char *active_app_name;
} sys_status_t;
void sys_status_init(void);
void sys_status_get(sys_status_t *out_status);
void sys_status_update(void);
void sys_status_set_wifi(wifi_state_t state, int8_t rssi);
void sys_status_set_ble(ble_state_t state);
void sys_status_set_status_bar_visible(bool visible);
bool sys_status_is_status_bar_visible(void);
void sys_status_set_active_app_name(const char *name);
#ifdef __cplusplus
}
#endif
#endif // OS_STATUS_H
+84
View File
@@ -0,0 +1,84 @@
#include "os_event.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "SYS_EVENT";
#define MAX_SUBSCRIBERS 16
#define EVENT_QUEUE_LEN 32
typedef struct {
sys_event_cb_t callback;
void *user_arg;
} subscriber_entry_t;
static subscriber_entry_t s_subscribers[MAX_SUBSCRIBERS];
static size_t s_subscriber_count = 0;
static QueueHandle_t s_event_queue = NULL;
static void sys_event_dispatch_task(void *pvParameters)
{
sys_event_t event;
while (1) {
if (xQueueReceive(s_event_queue, &event, portMAX_DELAY) == pdTRUE) {
for (size_t i = 0; i < s_subscriber_count; i++) {
if (s_subscribers[i].callback) {
s_subscribers[i].callback(&event, s_subscribers[i].user_arg);
}
}
}
}
}
esp_err_t sys_event_init(void)
{
if (s_event_queue != NULL) {
return ESP_OK;
}
s_event_queue = xQueueCreate(EVENT_QUEUE_LEN, sizeof(sys_event_t));
if (!s_event_queue) {
ESP_LOGE(TAG, "Failed to create event queue");
return ESP_ERR_NO_MEM;
}
BaseType_t ret = xTaskCreate(sys_event_dispatch_task, "sys_event_task", 3072, NULL, 10, NULL);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create event dispatcher task");
return ESP_FAIL;
}
ESP_LOGI(TAG, "System Event Bus initialized");
return ESP_OK;
}
esp_err_t sys_event_post(sys_event_id_t id, int32_t data, void *ptr_data)
{
if (!s_event_queue) {
return ESP_ERR_INVALID_STATE;
}
sys_event_t event = {
.id = id,
.data = data,
.ptr_data = ptr_data
};
if (xQueueSend(s_event_queue, &event, 0) != pdTRUE) {
ESP_LOGW(TAG, "Event queue full, dropped event %d", (int)id);
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
esp_err_t sys_event_subscribe(sys_event_cb_t callback, void *user_arg)
{
if (!callback) return ESP_ERR_INVALID_ARG;
if (s_subscriber_count >= MAX_SUBSCRIBERS) {
ESP_LOGE(TAG, "Max subscribers reached");
return ESP_ERR_NO_MEM;
}
s_subscribers[s_subscriber_count].callback = callback;
s_subscribers[s_subscriber_count].user_arg = user_arg;
s_subscriber_count++;
return ESP_OK;
}
+87
View File
@@ -0,0 +1,87 @@
#include "os_status.h"
#include <string.h>
#include "esp_system.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static sys_status_t s_status = {
.uptime_sec = 0,
.free_heap = 0,
.min_free_heap = 0,
.cpu_usage_pct = 0,
.wifi_state = WIFI_STATE_OFF,
.wifi_rssi = 0,
.ble_state = BLE_STATE_OFF,
.is_charging = true,
.battery_pct = 100,
.status_bar_visible = true,
.active_app_name = "Clock"
};
static portMUX_TYPE s_status_mux = portMUX_INITIALIZER_UNLOCKED;
void sys_status_init(void)
{
portENTER_CRITICAL(&s_status_mux);
s_status.uptime_sec = 0;
s_status.free_heap = esp_get_free_heap_size();
s_status.min_free_heap = esp_get_minimum_free_heap_size();
s_status.status_bar_visible = true;
portEXIT_CRITICAL(&s_status_mux);
}
void sys_status_get(sys_status_t *out_status)
{
if (!out_status) return;
portENTER_CRITICAL(&s_status_mux);
memcpy(out_status, &s_status, sizeof(sys_status_t));
portEXIT_CRITICAL(&s_status_mux);
}
void sys_status_update(void)
{
portENTER_CRITICAL(&s_status_mux);
s_status.uptime_sec = (uint32_t)(esp_timer_get_time() / 1000000ULL);
s_status.free_heap = esp_get_free_heap_size();
s_status.min_free_heap = esp_get_minimum_free_heap_size();
portEXIT_CRITICAL(&s_status_mux);
}
void sys_status_set_wifi(wifi_state_t state, int8_t rssi)
{
portENTER_CRITICAL(&s_status_mux);
s_status.wifi_state = state;
s_status.wifi_rssi = rssi;
portEXIT_CRITICAL(&s_status_mux);
}
void sys_status_set_ble(ble_state_t state)
{
portENTER_CRITICAL(&s_status_mux);
s_status.ble_state = state;
portEXIT_CRITICAL(&s_status_mux);
}
void sys_status_set_status_bar_visible(bool visible)
{
portENTER_CRITICAL(&s_status_mux);
s_status.status_bar_visible = visible;
portEXIT_CRITICAL(&s_status_mux);
}
bool sys_status_is_status_bar_visible(void)
{
bool visible;
portENTER_CRITICAL(&s_status_mux);
visible = s_status.status_bar_visible;
portEXIT_CRITICAL(&s_status_mux);
return visible;
}
void sys_status_set_active_app_name(const char *name)
{
portENTER_CRITICAL(&s_status_mux);
s_status.active_app_name = name;
portEXIT_CRITICAL(&s_status_mux);
}
+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);
}
+13
View File
@@ -0,0 +1,13 @@
idf_component_register(
SRCS
"sys_input.c"
INCLUDE_DIRS
"include"
REQUIRES
sys_core
driver
esp_driver_gpio
freertos
esp_timer
log
)
+31
View File
@@ -0,0 +1,31 @@
#ifndef OS_INPUT_H
#define OS_INPUT_H
#include <stdint.h>
#include <stdbool.h>
#include "driver/gpio.h"
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
INPUT_ACTION_NONE = 0,
INPUT_ACTION_CLICK, /* Short press (Next / Step / Advance) */
INPUT_ACTION_DOUBLE_CLICK,/* Double click (Back / Cancel) */
INPUT_ACTION_LONG_PRESS, /* Hold > 400ms (Select / Confirm / Open) */
INPUT_ACTION_HOLD_HOME, /* Hold > 1500ms (Return to App Launcher) */
} input_action_t;
typedef void (*sys_input_handler_t)(input_action_t action, void *user_arg);
esp_err_t sys_input_init(gpio_num_t button_gpio);
esp_err_t sys_input_register_handler(sys_input_handler_t handler, void *user_arg);
void sys_input_inject(input_action_t action);
#ifdef __cplusplus
}
#endif
#endif // OS_INPUT_H
+147
View File
@@ -0,0 +1,147 @@
#include "os_input.h"
#include "os_event.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
static const char *TAG = "SYS_INPUT";
#define DEBOUNCE_TIME_MS 25
#define CLICK_TIME_MAX_MS 350
#define DOUBLE_CLICK_GAP_MS 280
#define LONG_PRESS_TIME_MS 450
#define HOME_PRESS_TIME_MS 1500
typedef struct {
sys_input_handler_t handler;
void *user_arg;
} input_listener_t;
#define MAX_INPUT_LISTENERS 8
static input_listener_t s_listeners[MAX_INPUT_LISTENERS];
static size_t s_listener_count = 0;
static gpio_num_t s_button_pin = GPIO_NUM_9;
static void dispatch_action(input_action_t action)
{
sys_event_id_t evt_id = SYS_EVENT_CUSTOM;
switch (action) {
case INPUT_ACTION_CLICK:
evt_id = SYS_EVENT_INPUT_SHORT_PRESS;
break;
case INPUT_ACTION_DOUBLE_CLICK:
evt_id = SYS_EVENT_INPUT_DOUBLE_CLICK;
break;
case INPUT_ACTION_LONG_PRESS:
evt_id = SYS_EVENT_INPUT_LONG_PRESS;
break;
case INPUT_ACTION_HOLD_HOME:
evt_id = SYS_EVENT_INPUT_EXTRA_LONG_PRESS;
break;
default:
return;
}
sys_event_post(evt_id, (int32_t)action, NULL);
for (size_t i = 0; i < s_listener_count; i++) {
if (s_listeners[i].handler) {
s_listeners[i].handler(action, s_listeners[i].user_arg);
}
}
}
void sys_input_inject(input_action_t action)
{
dispatch_action(action);
}
esp_err_t sys_input_register_handler(sys_input_handler_t handler, void *user_arg)
{
if (!handler) return ESP_ERR_INVALID_ARG;
if (s_listener_count >= MAX_INPUT_LISTENERS) return ESP_ERR_NO_MEM;
s_listeners[s_listener_count].handler = handler;
s_listeners[s_listener_count].user_arg = user_arg;
s_listener_count++;
return ESP_OK;
}
static void button_scanner_task(void *pvParameters)
{
bool last_state = true;
uint32_t press_start_ms = 0;
uint32_t release_time_ms = 0;
uint8_t click_count = 0;
bool long_press_fired = false;
bool home_press_fired = false;
while (1) {
vTaskDelay(pdMS_TO_TICKS(10));
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000ULL);
bool current_state = (gpio_get_level(s_button_pin) == 0);
if (current_state && !last_state) {
press_start_ms = now_ms;
long_press_fired = false;
home_press_fired = false;
} else if (current_state && last_state) {
uint32_t held_duration = now_ms - press_start_ms;
if (!home_press_fired && held_duration >= HOME_PRESS_TIME_MS) {
home_press_fired = true;
click_count = 0;
dispatch_action(INPUT_ACTION_HOLD_HOME);
} else if (!long_press_fired && !home_press_fired && held_duration >= LONG_PRESS_TIME_MS) {
long_press_fired = true;
click_count = 0;
dispatch_action(INPUT_ACTION_LONG_PRESS);
}
} else if (!current_state && last_state) {
uint32_t press_duration = now_ms - press_start_ms;
if (press_duration >= DEBOUNCE_TIME_MS && !long_press_fired && !home_press_fired) {
click_count++;
release_time_ms = now_ms;
}
} else if (!current_state && !last_state) {
if (click_count > 0) {
uint32_t idle_time = now_ms - release_time_ms;
if (click_count >= 2) {
dispatch_action(INPUT_ACTION_DOUBLE_CLICK);
click_count = 0;
} else if (idle_time >= DOUBLE_CLICK_GAP_MS) {
dispatch_action(INPUT_ACTION_CLICK);
click_count = 0;
}
}
}
last_state = current_state;
}
}
esp_err_t sys_input_init(gpio_num_t button_gpio)
{
s_button_pin = button_gpio;
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << s_button_pin),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
esp_err_t err = gpio_config(&io_conf);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure button GPIO %d: %s", (int)s_button_pin, esp_err_to_name(err));
return err;
}
BaseType_t ret = xTaskCreate(button_scanner_task, "btn_scan_task", 3072, NULL, 12, NULL);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create button scanner task");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Input gesture engine started on GPIO %d", (int)s_button_pin);
return ESP_OK;
}