diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 82595040e..22e79a491 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -181,6 +181,8 @@ jobs: target: esp32s3 - path: 'components/serialization/example' target: esp32 + - path: 'components/smartpanlee-sc01-plus/example' + target: esp32s3 - path: 'components/socket/example' target: esp32 - path: 'components/st25dv/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index 7f2ef95a0..8ed892d8d 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -111,6 +111,7 @@ jobs: components/rx8130ce components/seeed-studio-round-display components/serialization + components/smartpanlee-sc01-plus components/socket components/st25dv components/state_machine diff --git a/components/display_drivers/idf_component.yml b/components/display_drivers/idf_component.yml old mode 100644 new mode 100755 index 9c10035f3..8a0bf68b6 --- a/components/display_drivers/idf_component.yml +++ b/components/display_drivers/idf_component.yml @@ -17,6 +17,7 @@ tags: - ILI9341 - SH8601 - ST7789 + - ST7796 dependencies: idf: version: '>=5.0' diff --git a/components/display_drivers/include/st7796.hpp b/components/display_drivers/include/st7796.hpp new file mode 100755 index 000000000..d6c0cc9c3 --- /dev/null +++ b/components/display_drivers/include/st7796.hpp @@ -0,0 +1,292 @@ +#pragma once + +#include +#include +#include +#include + +#include "display_drivers.hpp" + +namespace espp { +/** + * @brief Display driver for the ST7796 display controller. + * + * This implementation follows the same lightweight callback-based structure as + * the other ESPP display drivers, with initialization values adapted from the + * Smart Panlee SC01 Plus board configuration. + */ +class St7796 { +public: + /// Supported command values used by this driver wrapper. + enum class Command : uint8_t { + slpout = 0x11, ///< Exit sleep mode. + invoff = 0x20, ///< Disable display inversion. + invon = 0x21, ///< Enable display inversion. + dispon = 0x29, ///< Turn the display on. + caset = 0x2a, ///< Set the column address window. + raset = 0x2b, ///< Set the row address window. + ramwr = 0x2c, ///< Write pixel data to RAM. + madctl = 0x36, ///< Configure memory addressing and rotation. + colmod = 0x3a, ///< Configure color depth / pixel format. + }; + + /// Initialize the display controller state and send the power-on sequence. + /// \param config Driver transport and orientation configuration. + static void initialize(const display_drivers::Config &config) { + write_command_ = config.write_command; + lcd_send_lines_ = config.lcd_send_lines; + reset_pin_ = config.reset_pin; + dc_pin_ = config.data_command_pin; + offset_x_ = config.offset_x; + offset_y_ = config.offset_y; + mirror_x_ = config.mirror_x; + mirror_y_ = config.mirror_y; + mirror_portrait_ = config.mirror_portrait; + swap_xy_ = config.swap_xy; + swap_color_order_ = config.swap_color_order; + + display_drivers::init_pins(reset_pin_, dc_pin_, config.reset_value); + + uint8_t madctl = 0; + if (swap_color_order_) { + madctl |= LCD_CMD_BGR_BIT; + } + if (mirror_x_) { + madctl |= LCD_CMD_MX_BIT; + } + if (mirror_y_) { + madctl |= LCD_CMD_MY_BIT; + } + if (swap_xy_) { + madctl |= LCD_CMD_MV_BIT; + } + + auto init_commands = std::to_array>({ + {(uint8_t)Command::slpout, {}, 120}, + {(uint8_t)Command::madctl, {madctl}}, + {(uint8_t)Command::colmod, {0x55}}, + {0xf0, {0xc3}}, + {0xf0, {0x96}}, + {0xb4, {0x01}}, + {0xb5, {0x1e}}, + {0xb6, {0x80, 0x22, 0x3b}}, + {0xb7, {0xc6}}, + {0xb9, {0x02, 0xe0}}, + {0xc0, {0x80, 0x16}}, + {0xc1, {0x19}}, + {0xc2, {0xa7}}, + {0xc5, {0x16}}, + {0xe8, {0x40, 0x8a, 0x00, 0x00, 0x29, 0x19, 0xa5, 0x33}}, + {0xe0, + {0xf0, 0x07, 0x0d, 0x04, 0x05, 0x14, 0x36, 0x54, 0x4c, 0x38, 0x13, 0x14, 0x2e, 0x34}}, + {0xe1, + {0xf0, 0x10, 0x14, 0x0e, 0x0c, 0x08, 0x35, 0x44, 0x4c, 0x26, 0x10, 0x12, 0x2c, 0x32}}, + {0xf0, {0x3c}}, + {0xf0, {0x69}, 120}, + {(uint8_t)Command::dispon}, + }); + + send_commands(init_commands); + + if (config.invert_colors) { + write_command_(static_cast(Command::invon), {}, 0); + } else { + write_command_(static_cast(Command::invoff), {}, 0); + } + } + + /// Update the controller addressing mode for a new display rotation. + /// \param rotation New display rotation. + static void rotate(const DisplayRotation &rotation) { + uint8_t data = 0; + if (swap_color_order_) { + data |= LCD_CMD_BGR_BIT; + } + if (mirror_x_) { + data |= LCD_CMD_MX_BIT; + } + if (mirror_y_) { + data |= LCD_CMD_MY_BIT; + } + if (swap_xy_) { + data |= LCD_CMD_MV_BIT; + } + switch (rotation) { + case DisplayRotation::LANDSCAPE: + break; + case DisplayRotation::PORTRAIT: + if (mirror_portrait_) { + data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); + } else { + data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); + } + break; + case DisplayRotation::LANDSCAPE_INVERTED: + data ^= (LCD_CMD_MY_BIT | LCD_CMD_MX_BIT); + break; + case DisplayRotation::PORTRAIT_INVERTED: + if (mirror_portrait_) { + data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); + } else { + data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); + } + break; + } + std::scoped_lock lock{spi_mutex_}; + write_command_(static_cast(Command::madctl), {&data, 1}, 0); + } + + /// Flush an LVGL drawing area to the display. + /// \param disp LVGL display pointer. + /// \param area Area to flush. + /// \param color_map RGB565 pixel data for the area. + static void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + fill(disp, area, color_map, (1 << (int)display_drivers::Flags::FLUSH_BIT)); + } + + /// Set the active drawing area using an LVGL area. + /// \param area Area to make active on the controller. + static void set_drawing_area(const lv_area_t *area) { + set_drawing_area(area->x1, area->y1, area->x2, area->y2); + } + + /// Set the active drawing area using raw coordinates. + /// \param xs Starting x coordinate. + /// \param ys Starting y coordinate. + /// \param xe Ending x coordinate. + /// \param ye Ending y coordinate. + static void set_drawing_area(size_t xs, size_t ys, size_t xe, size_t ye) { + std::array data; + + int offset_x = 0; + int offset_y = 0; + get_offset_rotated(offset_x, offset_y); + + uint16_t start_x = xs + offset_x; + uint16_t end_x = xe + offset_x; + uint16_t start_y = ys + offset_y; + uint16_t end_y = ye + offset_y; + + data[0] = (start_x >> 8) & 0xFF; + data[1] = start_x & 0xFF; + data[2] = (end_x >> 8) & 0xFF; + data[3] = end_x & 0xFF; + write_command_(static_cast(Command::caset), data, 0); + + data[0] = (start_y >> 8) & 0xFF; + data[1] = start_y & 0xFF; + data[2] = (end_y >> 8) & 0xFF; + data[3] = end_y & 0xFF; + write_command_(static_cast(Command::raset), data, 0); + } + + /// Fill an LVGL area using the supplied color buffer. + /// \param disp LVGL display pointer. + /// \param area Area to fill. + /// \param color_map RGB565 pixel data for the area. + /// \param flags Optional transport flags. + static void fill(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map, + uint32_t flags = 0) { + std::scoped_lock lock{spi_mutex_}; + lv_draw_sw_rgb565_swap(color_map, lv_area_get_width(area) * lv_area_get_height(area)); + if (lcd_send_lines_) { + int offset_x = 0; + int offset_y = 0; + get_offset_rotated(offset_x, offset_y); + lcd_send_lines_(area->x1 + offset_x, area->y1 + offset_y, area->x2 + offset_x, + area->y2 + offset_y, color_map, flags); + } else { + set_drawing_area(area); + uint32_t size = lv_area_get_width(area) * lv_area_get_height(area); + write_command_(static_cast(Command::ramwr), {color_map, size * 2}, flags); + } + } + + /// Clear a rectangular region to a solid color. + /// \param x Starting x coordinate. + /// \param y Starting y coordinate. + /// \param width Width in pixels. + /// \param height Height in pixels. + /// \param color RGB565 fill color. + static void clear(size_t x, size_t y, size_t width, size_t height, uint16_t color = 0x0000) { + if (width == 0 || height == 0) { + return; + } + + set_drawing_area(x, y, x + width - 1, y + height - 1); + + uint32_t size = width * height; + static constexpr int max_pixels_to_send = 1024; + std::array color_data; + std::fill(color_data.begin(), color_data.end(), color); + for (int i = 0; i < size; i += max_pixels_to_send) { + size_t num_pixels = std::min((int)(size - i), max_pixels_to_send); + write_command_(static_cast(Command::ramwr), + {reinterpret_cast(color_data.data()), num_pixels * 2}, 0); + } + } + + /// Send a sequence of initialization commands to the controller. + /// \param commands Command list to send in order. + static void send_commands(std::span> commands) { + using namespace std::chrono_literals; + + for (const auto &[command, parameters, delay_ms] : commands) { + std::scoped_lock lock{spi_mutex_}; + write_command_(command, parameters, 0); + std::this_thread::sleep_for(delay_ms * 1ms); + } + } + + /// Set the controller coordinate offset used by this driver. + /// \param x X offset in pixels. + /// \param y Y offset in pixels. + static void set_offset(int x, int y) { + offset_x_ = x; + offset_y_ = y; + } + + /// Get the configured controller coordinate offset. + /// \param x Filled with the x offset. + /// \param y Filled with the y offset. + static void get_offset(int &x, int &y) { + x = offset_x_; + y = offset_y_; + } + + /// Get the coordinate offset after accounting for the current LVGL rotation. + /// \param x Filled with the rotated x offset. + /// \param y Filled with the rotated y offset. + static void get_offset_rotated(int &x, int &y) { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + x = offset_y_; + y = offset_x_; + break; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + x = offset_x_; + y = offset_y_; + break; + } + } + +protected: + static inline display_drivers::write_command_fn write_command_; + static inline display_drivers::send_lines_fn lcd_send_lines_; + static inline gpio_num_t reset_pin_; + static inline gpio_num_t dc_pin_; + static inline int offset_x_; + static inline int offset_y_; + static inline bool mirror_x_; + static inline bool mirror_y_; + static inline bool mirror_portrait_; + static inline bool swap_xy_; + static inline bool swap_color_order_; + static inline std::mutex spi_mutex_; +}; +} // namespace espp diff --git a/components/smartpanlee-sc01-plus/CMakeLists.txt b/components/smartpanlee-sc01-plus/CMakeLists.txt new file mode 100755 index 000000000..5dc307f54 --- /dev/null +++ b/components/smartpanlee-sc01-plus/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register( + INCLUDE_DIRS "include" + SRC_DIRS "src" + REQUIRES driver esp_driver_i2s esp_lcd fatfs sdmmc base_component display display_drivers ft5x06 i2c input_drivers interrupt led task + REQUIRED_IDF_TARGETS "esp32s3" + ) diff --git a/components/smartpanlee-sc01-plus/Kconfig b/components/smartpanlee-sc01-plus/Kconfig new file mode 100755 index 000000000..877f14f56 --- /dev/null +++ b/components/smartpanlee-sc01-plus/Kconfig @@ -0,0 +1,13 @@ +menu "Smart Panlee SC01 Plus Configuration" + config SMARTPANLEE_SC01_PLUS_INTERRUPT_STACK_SIZE + int "Interrupt stack size" + default 4096 + help + Size of the stack used for the touch interrupt handler task. + + config SMARTPANLEE_SC01_PLUS_AUDIO_TASK_STACK_SIZE + int "Audio task stack size" + default 4096 + help + Size of the stack used for the background audio playback task. +endmenu diff --git a/components/smartpanlee-sc01-plus/README.md b/components/smartpanlee-sc01-plus/README.md new file mode 100755 index 000000000..d3cd90859 --- /dev/null +++ b/components/smartpanlee-sc01-plus/README.md @@ -0,0 +1,37 @@ +# Smart Panlee SC01 Plus Board Support Package (BSP) Component + +[![Badge](https://components.espressif.com/components/espp/smartpanlee-sc01-plus/badge.svg)](https://components.espressif.com/components/espp/smartpanlee-sc01-plus) + +The Smart Panlee SC01 Plus (ZX3D50CE08S-USRC / WT32-SC01-PLUS class hardware) is +an ESP32-S3 touchscreen display module with a 3.5-inch 320x480 ST7796 LCD, an +FT5x06-family capacitive touch controller, onboard speaker wiring via I2S, SPI +microSD support, and RS-485 plus expansion GPIOs. + +The `espp::SmartPanleeSc01Plus` component provides a singleton hardware +abstraction for the board's display, touch controller, backlight, speaker audio +path, microSD card, and exposed peripheral pin mappings. + +## Supported Features + +- 3.5-inch 320x480 ST7796 display over 8080 8-bit parallel bus +- FT5x06 capacitive touch over I2C +- PWM backlight brightness control +- I2S speaker playback with software volume, mute, and sample-rate control +- SPI microSD card mounting helpers +- Exposed I2S, RS-485, and external GPIO pin maps for application use + +## Notes + +- This first BSP release focuses on the board features that are directly wired + and well documented in the published board references. +- The board's speaker path is driven directly over I2S using the published + `BCLK`, `LRCK`, and `DOUT` pins, without an external codec dependency in the + BSP. +- Touch handling follows the existing ESPP BSP pattern and uses the FT5x06 + controller plus the ESPP interrupt and LVGL touch-input helpers. + +## Example + +The [example](./example) shows how to initialize the display, register touch +input, play a click sound through the speaker path, adjust backlight +brightness, and optionally mount the microSD card. diff --git a/components/smartpanlee-sc01-plus/example/CMakeLists.txt b/components/smartpanlee-sc01-plus/example/CMakeLists.txt new file mode 100755 index 000000000..2f705b136 --- /dev/null +++ b/components/smartpanlee-sc01-plus/example/CMakeLists.txt @@ -0,0 +1,21 @@ +# 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.20) + +set(ENV{IDF_COMPONENT_MANAGER} "0") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +set(EXTRA_COMPONENT_DIRS + "../../../components/" +) + +set( + COMPONENTS + "main esptool_py smartpanlee-sc01-plus display lvgl task" + CACHE STRING + "List of components to include" + ) + +project(smartpanlee_sc01_plus_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/smartpanlee-sc01-plus/example/README.md b/components/smartpanlee-sc01-plus/example/README.md new file mode 100755 index 000000000..6338ffe00 --- /dev/null +++ b/components/smartpanlee-sc01-plus/example/README.md @@ -0,0 +1,35 @@ +# Smart Panlee SC01 Plus BSP Example + +This example demonstrates the core functionality of the `espp::SmartPanleeSc01Plus` +BSP component on the Smart Panlee SC01 Plus touchscreen display module. + +image + +## Features Demonstrated + +- ST7796 display initialization over the ESP32-S3 8-bit parallel LCD bus +- FT5x06 touch handling with LVGL integration +- I2S speaker playback with a bundled touch-click sound +- Backlight brightness control +- Optional microSD mounting and filesystem inspection +- Published peripheral pin maps for I2S, RS-485, and external GPIOs + +## Hardware Required + +- Smart Panlee SC01 Plus / WT32-SC01-PLUS compatible board +- USB-C cable for programming and power +- microSD card (optional) + +## Build and Flash + +Build the project and flash it to the board, then run monitor tool to view +serial output and verify that touching the screen draws circles and plays the +embedded click sound: + +``` +idf.py -p PORT flash monitor +``` + +(Replace PORT with the name of the serial port to use.) + +(To exit the serial monitor, type ``Ctrl-]``.) diff --git a/components/smartpanlee-sc01-plus/example/main/CMakeLists.txt b/components/smartpanlee-sc01-plus/example/main/CMakeLists.txt new file mode 100755 index 000000000..a3e606e36 --- /dev/null +++ b/components/smartpanlee-sc01-plus/example/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS "." + EMBED_TXTFILES click.wav) diff --git a/components/smartpanlee-sc01-plus/example/main/click.wav b/components/smartpanlee-sc01-plus/example/main/click.wav new file mode 100644 index 000000000..2183344a2 Binary files /dev/null and b/components/smartpanlee-sc01-plus/example/main/click.wav differ diff --git a/components/smartpanlee-sc01-plus/example/main/smartpanlee_sc01_plus_example.cpp b/components/smartpanlee-sc01-plus/example/main/smartpanlee_sc01_plus_example.cpp new file mode 100755 index 000000000..ef1e4fe14 --- /dev/null +++ b/components/smartpanlee-sc01-plus/example/main/smartpanlee_sc01_plus_example.cpp @@ -0,0 +1,240 @@ +/** + * @file smartpanlee_sc01_plus_example.cpp + * @brief Smart Panlee SC01 Plus BSP Example + */ + +#include +#include +#include + +#include "smartpanlee-sc01-plus.hpp" + +#include "task.hpp" + +using namespace std::chrono_literals; + +static constexpr size_t MAX_CIRCLES = 100; +static std::vector circles; +static size_t next_circle_index = 0; +static std::vector audio_bytes; +static std::recursive_mutex lvgl_mutex; +static lv_obj_t *background = nullptr; +static void draw_circle(int x0, int y0, int radius); +static void clear_circles(); +static void initialize_circles(); +static void rotate_display(); +static bool load_audio(size_t &out_size, size_t &out_sample_rate); +static void play_click(espp::SmartPanleeSc01Plus &board); + +extern "C" void app_main(void) { + espp::Logger logger({.tag = "SC01 Plus Example", .level = espp::Logger::Verbosity::INFO}); + logger.info("Starting example!"); + circles.reserve(MAX_CIRCLES); + + //! [smartpanlee sc01 plus example] + auto &board = espp::SmartPanleeSc01Plus::get(); + + if (!board.initialize_lcd()) { + logger.error("Failed to initialize LCD!"); + return; + } + + if (!board.initialize_display(board.display_width() * 40)) { + logger.error("Failed to initialize display!"); + return; + } + + auto touch_callback = [&](const auto &touch) { + static auto previous_touchpad_data = board.touchpad_convert(touch); + auto touchpad_data = board.touchpad_convert(touch); + if (touchpad_data != previous_touchpad_data) { + previous_touchpad_data = touchpad_data; + if (touchpad_data.num_touch_points > 0) { + play_click(board); + std::lock_guard lock(lvgl_mutex); + draw_circle(touchpad_data.x, touchpad_data.y, 10); + } + } + }; + + if (!board.initialize_touch(touch_callback)) { + logger.warn("Touch initialization did not complete cleanly"); + } + + if (!board.initialize_audio()) { + logger.warn("Audio initialization did not complete cleanly"); + } else { + size_t wav_size = 0; + size_t wav_sample_rate = 0; + if (load_audio(wav_size, wav_sample_rate)) { + logger.info("Loaded {} bytes of audio at {} Hz", wav_size, wav_sample_rate); + board.audio_sample_rate(wav_sample_rate); + board.volume(30.0f); + board.mute(false); + } else { + logger.warn("Could not load the embedded click sound"); + } + } + + board.brightness(80.0f); + + if (!board.initialize_sdcard()) { + logger.info("No microSD card mounted"); + } + + auto i2s = board.i2s_pins(); + auto rs485 = board.rs485_pins(); + logger.info("I2S pins: bclk={}, ws={}, dout={}", i2s.bclk, i2s.ws, i2s.dout); + logger.info("RS485 pins: rts={}, rxd={}, txd={}", rs485.rts, rs485.rxd, rs485.txd); + + lv_obj_t *bg = lv_obj_create(lv_screen_active()); + lv_obj_set_size(bg, board.display_width(), board.display_height()); + lv_obj_set_style_bg_color(bg, lv_color_make(8, 12, 24), 0); + + lv_obj_t *label = lv_label_create(lv_screen_active()); + lv_label_set_text(label, "Smart Panlee SC01 Plus\n\nTouch the screen to draw and play a click.\n" + "Press refresh to rotate.\nCheck serial output for SD card, pin, and " + "audio info."); + lv_obj_align(label, LV_ALIGN_TOP_LEFT, 16, 16); + lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_LEFT, 0); + + lv_obj_t *btn = lv_btn_create(lv_screen_active()); + lv_obj_set_size(btn, 56, 56); + lv_obj_align(btn, LV_ALIGN_TOP_RIGHT, -12, 12); + lv_obj_t *btn_label = lv_label_create(btn); + lv_label_set_text(btn_label, LV_SYMBOL_REFRESH); + lv_obj_align(btn_label, LV_ALIGN_CENTER, 0, 0); + background = bg; + initialize_circles(); + lv_obj_add_event_cb( + btn, + [](lv_event_t *event) { + (void)event; + rotate_display(); + }, + LV_EVENT_PRESSED, nullptr); + + lv_obj_set_scrollbar_mode(lv_screen_active(), LV_SCROLLBAR_MODE_OFF); + lv_obj_clear_flag(lv_screen_active(), LV_OBJ_FLAG_SCROLLABLE); + + espp::Task lv_task({.callback = [](std::mutex &m, std::condition_variable &cv) -> bool { + { + std::lock_guard lock(lvgl_mutex); + lv_task_handler(); + } + std::unique_lock lock(m); + cv.wait_for(lock, 16ms); + return false; + }, + .task_config = { + .name = "lv_task", + .stack_size_bytes = 6 * 1024, + }}); + lv_task.start(); + //! [smartpanlee sc01 plus example] + + while (true) { + std::this_thread::sleep_for(1s); + if (board.is_sd_card_available()) { + uint32_t size_mb = 0; + uint32_t free_mb = 0; + if (board.get_sd_card_info(&size_mb, &free_mb)) { + logger.info("microSD: size={} MB free={} MB", size_mb, free_mb); + } + } + } +} + +static void draw_circle(int x0, int y0, int radius) { + if (circles.empty()) { + return; + } + + auto *circle = circles[next_circle_index]; + lv_obj_set_size(circle, radius * 2, radius * 2); + lv_obj_align(circle, LV_ALIGN_TOP_LEFT, x0 - radius, y0 - radius); + lv_obj_clear_flag(circle, LV_OBJ_FLAG_HIDDEN); + + next_circle_index = (next_circle_index + 1) % circles.size(); +} + +static void clear_circles() { + for (auto *circle : circles) { + lv_obj_add_flag(circle, LV_OBJ_FLAG_HIDDEN); + } + next_circle_index = 0; +} + +static void initialize_circles() { + if (!circles.empty()) { + return; + } + + circles.reserve(MAX_CIRCLES); + for (size_t i = 0; i < MAX_CIRCLES; ++i) { + auto *circle = lv_obj_create(lv_screen_active()); + lv_obj_remove_style_all(circle); + lv_obj_set_size(circle, 20, 20); + lv_obj_set_style_radius(circle, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(circle, lv_palette_main(LV_PALETTE_CYAN), 0); + lv_obj_set_style_bg_opa(circle, LV_OPA_70, 0); + lv_obj_set_style_border_width(circle, 0, 0); + lv_obj_clear_flag(circle, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(circle, LV_OBJ_FLAG_HIDDEN); + circles.push_back(circle); + } +} + +static void rotate_display() { + auto &board = espp::SmartPanleeSc01Plus::get(); + std::lock_guard lock(lvgl_mutex); + clear_circles(); + static auto rotation = LV_DISPLAY_ROTATION_0; + rotation = static_cast((static_cast(rotation) + 1) % 4); + auto *display = lv_display_get_default(); + lv_disp_set_rotation(display, rotation); + if (background) { + lv_obj_set_size(background, board.rotated_display_width(), board.rotated_display_height()); + } +} + +static bool load_audio(size_t &out_size, size_t &out_sample_rate) { + if (!audio_bytes.empty()) { + out_size = audio_bytes.size(); + return true; + } + + extern const uint8_t click_wav_start[] asm("_binary_click_wav_start"); + extern const uint8_t click_wav_end[] asm("_binary_click_wav_end"); + audio_bytes = std::vector(click_wav_start, click_wav_end); + if (audio_bytes.size() < 44) { + audio_bytes.clear(); + return false; + } + + auto sample_rate = *(reinterpret_cast(&audio_bytes[24])); + if (audio_bytes.size() > 44) { + audio_bytes.erase(audio_bytes.begin(), audio_bytes.begin() + 44); + } + out_size = audio_bytes.size(); + out_sample_rate = sample_rate; + return true; +} + +static void play_click(espp::SmartPanleeSc01Plus &board) { + if (audio_bytes.empty()) { + return; + } + + auto audio_buffer_size = board.audio_buffer_size(); + if (audio_buffer_size == 0) { + return; + } + + size_t offset = 0; + while (offset < audio_bytes.size()) { + auto bytes_to_play = std::min(audio_buffer_size, audio_bytes.size() - offset); + board.play_audio(audio_bytes.data() + offset, static_cast(bytes_to_play)); + offset += bytes_to_play; + } +} diff --git a/components/smartpanlee-sc01-plus/example/partitions.csv b/components/smartpanlee-sc01-plus/example/partitions.csv new file mode 100755 index 000000000..030cf611b --- /dev/null +++ b/components/smartpanlee-sc01-plus/example/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size +nvs, data, nvs, 0xa000, 0x6000 +phy_init, data, phy, 0x10000, 0x1000 +factory, app, factory, 0x20000, 0x300000 +storage, data, spiffs, 0x320000, 0x4E0000 diff --git a/components/smartpanlee-sc01-plus/example/sdkconfig.defaults b/components/smartpanlee-sc01-plus/example/sdkconfig.defaults new file mode 100755 index 000000000..58f8b5d0f --- /dev/null +++ b/components/smartpanlee-sc01-plus/example/sdkconfig.defaults @@ -0,0 +1,24 @@ +CONFIG_IDF_TARGET="esp32s3" + +CONFIG_FREERTOS_HZ=1000 +CONFIG_COMPILER_OPTIMIZATION_PERF=y + +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y +CONFIG_ESPTOOLPY_FLASHSIZE="8MB" +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y + +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=12288 +CONFIG_ESP_TIMER_TASK_STACK_SIZE=6144 + +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_OFFSET=0x9000 +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" + +CONFIG_SMARTPANLEE_SC01_PLUS_INTERRUPT_STACK_SIZE=4096 + +CONFIG_LV_DEF_REFR_PERIOD=16 +CONFIG_LV_USE_THEME_DEFAULT=y +CONFIG_LV_THEME_DEFAULT_DARK=y +CONFIG_LV_THEME_DEFAULT_GROW=y +CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=30 diff --git a/components/smartpanlee-sc01-plus/idf_component.yml b/components/smartpanlee-sc01-plus/idf_component.yml new file mode 100755 index 000000000..7b39110f3 --- /dev/null +++ b/components/smartpanlee-sc01-plus/idf_component.yml @@ -0,0 +1,30 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "Smart Panlee SC01 Plus board support package (BSP) component for ESP-IDF" +url: "https://github.com/esp-cpp/espp/tree/main/components/smartpanlee-sc01-plus" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/smartpanlee_sc01_plus.html" +examples: + - path: example +tags: + - cpp + - Component + - BSP + - Smart Panlee + - SC01 Plus +dependencies: + idf: + version: ">=5.0" + espp/base_component: ">=1.0" + espp/display: ">=1.0" + espp/display_drivers: ">=1.0" + espp/ft5x06: ">=1.0" + espp/i2c: ">=1.0" + espp/input_drivers: ">=1.0" + espp/interrupt: ">=1.0" + espp/led: ">=1.0" + espp/task: ">=1.0" +targets: + - esp32s3 diff --git a/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp b/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp new file mode 100755 index 000000000..9d9bb0928 --- /dev/null +++ b/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp @@ -0,0 +1,364 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base_component.hpp" +#include "display.hpp" +#include "ft5x06.hpp" +#include "i2c.hpp" +#include "interrupt.hpp" +#include "led.hpp" +#include "st7796.hpp" +#include "task.hpp" +#include "touchpad_input.hpp" + +namespace espp { +/// The SmartPanleeSc01Plus class provides an interface to the Smart Panlee SC01 +/// Plus development board. +/// +/// The class provides access to the following features: +/// - ST7796 3.5-inch 320x480 display over an 8-bit Intel 8080 style bus +/// - FT5x06 capacitive touch controller +/// - PWM backlight control +/// - I2S audio playback for the onboard speaker path +/// - SPI microSD card mounting helpers +/// - Published I2S, RS-485, and expansion pin maps +/// +/// The class is a singleton and can be accessed using the get() method. +/// +/// \section smartpanlee_sc01_plus_example Example +/// \snippet smartpanlee_sc01_plus_example.cpp smartpanlee sc01 plus example +class SmartPanleeSc01Plus : public BaseComponent { +public: + /// Alias for the pixel type used by the display. + using Pixel = lv_color16_t; + /// Alias for the ST7796 display driver wrapper. + using DisplayDriver = espp::St7796; + /// Alias for the FT5x06-family capacitive touch controller. + using TouchDriver = espp::Ft5x06; + /// Alias for the touch data structure exposed to callers. + using TouchpadData = espp::TouchpadData; + /// Callback type invoked when fresh touch data is available. + using touch_callback_t = std::function; + + /// Published I2S pins for the SC01 Plus speaker interface. + struct I2sPins { + gpio_num_t mclk; ///< Optional master clock pin. + gpio_num_t bclk; ///< I2S bit clock pin. + gpio_num_t ws; ///< I2S word-select / LRCLK pin. + gpio_num_t dout; ///< I2S data-out pin. + gpio_num_t din; ///< Optional I2S data-in pin. + }; + + /// SPI microSD pin mapping published for the board. + struct SdCardPins { + gpio_num_t clk; ///< SPI clock pin. + gpio_num_t mosi; ///< SPI MOSI pin. + gpio_num_t miso; ///< SPI MISO pin. + gpio_num_t cs; ///< SPI chip-select pin. + }; + + /// RS-485 UART control and data pins published for the board. + struct Rs485Pins { + gpio_num_t rts; ///< RS-485 direction-control pin. + gpio_num_t rxd; ///< UART RX pin. + gpio_num_t txd; ///< UART TX pin. + }; + + /// External expansion GPIOs published for the board. + struct ExternalPins { + gpio_num_t io1; ///< External GPIO 1. + gpio_num_t io2; ///< External GPIO 2. + gpio_num_t io3; ///< External GPIO 3. + gpio_num_t io4; ///< External GPIO 4. + gpio_num_t io5; ///< External GPIO 5. + gpio_num_t io6; ///< External GPIO 6. + }; + + /// Configuration for mounting the optional microSD card. + struct SdCardConfig { + bool format_if_mount_failed{false}; ///< Format the card if mounting fails. + int max_files{5}; ///< Maximum number of simultaneously open files. + size_t allocation_unit_size{16 * 1024}; ///< FAT allocation unit size in bytes. + }; + + /// Mount point used when the optional microSD card is mounted. + static constexpr char mount_point[] = "/sdcard"; + + /// Access the singleton instance. + /// \return Reference to the singleton board-support instance. + static SmartPanleeSc01Plus &get() { + static SmartPanleeSc01Plus instance; + return instance; + } + + /// Deleted copy constructor. + SmartPanleeSc01Plus(const SmartPanleeSc01Plus &) = delete; + /// Deleted copy assignment operator. + SmartPanleeSc01Plus &operator=(const SmartPanleeSc01Plus &) = delete; + /// Deleted move constructor. + SmartPanleeSc01Plus(SmartPanleeSc01Plus &&) = delete; + /// Deleted move assignment operator. + SmartPanleeSc01Plus &operator=(SmartPanleeSc01Plus &&) = delete; + + /// Get the published I2S pin mapping for the board. + /// \return I2S pin mapping. + static constexpr I2sPins i2s_pins() { + return {GPIO_NUM_NC, GPIO_NUM_36, GPIO_NUM_35, GPIO_NUM_37, GPIO_NUM_NC}; + } + + /// Get the published SPI microSD pin mapping for the board. + /// \return MicroSD pin mapping. + static constexpr SdCardPins sd_card_pins() { + return {GPIO_NUM_39, GPIO_NUM_40, GPIO_NUM_38, GPIO_NUM_41}; + } + + /// Get the published RS-485 pin mapping for the board. + /// \return RS-485 pin mapping. + static constexpr Rs485Pins rs485_pins() { return {GPIO_NUM_2, GPIO_NUM_1, GPIO_NUM_42}; } + + /// Get the published external expansion GPIO mapping for the board. + /// \return External GPIO mapping. + static constexpr ExternalPins external_pins() { + return {GPIO_NUM_10, GPIO_NUM_11, GPIO_NUM_12, GPIO_NUM_13, GPIO_NUM_14, GPIO_NUM_21}; + } + + /// Get the internal I2C bus used for touch and board peripherals. + /// \return Reference to the internal I2C instance. + I2c &internal_i2c() { return internal_i2c_; } + /// Get the interrupt manager used by the BSP. + /// \return Reference to the interrupt manager. + espp::Interrupt &interrupts() { return interrupts_; } + + /// Initialize the touch controller and optional interrupt-driven callback. + /// \param callback Callback invoked when touch data changes. + /// \return True if touch support was initialized, false otherwise. + bool initialize_touch(const touch_callback_t &callback = nullptr); + /// Get the LVGL touch input wrapper created by initialize_display(). + /// \return Shared pointer to the touchpad input wrapper, or nullptr if not initialized. + std::shared_ptr touchpad_input() const { return touchpad_input_; } + /// Get the most recently cached touch data. + /// \return Latest cached touchpad data. + TouchpadData touchpad_data() const; + /// Read the cached touch data using the signature expected by TouchpadInput. + /// \param num_touch_points Filled with the number of active touch points. + /// \param x Filled with the x coordinate of the primary touch point. + /// \param y Filled with the y coordinate of the primary touch point. + /// \param btn_state Filled with the button state for LVGL compatibility. + void touchpad_read(uint8_t *num_touch_points, uint16_t *x, uint16_t *y, uint8_t *btn_state); + /// Convert raw touch data into display-oriented coordinates. + /// \param data Raw cached touch data. + /// \return Touch data transformed for the current display rotation and inversion settings. + TouchpadData touchpad_convert(const TouchpadData &data) const; + + /// Initialize the low-level LCD transport and backlight control. + /// \return True if the LCD interface was initialized, false otherwise. + bool initialize_lcd(); + /// Initialize the LVGL display wrapper. + /// \param pixel_buffer_size Display buffer size in pixels. + /// \return True if the display wrapper was initialized, false otherwise. + bool initialize_display(size_t pixel_buffer_size = 320 * 40); + /// Get the high-level LVGL display wrapper. + /// \return Shared pointer to the display wrapper, or nullptr if not initialized. + std::shared_ptr> display() const { return display_; } + /// Set the display backlight brightness. + /// \param brightness Brightness percentage in the range [0, 100]. + void brightness(float brightness); + /// Get the display backlight brightness. + /// \return Brightness percentage in the range [0, 100]. + float brightness() const; + + /// Get the number of bytes per pixel used by the panel. + /// \return Bytes per pixel. + static constexpr size_t bytes_per_pixel() { return sizeof(Pixel); } + /// Get the native display width. + /// \return Width in pixels. + static constexpr size_t display_width() { return lcd_width_; } + /// Get the native display height. + /// \return Height in pixels. + static constexpr size_t display_height() { return lcd_height_; } + /// Get the current display width after applying LVGL rotation. + /// \return Rotated width in pixels. + size_t rotated_display_width() const; + /// Get the current display height after applying LVGL rotation. + /// \return Rotated height in pixels. + size_t rotated_display_height() const; + + /// Initialize the speaker audio path using the board's published I2S pins. + /// \return True if audio playback support was initialized, false otherwise. + bool initialize_audio(); + /// Initialize the speaker audio path using the specified sample rate. + /// \param sample_rate Audio sample rate in Hz. + /// \return True if audio playback support was initialized, false otherwise. + bool initialize_audio(uint32_t sample_rate); + /// Initialize the speaker audio path using a custom task configuration. + /// \param sample_rate Audio sample rate in Hz. + /// \param task_config Task configuration for the background audio pump. + /// \return True if audio playback support was initialized, false otherwise. + bool initialize_audio(uint32_t sample_rate, const espp::Task::BaseConfig &task_config); + /// Set the audio sample rate. + /// \param sample_rate Audio sample rate in Hz. + void audio_sample_rate(uint32_t sample_rate); + /// Get the active audio sample rate. + /// \return Audio sample rate in Hz, or 0 if audio is not initialized. + uint32_t audio_sample_rate() const; + /// Get the internal playback buffer size. + /// \return Audio buffer size in bytes. + size_t audio_buffer_size() const; + /// Mute or unmute speaker playback. + /// \param mute True to mute playback, false to unmute it. + void mute(bool mute); + /// Query whether speaker playback is muted. + /// \return True if playback is muted. + bool is_muted() const; + /// Set the software playback volume. + /// \param volume Volume percentage in the range [0, 100]. + void volume(float volume); + /// Get the software playback volume. + /// \return Volume percentage in the range [0, 100]. + float volume() const; + /// Queue raw PCM audio bytes for playback. + /// \param data Audio payload to play. + void play_audio(std::span data); + /// Queue raw PCM audio bytes for playback. + /// \param data Pointer to PCM audio bytes. + /// \param num_bytes Number of bytes to play. + void play_audio(const uint8_t *data, uint32_t num_bytes); + + /// Initialize and mount the optional microSD card with default settings. + /// \return True if the card was mounted successfully, false otherwise. + bool initialize_sdcard(); + /// Initialize and mount the optional microSD card with custom settings. + /// \param config Mount configuration. + /// \return True if the card was mounted successfully, false otherwise. + bool initialize_sdcard(const SdCardConfig &config); + /// Check whether the microSD card is currently mounted. + /// \return True if the card is mounted and available. + bool is_sd_card_available() const; + /// Query mounted microSD capacity and free space. + /// \param size_mb Optional output for total capacity in megabytes. + /// \param free_mb Optional output for free space in megabytes. + /// \return True if card information was retrieved successfully, false otherwise. + bool get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const; + +protected: + SmartPanleeSc01Plus(); + + static bool panel_io_color_trans_done(esp_lcd_panel_io_handle_t panel_io, + esp_lcd_panel_io_event_data_t *edata, void *user_ctx); + static espp::Task::BaseConfig default_audio_task_config(); + bool initialize_i2s(uint32_t default_audio_rate); + bool audio_task_callback(std::mutex &m, std::condition_variable &cv, bool &task_notified); + bool update_touch(); + void write_command(uint8_t command, std::span parameters, uint32_t user_data); + void write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t *data, uint32_t user_data); + + static constexpr auto internal_i2c_port = I2C_NUM_1; + static constexpr auto internal_i2c_clock_speed = 400 * 1000; + static constexpr gpio_num_t internal_i2c_sda = GPIO_NUM_6; + static constexpr gpio_num_t internal_i2c_scl = GPIO_NUM_5; + + static constexpr size_t lcd_width_ = 320; + static constexpr size_t lcd_height_ = 480; + static constexpr int lcd_clock_speed_hz = 40 * 1000 * 1000; + static constexpr size_t lcd_max_transfer_bytes = lcd_width_ * 40 * sizeof(Pixel); + static constexpr gpio_num_t lcd_reset_io = GPIO_NUM_4; + static constexpr gpio_num_t lcd_backlight_io = GPIO_NUM_45; + static constexpr gpio_num_t lcd_dc_io = GPIO_NUM_0; + static constexpr gpio_num_t lcd_wr_io = GPIO_NUM_47; + static constexpr gpio_num_t lcd_cs_io = GPIO_NUM_NC; + static constexpr gpio_num_t lcd_d0_io = GPIO_NUM_9; + static constexpr gpio_num_t lcd_d1_io = GPIO_NUM_46; + static constexpr gpio_num_t lcd_d2_io = GPIO_NUM_3; + static constexpr gpio_num_t lcd_d3_io = GPIO_NUM_8; + static constexpr gpio_num_t lcd_d4_io = GPIO_NUM_18; + static constexpr gpio_num_t lcd_d5_io = GPIO_NUM_17; + static constexpr gpio_num_t lcd_d6_io = GPIO_NUM_16; + static constexpr gpio_num_t lcd_d7_io = GPIO_NUM_15; + static constexpr bool backlight_value = true; + static constexpr bool invert_colors = true; + static constexpr auto rotation = espp::DisplayRotation::LANDSCAPE; + static constexpr bool mirror_x = false; + static constexpr bool mirror_y = false; + static constexpr bool swap_xy = false; + static constexpr bool swap_color_order = true; + + static constexpr bool touch_swap_xy = false; + static constexpr bool touch_invert_x = false; + static constexpr bool touch_invert_y = false; + static constexpr gpio_num_t touch_interrupt = GPIO_NUM_7; + + static constexpr auto i2s_port = I2S_NUM_0; + static constexpr int audio_num_channels = 2; + static constexpr int audio_num_bytes_per_channel = 2; + static constexpr int audio_update_frequency = 60; + static constexpr int calc_audio_buffer_size(int sample_rate) { + return sample_rate * audio_num_channels * audio_num_bytes_per_channel / audio_update_frequency; + } + + I2c internal_i2c_{I2c::Config{.port = internal_i2c_port, + .sda_io_num = internal_i2c_sda, + .scl_io_num = internal_i2c_scl, + .sda_pullup_en = GPIO_PULLUP_ENABLE, + .scl_pullup_en = GPIO_PULLUP_ENABLE, + .clk_speed = internal_i2c_clock_speed}}; + + espp::Interrupt::PinConfig touch_interrupt_pin_{.gpio_num = touch_interrupt, + .callback = + [this](const auto &event) { + if (!event.active) { + return; + } + if (update_touch() && touch_callback_) { + touch_callback_(touchpad_data()); + } + }, + .active_level = espp::Interrupt::ActiveLevel::LOW, + .interrupt_type = + espp::Interrupt::Type::FALLING_EDGE, + .pullup_enabled = true}; + + espp::Interrupt interrupts_{ + {.interrupts = {}, + .task_config = {.name = "sc01+ interrupts", + .stack_size_bytes = CONFIG_SMARTPANLEE_SC01_PLUS_INTERRUPT_STACK_SIZE}}}; + + std::shared_ptr touch_driver_; + std::shared_ptr touchpad_input_; + mutable std::recursive_mutex touchpad_data_mutex_; + TouchpadData touchpad_data_; + touch_callback_t touch_callback_{nullptr}; + + std::shared_ptr> display_; + std::shared_ptr backlight_; + std::vector backlight_channel_configs_; + esp_lcd_i80_bus_handle_t lcd_bus_{nullptr}; + esp_lcd_panel_io_handle_t panel_io_{nullptr}; + + std::atomic audio_initialized_{false}; + std::atomic volume_{50.0f}; + std::atomic mute_{false}; + std::unique_ptr audio_task_{nullptr}; + i2s_chan_handle_t audio_tx_handle_{nullptr}; + StreamBufferHandle_t audio_tx_stream_{nullptr}; + i2s_std_config_t audio_std_cfg_{}; + std::vector audio_tx_buffer_; + + bool sd_card_initialized_{false}; + sdmmc_card_t *sdcard_{nullptr}; +}; +} // namespace espp diff --git a/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp b/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp new file mode 100755 index 000000000..82b446c8b --- /dev/null +++ b/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp @@ -0,0 +1,544 @@ +#include "smartpanlee-sc01-plus.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace espp { + +SmartPanleeSc01Plus::SmartPanleeSc01Plus() + : BaseComponent("SmartPanleeSc01Plus") {} + +bool SmartPanleeSc01Plus::initialize_touch(const SmartPanleeSc01Plus::touch_callback_t &callback) { + if (touch_driver_) { + logger_.warn("Touch already initialized, not initializing again!"); + return false; + } + + touch_driver_ = std::make_shared(TouchDriver::Config{ + .write = std::bind(&espp::I2c::write, &internal_i2c_, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3), + .read_register = + std::bind(&espp::I2c::read_at_register, &internal_i2c_, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), + .log_level = espp::Logger::Verbosity::WARN}); + + touch_callback_ = callback; + interrupts_.add_interrupt(touch_interrupt_pin_); + update_touch(); + return true; +} + +SmartPanleeSc01Plus::TouchpadData SmartPanleeSc01Plus::touchpad_data() const { + std::lock_guard lock(touchpad_data_mutex_); + return touchpad_data_; +} + +void SmartPanleeSc01Plus::touchpad_read(uint8_t *num_touch_points, uint16_t *x, uint16_t *y, + uint8_t *btn_state) { + std::lock_guard lock(touchpad_data_mutex_); + *num_touch_points = touchpad_data_.num_touch_points; + *x = touchpad_data_.x; + *y = touchpad_data_.y; + *btn_state = touchpad_data_.btn_state; +} + +SmartPanleeSc01Plus::TouchpadData +SmartPanleeSc01Plus::touchpad_convert(const SmartPanleeSc01Plus::TouchpadData &data) const { + TouchpadData temp_data; + temp_data.num_touch_points = data.num_touch_points; + temp_data.x = data.x; + temp_data.y = data.y; + temp_data.btn_state = data.btn_state; + if (temp_data.num_touch_points == 0) { + return temp_data; + } + if (touch_swap_xy) { + std::swap(temp_data.x, temp_data.y); + } + if (touch_invert_x) { + temp_data.x = lcd_width_ - (temp_data.x + 1); + } + if (touch_invert_y) { + temp_data.y = lcd_height_ - (temp_data.y + 1); + } + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_0: + break; + case LV_DISPLAY_ROTATION_90: + temp_data.y = lcd_height_ - (temp_data.y + 1); + std::swap(temp_data.x, temp_data.y); + break; + case LV_DISPLAY_ROTATION_180: + temp_data.x = lcd_width_ - (temp_data.x + 1); + temp_data.y = lcd_height_ - (temp_data.y + 1); + break; + case LV_DISPLAY_ROTATION_270: + temp_data.x = lcd_width_ - (temp_data.x + 1); + std::swap(temp_data.x, temp_data.y); + break; + default: + break; + } + return temp_data; +} + +bool SmartPanleeSc01Plus::panel_io_color_trans_done(esp_lcd_panel_io_handle_t panel_io, + esp_lcd_panel_io_event_data_t *edata, + void *user_ctx) { + (void)panel_io; + (void)edata; + (void)user_ctx; + auto *display = lv_display_get_default(); + if (display) { + lv_display_flush_ready(display); + } + return false; +} + +espp::Task::BaseConfig SmartPanleeSc01Plus::default_audio_task_config() { + return { + .name = "sc01+ audio", + .stack_size_bytes = CONFIG_SMARTPANLEE_SC01_PLUS_AUDIO_TASK_STACK_SIZE, + .priority = 20, + .core_id = 1, + }; +} + +bool SmartPanleeSc01Plus::initialize_i2s(uint32_t default_audio_rate) { + logger_.info("Initializing I2S audio output at {} Hz", default_audio_rate); + + i2s_chan_config_t chan_config = I2S_CHANNEL_DEFAULT_CONFIG(i2s_port, I2S_ROLE_MASTER); + chan_config.auto_clear = true; + ESP_ERROR_CHECK(i2s_new_channel(&chan_config, &audio_tx_handle_, nullptr)); + + auto pins = i2s_pins(); + audio_std_cfg_ = { + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(default_audio_rate), + .slot_cfg = + I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), + .gpio_cfg = + { + .mclk = pins.mclk, + .bclk = pins.bclk, + .ws = pins.ws, + .dout = pins.dout, + .din = pins.din, + .invert_flags = + { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false, + }, + }, + }; + audio_std_cfg_.clk_cfg.mclk_multiple = I2S_MCLK_MULTIPLE_256; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(audio_tx_handle_, &audio_std_cfg_)); + + auto buffer_size = calc_audio_buffer_size(default_audio_rate); + audio_tx_buffer_.resize(buffer_size); + audio_tx_stream_ = xStreamBufferCreate(buffer_size * 4, 0); + if (!audio_tx_stream_) { + logger_.error("Failed to create audio stream buffer"); + return false; + } + xStreamBufferReset(audio_tx_stream_); + + ESP_ERROR_CHECK(i2s_channel_enable(audio_tx_handle_)); + return true; +} + +bool SmartPanleeSc01Plus::initialize_lcd() { + if (panel_io_ || backlight_) { + logger_.warn("LCD already initialized, not initializing again!"); + return false; + } + + esp_lcd_i80_bus_config_t bus_config = { + .dc_gpio_num = lcd_dc_io, + .wr_gpio_num = lcd_wr_io, + .clk_src = LCD_CLK_SRC_DEFAULT, + .data_gpio_nums = {lcd_d0_io, lcd_d1_io, lcd_d2_io, lcd_d3_io, lcd_d4_io, lcd_d5_io, + lcd_d6_io, lcd_d7_io}, + .bus_width = 8, + .max_transfer_bytes = lcd_max_transfer_bytes, + .dma_burst_size = 64, + }; + ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &lcd_bus_)); + + esp_lcd_panel_io_i80_config_t io_config = { + .cs_gpio_num = lcd_cs_io, + .pclk_hz = lcd_clock_speed_hz, + .trans_queue_depth = 10, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .dc_levels = + { + .dc_idle_level = 0, + .dc_cmd_level = 0, + .dc_dummy_level = 0, + .dc_data_level = 1, + }, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(lcd_bus_, &io_config, &panel_io_)); + + backlight_channel_configs_.push_back({.gpio = static_cast(lcd_backlight_io), + .channel = LEDC_CHANNEL_0, + .timer = LEDC_TIMER_0, + .output_invert = !backlight_value}); + backlight_ = std::make_shared(Led::Config{.timer = LEDC_TIMER_0, + .frequency_hz = 5000, + .channels = backlight_channel_configs_, + .duty_resolution = LEDC_TIMER_10_BIT}); + + using namespace std::placeholders; + DisplayDriver::initialize(espp::display_drivers::Config{ + .write_command = std::bind(&SmartPanleeSc01Plus::write_command, this, _1, _2, _3), + .lcd_send_lines = + std::bind(&SmartPanleeSc01Plus::write_lcd_lines, this, _1, _2, _3, _4, _5, _6), + .reset_pin = lcd_reset_io, + .data_command_pin = GPIO_NUM_NC, + .reset_value = false, + .invert_colors = invert_colors, + .swap_color_order = swap_color_order, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y}); + + brightness(100.0f); + return true; +} + +bool SmartPanleeSc01Plus::initialize_display(size_t pixel_buffer_size) { + if (!panel_io_) { + logger_.error( + "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); + return false; + } + if (display_) { + logger_.warn("Display already initialized, not initializing again!"); + return false; + } + + display_ = std::make_shared>( + Display::LvglConfig{.width = lcd_width_, + .height = lcd_height_, + .flush_callback = DisplayDriver::flush, + .rotation_callback = DisplayDriver::rotate, + .rotation = rotation}, + Display::OledConfig{ + .set_brightness_callback = + [this](float brightness) { this->brightness(brightness * 100); }, + .get_brightness_callback = [this]() { return this->brightness() / 100.0f; }}, + Display::DynamicMemoryConfig{ + .pixel_buffer_size = pixel_buffer_size, + .double_buffered = true, + .allocation_flags = MALLOC_CAP_8BIT | MALLOC_CAP_DMA, + }); + + touchpad_input_ = std::make_shared(TouchpadInput::Config{ + .touchpad_read = + std::bind(&SmartPanleeSc01Plus::touchpad_read, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), + .swap_xy = touch_swap_xy, + .invert_x = touch_invert_x, + .invert_y = touch_invert_y, + .log_level = espp::Logger::Verbosity::WARN}); + + const esp_lcd_panel_io_callbacks_t callbacks = { + .on_color_trans_done = &SmartPanleeSc01Plus::panel_io_color_trans_done, + }; + ESP_ERROR_CHECK(esp_lcd_panel_io_register_event_callbacks(panel_io_, &callbacks, nullptr)); + + return true; +} + +bool SmartPanleeSc01Plus::initialize_audio() { + return initialize_audio(44100, default_audio_task_config()); +} + +bool SmartPanleeSc01Plus::initialize_audio(uint32_t sample_rate) { + return initialize_audio(sample_rate, default_audio_task_config()); +} + +bool SmartPanleeSc01Plus::initialize_audio(uint32_t sample_rate, + const espp::Task::BaseConfig &task_config) { + if (audio_initialized_) { + logger_.warn("Audio already initialized"); + return true; + } + if (!initialize_i2s(sample_rate)) { + logger_.error("Could not initialize I2S audio output"); + return false; + } + + using namespace std::placeholders; + audio_task_ = espp::Task::make_unique({ + .callback = std::bind(&SmartPanleeSc01Plus::audio_task_callback, this, _1, _2, _3), + .task_config = task_config, + }); + + audio_initialized_ = audio_task_ && audio_task_->start(); + return audio_initialized_; +} + +void SmartPanleeSc01Plus::brightness(float brightness) { + if (!backlight_) { + return; + } + backlight_->set_duty(backlight_channel_configs_.front().channel, + std::clamp(brightness, 0.0f, 100.0f)); +} + +float SmartPanleeSc01Plus::brightness() const { + if (!backlight_) { + return 0.0f; + } + auto duty = backlight_->get_duty(backlight_channel_configs_.front().channel); + return duty.value_or(0.0f); +} + +void SmartPanleeSc01Plus::audio_sample_rate(uint32_t sample_rate) { + if (!audio_initialized_ || !audio_tx_handle_) { + return; + } + + logger_.info("Setting audio sample rate to {} Hz", sample_rate); + i2s_channel_disable(audio_tx_handle_); + audio_std_cfg_.clk_cfg.sample_rate_hz = sample_rate; + i2s_channel_reconfig_std_clock(audio_tx_handle_, &audio_std_cfg_.clk_cfg); + xStreamBufferReset(audio_tx_stream_); + i2s_channel_enable(audio_tx_handle_); +} + +uint32_t SmartPanleeSc01Plus::audio_sample_rate() const { + if (!audio_initialized_) { + return 0; + } + return audio_std_cfg_.clk_cfg.sample_rate_hz; +} + +size_t SmartPanleeSc01Plus::audio_buffer_size() const { return audio_tx_buffer_.size(); } + +void SmartPanleeSc01Plus::mute(bool mute) { mute_ = mute; } + +bool SmartPanleeSc01Plus::is_muted() const { return mute_; } + +void SmartPanleeSc01Plus::volume(float volume) { volume_ = std::clamp(volume, 0.0f, 100.0f); } + +float SmartPanleeSc01Plus::volume() const { return volume_; } + +void SmartPanleeSc01Plus::play_audio(std::span data) { + play_audio(data.data(), static_cast(data.size())); +} + +void SmartPanleeSc01Plus::play_audio(const uint8_t *data, uint32_t num_bytes) { + if (!audio_initialized_ || !audio_tx_stream_ || !data || num_bytes == 0) { + return; + } + xStreamBufferSend(audio_tx_stream_, data, num_bytes, 0); +} + +size_t SmartPanleeSc01Plus::rotated_display_width() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_height_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_width_; + } +} + +size_t SmartPanleeSc01Plus::rotated_display_height() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_width_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_height_; + } +} + +bool SmartPanleeSc01Plus::initialize_sdcard(const SmartPanleeSc01Plus::SdCardConfig &config) { + if (sdcard_) { + logger_.error("SD card already initialized!"); + return false; + } + + esp_vfs_fat_sdmmc_mount_config_t mount_config = {}; + mount_config.format_if_mount_failed = config.format_if_mount_failed; + mount_config.max_files = config.max_files; + mount_config.allocation_unit_size = config.allocation_unit_size; + + sdmmc_host_t host = SDSPI_HOST_DEFAULT(); + host.slot = SPI2_HOST; + host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; + + spi_bus_config_t bus_config = {}; + bus_config.mosi_io_num = sd_card_pins().mosi; + bus_config.miso_io_num = sd_card_pins().miso; + bus_config.sclk_io_num = sd_card_pins().clk; + bus_config.quadwp_io_num = GPIO_NUM_NC; + bus_config.quadhd_io_num = GPIO_NUM_NC; + bus_config.max_transfer_sz = 4 * 1024; + auto ret = spi_bus_initialize((spi_host_device_t)host.slot, &bus_config, SDSPI_DEFAULT_DMA); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + logger_.error("Failed to initialize SD SPI bus ({})", esp_err_to_name(ret)); + return false; + } + + sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); + slot_config.gpio_cs = sd_card_pins().cs; + slot_config.host_id = (spi_host_device_t)host.slot; + + ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); + if (ret != ESP_OK) { + logger_.error("Failed to initialize the card ({})", esp_err_to_name(ret)); + return false; + } + + sd_card_initialized_ = true; + return true; +} + +bool SmartPanleeSc01Plus::initialize_sdcard() { return initialize_sdcard(SdCardConfig{}); } + +bool SmartPanleeSc01Plus::is_sd_card_available() const { return sd_card_initialized_; } + +bool SmartPanleeSc01Plus::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { + if (!sd_card_initialized_) { + return false; + } + + uint64_t total_bytes = 0; + uint64_t free_bytes = 0; + auto ret = esp_vfs_fat_info(mount_point, &total_bytes, &free_bytes); + if (ret != ESP_OK) { + logger_.error("Failed to get SD card information ({})", esp_err_to_name(ret)); + return false; + } + + if (size_mb) { + *size_mb = total_bytes / (1024 * 1024); + } + if (free_mb) { + *free_mb = free_bytes / (1024 * 1024); + } + return true; +} + +bool SmartPanleeSc01Plus::update_touch() { + if (!touch_driver_) { + return false; + } + + std::error_code ec; + TouchpadData temp_data; + touch_driver_->get_touch_point(&temp_data.num_touch_points, &temp_data.x, &temp_data.y, ec); + if (ec) { + logger_.error("Could not update touch driver: {}", ec.message()); + std::lock_guard lock(touchpad_data_mutex_); + touchpad_data_ = {}; + return false; + } + + std::lock_guard lock(touchpad_data_mutex_); + bool changed = temp_data != touchpad_data_; + touchpad_data_ = temp_data; + return changed; +} + +bool SmartPanleeSc01Plus::audio_task_callback(std::mutex &m, std::condition_variable &cv, + bool &task_notified) { + (void)m; + (void)cv; + (void)task_notified; + + auto available = xStreamBufferBytesAvailable(audio_tx_stream_); + auto buffer_size = audio_tx_buffer_.size(); + available = std::min(available, buffer_size); + + uint8_t *buffer = audio_tx_buffer_.data(); + std::memset(buffer, 0, buffer_size); + + size_t bytes_received = 0; + if (available > 0) { + bytes_received = xStreamBufferReceive(audio_tx_stream_, buffer, available, 0); + } + + if (mute_) { + std::memset(buffer, 0, bytes_received); + } else { + auto volume = volume_.load(); + if (volume < 100.0f) { + auto *samples = reinterpret_cast(buffer); + auto num_samples = bytes_received / sizeof(int16_t); + for (size_t i = 0; i < num_samples; ++i) { + auto scaled = static_cast((samples[i] * volume) / 100.0f); + samples[i] = static_cast(std::clamp( + scaled, std::numeric_limits::min(), std::numeric_limits::max())); + } + } + } + + i2s_channel_write(audio_tx_handle_, buffer, buffer_size, nullptr, portMAX_DELAY); + return false; +} + +void SmartPanleeSc01Plus::write_command(uint8_t command, std::span parameters, + uint32_t user_data) { + (void)user_data; + const uint8_t *data = parameters.empty() ? nullptr : parameters.data(); + ESP_ERROR_CHECK(esp_lcd_panel_io_tx_param(panel_io_, command, data, parameters.size())); +} + +void SmartPanleeSc01Plus::write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t *data, + uint32_t user_data) { + (void)user_data; + std::array window = { + static_cast((xs >> 8) & 0xFF), + static_cast(xs & 0xFF), + static_cast((xe >> 8) & 0xFF), + static_cast(xe & 0xFF), + }; + ESP_ERROR_CHECK(esp_lcd_panel_io_tx_param(panel_io_, + static_cast(DisplayDriver::Command::caset), + window.data(), window.size())); + window = { + static_cast((ys >> 8) & 0xFF), + static_cast(ys & 0xFF), + static_cast((ye >> 8) & 0xFF), + static_cast(ye & 0xFF), + }; + ESP_ERROR_CHECK(esp_lcd_panel_io_tx_param(panel_io_, + static_cast(DisplayDriver::Command::raset), + window.data(), window.size())); + size_t num_bytes = + static_cast(xe - xs + 1) * static_cast(ye - ys + 1) * sizeof(Pixel); + ESP_ERROR_CHECK(esp_lcd_panel_io_tx_color( + panel_io_, static_cast(DisplayDriver::Command::ramwr), data, num_bytes)); +} + +} // namespace espp diff --git a/doc/Doxyfile b/doc/Doxyfile index 404591115..3593046c4 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -144,6 +144,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/rx8130ce/example/main/rx8130ce_example.cpp \ $(PROJECT_PATH)/components/serialization/example/main/serialization_example.cpp \ $(PROJECT_PATH)/components/seeed-studio-round-display/example/main/seeed_studio_round_display_example.cpp \ + $(PROJECT_PATH)/components/smartpanlee-sc01-plus/example/main/smartpanlee_sc01_plus_example.cpp \ $(PROJECT_PATH)/components/socket/example/main/socket_example.cpp \ $(PROJECT_PATH)/components/st25dv/example/main/st25dv_example.cpp \ $(PROJECT_PATH)/components/state_machine/example/main/hfsm_example.cpp \ @@ -213,9 +214,10 @@ INPUT = \ $(PROJECT_PATH)/components/display/include/display.hpp \ $(PROJECT_PATH)/components/display_drivers/include/gc9a01.hpp \ $(PROJECT_PATH)/components/display_drivers/include/ili9341.hpp \ + $(PROJECT_PATH)/components/display_drivers/include/sh8601.hpp \ $(PROJECT_PATH)/components/display_drivers/include/ssd1351.hpp \ $(PROJECT_PATH)/components/display_drivers/include/st7789.hpp \ - $(PROJECT_PATH)/components/display_drivers/include/sh8601.hpp \ + $(PROJECT_PATH)/components/display_drivers/include/st7796.hpp \ $(PROJECT_PATH)/components/dns_server/include/dns_server.hpp \ $(PROJECT_PATH)/components/drv2605/include/drv2605.hpp \ $(PROJECT_PATH)/components/drv2605/include/drv2605_menu.hpp \ @@ -319,6 +321,7 @@ INPUT = \ $(PROJECT_PATH)/components/rx8130ce/include/rx8130ce.hpp \ $(PROJECT_PATH)/components/serialization/include/serialization.hpp \ $(PROJECT_PATH)/components/seeed-studio-round-display/include/seeed-studio-round-display.hpp \ + $(PROJECT_PATH)/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp \ $(PROJECT_PATH)/components/socket/include/socket.hpp \ $(PROJECT_PATH)/components/socket/include/udp_socket.hpp \ $(PROJECT_PATH)/components/socket/include/tcp_socket.hpp \ @@ -378,4 +381,3 @@ HAVE_DOT = NO GENERATE_LATEX = YES GENERATE_MAN = NO GENERATE_RTF = NO - diff --git a/doc/en/display/display_drivers.rst b/doc/en/display/display_drivers.rst old mode 100644 new mode 100755 index 1ee395282..b7311753a --- a/doc/en/display/display_drivers.rst +++ b/doc/en/display/display_drivers.rst @@ -18,6 +18,7 @@ API Reference .. include-build-file:: inc/gc9a01.inc .. include-build-file:: inc/ili9341.inc +.. include-build-file:: inc/sh8601.inc .. include-build-file:: inc/ssd1351.inc .. include-build-file:: inc/st7789.inc -.. include-build-file:: inc/sh8601.inc +.. include-build-file:: inc/st7796.inc diff --git a/doc/en/index.rst b/doc/en/index.rst old mode 100644 new mode 100755 index 5ffd2bb43..6a2d4fca4 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -66,6 +66,7 @@ This is the documentation for esp-idf c++ components, ESPP (`espp