From 1491821e960035e0241bcad9f736e6671140d6f2 Mon Sep 17 00:00:00 2001 From: dark98 Date: Thu, 3 Sep 2026 00:46:04 +0100 Subject: [PATCH] Initial Demo OS --- .gitignore | 16 ++ .vscode/settings.json | 19 ++ CMakeLists.txt | 7 + README.md | 76 ++++++ components/sys_app/CMakeLists.txt | 14 ++ components/sys_app/include/os_app.h | 38 +++ components/sys_app/include/sys_app.h | 0 components/sys_app/sys_app.c | 157 ++++++++++++ components/sys_cli/CMakeLists.txt | 16 ++ components/sys_cli/include/os_cli.h | 17 ++ components/sys_cli/include/sys_cli.h | 0 components/sys_cli/sys_cli.c | 189 ++++++++++++++ components/sys_core/CMakeLists.txt | 12 + components/sys_core/include/os_event.h | 52 ++++ components/sys_core/include/os_status.h | 51 ++++ components/sys_core/include/sys_event.h | 0 components/sys_core/include/sys_status.h | 0 components/sys_core/sys_event.c | 84 +++++++ components/sys_core/sys_status.c | 87 +++++++ components/sys_gfx/CMakeLists.txt | 16 ++ components/sys_gfx/include/font_3x5.h | 0 components/sys_gfx/include/font_5x7.h | 0 components/sys_gfx/include/oled_driver.h | 0 components/sys_gfx/include/os_font_3x5.h | 104 ++++++++ components/sys_gfx/include/os_font_5x7.h | 104 ++++++++ components/sys_gfx/include/os_gfx.h | 48 ++++ components/sys_gfx/include/os_oled.h | 46 ++++ components/sys_gfx/include/sys_gfx.h | 0 components/sys_gfx/oled_driver.c | 159 ++++++++++++ components/sys_gfx/sys_gfx.c | 305 +++++++++++++++++++++++ components/sys_input/CMakeLists.txt | 13 + components/sys_input/include/os_input.h | 31 +++ components/sys_input/include/sys_input.h | 0 components/sys_input/sys_input.c | 147 +++++++++++ main/CMakeLists.txt | 26 ++ main/apps/app_clock.c | 88 +++++++ main/apps/app_game.c | 137 ++++++++++ main/apps/app_settings.c | 99 ++++++++ main/apps/app_sysmon.c | 96 +++++++ main/apps/apps.h | 19 ++ main/font_5x7.h | 110 ++++++++ main/main.c | 117 +++++++++ main/oled_display.c | 260 +++++++++++++++++++ main/oled_display.h | 129 ++++++++++ sdkconfig.defaults | 19 ++ 45 files changed, 2908 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 components/sys_app/CMakeLists.txt create mode 100644 components/sys_app/include/os_app.h create mode 100644 components/sys_app/include/sys_app.h create mode 100644 components/sys_app/sys_app.c create mode 100644 components/sys_cli/CMakeLists.txt create mode 100644 components/sys_cli/include/os_cli.h create mode 100644 components/sys_cli/include/sys_cli.h create mode 100644 components/sys_cli/sys_cli.c create mode 100644 components/sys_core/CMakeLists.txt create mode 100644 components/sys_core/include/os_event.h create mode 100644 components/sys_core/include/os_status.h create mode 100644 components/sys_core/include/sys_event.h create mode 100644 components/sys_core/include/sys_status.h create mode 100644 components/sys_core/sys_event.c create mode 100644 components/sys_core/sys_status.c create mode 100644 components/sys_gfx/CMakeLists.txt create mode 100644 components/sys_gfx/include/font_3x5.h create mode 100644 components/sys_gfx/include/font_5x7.h create mode 100644 components/sys_gfx/include/oled_driver.h create mode 100644 components/sys_gfx/include/os_font_3x5.h create mode 100644 components/sys_gfx/include/os_font_5x7.h create mode 100644 components/sys_gfx/include/os_gfx.h create mode 100644 components/sys_gfx/include/os_oled.h create mode 100644 components/sys_gfx/include/sys_gfx.h create mode 100644 components/sys_gfx/oled_driver.c create mode 100644 components/sys_gfx/sys_gfx.c create mode 100644 components/sys_input/CMakeLists.txt create mode 100644 components/sys_input/include/os_input.h create mode 100644 components/sys_input/include/sys_input.h create mode 100644 components/sys_input/sys_input.c create mode 100644 main/CMakeLists.txt create mode 100644 main/apps/app_clock.c create mode 100644 main/apps/app_game.c create mode 100644 main/apps/app_settings.c create mode 100644 main/apps/app_sysmon.c create mode 100644 main/apps/apps.h create mode 100644 main/font_5x7.h create mode 100644 main/main.c create mode 100644 main/oled_display.c create mode 100644 main/oled_display.h create mode 100644 sdkconfig.defaults diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4536b63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# ESP-IDF build artifacts +build/ +sdkconfig +sdkconfig.old +sdkconfig.ci + +# VS Code +.vscode/* +!.vscode/settings.json +!.vscode/c_cpp_properties.json + +# Clangd / CLion / IDEs +.clangd/ +.idea/ +cmake-build-*/ + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cae13e7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "idf.target": "esp32c3", + "idf.flashType": "UART", + "idf.espIdfPath": "D:/SDK/.espressif/v6.1/esp-idf", + "idf.toolsPath": "C:/Espressif/tools", + "idf.pythonBinPath": "C:/Espressif/tools/python/v6.1/venv/Scripts/python.exe", + "idf.customExtraPaths": "C:/Espressif/tools/ccache/4.12.1/ccache-4.12.1-windows-x86_64;C:/Espressif/tools/cmake/4.0.3/bin;C:/Espressif/tools/dfu-util/0.11/dfu-util-0.11-win64;C:/Espressif/tools/esp-clang/esp-21.1.3_20260408/esp-clang/bin;C:/Espressif/tools/esp-clangd/esp-21.1.3_20260408/esp-clangd/bin;C:/Espressif/tools/esp-rom-elfs/20241011;C:/Espressif/tools/esp32ulp-elf/2.38_20240113/esp32ulp-elf/bin;C:/Espressif/tools/idf-exe/1.0.3;C:/Espressif/tools/ninja/1.12.1;C:/Espressif/tools/openocd-esp32/v0.12.0-esp32-20260703/openocd-esp32/bin;C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/bin;C:/Espressif/tools/xtensa-esp-elf/esp-15.2.0_20251204/xtensa-esp-elf/bin;C:/Espressif/tools/python/v6.1/venv/Scripts", + "idf.customExtraVars": { + "OPENOCD_SCRIPTS": "C:/Espressif/tools/openocd-esp32/v0.12.0-esp32-20260703/openocd-esp32/share/openocd/scripts", + "IDF_CCACHE_ENABLE": "1", + "ESP_ROM_ELF_DIR": "C:/Espressif/tools/esp-rom-elfs/20241011/" + }, + "files.associations": { + "*.h": "c", + "*.c": "c" + }, + "idf.currentSetup": "D:\\SDK\\.espressif\\v6.1\\esp-idf", + "idf.portWin": "COM4" +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..44e783c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,7 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(abrobot_esp32c3_oled_os) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fc7d0b --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# ABrobot ESP32-C3 0.42" OLED — Micro-OS + +A modular, lightweight **Embedded Micro-OS** built for the **ABrobot ESP32-C3** development board equipped with the integrated **0.42" (72x40) monochrome OLED display**. + +--- + +## 🏛️ System Architecture + +``` +d:/Projects/ESP32C3/OS/ +├── components/ +│ ├── sys_core/ # Event bus (sys_event) & global state telemetry (sys_status) +│ ├── sys_input/ # Single-button gesture engine (Click, Double-Click, Hold, Home) +│ ├── sys_gfx/ # 72x40 UI compositor, OLED driver, 5x7 & 3x5 fonts, status/action bars, widgets +│ ├── sys_app/ # App lifecycle manager, registry, and Home App Launcher +│ └── sys_cli/ # Interactive USB Serial/JTAG shell (ps, free, app, input, reboot) +├── main/ +│ ├── apps/ +│ │ ├── apps.h # System apps registry +│ │ ├── app_clock.c # Clock / Watchface app (multiple view modes, live seconds bar) +│ │ ├── app_sysmon.c # Real-time System Monitor (live RAM sparkline graph, tasks, specs) +│ │ ├── app_settings.c # Settings app (contrast, inverted colors, statusbar toggle, reboot) +│ │ └── app_game.c # Tiny Jump retro runner mini-game +│ ├── CMakeLists.txt +│ └── main.c # OS bootstrap, LED heartbeat, and 20 FPS compositor loop +├── sdkconfig.defaults # ESP32-C3 target & USB Serial/JTAG console defaults +└── README.md +``` + +--- + +## 🎮 Single-Button Gesture Controls (`GPIO 9`) + +| Gesture | Timing | Action | +| :--- | :--- | :--- | +| **Short Click** | $< 350\text{ ms}$ | **Next / Step / Jump:** Cycle menu items, change clock view, jump in mini-game | +| **Double Click** | 2 clicks in $280\text{ ms}$ | **Previous / Back:** Cycle menu backwards | +| **Long Press** | $\ge 450\text{ ms}$ | **Select / Toggle:** Launch selected app, toggle settings | +| **Hold (Home)** | $\ge 1500\text{ ms}$ | **Global Home:** Return to the system App Launcher from any app | + +--- + +## 💻 Interactive USB CLI Console + +Connect the board via USB-C and open the serial monitor to access the live OS terminal: + +```text +esp32c3-os> help + help List all commands + ps List active FreeRTOS tasks and memory + free Display system heap and RAM status + app App management (list, launch , home) + input Simulate button input (click, double, long, home) + contrast Set OLED contrast (0-255) + reboot Reboot the MCU +``` + +### Examples: +- Switch to the game remotely: `app launch game` +- Trigger a jump remotely: `input click` +- Inspect tasks: `ps` +- Return to home menu: `app home` + +--- + +## 🚀 How to Build and Flash + +In your ESP-IDF terminal environment: + +```bash +# 1. Build the project +idf.py build + +# 2. Flash to the board and open the serial monitor +idf.py -p COMx flash monitor +``` diff --git a/components/sys_app/CMakeLists.txt b/components/sys_app/CMakeLists.txt new file mode 100644 index 0000000..6ec76e9 --- /dev/null +++ b/components/sys_app/CMakeLists.txt @@ -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 +) + diff --git a/components/sys_app/include/os_app.h b/components/sys_app/include/os_app.h new file mode 100644 index 0000000..f768666 --- /dev/null +++ b/components/sys_app/include/os_app.h @@ -0,0 +1,38 @@ +#ifndef OS_APP_H +#define OS_APP_H + +#include +#include +#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 diff --git a/components/sys_app/include/sys_app.h b/components/sys_app/include/sys_app.h new file mode 100644 index 0000000..e69de29 diff --git a/components/sys_app/sys_app.c b/components/sys_app/sys_app.c new file mode 100644 index 0000000..e65c047 --- /dev/null +++ b/components/sys_app/sys_app.c @@ -0,0 +1,157 @@ +#include "os_app.h" +#include "os_gfx.h" +#include "os_status.h" +#include "os_event.h" +#include +#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]; +} diff --git a/components/sys_cli/CMakeLists.txt b/components/sys_cli/CMakeLists.txt new file mode 100644 index 0000000..0c6519e --- /dev/null +++ b/components/sys_cli/CMakeLists.txt @@ -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 +) + diff --git a/components/sys_cli/include/os_cli.h b/components/sys_cli/include/os_cli.h new file mode 100644 index 0000000..8021d85 --- /dev/null +++ b/components/sys_cli/include/os_cli.h @@ -0,0 +1,17 @@ +#ifndef OS_CLI_H +#define OS_CLI_H + +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +esp_err_t sys_cli_init(void); + +#ifdef __cplusplus +} +#endif + +#endif // OS_CLI_H diff --git a/components/sys_cli/include/sys_cli.h b/components/sys_cli/include/sys_cli.h new file mode 100644 index 0000000..e69de29 diff --git a/components/sys_cli/sys_cli.c b/components/sys_cli/sys_cli.c new file mode 100644 index 0000000..690390f --- /dev/null +++ b/components/sys_cli/sys_cli.c @@ -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 +#include +#include +#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 | 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 \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 , home)", + .hint = "", + .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 = "", + .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 = "", + .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; +} diff --git a/components/sys_core/CMakeLists.txt b/components/sys_core/CMakeLists.txt new file mode 100644 index 0000000..e666d8b --- /dev/null +++ b/components/sys_core/CMakeLists.txt @@ -0,0 +1,12 @@ +idf_component_register( + SRCS + "sys_event.c" + "sys_status.c" + INCLUDE_DIRS + "include" + REQUIRES + freertos + esp_timer + esp_system + log +) diff --git a/components/sys_core/include/os_event.h b/components/sys_core/include/os_event.h new file mode 100644 index 0000000..8afc91a --- /dev/null +++ b/components/sys_core/include/os_event.h @@ -0,0 +1,52 @@ +#ifndef OS_EVENT_H +#define OS_EVENT_H + +#include +#include +#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 diff --git a/components/sys_core/include/os_status.h b/components/sys_core/include/os_status.h new file mode 100644 index 0000000..5841828 --- /dev/null +++ b/components/sys_core/include/os_status.h @@ -0,0 +1,51 @@ +#ifndef OS_STATUS_H +#define OS_STATUS_H + +#include +#include + +#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 diff --git a/components/sys_core/include/sys_event.h b/components/sys_core/include/sys_event.h new file mode 100644 index 0000000..e69de29 diff --git a/components/sys_core/include/sys_status.h b/components/sys_core/include/sys_status.h new file mode 100644 index 0000000..e69de29 diff --git a/components/sys_core/sys_event.c b/components/sys_core/sys_event.c new file mode 100644 index 0000000..682a6d1 --- /dev/null +++ b/components/sys_core/sys_event.c @@ -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; +} diff --git a/components/sys_core/sys_status.c b/components/sys_core/sys_status.c new file mode 100644 index 0000000..4c6126f --- /dev/null +++ b/components/sys_core/sys_status.c @@ -0,0 +1,87 @@ +#include "os_status.h" +#include +#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); +} diff --git a/components/sys_gfx/CMakeLists.txt b/components/sys_gfx/CMakeLists.txt new file mode 100644 index 0000000..ba2038e --- /dev/null +++ b/components/sys_gfx/CMakeLists.txt @@ -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 +) + diff --git a/components/sys_gfx/include/font_3x5.h b/components/sys_gfx/include/font_3x5.h new file mode 100644 index 0000000..e69de29 diff --git a/components/sys_gfx/include/font_5x7.h b/components/sys_gfx/include/font_5x7.h new file mode 100644 index 0000000..e69de29 diff --git a/components/sys_gfx/include/oled_driver.h b/components/sys_gfx/include/oled_driver.h new file mode 100644 index 0000000..e69de29 diff --git a/components/sys_gfx/include/os_font_3x5.h b/components/sys_gfx/include/os_font_3x5.h new file mode 100644 index 0000000..d54036c --- /dev/null +++ b/components/sys_gfx/include/os_font_3x5.h @@ -0,0 +1,104 @@ +#ifndef OS_FONT_3X5_H +#define OS_FONT_3X5_H + +#include + +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 diff --git a/components/sys_gfx/include/os_font_5x7.h b/components/sys_gfx/include/os_font_5x7.h new file mode 100644 index 0000000..1f51f04 --- /dev/null +++ b/components/sys_gfx/include/os_font_5x7.h @@ -0,0 +1,104 @@ +#ifndef OS_FONT_5X7_H +#define OS_FONT_5X7_H + +#include + +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 diff --git a/components/sys_gfx/include/os_gfx.h b/components/sys_gfx/include/os_gfx.h new file mode 100644 index 0000000..36b830c --- /dev/null +++ b/components/sys_gfx/include/os_gfx.h @@ -0,0 +1,48 @@ +#ifndef OS_GFX_H +#define OS_GFX_H + +#include +#include +#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 diff --git a/components/sys_gfx/include/os_oled.h b/components/sys_gfx/include/os_oled.h new file mode 100644 index 0000000..aaee444 --- /dev/null +++ b/components/sys_gfx/include/os_oled.h @@ -0,0 +1,46 @@ +#ifndef OS_OLED_H +#define OS_OLED_H + +#include +#include +#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 diff --git a/components/sys_gfx/include/sys_gfx.h b/components/sys_gfx/include/sys_gfx.h new file mode 100644 index 0000000..e69de29 diff --git a/components/sys_gfx/oled_driver.c b/components/sys_gfx/oled_driver.c new file mode 100644 index 0000000..e23080e --- /dev/null +++ b/components/sys_gfx/oled_driver.c @@ -0,0 +1,159 @@ +#include "os_oled.h" +#include +#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; + } +} diff --git a/components/sys_gfx/sys_gfx.c b/components/sys_gfx/sys_gfx.c new file mode 100644 index 0000000..2bb14df --- /dev/null +++ b/components/sys_gfx/sys_gfx.c @@ -0,0 +1,305 @@ +#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 */ +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); +} diff --git a/components/sys_input/CMakeLists.txt b/components/sys_input/CMakeLists.txt new file mode 100644 index 0000000..757d7b1 --- /dev/null +++ b/components/sys_input/CMakeLists.txt @@ -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 +) diff --git a/components/sys_input/include/os_input.h b/components/sys_input/include/os_input.h new file mode 100644 index 0000000..ae321bc --- /dev/null +++ b/components/sys_input/include/os_input.h @@ -0,0 +1,31 @@ +#ifndef OS_INPUT_H +#define OS_INPUT_H + +#include +#include +#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 diff --git a/components/sys_input/include/sys_input.h b/components/sys_input/include/sys_input.h new file mode 100644 index 0000000..e69de29 diff --git a/components/sys_input/sys_input.c b/components/sys_input/sys_input.c new file mode 100644 index 0000000..ab03dfa --- /dev/null +++ b/components/sys_input/sys_input.c @@ -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; +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..f4158c2 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,26 @@ +idf_component_register( + SRCS + "main.c" + "apps/app_clock.c" + "apps/app_sysmon.c" + "apps/app_settings.c" + "apps/app_game.c" + INCLUDE_DIRS + "." + "apps" + REQUIRES + sys_core + sys_input + sys_gfx + sys_app + sys_cli + driver + esp_driver_gpio + esp_driver_i2c + spi_flash + esp_timer + freertos + log + esp_system + esp_hw_support +) diff --git a/main/apps/app_clock.c b/main/apps/app_clock.c new file mode 100644 index 0000000..7ce4e13 --- /dev/null +++ b/main/apps/app_clock.c @@ -0,0 +1,88 @@ +#include "apps.h" +#include "os_gfx.h" +#include "os_status.h" +#include +#include +#include "esp_timer.h" + +static uint8_t s_view_mode = 0; + +static void clock_init(void) +{ + s_view_mode = 0; +} + +static void clock_enter(void) +{ +} + +static void clock_update(uint32_t delta_ms) +{ + sys_status_t status; + sys_status_get(&status); + + sys_gfx_draw_status_bar(&status); + + uint32_t sec = status.uptime_sec % 60; + uint32_t min = (status.uptime_sec / 60) % 60; + uint32_t hrs = (status.uptime_sec / 3600); + + char time_str[16]; + char sub_str[20]; + + if (s_view_mode == 0) { + snprintf(time_str, sizeof(time_str), "%02lu:%02lu:%02lu", (unsigned long)hrs, (unsigned long)min, (unsigned long)sec); + sys_gfx_draw_str_5x7(12, 11, time_str, COLOR_WHITE); + + snprintf(sub_str, sizeof(sub_str), "RAM:%luK", (unsigned long)(status.free_heap / 1024)); + sys_gfx_draw_str_3x5(4, 21, sub_str, COLOR_WHITE); + + snprintf(sub_str, sizeof(sub_str), "CPU:160M"); + sys_gfx_draw_str_3x5(40, 21, sub_str, COLOR_WHITE); + + sys_gfx_draw_progress_bar(4, 27, 64, 4, (uint8_t)((sec * 100) / 59)); + sys_gfx_draw_action_bar("C:MODE", "H:HOME"); + } else if (s_view_mode == 1) { + snprintf(time_str, sizeof(time_str), "%02lu:%02lu", (unsigned long)min, (unsigned long)sec); + sys_gfx_draw_str_5x7(18, 14, time_str, COLOR_WHITE); + sys_gfx_draw_str_5x7(19, 14, time_str, COLOR_WHITE); + + snprintf(sub_str, sizeof(sub_str), "UP:%luh %lum", (unsigned long)hrs, (unsigned long)min); + sys_gfx_draw_str_3x5(12, 24, sub_str, COLOR_WHITE); + sys_gfx_draw_action_bar("C:MODE", "H:HOME"); + } else { + snprintf(sub_str, sizeof(sub_str), "UP:%02lu:%02lu:%02lu", (unsigned long)hrs, (unsigned long)min, (unsigned long)sec); + sys_gfx_draw_str_3x5(3, 10, sub_str, COLOR_WHITE); + + snprintf(sub_str, sizeof(sub_str), "HEAP:%lu B", (unsigned long)status.free_heap); + sys_gfx_draw_str_3x5(3, 17, sub_str, COLOR_WHITE); + + snprintf(sub_str, sizeof(sub_str), "MINH:%lu B", (unsigned long)status.min_free_heap); + sys_gfx_draw_str_3x5(3, 24, sub_str, COLOR_WHITE); + + sys_gfx_draw_action_bar("C:MODE", "H:HOME"); + } +} + +static void clock_handle_input(input_action_t action) +{ + if (action == INPUT_ACTION_CLICK) { + s_view_mode = (s_view_mode + 1) % 3; + } else if (action == INPUT_ACTION_LONG_PRESS) { + sys_gfx_show_toast("Mode Saved", 600); + } +} + +static void clock_exit(void) +{ +} + +const sys_app_t g_app_clock = { + .id = "clock", + .name = "Clock", + .init = clock_init, + .enter = clock_enter, + .update = clock_update, + .handle_input = clock_handle_input, + .exit = clock_exit, +}; diff --git a/main/apps/app_game.c b/main/apps/app_game.c new file mode 100644 index 0000000..240dbcf --- /dev/null +++ b/main/apps/app_game.c @@ -0,0 +1,137 @@ +#include "apps.h" +#include "os_gfx.h" +#include "os_status.h" +#include +#include + +#define GROUND_Y 34 + +typedef enum { + GAME_STATE_READY, + GAME_STATE_PLAYING, + GAME_STATE_OVER +} game_state_t; + +static game_state_t s_game_state = GAME_STATE_READY; +static int16_t s_player_y = GROUND_Y; +static int16_t s_player_vy = 0; +static bool s_is_jumping = false; + +static int16_t s_obs_x = 72; +static int16_t s_obs_h = 6; +static uint16_t s_score = 0; +static uint16_t s_high_score = 0; + +static void game_reset(void) +{ + s_player_y = GROUND_Y; + s_player_vy = 0; + s_is_jumping = false; + s_obs_x = 72; + s_obs_h = 5 + (rand() % 4); + s_score = 0; +} + +static void game_init(void) +{ + s_game_state = GAME_STATE_READY; + s_high_score = 0; + game_reset(); +} + +static void game_enter(void) +{ + s_game_state = GAME_STATE_READY; + game_reset(); +} + +static void game_update(uint32_t delta_ms) +{ + sys_gfx_draw_line(0, GROUND_Y + 1, OLED_WIDTH - 1, GROUND_Y + 1, COLOR_WHITE); + + if (s_game_state == GAME_STATE_READY) { + sys_gfx_draw_str_5x7(10, 8, "TINY JUMP", COLOR_WHITE); + sys_gfx_draw_str_3x5(8, 20, "CLICK TO START", COLOR_WHITE); + sys_gfx_draw_action_bar("C:JUMP", "H:HOME"); + sys_gfx_fill_rect(8, GROUND_Y - 5, 5, 5, COLOR_WHITE); + return; + } + + if (s_game_state == GAME_STATE_PLAYING) { + if (s_is_jumping) { + s_player_y += s_player_vy; + s_player_vy += 1; + if (s_player_y >= GROUND_Y) { + s_player_y = GROUND_Y; + s_player_vy = 0; + s_is_jumping = false; + } + } + + s_obs_x -= 3; + if (s_obs_x < -4) { + s_obs_x = 72 + (rand() % 16); + s_obs_h = 5 + (rand() % 5); + s_score++; + if (s_score > s_high_score) s_high_score = s_score; + } + + if (s_obs_x <= 13 && (s_obs_x + 3) >= 8) { + if ((s_player_y - 1) >= (GROUND_Y - s_obs_h + 1)) { + s_game_state = GAME_STATE_OVER; + sys_gfx_show_toast("GAME OVER", 800); + } + } + + sys_gfx_fill_rect(8, s_player_y - 5, 5, 5, COLOR_WHITE); + sys_gfx_fill_rect(s_obs_x, GROUND_Y - s_obs_h + 1, 3, s_obs_h, COLOR_WHITE); + + char score_str[12]; + snprintf(score_str, sizeof(score_str), "SC:%u", s_score); + sys_gfx_draw_str_3x5(45, 1, score_str, COLOR_WHITE); + + } else if (s_game_state == GAME_STATE_OVER) { + sys_gfx_draw_str_5x7(12, 6, "GAME OVER", COLOR_WHITE); + + char buf[16]; + snprintf(buf, sizeof(buf), "SCORE: %u", s_score); + sys_gfx_draw_str_3x5(16, 17, buf, COLOR_WHITE); + + snprintf(buf, sizeof(buf), "BEST: %u", s_high_score); + sys_gfx_draw_str_3x5(16, 24, buf, COLOR_WHITE); + + sys_gfx_draw_action_bar("C:RESTART", "H:HOME"); + } +} + +static void game_handle_input(input_action_t action) +{ + if (action == INPUT_ACTION_CLICK) { + if (s_game_state == GAME_STATE_READY) { + s_game_state = GAME_STATE_PLAYING; + game_reset(); + } else if (s_game_state == GAME_STATE_PLAYING) { + if (!s_is_jumping) { + s_is_jumping = true; + s_player_vy = -5; + } + } else if (s_game_state == GAME_STATE_OVER) { + s_game_state = GAME_STATE_PLAYING; + game_reset(); + } + } +} + +static void game_exit(void) +{ +} + +const sys_app_t g_app_game = { + .id = "game", + .name = "Tiny Jump", + .init = game_init, + .enter = game_enter, + .update = game_update, + .handle_input = game_handle_input, + .exit = game_exit, +}; diff --git a/main/apps/app_settings.c b/main/apps/app_settings.c new file mode 100644 index 0000000..b9cc1e0 --- /dev/null +++ b/main/apps/app_settings.c @@ -0,0 +1,99 @@ +#include "apps.h" +#include "os_gfx.h" +#include "os_status.h" +#include "os_oled.h" +#include +#include "esp_system.h" + +#define SETTINGS_COUNT 4 +static const char *s_menu_items[SETTINGS_COUNT] = { + "Contrast", + "Invert", + "Status Bar", + "Reboot" +}; + +static size_t s_selected_idx = 0; +static uint8_t s_contrast_level = 3; +static bool s_inverted = false; + +static void apply_contrast(void) +{ + uint8_t val = 255; + if (s_contrast_level == 0) val = 30; + else if (s_contrast_level == 1) val = 100; + else if (s_contrast_level == 2) val = 180; + oled_driver_set_contrast(val); +} + +static void settings_init(void) +{ + s_selected_idx = 0; + s_contrast_level = 3; + s_inverted = false; +} + +static void settings_enter(void) +{ +} + +static void settings_update(uint32_t delta_ms) +{ + sys_status_t status; + sys_status_get(&status); + sys_gfx_draw_status_bar(&status); + + sys_gfx_draw_menu(9, s_menu_items, SETTINGS_COUNT, s_selected_idx); + sys_gfx_draw_action_bar("C:NEXT", "H:TOGGLE"); +} + +static void settings_handle_input(input_action_t action) +{ + if (action == INPUT_ACTION_CLICK) { + s_selected_idx = (s_selected_idx + 1) % SETTINGS_COUNT; + } else if (action == INPUT_ACTION_DOUBLE_CLICK) { + if (s_selected_idx == 0) s_selected_idx = SETTINGS_COUNT - 1; + else s_selected_idx--; + } else if (action == INPUT_ACTION_LONG_PRESS) { + switch (s_selected_idx) { + case 0: + s_contrast_level = (s_contrast_level + 1) % 4; + apply_contrast(); + sys_gfx_show_toast("Contrast Chg", 600); + break; + case 1: + s_inverted = !s_inverted; + oled_driver_set_invert(s_inverted); + sys_gfx_show_toast(s_inverted ? "Inverted" : "Normal", 600); + break; + case 2: + { + bool vis = sys_status_is_status_bar_visible(); + sys_status_set_status_bar_visible(!vis); + sys_gfx_show_toast(!vis ? "Bar: ON" : "Bar: OFF", 600); + } + break; + case 3: + sys_gfx_show_toast("Rebooting...", 1000); + sys_gfx_flush(); + esp_restart(); + break; + default: + break; + } + } +} + +static void settings_exit(void) +{ +} + +const sys_app_t g_app_settings = { + .id = "settings", + .name = "Settings", + .init = settings_init, + .enter = settings_enter, + .update = settings_update, + .handle_input = settings_handle_input, + .exit = settings_exit, +}; diff --git a/main/apps/app_sysmon.c b/main/apps/app_sysmon.c new file mode 100644 index 0000000..18ffdbd --- /dev/null +++ b/main/apps/app_sysmon.c @@ -0,0 +1,96 @@ +#include "apps.h" +#include "os_gfx.h" +#include "os_status.h" +#include +#include +#include "esp_system.h" +#include "esp_chip_info.h" +#include "esp_flash.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define HISTORY_LEN 40 +static uint8_t s_heap_history[HISTORY_LEN] = {0}; +static size_t s_history_count = 0; +static uint8_t s_view = 0; + +static void sysmon_init(void) +{ + s_history_count = 0; + memset(s_heap_history, 50, sizeof(s_heap_history)); +} + +static void sysmon_enter(void) +{ +} + +static void sysmon_update(uint32_t delta_ms) +{ + sys_status_t status; + sys_status_get(&status); + sys_gfx_draw_status_bar(&status); + + static uint32_t sample_timer = 0; + sample_timer += delta_ms; + if (sample_timer >= 200) { + sample_timer = 0; + uint32_t kb = status.free_heap / 1024; + uint8_t norm_val = (kb > 300) ? 100 : (uint8_t)((kb * 100) / 300); + + if (s_history_count < HISTORY_LEN) { + s_heap_history[s_history_count++] = norm_val; + } else { + memmove(&s_heap_history[0], &s_heap_history[1], HISTORY_LEN - 1); + s_heap_history[HISTORY_LEN - 1] = norm_val; + } + } + + char buf[24]; + + if (s_view == 0) { + snprintf(buf, sizeof(buf), "RAM:%luK", (unsigned long)(status.free_heap / 1024)); + sys_gfx_draw_str_3x5(3, 9, buf, COLOR_WHITE); + + snprintf(buf, sizeof(buf), "TASKS:%u", (unsigned int)uxTaskGetNumberOfTasks()); + sys_gfx_draw_str_3x5(40, 9, buf, COLOR_WHITE); + + sys_gfx_draw_sparkline(3, 16, 66, 14, s_heap_history, s_history_count, 100); + sys_gfx_draw_action_bar("C:VIEW", "H:HOME"); + } else { + esp_chip_info_t chip; + esp_chip_info(&chip); + uint32_t flash_size = 0; + esp_flash_get_size(NULL, &flash_size); + + sys_gfx_draw_str_3x5(3, 9, "ESP32-C3 RISC-V", COLOR_WHITE); + + snprintf(buf, sizeof(buf), "REV:%d CORES:%d", chip.revision, chip.cores); + sys_gfx_draw_str_3x5(3, 16, buf, COLOR_WHITE); + + snprintf(buf, sizeof(buf), "FLASH:%lu MB", (unsigned long)(flash_size / (1024 * 1024))); + sys_gfx_draw_str_3x5(3, 23, buf, COLOR_WHITE); + + sys_gfx_draw_action_bar("C:VIEW", "H:HOME"); + } +} + +static void sysmon_handle_input(input_action_t action) +{ + if (action == INPUT_ACTION_CLICK) { + s_view = (s_view + 1) % 2; + } +} + +static void sysmon_exit(void) +{ +} + +const sys_app_t g_app_sysmon = { + .id = "sysmon", + .name = "Sys Monitor", + .init = sysmon_init, + .enter = sysmon_enter, + .update = sysmon_update, + .handle_input = sysmon_handle_input, + .exit = sysmon_exit, +}; diff --git a/main/apps/apps.h b/main/apps/apps.h new file mode 100644 index 0000000..429ef30 --- /dev/null +++ b/main/apps/apps.h @@ -0,0 +1,19 @@ +#ifndef APPS_H +#define APPS_H + +#include "os_app.h" + +#ifdef __cplusplus +extern "C" { +#endif + +extern const sys_app_t g_app_clock; +extern const sys_app_t g_app_sysmon; +extern const sys_app_t g_app_settings; +extern const sys_app_t g_app_game; + +#ifdef __cplusplus +} +#endif + +#endif // APPS_H diff --git a/main/font_5x7.h b/main/font_5x7.h new file mode 100644 index 0000000..abcf6c3 --- /dev/null +++ b/main/font_5x7.h @@ -0,0 +1,110 @@ +#ifndef FONT_5X7_H +#define FONT_5X7_H + +#include + +/** + * @brief Standard 5x7 ASCII font table (ASCII characters 32 ' ' to 127 '~') + * Each character is 5 bytes wide, 7 bits high (MSB to LSB or LSB to MSB). + * 1 byte per column. + */ +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 // FONT_5X7_H + diff --git a/main/main.c b/main/main.c new file mode 100644 index 0000000..54af2ed --- /dev/null +++ b/main/main.c @@ -0,0 +1,117 @@ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_chip_info.h" +#include "esp_flash.h" +#include "esp_log.h" +#include "esp_timer.h" +#include "driver/gpio.h" + +/* OS Subsystems */ +#include "os_event.h" +#include "os_status.h" +#include "os_gfx.h" +#include "os_input.h" +#include "os_app.h" +#include "os_cli.h" +#include "apps.h" + +static const char *TAG = "MICRO_OS"; + +#define PIN_LED_USER GPIO_NUM_8 +#define PIN_BUTTON_BOOT GPIO_NUM_9 +#define UI_REFRESH_HZ 20 +#define UI_FRAME_MS (1000 / UI_REFRESH_HZ) + +/* Task: Onboard LED Heartbeat */ +static void led_heartbeat_task(void *pvParameters) +{ + gpio_config_t io_conf = { + .pin_bit_mask = (1ULL << PIN_LED_USER), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&io_conf); + + while (1) { + gpio_set_level(PIN_LED_USER, 1); + vTaskDelay(pdMS_TO_TICKS(60)); + gpio_set_level(PIN_LED_USER, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + gpio_set_level(PIN_LED_USER, 1); + vTaskDelay(pdMS_TO_TICKS(60)); + gpio_set_level(PIN_LED_USER, 0); + vTaskDelay(pdMS_TO_TICKS(800)); + } +} + +/* Callback: Forward button gestures to App Manager */ +static void on_input_event(input_action_t action, void *user_arg) +{ + sys_app_dispatch_input(action); +} + +/* Task: OS Display & UI Compositor Loop */ +static void os_ui_task(void *pvParameters) +{ + ESP_LOGI(TAG, "Starting UI compositor loop @ %d FPS", UI_REFRESH_HZ); + uint32_t last_time_ms = (uint32_t)(esp_timer_get_time() / 1000ULL); + + while (1) { + uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000ULL); + uint32_t delta_ms = now_ms - last_time_ms; + last_time_ms = now_ms; + + sys_status_update(); + sys_gfx_clear(); + sys_app_update(delta_ms); + sys_gfx_render_toast_if_active(); + sys_gfx_flush(); + + vTaskDelay(pdMS_TO_TICKS(UI_FRAME_MS)); + } +} + +void app_main(void) +{ + ESP_LOGI(TAG, "============================================="); + ESP_LOGI(TAG, " Booting ABrobot ESP32-C3 Micro-OS v1.0.0 "); + ESP_LOGI(TAG, "============================================="); + + // 1. Core Services & Telemetry + sys_event_init(); + sys_status_init(); + + // 2. Display & Graphics Subsystem + esp_err_t err = sys_gfx_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize graphics subsystem: %s", esp_err_to_name(err)); + } + + // 3. Button & Gesture Engine + sys_input_init(PIN_BUTTON_BOOT); + sys_input_register_handler(on_input_event, NULL); + + // 4. Application Framework & Registry + sys_app_init(); + sys_app_register(&g_app_clock); + sys_app_register(&g_app_sysmon); + sys_app_register(&g_app_game); + sys_app_register(&g_app_settings); + + // Launch default startup app + sys_app_launch_by_id("clock"); + + // 5. Interactive USB Console Shell + sys_cli_init(); + + // 6. Launch OS Tasks + xTaskCreate(led_heartbeat_task, "led_hb_task", 2048, NULL, 5, NULL); + xTaskCreate(os_ui_task, "ui_task", 4096, NULL, 10, NULL); + + ESP_LOGI(TAG, "Micro-OS initialized and running."); +} diff --git a/main/oled_display.c b/main/oled_display.c new file mode 100644 index 0000000..a346a19 --- /dev/null +++ b/main/oled_display.c @@ -0,0 +1,260 @@ +#include "oled_display.h" +#include "font_5x7.h" +#include +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "OLED_DISPLAY"; + +/* Internal frame buffer: 72 columns x 5 pages = 360 bytes */ +static uint8_t s_frame_buffer[OLED_BUFFER_SIZE]; +static uint8_t s_column_offset = OLED_COLUMN_OFFSET; + +/* Helper: Send command to OLED */ +static esp_err_t oled_send_cmd(uint8_t cmd) +{ + i2c_cmd_handle_t link = i2c_cmd_link_create(); + i2c_master_start(link); + i2c_master_write_byte(link, (OLED_I2C_ADDR << 1) | I2C_MASTER_WRITE, true); + i2c_master_write_byte(link, 0x00, true); // Control byte: 0x00 = Command + i2c_master_write_byte(link, cmd, true); + i2c_master_stop(link); + esp_err_t ret = i2c_master_cmd_begin(OLED_I2C_PORT, link, pdMS_TO_TICKS(50)); + i2c_cmd_link_delete(link); + return ret; +} + +/* Helper: Send multiple commands to OLED */ +static esp_err_t oled_send_cmds(const uint8_t *cmds, size_t len) +{ + i2c_cmd_handle_t link = i2c_cmd_link_create(); + i2c_master_start(link); + i2c_master_write_byte(link, (OLED_I2C_ADDR << 1) | I2C_MASTER_WRITE, true); + i2c_master_write_byte(link, 0x00, true); // Control byte: 0x00 = Command + i2c_master_write(link, cmds, len, true); + i2c_master_stop(link); + esp_err_t ret = i2c_master_cmd_begin(OLED_I2C_PORT, link, pdMS_TO_TICKS(100)); + i2c_cmd_link_delete(link); + return ret; +} + +esp_err_t oled_init(gpio_num_t sda, gpio_num_t scl, uint8_t col_offset) +{ + s_column_offset = col_offset; + + // 1. Configure I2C bus + i2c_config_t conf = { + .mode = I2C_MODE_MASTER, + .sda_io_num = sda, + .scl_io_num = scl, + .sda_pullup_en = GPIO_PULLUP_ENABLE, + .scl_pullup_en = GPIO_PULLUP_ENABLE, + .master.clk_speed = OLED_I2C_FREQ_HZ, + }; + + esp_err_t err = i2c_param_config(OLED_I2C_PORT, &conf); + if (err != ESP_OK) { + ESP_LOGE(TAG, "i2c_param_config failed: %s", esp_err_to_name(err)); + return err; + } + + err = i2c_driver_install(OLED_I2C_PORT, conf.mode, 0, 0, 0); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "i2c_driver_install failed: %s", esp_err_to_name(err)); + return err; + } + + vTaskDelay(pdMS_TO_TICKS(50)); // Allow display power to stabilize + + // 2. Display initialization sequence for 72x40 OLED (SH1106 / SSD1306 compatible) + const uint8_t init_cmds[] = { + 0xAE, // Display OFF + 0xD5, 0x80, // Set Display Clock Divide Ratio / Osc Frequency + 0xA8, 0x27, // Set Multiplex Ratio (40 - 1 = 39 = 0x27) + 0xD3, 0x00, // Set Display Offset to 0 + 0x40, // Set Display Start Line to 0 + 0x8D, 0x14, // Enable Charge Pump + 0x20, 0x02, // Set Memory Addressing Mode to Page mode (compatible with SH1106) + 0xA1, // Set Segment Re-map (A0/A1) + 0xC8, // Set COM Output Scan Direction (C0/C8) + 0xDA, 0x12, // Set COM Pins hardware config + 0x81, 0xAF, // Set Contrast Control (0xAF) + 0xD9, 0xF1, // Set Pre-charge Period + 0xDB, 0x20, // Set VCOMH Deselect Level + 0xA4, // Resume to RAM content (Entire Display ON disabled) + 0xA6, // Normal display (non-inverted) + 0xAF // Display ON + }; + + err = oled_send_cmds(init_cmds, sizeof(init_cmds)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "OLED init commands failed: %s (Check I2C connections)", esp_err_to_name(err)); + return err; + } + + oled_clear(); + oled_update(); + + ESP_LOGI(TAG, "OLED initialized (72x40, offset: %d, SDA: %d, SCL: %d)", + s_column_offset, (int)sda, (int)scl); + return ESP_OK; +} + +void oled_clear(void) +{ + memset(s_frame_buffer, 0x00, sizeof(s_frame_buffer)); +} + +void oled_fill(void) +{ + memset(s_frame_buffer, 0xFF, sizeof(s_frame_buffer)); +} + +esp_err_t oled_update(void) +{ + for (uint8_t page = 0; page < OLED_PAGES; page++) { + // Page address + column address setup + 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; + } + + // Push 72 bytes of pixel data for the current page + i2c_cmd_handle_t link = i2c_cmd_link_create(); + i2c_master_start(link); + i2c_master_write_byte(link, (OLED_I2C_ADDR << 1) | I2C_MASTER_WRITE, true); + i2c_master_write_byte(link, 0x40, true); // Control byte: 0x40 = Data + i2c_master_write(link, &s_frame_buffer[page * OLED_WIDTH], OLED_WIDTH, true); + i2c_master_stop(link); + + err = i2c_master_cmd_begin(OLED_I2C_PORT, link, pdMS_TO_TICKS(50)); + i2c_cmd_link_delete(link); + + if (err != ESP_OK) { + return err; + } + } + return ESP_OK; +} + +esp_err_t oled_set_contrast(uint8_t contrast) +{ + uint8_t cmds[] = {0x81, contrast}; + return oled_send_cmds(cmds, sizeof(cmds)); +} + +esp_err_t oled_invert(bool invert) +{ + return oled_send_cmd(invert ? 0xA7 : 0xA6); +} + +void oled_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 OLED_COLOR_WHITE: + s_frame_buffer[index] |= (1 << bit); + break; + case OLED_COLOR_BLACK: + s_frame_buffer[index] &= ~(1 << bit); + break; + case OLED_COLOR_INVERT: + s_frame_buffer[index] ^= (1 << bit); + break; + default: + break; + } +} + +void oled_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_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 oled_draw_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color) +{ + if (w <= 0 || h <= 0) return; + oled_draw_line(x, y, x + w - 1, y, color); + oled_draw_line(x, y + h - 1, x + w - 1, y + h - 1, color); + oled_draw_line(x, y, x, y + h - 1, color); + oled_draw_line(x + w - 1, y, x + w - 1, y + h - 1, color); +} + +void oled_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_draw_pixel(i, j, color); + } + } +} + +void oled_draw_char(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_draw_pixel(x + col, y + row, color); + } else if (color == OLED_COLOR_WHITE) { + oled_draw_pixel(x + col, y + row, OLED_COLOR_BLACK); + } + } + } +} + +void oled_draw_string(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 { + oled_draw_char(cur_x, cur_y, *str, color); + cur_x += 6; // 5 pixels width + 1 pixel inter-character spacing + } + str++; + } +} + diff --git a/main/oled_display.h b/main/oled_display.h new file mode 100644 index 0000000..e3cd61a --- /dev/null +++ b/main/oled_display.h @@ -0,0 +1,129 @@ +#ifndef OLED_DISPLAY_H +#define OLED_DISPLAY_H + +#include +#include +#include "driver/gpio.h" +#include "driver/i2c.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Hardware Specifications for ABrobot ESP32-C3 0.42" OLED */ +#define OLED_I2C_PORT I2C_NUM_0 +#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 + +/* Display Resolution */ +#define OLED_WIDTH 72 +#define OLED_HEIGHT 40 +#define OLED_PAGES 5 /* (40 / 8) */ +#define OLED_BUFFER_SIZE (OLED_WIDTH * OLED_PAGES) + +/* Controller Column Offset (SSD1306/SH1106 internal RAM mapping) */ +#ifndef OLED_COLUMN_OFFSET +#define OLED_COLUMN_OFFSET 28 /* Common offset is 28 (or 26-30 depending on batch) */ +#endif + +/* Color modes */ +#define OLED_COLOR_BLACK 0 +#define OLED_COLOR_WHITE 1 +#define OLED_COLOR_INVERT 2 + +/** + * @brief Initialize I2C driver and SSD1306/SH1106 OLED display. + * + * @param sda GPIO number for SDA (default: GPIO_NUM_5) + * @param scl GPIO number for SCL (default: GPIO_NUM_6) + * @param col_offset Internal column offset (default: OLED_COLUMN_OFFSET) + * @return esp_err_t ESP_OK on success + */ +esp_err_t oled_init(gpio_num_t sda, gpio_num_t scl, uint8_t col_offset); + +/** + * @brief Clear the frame buffer (all black). + */ +void oled_clear(void); + +/** + * @brief Fill the entire frame buffer (all white). + */ +void oled_fill(void); + +/** + * @brief Push the frame buffer to the OLED display over I2C. + * + * @return esp_err_t ESP_OK on success + */ +esp_err_t oled_update(void); + +/** + * @brief Set display contrast. + * + * @param contrast Value 0 to 255 + * @return esp_err_t ESP_OK on success + */ +esp_err_t oled_set_contrast(uint8_t contrast); + +/** + * @brief Invert the display colors in hardware. + * + * @param invert true for inverted, false for normal + * @return esp_err_t ESP_OK on success + */ +esp_err_t oled_invert(bool invert); + +/** + * @brief Draw a single pixel in the frame buffer. + * + * @param x X coordinate (0 to 71) + * @param y Y coordinate (0 to 39) + * @param color OLED_COLOR_BLACK, OLED_COLOR_WHITE, or OLED_COLOR_INVERT + */ +void oled_draw_pixel(int16_t x, int16_t y, uint8_t color); + +/** + * @brief Draw a line using Bresenham's algorithm. + */ +void oled_draw_line(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color); + +/** + * @brief Draw an unfilled rectangle. + */ +void oled_draw_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color); + +/** + * @brief Draw a filled rectangle. + */ +void oled_fill_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t color); + +/** + * @brief Draw a single character using the 5x7 font. + * + * @param x X position (top-left) + * @param y Y position (top-left) + * @param c ASCII character + * @param color OLED_COLOR_WHITE or OLED_COLOR_BLACK + */ +void oled_draw_char(int16_t x, int16_t y, char c, uint8_t color); + +/** + * @brief Draw a text string using the 5x7 font. + * + * @param x X position (top-left) + * @param y Y position (top-left) + * @param str Null-terminated string + * @param color OLED_COLOR_WHITE or OLED_COLOR_BLACK + */ +void oled_draw_string(int16_t x, int16_t y, const char *str, uint8_t color); + +#ifdef __cplusplus +} +#endif + +#endif // OLED_DISPLAY_H + diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..c124d7b --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,19 @@ +# Target ESP32-C3 +CONFIG_IDF_TARGET="esp32c3" + +# CPU Frequency 160MHz +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160 + +# Enable USB Serial/JTAG console for direct USB-C flashing and serial monitor +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y + +# FreeRTOS configuration +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_USE_TRACE_FACILITY=y +CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y + +# Compiler optimization +CONFIG_COMPILER_OPTIMIZATION_DEFAULT=y +