#include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_vendor.h" #include "esp_lcd_panel_ops.h" #include "driver/gpio.h" #include "driver/spi_master.h" #include "esp_err.h" #include "esp_log.h" #include "esp_timer.h" #include "lvgl.h" #include "esp_lvgl_port.h" #include "gui.h" #include "gpio.h" #include "keypad.h" #include "bubble.h" #include "system.h" #define DEVKIT #undef DEVKIT static const char *TAG = "gui"; #define UNLOCK() lvgl_port_unlock() #define LOCK() lvgl_port_lock(0) // LCD Pin Configuration #define LCD_PIXEL_CLOCK_HZ (5 * 1000 * 1000) #define LCD_SPI_HOST SPI2_HOST #define PIN_NUM_nON 26 // ST7735S properties #define LCD_H_RES 160 #define LCD_V_RES 80 #define LCD_CMD_BITS 8 #define LCD_PARAM_BITS 8 #define LCD_COLOR_SPACE ESP_LCD_COLOR_SPACE_RGB #define LCD_BITS_PER_PIXEL 16 static esp_lcd_panel_handle_t panel_handle = NULL; esp_lcd_panel_io_handle_t io_handle = NULL; static lv_disp_t *disp = NULL; static lv_obj_t *imu_label = NULL; // Label for IMU data static lv_style_t style_mono8; typedef struct { int selected; int count; lv_obj_t *obj; } menu_context_t; static void gui_task(void *pvParameters); static void createBubble(lv_obj_t * scr); static void build_scrollable_menu(void); static void currentFocusIndex(menu_context_t *ctx); #define MAX_ITEMS 10 #define VISIBLE_ITEMS 3 #define MENU_MAX_STRING_LENGTH 30 static const char *menu_items[MAX_ITEMS] = { "V-Moda Crossfade", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10" }; static lv_obj_t *list; static lv_obj_t *buttons[MAX_ITEMS]; static int selected_index = 0; static lv_obj_t *_bubble = NULL; static lv_obj_t * _menu = NULL; static lv_obj_t *_currentPage = NULL; static GuiMode_t _mode = GUI_BUBBLE; /* 1. Prepare a default (unfocused) style */ static lv_style_t _styleUnfocusedBtn; /* 2. Prepare a focus style */ static lv_style_t _styleFocusedBtn; static menu_context_t _menuContext; static bool notify_lvgl_flush_ready(void *user_ctx) { if (disp) { lv_display_flush_ready(disp); } return true; } static void create_lvgl_demo(void) { lvgl_port_lock(0); // Create a screen with black background lv_obj_t *scr = lv_scr_act(); createBubble(scr); //build_scrollable_menu(); lvgl_port_unlock(); } static void lcd_init(void) { ESP_LOGI(TAG, "Initialize SPI bus"); spi_bus_config_t buscfg = { .sclk_io_num = PIN_NUM_CLK, .mosi_io_num = PIN_NUM_MOSI, .miso_io_num = -1, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = LCD_H_RES * LCD_V_RES * 2 }; ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO)); ESP_LOGI(TAG, "Install panel IO"); esp_lcd_panel_io_spi_config_t io_config = { .dc_gpio_num = PIN_NUM_DC, .cs_gpio_num = PIN_NUM_CS, .pclk_hz = LCD_PIXEL_CLOCK_HZ, .lcd_cmd_bits = LCD_CMD_BITS, .lcd_param_bits = LCD_PARAM_BITS, .spi_mode = 3, .trans_queue_depth = 10, .user_ctx = NULL, }; ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_SPI_HOST, &io_config, &io_handle)); esp_lcd_panel_dev_config_t panel_config = { .reset_gpio_num = PIN_NUM_RST, .rgb_endian = LCD_RGB_ENDIAN_BGR, .bits_per_pixel = LCD_BITS_PER_PIXEL, }; ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle)); ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true)); ESP_ERROR_CHECK(esp_lcd_panel_set_gap(panel_handle, 1, 26)); // ST7735S typically needs these offsets ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); } static void lvgl_init(void) { lv_init(); #if 1 const lvgl_port_cfg_t lvgl_cfg = { .task_priority = 4, // LVGL task priority .task_stack = 16384, // LVGL task stack size .task_affinity = 0, // LVGL task can run on any core .task_max_sleep_ms = 500, // Maximum sleep in LVGL task .timer_period_ms = 5 // LVGL timer period }; ESP_ERROR_CHECK(lvgl_port_init(&lvgl_cfg)); #endif const lvgl_port_display_cfg_t disp_cfg = { .io_handle = io_handle, .panel_handle = panel_handle, .buffer_size = LCD_H_RES * LCD_V_RES * 2, .double_buffer = false, .hres = LCD_H_RES, .vres = LCD_V_RES, .monochrome = false, .rotation = { .swap_xy = true, .mirror_x = true, .mirror_y = false, }, .flags = { //.buff_dma = true, .swap_bytes = true, } }; disp = lvgl_port_add_disp(&disp_cfg); //lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565); } static void createBubble(lv_obj_t * scr) { // 2) Create a bubble level of size 200×60, with range [−30°, +30°], initial 0°: lv_obj_t * level = bubble_create(scr, 150, 40, -10.0f, +10.0f, 0.0f); lv_obj_align(level, LV_ALIGN_CENTER, 0, 0); // 3) … Later, when you read your accelerometer or keypad‐derived angle … float new_angle = 10.0f; bubble_setValue(level, new_angle); // 4) You can call bubble_level_set_value(level, …) as often as you like. // Each call invalidates the object and LVGL will call the draw callback // (usually on the next tick, or immediately if LVGL is idle). _bubble = level; } void gui_start(void) { // Initialize LCD lcd_init(); // Initialize LVGL lvgl_init(); // Create UI create_lvgl_demo(); keypad_start(); gpio_set_level(PIN_NUM_BK_LIGHT, 1); xTaskCreate(gui_task, "gui_task", 4096, NULL, 5, NULL); } static uint32_t waitForKeyPress(void) { QueueHandle_t q = keypad_getQueue(); uint32_t ev = 0; if (xQueueReceive(q, &ev, portMAX_DELAY) == pdTRUE) { return ev; } return 0; } static void handleMainMenu(void) { lvgl_port_lock(0); // Create a screen with black background lv_obj_t *scr = lv_scr_act(); //create_menu(scr); lvgl_port_unlock(); } // ───── MENU CONFIG ───── #define ROW_H 20 // height of each row static const char * items[] = { // your menu entries "VMode Crossmade", "Second Choice", "Another Option", "Yet Another", "Yet Another", "Last One" }; #define ITEM_COUNT (sizeof(items)/sizeof(items[0])) // ───── STATE & HELPERS ───── static lv_obj_t * btn_array[ITEM_COUNT]; static int selected_idx = 0; // Called whenever a button is “activated” (Enter/Click) static void btn_click_cb(lv_event_t * e) { lv_obj_t * btn = lv_event_get_target(e); const char * txt = lv_label_get_text(lv_obj_get_child(btn, 0)); ESP_LOGI(TAG, "Activated: %s\n", txt); } // Repaint all rows so only btn_array[selected_idx] is highlighted static void refresh_highlight(void) { return; lvgl_port_lock(0); int index = 0; lv_obj_t *page = _currentPage; lv_obj_t * child = NULL; lv_obj_t * next = lv_obj_get_child(page, index); lv_obj_t * selected = NULL; while(next) { child = next; ESP_LOGI(TAG, "Child: %p", child); if (lv_obj_has_flag(child, LV_OBJ_FLAG_USER_1)) { lv_obj_set_style_bg_color(child, lv_color_hex(0xFF8800), 0); lv_obj_set_style_text_color(lv_obj_get_child(child,0), lv_color_white(), 0); selected = child; ESP_LOGI(TAG, "Selected"); } else { lv_obj_set_style_bg_color(child, lv_color_white(), 0); lv_obj_set_style_text_color(lv_obj_get_child(child,0), lv_color_black(), 0); } index++; next = lv_obj_get_child(page, index); } if (selected) { lv_obj_scroll_to_view(selected, LV_ANIM_ON); } lvgl_port_unlock(); } static void menuInc(int inc) { LOCK(); currentFocusIndex(&_menuContext); ESP_LOGI(TAG, "Current Index: %d", _menuContext.selected); lv_obj_t *next = NULL; // check if we are at the first or last in the page lv_obj_t *test = lv_obj_get_child(_currentPage, (inc > 0 ? -1 : 0)); if (_menuContext.obj != test) { next = lv_obj_get_child(_currentPage, _menuContext.selected + inc); lv_obj_add_state(next, LV_STATE_FOCUSED); lv_obj_clear_state(_menuContext.obj, LV_STATE_FOCUSED); lv_obj_scroll_to_view(next, LV_ANIM_ON); _menuContext.obj = next; _menuContext.selected += inc; } UNLOCK(); } static void menuNext(void) { menuInc(1); } static void menuPrevious(void) { menuInc(-1); } // Fire the “clicked” event on the selected row static void activate_selected(void) { LOCK(); lv_obj_send_event(_menuContext.obj, LV_EVENT_CLICKED, NULL); UNLOCK(); } static lv_obj_t * addMenuItem(lv_obj_t *page, const char *text) { lv_obj_t * btn = lv_btn_create(page); lv_obj_set_size(btn, LV_PCT(100), ROW_H); lv_obj_add_style(btn, &_styleUnfocusedBtn, LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_add_style(btn, &_styleFocusedBtn, LV_PART_MAIN | LV_STATE_FOCUSED); lv_obj_add_state(btn, LV_STATE_DEFAULT); // style it just like your old list // lv_obj_set_style_bg_color(btn, lv_color_white(), 0); lv_obj_set_style_radius(btn, 0, LV_PART_MAIN); lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN); lv_obj_clear_flag(btn, LV_OBJ_FLAG_SCROLLABLE); // label & center lv_obj_t * lbl = lv_label_create(btn); lv_label_set_text(lbl, text); lv_obj_set_style_radius(lbl, 0, LV_PART_MAIN | LV_STATE_ANY); lv_obj_set_style_border_width(lbl, 0, LV_PART_MAIN | LV_STATE_ANY); // click callback lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, NULL); return btn; } static int getSelectedIndex(lv_obj_t *page) { return -1; } static void currentFocusIndex(menu_context_t *ctx) { ctx->count = lv_obj_get_child_cnt(_currentPage); ctx->obj = NULL; ctx->selected = -1; // return the index of the currently focused object for(int i = 0; i < ctx->count; i++) { lv_obj_t * child = lv_obj_get_child(_currentPage, i); if (lv_obj_has_state(child, LV_STATE_FOCUSED)) { ctx->obj = child; ctx->selected = i; return; } } } // ───── BUILD THE MENU ───── static void build_scrollable_menu(void) { lv_style_init(&_styleFocusedBtn); lv_style_init(&_styleUnfocusedBtn); lv_style_set_bg_color(&_styleUnfocusedBtn, lv_color_make(0xff,0xff,0xff)); // gray bg lv_style_set_text_color(&_styleUnfocusedBtn, lv_color_hex(0xFF8800)); lv_style_set_bg_color(&_styleFocusedBtn, lv_color_make(0x33,0x99,0xFF)); // blue bg lv_style_set_text_color(&_styleUnfocusedBtn,lv_color_black()); // 2) Inside it, create the lv_menu and hide its sidebar/header lv_obj_t *menu = lv_menu_create(lv_scr_act()); _menu = menu; lv_obj_set_style_radius(menu, 0, LV_PART_MAIN | LV_STATE_ANY); lv_obj_set_style_border_width(menu, 0, LV_PART_MAIN | LV_STATE_ANY); lv_obj_set_size(menu, lv_pct(100), lv_pct(100)); lv_obj_center(menu); lv_obj_set_scrollbar_mode(menu, LV_SCROLLBAR_MODE_AUTO); lv_obj_t * main = lv_menu_page_create(menu, NULL); lv_obj_set_style_radius(main, 0, LV_PART_MAIN | LV_STATE_ANY); lv_obj_set_style_border_width(main, 0, LV_PART_MAIN | LV_STATE_ANY); lv_obj_set_scrollbar_mode(main, LV_SCROLLBAR_MODE_AUTO); lv_obj_set_size(main, lv_pct(100), lv_pct(100)); lv_menu_set_page(menu, main); lv_obj_t * tmpObj; lv_obj_t * calMenu = lv_menu_page_create(menu, NULL); lv_obj_set_style_radius(calMenu, 0, LV_PART_MAIN | LV_STATE_ANY); lv_obj_set_style_border_width(calMenu, 0, LV_PART_MAIN | LV_STATE_ANY); lv_obj_set_scrollbar_mode(calMenu, LV_SCROLLBAR_MODE_AUTO); tmpObj = addMenuItem(main, "Bluetooth"); lv_obj_add_state(tmpObj, LV_STATE_FOCUSED); //lv_obj_add_flag(tmpObj, LV_OBJ_FLAG_USER_1); tmpObj = addMenuItem(main, "Calibration"); lv_menu_set_load_page_event(menu, tmpObj, calMenu); tmpObj = addMenuItem(main, "Volume"); addMenuItem(main, "About"); addMenuItem(main, "Exit"); addMenuItem(calMenu, "Calibrate Level"); addMenuItem(calMenu, "Reset Calibration"); addMenuItem(calMenu, "Exit"); _currentPage = main; // 6) Initial highlight selected_idx = 0; refresh_highlight(); } static void gui_task(void *pvParameters) { (void)pvParameters; // Unused parameter system_subscribe(xTaskGetCurrentTaskHandle()); // Grab queue handle QueueHandle_t q = keypad_getQueue(); uint32_t ev = 0; LOCK(); // _mode = GUI_MENU; // lv_obj_remove_flag(_menu, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(_bubble, LV_OBJ_FLAG_HIDDEN); //lv_obj_add_flag(_menu, LV_OBJ_FLAG_HIDDEN); UNLOCK(); ESP_LOGI(TAG, "Start GUI Task..."); while (1) { if (xQueueReceive(q, &ev, pdMS_TO_TICKS(10)) == pdTRUE) { switch (ev) { case (KEY_UP << KEY_LONG_PRESS): { system_setZeroAngle(); break; } case (KEY_DOWN << KEY_LONG_PRESS): { system_clearZeroAngle(); break; } #if 0 case (KEY0 << KEY_SHORT_PRESS): if (_mode == GUI_MENU) { menuNext(); } ESP_LOGI(TAG, "MAIN: Button 1 SHORT"); break; case (KEY0 << KEY_LONG_PRESS): { if (_mode != GUI_MENU) { _mode = GUI_MENU; lv_obj_remove_flag(_menu, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(_bubble, LV_OBJ_FLAG_HIDDEN); } else if (_mode == GUI_MENU) { activate_selected(); _mode = GUI_BUBBLE; // lv_obj_remove_flag(_bubble, LV_OBJ_FLAG_HIDDEN); // lv_obj_add_flag(_menu, LV_OBJ_FLAG_HIDDEN); } break; } case (KEY1 << KEY_SHORT_PRESS): { if (_mode == GUI_MENU) { menuPrevious(); } break; } case (KEY1 << KEY_LONG_PRESS): ESP_LOGI(TAG, "MAIN: Button 2 LONG"); gpio_set_level(PIN_NUM_nON, 0); break; #endif default: break; } } if (_mode == GUI_BUBBLE) { uint32_t notifiedBits = 0; // clear on entry (first param), wait for any bit, block forever xTaskNotifyWait( 0xFFFFFFFF, // clear any old bits on entry 0xFFFFFFFF, // clear bits on exit ¬ifiedBits, pdMS_TO_TICKS(100)); if (notifiedBits & EM_EVENT_NEW_DATA) { ImuData_t d = system_getImuData(); bubble_setValue(_bubble, -d.angle); } } } }