Initial Demo OS
This commit is contained in:
+16
@@ -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-*/
|
||||||
|
|
||||||
Vendored
+19
@@ -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"
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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 <id>, 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
|
||||||
|
```
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
idf_component_register(
|
||||||
|
SRCS
|
||||||
|
"sys_event.c"
|
||||||
|
"sys_status.c"
|
||||||
|
INCLUDE_DIRS
|
||||||
|
"include"
|
||||||
|
REQUIRES
|
||||||
|
freertos
|
||||||
|
esp_timer
|
||||||
|
esp_system
|
||||||
|
log
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#include "apps.h"
|
||||||
|
#include "os_gfx.h"
|
||||||
|
#include "os_status.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
#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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#include "apps.h"
|
||||||
|
#include "os_gfx.h"
|
||||||
|
#include "os_status.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#include "apps.h"
|
||||||
|
#include "os_gfx.h"
|
||||||
|
#include "os_status.h"
|
||||||
|
#include "os_oled.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#include "apps.h"
|
||||||
|
#include "os_gfx.h"
|
||||||
|
#include "os_status.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#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,
|
||||||
|
};
|
||||||
@@ -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
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
#ifndef FONT_5X7_H
|
||||||
|
#define FONT_5X7_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#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.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
#include "oled_display.h"
|
||||||
|
#include "font_5x7.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
#ifndef OLED_DISPLAY_H
|
||||||
|
#define OLED_DISPLAY_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
Reference in New Issue
Block a user