/*############################################################################# ## Author: Shaun Reed ## ## Legal: All Content (c) 2025 Shaun Reed, all rights reserved ## ## ## ## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com ## ############################################################################## */ #ifndef DISPLAY_H #define DISPLAY_H #include #include #include "i2c.h" #include "panel_device.h" #include #include struct LCD { esp_lcd_panel_handle_t esp_panel_; esp_lcd_panel_io_handle_t esp_io_; /// LVGL display handle. lv_display_t* lv_display_; lv_array_t lv_objs_; struct IPanelDevice* device_; }; /** * Construct a new LCD using an object that implements IPanelDevice. * * @param device An object that implements the IPanelDevice interface. */ static struct LCD LCD_init(struct IPanelDevice* device) { struct LCD display; ESP_LOGI(LCD_TAG, "Creating panel IO handle"); display.esp_io_ = NULL; esp_lcd_panel_dev_config_t panel_config = { .reset_gpio_num = (gpio_num_t)(-1), .bits_per_pixel = 1, .vendor_config = device->vendor_config_cb(), }; esp_lcd_panel_io_i2c_config_t io_config = { .dev_addr = I2C_HW_ADDR, // User data to pass to the LVGL flush_ready callback. // See IPanelDevice::lvgl_flush_ready_cb .user_ctx = NULL, .control_phase_bytes = 1, .dc_bit_offset = 6, .lcd_cmd_bits = LCD_CMD_BITS, .lcd_param_bits = LCD_PARAM_BITS, .scl_speed_hz = LCD_PIXEL_CLOCK_HZ, }; ESP_ERROR_CHECK( esp_lcd_new_panel_io_i2c(I2C_get(), &io_config, &display.esp_io_)); // If the passed handle is already allocated, delete it. if (display.esp_panel_ != NULL) { ESP_LOGI(LCD_TAG, "Removing unused panel"); esp_lcd_panel_del(display.esp_panel_); } ESP_LOGI(LCD_TAG, "Installing vendor panel driver"); device->init_panel_cb(&panel_config, display.esp_io_, &display.esp_panel_); ESP_LOGI(LCD_TAG, "Resetting panel display"); ESP_ERROR_CHECK(esp_lcd_panel_reset(display.esp_panel_)); ESP_LOGI(LCD_TAG, "Initializing panel display"); ESP_ERROR_CHECK(esp_lcd_panel_init(display.esp_panel_)); ESP_LOGI(LCD_TAG, "Turning on panel display"); ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(display.esp_panel_, true)); if (!lv_is_initialized()) { ESP_LOGI(LCD_TAG, "Initialize LVGL"); lv_init(); } ESP_LOGI(LCD_TAG, "Creating LVGL display"); display.lv_display_ = lv_display_create(device->width_, device->height_); assert(display.lv_display_); // associate the i2c panel handle to the display lv_display_set_user_data(display.lv_display_, display.esp_panel_); device->register_rendering_data_cb(display.lv_display_, display.esp_io_, device->lv_buf_, device->lv_buf_size_); device->register_lvgl_tick_timer_cb(); ESP_LOGI(LCD_TAG, "Initializing LVGL array"); lv_array_init(&display.lv_objs_, 1, sizeof(lv_obj_t*)); return display; } /** * Create a LVGL label with some given text on the current display. * The name of the object can be reused to change text on this label later. * * @param display * @param text Text to write to the display. * @param long_mode LVGL long mode for text wider than the current display. * @param align LVGL alignment to use for placing the label on the display. * @return The index of the inserted label on the LVGL screen */ static uint32_t LCD_set_text_with_mode(struct LCD* display, const char* text, lv_label_long_mode_t long_mode, lv_align_t align) { // Lock the mutex due to the LVGL APIs are not thread-safe. _lock_acquire(&lv_lock_); ESP_LOGI(LCD_TAG, "Setting new text: %s", text); lv_obj_t* scr = lv_display_get_screen_active(display->lv_display_); lv_obj_t* new_object = lv_label_create(scr); // Set text and long mode. lv_label_set_long_mode(new_object, long_mode); lv_label_set_text(new_object, text); // Set the size of the screen. // If you use rotation 90 or 270 use lv_display_get_vertical_resolution. lv_obj_set_width( new_object, lv_display_get_horizontal_resolution(display->lv_display_)); lv_obj_align(new_object, align, 0, 0); uint32_t index = lv_array_size(&display->lv_objs_); if (lv_array_push_back(&display->lv_objs_, &new_object) != LV_RESULT_OK) { ESP_LOGE(LCD_TAG, "Failed to add new object to array"); } _lock_release(&lv_lock_); return 1; } /** * Create a LVGL label with some given text on the current display. * The name of the object can be reused to change text on this label later. * * @param display * @param text Text to write to the display. * @return The index of the inserted label on the LVGL screen */ static uint32_t LCD_set_text(struct LCD* display, const char* text) { return LCD_set_text_with_mode(display, text, LV_LABEL_LONG_SCROLL_CIRCULAR, LV_ALIGN_TOP_MID); } static void LCD_set_text_at_with_mode(struct LCD* display, const char* text, uint32_t i, lv_label_long_mode_t long_mode, lv_align_t align) { // Lock the mutex due to the LVGL APIs are not thread-safe. _lock_acquire(&lv_lock_); if (lv_array_is_empty(&display->lv_objs_)) { ESP_LOGI(LCD_TAG, "Cannot set text at index %d; The array is empty."); _lock_release(&lv_lock_); return; } lv_obj_t** ptr = (lv_obj_t**)lv_array_at(&display->lv_objs_, i); lv_obj_t* label = (lv_obj_t*)*ptr; if (label == NULL) { ESP_LOGE(LCD_TAG, "Failed to set text at index %d; Label is null", i); _lock_release(&lv_lock_); return; } lv_label_set_text(label, text); ESP_LOGI(LCD_TAG, "Setting text at index %d:", i); // Set text and long mode. lv_label_set_long_mode(label, long_mode); lv_label_set_text(label, text); // Set the size of the screen. // If you use rotation 90 or 270 use lv_display_get_vertical_resolution. lv_obj_set_width( label, lv_display_get_horizontal_resolution(display->lv_display_)); lv_obj_align(label, align, 0, 0); _lock_release(&lv_lock_); } static void LCD_set_text_at(struct LCD* display, const char* text, uint32_t i) { // Lock the mutex due to the LVGL APIs are not thread-safe. _lock_acquire(&lv_lock_); if (lv_array_is_empty(&display->lv_objs_)) { ESP_LOGI(LCD_TAG, "Cannot set text at index %d; The array is empty."); _lock_release(&lv_lock_); return; } lv_obj_t** ptr = (lv_obj_t**)lv_array_at(&display->lv_objs_, i); lv_obj_t* label = (lv_obj_t*)*ptr; if (label == NULL) { ESP_LOGE(LCD_TAG, "Failed to set text at index %d; Label is null", i); _lock_release(&lv_lock_); return; } lv_label_set_text(label, text); _lock_release(&lv_lock_); } static void LCD_clear(struct LCD* display) { _lock_acquire(&lv_lock_); uint32_t size = lv_array_size(&display->lv_objs_); ESP_LOGI(LCD_TAG, "Clearing %d LVGL objects", size); for (uint32_t i = 0; i < size && size > 0; i++) { ESP_LOGI(LCD_TAG, "Checking array index %d", i); lv_obj_t** ptr_to_delete = (lv_obj_t**)lv_array_at(&display->lv_objs_, i); lv_obj_t* to_delete = (lv_obj_t*)*ptr_to_delete; if (to_delete == NULL) { ESP_LOGE(LCD_TAG, "Failed to clear all LVGL objects"); continue; } if (lv_obj_is_valid(to_delete)) { ESP_LOGI(LCD_TAG, "Removing LVGL object"); lv_label_set_text(to_delete, "test"); lv_obj_delete(to_delete); } else { ESP_LOGE(LCD_TAG, "Error: LVGL object is not valid"); } } lv_array_clear(&display->lv_objs_); _lock_release(&lv_lock_); } #endif // DISPLAY_H