diff --git a/esp/cpp/07_lcd-panel/main/CMakeLists.txt b/esp/cpp/07_lcd-panel/main/CMakeLists.txt index 5032c42..7726d57 100644 --- a/esp/cpp/07_lcd-panel/main/CMakeLists.txt +++ b/esp/cpp/07_lcd-panel/main/CMakeLists.txt @@ -1,5 +1 @@ -idf_component_register( - SRCS main.cpp i2c.h display.cpp panel.cpp panel_device.cpp ssd1306.cpp - INCLUDE_DIRS . - REQUIRES driver -) +idf_component_register(SRC_DIRS . INCLUDE_DIRS .) diff --git a/esp/cpp/07_lcd-panel/main/display.cpp b/esp/cpp/07_lcd-panel/main/display.cpp index 38bd976..45956ea 100644 --- a/esp/cpp/07_lcd-panel/main/display.cpp +++ b/esp/cpp/07_lcd-panel/main/display.cpp @@ -6,6 +6,8 @@ #include #include "display.h" + +// TODO: Remove this dependency by relocating SSD1306::oledb_buffer_ #include "ssd1306.h" // LVGL library is not thread-safe, this example calls LVGL APIs from tasks. @@ -13,7 +15,7 @@ _lock_t Display::ScopedLock::lv_lock_; // Static TimeKeeper for managing ESP timers across all displays. -Display::TimeKeeper Display::timers_; +TimeKeeper Display::timers_; Display::Display(IPanelDevice &device) : panel_(device), @@ -47,10 +49,14 @@ void Display::set_text(const char *text, ESP_LOGI(TAG, "Display LVGL Scroll Text"); lv_obj_t *scr = lv_display_get_screen_active(lv_display_); - lv_objects_[name] = lv_label_create(scr); + + // Create the label if it's `name` doesn't already exist in the map keys. + if (!lv_objects_.count(name)) { + lv_objects_[name] = lv_label_create(scr); + } + auto obj = lv_objects_[name]; // Circular scroll. - auto obj = lv_objects_[name]; lv_label_set_long_mode(obj, long_mode); lv_label_set_text(obj, text); @@ -60,9 +66,9 @@ void Display::set_text(const char *text, lv_obj_align(obj, align, 0, 0); } -bool Display::lvgl_flush_ready(esp_lcd_panel_io_handle_t, - esp_lcd_panel_io_event_data_t *, - void *user_ctx) +bool Display::lvgl_flush_ready_cb(esp_lcd_panel_io_handle_t, + esp_lcd_panel_io_event_data_t *, + void *user_ctx) { auto *disp = (lv_display_t *) user_ctx; lv_display_flush_ready(disp); @@ -114,7 +120,7 @@ void Display::lvgl_flush_cb(lv_display_t *display, const lv_area_t *area, SSD1306::oled_buffer_)); } -void Display::lvgl_increase_tick(void *) +void Display::lvgl_increase_tick_cb(void *) { // Tell LVGL how many milliseconds has elapsed lv_tick_inc(LVGL_TICK_PERIOD_MS); @@ -152,7 +158,7 @@ void Display::register_draw_buffer() ESP_LOGI(TAG, "Register io panel callback for LVGL flush ready notification"); const esp_lcd_panel_io_callbacks_t cbs = { - .on_color_trans_done = Display::lvgl_flush_ready, + .on_color_trans_done = Display::lvgl_flush_ready_cb, }; ESP_ERROR_CHECK( esp_lcd_panel_io_register_event_callbacks(panel_.esp_io_, &cbs, @@ -163,7 +169,7 @@ void Display::register_lvgl_tick_timer() { ESP_LOGI(TAG, "Use esp_timer to increase LVGL tick"); const esp_timer_create_args_t esp_timer_args = { - .callback = &Display::lvgl_increase_tick, + .callback = &Display::lvgl_increase_tick_cb, .name = "lvgl_tick" }; timers_.start_new_timer_periodic(esp_timer_args, LVGL_TICK_PERIOD_MS * 1000); diff --git a/esp/cpp/07_lcd-panel/main/display.h b/esp/cpp/07_lcd-panel/main/display.h index 62971fd..6606a60 100644 --- a/esp/cpp/07_lcd-panel/main/display.h +++ b/esp/cpp/07_lcd-panel/main/display.h @@ -1,13 +1,12 @@ #ifndef DISPLAY_H #define DISPLAY_H -#include #include #include -#include +#include "time_keeper.h" #include "panel.h" #define LVGL_TICK_PERIOD_MS 5 @@ -15,148 +14,125 @@ #define LVGL_TASK_PRIORITY 2 class Display { - struct ScopedLock { - explicit ScopedLock() { _lock_acquire(&lv_lock_); } - - ~ScopedLock() { _lock_release(&lv_lock_); } - - // LVGL library is not thread-safe, this example calls LVGL APIs from tasks. - // We must use a mutex to protect it. - static _lock_t lv_lock_; - }; - - struct Timer { - explicit Timer(esp_timer_create_args_t args) : args_(args) - { - ESP_LOGI(TAG, "Creating esp_timer with name: %s", args.name); - ESP_ERROR_CHECK(esp_timer_create(&args, &esp_timer_)); - } - - ~Timer() - { - ESP_ERROR_CHECK(esp_timer_delete(esp_timer_)); - } - - - [[maybe_unused]] esp_timer_create_args_t args_{}; - esp_timer_handle_t esp_timer_ = nullptr; - }; - - struct TimeKeeper { - using TimerHandle = Timer *; - - // Timers should only be accessed using this method. - // For this reason managed_timers_ is private. - TimerHandle get_handle(const char *name) - { - return &managed_timers_.at(name); - } - - TimerHandle operator[](const char *name) { return get_handle(name); } - - TimerHandle create_timer(esp_timer_create_args_t args) - { - auto rt = managed_timers_.emplace(args.name, args); - if (!rt.second) { - ESP_LOGE(TAG, "Failed to insert timer into Display::managed_timers_"); - return nullptr; - } - return &rt.first->second; - } - - [[maybe_unused]] void stop_timer(const char *name) - { - ESP_ERROR_CHECK(esp_timer_stop(get_handle(name)->esp_timer_)); - } - - [[maybe_unused]] void delete_timer(const char *name) - { - managed_timers_.erase(name); - } - - [[maybe_unused]] void - start_new_timer_periodic(esp_timer_create_args_t args, - uint64_t period) - { - start_timer_periodic(create_timer(args)->args_.name, period); - } - - [[maybe_unused]] void start_timer_periodic(const char *name, - uint64_t period) - { - ESP_ERROR_CHECK( - esp_timer_start_periodic(get_handle(name)->esp_timer_, period)); - } - - [[maybe_unused]] void - start_new_timer_once(esp_timer_create_args_t args, - uint64_t timeout_us) - { - start_timer_once(create_timer(args)->args_.name, timeout_us); - } - - [[maybe_unused]] void start_timer_once(const char *name, - uint64_t timeout_us) - { - ESP_ERROR_CHECK( - esp_timer_start_once(get_handle(name)->esp_timer_, timeout_us)); - } - - private: - - // Timers should only be accessed using the get_handle method. - // ~Timer() will delete the timer if called. - std::unordered_map managed_timers_; - }; - public: + // + // CONSTRUCTORS + + Display(const Display &) = delete; + + Display(Display &) = delete; + + Display &operator=(Display &) = delete; + + /** + * Construct a new Display using an IPanelDevice. + * + * @param device An object that implements the IPanelDevice interface. + */ explicit Display(IPanelDevice &device); ~Display() = default; + // + // GETTERS + + /** + * Getter for accessing LVGL display handle. + * + * @sa ScopedLock for calling custom LVGL API's not implemented by Display. + */ [[nodiscard]] inline const lv_display_t *get() const { return lv_display_; } + /** + * Getter for accessing LVGL display handle. + * + * @sa ScopedLock for calling custom LVGL API's not implemented by Display. + */ [[nodiscard]] inline lv_display_t *get() { return lv_display_; } + /// Dereference operator for accessing LVGL display handle. [[nodiscard]] inline const lv_display_t *operator*() const { return get(); } + /// Dereference operator for accessing LVGL display handle. [[nodiscard]] inline lv_display_t *operator*() { return get(); } + // + // LVGL OPERATIONS + + /** + * 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 text Text to write to the display. + * @param name Name for the LVGL label object associated with this text. + * @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. + */ void set_text(const char *text, const char *name, lv_label_long_mode_t long_mode = LV_LABEL_LONG_SCROLL_CIRCULAR, lv_align_t align = LV_ALIGN_TOP_MID); - static bool lvgl_flush_ready(esp_lcd_panel_io_handle_t panel, - esp_lcd_panel_io_event_data_t *data, - void *user_ctx); + // + // TYPE DEFINITIONS + + /** + * Obtains LVGL API mutex lock for the duration of local scope. + * + * LVGL library is not thread-safe, this example calls LVGL APIs from tasks. + */ + struct ScopedLock { + explicit ScopedLock() { _lock_acquire(&lv_lock_); } + + ~ScopedLock() { _lock_release(&lv_lock_); } + + /// Mutex used to protect LVGL API calls. + static _lock_t lv_lock_; + }; + + // + // PUBLIC STATIC MEMBERS + + /// Public static TimeKeeper for managing ESP timers across all displays. + static TimeKeeper timers_; + +private: + /// Registers LVGL draw buffers for this display. + void register_draw_buffer(); + + /// Registers LVGL ticker timer callback for rendering this display. + static void register_lvgl_tick_timer(); + + static bool lvgl_flush_ready_cb(esp_lcd_panel_io_handle_t panel, + esp_lcd_panel_io_event_data_t *data, + void *user_ctx); static void lvgl_flush_cb(lv_display_t *display, const lv_area_t *area, uint8_t *px_map); - static void lvgl_increase_tick(void *arg); + static void lvgl_increase_tick_cb(void *arg); [[noreturn]] static void lvgl_port_task(void *arg); - // Public static TimeKeeper for managing ESP timers across all displays. - static TimeKeeper timers_; - -private: - void register_draw_buffer(); - - static void register_lvgl_tick_timer(); + // + // PRIVATE MEMBERS + /// Panel associated with this Display. Panel panel_; + /// LVGL display handle. lv_display_t *lv_display_; - // Draw buffer associated with the lv_display_t. + /// LVGL draw buffer associated with this Display's lv_display_t. void *lv_buf_; - // Objects stored in the screen associated with this display. - // @sa Display::set_text - // @sa lv_display_get_screen_active + /** + * LVGL object handles stored in the LVGL screen associated with this Display. + * + * @sa Display::set_text + * @sa lv_display_get_screen_active + */ std::unordered_map lv_objects_; }; diff --git a/esp/cpp/07_lcd-panel/main/main.cpp b/esp/cpp/07_lcd-panel/main/main.cpp index 9335442..b07d640 100644 --- a/esp/cpp/07_lcd-panel/main/main.cpp +++ b/esp/cpp/07_lcd-panel/main/main.cpp @@ -18,6 +18,11 @@ void setup() LV_LABEL_LONG_SCROLL, LV_ALIGN_CENTER); + d.set_text("Test test changing text", + "test-text1", + LV_LABEL_LONG_SCROLL, + LV_ALIGN_CENTER); + d.set_text("Hello hello hello hello hello hello hello hello!", "test-text2"); d.set_text("A random sentence with no meaning at all.", diff --git a/esp/cpp/07_lcd-panel/main/main.h b/esp/cpp/07_lcd-panel/main/main.h deleted file mode 100644 index 7c85b38..0000000 --- a/esp/cpp/07_lcd-panel/main/main.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef MAIN_H -#define MAIN_H - -#endif // MAIN_H diff --git a/esp/cpp/07_lcd-panel/main/panel.cpp b/esp/cpp/07_lcd-panel/main/panel.cpp deleted file mode 100644 index 694ab54..0000000 --- a/esp/cpp/07_lcd-panel/main/panel.cpp +++ /dev/null @@ -1,26 +0,0 @@ - -#include "panel.h" - -Panel::Panel(IPanelDevice &device) : - device_(&device), - esp_io_(nullptr), - esp_panel_(nullptr), - esp_panel_config_( - (esp_lcd_panel_dev_config_t) { - .reset_gpio_num = device_->rst_num_, - .bits_per_pixel = 1, - .vendor_config = device_->vendor_config(), - } - ) -{ - esp_io_ = device_->create_io_handle(); - - device_->create_panel(esp_panel_config_, esp_io_, esp_panel_); - - ESP_LOGI(TAG, "Resetting panel display"); - ESP_ERROR_CHECK(esp_lcd_panel_reset(esp_panel_)); - ESP_LOGI(TAG, "Initializing panel display"); - ESP_ERROR_CHECK(esp_lcd_panel_init(esp_panel_)); - ESP_LOGI(TAG, "Turning on panel display"); - ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(esp_panel_, true)); -} diff --git a/esp/cpp/07_lcd-panel/main/panel.h b/esp/cpp/07_lcd-panel/main/panel.h index f1ca325..ae9ef52 100644 --- a/esp/cpp/07_lcd-panel/main/panel.h +++ b/esp/cpp/07_lcd-panel/main/panel.h @@ -5,7 +5,29 @@ class Panel { public: - explicit Panel(IPanelDevice &device); + explicit Panel(IPanelDevice &device) : + device_(&device), + esp_io_(nullptr), + esp_panel_(nullptr), + esp_panel_config_( + (esp_lcd_panel_dev_config_t) { + .reset_gpio_num = device_->rst_num_, + .bits_per_pixel = 1, + .vendor_config = device_->vendor_config(), + } + ) + { + esp_io_ = device_->create_io_handle(); + + device_->create_panel(esp_panel_config_, esp_io_, esp_panel_); + + ESP_LOGI(TAG, "Resetting panel display"); + ESP_ERROR_CHECK(esp_lcd_panel_reset(esp_panel_)); + ESP_LOGI(TAG, "Initializing panel display"); + ESP_ERROR_CHECK(esp_lcd_panel_init(esp_panel_)); + ESP_LOGI(TAG, "Turning on panel display"); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(esp_panel_, true)); + } ~Panel() = default; diff --git a/esp/cpp/07_lcd-panel/main/panel_device.cpp b/esp/cpp/07_lcd-panel/main/panel_device.cpp deleted file mode 100644 index a4a4d96..0000000 --- a/esp/cpp/07_lcd-panel/main/panel_device.cpp +++ /dev/null @@ -1,2 +0,0 @@ - -#include "panel_device.h" diff --git a/esp/cpp/07_lcd-panel/main/ssd1306.cpp b/esp/cpp/07_lcd-panel/main/ssd1306.cpp index 75dab2e..9429023 100644 --- a/esp/cpp/07_lcd-panel/main/ssd1306.cpp +++ b/esp/cpp/07_lcd-panel/main/ssd1306.cpp @@ -2,22 +2,5 @@ #include "ssd1306.h" // To use LV_COLOR_FORMAT_I1 we need an extra buffer to hold the converted data. +// TODO: Remove this and SSD1306 can be header only. uint8_t SSD1306::oled_buffer_[LCD_H_RES * LCD_V_RES / 8]; - -SSD1306::SSD1306(I2C &i2c, - esp_lcd_panel_ssd1306_config_t config, - int width, - int height) : - IPanelDevice(i2c, - (esp_lcd_panel_io_i2c_config_t) { - .dev_addr = I2C_HW_ADDR, - .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, - }, - width, - height - ), - ssd1306_config_(config) { } diff --git a/esp/cpp/07_lcd-panel/main/ssd1306.h b/esp/cpp/07_lcd-panel/main/ssd1306.h index 5c75eab..5998c83 100644 --- a/esp/cpp/07_lcd-panel/main/ssd1306.h +++ b/esp/cpp/07_lcd-panel/main/ssd1306.h @@ -18,12 +18,15 @@ #include "panel_device.h" -// According to SSD1306 datasheet +// According to specific display hardware. // https://www.digikey.com/en/products/detail/winstar-display/WEA012864DWPP3N00003/20533255 #define SCREEN_WIDTH 128 // OLED display width, in pixels. #define SCREEN_HEIGHT 64 // OLED display height, in pixels. -#define LCD_H_RES SCREEN_WIDTH -#define LCD_V_RES SCREEN_HEIGHT + +// According to SSD1306 datasheet. +// https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf +#define LCD_H_RES 128 +#define LCD_V_RES 64 #define I2C_HW_ADDR 0x3C #define LCD_PIXEL_CLOCK_HZ (400 * 1000) // Bit number used to represent command and parameter @@ -40,7 +43,20 @@ public: esp_lcd_panel_ssd1306_config_t config, int width = SCREEN_WIDTH, int height = SCREEN_HEIGHT - ); + ) : + IPanelDevice(i2c, + (esp_lcd_panel_io_i2c_config_t) { + .dev_addr = I2C_HW_ADDR, + .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, + }, + width, + height + ), + ssd1306_config_(config) { } ~SSD1306() final = default; diff --git a/esp/cpp/07_lcd-panel/main/time_keeper.h b/esp/cpp/07_lcd-panel/main/time_keeper.h new file mode 100644 index 0000000..b869009 --- /dev/null +++ b/esp/cpp/07_lcd-panel/main/time_keeper.h @@ -0,0 +1,131 @@ + +#ifndef TIME_KEEPER_H +#define TIME_KEEPER_H + +#include +#include + +#include "i2c.h" + +/** + * Stores arguments and ESP timer handle for a Timer. + * In general Timers should be used via the TimeKeeper interface only. + * + * Timers cannot be copied, and are only created by a TimeKeeper instance. + * The public way to access a Timer is by obtaining a TimerHandle (Timer *). + * The TimeKeeper can delete existing Timers, calling it's destructor. + * The ESP timer will be deleted when this class desctructor is called. + */ +struct Timer { + Timer(const Timer &) = delete; + + Timer(Timer &) = delete; + + Timer &operator=(Timer &) = delete; + + explicit Timer(esp_timer_create_args_t args) : + args_(args), esp_timer_(nullptr) + { + ESP_LOGI(TAG, "Creating esp_timer with name: %s", args_.name); + ESP_ERROR_CHECK(esp_timer_create(&args, &esp_timer_)); + } + + ~Timer() + { + ESP_LOGI(TAG, "Destroying esp_timer with name: %s", args_.name); + ESP_ERROR_CHECK(esp_timer_delete(esp_timer_)); + } + + /// Arguments passed to ESP API during timer creation. + esp_timer_create_args_t args_; + + /// ESP timer handle. + esp_timer_handle_t esp_timer_; +}; + +/** + * ESP timer mananger class. + * + * Timers should only be accessed using the get_handle method. + * If the Timer destructor is called the underlying ESP timer will be deleted. + */ +struct TimeKeeper { + /// Timer handle type used for referring to Timers. + using TimerHandle = Timer *; + + TimerHandle get_handle(const char *name) + { + return &managed_timers_.at(name); + } + + TimerHandle operator[](const char *name) { return get_handle(name); } + + /** + * Create a new managed Timer with the provided ESP arguments. + * The timer can be retrieved later using the args.name field value. + * + * @param args ESP timer creation arguments. + * @return TimerHandle Handle to a Timer managed by this TimeKeeper. + * @sa get_handle + * @sa operator[](const char*) + */ + TimerHandle create_timer(esp_timer_create_args_t args) + { + auto rt = managed_timers_.emplace(args.name, args); + if (!rt.second) { + ESP_LOGE(TAG, "Display::Timer already exists with name %s", args.name); + return nullptr; + } + return &rt.first->second; + } + + /// Stop a Timer with the given name. + [[maybe_unused]] void stop_timer(const char *name) + { + ESP_ERROR_CHECK(esp_timer_stop(get_handle(name)->esp_timer_)); + } + + /// Delete a Timer with the given name. + [[maybe_unused]] void delete_timer(const char *name) + { + managed_timers_.erase(name); + } + + /// Create a Timer with the ESP args and call esp_timer_start_periodic. + [[maybe_unused]] void + start_new_timer_periodic(esp_timer_create_args_t args, + uint64_t period) + { + start_timer_periodic(create_timer(args)->args_.name, period); + } + + /// Calls esp_timer_start_periodic on the Timer with the given name. + [[maybe_unused]] void start_timer_periodic(const char *name, + uint64_t period) + { + ESP_ERROR_CHECK( + esp_timer_start_periodic(get_handle(name)->esp_timer_, period)); + } + + /// Create a Timer with the ESP args and call esp_timer_start_once. + [[maybe_unused]] void + start_new_timer_once(esp_timer_create_args_t args, + uint64_t timeout_us) + { + start_timer_once(create_timer(args)->args_.name, timeout_us); + } + + /// Calls esp_timer_start_once on the Timer with the given name. + [[maybe_unused]] void start_timer_once(const char *name, + uint64_t timeout_us) + { + ESP_ERROR_CHECK( + esp_timer_start_once(get_handle(name)->esp_timer_, timeout_us)); + } + +private: + /// Existing ESP timers created for this TimeKeeper instance. + std::unordered_map managed_timers_; +}; + +#endif // TIME_KEEPER_H